Mail Scan
Anti-virus / content-scan facade for inbound + outbound mail. Operators wire b.mail.scan.create({...}) once at boot, then call .scan(messageBytes, opts) from the MX listener (v0.9.45), submission listener (v0.9.47), or any custom pipeline that needs a verdict before delivering / forwarding a message.
## Backends
Two transport shapes are supported out of the box:
- **ICAP** (protocol: "icap" — default) — RFC 3507 Internet Content Adaptation Protocol. The framework speaks to a c-icap (or commercial) daemon over TCP, sends a REQMOD or RESPMOD request with the message body encapsulated, and parses the response through b.safeIcap.parse. The de-facto standard for Sophos / Symantec / Trend Micro / McAfee ICAP integrations.
- **ClamAV INSTREAM** (protocol: "clamav-instream") — the native ClamAV daemon protocol (no ICAP layer). Operator points at a clamd instance; the framework sends zINSTREAM\0, framed 4-byte-length-prefix chunks, then a zero-length terminator, and parses the line-shaped / response. See https://docs.clamav.net/manual/Usage/Configuration.html#instream
## Composition
- **b.safeIcap** owns the ICAP wire-protocol bounded parser (CRLF discipline, status-allowlist, body cap). Every ICAP byte routes through it before any field is trusted. - **b.guardArchive** is composed when opts.archiveEntries is supplied — the scanner refuses an archive with hostile entry metadata BEFORE shipping bytes to the AV daemon, so a zip-bomb can't reach the scanner's parser. Recursion-depth cap is the guard's profile-default. - **b.audit** receives every request / verdict / error / timeout via audit.safeEmit (the audit failure is drop-silent per the hot-path rule).
## Threat model
- **ICAP-response-injection (raw bytes → header injection)**: defended by b.safeIcap — bare-CR / bare-LF / NUL refused; status-code allowlist; bounded header / body / count caps. - **Parser-bomb on Encapsulated res-body** (hostile daemon ships arbitrary body length): defended by profile-tunable maxBodyBytes cap on the safeIcap parse path. - **DoS via slow daemon**: per-request wall-clock timeout (default 30s strict / 60s balanced / 120s permissive). After the timeout the scan resolves with { verdict: "error" } and the listener fails the message-handling step (operator's choice: tempfail / reject / accept-with-tag). - **Archive-bomb / zip-slip pre-AV**: defended by optional b.guardArchive.validateEntries composition when the operator enumerates entries before the AV scan.
## Why not "vendor an AV signature engine"?
AV signature databases are operator state, not framework state. ClamAV's signature set changes hourly; commercial scanners refresh their state through their own update channel. The framework's job is the wire-protocol parser + the operator-facing facade — the AV intelligence belongs to whatever daemon the operator deploys.
b.mail.scan.create(opts) #
{
host: string — required. ICAP / clamd hostname or IP.
port: number — required. ICAP port (default 1344) /
clamd port (default 3310).
service: string — ICAP service name (default "srv_clamav").
protocol: "icap" | "clamav-instream" — default "icap".
timeoutMs: number — per-request wall clock; default per profile.
profile: "strict" | "balanced" | "permissive".
posture: "hipaa" | "pci-dss" | "gdpr" | "soc2".
audit: b.audit instance (drop-silent on failure).
}
Build a mail-scan handle. Returns { scan(messageBytes, opts), profile, protocol, MailScanError } where .scan resolves to { verdict, icapResponse?, threats?, durationMs }:
- verdict: "clean" | "infected" | "error". - icapResponse: the structured b.safeIcap.parse result on ICAP backend (omitted on clamav-instream). - threats: ArraydurationMs: round-trip ms (audit / metrics).
var scanner = b.mail.scan.create({
host: "av.internal",
port: 1344,
service: "srv_clamav",
});
var verdict = await scanner.scan(rawMessage);
if (verdict.verdict === "infected") refuseMessage(verdict.threats);
b.mail.scan.compliancePosture(posture) #
Return the effective profile name for a compliance posture, or null for unknown posture names.
b.mail.scan.compliancePosture("hipaa"); // → "strict"
Last updated 2026-08-08T16:39:49.652Z by seeder.