package lists import "strings" // Compact trims, lowercases, normalizes, dedupes, and drops entries covered by a // broader wildcard in the same list (*@domain or *@*.suffix). func Compact(entries []string) []string { if len(entries) == 0 { return []string{} } unique := make([]string, 0, len(entries)) seen := make(map[string]struct{}, len(entries)) for _, value := range entries { entry := normalizeEntry(value) if entry == "" { continue } if _, ok := seen[entry]; ok { continue } seen[entry] = struct{}{} unique = append(unique, entry) } out := make([]string, 0, len(unique)) for i, entry := range unique { covered := false for j, other := range unique { if i == j { continue } if Covers(other, entry) && other != entry { covered = true break } } if !covered { out = append(out, entry) } } return out } // normalizeEntry lowercases and repairs common wildcard typos: // *domain.tld → *@domain.tld // *@*rumble.com → *@*.rumble.com func normalizeEntry(value string) string { entry := strings.ToLower(strings.TrimSpace(value)) if entry == "" { return "" } if strings.HasPrefix(entry, "*") && !strings.Contains(entry, "@") && strings.Contains(entry, ".") { entry = "*@" + strings.TrimPrefix(entry, "*") } local, domain, ok := splitAddress(entry) if ok && local == "*" && strings.HasPrefix(domain, "*") && !strings.HasPrefix(domain, "*.") { entry = "*@*." + strings.TrimPrefix(domain, "*") } return entry } // Covers reports whether broader covers specific. // Equal entries cover each other. // *@domain covers any local@domain for that domain. // *@*.suffix covers any local@host.suffix (domain glob). func Covers(broader, specific string) bool { broader = normalizeEntry(broader) specific = normalizeEntry(specific) if broader == "" || specific == "" { return false } if broader == specific { return true } broaderLocal, broaderDomain, broaderOK := splitAddress(broader) _, specificDomain, specificOK := splitAddress(specific) if !broaderOK || !specificOK { return false } if broaderLocal != "*" { return false } return domainMatches(broaderDomain, specificDomain) } func domainMatches(pattern, domain string) bool { if pattern == domain { return true } // *.suffix → any domain ending with .suffix (and longer than the suffix). if strings.HasPrefix(pattern, "*.") { suffix := pattern[1:] // ".shop", ".ac.in" return strings.HasSuffix(domain, suffix) && len(domain) > len(suffix) } return false } func splitAddress(entry string) (local, domain string, ok bool) { at := strings.LastIndex(entry, "@") if at <= 0 || at == len(entry)-1 { return "", "", false } return entry[:at], entry[at+1:], true } // FilterRecipientEntriesForDomain keeps recipient-deny entries that belong to // the given domain. Migadu rejects addresses for other domains on PATCH. // Bare local parts (no @) are kept and applied to every domain. func FilterRecipientEntriesForDomain(domainName string, entries []string) []string { domainName = strings.ToLower(strings.TrimSpace(domainName)) out := make([]string, 0, len(entries)) seen := make(map[string]struct{}, len(entries)) for _, value := range entries { entry := normalizeEntry(value) if entry == "" { continue } local, entryDomain, hasAt := splitAddress(entry) if hasAt { if entryDomain != domainName { continue } // Prefer local@domain form; keep as normalized. entry = local + "@" + entryDomain } // Bare local-part applies to this domain as-is. if _, ok := seen[entry]; ok { continue } seen[entry] = struct{}{} out = append(out, entry) } return out }