Problem/Motivation
WebformScheduledEmailManager::cronSend() collects every due record's
eid into an array, sends each one via $handler->sendMessage()
in a loop, and then deletes all processed records in a single query after the
loop finishes.
If any sendMessage() call throws an uncaught exception, cronSend()
aborts before that delete runs. Because nothing is deleted, every record that was
already processed in that run stays in the SUBMISSION_SEND state and is sent
again on the next cron run — which hits the same exception and aborts again. The result is
an unbounded resend loop: recipients receive the same scheduled emails over
and over until someone manually clears the queue.
This is easy to hit in production with a recipient address that passes Drupal/Webform's
(egulias RFC) email validation but is rejected by the mail transport's stricter validation.
For example a dotless domain such as user@gmail (or a truncated
user@gma) is RFC-valid — so Webform's validateMessage() accepts it —
but SendGrid's filter_var($value, FILTER_VALIDATE_EMAIL) rejects it and throws a
TypeException while building the recipient. Any mail backend that throws on send
(invalid recipient, transport error, etc.) triggers the loop. Related, but distinct:
#3198465 handled a
different cron exception (a deleted handler) by cleaning up records rather than by catching.
Steps to reproduce
- Add a Scheduled Email handler whose recipient comes from a submission value, scheduled
to send in the past (so it is immediately due). - Submit once with a recipient the mail transport rejects by throwing (e.g. SendGrid
Integration + a dotless-domain address likeuser@gmail), plus one or more
submissions with valid recipients. - Run cron. The throwing send aborts
cronSend(); no records are
deleted. - Run cron again: every record (including ones already sent) is re-sent. Repeat
indefinitely.
Proposed resolution
Wrap the per-record sendMessage() call in cronSend() in a
try/catch. On exception, log an error to the webform_scheduled_email channel and
treat the send as not-sent ($status = FALSE), so the loop continues and the
post-loop delete still runs. The failing record's eid is already collected before
the send, so it is deleted along with the rest — preventing an infinite retry of a poison
record while still recording the failure for follow-up.
// Send (translated) email. $message = $handler->getMessage($webform_submission); try { $status = $handler->sendMessage($webform_submission, $message); } catch (\Exception $e) { $status = FALSE; $this->getLogger('webform_scheduled_email')->error('Scheduled email not sent for submission @sid via the @handler handler: @message', [ '@sid' => $sid, '@handler' => $handler_id, '@message' => $e->getMessage(), ]); }
Remaining tasks
- Review the MR.
- The MR includes a functional test (a throwing mail backend + a single-handler fixture)
asserting that: the queue is cleared after a throwing send, a valid sibling email is still
delivered, an error is logged to thewebform_scheduled_emailchannel, and a
second cron run is a no-op. The test fails without the patch (the uncaught
exception propagates out of cron).
User interface changes
None.
API changes
None. cronSend() still returns the same stats array; a previously-uncaught
exception now resolves to an EMAIL_NOT_SENT result plus a logged error, and the
batch completes as designed.
Data model changes
None.
| Comment | File | Size | Author |
|---|---|---|---|
| #3 | webform_scheduled_email-catch-send-exceptions.patch | 1.33 KB | thronedigital |
Issue fork webform-3594252
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #3
thronedigital commentedComment #4
erlingx commentedLooks good. Tested and verified.
I reproduced the bug with the throwing mail backend as described in Steps to reproduce.
Ran the new test (WebformScheduledEmailExceptionTest) that failed as expected.
Applied the patch.
Manually verified the patch worked. Invalid email address logged with error and deleted from queue and all valid sibling emails were sent.
Ran test again that now passed.
Comment #7
liam morlandThanks!