// Package dmarc implements basic DMARC (RFC 7489) policy evaluation. // Only stdlib net is used for DNS. Supports p=none, p=quarantine, p=reject. package dmarc import ( "fmt" "net" "strings" "ghb.freebede.com/nahakubuilder/mailgosend/internal/spf" ) // Policy represents the DMARC disposition policy. type Policy int const ( PolicyNone Policy = iota // p=none PolicyQuarantine // p=quarantine PolicyReject // p=reject ) func (p Policy) String() string { return [...]string{"none", "quarantine", "reject"}[p] } // Result holds the DMARC evaluation outcome. type Result struct { Pass bool Policy Policy Disposition string // none | quarantine | reject Reason string } // Check evaluates DMARC policy for the given envelope-from domain. // spfPass: whether SPF passed for this envelope-from domain. // dkimDomains: set of signing domains that produced a valid DKIM signature. // Returns a Result and logs no external calls beyond DNS. func Check(fromDomain string, spfResult spf.Result, dkimDomains []string) *Result { record, err := fetchDMARC(fromDomain) if err != nil || record == "" { // No DMARC record — not a failure, but can't enforce. return &Result{ Pass: true, Policy: PolicyNone, Disposition: "none", Reason: "no DMARC record for " + fromDomain, } } policy := parsePolicy(record) // SPF alignment (relaxed): envelope-from domain must equal or be a subdomain // of the DMARC organisational domain. orgDomain := orgDomainOf(fromDomain) spfAligned := spfResult == spf.ResultPass // DKIM alignment (relaxed): at least one valid DKIM signature domain must // equal or be a subdomain of the org domain. dkimAligned := false for _, d := range dkimDomains { if strings.EqualFold(d, fromDomain) || strings.HasSuffix(strings.ToLower(d), "."+strings.ToLower(orgDomain)) { dkimAligned = true break } } pass := spfAligned || dkimAligned reason := fmt.Sprintf("SPF=%v DKIM-aligned=%v policy=%s", spfAligned, dkimAligned, policy) if pass { return &Result{ Pass: true, Policy: policy, Disposition: "none", Reason: reason, } } return &Result{ Pass: false, Policy: policy, Disposition: policy.String(), Reason: reason, } } // fetchDMARC queries TXT at _dmarc. and returns the first DMARC record. func fetchDMARC(domain string) (string, error) { txts, err := net.LookupTXT("_dmarc." + domain) if err != nil { if dnsErr, ok := err.(*net.DNSError); ok && dnsErr.IsNotFound { return "", nil } return "", err } for _, txt := range txts { txt = strings.TrimSpace(txt) if strings.HasPrefix(txt, "v=DMARC1") { return txt, nil } } return "", nil } // parsePolicy extracts the p= tag from a DMARC record. func parsePolicy(record string) Policy { for _, part := range strings.Split(record, ";") { part = strings.TrimSpace(part) if strings.HasPrefix(strings.ToLower(part), "p=") { switch strings.ToLower(part[2:]) { case "quarantine": return PolicyQuarantine case "reject": return PolicyReject } } } return PolicyNone } // orgDomainOf returns a simplified "organisational domain" — for this // implementation we use the domain as-is (a full PSL lookup is out of scope). func orgDomainOf(domain string) string { return strings.ToLower(domain) }