feat: Implement bounce/complaint logging and suppression integration to Amazon SES π€ #1159
Closed
mrjbj wants to merge 2 commits intoHiEventsDev:developfrom
Closed
feat: Implement bounce/complaint logging and suppression integration to Amazon SES π€ #1159mrjbj wants to merge 2 commits intoHiEventsDev:developfrom
mrjbj wants to merge 2 commits intoHiEventsDev:developfrom
Conversation
Complete SES bounce and complaint notification pipeline with admin management UI, building on the existing email_suppressions table and OutgoingMessageStatus::SUPPRESSED that were already in the codebase. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Production-tested improvements to the SES bounce handling: - Add BOUNCED status to OutgoingMessageStatus enum - Store ses_message_id when sending emails via SendEventEmailJob - Use ses_message_id for exact bounce/complaint correlation instead of fuzzy email matching - Use sendNow() instead of send() in SendEventEmailJob since BaseMail implements ShouldQueue (send() re-queues the mailable) - Fix DeleteEmailSuppressionAction return type (JsonResponse -> Response) to match the 204 No Content from deletedResponse() - Route ResendOrderConfirmationAction through SendOrderDetailsService instead of calling mailer directly Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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 subscribe to this conversation on GitHub.
Already have an account?
Sign in.
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.
What changes I've made
Completes the Amazon SES bounce/complaint notification pipeline that was partially scaffolded in the codebase (email_suppressions table, OutgoingMessageStatus::SUPPRESSED, suppression checks in the send pipeline).
This PR wires up the missing pieces for AWS SES provider such that SNS bounce and complaint notifications flow through to actual email suppression, and adds delivery status tracking so organizers can see which recipients bounced.
If a hard bounce is received from AWS, then a row is added into new table
email_supressions, with corresponding UI available on superadmin page. whenever outbound emails are sent, this table is checked and messages are suppressed as needed. Additionally, organizers can check recipients status via the recipients modal that pops up from link on message (see "Events>Guest Management>Messages then select a message and click on the recipients link in the "address bar" section of the message) to see if the result was "sent" or "suppressed" or "bounced". (Bounced is reserved for messages that are not on the suppressed list yet but bounced back from AWS the first time nonetheless).This PR adds AWS SNS bounce tracking for marketing (non-transactional) emails only. There is a separate PR that can come for transactional-emails once this foundation for AWS SNS web hook handling is in place. If instance is using an email provider not AWS SES, then this PR has no affect.
Backend
SES Webhook & Suppression Pipeline:
POST /api/public/webhooks/ses) receives SES bounce/complaint notifications via AWS SNSoutgoing_messagesfor audit trailnotificationType) and SES Event Destination format (eventType)X-SES-CONFIGURATION-SETheader when configured, enabling event publishing for SMTP-based SES sendingBounce Status Tracking:
OutgoingMessageStatusβ when a bounce/complaint arrives, the matchingoutgoing_messagesrow is updated from SENT toBOUNCED
ses_message_idcolumn added tooutgoing_messagesβ stores the SES message ID returned on send, enabling exact correlation between outboundemails and inbound bounce notifications (no fuzzy email matching)
sendNow()instead ofsend()to send synchronously within the queue job βBaseMailimplementsShouldQueue, sosend()was re-queuing the mailable in a nested job, preventingses_message_idcapture and error detectionBug Fixes:
DeleteEmailSuppressionActionreturn type fixed fromJsonResponsetoResponseβ the 204 No Content fromdeletedResponse()caused thefrontend to show "Failed to remove" despite successful deletion
ResendOrderConfirmationActionnow routes throughSendOrderDetailsServiceinstead of calling mailer directly β ensures consistent emailhandling
Admin API: list (with search/filter/pagination), create, and delete email suppressions
28 unit + feature tests
Frontend
/admin/email-suppressions)Configuration (all opt-in via .env)
SES_SUPPRESSION_ENABLEDβ enables suppression checks in the send pipeline (default: false)SES_CONFIGURATION_SETβ attaches the SES configuration set header to outgoing emailsAWS_SNS_TOPIC_ARNβ optional TopicArn validation on incoming webhooksAWS_SNS_VERIFY_SIGNATUREβ SNS signature verification (default: true)Why I've made these changes
SES bounce and complaint handling is essential for maintaining sender reputation when using AWS SES. Without it, emails continue sending to
addresses that have permanently bounced, spam complaints go untracked, and organizers have no visibility into delivery failures.
The codebase already had the foundation β the
email_suppressionsmigration, the SUPPRESSED status enum, and suppression checks inSendEventEmailMessagesService. This PR completes that intent by connecting AWS SES notifications to the suppression pipeline and adding deliverystatus tracking so organizers can see bounced recipients in the Recipients modal.
Non-SES users are unaffected: All SES-specific behavior is gated behind
SES_SUPPRESSION_ENABLEDandSES_CONFIGURATION_SET. When unset,BaseMail returns empty headers, suppression checks return false immediately, and the webhook endpoint is protected by SNS signature verification.
The
ses_message_idcolumn is nullable and unused without SES.How I've tested these changes
bounce@simulator.amazonses.comandcomplaint@simulator.amazonses.com, confirmedsuppression records created automatically via SNS webhook
ses_message_idis captured on send and matched on bounce β exact correlation confirmed in productionsendNow()fix: emails send synchronously within the queue job,SentMessageis returned, message ID is storedSendOrderDetailsServicephp artisan test --testsuite=Unitβ all tests passnpx tsc --noEmitβ no TypeScript errorsChecklist