Blog
How to block disposable email addresses
The honest version: a static list is the weakest of four available checks, it is stale within weeks of publication, and the usual implementation blocks customers you wanted.
VerifyInbox · published 11 September 2026 · 5 min read
Match the domain against a maintained blocklist, then check whether the mailbox actually exists — the two questions are independent and you need both. Refresh the list weekly, because new throwaway domains appear constantly, and decide deliberately what happens on a match rather than defaulting to a hard rejection.
The rest of this page is about the part the how-to articles leave out: why your blocklist keeps missing new domains, what the other three layers are, and which legitimate signups the standard implementation quietly throws away.
Why the blocklist keeps missing new domains
A disposable address provider needs one thing to defeat your list: a domain you have not heard of. Registering one costs a few dollars and pointing it at an existing throwaway service costs nothing, so the supply side of this problem is effectively free and the defence side is a subscription to a list somebody else maintains.
That asymmetry has a specific consequence, and it is worth stating plainly rather than selling around: any static list is out of date the week it ships. Ours included. We vendor 8,714 domains and refresh them weekly, and the honest claim that supports is "this catches the providers people actually use", not "this catches everything".
There is a second failure mode that costs more than the first, and almost every implementation has it. Here is a blocklist check written the way it is usually written, run on Python 3.13:
BLOCKLIST = {"mailinator.com", "guerrillamail.com", "yopmail.com"}
def domain_of(address: str) -> str | None:
_, _, domain = address.rpartition("@")
return domain.strip().rstrip(".").lower() or None
def is_listed(address: str) -> bool:
domain = domain_of(address)
return domain in BLOCKLIST if domain else False
for candidate in [
"[email protected]",
"[email protected]",
"[email protected]",
"[email protected]",
"[email protected]",
]:
print(f"{candidate:36} {is_listed(candidate)}")Output
[email protected] True
[email protected] True
[email protected] False
[email protected] False
[email protected] FalseLine three is the interesting one. mail.mailinator.com is a subdomain of a domain that is on the list, and to an exact-match lookup it is simply a different string, so it passes. Any provider that hands out subdomains defeats the check for free. Matching the registrable domain rather than the literal string is the fix, and it is the single highest-value improvement to a list-based check.
Line four is the one no amount of list maintenance fixes. A domain registered this morning is on nobody’s list this morning.
The four layers, strongest last
| Layer | What it catches | What it misses |
|---|---|---|
| 1. Domain blocklist | The providers people actually use, instantly and offline | Anything registered since the list was built; subdomains, if matched as strings |
| 2. MX fingerprint | New domains pointing at a known throwaway provider’s mail servers | A provider running its own MX per domain |
| 3. Mailbox verification | Whether the address exists at all — independent of whether it is throwaway | Nothing about intent. A burner mailbox that exists is deliverable |
| 4. Behaviour after signup | The accounts that never confirm, never return, never convert | Everything up to that point — it is a measurement, not a gate |
Layer two is the one most implementations skip and it is cheap: resolve the MX records for an unknown domain and compare the mail hosts with those of providers you already know. A throwaway service running a thousand domains off one mail cluster is visible in DNS whatever it calls itself this week. The MX lookup tool shows you the records for any domain.
What the verifier returns, and the rule behind it
Every verification classifies the address as part of the same call, so the disposable answer arrives with the verdict rather than needing a second lookup. The result carries disposable: true, mailboxType: "disposable", and the verdict the mail server actually gave.
That last part is a deliberate rule and it surprises people: a classifier never changes the verdict. A disposable address whose mailbox exists comes back as deliverable with the flag set, because the mail server said the mailbox is there and that is a fact. A disposable address whose mailbox does not exist comes back as undeliverable with reason: mailbox_not_found, because a hard rejection outranks every classifier.
The alternative — folding "disposable" into "invalid" — would be more convenient and less true. It would also make the two signals impossible to separate at the point where you need them separated, which is when you decide what your signup form does next.
The disposable email checker runs both halves with no account, three checks a day per IP.
What to do on a match — and why not always a hard block
The reflex is to reject the signup. For a paid product with a free trial that is usually right. For a free product it throws away people you wanted, and three groups in particular:
- People using a burner for privacy rather than abuse. Signing up to a newsletter with a throwaway address is a reasonable thing for a careful person to do, and it is not fraud.
- People using an alias service, which is not the same thing as a disposable provider. An alias forwards to a permanent mailbox and lasts as long as the owner wants it to. Some blocklists conflate the two; if yours does, you are blocking privacy-conscious paying customers.
- Plus-addressed and dotted addresses —
jane+signup@,j.a.n.e@. These are not disposable at all, they are one person organising their own inbox, and a surprising number of signup forms reject them outright.
A middle path costs almost nothing to implement and is what we would suggest by default: allow the signup, require a confirmation click before anything is granted, and hold back the expensive parts — a trial extension, a credit grant, an invite quota — until the address has been confirmed. A throwaway mailbox nobody checks fails that quietly, and a real person passes it in ten seconds.
What we do, and what it does not cover
Our own signup runs the same check: 8,714 vendored disposable domains, refreshed weekly from upstream and committed as a diff, applied to both password and OAuth signups. Publishing the number rather than an adjective is the point — it is a fact you can hold us to and it will be different next quarter.
What it does not cover, stated so you do not find out later: a domain registered after the last refresh. A provider running its own mail servers per domain, where the MX fingerprint gives nothing away. And spam traps — we have no spam-trap detection and do not claim any, because a trap address is indistinguishable from a real mailbox at the protocol level. Anyone selling you certainty about traps is selling you a guess with a label on it.
If a disposable domain is missing from the list, tell us — it is a vendored file and adding one is a commit, not a roadmap item.
Terms used on this page
Sources
Primary sources checked 11 September 2026. If something here is out of date, tell us and we will correct it.
More: How to find someone's email address · How to validate an email address in Python · Email validation regex: the pattern and its limits · Gmail, Yahoo and Microsoft sender requirements