Skip to content

DKIM / SPF / DMARC Builder & Checker — Go source

Build the three email-authentication DNS records — SPF, DKIM, and DMARC — as ready-to-paste TXT values, or look up a domain's live records over DNS-over-HTTPS. Import existing records, catch publishing mistakes, share via URL. Entirely client-side.

This is the Go implementation — the same logic the interactive tool runs, in a shareable, citable form.

// Package dkimspfdmarc is the Go twin of CosmoDev's src/lib/dkim-spf-dmarc.ts
// (dual source: the web lib is TypeScript, the CLI lib is Go — kept in
// lock-step). Pure + deterministic, never panics. The table-driven tests in
// dkim-spf-dmarc_test.go share vectors with src/lib/dkim-spf-dmarc.test.ts so
// the two implementations are held to the same contract.
//
// This twin ports the lib's PURE surface only: domain/selector helpers, the
// SPF / DKIM / DMARC record parsers (RFC 7208 / 6376 / 7489), the zone-file
// record builder + validator + paste-importer, and the shareable-URL codecs.
// The DoH network lookups (checkSPF / checkDKIM / checkDMARC in the TS lib)
// stay OUT — the twin works on record STRINGS, exactly like the TS pure
// surface.
//
// Optional-field mapping (TS → Go): `undefined`-able string fields become ""
// ("absent" conflates with "" where the TS value space never uses ""); p= and
// pct= become *string / *int so present-vs-absent stays distinguishable;
// number|null percent is *int (nil = null).
package dkimspfdmarc

import (
	"encoding/base64"
	"fmt"
	"math"
	"net/url"
	"regexp"
	"strconv"
	"strings"
	"unicode"
)

// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------

// SPFQualifier is one of '+', '-', '~', '?'.
type SPFQualifier = string

// SPFMechanism is one parsed SPF term (qualifier + kind + optional value).
type SPFMechanism struct {
	Qualifier SPFQualifier
	Kind      string // all|include|a|mx|ip4|ip6|exists|ptr
	// Value is the domain/IP/CIDR argument after the colon; nil when the term
	// carried no ":value" part.
	Value *string
}

// SPFRecord is the result of parsing one or more (newline-joined) TXT strings
// for SPF.
type SPFRecord struct {
	Valid       bool
	Version     string
	Mechanisms  []SPFMechanism
	Redirect    string
	Exp         string
	LookupCount int // mechanisms that cost a DNS query under RFC 7208 §4.6.4 (limit 10)
	RecordCount int // number of v=spf1 records found (>1 is a hard error for receivers)
	Warnings    []string
}

// DKIMRecord is the result of parsing a `<selector>._domainkey` TXT record.
type DKIMRecord struct {
	Valid     bool
	Version   string // upper-cased v= tag; "" when absent
	KeyType   string
	PublicKey *string // base64, whitespace-stripped; nil until successfully read
	KeyBits   *int    // approximate modulus size (RSA only; derived from DER byte length)
	Hashes    []string
	Services  []string
	Flags     []string
	Warnings  []string
}

// DMARCRecord is the result of parsing a `_dmarc` TXT record.
type DMARCRecord struct {
	Valid           bool
	Policy          string // none|quarantine|reject; "" until validated
	SubdomainPolicy string
	AggregateURIs   []string
	ForensicURIs    []string
	Percent         *int
	DKIMAlignment   string // "" absent, else "r"|"s"
	SPFAlignment    string // "" absent, else "r"|"s"
	Warnings        []string
}

// ---------------------------------------------------------------------------
// Shared helpers (mirror normalizeDomain / isDomainLike / isValidSelector)
// ---------------------------------------------------------------------------

var (
	schemeRe      = regexp.MustCompile(`(?i)^[a-z][a-z0-9+.-]*://`)
	mailtoRe      = regexp.MustCompile(`(?i)^mailto:`)
	trailingDots  = regexp.MustCompile(`\.+$`)
	domainRe      = regexp.MustCompile(`^(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z]{2,}$`)
	selectorRe    = regexp.MustCompile(`(?i)^[a-z0-9][a-z0-9._-]*$`)
	quotedRe      = regexp.MustCompile(`^"(.*)"$`)
	whitespaceRe  = regexp.MustCompile(`\s+`)
	spfLineRe     = regexp.MustCompile(`(?i)^v=spf1(?:\s|$)`)
	spfModifierRe = regexp.MustCompile(`(?i)^([a-z][a-z0-9-]*)=(.*)$`)
	spfMechRe     = regexp.MustCompile(`(?i)^([+\-~?])?([a-z0-9]+)(?::(.*))?$`)
	spfPayloadRe  = regexp.MustCompile(`(?i)v=spf1(\s|$)`)
	dkimPayloadRe = regexp.MustCompile(`(?i)v=DKIM`)
	dmarcPayRe    = regexp.MustCompile(`(?i)v=DMARC`)
	spfTokenRe    = regexp.MustCompile(`(?i)^v=spf1$`)
	spfLeadRe     = regexp.MustCompile(`(?i)^\s*v=spf1\s*`)
)

// NormalizeDomain strips a leading scheme, userinfo, path, query, port, and
// trailing dot(s) from user input, returning the bare lowercase host.
func NormalizeDomain(raw string) string {
	s := strings.TrimSpace(raw)
	s = schemeRe.ReplaceAllString(s, "")
	s = mailtoRe.ReplaceAllString(s, "")
	if strings.Contains(s, "@") {
		s = s[strings.LastIndex(s, "@")+1:]
	}
	s = strings.Split(s, "/")[0]
	s = strings.Split(s, "?")[0]
	s = strings.Split(s, ":")[0]
	s = trailingDots.ReplaceAllString(s, "")
	return strings.ToLower(s)
}

// IsDomainLike reports whether the string looks like a plausible multi-label
// domain (example.com).
func IsDomainLike(domain string) bool {
	return domainRe.MatchString(domain) && len(domain) <= 253
}

// IsValidSelector reports whether the selector is a safe single DNS label
// chain (no spaces, no traversal).
func IsValidSelector(selector string) bool {
	s := strings.TrimSpace(selector)
	return len(s) > 0 && len(s) <= 100 && selectorRe.MatchString(s) && !strings.Contains(s, "..")
}

// ---------------------------------------------------------------------------
// SPF — RFC 7208 (mirrors parseSPF)
// ---------------------------------------------------------------------------

var spfLookupKinds = map[string]bool{
	"include": true, "a": true, "mx": true, "exists": true, "ptr": true,
}

var spfKnownKinds = map[string]bool{
	"all": true, "include": true, "a": true, "mx": true,
	"ip4": true, "ip6": true, "exists": true, "ptr": true,
}

// ParseSPF parses one or more (newline-joined) TXT record strings for SPF.
func ParseSPF(txt string) SPFRecord {
	var lines []string
	for _, l := range strings.Split(txt, "\n") {
		t := strings.TrimSpace(l)
		t = quotedRe.ReplaceAllString(t, "$1")
		if t != "" {
			lines = append(lines, t)
		}
	}
	var spfLines []string
	for _, l := range lines {
		if spfLineRe.MatchString(l) {
			spfLines = append(spfLines, l)
		}
	}
	warnings := []string{}

	if len(spfLines) == 0 {
		return SPFRecord{
			Valid:      false,
			Mechanisms: []SPFMechanism{},
			Warnings:   []string{"No v=spf1 record found in the supplied text."},
		}
	}
	if len(spfLines) > 1 {
		warnings = append(warnings, fmt.Sprintf("%d SPF records found — RFC 7208 allows exactly one. Receivers treat this as a permanent error and ignore SPF for the whole domain.", len(spfLines)))
	}

	terms := strings.Fields(spfLines[0])
	version := terms[0]
	mechanisms := []SPFMechanism{}
	redirect := ""
	exp := ""

	for _, term := range terms[1:] {
		// Modifiers use '=': redirect= and exp=
		if m := spfModifierRe.FindStringSubmatch(term); m != nil {
			name := strings.ToLower(m[1])
			if name == "redirect" {
				redirect = m[2]
			} else if name == "exp" {
				exp = m[2]
			} else {
				warnings = append(warnings, fmt.Sprintf("Unknown modifier %q ignored.", term))
			}
			continue
		}
		idx := spfMechRe.FindStringSubmatchIndex(term)
		if idx == nil {
			warnings = append(warnings, fmt.Sprintf("Unrecognized term %q ignored.", term))
			continue
		}
		qualifier := "+"
		if idx[2] >= 0 {
			qualifier = term[idx[2]:idx[3]]
		}
		kind := strings.ToLower(term[idx[4]:idx[5]])
		var value *string
		if idx[6] >= 0 {
			v := term[idx[6]:idx[7]]
			value = &v
		}
		if !spfKnownKinds[kind] {
			warnings = append(warnings, fmt.Sprintf("Unknown mechanism %q ignored.", term))
			continue
		}
		if (value == nil || *value == "") && (kind == "include" || kind == "exists") {
			warnings = append(warnings, fmt.Sprintf("Mechanism %q is missing its required value.", term))
			continue
		}
		mechanisms = append(mechanisms, SPFMechanism{Qualifier: qualifier, Kind: kind, Value: value})
	}

	lookupCount := 0
	for _, mech := range mechanisms {
		if spfLookupKinds[mech.Kind] {
			lookupCount++
		}
	}
	if redirect != "" {
		lookupCount++
	}

	var all *SPFMechanism
	for i := range mechanisms {
		if mechanisms[i].Kind == "all" {
			all = &mechanisms[i]
			break
		}
	}
	hasPtr := false
	for _, mech := range mechanisms {
		if mech.Kind == "ptr" {
			hasPtr = true
			break
		}
	}
	if all == nil && redirect == "" {
		warnings = append(warnings, `No "all" mechanism and no "redirect=" — unmatched senders get a Neutral result, so anyone can still send mail that looks like this domain.`)
	}
	if all != nil && all.Qualifier == "+" {
		warnings = append(warnings, `"+all" explicitly allows every host on the internet to send mail as this domain — this defeats SPF entirely.`)
	} else if all != nil && all.Qualifier == "?" {
		warnings = append(warnings, `"?all" (Neutral) lets unmatched senders through with no protection. Prefer "~all" or "-all".`)
	}
	if hasPtr {
		warnings = append(warnings, `The "ptr" mechanism is deprecated (RFC 7208 §5.5) and should not be used.`)
	}
	if redirect != "" && all != nil {
		warnings = append(warnings, `A "redirect=" modifier is ignored when an "all" mechanism is present.`)
	}
	if lookupCount > 10 {
		warnings = append(warnings, fmt.Sprintf("%d DNS-lookup mechanisms — RFC 7208 §4.6.4 caps SPF at 10. Receivers that hit the cap return permerror and ignore the record.", lookupCount))
	}

	return SPFRecord{
		Valid:       true,
		Version:     version,
		Mechanisms:  mechanisms,
		Redirect:    redirect,
		Exp:         exp,
		LookupCount: lookupCount,
		RecordCount: len(spfLines),
		Warnings:    warnings,
	}
}

// ---------------------------------------------------------------------------
// DKIM — RFC 6376 (mirrors parseDKIM)
// ---------------------------------------------------------------------------

// decodeBase64Lenient decodes base64 to bytes without failing the caller on
// bad input (returns ok=false instead). Callers pass trimmed, non-empty values
// (empty p= is handled before this).
func decodeBase64Lenient(b64 string) ([]byte, bool) {
	clean := whitespaceRe.ReplaceAllString(b64, "")
	padded := clean + strings.Repeat("=", (4-len(clean)%4)%4)
	dst, err := base64.StdEncoding.DecodeString(padded)
	if err != nil {
		return nil, false
	}
	return dst, true
}

// parseTags splits a semicolon-separated tag list into a lowercase-key map,
// mirroring the shared loop in parseDKIM / parseDMARC. Malformed terms
// (no '=' or empty key) produce warnings and are skipped.
func parseTags(txt string, warnings *[]string) map[string]string {
	tags := map[string]string{}
	for _, part := range strings.Split(txt, ";") {
		term := strings.TrimSpace(quotedRe.ReplaceAllString(strings.TrimSpace(part), "$1"))
		if term == "" {
			continue
		}
		eq := strings.Index(term, "=")
		if eq <= 0 {
			*warnings = append(*warnings, fmt.Sprintf("Malformed tag %q ignored.", term))
			continue
		}
		tags[strings.ToLower(strings.TrimSpace(term[:eq]))] = strings.TrimSpace(term[eq+1:])
	}
	return tags
}

// splitColonList splits a ':'-separated tag value, trimmed and empty-removed.
func splitColonList(v string) []string {
	parts := strings.Split(v, ":")
	out := make([]string, 0, len(parts))
	for _, s := range parts {
		if t := strings.TrimSpace(s); t != "" {
			out = append(out, t)
		}
	}
	return out
}

// ParseDKIM parses a `<selector>._domainkey` TXT record.
func ParseDKIM(txt string) DKIMRecord {
	warnings := []string{}
	tags := parseTags(txt, &warnings)

	version, hasVersion := tags["v"]
	if hasVersion && version != "" && strings.ToUpper(version) != "DKIM1" {
		warnings = append(warnings, fmt.Sprintf("Unusual version tag v=%s (expected DKIM1).", version))
	}
	keyType := "rsa"
	if k, ok := tags["k"]; ok {
		keyType = k
	}
	p, hasP := tags["p"]
	hashes := splitColonList(tags["h"])
	services := splitColonList(tags["s"])
	flags := splitColonList(tags["t"])

	record := DKIMRecord{
		Valid:    true,
		KeyType:  keyType,
		Hashes:   hashes,
		Services: services,
		Flags:    flags,
		Warnings: warnings,
	}
	if hasVersion && version != "" {
		record.Version = strings.ToUpper(version)
	}

	if !hasP {
		record.Valid = false
		record.Warnings = append(record.Warnings, "No p= tag — this record is not a usable DKIM key.")
		return record
	}
	if p == "" {
		record.Warnings = append(record.Warnings, "p= is empty — the key is revoked. Receivers will treat mail signed with this selector as unsigned.")
		return record
	}

	decoded, ok := decodeBase64Lenient(p)
	if !ok {
		record.Warnings = append(record.Warnings, "The p= value is not valid base64 — the key could not be read.")
		return record
	}
	publicKey := whitespaceRe.ReplaceAllString(p, "")
	record.PublicKey = &publicKey

	if keyType == "rsa" {
		// SubjectPublicKeyInfo DER ≈ modulus bits/8 + ~24 bytes of ASN.1 overhead.
		// The estimate is close enough to classify 512/1024/2048/4096-bit keys.
		bits := max(0, len(decoded)-24) * 8
		record.KeyBits = &bits
		if bits < 1024 {
			record.Warnings = append(record.Warnings, fmt.Sprintf("Weak RSA key (~%d bits). Keys under 1024 bits are considered breakable; RFC 8301 discourages short keys.", bits))
		} else if bits < 2048 {
			record.Warnings = append(record.Warnings, fmt.Sprintf("RSA key of ~%d bits works today but is below the recommended 2048 bits (RFC 8301).", bits))
		}
	}
	hasFlag := func(f string) bool {
		for _, x := range flags {
			if x == f {
				return true
			}
		}
		return false
	}
	if hasFlag("y") {
		record.Warnings = append(record.Warnings, "t=y — the key is in test mode: receivers must treat signatures as if unsigned.")
	}
	if hasFlag("s") {
		record.Warnings = append(record.Warnings, "t=s — strict domain matching: the key cannot be used for subdomain signatures (informational).")
	}
	return record
}

// ---------------------------------------------------------------------------
// DMARC — RFC 7489 (mirrors parseDMARC)
// ---------------------------------------------------------------------------

var dmarcPolicies = map[string]bool{"none": true, "quarantine": true, "reject": true}

// tsNumber mirrors JS Number() for the pct= tag: '' → 0; numeric strings parse;
// anything else is not a number.
func tsNumber(s string) (float64, bool) {
	if s == "" {
		return 0, true
	}
	f, err := strconv.ParseFloat(s, 64)
	if err != nil {
		return 0, false
	}
	return f, true
}

// ParseDMARC parses a `_dmarc` TXT record.
func ParseDMARC(txt string) DMARCRecord {
	warnings := []string{}
	tags := parseTags(txt, &warnings)

	record := DMARCRecord{
		Valid:         true,
		AggregateURIs: []string{},
		ForensicURIs:  []string{},
		Warnings:      warnings,
	}
	version, hasVersion := tags["v"]

	if !hasVersion || version == "" {
		record.Valid = false
		record.Warnings = append(record.Warnings, "No v= tag — this is not a DMARC record.")
		return record
	}
	if strings.ToUpper(version) != "DMARC1" {
		record.Valid = false
		record.Warnings = append(record.Warnings, fmt.Sprintf("Unknown version v=%s (expected DMARC1).", version))
		return record
	}

	p, hasP := tags["p"]
	policy := strings.ToLower(p)
	if !hasP || policy == "" {
		record.Valid = false
		record.Warnings = append(record.Warnings, "No p= policy tag — DMARC requires it.")
		return record
	}
	if !dmarcPolicies[policy] {
		record.Valid = false
		record.Warnings = append(record.Warnings, fmt.Sprintf("Invalid policy p=%s (expected none, quarantine, or reject).", policy))
		return record
	}
	record.Policy = policy

	if sp, ok := tags["sp"]; ok && sp != "" {
		lower := strings.ToLower(sp)
		if dmarcPolicies[lower] {
			record.SubdomainPolicy = lower
		} else {
			record.Warnings = append(record.Warnings, fmt.Sprintf("Invalid sp=%s ignored (expected none, quarantine, or reject).", lower))
		}
	}

	if rua, ok := tags["rua"]; ok && rua != "" {
		record.AggregateURIs = splitCommaList(rua)
	}
	if ruf, ok := tags["ruf"]; ok && ruf != "" {
		record.ForensicURIs = splitCommaList(ruf)
	}

	if pct, ok := tags["pct"]; ok {
		if n, isNum := tsNumber(pct); !isNum || n != math.Trunc(n) || n < 0 || n > 100 {
			record.Warnings = append(record.Warnings, fmt.Sprintf("Invalid pct=%s ignored (must be 0-100).", pct))
		} else {
			percent := int(n)
			record.Percent = &percent
		}
	}

	if adkim, ok := tags["adkim"]; ok && adkim != "" {
		if adkim == "r" || adkim == "s" {
			record.DKIMAlignment = adkim
		} else {
			record.Warnings = append(record.Warnings, fmt.Sprintf("Invalid adkim=%s ignored (expected r or s).", adkim))
		}
	}
	if aspf, ok := tags["aspf"]; ok && aspf != "" {
		if aspf == "r" || aspf == "s" {
			record.SPFAlignment = aspf
		} else {
			record.Warnings = append(record.Warnings, fmt.Sprintf("Invalid aspf=%s ignored (expected r or s).", aspf))
		}
	}

	// Policy guidance
	if record.Policy == "none" {
		record.Warnings = append(record.Warnings, "p=none is monitor-only — no mail is quarantined or rejected, but you still need SPF/DKIM to pass for reports to be useful.")
	}
	if len(record.AggregateURIs) == 0 {
		record.Warnings = append(record.Warnings, "No rua= address — without aggregate reports you cannot see who is failing authentication. Add rua=mailto:reports@example.com.")
	} else if len(record.ForensicURIs) > 0 {
		record.Warnings = append(record.Warnings, "ruf= (forensic reports) is supported by few receivers and may leak message content to the report address (informational).")
	}
	if record.Percent != nil && *record.Percent < 100 && record.Policy != "none" {
		pct := *record.Percent
		record.Warnings = append(record.Warnings, fmt.Sprintf("pct=%d applies the policy to only %d%% of mail — the other %d%% is unaffected.", pct, pct, 100-pct))
	}
	return record
}

// splitCommaList splits a ','-separated tag value, trimmed and empty-removed.
func splitCommaList(v string) []string {
	parts := strings.Split(v, ",")
	out := make([]string, 0, len(parts))
	for _, s := range parts {
		if t := strings.TrimSpace(s); t != "" {
			out = append(out, t)
		}
	}
	return out
}

// ---------------------------------------------------------------------------
// Record builder (mirrors SpfPart / DkimPart / DmarcPart / DnsRecordsConfig)
// ---------------------------------------------------------------------------

// SpfPart is the builder state for the SPF record.
type SpfPart struct {
	Enabled bool
	// Terms are the raw terms after "v=spf1" — mechanisms and modifiers,
	// e.g. ["include:_spf.google.com", "~all"].
	Terms []string
}

// DkimPart is the builder state for the DKIM record.
type DkimPart struct {
	Enabled bool
	KeyType string // k= tag — "rsa" or "ed25519"
	PublicKey string // p= tag — the base64 public key from the mail provider
	Hashes  []string // h= tag values, e.g. ["sha256"]
	TestMode bool // t=y test mode
}

// DmarcPart is the builder state for the DMARC record.
type DmarcPart struct {
	Enabled bool
	Policy         string // p= tag. "" means not chosen yet.
	SubdomainPolicy string // sp= tag. "" omits it (subdomains inherit p=).
	Rua            []string // rua= aggregate report URIs
	Ruf            []string // ruf= forensic report URIs
	Percent        *int // pct= tag; nil omits it
	DKIMAlignment  string // "r" or "s"
	SPFAlignment   string // "r" or "s"
}

// DnsRecordsConfig is the full builder state for all three records.
type DnsRecordsConfig struct {
	Domain   string
	Selector string
	DKIM     DkimPart
	SPF      SpfPart
	DMARC    DmarcPart
}

// DnsIssue is one builder-validation finding.
type DnsIssue struct {
	Code     string // selector-missing|spf-multiple-records|spf-all-missing|dmarc-policy-missing|domain-not-fqdn
	Severity string // warn|info
	Value    *string
}

// ParsedDnsRecords is the result of importing pasted TXT records.
type ParsedDnsRecords struct {
	Config DnsRecordsConfig
	Found  struct {
		SPF  bool
		DKIM bool
		DMARC bool
	}
}

// DefaultDnsRecordsConfig returns fresh builder state: all three records on,
// Google-Workspace-shaped defaults.
func DefaultDnsRecordsConfig() DnsRecordsConfig {
	return DnsRecordsConfig{
		Domain:   "example.com",
		Selector: "google",
		SPF:      SpfPart{Enabled: true, Terms: []string{"include:_spf.google.com", "~all"}},
		DKIM:     DkimPart{Enabled: true, KeyType: "rsa", PublicKey: "", Hashes: []string{"sha256"}, TestMode: false},
		DMARC: DmarcPart{
			Enabled:       true,
			Policy:        "none",
			Rua:           []string{"mailto:dmarc@example.com"},
			Ruf:           []string{},
			Percent:       nil,
			DKIMAlignment: "r",
			SPFAlignment:  "r",
		},
	}
}

// cloneStrings copies a slice preserving the nil-vs-empty distinction (the TS
// lib deep-clones via JSON, where both survive as "array or absent").
func cloneStrings(s []string) []string {
	if s == nil {
		return nil
	}
	out := make([]string, len(s))
	copy(out, s)
	return out
}

func cloneConfig(c DnsRecordsConfig) DnsRecordsConfig {
	out := c
	out.SPF.Terms = cloneStrings(c.SPF.Terms)
	out.DKIM.Hashes = cloneStrings(c.DKIM.Hashes)
	out.DMARC.Rua = cloneStrings(c.DMARC.Rua)
	out.DMARC.Ruf = cloneStrings(c.DMARC.Ruf)
	if c.DMARC.Percent != nil {
		pct := *c.DMARC.Percent
		out.DMARC.Percent = &pct
	}
	return out
}

// SpfValue renders the SPF TXT value ("v=spf1 include:… ~all").
func SpfValue(part SpfPart) string {
	terms := make([]string, 0, len(part.Terms))
	for _, t := range part.Terms {
		if trimmed := strings.TrimSpace(t); trimmed != "" {
			terms = append(terms, trimmed)
		}
	}
	return strings.TrimRight("v=spf1 "+strings.Join(terms, " "), " \t\n\r")
}

// DkimValue renders the DKIM TXT value ("v=DKIM1; k=rsa; p=…").
func DkimValue(part DkimPart) string {
	keyType := strings.TrimSpace(part.KeyType)
	if keyType == "" {
		keyType = "rsa"
	}
	tags := []string{"v=DKIM1", "k=" + keyType}
	hashes := make([]string, 0, len(part.Hashes))
	for _, h := range part.Hashes {
		if trimmed := strings.TrimSpace(h); trimmed != "" {
			hashes = append(hashes, trimmed)
		}
	}
	if len(hashes) > 0 {
		tags = append(tags, "h="+strings.Join(hashes, ":"))
	}
	if part.TestMode {
		tags = append(tags, "t=y")
	}
	tags = append(tags, "p="+whitespaceRe.ReplaceAllString(part.PublicKey, ""))
	return strings.Join(tags, "; ")
}

// DmarcValue renders the DMARC TXT value ("v=DMARC1; p=…; rua=…").
func DmarcValue(part DmarcPart) string {
	policy := part.Policy
	if policy == "" {
		policy = "none"
	}
	tags := []string{"v=DMARC1", "p=" + policy}
	if part.SubdomainPolicy != "" {
		tags = append(tags, "sp="+part.SubdomainPolicy)
	}
	if len(part.Rua) > 0 {
		tags = append(tags, "rua="+strings.Join(part.Rua, ","))
	}
	if len(part.Ruf) > 0 {
		tags = append(tags, "ruf="+strings.Join(part.Ruf, ","))
	}
	if part.Percent != nil {
		tags = append(tags, "pct="+strconv.Itoa(*part.Percent))
	}
	tags = append(tags, "adkim="+part.DKIMAlignment, "aspf="+part.SPFAlignment)
	return strings.Join(tags, "; ")
}

// GenerateDnsRecords renders zone-file style output: every enabled record
// preceded by a comment naming the TXT record's host, so the whole block can
// be pasted at a provider. Never panics.
func GenerateDnsRecords(config DnsRecordsConfig) string {
	domain := NormalizeDomain(config.Domain)
	if domain == "" {
		domain = "example.com"
	}
	selector := strings.ToLower(strings.TrimSpace(config.Selector))
	if selector == "" {
		selector = "default"
	}
	out := []string{
		fmt.Sprintf("; Email authentication (SPF / DKIM / DMARC) records for %s", domain),
		"; Publish each block as a TXT record at your DNS provider.",
		"",
	}
	if config.SPF.Enabled {
		out = append(out, fmt.Sprintf("; SPF — TXT at %s (the zone apex; often written \"@\")", domain), fmt.Sprintf("%s. IN TXT \"%s\"", domain, SpfValue(config.SPF)), "")
	}
	if config.DKIM.Enabled {
		out = append(out, fmt.Sprintf("; DKIM — TXT at %s._domainkey.%s", selector, domain), fmt.Sprintf("%s._domainkey.%s. IN TXT \"%s\"", selector, domain, DkimValue(config.DKIM)), "")
	}
	if config.DMARC.Enabled {
		out = append(out, fmt.Sprintf("; DMARC — TXT at _dmarc.%s", domain), fmt.Sprintf("_dmarc.%s. IN TXT \"%s\"", domain, DmarcValue(config.DMARC)), "")
	}
	return strings.TrimRight(strings.Join(out, "\n"), " \t\n\r") + "\n"
}

// ValidateDnsRecords reports the five classic publishing mistakes.
func ValidateDnsRecords(config DnsRecordsConfig) []DnsIssue {
	issues := []DnsIssue{}
	domain := strings.TrimSpace(config.Domain)
	if domain != "" && !IsDomainLike(NormalizeDomain(domain)) {
		v := domain
		issues = append(issues, DnsIssue{Code: "domain-not-fqdn", Severity: "warn", Value: &v})
	}
	if config.SPF.Enabled {
		// More than one "v=spf1" token means more than one SPF record was
		// pasted in — receivers treat that as a permanent error.
		spfTokens := 0
		for _, t := range strings.Fields(SpfValue(config.SPF)) {
			if spfTokenRe.MatchString(t) {
				spfTokens++
			}
		}
		if spfTokens > 1 {
			v := strconv.Itoa(spfTokens)
			issues = append(issues, DnsIssue{Code: "spf-multiple-records", Severity: "warn", Value: &v})
		}
		rec := ParseSPF(SpfValue(config.SPF))
		hasAll := false
		for _, m := range rec.Mechanisms {
			if m.Kind == "all" {
				hasAll = true
				break
			}
		}
		if !hasAll && rec.Redirect == "" {
			issues = append(issues, DnsIssue{Code: "spf-all-missing", Severity: "warn"})
		}
	}
	if config.DKIM.Enabled && strings.TrimSpace(config.Selector) == "" {
		issues = append(issues, DnsIssue{Code: "selector-missing", Severity: "warn"})
	}
	if config.DMARC.Enabled && config.DMARC.Policy == "" {
		issues = append(issues, DnsIssue{Code: "dmarc-policy-missing", Severity: "warn"})
	}
	return issues
}

// txtPayload strips a zone-file prefix (`name IN TXT "…"`) down to the record
// value itself.
func txtPayload(line string) string {
	q := strings.Index(line, `"`)
	if q == -1 {
		return strings.TrimSpace(line)
	}
	end := strings.LastIndex(line, `"`)
	if end > q {
		return strings.TrimSpace(line[q+1 : end])
	}
	return strings.TrimSpace(line[q+1:])
}

// recordName returns the record host of a zone line
// ("_dmarc.example.com." → "_dmarc.example.com"); "" when the line is a bare
// value.
func recordName(line string) string {
	q := strings.Index(line, `"`)
	head := strings.TrimSpace(line)
	if q != -1 {
		head = strings.TrimSpace(line[:q])
	}
	if strings.IndexFunc(head, unicode.IsSpace) < 0 {
		return ""
	}
	return trailingDots.ReplaceAllString(strings.Fields(head)[0], "")
}

// ParseDnsRecords parses pasted TXT record values (bare values or full zone
// lines) into builder state. Parts not present keep their `prev` values.
// Never panics.
func ParseDnsRecords(text string, prev DnsRecordsConfig) ParsedDnsRecords {
	config := cloneConfig(prev)
	var found struct {
		SPF   bool
		DKIM  bool
		DMARC bool
	}
	var spfLines []string
	spfName := ""
	dkimPayload, dkimName := "", ""
	dmarcPayload, dmarcName := "", ""

	for _, raw := range strings.Split(text, "\n") {
		line := strings.TrimSpace(raw)
		if line == "" {
			continue
		}
		payload := txtPayload(line)
		name := recordName(line)
		switch {
		case spfPayloadRe.MatchString(payload):
			spfLines = append(spfLines, payload)
			if spfName == "" {
				spfName = name
			}
		case dkimPayloadRe.MatchString(payload) || strings.Contains(name, "._domainkey"):
			dkimPayload = payload
			dkimName = name
		case dmarcPayRe.MatchString(payload) || strings.HasPrefix(name, "_dmarc."):
			dmarcPayload = payload
			dmarcName = name
		}
	}

	// Sniff the record host for domain and selector: an explicit name wins.
	dmarcDomain := ""
	if strings.HasPrefix(dmarcName, "_dmarc.") {
		dmarcDomain = dmarcName[len("_dmarc."):]
	}
	dkimAt := strings.Index(dkimName, "._domainkey.")
	sniffDomain := dmarcDomain
	if sniffDomain == "" && dkimAt > 0 {
		sniffDomain = dkimName[dkimAt+len("._domainkey."):]
	}
	if sniffDomain == "" {
		sniffDomain = spfName
	}
	if sniffDomain != "" && IsDomainLike(sniffDomain) {
		config.Domain = sniffDomain
	}
	if dkimAt > 0 {
		config.Selector = dkimName[:dkimAt]
	}

	if len(spfLines) > 0 {
		// First record's terms are unpacked; any further whole records stay
		// as terms so ValidateDnsRecords can flag them.
		terms := []string{}
		for i, l := range spfLines {
			if i == 0 {
				for _, t := range strings.Fields(spfLeadRe.ReplaceAllString(l, "")) {
					if trimmed := strings.TrimSpace(t); trimmed != "" {
						terms = append(terms, trimmed)
					}
				}
			} else {
				terms = append(terms, strings.TrimSpace(l))
			}
		}
		config.SPF = SpfPart{Enabled: true, Terms: terms}
		found.SPF = true
	}

	if dkimPayload != "" {
		rec := ParseDKIM(dkimPayload)
		if rec.Valid {
			publicKey := ""
			if rec.PublicKey != nil {
				publicKey = *rec.PublicKey
			}
			testMode := false
			for _, f := range rec.Flags {
				if f == "y" {
					testMode = true
					break
				}
			}
			config.DKIM = DkimPart{
				Enabled:    true,
				KeyType:    rec.KeyType,
				PublicKey:  publicKey,
				Hashes:     rec.Hashes,
				TestMode:   testMode,
			}
			found.DKIM = true
		}
	}

	if dmarcPayload != "" {
		rec := ParseDMARC(dmarcPayload)
		if rec.Valid {
			// ParseDMARC only reports Valid=true once p= is set — policy is
			// defined.
			config.DMARC = DmarcPart{
				Enabled:         true,
				Policy:          rec.Policy,
				SubdomainPolicy: rec.SubdomainPolicy,
				Rua:             rec.AggregateURIs,
				Ruf:             rec.ForensicURIs,
				Percent:         rec.Percent,
				DKIMAlignment:   rec.DKIMAlignment,
				SPFAlignment:    rec.SPFAlignment,
			}
			if config.DMARC.DKIMAlignment == "" {
				config.DMARC.DKIMAlignment = "r"
			}
			if config.DMARC.SPFAlignment == "" {
				config.DMARC.SPFAlignment = "r"
			}
			found.DMARC = true
		}
	}

	return ParsedDnsRecords{Config: config, Found: found}
}

// ---------------------------------------------------------------------------
// URL codecs for shareable state (mirrors toQuery / fromQuery)
// ---------------------------------------------------------------------------

// enc mirrors encodeURIComponent: percent-encodes everything outside
// A-Za-z0-9 - _ . ! ~ * ' ( ) as UTF-8 bytes.
func enc(s string) string {
	var b strings.Builder
	for _, r := range s {
		if isURINeverEncoded(r) {
			b.WriteRune(r)
			continue
		}
		for _, c := range []byte(string(r)) {
			fmt.Fprintf(&b, "%%%02X", c)
		}
	}
	return b.String()
}

func isURINeverEncoded(r rune) bool {
	switch {
	case r >= 'A' && r <= 'Z', r >= 'a' && r <= 'z', r >= '0' && r <= '9':
		return true
	}
	switch r {
	case '-', '_', '.', '!', '~', '*', '\'', '(', ')':
		return true
	}
	return false
}

// dec mirrors the TS dec(): decodeURIComponent with a raw fallback on
// malformed input rather than an error.
func dec(s string) string {
	if d, err := url.PathUnescape(s); err == nil {
		return d
	}
	return s
}

// formEncode mirrors the URLSearchParams serializer: '+' for space; only
// * - . 0-9 A-Z _ a-z pass through; everything else percent-encoded.
func formEncode(s string) string {
	var b strings.Builder
	for _, r := range s {
		switch {
		case r == ' ':
			b.WriteByte('+')
		case r == '*' || r == '-' || r == '.' || r == '_' ||
			(r >= '0' && r <= '9') || (r >= 'A' && r <= 'Z') || (r >= 'a' && r <= 'z'):
			b.WriteRune(r)
		default:
			for _, c := range []byte(string(r)) {
				fmt.Fprintf(&b, "%%%02X", c)
			}
		}
	}
	return b.String()
}

// parseParamsLenient mirrors URLSearchParams over a query string: split on
// '&', split each pair at the first '=', decode with '+'→space, and KEEP the
// raw text of a malformed escape instead of failing the whole parse. First
// occurrence of a key wins (TS params.get semantics).
func parseParamsLenient(query string) map[string]string {
	m := map[string]string{}
	for _, pair := range strings.Split(query, "&") {
		if pair == "" {
			continue
		}
		k, v := pair, ""
		if eq := strings.Index(pair, "="); eq >= 0 {
			k, v = pair[:eq], pair[eq+1:]
		}
		dk, err := url.QueryUnescape(k)
		if err != nil {
			dk = k
		}
		dv, err := url.QueryUnescape(v)
		if err != nil {
			dv = v
		}
		if _, exists := m[dk]; !exists {
			m[dk] = dv
		}
	}
	return m
}

// sections splits "k=rsa|h=sha256|…" into a tag → value map. Mirrors the TS
// helper's slice semantics for a section without '=' (key drops its last
// character, which becomes the value).
func sections(param string) map[string]string {
	m := map[string]string{}
	for _, s := range strings.Split(param, "|") {
		eq := strings.Index(s, "=")
		if eq < 0 {
			if len(s) == 0 {
				m[""] = ""
				continue
			}
			m[s[:len(s)-1]] = s[len(s)-1:]
			continue
		}
		m[s[:eq]] = s[eq+1:]
	}
	return m
}

var policyValues = map[string]bool{"none": true, "quarantine": true, "reject": true}

// ToQuery encodes the builder state as a URL query string, mirroring toQuery:
// components are enc()'d BEFORE the compact format is assembled, so the
// '|'/','/'=' delimiters can never appear inside a component after decoding.
// Param presence (spf/dkim/dm) carries the enabled flags; absent sections
// fall back to defaults. Key order is d, sel, spf, dkim, dm — the same
// insertion order URLSearchParams emits.
func ToQuery(config DnsRecordsConfig) string {
	var parts []string
	if strings.TrimSpace(config.Domain) != "" {
		parts = append(parts, "d="+formEncode(enc(strings.TrimSpace(config.Domain))))
	}
	if strings.TrimSpace(config.Selector) != "" {
		parts = append(parts, "sel="+formEncode(enc(strings.TrimSpace(config.Selector))))
	}
	if config.SPF.Enabled {
		terms := make([]string, 0, len(config.SPF.Terms))
		for _, t := range config.SPF.Terms {
			terms = append(terms, enc(strings.TrimSpace(t)))
		}
		parts = append(parts, "spf="+formEncode(strings.Join(terms, ",")))
	}
	if config.DKIM.Enabled {
		hashes := make([]string, 0, len(config.DKIM.Hashes))
		for _, h := range config.DKIM.Hashes {
			hashes = append(hashes, enc(h))
		}
		secs := []string{
			"k=" + enc(config.DKIM.KeyType),
			"h=" + strings.Join(hashes, ","),
			"t=0",
			"p=" + enc(config.DKIM.PublicKey),
		}
		if config.DKIM.TestMode {
			secs[2] = "t=1"
		}
		parts = append(parts, "dkim="+formEncode(strings.Join(secs, "|")))
	}
	if config.DMARC.Enabled {
		pct := ""
		if config.DMARC.Percent != nil {
			pct = strconv.Itoa(*config.DMARC.Percent)
		}
		secs := []string{
			"p=" + config.DMARC.Policy,
			"sp=" + config.DMARC.SubdomainPolicy,
			"rua=" + joinEncoded(config.DMARC.Rua),
			"ruf=" + joinEncoded(config.DMARC.Ruf),
			"pct=" + pct,
			"adkim=" + config.DMARC.DKIMAlignment,
			"aspf=" + config.DMARC.SPFAlignment,
		}
		parts = append(parts, "dm="+formEncode(strings.Join(secs, "|")))
	}
	return strings.Join(parts, "&")
}

func joinEncoded(list []string) string {
	out := make([]string, 0, len(list))
	for _, s := range list {
		out = append(out, enc(s))
	}
	return strings.Join(out, ",")
}

// FromQuery decodes a ToQuery-style query string back into builder state,
// mirroring fromQuery. Returns nil when none of d/sel/spf/dkim/dm is present.
func FromQuery(query string) *DnsRecordsConfig {
	params := parseParamsLenient(query)
	if _, ok := params["d"]; !ok {
		if _, ok := params["sel"]; !ok {
			if _, ok := params["spf"]; !ok {
				if _, ok := params["dkim"]; !ok {
					if _, ok := params["dm"]; !ok {
						return nil
					}
				}
			}
		}
	}
	config := DefaultDnsRecordsConfig()
	if d, ok := params["d"]; ok {
		config.Domain = dec(d)
	}
	if sel, ok := params["sel"]; ok {
		config.Selector = dec(sel)
	}
	if spf, ok := params["spf"]; ok {
		terms := []string{}
		for _, t := range strings.Split(spf, ",") {
			if t != "" {
				terms = append(terms, dec(t))
			}
		}
		config.SPF = SpfPart{Enabled: true, Terms: terms}
	}
	if dkimParam, ok := params["dkim"]; ok {
		sec := sections(dkimParam)
		keyType := "rsa"
		if k, ok := sec["k"]; ok {
			keyType = dec(k)
		}
		hashes := []string{}
		for _, t := range strings.Split(sec["h"], ",") {
			if t != "" {
				hashes = append(hashes, dec(t))
			}
		}
		config.DKIM = DkimPart{
			Enabled:    true,
			KeyType:    keyType,
			Hashes:     hashes,
			TestMode:   sec["t"] == "1",
			PublicKey:  dec(sec["p"]),
		}
	}
	if dm, ok := params["dm"]; ok {
		sec := sections(dm)
		policy := "none"
		if p, ok := sec["p"]; ok {
			policy = p
		}
		sub := ""
		if s, ok := sec["sp"]; ok {
			sub = s
		}
		if !policyValues[policy] {
			policy = "none"
		}
		subdomainPolicy := ""
		if policyValues[sub] {
			subdomainPolicy = sub
		}
		config.DMARC = DmarcPart{
			Enabled:         true,
			Policy:          policy,
			SubdomainPolicy: subdomainPolicy,
			Rua:             decList(sec["rua"]),
			Ruf:             decList(sec["ruf"]),
			DKIMAlignment:   alignmentOr(sec["adkim"]),
			SPFAlignment:    alignmentOr(sec["aspf"]),
		}
		if raw, ok := sec["pct"]; ok && raw != "" {
			if f, err := strconv.ParseFloat(raw, 64); err == nil {
				pct := int(f)
				config.DMARC.Percent = &pct
			}
		}
	}
	return &config
}

func decList(param string) []string {
	out := []string{}
	for _, t := range strings.Split(param, ",") {
		if t != "" {
			out = append(out, dec(t))
		}
	}
	return out
}

func alignmentOr(v string) string {
	if v == "s" {
		return "s"
	}
	return "r"
}

Also available in 9 other languages

Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →