// Package policy holds pure enforcement functions: address matching, // inbound filtering, and (in a later phase) outbound checks. package policy import ( "errors" "fmt" "net/mail" "strings" ) // NormalizeAddr lower-cases an address and strips any display name/brackets. func NormalizeAddr(addr string) string { addr = strings.TrimSpace(addr) if a, err := mail.ParseAddress(addr); err == nil { return strings.ToLower(a.Address) } return strings.ToLower(strings.Trim(addr, "<> ")) } // MatchAddress reports whether addr matches any whitelist entry. // Entries beginning with '@' match a whole domain; others match exactly. func MatchAddress(entries []string, addr string) bool { norm := NormalizeAddr(addr) if norm == "" { return false } at := strings.LastIndex(norm, "@") domain := "" if at >= 0 { domain = norm[at:] // includes '@' } for _, e := range entries { e = strings.ToLower(strings.TrimSpace(e)) if strings.HasPrefix(e, "@") { if e == domain { return true } continue } if e == norm { return true } } 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 }