8 cryptorand "crypto/rand"
25 "golang.org/x/exp/maps"
27 "github.com/mjl-/adns"
29 "github.com/mjl-/mox/config"
30 "github.com/mjl-/mox/dkim"
31 "github.com/mjl-/mox/dmarc"
32 "github.com/mjl-/mox/dns"
33 "github.com/mjl-/mox/junk"
34 "github.com/mjl-/mox/mlog"
35 "github.com/mjl-/mox/mtasts"
36 "github.com/mjl-/mox/smtp"
37 "github.com/mjl-/mox/spf"
38 "github.com/mjl-/mox/tlsrpt"
41var ErrRequest = errors.New("bad request")
43// TXTStrings returns a TXT record value as one or more quoted strings, each max
44// 100 characters. In case of multiple strings, a multi-line record is returned.
45func TXTStrings(s string) string {
59 r += "\t\t\"" + s[:n] + "\"\n"
66// MakeDKIMEd25519Key returns a PEM buffer containing an ed25519 key for use
68// selector and domain can be empty. If not, they are used in the note.
69func MakeDKIMEd25519Key(selector, domain dns.Domain) ([]byte, error) {
70 _, privKey, err := ed25519.GenerateKey(cryptorand.Reader)
72 return nil, fmt.Errorf("generating key: %w", err)
75 pkcs8, err := x509.MarshalPKCS8PrivateKey(privKey)
77 return nil, fmt.Errorf("marshal key: %w", err)
82 Headers: map[string]string{
83 "Note": dkimKeyNote("ed25519", selector, domain),
88 if err := pem.Encode(b, block); err != nil {
89 return nil, fmt.Errorf("encoding pem: %w", err)
94func dkimKeyNote(kind string, selector, domain dns.Domain) string {
95 s := kind + " dkim private key"
97 if selector != zero && domain != zero {
98 s += fmt.Sprintf(" for %s._domainkey.%s", selector.ASCII, domain.ASCII)
100 s += fmt.Sprintf(", generated by mox on %s", time.Now().Format(time.RFC3339))
104// MakeDKIMRSAKey returns a PEM buffer containing an rsa key for use with
106// selector and domain can be empty. If not, they are used in the note.
107func MakeDKIMRSAKey(selector, domain dns.Domain) ([]byte, error) {
108 // 2048 bits seems reasonable in 2022, 1024 is on the low side, larger
109 // keys may not fit in UDP DNS response.
110 privKey, err := rsa.GenerateKey(cryptorand.Reader, 2048)
112 return nil, fmt.Errorf("generating key: %w", err)
115 pkcs8, err := x509.MarshalPKCS8PrivateKey(privKey)
117 return nil, fmt.Errorf("marshal key: %w", err)
122 Headers: map[string]string{
123 "Note": dkimKeyNote("rsa-2048", selector, domain),
128 if err := pem.Encode(b, block); err != nil {
129 return nil, fmt.Errorf("encoding pem: %w", err)
131 return b.Bytes(), nil
134// MakeAccountConfig returns a new account configuration for an email address.
135func MakeAccountConfig(addr smtp.Address) config.Account {
136 account := config.Account{
137 Domain: addr.Domain.Name(),
138 Destinations: map[string]config.Destination{
141 RejectsMailbox: "Rejects",
142 JunkFilter: &config.JunkFilter{
153 account.AutomaticJunkFlags.Enabled = true
154 account.AutomaticJunkFlags.JunkMailboxRegexp = "^(junk|spam)"
155 account.AutomaticJunkFlags.NeutralMailboxRegexp = "^(inbox|neutral|postmaster|dmarc|tlsrpt|rejects)"
156 account.SubjectPass.Period = 12 * time.Hour
160func writeFile(log mlog.Log, path string, data []byte) error {
161 os.MkdirAll(filepath.Dir(path), 0770)
163 f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0660)
165 return fmt.Errorf("creating file %s: %s", path, err)
170 log.Check(err, "closing file after error")
171 err = os.Remove(path)
172 log.Check(err, "removing file after error", slog.String("path", path))
175 if _, err := f.Write(data); err != nil {
176 return fmt.Errorf("writing file %s: %s", path, err)
178 if err := f.Close(); err != nil {
179 return fmt.Errorf("close file: %v", err)
185// MakeDomainConfig makes a new config for a domain, creating DKIM keys, using
186// accountName for DMARC and TLS reports.
187func MakeDomainConfig(ctx context.Context, domain, hostname dns.Domain, accountName string, withMTASTS bool) (config.Domain, []string, error) {
188 log := pkglog.WithContext(ctx)
191 year := now.Format("2006")
192 timestamp := now.Format("20060102T150405")
196 for _, p := range paths {
198 log.Check(err, "removing path for domain config", slog.String("path", p))
202 confDKIM := config.DKIM{
203 Selectors: map[string]config.Selector{},
206 addSelector := func(kind, name string, privKey []byte) error {
207 record := fmt.Sprintf("%s._domainkey.%s", name, domain.ASCII)
208 keyPath := filepath.Join("dkim", fmt.Sprintf("%s.%s.%s.privatekey.pkcs8.pem", record, timestamp, kind))
209 p := configDirPath(ConfigDynamicPath, keyPath)
210 if err := writeFile(log, p, privKey); err != nil {
213 paths = append(paths, p)
214 confDKIM.Selectors[name] = config.Selector{
217 // Messages in the wild have been observed with 2 hours and 1 year expiration.
219 PrivateKeyFile: keyPath,
224 addEd25519 := func(name string) error {
225 key, err := MakeDKIMEd25519Key(dns.Domain{ASCII: name}, domain)
227 return fmt.Errorf("making dkim ed25519 private key: %s", err)
229 return addSelector("ed25519", name, key)
232 addRSA := func(name string) error {
233 key, err := MakeDKIMRSAKey(dns.Domain{ASCII: name}, domain)
235 return fmt.Errorf("making dkim rsa private key: %s", err)
237 return addSelector("rsa2048", name, key)
240 if err := addEd25519(year + "a"); err != nil {
241 return config.Domain{}, nil, err
243 if err := addRSA(year + "b"); err != nil {
244 return config.Domain{}, nil, err
246 if err := addEd25519(year + "c"); err != nil {
247 return config.Domain{}, nil, err
249 if err := addRSA(year + "d"); err != nil {
250 return config.Domain{}, nil, err
253 // We sign with the first two. In case they are misused, the switch to the other
254 // keys is easy, just change the config. Operators should make the public key field
255 // of the misused keys empty in the DNS records to disable the misused keys.
256 confDKIM.Sign = []string{year + "a", year + "b"}
258 confDomain := config.Domain{
259 ClientSettingsDomain: "mail." + domain.Name(),
260 LocalpartCatchallSeparator: "+",
262 DMARC: &config.DMARC{
263 Account: accountName,
264 Localpart: "dmarc-reports",
267 TLSRPT: &config.TLSRPT{
268 Account: accountName,
269 Localpart: "tls-reports",
275 confDomain.MTASTS = &config.MTASTS{
276 PolicyID: time.Now().UTC().Format("20060102T150405"),
277 Mode: mtasts.ModeEnforce,
278 // We start out with 24 hour, and warn in the admin interface that users should
279 // increase it to weeks once the setup works.
280 MaxAge: 24 * time.Hour,
281 MX: []string{hostname.ASCII},
288 return confDomain, rpaths, nil
291// DKIMAdd adds a DKIM selector for a domain, generating a key and writing it to disk.
292func DKIMAdd(ctx context.Context, domain, selector dns.Domain, algorithm, hash string, headerRelaxed, bodyRelaxed, seal bool, headers []string, lifetime time.Duration) (rerr error) {
293 log := pkglog.WithContext(ctx)
296 log.Errorx("adding dkim key", rerr,
297 slog.Any("domain", domain),
298 slog.Any("selector", selector))
303 case "sha256", "sha1":
305 return fmt.Errorf("%w: unknown hash algorithm %q", ErrRequest, hash)
313 privKey, err = MakeDKIMRSAKey(selector, domain)
316 privKey, err = MakeDKIMEd25519Key(selector, domain)
319 err = fmt.Errorf("unknown algorithm")
322 return fmt.Errorf("%w: making dkim key: %v", ErrRequest, err)
325 // Only take lock now, we don't want to hold it while generating a key.
326 Conf.dynamicMutex.Lock()
327 defer Conf.dynamicMutex.Unlock()
330 d, ok := c.Domains[domain.Name()]
332 return fmt.Errorf("%w: domain does not exist", ErrRequest)
335 if _, ok := d.DKIM.Selectors[selector.Name()]; ok {
336 return fmt.Errorf("%w: selector already exists for domain", ErrRequest)
339 record := fmt.Sprintf("%s._domainkey.%s", selector.ASCII, domain.ASCII)
340 timestamp := time.Now().Format("20060102T150405")
341 keyPath := filepath.Join("dkim", fmt.Sprintf("%s.%s.%s.privatekey.pkcs8.pem", record, timestamp, kind))
342 p := configDirPath(ConfigDynamicPath, keyPath)
343 if err := writeFile(log, p, privKey); err != nil {
344 return fmt.Errorf("writing key file: %v", err)
348 if removePath != "" {
349 err := os.Remove(removePath)
350 log.Check(err, "removing path for dkim key", slog.String("path", removePath))
354 nsel := config.Selector{
356 Canonicalization: config.Canonicalization{
357 HeaderRelaxed: headerRelaxed,
358 BodyRelaxed: bodyRelaxed,
361 DontSealHeaders: !seal,
362 Expiration: lifetime.String(),
363 PrivateKeyFile: keyPath,
366 // All good, time to update the config.
368 nd.DKIM.Selectors = map[string]config.Selector{}
369 for name, osel := range d.DKIM.Selectors {
370 nd.DKIM.Selectors[name] = osel
372 nd.DKIM.Selectors[selector.Name()] = nsel
374 nc.Domains = map[string]config.Domain{}
375 for name, dom := range c.Domains {
376 nc.Domains[name] = dom
378 nc.Domains[domain.Name()] = nd
380 if err := writeDynamic(ctx, log, nc); err != nil {
381 return fmt.Errorf("writing domains.conf: %w", err)
384 log.Info("dkim key added", slog.Any("domain", domain), slog.Any("selector", selector))
385 removePath = "" // Prevent cleanup of key file.
389// DKIMRemove removes the selector from the domain, moving the key file out of the way.
390func DKIMRemove(ctx context.Context, domain, selector dns.Domain) (rerr error) {
391 log := pkglog.WithContext(ctx)
394 log.Errorx("removing dkim key", rerr,
395 slog.Any("domain", domain),
396 slog.Any("selector", selector))
400 Conf.dynamicMutex.Lock()
401 defer Conf.dynamicMutex.Unlock()
404 d, ok := c.Domains[domain.Name()]
406 return fmt.Errorf("%w: domain does not exist", ErrRequest)
409 sel, ok := d.DKIM.Selectors[selector.Name()]
411 return fmt.Errorf("%w: selector does not exist for domain", ErrRequest)
414 nsels := map[string]config.Selector{}
415 for name, sel := range d.DKIM.Selectors {
416 if name != selector.Name() {
420 nsign := make([]string, 0, len(d.DKIM.Sign))
421 for _, name := range d.DKIM.Sign {
422 if name != selector.Name() {
423 nsign = append(nsign, name)
428 nd.DKIM = config.DKIM{Selectors: nsels, Sign: nsign}
430 nc.Domains = map[string]config.Domain{}
431 for name, dom := range c.Domains {
432 nc.Domains[name] = dom
434 nc.Domains[domain.Name()] = nd
436 if err := writeDynamic(ctx, log, nc); err != nil {
437 return fmt.Errorf("writing domains.conf: %w", err)
440 // Move away a DKIM private key to a subdirectory "old". But only if
441 // not in use by other domains.
442 usedKeyPaths := gatherUsedKeysPaths(nc)
443 moveAwayKeys(log, map[string]config.Selector{selector.Name(): sel}, usedKeyPaths)
445 log.Info("dkim key removed", slog.Any("domain", domain), slog.Any("selector", selector))
449// DomainAdd adds the domain to the domains config, rewriting domains.conf and
452// accountName is used for DMARC/TLS report and potentially for the postmaster address.
453// If the account does not exist, it is created with localpart. Localpart must be
454// set only if the account does not yet exist.
455func DomainAdd(ctx context.Context, domain dns.Domain, accountName string, localpart smtp.Localpart) (rerr error) {
456 log := pkglog.WithContext(ctx)
459 log.Errorx("adding domain", rerr,
460 slog.Any("domain", domain),
461 slog.String("account", accountName),
462 slog.Any("localpart", localpart))
466 Conf.dynamicMutex.Lock()
467 defer Conf.dynamicMutex.Unlock()
470 if _, ok := c.Domains[domain.Name()]; ok {
471 return fmt.Errorf("%w: domain already present", ErrRequest)
474 // Compose new config without modifying existing data structures. If we fail, we
477 nc.Domains = map[string]config.Domain{}
478 for name, d := range c.Domains {
482 // Only enable mta-sts for domain if there is a listener with mta-sts.
484 for _, l := range Conf.Static.Listeners {
485 if l.MTASTSHTTPS.Enabled {
491 confDomain, cleanupFiles, err := MakeDomainConfig(ctx, domain, Conf.Static.HostnameDomain, accountName, withMTASTS)
493 return fmt.Errorf("preparing domain config: %v", err)
496 for _, f := range cleanupFiles {
498 log.Check(err, "cleaning up file after error", slog.String("path", f))
502 if _, ok := c.Accounts[accountName]; ok && localpart != "" {
503 return fmt.Errorf("%w: account already exists (leave localpart empty when using an existing account)", ErrRequest)
504 } else if !ok && localpart == "" {
505 return fmt.Errorf("%w: account does not yet exist (specify a localpart)", ErrRequest)
506 } else if accountName == "" {
507 return fmt.Errorf("%w: account name is empty", ErrRequest)
509 nc.Accounts[accountName] = MakeAccountConfig(smtp.NewAddress(localpart, domain))
510 } else if accountName != Conf.Static.Postmaster.Account {
511 nacc := nc.Accounts[accountName]
512 nd := map[string]config.Destination{}
513 for k, v := range nacc.Destinations {
516 pmaddr := smtp.NewAddress("postmaster", domain)
517 nd[pmaddr.String()] = config.Destination{}
518 nacc.Destinations = nd
519 nc.Accounts[accountName] = nacc
522 nc.Domains[domain.Name()] = confDomain
524 if err := writeDynamic(ctx, log, nc); err != nil {
525 return fmt.Errorf("writing domains.conf: %w", err)
527 log.Info("domain added", slog.Any("domain", domain))
528 cleanupFiles = nil // All good, don't cleanup.
532// DomainRemove removes domain from the config, rewriting domains.conf.
534// No accounts are removed, also not when they still reference this domain.
535func DomainRemove(ctx context.Context, domain dns.Domain) (rerr error) {
536 log := pkglog.WithContext(ctx)
539 log.Errorx("removing domain", rerr, slog.Any("domain", domain))
543 Conf.dynamicMutex.Lock()
544 defer Conf.dynamicMutex.Unlock()
547 domConf, ok := c.Domains[domain.Name()]
549 return fmt.Errorf("%w: domain does not exist", ErrRequest)
552 // Compose new config without modifying existing data structures. If we fail, we
555 nc.Domains = map[string]config.Domain{}
557 for name, d := range c.Domains {
563 if err := writeDynamic(ctx, log, nc); err != nil {
564 return fmt.Errorf("writing domains.conf: %w", err)
567 // Move away any DKIM private keys to a subdirectory "old". But only if
568 // they are not in use by other domains.
569 usedKeyPaths := gatherUsedKeysPaths(nc)
570 moveAwayKeys(log, domConf.DKIM.Selectors, usedKeyPaths)
572 log.Info("domain removed", slog.Any("domain", domain))
576func gatherUsedKeysPaths(nc config.Dynamic) map[string]bool {
577 usedKeyPaths := map[string]bool{}
578 for _, dc := range nc.Domains {
579 for _, sel := range dc.DKIM.Selectors {
580 usedKeyPaths[filepath.Clean(sel.PrivateKeyFile)] = true
586func moveAwayKeys(log mlog.Log, sels map[string]config.Selector, usedKeyPaths map[string]bool) {
587 for _, sel := range sels {
588 if sel.PrivateKeyFile == "" || usedKeyPaths[filepath.Clean(sel.PrivateKeyFile)] {
591 src := ConfigDirPath(sel.PrivateKeyFile)
592 dst := ConfigDirPath(filepath.Join(filepath.Dir(sel.PrivateKeyFile), "old", filepath.Base(sel.PrivateKeyFile)))
593 _, err := os.Stat(dst)
595 err = fmt.Errorf("destination already exists")
596 } else if os.IsNotExist(err) {
597 os.MkdirAll(filepath.Dir(dst), 0770)
598 err = os.Rename(src, dst)
601 log.Errorx("renaming dkim private key file for removed domain", err, slog.String("src", src), slog.String("dst", dst))
606// DomainSave calls xmodify with a shallow copy of the domain config. xmodify
607// can modify the config, but must clone all referencing data it changes.
608// xmodify may employ panic-based error handling. After xmodify returns, the
609// modified config is verified, saved and takes effect.
610func DomainSave(ctx context.Context, domainName string, xmodify func(config *config.Domain) error) (rerr error) {
611 log := pkglog.WithContext(ctx)
614 log.Errorx("saving domain config", rerr)
618 Conf.dynamicMutex.Lock()
619 defer Conf.dynamicMutex.Unlock()
621 nc := Conf.Dynamic // Shallow copy.
622 dom, ok := nc.Domains[domainName] // dom is a shallow copy.
624 return fmt.Errorf("%w: domain not present", ErrRequest)
627 if err := xmodify(&dom); err != nil {
631 // Compose new config without modifying existing data structures. If we fail, we
633 nc.Domains = map[string]config.Domain{}
634 for name, d := range Conf.Dynamic.Domains {
637 nc.Domains[domainName] = dom
639 if err := writeDynamic(ctx, log, nc); err != nil {
640 return fmt.Errorf("writing domains.conf: %w", err)
643 log.Info("domain saved")
647// ConfigSave calls xmodify with a shallow copy of the dynamic config. xmodify
648// can modify the config, but must clone all referencing data it changes.
649// xmodify may employ panic-based error handling. After xmodify returns, the
650// modified config is verified, saved and takes effect.
651func ConfigSave(ctx context.Context, xmodify func(config *config.Dynamic)) (rerr error) {
652 log := pkglog.WithContext(ctx)
655 log.Errorx("saving config", rerr)
659 Conf.dynamicMutex.Lock()
660 defer Conf.dynamicMutex.Unlock()
662 nc := Conf.Dynamic // Shallow copy.
665 if err := writeDynamic(ctx, log, nc); err != nil {
666 return fmt.Errorf("writing domains.conf: %w", err)
669 log.Info("config saved")
673// DomainSPFIPs returns IPs to include in SPF records for domains. It includes the
674// IPs on listeners that have SMTP enabled, and includes IPs configured for SOCKS
676func DomainSPFIPs() (ips []net.IP) {
677 for _, l := range Conf.Static.Listeners {
678 if !l.SMTP.Enabled || l.IPsNATed {
682 if len(l.NATIPs) > 0 {
685 for _, ipstr := range ipstrs {
686 ip := net.ParseIP(ipstr)
687 ips = append(ips, ip)
690 for _, t := range Conf.Static.Transports {
692 ips = append(ips, t.Socks.IPs...)
698// 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.
700// DomainRecords returns text lines describing DNS records required for configuring
703// If certIssuerDomainName is set, CAA records to limit TLS certificate issuance to
704// that caID will be suggested. If acmeAccountURI is also set, CAA records also
705// restricting issuance to that account ID will be suggested.
706func DomainRecords(domConf config.Domain, domain dns.Domain, hasDNSSEC bool, certIssuerDomainName, acmeAccountURI string) ([]string, error) {
708 h := Conf.Static.HostnameDomain.ASCII
710 // The first line with ";" is used by ../testdata/integration/moxacmepebble.sh and
711 // ../testdata/integration/moxmail2.sh for selecting DNS records
713 "; Time To Live of 5 minutes, may be recognized if importing as a zone file.",
714 "; Once your setup is working, you may want to increase the TTL.",
719 if public, ok := Conf.Static.Listeners["public"]; ok && public.TLS != nil && (len(public.TLS.HostPrivateRSA2048Keys) > 0 || len(public.TLS.HostPrivateECDSAP256Keys) > 0) {
720 records = append(records,
721 `; DANE: These records indicate that a remote mail server trying to deliver email`,
722 `; with SMTP (TCP port 25) must verify the TLS certificate with DANE-EE (3), based`,
723 `; on the certificate public key ("SPKI", 1) that is SHA2-256-hashed (1) to the`,
724 `; hexadecimal hash. DANE-EE verification means only the certificate or public`,
725 `; key is verified, not whether the certificate is signed by a (centralized)`,
726 `; certificate authority (CA), is expired, or matches the host name.`,
728 `; NOTE: Create the records below only once: They are for the machine, and apply`,
729 `; to all hosted domains.`,
732 records = append(records,
734 "; WARNING: Domain does not appear to be DNSSEC-signed. To enable DANE, first",
735 "; enable DNSSEC on your domain, then add the TLSA records. Records below have been",
739 addTLSA := func(privKey crypto.Signer) error {
740 spkiBuf, err := x509.MarshalPKIXPublicKey(privKey.Public())
742 return fmt.Errorf("marshal SubjectPublicKeyInfo for DANE record: %v", err)
744 sum := sha256.Sum256(spkiBuf)
745 tlsaRecord := adns.TLSA{
746 Usage: adns.TLSAUsageDANEEE,
747 Selector: adns.TLSASelectorSPKI,
748 MatchType: adns.TLSAMatchTypeSHA256,
753 s = fmt.Sprintf("_25._tcp.%-*s TLSA %s", 20+len(d)-len("_25._tcp."), h+".", tlsaRecord.Record())
755 s = fmt.Sprintf(";; _25._tcp.%-*s TLSA %s", 20+len(d)-len(";; _25._tcp."), h+".", tlsaRecord.Record())
757 records = append(records, s)
760 for _, privKey := range public.TLS.HostPrivateECDSAP256Keys {
761 if err := addTLSA(privKey); err != nil {
765 for _, privKey := range public.TLS.HostPrivateRSA2048Keys {
766 if err := addTLSA(privKey); err != nil {
770 records = append(records, "")
774 records = append(records,
775 "; For the machine, only needs to be created once, for the first domain added:",
777 "; SPF-allow host for itself, resulting in relaxed DMARC pass for (postmaster)",
778 "; messages (DSNs) sent from host:",
783 if d != h && Conf.Static.HostTLSRPT.ParsedLocalpart != "" {
786 Opaque: smtp.NewAddress(Conf.Static.HostTLSRPT.ParsedLocalpart, Conf.Static.HostnameDomain).Pack(false),
788 tlsrptr := tlsrpt.Record{Version: "TLSRPTv1", RUAs: [][]tlsrpt.RUA{{tlsrpt.RUA(uri.String())}}}
789 records = append(records,
790 "; For the machine, only needs to be created once, for the first domain added:",
792 "; Request reporting about success/failures of TLS connections to (MX) host, for DANE.",
793 fmt.Sprintf(`_smtp._tls.%-*s TXT "%s"`, 20+len(d)-len("_smtp._tls."), h+".", tlsrptr.String()),
798 records = append(records,
799 "; Deliver email for the domain to this host.",
800 fmt.Sprintf("%s. MX 10 %s.", d, h),
803 "; Outgoing messages will be signed with the first two DKIM keys. The other two",
804 "; configured for backup, switching to them is just a config change.",
806 var selectors []string
807 for name := range domConf.DKIM.Selectors {
808 selectors = append(selectors, name)
810 sort.Slice(selectors, func(i, j int) bool {
811 return selectors[i] < selectors[j]
813 for _, name := range selectors {
814 sel := domConf.DKIM.Selectors[name]
815 dkimr := dkim.Record{
817 Hashes: []string{"sha256"},
818 PublicKey: sel.Key.Public(),
820 if _, ok := sel.Key.(ed25519.PrivateKey); ok {
821 dkimr.Key = "ed25519"
822 } else if _, ok := sel.Key.(*rsa.PrivateKey); !ok {
823 return nil, fmt.Errorf("unrecognized private key for DKIM selector %q: %T", name, sel.Key)
825 txt, err := dkimr.Record()
827 return nil, fmt.Errorf("making DKIM DNS TXT record: %v", err)
831 records = append(records,
832 "; NOTE: The following is a single long record split over several lines for use",
833 "; in zone files. When adding through a DNS operator web interface, combine the",
834 "; strings into a single string, without ().",
837 s := fmt.Sprintf("%s._domainkey.%s. TXT %s", name, d, TXTStrings(txt))
838 records = append(records, s)
841 dmarcr := dmarc.DefaultRecord
842 dmarcr.Policy = "reject"
843 if domConf.DMARC != nil {
846 Opaque: smtp.NewAddress(domConf.DMARC.ParsedLocalpart, domConf.DMARC.DNSDomain).Pack(false),
848 dmarcr.AggregateReportAddresses = []dmarc.URI{
849 {Address: uri.String(), MaxSize: 10, Unit: "m"},
852 dspfr := spf.Record{Version: "spf1"}
853 for _, ip := range DomainSPFIPs() {
858 dspfr.Directives = append(dspfr.Directives, spf.Directive{Mechanism: mech, IP: ip})
860 dspfr.Directives = append(dspfr.Directives,
861 spf.Directive{Mechanism: "mx"},
862 spf.Directive{Qualifier: "~", Mechanism: "all"},
864 dspftxt, err := dspfr.Record()
866 return nil, fmt.Errorf("making domain spf record: %v", err)
868 records = append(records,
871 "; Specify the MX host is allowed to send for our domain and for itself (for DSNs).",
872 "; ~all means softfail for anything else, which is done instead of -all to prevent older",
873 "; mail servers from rejecting the message because they never get to looking for a dkim/dmarc pass.",
874 fmt.Sprintf(`%s. TXT "%s"`, d, dspftxt),
877 "; Emails that fail the DMARC check (without aligned DKIM and without aligned SPF)",
878 "; should be rejected, and request reports. If you email through mailing lists that",
879 "; strip DKIM-Signature headers and don't rewrite the From header, you may want to",
880 "; set the policy to p=none.",
881 fmt.Sprintf(`_dmarc.%s. TXT "%s"`, d, dmarcr.String()),
885 if sts := domConf.MTASTS; sts != nil {
886 records = append(records,
887 "; Remote servers can use MTA-STS to verify our TLS certificate with the",
888 "; WebPKI pool of CA's (certificate authorities) when delivering over SMTP with",
890 fmt.Sprintf(`mta-sts.%s. CNAME %s.`, d, h),
891 fmt.Sprintf(`_mta-sts.%s. TXT "v=STSv1; id=%s"`, d, sts.PolicyID),
895 records = append(records,
896 "; Note: No MTA-STS to indicate TLS should be used. Either because disabled for the",
897 "; domain or because mox.conf does not have a listener with MTA-STS configured.",
902 if domConf.TLSRPT != nil {
905 Opaque: smtp.NewAddress(domConf.TLSRPT.ParsedLocalpart, domConf.TLSRPT.DNSDomain).Pack(false),
907 tlsrptr := tlsrpt.Record{Version: "TLSRPTv1", RUAs: [][]tlsrpt.RUA{{tlsrpt.RUA(uri.String())}}}
908 records = append(records,
909 "; Request reporting about TLS failures.",
910 fmt.Sprintf(`_smtp._tls.%s. TXT "%s"`, d, tlsrptr.String()),
915 if domConf.ClientSettingsDomain != "" && domConf.ClientSettingsDNSDomain != Conf.Static.HostnameDomain {
916 records = append(records,
917 "; Client settings will reference a subdomain of the hosted domain, making it",
918 "; easier to migrate to a different server in the future by not requiring settings",
919 "; in all clients to be updated.",
920 fmt.Sprintf(`%-*s CNAME %s.`, 20+len(d), domConf.ClientSettingsDNSDomain.ASCII+".", h),
925 records = append(records,
926 "; Autoconfig is used by Thunderbird. Autodiscover is (in theory) used by Microsoft.",
927 fmt.Sprintf(`autoconfig.%s. CNAME %s.`, d, h),
928 fmt.Sprintf(`_autodiscover._tcp.%s. SRV 0 1 443 %s.`, d, h),
932 "; For secure IMAP and submission autoconfig, point to mail host.",
933 fmt.Sprintf(`_imaps._tcp.%s. SRV 0 1 993 %s.`, d, h),
934 fmt.Sprintf(`_submissions._tcp.%s. SRV 0 1 465 %s.`, d, h),
937 "; Next records specify POP3 and non-TLS ports are not to be used.",
938 "; These are optional and safe to leave out (e.g. if you have to click a lot in a",
939 "; DNS admin web interface).",
940 fmt.Sprintf(`_imap._tcp.%s. SRV 0 0 0 .`, d),
941 fmt.Sprintf(`_submission._tcp.%s. SRV 0 0 0 .`, d),
942 fmt.Sprintf(`_pop3._tcp.%s. SRV 0 0 0 .`, d),
943 fmt.Sprintf(`_pop3s._tcp.%s. SRV 0 0 0 .`, d),
946 if certIssuerDomainName != "" {
948 records = append(records,
951 "; You could mark Let's Encrypt as the only Certificate Authority allowed to",
952 "; sign TLS certificates for your domain.",
953 fmt.Sprintf(`%s. CAA 0 issue "%s"`, d, certIssuerDomainName),
955 if acmeAccountURI != "" {
958 records = append(records,
960 "; Optionally limit certificates for this domain to the account ID and methods used by mox.",
961 fmt.Sprintf(`;; %s. CAA 0 issue "%s; accounturi=%s; validationmethods=tls-alpn-01,http-01"`, d, certIssuerDomainName, acmeAccountURI),
963 "; Or alternatively only limit for email-specific subdomains, so you can use",
964 "; other accounts/methods for other subdomains.",
965 fmt.Sprintf(`;; autoconfig.%s. CAA 0 issue "%s; accounturi=%s; validationmethods=tls-alpn-01,http-01"`, d, certIssuerDomainName, acmeAccountURI),
966 fmt.Sprintf(`;; mta-sts.%s. CAA 0 issue "%s; accounturi=%s; validationmethods=tls-alpn-01,http-01"`, d, certIssuerDomainName, acmeAccountURI),
968 if domConf.ClientSettingsDomain != "" && domConf.ClientSettingsDNSDomain != Conf.Static.HostnameDomain {
969 records = append(records,
970 fmt.Sprintf(`;; %-*s CAA 0 issue "%s; accounturi=%s; validationmethods=tls-alpn-01,http-01"`, 20-3+len(d), domConf.ClientSettingsDNSDomain.ASCII, certIssuerDomainName, acmeAccountURI),
973 if strings.HasSuffix(h, "."+d) {
974 records = append(records,
976 "; And the mail hostname.",
977 fmt.Sprintf(`;; %-*s CAA 0 issue "%s; accounturi=%s; validationmethods=tls-alpn-01,http-01"`, 20-3+len(d), h+".", certIssuerDomainName, acmeAccountURI),
981 // The string "will be suggested" is used by
982 // ../testdata/integration/moxacmepebble.sh and ../testdata/integration/moxmail2.sh
983 // as end of DNS records.
984 records = append(records,
986 "; Note: After starting up, once an ACME account has been created, CAA records",
987 "; that restrict issuance to the account will be suggested.",
994// AccountAdd adds an account and an initial address and reloads the configuration.
996// The new account does not have a password, so cannot yet log in. Email can be
999// Catchall addresses are not supported for AccountAdd. Add separately with AddressAdd.
1000func AccountAdd(ctx context.Context, account, address string) (rerr error) {
1001 log := pkglog.WithContext(ctx)
1004 log.Errorx("adding account", rerr, slog.String("account", account), slog.String("address", address))
1008 addr, err := smtp.ParseAddress(address)
1010 return fmt.Errorf("%w: parsing email address: %v", ErrRequest, err)
1013 Conf.dynamicMutex.Lock()
1014 defer Conf.dynamicMutex.Unlock()
1017 if _, ok := c.Accounts[account]; ok {
1018 return fmt.Errorf("%w: account already present", ErrRequest)
1021 if err := checkAddressAvailable(addr); err != nil {
1022 return fmt.Errorf("%w: address not available: %v", ErrRequest, err)
1025 // Compose new config without modifying existing data structures. If we fail, we
1028 nc.Accounts = map[string]config.Account{}
1029 for name, a := range c.Accounts {
1030 nc.Accounts[name] = a
1032 nc.Accounts[account] = MakeAccountConfig(addr)
1034 if err := writeDynamic(ctx, log, nc); err != nil {
1035 return fmt.Errorf("writing domains.conf: %w", err)
1037 log.Info("account added", slog.String("account", account), slog.Any("address", addr))
1041// AccountRemove removes an account and reloads the configuration.
1042func AccountRemove(ctx context.Context, account string) (rerr error) {
1043 log := pkglog.WithContext(ctx)
1046 log.Errorx("adding account", rerr, slog.String("account", account))
1050 Conf.dynamicMutex.Lock()
1051 defer Conf.dynamicMutex.Unlock()
1054 if _, ok := c.Accounts[account]; !ok {
1055 return fmt.Errorf("%w: account does not exist", ErrRequest)
1058 // Compose new config without modifying existing data structures. If we fail, we
1061 nc.Accounts = map[string]config.Account{}
1062 for name, a := range c.Accounts {
1063 if name != account {
1064 nc.Accounts[name] = a
1068 if err := writeDynamic(ctx, log, nc); err != nil {
1069 return fmt.Errorf("writing domains.conf: %w", err)
1072 odir := filepath.Join(DataDirPath("accounts"), account)
1073 tmpdir := filepath.Join(DataDirPath("tmp"), "oldaccount-"+account)
1074 if err := os.Rename(odir, tmpdir); err != nil {
1075 log.Errorx("moving old account data directory out of the way", err, slog.String("account", account))
1076 return fmt.Errorf("account removed, but account data directory %q could not be moved out of the way: %v", odir, err)
1078 if err := os.RemoveAll(tmpdir); err != nil {
1079 log.Errorx("removing old account data directory", err, slog.String("account", account))
1080 return fmt.Errorf("account removed, its data directory moved to %q, but removing failed: %v", odir, err)
1083 log.Info("account removed", slog.String("account", account))
1087// checkAddressAvailable checks that the address after canonicalization is not
1088// already configured, and that its localpart does not contain the catchall
1089// localpart separator.
1091// Must be called with config lock held.
1092func checkAddressAvailable(addr smtp.Address) error {
1093 dc, ok := Conf.Dynamic.Domains[addr.Domain.Name()]
1095 return fmt.Errorf("domain does not exist")
1097 lp := CanonicalLocalpart(addr.Localpart, dc)
1098 if _, ok := Conf.accountDestinations[smtp.NewAddress(lp, addr.Domain).String()]; ok {
1099 return fmt.Errorf("canonicalized address %s already configured", smtp.NewAddress(lp, addr.Domain))
1100 } else if dc.LocalpartCatchallSeparator != "" && strings.Contains(string(addr.Localpart), dc.LocalpartCatchallSeparator) {
1101 return fmt.Errorf("localpart cannot include domain catchall separator %s", dc.LocalpartCatchallSeparator)
1102 } else if _, ok := dc.Aliases[lp.String()]; ok {
1103 return fmt.Errorf("address in use as alias")
1108// AddressAdd adds an email address to an account and reloads the configuration. If
1109// address starts with an @ it is treated as a catchall address for the domain.
1110func AddressAdd(ctx context.Context, address, account string) (rerr error) {
1111 log := pkglog.WithContext(ctx)
1114 log.Errorx("adding address", rerr, slog.String("address", address), slog.String("account", account))
1118 Conf.dynamicMutex.Lock()
1119 defer Conf.dynamicMutex.Unlock()
1122 a, ok := c.Accounts[account]
1124 return fmt.Errorf("%w: account does not exist", ErrRequest)
1128 if strings.HasPrefix(address, "@") {
1129 d, err := dns.ParseDomain(address[1:])
1131 return fmt.Errorf("%w: parsing domain: %v", ErrRequest, err)
1134 destAddr = "@" + dname
1135 if _, ok := Conf.Dynamic.Domains[dname]; !ok {
1136 return fmt.Errorf("%w: domain does not exist", ErrRequest)
1137 } else if _, ok := Conf.accountDestinations[destAddr]; ok {
1138 return fmt.Errorf("%w: catchall address already configured for domain", ErrRequest)
1141 addr, err := smtp.ParseAddress(address)
1143 return fmt.Errorf("%w: parsing email address: %v", ErrRequest, err)
1146 if err := checkAddressAvailable(addr); err != nil {
1147 return fmt.Errorf("%w: address not available: %v", ErrRequest, err)
1149 destAddr = addr.String()
1152 // Compose new config without modifying existing data structures. If we fail, we
1155 nc.Accounts = map[string]config.Account{}
1156 for name, a := range c.Accounts {
1157 nc.Accounts[name] = a
1159 nd := map[string]config.Destination{}
1160 for name, d := range a.Destinations {
1163 nd[destAddr] = config.Destination{}
1165 nc.Accounts[account] = a
1167 if err := writeDynamic(ctx, log, nc); err != nil {
1168 return fmt.Errorf("writing domains.conf: %w", err)
1170 log.Info("address added", slog.String("address", address), slog.String("account", account))
1174// AddressRemove removes an email address and reloads the configuration.
1175// Address can be a catchall address for the domain of the form "@<domain>".
1177// If the address is member of an alias, remove it from from the alias, unless it
1178// is the last member.
1179func AddressRemove(ctx context.Context, address string) (rerr error) {
1180 log := pkglog.WithContext(ctx)
1183 log.Errorx("removing address", rerr, slog.String("address", address))
1187 Conf.dynamicMutex.Lock()
1188 defer Conf.dynamicMutex.Unlock()
1190 ad, ok := Conf.accountDestinations[address]
1192 return fmt.Errorf("%w: address does not exists", ErrRequest)
1195 // Compose new config without modifying existing data structures. If we fail, we
1197 a, ok := Conf.Dynamic.Accounts[ad.Account]
1199 return fmt.Errorf("internal error: cannot find account")
1202 na.Destinations = map[string]config.Destination{}
1204 for destAddr, d := range a.Destinations {
1205 if destAddr != address {
1206 na.Destinations[destAddr] = d
1212 return fmt.Errorf("%w: address not removed, likely a postmaster/reporting address", ErrRequest)
1215 // Also remove matching address from FromIDLoginAddresses, composing a new slice.
1216 var fromIDLoginAddresses []string
1218 var pa smtp.Address // For non-catchall addresses (most).
1220 if strings.HasPrefix(address, "@") {
1221 dom, err = dns.ParseDomain(address[1:])
1223 return fmt.Errorf("%w: parsing domain for catchall address: %v", ErrRequest, err)
1226 pa, err = smtp.ParseAddress(address)
1228 return fmt.Errorf("%w: parsing address: %v", ErrRequest, err)
1232 for i, fa := range a.ParsedFromIDLoginAddresses {
1233 if fa.Domain != dom {
1234 // Keep for different domain.
1235 fromIDLoginAddresses = append(fromIDLoginAddresses, a.FromIDLoginAddresses[i])
1238 if strings.HasPrefix(address, "@") {
1241 dc, ok := Conf.Dynamic.Domains[dom.Name()]
1243 return fmt.Errorf("%w: unknown domain in fromid login address %q", ErrRequest, fa.Pack(true))
1245 flp := CanonicalLocalpart(fa.Localpart, dc)
1246 alp := CanonicalLocalpart(pa.Localpart, dc)
1248 // Keep for different localpart.
1249 fromIDLoginAddresses = append(fromIDLoginAddresses, a.FromIDLoginAddresses[i])
1252 na.FromIDLoginAddresses = fromIDLoginAddresses
1254 // And remove as member from aliases configured in domains.
1255 domains := maps.Clone(Conf.Dynamic.Domains)
1256 for _, aa := range na.Aliases {
1257 if aa.SubscriptionAddress != address {
1261 aliasAddr := fmt.Sprintf("%s@%s", aa.Alias.LocalpartStr, aa.Alias.Domain.Name())
1263 dom, ok := Conf.Dynamic.Domains[aa.Alias.Domain.Name()]
1265 return fmt.Errorf("cannot find domain for alias %s", aliasAddr)
1267 a, ok := dom.Aliases[aa.Alias.LocalpartStr]
1269 return fmt.Errorf("cannot find alias %s", aliasAddr)
1271 a.Addresses = slices.Clone(a.Addresses)
1272 a.Addresses = slices.DeleteFunc(a.Addresses, func(v string) bool { return v == address })
1273 if len(a.Addresses) == 0 {
1274 return fmt.Errorf("address is last member of alias %s, add new members or remove alias first", aliasAddr)
1276 a.ParsedAddresses = nil // Filled when parsing config.
1277 dom.Aliases = maps.Clone(dom.Aliases)
1278 dom.Aliases[aa.Alias.LocalpartStr] = a
1279 domains[aa.Alias.Domain.Name()] = dom
1281 na.Aliases = nil // Filled when parsing config.
1284 nc.Accounts = map[string]config.Account{}
1285 for name, a := range Conf.Dynamic.Accounts {
1286 nc.Accounts[name] = a
1288 nc.Accounts[ad.Account] = na
1289 nc.Domains = domains
1291 if err := writeDynamic(ctx, log, nc); err != nil {
1292 return fmt.Errorf("writing domains.conf: %w", err)
1294 log.Info("address removed", slog.String("address", address), slog.String("account", ad.Account))
1298func AliasAdd(ctx context.Context, addr smtp.Address, alias config.Alias) error {
1299 return DomainSave(ctx, addr.Domain.Name(), func(d *config.Domain) error {
1300 if _, ok := d.Aliases[addr.Localpart.String()]; ok {
1301 return fmt.Errorf("%w: alias already present", ErrRequest)
1303 if d.Aliases == nil {
1304 d.Aliases = map[string]config.Alias{}
1306 d.Aliases = maps.Clone(d.Aliases)
1307 d.Aliases[addr.Localpart.String()] = alias
1312func AliasUpdate(ctx context.Context, addr smtp.Address, alias config.Alias) error {
1313 return DomainSave(ctx, addr.Domain.Name(), func(d *config.Domain) error {
1314 a, ok := d.Aliases[addr.Localpart.String()]
1316 return fmt.Errorf("%w: alias does not exist", ErrRequest)
1318 a.PostPublic = alias.PostPublic
1319 a.ListMembers = alias.ListMembers
1320 a.AllowMsgFrom = alias.AllowMsgFrom
1321 d.Aliases = maps.Clone(d.Aliases)
1322 d.Aliases[addr.Localpart.String()] = a
1327func AliasRemove(ctx context.Context, addr smtp.Address) error {
1328 return DomainSave(ctx, addr.Domain.Name(), func(d *config.Domain) error {
1329 _, ok := d.Aliases[addr.Localpart.String()]
1331 return fmt.Errorf("%w: alias does not exist", ErrRequest)
1333 d.Aliases = maps.Clone(d.Aliases)
1334 delete(d.Aliases, addr.Localpart.String())
1339func AliasAddressesAdd(ctx context.Context, addr smtp.Address, addresses []string) error {
1340 if len(addresses) == 0 {
1341 return fmt.Errorf("%w: at least one address required", ErrRequest)
1343 return DomainSave(ctx, addr.Domain.Name(), func(d *config.Domain) error {
1344 alias, ok := d.Aliases[addr.Localpart.String()]
1346 return fmt.Errorf("%w: no such alias", ErrRequest)
1348 alias.Addresses = append(slices.Clone(alias.Addresses), addresses...)
1349 alias.ParsedAddresses = nil
1350 d.Aliases = maps.Clone(d.Aliases)
1351 d.Aliases[addr.Localpart.String()] = alias
1356func AliasAddressesRemove(ctx context.Context, addr smtp.Address, addresses []string) error {
1357 if len(addresses) == 0 {
1358 return fmt.Errorf("%w: need at least one address", ErrRequest)
1360 return DomainSave(ctx, addr.Domain.Name(), func(d *config.Domain) error {
1361 alias, ok := d.Aliases[addr.Localpart.String()]
1363 return fmt.Errorf("%w: no such alias", ErrRequest)
1365 alias.Addresses = slices.DeleteFunc(slices.Clone(alias.Addresses), func(addr string) bool {
1367 addresses = slices.DeleteFunc(addresses, func(a string) bool { return a == addr })
1368 return n > len(addresses)
1370 if len(addresses) > 0 {
1371 return fmt.Errorf("%w: address not found: %s", ErrRequest, strings.Join(addresses, ", "))
1373 alias.ParsedAddresses = nil
1374 d.Aliases = maps.Clone(d.Aliases)
1375 d.Aliases[addr.Localpart.String()] = alias
1380// AccountSave updates the configuration of an account. Function xmodify is called
1381// with a shallow copy of the current configuration of the account. It must not
1382// change referencing fields (e.g. existing slice/map/pointer), they may still be
1383// in use, and the change may be rolled back. Referencing values must be copied and
1384// replaced by the modify. The function may raise a panic for error handling.
1385func AccountSave(ctx context.Context, account string, xmodify func(acc *config.Account)) (rerr error) {
1386 log := pkglog.WithContext(ctx)
1389 log.Errorx("saving account fields", rerr, slog.String("account", account))
1393 Conf.dynamicMutex.Lock()
1394 defer Conf.dynamicMutex.Unlock()
1397 acc, ok := c.Accounts[account]
1399 return fmt.Errorf("%w: account not present", ErrRequest)
1404 // Compose new config without modifying existing data structures. If we fail, we
1407 nc.Accounts = map[string]config.Account{}
1408 for name, a := range c.Accounts {
1409 nc.Accounts[name] = a
1411 nc.Accounts[account] = acc
1413 if err := writeDynamic(ctx, log, nc); err != nil {
1414 return fmt.Errorf("writing domains.conf: %w", err)
1416 log.Info("account fields saved", slog.String("account", account))
1423 TLSModeImmediate TLSMode = 0
1424 TLSModeSTARTTLS TLSMode = 1
1425 TLSModeNone TLSMode = 2
1428type ProtocolConfig struct {
1434type ClientConfig struct {
1436 Submission ProtocolConfig
1439// ClientConfigDomain returns a single IMAP and Submission client configuration for
1441func ClientConfigDomain(d dns.Domain) (rconfig ClientConfig, rerr error) {
1442 var haveIMAP, haveSubmission bool
1444 domConf, ok := Conf.Domain(d)
1446 return ClientConfig{}, fmt.Errorf("%w: unknown domain", ErrRequest)
1449 gather := func(l config.Listener) (done bool) {
1450 host := Conf.Static.HostnameDomain
1451 if l.Hostname != "" {
1452 host = l.HostnameDomain
1454 if domConf.ClientSettingsDomain != "" {
1455 host = domConf.ClientSettingsDNSDomain
1457 if !haveIMAP && l.IMAPS.Enabled {
1458 rconfig.IMAP.Host = host
1459 rconfig.IMAP.Port = config.Port(l.IMAPS.Port, 993)
1460 rconfig.IMAP.TLSMode = TLSModeImmediate
1463 if !haveIMAP && l.IMAP.Enabled {
1464 rconfig.IMAP.Host = host
1465 rconfig.IMAP.Port = config.Port(l.IMAP.Port, 143)
1466 rconfig.IMAP.TLSMode = TLSModeSTARTTLS
1468 rconfig.IMAP.TLSMode = TLSModeNone
1472 if !haveSubmission && l.Submissions.Enabled {
1473 rconfig.Submission.Host = host
1474 rconfig.Submission.Port = config.Port(l.Submissions.Port, 465)
1475 rconfig.Submission.TLSMode = TLSModeImmediate
1476 haveSubmission = true
1478 if !haveSubmission && l.Submission.Enabled {
1479 rconfig.Submission.Host = host
1480 rconfig.Submission.Port = config.Port(l.Submission.Port, 587)
1481 rconfig.Submission.TLSMode = TLSModeSTARTTLS
1483 rconfig.Submission.TLSMode = TLSModeNone
1485 haveSubmission = true
1487 return haveIMAP && haveSubmission
1490 // Look at the public listener first. Most likely the intended configuration.
1491 if public, ok := Conf.Static.Listeners["public"]; ok {
1496 // Go through the other listeners in consistent order.
1497 names := maps.Keys(Conf.Static.Listeners)
1499 for _, name := range names {
1500 if gather(Conf.Static.Listeners[name]) {
1504 return ClientConfig{}, fmt.Errorf("%w: no listeners found for imap and/or submission", ErrRequest)
1507// ClientConfigs holds the client configuration for IMAP/Submission for a
1509type ClientConfigs struct {
1510 Entries []ClientConfigsEntry
1513type ClientConfigsEntry struct {
1521// ClientConfigsDomain returns the client configs for IMAP/Submission for a
1523func ClientConfigsDomain(d dns.Domain) (ClientConfigs, error) {
1524 domConf, ok := Conf.Domain(d)
1526 return ClientConfigs{}, fmt.Errorf("%w: unknown domain", ErrRequest)
1529 c := ClientConfigs{}
1530 c.Entries = []ClientConfigsEntry{}
1531 var listeners []string
1533 for name := range Conf.Static.Listeners {
1534 listeners = append(listeners, name)
1536 sort.Slice(listeners, func(i, j int) bool {
1537 return listeners[i] < listeners[j]
1540 note := func(tls bool, requiretls bool) string {
1542 return "plain text, no STARTTLS configured"
1545 return "STARTTLS required"
1547 return "STARTTLS optional"
1550 for _, name := range listeners {
1551 l := Conf.Static.Listeners[name]
1552 host := Conf.Static.HostnameDomain
1553 if l.Hostname != "" {
1554 host = l.HostnameDomain
1556 if domConf.ClientSettingsDomain != "" {
1557 host = domConf.ClientSettingsDNSDomain
1559 if l.Submissions.Enabled {
1560 c.Entries = append(c.Entries, ClientConfigsEntry{"Submission (SMTP)", host, config.Port(l.Submissions.Port, 465), name, "with TLS"})
1562 if l.IMAPS.Enabled {
1563 c.Entries = append(c.Entries, ClientConfigsEntry{"IMAP", host, config.Port(l.IMAPS.Port, 993), name, "with TLS"})
1565 if l.Submission.Enabled {
1566 c.Entries = append(c.Entries, ClientConfigsEntry{"Submission (SMTP)", host, config.Port(l.Submission.Port, 587), name, note(l.TLS != nil, !l.Submission.NoRequireSTARTTLS)})
1569 c.Entries = append(c.Entries, ClientConfigsEntry{"IMAP", host, config.Port(l.IMAPS.Port, 143), name, note(l.TLS != nil, !l.IMAP.NoRequireSTARTTLS)})
1576// IPs returns ip addresses we may be listening/receiving mail on or
1577// connecting/sending from to the outside.
1578func IPs(ctx context.Context, receiveOnly bool) ([]net.IP, error) {
1579 log := pkglog.WithContext(ctx)
1581 // Try to gather all IPs we are listening on by going through the config.
1582 // If we encounter 0.0.0.0 or ::, we'll gather all local IPs afterwards.
1584 var ipv4all, ipv6all bool
1585 for _, l := range Conf.Static.Listeners {
1586 // If NATed, we don't know our external IPs.
1591 if len(l.NATIPs) > 0 {
1594 for _, s := range check {
1595 ip := net.ParseIP(s)
1596 if ip.IsUnspecified() {
1597 if ip.To4() != nil {
1604 ips = append(ips, ip)
1608 // We'll list the IPs on the interfaces. How useful is this? There is a good chance
1609 // we're listening on all addresses because of a load balancer/firewall.
1610 if ipv4all || ipv6all {
1611 ifaces, err := net.Interfaces()
1613 return nil, fmt.Errorf("listing network interfaces: %v", err)
1615 for _, iface := range ifaces {
1616 if iface.Flags&net.FlagUp == 0 {
1619 addrs, err := iface.Addrs()
1621 return nil, fmt.Errorf("listing addresses for network interface: %v", err)
1623 if len(addrs) == 0 {
1627 for _, addr := range addrs {
1628 ip, _, err := net.ParseCIDR(addr.String())
1630 log.Errorx("bad interface addr", err, slog.Any("address", addr))
1633 v4 := ip.To4() != nil
1634 if ipv4all && v4 || ipv6all && !v4 {
1635 ips = append(ips, ip)
1645 for _, t := range Conf.Static.Transports {
1647 ips = append(ips, t.Socks.IPs...)