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

legit

v3.1.1

Published

Check that email addresses are really able to accept emails by pinging the DNS and checking for active MX records.

Readme

legit

DNS-based email validation for Node.js. Checks whether an email address's domain has active mail server records before you try to send to it.

Zero runtime dependencies. Full TypeScript support. Node.js ≥ 18.


Why DNS validation?

Format validation ([email protected] passes a regex) tells you almost nothing about whether an email is deliverable. DNS validation goes one step further: it queries the domain's MX records to confirm there is actually a mail server waiting to receive messages.

This catches a large class of bad addresses at the point of collection — typos like [email protected], defunct domains, and domains that have explicitly opted out of receiving email — without the cost and complexity of SMTP probing.

It does not tell you whether a specific mailbox exists. See Limitations for the full picture.


Installation

npm install legit

Quick start

import legit from 'legit';

const result = await legit('[email protected]');

if (result.isValid) {
  // result.mxArray is MxRecord[] — TypeScript knows this
  console.log('Primary mail server:', result.mxArray[0].exchange);
} else {
  // result.mxRecordSetExists is boolean — TypeScript knows this too
  console.log('Domain cannot receive email');
}

How it works

Every call follows this decision tree:

Input
  │
  ├─ Structurally invalid? (no @, bad local part, etc.)
  │     → rejects with TypeError immediately — no DNS query issued
  │
  └─ Valid structure
        │
        ├─ MX records found?
        │     ├─ Null MX (RFC 7505)? → { isValid: false, mxRecordSetExists: true }
        │     └─ Real MX records    → { isValid: true, mxArray: [...sorted by priority] }
        │
        └─ No MX records (ENODATA)
              │
              ├─ A record exists? (RFC 5321 §5.1 fallback)
              │     → { isValid: true, mxArray: [{ exchange: domain, priority: 0 }] }
              │
              └─ No A record / domain not found (ENOTFOUND)
                    → { isValid: false, mxRecordSetExists: false }

Unexpected DNS errors (ESERVFAIL, ECONNREFUSED, timeout) reject the promise so you can distinguish infrastructure problems from a simple invalid domain.


API

legit(emailAddress, options?)

import legit from 'legit';
import type { LegitOptions, LegitResult } from 'legit';

Parameters

| Name | Type | Description | |---|---|---| | emailAddress | string | The email address to validate | | options | LegitOptions | Optional configuration (see below) |

Returns Promise<LegitResult>


LegitOptions

interface LegitOptions {
  timeout?: number; // DNS lookup timeout in ms. Default: 10000
}

LegitResult

A discriminated union on the isValid field. Checking result.isValid narrows the type automatically.

type LegitResult =
  | { isValid: true;  mxArray: MxRecord[] }
  | { isValid: false; mxArray: null; mxRecordSetExists: boolean }

When isValid is true

| Field | Type | Description | |---|---|---| | isValid | true | The domain has active mail server records | | mxArray | MxRecord[] | All MX records, sorted by priority ascending (lowest number = highest priority). Each record has exchange: string and priority: number. |

Note: When the domain has no MX records but has an A record (RFC 5321 §5.1 fallback), mxArray contains a single synthetic entry { exchange: domain, priority: 0 }.

When isValid is false

| Field | Type | Description | |---|---|---| | isValid | false | The domain cannot receive email | | mxArray | null | Always null | | mxRecordSetExists | boolean | false — domain does not exist in DNS at all, or exists but has no MX or A records. true — domain exists but has a null MX record (RFC 7505), meaning it explicitly opts out of receiving email. |


Examples

Valid domain

const result = await legit('[email protected]');
// {
//   isValid: true,
//   mxArray: [
//     { exchange: 'aspmx.l.google.com',      priority: 1  },
//     { exchange: 'alt1.aspmx.l.google.com', priority: 5  },
//     { exchange: 'alt2.aspmx.l.google.com', priority: 5  },
//     { exchange: 'alt3.aspmx.l.google.com', priority: 10 },
//     { exchange: 'alt4.aspmx.l.google.com', priority: 10 }
//   ]
// }

Domain that does not exist

const result = await legit('[email protected]');
// {
//   isValid: false,
//   mxArray: null,
//   mxRecordSetExists: false
// }

Domain with null MX (explicitly rejects all email per RFC 7505)

const result = await legit('[email protected]');
// {
//   isValid: false,
//   mxArray: null,
//   mxRecordSetExists: true
// }

Structurally invalid address

// These all reject with a TypeError — no DNS query is made
await legit('notanemail');          // no @ sign
await legit('');                    // empty string
await legit('[email protected]');   // local part starts with dot
await legit('[email protected]'); // consecutive dots
await legit('a'.repeat(65) + '@example.com'); // local part > 64 chars

Error handling

The promise resolves for all expected DNS outcomes. It only rejects for two reasons:

| Rejection type | Cause | What to do | |---|---|---| | TypeError | Email address is structurally invalid | Caught before any DNS query. Treat as a user input error. | | Error | Unexpected DNS failure (ESERVFAIL, ECONNREFUSED) or timeout | Indicates an infrastructure problem. Consider retrying or degrading gracefully. |

import legit from 'legit';

try {
  const result = await legit(emailFromUser, { timeout: 5000 });

  if (result.isValid) {
    await sendWelcomeEmail(emailFromUser);
  } else {
    showError('That email address does not appear to accept mail.');
  }
} catch (err) {
  if (err instanceof TypeError) {
    // Invalid format — tell the user
    showError('Please enter a valid email address.');
  } else {
    // DNS infrastructure problem — fail open or retry
    console.error('DNS validation failed, proceeding anyway:', err);
    await sendWelcomeEmail(emailFromUser);
  }
}

Common patterns

Express / Fastify registration endpoint

import legit from 'legit';

app.post('/register', async (req, res) => {
  const { email } = req.body;

  let result;
  try {
    result = await legit(email, { timeout: 5000 });
  } catch (err) {
    if (err instanceof TypeError) {
      return res.status(400).json({ error: 'Invalid email address format.' });
    }
    // DNS unavailable — fail open so users can still register
    return next(); // or proceed with registration
  }

  if (!result.isValid) {
    return res.status(400).json({
      error: 'Email domain does not accept mail. Please check the address and try again.',
    });
  }

  // Proceed with registration
});

Validating a list of addresses

import legit from 'legit';

async function filterValidAddresses(emails: string[]): Promise<string[]> {
  const results = await Promise.allSettled(
    emails.map(email => legit(email, { timeout: 5000 }))
  );

  return emails.filter((_, i) => {
    const outcome = results[i];
    return outcome.status === 'fulfilled' && outcome.value.isValid;
  });
}

Checking which mail server will handle delivery

import legit from 'legit';

const result = await legit('[email protected]');

if (result.isValid && result.mxArray.length > 0) {
  const primary = result.mxArray[0]; // always lowest priority number = highest preference
  console.log(`Primary mail server: ${primary.exchange} (priority ${primary.priority})`);
}

Internationalised domains (IDN)

Unicode domain names are automatically normalised to their ACE/punycode form before the DNS lookup. You do not need to encode them yourself.

// Both of these work the same way
await legit('user@münchen.de');      // unicode
await legit('[email protected]'); // punycode equivalent

Limitations

Understanding what legit does not do is just as important as understanding what it does.

It does not confirm a specific mailbox exists. MX records tell you the domain has a mail server. They say nothing about whether alice@ or bob@ that domain is a real, active account. The only way to confirm that without actually sending is SMTP probing (connecting to port 25 and issuing a RCPT TO), which is frequently blocked by firewalls, triggers spam filters, and is unreliable against catch-all configurations.

It does not validate the full local part. Basic structural checks are applied (length ≤ 64 chars, no leading/trailing/consecutive dots in unquoted addresses), but the full RFC 5322 local-part grammar is not enforced. Addresses like user [email protected] (unquoted space) are not rejected.

It does not detect disposable email domains. Domains like mailinator.com or guerrillamail.com have perfectly valid MX records. Blocking them requires a regularly-updated blocklist, which is a policy decision outside the scope of DNS validation.

It does not verify that MX hostnames resolve to IP addresses. A domain can have MX records that point to hostnames with no A record ("dangling MX"). These would still return isValid: true even though mail delivery would fail.

Results can become stale. DNS is cached and changes over time. A domain that passes validation today could remove its MX records tomorrow. For critical use cases, re-validate periodically rather than storing results indefinitely.


Requirements

  • Node.js ≥ 18.0.0
  • No runtime dependencies

License

MIT © 2015–2026 Martyn Davies and contributors