inbound-antispam-panel: Filter lists, kind column, matches, note on a sheet
Follows the design commit before it (owner's words quoted there). The lists' tables are Value, Kind, Matched and Remove. The kind tag opens the entry's sheet: note, day added, how often it decided a message in the days the log keeps, and the last match. Matches are read off the journal on each view, nothing is stored (inbound.Lists.Usage, store.ListSpamListHits). A row counts for an entry when the list decided the message - a rejected row whose reply is the deny rule's message, an accepted row whose verdict carries the allow rule's symbol for that kind - and the entry fits the row's sender or client address the way rspamd matches it: an address whole, a domain as the sender's domain, a network containing the client (an IPv4-mapped address as IPv4). A failure to count shows the lists with "never" and logs why. For that the journal milter keeps the lists' own symbols ahead of its six-symbol cap: rspamd writes symbols heaviest first, so an allow entry's -100 came last and was cut behind any six others. The plan gains "Filter list usage": what counting from the journal implies, and - the owner asked for it in writing - the conditions under which a counter kept with each entry becomes worth a migration (the journal cut by its row cap and not by age; a short retention; counts that must outlive the journal; a slow page). multimap.conf says which of its texts SelfPost reads back. Tests: Usage over every kind on both lists and the rows that are nobody's; the milter's symbol order; the store query; the page and its sheets; a handler test from journal rows to the numbers on the page. Seen: the fixture page with a sheet open, headless Edge at 1360 px - the same as the accepted drawing. Checked on the test server, uncommitted tree: go vet ./... ok, go test ./... ok (22 packages), make e2e ok. Not seen in a real run: a count above zero. It needs an entry added in the panel, which I cannot sign in to, and then mail that the list decides; in particular the deny rule's message arriving in mail.log as the reply of a milter-reject line is what the trial with rspamc showed, not yet what Postfix logged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -142,12 +142,16 @@ func (s *inboundSession) record() {
|
||||
}
|
||||
}
|
||||
|
||||
// What the journal keeps of a verdict: enough to see why, on one table row.
|
||||
// What the journal keeps of a verdict: enough to see why, on one line.
|
||||
const (
|
||||
verdictMaxSymbols = 6
|
||||
verdictMaxBytes = 255
|
||||
)
|
||||
|
||||
// ListSymbolPrefix starts the names of the symbols the filter lists' rules add
|
||||
// (deploy/antispam/rspamd/multimap.conf): SELFPOST_ALLOW_IP, _FROM, _DOMAIN.
|
||||
const ListSymbolPrefix = "SELFPOST_"
|
||||
|
||||
var (
|
||||
// "default: False [7.20 / 15.00]" - the score and the reject threshold.
|
||||
rspamdScore = regexp.MustCompile(`\[(-?[0-9]+(?:\.[0-9]+)?) / (-?[0-9]+(?:\.[0-9]+)?)\]`)
|
||||
@@ -182,15 +186,28 @@ func rspamdVerdict(actionHeader, resultHeader string) (action, detail string) {
|
||||
}
|
||||
detail = strings.Join(parts, ", ")
|
||||
|
||||
var symbols []string
|
||||
// The filter lists' own symbols come first and are kept whatever else there
|
||||
// is: Filter lists counts how often an entry decided by finding them here,
|
||||
// and rspamd writes symbols heaviest first, which puts an allow entry's
|
||||
// -100 last, behind any six others.
|
||||
var symbols, rest []string
|
||||
for _, m := range rspamdSymbol.FindAllStringSubmatch(resultHeader, -1) {
|
||||
if strings.Trim(m[2], "-0.") == "" { // weight zero: informational
|
||||
continue
|
||||
}
|
||||
symbols = append(symbols, m[1]+"("+m[2]+")")
|
||||
if strings.HasPrefix(m[1], ListSymbolPrefix) {
|
||||
if len(symbols) < verdictMaxSymbols {
|
||||
symbols = append(symbols, m[1]+"("+m[2]+")")
|
||||
}
|
||||
continue
|
||||
}
|
||||
rest = append(rest, m[1]+"("+m[2]+")")
|
||||
}
|
||||
for _, sym := range rest {
|
||||
if len(symbols) == verdictMaxSymbols {
|
||||
break
|
||||
}
|
||||
symbols = append(symbols, sym)
|
||||
}
|
||||
if len(symbols) > 0 {
|
||||
if detail != "" {
|
||||
|
||||
@@ -222,3 +222,15 @@ func TestInboundAcceptsWhenTheStoreFails(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// rspamd writes symbols heaviest first, so an allow entry's -100 comes last.
|
||||
// Filter lists counts an entry's matches by finding that symbol in the journal:
|
||||
// it is kept whatever else there is.
|
||||
func TestRspamdVerdictKeepsTheListSymbols(t *testing.T) {
|
||||
result := "default: False [-91.00 / 15.00]; SYM_A(3.00)[]; SYM_B(2.00)[]; SYM_C(1.50)[]; SYM_D(1.00)[]; SYM_E(0.80)[]; SYM_F(0.50)[]; SYM_G(0.20)[]; ARC_NA(0.00)[]; SELFPOST_ALLOW_FROM(-100.00)[billing@vendor.example]"
|
||||
_, detail := rspamdVerdict("no action", result)
|
||||
want := "no action, score -91.00 / 15.00; SELFPOST_ALLOW_FROM(-100.00) SYM_A(3.00) SYM_B(2.00) SYM_C(1.50) SYM_D(1.00) SYM_E(0.80)"
|
||||
if detail != want {
|
||||
t.Errorf("detail = %q\nwant %q", detail, want)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user