Why a mail provider is the wrong shape for QA
Every way of giving a test suite email addresses has been tried, and each one leaks in a different place:
| Approach | What it costs | Where it breaks |
|---|---|---|
One shared inbox (qa@company.com) | Nothing to set up. | Every test reads every other test’s mail. The first poll of a new run finds last run’s code. Parallel runs are impossible. |
Plus-addressing (qa+run42@company.com) | Nothing to set up, if the provider supports it. | It is still one mailbox: one quota, one IMAP login shared by the whole team, and a sign-up form that strips the tag or refuses the +. |
| A mailbox per tester from the provider | A seat each, and a ticket to create one. | Nobody provisions a mailbox per test run. Testers reuse theirs, and the shared-inbox problem returns one person at a time. |
| A catch-all domain pointed here | One DNS record, once. | The mailbox is public to anyone who knows the address, and mail lives 5 days. Both are fine for a code that matters for eleven seconds; neither is fine for real customer mail. |
The fourth row is this guide. It is the same mechanism the public disposable domains run on — a server that accepts every address on a domain rather than a list of mailboxes — applied to a name nobody but you is using.
The one DNS record
At your DNS provider, add a single MX record on the apex of a domain you own. No other record here, no TXT to prove anything, no account on this site:
MX record for the domain10 smtp.grabmail.io
- Publish the MX. Priority 10, pointing at
smtp.grabmail.io. Delete any other MX on the domain — mail can only be delivered to one place, and a leftover record sends part of it somewhere else. - Wait for DNS. Usually minutes; occasionally the TTL of whatever record was there before.
dig MX qa-example.comshows when it has taken. - Send one message to any address on it. The first delivery is what connects the domain: the server looks the MX up at that moment, sees itself, and accepts. From then on every address on the domain is a mailbox.
A naming scheme that says who made the mailbox
With every address valid, the local part is free to carry information — and at two in the morning, with a failed run and a mailbox open, you will want it to. Three parts, joined by dashes, in this order:
| Part | Example | What it buys you |
|---|---|---|
| What made it | signup, reset, e2e, alice | One glance says which flow or which person the mailbox belongs to. |
| Which run | 1234567890 (the CI run id), a branch name, a date | Every mailbox from one pipeline run shares a token you can search for. |
| Randomness | 3f9a1c2e | Eight characters of it. This is the part that makes two tests, two shards or two retries never share a mailbox. |
| Never: anything real | a customer name, a real user id, a ticket number that names a customer | The mailbox is public to anyone who knows the address. Nothing in the address should be worth knowing. |
// one helper, every runner: who made it, which run, and eight random characters
export const testAddress = (who: string, run = process.env.GITHUB_RUN_ID ?? 'local') =>
`${who}-${run}-${crypto.randomUUID().slice(0, 8)}@qa-example.com`;
testAddress('signup'); // signup-1234567890-3f9a1c2e@qa-example.comReading any address, the same three calls
Nothing about the API changes for your own domain. The same listing call, the same message call, the same delete — no key on the public tier, and the domain in the address is the only thing that differs:
$ curl -sG https://grabmail.io/api/v1/mailbox --data-urlencode "address=signup-1234567890-3f9a1c2e@qa-example.com"Which means every helper on this site works unchanged after one constant is edited: the Playwright fixture, the Cypress task, the Python and Node modules. Three things are worth knowing that the public domains do not make you think about:
- A
404means the MX is not there yet - The listing answers
404for a domain that is not hosted here. On your own domain that is a DNS problem — the record has not propagated, or points elsewhere — not an API one.dig MXfirst. - The alias works on your domain too
- Every mailbox, on any domain, has a second address on a separate domain that delivers into it and cannot read it. Hand that to a site when you would rather it could not open the inbox; poll your own address.
- A busy address is paged
- A catch-all address that collects bounces or a day of notifications can hold more than one page of 200. Pass
nextback asbeforeuntil it isnull.
Keeping a test domain usable
A domain of your own is not on any disposable-mail blocklist, which is often the reason to use one in the first place: the application under test refuses grabmail.io and every other public domain, exactly as it should. Four habits keep it that way:
- Use a dedicated domain —
qa-example.com, not a subdomain of production and not the domain customers write to. The service takes registrable domains only (example.com, nevermail.example.com), and a test domain should have no other job. - Do not publish it. The lists are built from what appears on public temp-mail sites and in shared address dumps. A domain that only ever appears in your own test suite has no way onto them.
- Lock it with SPF and DMARC. A receive-only domain with no SPF is a domain anyone can spoof mail from; two records close that, and cost nothing.
- Do not point real mail at it. The moment a staging system sends customer notifications to a catch-all address, a public mailbox holds customer data. Test domains carry test mail.
If the application refuses all catch-all domains — some fraud checks do, by probing whether a random address on the domain is accepted — there is a paid pool of ordinary-looking .com domains kept off the lists, described on the pricing page. Why sign-up forms block disposable email explains what each kind of check sees.
For a team: one domain, many testers, many pipelines
One catch-all domain serves everyone, because the local part is the only thing that has to differ and it is free. What a team needs is agreement on the naming above and three small conventions:
- A prefix per pipeline and per person
e2e-,nightly-,alice-. Searching a job log for the prefix finds every mailbox it made; searching for a colleague’s finds the bug they are looking at.- Nothing shared, ever
- There is no “team inbox” on the domain and no fixture that hands out a fixed address. If two people need the same mailbox, one of them sends the other the address.
- The rate limits are per client
- One read a second per address, 1200 requests a minute per client — a CI runner is one client, a laptop is another. A team of ten running suites at once is ten clients, not one.
The limits that apply
Your own domain gets the same service as the public ones, with the same ceilings. None of them is adjustable, and none is a problem for a test suite:
| Limit | Value | What it means for QA |
|---|---|---|
| Retention | 5 days per message | Every run creates its own mail; nothing is ever fetched from a previous week. The exact rules. |
| Attachments | 5 MB per message | Enough for an invoice PDF or a CSV export; a larger message is refused at SMTP time, so the sender is told. |
| Reads | 1 per second per address, 1200 per minute per client | Twenty mailboxes polled once a second from one runner. Past it, 429 with Retry-After. |
| Privacy | None — anyone who knows an address can read it | Random local parts, an unpublished domain, and no real customer mail. |
| Sending | None | The domain receives only. Your application sends through its own provider, as in production. |
Before you call it done
- A dedicated registrable domain, with one MX record:
10 smtp.grabmail.io, and no other MX. - One message sent to any address on it, and read back over the API.
- SPF and DMARC published, so nobody can send as the domain.
- A naming helper — who, which run, eight random characters — used by every suite.
- The domain kept in environment configuration, never in code, screenshots or tickets.
- Nothing that sends real customer mail pointed at it.
From here the suites are the ones already written: Playwright with the domain in its fixture, the runner-agnostic discipline, and the GitHub Actions workflow with the domain in a variable.
Questions
Can I use a subdomain, like test.company.com?
No — the service takes registrable domains only (company.com, or company.co.uk), because whoever controls a domain controls every name beneath it and two parties must not be able to hold overlapping halves of one namespace. Register a cheap dedicated domain for testing; it is the better practice anyway.
How long until the MX record works?
As soon as DNS serves it, which is usually minutes. If there was a previous MX, its TTL applies. The domain is connected by the first message that arrives after that — nothing else has to happen, and dig MX tells you when the record is live.
Does it cost anything?
No. Connecting a domain, every address on it and the API are free, with no account. The only paid thing on this site is a pool of domains kept off the disposable-mail blocklists, for people who cannot use a domain of their own.
Can someone else read mail on my domain?
Anyone who knows an address can, exactly as on the public domains. What your own domain changes is guessability: its addresses are on a name nobody else is using. Random local parts and an unpublished domain make guessing impractical; they do not make the mailbox private, and nothing here does.
What happens to mail sent to the domain before I connected it?
Nothing reaches here until the MX points here. Mail sent while the old record was live went to the old server, or bounced; mail sent after DNS switched arrives and connects the domain.
How do I disconnect the domain?
Remove the MX record. New mail stops arriving at once; whatever is already in the mailboxes expires on its own within 5 days. Nothing about the domain is kept after that.
The application refuses my test domain too. What now?
Some fraud checks refuse any domain that accepts a random address — a catch-all probe — rather than checking a list. For those, a domain that behaves like an ordinary mailbox provider is needed, which is what the paid pool provides; why sign-up forms block disposable email explains which check you are facing.


