first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,106 @@
# email-utils
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## 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.