Skip to content

feat: Implement bounce/complaint logging and suppression integration to Amazon SES πŸ€– #1159

Closed
mrjbj wants to merge 2 commits intoHiEventsDev:developfrom
mrjbj:feature/ses-bounce-handling
Closed

feat: Implement bounce/complaint logging and suppression integration to Amazon SES πŸ€– #1159
mrjbj wants to merge 2 commits intoHiEventsDev:developfrom
mrjbj:feature/ses-bounce-handling

Conversation

@mrjbj
Copy link
Copy Markdown

@mrjbj mrjbj commented Apr 11, 2026

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:

  • SNS webhook endpoint (POST /api/public/webhooks/ses) receives SES bounce/complaint notifications via AWS SNS
  • Native OpenSSL SNS signature verification (no external package dependency)
  • Bounce and complaint handlers extract recipient emails and create suppression records
  • SendEventEmailJob checks suppressions before sending, records SUPPRESSED status in outgoing_messages for audit trail
  • Duplicate notification handling via SNS MessageId cache
  • SSRF protection on SNS SubscriptionConfirmation URLs
  • Supports both SES Notification format (notificationType) and SES Event Destination format (eventType)
  • BaseMail adds X-SES-CONFIGURATION-SET header when configured, enabling event publishing for SMTP-based SES sending

Bounce Status Tracking:

  • BOUNCED status added to OutgoingMessageStatus β€” when a bounce/complaint arrives, the matching outgoing_messages row is updated from SENT to
    BOUNCED
  • ses_message_id column added to outgoing_messages β€” stores the SES message ID returned on send, enabling exact correlation between outbound
    emails and inbound bounce notifications (no fuzzy email matching)
  • SendEventEmailJob uses sendNow() instead of send() to send synchronously within the queue job β€” BaseMail implements ShouldQueue, so
    send() was re-queuing the mailable in a nested job, preventing ses_message_id capture and error detection

Bug Fixes:

  • DeleteEmailSuppressionAction return type fixed from JsonResponse to Response β€” the 204 No Content from deletedResponse() caused the
    frontend to show "Failed to remove" despite successful deletion
  • ResendOrderConfirmationAction now routes through SendOrderDetailsService instead of calling mailer directly β€” ensures consistent email
    handling

Admin API: list (with search/filter/pagination), create, and delete email suppressions

28 unit + feature tests

Frontend

  • Admin email suppressions management page (/admin/email-suppressions)
  • Search by email, filter by reason (bounce/complaint) and source (SES/manual)
  • Manual suppression entry and soft-delete removal
  • Translations for all supported locales

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 emails
  • AWS_SNS_TOPIC_ARN β€” optional TopicArn validation on incoming webhooks
  • AWS_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_suppressions migration, the SUPPRESSED status enum, and suppression checks in
SendEventEmailMessagesService. This PR completes that intent by connecting AWS SES notifications to the suppression pipeline and adding delivery
status 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_ENABLED and SES_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_id column is nullable and unused without SES.

How I've tested these changes

  • End-to-end in production with real AWS SES: sent emails to bounce@simulator.amazonses.com and complaint@simulator.amazonses.com, confirmed
    suppression records created automatically via SNS webhook
  • Verified bounced recipients show BOUNCED status and suppressed recipients show SUPPRESSED status in the Recipients modal
  • Verified ses_message_id is captured on send and matched on bounce β€” exact correlation confirmed in production
  • Verified sendNow() fix: emails send synchronously within the queue job, SentMessage is returned, message ID is stored
  • Verified admin UI: search, filter, manual add/remove all functional (including the delete response fix)
  • Verified resend order confirmation routes through SendOrderDetailsService
  • php artisan test --testsuite=Unit β€” all tests pass
  • npx tsc --noEmit β€” no TypeScript errors

Checklist

  • I have read the contributing guidelines.
  • My code follows the coding standards of the project.
  • I have tested my changes, and they work as expected.

mrjbj and others added 2 commits April 11, 2026 17:11
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>
@mrjbj mrjbj changed the title feat: Implement bounce/complaint logging and suppression integration to Amazon SES feat: Implement bounce/complaint logging and suppression integration to Amazon SES πŸ€– Apr 13, 2026
@mrjbj mrjbj closed this Apr 13, 2026
@github-actions github-actions bot locked and limited conversation to collaborators Apr 13, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant