The "Allow online reputation checks" setting did nothing. It saved a value, the
plugin sent a Settings-ID header, and the engine ignored it — the profiles that
header named were never written. Three separate faults had to be fixed before
one checkbox changed one score:
- the profiles did not exist
- a settings block is only selectable by header if it declares an `id`
- a local.d file holds the CONTENTS of a module block, not the block; the
outer `settings { }` wrapper nested them a level too deep and rspamd logged
"loaded 0 elements of settings"
Proven rather than assumed, using Spamhaus's documented dbltest.com:
webmail_offline -> no reputation symbols
webmail_network -> DBL_SPAM fires
Which lookups exist at all is narrowed in rbl.conf to the three chosen:
Spamhaus ZEN (sending IP), Spamhaus DBL (domains in links), DNSWL (an
allow-list, which can only subtract score). Everything else is off — commercial
sender-reputation businesses, and anything that sends a fingerprint of the
message body to a third party. NiX Spam was dropped after checking: its zone
returns SERVFAIL from every public resolver, so it could only ever have added
failing lookups.
The URL blocklists are keyed by quoted uppercase names deeper in the stock
config and survived a first pass of this work; the comment says how to verify.
Help text now names the services and says what they learn, rather than vaguely
warning about "third-party servers".
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018SqJYt7jD7oUEkoY68sBZo
Second-pass spam filtering per SPEC Appendix C. The engine is Rspamd in its own
container; this plugin is glue and policy, never a spam engine of its own.
Classification (§C.4) hooks message.new, acts only on inbox arrivals of enabled
accounts, and runs deny list -> allow list -> engine -> thresholds. Every
failure path ends in "deliver normally, unclassified": an unreachable engine, a
missing blob, an account with no Junk folder, a failed verdict write. Nothing
here may delay or lose mail, so no service declares depends_on: rspamd.
Two containers, started with everything else rather than behind a profile:
rspamd, and a redis of its own. The separate redis is not incidental — the app's
redis holds sync leases and arq queues under noeviction, and an ever-growing
Bayes corpus must never be able to fail a job enqueue. This one takes RDB
snapshots and evicts, because its data is derived and retrainable.
Network checks (RBL/SURBL/fuzzy) are off: they leak message-derived hashes and
URLs to third parties, which a self-hosted server's users did not ask for. The
engine makes no outbound connections in the default configuration.
The controller password is generated into .env like MASTER_KEY. It is internal;
humans will reach Rspamd through the admin panel, authenticated as the ordinary
administrator, rather than typing it.
local.d/groups.conf zeroes six SMTP-time symbols — DATE_IN_PAST,
PREVIOUSLY_DELIVERED, HFILTER_HOSTNAME_UNKNOWN, RCVD_NO_TLS_LAST, ARC_NA,
DMARC_NA — and greylisting is disabled. Measured against 398 real delivered
messages, those checks were firing on 44-71% of ordinary mail and pushing 11.3%
of it over the banner threshold; scoring mail at rest for facts true of every
delivered message is simply the wrong question. After: median 3.09 -> 0.70,
p95 7.00 -> 3.40, banner rate 11.3% -> 1.0%. The spec's 12.0/6.0 thresholds were
never the problem and are unchanged. Symbols are zeroed rather than removed so
verdicts stay explainable, and they matter again at the M6 edge (§C.7).
Still to come in C1: the §C.6 router and UI slots. C2 adds the learning loop —
until Bayes is trained, genuine scams and legitimate bulk mail overlap in the
5-8 band and no threshold separates them.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018SqJYt7jD7oUEkoY68sBZo