DEV Community

Cover image for How to Verify Email Addresses Programmatically: SMTP, MX, and API-Based Validation Explained
MailValid.io
MailValid.io

Posted on

How to Verify Email Addresses Programmatically: SMTP, MX, and API-Based Validation Explained

Bad email addresses in a signup form don't just clutter your database, they wreck deliverability. A high bounce rate signals to mailbox providers like Gmail and Outlook that your sending domain isn't trustworthy, and that reputation hit follows every email you send afterward, not just the bad ones.

If you're building a signup flow, a CRM integration, or a bulk import pipeline, "email verification" usually gets bolted on as a regex check and forgotten. That's a mistake, regex can only tell you an address is syntactically plausible, not that it exists. This post walks through the three layers of email verification, what each one actually checks, and how to decide which one belongs in your stack.

Why regex alone isn't verification

A regex pattern confirms an address is formatted correctly, it says nothing about whether the mailbox exists. john@company-that-does-not-exist.zzz will pass most email regex patterns without complaint. So will a real-looking address at a domain with no mail server configured, or a mailbox that was deleted two years ago.

True verification requires checking three separate things in sequence: syntax, domain, and mailbox existence. Skipping straight from syntax to "trust it" is why forms full of validated-looking emails still produce 8–15% hard bounce rates on the first send.

Layer 1: Syntax validation

Syntax validation confirms the address follows RFC 5321/5322 structure — local-part@domain.tld — before any network call is made. This is the cheapest check and should always run first, client-side or server-side, to reject obviously malformed input (name@, @domain.com, missing TLD) before you spend a network round-trip on it.

A reasonably strict pattern catches the common cases, but don't over-engineer a fully RFC-compliant regex by hand. The spec allows for edge cases (quoted strings, escaped characters) that are rarely worth supporting and easy to get wrong. Use a maintained library or an API's syntax layer instead of a hand-rolled 200-character regex.

Layer 2: MX record / domain validation

MX record validation checks whether the domain after the @ symbol actually has a mail server configured to receive email. This is a DNS lookup, not an SMTP connection, fast, cheap, and it eliminates a large chunk of fake or typo'd domains (gmial.com, company.con) before you go further.

dig MX gmail.com +short
# 10 alt1.gmail-smtp-in.l.google.com.
# 5 gmail-smtp-in.l.google.com.
# ...
Enter fullscreen mode Exit fullscreen mode

No MX record (and no fallback A record) means no mail can be delivered to that domain, full stop — it's a safe automatic reject.

Layer 3: SMTP handshake verification

SMTP verification opens a connection to the recipient's mail server and asks whether a specific mailbox exists, without actually sending a message. This is the layer that catches deleted accounts, typos in the local part (jhon@ vs john@), and mailboxes that were never created, the failures that syntax and MX checks can't see.

The sequence looks like this:

  1. Connect to the domain's MX server on port 25
  2. HELO/EHLO handshake to identify your sending server
  3. MAIL FROM: verify@yourdomain.com
  4. RCPT TO: target@theirdomain.com
  5. Read the response code - 250 means the mailbox accepts mail, 550 means it doesn't
  6. QUIT without sending DATA (so no message is actually delivered)

This is the most accurate layer, and also the most operationally fragile: many mail servers rate-limit or block repeated SMTP probes from unfamiliar IPs, especially if you're running checks at volume from a server with no sending history. That's the practical reason most teams end up using an email verification API rather than opening raw SMTP connections from their own infrastructure. A dedicated service maintains IP reputation and rotation specifically for this, so your app's own sending domain never takes the hit.

Catch-all domains and why they break SMTP checks

A catch-all domain accepts mail to any local part at that domain, so SMTP verification can't distinguish a real mailbox from a nonexistent one. If xyz123nonsense@company.com returns the same 250 OK as ceo@company.com, the server is configured to accept everything and reject nothing at the mailbox level.

This shows up more often than you'd expect on smaller business domains where the mail admin never disabled catch-all routing. A verification pipeline should flag these as a distinct status: catch-all or unknown, rather than lumping them in with confirmed-valid or confirmed-invalid results. Treating a catch-all result as a hard "valid" inflates your list quality numbers without actually reducing bounce risk.

Build vs. buy: when an email verification API makes sense

Running your own SMTP verification at low volume (a signup form doing a handful of checks a day) is fine to build in-house. It stops being fine once you need to verify lists in bulk, because you run into IP reputation limits, greylisting delays, and the maintenance burden of keeping disposable-domain and role-based-address blocklists current.

Self-built SMTP check Verification API
Setup time Hours to days Minutes (API key)
IP reputation risk Your infrastructure absorbs it Provider's dedicated IPs absorb it
Disposable/role-address detection You maintain the list Maintained for you
Catch-all handling Manual logic required Built-in status
Bulk list cleaning (100k+) Slow, rate-limit prone Built for volume
Best for Low-volume, internal tooling Signup forms, bulk list cleaning, CRM sync

If you're past the "quick regex check" stage and need mailbox-level accuracy without babysitting SMTP rate limits, an API like MailValid handles syntax, MX, SMTP, catch-all, and disposable-domain detection behind a single endpoint — useful if you'd rather ship the feature than maintain the verification layer.

A minimal Node.js example

Here's a bare-bones example combining syntax and MX checks (the two layers safe to run yourself), with a placeholder for handing off SMTP-level verification to an API:


javascriptconst dns = require('dns').promises;

const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

async function validateSyntax(email) {
  return EMAIL_REGEX.test(email);
}

async function validateMX(email) {
  const domain = email.split('@')[1];
  try {
    const records = await dns.resolveMx(domain);
    return records && records.length > 0;
  } catch (err) {
    return false; // no MX record, or domain doesn't resolve
  }
}

async function verifyEmail(email) {
  if (!(await validateSyntax(email))) return { valid: false, reason: 'syntax' };
  if (!(await validateMX(email))) return { valid: false, reason: 'no_mx_record' };

  // SMTP-level (mailbox existence) verification is best delegated to an API
  // to avoid IP reputation issues from raw SMTP probing at scale.
  // const result = await mailValidClient.verify(email);

  return { valid: true, reason: 'passed_syntax_and_mx' };
}

Enter fullscreen mode Exit fullscreen mode

This gets you two-thirds of the way there for free. The remaining one-third, mailbox-level confirmation is where the infrastructure tradeoffs above actually matter.

Frequently Asked Questions

Does email verification guarantee zero bounces?
No. Verification confirms a mailbox accepted the address at check time, but mailboxes get deleted, quotas fill up, and spam filters can still soft-bounce a valid address. Verification reduces hard bounces significantly; it doesn't eliminate all bounce risk.

Is SMTP verification the same as sending a test email?
No. SMTP verification stops before the DATA command, so no message is actually delivered or seen by the recipient. It only confirms the server's response to RCPT TO.

Why do some verification tools mark valid-looking emails as "unknown"?
Usually because the domain is a catch-all, the mail server is temporarily unreachable (greylisting), or the server refuses to confirm mailbox existence for anti-harvesting reasons. "Unknown" is a legitimate result, not a failure of the tool.

Can I do this entirely client-side in the browser?
Only the syntax layer. MX and SMTP checks require server-side DNS/network access that browsers don't expose for security reasons.

Top comments (0)