DEV Community

Email validation with disposable detection, blocklists and allowlists in TypeScript

An email can have valid syntax and still fail your application's signup rules. You might want to reject disposable providers, block a specific mailbox, or accept only addresses from a company's domain.

I built mailrules to bring those decisions into one configurable TypeScript/JavaScript API. Version 0.1.0 is available on npm.

Start with a reusable validator

npm install mailrules
import { createEmailValidator } from 'mailrules';

const validator = createEmailValidator({
  blocklist: {
    domains: ['spam.example'],
    emails: ['a****@example.com'],
  },
  allowlist: {
    emails: ['t******@mailinator.com'],
  },
});

validator.check('p*****@mailinator.com').reasons; // ['disposable']
validator.check('p*****@spam.example').reasons; // ['blocked']
validator.check('t******@mailinator.com').valid; // true
validator.check(null).reasons; // ['invalid_format']

Create the validator once and reuse it. Lists are copied, normalized and indexed during construction. Validation is synchronous and makes no network requests.

The default entry uses validator.js for syntax and fakeout for disposable domains. The underlying dataset comes from the disposable-email-domains project.

Make the policy explicit

An allowlist entry normally creates an exception to disposable-email rejection. To restrict access to specific domains or addresses, use allowlistMode: 'only':

const teamOnly = createEmailValidator({
  allowlistMode: 'only',
  allowlist: {
    domains: ['company.example'],
    emails: ['co********@partner.example'],
  },
});

teamOnly.check('s****@company.example').valid; // true
teamOnly.check('s****@eu.company.example').valid; // true
teamOnly.check('s******@gmail.com').reasons; // ['not_allowed']

Domain rules include subdomains by default. Set includeSubdomains: false to require exact domain matches.

Rejection checks run in a defined order:

  • Invalid email syntax.
  • Explicit blocklist match.
  • Missing allowlist match in 'only' mode.
  • Disposable domain without an allowed exception.

An explicit block always wins over an allowlist exception. The result includes a stable reason code so the application can decide what feedback to show.

Bring your own disposable-domain data

Use the core entry when you want to manage that data yourself:

import { createEmailValidator } from 'mailrules/core';

const customValidator = createEmailValidator({
  disposableDomains: ['burner.example', 'temporary.example'],
});

customValidator.check('u***@burner.example').reasons; // ['disposable']

mailrules/core defaults to an empty disposable dataset and does not import fakeout. The npm package still declares fakeout as a dependency.

A supplied dataset replaces the default dataset. Installed data does not refresh automatically. Update dependencies and redeploy to pick up changes, or supply updated data and construct a new validator.

What the result tells you

valid: true means the address passed syntax checks and your configured rules. It does not establish that the mailbox exists or belongs to the person submitting it. There are no DNS, MX or SMTP checks; ownership still requires a confirmation flow.

Domain names are normalized, while local-part case and plus tags are preserved. Exact mailbox rules therefore distinguish U***@example.com from u***@example.com.

The package supports ESM, CommonJS, Node.js 22.12+ and modern browser bundlers. Apply the same policy on the server when accepting form submissions.

I'd appreciate feedback on the rule precedence and API, especially from developers who have had to handle exceptions to disposable-email blocking.

Read on DEV Community ↗ ← Back to News

Comments

No comments yet. Start the discussion.