# 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 checks - `extractDomain` — isolate the domain segment after the last `@` - `isValidEmail` — lightweight RFC 5321 structure check (length limits, unique `@`, non-empty parts, dot-containing domain) - Throws `SuppressionError` with code `SUPPRESSION.INVALID_EMAIL` on malformed input (where applicable) - Zero external dependencies; safe in edge/browser runtimes ## Usage: Basic Example ```ts 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 ```ts 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: 1. Must be a string of at most 254 characters 2. Exactly one `@` character, not at position 0 3. Local part: 1–64 characters, no leading/trailing whitespace 4. 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.