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
+13
View File
@@ -194,6 +194,14 @@ func (l *Lists) LastWritten() time.Time {
return newest
}
// Listable reports whether value could be added to a list as an entry of kind.
// The Inbound log asks before it offers a row's sender to the lists: an
// envelope sender may be spelt in ways the lists do not take.
func Listable(kind, value string) bool {
_, err := normalizeListValue(kind, value)
return err == nil
}
// normalizeListValue validates one entry and returns the form it is stored and
// written in: lower case for an address or a domain, the canonical text of an
// address or a network for an IP.
@@ -250,6 +258,11 @@ func normalizeListValue(kind, raw string) (string, error) {
if p.Bits() == 0 {
return "", invalid("%q is every address there is", v)
}
// A network of one address is that address: 192.0.2.7/32 and
// 192.0.2.7 are one entry, not two that look different.
if p.Bits() == p.Addr().BitLen() {
return p.Addr().String(), nil
}
return p.Masked().String(), nil
}
a, err := netip.ParseAddr(v)
+27
View File
@@ -46,6 +46,8 @@ func TestNormalizeListValue(t *testing.T) {
{store.SpamEntryDomain, "sub.evil.example", "sub.evil.example"},
{store.SpamEntryIP, "192.0.2.7", "192.0.2.7"},
{store.SpamEntryIP, "192.0.2.77/24", "192.0.2.0/24"},
{store.SpamEntryIP, "192.0.2.7/32", "192.0.2.7"},
{store.SpamEntryIP, "2001:db8::1/128", "2001:db8::1"},
{store.SpamEntryIP, "::ffff:192.0.2.7", "192.0.2.7"},
{store.SpamEntryIP, "2001:DB8:0:0::1", "2001:db8::1"},
{store.SpamEntryIP, "2001:db8:1::5/48", "2001:db8:1::/48"},
@@ -368,3 +370,28 @@ func TestListsUsageWithNoEntries(t *testing.T) {
t.Errorf("Usage = %v, %v", usage, err)
}
}
// The same address written as a network of one is the same entry.
func TestListsOneAddressNetworkIsTheAddress(t *testing.T) {
l, _, dir := testLists(t)
if _, err := l.Add(store.SpamListDeny, store.SpamEntryIP, "192.0.2.7", ""); err != nil {
t.Fatalf("Add: %v", err)
}
if _, err := l.Add(store.SpamListDeny, store.SpamEntryIP, "192.0.2.7/32", ""); !errors.Is(err, store.ErrSpamListEntryExists) {
t.Errorf("192.0.2.7/32 after 192.0.2.7: %v, want ErrSpamListEntryExists", err)
}
if got := strings.Join(mapLines(t, dir, "deny_ip.map"), " "); got != "192.0.2.7" {
t.Errorf("deny_ip.map = %q", got)
}
}
func TestListable(t *testing.T) {
for value, want := range map[string]bool{
"billing@vendor.example": true, "srs0=ab=cd=example.org=user@forwarder.example": true,
"o'brien@example.org": false, "a#b@example.org": false, "": false, "not-an-address": false,
} {
if got := Listable(store.SpamEntryAddress, value); got != want {
t.Errorf("Listable(address, %q) = %v, want %v", value, got, want)
}
}
}