How to Detect Catch-All Email Domains in Node and Python
Catch-all (accept-all) domains are the quiet killer of email verification accuracy. On a normal domain, an SMTP server rejects mail for a mailbox that does not exist - so if it accepts, the mailbox is probably real. On a catch-all domain, the server accepts mail for every address, existent or not. That single fact breaks the core assumption most verifiers rely on. Here is how to detect catch-all domains yourself, and how to skip the plumbing when you just want the answer.
The idea in one sentence
Ask the mail server to accept a random address that almost certainly does not exist. If it says yes, the domain is catch-all.
Doing it by hand (Node.js)
The check is a short SMTP conversation: resolve the domain's highest-priority MX host, connect on port 25, and issue a RCPT TO for a guaranteed-nonexistent mailbox.
import dns from 'node:dns/promises'
import net from 'node:net'
async function isCatchAll(domain) {
const mx = (await dns.resolveMx(domain))
.sort((a, b) => a.priority - b.priority)[0]?.exchange
if (!mx) return { catchAll: null, reason: 'no MX' }
const random = `verify-probe-${Math.random().toString(36).slice(2)}@${domain}`
return new Promise((resolve) => {
const socket = net.createConnection(25, mx)
let step = 0
socket.setTimeout(8000)
socket.on('data', (buf) => {
const code = parseInt(buf.toString().slice(0, 3), 10)
if (step === 0) {
socket.write(`HELO verify.local\r\n`); step++
} else if (step === 1) {
socket.write(`MAIL FROM:<probe@verify.local>\r\n`); step++
} else if (step === 2) {
socket.write(`RCPT TO:<${random}>\r\n`); step++
} else {
socket.write('QUIT\r\n'); socket.end()
resolve({ catchAll: code === 250, rcptCode: code })
}
})
socket.on('timeout', () => {
socket.destroy()
resolve({ catchAll: null, reason: 'timeout / port 25 blocked' })
})
socket.on('error', () => resolve({ catchAll: null, reason: 'connection error' }))
})
}
A 250 on the random address means the server accepts everything - catch-all. A 550 means it rejects unknowns, so acceptance of a real address on that domain is meaningful.
Why this is annoying in production
The technique is correct but operationally painful:
- Most networks block outbound port 25. Cloud providers (AWS, GCP, most PaaS) close it by default, so your probe never connects and every result comes back inconclusive.
- Greylisting and rate limits make single probes flaky; you need retries and backoff.
- Your IP reputation matters - hammering MX servers from one address gets you throttled or blocklisted.
- Catch-all is only one signal. You still need syntax, MX, disposable, and role-account checks to make a send decision.
Skipping the plumbing
If you just want a reliable answer, call an API that already runs the probe from reputation-managed infrastructure. Here is the same check with Verifly:
import requests
r = requests.get(
"https://verifly.email/api/v1/verify",
params={"email": "someone@example.com"},
headers={"Authorization": "Bearer vf_your_api_key"},
)
data = r.json()
print(data["details"]["is_catch_all"]) # True / False
print(data["result"]) # deliverable / risky / undeliverable
const res = await fetch(
`https://verifly.email/api/v1/verify?email=someone@example.com`,
{
headers: { Authorization: 'Bearer vf_your_api_key' },
}
)
const { details, result } = await res.json()
console.log(details.is_catch_all, result)
One call returns the catch-all flag alongside syntax, MX, disposable, and role signals, so you get a full risk picture instead of stitching four checks together. It is pay-as-you-go (100 free credits, no monthly fee), which suits the bursty way verification actually gets used.
You can also try any domain in the browser with the free catch-all email verifier - no signup - or read the deeper explainer on what a catch-all email is.
How to treat catch-all addresses
Do not throw them away and do not trust them blindly. Treat catch-all as a middle tier: riskier than a confirmed mailbox, far safer than a hard-invalid or disposable one. For cold outreach, throttle or exclude them; for warm, opted-in contacts, send normally. Segmenting them separately is what gives you that control.
Disclosure: I maintain Verifly. The hand-rolled Node example above works on its own with no dependency on the API.
Comments
No comments yet. Start the discussion.