DEV Community

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.