July 18, 2026
How to Build Reliable Email Verification Tests with Mailgun, IMAP Polling, and Browser Automation
Learn how to build email verification tests with IMAP, Mailgun test inboxes, and browser automation, including polling logic, wait strategies, and maintenance tradeoffs.
Signup and password reset flows look simple until you try to automate them end to end. The application sends an email, the email arrives asynchronously, the verification link expires, and the browser session may already be half-forgotten by the time the mailbox check succeeds. That gap between “the app sent it” and “the test can prove it arrived and worked” is where many suites become flaky.
Teams usually end up with one of three patterns: mocking the mail layer too aggressively, polling a mailbox with brittle timing, or giving up and covering email flows manually. None of those is ideal if the workflow matters to revenue, account recovery, or compliance. The practical middle ground is to build email verification tests with IMAP, use a disposable inbox strategy for test messages, and connect the mailbox step to browser automation with explicit waits and tight assertions.
This article walks through that implementation path, using Mailgun as the sending layer example, IMAP polling as the retrieval mechanism, and browser automation as the user-facing driver. It also explains where the maintenance burden grows, which failure modes matter, and when a maintained platform can reduce the amount of plumbing your team owns.
What “reliable” means for email verification tests
A reliable email test does not just confirm that a message was sent. It should answer a narrower set of questions:
- Did the application generate the right email for the right user?
- Did the message arrive in a mailbox that the test can access?
- Did the test extract the right token or link from the message body?
- Did the browser use that token or link successfully?
- Did the app transition to the expected state after verification?
That last step matters. A test that only inspects mailbox contents is still vulnerable to false confidence, because the app may generate a broken link, the token may already be invalid, or the page may reject the callback after a redirect.
The goal is not “email arrived.” The goal is “the full user journey completed using the same mechanism a user would use.”
When teams discuss email verification, they often conflate three different layers:
- the email transport and mailbox retrieval layer,
- the business logic that generates verification content,
- the browser flow that consumes the message.
Separating those layers makes the test design much easier.
Why Mailgun and IMAP often show up together
Mailgun is commonly used for application email delivery, especially when teams want a transactional provider with webhooks, routing rules, and deliverability tooling. Its official documentation covers sending, inbound routing, message parsing, and API behavior. For verification tests, the relevant property is not just that Mailgun can send mail, but that your team can route test messages to an inbox you control or inspect.
IMAP, defined in the RFC 9051 standard, is the mailbox protocol that lets your test code connect to a real inbox and search for messages. That matters because browser-driven email verification tests usually need to work with the actual email content, not a stubbed payload.
A common setup looks like this:
- Your app sends verification email through Mailgun.
- Test users use a mailbox domain or address pattern reserved for automation.
- A polling worker connects over IMAP and watches for the expected message.
- The test extracts the verification URL or OTP from the message body.
- Browser automation opens the URL, submits the token, and validates the post-verification page.
This approach is more realistic than a fake mail service, but it also creates more moving parts. That tradeoff is acceptable when the flow is critical, and the team is willing to own the infrastructure.
A practical architecture for email verification tests
For most teams, the cleanest implementation has four components.
1) A mailbox strategy
Avoid using a shared human inbox. Use one of these instead:
- a dedicated test domain with catch-all mailboxes,
- plus-addressing rules if your provider and application support them,
- unique mailbox aliases per test run or per CI job.
The important property is isolation. If two tests can receive the same message, you will eventually get the wrong email from the wrong run.
A good naming pattern is something like:
verify+<run-id>@test.example.compassword-reset+<job-id>@test.example.com
Using a run ID makes it easier to correlate failures with mailbox contents, and it reduces the chance of picking up stale mail.
2) A polling worker
Email is asynchronous, so your test cannot assume immediate delivery. Polling is usually the simplest reliable approach, provided it has strict bounds and clear diagnostics.
A polling worker should:
- connect to IMAP with TLS,
- search by recipient, subject, sender, or message-id if available,
- ignore stale messages from earlier runs,
- stop after a deadline,
- dump enough metadata for debugging when it times out.
3) A parser for the email body
If you control the email template, make parsing easy. Put the verification link on its own line, and include a stable marker in the subject or body. Regex is acceptable, but the more the parser depends on presentation details, the more breakage you will see from copy changes.
A verification link extractor might look for something like:
- a URL containing
/verify-email?token=..., or - a six-digit OTP formatted in a monospaced block.
If the body is HTML, parse the HTML version rather than trying to scrape raw text from MIME boundaries.
4) Browser automation
Once the test has a token or link, it should continue in the browser with the same session that created the request, or a fresh session if that is how the product behaves.
Playwright is a reasonable example here because it gives you explicit control over browser context, timeouts, and navigation states. Selenium and Cypress can also work, but the underlying pattern is the same: keep browser assertions close to the user-visible result, not just the network response.
Example: polling IMAP for a verification message
The exact IMAP library does not matter as much as the discipline around search criteria and timeout handling. In Python, a polling helper might look like this:
import imaplib
import email
import time
def wait_for_email(host, user, password, subject, timeout=120, interval=5): deadline = time.time() + timeout while time.time() < deadline: with imaplib.IMAP4_SSL(host) as mail: mail.login(user, password) mail.select(‘INBOX’) status, data = mail.search(None, f’(SUBJECT “{subject}”)’) if status == ‘OK’ and data[0]: msg_id = data[0].split()[-1] status, fetched = mail.fetch(msg_id, ‘(RFC822)’) raw = fetched[0][1] return email.message_from_bytes(raw) time.sleep(interval) raise TimeoutError(f’No email with subject {subject!r} found in time’)
This is intentionally minimal. In production, you will likely want to improve it in several ways:
- search by recipient and subject together,
- track UID values instead of message sequence numbers,
- ignore old messages with a
Datecutoff or a run marker, - retry transient login failures separately from the overall wait timeout,
- log mailbox state when the search fails.
A simple helper is enough to prove the flow, but a robust helper needs defensive code around mailbox churn and network instability.
Extracting the verification link or OTP
Once you have the message, the parser should choose the most stable source of truth available. If the email contains both text and HTML parts, prefer the variant that is simplest to parse and least likely to be reformatted by a template engine.
For a verification link, the HTML body may contain an anchor tag like this:
<a href="https://app.example.com/verify?token=abc123">Verify your email</a>
A minimal HTML parser can extract the href, but the test should still assert that the visible text and destination both make sense. If the link is a redirect through a tracking domain, the test may need to follow the redirect chain carefully and assert on the final location.
For OTP-based flows, treat the code as sensitive and short-lived. The test should extract it, enter it quickly, and avoid reusing it after expiry. That means your timeout budget should account for mailbox latency, not just browser time.
If the code expires after 5 minutes, a 4 minute polling window with 1 minute browser retries is not a good test design. The test budget should leave room for the user action, not just message retrieval.
Wiring mailbox polling into browser automation
The browser step is where many suites become either too strict or too vague. The test should check the behavior that matters, without binding itself to unstable DOM details.
A Playwright flow might look like this:
import { test, expect } from '@playwright/test';
test('verifies email after signup', async ({ page }) => {
await page.goto('https://app.example.com/signup');
await page.getByLabel('Email').fill('verify+run-42@test.example.com');
await page.getByRole('button', { name: 'Create account' }).click();
const link = await getVerificationLinkFromMailbox(‘verify+run-42@test.example.com’); const verificationPage = await page.context().newPage(); await verificationPage.goto(link);
await expect(verificationPage.getByText(‘Email verified’)).toBeVisible(); });
In practice, you should consider a few refinements:
- capture the mailbox lookup after the signup is confirmed, not before,
- keep the browser context alive if the verification requires the same session cookies,
- assert on a post-verification condition that users actually observe, such as access to account settings or a changed banner.
If the verification link is one-time use, the test should not open it twice. A surprising number of failures come from debug logging that “helpfully” clicks the same URL again.
Common failure modes and how to reduce them
Stale messages
If your inbox is shared across runs, stale messages are the fastest way to get false positives. Always scope by run ID, timestamp, or unique recipient.
Overly broad mailbox searches
Searching only by subject can match unrelated automation from another suite. Add sender, recipient, and time boundaries when possible.
Polling too aggressively
Very short poll intervals create unnecessary mailbox load and noisy logs. A 3 to 10 second interval is often enough for CI. The exact number depends on message latency and how much parallelism you run.
Parsing presentation instead of content
If a template redesign breaks your parser, the parser is probably too close to layout details. Prefer stable tokens, structured URLs, or explicit markers in the body.
Browser and mailbox timeouts that do not align
A browser test can time out while the mailbox wait still has room left, or vice versa. Tie them to a shared overall budget, then subdivide that budget by step.
Environment drift
Different environments may send from different domains, use different templates, or suppress delivery on sandboxed tenants. Keep environment-specific configuration explicit.
Recommended assertions for verification flows
A useful email verification test usually needs at least these assertions:
- the signup request succeeds,
- the verification email arrives,
- the email subject and sender are correct,
- the verification link or OTP is present,
- the link or OTP works once,
- the app marks the account verified,
- protected pages become accessible afterward.
The assertion should target the user outcome, not just the transport layer.
For example, after clicking the verification link, check for a durable state change such as:
- a profile page showing “verified,”
- the disappearance of a “verify your email” banner,
- access to a feature that previously required verification.
That kind of check catches cases where the email arrived, but the backend callback failed.
Where this architecture becomes expensive to maintain
This setup is effective, but the ownership costs are real.
You are maintaining:
- mailbox credentials and lifecycle management,
- IMAP connectivity and retry behavior,
- parsing logic for one or more message formats,
- browser automation code,
- CI secrets and environment-specific configuration,
- debugging workflows for timeouts and intermittent delivery failures.
The hidden cost is not just code volume. It is also organizational. One engineer may understand how the mailbox search works, another may know the browser session requirements, and a third may own the email templates. When those pieces drift, the test suite becomes a support queue.
A custom implementation makes sense when you need full control, especially if email verification is part of a regulated workflow or you need to inspect message content deeply. But once the team starts adding workarounds for locator churn, mailbox cleanup, message parsing exceptions, and CI-specific race conditions, the maintenance burden can become larger than the original test logic.
How to keep the suite maintainable
A few design decisions pay off quickly:
Use stable test data conventions
Choose a deterministic mailbox naming scheme, and document it. Avoid arbitrary one-off addresses generated by each test author.
Keep email templates machine-friendly
If your team owns the templates, add stable markers for verification links and OTPs. Do not rely on human-readable copy alone.
Separate mailbox retrieval from browser actions
Put IMAP polling in a helper module or service, not inline in every test. That keeps retry rules and diagnostics consistent.
Record enough metadata to debug failures
When a test times out, log the mailbox address, sender expected, subject expected, timestamps, and whether any partial matches were seen. Without that, failures are hard to distinguish from misconfiguration.
Prefer assertions that mirror product behavior
The page title is usually less useful than the account state. The network response is less useful than the verified landing page.
When a maintained platform can be simpler
Some teams eventually decide that the plumbing is the problem, not the verification scenario. That is where a maintained browser testing platform can simplify the stack, especially if it already supports email and SMS flows natively. For example, Endtest provides managed inboxes for receiving messages, parsing them, and continuing the test flow without assembling your own IMAP polling layer. It also offers agentic AI features such as AI Assertions, which can help teams express what should be true in the page or logs without binding every check to a brittle selector.
That does not make custom code obsolete. It does mean some teams can trade mailbox glue code and parser maintenance for platform-managed steps that are easier to review and hand off. If your primary pain is not the verification logic itself, but the upkeep around flaky locators and retrieval plumbing, that is a meaningful simplification.
Choosing between custom plumbing and a maintained platform
A custom Mailgun plus IMAP setup is a good fit when:
- the team needs fine-grained control over delivery and parsing,
- the app uses unusual email formats or token handling,
- email verification is only one part of a broader framework you already own,
- you have the engineering bandwidth to maintain the helper code.
A maintained platform is often a better fit when:
- the team wants to reduce test infrastructure ownership,
- the mailbox and browser steps keep breaking for non-product reasons,
- test authors are spending too much time on retry logic and locator cleanup,
- email verification is a frequent workflow, not an occasional edge case.
The question is not whether IMAP polling is technically valid. It is. The real question is whether your organization wants to keep owning that plumbing over time.
Practical checklist for implementation
Before you write the first test, confirm these items:
- test mailbox naming is isolated by run or job,
- Mailgun routes or sends to a predictable destination,
- IMAP credentials are stored securely in CI,
- polling logic has an upper bound and clear logs,
- the parser handles both text and HTML bodies if needed,
- the browser step asserts the final verified state,
- expired or duplicate emails are filtered out,
- the suite has a cleanup policy for leftover messages and temporary accounts.
If any one of those is missing, the suite can still work, but failures will be harder to diagnose.
Final thoughts
Building email verification tests with IMAP is a solid approach when you need end-to-end confidence across mailbox delivery and browser behavior. Mailgun gives you a realistic sending path, IMAP polling gives you deterministic retrieval, and browser automation proves the user can complete the flow. The technical challenge is not any one step on its own, it is the integration and the maintenance around it.
For teams that want maximum control, the custom route is justified. For teams that mainly want stable coverage of signup, recovery, and notification flows, the long-term cost of mailbox plumbing, parsing code, and retry handling may be higher than it first appears. In that case, a maintained platform can be the simpler operational choice, especially when it reduces the number of moving parts your team has to debug in CI.