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

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) #

stable0.9.81
{
  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) #

stable0.9.81

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.