How do you wait for an email in CI tests?
Replace fixed sleeps with a bounded, filtered wait that can explain why the message did not produce the expected result.
Create a unique inbox or tagged recipient for the CI run, record the time immediately before triggering the application email, then poll with backoff, use a provider wait operation, or consume an event until a message matches the recipient, direction, sender context, type, and time boundary. Stop at a fixed deadline. If several messages match, fail as ambiguous. Extract the required code or link, submit it once, and assert the final application state. Keep mailbox credentials, codes, and message bodies out of general CI logs.
Workflow at a glance
Poll narrowly and stop predictably
- 01Query a small page
Filter mailbox, recipient, direction, sender context, and received time.
- 02Classify the result
Return no candidate, one match, ambiguity, or parse failure.
- 03Back off with jitter
Avoid synchronized load across parallel workers.
- 04Fail at the deadline
Report the last transport and query evidence without leaking content.
Allocate the recipient before the test action
Parallel jobs cannot safely share one inbox and select the newest message. Create a mailbox or unique address tag per run, keep its credential scoped, and record the recipient in test state rather than broad logs.
Capture the trigger timestamp immediately before the signup, reset, invitation, or notification action. This prevents an older matching message from satisfying the test.
Poll narrowly and stop predictably
-
01
Query a small page
Filter mailbox, recipient, direction, sender context, and received time.
-
02
Classify the result
Return no candidate, one match, ambiguity, or parse failure.
-
03
Back off with jitter
Avoid synchronized load across parallel workers.
-
04
Fail at the deadline
Report the last transport and query evidence without leaking content.
Use a purpose-built wait when the task is an OTP
A command such as mails code --to "$TEST_EMAIL" --timeout 60 packages the bounded wait and extraction path. The test should still know which application action triggered the message and should reject a returned value that does not produce verified state.
Do not request a new OTP every time the inbox is empty. Multiple active codes make selection ambiguous and may trigger application rate limits.
mails code --to "$TEST_EMAIL" --timeout 60Report the failed layer without exposing the mailbox
| Result | Meaning | Next check |
|---|---|---|
| No inbound event | Routing or application send may have failed | Application log and provider event |
| Message found, no value | Template or parser mismatch | Redacted fixture and extraction rule |
| Several matches | Isolation or retry problem | Recipient, timestamp, and trigger count |
| Value rejected | Stale code or application rule | Final endpoint and current message selection |
Sources and product scope
Product behavior is verified against the mails0 source and documentation. External comparisons link to official vendor documentation checked on 2026-08-15.
- mails0 source repositoryImplementation, license, and deployment source
- mails0 code extraction documentationBounded verification-code retrieval
Related questions
How long should a CI test wait for email?
Set a deadline from observed delivery behavior and the suite budget. Report elapsed time and last known state; do not wait indefinitely.
Are webhooks better than polling in CI?
Usually a bounded wait is simpler for a short-lived CI process. Use webhooks when the test infrastructure already has a durable event path and needs that behavior under test.
Why did the test use an old verification code?
The query likely lacked a trigger timestamp, unique recipient, or exact filter. Record the boundary before requesting the code and fail if several current candidates remain.
Prove the smallest useful email loop.
Start with one scoped inbox and one expected message. Add durable identity, sending, and operational complexity only when the first loop works.