Skip to content

SmtpDriver leaves Symfony Mailer pingThreshold at 100s default — causes intermittent 451 4.4.2 against SES #4614

Description

@ekumanov

Current Behavior

Forums using smtp mail driver against AWS SES (and likely other SMTP relays with sub-100s idle timeouts) periodically log:

flarum.ERROR: Failed to send email. {"recipient_email":"...","reason":"Expected response code \"250\" but got code \"451\", with message \"451 4.4.2 Timeout waiting for data from client.\".","exception_class":"Symfony\\Component\\Mailer\\Exception\\UnexpectedResponseException"}

The pattern on a forum that recycles its queue worker every 10 minutes is striking — failures cluster late in each worker's life, never near the start:

Failure Worker spawned Worker exited Failure age within worker
13:41:53 13:32:39 13:42:42 T+9:14
16:28:43 16:20:42 16:30:44 T+8:01

That shape rules out network blips and points at connection-age dependence inside a single worker.

Root Cause

Symfony\Component\Mailer\Transport\Smtp\SmtpTransport::$pingThreshold defaults to 100 seconds. The transport reuses the SMTP connection across sends within a process; before each send it only checks the connection (NOOP) if the inter-message gap exceeds the threshold:

https://github.com/symfony/symfony/blob/7.x/src/Symfony/Component/Mailer/Transport/Smtp/SmtpTransport.php

SES closes idle connections faster than 100s (operationally observed in the 30–90s range — AWS doesn't publish an exact number). So when emails are spaced 30–100s apart, SES has already torn down the connection but the transport skips the NOOP and writes DATA + body into the dead pipe. SES eventually responds with 451 4.4.2 Timeout waiting for data from client to whatever zombie state remains.

Flarum\Mail\SmtpDriver::buildTransport() builds the transport via the factory and returns it without configuring the ping threshold:

https://github.com/flarum/framework/blob/main/framework/core/src/Mail/SmtpDriver.php

Steps to Reproduce

  1. Configure SMTP against AWS SES (email-smtp.<region>.amazonaws.com:587, STARTTLS).
  2. Run a long-lived queue:work worker with a non-trivial trickle of email-bound notifications (mentions / replies / private discussions / etc.).
  3. After enough idle gaps in the 30–90s range, observe 451 4.4.2 Timeout waiting for data from client in storage/logs/flarum-YYYY-MM-DD.log.

Expected Behavior

The mailer should detect a closed connection before attempting the next send and reconnect transparently. Symfony exposes SmtpTransport::setPingThreshold(int $seconds) for exactly this; the default just isn't tuned for SES.

Suggested Fix

In framework/core/src/Mail/SmtpDriver::buildTransport(), after building the transport, lower the ping threshold:

$transport = $this->factory->create(new Dsn(...));

if ($transport instanceof SmtpTransport) {
    $transport->setPingThreshold(20);
}

return $transport;

20s is well under any reasonable SMTP idle timeout but rare enough that NOOP overhead is negligible. Could also be made configurable via a setting (mail_smtp_ping_threshold) for forums that need to tune it.

Happy to send a PR.

Environment

  • Flarum core: 2.0.0-rc.1
  • PHP: 8.4.20
  • Mail driver: smtp → AWS SES (eu-central-1)
  • Confirmed default is 100s in current main branch of flarum/framework.

Possible Solution

See "Suggested Fix" above. Has been applied as a local vendor/ patch on our forum and is being observed; will report back if the failure rate doesn't drop to zero.

Additional Context

This issue likely affects everyone using the smtp driver against SES, but is invisible until the queue worker is long-lived enough for the cumulative failure rate to be noticed in logs (we only saw it because we set up a daily error-tally cron). On installs that send via the synchronous "sync" queue driver, it would surface as occasional bounced notifications without an obvious pattern.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions