Show what the spam filter said, in the message details

The filter in front of the mailbox scores every delivered message and
writes its working into headers, and none of it was being read. A message
in Junk gave no reason for being there.

Nothing here scores anything. The headers are parsed and shown, so this
cannot disagree with the filter that actually made the decision.

Two formats cover what sits in front of a Stalwart mailbox in practice:
the SpamAssassin-shaped X-Spam-* set, which Stalwart's own filter writes,
and Rspamd's X-Spamd-Result. A header in neither shape is left unread
rather than guessed at, since a misparsed score shown confidently is worse
than no panel at all. Mail that arrived without any of them shows nothing.

Rules are listed largest mover first and signed, because which way a rule
pushed is the point, and the biggest contributor is the answer to why the
message scored what it did.

Two things it deliberately will not do. A score is always given the
threshold it was measured against, because 6.7 is damning against 5 and
unremarkable against 15 -- the number alone is not something a reader can
act on; where no threshold was stated, it says so rather than implying
one. And where the filter recorded no verdict, none is derived from score
against threshold: the filter applies policy we cannot see, and putting a
verdict in its mouth would be inventing one.

The mock writes the same headers at delivery -- spam in Junk, clean in the
Inbox, nothing on mail this account wrote -- so the panel can be developed
and demoed against it.
This commit is contained in:
2026-09-01 21:45:22 -07:00
parent 34578ba426
commit 63c2839602
7 changed files with 335 additions and 0 deletions
+12
View File
@@ -231,6 +231,18 @@ same query string — so what it builds can be read, edited and learned from.
- **Show original**, **Show headers**, **Download (.eml)** and **Print**.
- **Unsubscribe** where the message carries `List-Unsubscribe`.
- **Sender details** expand to the full From/To/Cc/Reply-To with addresses.
- **What the spam filter said** sits in those details, read back off the
message rather than scored here: the verdict, the score, the threshold it was
measured against, and the rules that moved it, largest mover first and signed
so which way each pushed is visible. Both the SpamAssassin-shaped `X-Spam-*`
set that Stalwart's own filter writes and Rspamd's `X-Spamd-Result` are read;
anything else is left alone rather than guessed at. Two things it will not
do: a score is always given the threshold it was measured against, because
6.7 is damning against 5 and unremarkable against 15 and the number alone is
not something a reader can act on — where no threshold was stated, it says
so; and where the filter recorded no verdict, none is invented from the score,
since the filter applies policy ihasmail cannot see. Mail that arrived without
these headers shows nothing.
- **Message body theming** is off by default — sender HTML is left exactly as it
was designed, on a light card. One setting lets mail that brings no colours of
its own follow the app's theme instead.