7 cryptorand "crypto/rand"
20 "golang.org/x/exp/maps"
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"
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 {
45 r += `"` + s[:n] + `"`
51// MakeDKIMEd25519Key returns a PEM buffer containing an ed25519 key for use
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)
57 return nil, fmt.Errorf("generating key: %w", err)
60 pkcs8, err := x509.MarshalPKCS8PrivateKey(privKey)
62 return nil, fmt.Errorf("marshal key: %w", err)
67 Headers: map[string]string{
68 "Note": dkimKeyNote("ed25519", selector, domain),
73 if err := pem.Encode(b, block); err != nil {
74 return nil, fmt.Errorf("encoding pem: %w", err)
79func dkimKeyNote(kind string, selector, domain dns.Domain) string {
80 s := kind + " dkim private key"
82 if selector != zero && domain != zero {
83 s += fmt.Sprintf(" for %s._domainkey.%s", selector.ASCII, domain.ASCII)
85 s += fmt.Sprintf(", generated by mox on %s", time.Now().Format(time.RFC3339))
89// MakeDKIMEd25519Key returns a PEM buffer containing an rsa key for use with
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)
97 return nil, fmt.Errorf("generating key: %w", err)
100 pkcs8, err := x509.MarshalPKCS8PrivateKey(privKey)
102 return nil, fmt.Errorf("marshal key: %w", err)
107 Headers: map[string]string{
108 "Note": dkimKeyNote("rsa", selector, domain),
113 if err := pem.Encode(b, block); err != nil {
114 return nil, fmt.Errorf("encoding pem: %w", err)
116 return b.Bytes(), nil
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{
126 RejectsMailbox: "Rejects",
127 JunkFilter: &config.JunkFilter{
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
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)
151 year := now.Format("2006")
152 timestamp := now.Format("20060102T150405")
156 for _, p := range paths {
158 log.Check(err, "removing path for domain config", mlog.Field("path", p))
162 writeFile := func(path string, data []byte) error {
163 os.MkdirAll(filepath.Dir(path), 0770)
165 f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0660)
167 return fmt.Errorf("creating file %s: %s", path, err)
171 err := os.Remove(path)
172 log.Check(err, "removing file after error")
174 log.Check(err, "closing file after error")
177 if _, err := f.Write(data); err != nil {
178 return fmt.Errorf("writing file %s: %s", path, err)
180 if err := f.Close(); err != nil {
181 return fmt.Errorf("close file: %v", err)
187 confDKIM := config.DKIM{
188 Selectors: map[string]config.Selector{},
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 {
198 paths = append(paths, p)
199 confDKIM.Selectors[name] = config.Selector{
202 // Messages in the wild have been observed with 2 hours and 1 year expiration.
204 PrivateKeyFile: keyPath,
209 addEd25519 := func(name string) error {
210 key, err := MakeDKIMEd25519Key(dns.Domain{ASCII: name}, domain)
212 return fmt.Errorf("making dkim ed25519 private key: %s", err)
214 return addSelector("ed25519", name, key)
217 addRSA := func(name string) error {
218 key, err := MakeDKIMRSAKey(dns.Domain{ASCII: name}, domain)
220 return fmt.Errorf("making dkim rsa private key: %s", err)
222 return addSelector("rsa", name, key)
225 if err := addEd25519(year + "a"); err != nil {
226 return config.Domain{}, nil, err
228 if err := addRSA(year + "b"); err != nil {
229 return config.Domain{}, nil, err
231 if err := addEd25519(year + "c"); err != nil {
232 return config.Domain{}, nil, err
234 if err := addRSA(year + "d"); err != nil {
235 return config.Domain{}, nil, err
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"}
243 confDomain := config.Domain{
244 LocalpartCatchallSeparator: "+",
246 DMARC: &config.DMARC{
247 Account: accountName,
248 Localpart: "dmarc-reports",
251 TLSRPT: &config.TLSRPT{
252 Account: accountName,
253 Localpart: "tls-reports",
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},
272 return confDomain, rpaths, nil
275// DomainAdd adds the domain to the domains config, rewriting domains.conf and
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)
285 log.Errorx("adding domain", rerr, mlog.Field("domain", domain), mlog.Field("account", accountName), mlog.Field("localpart", localpart))
289 Conf.dynamicMutex.Lock()
290 defer Conf.dynamicMutex.Unlock()
293 if _, ok := c.Domains[domain.Name()]; ok {
294 return fmt.Errorf("domain already present")
297 // Compose new config without modifying existing data structures. If we fail, we
300 nc.Domains = map[string]config.Domain{}
301 for name, d := range c.Domains {
305 // Only enable mta-sts for domain if there is a listener with mta-sts.
307 for _, l := range Conf.Static.Listeners {
308 if l.MTASTSHTTPS.Enabled {
314 confDomain, cleanupFiles, err := MakeDomainConfig(ctx, domain, Conf.Static.HostnameDomain, accountName, withMTASTS)
316 return fmt.Errorf("preparing domain config: %v", err)
319 for _, f := range cleanupFiles {
321 log.Check(err, "cleaning up file after error", mlog.Field("path", f))
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")
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 {
339 pmaddr := smtp.Address{Localpart: "postmaster", Domain: domain}
340 nd[pmaddr.String()] = config.Destination{}
341 nacc.Destinations = nd
342 nc.Accounts[accountName] = nacc
345 nc.Domains[domain.Name()] = confDomain
347 if err := writeDynamic(ctx, log, nc); err != nil {
348 return fmt.Errorf("writing domains.conf: %v", err)
350 log.Info("domain added", mlog.Field("domain", domain))
351 cleanupFiles = nil // All good, don't cleanup.
355// DomainRemove removes domain from the config, rewriting domains.conf.
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)
362 log.Errorx("removing domain", rerr, mlog.Field("domain", domain))
366 Conf.dynamicMutex.Lock()
367 defer Conf.dynamicMutex.Unlock()
370 domConf, ok := c.Domains[domain.Name()]
372 return fmt.Errorf("domain does not exist")
375 // Compose new config without modifying existing data structures. If we fail, we
378 nc.Domains = map[string]config.Domain{}
380 for name, d := range c.Domains {
386 if err := writeDynamic(ctx, log, nc); err != nil {
387 return fmt.Errorf("writing domains.conf: %v", err)
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
398 for _, sel := range domConf.DKIM.Selectors {
399 if sel.PrivateKeyFile == "" || usedKeyPaths[filepath.Clean(sel.PrivateKeyFile)] {
402 src := ConfigDirPath(sel.PrivateKeyFile)
403 dst := ConfigDirPath(filepath.Join(filepath.Dir(sel.PrivateKeyFile), "old", filepath.Base(sel.PrivateKeyFile)))
404 _, err := os.Stat(dst)
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)
412 log.Errorx("renaming dkim private key file for removed domain", err, mlog.Field("src", src), mlog.Field("dst", dst))
416 log.Info("domain removed", mlog.Field("domain", domain))
420func WebserverConfigSet(ctx context.Context, domainRedirects map[string]string, webhandlers []config.WebHandler) (rerr error) {
421 log := xlog.WithContext(ctx)
424 log.Errorx("saving webserver config", rerr)
428 Conf.dynamicMutex.Lock()
429 defer Conf.dynamicMutex.Unlock()
431 // Compose new config without modifying existing data structures. If we fail, we
434 nc.WebDomainRedirects = domainRedirects
435 nc.WebHandlers = webhandlers
437 if err := writeDynamic(ctx, log, nc); err != nil {
438 return fmt.Errorf("writing domains.conf: %v", err)
441 log.Info("webserver config saved")
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.
447// DomainRecords returns text lines describing DNS records required for configuring
449func DomainRecords(domConf config.Domain, domain dns.Domain) ([]string, error) {
451 h := Conf.Static.HostnameDomain.ASCII
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.",
461 records = append(records,
462 "; For the machine, only needs to be created once, for the first domain added.",
468 records = append(records,
469 "; Deliver email for the domain to this host.",
470 fmt.Sprintf("%s. MX 10 %s.", d, h),
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.",
476 var selectors []string
477 for name := range domConf.DKIM.Selectors {
478 selectors = append(selectors, name)
480 sort.Slice(selectors, func(i, j int) bool {
481 return selectors[i] < selectors[j]
483 for _, name := range selectors {
484 sel := domConf.DKIM.Selectors[name]
485 dkimr := dkim.Record{
487 Hashes: []string{"sha256"},
488 PublicKey: sel.Key.Public(),
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)
495 txt, err := dkimr.Record()
497 return nil, fmt.Errorf("making DKIM DNS TXT record: %v", err)
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).",
506 s := fmt.Sprintf("%s._domainkey.%s. IN TXT %s", name, d, TXTStrings(txt))
507 records = append(records, s)
510 dmarcr := dmarc.DefaultRecord
511 dmarcr.Policy = "reject"
512 if domConf.DMARC != nil {
515 Opaque: smtp.NewAddress(domConf.DMARC.ParsedLocalpart, domConf.DMARC.DNSDomain).Pack(false),
517 dmarcr.AggregateReportAddresses = []dmarc.URI{
518 {Address: uri.String(), MaxSize: 10, Unit: "m"},
521 records = append(records,
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),
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()),
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),
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.",
553 if domConf.TLSRPT != nil {
556 Opaque: smtp.NewAddress(domConf.TLSRPT.ParsedLocalpart, domConf.TLSRPT.DNSDomain).Pack(false),
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()),
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),
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),
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),
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),
595// AccountAdd adds an account and an initial address and reloads the configuration.
597// The new account does not have a password, so cannot yet log in. Email can be
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)
605 log.Errorx("adding account", rerr, mlog.Field("account", account), mlog.Field("address", address))
609 addr, err := smtp.ParseAddress(address)
611 return fmt.Errorf("parsing email address: %v", err)
614 Conf.dynamicMutex.Lock()
615 defer Conf.dynamicMutex.Unlock()
618 if _, ok := c.Accounts[account]; ok {
619 return fmt.Errorf("account already present")
622 if err := checkAddressAvailable(addr); err != nil {
623 return fmt.Errorf("address not available: %v", err)
626 // Compose new config without modifying existing data structures. If we fail, we
629 nc.Accounts = map[string]config.Account{}
630 for name, a := range c.Accounts {
631 nc.Accounts[name] = a
633 nc.Accounts[account] = MakeAccountConfig(addr)
635 if err := writeDynamic(ctx, log, nc); err != nil {
636 return fmt.Errorf("writing domains.conf: %v", err)
638 log.Info("account added", mlog.Field("account", account), mlog.Field("address", addr))
642// AccountRemove removes an account and reloads the configuration.
643func AccountRemove(ctx context.Context, account string) (rerr error) {
644 log := xlog.WithContext(ctx)
647 log.Errorx("adding account", rerr, mlog.Field("account", account))
651 Conf.dynamicMutex.Lock()
652 defer Conf.dynamicMutex.Unlock()
655 if _, ok := c.Accounts[account]; !ok {
656 return fmt.Errorf("account does not exist")
659 // Compose new config without modifying existing data structures. If we fail, we
662 nc.Accounts = map[string]config.Account{}
663 for name, a := range c.Accounts {
665 nc.Accounts[name] = a
669 if err := writeDynamic(ctx, log, nc); err != nil {
670 return fmt.Errorf("writing domains.conf: %v", err)
672 log.Info("account removed", mlog.Field("account", account))
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.
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)
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)
700 log.Errorx("adding address", rerr, mlog.Field("address", address), mlog.Field("account", account))
704 Conf.dynamicMutex.Lock()
705 defer Conf.dynamicMutex.Unlock()
708 a, ok := c.Accounts[account]
710 return fmt.Errorf("account does not exist")
714 if strings.HasPrefix(address, "@") {
715 d, err := dns.ParseDomain(address[1:])
717 return fmt.Errorf("parsing domain: %v", err)
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")
727 addr, err := smtp.ParseAddress(address)
729 return fmt.Errorf("parsing email address: %v", err)
732 if err := checkAddressAvailable(addr); err != nil {
733 return fmt.Errorf("address not available: %v", err)
735 destAddr = addr.String()
738 // Compose new config without modifying existing data structures. If we fail, we
741 nc.Accounts = map[string]config.Account{}
742 for name, a := range c.Accounts {
743 nc.Accounts[name] = a
745 nd := map[string]config.Destination{}
746 for name, d := range a.Destinations {
749 nd[destAddr] = config.Destination{}
751 nc.Accounts[account] = a
753 if err := writeDynamic(ctx, log, nc); err != nil {
754 return fmt.Errorf("writing domains.conf: %v", err)
756 log.Info("address added", mlog.Field("address", address), mlog.Field("account", account))
760// AddressRemove removes an email address and reloads the configuration.
761func AddressRemove(ctx context.Context, address string) (rerr error) {
762 log := xlog.WithContext(ctx)
765 log.Errorx("removing address", rerr, mlog.Field("address", address))
769 Conf.dynamicMutex.Lock()
770 defer Conf.dynamicMutex.Unlock()
772 ad, ok := Conf.accountDestinations[address]
774 return fmt.Errorf("address does not exists")
777 // Compose new config without modifying existing data structures. If we fail, we
779 a, ok := Conf.Dynamic.Accounts[ad.Account]
781 return fmt.Errorf("internal error: cannot find account")
784 na.Destinations = map[string]config.Destination{}
786 for destAddr, d := range a.Destinations {
787 if destAddr != address {
788 na.Destinations[destAddr] = d
794 return fmt.Errorf("address not removed, likely a postmaster/reporting address")
797 nc.Accounts = map[string]config.Account{}
798 for name, a := range Conf.Dynamic.Accounts {
799 nc.Accounts[name] = a
801 nc.Accounts[ad.Account] = na
803 if err := writeDynamic(ctx, log, nc); err != nil {
804 return fmt.Errorf("writing domains.conf: %v", err)
806 log.Info("address removed", mlog.Field("address", address), mlog.Field("account", ad.Account))
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)
815 log.Errorx("saving account full name", rerr, mlog.Field("account", account))
819 Conf.dynamicMutex.Lock()
820 defer Conf.dynamicMutex.Unlock()
823 acc, ok := c.Accounts[account]
825 return fmt.Errorf("account not present")
828 // Compose new config without modifying existing data structures. If we fail, we
831 nc.Accounts = map[string]config.Account{}
832 for name, a := range c.Accounts {
833 nc.Accounts[name] = a
836 acc.FullName = fullName
837 nc.Accounts[account] = acc
839 if err := writeDynamic(ctx, log, nc); err != nil {
840 return fmt.Errorf("writing domains.conf: %v", err)
842 log.Info("account full name saved", mlog.Field("account", account))
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)
851 log.Errorx("saving destination", rerr, mlog.Field("account", account), mlog.Field("destname", destName), mlog.Field("destination", newDest))
855 Conf.dynamicMutex.Lock()
856 defer Conf.dynamicMutex.Unlock()
859 acc, ok := c.Accounts[account]
861 return fmt.Errorf("account not present")
864 if _, ok := acc.Destinations[destName]; !ok {
865 return fmt.Errorf("destination not present")
868 // Compose new config without modifying existing data structures. If we fail, we
871 nc.Accounts = map[string]config.Account{}
872 for name, a := range c.Accounts {
873 nc.Accounts[name] = a
875 nd := map[string]config.Destination{}
876 for dn, d := range acc.Destinations {
879 nd[destName] = newDest
880 nacc := nc.Accounts[account]
881 nacc.Destinations = nd
882 nc.Accounts[account] = nacc
884 if err := writeDynamic(ctx, log, nc); err != nil {
885 return fmt.Errorf("writing domains.conf: %v", err)
887 log.Info("destination saved", mlog.Field("account", account), mlog.Field("destname", destName))
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)
896 log.Errorx("saving account limits", rerr, mlog.Field("account", account))
900 Conf.dynamicMutex.Lock()
901 defer Conf.dynamicMutex.Unlock()
904 acc, ok := c.Accounts[account]
906 return fmt.Errorf("account not present")
909 // Compose new config without modifying existing data structures. If we fail, we
912 nc.Accounts = map[string]config.Account{}
913 for name, a := range c.Accounts {
914 nc.Accounts[name] = a
916 acc.MaxOutgoingMessagesPerDay = maxOutgoingMessagesPerDay
917 acc.MaxFirstTimeRecipientsPerDay = maxFirstTimeRecipientsPerDay
918 nc.Accounts[account] = acc
920 if err := writeDynamic(ctx, log, nc); err != nil {
921 return fmt.Errorf("writing domains.conf: %v", err)
923 log.Info("account limits saved", mlog.Field("account", account))
930 TLSModeImmediate TLSMode = 0
931 TLSModeSTARTTLS TLSMode = 1
932 TLSModeNone TLSMode = 2
935type ProtocolConfig struct {
941type ClientConfig struct {
943 Submission ProtocolConfig
946// ClientConfigDomain returns a single IMAP and Submission client configuration for
948func ClientConfigDomain(d dns.Domain) (rconfig ClientConfig, rerr error) {
949 var haveIMAP, haveSubmission bool
951 if _, ok := Conf.Domain(d); !ok {
952 return ClientConfig{}, fmt.Errorf("unknown domain")
955 gather := func(l config.Listener) (done bool) {
956 host := Conf.Static.HostnameDomain
957 if l.Hostname != "" {
958 host = l.HostnameDomain
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
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
971 rconfig.IMAP.TLSMode = TLSModeNone
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
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
986 rconfig.Submission.TLSMode = TLSModeNone
988 haveSubmission = true
990 return haveIMAP && haveSubmission
993 // Look at the public listener first. Most likely the intended configuration.
994 if public, ok := Conf.Static.Listeners["public"]; ok {
999 // Go through the other listeners in consistent order.
1000 names := maps.Keys(Conf.Static.Listeners)
1002 for _, name := range names {
1003 if gather(Conf.Static.Listeners[name]) {
1007 return ClientConfig{}, fmt.Errorf("no listeners found for imap and/or submission")
1010// ClientConfigs holds the client configuration for IMAP/Submission for a
1012type ClientConfigs struct {
1013 Entries []ClientConfigsEntry
1016type ClientConfigsEntry struct {
1024// ClientConfigsDomain returns the client configs for IMAP/Submission for a
1026func ClientConfigsDomain(d dns.Domain) (ClientConfigs, error) {
1027 _, ok := Conf.Domain(d)
1029 return ClientConfigs{}, fmt.Errorf("unknown domain")
1032 c := ClientConfigs{}
1033 c.Entries = []ClientConfigsEntry{}
1034 var listeners []string
1036 for name := range Conf.Static.Listeners {
1037 listeners = append(listeners, name)
1039 sort.Slice(listeners, func(i, j int) bool {
1040 return listeners[i] < listeners[j]
1043 note := func(tls bool, requiretls bool) string {
1045 return "plain text, no STARTTLS configured"
1048 return "STARTTLS required"
1050 return "STARTTLS optional"
1053 for _, name := range listeners {
1054 l := Conf.Static.Listeners[name]
1055 host := Conf.Static.HostnameDomain
1056 if l.Hostname != "" {
1057 host = l.HostnameDomain
1059 if l.Submissions.Enabled {
1060 c.Entries = append(c.Entries, ClientConfigsEntry{"Submission (SMTP)", host, config.Port(l.Submissions.Port, 465), name, "with TLS"})
1062 if l.IMAPS.Enabled {
1063 c.Entries = append(c.Entries, ClientConfigsEntry{"IMAP", host, config.Port(l.IMAPS.Port, 993), name, "with TLS"})
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)})
1069 c.Entries = append(c.Entries, ClientConfigsEntry{"IMAP", host, config.Port(l.IMAPS.Port, 143), name, note(l.TLS != nil, !l.IMAP.NoRequireSTARTTLS)})
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)
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.
1084 var ipv4all, ipv6all bool
1085 for _, l := range Conf.Static.Listeners {
1086 // If NATed, we don't know our external IPs.
1091 if len(l.NATIPs) > 0 {
1094 for _, s := range check {
1095 ip := net.ParseIP(s)
1096 if ip.IsUnspecified() {
1097 if ip.To4() != nil {
1104 ips = append(ips, ip)
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()
1113 return nil, fmt.Errorf("listing network interfaces: %v", err)
1115 for _, iface := range ifaces {
1116 if iface.Flags&net.FlagUp == 0 {
1119 addrs, err := iface.Addrs()
1121 return nil, fmt.Errorf("listing addresses for network interface: %v", err)
1123 if len(addrs) == 0 {
1127 for _, addr := range addrs {
1128 ip, _, err := net.ParseCIDR(addr.String())
1130 log.Errorx("bad interface addr", err, mlog.Field("address", addr))
1133 v4 := ip.To4() != nil
1134 if ipv4all && v4 || ipv6all && !v4 {
1135 ips = append(ips, ip)
1145 for _, t := range Conf.Static.Transports {
1147 ips = append(ips, t.Socks.IPs...)