<?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: Maulik Jain</title>
    <description>The latest articles on DEV Community by Maulik Jain (@maulik1807).</description>
    <link>https://dev.to/maulik1807</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3868923%2Fc3839397-fdb8-4848-b928-a10413ac5928.png</url>
      <title>DEV Community: Maulik Jain</title>
      <link>https://dev.to/maulik1807</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/maulik1807"/>
    <language>en</language>
    <item>
      <title>I built an email verification API from scratch</title>
      <dc:creator>Maulik Jain</dc:creator>
      <pubDate>Fri, 10 Apr 2026 16:29:51 +0000</pubDate>
      <link>https://dev.to/maulik1807/i-built-an-email-verification-api-from-scratch-2aca</link>
      <guid>https://dev.to/maulik1807/i-built-an-email-verification-api-from-scratch-2aca</guid>
      <description>&lt;p&gt;Most email verification services are a black box. You send them an address, they send back a result, and you have absolutely no idea what happened in between — or what they did with the data.&lt;/p&gt;

&lt;p&gt;I wanted to understand what "real" email verification actually looks like under the hood, so I built one from scratch in Node.js. No paid third-party APIs. No external dependencies beyond standard DNS and TCP. Open source so anyone can read exactly what it does with their data.&lt;/p&gt;

&lt;p&gt;Here's how it works.&lt;/p&gt;

&lt;p&gt;The Pipeline&lt;br&gt;
Every address goes through up to 7 checks in sequence. The pipeline is fail-fast — if an early check fails definitively, later ones are skipped.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Syntax Validation
Not just a basic regex. Full RFC 5322 compliance — checks local part length, quoted strings, valid special characters, domain format, and TLD presence.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;// src/services/syntaxChecker.js&lt;br&gt;
const RFC5322 = /^[a-zA-Z0-9.!#$%&amp;amp;'&lt;em&gt;+/=?^_`{|}~-]+@&lt;a href="https://dev.to?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9]"&gt;a-zA-Z0-9&lt;/a&gt;?(?:.&lt;a href="https://dev.to?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9]"&gt;a-zA-Z0-9&lt;/a&gt;?)&lt;/em&gt;$/;&lt;/p&gt;

&lt;p&gt;If this fails, we stop immediately — no point doing a DNS lookup on not_an_email.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;MX Record Lookup
Checks whether the domain actually has mail servers configured. This catches things that syntax validation never would:&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="mailto:user@gmail.con"&gt;user@gmail.con&lt;/a&gt; — syntactically valid, no MX records&lt;br&gt;
&lt;a href="mailto:user@thisdomaindoesnotexist.xyz"&gt;user@thisdomaindoesnotexist.xyz&lt;/a&gt; — looks fine, undeliverable&lt;br&gt;
Defunct company domains that still resolve but stopped accepting mail&lt;br&gt;
Results are cached in memory for 10 minutes (configurable via MX_CACHE_TTL_MS) to avoid hammering DNS on repeated lookups for the same domain.&lt;/p&gt;

&lt;p&gt;const cached = cache.get(domain);&lt;br&gt;
if (cached &amp;amp;&amp;amp; cached.expiresAt &amp;gt; Date.now()) {&lt;br&gt;
  return cached.result;&lt;br&gt;
}&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Disposable Domain Detection&lt;br&gt;
Checked against a blocklist of 5,361 known throwaway providers — Mailinator, TempMail, Guerrilla Mail, and thousands of others. The list is auto-generated via a script and can be refreshed with npm run download-blocklist.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Role-Based Address Detection&lt;br&gt;
35 patterns that indicate a shared inbox rather than a real person:&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;const ROLE_ADDRESSES = new Set([&lt;br&gt;
  'admin', 'noreply', 'no-reply', 'support', 'info',&lt;br&gt;
  'help', 'contact', 'sales', 'billing', 'abuse',&lt;br&gt;
  // ... 25 more&lt;br&gt;
]);&lt;/p&gt;

&lt;p&gt;Useful for signup flows and lead generation — &lt;a href="mailto:sales@company.com"&gt;sales@company.com&lt;/a&gt; is rarely someone's personal inbox.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Typo Detection
Levenshtein distance comparison against 30 major providers. Catches the typos that users actually make:&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;gmial.com → gmail.com&lt;br&gt;
hotmial.com → hotmail.com&lt;br&gt;
outloook.com → outlook.com&lt;br&gt;
Threshold is set to 2 — close enough to catch typos, far enough to avoid false positives.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Catch-All Detection
Some domains are configured to accept every incoming address regardless of whether the mailbox exists. &lt;a href="mailto:anything@thatdomain.com"&gt;anything@thatdomain.com&lt;/a&gt; gets through.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Detection works by probing a randomly generated address (e.g. &lt;a href="mailto:_verify_abc123_nonexistent@domain.com"&gt;_verify_abc123_nonexistent@domain.com&lt;/a&gt;). If the server accepts it, the domain is catch-all.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;SMTP Mailbox Probe
The most interesting part. A raw TCP connection to port 25 of the MX host, performing the minimum possible handshake:&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;→ EHLO verify.local&lt;br&gt;
← 250 OK&lt;br&gt;
→ MAIL FROM:&lt;a href="mailto:verify@verify.local"&gt;verify@verify.local&lt;/a&gt;&lt;br&gt;
← 250 OK&lt;br&gt;
→ RCPT TO:&lt;a href="mailto:user@example.com"&gt;user@example.com&lt;/a&gt;&lt;br&gt;
← 250 OK  (exists) or 550 (doesn't exist)&lt;br&gt;
→ QUIT&lt;/p&gt;

&lt;p&gt;No message is ever sent. No DATA command. The connection is closed immediately after RCPT TO.&lt;/p&gt;

&lt;p&gt;Response codes mapped to results:&lt;/p&gt;

&lt;p&gt;250, 251 → mailbox exists&lt;br&gt;
550, 551, 552, 553, 554 → mailbox does not exist&lt;br&gt;
421, 450, 451, 452 → temporary / unknown&lt;br&gt;
Honest caveat: Most cloud hosting providers block outbound port 25. Railway (where this is deployed) is no exception, so this check typically returns "unknown". The address isn't marked invalid — it takes a -15 point penalty instead of -50. The other six checks still run fully and provide strong signal.&lt;/p&gt;

&lt;p&gt;The Scoring System&lt;br&gt;
Every result includes a 0–100 deliverability score:&lt;/p&gt;

&lt;p&gt;Condition   Effect&lt;br&gt;
Base score  100&lt;br&gt;
Disposable domain   -40&lt;br&gt;
Mailbox not found   -50&lt;br&gt;
Mailbox unknown -15&lt;br&gt;
Catch-all domain    -10&lt;br&gt;
Role-based address  -10&lt;br&gt;
No MX / invalid syntax  score → 0&lt;br&gt;
The Privacy Question&lt;br&gt;
First thing someone asked when I posted about this: "How do we know you're not harvesting addresses and selling them to spammers?"&lt;/p&gt;

&lt;p&gt;The honest answer: you shouldn't just trust me. The code is open source for exactly this reason. The pipeline is stateless — addresses are never written to disk, a database, or any external service. Read src/services/verifyEmail.js and verify it yourself.&lt;/p&gt;

&lt;p&gt;A Docker image for self-hosting is on the roadmap, which eliminates the trust question entirely.&lt;/p&gt;

&lt;p&gt;The API&lt;br&gt;
Single address:&lt;/p&gt;

&lt;p&gt;GET /api/v1/verify?email=&lt;a href="mailto:user@example.com"&gt;user@example.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Bulk (up to 50):&lt;/p&gt;

&lt;p&gt;POST /api/v1/verify/bulk&lt;br&gt;
{ "emails": ["&lt;a href="mailto:user@example.com"&gt;user@example.com&lt;/a&gt;", "&lt;a href="mailto:test@gmail.com"&gt;test@gmail.com&lt;/a&gt;"] }&lt;/p&gt;

&lt;p&gt;Response:&lt;/p&gt;

&lt;p&gt;{&lt;br&gt;
  "email": "&lt;a href="mailto:user@gmail.com"&gt;user@gmail.com&lt;/a&gt;",&lt;br&gt;
  "valid": true,&lt;br&gt;
  "score": 85,&lt;br&gt;
  "reason": "smtp_timeout",&lt;br&gt;
  "suggestion": null,&lt;br&gt;
  "checks": {&lt;br&gt;
    "syntax": true,&lt;br&gt;
    "mx_found": true,&lt;br&gt;
    "mx_host": "gmail-smtp-in.l.google.com",&lt;br&gt;
    "disposable": false,&lt;br&gt;
    "role_based": false,&lt;br&gt;
    "catch_all": false,&lt;br&gt;
    "smtp_connectable": false,&lt;br&gt;
    "mailbox_exists": "unknown"&lt;br&gt;
  },&lt;br&gt;
  "processing_time_ms": 538&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;Live on RapidAPI: &lt;a href="https://rapidapi.com/maulik1807/api/email-verification-and-validation1" rel="noopener noreferrer"&gt;https://rapidapi.com/maulik1807/api/email-verification-and-validation1&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/Maulik2007/email-verify-api" rel="noopener noreferrer"&gt;https://github.com/Maulik2007/email-verify-api&lt;/a&gt;&lt;/p&gt;

</description>
      <category>node</category>
      <category>webdev</category>
      <category>opensource</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
