1package mox
2
3import (
4 "bytes"
5 "context"
6 "crypto"
7 "crypto/ed25519"
8 cryptorand "crypto/rand"
9 "crypto/rsa"
10 "crypto/sha256"
11 "crypto/x509"
12 "encoding/pem"
13 "fmt"
14 "net"
15 "net/url"
16 "os"
17 "path/filepath"
18 "sort"
19 "strings"
20 "time"
21
22 "golang.org/x/exp/maps"
23
24 "github.com/mjl-/adns"
25
26 "github.com/mjl-/mox/config"
27 "github.com/mjl-/mox/dkim"
28 "github.com/mjl-/mox/dmarc"
29 "github.com/mjl-/mox/dns"
30 "github.com/mjl-/mox/junk"
31 "github.com/mjl-/mox/mlog"
32 "github.com/mjl-/mox/mtasts"
33 "github.com/mjl-/mox/smtp"
34 "github.com/mjl-/mox/tlsrpt"
35)
36
37// TXTStrings returns a TXT record value as one or more quoted strings, each max
38// 100 characters. In case of multiple strings, a multi-line record is returned.
39func TXTStrings(s string) string {
40 if len(s) <= 100 {
41 return `"` + s + `"`
42 }
43
44 r := "(\n"
45 for len(s) > 0 {
46 n := len(s)
47 if n > 100 {
48 n = 100
49 }
50 if r != "" {
51 r += " "
52 }
53 r += "\t\t\"" + s[:n] + "\"\n"
54 s = s[n:]
55 }
56 r += "\t)"
57 return r
58}
59
60// MakeDKIMEd25519Key returns a PEM buffer containing an ed25519 key for use
61// with DKIM.
62// selector and domain can be empty. If not, they are used in the note.
63func MakeDKIMEd25519Key(selector, domain dns.Domain) ([]byte, error) {
64 _, privKey, err := ed25519.GenerateKey(cryptorand.Reader)
65 if err != nil {
66 return nil, fmt.Errorf("generating key: %w", err)
67 }
68
69 pkcs8, err := x509.MarshalPKCS8PrivateKey(privKey)
70 if err != nil {
71 return nil, fmt.Errorf("marshal key: %w", err)
72 }
73
74 block := &pem.Block{
75 Type: "PRIVATE KEY",
76 Headers: map[string]string{
77 "Note": dkimKeyNote("ed25519", selector, domain),
78 },
79 Bytes: pkcs8,
80 }
81 b := &bytes.Buffer{}
82 if err := pem.Encode(b, block); err != nil {
83 return nil, fmt.Errorf("encoding pem: %w", err)
84 }
85 return b.Bytes(), nil
86}
87
88func dkimKeyNote(kind string, selector, domain dns.Domain) string {
89 s := kind + " dkim private key"
90 var zero dns.Domain
91 if selector != zero && domain != zero {
92 s += fmt.Sprintf(" for %s._domainkey.%s", selector.ASCII, domain.ASCII)
93 }
94 s += fmt.Sprintf(", generated by mox on %s", time.Now().Format(time.RFC3339))
95 return s
96}
97
98// MakeDKIMEd25519Key returns a PEM buffer containing an rsa key for use with
99// DKIM.
100// selector and domain can be empty. If not, they are used in the note.
101func MakeDKIMRSAKey(selector, domain dns.Domain) ([]byte, error) {
102 // 2048 bits seems reasonable in 2022, 1024 is on the low side, larger
103 // keys may not fit in UDP DNS response.
104 privKey, err := rsa.GenerateKey(cryptorand.Reader, 2048)
105 if err != nil {
106 return nil, fmt.Errorf("generating key: %w", err)
107 }
108
109 pkcs8, err := x509.MarshalPKCS8PrivateKey(privKey)
110 if err != nil {
111 return nil, fmt.Errorf("marshal key: %w", err)
112 }
113
114 block := &pem.Block{
115 Type: "PRIVATE KEY",
116 Headers: map[string]string{
117 "Note": dkimKeyNote("rsa-2048", selector, domain),
118 },
119 Bytes: pkcs8,
120 }
121 b := &bytes.Buffer{}
122 if err := pem.Encode(b, block); err != nil {
123 return nil, fmt.Errorf("encoding pem: %w", err)
124 }
125 return b.Bytes(), nil
126}
127
128// MakeAccountConfig returns a new account configuration for an email address.
129func MakeAccountConfig(addr smtp.Address) config.Account {
130 account := config.Account{
131 Domain: addr.Domain.Name(),
132 Destinations: map[string]config.Destination{
133 addr.String(): {},
134 },
135 RejectsMailbox: "Rejects",
136 JunkFilter: &config.JunkFilter{
137 Threshold: 0.95,
138 Params: junk.Params{
139 Onegrams: true,
140 MaxPower: .01,
141 TopWords: 10,
142 IgnoreWords: .1,
143 RareWords: 2,
144 },
145 },
146 }
147 account.AutomaticJunkFlags.Enabled = true
148 account.AutomaticJunkFlags.JunkMailboxRegexp = "^(junk|spam)"
149 account.AutomaticJunkFlags.NeutralMailboxRegexp = "^(inbox|neutral|postmaster|dmarc|tlsrpt|rejects)"
150 account.SubjectPass.Period = 12 * time.Hour
151 return account
152}
153
154// MakeDomainConfig makes a new config for a domain, creating DKIM keys, using
155// accountName for DMARC and TLS reports.
156func MakeDomainConfig(ctx context.Context, domain, hostname dns.Domain, accountName string, withMTASTS bool) (config.Domain, []string, error) {
157 log := xlog.WithContext(ctx)
158
159 now := time.Now()
160 year := now.Format("2006")
161 timestamp := now.Format("20060102T150405")
162
163 var paths []string
164 defer func() {
165 for _, p := range paths {
166 err := os.Remove(p)
167 log.Check(err, "removing path for domain config", mlog.Field("path", p))
168 }
169 }()
170
171 writeFile := func(path string, data []byte) error {
172 os.MkdirAll(filepath.Dir(path), 0770)
173
174 f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0660)
175 if err != nil {
176 return fmt.Errorf("creating file %s: %s", path, err)
177 }
178 defer func() {
179 if f != nil {
180 err := f.Close()
181 log.Check(err, "closing file after error")
182 err = os.Remove(path)
183 log.Check(err, "removing file after error", mlog.Field("path", path))
184 }
185 }()
186 if _, err := f.Write(data); err != nil {
187 return fmt.Errorf("writing file %s: %s", path, err)
188 }
189 if err := f.Close(); err != nil {
190 return fmt.Errorf("close file: %v", err)
191 }
192 f = nil
193 return nil
194 }
195
196 confDKIM := config.DKIM{
197 Selectors: map[string]config.Selector{},
198 }
199
200 addSelector := func(kind, name string, privKey []byte) error {
201 record := fmt.Sprintf("%s._domainkey.%s", name, domain.ASCII)
202 keyPath := filepath.Join("dkim", fmt.Sprintf("%s.%s.%s.privatekey.pkcs8.pem", record, timestamp, kind))
203 p := configDirPath(ConfigDynamicPath, keyPath)
204 if err := writeFile(p, privKey); err != nil {
205 return err
206 }
207 paths = append(paths, p)
208 confDKIM.Selectors[name] = config.Selector{
209 // Example from RFC has 5 day between signing and expiration. ../rfc/6376:1393
210 // Expiration is not intended as antireplay defense, but it may help. ../rfc/6376:1340
211 // Messages in the wild have been observed with 2 hours and 1 year expiration.
212 Expiration: "72h",
213 PrivateKeyFile: keyPath,
214 }
215 return nil
216 }
217
218 addEd25519 := func(name string) error {
219 key, err := MakeDKIMEd25519Key(dns.Domain{ASCII: name}, domain)
220 if err != nil {
221 return fmt.Errorf("making dkim ed25519 private key: %s", err)
222 }
223 return addSelector("ed25519", name, key)
224 }
225
226 addRSA := func(name string) error {
227 key, err := MakeDKIMRSAKey(dns.Domain{ASCII: name}, domain)
228 if err != nil {
229 return fmt.Errorf("making dkim rsa private key: %s", err)
230 }
231 return addSelector("rsa2048", name, key)
232 }
233
234 if err := addEd25519(year + "a"); err != nil {
235 return config.Domain{}, nil, err
236 }
237 if err := addRSA(year + "b"); err != nil {
238 return config.Domain{}, nil, err
239 }
240 if err := addEd25519(year + "c"); err != nil {
241 return config.Domain{}, nil, err
242 }
243 if err := addRSA(year + "d"); err != nil {
244 return config.Domain{}, nil, err
245 }
246
247 // We sign with the first two. In case they are misused, the switch to the other
248 // keys is easy, just change the config. Operators should make the public key field
249 // of the misused keys empty in the DNS records to disable the misused keys.
250 confDKIM.Sign = []string{year + "a", year + "b"}
251
252 confDomain := config.Domain{
253 LocalpartCatchallSeparator: "+",
254 DKIM: confDKIM,
255 DMARC: &config.DMARC{
256 Account: accountName,
257 Localpart: "dmarc-reports",
258 Mailbox: "DMARC",
259 },
260 TLSRPT: &config.TLSRPT{
261 Account: accountName,
262 Localpart: "tls-reports",
263 Mailbox: "TLSRPT",
264 },
265 }
266
267 if withMTASTS {
268 confDomain.MTASTS = &config.MTASTS{
269 PolicyID: time.Now().UTC().Format("20060102T150405"),
270 Mode: mtasts.ModeEnforce,
271 // We start out with 24 hour, and warn in the admin interface that users should
272 // increase it to weeks once the setup works.
273 MaxAge: 24 * time.Hour,
274 MX: []string{hostname.ASCII},
275 }
276 }
277
278 rpaths := paths
279 paths = nil
280
281 return confDomain, rpaths, nil
282}
283
284// DomainAdd adds the domain to the domains config, rewriting domains.conf and
285// marking it loaded.
286//
287// accountName is used for DMARC/TLS report and potentially for the postmaster address.
288// If the account does not exist, it is created with localpart. Localpart must be
289// set only if the account does not yet exist.
290func DomainAdd(ctx context.Context, domain dns.Domain, accountName string, localpart smtp.Localpart) (rerr error) {
291 log := xlog.WithContext(ctx)
292 defer func() {
293 if rerr != nil {
294 log.Errorx("adding domain", rerr, mlog.Field("domain", domain), mlog.Field("account", accountName), mlog.Field("localpart", localpart))
295 }
296 }()
297
298 Conf.dynamicMutex.Lock()
299 defer Conf.dynamicMutex.Unlock()
300
301 c := Conf.Dynamic
302 if _, ok := c.Domains[domain.Name()]; ok {
303 return fmt.Errorf("domain already present")
304 }
305
306 // Compose new config without modifying existing data structures. If we fail, we
307 // leave no trace.
308 nc := c
309 nc.Domains = map[string]config.Domain{}
310 for name, d := range c.Domains {
311 nc.Domains[name] = d
312 }
313
314 // Only enable mta-sts for domain if there is a listener with mta-sts.
315 var withMTASTS bool
316 for _, l := range Conf.Static.Listeners {
317 if l.MTASTSHTTPS.Enabled {
318 withMTASTS = true
319 break
320 }
321 }
322
323 confDomain, cleanupFiles, err := MakeDomainConfig(ctx, domain, Conf.Static.HostnameDomain, accountName, withMTASTS)
324 if err != nil {
325 return fmt.Errorf("preparing domain config: %v", err)
326 }
327 defer func() {
328 for _, f := range cleanupFiles {
329 err := os.Remove(f)
330 log.Check(err, "cleaning up file after error", mlog.Field("path", f))
331 }
332 }()
333
334 if _, ok := c.Accounts[accountName]; ok && localpart != "" {
335 return fmt.Errorf("account already exists (leave localpart empty when using an existing account)")
336 } else if !ok && localpart == "" {
337 return fmt.Errorf("account does not yet exist (specify a localpart)")
338 } else if accountName == "" {
339 return fmt.Errorf("account name is empty")
340 } else if !ok {
341 nc.Accounts[accountName] = MakeAccountConfig(smtp.Address{Localpart: localpart, Domain: domain})
342 } else if accountName != Conf.Static.Postmaster.Account {
343 nacc := nc.Accounts[accountName]
344 nd := map[string]config.Destination{}
345 for k, v := range nacc.Destinations {
346 nd[k] = v
347 }
348 pmaddr := smtp.Address{Localpart: "postmaster", Domain: domain}
349 nd[pmaddr.String()] = config.Destination{}
350 nacc.Destinations = nd
351 nc.Accounts[accountName] = nacc
352 }
353
354 nc.Domains[domain.Name()] = confDomain
355
356 if err := writeDynamic(ctx, log, nc); err != nil {
357 return fmt.Errorf("writing domains.conf: %v", err)
358 }
359 log.Info("domain added", mlog.Field("domain", domain))
360 cleanupFiles = nil // All good, don't cleanup.
361 return nil
362}
363
364// DomainRemove removes domain from the config, rewriting domains.conf.
365//
366// No accounts are removed, also not when they still reference this domain.
367func DomainRemove(ctx context.Context, domain dns.Domain) (rerr error) {
368 log := xlog.WithContext(ctx)
369 defer func() {
370 if rerr != nil {
371 log.Errorx("removing domain", rerr, mlog.Field("domain", domain))
372 }
373 }()
374
375 Conf.dynamicMutex.Lock()
376 defer Conf.dynamicMutex.Unlock()
377
378 c := Conf.Dynamic
379 domConf, ok := c.Domains[domain.Name()]
380 if !ok {
381 return fmt.Errorf("domain does not exist")
382 }
383
384 // Compose new config without modifying existing data structures. If we fail, we
385 // leave no trace.
386 nc := c
387 nc.Domains = map[string]config.Domain{}
388 s := domain.Name()
389 for name, d := range c.Domains {
390 if name != s {
391 nc.Domains[name] = d
392 }
393 }
394
395 if err := writeDynamic(ctx, log, nc); err != nil {
396 return fmt.Errorf("writing domains.conf: %v", err)
397 }
398
399 // Move away any DKIM private keys to a subdirectory "old". But only if
400 // they are not in use by other domains.
401 usedKeyPaths := map[string]bool{}
402 for _, dc := range nc.Domains {
403 for _, sel := range dc.DKIM.Selectors {
404 usedKeyPaths[filepath.Clean(sel.PrivateKeyFile)] = true
405 }
406 }
407 for _, sel := range domConf.DKIM.Selectors {
408 if sel.PrivateKeyFile == "" || usedKeyPaths[filepath.Clean(sel.PrivateKeyFile)] {
409 continue
410 }
411 src := ConfigDirPath(sel.PrivateKeyFile)
412 dst := ConfigDirPath(filepath.Join(filepath.Dir(sel.PrivateKeyFile), "old", filepath.Base(sel.PrivateKeyFile)))
413 _, err := os.Stat(dst)
414 if err == nil {
415 err = fmt.Errorf("destination already exists")
416 } else if os.IsNotExist(err) {
417 os.MkdirAll(filepath.Dir(dst), 0770)
418 err = os.Rename(src, dst)
419 }
420 if err != nil {
421 log.Errorx("renaming dkim private key file for removed domain", err, mlog.Field("src", src), mlog.Field("dst", dst))
422 }
423 }
424
425 log.Info("domain removed", mlog.Field("domain", domain))
426 return nil
427}
428
429func WebserverConfigSet(ctx context.Context, domainRedirects map[string]string, webhandlers []config.WebHandler) (rerr error) {
430 log := xlog.WithContext(ctx)
431 defer func() {
432 if rerr != nil {
433 log.Errorx("saving webserver config", rerr)
434 }
435 }()
436
437 Conf.dynamicMutex.Lock()
438 defer Conf.dynamicMutex.Unlock()
439
440 // Compose new config without modifying existing data structures. If we fail, we
441 // leave no trace.
442 nc := Conf.Dynamic
443 nc.WebDomainRedirects = domainRedirects
444 nc.WebHandlers = webhandlers
445
446 if err := writeDynamic(ctx, log, nc); err != nil {
447 return fmt.Errorf("writing domains.conf: %v", err)
448 }
449
450 log.Info("webserver config saved")
451 return nil
452}
453
454// 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.
455
456// DomainRecords returns text lines describing DNS records required for configuring
457// a domain.
458func DomainRecords(domConf config.Domain, domain dns.Domain, hasDNSSEC bool) ([]string, error) {
459 d := domain.ASCII
460 h := Conf.Static.HostnameDomain.ASCII
461
462 records := []string{
463 "; Time To Live of 5 minutes, may be recognized if importing as a zone file.",
464 "; Once your setup is working, you may want to increase the TTL.",
465 "$TTL 300",
466 "",
467 }
468
469 if public, ok := Conf.Static.Listeners["public"]; ok && public.TLS != nil && (len(public.TLS.HostPrivateRSA2048Keys) > 0 || len(public.TLS.HostPrivateECDSAP256Keys) > 0) {
470 records = append(records,
471 "; DANE: These records indicate that a remote mail server trying to deliver email",
472 "; with SMTP (TCP port 25) must verify the TLS certificate with DANE-EE (3), based",
473 "; on the certificate public key (\"SPKI\", 1) that is SHA2-256-hashed (1) to the",
474 "; hexadecimal hash. DANE-EE verification means only the certificate or public",
475 "; key is verified, not whether the certificate is signed by a (centralized)",
476 "; certificate authority (CA), is expired, or matches the host name.",
477 ";",
478 "; NOTE: Create the records below only once: They are for the machine, and apply",
479 "; to all hosted domains.",
480 )
481 if !hasDNSSEC {
482 records = append(records,
483 ";",
484 "; WARNING: Domain does not appear to be DNSSEC-signed. To enable DANE, first",
485 "; enable DNSSEC on your domain, then add the TLSA records. Records below have been",
486 "; commented out.",
487 )
488 }
489 addTLSA := func(privKey crypto.Signer) error {
490 spkiBuf, err := x509.MarshalPKIXPublicKey(privKey.Public())
491 if err != nil {
492 return fmt.Errorf("marshal SubjectPublicKeyInfo for DANE record: %v", err)
493 }
494 sum := sha256.Sum256(spkiBuf)
495 tlsaRecord := adns.TLSA{
496 Usage: adns.TLSAUsageDANEEE,
497 Selector: adns.TLSASelectorSPKI,
498 MatchType: adns.TLSAMatchTypeSHA256,
499 CertAssoc: sum[:],
500 }
501 var s string
502 if hasDNSSEC {
503 s = fmt.Sprintf("_25._tcp.%-*s TLSA %s", 20+len(d)-len("_25._tcp."), h+".", tlsaRecord.Record())
504 } else {
505 s = fmt.Sprintf(";; _25._tcp.%-*s TLSA %s", 20+len(d)-len(";; _25._tcp."), h+".", tlsaRecord.Record())
506 }
507 records = append(records, s)
508 return nil
509 }
510 for _, privKey := range public.TLS.HostPrivateECDSAP256Keys {
511 if err := addTLSA(privKey); err != nil {
512 return nil, err
513 }
514 }
515 for _, privKey := range public.TLS.HostPrivateRSA2048Keys {
516 if err := addTLSA(privKey); err != nil {
517 return nil, err
518 }
519 }
520 records = append(records, "")
521 }
522
523 if d != h {
524 records = append(records,
525 "; For the machine, only needs to be created once, for the first domain added:",
526 "; ",
527 "; SPF-allow host for itself, resulting in relaxed DMARC pass for (postmaster)",
528 "; messages (DSNs) sent from host:",
529 fmt.Sprintf(`%-*s TXT "v=spf1 a -all"`, 20+len(d), h+"."), // ../rfc/7208:2263 ../rfc/7208:2287
530 "",
531 )
532 }
533 if d != h && Conf.Static.HostTLSRPT.ParsedLocalpart != "" {
534 uri := url.URL{
535 Scheme: "mailto",
536 Opaque: smtp.NewAddress(Conf.Static.HostTLSRPT.ParsedLocalpart, Conf.Static.HostnameDomain).Pack(false),
537 }
538 tlsrptr := tlsrpt.Record{Version: "TLSRPTv1", RUAs: [][]tlsrpt.RUA{{tlsrpt.RUA(uri.String())}}}
539 records = append(records,
540 "; For the machine, only needs to be created once, for the first domain added:",
541 "; ",
542 "; Request reporting about success/failures of TLS connections to (MX) host, for DANE.",
543 fmt.Sprintf(`_smtp._tls.%-*s TXT "%s"`, 20+len(d)-len("_smtp._tls."), h+".", tlsrptr.String()),
544 "",
545 )
546 }
547
548 records = append(records,
549 "; Deliver email for the domain to this host.",
550 fmt.Sprintf("%s. MX 10 %s.", d, h),
551 "",
552
553 "; Outgoing messages will be signed with the first two DKIM keys. The other two",
554 "; configured for backup, switching to them is just a config change.",
555 )
556 var selectors []string
557 for name := range domConf.DKIM.Selectors {
558 selectors = append(selectors, name)
559 }
560 sort.Slice(selectors, func(i, j int) bool {
561 return selectors[i] < selectors[j]
562 })
563 for _, name := range selectors {
564 sel := domConf.DKIM.Selectors[name]
565 dkimr := dkim.Record{
566 Version: "DKIM1",
567 Hashes: []string{"sha256"},
568 PublicKey: sel.Key.Public(),
569 }
570 if _, ok := sel.Key.(ed25519.PrivateKey); ok {
571 dkimr.Key = "ed25519"
572 } else if _, ok := sel.Key.(*rsa.PrivateKey); !ok {
573 return nil, fmt.Errorf("unrecognized private key for DKIM selector %q: %T", name, sel.Key)
574 }
575 txt, err := dkimr.Record()
576 if err != nil {
577 return nil, fmt.Errorf("making DKIM DNS TXT record: %v", err)
578 }
579
580 if len(txt) > 100 {
581 records = append(records,
582 "; NOTE: The following strings must be added to DNS as single record.",
583 )
584 }
585 s := fmt.Sprintf("%s._domainkey.%s. TXT %s", name, d, TXTStrings(txt))
586 records = append(records, s)
587
588 }
589 dmarcr := dmarc.DefaultRecord
590 dmarcr.Policy = "reject"
591 if domConf.DMARC != nil {
592 uri := url.URL{
593 Scheme: "mailto",
594 Opaque: smtp.NewAddress(domConf.DMARC.ParsedLocalpart, domConf.DMARC.DNSDomain).Pack(false),
595 }
596 dmarcr.AggregateReportAddresses = []dmarc.URI{
597 {Address: uri.String(), MaxSize: 10, Unit: "m"},
598 }
599 }
600 records = append(records,
601 "",
602
603 "; Specify the MX host is allowed to send for our domain and for itself (for DSNs).",
604 "; ~all means softfail for anything else, which is done instead of -all to prevent older",
605 "; mail servers from rejecting the message because they never get to looking for a dkim/dmarc pass.",
606 fmt.Sprintf(`%s. TXT "v=spf1 mx ~all"`, d),
607 "",
608
609 "; Emails that fail the DMARC check (without aligned DKIM and without aligned SPF)",
610 "; should be rejected, and request reports. If you email through mailing lists that",
611 "; strip DKIM-Signature headers and don't rewrite the From header, you may want to",
612 "; set the policy to p=none.",
613 fmt.Sprintf(`_dmarc.%s. TXT "%s"`, d, dmarcr.String()),
614 "",
615 )
616
617 if sts := domConf.MTASTS; sts != nil {
618 records = append(records,
619 "; Remote servers can use MTA-STS to verify our TLS certificate with the",
620 "; WebPKI pool of CA's (certificate authorities) when delivering over SMTP with",
621 "; STARTTLSTLS.",
622 fmt.Sprintf(`mta-sts.%s. CNAME %s.`, d, h),
623 fmt.Sprintf(`_mta-sts.%s. TXT "v=STSv1; id=%s"`, d, sts.PolicyID),
624 "",
625 )
626 } else {
627 records = append(records,
628 "; Note: No MTA-STS to indicate TLS should be used. Either because disabled for the",
629 "; domain or because mox.conf does not have a listener with MTA-STS configured.",
630 "",
631 )
632 }
633
634 if domConf.TLSRPT != nil {
635 uri := url.URL{
636 Scheme: "mailto",
637 Opaque: smtp.NewAddress(domConf.TLSRPT.ParsedLocalpart, domConf.TLSRPT.DNSDomain).Pack(false),
638 }
639 tlsrptr := tlsrpt.Record{Version: "TLSRPTv1", RUAs: [][]tlsrpt.RUA{{tlsrpt.RUA(uri.String())}}}
640 records = append(records,
641 "; Request reporting about TLS failures.",
642 fmt.Sprintf(`_smtp._tls.%s. TXT "%s"`, d, tlsrptr.String()),
643 "",
644 )
645 }
646
647 records = append(records,
648 "; Autoconfig is used by Thunderbird. Autodiscover is (in theory) used by Microsoft.",
649 fmt.Sprintf(`autoconfig.%s. CNAME %s.`, d, h),
650 fmt.Sprintf(`_autodiscover._tcp.%s. SRV 0 1 443 %s.`, d, h),
651 "",
652
653 // ../rfc/6186:133 ../rfc/8314:692
654 "; For secure IMAP and submission autoconfig, point to mail host.",
655 fmt.Sprintf(`_imaps._tcp.%s. SRV 0 1 993 %s.`, d, h),
656 fmt.Sprintf(`_submissions._tcp.%s. SRV 0 1 465 %s.`, d, h),
657 "",
658 // ../rfc/6186:242
659 "; Next records specify POP3 and non-TLS ports are not to be used.",
660 "; These are optional and safe to leave out (e.g. if you have to click a lot in a",
661 "; DNS admin web interface).",
662 fmt.Sprintf(`_imap._tcp.%s. SRV 0 1 143 .`, d),
663 fmt.Sprintf(`_submission._tcp.%s. SRV 0 1 587 .`, d),
664 fmt.Sprintf(`_pop3._tcp.%s. SRV 0 1 110 .`, d),
665 fmt.Sprintf(`_pop3s._tcp.%s. SRV 0 1 995 .`, d),
666 "",
667
668 "; Optional:",
669 "; You could mark Let's Encrypt as the only Certificate Authority allowed to",
670 "; sign TLS certificates for your domain.",
671 fmt.Sprintf("%s. CAA 0 issue \"letsencrypt.org\"", d),
672 )
673 return records, nil
674}
675
676// AccountAdd adds an account and an initial address and reloads the configuration.
677//
678// The new account does not have a password, so cannot yet log in. Email can be
679// delivered.
680//
681// Catchall addresses are not supported for AccountAdd. Add separately with AddressAdd.
682func AccountAdd(ctx context.Context, account, address string) (rerr error) {
683 log := xlog.WithContext(ctx)
684 defer func() {
685 if rerr != nil {
686 log.Errorx("adding account", rerr, mlog.Field("account", account), mlog.Field("address", address))
687 }
688 }()
689
690 addr, err := smtp.ParseAddress(address)
691 if err != nil {
692 return fmt.Errorf("parsing email address: %v", err)
693 }
694
695 Conf.dynamicMutex.Lock()
696 defer Conf.dynamicMutex.Unlock()
697
698 c := Conf.Dynamic
699 if _, ok := c.Accounts[account]; ok {
700 return fmt.Errorf("account already present")
701 }
702
703 if err := checkAddressAvailable(addr); err != nil {
704 return fmt.Errorf("address not available: %v", err)
705 }
706
707 // Compose new config without modifying existing data structures. If we fail, we
708 // leave no trace.
709 nc := c
710 nc.Accounts = map[string]config.Account{}
711 for name, a := range c.Accounts {
712 nc.Accounts[name] = a
713 }
714 nc.Accounts[account] = MakeAccountConfig(addr)
715
716 if err := writeDynamic(ctx, log, nc); err != nil {
717 return fmt.Errorf("writing domains.conf: %v", err)
718 }
719 log.Info("account added", mlog.Field("account", account), mlog.Field("address", addr))
720 return nil
721}
722
723// AccountRemove removes an account and reloads the configuration.
724func AccountRemove(ctx context.Context, account string) (rerr error) {
725 log := xlog.WithContext(ctx)
726 defer func() {
727 if rerr != nil {
728 log.Errorx("adding account", rerr, mlog.Field("account", account))
729 }
730 }()
731
732 Conf.dynamicMutex.Lock()
733 defer Conf.dynamicMutex.Unlock()
734
735 c := Conf.Dynamic
736 if _, ok := c.Accounts[account]; !ok {
737 return fmt.Errorf("account does not exist")
738 }
739
740 // Compose new config without modifying existing data structures. If we fail, we
741 // leave no trace.
742 nc := c
743 nc.Accounts = map[string]config.Account{}
744 for name, a := range c.Accounts {
745 if name != account {
746 nc.Accounts[name] = a
747 }
748 }
749
750 if err := writeDynamic(ctx, log, nc); err != nil {
751 return fmt.Errorf("writing domains.conf: %v", err)
752 }
753 log.Info("account removed", mlog.Field("account", account))
754 return nil
755}
756
757// checkAddressAvailable checks that the address after canonicalization is not
758// already configured, and that its localpart does not contain the catchall
759// localpart separator.
760//
761// Must be called with config lock held.
762func checkAddressAvailable(addr smtp.Address) error {
763 if dc, ok := Conf.Dynamic.Domains[addr.Domain.Name()]; !ok {
764 return fmt.Errorf("domain does not exist")
765 } else if lp, err := CanonicalLocalpart(addr.Localpart, dc); err != nil {
766 return fmt.Errorf("canonicalizing localpart: %v", err)
767 } else if _, ok := Conf.accountDestinations[smtp.NewAddress(lp, addr.Domain).String()]; ok {
768 return fmt.Errorf("canonicalized address %s already configured", smtp.NewAddress(lp, addr.Domain))
769 } else if dc.LocalpartCatchallSeparator != "" && strings.Contains(string(addr.Localpart), dc.LocalpartCatchallSeparator) {
770 return fmt.Errorf("localpart cannot include domain catchall separator %s", dc.LocalpartCatchallSeparator)
771 }
772 return nil
773}
774
775// AddressAdd adds an email address to an account and reloads the configuration. If
776// address starts with an @ it is treated as a catchall address for the domain.
777func AddressAdd(ctx context.Context, address, account string) (rerr error) {
778 log := xlog.WithContext(ctx)
779 defer func() {
780 if rerr != nil {
781 log.Errorx("adding address", rerr, mlog.Field("address", address), mlog.Field("account", account))
782 }
783 }()
784
785 Conf.dynamicMutex.Lock()
786 defer Conf.dynamicMutex.Unlock()
787
788 c := Conf.Dynamic
789 a, ok := c.Accounts[account]
790 if !ok {
791 return fmt.Errorf("account does not exist")
792 }
793
794 var destAddr string
795 if strings.HasPrefix(address, "@") {
796 d, err := dns.ParseDomain(address[1:])
797 if err != nil {
798 return fmt.Errorf("parsing domain: %v", err)
799 }
800 dname := d.Name()
801 destAddr = "@" + dname
802 if _, ok := Conf.Dynamic.Domains[dname]; !ok {
803 return fmt.Errorf("domain does not exist")
804 } else if _, ok := Conf.accountDestinations[destAddr]; ok {
805 return fmt.Errorf("catchall address already configured for domain")
806 }
807 } else {
808 addr, err := smtp.ParseAddress(address)
809 if err != nil {
810 return fmt.Errorf("parsing email address: %v", err)
811 }
812
813 if err := checkAddressAvailable(addr); err != nil {
814 return fmt.Errorf("address not available: %v", err)
815 }
816 destAddr = addr.String()
817 }
818
819 // Compose new config without modifying existing data structures. If we fail, we
820 // leave no trace.
821 nc := c
822 nc.Accounts = map[string]config.Account{}
823 for name, a := range c.Accounts {
824 nc.Accounts[name] = a
825 }
826 nd := map[string]config.Destination{}
827 for name, d := range a.Destinations {
828 nd[name] = d
829 }
830 nd[destAddr] = config.Destination{}
831 a.Destinations = nd
832 nc.Accounts[account] = a
833
834 if err := writeDynamic(ctx, log, nc); err != nil {
835 return fmt.Errorf("writing domains.conf: %v", err)
836 }
837 log.Info("address added", mlog.Field("address", address), mlog.Field("account", account))
838 return nil
839}
840
841// AddressRemove removes an email address and reloads the configuration.
842func AddressRemove(ctx context.Context, address string) (rerr error) {
843 log := xlog.WithContext(ctx)
844 defer func() {
845 if rerr != nil {
846 log.Errorx("removing address", rerr, mlog.Field("address", address))
847 }
848 }()
849
850 Conf.dynamicMutex.Lock()
851 defer Conf.dynamicMutex.Unlock()
852
853 ad, ok := Conf.accountDestinations[address]
854 if !ok {
855 return fmt.Errorf("address does not exists")
856 }
857
858 // Compose new config without modifying existing data structures. If we fail, we
859 // leave no trace.
860 a, ok := Conf.Dynamic.Accounts[ad.Account]
861 if !ok {
862 return fmt.Errorf("internal error: cannot find account")
863 }
864 na := a
865 na.Destinations = map[string]config.Destination{}
866 var dropped bool
867 for destAddr, d := range a.Destinations {
868 if destAddr != address {
869 na.Destinations[destAddr] = d
870 } else {
871 dropped = true
872 }
873 }
874 if !dropped {
875 return fmt.Errorf("address not removed, likely a postmaster/reporting address")
876 }
877 nc := Conf.Dynamic
878 nc.Accounts = map[string]config.Account{}
879 for name, a := range Conf.Dynamic.Accounts {
880 nc.Accounts[name] = a
881 }
882 nc.Accounts[ad.Account] = na
883
884 if err := writeDynamic(ctx, log, nc); err != nil {
885 return fmt.Errorf("writing domains.conf: %v", err)
886 }
887 log.Info("address removed", mlog.Field("address", address), mlog.Field("account", ad.Account))
888 return nil
889}
890
891// AccountFullNameSave updates the full name for an account and reloads the configuration.
892func AccountFullNameSave(ctx context.Context, account, fullName string) (rerr error) {
893 log := xlog.WithContext(ctx)
894 defer func() {
895 if rerr != nil {
896 log.Errorx("saving account full name", rerr, mlog.Field("account", account))
897 }
898 }()
899
900 Conf.dynamicMutex.Lock()
901 defer Conf.dynamicMutex.Unlock()
902
903 c := Conf.Dynamic
904 acc, ok := c.Accounts[account]
905 if !ok {
906 return fmt.Errorf("account not present")
907 }
908
909 // Compose new config without modifying existing data structures. If we fail, we
910 // leave no trace.
911 nc := c
912 nc.Accounts = map[string]config.Account{}
913 for name, a := range c.Accounts {
914 nc.Accounts[name] = a
915 }
916
917 acc.FullName = fullName
918 nc.Accounts[account] = acc
919
920 if err := writeDynamic(ctx, log, nc); err != nil {
921 return fmt.Errorf("writing domains.conf: %v", err)
922 }
923 log.Info("account full name saved", mlog.Field("account", account))
924 return nil
925}
926
927// DestinationSave updates a destination for an account and reloads the configuration.
928func DestinationSave(ctx context.Context, account, destName string, newDest config.Destination) (rerr error) {
929 log := xlog.WithContext(ctx)
930 defer func() {
931 if rerr != nil {
932 log.Errorx("saving destination", rerr, mlog.Field("account", account), mlog.Field("destname", destName), mlog.Field("destination", newDest))
933 }
934 }()
935
936 Conf.dynamicMutex.Lock()
937 defer Conf.dynamicMutex.Unlock()
938
939 c := Conf.Dynamic
940 acc, ok := c.Accounts[account]
941 if !ok {
942 return fmt.Errorf("account not present")
943 }
944
945 if _, ok := acc.Destinations[destName]; !ok {
946 return fmt.Errorf("destination not present")
947 }
948
949 // Compose new config without modifying existing data structures. If we fail, we
950 // leave no trace.
951 nc := c
952 nc.Accounts = map[string]config.Account{}
953 for name, a := range c.Accounts {
954 nc.Accounts[name] = a
955 }
956 nd := map[string]config.Destination{}
957 for dn, d := range acc.Destinations {
958 nd[dn] = d
959 }
960 nd[destName] = newDest
961 nacc := nc.Accounts[account]
962 nacc.Destinations = nd
963 nc.Accounts[account] = nacc
964
965 if err := writeDynamic(ctx, log, nc); err != nil {
966 return fmt.Errorf("writing domains.conf: %v", err)
967 }
968 log.Info("destination saved", mlog.Field("account", account), mlog.Field("destname", destName))
969 return nil
970}
971
972// AccountLimitsSave saves new message sending limits for an account.
973func AccountLimitsSave(ctx context.Context, account string, maxOutgoingMessagesPerDay, maxFirstTimeRecipientsPerDay int) (rerr error) {
974 log := xlog.WithContext(ctx)
975 defer func() {
976 if rerr != nil {
977 log.Errorx("saving account limits", rerr, mlog.Field("account", account))
978 }
979 }()
980
981 Conf.dynamicMutex.Lock()
982 defer Conf.dynamicMutex.Unlock()
983
984 c := Conf.Dynamic
985 acc, ok := c.Accounts[account]
986 if !ok {
987 return fmt.Errorf("account not present")
988 }
989
990 // Compose new config without modifying existing data structures. If we fail, we
991 // leave no trace.
992 nc := c
993 nc.Accounts = map[string]config.Account{}
994 for name, a := range c.Accounts {
995 nc.Accounts[name] = a
996 }
997 acc.MaxOutgoingMessagesPerDay = maxOutgoingMessagesPerDay
998 acc.MaxFirstTimeRecipientsPerDay = maxFirstTimeRecipientsPerDay
999 nc.Accounts[account] = acc
1000
1001 if err := writeDynamic(ctx, log, nc); err != nil {
1002 return fmt.Errorf("writing domains.conf: %v", err)
1003 }
1004 log.Info("account limits saved", mlog.Field("account", account))
1005 return nil
1006}
1007
1008type TLSMode uint8
1009
1010const (
1011 TLSModeImmediate TLSMode = 0
1012 TLSModeSTARTTLS TLSMode = 1
1013 TLSModeNone TLSMode = 2
1014)
1015
1016type ProtocolConfig struct {
1017 Host dns.Domain
1018 Port int
1019 TLSMode TLSMode
1020}
1021
1022type ClientConfig struct {
1023 IMAP ProtocolConfig
1024 Submission ProtocolConfig
1025}
1026
1027// ClientConfigDomain returns a single IMAP and Submission client configuration for
1028// a domain.
1029func ClientConfigDomain(d dns.Domain) (rconfig ClientConfig, rerr error) {
1030 var haveIMAP, haveSubmission bool
1031
1032 if _, ok := Conf.Domain(d); !ok {
1033 return ClientConfig{}, fmt.Errorf("unknown domain")
1034 }
1035
1036 gather := func(l config.Listener) (done bool) {
1037 host := Conf.Static.HostnameDomain
1038 if l.Hostname != "" {
1039 host = l.HostnameDomain
1040 }
1041 if !haveIMAP && l.IMAPS.Enabled {
1042 rconfig.IMAP.Host = host
1043 rconfig.IMAP.Port = config.Port(l.IMAPS.Port, 993)
1044 rconfig.IMAP.TLSMode = TLSModeImmediate
1045 haveIMAP = true
1046 }
1047 if !haveIMAP && l.IMAP.Enabled {
1048 rconfig.IMAP.Host = host
1049 rconfig.IMAP.Port = config.Port(l.IMAP.Port, 143)
1050 rconfig.IMAP.TLSMode = TLSModeSTARTTLS
1051 if l.TLS == nil {
1052 rconfig.IMAP.TLSMode = TLSModeNone
1053 }
1054 haveIMAP = true
1055 }
1056 if !haveSubmission && l.Submissions.Enabled {
1057 rconfig.Submission.Host = host
1058 rconfig.Submission.Port = config.Port(l.Submissions.Port, 465)
1059 rconfig.Submission.TLSMode = TLSModeImmediate
1060 haveSubmission = true
1061 }
1062 if !haveSubmission && l.Submission.Enabled {
1063 rconfig.Submission.Host = host
1064 rconfig.Submission.Port = config.Port(l.Submission.Port, 587)
1065 rconfig.Submission.TLSMode = TLSModeSTARTTLS
1066 if l.TLS == nil {
1067 rconfig.Submission.TLSMode = TLSModeNone
1068 }
1069 haveSubmission = true
1070 }
1071 return haveIMAP && haveSubmission
1072 }
1073
1074 // Look at the public listener first. Most likely the intended configuration.
1075 if public, ok := Conf.Static.Listeners["public"]; ok {
1076 if gather(public) {
1077 return
1078 }
1079 }
1080 // Go through the other listeners in consistent order.
1081 names := maps.Keys(Conf.Static.Listeners)
1082 sort.Strings(names)
1083 for _, name := range names {
1084 if gather(Conf.Static.Listeners[name]) {
1085 return
1086 }
1087 }
1088 return ClientConfig{}, fmt.Errorf("no listeners found for imap and/or submission")
1089}
1090
1091// ClientConfigs holds the client configuration for IMAP/Submission for a
1092// domain.
1093type ClientConfigs struct {
1094 Entries []ClientConfigsEntry
1095}
1096
1097type ClientConfigsEntry struct {
1098 Protocol string
1099 Host dns.Domain
1100 Port int
1101 Listener string
1102 Note string
1103}
1104
1105// ClientConfigsDomain returns the client configs for IMAP/Submission for a
1106// domain.
1107func ClientConfigsDomain(d dns.Domain) (ClientConfigs, error) {
1108 _, ok := Conf.Domain(d)
1109 if !ok {
1110 return ClientConfigs{}, fmt.Errorf("unknown domain")
1111 }
1112
1113 c := ClientConfigs{}
1114 c.Entries = []ClientConfigsEntry{}
1115 var listeners []string
1116
1117 for name := range Conf.Static.Listeners {
1118 listeners = append(listeners, name)
1119 }
1120 sort.Slice(listeners, func(i, j int) bool {
1121 return listeners[i] < listeners[j]
1122 })
1123
1124 note := func(tls bool, requiretls bool) string {
1125 if !tls {
1126 return "plain text, no STARTTLS configured"
1127 }
1128 if requiretls {
1129 return "STARTTLS required"
1130 }
1131 return "STARTTLS optional"
1132 }
1133
1134 for _, name := range listeners {
1135 l := Conf.Static.Listeners[name]
1136 host := Conf.Static.HostnameDomain
1137 if l.Hostname != "" {
1138 host = l.HostnameDomain
1139 }
1140 if l.Submissions.Enabled {
1141 c.Entries = append(c.Entries, ClientConfigsEntry{"Submission (SMTP)", host, config.Port(l.Submissions.Port, 465), name, "with TLS"})
1142 }
1143 if l.IMAPS.Enabled {
1144 c.Entries = append(c.Entries, ClientConfigsEntry{"IMAP", host, config.Port(l.IMAPS.Port, 993), name, "with TLS"})
1145 }
1146 if l.Submission.Enabled {
1147 c.Entries = append(c.Entries, ClientConfigsEntry{"Submission (SMTP)", host, config.Port(l.Submission.Port, 587), name, note(l.TLS != nil, !l.Submission.NoRequireSTARTTLS)})
1148 }
1149 if l.IMAP.Enabled {
1150 c.Entries = append(c.Entries, ClientConfigsEntry{"IMAP", host, config.Port(l.IMAPS.Port, 143), name, note(l.TLS != nil, !l.IMAP.NoRequireSTARTTLS)})
1151 }
1152 }
1153
1154 return c, nil
1155}
1156
1157// IPs returns ip addresses we may be listening/receiving mail on or
1158// connecting/sending from to the outside.
1159func IPs(ctx context.Context, receiveOnly bool) ([]net.IP, error) {
1160 log := xlog.WithContext(ctx)
1161
1162 // Try to gather all IPs we are listening on by going through the config.
1163 // If we encounter 0.0.0.0 or ::, we'll gather all local IPs afterwards.
1164 var ips []net.IP
1165 var ipv4all, ipv6all bool
1166 for _, l := range Conf.Static.Listeners {
1167 // If NATed, we don't know our external IPs.
1168 if l.IPsNATed {
1169 return nil, nil
1170 }
1171 check := l.IPs
1172 if len(l.NATIPs) > 0 {
1173 check = l.NATIPs
1174 }
1175 for _, s := range check {
1176 ip := net.ParseIP(s)
1177 if ip.IsUnspecified() {
1178 if ip.To4() != nil {
1179 ipv4all = true
1180 } else {
1181 ipv6all = true
1182 }
1183 continue
1184 }
1185 ips = append(ips, ip)
1186 }
1187 }
1188
1189 // We'll list the IPs on the interfaces. How useful is this? There is a good chance
1190 // we're listening on all addresses because of a load balancer/firewall.
1191 if ipv4all || ipv6all {
1192 ifaces, err := net.Interfaces()
1193 if err != nil {
1194 return nil, fmt.Errorf("listing network interfaces: %v", err)
1195 }
1196 for _, iface := range ifaces {
1197 if iface.Flags&net.FlagUp == 0 {
1198 continue
1199 }
1200 addrs, err := iface.Addrs()
1201 if err != nil {
1202 return nil, fmt.Errorf("listing addresses for network interface: %v", err)
1203 }
1204 if len(addrs) == 0 {
1205 continue
1206 }
1207
1208 for _, addr := range addrs {
1209 ip, _, err := net.ParseCIDR(addr.String())
1210 if err != nil {
1211 log.Errorx("bad interface addr", err, mlog.Field("address", addr))
1212 continue
1213 }
1214 v4 := ip.To4() != nil
1215 if ipv4all && v4 || ipv6all && !v4 {
1216 ips = append(ips, ip)
1217 }
1218 }
1219 }
1220 }
1221
1222 if receiveOnly {
1223 return ips, nil
1224 }
1225
1226 for _, t := range Conf.Static.Transports {
1227 if t.Socks != nil {
1228 ips = append(ips, t.Socks.IPs...)
1229 }
1230 }
1231
1232 return ips, nil
1233}
1234