npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

mr-email-checker

v0.1.1

Published

Production-grade email validation for Node.js — syntax, MX records, SMTP mailbox verification, disposable/free-provider detection, custom-domain classification, catch-all detection, typo suggestions and risk scoring.

Readme

Production-grade email validation & verification for Node.js — zero runtime dependencies.

mr-email-checker answers the questions you actually have about an email address before you let it into your database:

  • Is the syntax valid? — a practical RFC 5321/5322 subset, IDN/Unicode aware, quoted local parts, sub-addressing (user+tag@…).
  • Can the domain receive mail? — real MX lookups with A/AAAA implicit-MX fallback and RFC 7505 null-MX handling.
  • Does the mailbox exist? — optional live SMTP RCPT TO probe with catch-all detection (no message is ever sent).
  • Is it a free/public provider? (gmail, yahoo, outlook, proton …) — ~4.5k domains.
  • Is it a disposable/temporary address? (mailinator, 10minutemail …) — ~138k domains from merged community lists.
  • Is it a purchased/custom mailbox domain? — and who hosts it (Google Workspace, Microsoft 365, Zoho, Fastmail, Proton, Cloudflare Email Routing, …).
  • Is it a role account? (info@, support@, admin@ …)
  • Did they make a typo? — "did you mean [email protected]?"

Typo detection uses Damerau-Levenshtein distance (a single adjacent-letter swap counts as one edit, matching how people actually mistype) with a tight default threshold of 1. This catches real slips like gmial.comgmail.com while not misfiring on legitimate lookalike domains such as yopmail.com (which is a real disposable provider, not a typo of hotmail.com). Known providers are never flagged as typos.

Every check folds into a single status verdict (deliverable / risky / undeliverable / unknown), a 0–100 deliverability score and a low/medium/high risk bucket.

Accuracy guarantee: only authoritative DNS answers can mark an address undeliverable. If the lookup itself fails (VPN, firewall, resolver down), the checker retries, falls back to DNS-over-HTTPS (Cloudflare → Google), and — if even that is unreachable — reports status: "unknown" instead of falsely condemning a real address. Transport failures are never cached.


Install

npm install mr-email-checker
# or
pnpm add mr-email-checker
# or
yarn add mr-email-checker

Requires Node.js ≥ 18. Ships dual ESM + CJS builds and full TypeScript types. No dependencies.


Quick start

import { verifyEmail } from "mr-email-checker";

const report = await verifyEmail("[email protected]");

console.log(report.status);         // "deliverable" — the one field to act on
console.log(report.valid);          // true
console.log(report.score);          // 82
console.log(report.risk);           // "low"
console.log(report.domain.kind);    // "free"
console.log(report.canonical);      // "[email protected]"  (gmail dots + tag stripped)

CommonJS works exactly the same:

const { verifyEmail } = require("mr-email-checker");

What you get back

verifyEmail resolves to an EmailReport:

interface EmailReport {
  input: string;                    // exactly what you passed in
  email: string | null;             // normalized (lowercased domain, punycoded IDN)
  canonical: string | null;         // dedupe key (gmail dots removed, +tag stripped)
  status: EmailStatus;              // "deliverable" | "risky" | "undeliverable" | "unknown"
  valid: boolean;                   // never proven undeliverable (fails open on network errors)
  score: number;                    // 0–100 deliverability confidence
  risk: "low" | "medium" | "high";
  reasons: ReasonCode[];            // machine-readable signals, e.g. ["free_provider", "role_account"]

  syntax: SyntaxResult;             // localPart, domain, subaddress, quoted, unicode…
  mx: MxResult;                     // status, records[], usedImplicitMx, error
  smtp: SmtpResult;                 // verdict, code, message, catchAll, latencyMs
  domain: DomainInfo;               // kind, isFreeProvider, isDisposable, isCustomDomain, mailProvider

  isRoleAccount: boolean;           // info@, support@, admin@…
  didYouMean: TypoSuggestion | null;// { suggestion, domain, distance }
  durationMs: number;
}

status — the one field to act on

| status | Meaning | Typical action | | --- | --- | --- | | deliverable | The domain verifiably accepts mail and nothing disproved the mailbox. | Accept. | | risky | Mail arrives, but you probably don't want it: disposable domain or catch-all server. | Your call — block disposables, soft-accept catch-alls. | | undeliverable | Proven bad: invalid syntax, non-existent domain, no mail servers (or null MX), or the mailbox was rejected by SMTP. | Reject. | | unknown | Verification itself failed (DNS unreachable even over DoH). Not evidence against the address. | Accept and re-verify later, or queue for retry. |

Only authoritative DNS answers produce undeliverable. A resolver that is down, blocked or refusing connections yields unknown — never a false no_mx_records.

Example output

// verifyEmail("[email protected]")
{
  "email": "[email protected]",
  "status": "deliverable",
  "valid": true,
  "score": 72,
  "risk": "low",
  "reasons": ["role_account", "custom_domain"],
  "isRoleAccount": true,
  "domain": {
    "kind": "custom",
    "isFreeProvider": false,
    "isDisposable": false,
    "isCustomDomain": true,      // a purchased / self-hosted mailbox domain
    "mailProvider": "microsoft-365"  // …hosted on Microsoft 365
  },
  "didYouMean": null
}

Verifying the mailbox itself (SMTP)

By default mr-email-checker does not open an SMTP connection — it validates syntax, DNS/MX and classification, which is safe everywhere and fast. To actually probe whether the specific mailbox exists, enable SMTP:

const report = await verifyEmail("[email protected]", {
  smtp: {
    enabled: true,
    from: "[email protected]", // use a real domain you control
    heloHost: "yourdomain.com",
    timeoutMs: 5000,
    detectCatchAll: true,          // also probe a random mailbox
  },
});

console.log(report.smtp.verdict);
// "deliverable" | "undeliverable" | "catch_all" | "greylisted" | "blocked" | "timeout" | "skipped"

The probe performs EHLO → MAIL FROM → RCPT TO and then immediately quits — no email is delivered.

⚠️ Port 25 is blocked outbound on almost every cloud (AWS, GCP, Azure, most PaaS/serverless). When the handshake can't complete you'll get blocked or timeout rather than a false negative — the score simply falls back to the DNS-based baseline. Run SMTP probing from an environment with unfiltered port 25, ideally with a warm sending IP and proper reverse DNS, or you'll be rate-limited/greylisted by receivers.

Catch-all domains accept every address, so a mailbox can't be individually confirmed — the verdict becomes catch_all and the score is deliberately capped.


Batch verification

import { verifyEmails } from "mr-email-checker";

const reports = await verifyEmails(
  ["[email protected]", "[email protected]", "[email protected]"],
  { concurrency: 20 }, // default 10
);

DNS results are cached (TTL configurable) so repeated domains in a batch cost one lookup.


Lightweight checks

import { isValidEmail, isValidSyntax } from "mr-email-checker";

isValidSyntax("[email protected]");            // true  — synchronous, no network
await isValidEmail("[email protected]");       // boolean — syntax + MX

Using the checks individually

Every stage is exported so you can compose your own pipeline:

import {
  checkSyntax,   // (input) => SyntaxResult          — sync, no network
  checkMx,       // (domain, opts) => Promise<MxResult>
  checkSmtp,     // (email, domain, mxRecords, opts) => Promise<SmtpResult>
  classifyDomain,// (domain, mx, opts) => DomainInfo  — free / disposable / custom
  suggestTypo,   // (domain, threshold?) => TypoSuggestion | null
  canonicalize,  // (localPart, domain) => string
} from "mr-email-checker";

const s = checkSyntax("bücher@münchen.de");
// { valid: true, domain: "xn--mnchen-3ya.de", unicode: true, … }

You can also reach the raw datasets:

import { getDisposableSet, getFreeProviderSet, BUNDLED_COUNTS, identifyProvider } from "mr-email-checker";

getDisposableSet().has("mailinator.com");        // true
getFreeProviderSet().has("gmail.com");           // true
BUNDLED_COUNTS;                                  // { disposable: 138301, freeProviders: 4463 }
identifyProvider(["acme.mail.protection.outlook.com"]); // "microsoft-365"

Options

interface VerifyOptions {
  dnsTimeoutMs?: number;          // default 5000
  smtp?: SmtpOptions;             // disabled by default
  extraDisposableDomains?: Iterable<string>; // merge with the built-in list
  extraFreeProviders?: Iterable<string>;
  allowlist?: Iterable<string>;   // never treat these as disposable
  blocklist?: Iterable<string>;   // always treat these as disposable
  suggestTypos?: boolean;         // default true
  typoThreshold?: number;         // max Damerau edit distance, default 1
  cacheTtlMs?: number;            // DNS cache TTL, default 300000 (0 disables)
  resolveMx?: (domain: string) => Promise<MxRecord[]>; // inject a custom resolver (DoH, tests)
  dohFallback?: boolean;          // default true — DoH rescue when the system resolver is unreachable
}

interface SmtpOptions {
  enabled?: boolean;              // default false
  from?: string;                  // MAIL FROM address
  heloHost?: string;              // EHLO/HELO hostname
  port?: number;                  // default 25
  timeoutMs?: number;             // default 5000
  detectCatchAll?: boolean;       // default true
  maxHosts?: number;              // MX hosts to try, default 2
}

Custom lists & a DoH resolver

DNS-over-HTTPS is already built in as an automatic fallback (dohFallback, on by default): if the system resolver refuses or drops queries, the lookup is retried via Cloudflare and then Google DoH before anything is reported as unknown. The resolveMx option below is only needed when you want to replace the system resolver entirely.

await verifyEmail("[email protected]", {
  extraFreeProviders: ["corp.internal"],
  blocklist: ["spammydomain.xyz"],
  allowlist: ["mailinator.com"],       // if you actually want to accept it
  resolveMx: async (domain) => {
    // e.g. Cloudflare DNS-over-HTTPS instead of the system resolver
    const res = await fetch(`https://cloudflare-dns.com/dns-query?name=${domain}&type=MX`, {
      headers: { accept: "application/dns-json" },
    });
    const json = await res.json();
    return (json.Answer ?? []).map((a: any) => {
      const [priority, exchange] = a.data.split(" ");
      return { exchange: exchange.replace(/\.$/, ""), priority: Number(priority) };
    });
  },
});

How the score is built

The score starts from a DNS-backed baseline and adjusts on every signal:

| Signal | Effect | | ---------------------------------------- | ----------------- | | Disposable domain | hard floor → 2 | | No MX / domain doesn't exist | hard floor → 5 | | DNS unreachable (status: "unknown") | neutral 45 | | Valid syntax + MX present | baseline 55 | | Known free provider | +15 | | Custom/purchased domain | +10 | | Recognised professional host (Workspace…)| +5 | | Implicit MX (A-record only) | −5 | | SMTP deliverable | +35 | | SMTP undeliverable | hard floor → 8 | | SMTP catch_all | +5 (unconfirmable)| | SMTP greylisted | +8 | | Role account (info@, support@) | −8 | | Likely typo | −25 |

Buckets: ≥70 low, 40–69 medium, <40 high risk.

The exact weights are pragmatic defaults, not a law of nature. If you need different behaviour, read the raw signals off the report and apply your own policy.


reasons codes

report.reasons is a stable, machine-readable list you can branch on:

invalid_syntax, no_mx_records, no_dns_records, dns_error, disposable_domain, free_provider, custom_domain, role_account, mailbox_exists, mailbox_not_found, catch_all_domain, smtp_greylisted, smtp_blocked, smtp_timeout, smtp_skipped, possible_typo, subaddressed, unicode_local_part, quoted_local_part, and more.


The datasets

The disposable (~138k) and free-provider (~4.5k) lists are merged from public, community-maintained sources:

They're shipped gzip-compressed and base64-embedded, then inflated lazily with node:zlib on first use — so importing the package is instant and there are still zero runtime dependencies. Major providers (gmail, outlook, proton, …) are force-protected from ever being mislabelled as disposable.

Regenerate them anytime:

node scripts/update-lists.mjs

Design notes & limitations

  • No email is ever sent. The SMTP check ends at RCPT TO and quits.
  • A network failure is never evidence. Only authoritative DNS answers (NXDOMAIN, no records, null MX) can produce undeliverable. Resolver/transport errors retry, fall back to DoH, and finally surface as status: "unknown" with a dns_error reason — and are never cached.
  • SMTP is best-effort. Greylisting, catch-all servers, tarpitting and port-25 filtering mean a live mailbox check is a strong signal, not a guarantee. Treat deliverable as high-confidence and catch_all/blocked/timeout as "unknown".
  • Syntax is a practical subset. It accepts what real MTAs accept and rejects the pathological-but-technically-legal corners (bare IP literals, comments) that never appear in genuine signups.
  • This is validation, not authentication. It tells you an address is well-formed and reachable — not that the person controls it. Pair it with a confirmation email for anything security-sensitive.

Development

npm install
npm run build       # tsup → dist (ESM + CJS + d.ts)
npm run typecheck   # tsc --noEmit
npm test            # vitest

License

MIT © Rohit Tiwari