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