initial
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
// 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.<domain> 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)
|
||||
}
|
||||
Reference in New Issue
Block a user