4.0 KiB
email-utils
(no submodules)
Purpose
email-utils provides three pure, side-effect-free helper functions used throughout @suaveplan/suppression to normalise, decompose, and validate email addresses. They exist as shared utilities so that the store, importer, and exporter all apply identical normalisation semantics — preventing duplicates caused by case differences or leading/trailing spaces.
All functions operate on string values only; no external libraries or Node.js builtins are required, keeping the module universally usable in both server and edge environments.
Features
normalizeEmail— trim + lowercase canonical form used for all storage and equality checksextractDomain— isolate the domain segment after the last@isValidEmail— lightweight RFC 5321 structure check (length limits, unique@, non-empty parts, dot-containing domain)- Throws
SuppressionErrorwith codeSUPPRESSION.INVALID_EMAILon malformed input (where applicable) - Zero external dependencies; safe in edge/browser runtimes
Usage: Basic Example
import {
normalizeEmail,
extractDomain,
isValidEmail,
} from "@suaveplan/suppression";
console.log(normalizeEmail(" USER@Example.COM ")); // "user@example.com"
console.log(extractDomain("user@example.com")); // "example.com"
console.log(isValidEmail("user@example.com")); // true
console.log(isValidEmail("not-an-email")); // false
Usage: Advanced Example
import { normalizeEmail, isValidEmail, extractDomain } from "@suaveplan/suppression";
import { SuppressionError, SUPPRESSION_CODES } from "@suaveplan/suppression/errors";
function safeNormalise(raw: string): string {
if (!isValidEmail(raw.trim())) {
throw new SuppressionError(
SUPPRESSION_CODES.INVALID_EMAIL,
"suppression.invalid_email",
{ context: { raw } }
);
}
return normalizeEmail(raw);
}
// Domain grouping
const emails = ["Alice@Example.com", "bob@example.com", "carol@other.io"];
const byDomain = Map.groupBy(
emails.map(normalizeEmail),
(e) => extractDomain(e)
);
console.log([...byDomain.keys()]); // ["example.com", "other.io"]
API Reference
normalizeEmail(email: string): string
Trims leading/trailing whitespace and converts the entire string to lower-case. Does not validate the result — use isValidEmail if validation is needed.
Parameters:
email— raw email string
Returns: normalised email string
extractDomain(email: string): string
Returns the substring after the last @ character, converted to lower-case.
Parameters:
email— email address (need not be normalised)
Returns: domain string (e.g. "example.com")
Throws: SuppressionError with code SUPPRESSION.INVALID_EMAIL when no @ is found.
isValidEmail(email: string): boolean
Performs a lightweight structural validity check:
- Must be a string of at most 254 characters
- Exactly one
@character, not at position 0 - Local part: 1–64 characters, no leading/trailing whitespace
- Domain: non-empty, contains at least one
., no leading/trailing dot or hyphen
Parameters:
email— string to test
Returns: true if all checks pass, false otherwise. Never throws.
Implementation Notes
normalizeEmail is intentionally minimal — it does not attempt Unicode NFKC normalisation or address parsing (angle-bracket stripping, comment removal). Production email pipelines should apply RFC 5321 full parsing upstream before calling this function.
extractDomain uses lastIndexOf("@") rather than indexOf to correctly handle the rare-but-valid case of multiple @ characters in quoted local parts, then lowercases the result for consistent domain matching.
isValidEmail deliberately avoids regular expressions to keep the logic readable and auditable. The length limits (254 total, 64 local) derive from RFC 5321 §4.5.3. IP-literal domains ([192.168.1.1]) and quoted local parts are not supported, matching the practical subset of addresses used in transactional email.