I was asked to add stronger email validation to our forms at work. Honestly, I thought we were overcomplicating it at first.
Then I started looking into the details.
An email can have valid syntax and still come from a disposable provider. A signup form might need to reject certain domains, restrict access to a company, or flag shared addresses like support@. Someone typing gmial.com probably needs a helpful suggestion.
Those decisions depend on the application. I wanted a way to define them once, inspect the result, and reuse the same policy across projects.
That is what I have been building with mailrules, an MIT-licensed email validation library for JavaScript and TypeScript.
The latest addition is a live playground. You can enter an address, change the rules, see the decision, and copy the configuration into your project before installing anything.
Start with a small signup policy
mailrules uses validator.js for syntax checks and fakeout for its default disposable-domain data.
npm install mailrules
Here is a policy that rejects known disposable domains and a custom blocked domain, suggests possible typos, and warns about role addresses:
import { createEmailValidator } from 'mailrules';
const validator = createEmailValidator({
blocklist: { domains: ['spam.example'] },
suggestions: true,
roleAccounts: 'warn',
});
validator.check('alice@example.com').valid;
// true
validator.check('hello@mailinator.com').reasons;
// ['disposable']
validator.check('person@spam.example').reasons;
// ['blocked']
validator.check('support@example.com').warnings?.[0]?.code;
// 'role_account' — a warning; the address is still accepted
validator.check('alex@gmial.com').suggestion;
// 'alex@gmail.com'
Suggestions are advisory. They never replace the submitted address or change whether it passes validation. A result can contain both a rejection and a suggestion; for example, the installed disposable dataset also flags gmial.com. If someone chooses the suggested address, validate that new value too.
See why an address was rejected
A boolean is useful for a form, but it does not explain which configuration entry caused a rejection. For that, there is explain():
const result = validator.explain('person@sub.spam.example');
result.reason;
// 'blocked'
result.matchedRule;
// {
// source: 'blocklist',
// type: 'domain',
// value: 'spam.example',
// matchedSubdomain: true
// }
Domain rules include subdomains by default. You can turn that off with includeSubdomains: false.
Allowlists also need predictable behavior: an explicit block always wins in mailrules. An allowlist can exempt an address from disposable rejection, but it does not override a blocklist entry. For an invitation-only or company-only form, allowlistMode: 'only' requires an allowlist match.
Try the policy before wiring it into a form
In the playground, you can edit domain and mailbox lists, switch role-address and free-provider policies, inspect the result, and copy JavaScript or React examples.
Role addresses and free providers each support classification, warnings, or blocking. Both checks are off by default. A provider-list non-match does not prove an address belongs to a business, and a Gmail address is not inherently a bad signup.
The playground also has a working React Hook Form + Zod example, so you can see how a policy affects field errors and normalized output. Validation happens locally in your browser; the playground does not send the entered address to a validation service.
Reuse it with Zod and React Hook Form
The optional Zod 4 adapter turns the same validator into a schema:
npm install mailrules zod
import { z } from 'zod';
import { zodEmail } from 'mailrules/zod';
// Reuse the validator from the first example.
const signupSchema = z.object({
email: zodEmail(validator),
});
signupSchema.parse({ email: ' Alice@EXAMPLE.COM ' });
// { email: 'Alice@example.com' }
signupSchema.safeParse({ email: 'hello@mailinator.com' }).success;
// false
You can pass that schema to React Hook Form through zodResolver. The integration guide includes the full form example and a Standard Schema adapter for tools that support it.
Warnings remain successful schema results. Use check() or explain() when you want to display those warnings separately. Apply the policy on the backend accepting submissions as well, since browser checks can be bypassed.
React, Zod, and the playground are not required downloads for a normal mailrules installation. Install Zod when you use its adapter. The playground lives in the same repository but is excluded from the npm package.
There is also a CSV command
For reviewing an existing list with an email column:
npx mailrules --input contacts.csv --status rejected --output rejected.csv
This exports rejected rows with validation reasons. You can also export accepted rows or supply a JSON rules configuration. The CLI runs on Node.js 22.12 or newer and processes bounded files in memory; the guide documents its limits.
What “accepted” means here
mailrules checks syntax and your configured application rules locally, in a browser bundle or Node.js. It does not check DNS, MX records, or SMTP, and an accepted result does not establish that a mailbox exists or that the person submitting it owns it.
There are tools with a broader verification scope, including email-validator-js, which documents MX and SMTP checks and a hosted API. For this project, I wanted reusable local policies with clear explanations.
If you have dealt with messy signup forms, I would love to hear about a rule that was harder to get right than you expected. Especially cases where a strict rule ended up rejecting legitimate users.
Top comments (0)