Skip to content

[2.1] feat: Email bounce & complaint handling#4827

Draft
imorland wants to merge 2 commits into
2.xfrom
im/mail-bounce-handling
Draft

[2.1] feat: Email bounce & complaint handling#4827
imorland wants to merge 2 commits into
2.xfrom
im/mail-bounce-handling

Conversation

@imorland

Copy link
Copy Markdown
Member

The problem

When Flarum sends mail to an address that hard-bounces (mailbox doesn't exist, permanently rejected) or is marked as spam, Flarum currently has no idea. It keeps sending to that address indefinitely. Repeated delivery to dead addresses and to people who filed spam complaints is the single fastest way to damage a forum's sender reputation and get its domain throttled or blocklisted by the mail provider.

Two things that look like they'd help, but don't:

  • EmailSendFailed only fires on submission failure (bad credentials, API rejects the request). A bounce happens asynchronously, minutes-to-hours after the provider already accepted the message — the transport never sees it.
  • is_email_confirmed does not gate outbound mail at all (notifications gate solely on shouldEmail()), so it can't be repurposed for this.

Bounces are only knowable via the provider's inbound webhook — Mailgun, Postmark, etc. POST delivery events back to us.

How this solves it

An abstract, provider-agnostic pipeline in core:

  • New opt-in driver capability interfacesHandlesWebhooks (receive + verify + parse inbound events) and ProvisionsWebhooks (register the webhook with the provider automatically). DriverInterface is untouched, so third-party mail drivers are unaffected.
  • A single inbound route POST /mail/webhook[/{driver}]WebhookController resolves the active driver, hands the request to its parser, and dispatches provider-neutral EmailBounced / EmailComplained events. Verification is owned entirely by each provider's parser (HMAC signature, IP allowlist, …) — the controller never assumes how a provider authenticates.
  • Leverages Symfony Mailer's webhook bridges (symfony/webhook + symfony/remote-event, newly required) so signature/IP verification and payload normalisation come from Symfony, and the crucial soft-vs-hard bounce distinction (transient deferrals are ignored) is handled for us.

On top of the pipeline:

  • Bounce state on the user (email_bounced_at + email_bounce_reason), plus a permanent email_bounce_events log so statistics survive a user fixing their address.
  • Suppression of notification email to bounced/complained users, behind a mail_suppress_bounced setting that defaults off — no behaviour change unless an admin opts in. Transactional mail (e.g. email confirmation) is deliberately not suppressed, so users can still recover.
  • In-app notice prompting affected users to update their email; it clears automatically once they do.
  • Admin visibility — an "Email delivery" column and is:bounced filter on the user list (shown only when the active driver can report bounces), and a dashboard widget with bounce volume over time plus currently-affected / recovered counts, warning when the return path isn't configured.
  • GDPR-aware — the bundled GDPR extension scrubs the email from bounce-event rows on anonymisation (deletes them on erasure), and anonymised users are filtered out of all notifications so their placeholder addresses never generate mail.

Drivers

Implemented for the two core drivers that support it: Mailgun (HMAC-signed webhooks + automatic provisioning) and Postmark (IP-verified webhooks; URL registered manually in the Postmark dashboard). SMTP and Sendmail have no feedback channel and are unaffected.

The interface is deliberately shaped to fit providers that verify by shared secret, IP allowlist, or SNS certificate, so additional drivers can implement it without core changes. Longer term, the Mailgun and Postmark drivers may be split out of core into their own extensions, and dedicated provider extensions are already planned (fof/ses-mail, fof/brevo-mail) which will build on these same interfaces.

Notes

  • Targeting 2.1: this adds new public API surface (the capability interfaces, events, route, settings) and one behaviour-changing (opt-in, default-off) setting, so it is not for the 2.0 RC line.
  • A documentation PR will follow once the 2.1 cycle begins.

Reviewing

  • The capability interfaces are the public contract — Flarum\Mail\HandlesWebhooks / Flarum\Mail\ProvisionsWebhooks.
  • WebhookController is the security-sensitive entry point: verification is delegated to each driver's parser, and the route is CSRF-exempt by design (the provider signature / source IP is the authentication).

imorland and others added 2 commits July 23, 2026 18:15
Add an abstract, provider-agnostic pipeline for receiving bounce and
spam-complaint webhooks from mail providers, flagging affected users,
optionally suppressing further mail (opt-in, default off), prompting
users to fix their address, and surfacing bounce data to admins.

Implemented for the Mailgun (HMAC-signed webhooks + auto-provisioning)
and Postmark (IP-verified webhooks) core drivers via new opt-in
HandlesWebhooks / ProvisionsWebhooks driver interfaces, so third-party
drivers can add support without core changes.

GDPR-aware: bounce-event PII is scrubbed on anonymisation, and
anonymised users are excluded from notifications.
@imorland imorland added this to the 2.1 milestone Jul 23, 2026
@imorland imorland added type/feature dependencies Pull requests that update a dependency file type/reliability labels Jul 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file type/feature type/reliability

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants