Most teams automate unit tests, linting, and browser checks, but leave email verification as a manual step performed by a QA engineer before release. That works until someone is busy, the message fails silently, or a template change ships broken.
Moving email checks into the CI/CD pipeline closes that gap. The challenge is doing it deterministically, without a shared inbox and without sending messages to real users.
The Problem with Manual Email Checks
Manual email testing introduces delays and inconsistency. A tester signs up with a personal address, waits, finds the message, and clicks the link. The results are hard to reproduce, and the same address cannot cleanly represent two parallel test runs.
Automation solves these problems but adds new requirements:
- A fresh, isolated inbox per test run
- A way to read messages without human interaction
- Reliable assertions on content and links
- Clean teardown so tests do not interfere with each other
Designing a Deterministic Email Test
A deterministic test controls all inputs and waits only for things it can verify. Instead of sleeping for a fixed period, poll for the expected message with a defined interval and timeout.
The general shape of the test is:
- Provision a unique address for the run
- Trigger the email through the application's own API or UI
- Poll the inbox until the message arrives or the timeout expires
- Assert on subject, key content, and link targets
- Clean up test data
Services such as the Public Inbox and the Public Mailbox API are designed for exactly this: a programmatic address that lives only as long as your run.
A Pipeline Walkthrough
Consider a common signup-verification flow and how it fits into a typical pipeline.
Provisioning a Test Address
Each job should generate a unique address so parallel runs never read each other's mail. A timestamp or build ID makes addresses easy to trace.
Triggering and Waiting
Trigger the email by exercising the real application endpoint. Then poll the mailbox API for a bounded amount of time.
for i in $(seq 1 20); do
body=$(curl -s "$MAILBOX_API/messages?to=$TEST_ADDRESS")
if [ -n "$body" ] && [ "$body" != "[]" ]; then
echo "$body" > message.json
break
fi
sleep 3
done
If the loop exits without a message, the test should fail loudly rather than continue with empty data.
Asserting and Cleaning Up
Parse the message, assert on the verification link, and complete the flow by opening that link. Then delete the test account and message so the next run starts clean.
Wiring It into the Pipeline
Below is a minimal GitHub Actions job that runs an email test after deploying to a preview environment.
name: email-tests
on: [push]
jobs:
verify-email:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm run test:email
env:
APP_URL: ${{ secrets.PREVIEW_URL }}
MAILBOX_API: ${{ secrets.MAILBOX_API }}
The test script handles provisioning and polling internally, so the workflow stays readable and the email logic remains in version control with the rest of the suite.
Handling Flakiness and Secrets
Email tests flake for predictable reasons, and each has a fix.
- Fixed sleep timers: replace with polling and a timeout
- Shared inboxes: use a unique address per run
- Real recipients: always target test addresses in non-production environments
- Credential sprawl: store API keys as pipeline secrets, never in code
- Background processing delays: allow enough timeout for the slowest queue
Also consider running email tests as a separate stage so a slow mail queue does not block fast unit tests, and so failures are easy to identify.
Conclusion
Email testing belongs in the pipeline alongside the rest of your automated checks. With a disposable inbox, a polling strategy, and clear assertions, verification becomes fast and repeatable. Add it as its own stage, keep secrets out of code, and your releases become measurably safer without adding manual work.




