From 85642d5b12ba9c5044b67e0dc340eb625a530fe3 Mon Sep 17 00:00:00 2001 From: Steve Cliff Date: Sat, 27 Jun 2026 11:42:44 +0100 Subject: [PATCH] feat(policy): add ValidWhitelistAddress Co-Authored-By: Claude Opus 4.8 (1M context) --- internal/policy/policy.go | 30 ++++++++++++++++++++++++++++++ internal/policy/policy_test.go | 18 ++++++++++++++++++ 2 files changed, 48 insertions(+) create mode 100644 internal/policy/policy_test.go diff --git a/internal/policy/policy.go b/internal/policy/policy.go index 7c0e648..b651c59 100644 --- a/internal/policy/policy.go +++ b/internal/policy/policy.go @@ -3,6 +3,8 @@ package policy import ( + "errors" + "fmt" "net/mail" "strings" ) @@ -42,3 +44,31 @@ func MatchAddress(entries []string, addr string) bool { } return false } + +// ValidWhitelistAddress reports an error if s is not a usable whitelist entry. +// Accepted forms: a bare address "local@domain", or a domain wildcard "@domain". +// The domain must contain at least one dot. Display-name forms ("Bob ") +// are rejected because the store keeps the raw string, which would never match. +func ValidWhitelistAddress(s string) error { + s = strings.TrimSpace(s) + if s == "" { + return errors.New("address must not be empty") + } + bad := fmt.Errorf("invalid address %q: expected local@domain or @domain", s) + if strings.HasPrefix(s, "@") { + domain := s[1:] + if domain == "" || strings.Contains(domain, "@") || !strings.Contains(domain, ".") { + return bad + } + return nil + } + addr, err := mail.ParseAddress(s) + if err != nil || !strings.EqualFold(addr.Address, s) { + return bad // parse failure or a display-name/extra-token form + } + at := strings.LastIndex(addr.Address, "@") + if at < 1 || !strings.Contains(addr.Address[at+1:], ".") { + return bad + } + return nil +} diff --git a/internal/policy/policy_test.go b/internal/policy/policy_test.go new file mode 100644 index 0000000..e180ccf --- /dev/null +++ b/internal/policy/policy_test.go @@ -0,0 +1,18 @@ +package policy + +import "testing" + +func TestValidWhitelistAddress(t *testing.T) { + good := []string{"tk555@protonmail.com", "a.b@sub.example.co.uk", "@example.com", "@sub.example.com"} + for _, s := range good { + if err := ValidWhitelistAddress(s); err != nil { + t.Errorf("ValidWhitelistAddress(%q) = %v, want nil", s, err) + } + } + bad := []string{"", " ", "notanaddress", "@", "@nodot", "a@nodot", "Bob ", "a@b@c.com"} + for _, s := range bad { + if err := ValidWhitelistAddress(s); err == nil { + t.Errorf("ValidWhitelistAddress(%q) = nil, want error", s) + } + } +}