8 cryptorand "crypto/rand"
22 "golang.org/x/exp/maps"
24 "github.com/mjl-/adns"
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"
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 {
53 r += "\t\t\"" + s[:n] + "\"\n"
60// MakeDKIMEd25519Key returns a PEM buffer containing an ed25519 key for use
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)
66 return nil, fmt.Errorf("generating key: %w", err)
69 pkcs8, err := x509.MarshalPKCS8PrivateKey(privKey)
71 return nil, fmt.Errorf("marshal key: %w", err)
76 Headers: map[string]string{
77 "Note": dkimKeyNote("ed25519", selector, domain),
82 if err := pem.Encode(b, block); err != nil {
83 return nil, fmt.Errorf("encoding pem: %w", err)
88func dkimKeyNote(kind string, selector, domain dns.Domain) string {
89 s := kind + " dkim private key"
91 if selector != zero && domain != zero {
92 s += fmt.Sprintf(" for %s._domainkey.%s", selector.ASCII, domain.ASCII)
94 s += fmt.Sprintf(", generated by mox on %s", time.Now().Format(time.RFC3339))
98// MakeDKIMEd25519Key returns a PEM buffer containing an rsa key for use with
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)
106 return nil, fmt.Errorf("generating key: %w", err)
109 pkcs8, err := x509.MarshalPKCS8PrivateKey(privKey)
111 return nil, fmt.Errorf("marshal key: %w", err)
116 Headers: map[string]string{
117 "Note": dkimKeyNote("rsa-2048", selector, domain),
122 if err := pem.Encode(b, block); err != nil {
123 return nil, fmt.Errorf("encoding pem: %w", err)
125 return b.Bytes(), nil
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{
135 RejectsMailbox: "Rejects",
136 JunkFilter: &config.JunkFilter{
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
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)
160 year := now.Format("2006")
161 timestamp := now.Format("20060102T150405")
165 for _, p := range paths {
167 log.Check(err, "removing path for domain config", mlog.Field("path", p))
171 writeFile := func(path string, data []byte) error {
172 os.MkdirAll(filepath.Dir(path), 0770)
174 f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0660)
176 return fmt.Errorf("creating file %s: %s", path, err)
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))
186 if _, err := f.Write(data); err != nil {
187 return fmt.Errorf("writing file %s: %s", path, err)
189 if err := f.Close(); err != nil {
190 return fmt.Errorf("close file: %v", err)
196 confDKIM := config.DKIM{
197 Selectors: map[string]config.Selector{},
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 {
207 paths = append(paths, p)
208 confDKIM.Selectors[name] = config.Selector{
211 // Messages in the wild have been observed with 2 hours and 1 year expiration.
213 PrivateKeyFile: keyPath,
218 addEd25519 := func(name string) error {
219 key, err := MakeDKIMEd25519Key(dns.Domain{ASCII: name}, domain)
221 return fmt.Errorf("making dkim ed25519 private key: %s", err)
223 return addSelector("ed25519", name, key)
226 addRSA := func(name string) error {
227 key, err := MakeDKIMRSAKey(dns.Domain{ASCII: name}, domain)
229 return fmt.Errorf("making dkim rsa private key: %s", err)
231 return addSelector("rsa2048", name, key)
234 if err := addEd25519(year + "a"); err != nil {
235 return config.Domain{}, nil, err
237 if err := addRSA(year + "b"); err != nil {
238 return config.Domain{}, nil, err
240 if err := addEd25519(year + "c"); err != nil {
241 return config.Domain{}, nil, err
243 if err := addRSA(year + "d"); err != nil {
244 return config.Domain{}, nil, err
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"}
252 confDomain := config.Domain{
253 LocalpartCatchallSeparator: "+",
255 DMARC: &config.DMARC{
256 Account: accountName,
257 Localpart: "dmarc-reports",
260 TLSRPT: &config.TLSRPT{
261 Account: accountName,
262 Localpart: "tls-reports",
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},
281 return confDomain, rpaths, nil
284// DomainAdd adds the domain to the domains config, rewriting domains.conf and
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)
294 log.Errorx("adding domain", rerr, mlog.Field("domain", domain), mlog.Field("account", accountName), mlog.Field("localpart", localpart))
298 Conf.dynamicMutex.Lock()
299 defer Conf.dynamicMutex.Unlock()
302 if _, ok := c.Domains[domain.Name()]; ok {
303 return fmt.Errorf("domain already present")
306 // Compose new config without modifying existing data structures. If we fail, we
309 nc.Domains = map[string]config.Domain{}
310 for name, d := range c.Domains {
314 // Only enable mta-sts for domain if there is a listener with mta-sts.
316 for _, l := range Conf.Static.Listeners {
317 if l.MTASTSHTTPS.Enabled {
323 confDomain, cleanupFiles, err := MakeDomainConfig(ctx, domain, Conf.Static.HostnameDomain, accountName, withMTASTS)
325 return fmt.Errorf("preparing domain config: %v", err)
328 for _, f := range cleanupFiles {
330 log.Check(err, "cleaning up file after error", mlog.Field("path", f))
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")
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 {
348 pmaddr := smtp.Address{Localpart: "postmaster", Domain: domain}
349 nd[pmaddr.String()] = config.Destination{}
350 nacc.Destinations = nd
351 nc.Accounts[accountName] = nacc
354 nc.Domains[domain.Name()] = confDomain
356 if err := writeDynamic(ctx, log, nc); err != nil {
357 return fmt.Errorf("writing domains.conf: %v", err)
359 log.Info("domain added", mlog.Field("domain", domain))
360 cleanupFiles = nil // All good, don't cleanup.
364// DomainRemove removes domain from the config, rewriting domains.conf.
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)
371 log.Errorx("removing domain", rerr, mlog.Field("domain", domain))
375 Conf.dynamicMutex.Lock()
376 defer Conf.dynamicMutex.Unlock()
379 domConf, ok := c.Domains[domain.Name()]
381 return fmt.Errorf("domain does not exist")
384 // Compose new config without modifying existing data structures. If we fail, we
387 nc.Domains = map[string]config.Domain{}
389 for name, d := range c.Domains {
395 if err := writeDynamic(ctx, log, nc); err != nil {
396 return fmt.Errorf("writing domains.conf: %v", err)
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
407 for _, sel := range domConf.DKIM.Selectors {
408 if sel.PrivateKeyFile == "" || usedKeyPaths[filepath.Clean(sel.PrivateKeyFile)] {
411 src := ConfigDirPath(sel.PrivateKeyFile)
412 dst := ConfigDirPath(filepath.Join(filepath.Dir(sel.PrivateKeyFile), "old", filepath.Base(sel.PrivateKeyFile)))
413 _, err := os.Stat(dst)
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)
421 log.Errorx("renaming dkim private key file for removed domain", err, mlog.Field("src", src), mlog.Field("dst", dst))
425 log.Info("domain removed", mlog.Field("domain", domain))
429func WebserverConfigSet(ctx context.Context, domainRedirects map[string]string, webhandlers []config.WebHandler) (rerr error) {
430 log := xlog.WithContext(ctx)
433 log.Errorx("saving webserver config", rerr)
437 Conf.dynamicMutex.Lock()
438 defer Conf.dynamicMutex.Unlock()
440 // Compose new config without modifying existing data structures. If we fail, we
443 nc.WebDomainRedirects = domainRedirects
444 nc.WebHandlers = webhandlers
446 if err := writeDynamic(ctx, log, nc); err != nil {
447 return fmt.Errorf("writing domains.conf: %v", err)
450 log.Info("webserver config saved")
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.
456// DomainRecords returns text lines describing DNS records required for configuring
458func DomainRecords(domConf config.Domain, domain dns.Domain, hasDNSSEC bool) ([]string, error) {
460 h := Conf.Static.HostnameDomain.ASCII
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.",
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.",
478 "; NOTE: Create the records below only once: They are for the machine, and apply",
479 "; to all hosted domains.",
482 records = append(records,
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",
489 addTLSA := func(privKey crypto.Signer) error {
490 spkiBuf, err := x509.MarshalPKIXPublicKey(privKey.Public())
492 return fmt.Errorf("marshal SubjectPublicKeyInfo for DANE record: %v", err)
494 sum := sha256.Sum256(spkiBuf)
495 tlsaRecord := adns.TLSA{
496 Usage: adns.TLSAUsageDANEEE,
497 Selector: adns.TLSASelectorSPKI,
498 MatchType: adns.TLSAMatchTypeSHA256,
503 s = fmt.Sprintf("_25._tcp.%-*s TLSA %s", 20+len(d)-len("_25._tcp."), h+".", tlsaRecord.Record())
505 s = fmt.Sprintf(";; _25._tcp.%-*s TLSA %s", 20+len(d)-len(";; _25._tcp."), h+".", tlsaRecord.Record())
507 records = append(records, s)
510 for _, privKey := range public.TLS.HostPrivateECDSAP256Keys {
511 if err := addTLSA(privKey); err != nil {
515 for _, privKey := range public.TLS.HostPrivateRSA2048Keys {
516 if err := addTLSA(privKey); err != nil {
520 records = append(records, "")
524 records = append(records,
525 "; For the machine, only needs to be created once, for the first domain added:",
527 "; SPF-allow host for itself, resulting in relaxed DMARC pass for (postmaster)",
528 "; messages (DSNs) sent from host:",
533 if d != h && Conf.Static.HostTLSRPT.ParsedLocalpart != "" {
536 Opaque: smtp.NewAddress(Conf.Static.HostTLSRPT.ParsedLocalpart, Conf.Static.HostnameDomain).Pack(false),
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:",
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()),
548 records = append(records,
549 "; Deliver email for the domain to this host.",
550 fmt.Sprintf("%s. MX 10 %s.", d, h),
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.",
556 var selectors []string
557 for name := range domConf.DKIM.Selectors {
558 selectors = append(selectors, name)
560 sort.Slice(selectors, func(i, j int) bool {
561 return selectors[i] < selectors[j]
563 for _, name := range selectors {
564 sel := domConf.DKIM.Selectors[name]
565 dkimr := dkim.Record{
567 Hashes: []string{"sha256"},
568 PublicKey: sel.Key.Public(),
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)
575 txt, err := dkimr.Record()
577 return nil, fmt.Errorf("making DKIM DNS TXT record: %v", err)
581 records = append(records,
582 "; NOTE: The following strings must be added to DNS as single record.",
585 s := fmt.Sprintf("%s._domainkey.%s. TXT %s", name, d, TXTStrings(txt))
586 records = append(records, s)
589 dmarcr := dmarc.DefaultRecord
590 dmarcr.Policy = "reject"
591 if domConf.DMARC != nil {
594 Opaque: smtp.NewAddress(domConf.DMARC.ParsedLocalpart, domConf.DMARC.DNSDomain).Pack(false),
596 dmarcr.AggregateReportAddresses = []dmarc.URI{
597 {Address: uri.String(), MaxSize: 10, Unit: "m"},
600 records = append(records,
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),
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()),
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",
622 fmt.Sprintf(`mta-sts.%s. CNAME %s.`, d, h),
623 fmt.Sprintf(`_mta-sts.%s. TXT "v=STSv1; id=%s"`, d, sts.PolicyID),
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.",
634 if domConf.TLSRPT != nil {
637 Opaque: smtp.NewAddress(domConf.TLSRPT.ParsedLocalpart, domConf.TLSRPT.DNSDomain).Pack(false),
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()),
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),
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),
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),
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),
676// AccountAdd adds an account and an initial address and reloads the configuration.
678// The new account does not have a password, so cannot yet log in. Email can be
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)
686 log.Errorx("adding account", rerr, mlog.Field("account", account), mlog.Field("address", address))
690 addr, err := smtp.ParseAddress(address)
692 return fmt.Errorf("parsing email address: %v", err)
695 Conf.dynamicMutex.Lock()
696 defer Conf.dynamicMutex.Unlock()
699 if _, ok := c.Accounts[account]; ok {
700 return fmt.Errorf("account already present")
703 if err := checkAddressAvailable(addr); err != nil {
704 return fmt.Errorf("address not available: %v", err)
707 // Compose new config without modifying existing data structures. If we fail, we
710 nc.Accounts = map[string]config.Account{}
711 for name, a := range c.Accounts {
712 nc.Accounts[name] = a
714 nc.Accounts[account] = MakeAccountConfig(addr)
716 if err := writeDynamic(ctx, log, nc); err != nil {
717 return fmt.Errorf("writing domains.conf: %v", err)
719 log.Info("account added", mlog.Field("account", account), mlog.Field("address", addr))
723// AccountRemove removes an account and reloads the configuration.
724func AccountRemove(ctx context.Context, account string) (rerr error) {
725 log := xlog.WithContext(ctx)
728 log.Errorx("adding account", rerr, mlog.Field("account", account))
732 Conf.dynamicMutex.Lock()
733 defer Conf.dynamicMutex.Unlock()
736 if _, ok := c.Accounts[account]; !ok {
737 return fmt.Errorf("account does not exist")
740 // Compose new config without modifying existing data structures. If we fail, we
743 nc.Accounts = map[string]config.Account{}
744 for name, a := range c.Accounts {
746 nc.Accounts[name] = a
750 if err := writeDynamic(ctx, log, nc); err != nil {
751 return fmt.Errorf("writing domains.conf: %v", err)
753 log.Info("account removed", mlog.Field("account", account))
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.
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)
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)
781 log.Errorx("adding address", rerr, mlog.Field("address", address), mlog.Field("account", account))
785 Conf.dynamicMutex.Lock()
786 defer Conf.dynamicMutex.Unlock()
789 a, ok := c.Accounts[account]
791 return fmt.Errorf("account does not exist")
795 if strings.HasPrefix(address, "@") {
796 d, err := dns.ParseDomain(address[1:])
798 return fmt.Errorf("parsing domain: %v", err)
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")
808 addr, err := smtp.ParseAddress(address)
810 return fmt.Errorf("parsing email address: %v", err)
813 if err := checkAddressAvailable(addr); err != nil {
814 return fmt.Errorf("address not available: %v", err)
816 destAddr = addr.String()
819 // Compose new config without modifying existing data structures. If we fail, we
822 nc.Accounts = map[string]config.Account{}
823 for name, a := range c.Accounts {
824 nc.Accounts[name] = a
826 nd := map[string]config.Destination{}
827 for name, d := range a.Destinations {
830 nd[destAddr] = config.Destination{}
832 nc.Accounts[account] = a
834 if err := writeDynamic(ctx, log, nc); err != nil {
835 return fmt.Errorf("writing domains.conf: %v", err)
837 log.Info("address added", mlog.Field("address", address), mlog.Field("account", account))
841// AddressRemove removes an email address and reloads the configuration.
842func AddressRemove(ctx context.Context, address string) (rerr error) {
843 log := xlog.WithContext(ctx)
846 log.Errorx("removing address", rerr, mlog.Field("address", address))
850 Conf.dynamicMutex.Lock()
851 defer Conf.dynamicMutex.Unlock()
853 ad, ok := Conf.accountDestinations[address]
855 return fmt.Errorf("address does not exists")
858 // Compose new config without modifying existing data structures. If we fail, we
860 a, ok := Conf.Dynamic.Accounts[ad.Account]
862 return fmt.Errorf("internal error: cannot find account")
865 na.Destinations = map[string]config.Destination{}
867 for destAddr, d := range a.Destinations {
868 if destAddr != address {
869 na.Destinations[destAddr] = d
875 return fmt.Errorf("address not removed, likely a postmaster/reporting address")
878 nc.Accounts = map[string]config.Account{}
879 for name, a := range Conf.Dynamic.Accounts {
880 nc.Accounts[name] = a
882 nc.Accounts[ad.Account] = na
884 if err := writeDynamic(ctx, log, nc); err != nil {
885 return fmt.Errorf("writing domains.conf: %v", err)
887 log.Info("address removed", mlog.Field("address", address), mlog.Field("account", ad.Account))
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)
896 log.Errorx("saving account full name", 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
917 acc.FullName = fullName
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 full name saved", mlog.Field("account", account))
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)
932 log.Errorx("saving destination", rerr, mlog.Field("account", account), mlog.Field("destname", destName), mlog.Field("destination", newDest))
936 Conf.dynamicMutex.Lock()
937 defer Conf.dynamicMutex.Unlock()
940 acc, ok := c.Accounts[account]
942 return fmt.Errorf("account not present")
945 if _, ok := acc.Destinations[destName]; !ok {
946 return fmt.Errorf("destination not present")
949 // Compose new config without modifying existing data structures. If we fail, we
952 nc.Accounts = map[string]config.Account{}
953 for name, a := range c.Accounts {
954 nc.Accounts[name] = a
956 nd := map[string]config.Destination{}
957 for dn, d := range acc.Destinations {
960 nd[destName] = newDest
961 nacc := nc.Accounts[account]
962 nacc.Destinations = nd
963 nc.Accounts[account] = nacc
965 if err := writeDynamic(ctx, log, nc); err != nil {
966 return fmt.Errorf("writing domains.conf: %v", err)
968 log.Info("destination saved", mlog.Field("account", account), mlog.Field("destname", destName))
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)
977 log.Errorx("saving account limits", rerr, mlog.Field("account", account))
981 Conf.dynamicMutex.Lock()
982 defer Conf.dynamicMutex.Unlock()
985 acc, ok := c.Accounts[account]
987 return fmt.Errorf("account not present")
990 // Compose new config without modifying existing data structures. If we fail, we
993 nc.Accounts = map[string]config.Account{}
994 for name, a := range c.Accounts {
995 nc.Accounts[name] = a
997 acc.MaxOutgoingMessagesPerDay = maxOutgoingMessagesPerDay
998 acc.MaxFirstTimeRecipientsPerDay = maxFirstTimeRecipientsPerDay
999 nc.Accounts[account] = acc
1001 if err := writeDynamic(ctx, log, nc); err != nil {
1002 return fmt.Errorf("writing domains.conf: %v", err)
1004 log.Info("account limits saved", mlog.Field("account", account))
1011 TLSModeImmediate TLSMode = 0
1012 TLSModeSTARTTLS TLSMode = 1
1013 TLSModeNone TLSMode = 2
1016type ProtocolConfig struct {
1022type ClientConfig struct {
1024 Submission ProtocolConfig
1027// ClientConfigDomain returns a single IMAP and Submission client configuration for
1029func ClientConfigDomain(d dns.Domain) (rconfig ClientConfig, rerr error) {
1030 var haveIMAP, haveSubmission bool
1032 if _, ok := Conf.Domain(d); !ok {
1033 return ClientConfig{}, fmt.Errorf("unknown domain")
1036 gather := func(l config.Listener) (done bool) {
1037 host := Conf.Static.HostnameDomain
1038 if l.Hostname != "" {
1039 host = l.HostnameDomain
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
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
1052 rconfig.IMAP.TLSMode = TLSModeNone
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
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
1067 rconfig.Submission.TLSMode = TLSModeNone
1069 haveSubmission = true
1071 return haveIMAP && haveSubmission
1074 // Look at the public listener first. Most likely the intended configuration.
1075 if public, ok := Conf.Static.Listeners["public"]; ok {
1080 // Go through the other listeners in consistent order.
1081 names := maps.Keys(Conf.Static.Listeners)
1083 for _, name := range names {
1084 if gather(Conf.Static.Listeners[name]) {
1088 return ClientConfig{}, fmt.Errorf("no listeners found for imap and/or submission")
1091// ClientConfigs holds the client configuration for IMAP/Submission for a
1093type ClientConfigs struct {
1094 Entries []ClientConfigsEntry
1097type ClientConfigsEntry struct {
1105// ClientConfigsDomain returns the client configs for IMAP/Submission for a
1107func ClientConfigsDomain(d dns.Domain) (ClientConfigs, error) {
1108 _, ok := Conf.Domain(d)
1110 return ClientConfigs{}, fmt.Errorf("unknown domain")
1113 c := ClientConfigs{}
1114 c.Entries = []ClientConfigsEntry{}
1115 var listeners []string
1117 for name := range Conf.Static.Listeners {
1118 listeners = append(listeners, name)
1120 sort.Slice(listeners, func(i, j int) bool {
1121 return listeners[i] < listeners[j]
1124 note := func(tls bool, requiretls bool) string {
1126 return "plain text, no STARTTLS configured"
1129 return "STARTTLS required"
1131 return "STARTTLS optional"
1134 for _, name := range listeners {
1135 l := Conf.Static.Listeners[name]
1136 host := Conf.Static.HostnameDomain
1137 if l.Hostname != "" {
1138 host = l.HostnameDomain
1140 if l.Submissions.Enabled {
1141 c.Entries = append(c.Entries, ClientConfigsEntry{"Submission (SMTP)", host, config.Port(l.Submissions.Port, 465), name, "with TLS"})
1143 if l.IMAPS.Enabled {
1144 c.Entries = append(c.Entries, ClientConfigsEntry{"IMAP", host, config.Port(l.IMAPS.Port, 993), name, "with TLS"})
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)})
1150 c.Entries = append(c.Entries, ClientConfigsEntry{"IMAP", host, config.Port(l.IMAPS.Port, 143), name, note(l.TLS != nil, !l.IMAP.NoRequireSTARTTLS)})
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)
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.
1165 var ipv4all, ipv6all bool
1166 for _, l := range Conf.Static.Listeners {
1167 // If NATed, we don't know our external IPs.
1172 if len(l.NATIPs) > 0 {
1175 for _, s := range check {
1176 ip := net.ParseIP(s)
1177 if ip.IsUnspecified() {
1178 if ip.To4() != nil {
1185 ips = append(ips, ip)
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()
1194 return nil, fmt.Errorf("listing network interfaces: %v", err)
1196 for _, iface := range ifaces {
1197 if iface.Flags&net.FlagUp == 0 {
1200 addrs, err := iface.Addrs()
1202 return nil, fmt.Errorf("listing addresses for network interface: %v", err)
1204 if len(addrs) == 0 {
1208 for _, addr := range addrs {
1209 ip, _, err := net.ParseCIDR(addr.String())
1211 log.Errorx("bad interface addr", err, mlog.Field("address", addr))
1214 v4 := ip.To4() != nil
1215 if ipv4all && v4 || ipv6all && !v4 {
1216 ips = append(ips, ip)
1226 for _, t := range Conf.Static.Transports {
1228 ips = append(ips, t.Socks.IPs...)