testing complete
This commit is contained in:
parent
10395f0480
commit
e64a08d05e
|
|
@ -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.33
|
* Version: 1.1.0
|
||||||
* Requires at least: 6.0
|
* Requires at least: 6.0
|
||||||
* Requires PHP: 7.4
|
* Requires PHP: 7.4
|
||||||
* Author: FECA
|
* Author: FECA
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ Contributors: feca
|
||||||
Requires at least: 6.0
|
Requires at least: 6.0
|
||||||
Tested up to: 6.5
|
Tested up to: 6.5
|
||||||
Requires PHP: 7.4
|
Requires PHP: 7.4
|
||||||
Stable tag: 1.0.0
|
Stable tag: 1.1.0
|
||||||
License: GPLv2 or later
|
License: GPLv2 or later
|
||||||
License URI: https://www.gnu.org/licenses/gpl-2.0.html
|
License URI: https://www.gnu.org/licenses/gpl-2.0.html
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,8 @@ trait AdminRequestHelpers
|
||||||
. '.feca-mailshots-admin .feca-minw-240{min-width:240px;}'
|
. '.feca-mailshots-admin .feca-minw-240{min-width:240px;}'
|
||||||
. '.feca-mailshots-admin .feca-control input,.feca-mailshots-admin .feca-control select,.feca-mailshots-admin .feca-control textarea{margin:0;}'
|
. '.feca-mailshots-admin .feca-control input,.feca-mailshots-admin .feca-control select,.feca-mailshots-admin .feca-control textarea{margin:0;}'
|
||||||
. '.feca-mailshots-admin .feca-control label{white-space:nowrap;padding-left:var(--feca-space-1);font-weight:600;}'
|
. '.feca-mailshots-admin .feca-control label{white-space:nowrap;padding-left:var(--feca-space-1);font-weight:600;}'
|
||||||
|
. '.feca-mailshots-admin .feca-inline-control-group{display:flex;align-items:flex-end;gap:var(--feca-space-2);flex-wrap:wrap;padding:8px 10px;border:1px solid #dcdcde;background:#f6f7f7;border-radius:var(--feca-radius);}'
|
||||||
|
. '.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{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-button-row .button{margin:0 !important;}'
|
||||||
. '.feca-mailshots-admin .feca-button-danger{color:#b32d2e;border-color:#d63638;}'
|
. '.feca-mailshots-admin .feca-button-danger{color:#b32d2e;border-color:#d63638;}'
|
||||||
|
|
@ -105,6 +107,7 @@ trait AdminRequestHelpers
|
||||||
. '@media (max-width: 782px){'
|
. '@media (max-width: 782px){'
|
||||||
. '.feca-mailshots-admin .feca-control-row{display:block;}'
|
. '.feca-mailshots-admin .feca-control-row{display:block;}'
|
||||||
. '.feca-mailshots-admin .feca-control{min-width:0;width:100%;}'
|
. '.feca-mailshots-admin .feca-control{min-width:0;width:100%;}'
|
||||||
|
. '.feca-mailshots-admin .feca-inline-control-group{display:block;}'
|
||||||
. '}'
|
. '}'
|
||||||
. '</style>';
|
. '</style>';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ final class MailshotTestAdminPage
|
||||||
private const NONCE_ACTION = 'feca_mailshots_test';
|
private const NONCE_ACTION = 'feca_mailshots_test';
|
||||||
private const DOWNLOAD_MEMORY_LIMIT_ENV = 'FECA_MAILSHOTS_DOWNLOAD_MEMORY_LIMIT';
|
private const DOWNLOAD_MEMORY_LIMIT_ENV = 'FECA_MAILSHOTS_DOWNLOAD_MEMORY_LIMIT';
|
||||||
private const DOWNLOAD_MEMORY_LIMIT_OPTION = 'feca_mailshots_download_memory_limit';
|
private const DOWNLOAD_MEMORY_LIMIT_OPTION = 'feca_mailshots_download_memory_limit';
|
||||||
|
private const DEFAULT_DOWNLOAD_MEMORY_LIMIT = '512M';
|
||||||
|
|
||||||
/** @var callable(): MailshotRunService */
|
/** @var callable(): MailshotRunService */
|
||||||
private $runServiceFactory;
|
private $runServiceFactory;
|
||||||
|
|
@ -54,7 +55,17 @@ final class MailshotTestAdminPage
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$pageErrors = [];
|
||||||
|
$memoryError = $this->maybeRaiseMemoryLimit($this->resolveDownloadMemoryLimitTarget());
|
||||||
|
if ($memoryError !== null) {
|
||||||
|
$pageErrors[] = $memoryError;
|
||||||
|
}
|
||||||
|
try {
|
||||||
$mailshots = $this->mailshotService()->list();
|
$mailshots = $this->mailshotService()->list();
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$mailshots = [];
|
||||||
|
$pageErrors[] = $this->diagnosticError('Unable to load mailshots', 'loading test page', $e);
|
||||||
|
}
|
||||||
$selectedMailshotId = (int) ($this->wp->requestParam('mailshot_id', '0') ?? '0');
|
$selectedMailshotId = (int) ($this->wp->requestParam('mailshot_id', '0') ?? '0');
|
||||||
if ($selectedMailshotId <= 0 && $mailshots !== []) {
|
if ($selectedMailshotId <= 0 && $mailshots !== []) {
|
||||||
$selectedMailshotId = (int) ($mailshots[0]['id'] ?? 0);
|
$selectedMailshotId = (int) ($mailshots[0]['id'] ?? 0);
|
||||||
|
|
@ -62,7 +73,15 @@ final class MailshotTestAdminPage
|
||||||
|
|
||||||
$preview = ['ok' => false, 'rows' => [], 'errors' => ['Create a mailshot before loading recipients.']];
|
$preview = ['ok' => false, 'rows' => [], 'errors' => ['Create a mailshot before loading recipients.']];
|
||||||
if ($selectedMailshotId > 0) {
|
if ($selectedMailshotId > 0) {
|
||||||
|
try {
|
||||||
$preview = $this->runService()->previewRecipients($selectedMailshotId, 100);
|
$preview = $this->runService()->previewRecipients($selectedMailshotId, 100);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$preview = [
|
||||||
|
'ok' => false,
|
||||||
|
'rows' => [],
|
||||||
|
'errors' => [$this->diagnosticError('Recipient preview failed', 'loading recipient sample', $e)],
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
$rows = is_array($preview['rows'] ?? null) ? $preview['rows'] : [];
|
$rows = is_array($preview['rows'] ?? null) ? $preview['rows'] : [];
|
||||||
|
|
||||||
|
|
@ -70,7 +89,12 @@ final class MailshotTestAdminPage
|
||||||
if ($selectedRecipientIndex < -1) {
|
if ($selectedRecipientIndex < -1) {
|
||||||
$selectedRecipientIndex = 0;
|
$selectedRecipientIndex = 0;
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
$defaultEmail = $this->runService()->defaultTestEmail()['default_test_email'] ?? '';
|
$defaultEmail = $this->runService()->defaultTestEmail()['default_test_email'] ?? '';
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$defaultEmail = '';
|
||||||
|
$pageErrors[] = $this->diagnosticError('Unable to load default test email', 'loading test page', $e);
|
||||||
|
}
|
||||||
$testEmail = (string) ($this->wp->requestParam('test_email', (string) $defaultEmail) ?? $defaultEmail);
|
$testEmail = (string) ($this->wp->requestParam('test_email', (string) $defaultEmail) ?? $defaultEmail);
|
||||||
$result = $this->result();
|
$result = $this->result();
|
||||||
if ((string) ($this->wp->requestParam('render_test', '0') ?? '0') === '1') {
|
if ((string) ($this->wp->requestParam('render_test', '0') ?? '0') === '1') {
|
||||||
|
|
@ -78,8 +102,12 @@ final class MailshotTestAdminPage
|
||||||
$result = ['ok' => false, 'errors' => ['Choose a specific recipient row for Render Test.']];
|
$result = ['ok' => false, 'errors' => ['Choose a specific recipient row for Render Test.']];
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
$this->maybeRaiseMemoryLimit($this->resolveDownloadMemoryLimitTarget());
|
$memoryError = $this->maybeRaiseMemoryLimit($this->resolveDownloadMemoryLimitTarget());
|
||||||
|
if ($memoryError !== null) {
|
||||||
|
$result = ['ok' => false, 'errors' => [$memoryError]];
|
||||||
|
} else {
|
||||||
$result = $this->runService()->renderTest($selectedMailshotId, $selectedRecipientIndex);
|
$result = $this->runService()->renderTest($selectedMailshotId, $selectedRecipientIndex);
|
||||||
|
}
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
$result = ['ok' => false, 'errors' => ['Render test failed: ' . $e->getMessage()]];
|
$result = ['ok' => false, 'errors' => ['Render test failed: ' . $e->getMessage()]];
|
||||||
}
|
}
|
||||||
|
|
@ -93,6 +121,14 @@ final class MailshotTestAdminPage
|
||||||
echo $this->renderAdminUiStyles();
|
echo $this->renderAdminUiStyles();
|
||||||
echo '<p>Render with a selected recipient context, then optionally send one test email.</p>';
|
echo '<p>Render with a selected recipient context, then optionally send one test email.</p>';
|
||||||
|
|
||||||
|
if ($pageErrors !== []) {
|
||||||
|
echo '<div class="feca-banner feca-banner-error"><strong>Test page diagnostics</strong>';
|
||||||
|
foreach ($pageErrors as $pageError) {
|
||||||
|
echo '<p class="feca-banner-note">' . htmlspecialchars($pageError, ENT_QUOTES) . '</p>';
|
||||||
|
}
|
||||||
|
echo '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
if ($result !== null) {
|
if ($result !== null) {
|
||||||
$ok = !empty($result['ok']);
|
$ok = !empty($result['ok']);
|
||||||
$bannerClass = $ok ? 'feca-banner-success' : 'feca-banner-error';
|
$bannerClass = $ok ? 'feca-banner-success' : 'feca-banner-error';
|
||||||
|
|
@ -167,13 +203,15 @@ final class MailshotTestAdminPage
|
||||||
echo '<option value="' . $idx . '"' . $sel . '>' . htmlspecialchars($text) . '</option>';
|
echo '<option value="' . $idx . '"' . $sel . '>' . htmlspecialchars($text) . '</option>';
|
||||||
}
|
}
|
||||||
echo '</select></div>';
|
echo '</select></div>';
|
||||||
|
echo '<div class="feca-inline-control-group">';
|
||||||
echo '<div class="feca-control feca-control-min-420">';
|
echo '<div class="feca-control feca-control-min-420">';
|
||||||
echo '<label for="mst_test_email"><strong>Test email address</strong></label>';
|
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 '<input id="mst_test_email" class="regular-text" type="email" name="test_email" value="' . htmlspecialchars($testEmail, ENT_QUOTES) . '">';
|
||||||
echo '</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 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" 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>';
|
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>';
|
||||||
echo '</form>';
|
echo '</form>';
|
||||||
echo '<script>(function(){';
|
echo '<script>(function(){';
|
||||||
echo 'var apiUrl=' . json_encode($this->wp->adminUrl('admin-post.php?action=feca_mailshots_test_api&op=send_test'), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . ';';
|
echo 'var apiUrl=' . json_encode($this->wp->adminUrl('admin-post.php?action=feca_mailshots_test_api&op=send_test'), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . ';';
|
||||||
|
|
@ -181,7 +219,7 @@ final class MailshotTestAdminPage
|
||||||
echo 'var inlineResult=document.getElementById("mst-inline-result");';
|
echo 'var inlineResult=document.getElementById("mst-inline-result");';
|
||||||
echo 'var sendButton=document.getElementById("mst-send-test-button");';
|
echo 'var sendButton=document.getElementById("mst-send-test-button");';
|
||||||
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 '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){return {ok:false,errors:["Send test failed: server returned HTTP "+response.status+" instead of JSON. Check the WordPress/PHP error log for the underlying fatal error."]};}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;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 'window.fecaConfirmTestSend=function(){';
|
echo 'window.fecaConfirmTestSend=function(){';
|
||||||
echo 'var select=document.getElementById("mst_recipient_index");';
|
echo 'var select=document.getElementById("mst_recipient_index");';
|
||||||
echo 'if(!select){return confirm("Send one test email to the entered address?");}';
|
echo 'if(!select){return confirm("Send one test email to the entered address?");}';
|
||||||
|
|
@ -274,10 +312,14 @@ final class MailshotTestAdminPage
|
||||||
}
|
}
|
||||||
|
|
||||||
$op = (string) ($this->wp->requestParam('op', '') ?? '');
|
$op = (string) ($this->wp->requestParam('op', '') ?? '');
|
||||||
$service = $this->runService();
|
$stage = 'initializing test API';
|
||||||
|
$this->registerFatalJsonTrap('test API', $stage);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
$stage = 'loading run service';
|
||||||
|
$service = $this->runService();
|
||||||
if ($op === 'default_test_email') {
|
if ($op === 'default_test_email') {
|
||||||
|
$stage = 'loading default test email';
|
||||||
$this->wp->sendJson(['ok' => true] + $service->defaultTestEmail());
|
$this->wp->sendJson(['ok' => true] + $service->defaultTestEmail());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -286,13 +328,19 @@ final class MailshotTestAdminPage
|
||||||
if (!$this->enforceMutationGuardOrJson(self::CAPABILITY, self::NONCE_ACTION)) {
|
if (!$this->enforceMutationGuardOrJson(self::CAPABILITY, self::NONCE_ACTION)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
$this->maybeRaiseMemoryLimit($this->resolveDownloadMemoryLimitTarget());
|
$stage = 'raising memory limit for render test';
|
||||||
|
$memoryError = $this->maybeRaiseMemoryLimit($this->resolveDownloadMemoryLimitTarget());
|
||||||
|
if ($memoryError !== null) {
|
||||||
|
$this->wp->sendJson(['ok' => false, 'errors' => [$memoryError]], 500);
|
||||||
|
return;
|
||||||
|
}
|
||||||
$mailshotId = (int) ($this->wp->requestParam('mailshot_id', '0') ?? '0');
|
$mailshotId = (int) ($this->wp->requestParam('mailshot_id', '0') ?? '0');
|
||||||
$idx = (int) ($this->wp->requestParam('recipient_index', '0') ?? '0');
|
$idx = (int) ($this->wp->requestParam('recipient_index', '0') ?? '0');
|
||||||
if ($idx < 0) {
|
if ($idx < 0) {
|
||||||
$this->wp->sendJson(['ok' => false, 'errors' => ['Choose a specific recipient row for Render Test.']]);
|
$this->wp->sendJson(['ok' => false, 'errors' => ['Choose a specific recipient row for Render Test.']]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
$stage = 'rendering selected recipient';
|
||||||
$this->wp->sendJson($service->renderTest($mailshotId, $idx));
|
$this->wp->sendJson($service->renderTest($mailshotId, $idx));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -301,21 +349,28 @@ final class MailshotTestAdminPage
|
||||||
if (!$this->enforceMutationGuardOrJson(self::CAPABILITY, self::NONCE_ACTION)) {
|
if (!$this->enforceMutationGuardOrJson(self::CAPABILITY, self::NONCE_ACTION)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
$this->maybeRaiseMemoryLimit($this->resolveDownloadMemoryLimitTarget());
|
$stage = 'raising memory limit for send test';
|
||||||
|
$memoryError = $this->maybeRaiseMemoryLimit($this->resolveDownloadMemoryLimitTarget());
|
||||||
|
if ($memoryError !== null) {
|
||||||
|
$this->wp->sendJson(['ok' => false, 'errors' => [$memoryError]], 500);
|
||||||
|
return;
|
||||||
|
}
|
||||||
$mailshotId = (int) ($this->wp->requestParam('mailshot_id', '0') ?? '0');
|
$mailshotId = (int) ($this->wp->requestParam('mailshot_id', '0') ?? '0');
|
||||||
$idx = (int) ($this->wp->requestParam('recipient_index', '0') ?? '0');
|
$idx = (int) ($this->wp->requestParam('recipient_index', '0') ?? '0');
|
||||||
$to = (string) ($this->wp->requestParam('test_email', '') ?? '');
|
$to = (string) ($this->wp->requestParam('test_email', '') ?? '');
|
||||||
if ($idx < 0) {
|
if ($idx < 0) {
|
||||||
|
$stage = 'sending test email for all recipient rows';
|
||||||
$this->wp->sendJson($service->sendTestAll($mailshotId, $to));
|
$this->wp->sendJson($service->sendTestAll($mailshotId, $to));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
$stage = 'sending test email for selected recipient';
|
||||||
$this->wp->sendJson($service->sendTest($mailshotId, $idx, $to));
|
$this->wp->sendJson($service->sendTest($mailshotId, $idx, $to));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->wp->sendJson(['ok' => false, 'error' => 'Unknown op'], 400);
|
$this->wp->sendJson(['ok' => false, 'error' => 'Unknown op'], 400);
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
$this->wp->sendJson(['ok' => false, 'error' => $e->getMessage()], 500);
|
$this->wp->sendJson(['ok' => false, 'errors' => [$this->diagnosticError('Test API failed', $stage, $e)]], 500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -330,7 +385,13 @@ final class MailshotTestAdminPage
|
||||||
$this->redirect($mailshotId, $idx, (string) ($this->wp->requestParam('test_email', '') ?? ''), true);
|
$this->redirect($mailshotId, $idx, (string) ($this->wp->requestParam('test_email', '') ?? ''), true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
$this->maybeRaiseMemoryLimit($this->resolveDownloadMemoryLimitTarget());
|
$memoryError = $this->maybeRaiseMemoryLimit($this->resolveDownloadMemoryLimitTarget());
|
||||||
|
if ($memoryError !== null) {
|
||||||
|
$result = ['ok' => false, 'errors' => [$memoryError], 'ui_action' => 'render'];
|
||||||
|
$this->wp->updateOption(self::RESULT_OPTION_KEY, $result);
|
||||||
|
$this->redirect($mailshotId, $idx, (string) ($this->wp->requestParam('test_email', '') ?? ''));
|
||||||
|
return;
|
||||||
|
}
|
||||||
$this->redirect($mailshotId, $idx, (string) ($this->wp->requestParam('test_email', '') ?? ''), true);
|
$this->redirect($mailshotId, $idx, (string) ($this->wp->requestParam('test_email', '') ?? ''), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -342,15 +403,21 @@ final class MailshotTestAdminPage
|
||||||
$mailshotId = (int) ($this->wp->requestParam('mailshot_id', '0') ?? '0');
|
$mailshotId = (int) ($this->wp->requestParam('mailshot_id', '0') ?? '0');
|
||||||
$idx = (int) ($this->wp->requestParam('recipient_index', '0') ?? '0');
|
$idx = (int) ($this->wp->requestParam('recipient_index', '0') ?? '0');
|
||||||
$email = (string) ($this->wp->requestParam('test_email', '') ?? '');
|
$email = (string) ($this->wp->requestParam('test_email', '') ?? '');
|
||||||
|
$stage = 'initializing send test';
|
||||||
try {
|
try {
|
||||||
$this->maybeRaiseMemoryLimit($this->resolveDownloadMemoryLimitTarget());
|
$stage = 'raising memory limit for send test';
|
||||||
if ($idx < 0) {
|
$memoryError = $this->maybeRaiseMemoryLimit($this->resolveDownloadMemoryLimitTarget());
|
||||||
|
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 = $this->runService()->sendTestAll($mailshotId, $email);
|
||||||
} else {
|
} else {
|
||||||
|
$stage = 'sending test email for selected recipient';
|
||||||
$result = $this->runService()->sendTest($mailshotId, $idx, $email);
|
$result = $this->runService()->sendTest($mailshotId, $idx, $email);
|
||||||
}
|
}
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
$result = ['ok' => false, 'errors' => ['Send test failed: ' . $e->getMessage()]];
|
$result = ['ok' => false, 'errors' => [$this->diagnosticError('Send test failed', $stage, $e)]];
|
||||||
}
|
}
|
||||||
unset($result['rendered']);
|
unset($result['rendered']);
|
||||||
$result['ui_action'] = 'send';
|
$result['ui_action'] = 'send';
|
||||||
|
|
@ -378,10 +445,10 @@ final class MailshotTestAdminPage
|
||||||
return is_array($raw) ? $raw : null;
|
return is_array($raw) ? $raw : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function maybeRaiseMemoryLimit(string $target): void
|
private function maybeRaiseMemoryLimit(string $target): ?string
|
||||||
{
|
{
|
||||||
if ($target === '' || !function_exists('ini_get') || !function_exists('ini_set')) {
|
if ($target === '' || !function_exists('ini_get') || !function_exists('ini_set')) {
|
||||||
return;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$current = (string) ini_get('memory_limit');
|
$current = (string) ini_get('memory_limit');
|
||||||
|
|
@ -389,10 +456,17 @@ final class MailshotTestAdminPage
|
||||||
$targetBytes = $this->memoryLimitToBytes($target);
|
$targetBytes = $this->memoryLimitToBytes($target);
|
||||||
|
|
||||||
if ($currentBytes < 0 || $targetBytes <= 0 || $currentBytes >= $targetBytes) {
|
if ($currentBytes < 0 || $targetBytes <= 0 || $currentBytes >= $targetBytes) {
|
||||||
return;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ini_set('memory_limit', $target);
|
@ini_set('memory_limit', $target);
|
||||||
|
$new = (string) ini_get('memory_limit');
|
||||||
|
$newBytes = $this->memoryLimitToBytes($new);
|
||||||
|
if ($newBytes >= $targetBytes || $newBytes < 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Mailshot Test needs PHP memory_limit ' . $target . ' for this action, but the server is still using ' . $new . '. Increase the FECA Mailshots Setup memory limit or the hosting PHP memory_limit before retrying.';
|
||||||
}
|
}
|
||||||
|
|
||||||
private function resolveDownloadMemoryLimitTarget(): string
|
private function resolveDownloadMemoryLimitTarget(): string
|
||||||
|
|
@ -413,7 +487,7 @@ final class MailshotTestAdminPage
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return '';
|
return self::DEFAULT_DOWNLOAD_MEMORY_LIMIT;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function memoryLimitToBytes(string $limit): int
|
private function memoryLimitToBytes(string $limit): int
|
||||||
|
|
@ -444,6 +518,54 @@ final class MailshotTestAdminPage
|
||||||
return (int) $value;
|
return (int) $value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function registerFatalJsonTrap(string $context, string &$stage): void
|
||||||
|
{
|
||||||
|
if (!function_exists('register_shutdown_function')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
register_shutdown_function(function () use ($context, &$stage): void {
|
||||||
|
$error = error_get_last();
|
||||||
|
if (!is_array($error)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$type = (int) ($error['type'] ?? 0);
|
||||||
|
if (!in_array($type, [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR, E_RECOVERABLE_ERROR], true)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$message = trim((string) ($error['message'] ?? 'Unknown fatal error.'));
|
||||||
|
$file = basename((string) ($error['file'] ?? 'unknown'));
|
||||||
|
$line = (int) ($error['line'] ?? 0);
|
||||||
|
$payload = [
|
||||||
|
'ok' => false,
|
||||||
|
'errors' => [
|
||||||
|
'Mailshots ' . $context . ' fatal error during ' . $stage . ': ' . $message . ' [' . $file . ':' . $line . ']',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
if (!headers_sent()) {
|
||||||
|
if (function_exists('status_header')) {
|
||||||
|
status_header(500);
|
||||||
|
} else {
|
||||||
|
http_response_code(500);
|
||||||
|
}
|
||||||
|
header('Content-Type: application/json; charset=UTF-8');
|
||||||
|
}
|
||||||
|
echo json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private function diagnosticError(string $prefix, string $stage, \Throwable $e): string
|
||||||
|
{
|
||||||
|
$message = $prefix . ' during ' . $stage . ': ' . $e->getMessage();
|
||||||
|
$file = $e->getFile();
|
||||||
|
$line = $e->getLine();
|
||||||
|
if ($file !== '' && $line > 0) {
|
||||||
|
$message .= ' [' . get_class($e) . ' at ' . basename($file) . ':' . $line . ']';
|
||||||
|
}
|
||||||
|
return $message;
|
||||||
|
}
|
||||||
|
|
||||||
private function runService(): MailshotRunService
|
private function runService(): MailshotRunService
|
||||||
{
|
{
|
||||||
return ($this->runServiceFactory)();
|
return ($this->runServiceFactory)();
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,9 @@ final class RunMailshotAdminPage
|
||||||
private const RESULT_OPTION_KEY = 'feca_mailshots_run_ui_result';
|
private const RESULT_OPTION_KEY = 'feca_mailshots_run_ui_result';
|
||||||
private const CAPABILITY = 'edit_pages';
|
private const CAPABILITY = 'edit_pages';
|
||||||
private const NONCE_ACTION = 'feca_mailshots_run';
|
private const NONCE_ACTION = 'feca_mailshots_run';
|
||||||
|
private const DOWNLOAD_MEMORY_LIMIT_ENV = 'FECA_MAILSHOTS_DOWNLOAD_MEMORY_LIMIT';
|
||||||
|
private const DOWNLOAD_MEMORY_LIMIT_OPTION = 'feca_mailshots_download_memory_limit';
|
||||||
|
private const DEFAULT_DOWNLOAD_MEMORY_LIMIT = '512M';
|
||||||
|
|
||||||
/** @var callable(): MailshotRunService */
|
/** @var callable(): MailshotRunService */
|
||||||
private $runServiceFactory;
|
private $runServiceFactory;
|
||||||
|
|
@ -54,15 +57,34 @@ final class RunMailshotAdminPage
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$pageErrors = [];
|
||||||
|
$memoryError = $this->maybeRaiseMemoryLimit($this->resolveDownloadMemoryLimitTarget());
|
||||||
|
if ($memoryError !== null) {
|
||||||
|
$pageErrors[] = $memoryError;
|
||||||
|
}
|
||||||
|
try {
|
||||||
$mailshots = $this->mailshotService()->list();
|
$mailshots = $this->mailshotService()->list();
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$mailshots = [];
|
||||||
|
$pageErrors[] = $this->diagnosticError('Unable to load mailshots', 'loading run page', $e);
|
||||||
|
}
|
||||||
$selectedMailshotId = (int) ($this->wp->requestParam('mailshot_id', '0') ?? '0');
|
$selectedMailshotId = (int) ($this->wp->requestParam('mailshot_id', '0') ?? '0');
|
||||||
if ($selectedMailshotId <= 0 && $mailshots !== []) {
|
if ($selectedMailshotId <= 0 && $mailshots !== []) {
|
||||||
$selectedMailshotId = (int) ($mailshots[0]['id'] ?? 0);
|
$selectedMailshotId = (int) ($mailshots[0]['id'] ?? 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
$result = $this->result();
|
$result = $this->result();
|
||||||
|
try {
|
||||||
$lastRunRows = $selectedMailshotId > 0 ? $this->mailshotService()->lastRun($selectedMailshotId) : [];
|
$lastRunRows = $selectedMailshotId > 0 ? $this->mailshotService()->lastRun($selectedMailshotId) : [];
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$lastRunRows = [];
|
||||||
|
$pageErrors[] = $this->diagnosticError('Unable to load last-run rows', 'loading run page', $e);
|
||||||
|
}
|
||||||
|
try {
|
||||||
$recipientSummary = $selectedMailshotId > 0 ? $this->runService()->recipientCount($selectedMailshotId) : ['ok' => false, 'errors' => ['No mailshot selected.']];
|
$recipientSummary = $selectedMailshotId > 0 ? $this->runService()->recipientCount($selectedMailshotId) : ['ok' => false, 'errors' => ['No mailshot selected.']];
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$recipientSummary = ['ok' => false, 'errors' => [$this->diagnosticError('Recipient count failed', 'loading run page', $e)]];
|
||||||
|
}
|
||||||
$runCount = !empty($recipientSummary['ok']) ? (int) ($recipientSummary['count'] ?? 0) : 0;
|
$runCount = !empty($recipientSummary['ok']) ? (int) ($recipientSummary['count'] ?? 0) : 0;
|
||||||
$action = htmlspecialchars($this->wp->adminUrl('admin-post.php'));
|
$action = htmlspecialchars($this->wp->adminUrl('admin-post.php'));
|
||||||
|
|
||||||
|
|
@ -70,6 +92,14 @@ final class RunMailshotAdminPage
|
||||||
echo $this->renderAdminUiStyles();
|
echo $this->renderAdminUiStyles();
|
||||||
echo '<p>Execute full sends and retry failures from the current mailshot last-run rows.</p>';
|
echo '<p>Execute full sends and retry failures from the current mailshot last-run rows.</p>';
|
||||||
|
|
||||||
|
if ($pageErrors !== []) {
|
||||||
|
echo '<div class="feca-banner feca-banner-error"><strong>Run page diagnostics</strong>';
|
||||||
|
foreach ($pageErrors as $pageError) {
|
||||||
|
echo '<p class="feca-banner-note">' . htmlspecialchars($pageError, ENT_QUOTES) . '</p>';
|
||||||
|
}
|
||||||
|
echo '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
if ($result !== null) {
|
if ($result !== null) {
|
||||||
$ok = !empty($result['ok']);
|
$ok = !empty($result['ok']);
|
||||||
$bannerClass = $ok ? 'feca-banner-success' : 'feca-banner-error';
|
$bannerClass = $ok ? 'feca-banner-success' : 'feca-banner-error';
|
||||||
|
|
@ -191,11 +221,21 @@ final class RunMailshotAdminPage
|
||||||
}
|
}
|
||||||
|
|
||||||
$op = (string) ($this->wp->requestParam('op', '') ?? '');
|
$op = (string) ($this->wp->requestParam('op', '') ?? '');
|
||||||
$service = $this->runService();
|
$stage = 'initializing run API';
|
||||||
|
$this->registerFatalJsonTrap('run API', $stage);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
$stage = 'raising memory limit for run action';
|
||||||
|
$memoryError = $this->maybeRaiseMemoryLimit($this->resolveDownloadMemoryLimitTarget());
|
||||||
|
if ($memoryError !== null) {
|
||||||
|
$this->wp->sendJson(['ok' => false, 'errors' => [$memoryError]], 500);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$stage = 'loading run service';
|
||||||
|
$service = $this->runService();
|
||||||
if ($op === 'run_mailshot') {
|
if ($op === 'run_mailshot') {
|
||||||
$mailshotId = (int) ($this->wp->requestParam('mailshot_id', '0') ?? '0');
|
$mailshotId = (int) ($this->wp->requestParam('mailshot_id', '0') ?? '0');
|
||||||
|
$stage = 'running mailshot';
|
||||||
$this->wp->sendJson($service->runMailshot($mailshotId));
|
$this->wp->sendJson($service->runMailshot($mailshotId));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -203,12 +243,14 @@ final class RunMailshotAdminPage
|
||||||
if ($op === 'run_mailshot_selected') {
|
if ($op === 'run_mailshot_selected') {
|
||||||
$mailshotId = (int) ($this->wp->requestParam('mailshot_id', '0') ?? '0');
|
$mailshotId = (int) ($this->wp->requestParam('mailshot_id', '0') ?? '0');
|
||||||
$selectedRecipientKeys = $this->selectedRecipientKeysFromRequest();
|
$selectedRecipientKeys = $this->selectedRecipientKeysFromRequest();
|
||||||
|
$stage = 'running selected recipient rows';
|
||||||
$this->wp->sendJson($service->runMailshotSelected($mailshotId, $selectedRecipientKeys));
|
$this->wp->sendJson($service->runMailshotSelected($mailshotId, $selectedRecipientKeys));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($op === 'retry_failed') {
|
if ($op === 'retry_failed') {
|
||||||
$mailshotId = (int) ($this->wp->requestParam('mailshot_id', '0') ?? '0');
|
$mailshotId = (int) ($this->wp->requestParam('mailshot_id', '0') ?? '0');
|
||||||
|
$stage = 'retrying failed recipients';
|
||||||
$this->wp->sendJson($service->retryFailed($mailshotId));
|
$this->wp->sendJson($service->retryFailed($mailshotId));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -216,13 +258,14 @@ final class RunMailshotAdminPage
|
||||||
if ($op === 'retry_recipient') {
|
if ($op === 'retry_recipient') {
|
||||||
$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', '') ?? '');
|
$recipientKey = (string) ($this->wp->requestParam('recipient_key', '') ?? '');
|
||||||
|
$stage = 'retrying one recipient';
|
||||||
$this->wp->sendJson($service->retryRecipient($mailshotId, $recipientKey));
|
$this->wp->sendJson($service->retryRecipient($mailshotId, $recipientKey));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->wp->sendJson(['ok' => false, 'error' => 'Unknown op'], 400);
|
$this->wp->sendJson(['ok' => false, 'error' => 'Unknown op'], 400);
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
$this->wp->sendJson(['ok' => false, 'error' => $e->getMessage()], 500);
|
$this->wp->sendJson(['ok' => false, 'errors' => [$this->diagnosticError('Run API failed', $stage, $e)]], 500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -269,8 +312,9 @@ final class RunMailshotAdminPage
|
||||||
$mailshotId = (int) ($this->wp->requestParam('mailshot_id', '0') ?? '0');
|
$mailshotId = (int) ($this->wp->requestParam('mailshot_id', '0') ?? '0');
|
||||||
$completed = false;
|
$completed = false;
|
||||||
$fatalHandled = false;
|
$fatalHandled = false;
|
||||||
|
$stage = 'initializing run action';
|
||||||
|
|
||||||
register_shutdown_function(function () use (&$completed, &$fatalHandled, $mailshotId): void {
|
register_shutdown_function(function () use (&$completed, &$fatalHandled, $mailshotId, &$stage): void {
|
||||||
if ($completed || $fatalHandled) {
|
if ($completed || $fatalHandled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -279,14 +323,23 @@ final class RunMailshotAdminPage
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
$fatalHandled = true;
|
$fatalHandled = true;
|
||||||
$this->storeRunFailure('Run action failed: ' . (string) ($error['message'] ?? 'Unknown fatal error.'));
|
$file = basename((string) ($error['file'] ?? 'unknown'));
|
||||||
|
$line = (int) ($error['line'] ?? 0);
|
||||||
|
$this->storeRunFailure('Run action fatal error during ' . $stage . ': ' . (string) ($error['message'] ?? 'Unknown fatal error.') . ' [' . $file . ':' . $line . ']');
|
||||||
$this->redirect($mailshotId);
|
$this->redirect($mailshotId);
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
$stage = 'raising memory limit for run action';
|
||||||
|
$memoryError = $this->maybeRaiseMemoryLimit($this->resolveDownloadMemoryLimitTarget());
|
||||||
|
if ($memoryError !== null) {
|
||||||
|
$result = ['ok' => false, 'errors' => [$memoryError]];
|
||||||
|
} else {
|
||||||
|
$stage = 'executing run action';
|
||||||
$result = $action($this, $mailshotId);
|
$result = $action($this, $mailshotId);
|
||||||
|
}
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
$result = ['ok' => false, 'errors' => ['Run action failed: ' . $e->getMessage()]];
|
$result = ['ok' => false, 'errors' => [$this->diagnosticError('Run action failed', $stage, $e)]];
|
||||||
}
|
}
|
||||||
|
|
||||||
$completed = true;
|
$completed = true;
|
||||||
|
|
@ -330,6 +383,127 @@ final class RunMailshotAdminPage
|
||||||
return ($this->mailshotServiceFactory)();
|
return ($this->mailshotServiceFactory)();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function maybeRaiseMemoryLimit(string $target): ?string
|
||||||
|
{
|
||||||
|
if ($target === '' || !function_exists('ini_get') || !function_exists('ini_set')) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$current = (string) ini_get('memory_limit');
|
||||||
|
$currentBytes = $this->memoryLimitToBytes($current);
|
||||||
|
$targetBytes = $this->memoryLimitToBytes($target);
|
||||||
|
|
||||||
|
if ($currentBytes < 0 || $targetBytes <= 0 || $currentBytes >= $targetBytes) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ini_set('memory_limit', $target);
|
||||||
|
$new = (string) ini_get('memory_limit');
|
||||||
|
$newBytes = $this->memoryLimitToBytes($new);
|
||||||
|
if ($newBytes >= $targetBytes || $newBytes < 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Run Mailshot needs PHP memory_limit ' . $target . ' for this action, but the server is still using ' . $new . '. Increase the FECA Mailshots Setup memory limit or the hosting PHP memory_limit before retrying.';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolveDownloadMemoryLimitTarget(): string
|
||||||
|
{
|
||||||
|
$optionValue = $this->wp->getOption(self::DOWNLOAD_MEMORY_LIMIT_OPTION, '');
|
||||||
|
if (is_string($optionValue)) {
|
||||||
|
$value = trim($optionValue);
|
||||||
|
if ($this->memoryLimitToBytes($value) > 0) {
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$envValue = getenv(self::DOWNLOAD_MEMORY_LIMIT_ENV);
|
||||||
|
if (is_string($envValue)) {
|
||||||
|
$value = trim($envValue);
|
||||||
|
if ($this->memoryLimitToBytes($value) > 0) {
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::DEFAULT_DOWNLOAD_MEMORY_LIMIT;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function memoryLimitToBytes(string $limit): int
|
||||||
|
{
|
||||||
|
$value = trim($limit);
|
||||||
|
if ($value === '') {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if ($value === '-1') {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
$unit = strtolower(substr($value, -1));
|
||||||
|
if (ctype_alpha($unit)) {
|
||||||
|
$number = (float) substr($value, 0, -1);
|
||||||
|
switch ($unit) {
|
||||||
|
case 'g':
|
||||||
|
return (int) ($number * 1024 * 1024 * 1024);
|
||||||
|
case 'm':
|
||||||
|
return (int) ($number * 1024 * 1024);
|
||||||
|
case 'k':
|
||||||
|
return (int) ($number * 1024);
|
||||||
|
default:
|
||||||
|
return (int) $number;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int) $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function registerFatalJsonTrap(string $context, string &$stage): void
|
||||||
|
{
|
||||||
|
if (!function_exists('register_shutdown_function')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
register_shutdown_function(function () use ($context, &$stage): void {
|
||||||
|
$error = error_get_last();
|
||||||
|
if (!is_array($error)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$type = (int) ($error['type'] ?? 0);
|
||||||
|
if (!in_array($type, [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR, E_RECOVERABLE_ERROR], true)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$message = trim((string) ($error['message'] ?? 'Unknown fatal error.'));
|
||||||
|
$file = basename((string) ($error['file'] ?? 'unknown'));
|
||||||
|
$line = (int) ($error['line'] ?? 0);
|
||||||
|
$payload = [
|
||||||
|
'ok' => false,
|
||||||
|
'errors' => [
|
||||||
|
'Mailshots ' . $context . ' fatal error during ' . $stage . ': ' . $message . ' [' . $file . ':' . $line . ']',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
if (!headers_sent()) {
|
||||||
|
if (function_exists('status_header')) {
|
||||||
|
status_header(500);
|
||||||
|
} else {
|
||||||
|
http_response_code(500);
|
||||||
|
}
|
||||||
|
header('Content-Type: application/json; charset=UTF-8');
|
||||||
|
}
|
||||||
|
echo json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private function diagnosticError(string $prefix, string $stage, \Throwable $e): string
|
||||||
|
{
|
||||||
|
$message = $prefix . ' during ' . $stage . ': ' . $e->getMessage();
|
||||||
|
$file = $e->getFile();
|
||||||
|
$line = $e->getLine();
|
||||||
|
if ($file !== '' && $line > 0) {
|
||||||
|
$message .= ' [' . get_class($e) . ' at ' . basename($file) . ':' . $line . ']';
|
||||||
|
}
|
||||||
|
return $message;
|
||||||
|
}
|
||||||
|
|
||||||
/** @param list<array<string,mixed>> $mailshots */
|
/** @param list<array<string,mixed>> $mailshots */
|
||||||
private function selectedMailshotDataSource(array $mailshots, int $mailshotId): string
|
private function selectedMailshotDataSource(array $mailshots, int $mailshotId): string
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -99,11 +99,11 @@ final class SetupAdminPage
|
||||||
$this->field('Members DB Name', 'members_db_name', $saved['members_db_name'] ?? '');
|
$this->field('Members DB Name', 'members_db_name', $saved['members_db_name'] ?? '');
|
||||||
$this->field('FEN DB Name', 'fen_db_name', $saved['fen_db_name'] ?? '');
|
$this->field('FEN DB Name', 'fen_db_name', $saved['fen_db_name'] ?? '');
|
||||||
$this->field(
|
$this->field(
|
||||||
'Download PDF Memory Limit',
|
'PDF/Test Memory Limit',
|
||||||
'download_memory_limit',
|
'download_memory_limit',
|
||||||
$downloadMemoryLimit,
|
$downloadMemoryLimit,
|
||||||
'text',
|
'text',
|
||||||
'PHP memory_limit value used for Download PDF actions (for example: 256M, 512M, 1G). Suggested value 512M.'
|
'PHP memory_limit value used for Download PDF and Mailshot Test actions (for example: 256M, 512M, 1G). Suggested value 512M.'
|
||||||
);
|
);
|
||||||
echo '</table>';
|
echo '</table>';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -138,22 +138,27 @@ final class MailshotRunService
|
||||||
return ['ok' => false, 'errors' => ['Test email address is required.']];
|
return ['ok' => false, 'errors' => ['Test email address is required.']];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$stage = 'initializing send test';
|
||||||
try {
|
try {
|
||||||
|
$stage = 'loading mail credentials';
|
||||||
$creds = $this->credentials->credentials();
|
$creds = $this->credentials->credentials();
|
||||||
if ($creds === null) {
|
if ($creds === null) {
|
||||||
return ['ok' => false, 'errors' => ['Missing mail credentials. Configure FECA Mailshots Profile page first.']];
|
return ['ok' => false, 'errors' => ['Missing mail credentials. Configure FECA Mailshots Profile page first.']];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$stage = 'rendering selected recipient';
|
||||||
$render = $this->renderTest($mailshotId, $recipientIndex);
|
$render = $this->renderTest($mailshotId, $recipientIndex);
|
||||||
if (($render['ok'] ?? false) !== true) {
|
if (($render['ok'] ?? false) !== true) {
|
||||||
return $render;
|
return $render;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$stage = 'loading mailshot definition';
|
||||||
$mailshot = $this->mailshots->find($mailshotId);
|
$mailshot = $this->mailshots->find($mailshotId);
|
||||||
if ($mailshot === null) {
|
if ($mailshot === null) {
|
||||||
return ['ok' => false, 'errors' => ['Mailshot not found.']];
|
return ['ok' => false, 'errors' => ['Mailshot not found.']];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$stage = 'preparing recipients and attachments';
|
||||||
$cc = $this->splitAddresses((string) ($mailshot['CC'] ?? ''));
|
$cc = $this->splitAddresses((string) ($mailshot['CC'] ?? ''));
|
||||||
$bcc = $this->splitAddresses((string) ($mailshot['BCC'] ?? ''));
|
$bcc = $this->splitAddresses((string) ($mailshot['BCC'] ?? ''));
|
||||||
$attachments = array_merge(
|
$attachments = array_merge(
|
||||||
|
|
@ -163,6 +168,7 @@ final class MailshotRunService
|
||||||
|
|
||||||
$attemptId = 'test_' . $mailshotId . '_' . $recipientIndex . '_' . gmdate('YmdHis');
|
$attemptId = 'test_' . $mailshotId . '_' . $recipientIndex . '_' . gmdate('YmdHis');
|
||||||
|
|
||||||
|
$stage = 'sending SMTP message';
|
||||||
$send = $this->smtp->send(
|
$send = $this->smtp->send(
|
||||||
$creds,
|
$creds,
|
||||||
[$testEmail],
|
[$testEmail],
|
||||||
|
|
@ -175,6 +181,7 @@ final class MailshotRunService
|
||||||
);
|
);
|
||||||
$warnings = [];
|
$warnings = [];
|
||||||
try {
|
try {
|
||||||
|
$stage = 'appending IMAP sent copy';
|
||||||
$this->imap->appendSent($creds, (string) $send['raw_mime'], $attemptId);
|
$this->imap->appendSent($creds, (string) $send['raw_mime'], $attemptId);
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
$warnings[] = 'IMAP sent-copy failed: ' . $e->getMessage();
|
$warnings[] = 'IMAP sent-copy failed: ' . $e->getMessage();
|
||||||
|
|
@ -187,7 +194,7 @@ final class MailshotRunService
|
||||||
'sent_at' => gmdate('c'),
|
'sent_at' => gmdate('c'),
|
||||||
];
|
];
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
return ['ok' => false, 'errors' => ['Send test failed: ' . $e->getMessage()]];
|
return ['ok' => false, 'errors' => [$this->diagnosticError('Send test failed', $stage, $e)]];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -199,17 +206,21 @@ final class MailshotRunService
|
||||||
return ['ok' => false, 'errors' => ['Test email address is required.']];
|
return ['ok' => false, 'errors' => ['Test email address is required.']];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$stage = 'initializing send-all test';
|
||||||
try {
|
try {
|
||||||
|
$stage = 'loading mail credentials';
|
||||||
$creds = $this->credentials->credentials();
|
$creds = $this->credentials->credentials();
|
||||||
if ($creds === null) {
|
if ($creds === null) {
|
||||||
return ['ok' => false, 'errors' => ['Missing mail credentials. Configure FECA Mailshots Profile page first.']];
|
return ['ok' => false, 'errors' => ['Missing mail credentials. Configure FECA Mailshots Profile page first.']];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$stage = 'loading recipients';
|
||||||
[$mailshot, $rows] = $this->loadMailshotAndRows($mailshotId);
|
[$mailshot, $rows] = $this->loadMailshotAndRows($mailshotId);
|
||||||
if ($rows === []) {
|
if ($rows === []) {
|
||||||
return ['ok' => false, 'errors' => ['Recipient query returned zero rows.']];
|
return ['ok' => false, 'errors' => ['Recipient query returned zero rows.']];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$stage = 'preparing mailshot addresses';
|
||||||
$cc = $this->splitAddresses((string) ($mailshot['CC'] ?? ''));
|
$cc = $this->splitAddresses((string) ($mailshot['CC'] ?? ''));
|
||||||
$bcc = $this->splitAddresses((string) ($mailshot['BCC'] ?? ''));
|
$bcc = $this->splitAddresses((string) ($mailshot['BCC'] ?? ''));
|
||||||
$counters = ['attempted' => 0, 'sent' => 0, 'failed' => 0, 'warnings' => 0];
|
$counters = ['attempted' => 0, 'sent' => 0, 'failed' => 0, 'warnings' => 0];
|
||||||
|
|
@ -219,16 +230,19 @@ final class MailshotRunService
|
||||||
$counters['attempted']++;
|
$counters['attempted']++;
|
||||||
[$recipientKeyField, $recipientKey] = $this->detectRecipientKey((array) $row, $index);
|
[$recipientKeyField, $recipientKey] = $this->detectRecipientKey((array) $row, $index);
|
||||||
try {
|
try {
|
||||||
|
$rowStage = 'rendering template';
|
||||||
$render = $this->renderer->render(
|
$render = $this->renderer->render(
|
||||||
(string) $mailshot['Subject'],
|
(string) $mailshot['Subject'],
|
||||||
(string) $mailshot['Message'],
|
(string) $mailshot['Message'],
|
||||||
(string) ($mailshot['PDFAttachment'] ?? ''),
|
(string) ($mailshot['PDFAttachment'] ?? ''),
|
||||||
(array) $row
|
(array) $row
|
||||||
);
|
);
|
||||||
|
$rowStage = 'preparing attachments';
|
||||||
$attachments = array_merge(
|
$attachments = array_merge(
|
||||||
$this->staticAttachments($mailshot),
|
$this->staticAttachments($mailshot),
|
||||||
$this->renderedPdfAttachments($mailshot, (array) $row, $render)
|
$this->renderedPdfAttachments($mailshot, (array) $row, $render)
|
||||||
);
|
);
|
||||||
|
$rowStage = 'sending SMTP message';
|
||||||
$send = $this->smtp->send(
|
$send = $this->smtp->send(
|
||||||
$creds,
|
$creds,
|
||||||
[$testEmail],
|
[$testEmail],
|
||||||
|
|
@ -248,7 +262,7 @@ final class MailshotRunService
|
||||||
}
|
}
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
$counters['failed']++;
|
$counters['failed']++;
|
||||||
$errors[] = 'Recipient ' . $recipientKeyField . '=' . $recipientKey . ': ' . $e->getMessage();
|
$errors[] = 'Recipient ' . $recipientKeyField . '=' . $recipientKey . ' failed during ' . $rowStage . ': ' . $e->getMessage();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -258,70 +272,92 @@ final class MailshotRunService
|
||||||
'sent_at' => gmdate('c'),
|
'sent_at' => gmdate('c'),
|
||||||
] + $counters + ['errors' => $errors];
|
] + $counters + ['errors' => $errors];
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
return ['ok' => false, 'errors' => ['Send test failed: ' . $e->getMessage()]];
|
return ['ok' => false, 'errors' => [$this->diagnosticError('Send test failed', $stage, $e)]];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function diagnosticError(string $prefix, string $stage, \Throwable $e): string
|
||||||
|
{
|
||||||
|
$message = $prefix . ' during ' . $stage . ': ' . $e->getMessage();
|
||||||
|
$file = $e->getFile();
|
||||||
|
$line = $e->getLine();
|
||||||
|
if ($file !== '' && $line > 0) {
|
||||||
|
$message .= ' [' . get_class($e) . ' at ' . basename($file) . ':' . $line . ']';
|
||||||
|
}
|
||||||
|
return $message;
|
||||||
|
}
|
||||||
|
|
||||||
/** @return array<string, mixed> */
|
/** @return array<string, mixed> */
|
||||||
public function runMailshot(int $mailshotId): array
|
public function runMailshot(int $mailshotId): array
|
||||||
{
|
{
|
||||||
|
$stage = 'initializing full run';
|
||||||
try {
|
try {
|
||||||
return $this->runMailshotUnsafe($mailshotId);
|
return $this->runMailshotUnsafe($mailshotId, $stage);
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
return ['ok' => false, 'errors' => ['Run mailshot failed: ' . $e->getMessage()]];
|
return ['ok' => false, 'errors' => [$this->diagnosticError('Run mailshot failed', $stage, $e)]];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @return array<string, mixed> */
|
/** @return array<string, mixed> */
|
||||||
private function runMailshotUnsafe(int $mailshotId): array
|
private function runMailshotUnsafe(int $mailshotId, string &$stage): array
|
||||||
{
|
{
|
||||||
|
$stage = 'loading mailshot definition';
|
||||||
$mailshot = $this->mailshots->find($mailshotId);
|
$mailshot = $this->mailshots->find($mailshotId);
|
||||||
if ($mailshot === null) {
|
if ($mailshot === null) {
|
||||||
return ['ok' => false, 'errors' => ['Mailshot not found.']];
|
return ['ok' => false, 'errors' => ['Mailshot not found.']];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$stage = 'loading mail credentials';
|
||||||
$creds = $this->credentials->credentials();
|
$creds = $this->credentials->credentials();
|
||||||
if ($creds === null) {
|
if ($creds === null) {
|
||||||
return ['ok' => false, 'errors' => ['Missing mail credentials. Configure FECA Mailshots Profile page first.']];
|
return ['ok' => false, 'errors' => ['Missing mail credentials. Configure FECA Mailshots Profile page first.']];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$stage = 'loading recipient rows';
|
||||||
[$mailshot, $rows] = $this->loadMailshotAndRows($mailshotId);
|
[$mailshot, $rows] = $this->loadMailshotAndRows($mailshotId);
|
||||||
if ($rows === []) {
|
if ($rows === []) {
|
||||||
return ['ok' => false, 'errors' => ['Recipient query returned zero rows.']];
|
return ['ok' => false, 'errors' => ['Recipient query returned zero rows.']];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$stage = 'clearing previous run rows';
|
||||||
$this->lastRun->clearForMailshot($mailshotId);
|
$this->lastRun->clearForMailshot($mailshotId);
|
||||||
|
$stage = 'sending recipient rows';
|
||||||
return $this->executeSendLoop($mailshotId, $mailshot, $rows, $creds, true);
|
return $this->executeSendLoop($mailshotId, $mailshot, $rows, $creds, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @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
|
||||||
{
|
{
|
||||||
|
$stage = 'initializing selected run';
|
||||||
try {
|
try {
|
||||||
return $this->runMailshotSelectedUnsafe($mailshotId, $selectedRecipientKeys);
|
return $this->runMailshotSelectedUnsafe($mailshotId, $selectedRecipientKeys, $stage);
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
return ['ok' => false, 'errors' => ['Run selected mailshot failed: ' . $e->getMessage()]];
|
return ['ok' => false, 'errors' => [$this->diagnosticError('Run selected mailshot failed', $stage, $e)]];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @param list<string> $selectedRecipientKeys @return array<string, mixed> */
|
/** @param list<string> $selectedRecipientKeys @return array<string, mixed> */
|
||||||
private function runMailshotSelectedUnsafe(int $mailshotId, array $selectedRecipientKeys): array
|
private function runMailshotSelectedUnsafe(int $mailshotId, array $selectedRecipientKeys, string &$stage): array
|
||||||
{
|
{
|
||||||
|
$stage = 'validating selected recipient rows';
|
||||||
$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 === []) {
|
||||||
return ['ok' => false, 'errors' => ['No selected recipient rows were provided.']];
|
return ['ok' => false, 'errors' => ['No selected recipient rows were provided.']];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$stage = 'loading mail credentials';
|
||||||
$creds = $this->credentials->credentials();
|
$creds = $this->credentials->credentials();
|
||||||
if ($creds === null) {
|
if ($creds === null) {
|
||||||
return ['ok' => false, 'errors' => ['Missing mail credentials. Configure FECA Mailshots Profile page first.']];
|
return ['ok' => false, 'errors' => ['Missing mail credentials. Configure FECA Mailshots Profile page first.']];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$stage = 'loading recipient rows';
|
||||||
[$mailshot, $rows] = $this->loadMailshotAndRows($mailshotId);
|
[$mailshot, $rows] = $this->loadMailshotAndRows($mailshotId);
|
||||||
if ($rows === []) {
|
if ($rows === []) {
|
||||||
return ['ok' => false, 'errors' => ['Recipient query returned zero rows.']];
|
return ['ok' => false, 'errors' => ['Recipient query returned zero rows.']];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$stage = 'matching selected recipient rows';
|
||||||
$selectedSet = array_fill_keys($selectedRecipientKeys, true);
|
$selectedSet = array_fill_keys($selectedRecipientKeys, true);
|
||||||
$filteredRows = [];
|
$filteredRows = [];
|
||||||
foreach (array_values($rows) as $index => $row) {
|
foreach (array_values($rows) as $index => $row) {
|
||||||
|
|
@ -335,7 +371,9 @@ final class MailshotRunService
|
||||||
return ['ok' => false, 'errors' => ['None of the selected recipient rows exist in the current query result.']];
|
return ['ok' => false, 'errors' => ['None of the selected recipient rows exist in the current query result.']];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$stage = 'clearing previous run rows';
|
||||||
$this->lastRun->clearForMailshot($mailshotId);
|
$this->lastRun->clearForMailshot($mailshotId);
|
||||||
|
$stage = 'sending selected recipient rows';
|
||||||
$result = $this->executeSendLoop($mailshotId, $mailshot, $filteredRows, $creds, true);
|
$result = $this->executeSendLoop($mailshotId, $mailshot, $filteredRows, $creds, true);
|
||||||
$result['selected_count'] = count($filteredRows);
|
$result['selected_count'] = count($filteredRows);
|
||||||
return $result;
|
return $result;
|
||||||
|
|
@ -344,28 +382,33 @@ final class MailshotRunService
|
||||||
/** @return array<string, mixed> */
|
/** @return array<string, mixed> */
|
||||||
public function retryFailed(int $mailshotId): array
|
public function retryFailed(int $mailshotId): array
|
||||||
{
|
{
|
||||||
|
$stage = 'initializing retry failed';
|
||||||
try {
|
try {
|
||||||
return $this->retryFailedUnsafe($mailshotId);
|
return $this->retryFailedUnsafe($mailshotId, $stage);
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
return ['ok' => false, 'errors' => ['Retry failed sends failed: ' . $e->getMessage()]];
|
return ['ok' => false, 'errors' => [$this->diagnosticError('Retry failed sends failed', $stage, $e)]];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @return array<string, mixed> */
|
/** @return array<string, mixed> */
|
||||||
private function retryFailedUnsafe(int $mailshotId): array
|
private function retryFailedUnsafe(int $mailshotId, string &$stage): array
|
||||||
{
|
{
|
||||||
|
$stage = 'loading mail credentials';
|
||||||
$creds = $this->credentials->credentials();
|
$creds = $this->credentials->credentials();
|
||||||
if ($creds === null) {
|
if ($creds === null) {
|
||||||
return ['ok' => false, 'errors' => ['Missing mail credentials. Configure FECA Mailshots Profile page first.']];
|
return ['ok' => false, 'errors' => ['Missing mail credentials. Configure FECA Mailshots Profile page first.']];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$stage = 'loading recipient rows';
|
||||||
[$mailshot, $rows] = $this->loadMailshotAndRows($mailshotId);
|
[$mailshot, $rows] = $this->loadMailshotAndRows($mailshotId);
|
||||||
|
$stage = 'loading failed last-run rows';
|
||||||
$failed = array_values(array_filter($this->lastRun->listForMailshot($mailshotId), static fn(array $r): bool => (string) ($r['status'] ?? '') === 'failed'));
|
$failed = array_values(array_filter($this->lastRun->listForMailshot($mailshotId), static fn(array $r): bool => (string) ($r['status'] ?? '') === 'failed'));
|
||||||
|
|
||||||
if ($failed === []) {
|
if ($failed === []) {
|
||||||
return ['ok' => true, 'attempted' => 0, 'sent' => 0, 'failed' => 0, 'warnings' => 0, 'log' => []];
|
return ['ok' => true, 'attempted' => 0, 'sent' => 0, 'failed' => 0, 'warnings' => 0, 'log' => []];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$stage = 'matching failed recipient rows';
|
||||||
$retryRows = [];
|
$retryRows = [];
|
||||||
foreach ($failed as $f) {
|
foreach ($failed as $f) {
|
||||||
$keyField = (string) ($f['recipient_key_field'] ?? '');
|
$keyField = (string) ($f['recipient_key_field'] ?? '');
|
||||||
|
|
@ -376,33 +419,39 @@ final class MailshotRunService
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$stage = 'sending failed recipient rows';
|
||||||
return $this->executeSendLoop($mailshotId, $mailshot, $retryRows, $creds, false);
|
return $this->executeSendLoop($mailshotId, $mailshot, $retryRows, $creds, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @return array<string, mixed> */
|
/** @return array<string, mixed> */
|
||||||
public function retryRecipient(int $mailshotId, string $recipientKey): array
|
public function retryRecipient(int $mailshotId, string $recipientKey): array
|
||||||
{
|
{
|
||||||
|
$stage = 'initializing recipient retry';
|
||||||
try {
|
try {
|
||||||
return $this->retryRecipientUnsafe($mailshotId, $recipientKey);
|
return $this->retryRecipientUnsafe($mailshotId, $recipientKey, $stage);
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
return ['ok' => false, 'errors' => ['Retry recipient failed: ' . $e->getMessage()]];
|
return ['ok' => false, 'errors' => [$this->diagnosticError('Retry recipient failed', $stage, $e)]];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @return array<string, mixed> */
|
/** @return array<string, mixed> */
|
||||||
private function retryRecipientUnsafe(int $mailshotId, string $recipientKey): array
|
private function retryRecipientUnsafe(int $mailshotId, string $recipientKey, string &$stage): array
|
||||||
{
|
{
|
||||||
|
$stage = 'validating recipient retry request';
|
||||||
$recipientKey = trim($recipientKey);
|
$recipientKey = trim($recipientKey);
|
||||||
if ($recipientKey === '') {
|
if ($recipientKey === '') {
|
||||||
return ['ok' => false, 'errors' => ['recipient_key is required.']];
|
return ['ok' => false, 'errors' => ['recipient_key is required.']];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$stage = 'loading mail credentials';
|
||||||
$creds = $this->credentials->credentials();
|
$creds = $this->credentials->credentials();
|
||||||
if ($creds === null) {
|
if ($creds === null) {
|
||||||
return ['ok' => false, 'errors' => ['Missing mail credentials. Configure FECA Mailshots Profile page first.']];
|
return ['ok' => false, 'errors' => ['Missing mail credentials. Configure FECA Mailshots Profile page first.']];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$stage = 'loading recipient rows';
|
||||||
[$mailshot, $rows] = $this->loadMailshotAndRows($mailshotId);
|
[$mailshot, $rows] = $this->loadMailshotAndRows($mailshotId);
|
||||||
|
$stage = 'loading last-run rows';
|
||||||
$entries = $this->lastRun->listForMailshot($mailshotId);
|
$entries = $this->lastRun->listForMailshot($mailshotId);
|
||||||
$candidate = null;
|
$candidate = null;
|
||||||
foreach ($entries as $e) {
|
foreach ($entries as $e) {
|
||||||
|
|
@ -415,11 +464,13 @@ final class MailshotRunService
|
||||||
return ['ok' => false, 'errors' => ['No last-run row found for recipient_key.']];
|
return ['ok' => false, 'errors' => ['No last-run row found for recipient_key.']];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$stage = 'matching recipient row';
|
||||||
$row = $this->findRecipientByKey($rows, (string) ($candidate['recipient_key_field'] ?? ''), $recipientKey);
|
$row = $this->findRecipientByKey($rows, (string) ($candidate['recipient_key_field'] ?? ''), $recipientKey);
|
||||||
if ($row === null) {
|
if ($row === null) {
|
||||||
return ['ok' => false, 'errors' => ['Recipient not found in current data source result.']];
|
return ['ok' => false, 'errors' => ['Recipient not found in current data source result.']];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$stage = 'sending recipient retry';
|
||||||
return $this->executeSendLoop($mailshotId, $mailshot, [$row], $creds, false);
|
return $this->executeSendLoop($mailshotId, $mailshot, [$row], $creds, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -758,6 +809,7 @@ final class MailshotRunService
|
||||||
*/
|
*/
|
||||||
private function executeSendLoop(int $mailshotId, array $mailshot, array $rows, array $creds, bool $freshRun): array
|
private function executeSendLoop(int $mailshotId, array $mailshot, array $rows, array $creds, bool $freshRun): array
|
||||||
{
|
{
|
||||||
|
$stage = 'preparing mailshot addresses';
|
||||||
$cc = $this->splitAddresses((string) ($mailshot['CC'] ?? ''));
|
$cc = $this->splitAddresses((string) ($mailshot['CC'] ?? ''));
|
||||||
$bcc = $this->splitAddresses((string) ($mailshot['BCC'] ?? ''));
|
$bcc = $this->splitAddresses((string) ($mailshot['BCC'] ?? ''));
|
||||||
$recipientEmailField = trim((string) ($mailshot['RecipientEmailField'] ?? ''));
|
$recipientEmailField = trim((string) ($mailshot['RecipientEmailField'] ?? ''));
|
||||||
|
|
@ -766,6 +818,7 @@ final class MailshotRunService
|
||||||
$log = [];
|
$log = [];
|
||||||
|
|
||||||
foreach (array_values($rows) as $index => $row) {
|
foreach (array_values($rows) as $index => $row) {
|
||||||
|
$stage = 'detecting recipient address';
|
||||||
$counters['attempted']++;
|
$counters['attempted']++;
|
||||||
$recipientEmail = $this->detectEmail($row, $recipientEmailField);
|
$recipientEmail = $this->detectEmail($row, $recipientEmailField);
|
||||||
[$recipientKeyField, $recipientKey] = $this->detectRecipientKey($row, $index);
|
[$recipientKeyField, $recipientKey] = $this->detectRecipientKey($row, $index);
|
||||||
|
|
@ -789,6 +842,7 @@ final class MailshotRunService
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
$stage = 'rendering template for recipient ' . $recipientKey;
|
||||||
$render = $this->renderer->render(
|
$render = $this->renderer->render(
|
||||||
(string) $mailshot['Subject'],
|
(string) $mailshot['Subject'],
|
||||||
(string) $mailshot['Message'],
|
(string) $mailshot['Message'],
|
||||||
|
|
@ -813,10 +867,12 @@ final class MailshotRunService
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
$stage = 'preparing attachments for recipient ' . $recipientKey;
|
||||||
$attachments = array_merge(
|
$attachments = array_merge(
|
||||||
$this->staticAttachments($mailshot),
|
$this->staticAttachments($mailshot),
|
||||||
$this->renderedPdfAttachments($mailshot, $row, $render)
|
$this->renderedPdfAttachments($mailshot, $row, $render)
|
||||||
);
|
);
|
||||||
|
$stage = 'sending SMTP message for recipient ' . $recipientKey;
|
||||||
$smtp = $this->smtp->send(
|
$smtp = $this->smtp->send(
|
||||||
$creds,
|
$creds,
|
||||||
[$recipientEmail],
|
[$recipientEmail],
|
||||||
|
|
@ -830,6 +886,7 @@ final class MailshotRunService
|
||||||
|
|
||||||
$warning = null;
|
$warning = null;
|
||||||
try {
|
try {
|
||||||
|
$stage = 'appending IMAP sent copy for recipient ' . $recipientKey;
|
||||||
$attemptId = sprintf('run_%d_%s_%d', $mailshotId, $recipientKey, time());
|
$attemptId = sprintf('run_%d_%s_%d', $mailshotId, $recipientKey, time());
|
||||||
$this->imap->appendSent($creds, (string) $smtp['raw_mime'], $attemptId);
|
$this->imap->appendSent($creds, (string) $smtp['raw_mime'], $attemptId);
|
||||||
} catch (\Throwable $imapErr) {
|
} catch (\Throwable $imapErr) {
|
||||||
|
|
@ -853,7 +910,8 @@ final class MailshotRunService
|
||||||
]);
|
]);
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
$counters['failed']++;
|
$counters['failed']++;
|
||||||
$log[] = ['recipient_key' => $recipientKey, 'status' => 'failed', 'error' => $e->getMessage()];
|
$error = $this->diagnosticError('Recipient send failed', $stage, $e);
|
||||||
|
$log[] = ['recipient_key' => $recipientKey, 'status' => 'failed', 'error' => $error];
|
||||||
$this->lastRun->create([
|
$this->lastRun->create([
|
||||||
'mailshot_id' => $mailshotId,
|
'mailshot_id' => $mailshotId,
|
||||||
'data_source' => (string) ($mailshot['DataSource'] ?? ''),
|
'data_source' => (string) ($mailshot['DataSource'] ?? ''),
|
||||||
|
|
@ -862,7 +920,7 @@ final class MailshotRunService
|
||||||
'recipient_key_field' => $recipientKeyField,
|
'recipient_key_field' => $recipientKeyField,
|
||||||
'recipient_email_last' => $recipientEmail,
|
'recipient_email_last' => $recipientEmail,
|
||||||
'status' => 'failed',
|
'status' => 'failed',
|
||||||
'error_message' => $e->getMessage(),
|
'error_message' => $error,
|
||||||
'attempt_count' => $freshRun ? 1 : 2,
|
'attempt_count' => $freshRun ? 1 : 2,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,4 +26,6 @@
|
||||||
* [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] 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
|
* [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.
|
* [ ] 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. Problem characters include quote (All Saint's) and e accent (café).
|
||||||
|
* [ ] (patch 32, 33) Test email including these characters critical errors wordpress. Perhaps because "Last Run Rows" was non-empty and related to a previous different mailshot / data source. Still critical errors after attempt to make more robust.
|
||||||
|
* [ ] patch 30 also criticals sending to "test" mailshot, but OK sending to fen contacts.
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue