inbound-antispam-panel: three things the reviews left as found
test / test (push) Canceled after 0s

The owner asked for them to be put right (2026-10-12).

  - A refusal is not logged twice after an unclean stop. The tailer
    saves its place every five seconds, so on start it reads some
    lines again; for the lines it reads on start a row is written only
    if the same row is not there already (store.InsertInboundSpamOnce:
    same moment, client, sender, recipient, decision, engine, detail).
    Lines read as they are written are always new, so two identical
    refusals in one second stay two rows.
  - The Inbound log offers Deny / Allow sender only for a sender the
    lists would take (inbound.Listable): an envelope sender may be
    spelt in ways the form refuses, and the link led to a refusal.
  - A network of one address is stored as the address: 192.0.2.7/32
    and 192.0.2.7 were two entries of one list.

Checked on the test server, uncommitted tree: go vet ./... ok,
go test ./... ok (22 packages), make e2e ok.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mix
2026-10-12 08:17:23 +03:00
co-authored by Claude Opus 5.5
parent c731a98a0f
commit 89774ab847
14 changed files with 192 additions and 39 deletions
@@ -90,7 +90,7 @@ func (h *Handlers) HandleInboundLog(w http.ResponseWriter, r *http.Request) {
table := make([]view.InLogRow, len(rows))
for i, row := range rows {
table[i] = out.NewRow(view.InLogRowInput{
ID: row.ID, At: row.CreatedAt, From: row.From, Client: row.ClientIP, To: row.To, Subject: row.Subject,
ID: row.ID, Listable: inbound.Listable(store.SpamEntryAddress, row.From), At: row.CreatedAt, From: row.From, Client: row.ClientIP, To: row.To, Subject: row.Subject,
Decision: row.Decision, Engine: row.Engine, Detail: row.Detail,
})
}
+1 -1
View File
@@ -427,7 +427,7 @@ func inLogFixture() *InLog {
panic(err)
}
id++
return p.NewRow(InLogRowInput{ID: id, At: at, From: from, Client: client, To: to, Subject: subject,
return p.NewRow(InLogRowInput{ID: id, Listable: true, At: at, From: from, Client: client, To: to, Subject: subject,
Decision: decision, Engine: engine, Detail: detail})
}
return p.WithRows([]InLogRow{
+5 -2
View File
@@ -85,7 +85,10 @@ type InLogRow struct {
// From may be empty: refused mail is read from mail.log, which has no subject,
// and a filter that said nothing has no detail.
type InLogRowInput struct {
ID int64
ID int64
// Listable says the sender is an address the filter lists would take. A
// row whose sender they would refuse offers no way into them.
Listable bool
At time.Time
From string
Client string
@@ -149,7 +152,7 @@ func (p *InLog) NewRow(in InLogRowInput) InLogRow {
Rows: whyRows(in.Decision, in.Engine, in.Detail, in.Subject),
},
}
if p.IsGlobal {
if p.IsGlobal && in.Listable {
r.Action = senderAction(in.Decision, in.Engine, in.From)
}
return r
+8 -1
View File
@@ -147,10 +147,17 @@ func TestInLogSenderAction(t *testing.T) {
// The address in the link is data: it cannot leave the attribute.
p := NewInLog(admin(), 90, nil, InLogFilter{})
row := p.NewRow(InLogRowInput{At: time.Now(), From: `"><script>1</script>@x`, To: "b@acme.io", Decision: "accept", Engine: "rspamd"})
// (The handler would not call such a sender listable; the view must not
// depend on that.)
row := p.NewRow(InLogRowInput{Listable: true, At: time.Now(), From: `"><script>1</script>@x`, To: "b@acme.io", Decision: "accept", Engine: "rspamd"})
out := renderSignedIn(t, "in-log", p.WithRows([]InLogRow{row}, 1, 1))
pageLacks(t, "Inbound log", out, `<script>1`)
pageHas(t, "Inbound log", out, `value=%22%3E%3Cscript%3E1%3C%2Fscript%3E%40x`)
// A sender the lists would refuse is offered no way into them.
if row := p.NewRow(InLogRowInput{At: time.Now(), From: "o'brien@example.org", To: "b@acme.io", Decision: "accept", Engine: "rspamd"}); row.Action != nil {
t.Errorf("a sender that is not listable is offered %+v", *row.Action)
}
}
// What a sender wrote is text on the page: an address, a subject or a reply from