Email Validator — JavaScript source
Validate email addresses one at a time or in bulk. Checks syntax, length limits, local-part and domain rules, plus-addressing, and IP-literal domains - all in your browser.
This is the JavaScript implementation — the same logic the interactive tool runs, in a shareable, citable form.
/**
* email-validator - JavaScript polyglot showcase port.
*
* Language: JavaScript (ES module).
*
* CosmoDev polyglot showcase: the pure logic of the email-validator tool,
* ported from src/lib/email-validator.ts. This file is display source - part
* of CosmoDev's polyglot tool pages, where each tool's logic is shown side by
* side in several languages.
*
* RFC 5321/5322-inspired email validation. Pure, deterministic, never throws.
* Errs on the side of practical deliverability (provider-friendly) while still
* recognising the legal-but-unusual forms (quoted local parts, IP-literal
* domains).
*/
// RFC-inspired length ceilings: local part, domain, and total address.
const LOCAL_MAX = 64;
const DOMAIN_MAX = 253;
const TOTAL_MAX = 320;
// Characters permitted in an unquoted (atom) local part. This is the
// printable-ASCII subset from RFC 5322 - alphanumerics plus a fixed set of
// specials. Anything else forces the local part to be quoted.
const LOCAL_CHARS = /^[A-Za-z0-9.!#$%&'*+/=?^_`{|}~-]+$/;
/**
* Split an email into local + domain, honouring a quoted ("...") local part.
* Returns { local, domain, quoted } or null when the address cannot be split
* into exactly one '@' in the right place.
*/
function splitLocalDomain(email) {
if (email.startsWith('"')) {
// Walk the quoted string; a backslash escapes the next byte (so `\"`
// does not terminate the quote).
let i = 1;
while (i < email.length) {
const ch = email[i];
if (ch === '\\') { i += 2; continue; }
if (ch === '"') break;
i += 1;
}
if (email[i] !== '"') return null; // unterminated quote
const at = i + 1;
if (email[at] !== '@') return null; // '@' must immediately follow the closing quote
if (email.indexOf('@', at + 1) !== -1) return null; // stray '@' inside the domain
return { local: email.slice(0, at), domain: email.slice(at + 1), quoted: true };
}
const first = email.indexOf('@');
if (first === -1) return null;
if (email.indexOf('@', first + 1) !== -1) return null; // multiple '@'
return { local: email.slice(0, first), domain: email.slice(first + 1), quoted: false };
}
/** True when `s` is a dotted-quad: four octets, each 0-255, with no leading zeros. */
function isIPv4(s) {
const parts = s.split('.');
if (parts.length !== 4) return false;
return parts.every((p) => {
if (!/^\d+$/.test(p)) return false;
const n = Number(p);
// `String(n) === p` rejects leading zeros ("01") and numeric overflow forms.
return n >= 0 && n <= 255 && String(n) === p;
});
}
/** Push domain-level problems into the shared reasons/warnings arrays. */
function validateDomain(domain, reasons, warnings) {
if (!domain) {
reasons.push('Domain is empty');
return;
}
if (domain.length > DOMAIN_MAX) {
reasons.push(`Domain exceeds ${DOMAIN_MAX} characters`);
}
// IP-literal domain: [1.2.3.4] or [IPv6:...].
if (domain.startsWith('[') && domain.endsWith(']')) {
const inner = domain.slice(1, -1);
if (/^ipv6:/i.test(inner)) {
warnings.push('IPv6 literal domain (uncommon; ensure your provider supports it)');
return;
}
if (isIPv4(inner)) {
warnings.push('IP-literal domain (uncommon; ensure your provider supports it)');
return;
}
reasons.push('Invalid IP-literal domain');
return;
}
if (domain.startsWith('[') || domain.endsWith(']')) {
reasons.push('Malformed IP-literal domain (unmatched brackets)');
return;
}
if (!domain.includes('.')) {
reasons.push('Domain must contain at least one dot (e.g. example.com)');
return;
}
const labels = domain.split('.');
for (const label of labels) {
if (!label) {
reasons.push('Domain contains an empty label (consecutive or trailing dots)');
continue;
}
if (label.length > 63) reasons.push('Domain label exceeds 63 characters');
if (!/^[A-Za-z0-9-]+$/.test(label)) reasons.push('Domain label contains invalid characters');
if (label.startsWith('-') || label.endsWith('-')) reasons.push('Domain label starts or ends with a hyphen');
}
// The TLD is the final label; require at least two ASCII letters so that
// bare/host-name domains and numeric tails are rejected.
const tld = labels[labels.length - 1];
if (!/^[A-Za-z]{2,}$/.test(tld)) reasons.push('Top-level domain must be at least two letters');
}
/**
* Validate a single email address; returns a structured verdict, never throws.
* `valid` is true iff `reasons` is empty. `warnings` never affect validity.
*/
export function validateEmail(raw) {
const reasons = [];
const warnings = [];
const email = raw.trim();
if (!email) {
return { valid: false, reasons: ['Email is empty'], warnings };
}
if (email.length > TOTAL_MAX) {
reasons.push(`Email exceeds maximum length of ${TOTAL_MAX} characters`);
}
const split = splitLocalDomain(email);
if (!split) {
reasons.push('Email must contain exactly one "@" separating local part and domain');
return { valid: false, reasons, warnings };
}
const { local, domain, quoted } = split;
if (quoted) {
// Quoted local parts are RFC-legal but almost universally rejected by
// mailbox providers - flag as a warning, only length-check structurally.
if (local.length > LOCAL_MAX) reasons.push(`Local part exceeds ${LOCAL_MAX} characters`);
warnings.push('Quoted local part (rarely supported by providers)');
} else if (!local) {
reasons.push('Local part is empty');
} else {
if (local.length > LOCAL_MAX) reasons.push(`Local part exceeds ${LOCAL_MAX} characters`);
if (local.startsWith('.') || local.endsWith('.')) reasons.push('Local part starts or ends with a dot');
if (local.includes('..')) reasons.push('Local part contains consecutive dots');
if (!LOCAL_CHARS.test(local)) reasons.push('Local part contains invalid characters');
}
// Plus-addressing (`user+tag@`) is valid and delivers to the base mailbox,
// but callers filtering on exact address may want to know.
if (!quoted && local.includes('+')) {
warnings.push('Plus-addressing (tag) detected - delivers to the base mailbox');
}
validateDomain(domain, reasons, warnings);
const valid = reasons.length === 0;
return {
valid,
local,
domain,
normalized: local && domain ? `${local}@${domain.toLowerCase()}` : undefined,
reasons,
warnings,
};
}
/**
* Validate many emails (one per line). Blank / whitespace-only lines are
* skipped. Line endings may be LF or CRLF.
*/
export function validateBatch(input) {
if (!input) return [];
return input
.split(/\r?\n/)
.map((line) => line.trim())
.filter((line) => line.length > 0)
.map(validateEmail);
}
Also available in 13 other languages
Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →