Email testing in GitHub Actions
Give every workflow run its own inbox, pass the address to the suite as an environment variable, and sweep it up even when the job fails.
How it fits together
The pattern is the same as locally, with two CI-specific details: the key comes from repository secrets, and teardown runs under if: always() because that is exactly when inboxes get left behind.
The workflow
# .github/workflows/e2e.yml
name: e2e
on: [push]
jobs:
test:
runs-on: ubuntu-latest
env:
MAILFLAT_API_KEY: ${{ secrets.MAILFLAT_API_KEY }}
MAILFLAT_API: https://mailflat.net/api/v1
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20 }
- name: Open an inbox for this run
run: |
ADDRESS=$(curl -s -X POST "$MAILFLAT_API/inboxes" \
-H "X-API-Key: $MAILFLAT_API_KEY" \
-H "Content-Type: application/json" \
-d "{\"prefix\":\"ci-${GITHUB_RUN_ID}\",\"retention_hours\":2}" | jq -r .address)
echo "TEST_EMAIL=$ADDRESS" >> "$GITHUB_ENV"
- run: npm ci
- run: npx playwright test
- name: Tear down
if: always()
run: |
curl -s -X DELETE "$MAILFLAT_API/inboxes/$TEST_EMAIL" \
-H "X-API-Key: $MAILFLAT_API_KEY" || true
Or let the suite do it
If your tests already create an inbox per test, the workflow only needs the key.
- run: npx playwright test
env:
MAILFLAT_API_KEY: ${{ secrets.MAILFLAT_API_KEY }}
Worth knowing
retention_hours is the safety net: even if the teardown step never runs, the mail expires on its own.
An account key printed into a public build log grants access to every inbox you own — keep it in secrets.
See also
Testing & CI
Give every CI run its own real inbox: the four API calls, parallel shards, teardown, and how to keep an end-to-end suite fast and honest.
curlEvery endpoint in one terminal. Useful for smoke checks, CI shell steps, and for seeing exactly what your language client sends.
API Keys & AuthAccount keys versus session tokens: which endpoints accept which credential, how to create and rotate keys, and why the wrong pair returns 401.