Regex-only email validation lets addresses with non-existent domains into your database — contacts that will hard-bounce on every send, dragging down your sender reputation and triggering ISP throttling. CWE-20 (Improper Input Validation) applies directly: accepting structurally plausible but deliverability-dead addresses is a data integrity failure that accumulates silently. A domain with no MX record cannot receive mail; sending to it raises your bounce rate above the 2% threshold that ESP providers use to flag accounts for review or suspension. The cost is real: recovery from a suspended sender account typically requires list pruning, a warm-up period, and manual outreach to the ESP — days of lost campaign throughput.
Critical because a single ingest path without MX verification poisons the entire list with undeliverable addresses, compounding with each import until bounce rates trigger ESP account action.
Add MX verification to every email ingest path before the address is persisted. The Node.js built-in dns/promises module requires no extra dependencies:
import dns from 'dns/promises'
async function hasValidMx(email: string): Promise<boolean> {
const domain = email.split('@')[1]
if (!domain) return false
try {
const records = await dns.resolveMx(domain)
return records.length > 0
} catch {
return false // NXDOMAIN or SERVFAIL
}
}
// Reject before storage:
if (!(await hasValidMx(email))) {
return res.status(422).json({ error: 'Email domain cannot receive mail' })
}
Cache results per domain with a 6-hour TTL (see ab-000857) to avoid redundant DNS round-trips on repeated submissions.
ID: data-quality-list-hygiene.email-validation.mx-record-verification
Severity: critical
What to look for: Enumerate all code paths where a new email address enters the system (signup form handler, API ingest endpoint, import processor, CSV upload handler). Count every ingest path found. For each, look for DNS MX record lookups — either via a library like dns.promises.resolveMx() in Node.js, an external validation API call, or a dedicated validation service. Quote the actual function or method name that performs the MX lookup. A regex-only check does not count as pass — do not pass if MX verification is absent from any ingest path.
Pass criteria: Count all email ingest paths and report the ratio: "N of N ingest paths perform MX verification." 100% of ingest paths must perform MX verification — at least 1 ingest path exists and all perform the lookup. Every new email address has its domain's MX record resolved before the contact is persisted. Addresses with no valid MX record are rejected or flagged as undeliverable before storage. Report the count of ingest paths even on pass.
Fail criteria: Email validation relies only on format/regex checks without verifying that the domain can receive mail. Or MX lookup is performed asynchronously after storage without a rejection path for failures.
Skip (N/A) when: The system never stores email addresses (purely transactional, email comes from external auth provider with no first-party list).
Detail on fail: Describe what validation is performed. Example: "Signup handler validates email format with regex only — no DNS MX lookup performed" or "MX lookup is scheduled async after storage but invalid domains are never removed or flagged"
Cross-reference: Check data-quality-list-hygiene.email-validation.validation-cache — MX results should be cached to avoid redundant DNS lookups on repeated submissions for the same domain.
Remediation: Add MX record lookup to the ingest path before persisting the address:
import dns from 'dns/promises'
async function hasValidMx(email: string): Promise<boolean> {
const domain = email.split('@')[1]
if (!domain) return false
try {
const records = await dns.resolveMx(domain)
return records.length > 0
} catch {
return false // NXDOMAIN or SERVFAIL — domain cannot receive mail
}
}
// In your ingest handler:
if (!(await hasValidMx(email))) {
return res.status(422).json({ error: 'Email domain cannot receive mail' })
}
Cache results to avoid re-checking the same domain on every submission. A TTL of 1-24 hours is typical. For high-volume imports, batch the lookups and process failures in a second pass rather than blocking each row individually.