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
+26
View File
@@ -63,6 +63,32 @@ func (s *Store) InsertInboundSpam(e InboundSpamEntry) error {
return nil
}
// InsertInboundSpamOnce records a journal row unless the same row is there
// already - same moment, client, sender, recipient, decision, engine and
// detail - and says whether it wrote. The log tailer uses it for the lines it
// reads when it starts: after an unclean stop those are lines it may have read
// before, and a refusal read twice is otherwise two rows. e.At must be set; a
// row dated "now" is never the same as another.
func (s *Store) InsertInboundSpamOnce(e InboundSpamEntry) (bool, error) {
at := e.At.UTC().Format(time.RFC3339)
res, err := s.db.Exec(
`INSERT INTO inbound_spam_log
(inbound_domain, client_ip, from_addr, to_addr, subject, decision, engine, detail, created_at)
SELECT ?, ?, ?, ?, ?, ?, ?, ?, ?
WHERE NOT EXISTS (
SELECT 1 FROM inbound_spam_log
WHERE created_at = ? AND client_ip = ? AND from_addr = ? AND to_addr = ?
AND decision = ? AND engine = ? AND detail = ?)`,
e.InboundDomain, e.ClientIP, e.From, e.To, e.Subject, e.Decision, e.Engine, e.Detail, at,
at, e.ClientIP, e.From, e.To, e.Decision, e.Engine, e.Detail,
)
if err != nil {
return false, fmt.Errorf("insert inbound_spam_log once: %w", err)
}
n, _ := res.RowsAffected()
return n > 0, nil
}
// PruneInboundSpam deletes journal rows older than cutoff and then, if more
// than keep rows are left, the oldest of those. It returns how many went.
func (s *Store) PruneInboundSpam(cutoff time.Time, keep int) (int64, error) {
+26
View File
@@ -310,3 +310,29 @@ func TestPruneInboundSpamCapIsByTime(t *testing.T) {
t.Errorf("left = %+v, %v; want the two newest by time", rows, err)
}
}
func TestInsertInboundSpamOnce(t *testing.T) {
st := openTestStore(t)
e := InboundSpamEntry{
InboundDomain: "in.example.com", ClientIP: "203.0.113.7", From: "a@example.net", To: "nobody@in.example.com",
Decision: SpamDecisionReject, Engine: SpamEnginePostfix, Detail: "550 5.1.1 no",
At: time.Date(2026, 10, 12, 9, 0, 5, 0, time.UTC),
}
for i, want := range []bool{true, false, false} {
if wrote, err := st.InsertInboundSpamOnce(e); err != nil || wrote != want {
t.Fatalf("attempt %d: wrote %v, %v; want %v", i+1, wrote, err, want)
}
}
// Anything that tells two refusals apart makes a second row.
later, other := e, e
later.At = e.At.Add(time.Second)
other.To = "somebody@in.example.com"
for name, v := range map[string]InboundSpamEntry{"a second later": later, "another recipient": other} {
if wrote, err := st.InsertInboundSpamOnce(v); err != nil || !wrote {
t.Errorf("%s: wrote %v, %v", name, wrote, err)
}
}
if n, err := st.CountInboundSpam(InboundSpamFilter{}); err != nil || n != 3 {
t.Errorf("rows = %d, %v; want 3", n, err)
}
}