[2.1] feat: Email bounce & complaint handling#4827
Draft
imorland wants to merge 2 commits into
Draft
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
EmailSendFailedonly 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_confirmeddoes not gate outbound mail at all (notifications gate solely onshouldEmail()), 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:
HandlesWebhooks(receive + verify + parse inbound events) andProvisionsWebhooks(register the webhook with the provider automatically).DriverInterfaceis untouched, so third-party mail drivers are unaffected.POST /mail/webhook[/{driver}]→WebhookControllerresolves the active driver, hands the request to its parser, and dispatches provider-neutralEmailBounced/EmailComplainedevents. Verification is owned entirely by each provider's parser (HMAC signature, IP allowlist, …) — the controller never assumes how a provider authenticates.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:
email_bounced_at+email_bounce_reason), plus a permanentemail_bounce_eventslog so statistics survive a user fixing their address.mail_suppress_bouncedsetting 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.is:bouncedfilter 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.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
Reviewing
Flarum\Mail\HandlesWebhooks/Flarum\Mail\ProvisionsWebhooks.WebhookControlleris 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).