iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

Emails (and pitfalls)

Validating email addresses with a regex is a famous trap. The RFC 5321 / 5322 grammar is huge and weird; almost no production regex captures it exactly. The right answer: a SIMPLE regex for shape, a DNS lookup for deliverability, and a confirmation email for proof of control.

Practical regex + DNS + confirmation

EXAMPLE
// 1) The full RFC grammar is impractical
// RFC 5321/5322 allows quoted local-parts, comments inside addresses,
// internationalised domains, IP-literal hosts, and more.
// The 'official' regex is ~6300 chars and still misses corner cases.
//
// You almost never want to validate RFC 5322 exactly. You want to confirm:
//   1) the SHAPE looks plausibly like an address
//   2) the DOMAIN exists and has MX (or A) records
//   3) the recipient confirms control via a magic-link email

// 2) A pragmatic shape check
const EMAIL = /^[\w.!#$%&'*+\/=?^`{|}~\-]+@(?:[A-Za-z0-9](?:[A-Za-z0-9\-]{0,61}[A-Za-z0-9])?\.)+[A-Za-z]{2,}$/;

function validShape(s) {
    if (typeof s !== 'string') return false;
    if (s.length > 320) return false;        // RFC max
    if (s.split('@').length !== 2) return false;
    return EMAIL.test(s);
}

validShape('mara@example.com');      // true
validShape('mara+tag@example.co');    // true
validShape('mara@localhost');          // false (no TLD)
validShape('mara@@example.com');       // false
validShape('mara@ex_ample.com');       // false (underscore not allowed in domain labels)

// 3) Why a tighter regex is risky
// • Disallows valid addresses with tags ($+$ in local-part)
// • Disallows valid TLDs (over 1500 registered)
// • Disallows internationalised email addresses (mara@münchen.de) — needs Unicode awareness
// • Disallows quoted local-parts ("john.doe"@example.com — rare but valid)
//
// Be conservative; reject obvious garbage, let the rest through to deliverability check.

// 4) Internationalised addresses (RFC 6531)
// Add Unicode letter / digit classes if you support EAI:
const EAI_EMAIL = new RegExp(
    "^[\\p{L}\\p{N}.!#\$%&'*+/=?^`{|}~\\-]+@(?:[\\p{L}\\p{N}](?:[\\p{L}\\p{N}\\-]{0,61}[\\p{L}\\p{N}])?\\.)+[\\p{L}]{2,}$",
    'u',
);
EAI_EMAIL.test('user@münchen.de');     // true (with /u flag and \\p{...})

// 5) Server-side library — preferred
import { validate } from 'email-validator-pkg';   // hypothetical; pick a maintained one
await validate('mara@example.com', { checkMx: true });

// In production, use:
//   Node: validator.isEmail (basic), or @sideway/address, or email-validator
//   Python: email-validator (PyPI, syntactically + DNS check)
//   PHP: filter_var($email, FILTER_VALIDATE_EMAIL)
//   Ruby: ActiveSupport's email regex or rfc6555 gem
//   Rust: validator crate

// 6) DNS check — does the domain exist?
import { promises as dns } from 'node:dns';

async function domainHasMx(domain) {
    try {
        const mx = await dns.resolveMx(domain);
        if (mx.length) return true;
    } catch (e) { /* fall through */ }
    try {
        const a = await dns.resolve4(domain);
        return a.length > 0;                                // RFC fallback: A record OK
    } catch (e) {
        return false;
    }
}

await domainHasMx('example.com');     // true on the public internet

// 7) Disposable email blocklist
// • Stops most throwaway providers (10minutemail, guerrillamail, mailinator)
// • Maintained lists on GitHub (e.g. disposable-email-domains)
// • Use for signups where you want sticky users; allow legit subdomains explicitly

import { isDisposableEmail } from 'disposable-email-domains-js';
isDisposableEmail('mara@mailinator.com');

// 8) Plus-tag handling
// Some apps strip '+tag' before storing the address; others preserve it.
// • Gmail: 'user+tag@gmail.com' delivers to user@gmail.com
// • Many providers also normalise dots
// • If storing for dedup, normalise: lowercase + strip plus-tag + strip dots (Gmail-specific only)

function normaliseEmail(s) {
    const [local, domain] = s.toLowerCase().split('@');
    let l = local.split('+')[0];
    if (domain === 'gmail.com' || domain === 'googlemail.com') l = l.replace(/\./g, '');
    return `${l}@${domain}`;
}
normaliseEmail('Mara.A+tag@gmail.com');    // 'maraa@gmail.com'

// Be careful — don't normalise in transit; only for dedup, not for sending.

// 9) Length limits
// RFC 5321: max 320 chars total. Local-part: 64. Domain: 255. Enforce:
function validLength(email) {
    if (email.length > 320) return false;
    const [local, domain] = email.split('@');
    return local.length <= 64 && domain.length <= 255;
}

// 10) Confirmation email — the only real proof
// Even the perfect regex + DNS lookup doesn't prove the user controls the inbox.
// Send a magic link / code with short TTL; require click-through before granting access.
import crypto from 'node:crypto';

async function sendConfirmation(email) {
    const token = crypto.randomBytes(32).toString('base64url');
    await db.confirmations.insert({ email, token, expiresAt: Date.now() + 24 * 60 * 60 * 1000 });
    await mailer.send({
        to: email,
        subject: 'Confirm your email',
        body: `Click to confirm: https://example.com/confirm?token=${token}`,
    });
}

// 11) Common email patterns to handle (or reject explicitly)
//   • Plus tags:        valid
//   • Dots in local:    valid (Gmail folds them, others don't)
//   • UPPERCASE:        case-insensitive in the domain, but the local-part is technically case-sensitive (rarely used)
//   • Quoted local:     "hello world"@example.com — valid but rejected by many APIs
//   • IP literal:       user@[192.0.2.1] — valid; usually rejected for UX reasons
//   • IDN domain:       user@münchen.de — valid in EAI; convert to Punycode for DNS
//   • Subaddressing:    user.name+filter@example.com — same as plus tag

// 12) Avoid these regex anti-patterns
//   • /^.+@.+\..+$/         — matches '@.@.' and other garbage
//   • Long lists of TLDs    — always wrong as ICANN adds new ones
//   • Anchoring with /m flag — multi-line matching makes injection easier
//   • Greedy quantifiers without anchors — backtracking risk on adversarial input

// 13) Performance + DOS
//   • Catastrophic backtracking — keep the regex linear; avoid (a+)+ style patterns
//   • Cap input length BEFORE running the regex (e.g. 320 chars max for emails)
//   • For untrusted input at scale, use RE2 (Go, Rust) — guaranteed linear runtime
//   • Run a unit test with property-based fuzzing (fast-check, hypothesis) on the validator

// 14) UX checklist
//   • Validate on blur, not on every keystroke
//   • Show actionable errors ('Did you mean gmail.com?') — use a typo-correction library (mailcheck.js)
//   • Allow paste; trim whitespace; lowercase by default; preserve original on display
//   • Don't tell the user the email DOESN'T exist (privacy) — say 'we'll send a code if it's registered'
//   • Always re-verify on email change (re-send confirmation)

// 15) Common bugs
//   • Overly strict regex rejects legit addresses (Gmail with dots, .au TLD when only [a-z]{2,3} accepted)
//   • Mixing case-sensitive comparison in lookups — normalise to lowercase before storing
//   • Forgetting subdomain support in domain part (sub.example.com)
//   • Trusting client-side validation only — always re-check server side
//   • Confirming via 'click here' that uses GET — pre-fetchers may auto-confirm; use POST + token
//   • Letting the regex itself become an injection surface — never interpolate user-controlled regex fragments

Why it matters

Don’t try to validate RFC 5322 with a regex — use a forgiving shape check, then a DNS MX lookup, then a confirmation email. That three-step pattern handles internationalised addresses, future TLDs, and the long tail of valid quirks while still keeping obvious garbage out, and it produces the only signal that actually matters: the recipient controls the inbox.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
// Pragmatic email check (not RFC-perfect — that's almost impossible by regex):
const basic = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
// For high-stakes use, prefer sending a verification email instead of stricter regex.
Try it Yourself »

Discussion

Loading…