serendipity_hq/bundle-aws-ses-monitor
A Symfony bundle for monitoring Amazon SES: it consumes SNS bounce and complaint notifications, stores them via Doctrine, and fires events so you can suppress bad addresses, handle feedback, and protect your sender reputation.
Read the repo before the README. Check Resources/config/ (services + routing), the Entity/ folder (Email, Bounce, Complaint, Delivery, Topic, Subscription), the setup Command/, and the SwiftMailer filter class. That's the whole package in ~20 files; the value is the SNS/SES wiring and the state model, not the Symfony plumbing.
Reality check for a Laravel team: this is a Symfony bundle with Doctrine ORM entities. It will not drop into a Laravel app. Two viable paths:
MessageSending listener, a ses_suppressions table, and an Artisan command that creates the SNS topic/subscription and calls SetIdentityNotificationTopic. For a Laravel-only team this is almost always cheaper than path 1.Minimal Symfony setup (if you go sidecar): composer require serendipity_hq/bundle-aws-ses-monitor, enable the bundle in config/bundles.php, import the bundle routing under a public prefix, configure serendipity_hq_aws_ses_monitor with AWS key/secret/region and db_driver: orm, then doctrine:schema:update (or generate migrations). Run the setup command — it creates the SNS topic, subscribes your endpoint, and attaches the topic to the SES identity for Bounce/Complaint/Delivery. It prints the endpoint and subscription ARN, which is your confirmation that it worked.
First use case: bounce suppression. Send to a known-bouncing address, confirm the Email row flips to bounced, then assert the next send to that address is refused by the SwiftMailer filter. That single loop exercises webhook → parsing → persistence → filter, which is the entire package.
Suppression check at send time. In Symfony, register the bundle's SwiftMailer plugin and it blocks automatically. In Laravel, a MessageSending listener that looks up the recipient and returns false — same effect, but you own the query, so keep it fast (below).
Webhook as the ingestion path. One POST endpoint, branch on x-amz-sns-message-type:
SubscriptionConfirmation → GET the SubscribeURL (you must actually visit it; just returning 200 does nothing).Notification → json_decode the Message string, switch on notificationType (Bounce / Complaint / Delivery), and persist. The Message is a nested JSON string, not an object — this trips everyone once.Correlate on message ID. SES notifications carry mail.messageId. Persist the SES MessageId you get back from the send (or the X-SES-MESSAGE-ID header) so bounces attach to a recipient instead of landing in an orphan pool. Without this the bundle receives everything and matches nothing.
Project the hot path. A Doctrine/DB round-trip on every outgoing mail is wasteful. Mirror is_bounced / is_complained into Redis or a narrow indexed suppressions(email) table, fed by the webhook handler. The full entity graph stays for audit; the send path hits the projection.
Env-scoped topic names. Configure topic names as ses-monitor-{env}. A staging deploy that grabs prod's SES notification config is a genuinely bad afternoon.
Run setup in deploy, guarded. Fire the setup command once per environment behind a config flag; it's designed to be re-runnable but will happily rewrite SES notification config on every deploy otherwise.
Re-check inside queued jobs. Laravel Mailables queued before a bounce arrives will still send. Check suppression in the MessageSending listener and at the top of the job/Mailable build().
Operational commands. You will need suppression:remove <email> (complaint appeals are real) and a bulk importer for historical bounces from CloudWatch/S3 logs.
DB::table() against the mapped tables, or port the entities.auth you get a 302 on the subscription POST and a subscription that never delivers events — with no error surfaced in SES.SetIdentityNotificationTopic is a full replace per (identity, notification type). If Terraform or another app also manages SES notifications, you will fight each other on every deploy. Make one system the owner.sns:CreateTopic, sns:Subscribe, sns:ListSubscriptionsByTopic, ses:SetIdentityNotificationTopic, ses:GetIdentityNotificationAttributes. Miss the last one and the setup command dies mid-run, leaving an orphan topic and no subscription.Content-Type: text/plain, some proxies mangle it, and getContent() can only be read once. Dump the body before any parsing, and replay captured Bounce/Complaint/Delivery fixtures locally instead of poking AWS.sns.<region>.amazonaws.com for the signing certificate. Don't hard-cache certs indefinitely; key rotation will reject valid messages.How can I help you explore Laravel packages today?