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:
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user