10 cryptorand "crypto/rand"
34 "golang.org/x/text/unicode/norm"
36 "github.com/mjl-/autocert"
38 "github.com/mjl-/sconf"
40 "github.com/mjl-/mox/autotls"
41 "github.com/mjl-/mox/config"
42 "github.com/mjl-/mox/dkim"
43 "github.com/mjl-/mox/dns"
44 "github.com/mjl-/mox/message"
45 "github.com/mjl-/mox/mlog"
46 "github.com/mjl-/mox/moxio"
47 "github.com/mjl-/mox/mtasts"
48 "github.com/mjl-/mox/smtp"
51var pkglog = mlog.New("mox", nil)
53// Pedantic enables stricter parsing.
56// Config paths are set early in program startup. They will point to files in
59 ConfigStaticPath string
60 ConfigDynamicPath string
61 Conf = Config{Log: map[string]slog.Level{"": slog.LevelError}}
64var ErrConfig = errors.New("config error")
66// Set by packages webadmin, webaccount, webmail, webapisrv to prevent cyclic dependencies.
67var NewWebadminHandler = func(basePath string, isForwarded bool) http.Handler { return nopHandler }
68var NewWebaccountHandler = func(basePath string, isForwarded bool) http.Handler { return nopHandler }
69var NewWebmailHandler = func(maxMsgSize int64, basePath string, isForwarded bool, accountPath string) http.Handler {
72var NewWebapiHandler = func(maxMsgSize int64, basePath string, isForwarded bool) http.Handler { return nopHandler }
74var nopHandler = http.HandlerFunc(nil)
76// Config as used in the code, a processed version of what is in the config file.
78// Use methods to lookup a domain/account/address in the dynamic configuration.
80 Static config.Static // Does not change during the lifetime of a running instance.
82 logMutex sync.Mutex // For accessing the log levels.
83 Log map[string]slog.Level
85 dynamicMutex sync.Mutex
86 Dynamic config.Dynamic // Can only be accessed directly by tests. Use methods on Config for locked access.
87 dynamicMtime time.Time
88 DynamicLastCheck time.Time // For use by quickstart only to skip checks.
90 // From canonical full address (localpart@domain, lower-cased when
91 // case-insensitive, stripped of catchall separator) to account and address.
92 // Domains are IDNA names in utf8. Dynamic config lock must be held when accessing.
93 AccountDestinationsLocked map[string]AccountDestination
95 // Like AccountDestinationsLocked, but for aliases.
96 aliases map[string]config.Alias
99type AccountDestination struct {
100 Catchall bool // If catchall destination for its domain.
101 Localpart smtp.Localpart // In original casing as written in config file.
103 Destination config.Destination
106// LogLevelSet sets a new log level for pkg. An empty pkg sets the default log
107// value that is used if no explicit log level is configured for a package.
108// This change is ephemeral, no config file is changed.
109func (c *Config) LogLevelSet(log mlog.Log, pkg string, level slog.Level) {
111 defer c.logMutex.Unlock()
112 l := c.copyLogLevels()
115 log.Print("log level changed", slog.String("pkg", pkg), slog.Any("level", mlog.LevelStrings[level]))
116 mlog.SetConfig(c.Log)
119// LogLevelRemove removes a configured log level for a package.
120func (c *Config) LogLevelRemove(log mlog.Log, pkg string) {
122 defer c.logMutex.Unlock()
123 l := c.copyLogLevels()
126 log.Print("log level cleared", slog.String("pkg", pkg))
127 mlog.SetConfig(c.Log)
130// copyLogLevels returns a copy of c.Log, for modifications.
131// must be called with log lock held.
132func (c *Config) copyLogLevels() map[string]slog.Level {
133 m := map[string]slog.Level{}
134 for pkg, level := range c.Log {
140// LogLevels returns a copy of the current log levels.
141func (c *Config) LogLevels() map[string]slog.Level {
143 defer c.logMutex.Unlock()
144 return c.copyLogLevels()
147// DynamicLockUnlock locks the dynamic config, will try updating the latest state
148// from disk, and return an unlock function. Should be called as "defer
149// Conf.DynamicLockUnlock()()".
150func (c *Config) DynamicLockUnlock() func() {
151 c.dynamicMutex.Lock()
153 if now.Sub(c.DynamicLastCheck) > time.Second {
154 c.DynamicLastCheck = now
155 if fi, err := os.Stat(ConfigDynamicPath); err != nil {
156 pkglog.Errorx("stat domains config", err)
157 } else if !fi.ModTime().Equal(c.dynamicMtime) {
158 if errs := c.loadDynamic(); len(errs) > 0 {
159 pkglog.Errorx("loading domains config", errs[0], slog.Any("errors", errs))
161 pkglog.Info("domains config reloaded")
162 c.dynamicMtime = fi.ModTime()
166 return c.dynamicMutex.Unlock
169func (c *Config) withDynamicLock(fn func()) {
170 defer c.DynamicLockUnlock()()
174// must be called with dynamic lock held.
175func (c *Config) loadDynamic() []error {
176 d, mtime, accDests, aliases, err := ParseDynamicConfig(context.Background(), pkglog, ConfigDynamicPath, c.Static)
181 c.dynamicMtime = mtime
182 c.AccountDestinationsLocked = accDests
184 c.allowACMEHosts(pkglog, true)
188// DynamicConfig returns a shallow copy of the dynamic config. Must not be modified.
189func (c *Config) DynamicConfig() (config config.Dynamic) {
190 c.withDynamicLock(func() {
191 config = c.Dynamic // Shallow copy.
196func (c *Config) Domains() (l []string) {
197 c.withDynamicLock(func() {
198 for name := range c.Dynamic.Domains {
202 sort.Slice(l, func(i, j int) bool {
208func (c *Config) Accounts() (l []string) {
209 c.withDynamicLock(func() {
210 for name := range c.Dynamic.Accounts {
217func (c *Config) AccountsDisabled() (all, disabled []string) {
218 c.withDynamicLock(func() {
219 for name, conf := range c.Dynamic.Accounts {
220 all = append(all, name)
221 if conf.LoginDisabled != "" {
222 disabled = append(disabled, name)
229// DomainLocalparts returns a mapping of encoded localparts to account names for a
230// domain, and encoded localparts to aliases. An empty localpart is a catchall
231// destination for a domain.
232func (c *Config) DomainLocalparts(d dns.Domain) (map[string]string, map[string]config.Alias) {
233 suffix := "@" + d.Name()
234 m := map[string]string{}
235 aliases := map[string]config.Alias{}
236 c.withDynamicLock(func() {
237 for addr, ad := range c.AccountDestinationsLocked {
238 if strings.HasSuffix(addr, suffix) {
242 m[ad.Localpart.String()] = ad.Account
246 for addr, a := range c.aliases {
247 if strings.HasSuffix(addr, suffix) {
248 aliases[a.LocalpartStr] = a
255func (c *Config) Domain(d dns.Domain) (dom config.Domain, ok bool) {
256 c.withDynamicLock(func() {
257 dom, ok = c.Dynamic.Domains[d.Name()]
262func (c *Config) DomainConfigs() (doms []config.Domain) {
263 c.withDynamicLock(func() {
264 doms = make([]config.Domain, 0, len(c.Dynamic.Domains))
265 for _, d := range c.Dynamic.Domains {
266 doms = append(doms, d)
272func (c *Config) Account(name string) (acc config.Account, ok bool) {
273 c.withDynamicLock(func() {
274 acc, ok = c.Dynamic.Accounts[name]
279func (c *Config) AccountDestination(addr string) (accDest AccountDestination, alias *config.Alias, ok bool) {
280 c.withDynamicLock(func() {
281 accDest, ok = c.AccountDestinationsLocked[addr]
284 a, ok = c.aliases[addr]
293func (c *Config) Routes(accountName string, domain dns.Domain) (accountRoutes, domainRoutes, globalRoutes []config.Route) {
294 c.withDynamicLock(func() {
295 acc := c.Dynamic.Accounts[accountName]
296 accountRoutes = acc.Routes
298 dom := c.Dynamic.Domains[domain.Name()]
299 domainRoutes = dom.Routes
301 globalRoutes = c.Dynamic.Routes
306func (c *Config) IsClientSettingsDomain(d dns.Domain) (is bool) {
307 c.withDynamicLock(func() {
308 _, is = c.Dynamic.ClientSettingDomains[d]
313func (c *Config) allowACMEHosts(log mlog.Log, checkACMEHosts bool) {
314 for _, l := range c.Static.Listeners {
315 if l.TLS == nil || l.TLS.ACME == "" {
319 m := c.Static.ACME[l.TLS.ACME].Manager
320 hostnames := map[dns.Domain]struct{}{}
322 hostnames[c.Static.HostnameDomain] = struct{}{}
323 if l.HostnameDomain.ASCII != "" {
324 hostnames[l.HostnameDomain] = struct{}{}
327 for _, dom := range c.Dynamic.Domains {
328 // Do not allow TLS certificates for domains for which we only accept DMARC/TLS
329 // reports as external party.
334 // Do not fetch TLS certs for disabled domains. The A/AAAA records may not be
335 // configured or still point to a previous machine before a migration.
340 if l.AutoconfigHTTPS.Enabled && !l.AutoconfigHTTPS.NonTLS {
341 if d, err := dns.ParseDomain("autoconfig." + dom.Domain.ASCII); err != nil {
342 log.Errorx("parsing autoconfig domain", err, slog.Any("domain", dom.Domain))
344 hostnames[d] = struct{}{}
348 if l.MTASTSHTTPS.Enabled && dom.MTASTS != nil && !l.MTASTSHTTPS.NonTLS {
349 d, err := dns.ParseDomain("mta-sts." + dom.Domain.ASCII)
351 log.Errorx("parsing mta-sts domain", err, slog.Any("domain", dom.Domain))
353 hostnames[d] = struct{}{}
357 if dom.ClientSettingsDomain != "" {
358 hostnames[dom.ClientSettingsDNSDomain] = struct{}{}
362 if l.WebserverHTTPS.Enabled {
363 for from := range c.Dynamic.WebDNSDomainRedirects {
364 hostnames[from] = struct{}{}
366 for _, wh := range c.Dynamic.WebHandlers {
367 hostnames[wh.DNSDomain] = struct{}{}
371 public := c.Static.Listeners["public"]
373 if len(public.NATIPs) > 0 {
379 m.SetAllowedHostnames(log, dns.StrictResolver{Pkg: "autotls", Log: log.Logger}, hostnames, ips, checkACMEHosts)
383// todo future: write config parsing & writing code that can read a config and remembers the exact tokens including newlines and comments, and can write back a modified file. the goal is to be able to write a config file automatically (after changing fields through the ui), but not loose comments and whitespace, to still get useful diffs for storing the config in a version control system.
385// WriteDynamicLocked prepares an updated internal state for the new dynamic
386// config, then writes it to disk and activates it.
388// Returns ErrConfig if the configuration is not valid.
390// Must be called with config lock held.
391func WriteDynamicLocked(ctx context.Context, log mlog.Log, c config.Dynamic) error {
392 accDests, aliases, errs := prepareDynamicConfig(ctx, log, ConfigDynamicPath, Conf.Static, &c)
394 errstrs := make([]string, len(errs))
395 for i, err := range errs {
396 errstrs[i] = err.Error()
398 return fmt.Errorf("%w: %s", ErrConfig, strings.Join(errstrs, "; "))
402 err := sconf.Write(&b, c)
406 f, err := os.OpenFile(ConfigDynamicPath, os.O_WRONLY, 0660)
413 log.Check(err, "closing file after error")
417 if _, err := f.Write(buf); err != nil {
418 return fmt.Errorf("write domains.conf: %v", err)
420 if err := f.Truncate(int64(len(buf))); err != nil {
421 return fmt.Errorf("truncate domains.conf after write: %v", err)
423 if err := f.Sync(); err != nil {
424 return fmt.Errorf("sync domains.conf after write: %v", err)
426 if err := moxio.SyncDir(log, filepath.Dir(ConfigDynamicPath)); err != nil {
427 return fmt.Errorf("sync dir of domains.conf after write: %v", err)
432 return fmt.Errorf("stat after writing domains.conf: %v", err)
435 if err := f.Close(); err != nil {
436 return fmt.Errorf("close written domains.conf: %v", err)
440 Conf.dynamicMtime = fi.ModTime()
441 Conf.DynamicLastCheck = time.Now()
443 Conf.AccountDestinationsLocked = accDests
444 Conf.aliases = aliases
446 Conf.allowACMEHosts(log, true)
451// MustLoadConfig loads the config, quitting on errors.
452func MustLoadConfig(doLoadTLSKeyCerts, checkACMEHosts bool) {
453 errs := LoadConfig(context.Background(), pkglog, doLoadTLSKeyCerts, checkACMEHosts)
455 pkglog.Error("loading config file: multiple errors")
456 for _, err := range errs {
457 pkglog.Errorx("config error", err)
459 pkglog.Fatal("stopping after multiple config errors")
460 } else if len(errs) == 1 {
461 pkglog.Fatalx("loading config file", errs[0])
465// LoadConfig attempts to parse and load a config, returning any errors
467func LoadConfig(ctx context.Context, log mlog.Log, doLoadTLSKeyCerts, checkACMEHosts bool) []error {
468 Shutdown, ShutdownCancel = context.WithCancel(context.Background())
469 Context, ContextCancel = context.WithCancel(context.Background())
471 c, errs := ParseConfig(ctx, log, ConfigStaticPath, false, doLoadTLSKeyCerts, checkACMEHosts)
476 mlog.SetConfig(c.Log)
481// SetConfig sets a new config. Not to be used during normal operation.
482func SetConfig(c *Config) {
483 // Cannot just assign *c to Conf, it would copy the mutex.
484 Conf = Config{c.Static, sync.Mutex{}, c.Log, sync.Mutex{}, c.Dynamic, c.dynamicMtime, c.DynamicLastCheck, c.AccountDestinationsLocked, c.aliases}
486 // If we have non-standard CA roots, use them for all HTTPS requests.
487 if Conf.Static.TLS.CertPool != nil {
488 http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{
489 RootCAs: Conf.Static.TLS.CertPool,
493 SetPedantic(c.Static.Pedantic)
496// Set pedantic in all packages.
497func SetPedantic(p bool) {
505// ParseConfig parses the static config at path p. If checkOnly is true, no changes
506// are made, such as registering ACME identities. If doLoadTLSKeyCerts is true,
507// the TLS KeyCerts configuration is loaded and checked. This is used during the
508// quickstart in the case the user is going to provide their own certificates.
509// If checkACMEHosts is true, the hosts allowed for acme are compared with the
510// explicitly configured ips we are listening on.
511func ParseConfig(ctx context.Context, log mlog.Log, p string, checkOnly, doLoadTLSKeyCerts, checkACMEHosts bool) (c *Config, errs []error) {
513 Static: config.Static{
520 if os.IsNotExist(err) && os.Getenv("MOXCONF") == "" {
521 return nil, []error{fmt.Errorf("open config file: %v (hint: use mox -config ... or set MOXCONF=...)", err)}
523 return nil, []error{fmt.Errorf("open config file: %v", err)}
526 if err := sconf.Parse(f, &c.Static); err != nil {
527 return nil, []error{fmt.Errorf("parsing %s%v", p, err)}
530 if xerrs := PrepareStaticConfig(ctx, log, p, c, checkOnly, doLoadTLSKeyCerts); len(xerrs) > 0 {
534 pp := filepath.Join(filepath.Dir(p), "domains.conf")
535 c.Dynamic, c.dynamicMtime, c.AccountDestinationsLocked, c.aliases, errs = ParseDynamicConfig(ctx, log, pp, c.Static)
538 c.allowACMEHosts(log, checkACMEHosts)
544// PrepareStaticConfig parses the static config file and prepares data structures
545// for starting mox. If checkOnly is set no substantial changes are made, like
546// creating an ACME registration.
547func PrepareStaticConfig(ctx context.Context, log mlog.Log, configFile string, conf *Config, checkOnly, doLoadTLSKeyCerts bool) (errs []error) {
548 addErrorf := func(format string, args ...any) {
549 errs = append(errs, fmt.Errorf(format, args...))
554 // check that mailbox is in unicode NFC normalized form.
555 checkMailboxNormf := func(mailbox string, format string, args ...any) {
556 s := norm.NFC.String(mailbox)
558 msg := fmt.Sprintf(format, args...)
559 addErrorf("%s: mailbox %q is not in NFC normalized form, should be %q", msg, mailbox, s)
563 // Post-process logging config.
564 if logLevel, ok := mlog.Levels[c.LogLevel]; ok {
565 conf.Log = map[string]slog.Level{"": logLevel}
567 addErrorf("invalid log level %q", c.LogLevel)
569 for pkg, s := range c.PackageLogLevels {
570 if logLevel, ok := mlog.Levels[s]; ok {
571 conf.Log[pkg] = logLevel
573 addErrorf("invalid package log level %q", s)
580 u, err := user.Lookup(c.User)
582 uid, err := strconv.ParseUint(c.User, 10, 32)
584 addErrorf("parsing unknown user %s as uid: %v (hint: add user mox with \"useradd -d $PWD mox\" or specify a different username on the quickstart command-line)", c.User, err)
586 // We assume the same gid as uid.
591 if uid, err := strconv.ParseUint(u.Uid, 10, 32); err != nil {
592 addErrorf("parsing uid %s: %v", u.Uid, err)
596 if gid, err := strconv.ParseUint(u.Gid, 10, 32); err != nil {
597 addErrorf("parsing gid %s: %v", u.Gid, err)
603 hostname, err := dns.ParseDomain(c.Hostname)
605 addErrorf("parsing hostname: %s", err)
606 } else if hostname.Name() != c.Hostname {
607 addErrorf("hostname must be in unicode form %q instead of %q", hostname.Name(), c.Hostname)
609 c.HostnameDomain = hostname
611 if c.HostTLSRPT.Account != "" {
612 tlsrptLocalpart, err := smtp.ParseLocalpart(c.HostTLSRPT.Localpart)
614 addErrorf("invalid localpart %q for host tlsrpt: %v", c.HostTLSRPT.Localpart, err)
615 } else if tlsrptLocalpart.IsInternational() {
616 // Does not appear documented in
../rfc/8460, but similar to DMARC it makes sense
617 // to keep this ascii-only addresses.
618 addErrorf("host TLSRPT localpart %q is an internationalized address, only conventional ascii-only address allowed for interopability", tlsrptLocalpart)
620 c.HostTLSRPT.ParsedLocalpart = tlsrptLocalpart
623 // Return private key for host name for use with an ACME. Used to return the same
624 // private key as pre-generated for use with DANE, with its public key in DNS.
625 // We only use this key for Listener's that have this ACME configured, and for
626 // which the effective listener host name (either specific to the listener, or the
627 // global name) is requested. Other host names can get a fresh private key, they
628 // don't appear in DANE records.
630 // - run 0: only use listener with explicitly matching host name in listener
631 // (default quickstart config does not set it).
632 // - run 1: only look at public listener (and host matching mox host name)
633 // - run 2: all listeners (and host matching mox host name)
634 findACMEHostPrivateKey := func(acmeName, host string, keyType autocert.KeyType, run int) crypto.Signer {
635 for listenerName, l := range Conf.Static.Listeners {
636 if l.TLS == nil || l.TLS.ACME != acmeName {
639 if run == 0 && host != l.HostnameDomain.ASCII {
642 if run == 1 && listenerName != "public" || host != Conf.Static.HostnameDomain.ASCII {
646 case autocert.KeyRSA2048:
647 if len(l.TLS.HostPrivateRSA2048Keys) == 0 {
650 return l.TLS.HostPrivateRSA2048Keys[0]
651 case autocert.KeyECDSAP256:
652 if len(l.TLS.HostPrivateECDSAP256Keys) == 0 {
655 return l.TLS.HostPrivateECDSAP256Keys[0]
662 // Make a function for an autocert.Manager.GetPrivateKey, using findACMEHostPrivateKey.
663 makeGetPrivateKey := func(acmeName string) func(host string, keyType autocert.KeyType) (crypto.Signer, error) {
664 return func(host string, keyType autocert.KeyType) (crypto.Signer, error) {
665 key := findACMEHostPrivateKey(acmeName, host, keyType, 0)
667 key = findACMEHostPrivateKey(acmeName, host, keyType, 1)
670 key = findACMEHostPrivateKey(acmeName, host, keyType, 2)
673 log.Debug("found existing private key for certificate for host",
674 slog.String("acmename", acmeName),
675 slog.String("host", host),
676 slog.Any("keytype", keyType))
679 log.Debug("generating new private key for certificate for host",
680 slog.String("acmename", acmeName),
681 slog.String("host", host),
682 slog.Any("keytype", keyType))
684 case autocert.KeyRSA2048:
685 return rsa.GenerateKey(cryptorand.Reader, 2048)
686 case autocert.KeyECDSAP256:
687 return ecdsa.GenerateKey(elliptic.P256(), cryptorand.Reader)
689 return nil, fmt.Errorf("unrecognized requested key type %v", keyType)
693 for name, acme := range c.ACME {
694 addAcmeErrorf := func(format string, args ...any) {
695 addErrorf("acme provider %s: %s", name, fmt.Sprintf(format, args...))
700 if acme.ExternalAccountBinding != nil {
701 eabKeyID = acme.ExternalAccountBinding.KeyID
702 p := configDirPath(configFile, acme.ExternalAccountBinding.KeyFile)
703 buf, err := os.ReadFile(p)
705 addAcmeErrorf("reading external account binding key: %s", err)
707 dec := make([]byte, base64.RawURLEncoding.DecodedLen(len(buf)))
708 n, err := base64.RawURLEncoding.Decode(dec, buf)
710 addAcmeErrorf("parsing external account binding key as base64: %s", err)
721 acmeDir := dataDirPath(configFile, c.DataDir, "acme")
722 os.MkdirAll(acmeDir, 0770)
723 manager, err := autotls.Load(name, acmeDir, acme.ContactEmail, acme.DirectoryURL, eabKeyID, eabKey, makeGetPrivateKey(name), Shutdown.Done())
725 addAcmeErrorf("loading ACME identity: %s", err)
727 acme.Manager = manager
729 // Help configurations from older quickstarts.
730 if acme.IssuerDomainName == "" && acme.DirectoryURL == "https://acme-v02.api.letsencrypt.org/directory" {
731 acme.IssuerDomainName = "letsencrypt.org"
737 var haveUnspecifiedSMTPListener bool
738 for name, l := range c.Listeners {
739 addListenerErrorf := func(format string, args ...any) {
740 addErrorf("listener %s: %s", name, fmt.Sprintf(format, args...))
743 if l.Hostname != "" {
744 d, err := dns.ParseDomain(l.Hostname)
746 addListenerErrorf("parsing hostname %q: %s", l.Hostname, err)
751 if l.TLS.ACME != "" && len(l.TLS.KeyCerts) != 0 {
752 addListenerErrorf("cannot have ACME and static key/certificates")
753 } else if l.TLS.ACME != "" {
754 acme, ok := c.ACME[l.TLS.ACME]
756 addListenerErrorf("unknown ACME provider %q", l.TLS.ACME)
759 // If only checking or with missing ACME definition, we don't have an acme manager,
760 // so set an empty tls config to continue.
761 var tlsconfig, tlsconfigFallback *tls.Config
762 if checkOnly || acme.Manager == nil {
763 tlsconfig = &tls.Config{}
764 tlsconfigFallback = &tls.Config{}
766 hostname := c.HostnameDomain
767 if l.Hostname != "" {
768 hostname = l.HostnameDomain
770 // If SNI is absent, we will use the listener hostname, but reject connections with
771 // an SNI hostname that is not allowlisted.
772 // Incoming SMTP deliveries use tlsconfigFallback for interoperability. TLS
773 // connections for unknown SNI hostnames fall back to a certificate for the
774 // listener hostname instead of causing the TLS connection to fail.
775 tlsconfig = acme.Manager.TLSConfig(hostname, true, false)
776 tlsconfigFallback = acme.Manager.TLSConfig(hostname, true, true)
777 l.TLS.ACMEConfig = acme.Manager.ACMETLSConfig
779 l.TLS.Config = tlsconfig
780 l.TLS.ConfigFallback = tlsconfigFallback
781 } else if len(l.TLS.KeyCerts) != 0 {
782 if doLoadTLSKeyCerts {
783 if err := loadTLSKeyCerts(configFile, "listener "+name, l.TLS); err != nil {
784 addListenerErrorf("%w", err)
788 addListenerErrorf("cannot have TLS config without ACME and without static keys/certificates")
790 for _, privKeyFile := range l.TLS.HostPrivateKeyFiles {
791 keyPath := configDirPath(configFile, privKeyFile)
792 privKey, err := loadPrivateKeyFile(keyPath)
794 addListenerErrorf("parsing host private key for DANE and ACME certificates: %v", err)
797 switch k := privKey.(type) {
798 case *rsa.PrivateKey:
799 if k.N.BitLen() != 2048 {
800 log.Error("need rsa key with 2048 bits, for host private key for DANE/ACME certificates, ignoring",
801 slog.String("listener", name),
802 slog.String("file", keyPath),
803 slog.Int("bits", k.N.BitLen()))
806 l.TLS.HostPrivateRSA2048Keys = append(l.TLS.HostPrivateRSA2048Keys, k)
807 case *ecdsa.PrivateKey:
808 if k.Curve != elliptic.P256() {
809 log.Error("unrecognized ecdsa curve for host private key for DANE/ACME certificates, ignoring", slog.String("listener", name), slog.String("file", keyPath))
812 l.TLS.HostPrivateECDSAP256Keys = append(l.TLS.HostPrivateECDSAP256Keys, k)
814 log.Error("unrecognized key type for host private key for DANE/ACME certificates, ignoring",
815 slog.String("listener", name),
816 slog.String("file", keyPath),
817 slog.String("keytype", fmt.Sprintf("%T", privKey)))
821 if l.TLS.ACME != "" && (len(l.TLS.HostPrivateRSA2048Keys) == 0) != (len(l.TLS.HostPrivateECDSAP256Keys) == 0) {
822 log.Warn("uncommon configuration with either only an RSA 2048 or ECDSA P256 host private key for DANE/ACME certificates; this ACME implementation can retrieve certificates for both type of keys, it is recommended to set either both or none; continuing")
826 var minVersion uint16 = tls.VersionTLS12
827 if l.TLS.MinVersion != "" {
828 versions := map[string]uint16{
829 "TLSv1.0": tls.VersionTLS10,
830 "TLSv1.1": tls.VersionTLS11,
831 "TLSv1.2": tls.VersionTLS12,
832 "TLSv1.3": tls.VersionTLS13,
834 v, ok := versions[l.TLS.MinVersion]
836 addListenerErrorf("unknown TLS mininum version %q", l.TLS.MinVersion)
840 if l.TLS.Config != nil {
841 l.TLS.Config.MinVersion = minVersion
843 if l.TLS.ConfigFallback != nil {
844 l.TLS.ConfigFallback.MinVersion = minVersion
846 if l.TLS.ACMEConfig != nil {
847 l.TLS.ACMEConfig.MinVersion = minVersion
850 var needsTLS []string
851 needtls := func(s string, v bool) {
853 needsTLS = append(needsTLS, s)
856 needtls("IMAPS", l.IMAPS.Enabled)
857 needtls("SMTP", l.SMTP.Enabled && !l.SMTP.NoSTARTTLS)
858 needtls("Submissions", l.Submissions.Enabled)
859 needtls("Submission", l.Submission.Enabled && !l.Submission.NoRequireSTARTTLS)
860 needtls("AccountHTTPS", l.AccountHTTPS.Enabled)
861 needtls("AdminHTTPS", l.AdminHTTPS.Enabled)
862 needtls("AutoconfigHTTPS", l.AutoconfigHTTPS.Enabled && !l.AutoconfigHTTPS.NonTLS)
863 needtls("MTASTSHTTPS", l.MTASTSHTTPS.Enabled && !l.MTASTSHTTPS.NonTLS)
864 needtls("WebserverHTTPS", l.WebserverHTTPS.Enabled)
865 if len(needsTLS) > 0 {
866 addListenerErrorf("no tls config specified, but requires tls for %s", strings.Join(needsTLS, ", "))
869 if l.AutoconfigHTTPS.Enabled && l.MTASTSHTTPS.Enabled && l.AutoconfigHTTPS.Port == l.MTASTSHTTPS.Port && l.AutoconfigHTTPS.NonTLS != l.MTASTSHTTPS.NonTLS {
870 addListenerErrorf("autoconfig and mta-sts enabled on same port but with both http and https")
874 haveUnspecifiedSMTPListener = true
876 for _, ipstr := range l.IPs {
877 ip := net.ParseIP(ipstr)
879 addListenerErrorf("invalid IP %q", ipstr)
882 if ip.IsUnspecified() {
883 haveUnspecifiedSMTPListener = true
886 if len(c.SpecifiedSMTPListenIPs) >= 2 {
887 haveUnspecifiedSMTPListener = true
888 } else if len(c.SpecifiedSMTPListenIPs) > 0 && (c.SpecifiedSMTPListenIPs[0].To4() == nil) == (ip.To4() == nil) {
889 haveUnspecifiedSMTPListener = true
891 c.SpecifiedSMTPListenIPs = append(c.SpecifiedSMTPListenIPs, ip)
895 for _, s := range l.SMTP.DNSBLs {
896 d, err := dns.ParseDomain(s)
898 addListenerErrorf("parsing DNSBL zone %q: %s", s, err)
901 l.SMTP.DNSBLZones = append(l.SMTP.DNSBLZones, d)
903 if l.IPsNATed && len(l.NATIPs) > 0 {
904 addListenerErrorf("both IPsNATed and NATIPs configued (remove deprecated IPsNATed)")
906 for _, ipstr := range l.NATIPs {
907 ip := net.ParseIP(ipstr)
909 addListenerErrorf("invalid ip %q", ipstr)
910 } else if ip.IsUnspecified() || ip.IsLoopback() {
911 addListenerErrorf("NAT ip that is the unspecified or loopback address %s", ipstr)
914 checkPath := func(kind string, enabled bool, path string) {
915 if enabled && path != "" && !strings.HasPrefix(path, "/") {
916 addListenerErrorf("%s with path %q that must start with a slash", kind, path)
919 checkPath("AccountHTTP", l.AccountHTTP.Enabled, l.AccountHTTP.Path)
920 checkPath("AccountHTTPS", l.AccountHTTPS.Enabled, l.AccountHTTPS.Path)
921 checkPath("AdminHTTP", l.AdminHTTP.Enabled, l.AdminHTTP.Path)
922 checkPath("AdminHTTPS", l.AdminHTTPS.Enabled, l.AdminHTTPS.Path)
923 c.Listeners[name] = l
925 if haveUnspecifiedSMTPListener {
926 c.SpecifiedSMTPListenIPs = nil
929 var zerouse config.SpecialUseMailboxes
930 if len(c.DefaultMailboxes) > 0 && (c.InitialMailboxes.SpecialUse != zerouse || len(c.InitialMailboxes.Regular) > 0) {
931 addErrorf("cannot have both DefaultMailboxes and InitialMailboxes")
933 // DefaultMailboxes is deprecated.
934 for _, mb := range c.DefaultMailboxes {
935 checkMailboxNormf(mb, "default mailbox")
937 checkSpecialUseMailbox := func(nameOpt string) {
939 checkMailboxNormf(nameOpt, "special-use initial mailbox")
940 if strings.EqualFold(nameOpt, "inbox") {
941 addErrorf("initial mailbox cannot be set to Inbox (Inbox is always created)")
945 checkSpecialUseMailbox(c.InitialMailboxes.SpecialUse.Archive)
946 checkSpecialUseMailbox(c.InitialMailboxes.SpecialUse.Draft)
947 checkSpecialUseMailbox(c.InitialMailboxes.SpecialUse.Junk)
948 checkSpecialUseMailbox(c.InitialMailboxes.SpecialUse.Sent)
949 checkSpecialUseMailbox(c.InitialMailboxes.SpecialUse.Trash)
950 for _, name := range c.InitialMailboxes.Regular {
951 checkMailboxNormf(name, "regular initial mailbox")
952 if strings.EqualFold(name, "inbox") {
953 addErrorf("initial regular mailbox cannot be set to Inbox (Inbox is always created)")
957 checkTransportSMTP := func(name string, isTLS bool, t *config.TransportSMTP) {
958 addTransportErrorf := func(format string, args ...any) {
959 addErrorf("transport %s: %s", name, fmt.Sprintf(format, args...))
963 t.DNSHost, err = dns.ParseDomain(t.Host)
965 addTransportErrorf("bad host %s: %v", t.Host, err)
968 if isTLS && t.STARTTLSInsecureSkipVerify {
969 addTransportErrorf("cannot have STARTTLSInsecureSkipVerify with immediate TLS")
971 if isTLS && t.NoSTARTTLS {
972 addTransportErrorf("cannot have NoSTARTTLS with immediate TLS")
978 seen := map[string]bool{}
979 for _, m := range t.Auth.Mechanisms {
981 addTransportErrorf("duplicate authentication mechanism %s", m)
985 case "SCRAM-SHA-256-PLUS":
986 case "SCRAM-SHA-256":
987 case "SCRAM-SHA-1-PLUS":
992 addTransportErrorf("unknown authentication mechanism %s", m)
996 t.Auth.EffectiveMechanisms = t.Auth.Mechanisms
997 if len(t.Auth.EffectiveMechanisms) == 0 {
998 t.Auth.EffectiveMechanisms = []string{"SCRAM-SHA-256-PLUS", "SCRAM-SHA-256", "SCRAM-SHA-1-PLUS", "SCRAM-SHA-1", "CRAM-MD5"}
1002 checkTransportSocks := func(name string, t *config.TransportSocks) {
1003 addTransportErrorf := func(format string, args ...any) {
1004 addErrorf("transport %s: %s", name, fmt.Sprintf(format, args...))
1007 _, _, err := net.SplitHostPort(t.Address)
1009 addTransportErrorf("bad address %s: %v", t.Address, err)
1011 for _, ipstr := range t.RemoteIPs {
1012 ip := net.ParseIP(ipstr)
1014 addTransportErrorf("bad ip %s", ipstr)
1016 t.IPs = append(t.IPs, ip)
1019 t.Hostname, err = dns.ParseDomain(t.RemoteHostname)
1021 addTransportErrorf("bad hostname %s: %v", t.RemoteHostname, err)
1025 checkTransportDirect := func(name string, t *config.TransportDirect) {
1026 addTransportErrorf := func(format string, args ...any) {
1027 addErrorf("transport %s: %s", name, fmt.Sprintf(format, args...))
1030 if t.DisableIPv4 && t.DisableIPv6 {
1031 addTransportErrorf("both IPv4 and IPv6 are disabled, enable at least one")
1042 for name, t := range c.Transports {
1043 addTransportErrorf := func(format string, args ...any) {
1044 addErrorf("transport %s: %s", name, fmt.Sprintf(format, args...))
1048 if t.Submissions != nil {
1050 checkTransportSMTP(name, true, t.Submissions)
1052 if t.Submission != nil {
1054 checkTransportSMTP(name, false, t.Submission)
1058 checkTransportSMTP(name, false, t.SMTP)
1062 checkTransportSocks(name, t.Socks)
1064 if t.Direct != nil {
1066 checkTransportDirect(name, t.Direct)
1069 addTransportErrorf("cannot have multiple methods in a transport")
1073 // Load CA certificate pool.
1074 if c.TLS.CA != nil {
1075 if c.TLS.CA.AdditionalToSystem {
1077 c.TLS.CertPool, err = x509.SystemCertPool()
1079 addErrorf("fetching system CA cert pool: %v", err)
1082 c.TLS.CertPool = x509.NewCertPool()
1084 for _, certfile := range c.TLS.CA.CertFiles {
1085 p := configDirPath(configFile, certfile)
1086 pemBuf, err := os.ReadFile(p)
1088 addErrorf("reading TLS CA cert file: %v", err)
1090 } else if !c.TLS.CertPool.AppendCertsFromPEM(pemBuf) {
1091 // todo: can we check more fully if we're getting some useful data back?
1092 addErrorf("no CA certs added from %q", p)
1099// PrepareDynamicConfig parses the dynamic config file given a static file.
1100func ParseDynamicConfig(ctx context.Context, log mlog.Log, dynamicPath string, static config.Static) (c config.Dynamic, mtime time.Time, accDests map[string]AccountDestination, aliases map[string]config.Alias, errs []error) {
1101 addErrorf := func(format string, args ...any) {
1102 errs = append(errs, fmt.Errorf(format, args...))
1105 f, err := os.Open(dynamicPath)
1107 addErrorf("parsing domains config: %v", err)
1113 addErrorf("stat domains config: %v", err)
1115 if err := sconf.Parse(f, &c); err != nil {
1116 addErrorf("parsing dynamic config file: %v", err)
1120 accDests, aliases, errs = prepareDynamicConfig(ctx, log, dynamicPath, static, &c)
1121 return c, fi.ModTime(), accDests, aliases, errs
1124func prepareDynamicConfig(ctx context.Context, log mlog.Log, dynamicPath string, static config.Static, c *config.Dynamic) (accDests map[string]AccountDestination, aliases map[string]config.Alias, errs []error) {
1125 addErrorf := func(format string, args ...any) {
1126 errs = append(errs, fmt.Errorf(format, args...))
1129 // Check that mailbox is in unicode NFC normalized form.
1130 checkMailboxNormf := func(mailbox string, what string, errorf func(format string, args ...any)) {
1131 s := norm.NFC.String(mailbox)
1133 errorf("%s: mailbox %q is not in NFC normalized form, should be %q", what, mailbox, s)
1137 // Validate postmaster account exists.
1138 if _, ok := c.Accounts[static.Postmaster.Account]; !ok {
1139 addErrorf("postmaster account %q does not exist", static.Postmaster.Account)
1141 checkMailboxNormf(static.Postmaster.Mailbox, "postmaster mailbox", addErrorf)
1143 accDests = map[string]AccountDestination{}
1144 aliases = map[string]config.Alias{}
1146 // Validate host TLSRPT account/address.
1147 if static.HostTLSRPT.Account != "" {
1148 if _, ok := c.Accounts[static.HostTLSRPT.Account]; !ok {
1149 addErrorf("host tlsrpt account %q does not exist", static.HostTLSRPT.Account)
1151 checkMailboxNormf(static.HostTLSRPT.Mailbox, "host tlsrpt mailbox", addErrorf)
1153 // Localpart has been parsed already.
1155 addrFull := smtp.NewAddress(static.HostTLSRPT.ParsedLocalpart, static.HostnameDomain).String()
1156 dest := config.Destination{
1157 Mailbox: static.HostTLSRPT.Mailbox,
1158 HostTLSReports: true,
1160 accDests[addrFull] = AccountDestination{false, static.HostTLSRPT.ParsedLocalpart, static.HostTLSRPT.Account, dest}
1163 var haveSTSListener, haveWebserverListener bool
1164 for _, l := range static.Listeners {
1165 if l.MTASTSHTTPS.Enabled {
1166 haveSTSListener = true
1168 if l.WebserverHTTP.Enabled || l.WebserverHTTPS.Enabled {
1169 haveWebserverListener = true
1173 checkRoutes := func(descr string, routes []config.Route) {
1174 parseRouteDomains := func(l []string) []string {
1176 for _, e := range l {
1182 if strings.HasPrefix(e, ".") {
1186 d, err := dns.ParseDomain(e)
1188 addErrorf("%s: invalid domain %s: %v", descr, e, err)
1190 r = append(r, prefix+d.ASCII)
1195 for i := range routes {
1196 routes[i].FromDomainASCII = parseRouteDomains(routes[i].FromDomain)
1197 routes[i].ToDomainASCII = parseRouteDomains(routes[i].ToDomain)
1199 routes[i].ResolvedTransport, ok = static.Transports[routes[i].Transport]
1201 addErrorf("%s: route references undefined transport %s", descr, routes[i].Transport)
1206 checkRoutes("global routes", c.Routes)
1208 // Validate domains.
1209 c.ClientSettingDomains = map[dns.Domain]struct{}{}
1210 for d, domain := range c.Domains {
1211 addDomainErrorf := func(format string, args ...any) {
1212 addErrorf(fmt.Sprintf("domain %v: %s", d, fmt.Sprintf(format, args...)))
1215 dnsdomain, err := dns.ParseDomain(d)
1217 addDomainErrorf("parsing domain: %s", err)
1218 } else if dnsdomain.Name() != d {
1219 addDomainErrorf("must be specified in unicode form, %s", dnsdomain.Name())
1222 domain.Domain = dnsdomain
1224 if domain.ClientSettingsDomain != "" {
1225 csd, err := dns.ParseDomain(domain.ClientSettingsDomain)
1227 addDomainErrorf("bad client settings domain %q: %s", domain.ClientSettingsDomain, err)
1229 domain.ClientSettingsDNSDomain = csd
1230 c.ClientSettingDomains[csd] = struct{}{}
1233 for _, sign := range domain.DKIM.Sign {
1234 if _, ok := domain.DKIM.Selectors[sign]; !ok {
1235 addDomainErrorf("unknown selector %s for signing", sign)
1238 for name, sel := range domain.DKIM.Selectors {
1239 addSelectorErrorf := func(format string, args ...any) {
1240 addDomainErrorf("selector %s: %s", name, fmt.Sprintf(format, args...))
1243 seld, err := dns.ParseDomain(name)
1245 addSelectorErrorf("parsing selector: %s", err)
1246 } else if seld.Name() != name {
1247 addSelectorErrorf("must be specified in unicode form, %q", seld.Name())
1251 if sel.Expiration != "" {
1252 exp, err := time.ParseDuration(sel.Expiration)
1254 addSelectorErrorf("invalid expiration %q: %v", sel.Expiration, err)
1256 sel.ExpirationSeconds = int(exp / time.Second)
1260 sel.HashEffective = sel.Hash
1261 switch sel.HashEffective {
1263 sel.HashEffective = "sha256"
1265 log.Error("using sha1 with DKIM is deprecated as not secure enough, switch to sha256")
1268 addSelectorErrorf("unsupported hash %q", sel.HashEffective)
1271 pemBuf, err := os.ReadFile(configDirPath(dynamicPath, sel.PrivateKeyFile))
1273 addSelectorErrorf("reading private key: %s", err)
1276 p, _ := pem.Decode(pemBuf)
1278 addSelectorErrorf("private key has no PEM block")
1281 key, err := x509.ParsePKCS8PrivateKey(p.Bytes)
1283 addSelectorErrorf("parsing private key: %s", err)
1286 switch k := key.(type) {
1287 case *rsa.PrivateKey:
1288 if k.N.BitLen() < 1024 {
1290 // Let's help user do the right thing.
1291 addSelectorErrorf("rsa keys should be >= 1024 bits, is %d bits", k.N.BitLen())
1294 sel.Algorithm = fmt.Sprintf("rsa-%d", k.N.BitLen())
1295 case ed25519.PrivateKey:
1296 if sel.HashEffective != "sha256" {
1297 addSelectorErrorf("hash algorithm %q is not supported with ed25519, only sha256 is", sel.HashEffective)
1300 sel.Algorithm = "ed25519"
1302 addSelectorErrorf("private key type %T not yet supported", key)
1305 if len(sel.Headers) == 0 {
1309 // By default we seal signed headers, and we sign user-visible headers to
1310 // prevent/limit reuse of previously signed messages: All addressing fields, date
1311 // and subject, message-referencing fields, parsing instructions (content-type).
1312 sel.HeadersEffective = strings.Split("From,To,Cc,Bcc,Reply-To,References,In-Reply-To,Subject,Date,Message-Id,Content-Type", ",")
1315 for _, h := range sel.Headers {
1316 from = from || strings.EqualFold(h, "From")
1318 if strings.EqualFold(h, "DKIM-Signature") || strings.EqualFold(h, "Received") || strings.EqualFold(h, "Return-Path") {
1319 log.Error("DKIM-signing header %q is recommended against as it may be modified in transit")
1323 addSelectorErrorf("From-field must always be DKIM-signed")
1325 sel.HeadersEffective = sel.Headers
1328 domain.DKIM.Selectors[name] = sel
1331 if domain.MTASTS != nil {
1332 if !haveSTSListener {
1333 addDomainErrorf("MTA-STS enabled, but there is no listener for MTASTS", d)
1335 sts := domain.MTASTS
1336 if sts.PolicyID == "" {
1337 addDomainErrorf("invalid empty MTA-STS PolicyID")
1340 case mtasts.ModeNone, mtasts.ModeTesting, mtasts.ModeEnforce:
1342 addDomainErrorf("invalid mtasts mode %q", sts.Mode)
1346 checkRoutes("routes for domain", domain.Routes)
1348 c.Domains[d] = domain
1351 // To determine ReportsOnly.
1352 domainHasAddress := map[string]bool{}
1354 // Validate email addresses.
1355 for accName, acc := range c.Accounts {
1356 addAccountErrorf := func(format string, args ...any) {
1357 addErrorf("account %q: %s", accName, fmt.Sprintf(format, args...))
1361 acc.DNSDomain, err = dns.ParseDomain(acc.Domain)
1363 addAccountErrorf("parsing domain %s: %s", acc.Domain, err)
1366 if strings.EqualFold(acc.RejectsMailbox, "Inbox") {
1367 addAccountErrorf("cannot set RejectsMailbox to inbox, messages will be removed automatically from the rejects mailbox")
1369 checkMailboxNormf(acc.RejectsMailbox, "rejects mailbox", addErrorf)
1371 if len(acc.LoginDisabled) > 256 {
1372 addAccountErrorf("message for disabled login must be <256 characters")
1374 for _, c := range acc.LoginDisabled {
1375 // For IMAP and SMTP. IMAP only allows UTF8 after "ENABLE IMAPrev2".
1376 if c < ' ' || c >= 0x7f {
1377 addAccountErrorf("message cannot contain control characters including newlines, and must be ascii-only")
1381 if acc.AutomaticJunkFlags.JunkMailboxRegexp != "" {
1382 r, err := regexp.Compile(acc.AutomaticJunkFlags.JunkMailboxRegexp)
1384 addAccountErrorf("invalid JunkMailboxRegexp regular expression: %v", err)
1388 if acc.AutomaticJunkFlags.NeutralMailboxRegexp != "" {
1389 r, err := regexp.Compile(acc.AutomaticJunkFlags.NeutralMailboxRegexp)
1391 addAccountErrorf("invalid NeutralMailboxRegexp regular expression: %v", err)
1393 acc.NeutralMailbox = r
1395 if acc.AutomaticJunkFlags.NotJunkMailboxRegexp != "" {
1396 r, err := regexp.Compile(acc.AutomaticJunkFlags.NotJunkMailboxRegexp)
1398 addAccountErrorf("invalid NotJunkMailboxRegexp regular expression: %v", err)
1400 acc.NotJunkMailbox = r
1403 if acc.JunkFilter != nil {
1404 params := acc.JunkFilter.Params
1405 if params.MaxPower < 0 || params.MaxPower > 0.5 {
1406 addAccountErrorf("junk filter MaxPower must be >= 0 and < 0.5")
1408 if params.TopWords < 0 {
1409 addAccountErrorf("junk filter TopWords must be >= 0")
1411 if params.IgnoreWords < 0 || params.IgnoreWords > 0.5 {
1412 addAccountErrorf("junk filter IgnoreWords must be >= 0 and < 0.5")
1414 if params.RareWords < 0 {
1415 addAccountErrorf("junk filter RareWords must be >= 0")
1419 acc.ParsedFromIDLoginAddresses = make([]smtp.Address, len(acc.FromIDLoginAddresses))
1420 for i, s := range acc.FromIDLoginAddresses {
1421 a, err := smtp.ParseAddress(s)
1423 addAccountErrorf("invalid fromid login address %q: %v", s, err)
1425 // We check later on if address belongs to account.
1426 dom, ok := c.Domains[a.Domain.Name()]
1428 addAccountErrorf("unknown domain in fromid login address %q", s)
1429 } else if dom.LocalpartCatchallSeparator == "" {
1430 addAccountErrorf("localpart catchall separator not configured for domain for fromid login address %q", s)
1432 acc.ParsedFromIDLoginAddresses[i] = a
1435 // Clear any previously derived state.
1438 c.Accounts[accName] = acc
1440 if acc.OutgoingWebhook != nil {
1441 u, err := url.Parse(acc.OutgoingWebhook.URL)
1442 if err == nil && (u.Scheme != "http" && u.Scheme != "https") {
1443 err = errors.New("scheme must be http or https")
1446 addAccountErrorf("parsing outgoing hook url %q: %v", acc.OutgoingWebhook.URL, err)
1449 // note: outgoing hook events are in ../queue/hooks.go, ../mox-/config.go, ../queue.go and ../webapi/gendoc.sh. keep in sync.
1450 outgoingHookEvents := []string{"delivered", "suppressed", "delayed", "failed", "relayed", "expanded", "canceled", "unrecognized"}
1451 for _, e := range acc.OutgoingWebhook.Events {
1452 if !slices.Contains(outgoingHookEvents, e) {
1453 addAccountErrorf("unknown outgoing hook event %q", e)
1457 if acc.IncomingWebhook != nil {
1458 u, err := url.Parse(acc.IncomingWebhook.URL)
1459 if err == nil && (u.Scheme != "http" && u.Scheme != "https") {
1460 err = errors.New("scheme must be http or https")
1463 addAccountErrorf("parsing incoming hook url %q: %v", acc.IncomingWebhook.URL, err)
1467 // todo deprecated: only localpart as keys for Destinations, we are replacing them with full addresses. if domains.conf is written, we won't have to do this again.
1468 replaceLocalparts := map[string]string{}
1470 for addrName, dest := range acc.Destinations {
1471 addDestErrorf := func(format string, args ...any) {
1472 addAccountErrorf("destination %q: %s", addrName, fmt.Sprintf(format, args...))
1475 checkMailboxNormf(dest.Mailbox, "destination mailbox", addDestErrorf)
1477 if dest.SMTPError != "" {
1478 if len(dest.SMTPError) > 256 {
1479 addDestErrorf("smtp error must be smaller than 256 bytes")
1481 for _, c := range dest.SMTPError {
1482 if c < ' ' || c >= 0x7f {
1483 addDestErrorf("smtp error cannot contain contain control characters (including newlines) or non-ascii")
1488 if dest.Mailbox != "" {
1489 addDestErrorf("cannot have both SMTPError and Mailbox")
1491 if len(dest.Rulesets) != 0 {
1492 addDestErrorf("cannot have both SMTPError and Rulesets")
1495 t := strings.SplitN(dest.SMTPError, " ", 2)
1498 addDestErrorf("smtp error must be 421 or 550 (with optional message), not %q", dest.SMTPError)
1501 dest.SMTPErrorCode = smtp.C451LocalErr
1502 dest.SMTPErrorSecode = smtp.SeSys3Other0
1503 dest.SMTPErrorMsg = "error processing"
1505 dest.SMTPErrorCode = smtp.C550MailboxUnavail
1506 dest.SMTPErrorSecode = smtp.SeAddr1UnknownDestMailbox1
1507 dest.SMTPErrorMsg = "no such user(s)"
1510 dest.SMTPErrorMsg = strings.TrimSpace(t[1])
1512 acc.Destinations[addrName] = dest
1515 if dest.MessageAuthRequiredSMTPError != "" {
1516 if len(dest.MessageAuthRequiredSMTPError) > 256 {
1517 addDestErrorf("message authentication required smtp error must be smaller than 256 bytes")
1519 for _, c := range dest.MessageAuthRequiredSMTPError {
1520 if c < ' ' || c >= 0x7f {
1521 addDestErrorf("message authentication required smtp error cannot contain contain control characters (including newlines) or non-ascii")
1527 for i, rs := range dest.Rulesets {
1528 addRulesetErrorf := func(format string, args ...any) {
1529 addDestErrorf("ruleset %d: %s", i+1, fmt.Sprintf(format, args...))
1532 checkMailboxNormf(rs.Mailbox, "ruleset mailbox", addRulesetErrorf)
1536 if rs.SMTPMailFromRegexp != "" {
1538 r, err := regexp.Compile(rs.SMTPMailFromRegexp)
1540 addRulesetErrorf("invalid SMTPMailFrom regular expression: %v", err)
1542 c.Accounts[accName].Destinations[addrName].Rulesets[i].SMTPMailFromRegexpCompiled = r
1544 if rs.MsgFromRegexp != "" {
1546 r, err := regexp.Compile(rs.MsgFromRegexp)
1548 addRulesetErrorf("invalid MsgFrom regular expression: %v", err)
1550 c.Accounts[accName].Destinations[addrName].Rulesets[i].MsgFromRegexpCompiled = r
1552 if rs.VerifiedDomain != "" {
1554 d, err := dns.ParseDomain(rs.VerifiedDomain)
1556 addRulesetErrorf("invalid VerifiedDomain: %v", err)
1558 c.Accounts[accName].Destinations[addrName].Rulesets[i].VerifiedDNSDomain = d
1561 var hdr [][2]*regexp.Regexp
1562 for k, v := range rs.HeadersRegexp {
1564 if strings.ToLower(k) != k {
1565 addRulesetErrorf("header field %q must only have lower case characters", k)
1567 if strings.ToLower(v) != v {
1568 addRulesetErrorf("header value %q must only have lower case characters", v)
1570 rk, err := regexp.Compile(k)
1572 addRulesetErrorf("invalid rule header regexp %q: %v", k, err)
1574 rv, err := regexp.Compile(v)
1576 addRulesetErrorf("invalid rule header regexp %q: %v", v, err)
1578 hdr = append(hdr, [...]*regexp.Regexp{rk, rv})
1580 c.Accounts[accName].Destinations[addrName].Rulesets[i].HeadersRegexpCompiled = hdr
1583 addRulesetErrorf("ruleset must have at least one rule")
1586 if rs.IsForward && rs.ListAllowDomain != "" {
1587 addRulesetErrorf("ruleset cannot have both IsForward and ListAllowDomain")
1590 if rs.SMTPMailFromRegexp == "" || rs.VerifiedDomain == "" {
1591 addRulesetErrorf("ruleset with IsForward must have both SMTPMailFromRegexp and VerifiedDomain too")
1594 if rs.ListAllowDomain != "" {
1595 d, err := dns.ParseDomain(rs.ListAllowDomain)
1597 addRulesetErrorf("invalid ListAllowDomain %q: %v", rs.ListAllowDomain, err)
1599 c.Accounts[accName].Destinations[addrName].Rulesets[i].ListAllowDNSDomain = d
1602 checkMailboxNormf(rs.AcceptRejectsToMailbox, "rejects mailbox", addRulesetErrorf)
1603 if strings.EqualFold(rs.AcceptRejectsToMailbox, "inbox") {
1604 addRulesetErrorf("AcceptRejectsToMailbox cannot be set to Inbox")
1608 // Catchall destination for domain.
1609 if strings.HasPrefix(addrName, "@") {
1610 d, err := dns.ParseDomain(addrName[1:])
1612 addDestErrorf("parsing domain %q", addrName[1:])
1614 } else if _, ok := c.Domains[d.Name()]; !ok {
1615 addDestErrorf("unknown domain for address")
1618 domainHasAddress[d.Name()] = true
1619 addrFull := "@" + d.Name()
1620 if _, ok := accDests[addrFull]; ok {
1621 addDestErrorf("duplicate canonicalized catchall destination address %s", addrFull)
1623 accDests[addrFull] = AccountDestination{true, "", accName, dest}
1627 // todo deprecated: remove support for parsing destination as just a localpart instead full address.
1628 var address smtp.Address
1629 if localpart, err := smtp.ParseLocalpart(addrName); err != nil && errors.Is(err, smtp.ErrBadLocalpart) {
1630 address, err = smtp.ParseAddress(addrName)
1632 addDestErrorf("invalid email address")
1634 } else if _, ok := c.Domains[address.Domain.Name()]; !ok {
1635 addDestErrorf("unknown domain for address")
1640 addDestErrorf("invalid localpart %q", addrName)
1643 address = smtp.NewAddress(localpart, acc.DNSDomain)
1644 if _, ok := c.Domains[acc.DNSDomain.Name()]; !ok {
1645 addDestErrorf("unknown domain %s", acc.DNSDomain.Name())
1648 replaceLocalparts[addrName] = address.Pack(true)
1651 origLP := address.Localpart
1652 dc := c.Domains[address.Domain.Name()]
1653 domainHasAddress[address.Domain.Name()] = true
1654 lp := CanonicalLocalpart(address.Localpart, dc)
1655 if dc.LocalpartCatchallSeparator != "" && strings.Contains(string(address.Localpart), dc.LocalpartCatchallSeparator) {
1656 addDestErrorf("localpart of address %s includes domain catchall separator %s", address, dc.LocalpartCatchallSeparator)
1658 address.Localpart = lp
1660 addrFull := address.Pack(true)
1661 if _, ok := accDests[addrFull]; ok {
1662 addDestErrorf("duplicate canonicalized destination address %s", addrFull)
1664 accDests[addrFull] = AccountDestination{false, origLP, accName, dest}
1667 for lp, addr := range replaceLocalparts {
1668 dest, ok := acc.Destinations[lp]
1670 addAccountErrorf("could not find localpart %q to replace with address in destinations", lp)
1672 log.Warn(`deprecation warning: support for account destination addresses specified as just localpart ("username") instead of full email address will be removed in the future; update domains.conf, for each Account, for each Destination, ensure each key is an email address by appending "@" and the default domain for the account`,
1673 slog.Any("localpart", lp),
1674 slog.Any("address", addr),
1675 slog.String("account", accName))
1676 acc.Destinations[addr] = dest
1677 delete(acc.Destinations, lp)
1681 // Now that all addresses are parsed, check if all fromid login addresses match
1682 // configured addresses.
1683 for i, a := range acc.ParsedFromIDLoginAddresses {
1684 // For domain catchall.
1685 if _, ok := accDests["@"+a.Domain.Name()]; ok {
1688 dc := c.Domains[a.Domain.Name()]
1689 a.Localpart = CanonicalLocalpart(a.Localpart, dc)
1690 if _, ok := accDests[a.Pack(true)]; !ok {
1691 addAccountErrorf("fromid login address %q does not match its destination addresses", acc.FromIDLoginAddresses[i])
1695 checkRoutes("routes for account", acc.Routes)
1698 // Set DMARC destinations.
1699 for d, domain := range c.Domains {
1700 addDomainErrorf := func(format string, args ...any) {
1701 addErrorf("domain %s: %s", d, fmt.Sprintf(format, args...))
1704 dmarc := domain.DMARC
1708 if _, ok := c.Accounts[dmarc.Account]; !ok {
1709 addDomainErrorf("DMARC account %q does not exist", dmarc.Account)
1711 lp, err := smtp.ParseLocalpart(dmarc.Localpart)
1713 addDomainErrorf("invalid DMARC localpart %q: %s", dmarc.Localpart, err)
1715 if lp.IsInternational() {
1717 addDomainErrorf("DMARC localpart %q is an internationalized address, only conventional ascii-only address possible for interopability", lp)
1719 addrdom := domain.Domain
1720 if dmarc.Domain != "" {
1721 addrdom, err = dns.ParseDomain(dmarc.Domain)
1723 addDomainErrorf("DMARC domain %q: %s", dmarc.Domain, err)
1724 } else if _, ok := c.Domains[addrdom.Name()]; !ok {
1725 addDomainErrorf("unknown domain %q for DMARC address", addrdom)
1728 if addrdom == domain.Domain {
1729 domainHasAddress[addrdom.Name()] = true
1732 domain.DMARC.ParsedLocalpart = lp
1733 domain.DMARC.DNSDomain = addrdom
1734 c.Domains[d] = domain
1735 addrFull := smtp.NewAddress(lp, addrdom).String()
1736 dest := config.Destination{
1737 Mailbox: dmarc.Mailbox,
1740 checkMailboxNormf(dmarc.Mailbox, "DMARC mailbox for account", addDomainErrorf)
1741 accDests[addrFull] = AccountDestination{false, lp, dmarc.Account, dest}
1744 // Set TLSRPT destinations.
1745 for d, domain := range c.Domains {
1746 addDomainErrorf := func(format string, args ...any) {
1747 addErrorf("domain %s: %s", d, fmt.Sprintf(format, args...))
1750 tlsrpt := domain.TLSRPT
1754 if _, ok := c.Accounts[tlsrpt.Account]; !ok {
1755 addDomainErrorf("TLSRPT account %q does not exist", tlsrpt.Account)
1757 lp, err := smtp.ParseLocalpart(tlsrpt.Localpart)
1759 addDomainErrorf("invalid TLSRPT localpart %q: %s", tlsrpt.Localpart, err)
1761 if lp.IsInternational() {
1762 // Does not appear documented in
../rfc/8460, but similar to DMARC it makes sense
1763 // to keep this ascii-only addresses.
1764 addDomainErrorf("TLSRPT localpart %q is an internationalized address, only conventional ascii-only address allowed for interopability", lp)
1766 addrdom := domain.Domain
1767 if tlsrpt.Domain != "" {
1768 addrdom, err = dns.ParseDomain(tlsrpt.Domain)
1770 addDomainErrorf("TLSRPT domain %q: %s", tlsrpt.Domain, err)
1771 } else if _, ok := c.Domains[addrdom.Name()]; !ok {
1772 addDomainErrorf("unknown domain %q for TLSRPT address", tlsrpt.Domain)
1775 if addrdom == domain.Domain {
1776 domainHasAddress[addrdom.Name()] = true
1779 domain.TLSRPT.ParsedLocalpart = lp
1780 domain.TLSRPT.DNSDomain = addrdom
1781 c.Domains[d] = domain
1782 addrFull := smtp.NewAddress(lp, addrdom).String()
1783 dest := config.Destination{
1784 Mailbox: tlsrpt.Mailbox,
1785 DomainTLSReports: true,
1787 checkMailboxNormf(tlsrpt.Mailbox, "TLSRPT mailbox", addDomainErrorf)
1788 accDests[addrFull] = AccountDestination{false, lp, tlsrpt.Account, dest}
1791 // Set ReportsOnly for domains, based on whether we have seen addresses (possibly
1792 // from DMARC or TLS reporting).
1793 for d, domain := range c.Domains {
1794 domain.ReportsOnly = !domainHasAddress[domain.Domain.Name()]
1795 c.Domains[d] = domain
1798 // Aliases, per domain. Also add references to accounts.
1799 for d, domain := range c.Domains {
1800 for lpstr, a := range domain.Aliases {
1801 addAliasErrorf := func(format string, args ...any) {
1802 addErrorf("domain %s: alias %s: %s", d, lpstr, fmt.Sprintf(format, args...))
1806 a.LocalpartStr = lpstr
1807 var clp smtp.Localpart
1808 lp, err := smtp.ParseLocalpart(lpstr)
1810 addAliasErrorf("parsing alias: %v", err)
1812 } else if domain.LocalpartCatchallSeparator != "" && strings.Contains(string(lp), domain.LocalpartCatchallSeparator) {
1813 addAliasErrorf("alias contains localpart catchall separator")
1816 clp = CanonicalLocalpart(lp, domain)
1819 addr := smtp.NewAddress(clp, domain.Domain).Pack(true)
1820 if _, ok := aliases[addr]; ok {
1821 addAliasErrorf("duplicate alias address %q", addr)
1824 if _, ok := accDests[addr]; ok {
1825 addAliasErrorf("alias %q already present as regular address", addr)
1828 if len(a.Addresses) == 0 {
1829 // Not currently possible, Addresses isn't optional.
1830 addAliasErrorf("alias %q needs at least one destination address", addr)
1833 a.ParsedAddresses = make([]config.AliasAddress, 0, len(a.Addresses))
1834 seen := map[string]bool{}
1835 for _, destAddr := range a.Addresses {
1836 da, err := smtp.ParseAddress(destAddr)
1838 addAliasErrorf("parsing destination address %q: %v", destAddr, err)
1841 dastr := da.Pack(true)
1842 accDest, ok := accDests[dastr]
1844 addAliasErrorf("references non-existent address %q", destAddr)
1848 addAliasErrorf("duplicate address %q", destAddr)
1852 aa := config.AliasAddress{Address: da, AccountName: accDest.Account, Destination: accDest.Destination}
1853 a.ParsedAddresses = append(a.ParsedAddresses, aa)
1855 a.Domain = domain.Domain
1856 c.Domains[d].Aliases[lpstr] = a
1859 for _, aa := range a.ParsedAddresses {
1860 acc := c.Accounts[aa.AccountName]
1863 addrs = make([]string, len(a.ParsedAddresses))
1864 for i := range a.ParsedAddresses {
1865 addrs[i] = a.ParsedAddresses[i].Address.Pack(true)
1868 // Keep the non-sensitive fields.
1869 accAlias := config.Alias{
1870 PostPublic: a.PostPublic,
1871 ListMembers: a.ListMembers,
1872 AllowMsgFrom: a.AllowMsgFrom,
1873 LocalpartStr: a.LocalpartStr,
1876 acc.Aliases = append(acc.Aliases, config.AddressAlias{SubscriptionAddress: aa.Address.Pack(true), Alias: accAlias, MemberAddresses: addrs})
1877 c.Accounts[aa.AccountName] = acc
1882 // Check webserver configs.
1883 if (len(c.WebDomainRedirects) > 0 || len(c.WebHandlers) > 0) && !haveWebserverListener {
1884 addErrorf("WebDomainRedirects or WebHandlers configured but no listener with WebserverHTTP or WebserverHTTPS enabled")
1887 c.WebDNSDomainRedirects = map[dns.Domain]dns.Domain{}
1888 for from, to := range c.WebDomainRedirects {
1889 addRedirectErrorf := func(format string, args ...any) {
1890 addErrorf("web redirect %s to %s: %s", from, to, fmt.Sprintf(format, args...))
1893 fromdom, err := dns.ParseDomain(from)
1895 addRedirectErrorf("parsing domain for redirect %s: %v", from, err)
1897 todom, err := dns.ParseDomain(to)
1899 addRedirectErrorf("parsing domain for redirect %s: %v", to, err)
1900 } else if fromdom == todom {
1901 addRedirectErrorf("will not redirect domain %s to itself", todom)
1903 var zerodom dns.Domain
1904 if _, ok := c.WebDNSDomainRedirects[fromdom]; ok && fromdom != zerodom {
1905 addRedirectErrorf("duplicate redirect domain %s", from)
1907 c.WebDNSDomainRedirects[fromdom] = todom
1910 for i := range c.WebHandlers {
1911 wh := &c.WebHandlers[i]
1913 addHandlerErrorf := func(format string, args ...any) {
1914 addErrorf("webhandler %s %s: %s", wh.Domain, wh.PathRegexp, fmt.Sprintf(format, args...))
1917 if wh.LogName == "" {
1918 wh.Name = fmt.Sprintf("%d", i)
1920 wh.Name = wh.LogName
1923 dom, err := dns.ParseDomain(wh.Domain)
1925 addHandlerErrorf("parsing domain: %v", err)
1929 if !strings.HasPrefix(wh.PathRegexp, "^") {
1930 addHandlerErrorf("path regexp must start with a ^")
1932 re, err := regexp.Compile(wh.PathRegexp)
1934 addHandlerErrorf("compiling regexp: %v", err)
1939 if wh.WebStatic != nil {
1942 if ws.StripPrefix != "" && !strings.HasPrefix(ws.StripPrefix, "/") {
1943 addHandlerErrorf("static: prefix to strip %s must start with a slash", ws.StripPrefix)
1945 for k := range ws.ResponseHeaders {
1947 k := strings.TrimSpace(xk)
1948 if k != xk || k == "" {
1949 addHandlerErrorf("static: bad header %q", xk)
1953 if wh.WebRedirect != nil {
1955 wr := wh.WebRedirect
1956 if wr.BaseURL != "" {
1957 u, err := url.Parse(wr.BaseURL)
1959 addHandlerErrorf("redirect: parsing redirect url %s: %v", wr.BaseURL, err)
1965 addHandlerErrorf("redirect: BaseURL must have empty path", wr.BaseURL)
1969 if wr.OrigPathRegexp != "" && wr.ReplacePath != "" {
1970 re, err := regexp.Compile(wr.OrigPathRegexp)
1972 addHandlerErrorf("compiling regexp %s: %v", wr.OrigPathRegexp, err)
1975 } else if wr.OrigPathRegexp != "" || wr.ReplacePath != "" {
1976 addHandlerErrorf("redirect: must have either both OrigPathRegexp and ReplacePath, or neither")
1977 } else if wr.BaseURL == "" {
1978 addHandlerErrorf("must at least one of BaseURL and OrigPathRegexp+ReplacePath")
1980 if wr.StatusCode != 0 && (wr.StatusCode < 300 || wr.StatusCode >= 400) {
1981 addHandlerErrorf("redirect: invalid redirect status code %d", wr.StatusCode)
1984 if wh.WebForward != nil {
1987 u, err := url.Parse(wf.URL)
1989 addHandlerErrorf("forward: parsing url %s: %v", wf.URL, err)
1993 for k := range wf.ResponseHeaders {
1995 k := strings.TrimSpace(xk)
1996 if k != xk || k == "" {
1997 addHandlerErrorf("forrward: bad header %q", xk)
2001 if wh.WebInternal != nil {
2003 wi := wh.WebInternal
2004 if !strings.HasPrefix(wi.BasePath, "/") || !strings.HasSuffix(wi.BasePath, "/") {
2005 addHandlerErrorf("internal service: base path %q must start and end with /", wi.BasePath)
2007 // todo: we could make maxMsgSize and accountPath configurable
2008 const isForwarded = false
2011 wi.Handler = NewWebadminHandler(wi.BasePath, isForwarded)
2013 wi.Handler = NewWebaccountHandler(wi.BasePath, isForwarded)
2016 wi.Handler = NewWebmailHandler(config.DefaultMaxMsgSize, wi.BasePath, isForwarded, accountPath)
2018 wi.Handler = NewWebapiHandler(config.DefaultMaxMsgSize, wi.BasePath, isForwarded)
2020 addHandlerErrorf("internal service: unknown service %q", wi.Service)
2022 wi.Handler = SafeHeaders(http.StripPrefix(wi.BasePath[:len(wi.BasePath)-1], wi.Handler))
2025 addHandlerErrorf("must have exactly one handler, not %d", n)
2029 c.MonitorDNSBLZones = nil
2030 for _, s := range c.MonitorDNSBLs {
2031 d, err := dns.ParseDomain(s)
2033 addErrorf("dnsbl %s: parsing dnsbl zone: %v", s, err)
2036 if slices.Contains(c.MonitorDNSBLZones, d) {
2037 addErrorf("dnsbl %s: duplicate zone", s)
2040 c.MonitorDNSBLZones = append(c.MonitorDNSBLZones, d)
2046func loadPrivateKeyFile(keyPath string) (crypto.Signer, error) {
2047 keyBuf, err := os.ReadFile(keyPath)
2049 return nil, fmt.Errorf("reading host private key: %v", err)
2051 b, _ := pem.Decode(keyBuf)
2053 return nil, fmt.Errorf("parsing pem block for private key: %v", err)
2058 privKey, err = x509.ParsePKCS8PrivateKey(b.Bytes)
2059 case "RSA PRIVATE KEY":
2060 privKey, err = x509.ParsePKCS1PrivateKey(b.Bytes)
2061 case "EC PRIVATE KEY":
2062 privKey, err = x509.ParseECPrivateKey(b.Bytes)
2064 err = fmt.Errorf("unknown pem type %q", b.Type)
2067 return nil, fmt.Errorf("parsing private key: %v", err)
2069 if k, ok := privKey.(crypto.Signer); ok {
2072 return nil, fmt.Errorf("parsed private key not a crypto.Signer, but %T", privKey)
2075func loadTLSKeyCerts(configFile, kind string, ctls *config.TLS) error {
2076 certs := []tls.Certificate{}
2077 for _, kp := range ctls.KeyCerts {
2078 certPath := configDirPath(configFile, kp.CertFile)
2079 keyPath := configDirPath(configFile, kp.KeyFile)
2080 cert, err := loadX509KeyPairPrivileged(certPath, keyPath)
2082 return fmt.Errorf("tls config for %q: parsing x509 key pair: %v", kind, err)
2084 certs = append(certs, cert)
2086 ctls.Config = &tls.Config{
2087 Certificates: certs,
2089 ctls.ConfigFallback = ctls.Config
2093// load x509 key/cert files from file descriptor possibly passed in by privileged
2095func loadX509KeyPairPrivileged(certPath, keyPath string) (tls.Certificate, error) {
2096 certBuf, err := readFilePrivileged(certPath)
2098 return tls.Certificate{}, fmt.Errorf("reading tls certificate: %v", err)
2100 keyBuf, err := readFilePrivileged(keyPath)
2102 return tls.Certificate{}, fmt.Errorf("reading tls key: %v", err)
2104 return tls.X509KeyPair(certBuf, keyBuf)
2107// like os.ReadFile, but open privileged file possibly passed in by root process.
2108func readFilePrivileged(path string) ([]byte, error) {
2109 f, err := OpenPrivileged(path)
2114 return io.ReadAll(f)