DEV Community

james-sib
james-sib

Posted on • Originally published at verifly.email

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' }))
  })
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
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)
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (1)

Collapse
 
merbayerp profile image
Mustafa ERBAY

Nice write-up, and I appreciate the disclosure at the end. The Node example is a good way to explain the underlying idea instead of treating email verification as a black box.

One thing I’d be a bit more careful about is the implication that a single RCPT TO response is enough to classify a domain as catch-all. In practice, some mail servers defer recipient validation, tarp it, or intentionally behave differently for anti-abuse reasons. So I’d treat it as a strong signal rather than definitive proof.

Still, I like that you explain the trade-offs instead of pretending email verification is an exact science.