Moved to regular smtp sender
This commit is contained in:
parent
535a798d62
commit
be8139659f
|
|
@ -69,6 +69,14 @@ Examples:
|
|||
- `credentials/.env` and key material are ignored by `.gitignore`.
|
||||
- Per-user mail credentials are stored encrypted in the mailshots DB.
|
||||
|
||||
## Mail Sending Notes
|
||||
|
||||
- Mailshots send through WordPress's `wp_mail()`/PHPMailer service, configured with the current user's saved SMTP credentials for each send.
|
||||
- SMTP EHLO uses the domain from the configured From email address.
|
||||
- Standard message headers and MIME formatting are generated by WordPress/PHPMailer.
|
||||
- BCC recipients are delivered through PHPMailer recipient handling and are not rendered into the message headers.
|
||||
- Generated in-memory attachments, including rendered PDFs, are added to PHPMailer as string attachments.
|
||||
|
||||
## Important References
|
||||
|
||||
- Environment baseline: `requirements/environment.md`
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
|
|
@ -3,7 +3,3 @@ Split advertiser name into forename and surname - done in twig in message
|
|||
Add ad sizes into mailmerge - done
|
||||
|
||||
Why does filter ads and advertisers where issue(125) and ads.Price != 0 and not (advertisers.Categories contains 'discount') identify fewer records than expected. - donei
|
||||
|
||||
|
||||
|
||||
tig
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
* Plugin Name: FECA Mailshots
|
||||
* Plugin URI: https://fenedge.co.uk/
|
||||
* Description: FECA mailshots plugin.
|
||||
* Version: 1.1.6
|
||||
* Version: 1.1.8
|
||||
* Requires at least: 6.0
|
||||
* Requires PHP: 7.4
|
||||
* Author: FECA
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ trait AdminRequestHelpers
|
|||
. '.feca-mailshots-admin .feca-inline-control-group .feca-control{padding:0;}'
|
||||
. '.feca-mailshots-admin .feca-button-row{display:flex;gap:var(--feca-space-2);align-items:center;flex-wrap:wrap;margin-top:var(--feca-space-3);margin-bottom:0;padding:0;}'
|
||||
. '.feca-mailshots-admin .feca-button-row .button{margin:0 !important;}'
|
||||
. '.feca-mailshots-admin .feca-two-line-button{height:auto;min-height:44px;line-height:1.25;white-space:normal;max-width:260px;text-align:center;padding-top:6px;padding-bottom:6px;}'
|
||||
. '.feca-mailshots-admin .feca-button-danger{color:#b32d2e;border-color:#d63638;}'
|
||||
. '.feca-mailshots-admin .feca-button-danger:hover{color:#8a2424;border-color:#8a2424;background:#fff5f5;}'
|
||||
. '.feca-mailshots-admin .feca-banner{padding:var(--feca-space-2) var(--feca-space-3);border:1px solid #dcdcde;background:#fff;margin:var(--feca-space-3) 0;border-radius:var(--feca-radius);}'
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ final class MailshotTestAdminPage
|
|||
$this->wp->addAction('admin_post_feca_mailshots_test_api', [$this, 'handleApi']);
|
||||
$this->wp->addAction('admin_post_feca_mailshots_test_render_ui', [$this, 'handleRenderUi']);
|
||||
$this->wp->addAction('admin_post_feca_mailshots_test_send_ui', [$this, 'handleSendUi']);
|
||||
$this->wp->addAction('admin_post_feca_mailshots_test_send_all_ui', [$this, 'handleSendAllUi']);
|
||||
}
|
||||
|
||||
public function registerMenu(): void
|
||||
|
|
@ -208,7 +209,8 @@ final class MailshotTestAdminPage
|
|||
echo '<label for="mst_test_email"><strong>Test email address</strong></label>';
|
||||
echo '<input id="mst_test_email" class="regular-text" type="email" name="test_email" value="' . htmlspecialchars($testEmail, ENT_QUOTES) . '">';
|
||||
echo '</div>';
|
||||
echo '<div class="feca-control"><label> </label><button class="button button-primary" type="submit" name="action" value="feca_mailshots_test_send_ui" id="mst-send-test-button" onclick="return window.fecaConfirmTestSend ? window.fecaConfirmTestSend() : confirm(\'Send one test email to the entered address?\');">Send Test Email</button></div>';
|
||||
echo '<div class="feca-control"><label> </label><button class="button button-primary feca-two-line-button" type="submit" name="action" value="feca_mailshots_test_send_ui" id="mst-send-test-button" onclick="return window.fecaConfirmTestSend ? window.fecaConfirmTestSend(\'selected\') : confirm(\'Send one test email to the entered address?\');">Send Test for selected row to Test Email Address</button></div>';
|
||||
echo '<div class="feca-control"><label> </label><button class="button feca-two-line-button" type="submit" name="action" value="feca_mailshots_test_send_all_ui" id="mst-send-test-all-button" onclick="return window.fecaConfirmTestSend ? window.fecaConfirmTestSend(\'all\') : confirm(\'Send test emails to the entered address for every recipient row?\');">Send Test for all to Test Email Address</button></div>';
|
||||
echo '</div>';
|
||||
echo '<div class="feca-control"><label> </label><button class="button" type="submit" name="action" value="feca_mailshots_test_render_ui">Render Test (No Send)</button></div>';
|
||||
echo '</div>';
|
||||
|
|
@ -218,17 +220,20 @@ final class MailshotTestAdminPage
|
|||
echo 'var form=document.getElementById("mst-action-form");';
|
||||
echo 'var inlineResult=document.getElementById("mst-inline-result");';
|
||||
echo 'var sendButton=document.getElementById("mst-send-test-button");';
|
||||
echo 'var sendAllButton=document.getElementById("mst-send-test-all-button");';
|
||||
echo 'var sendActions=["feca_mailshots_test_send_ui","feca_mailshots_test_send_all_ui"];';
|
||||
echo 'function recipientCount(select){var count=parseInt(select&&select.getAttribute("data-recipient-count")?select.getAttribute("data-recipient-count"):"0",10);return Number.isFinite(count)&&count>=0?count:0;}';
|
||||
echo 'function showResult(ok,title,lines){if(!inlineResult){return;}var wrap=document.createElement("div");wrap.className="feca-banner "+(ok?"feca-banner-success":"feca-banner-error");var strong=document.createElement("strong");strong.textContent=title;wrap.appendChild(strong);(lines||[]).forEach(function(line){var p=document.createElement("p");p.className="feca-banner-note";p.textContent=String(line||"");wrap.appendChild(p);});inlineResult.innerHTML="";inlineResult.appendChild(wrap);wrap.scrollIntoView({block:"nearest"});}';
|
||||
echo 'if(form){form.addEventListener("submit",function(ev){var submitter=ev.submitter;if(!submitter||String(submitter.value||"")!=="feca_mailshots_test_send_ui"){return;}ev.preventDefault();var payload=new URLSearchParams(new FormData(form));payload.delete("action");if(sendButton){sendButton.disabled=true;}showResult(true,"Sending test email...",["Rendering PDF attachment and sending message."]);fetch(apiUrl,{method:"POST",credentials:"same-origin",headers:{"Content-Type":"application/x-www-form-urlencoded; charset=UTF-8"},body:payload.toString()}).then(function(response){return response.text().then(function(text){var data=null;try{data=JSON.parse(text);}catch(e){var raw=String(text||"");var snippet="";if(raw){var doc=(new DOMParser()).parseFromString(raw,"text/html");snippet=String((doc&&doc.body&&doc.body.textContent)?doc.body.textContent:raw).split("\n").join(" ").split("\r").join(" ").split("\t").join(" ").trim().slice(0,300);}var msg="Send test failed: server returned HTTP "+response.status+" instead of JSON.";if(snippet){msg+=" Response began: "+snippet;}else{msg+=" Check the WordPress/PHP error log for the underlying fatal error.";}return {ok:false,errors:[msg]};}if(!response.ok&&data&&data.ok!==false){data.ok=false;}return data;});}).then(function(data){if(data&&data.ok){var lines=[];if(data.sent_to){lines.push("Sent to: "+data.sent_to);}if(data.sent_at){lines.push("Sent at: "+data.sent_at);}if(data.warnings&&data.warnings.length){lines=lines.concat(data.warnings.map(function(w){return "Warning: "+w;}));}showResult(true,"Test action succeeded.",lines.length?lines:["Sent."]);return;}var errors=(data&&data.errors&&data.errors.length)?data.errors:[(data&&data.error)?data.error:"Unknown send-test failure."];showResult(false,"Test action failed.",errors);}).catch(function(error){showResult(false,"Test action failed.",[error&&error.message?error.message:"Request failed."]);}).finally(function(){if(sendButton){sendButton.disabled=false;}});});}';
|
||||
echo 'if(form){form.addEventListener("submit",function(ev){var submitter=ev.submitter;var action=submitter?String(submitter.value||""):"";if(sendActions.indexOf(action)===-1){return;}ev.preventDefault();var select=document.getElementById("mst_recipient_index");var sendAll=action==="feca_mailshots_test_send_all_ui";if(!sendAll&&select&&String(select.value||"0")==="-1"){showResult(false,"Test action failed.",["Choose a specific recipient row, or use Send Test for all to Test Email Address."]);return;}var payload=new URLSearchParams(new FormData(form));payload.delete("action");if(sendAll){payload.set("recipient_index","-1");payload.set("send_scope","all");}else{payload.set("send_scope","selected");}if(sendButton){sendButton.disabled=true;}if(sendAllButton){sendAllButton.disabled=true;}showResult(true,"Sending test email...",[sendAll?"Rendering PDF attachments and sending messages.":"Rendering PDF attachment and sending message."]);fetch(apiUrl,{method:"POST",credentials:"same-origin",headers:{"Content-Type":"application/x-www-form-urlencoded; charset=UTF-8"},body:payload.toString()}).then(function(response){return response.text().then(function(text){var data=null;try{data=JSON.parse(text);}catch(e){var raw=String(text||"");var snippet="";if(raw){var doc=(new DOMParser()).parseFromString(raw,"text/html");snippet=String((doc&&doc.body&&doc.body.textContent)?doc.body.textContent:raw).split("\n").join(" ").split("\r").join(" ").split("\t").join(" ").trim().slice(0,300);}var msg="Send test failed: server returned HTTP "+response.status+" instead of JSON.";if(snippet){msg+=" Response began: "+snippet;}else{msg+=" Check the WordPress/PHP error log for the underlying fatal error.";}return {ok:false,errors:[msg]};}if(!response.ok&&data&&data.ok!==false){data.ok=false;}return data;});}).then(function(data){if(data&&data.ok){var lines=[];if(data.sent_to){lines.push("Sent to: "+data.sent_to);}if(data.sent_at){lines.push("Sent at: "+data.sent_at);}if(data.warnings&&data.warnings.length){lines=lines.concat(data.warnings.map(function(w){return "Warning: "+w;}));}showResult(true,"Test action succeeded.",lines.length?lines:["Sent."]);return;}var errors=(data&&data.errors&&data.errors.length)?data.errors:[(data&&data.error)?data.error:"Unknown send-test failure."];showResult(false,"Test action failed.",errors);}).catch(function(error){showResult(false,"Test action failed.",[error&&error.message?error.message:"Request failed."]);}).finally(function(){if(sendButton){sendButton.disabled=false;}if(sendAllButton){sendAllButton.disabled=false;}});});}';
|
||||
echo 'window.fecaConfirmTestSend=function(){';
|
||||
echo 'var scope=arguments.length>0?String(arguments[0]||"selected"):"selected";';
|
||||
echo 'var select=document.getElementById("mst_recipient_index");';
|
||||
echo 'if(!select){return confirm("Send one test email to the entered address?");}';
|
||||
echo 'var value=String(select.value||"0");';
|
||||
echo 'if(value==="-1"){';
|
||||
echo 'var count=parseInt(select.getAttribute("data-recipient-count")||"0",10);';
|
||||
echo 'if(!Number.isFinite(count)||count<0){count=0;}';
|
||||
echo 'if(scope==="all"){';
|
||||
echo 'var count=recipientCount(select);';
|
||||
echo 'return confirm("Send "+count+" emails to the entered address?");';
|
||||
echo '}';
|
||||
echo 'if(String(select.value||"0")==="-1"){return true;}';
|
||||
echo 'return confirm("Send one test email to the entered address?");';
|
||||
echo '};';
|
||||
echo '})();</script>';
|
||||
|
|
@ -358,11 +363,16 @@ final class MailshotTestAdminPage
|
|||
$mailshotId = (int) ($this->wp->requestParam('mailshot_id', '0') ?? '0');
|
||||
$idx = (int) ($this->wp->requestParam('recipient_index', '0') ?? '0');
|
||||
$to = (string) ($this->wp->requestParam('test_email', '') ?? '');
|
||||
if ($idx < 0) {
|
||||
$scope = (string) ($this->wp->requestParam('send_scope', 'selected') ?? 'selected');
|
||||
if ($scope === 'all') {
|
||||
$stage = 'sending test email for all recipient rows';
|
||||
$this->wp->sendJson($service->sendTestAll($mailshotId, $to));
|
||||
return;
|
||||
}
|
||||
if ($idx < 0) {
|
||||
$this->wp->sendJson(['ok' => false, 'errors' => ['Choose a specific recipient row, or use Send Test for all to Test Email Address.']]);
|
||||
return;
|
||||
}
|
||||
$stage = 'sending test email for selected recipient';
|
||||
$this->wp->sendJson($service->sendTest($mailshotId, $idx, $to));
|
||||
return;
|
||||
|
|
@ -410,8 +420,7 @@ final class MailshotTestAdminPage
|
|||
if ($memoryError !== null) {
|
||||
$result = ['ok' => false, 'errors' => [$memoryError]];
|
||||
} elseif ($idx < 0) {
|
||||
$stage = 'sending test email for all recipient rows';
|
||||
$result = $this->runService()->sendTestAll($mailshotId, $email);
|
||||
$result = ['ok' => false, 'errors' => ['Choose a specific recipient row, or use Send Test for all to Test Email Address.']];
|
||||
} else {
|
||||
$stage = 'sending test email for selected recipient';
|
||||
$result = $this->runService()->sendTest($mailshotId, $idx, $email);
|
||||
|
|
@ -425,6 +434,33 @@ final class MailshotTestAdminPage
|
|||
$this->redirect($mailshotId, $idx, $email);
|
||||
}
|
||||
|
||||
public function handleSendAllUi(): void
|
||||
{
|
||||
if (!$this->enforceMutationGuardOrJson(self::CAPABILITY, self::NONCE_ACTION)) {
|
||||
return;
|
||||
}
|
||||
$mailshotId = (int) ($this->wp->requestParam('mailshot_id', '0') ?? '0');
|
||||
$idx = (int) ($this->wp->requestParam('recipient_index', '0') ?? '0');
|
||||
$email = (string) ($this->wp->requestParam('test_email', '') ?? '');
|
||||
$stage = 'initializing send test for all recipient rows';
|
||||
try {
|
||||
$stage = 'raising memory limit for send test';
|
||||
$memoryError = $this->maybeRaiseMemoryLimit($this->resolveDownloadMemoryLimitTarget());
|
||||
if ($memoryError !== null) {
|
||||
$result = ['ok' => false, 'errors' => [$memoryError]];
|
||||
} else {
|
||||
$stage = 'sending test email for all recipient rows';
|
||||
$result = $this->runService()->sendTestAll($mailshotId, $email);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$result = ['ok' => false, 'errors' => [$this->diagnosticError('Send test failed', $stage, $e)]];
|
||||
}
|
||||
unset($result['rendered']);
|
||||
$result['ui_action'] = 'send';
|
||||
$this->wp->updateOption(self::RESULT_OPTION_KEY, $result);
|
||||
$this->redirect($mailshotId, $idx, $email);
|
||||
}
|
||||
|
||||
private function redirect(int $mailshotId, int $recipientIndex, string $testEmail, bool $renderTest = false): void
|
||||
{
|
||||
$url = $this->wp->adminUrl('admin.php?page=feca-mailshots-test&mailshot_id=' . $mailshotId . '&recipient_index=' . $recipientIndex . '&test_email=' . rawurlencode($testEmail));
|
||||
|
|
|
|||
|
|
@ -1,238 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace FecaMailshots\Infrastructure;
|
||||
|
||||
use FecaMailshots\Application\SmtpSender;
|
||||
|
||||
final class BasicSmtpSender implements SmtpSender
|
||||
{
|
||||
public function send(array $credentials, array $to, array $cc, array $bcc, string $subject, string $htmlBody, ?string $replyTo = null, array $attachments = []): array
|
||||
{
|
||||
$host = (string) ($credentials['smtp_host'] ?? '');
|
||||
$port = (int) ($credentials['smtp_port'] ?? 0);
|
||||
$user = (string) ($credentials['smtp_user'] ?? '');
|
||||
$pass = (string) ($credentials['smtp_password'] ?? '');
|
||||
$from = (string) ($credentials['smtp_from_email'] ?? '');
|
||||
$fromName = (string) ($credentials['smtp_from_name'] ?? '');
|
||||
|
||||
if ($host === '' || $port <= 0 || $user === '' || $pass === '' || $from === '') {
|
||||
throw new \RuntimeException('Missing SMTP credentials.');
|
||||
}
|
||||
|
||||
$transport = !empty($credentials['smtp_require_tls']) ? 'tls://' : '';
|
||||
$fp = @stream_socket_client($transport . $host . ':' . $port, $errno, $errstr, 20);
|
||||
if (!is_resource($fp)) {
|
||||
throw new \RuntimeException('SMTP connect failed: ' . $errstr);
|
||||
}
|
||||
|
||||
try {
|
||||
$this->expect($fp, [220]);
|
||||
$this->cmd($fp, 'EHLO localhost', [250]);
|
||||
if (empty($credentials['smtp_require_tls'])) {
|
||||
// try opportunistic STARTTLS
|
||||
$line = $this->cmd($fp, 'STARTTLS', [220], false);
|
||||
if ($line !== null) {
|
||||
if (!stream_socket_enable_crypto($fp, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) {
|
||||
throw new \RuntimeException('Failed to enable STARTTLS crypto.');
|
||||
}
|
||||
$this->cmd($fp, 'EHLO localhost', [250]);
|
||||
}
|
||||
}
|
||||
|
||||
$this->cmd($fp, 'AUTH LOGIN', [334]);
|
||||
$this->cmd($fp, base64_encode($user), [334]);
|
||||
$this->cmd($fp, base64_encode($pass), [235]);
|
||||
$this->cmd($fp, 'MAIL FROM:<' . $from . '>', [250]);
|
||||
|
||||
$allRecipients = array_values(array_unique(array_merge($to, $cc, $bcc)));
|
||||
foreach ($allRecipients as $recipient) {
|
||||
$this->cmd($fp, 'RCPT TO:<' . trim($recipient) . '>', [250, 251]);
|
||||
}
|
||||
|
||||
$this->cmd($fp, 'DATA', [354]);
|
||||
|
||||
$raw = $this->buildMime($from, $fromName, $to, $cc, $bcc, $subject, $htmlBody, $replyTo, $attachments);
|
||||
fwrite($fp, $this->dotStuff($raw));
|
||||
fwrite($fp, "\r\n.\r\n");
|
||||
$this->expect($fp, [250]);
|
||||
$this->cmd($fp, 'QUIT', [221], false);
|
||||
|
||||
return ['raw_mime' => $raw];
|
||||
} finally {
|
||||
fclose($fp);
|
||||
}
|
||||
}
|
||||
|
||||
/** @param list<string> $to @param list<string> $cc @param list<string> $bcc */
|
||||
private function buildMime(string $from, string $fromName, array $to, array $cc, array $bcc, string $subject, string $htmlBody, ?string $replyTo, array $attachments): string
|
||||
{
|
||||
$headers = [];
|
||||
$fromHeader = $fromName !== '' ? sprintf('%s <%s>', $this->encodeHeaderValue($fromName), $from) : $from;
|
||||
$headers[] = 'From: ' . $fromHeader;
|
||||
$headers[] = 'To: ' . implode(', ', $to);
|
||||
if ($cc !== []) {
|
||||
$headers[] = 'Cc: ' . implode(', ', $cc);
|
||||
}
|
||||
if ($bcc !== []) {
|
||||
$headers[] = 'Bcc: ' . implode(', ', $bcc);
|
||||
}
|
||||
if ($replyTo !== null && trim($replyTo) !== '') {
|
||||
$headers[] = 'Reply-To: ' . trim($replyTo);
|
||||
}
|
||||
$headers[] = 'Subject: ' . $this->encodeHeaderValue($subject);
|
||||
$headers[] = 'MIME-Version: 1.0';
|
||||
if ($attachments === []) {
|
||||
$headers[] = 'Content-Type: text/html; charset=UTF-8';
|
||||
$headers[] = 'Content-Transfer-Encoding: quoted-printable';
|
||||
return implode("\r\n", $headers) . "\r\n\r\n" . $this->encodeQuotedPrintable($htmlBody);
|
||||
}
|
||||
|
||||
$boundary = 'feca_mailshots_' . bin2hex(random_bytes(12));
|
||||
$mime = implode("\r\n", array_merge($headers, ['Content-Type: multipart/mixed; boundary="' . $boundary . '"'])) . "\r\n\r\n";
|
||||
$mime .= '--' . $boundary . "\r\n";
|
||||
$mime .= 'Content-Type: text/html; charset=UTF-8' . "\r\n";
|
||||
$mime .= 'Content-Transfer-Encoding: quoted-printable' . "\r\n\r\n";
|
||||
$mime .= $this->encodeQuotedPrintable($htmlBody) . "\r\n";
|
||||
|
||||
foreach ($attachments as $attachment) {
|
||||
$filename = trim((string) ($attachment['filename'] ?? 'attachment.bin'));
|
||||
$mimeType = trim((string) ($attachment['mime_type'] ?? 'application/octet-stream'));
|
||||
$bytes = (string) ($attachment['content_bytes'] ?? '');
|
||||
if ($filename === '' || $bytes === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$escapedFilename = addcslashes($filename, '"\\');
|
||||
$mime .= '--' . $boundary . "\r\n";
|
||||
$mime .= 'Content-Type: ' . $mimeType . '; name="' . $escapedFilename . '"' . "\r\n";
|
||||
$mime .= 'Content-Transfer-Encoding: base64' . "\r\n";
|
||||
$mime .= 'Content-Disposition: attachment; filename="' . $escapedFilename . '"' . "\r\n\r\n";
|
||||
$mime .= $this->encodeBase64Chunked($bytes) . "\r\n";
|
||||
}
|
||||
|
||||
$mime .= '--' . $boundary . '--' . "\r\n";
|
||||
return $mime;
|
||||
}
|
||||
|
||||
private function encodeHeaderValue(string $value): string
|
||||
{
|
||||
$value = preg_replace('/[\r\n]+/', ' ', $value);
|
||||
$value = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/', '', is_string($value) ? $value : '');
|
||||
$value = trim(preg_replace('/[ \t]+/', ' ', is_string($value) ? $value : '') ?? '');
|
||||
if ($value === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
$chars = [];
|
||||
if (preg_match_all('/./us', $value, $matches) === 1) {
|
||||
$chars = $matches[0];
|
||||
} else {
|
||||
$chars = str_split($value);
|
||||
}
|
||||
|
||||
$chunks = [];
|
||||
$chunk = '';
|
||||
foreach ($chars as $char) {
|
||||
if ($chunk !== '' && strlen($chunk . $char) > 45) {
|
||||
$chunks[] = $chunk;
|
||||
$chunk = '';
|
||||
}
|
||||
$chunk .= $char;
|
||||
}
|
||||
if ($chunk !== '') {
|
||||
$chunks[] = $chunk;
|
||||
}
|
||||
|
||||
$encoded = array_map(static fn(string $chunk): string => '=?UTF-8?B?' . base64_encode($chunk) . '?=', $chunks);
|
||||
return implode("\r\n ", $encoded);
|
||||
}
|
||||
|
||||
private function encodeBase64Chunked(string $bytes): string
|
||||
{
|
||||
$stream = fopen('php://temp', 'w+b');
|
||||
if (!is_resource($stream)) {
|
||||
throw new \RuntimeException('Unable to allocate temp stream for attachment encoding.');
|
||||
}
|
||||
|
||||
try {
|
||||
if (fwrite($stream, $bytes) === false) {
|
||||
throw new \RuntimeException('Failed writing attachment bytes to temp stream.');
|
||||
}
|
||||
rewind($stream);
|
||||
$filter = stream_filter_append(
|
||||
$stream,
|
||||
'convert.base64-encode',
|
||||
STREAM_FILTER_READ,
|
||||
['line-length' => 76, 'line-break-chars' => "\r\n"]
|
||||
);
|
||||
if ($filter === false) {
|
||||
throw new \RuntimeException('Failed to initialize base64 stream filter.');
|
||||
}
|
||||
|
||||
$encoded = stream_get_contents($stream);
|
||||
if (!is_string($encoded)) {
|
||||
throw new \RuntimeException('Failed to read encoded attachment bytes.');
|
||||
}
|
||||
return rtrim($encoded, "\r\n");
|
||||
} finally {
|
||||
fclose($stream);
|
||||
}
|
||||
}
|
||||
|
||||
private function encodeQuotedPrintable(string $body): string
|
||||
{
|
||||
$body = preg_replace("/\r\n|\r|\n/", "\r\n", $body) ?? $body;
|
||||
return quoted_printable_encode($body);
|
||||
}
|
||||
|
||||
private function dotStuff(string $data): string
|
||||
{
|
||||
$data = preg_replace("/\r\n|\r|\n/", "\r\n", $data) ?? $data;
|
||||
return preg_replace('/(?m)^\./', '..', $data) ?? $data;
|
||||
}
|
||||
|
||||
/** @param list<int> $codes */
|
||||
private function cmd($fp, string $cmd, array $codes, bool $throwOnMismatch = true): ?string
|
||||
{
|
||||
fwrite($fp, $cmd . "\r\n");
|
||||
return $this->expect($fp, $codes, $throwOnMismatch);
|
||||
}
|
||||
|
||||
/** @param list<int> $codes */
|
||||
private function expect($fp, array $codes, bool $throwOnMismatch = true): ?string
|
||||
{
|
||||
$lastLine = null;
|
||||
while (true) {
|
||||
$line = fgets($fp, 4096);
|
||||
if ($line === false) {
|
||||
if ($throwOnMismatch) {
|
||||
throw new \RuntimeException('SMTP read failed.');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
$lastLine = $line;
|
||||
if (!preg_match('/^(\d{3})([\s-])/', $line, $m)) {
|
||||
if ($throwOnMismatch) {
|
||||
throw new \RuntimeException('SMTP malformed response: ' . trim($line));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
$code = (int) $m[1];
|
||||
if (!in_array($code, $codes, true)) {
|
||||
if ($throwOnMismatch) {
|
||||
throw new \RuntimeException('SMTP unexpected response: ' . trim($line));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
$continuation = $m[2] === '-';
|
||||
if (!$continuation) {
|
||||
return $lastLine;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,159 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace FecaMailshots\Infrastructure;
|
||||
|
||||
use FecaMailshots\Application\SmtpSender;
|
||||
|
||||
final class WordPressSmtpSender implements SmtpSender
|
||||
{
|
||||
public function send(array $credentials, array $to, array $cc, array $bcc, string $subject, string $htmlBody, ?string $replyTo = null, array $attachments = []): array
|
||||
{
|
||||
if (!function_exists('wp_mail') || !function_exists('add_action')) {
|
||||
throw new \RuntimeException('WordPress mail service is unavailable.');
|
||||
}
|
||||
|
||||
$host = trim((string) ($credentials['smtp_host'] ?? ''));
|
||||
$port = (int) ($credentials['smtp_port'] ?? 0);
|
||||
$user = trim((string) ($credentials['smtp_user'] ?? ''));
|
||||
$pass = (string) ($credentials['smtp_password'] ?? '');
|
||||
$from = trim((string) ($credentials['smtp_from_email'] ?? ''));
|
||||
$fromName = trim((string) ($credentials['smtp_from_name'] ?? ''));
|
||||
|
||||
if ($host === '' || $port <= 0 || $user === '' || $pass === '' || $from === '') {
|
||||
throw new \RuntimeException('Missing SMTP credentials.');
|
||||
}
|
||||
|
||||
$ehloDomain = $this->mailDomainFromAddress($from);
|
||||
$capturedMailer = null;
|
||||
|
||||
$configureMailer = function ($phpmailer) use ($host, $port, $user, $pass, $from, $fromName, $ehloDomain, $credentials, $attachments, &$capturedMailer): void {
|
||||
$capturedMailer = $phpmailer;
|
||||
|
||||
if (method_exists($phpmailer, 'isSMTP')) {
|
||||
$phpmailer->isSMTP();
|
||||
}
|
||||
|
||||
$phpmailer->Host = $host;
|
||||
$phpmailer->Port = $port;
|
||||
$phpmailer->SMTPAuth = true;
|
||||
$phpmailer->Username = $user;
|
||||
$phpmailer->Password = $pass;
|
||||
$phpmailer->Helo = $ehloDomain;
|
||||
$phpmailer->CharSet = 'UTF-8';
|
||||
$phpmailer->MessageID = '<' . gmdate('YmdHis') . '.' . bin2hex(random_bytes(16)) . '@' . $ehloDomain . '>';
|
||||
|
||||
if (!empty($credentials['smtp_require_tls'])) {
|
||||
$phpmailer->SMTPSecure = defined('\PHPMailer\PHPMailer\PHPMailer::ENCRYPTION_SMTPS')
|
||||
? \PHPMailer\PHPMailer\PHPMailer::ENCRYPTION_SMTPS
|
||||
: 'ssl';
|
||||
} else {
|
||||
$phpmailer->SMTPAutoTLS = true;
|
||||
}
|
||||
|
||||
if (method_exists($phpmailer, 'setFrom')) {
|
||||
$phpmailer->setFrom($from, $fromName, false);
|
||||
}
|
||||
|
||||
foreach ($attachments as $attachment) {
|
||||
$filename = trim((string) ($attachment['filename'] ?? 'attachment.bin'));
|
||||
$mimeType = trim((string) ($attachment['mime_type'] ?? 'application/octet-stream'));
|
||||
$bytes = (string) ($attachment['content_bytes'] ?? '');
|
||||
if ($filename === '' || $bytes === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!method_exists($phpmailer, 'addStringAttachment')) {
|
||||
throw new \RuntimeException('WordPress mail service does not support in-memory attachments.');
|
||||
}
|
||||
$phpmailer->addStringAttachment($bytes, $filename, 'base64', $mimeType);
|
||||
}
|
||||
};
|
||||
|
||||
add_action('phpmailer_init', $configureMailer);
|
||||
try {
|
||||
$headers = $this->headers($from, $fromName, $cc, $bcc, $replyTo);
|
||||
$ok = wp_mail($to, $subject, $htmlBody, $headers);
|
||||
} finally {
|
||||
if (function_exists('remove_action')) {
|
||||
remove_action('phpmailer_init', $configureMailer);
|
||||
}
|
||||
}
|
||||
|
||||
if ($ok !== true) {
|
||||
$error = is_object($capturedMailer) && isset($capturedMailer->ErrorInfo) && (string) $capturedMailer->ErrorInfo !== ''
|
||||
? ': ' . (string) $capturedMailer->ErrorInfo
|
||||
: '.';
|
||||
throw new \RuntimeException('WordPress mail send failed' . $error);
|
||||
}
|
||||
|
||||
$rawMime = '';
|
||||
if (is_object($capturedMailer) && method_exists($capturedMailer, 'getSentMIMEMessage')) {
|
||||
$rawMime = (string) $capturedMailer->getSentMIMEMessage();
|
||||
}
|
||||
if ($rawMime === '') {
|
||||
throw new \RuntimeException('WordPress mail send succeeded but raw MIME was unavailable for Sent-folder append.');
|
||||
}
|
||||
|
||||
return ['raw_mime' => $rawMime];
|
||||
}
|
||||
|
||||
/** @param list<string> $cc @param list<string> $bcc */
|
||||
private function headers(string $from, string $fromName, array $cc, array $bcc, ?string $replyTo): array
|
||||
{
|
||||
$headers = [
|
||||
'Content-Type: text/html; charset=UTF-8',
|
||||
'From: ' . ($fromName !== '' ? sprintf('%s <%s>', $fromName, $from) : $from),
|
||||
];
|
||||
|
||||
foreach ($cc as $address) {
|
||||
$address = trim($address);
|
||||
if ($address !== '') {
|
||||
$headers[] = 'Cc: ' . $address;
|
||||
}
|
||||
}
|
||||
foreach ($bcc as $address) {
|
||||
$address = trim($address);
|
||||
if ($address !== '') {
|
||||
$headers[] = 'Bcc: ' . $address;
|
||||
}
|
||||
}
|
||||
|
||||
$replyTo = trim((string) $replyTo);
|
||||
if ($replyTo !== '') {
|
||||
$headers[] = 'Reply-To: ' . $replyTo;
|
||||
}
|
||||
|
||||
return $headers;
|
||||
}
|
||||
|
||||
private function mailDomainFromAddress(string $address): string
|
||||
{
|
||||
$address = trim($address);
|
||||
$at = strrpos($address, '@');
|
||||
if ($at === false) {
|
||||
throw new \RuntimeException('SMTP From email must contain a domain for SMTP EHLO.');
|
||||
}
|
||||
|
||||
$domain = strtolower(trim(substr($address, $at + 1), " \t\r\n.<>"));
|
||||
if ($domain === '' || strlen($domain) > 253) {
|
||||
throw new \RuntimeException('SMTP From email domain is invalid.');
|
||||
}
|
||||
|
||||
$labels = explode('.', $domain);
|
||||
foreach ($labels as $label) {
|
||||
if (
|
||||
$label === ''
|
||||
|| strlen($label) > 63
|
||||
|| preg_match('/[^a-z0-9-]/', $label) === 1
|
||||
|| str_starts_with($label, '-')
|
||||
|| str_ends_with($label, '-')
|
||||
) {
|
||||
throw new \RuntimeException('SMTP From email domain is invalid.');
|
||||
}
|
||||
}
|
||||
|
||||
return $domain;
|
||||
}
|
||||
}
|
||||
|
|
@ -26,12 +26,12 @@ use FecaMailshots\Application\PdfAssetService;
|
|||
use FecaMailshots\Application\SecretKeyProvider;
|
||||
use FecaMailshots\Application\TemplateRenderer;
|
||||
use FecaMailshots\Domain\DslParser;
|
||||
use FecaMailshots\Infrastructure\BasicSmtpSender;
|
||||
use FecaMailshots\Infrastructure\DatabaseRouter;
|
||||
use FecaMailshots\Infrastructure\MailshotSchemaInstaller;
|
||||
use FecaMailshots\Infrastructure\DatabaseSourceMetadataProvider;
|
||||
use FecaMailshots\Infrastructure\PhpImapAppender;
|
||||
use FecaMailshots\Infrastructure\PdoDatabaseRouter;
|
||||
use FecaMailshots\Infrastructure\WordPressSmtpSender;
|
||||
use FecaMailshots\Infrastructure\WordPressSaltSecretKeyProvider;
|
||||
use FecaMailshots\Repository\AttachmentRepository;
|
||||
use FecaMailshots\Repository\LastRunRepository;
|
||||
|
|
@ -98,7 +98,7 @@ final class Plugin
|
|||
return $c->get(PdfAssetRepository::class)->findByName($name);
|
||||
}
|
||||
));
|
||||
$c->set(SmtpSender::class, static fn() => new BasicSmtpSender());
|
||||
$c->set(SmtpSender::class, static fn() => new WordPressSmtpSender());
|
||||
$c->set(ImapAppender::class, static fn() => new PhpImapAppender());
|
||||
$c->set(MailCredentialsProvider::class, static fn(Container $c) => new PerUserMailCredentialsProvider(
|
||||
$c->get(MailCredentialRepository::class),
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ test.describe('new selected-rows and all-recipients features', () => {
|
|||
await dialog.dismiss();
|
||||
});
|
||||
|
||||
await page.getByRole('button', { name: 'Send Test Email' }).click();
|
||||
await page.getByRole('button', { name: 'Send Test for all to Test Email Address' }).click();
|
||||
await expect.poll(() => dialogText).toContain(`Send ${recipientCount} emails to the entered address?`);
|
||||
} finally {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -1,114 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once dirname(__DIR__, 2) . '/feca_mailshots_plugin/src/autoload.php';
|
||||
|
||||
use FecaMailshots\Infrastructure\BasicSmtpSender;
|
||||
|
||||
$sender = new BasicSmtpSender();
|
||||
$ref = new ReflectionClass($sender);
|
||||
$buildMime = $ref->getMethod('buildMime');
|
||||
$buildMime->setAccessible(true);
|
||||
|
||||
$plain = $buildMime->invoke(
|
||||
$sender,
|
||||
'from@example.org',
|
||||
'From Name',
|
||||
['to@example.org'],
|
||||
[],
|
||||
[],
|
||||
"All Saint's café",
|
||||
'<p>Hello</p>',
|
||||
null,
|
||||
[]
|
||||
);
|
||||
|
||||
if (!is_string($plain) || strpos($plain, 'Content-Type: text/html; charset=UTF-8') === false) {
|
||||
fwrite(STDERR, "Plain MIME rendering missing HTML content-type header\n");
|
||||
exit(1);
|
||||
}
|
||||
if (strpos($plain, "Subject: All Saint's café") !== false) {
|
||||
fwrite(STDERR, "Subject header should not contain raw UTF-8/punctuation text\n");
|
||||
exit(1);
|
||||
}
|
||||
if (strpos($plain, 'Subject: =?UTF-8?B?QWxsIFNhaW50J3MgY2Fmw6k=?=') === false) {
|
||||
fwrite(STDERR, "Subject header missing expected RFC 2047 encoding\n");
|
||||
exit(1);
|
||||
}
|
||||
if (strpos($plain, 'From: =?UTF-8?B?RnJvbSBOYW1l?= <from@example.org>') === false) {
|
||||
fwrite(STDERR, "From display name missing expected RFC 2047 encoding\n");
|
||||
exit(1);
|
||||
}
|
||||
if (strpos($plain, 'Content-Transfer-Encoding: quoted-printable') === false) {
|
||||
fwrite(STDERR, "Plain MIME rendering should use quoted-printable HTML body encoding\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$longHtml = '<p>' . str_repeat('LongHtmlSegment', 120) . '</p>';
|
||||
$longPlain = $buildMime->invoke(
|
||||
$sender,
|
||||
'from@example.org',
|
||||
'From Name',
|
||||
['to@example.org'],
|
||||
[],
|
||||
[],
|
||||
'Subject',
|
||||
$longHtml,
|
||||
null,
|
||||
[]
|
||||
);
|
||||
|
||||
if (!is_string($longPlain)) {
|
||||
fwrite(STDERR, "Long plain MIME rendering did not return a string\n");
|
||||
exit(1);
|
||||
}
|
||||
foreach (preg_split('/\r\n/', $longPlain) ?: [] as $line) {
|
||||
if (strlen($line) > 998) {
|
||||
fwrite(STDERR, "Plain MIME contains an RFC 5322 overlong line\n");
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
$bytes = random_bytes(256 * 1024);
|
||||
$mime = $buildMime->invoke(
|
||||
$sender,
|
||||
'from@example.org',
|
||||
'From Name',
|
||||
['to@example.org'],
|
||||
[],
|
||||
[],
|
||||
'Subject',
|
||||
'<p>Hello</p>',
|
||||
null,
|
||||
[[
|
||||
'filename' => 'sample.jpg',
|
||||
'mime_type' => 'image/jpeg',
|
||||
'content_bytes' => $bytes,
|
||||
]]
|
||||
);
|
||||
|
||||
if (!is_string($mime) || strpos($mime, 'multipart/mixed') === false) {
|
||||
fwrite(STDERR, "Multipart MIME expected for attachment payload\n");
|
||||
exit(1);
|
||||
}
|
||||
if (strpos($mime, 'Content-Type: image/jpeg; name="sample.jpg"') === false) {
|
||||
fwrite(STDERR, "Attachment MIME part missing expected content-type/filename\n");
|
||||
exit(1);
|
||||
}
|
||||
if (strpos($mime, 'Content-Transfer-Encoding: quoted-printable') === false) {
|
||||
fwrite(STDERR, "Multipart HTML part should use quoted-printable body encoding\n");
|
||||
exit(1);
|
||||
}
|
||||
if (strpos($mime, base64_encode(substr($bytes, 0, 24))) === false) {
|
||||
fwrite(STDERR, "Attachment bytes do not appear to be base64-encoded into MIME payload\n");
|
||||
exit(1);
|
||||
}
|
||||
foreach (preg_split('/\r\n/', $mime) ?: [] as $line) {
|
||||
if (strlen($line) > 998) {
|
||||
fwrite(STDERR, "Multipart MIME contains an RFC 5322 overlong line\n");
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
echo "BasicSmtpSender MIME regression test passed\n";
|
||||
|
|
@ -0,0 +1,186 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
$GLOBALS['feca_test_actions'] = [];
|
||||
$GLOBALS['feca_test_mailer'] = null;
|
||||
$GLOBALS['feca_test_last_mail'] = null;
|
||||
|
||||
if (!function_exists('add_action')) {
|
||||
function add_action(string $hook, callable $callback): void
|
||||
{
|
||||
$GLOBALS['feca_test_actions'][$hook][] = $callback;
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('remove_action')) {
|
||||
function remove_action(string $hook, callable $callback): void
|
||||
{
|
||||
if (!isset($GLOBALS['feca_test_actions'][$hook])) {
|
||||
return;
|
||||
}
|
||||
$GLOBALS['feca_test_actions'][$hook] = array_values(array_filter(
|
||||
$GLOBALS['feca_test_actions'][$hook],
|
||||
static fn(callable $registered): bool => $registered !== $callback
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('wp_mail')) {
|
||||
function wp_mail($to, string $subject, string $message, $headers = '', $attachments = []): bool
|
||||
{
|
||||
$mailer = new FecaTestMailer();
|
||||
foreach ($GLOBALS['feca_test_actions']['phpmailer_init'] ?? [] as $callback) {
|
||||
$callback($mailer);
|
||||
}
|
||||
|
||||
$headersList = is_array($headers) ? $headers : preg_split('/\r\n|\r|\n/', (string) $headers);
|
||||
foreach ($headersList ?: [] as $header) {
|
||||
$header = trim((string) $header);
|
||||
if (stripos($header, 'Bcc:') === 0) {
|
||||
$mailer->bcc[] = trim(substr($header, 4));
|
||||
}
|
||||
if (stripos($header, 'Cc:') === 0) {
|
||||
$mailer->cc[] = trim(substr($header, 3));
|
||||
}
|
||||
}
|
||||
|
||||
$mailer->to = is_array($to) ? array_values($to) : [(string) $to];
|
||||
$mailer->Subject = $subject;
|
||||
$mailer->Body = $message;
|
||||
$mailer->sentMime = "Date: Fri, 26 Jun 2026 10:00:00 +0000\r\n"
|
||||
. "Message-ID: <fixture@example.org>\r\n"
|
||||
. "To: " . implode(', ', $mailer->to) . "\r\n"
|
||||
. "Cc: " . implode(', ', $mailer->cc) . "\r\n"
|
||||
. "Subject: " . $subject . "\r\n\r\n"
|
||||
. $message;
|
||||
|
||||
$GLOBALS['feca_test_mailer'] = $mailer;
|
||||
$GLOBALS['feca_test_last_mail'] = [
|
||||
'to' => $to,
|
||||
'subject' => $subject,
|
||||
'message' => $message,
|
||||
'headers' => $headers,
|
||||
'attachments' => $attachments,
|
||||
];
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
final class FecaTestMailer
|
||||
{
|
||||
public string $Host = '';
|
||||
public int $Port = 0;
|
||||
public bool $SMTPAuth = false;
|
||||
public string $Username = '';
|
||||
public string $Password = '';
|
||||
public string $Helo = '';
|
||||
public string $CharSet = '';
|
||||
public string $SMTPSecure = '';
|
||||
public string $MessageID = '';
|
||||
public bool $SMTPAutoTLS = false;
|
||||
public string $ErrorInfo = '';
|
||||
public string $Subject = '';
|
||||
public string $Body = '';
|
||||
public string $sentMime = '';
|
||||
/** @var list<string> */
|
||||
public array $to = [];
|
||||
/** @var list<string> */
|
||||
public array $cc = [];
|
||||
/** @var list<string> */
|
||||
public array $bcc = [];
|
||||
/** @var list<array{bytes:string,filename:string,encoding:string,mime_type:string}> */
|
||||
public array $stringAttachments = [];
|
||||
|
||||
public function isSMTP(): void
|
||||
{
|
||||
}
|
||||
|
||||
public function setFrom(string $address, string $name = '', bool $auto = true): void
|
||||
{
|
||||
}
|
||||
|
||||
public function addStringAttachment(string $bytes, string $filename, string $encoding, string $mimeType): void
|
||||
{
|
||||
$this->stringAttachments[] = [
|
||||
'bytes' => $bytes,
|
||||
'filename' => $filename,
|
||||
'encoding' => $encoding,
|
||||
'mime_type' => $mimeType,
|
||||
];
|
||||
}
|
||||
|
||||
public function getSentMIMEMessage(): string
|
||||
{
|
||||
return $this->sentMime;
|
||||
}
|
||||
}
|
||||
|
||||
require_once dirname(__DIR__, 2) . '/feca_mailshots_plugin/src/autoload.php';
|
||||
|
||||
use FecaMailshots\Infrastructure\WordPressSmtpSender;
|
||||
|
||||
$sender = new WordPressSmtpSender();
|
||||
$result = $sender->send(
|
||||
[
|
||||
'smtp_host' => 'smtp.example.org',
|
||||
'smtp_port' => 587,
|
||||
'smtp_user' => 'editor@example.org',
|
||||
'smtp_password' => 'secret',
|
||||
'smtp_from_email' => 'Editor@Example.ORG',
|
||||
'smtp_from_name' => 'Editor',
|
||||
'smtp_require_tls' => false,
|
||||
],
|
||||
['to@example.org'],
|
||||
['copy@example.org'],
|
||||
['hidden@example.org'],
|
||||
'Subject',
|
||||
'<p>Hello</p>',
|
||||
'reply@example.org',
|
||||
[[
|
||||
'filename' => 'notice.pdf',
|
||||
'mime_type' => 'application/pdf',
|
||||
'content_bytes' => '%PDF fixture',
|
||||
]]
|
||||
);
|
||||
|
||||
$mailer = $GLOBALS['feca_test_mailer'];
|
||||
if (!$mailer instanceof FecaTestMailer) {
|
||||
fwrite(STDERR, "WordPress mailer was not configured\n");
|
||||
exit(1);
|
||||
}
|
||||
if ($mailer->Host !== 'smtp.example.org' || $mailer->Port !== 587 || $mailer->Username !== 'editor@example.org') {
|
||||
fwrite(STDERR, "SMTP credentials were not applied to WordPress mailer\n");
|
||||
exit(1);
|
||||
}
|
||||
if ($mailer->Helo !== 'example.org') {
|
||||
fwrite(STDERR, "SMTP EHLO domain should be derived from From email\n");
|
||||
exit(1);
|
||||
}
|
||||
if (preg_match('/^<\\d{14}\\.[a-f0-9]{32}@example\\.org>$/', $mailer->MessageID) !== 1) {
|
||||
fwrite(STDERR, "Message-ID should be generated with the From email domain\n");
|
||||
exit(1);
|
||||
}
|
||||
if ($mailer->bcc !== ['hidden@example.org']) {
|
||||
fwrite(STDERR, "BCC recipients should still be delivered through WordPress mail\n");
|
||||
exit(1);
|
||||
}
|
||||
if (preg_match('/^Bcc:/mi', (string) $result['raw_mime']) === 1) {
|
||||
fwrite(STDERR, "Raw MIME should not expose Bcc header\n");
|
||||
exit(1);
|
||||
}
|
||||
if ($mailer->stringAttachments === [] || $mailer->stringAttachments[0]['filename'] !== 'notice.pdf') {
|
||||
fwrite(STDERR, "In-memory attachment was not added to WordPress mailer\n");
|
||||
exit(1);
|
||||
}
|
||||
if (($GLOBALS['feca_test_actions']['phpmailer_init'] ?? []) !== []) {
|
||||
fwrite(STDERR, "phpmailer_init hook should be removed after send\n");
|
||||
exit(1);
|
||||
}
|
||||
if (strpos((string) $result['raw_mime'], 'Message-ID: <fixture@example.org>') === false) {
|
||||
fwrite(STDERR, "Raw MIME from WordPress mailer was not returned\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
echo "WordPressSmtpSender regression test passed\n";
|
||||
Loading…
Reference in New Issue