I stopped trusting deploy checks that only say "email arrived" a while ago. In real CI/CD pipelines, that line hides too much: which deploy triggered it, which inbox caught it, whether the message was delayed, and whether someone retried the flow from a laptop five minutes later. When the evidence is fuzzy, teams argue instead of shipping.
What worked better for us was treating email verification like any other release artifact. The deploy creates one isolated mailbox target, SES emits delivery events, SQS collects the receipts, and the pipeline decides from that record instead of from inbox vibes. It is not flashy, but it is very calm in production, which is what I want at 3 AM.
Why inbox-only deploy checks keep lying
A plain inbox poll is fine for local testing. It gets sketchy once multiple branches, retries, and manual checks overlap. One message shows up and everybody assumes the current release is safe, but the message may belong to the previous run. I have seen teams keep weird notes like temp org mail or tepm mail com in runbooks just to remember how they tested something last week. That is a smell, not a workflow.
The bigger issue is provenance. If your deploy gate cannot answer these three questions fast, it is too weak:
- Which release produced the email?
- Which recipient alias was expected?
- Which event proved delivery or failure?
This is also where email sandbox reviews matter. Shared inboxes become shared evidence, and shared evidence gets messy pretty quick.
The AWS pattern that finally held up
The pattern is small:
- Generate a release-scoped alias before traffic shifts.
- Send the verification email through SES using that alias.
- Subscribe SES event notifications to SQS.
- Have the pipeline wait for a matching receipt with release metadata.
- Fail the deploy if the receipt never arrives or does not match the expected release.
The key detail is that the inbox is not the source of truth anymore. The receipt is. The inbox can still be useful for spot checks or rendering bugs, but the gate uses the structured event. Kinda boring, yes, but boring systems save on-call sleep.
If you need a disposable target for non-production verification, keep it narrow and contextual. I have used temp mail so for throwaway email checks in lower-risk validation steps, but never as a replacement for release metadata. The useful part is isolating the alias per run, not just grabbing any random mailbox.
A small receipt flow with SES and SQS
You do not need a big service here. A small job in the pipeline can poll SQS for a receipt keyed by release ID and recipient alias.
set -euo pipefail
export RELEASE_ID="${GITHUB_SHA::8}-${GITHUB_RUN_ATTEMPT}"
export MAIL_ALIAS="verify+${RELEASE_ID}@example.test"
aws sqs receive-message \
--queue-url "$SES_EVENTS_QUEUE_URL" \
--max-number-of-messages 5 \
--wait-time-seconds 20 \
--message-attribute-names All \
--attribute-names All
The message body or attributes should let you match:
- the destination alias
- the SES message ID
- the deploy or commit identifier
- the event type such as delivered, bounced, or rejected
Once that exists, the gate is simple. The pipeline checks for one delivery receipt from the current release window and moves on. If it sees an old message, the metadata mismatch makes the result useless on purpose. That is good. False confidence is worse than a red build.
I also like keeping a short audit file beside the pipeline logs. That idea lines up with replayable automation logs: when a deploy gate fails, you want enough context to replay the decision without reading 900 lines of console output.
What to store with each receipt
Do not overdesign the schema. A tiny JSON object is enough:
{
"release_id": "4f3b9b2a-1",
"alias": "verify+4f3b9b2a-1@example.test",
"ses_message_id": "010201...",
"event_type": "Delivery",
"observed_at": "2026-08-17T20:21:00Z",
"commit_sha": "4f3b9b2a",
"workflow_run": "1854921"
}
I would also store the trigger step that sent the email, because many flaky checks are not actually mail problems. They are ordering problems. The app was still warming up, the queue worker was delayed, or the wrong environment variable landed in the pod. A receipt record gives you one place to see that, even if the phrasing in the alert is a bit wonky or anoying.
For reliability, keep the alias lifecycle short. Create it for the release, expire it after the window, and never let the next deploy inherit it. That one rule removes a lot of head-scratching. It also makes throwaway email usage less sloppy and a bit more defendable in audits.
Q&A
Why not just poll the inbox directly?
Because inbox polling tells you a message exists, not whether it belongs to this release. SQS receipts make the deploy decision more explainable.
Should the receipt gate block every environment?
No. I usually gate staging and production-adjacent flows, then keep preview environments lighter. If every tiny env blocks on mail proof, teams will bypass it sooner or later.
What if SES is delayed?
Use a clear timeout and report the difference between "no receipt" and "negative receipt." Those are different failures, and mixing them makes triage slower than it needs to be.
Top comments (0)