Move docs/ package mirror into document-navigator plugin
Relocate the ~6k-file docs tree from the repo root into plugins/document-navigator/docs/ so the plugin is self-contained and portable. docs/superpowers/ (specs and plans) stays at the repo root. Moved via cp+rm rather than git mv because directory rename() fails with EPERM on this WSL /mnt DrvFs mount; git rename-detection pairs the identical-content files at commit time. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user