Guard Email
RFC 822 / 5322 single-address validator + RFC 5322 message gate with header-injection defense, domain-side IDN / Punycode handling, mixed-script confusable detection, label length caps, IP-literal denial, and sub-address handling.
Two entry shapes: - validateAddress(addr, opts) — single mailbox (RFC 5321 atext@DNS-domain). Caps RFC 5321 §4.5.3.1 local-part 64 / domain 255 / address 320. Flags multi-@, IP literals, Punycode, mixed-script confusables, and codepoint-class threats (BIDI / control / null / zero-width).
Scope of Unicode handling: the DOMAIN side recognizes IDN / Punycode (xn--) labels and mixed-script confusables, gated by allowedScripts (RFC 5890 / RFC 5891). The LOCAL part is ASCII atext only (RFC 5321 §4.1.2 / RFC 5322 §3.2.3) — a unicode mailbox (RFC 6531 SMTPUTF8 / EAI) is NOT accepted and surfaces as an address-syntax issue. This is deliberate: a unicode local-part widens the homograph / confusable attack surface beyond the domain (where registry IDN policy and Punycode normalization apply) into the unregulated mailbox name, where no equivalent normalization authority exists. RFC 6531 local-part acceptance re-opens behind an explicit allowUnicodeLocalPart opt-in when operator demand for genuine EAI mailboxes lands; until then the conservative ASCII contract holds by default. - validateMessage(rfc822, opts) — full RFC 5322 message. Splits header section, unfolds folded headers, walks every single-line header for embedded CR/LF, drives address checks on From / To / Cc / Bcc / Reply-To / Sender / Return-Path, and scans the message body for SMTP-smuggling (bare-CR / bare-LF / \r?\n.\r?\nMAIL FROM: class — CVE-2023-51764 / 51765 / 51766) plus RFC 5322 §2.1.1 line cap.
Profiles ship in pairs: - strict / balanced / permissive — operator scope. - hipaa / pci-dss / gdpr / soc2 — compliance posture.
Header injection, SMTP smuggling, multi-@, and null-byte are reject at every profile — universally exploitable, no sanitization is safe.
b.guardEmail.validateAddress(input, opts) #
{
profile: "strict" | "balanced" | "permissive",
compliancePosture: "hipaa" | "pci-dss" | "gdpr" | "soc2",
multiAtPolicy: "reject" | "audit" | "allow",
ipLiteralPolicy: "reject" | "audit" | "allow",
addressCommentPolicy: "reject" | "audit" | "allow",
punycodePolicy: "reject" | "audit" | "allow",
mixedScriptPolicy: "reject" | "audit" | "allow",
allowedScripts: string[] | null,
maxLocalPartBytes: number,
maxDomainBytes: number,
maxAddressBytes: number,
}
Validate a single email address against RFC 5321 atext@DNS-domain shape with the active profile's policies. Returns { ok, issues }; issues[] carries kind / severity / ruleId / snippet for every detector that fired. Never throws on input — bad shapes surface as bad-input issues so the caller can route on them.
Detectors run in order: total-address cap, multi-@ count, RFC 5322 comment syntax, IP literal [...], local-part / domain caps, Punycode (xn--) labels, mixed-script confusables (Latin / Cyrillic / Greek / Armenian / Cherokee), strict-ASCII regex shape, and codepoint-class threats (BIDI / null / control / zero-width).
The local-part is validated as ASCII atext only (RFC 5321 §4.1.2 / RFC 5322 §3.2.3). A unicode local-part (RFC 6531 SMTPUTF8 / EAI) is rejected as an address-syntax issue — keeping the mailbox name ASCII bounds homograph / confusable exposure to the domain side, where Punycode normalization and allowedScripts gating apply. RFC 6531 local-part acceptance re-opens behind a future explicit allowUnicodeLocalPart opt-in on operator demand. Domain-side IDN / Punycode and mixed-script handling are already supported.
var guardEmail = require("./lib/guard-email");
var rv = guardEmail.validateAddress("alice@example.com",
{ profile: "strict" });
rv.ok; // → true
rv.issues.length; // → 0
var bad = guardEmail.validateAddress("user@[10.0.0.1]",
{ profile: "strict" });
bad.ok; // → false
bad.issues[0].kind; // → "ip-literal"
b.guardEmail.validateMessage(input, opts) #
{
profile: "strict" | "balanced" | "permissive",
compliancePosture: "hipaa" | "pci-dss" | "gdpr" | "soc2",
crlfHeaderInjectionPolicy: "reject" | "audit" | "allow",
smtpSmugglingPolicy: "reject" | "audit" | "allow",
bareCrPolicy: "reject" | "audit" | "allow",
bareLfPolicy: "reject" | "audit" | "allow",
displayNameSpoofPolicy: "reject" | "audit" | "allow",
bomPolicy: "reject" | "audit" | "strip" | "allow",
maxHeaderLineBytes: number,
maxHeaders: number,
maxBytes: number,
}
Validate a complete RFC 5322 message (headers + body) against the active profile. Splits the header section, unfolds folded continuation lines, walks every single-line header for embedded CR/LF (header-injection class), and runs validateAddress on each envelope under address-bearing headers (From / To / Cc / Bcc / Reply-To / Sender / Return-Path). Body is scanned for SMTP-smuggling vectors (bare CR / bare LF / smuggled MAIL FROM: after a bare line ending — CVE-2023-51764 / 51765 / 51766 class). Caps RFC 5322 §2.1.1 998-byte line, configurable header count, and total maxBytes.
var guardEmail = require("./lib/guard-email");
var msg = "From: alice@example.com\r\n" +
"To: bob@example.com\r\n" +
"Subject: hello\r\n" +
"Date: Mon, 5 May 2026 10:00:00 +0000\r\n\r\n" +
"Hello.\r\n";
var rv = guardEmail.validateMessage(msg, { profile: "strict" });
rv.ok; // → true
// Header injection: a CRLF inside the From value forges a Bcc.
var bad = "From: alice@example.com\r\nBcc: leak@evil\r\n" +
"To: bob@example.com\r\nSubject: hi\r\n\r\nbody\r\n";
var injected = guardEmail.validateMessage(bad, { profile: "strict" });
injected.ok; // → true (well-formed; injected-line is its own header)
b.guardEmail.validate(input, opts) #
{
profile: "strict" | "balanced" | "permissive",
compliancePosture: "hipaa" | "pci-dss" | "gdpr" | "soc2",
}
Auto-routing entry: a string with no newline AND no : is treated as a single address (delegates to validateAddress); otherwise the input is treated as a full RFC 5322 message (delegates to validateMessage). Operators who want a fixed shape — never the heuristic — call the specific entry directly.
var guardEmail = require("./lib/guard-email");
guardEmail.validate("alice@example.com",
{ profile: "strict" }).ok; // → true
var msg = "From: a@example.com\r\nTo: b@example.com\r\n" +
"Subject: x\r\nDate: Mon, 5 May 2026 10:00:00 +0000\r\n\r\nhi\r\n";
guardEmail.validate(msg,
{ profile: "strict" }).ok; // → true
b.guardEmail.sanitize(input, opts) #
{
profile: "strict" | "balanced" | "permissive",
compliancePosture: "hipaa" | "pci-dss" | "gdpr" | "soc2",
bidiPolicy: "reject" | "audit" | "strip" | "allow",
controlPolicy: "reject" | "audit" | "strip" | "allow",
zeroWidthPolicy: "reject" | "audit" | "strip" | "allow",
}
Best-effort sanitize for email content. THROWS on critical-severity issues (SMTP smuggling / CRLF header injection / multi-@ / mixed-script confusable / null byte) — these have no safe sanitization. Lower-severity codepoint-class threats (BIDI / zero- width / control / BOM) are stripped per the active profile. Never silently drops a smuggling vector: the caller either gets sanitized text or a thrown GuardEmailError.
var guardEmail = require("./lib/guard-email");
// CRLF in the From value is a header-injection vector — sanitize
// refuses rather than silently dropping the bytes.
var hostile = "From: alice@example.com\rBcc: leak@evil\r\n" +
"To: bob@example.com\r\nSubject: hi\r\n\r\nbody\r\n";
var threw = false;
try { guardEmail.sanitize(hostile, { profile: "strict" }); }
catch (e) { threw = (e.code || "").indexOf("email.") === 0; }
threw; // → true
// Benign input with a stray BIDI override is stripped under balanced.
var clean = guardEmail.sanitize("hello world",
{ profile: "balanced" });
clean; // → "hello world"
b.guardEmail.gate(opts) #
{
profile: "strict" | "balanced" | "permissive",
compliancePosture: "hipaa" | "pci-dss" | "gdpr" | "soc2",
name: string, // gate identifier surfaced in audit metadata
}
Build a guard gate compatible with the b.guardAll family dispatch. The returned gate's async check(ctx) method accepts a request-shaped context, runs validateMessage against the extracted bytes, and returns { ok, action, issues? } where action is serve (no issues), audit-only (warn-level), or refuse (high / critical severity).
var guardEmail = require("./lib/guard-email");
var g = guardEmail.gate({ profile: "strict" });
typeof g.check; // → "function"
var msg = "From: alice@example.com\r\nTo: bob@example.com\r\n" +
"Subject: hi\r\nDate: Mon, 5 May 2026 10:00:00 +0000\r\n\r\nbody\r\n";
g.check({ body: Buffer.from(msg, "utf8") }).then(function (rv) {
rv.action; // → "serve"
});
b.guardEmail.compliancePosture(name) #
Look up a compliance-posture overlay by name (one of "hipaa" / "pci-dss" / "gdpr" / "soc2"). Returns a fresh clone of the posture overlay so the caller may mutate it freely without disturbing the shared table. Throws GuardEmailError with code "email.bad-posture" when the name is not one this guard maps. Wired by gateContract.defineGuard through gateContract.lookupCompliancePosture, so the clone semantics and error code are identical across every guard in the family.
var posture = b.guardEmail.compliancePosture("hipaa");
posture; // → overlay clone (mutable)
try {
b.guardEmail.compliancePosture("not-a-regime");
} catch (e) {
e.code; // → "email.bad-posture"
}
b.guardEmail.buildProfile(opts) #
{
extends: string|string[], // base profile name(s) to compose
...: any guard key, // inline override of resolved keys
}
Compose a derived profile from one or more named bases plus inline overrides, resolving names through this guard's own PROFILES table. opts.extends is a base profile name ("strict" / "balanced" / "permissive") or an array of names — later entries shadow earlier ones, and inline opts keys win last. Wired by gateContract.defineGuard through gateContract.makeProfileBuilder, so operator-defined profiles stay traceable to a baseline instead of a hand-typed dictionary.
var custom = b.guardEmail.buildProfile({ extends: "strict" });
custom; // → composed profile object
b.guardEmail.loadRulePack(pack) #
Register an operator-supplied rule pack with this guard's rule-pack registry. The pack is identified by pack.id (a non-empty string) and stored for later dispatch by gates that opt in via opts.rulePackId. Returns the pack unchanged on success; throws GuardEmailError with code "email.bad-opt" when pack is missing or pack.id is not a non-empty string. Wired by gateContract.defineGuard through gateContract.makeRulePackLoader, so storage shape and validation are identical across the family.
var pack = b.guardEmail.loadRulePack({ id: "tenant-policy", rules: [] });
pack.id; // → "tenant-policy"
b.guardEmail.resolveOpts(opts?) #
{
profile: string, // one of PROFILES; default this guard's default
compliancePosture: string, // overlay one of hipaa/pci-dss/gdpr/soc2
}
Resolve caller opts against this guard's PROFILES + compliance-posture overlays into the fully-defaulted option set the guard runs on — the same resolution validate / sanitize / gate apply internally. Wired by gateContract.defineGuard from the guard's binding config (profiles / postures / defaults / error class), so a guard's bespoke gate calls resolveOpts instead of re-declaring the per-guard resolver wrapper. Throws GuardEmailError with code "email.bad-opt" / "email.bad-posture" on an unknown profile or posture name.
var resolved = b.guardEmail.resolveOpts({ profile: "strict" });
resolved.profile; // → "strict"
Last updated 2026-08-08T16:39:49.652Z by seeder.