// The DNS record set a sending identity needs (spec §7). Pure: SES's DKIM // tokens in, resource record sets out. Who WRITES them is the one difference // between a platform domain (CARLOS owns the zone) and a custom domain (the // customer publishes the same set by hand) — the set itself is identical, // which is what makes the two paths one code path. package mail import ( "fmt" "strings" "github.com/carlosframework/platform/internal/dnsrec" ) // recordTTL is short on purpose: these records change on rotation and on // teardown, and a long TTL turns "we removed it" into "it is still being // answered for a day". const recordTTL = 300 // MailFromDomain is the custom MAIL FROM subdomain for a domain in a region. // // The region is IN THE NAME, and it is there from the first domain ever // verified. SES's MAIL FROM MX target is region-specific, so one subdomain // cannot serve two regions without ambiguity — and for a custom domain this // record lives in the CUSTOMER's zone, which makes it the one record we // cannot republish for them later. The full region name is used rather than // an abbreviation so there is no code table to keep in sync. func MailFromDomain(domain, region string) string { return "mail-" + region + "." + domain } // RecordSet renders every record a sending identity needs. rua is the DMARC // aggregate-report mailto target; empty means publish no rua (spec §7). // // Note what is NOT here: any record at `domain` itself. The edge already owns // an A record at an app's host, so every name below is a distinct label // underneath it. That separation is what lets the email pass and the // provisioning reconciler write into one zone without knowing about each // other, and TestNoRecordSitsAtTheDomainApex exists to keep it true. func RecordSet(domain, region string, dkimTokens []string, rua string) []dnsrec.Record { out := make([]dnsrec.Record, 0, len(dkimTokens)+3) for _, tok := range dkimTokens { out = append(out, dnsrec.Record{ Name: tok + "._domainkey." + domain, Type: dnsrec.TypeCNAME, Values: []string{tok + ".dkim.amazonses.com"}, TTL: recordTTL, }) } mailFrom := MailFromDomain(domain, region) out = append(out, dnsrec.Record{ Name: mailFrom, Type: dnsrec.TypeMX, Values: []string{fmt.Sprintf("10 feedback-smtp.%s.amazonses.com", region)}, TTL: recordTTL, }, // SPF is checked against the MAIL FROM domain, so it belongs here // and not at the identity domain. dnsrec.Record{ Name: mailFrom, Type: dnsrec.TypeTXT, Values: []string{"v=spf1 include:amazonses.com ~all"}, TTL: recordTTL, }, dnsrec.Record{ Name: "_dmarc." + domain, Type: dnsrec.TypeTXT, Values: []string{dmarcValue(rua)}, TTL: recordTTL, }, ) return out } // dmarcValue builds the DMARC policy. // // adkim=s and aspf=s are NOT hygiene — they are the tenant boundary. DMARC's // default relaxed alignment compares organizational domains, so on a shared // registrable domain like oncarlos.com one tenant's DKIM signature would // authenticate another tenant's From address. Strict alignment is what makes // that impossible. // // p=none because a tenant whose first send is rejected by its own DMARC // record learns nothing useful; tightening it is a follow-up gated on // aggregate reports actually arriving. func dmarcValue(rua string) string { v := "v=DMARC1; p=none; adkim=s; aspf=s" if strings.TrimSpace(rua) != "" { v += "; rua=mailto:" + strings.TrimSpace(rua) } return v }