feat: AWS SES integration — Webhook to detect AWS bounces, updates email-suppression table, Message Tracking UI#1163
Open
mrjbj wants to merge 5 commits intoHiEventsDev:developfrom
Conversation
… suppression, transactional tracking, and Message Tracking UI
## Overview
Complete integration with AWS SES event notifications via SNS webhooks.
Tracks delivery status for both marketing (broadcast) and transactional
emails, suppresses addresses that bounce or complain, and provides a
full Message Tracking UI for event organizers.
## Architecture
```
┌──────────────┐ ┌─────────┐ ┌──────────────────┐
│ Hi.Events │───▶│ AWS SES │───▶│ Recipient Inbox │
│ (send mail) │ │ │ └──────────────────┘
└──────┬───────┘ └────┬────┘ │
│ │ ┌─────▼──────┐
│ SES Message │ │ Bounce / │
│ ID captured │ │ Complaint │
│ │ └─────┬──────┘
│ ┌────▼────┐ │
│ │ AWS SNS │◀──────────┘
│ │ Topic │ Configuration Set
│ └────┬────┘ routes events
│ │
│ ┌───────────▼───────────┐
│ │ POST /webhooks/ses │
│ │ (SNS Notification) │
│ └───────────┬───────────┘
│ │
│ ┌───────────▼───────────┐
│ │ IncomingSesWebhook │
│ │ Handler │
│ │ • Verify signature │
│ │ • Deduplicate │
│ │ • Route by type │
│ └──┬──────┬──────┬──────┘
│ │ │ │
│ ┌────▼─┐ ┌──▼──┐ ┌▼────────┐
│ │Bounce│ │Comp-│ │Delivery │
│ │Hndlr │ │laint│ │Handler │
│ └──┬───┘ └──┬──┘ └────┬─────┘
│ │ │ │
│ ┌──▼────────▼──┐ ┌──▼──────────────┐
└──▶│ Suppression │ │ Status Update │
│ Service │ │ SENT → DELIVERED │
│ (suppress │ │ SENT → BOUNCED │
│ future │ │ DELIVERED → │
│ sends) │ │ BOUNCED │
└──────────────┘ └──────────────────┘
```
## Database
### New Tables
- `email_suppressions` — account-scoped suppression list with bounce/complaint reason tracking
- `outgoing_transaction_messages` — tracks all transactional email sends (order confirmations, attendee tickets, waitlist notifications)
### Modified Tables
- `outgoing_messages` — added `ses_message_id` for bounce correlation
## Message Lifecycle
```
SENT ──▶ DELIVERED (SES delivery confirmation)
│ │
└──────────┴──▶ BOUNCED (SES bounce/complaint notification)
SUPPRESSED (address on suppression list, email not sent)
FAILED (send error)
```
## Suppression Rules
| Event Type | Bounce Type | Suppresses |
|-------------------|-------------|----------------------|
| Permanent Bounce | Hard | All emails |
| Transient Bounce | Soft | Marketing only |
| Complaint | Abuse etc. | Marketing only |
Suppressions upgrade: a transient bounce followed by a permanent bounce
updates the record to suppress all email types.
## Configuration
```env
# SES event handling (all optional, feature is additive)
SES_CONFIGURATION_SET=your-config-set # Routes events to SNS
AWS_SNS_TOPIC_ARN=arn:aws:sns:... # Validates incoming topic
AWS_SNS_VERIFY_SIGNATURE=true # Verify SNS signatures
SES_SUPPRESSION_ENABLED=false # Enable pre-send checks
```
## Frontend: Message Tracking
Three-tab page replacing "Delivery Issues":
- **Delivery Issues** — failed/bounced/suppressed transactional emails with resolve workflow (update email, resend, auto-resolve)
- **Marketing** — all broadcast messages with status/date filters and search
- **Transactions** — all transactional messages with email type/status/date filters and search
## Key Backend Files
- `app/Http/Actions/Common/Webhooks/SesIncomingWebhookAction.php` — webhook entry
- `app/Services/Application/Handlers/Email/Ses/IncomingSesWebhookHandler.php` — SNS routing
- `app/Services/Domain/Email/Ses/EventHandlers/{Bounce,Complaint,Delivery}Handler.php`
- `app/Services/Domain/Email/EmailSuppressionService.php` — suppression logic
- `app/Services/Domain/Email/TransactionalEmailTrackingService.php` — wraps sends
- `app/Http/Actions/TransactionMessages/{Resend,Resolve}TransactionMessageAction.php`
## Development
SNS webhook simulator for local testing without AWS infrastructure:
```bash
./docker/development/scripts/sns-webhook-simulator.sh help
./docker/development/scripts/sns-webhook-simulator.sh bounce marketing:10
./docker/development/scripts/sns-webhook-simulator.sh delivery transaction:1
./docker/development/scripts/sns-webhook-simulator.sh list-suppressions
```
Requires `AWS_SNS_VERIFY_SIGNATURE=false` and `SES_SUPPRESSION_ENABLED=true`
in `docker/development/.env` (already set).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…Issue modal Resending creates a new tracking row with a new ses_message_id, so delivery confirmation updates the new row, not the original failure. Simplified to two checkboxes: "Resend this email" and "Resolve now". Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… sorting Consolidate delivery issue handling into unified endpoints, replace per-type resolve/failures routes with a single /delivery-issues API, and clean up dead code from the prior design. Backend: - Add unified delivery-issues endpoints (get, resolve) for both announcement and transaction messages - Add resend-with-email-update support for outgoing messages - Add retry tracking with ExtractsRetryHeader trait - Add resolved_at, resolution_type, and retry columns via migrations - Return updated_at from message resources; allow sorting by it - Remove dead GetTransactionMessageFailures and ResolveTransactionMessage actions/handlers/routes Frontend: - Refactor Message Tracking into Announcements and Transactions tabs with inline issue filtering, sortable columns, and resolve workflow - Add Created and Updated date columns, default sort by created_at desc so resolving issues doesn't reorder the list - Add standalone Delivery Issues page with route - Add Resolve Delivery Issue modal with resend-to-new-address support - Remove dead useResolveTransactionMessage, useGetTransactionMessageFailures, and unused API client methods - Fix resend mutations to invalidate delivery issues query cache - Add translations for all supported languages Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add a subtle refresh icon next to the page title so users can pull updated delivery statuses without a full browser reload, since SES webhook status changes arrive asynchronously. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Subtle 90-degree forward-and-back rotation on click to give visual feedback that the refresh was triggered. 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 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.
What changes I've made:
Comprehensive AWS Integration to SNS Delivery Events
Why I've made these changes
Overview
Current State
System currently includes
messagesandoutgoing_messagestables to track announcement messages sent out by organizers to recipients. However, there is no integration with inbound AWS Delivery Event notifications sent via SNS to hi-events system. So currently, organizers can send outbound announcement emails but the delivery status of those emails do not get updated and importantly, if a recipient bounces, the system does not know about it and may resend to them again.Improvements - Bounce Collection & Email Suppression
This PR includes a new table
email_suppressionsthat gets populated by inbound AWS Delivery Events and is consulted account wide before future outbound emails are released for delivery. This way, if a bounce is detected, future messages to that email will be suppressed, account wide. Bounces are classified aspermanentortransient. Permanent bounces suppress both announcement messages and transaction notifications, while transient bounces only suppress announcement messages but continue to send the transaction related ones.This PR adds a
message_idfield to theoutgoing_messagestable (so AWS Delivery Events can find the outbound message they relate to) but otherwise leaves the existing table alone. Outgoing_messages continues to reflect announcement related emails only. This PR includes a secondary tableoutgoing_transaction_messagesto track emails related to transaction events, like new orders, tickets, waitlist, events by recipient along with their AWS delivery status.Improvements - User Interface
It is currently difficult to review status of outbound emails, as they are buried in the "To:" link within the address section of each message. Users have to navigate to the message, click the link and then can see the recipients of that message and the delivery status (which is currently always set to "sent" since no AWS Delivery Event integration but with this PR will be updated to BOUNCED or SUPPRESSED or DELIVERED as appropriate).
Users can view and resend from the new "Message Tracking" page available in Guest Management. Message tracking has three tabs - one for "delivery failures" (e.g. bounces that should be resolved by updating email address on the ticket, order or message and resent), and one tab for event announcements and transactions respectively.
Superadmin users can access the
email_suppressionlist to add or remove email suppressions that affect subsequent transaction or announcement activity. This is placed at superadmin account level because if an email gets a hard bounce for one event, it should be bad for any event elsewhere.All of this foundation is packaged up into one PR. I have it running in my instance in production and have confirmed that it works, though my volumes are not very high, so performance testing might be warranted separately.
This only works for AWS SES integration over SNS. If system is using other email provider, this functionality is skipped as if it never existed at all.
Technical details below.
Architecture
Database
New Tables
email_suppressions— account-scoped suppression list with bounce/complaint reason trackingoutgoing_transaction_messages— tracks all transactional email sends (order confirmations, attendee tickets, waitlist notifications)Modified Tables
outgoing_messages— addedses_message_idfor bounce correlation to specific outbound email sent earlier and to which the inboudn SNS Delivery Event relates.Message Lifecycle
Suppression Rules
Suppressions upgrade: a transient bounce followed by a permanent bounce updates the record to suppress all email types.
Configuration
Frontend: Message Tracking
Three-tab page replacing "Delivery Issues":
Key Backend Files
app/Http/Actions/Common/Webhooks/SesIncomingWebhookAction.php— webhook entryapp/Services/Application/Handlers/Email/Ses/IncomingSesWebhookHandler.php— SNS routingapp/Services/Domain/Email/Ses/EventHandlers/{Bounce,Complaint,Delivery}Handler.phpapp/Services/Domain/Email/EmailSuppressionService.php— suppression logicapp/Services/Domain/Email/TransactionalEmailTrackingService.php— wraps sendsapp/Http/Actions/TransactionMessages/{Resend,Resolve}TransactionMessageAction.phpDevelopment
SNS webhook simulator for local testing without AWS infrastructure:
./docker/development/scripts/sns-webhook-simulator.sh help ./docker/development/scripts/sns-webhook-simulator.sh bounce marketing:10 ./docker/development/scripts/sns-webhook-simulator.sh delivery transaction:1 ./docker/development/scripts/sns-webhook-simulator.sh list-suppressionsRequires
AWS_SNS_VERIFY_SIGNATURE=falseandSES_SUPPRESSION_ENABLED=trueindocker/development/.env(already set).How I've tested these changes
Automated tests (389 passing)
signature rejection, deduplication, invalid JSON handling
transaction messages as bounced
addresses not suppressed
FAILED on error
Manual testing with sns-webhook-simulator.sh
Manual UI testing in browser (local dev)
correctly
What we haven't tested end-to-end yet
Checklist