13 "github.com/mjl-/adns"
15 "github.com/mjl-/mox/config"
16 "github.com/mjl-/mox/dkim"
17 "github.com/mjl-/mox/dmarc"
18 "github.com/mjl-/mox/dns"
19 "github.com/mjl-/mox/mox-"
20 "github.com/mjl-/mox/smtp"
21 "github.com/mjl-/mox/spf"
22 "github.com/mjl-/mox/tlsrpt"
26// todo: find a way to automatically create the dns records as it would greatly simplify setting up email for a domain. we could also dynamically make changes, e.g. providing grace periods after disabling a dkim key, only automatically removing the dkim dns key after a few days. but this requires some kind of api and authentication to the dns server. there doesn't appear to be a single commonly used api for dns management. each of the numerous cloud providers have their own APIs and rather large SKDs to use them. we don't want to link all of them in.
28// DomainRecords returns text lines describing DNS records required for configuring
31// If certIssuerDomainName is set, CAA records to limit TLS certificate issuance to
32// that caID will be suggested. If acmeAccountURI is also set, CAA records also
33// restricting issuance to that account ID will be suggested.
34func DomainRecords(domConf config.Domain, domain dns.Domain, hasDNSSEC bool, certIssuerDomainName, acmeAccountURI string) ([]string, error) {
36 h := mox.Conf.Static.HostnameDomain.ASCII
38 // The first line with ";" is used by ../testdata/integration/moxacmepebble.sh and
39 // ../testdata/integration/moxmail2.sh for selecting DNS records
41 "; Time To Live of 5 minutes, may be recognized if importing as a zone file.",
42 "; Once your setup is working, you may want to increase the TTL.",
47 if public, ok := mox.Conf.Static.Listeners["public"]; ok && public.TLS != nil && (len(public.TLS.HostPrivateRSA2048Keys) > 0 || len(public.TLS.HostPrivateECDSAP256Keys) > 0) {
48 records = append(records,
49 `; DANE: These records indicate that a remote mail server trying to deliver email`,
50 `; with SMTP (TCP port 25) must verify the TLS certificate with DANE-EE (3), based`,
51 `; on the certificate public key ("SPKI", 1) that is SHA2-256-hashed (1) to the`,
52 `; hexadecimal hash. DANE-EE verification means only the certificate or public`,
53 `; key is verified, not whether the certificate is signed by a (centralized)`,
54 `; certificate authority (CA), is expired, or matches the host name.`,
56 `; NOTE: Create the records below only once: They are for the machine, and apply`,
57 `; to all hosted domains.`,
60 records = append(records,
62 "; WARNING: Domain does not appear to be DNSSEC-signed. To enable DANE, first",
63 "; enable DNSSEC on your domain, then add the TLSA records. Records below have been",
67 addTLSA := func(privKey crypto.Signer) error {
68 spkiBuf, err := x509.MarshalPKIXPublicKey(privKey.Public())
70 return fmt.Errorf("marshal SubjectPublicKeyInfo for DANE record: %v", err)
72 sum := sha256.Sum256(spkiBuf)
73 tlsaRecord := adns.TLSA{
74 Usage: adns.TLSAUsageDANEEE,
75 Selector: adns.TLSASelectorSPKI,
76 MatchType: adns.TLSAMatchTypeSHA256,
81 s = fmt.Sprintf("_25._tcp.%-*s TLSA %s", 20+len(d)-len("_25._tcp."), h+".", tlsaRecord.Record())
83 s = fmt.Sprintf(";; _25._tcp.%-*s TLSA %s", 20+len(d)-len(";; _25._tcp."), h+".", tlsaRecord.Record())
85 records = append(records, s)
88 for _, privKey := range public.TLS.HostPrivateECDSAP256Keys {
89 if err := addTLSA(privKey); err != nil {
93 for _, privKey := range public.TLS.HostPrivateRSA2048Keys {
94 if err := addTLSA(privKey); err != nil {
98 records = append(records, "")
102 records = append(records,
103 "; For the machine, only needs to be created once, for the first domain added:",
105 "; SPF-allow host for itself, resulting in relaxed DMARC pass for (postmaster)",
106 "; messages (DSNs) sent from host:",
111 if d != h && mox.Conf.Static.HostTLSRPT.ParsedLocalpart != "" {
114 Opaque: smtp.NewAddress(mox.Conf.Static.HostTLSRPT.ParsedLocalpart, mox.Conf.Static.HostnameDomain).Pack(false),
116 tlsrptr := tlsrpt.Record{Version: "TLSRPTv1", RUAs: [][]tlsrpt.RUA{{tlsrpt.RUA(uri.String())}}}
117 records = append(records,
118 "; For the machine, only needs to be created once, for the first domain added:",
120 "; Request reporting about success/failures of TLS connections to (MX) host, for DANE.",
121 fmt.Sprintf(`_smtp._tls.%-*s TXT "%s"`, 20+len(d)-len("_smtp._tls."), h+".", tlsrptr.String()),
126 records = append(records,
127 "; Deliver email for the domain to this host.",
128 fmt.Sprintf("%s. MX 10 %s.", d, h),
131 "; Outgoing messages will be signed with the first two DKIM keys. The other two",
132 "; configured for backup, switching to them is just a config change.",
134 var selectors []string
135 for name := range domConf.DKIM.Selectors {
136 selectors = append(selectors, name)
138 slices.Sort(selectors)
139 for _, name := range selectors {
140 sel := domConf.DKIM.Selectors[name]
141 dkimr := dkim.Record{
143 Hashes: []string{"sha256"},
144 PublicKey: sel.Key.Public(),
146 if _, ok := sel.Key.(ed25519.PrivateKey); ok {
147 dkimr.Key = "ed25519"
148 } else if _, ok := sel.Key.(*rsa.PrivateKey); !ok {
149 return nil, fmt.Errorf("unrecognized private key for DKIM selector %q: %T", name, sel.Key)
151 txt, err := dkimr.Record()
153 return nil, fmt.Errorf("making DKIM DNS TXT record: %v", err)
157 records = append(records,
158 "; NOTE: The following is a single long record split over several lines for use",
159 "; in zone files. When adding through a DNS operator web interface, combine the",
160 "; strings into a single string, without ().",
163 s := fmt.Sprintf("%s._domainkey.%s. TXT %s", name, d, mox.TXTStrings(txt))
164 records = append(records, s)
167 dmarcr := dmarc.DefaultRecord
168 dmarcr.Policy = "reject"
169 if domConf.DMARC != nil {
172 Opaque: smtp.NewAddress(domConf.DMARC.ParsedLocalpart, domConf.DMARC.DNSDomain).Pack(false),
174 dmarcr.AggregateReportAddresses = []dmarc.URI{
175 {Address: uri.String(), MaxSize: 10, Unit: "m"},
178 dspfr := spf.Record{Version: "spf1"}
179 for _, ip := range mox.DomainSPFIPs() {
184 dspfr.Directives = append(dspfr.Directives, spf.Directive{Mechanism: mech, IP: ip})
186 dspfr.Directives = append(dspfr.Directives,
187 spf.Directive{Mechanism: "mx"},
188 spf.Directive{Qualifier: "~", Mechanism: "all"},
190 dspftxt, err := dspfr.Record()
192 return nil, fmt.Errorf("making domain spf record: %v", err)
194 records = append(records,
197 "; Specify the MX host is allowed to send for our domain and for itself (for DSNs).",
198 "; ~all means softfail for anything else, which is done instead of -all to prevent older",
199 "; mail servers from rejecting the message because they never get to looking for a dkim/dmarc pass.",
200 fmt.Sprintf(`%s. TXT "%s"`, d, dspftxt),
203 "; Emails that fail the DMARC check (without aligned DKIM and without aligned SPF)",
204 "; should be rejected, and request reports. If you email through mailing lists that",
205 "; strip DKIM-Signature headers and don't rewrite the From header, you may want to",
206 "; set the policy to p=none.",
207 fmt.Sprintf(`_dmarc.%s. TXT "%s"`, d, dmarcr.String()),
211 if sts := domConf.MTASTS; sts != nil {
212 records = append(records,
213 "; Remote servers can use MTA-STS to verify our TLS certificate with the",
214 "; WebPKI pool of CA's (certificate authorities) when delivering over SMTP with",
216 fmt.Sprintf(`mta-sts.%s. CNAME %s.`, d, h),
217 fmt.Sprintf(`_mta-sts.%s. TXT "v=STSv1; id=%s"`, d, sts.PolicyID),
221 records = append(records,
222 "; Note: No MTA-STS to indicate TLS should be used. Either because disabled for the",
223 "; domain or because mox.conf does not have a listener with MTA-STS configured.",
228 if domConf.TLSRPT != nil {
231 Opaque: smtp.NewAddress(domConf.TLSRPT.ParsedLocalpart, domConf.TLSRPT.DNSDomain).Pack(false),
233 tlsrptr := tlsrpt.Record{Version: "TLSRPTv1", RUAs: [][]tlsrpt.RUA{{tlsrpt.RUA(uri.String())}}}
234 records = append(records,
235 "; Request reporting about TLS failures.",
236 fmt.Sprintf(`_smtp._tls.%s. TXT "%s"`, d, tlsrptr.String()),
241 if domConf.ClientSettingsDomain != "" && domConf.ClientSettingsDNSDomain != mox.Conf.Static.HostnameDomain {
242 records = append(records,
243 "; Client settings will reference a subdomain of the hosted domain, making it",
244 "; easier to migrate to a different server in the future by not requiring settings",
245 "; in all clients to be updated.",
246 fmt.Sprintf(`%-*s CNAME %s.`, 20+len(d), domConf.ClientSettingsDNSDomain.ASCII+".", h),
251 records = append(records,
252 "; Autoconfig is used by Thunderbird. Autodiscover is (in theory) used by Microsoft.",
253 fmt.Sprintf(`autoconfig.%s. CNAME %s.`, d, h),
254 fmt.Sprintf(`_autodiscover._tcp.%s. SRV 0 1 443 %s.`, d, h),
258 "; For secure IMAP and submission autoconfig, point to mail host.",
259 fmt.Sprintf(`_imaps._tcp.%s. SRV 0 1 993 %s.`, d, h),
260 fmt.Sprintf(`_submissions._tcp.%s. SRV 0 1 465 %s.`, d, h),
263 "; Next records specify POP3 and non-TLS ports are not to be used.",
264 "; These are optional and safe to leave out (e.g. if you have to click a lot in a",
265 "; DNS admin web interface).",
266 fmt.Sprintf(`_imap._tcp.%s. SRV 0 0 0 .`, d),
267 fmt.Sprintf(`_submission._tcp.%s. SRV 0 0 0 .`, d),
268 fmt.Sprintf(`_pop3._tcp.%s. SRV 0 0 0 .`, d),
269 fmt.Sprintf(`_pop3s._tcp.%s. SRV 0 0 0 .`, d),
272 if certIssuerDomainName != "" {
274 records = append(records,
277 "; You could mark Let's Encrypt as the only Certificate Authority allowed to",
278 "; sign TLS certificates for your domain.",
279 fmt.Sprintf(`%s. CAA 0 issue "%s"`, d, certIssuerDomainName),
281 if acmeAccountURI != "" {
284 records = append(records,
286 "; Optionally limit certificates for this domain to the account ID and methods used by mox.",
287 fmt.Sprintf(`;; %s. CAA 0 issue "%s; accounturi=%s; validationmethods=tls-alpn-01,http-01"`, d, certIssuerDomainName, acmeAccountURI),
289 "; Or alternatively only limit for email-specific subdomains, so you can use",
290 "; other accounts/methods for other subdomains.",
291 fmt.Sprintf(`;; autoconfig.%s. CAA 0 issue "%s; accounturi=%s; validationmethods=tls-alpn-01,http-01"`, d, certIssuerDomainName, acmeAccountURI),
292 fmt.Sprintf(`;; mta-sts.%s. CAA 0 issue "%s; accounturi=%s; validationmethods=tls-alpn-01,http-01"`, d, certIssuerDomainName, acmeAccountURI),
294 if domConf.ClientSettingsDomain != "" && domConf.ClientSettingsDNSDomain != mox.Conf.Static.HostnameDomain {
295 records = append(records,
296 fmt.Sprintf(`;; %-*s CAA 0 issue "%s; accounturi=%s; validationmethods=tls-alpn-01,http-01"`, 20-3+len(d), domConf.ClientSettingsDNSDomain.ASCII, certIssuerDomainName, acmeAccountURI),
299 if strings.HasSuffix(h, "."+d) {
300 records = append(records,
302 "; And the mail hostname.",
303 fmt.Sprintf(`;; %-*s CAA 0 issue "%s; accounturi=%s; validationmethods=tls-alpn-01,http-01"`, 20-3+len(d), h+".", certIssuerDomainName, acmeAccountURI),
307 // The string "will be suggested" is used by
308 // ../testdata/integration/moxacmepebble.sh and ../testdata/integration/moxmail2.sh
309 // as end of DNS records.
310 records = append(records,
312 "; Note: After starting up, once an ACME account has been created, CAA records",
313 "; that restrict issuance to the account will be suggested.",