Mail Spam Score
Operator-supplied spam scorer facade. The framework deliberately does NOT vendor a spam-classifier engine — the bayes corpora, URIBL caches, neural models, and per-recipient training are all operator state. Instead, b.mail.spamScore.create wraps the operator's chosen scorer (SpamAssassin via spamc, Rspamd HTTP API, Cloudmark, Vade, in-house) in a uniform threshold-driven verdict pipeline that the MX listener (v0.9.45) and submission listener (v0.9.47) consume.
## Operator-supplied scorer contract
async function scorer({ rawBytes, headers, envelope }) {
// call out to SpamAssassin / Rspamd / commercial scorer
return { score: 7.3, reasons: ["BAYES_99", "URIBL_RED"] };
}
- score MUST be a finite number (any range; the threshold is operator-tuned). Negative scores mean "ham-shaped"; positive scores mean "spam-shaped". Convention matches SpamAssassin. - reasons MUST be an Array of short ASCII tags. The facade caps each tag at 256 bytes and refuses control bytes; the cap protects audit storage + outbound headers (X-Spam-Status: ...) from hostile expansion via a compromised scorer.
## Thresholds
- **strict** — 5.0 (matches SpamAssassin's default
required_score). - **balanced** — 7.5.
- **permissive** — 10.0.
Operators tune via opts.threshold per-instance. The verdict is "accept" (score < threshold), "score-tag" (score === threshold — add X-Spam-Status header but deliver), or "refuse" (score > threshold — return SMTP 550).
## Composition
- **b.audit** receives every score / accept / score_tag / refuse decision. Audit failure is drop-silent (hot path).
## Threat model
- **Hostile reason-tag** (compromised scorer injects CRLF into a tag, smuggling extra X-Spam-* headers into the outbound wrapper): defended by per-tag length cap + control-byte refusal. - **NaN / Infinity score** (scorer bug): refused as mail-spam-score/bad-score; the listener treats the message as unscanned (operator's tempfail policy applies). - **Slow scorer DoS**: the scorer function is operator code, so timing belongs to the operator. The listener wraps the .score() promise in its own per-connection deadline.
b.mail.spamScore.create(opts) #
{
scorer: async fn({ rawBytes, headers, envelope }) → { score, reasons } — required
threshold: number — overrides profile default
profile: "strict" | "balanced" | "permissive"
posture: "hipaa" | "pci-dss" | "gdpr" | "soc2"
audit: b.audit instance
}
Build a spam-score handle. Returns { score(message, opts), threshold, profile, MailSpamScoreError } where .score resolves to { score, reasons, verdict }. verdict is "accept" / "score-tag" / "refuse" based on threshold comparison.
var spam = b.mail.spamScore.create({
scorer: async function (ctx) {
return await callSpamAssassin(ctx.rawBytes);
},
});
var v = await spam.score({ rawBytes: msg });
if (v.verdict === "refuse") refuseConnection(v.reasons.join(","));
b.mail.spamScore.compliancePosture(posture) #
Return the effective profile name for a compliance posture, or null for unknown posture names.
b.mail.spamScore.compliancePosture("hipaa"); // → "strict"
Last updated 2026-08-08T16:39:49.652Z by seeder.