The community build scores with authentication, network, heuristic, and Bayesian signals. More advanced content-analysis stages exist as a commercial extension and are deliberately absent here — the fields they would fill stay empty rather than being faked.
The pipeline, in order
- Trusted-peer check (ADR-031). Loopback and
[security] trusted_ipsskip SPF, DNSBL, and DMARC-policy rejection — but never content analysis, because a compromised local application can still spam. - Authentication verdicts. SPF, DKIM, DMARC, and ARC results (from the inbound verification pass) feed the score. A DMARC
p=rejectfailure is rejected outright with550 5.7.26before content scoring — that is an authentication decision, not a spam score. - DNSBL. The connecting IP is checked against the blocklists you configure (
[spam] dnsbl_servers, empty by default). A listing weighs into the score. - Content signals. Heuristics (URL-shortener chains, header forgery, structural anomalies) and a Bayesian classifier trained per organization. Two guardrails keep the Bayes honest: it trains on exactly the representation it scores (no train/score drift), and votes in its statistical dead zone are discarded rather than coerced into a verdict.
- Disposition (ADR-080). One shared function maps the final verdict to what happens:
Reject→550;Defer→451;Junk→ the Junk folder; suspected and bulk → the Inbox, tagged viaX-Spam-Status, never junked. Only a confident verdict moves mail out of sight.
- Trusted-reply whitelist. A correspondent you have replied to is whitelisted — but the whitelist is only honored when the inbound envelope is authenticated (SPF/DKIM aligned), so a forged
From:of a known contact gains nothing. - Spamtrap capture (ADR-077). Mail to a designated trap recipient is accepted and force-filed as
$junkground truth for the corpus, rather than rejected — the trap exists to collect exactly that mail.
No greylisting
There is no greylisting step, and no configuration turns one on. TheRCPT-time gate — temp-fail an unknown (IP, sender, recipient) triple with 451 and wait for a retry — was retired (ADR-128), because the assumption it rested on stopped holding: modern botnets retry, large legitimate senders answer from IP pools that never re-present the same triple, and the cost fell on exactly the correspondents you most want to reach you, the ones writing to you for the first time. It also meant accepting a database write before authentication.
Authentication enforcement and the scored pipeline carry that load instead. If you are migrating from a server where greylisting was doing visible work, the equivalents here are DNSBL weighting, the authentication verdicts, and the Bayesian corpus — not a knob to re-enable.
A score-conditional variant is specified — greylist only in the ambiguous score band, where a retry is evidence rather than a toll. It is not active in this build and is not operator-settable; the only thing running today is the janitor that ages out the old triple table. Nothing on this page depends on it.
Training and feedback
The Bayes corpus learns from user actions: moving a message to Junk trains it as spam, rescuing one from Junk trains it as ham (with a cap on repeated ham votes for the same message class, so one enthusiastic user cannot skew the model). The corpus is versioned by epoch: when the token representation changes, the model is rebuilt from the stored corpus rather than mixing incompatible generations:/admin/v1/spam/stats, /spam/test, /spam/feedback/{tenant}, and /spam/retrain/{tenant} expose the same operations for tooling. POST /spam/retrain/{tenant} runs a real retrain and returns its result (accuracy, samples used, model version); when no retrainer is configured — spam.retrain_enabled defaults to false since no box ships a training script — it answers 503 naming the two settings to change, never a success for work that did not happen.
Configuration
Anti-abuse: rate limiting and fail2ban
Protection against brute force and floods is in the binary — do not install the fail2ban daemon next to OxiMail:- Rate limiter: token bucket per IP (
[rate_limit]), covering HTTP and SMTP surfaces. - Fail2ban: authentication failures on every protocol (JMAP, IMAP, SMTP AUTH, DAV, ManageSieve) are tracked per IP; crossing
[security] fail2ban_max_attemptsbans the IP with escalation (60 min → 6 h → 24 h). Behind a proxy, the real client IP is read fromX-Forwarded-For(only from[server] trusted_proxies) or the PROXY protocol, so bans hit the attacker, not your proxy. - Operator surface:
oximail unban <ip>,oximail ban list|add, andGET/DELETE /admin/v1/bans— see Operations.
[security] trusted_ips bypasses both mechanisms for your own infrastructure.
Reading the verdict on a message
Every scored message carries anX-Spam-Status header with the verdict and the contributing signals — the first place to look when a user asks “why was this junked” (or “why was this not junked”). The delivery decision it produced is in the server logs with full context.