failed attempt to avoid wordpress critical error sending test mailshot
This commit is contained in:
parent
ce50edc04a
commit
10395f0480
|
|
@ -3,7 +3,7 @@
|
||||||
* Plugin Name: FECA Mailshots
|
* Plugin Name: FECA Mailshots
|
||||||
* Plugin URI: https://fenedge.co.uk/
|
* Plugin URI: https://fenedge.co.uk/
|
||||||
* Description: FECA mailshots plugin.
|
* Description: FECA mailshots plugin.
|
||||||
* Version: 1.0.31
|
* Version: 1.0.33
|
||||||
* Requires at least: 6.0
|
* Requires at least: 6.0
|
||||||
* Requires PHP: 7.4
|
* Requires PHP: 7.4
|
||||||
* Author: FECA
|
* Author: FECA
|
||||||
|
|
|
||||||
|
|
@ -231,10 +231,7 @@ final class RunMailshotAdminPage
|
||||||
if (!$this->enforceMutationGuardOrJson(self::CAPABILITY, self::NONCE_ACTION)) {
|
if (!$this->enforceMutationGuardOrJson(self::CAPABILITY, self::NONCE_ACTION)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
$mailshotId = (int) ($this->wp->requestParam('mailshot_id', '0') ?? '0');
|
$this->handleRunAction(static fn(self $page, int $mailshotId): array => $page->runService()->runMailshot($mailshotId));
|
||||||
$result = $this->runService()->runMailshot($mailshotId);
|
|
||||||
$this->wp->updateOption(self::RESULT_OPTION_KEY, $result);
|
|
||||||
$this->redirect($mailshotId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function handleRunSelectedUi(): void
|
public function handleRunSelectedUi(): void
|
||||||
|
|
@ -242,11 +239,9 @@ final class RunMailshotAdminPage
|
||||||
if (!$this->enforceMutationGuardOrJson(self::CAPABILITY, self::NONCE_ACTION)) {
|
if (!$this->enforceMutationGuardOrJson(self::CAPABILITY, self::NONCE_ACTION)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
$mailshotId = (int) ($this->wp->requestParam('mailshot_id', '0') ?? '0');
|
$this->handleRunAction(static function (self $page, int $mailshotId): array {
|
||||||
$selectedRecipientKeys = $this->selectedRecipientKeysFromRequest();
|
return $page->runService()->runMailshotSelected($mailshotId, $page->selectedRecipientKeysFromRequest());
|
||||||
$result = $this->runService()->runMailshotSelected($mailshotId, $selectedRecipientKeys);
|
});
|
||||||
$this->wp->updateOption(self::RESULT_OPTION_KEY, $result);
|
|
||||||
$this->redirect($mailshotId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function handleRetryFailedUi(): void
|
public function handleRetryFailedUi(): void
|
||||||
|
|
@ -254,10 +249,7 @@ final class RunMailshotAdminPage
|
||||||
if (!$this->enforceMutationGuardOrJson(self::CAPABILITY, self::NONCE_ACTION)) {
|
if (!$this->enforceMutationGuardOrJson(self::CAPABILITY, self::NONCE_ACTION)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
$mailshotId = (int) ($this->wp->requestParam('mailshot_id', '0') ?? '0');
|
$this->handleRunAction(static fn(self $page, int $mailshotId): array => $page->runService()->retryFailed($mailshotId));
|
||||||
$result = $this->runService()->retryFailed($mailshotId);
|
|
||||||
$this->wp->updateOption(self::RESULT_OPTION_KEY, $result);
|
|
||||||
$this->redirect($mailshotId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function handleRetryRecipientUi(): void
|
public function handleRetryRecipientUi(): void
|
||||||
|
|
@ -265,13 +257,52 @@ final class RunMailshotAdminPage
|
||||||
if (!$this->enforceMutationGuardOrJson(self::CAPABILITY, self::NONCE_ACTION)) {
|
if (!$this->enforceMutationGuardOrJson(self::CAPABILITY, self::NONCE_ACTION)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
$this->handleRunAction(static function (self $page, int $mailshotId): array {
|
||||||
|
$recipientKey = (string) ($page->wp->requestParam('recipient_key', '') ?? '');
|
||||||
|
return $page->runService()->retryRecipient($mailshotId, $recipientKey);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param callable(self,int):array<string,mixed> $action */
|
||||||
|
private function handleRunAction(callable $action): void
|
||||||
|
{
|
||||||
$mailshotId = (int) ($this->wp->requestParam('mailshot_id', '0') ?? '0');
|
$mailshotId = (int) ($this->wp->requestParam('mailshot_id', '0') ?? '0');
|
||||||
$recipientKey = (string) ($this->wp->requestParam('recipient_key', '') ?? '');
|
$completed = false;
|
||||||
$result = $this->runService()->retryRecipient($mailshotId, $recipientKey);
|
$fatalHandled = false;
|
||||||
|
|
||||||
|
register_shutdown_function(function () use (&$completed, &$fatalHandled, $mailshotId): void {
|
||||||
|
if ($completed || $fatalHandled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$error = error_get_last();
|
||||||
|
if (!is_array($error) || !in_array((int) ($error['type'] ?? 0), [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR], true)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$fatalHandled = true;
|
||||||
|
$this->storeRunFailure('Run action failed: ' . (string) ($error['message'] ?? 'Unknown fatal error.'));
|
||||||
|
$this->redirect($mailshotId);
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
$result = $action($this, $mailshotId);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$result = ['ok' => false, 'errors' => ['Run action failed: ' . $e->getMessage()]];
|
||||||
|
}
|
||||||
|
|
||||||
|
$completed = true;
|
||||||
$this->wp->updateOption(self::RESULT_OPTION_KEY, $result);
|
$this->wp->updateOption(self::RESULT_OPTION_KEY, $result);
|
||||||
$this->redirect($mailshotId);
|
$this->redirect($mailshotId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function storeRunFailure(string $message): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$this->wp->updateOption(self::RESULT_OPTION_KEY, ['ok' => false, 'errors' => [$message]]);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
error_log('Mailshots run failure could not be stored: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private function redirect(int $mailshotId): void
|
private function redirect(int $mailshotId): void
|
||||||
{
|
{
|
||||||
$url = $this->wp->adminUrl('admin.php?page=feca-mailshots-run&mailshot_id=' . $mailshotId);
|
$url = $this->wp->adminUrl('admin.php?page=feca-mailshots-run&mailshot_id=' . $mailshotId);
|
||||||
|
|
|
||||||
|
|
@ -264,6 +264,16 @@ final class MailshotRunService
|
||||||
|
|
||||||
/** @return array<string, mixed> */
|
/** @return array<string, mixed> */
|
||||||
public function runMailshot(int $mailshotId): array
|
public function runMailshot(int $mailshotId): array
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
return $this->runMailshotUnsafe($mailshotId);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
return ['ok' => false, 'errors' => ['Run mailshot failed: ' . $e->getMessage()]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<string, mixed> */
|
||||||
|
private function runMailshotUnsafe(int $mailshotId): array
|
||||||
{
|
{
|
||||||
$mailshot = $this->mailshots->find($mailshotId);
|
$mailshot = $this->mailshots->find($mailshotId);
|
||||||
if ($mailshot === null) {
|
if ($mailshot === null) {
|
||||||
|
|
@ -286,6 +296,16 @@ final class MailshotRunService
|
||||||
|
|
||||||
/** @param list<string> $selectedRecipientKeys @return array<string, mixed> */
|
/** @param list<string> $selectedRecipientKeys @return array<string, mixed> */
|
||||||
public function runMailshotSelected(int $mailshotId, array $selectedRecipientKeys): array
|
public function runMailshotSelected(int $mailshotId, array $selectedRecipientKeys): array
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
return $this->runMailshotSelectedUnsafe($mailshotId, $selectedRecipientKeys);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
return ['ok' => false, 'errors' => ['Run selected mailshot failed: ' . $e->getMessage()]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param list<string> $selectedRecipientKeys @return array<string, mixed> */
|
||||||
|
private function runMailshotSelectedUnsafe(int $mailshotId, array $selectedRecipientKeys): array
|
||||||
{
|
{
|
||||||
$selectedRecipientKeys = array_values(array_filter(array_map(static fn($v): string => trim((string) $v), $selectedRecipientKeys), static fn(string $v): bool => $v !== ''));
|
$selectedRecipientKeys = array_values(array_filter(array_map(static fn($v): string => trim((string) $v), $selectedRecipientKeys), static fn(string $v): bool => $v !== ''));
|
||||||
if ($selectedRecipientKeys === []) {
|
if ($selectedRecipientKeys === []) {
|
||||||
|
|
@ -323,6 +343,16 @@ final class MailshotRunService
|
||||||
|
|
||||||
/** @return array<string, mixed> */
|
/** @return array<string, mixed> */
|
||||||
public function retryFailed(int $mailshotId): array
|
public function retryFailed(int $mailshotId): array
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
return $this->retryFailedUnsafe($mailshotId);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
return ['ok' => false, 'errors' => ['Retry failed sends failed: ' . $e->getMessage()]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<string, mixed> */
|
||||||
|
private function retryFailedUnsafe(int $mailshotId): array
|
||||||
{
|
{
|
||||||
$creds = $this->credentials->credentials();
|
$creds = $this->credentials->credentials();
|
||||||
if ($creds === null) {
|
if ($creds === null) {
|
||||||
|
|
@ -351,6 +381,16 @@ final class MailshotRunService
|
||||||
|
|
||||||
/** @return array<string, mixed> */
|
/** @return array<string, mixed> */
|
||||||
public function retryRecipient(int $mailshotId, string $recipientKey): array
|
public function retryRecipient(int $mailshotId, string $recipientKey): array
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
return $this->retryRecipientUnsafe($mailshotId, $recipientKey);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
return ['ok' => false, 'errors' => ['Retry recipient failed: ' . $e->getMessage()]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<string, mixed> */
|
||||||
|
private function retryRecipientUnsafe(int $mailshotId, string $recipientKey): array
|
||||||
{
|
{
|
||||||
$recipientKey = trim($recipientKey);
|
$recipientKey = trim($recipientKey);
|
||||||
if ($recipientKey === '') {
|
if ($recipientKey === '') {
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,7 @@ final class BasicSmtpSender implements SmtpSender
|
||||||
private function buildMime(string $from, string $fromName, array $to, array $cc, array $bcc, string $subject, string $htmlBody, ?string $replyTo, array $attachments): string
|
private function buildMime(string $from, string $fromName, array $to, array $cc, array $bcc, string $subject, string $htmlBody, ?string $replyTo, array $attachments): string
|
||||||
{
|
{
|
||||||
$headers = [];
|
$headers = [];
|
||||||
$fromHeader = $fromName !== '' ? sprintf('%s <%s>', $fromName, $from) : $from;
|
$fromHeader = $fromName !== '' ? sprintf('%s <%s>', $this->encodeHeaderValue($fromName), $from) : $from;
|
||||||
$headers[] = 'From: ' . $fromHeader;
|
$headers[] = 'From: ' . $fromHeader;
|
||||||
$headers[] = 'To: ' . implode(', ', $to);
|
$headers[] = 'To: ' . implode(', ', $to);
|
||||||
if ($cc !== []) {
|
if ($cc !== []) {
|
||||||
|
|
@ -81,7 +81,7 @@ final class BasicSmtpSender implements SmtpSender
|
||||||
if ($replyTo !== null && trim($replyTo) !== '') {
|
if ($replyTo !== null && trim($replyTo) !== '') {
|
||||||
$headers[] = 'Reply-To: ' . trim($replyTo);
|
$headers[] = 'Reply-To: ' . trim($replyTo);
|
||||||
}
|
}
|
||||||
$headers[] = 'Subject: ' . $subject;
|
$headers[] = 'Subject: ' . $this->encodeHeaderValue($subject);
|
||||||
$headers[] = 'MIME-Version: 1.0';
|
$headers[] = 'MIME-Version: 1.0';
|
||||||
if ($attachments === []) {
|
if ($attachments === []) {
|
||||||
$headers[] = 'Content-Type: text/html; charset=UTF-8';
|
$headers[] = 'Content-Type: text/html; charset=UTF-8';
|
||||||
|
|
@ -115,6 +115,39 @@ final class BasicSmtpSender implements SmtpSender
|
||||||
return $mime;
|
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
|
private function encodeBase64Chunked(string $bytes): string
|
||||||
{
|
{
|
||||||
$stream = fopen('php://temp', 'w+b');
|
$stream = fopen('php://temp', 'w+b');
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,10 @@
|
||||||
* [X] "contacts and accounts and issues where fen1-contact and issue(125) and member-or-affiliate-or-parish-council" gives too many (103) - need is_deleted filter generates error: "SQLSTATE[HY000]: General error: 1267 Illegal mix of collations (utf8mb4_general_ci,IMPLICIT) and (utf8mb4_unicode_ci,IMPLICIT) for operation '='"
|
* [X] "contacts and accounts and issues where fen1-contact and issue(125) and member-or-affiliate-or-parish-council" gives too many (103) - need is_deleted filter generates error: "SQLSTATE[HY000]: General error: 1267 Illegal mix of collations (utf8mb4_general_ci,IMPLICIT) and (utf8mb4_unicode_ci,IMPLICIT) for operation '='"
|
||||||
* [X] "contacts and accounts and issues where fen1-contact and issue(125) and member-or-affiliate-or-parish-council and account-has-article-in-issue(125)" validates OK, but preview gives: General error: 1267 Illegal mix of collations (utf8mb4_general_ci,IMPLICIT) and (utf8mb4_unicode_ci,IMPLICIT) for operation '='
|
* [X] "contacts and accounts and issues where fen1-contact and issue(125) and member-or-affiliate-or-parish-council and account-has-article-in-issue(125)" validates OK, but preview gives: General error: 1267 Illegal mix of collations (utf8mb4_general_ci,IMPLICIT) and (utf8mb4_unicode_ci,IMPLICIT) for operation '='
|
||||||
* [X] issues.CopyDate and issue.PublicationDate render as e.g. "2026-02-23 00:00:00", would prefer to render dates as e.g. "Friday 23 February 2026"
|
* [X] issues.CopyDate and issue.PublicationDate render as e.g. "2026-02-23 00:00:00", would prefer to render dates as e.g. "Friday 23 February 2026"
|
||||||
* [ ] I made an edit to a mailshot (), with a 9kb message. Save did not close the dialog - but did not show any errors. There was a long pause (long enough to start typing this report) before it appeared to have had an effect (mailshot saved message). But Edit button on Mailshot doesn't appear to do anything now browser shows page still loading. https://fenedge.co.uk is not responding.
|
* [X] I made an edit to a mailshot (), with a 9kb message. Save did not close the dialog - but did not show any errors. There was a long pause (long enough to start typing this report) before it appeared to have had an effect (mailshot saved message). But Edit button on Mailshot doesn't appear to do anything now browser shows page still loading. https://fenedge.co.uk is not responding.
|
||||||
* [ ] Edit Data Source save DSL="ads and advertisers where issue(125)" takes a very long long time to repond.
|
* [X] Edit Data Source save DSL="ads and advertisers where issue(125)" takes a very long long time to repond.
|
||||||
* [X] Downloaded merged pdf or .zip should be named after maiilshot purpose
|
* [X] Downloaded merged pdf or .zip should be named after maiilshot purpose
|
||||||
* [X] Mailshot data sources. Add "Duplicate" button per row.
|
* [X] Mailshot data sources. Add "Duplicate" button per row.
|
||||||
|
* [X] Select a single row in "review recipients". "Run Mailshot to selected rows" says "Run mailshot to 1 selected rows?". Yes -> error "None of the selected recipient rows exist in the current query result."
|
||||||
|
* [X] Error report from wordpress. - packaging was missing a file
|
||||||
|
* [ ] Subject to "Memory box café generates an error from receiveing yahoo email "subject contains an invalid character". Need to check rules for character set in subject and modify special characters accordingly.
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ $plain = $buildMime->invoke(
|
||||||
['to@example.org'],
|
['to@example.org'],
|
||||||
[],
|
[],
|
||||||
[],
|
[],
|
||||||
'Subject',
|
"All Saint's café",
|
||||||
'<p>Hello</p>',
|
'<p>Hello</p>',
|
||||||
null,
|
null,
|
||||||
[]
|
[]
|
||||||
|
|
@ -28,6 +28,18 @@ if (!is_string($plain) || strpos($plain, 'Content-Type: text/html; charset=UTF-8
|
||||||
fwrite(STDERR, "Plain MIME rendering missing HTML content-type header\n");
|
fwrite(STDERR, "Plain MIME rendering missing HTML content-type header\n");
|
||||||
exit(1);
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
$bytes = random_bytes(256 * 1024);
|
$bytes = random_bytes(256 * 1024);
|
||||||
$mime = $buildMime->invoke(
|
$mime = $buildMime->invoke(
|
||||||
|
|
@ -61,4 +73,3 @@ if (strpos($mime, base64_encode(substr($bytes, 0, 24))) === false) {
|
||||||
}
|
}
|
||||||
|
|
||||||
echo "BasicSmtpSender MIME regression test passed\n";
|
echo "BasicSmtpSender MIME regression test passed\n";
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue