7 layers to verify an email address

email-probe started with a simple problem: marketing emails were bouncing at 15-20%, costing thousands in ESP fees and damaging sender reputation. Existing verification services were too expensive or unreliable. So I built my own.

Here’s how the 7-layer pipeline works, layer by layer.

Layer 1: Syntax validation (RFC 5322)

The first and fastest check. Is the email address syntactically valid?

RFC 5322 defines the spec. In practice, most invalid addresses are simple: missing @, double dots, invalid characters. But edge cases exist. Quoted local parts, comments in parentheses, Unicode domains.

I started with a regex-based validator. It caught 95% of syntax errors and ran in microseconds. The remaining 5% of edge cases (nested comments, obsolete folding whitespace) I documented as known limitations. Spending weeks implementing full RFC compliance would have been overkill. Those edge cases don’t appear in real marketing lists.

Key insight: early termination. If syntax fails, the pipeline stops. No point checking DNS for notanemail.

Layer 2: DNS / MX record check

Does the domain accept email? If there’s no MX record, there’s no mail server. Simple.

Implementation: Node.js dns.resolveMx(). Async, fast, built-in. Handles priority ordering (lower number = higher priority MX server).

Common failure modes: domains that exist but have no mail configuration (parked domains, landing pages). These accounted for ~8% of bounces on typical lists.

Layer 3: Disposable address detection

Throwaway email services (Mailinator, Guerrilla Mail, 10MinuteMail) are used for signup spam and free-trial abuse. These addresses are technically valid. They receive email. But they’re worthless for long-term engagement.

I maintain a list of ~4000 disposable domains, updated periodically from community-maintained blocklists. The check is a simple set lookup. O(1), sub-millisecond.

The challenge: new disposable services appear constantly. A static list goes stale within months. The fix: automated weekly updates from upstream blocklists plus a manual review queue for domains flagged by the catch-all layer.

Layer 4: Role account detection

info@, admin@, support@, sales@, noreply@ go to distribution lists or are unmonitored. They have high bounce rates and low engagement.

Detection is pattern-matching against a known list of role prefixes. False positives are rare. People don’t name themselves “admin.” The list is small, about 30 patterns, and stable over time.

Layer 5: Typo correction

gmial.com becomes gmail.com. yaho.com becomes yahoo.com. hotnail.com becomes hotmail.com.

This is the “helpful” layer. Instead of rejecting typos, it suggests corrections. Implemented using Levenshtein distance against a list of common email providers. Distance threshold: 2 edits or fewer. Suggestion confidence: high (edit distance 1) vs. medium (edit distance 2).

False positive risk: short domain typos that are actually valid custom domains. Solution: only suggest corrections for known major providers (Gmail, Yahoo, Outlook), never for custom domains.

Layer 6: Catch-all detection

Some domains accept mail for any address (*@example.com). Mail to definitely-not-a-real-user@catchall-domain.com will be accepted by the server but never read by a human.

Detection approach: MX servers that accept all addresses typically don’t expose this in DNS. The reliable method is SMTP verification (Layer 7). But as a heuristic: domains that accept mail for a random 32-character local part are almost certainly catch-alls. This can be tested during the SMTP check.

Catch-all domains are the hardest problem in email verification. The server will tell you “yes, that address exists” even when it doesn’t. The only defense: track engagement over time. If emails to addresses at a domain consistently get zero opens, zero clicks, treat the domain as a catch-all.

Layer 7: Live SMTP verification

The gold standard, and the slowest layer. Connect to the mail server and ask if a specific mailbox exists.

The SMTP conversation:

HELO verify
MAIL FROM: <verifier@email-probe.dev>
RCPT TO: <target@example.com>

If the server responds 250 OK to RCPT TO, the mailbox exists. If it responds 550, it doesn’t. If it responds 450/451/452, the server is rate-limiting or temporarily unavailable. Retry with backoff.

Gotchas:

  • Some servers respond 250 to everything (greylisting). Need to test with a known-invalid address first.
  • SMTP connections are slow (100-500ms per verification). Layer 7 is only reached by addresses that pass all faster checks.
  • Rate limiting is aggressive. You need exponential backoff and connection pooling.

Pipeline architecture

The 7 layers run in sequence with early termination:

Syntax -> DNS/MX -> Disposable -> Role -> Typo -> Catch-all -> SMTP
  |        |          |           |       |         |         |
 early   early      early        early   early     early   verified
 exit    exit       exit         exit    exit      exit    (or not)

Average verification time: ~50ms for invalid addresses (early exit at layers 1-2), ~200ms for valid addresses (all layers including SMTP). Without early termination, every address would take ~300ms. The ~60% time savings lets us process 10x the volume for the same compute cost.

Results

After deploying email-probe at CribStore:

  • Bounce rate: 18% to 0.8% (95.5% reduction)
  • ESP costs: dropped by ~$3,000/month
  • Sender reputation: domain reputation score improved from “poor” to “high” within 2 weeks
  • False positive rate: <0.1% (addresses incorrectly rejected)

The SDK is now published on npm with zero runtime dependencies. Full unit test coverage on every verification layer. Dual ESM/CJS build so it works in any Node.js project.

The key insight wasn’t any individual verification technique. It was the sequential pipeline with early termination. Fast checks first, slow checks last, stop as soon as you know the answer. This pattern applies to any multi-step verification system, not just email.