<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: MailValid.io</title>
    <description>The latest articles on DEV Community by MailValid.io (@mailvalid).</description>
    <link>https://dev.to/mailvalid</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4024408%2Fa6685845-84b9-4c7d-a301-94e8b76fed98.png</url>
      <title>DEV Community: MailValid.io</title>
      <link>https://dev.to/mailvalid</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mailvalid"/>
    <language>en</language>
    <item>
      <title>Your Email List Is Decaying Right Now</title>
      <dc:creator>MailValid.io</dc:creator>
      <pubDate>Sun, 12 Jul 2026 18:11:13 +0000</pubDate>
      <link>https://dev.to/mailvalid/your-email-list-is-decaying-right-now-2cph</link>
      <guid>https://dev.to/mailvalid/your-email-list-is-decaying-right-now-2cph</guid>
      <description>&lt;p&gt;Verifying an email address at signup only proves it was valid at that moment. It says nothing about whether it's still valid six months later and for most contact databases, a meaningful share of it won't be. If your app verifies once at signup and never again, you're shipping a slow leak, not a fix.&lt;/p&gt;

&lt;p&gt;This post covers why lists decay even without a single unsubscribe, what a re-verification job should actually check, and a basic cron-based implementation for keeping a contacts table clean over time.&lt;/p&gt;

&lt;h2&gt;Why valid-at-signup doesn't mean valid-forever&lt;/h2&gt;

&lt;p&gt;Email addresses have a shelf life, and &lt;a href="https://mailvalid.io/" rel="noopener noreferrer"&gt;email verification&lt;/a&gt; once at collection time only confirms the address was live on that specific day. Roughly 22.5% of email contacts become invalid over the course of a year — around 2.1% per month: driven by job changes, account deactivation, provider migrations, and abandoned inboxes. B2B contact lists decay faster than consumer lists, commonly in the 25–30% annual range, largely because employees change jobs and their work addresses get deactivated almost immediately.&lt;/p&gt;

&lt;p&gt;If your database has contacts older than a few months and you've never re-verified, you're very likely sending to a nontrivial number of dead addresses without knowing it.&lt;/p&gt;

&lt;h2&gt;What causes Email list decay&lt;/h2&gt;

&lt;p&gt;Email list decay is driven by five recurring patterns, and only one of them is under your control.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Job changes — the single biggest driver for B2B lists; an estimated 70% of B2B job-related email addresses change within 12 months of collection.&lt;/li&gt;
&lt;li&gt;Company changes — mergers, acquisitions, rebrands, or shutdowns can invalidate an entire domain's worth of addresses at once.&lt;/li&gt;
&lt;li&gt;Provider migrations — a contact moves from one email provider to another and abandons the old address.&lt;/li&gt;
&lt;li&gt;Account deactivation — mailbox providers periodically purge inactive accounts per their own retention policies.&lt;/li&gt;
&lt;li&gt;Signup typos — the one decay source that real-time syntax/MX verification at signup actually prevents.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The first four happen entirely outside your app, on a timeline you don't control, which is exactly why a one-time verification at signup can't be the whole strategy.&lt;/p&gt;

&lt;h2&gt;Email Spam traps: the risk re-verification catches that bounces don't&lt;/h2&gt;

&lt;p&gt;A recycled spam trap is a real address that mailbox providers have reclaimed after roughly 12 months of inactivity, then repurposed specifically to catch senders with stale lists. This is the mechanism that makes re-verification more than a bounce-rate optimization — it's a reputation risk mitigation.&lt;/p&gt;

&lt;p&gt;There are three recognized spam trap types:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Typo traps - created from signup typos (&lt;a href="mailto:user@yahooo.com"&gt;user@yahooo.com&lt;/a&gt;), catching senders who never validated syntax carefully.&lt;/li&gt;
&lt;li&gt;Recycled traps - formerly legitimate addresses that went dormant for a year or more and were reclaimed by the provider as a monitoring mechanism.&lt;/li&gt;
&lt;li&gt;Pristine traps - addresses that were never real signups at all, created solely to catch senders using purchased or scraped lists.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Recycled traps are the one your own signup-time verification can't catch, because the address was real and valid when the person signed up. It only becomes a trap after months of dormancy — which is precisely the gap a scheduled re-verification job is designed to close.&lt;/p&gt;

&lt;h2&gt;Designing the re-verification job&lt;/h2&gt;

&lt;p&gt;A re-verification job needs three things a one-off signup check doesn't:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;A cadence - how often each contact gets re-checked (see the how-often section)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Prioritization - verifying your entire table every night at scale is wasteful; segment by last-engagement date or last-verified date and check the stalest/riskiest contacts first&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A status write-back, not a delete — store the verification result (valid, invalid, catch-all, disposable, unknown) and last-checked timestamp rather than silently dropping rows, so your sending logic can make the suppress/send decision explicitly&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;A scheduled job example (Node.js + cron)&lt;/h3&gt;

&lt;p&gt;javascriptconst cron = require('node-cron');&lt;br&gt;
const db = require('./db');&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Runs nightly at 2am — pulls contacts not verified in 90+ days,&lt;/span&gt;
&lt;span class="c1"&gt;// oldest-first, capped at a batch size to respect rate limits.&lt;/span&gt;
&lt;span class="nx"&gt;cron&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;schedule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0 2 * * *&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;staleContacts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`
    SELECT id, email FROM contacts
    WHERE last_verified_at &amp;lt; NOW() - INTERVAL '90 days'
       OR last_verified_at IS NULL
    ORDER BY last_verified_at ASC NULLS FIRST
    LIMIT 5000
  `&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;contact&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;staleContacts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;verifyEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;contact&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// your verification call&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`
      UPDATE contacts
      SET verification_status = $1, last_verified_at = NOW()
      WHERE id = $2
    `&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;contact&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;invalid&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`UPDATE contacts SET suppressed = true WHERE id = $1`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;contact&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The batch size and interval here are illustrative, tune them to your sending volume and your verification provider's rate limits. The important structural choice is the last_verified_at column: it turns re-verification into an ongoing, queryable process instead of a one-time event.&lt;/p&gt;

&lt;h2&gt;How often should you re-verify?&lt;/h2&gt;

&lt;p&gt;There's no universal answer, but the decay data gives a reasonable default: at roughly 2% monthly decay, a 90-day re-verification cycle catches most staleness before it compounds into a meaningful bounce or spam-trap risk, without re-checking addresses that haven't had time to go stale. High-value B2B lists with faster job-change turnover may justify a 30–60 day cycle; low-churn consumer lists can often stretch to 6 months.&lt;/p&gt;

&lt;p&gt;If you're re-verifying manually or building this in-house, an &lt;a href="https://mailvalid.io/docs#bulk-verification" rel="noopener noreferrer"&gt;email verification API&lt;/a&gt; that returns a distinct catch-all status (rather than lumping it in with valid) matters more here than at signup time, a domain's catch-all configuration can change between checks, and treating it as a hard valid inflates your list-quality metrics without actually reducing risk. MailValid supports exactly this: bulk re-verification with status codes designed for exactly this kind of scheduled job, plus a real-time API for the signup-time check.&lt;/p&gt;

&lt;h3&gt;Frequently Asked Questions&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Does re-verification replace signup-time verification?&lt;/strong&gt;&lt;br&gt;
No, they catch different things. Signup-time verification (syntax + MX + SMTP) prevents typo'd and fake addresses from entering your database at all. Re-verification catches addresses that were genuinely valid at signup but have since gone stale.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Will re-verification hurt my sender reputation the way sending mail does?&lt;/strong&gt;&lt;br&gt;
No. SMTP-level verification checks mailbox existence via a RCPT TO command without completing delivery (no DATA command is sent), so it doesn't count as a send and doesn't itself generate spam complaints, provided it's run through a provider with dedicated verification IP infrastructure rather than your own sending domain.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Should I delete contacts that fail re-verification?&lt;/strong&gt;&lt;br&gt;
Suppressing them from sends is safer than deleting outright, a suppressed contact might resubscribe, correct a domain-level issue, or represent a data point you still want for reporting. Deletion is a business decision, not a deliverability requirement.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What if a contact I haven't emailed in a year suddenly becomes a spam trap?&lt;/strong&gt;&lt;br&gt;
This is exactly the recycled-trap scenario. If your last engagement with an address predates your last verification by many months, treat it as higher risk and prioritize it in your re-verification queue rather than assuming dormancy is harmless.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>database</category>
      <category>api</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How to Verify Email Addresses Programmatically: SMTP, MX, and API-Based Validation Explained</title>
      <dc:creator>MailValid.io</dc:creator>
      <pubDate>Fri, 10 Jul 2026 19:48:53 +0000</pubDate>
      <link>https://dev.to/mailvalid/how-to-verify-email-addresses-programmatically-smtp-mx-and-api-based-validation-explained-5b3a</link>
      <guid>https://dev.to/mailvalid/how-to-verify-email-addresses-programmatically-smtp-mx-and-api-based-validation-explained-5b3a</guid>
      <description>&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;If you're building a signup flow, a CRM integration, or a bulk import pipeline, "&lt;a href="https://mailvalid.io/" rel="noopener noreferrer"&gt;email verification&lt;/a&gt;" 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why regex alone isn't verification
&lt;/h2&gt;

&lt;p&gt;A regex pattern confirms an address is formatted correctly, it says nothing about whether the mailbox exists. &lt;a href="mailto:john@company-that-does-not-exist.zzz"&gt;john@company-that-does-not-exist.zzz&lt;/a&gt; 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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 1: Syntax validation
&lt;/h3&gt;

&lt;p&gt;Syntax validation confirms the address follows RFC 5321/5322 structure — &lt;a href="mailto:local-part@domain.tld"&gt;local-part@domain.tld&lt;/a&gt; — 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@, &lt;a class="mentioned-user" href="https://dev.to/domain"&gt;@domain&lt;/a&gt;.com, missing TLD) before you spend a network round-trip on it.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 2: MX record / domain validation
&lt;/h3&gt;

&lt;p&gt;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.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;dig MX gmail.com +short
&lt;/span&gt;&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;10 alt1.gmail-smtp-in.l.google.com.
&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;5 gmail-smtp-in.l.google.com.
&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 3: SMTP handshake verification
&lt;/h3&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;The sequence looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Connect to the domain's MX server on port 25&lt;/li&gt;
&lt;li&gt;HELO/EHLO handshake to identify your sending server&lt;/li&gt;
&lt;li&gt;MAIL FROM: &lt;a href="mailto:verify@yourdomain.com"&gt;verify@yourdomain.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;RCPT TO: &lt;a href="mailto:target@theirdomain.com"&gt;target@theirdomain.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Read the response code - 250 means the mailbox accepts mail, 550 means it doesn't&lt;/li&gt;
&lt;li&gt;QUIT without sending DATA (so no message is actually delivered)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;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 &lt;a href="https://mailvalid.io/docs#single-verification" rel="noopener noreferrer"&gt;email verification API&lt;/a&gt; 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Catch-all domains and why they break SMTP checks
&lt;/h2&gt;

&lt;p&gt;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 &lt;a href="mailto:xyz123nonsense@company.com"&gt;xyz123nonsense@company.com&lt;/a&gt; returns the same 250 OK as &lt;a href="mailto:ceo@company.com"&gt;ceo@company.com&lt;/a&gt;, the server is configured to accept everything and reject nothing at the mailbox level.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build vs. buy: when an email verification API makes sense
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

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

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  A minimal Node.js example
&lt;/h2&gt;

&lt;p&gt;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:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;
&lt;span class="nx"&gt;javascriptconst&lt;/span&gt; &lt;span class="nx"&gt;dns&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dns&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;promises&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;EMAIL_REGEX&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/^&lt;/span&gt;&lt;span class="se"&gt;[^\s&lt;/span&gt;&lt;span class="sr"&gt;@&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;+@&lt;/span&gt;&lt;span class="se"&gt;[^\s&lt;/span&gt;&lt;span class="sr"&gt;@&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;\.[^\s&lt;/span&gt;&lt;span class="sr"&gt;@&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;+$/&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;validateSyntax&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;EMAIL_REGEX&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;validateMX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;domain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;records&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;dns&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolveMx&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;records&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;records&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// no MX record, or domain doesn't resolve&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;verifyEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;validateSyntax&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;valid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;syntax&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;validateMX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;valid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;no_mx_record&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

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

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;valid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;passed_syntax_and_mx&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;/p&gt;

&lt;h3&gt;
  
  
  Frequently Asked Questions
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Does email verification guarantee zero bounces?&lt;/strong&gt;&lt;br&gt;
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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is SMTP verification the same as sending a test email?&lt;/strong&gt;&lt;br&gt;
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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why do some verification tools mark valid-looking emails as "unknown"?&lt;/strong&gt;&lt;br&gt;
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.&lt;/p&gt;

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

</description>
      <category>webdev</category>
      <category>api</category>
      <category>smtp</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
