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.
89 // From canonical full address (localpart@domain, lower-cased when
90 // case-insensitive, stripped of catchall separator) to account and address.
91 // Domains are IDNA names in utf8.
92 accountDestinations map[string]AccountDestination
93 // Like accountDestinations, but for aliases.
94 aliases map[string]config.Alias
97type AccountDestination struct {
98 Catchall bool // If catchall destination for its domain.
99 Localpart smtp.Localpart // In original casing as written in config file.
101 Destination config.Destination
104// LogLevelSet sets a new log level for pkg. An empty pkg sets the default log
105// value that is used if no explicit log level is configured for a package.
106// This change is ephemeral, no config file is changed.
107func (c *Config) LogLevelSet(log mlog.Log, pkg string, level slog.Level) {
109 defer c.logMutex.Unlock()
110 l := c.copyLogLevels()
113 log.Print("log level changed", slog.String("pkg", pkg), slog.Any("level", mlog.LevelStrings[level]))
114 mlog.SetConfig(c.Log)
117// LogLevelRemove removes a configured log level for a package.
118func (c *Config) LogLevelRemove(log mlog.Log, pkg string) {
120 defer c.logMutex.Unlock()
121 l := c.copyLogLevels()
124 log.Print("log level cleared", slog.String("pkg", pkg))
125 mlog.SetConfig(c.Log)
128// copyLogLevels returns a copy of c.Log, for modifications.
129// must be called with log lock held.
130func (c *Config) copyLogLevels() map[string]slog.Level {
131 m := map[string]slog.Level{}
132 for pkg, level := range c.Log {
138// LogLevels returns a copy of the current log levels.
139func (c *Config) LogLevels() map[string]slog.Level {
141 defer c.logMutex.Unlock()
142 return c.copyLogLevels()
145func (c *Config) withDynamicLock(fn func()) {
146 c.dynamicMutex.Lock()
147 defer c.dynamicMutex.Unlock()
149 if now.Sub(c.DynamicLastCheck) > time.Second {
150 c.DynamicLastCheck = now
151 if fi, err := os.Stat(ConfigDynamicPath); err != nil {
152 pkglog.Errorx("stat domains config", err)
153 } else if !fi.ModTime().Equal(c.dynamicMtime) {
154 if errs := c.loadDynamic(); len(errs) > 0 {
155 pkglog.Errorx("loading domains config", errs[0], slog.Any("errors", errs))
157 pkglog.Info("domains config reloaded")
158 c.dynamicMtime = fi.ModTime()
165// must be called with dynamic lock held.
166func (c *Config) loadDynamic() []error {
167 d, mtime, accDests, aliases, err := ParseDynamicConfig(context.Background(), pkglog, ConfigDynamicPath, c.Static)
172 c.dynamicMtime = mtime
173 c.accountDestinations = accDests
175 c.allowACMEHosts(pkglog, true)
179// DynamicConfig returns a shallow copy of the dynamic config. Must not be modified.
180func (c *Config) DynamicConfig() (config config.Dynamic) {
181 c.withDynamicLock(func() {
182 config = c.Dynamic // Shallow copy.
187func (c *Config) Domains() (l []string) {
188 c.withDynamicLock(func() {
189 for name := range c.Dynamic.Domains {
193 sort.Slice(l, func(i, j int) bool {
199func (c *Config) Accounts() (l []string) {
200 c.withDynamicLock(func() {
201 for name := range c.Dynamic.Accounts {
208// DomainLocalparts returns a mapping of encoded localparts to account names for a
209// domain, and encoded localparts to aliases. An empty localpart is a catchall
210// destination for a domain.
211func (c *Config) DomainLocalparts(d dns.Domain) (map[string]string, map[string]config.Alias) {
212 suffix := "@" + d.Name()
213 m := map[string]string{}
214 aliases := map[string]config.Alias{}
215 c.withDynamicLock(func() {
216 for addr, ad := range c.accountDestinations {
217 if strings.HasSuffix(addr, suffix) {
221 m[ad.Localpart.String()] = ad.Account
225 for addr, a := range c.aliases {
226 if strings.HasSuffix(addr, suffix) {
227 aliases[a.LocalpartStr] = a
234func (c *Config) Domain(d dns.Domain) (dom config.Domain, ok bool) {
235 c.withDynamicLock(func() {
236 dom, ok = c.Dynamic.Domains[d.Name()]
241func (c *Config) Account(name string) (acc config.Account, ok bool) {
242 c.withDynamicLock(func() {
243 acc, ok = c.Dynamic.Accounts[name]
248func (c *Config) AccountDestination(addr string) (accDest AccountDestination, alias *config.Alias, ok bool) {
249 c.withDynamicLock(func() {
250 accDest, ok = c.accountDestinations[addr]
253 a, ok = c.aliases[addr]
262func (c *Config) Routes(accountName string, domain dns.Domain) (accountRoutes, domainRoutes, globalRoutes []config.Route) {
263 c.withDynamicLock(func() {
264 acc := c.Dynamic.Accounts[accountName]
265 accountRoutes = acc.Routes
267 dom := c.Dynamic.Domains[domain.Name()]
268 domainRoutes = dom.Routes
270 globalRoutes = c.Dynamic.Routes
275func (c *Config) IsClientSettingsDomain(d dns.Domain) (is bool) {
276 c.withDynamicLock(func() {
277 _, is = c.Dynamic.ClientSettingDomains[d]
282func (c *Config) allowACMEHosts(log mlog.Log, checkACMEHosts bool) {
283 for _, l := range c.Static.Listeners {
284 if l.TLS == nil || l.TLS.ACME == "" {
288 m := c.Static.ACME[l.TLS.ACME].Manager
289 hostnames := map[dns.Domain]struct{}{}
291 hostnames[c.Static.HostnameDomain] = struct{}{}
292 if l.HostnameDomain.ASCII != "" {
293 hostnames[l.HostnameDomain] = struct{}{}
296 for _, dom := range c.Dynamic.Domains {
297 // Do not allow TLS certificates for domains for which we only accept DMARC/TLS
298 // reports as external party.
303 if l.AutoconfigHTTPS.Enabled && !l.AutoconfigHTTPS.NonTLS {
304 if d, err := dns.ParseDomain("autoconfig." + dom.Domain.ASCII); err != nil {
305 log.Errorx("parsing autoconfig domain", err, slog.Any("domain", dom.Domain))
307 hostnames[d] = struct{}{}
311 if l.MTASTSHTTPS.Enabled && dom.MTASTS != nil && !l.MTASTSHTTPS.NonTLS {
312 d, err := dns.ParseDomain("mta-sts." + dom.Domain.ASCII)
314 log.Errorx("parsing mta-sts domain", err, slog.Any("domain", dom.Domain))
316 hostnames[d] = struct{}{}
320 if dom.ClientSettingsDomain != "" {
321 hostnames[dom.ClientSettingsDNSDomain] = struct{}{}
325 if l.WebserverHTTPS.Enabled {
326 for from := range c.Dynamic.WebDNSDomainRedirects {
327 hostnames[from] = struct{}{}
329 for _, wh := range c.Dynamic.WebHandlers {
330 hostnames[wh.DNSDomain] = struct{}{}
334 public := c.Static.Listeners["public"]
336 if len(public.NATIPs) > 0 {
342 m.SetAllowedHostnames(log, dns.StrictResolver{Pkg: "autotls", Log: log.Logger}, hostnames, ips, checkACMEHosts)
346// 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.
348// must be called with lock held.
349// Returns ErrConfig if the configuration is not valid.
350func writeDynamic(ctx context.Context, log mlog.Log, c config.Dynamic) error {
351 accDests, aliases, errs := prepareDynamicConfig(ctx, log, ConfigDynamicPath, Conf.Static, &c)
353 errstrs := make([]string, len(errs))
354 for i, err := range errs {
355 errstrs[i] = err.Error()
357 return fmt.Errorf("%w: %s", ErrConfig, strings.Join(errstrs, "; "))
361 err := sconf.Write(&b, c)
365 f, err := os.OpenFile(ConfigDynamicPath, os.O_WRONLY, 0660)
372 log.Check(err, "closing file after error")
376 if _, err := f.Write(buf); err != nil {
377 return fmt.Errorf("write domains.conf: %v", err)
379 if err := f.Truncate(int64(len(buf))); err != nil {
380 return fmt.Errorf("truncate domains.conf after write: %v", err)
382 if err := f.Sync(); err != nil {
383 return fmt.Errorf("sync domains.conf after write: %v", err)
385 if err := moxio.SyncDir(log, filepath.Dir(ConfigDynamicPath)); err != nil {
386 return fmt.Errorf("sync dir of domains.conf after write: %v", err)
391 return fmt.Errorf("stat after writing domains.conf: %v", err)
394 if err := f.Close(); err != nil {
395 return fmt.Errorf("close written domains.conf: %v", err)
399 Conf.dynamicMtime = fi.ModTime()
400 Conf.DynamicLastCheck = time.Now()
402 Conf.accountDestinations = accDests
403 Conf.aliases = aliases
405 Conf.allowACMEHosts(log, true)
410// MustLoadConfig loads the config, quitting on errors.
411func MustLoadConfig(doLoadTLSKeyCerts, checkACMEHosts bool) {
412 errs := LoadConfig(context.Background(), pkglog, doLoadTLSKeyCerts, checkACMEHosts)
414 pkglog.Error("loading config file: multiple errors")
415 for _, err := range errs {
416 pkglog.Errorx("config error", err)
418 pkglog.Fatal("stopping after multiple config errors")
419 } else if len(errs) == 1 {
420 pkglog.Fatalx("loading config file", errs[0])
424// LoadConfig attempts to parse and load a config, returning any errors
426func LoadConfig(ctx context.Context, log mlog.Log, doLoadTLSKeyCerts, checkACMEHosts bool) []error {
427 Shutdown, ShutdownCancel = context.WithCancel(context.Background())
428 Context, ContextCancel = context.WithCancel(context.Background())
430 c, errs := ParseConfig(ctx, log, ConfigStaticPath, false, doLoadTLSKeyCerts, checkACMEHosts)
435 mlog.SetConfig(c.Log)
440// SetConfig sets a new config. Not to be used during normal operation.
441func SetConfig(c *Config) {
442 // Cannot just assign *c to Conf, it would copy the mutex.
443 Conf = Config{c.Static, sync.Mutex{}, c.Log, sync.Mutex{}, c.Dynamic, c.dynamicMtime, c.DynamicLastCheck, c.accountDestinations, c.aliases}
445 // If we have non-standard CA roots, use them for all HTTPS requests.
446 if Conf.Static.TLS.CertPool != nil {
447 http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{
448 RootCAs: Conf.Static.TLS.CertPool,
452 SetPedantic(c.Static.Pedantic)
455// Set pedantic in all packages.
456func SetPedantic(p bool) {
464// ParseConfig parses the static config at path p. If checkOnly is true, no changes
465// are made, such as registering ACME identities. If doLoadTLSKeyCerts is true,
466// the TLS KeyCerts configuration is loaded and checked. This is used during the
467// quickstart in the case the user is going to provide their own certificates.
468// If checkACMEHosts is true, the hosts allowed for acme are compared with the
469// explicitly configured ips we are listening on.
470func ParseConfig(ctx context.Context, log mlog.Log, p string, checkOnly, doLoadTLSKeyCerts, checkACMEHosts bool) (c *Config, errs []error) {
472 Static: config.Static{
479 if os.IsNotExist(err) && os.Getenv("MOXCONF") == "" {
480 return nil, []error{fmt.Errorf("open config file: %v (hint: use mox -config ... or set MOXCONF=...)", err)}
482 return nil, []error{fmt.Errorf("open config file: %v", err)}
485 if err := sconf.Parse(f, &c.Static); err != nil {
486 return nil, []error{fmt.Errorf("parsing %s%v", p, err)}
489 if xerrs := PrepareStaticConfig(ctx, log, p, c, checkOnly, doLoadTLSKeyCerts); len(xerrs) > 0 {
493 pp := filepath.Join(filepath.Dir(p), "domains.conf")
494 c.Dynamic, c.dynamicMtime, c.accountDestinations, c.aliases, errs = ParseDynamicConfig(ctx, log, pp, c.Static)
497 c.allowACMEHosts(log, checkACMEHosts)
503// PrepareStaticConfig parses the static config file and prepares data structures
504// for starting mox. If checkOnly is set no substantial changes are made, like
505// creating an ACME registration.
506func PrepareStaticConfig(ctx context.Context, log mlog.Log, configFile string, conf *Config, checkOnly, doLoadTLSKeyCerts bool) (errs []error) {
507 addErrorf := func(format string, args ...any) {
508 errs = append(errs, fmt.Errorf(format, args...))
513 // check that mailbox is in unicode NFC normalized form.
514 checkMailboxNormf := func(mailbox string, format string, args ...any) {
515 s := norm.NFC.String(mailbox)
517 msg := fmt.Sprintf(format, args...)
518 addErrorf("%s: mailbox %q is not in NFC normalized form, should be %q", msg, mailbox, s)
522 // Post-process logging config.
523 if logLevel, ok := mlog.Levels[c.LogLevel]; ok {
524 conf.Log = map[string]slog.Level{"": logLevel}
526 addErrorf("invalid log level %q", c.LogLevel)
528 for pkg, s := range c.PackageLogLevels {
529 if logLevel, ok := mlog.Levels[s]; ok {
530 conf.Log[pkg] = logLevel
532 addErrorf("invalid package log level %q", s)
539 u, err := user.Lookup(c.User)
541 uid, err := strconv.ParseUint(c.User, 10, 32)
543 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)
545 // We assume the same gid as uid.
550 if uid, err := strconv.ParseUint(u.Uid, 10, 32); err != nil {
551 addErrorf("parsing uid %s: %v", u.Uid, err)
555 if gid, err := strconv.ParseUint(u.Gid, 10, 32); err != nil {
556 addErrorf("parsing gid %s: %v", u.Gid, err)
562 hostname, err := dns.ParseDomain(c.Hostname)
564 addErrorf("parsing hostname: %s", err)
565 } else if hostname.Name() != c.Hostname {
566 addErrorf("hostname must be in unicode form %q instead of %q", hostname.Name(), c.Hostname)
568 c.HostnameDomain = hostname
570 if c.HostTLSRPT.Account != "" {
571 tlsrptLocalpart, err := smtp.ParseLocalpart(c.HostTLSRPT.Localpart)
573 addErrorf("invalid localpart %q for host tlsrpt: %v", c.HostTLSRPT.Localpart, err)
574 } else if tlsrptLocalpart.IsInternational() {
575 // Does not appear documented in
../rfc/8460, but similar to DMARC it makes sense
576 // to keep this ascii-only addresses.
577 addErrorf("host TLSRPT localpart %q is an internationalized address, only conventional ascii-only address allowed for interopability", tlsrptLocalpart)
579 c.HostTLSRPT.ParsedLocalpart = tlsrptLocalpart
582 // Return private key for host name for use with an ACME. Used to return the same
583 // private key as pre-generated for use with DANE, with its public key in DNS.
584 // We only use this key for Listener's that have this ACME configured, and for
585 // which the effective listener host name (either specific to the listener, or the
586 // global name) is requested. Other host names can get a fresh private key, they
587 // don't appear in DANE records.
589 // - run 0: only use listener with explicitly matching host name in listener
590 // (default quickstart config does not set it).
591 // - run 1: only look at public listener (and host matching mox host name)
592 // - run 2: all listeners (and host matching mox host name)
593 findACMEHostPrivateKey := func(acmeName, host string, keyType autocert.KeyType, run int) crypto.Signer {
594 for listenerName, l := range Conf.Static.Listeners {
595 if l.TLS == nil || l.TLS.ACME != acmeName {
598 if run == 0 && host != l.HostnameDomain.ASCII {
601 if run == 1 && listenerName != "public" || host != Conf.Static.HostnameDomain.ASCII {
605 case autocert.KeyRSA2048:
606 if len(l.TLS.HostPrivateRSA2048Keys) == 0 {
609 return l.TLS.HostPrivateRSA2048Keys[0]
610 case autocert.KeyECDSAP256:
611 if len(l.TLS.HostPrivateECDSAP256Keys) == 0 {
614 return l.TLS.HostPrivateECDSAP256Keys[0]
621 // Make a function for an autocert.Manager.GetPrivateKey, using findACMEHostPrivateKey.
622 makeGetPrivateKey := func(acmeName string) func(host string, keyType autocert.KeyType) (crypto.Signer, error) {
623 return func(host string, keyType autocert.KeyType) (crypto.Signer, error) {
624 key := findACMEHostPrivateKey(acmeName, host, keyType, 0)
626 key = findACMEHostPrivateKey(acmeName, host, keyType, 1)
629 key = findACMEHostPrivateKey(acmeName, host, keyType, 2)
632 log.Debug("found existing private key for certificate for host",
633 slog.String("acmename", acmeName),
634 slog.String("host", host),
635 slog.Any("keytype", keyType))
638 log.Debug("generating new private key for certificate for host",
639 slog.String("acmename", acmeName),
640 slog.String("host", host),
641 slog.Any("keytype", keyType))
643 case autocert.KeyRSA2048:
644 return rsa.GenerateKey(cryptorand.Reader, 2048)
645 case autocert.KeyECDSAP256:
646 return ecdsa.GenerateKey(elliptic.P256(), cryptorand.Reader)
648 return nil, fmt.Errorf("unrecognized requested key type %v", keyType)
652 for name, acme := range c.ACME {
655 if acme.ExternalAccountBinding != nil {
656 eabKeyID = acme.ExternalAccountBinding.KeyID
657 p := configDirPath(configFile, acme.ExternalAccountBinding.KeyFile)
658 buf, err := os.ReadFile(p)
660 addErrorf("reading external account binding key for acme provider %q: %s", name, err)
662 dec := make([]byte, base64.RawURLEncoding.DecodedLen(len(buf)))
663 n, err := base64.RawURLEncoding.Decode(dec, buf)
665 addErrorf("parsing external account binding key as base64 for acme provider %q: %s", name, err)
676 acmeDir := dataDirPath(configFile, c.DataDir, "acme")
677 os.MkdirAll(acmeDir, 0770)
678 manager, err := autotls.Load(name, acmeDir, acme.ContactEmail, acme.DirectoryURL, eabKeyID, eabKey, makeGetPrivateKey(name), Shutdown.Done())
680 addErrorf("loading ACME identity for %q: %s", name, err)
682 acme.Manager = manager
684 // Help configurations from older quickstarts.
685 if acme.IssuerDomainName == "" && acme.DirectoryURL == "https://acme-v02.api.letsencrypt.org/directory" {
686 acme.IssuerDomainName = "letsencrypt.org"
692 var haveUnspecifiedSMTPListener bool
693 for name, l := range c.Listeners {
694 if l.Hostname != "" {
695 d, err := dns.ParseDomain(l.Hostname)
697 addErrorf("bad listener hostname %q: %s", l.Hostname, err)
702 if l.TLS.ACME != "" && len(l.TLS.KeyCerts) != 0 {
703 addErrorf("listener %q: cannot have ACME and static key/certificates", name)
704 } else if l.TLS.ACME != "" {
705 acme, ok := c.ACME[l.TLS.ACME]
707 addErrorf("listener %q: unknown ACME provider %q", name, l.TLS.ACME)
710 // If only checking or with missing ACME definition, we don't have an acme manager,
711 // so set an empty tls config to continue.
712 var tlsconfig, tlsconfigFallback *tls.Config
713 if checkOnly || acme.Manager == nil {
714 tlsconfig = &tls.Config{}
715 tlsconfigFallback = &tls.Config{}
717 hostname := c.HostnameDomain
718 if l.Hostname != "" {
719 hostname = l.HostnameDomain
721 // If SNI is absent, we will use the listener hostname, but reject connections with
722 // an SNI hostname that is not allowlisted.
723 // Incoming SMTP deliveries use tlsconfigFallback for interoperability. TLS
724 // connections for unknown SNI hostnames fall back to a certificate for the
725 // listener hostname instead of causing the TLS connection to fail.
726 tlsconfig = acme.Manager.TLSConfig(hostname, true, false)
727 tlsconfigFallback = acme.Manager.TLSConfig(hostname, true, true)
728 l.TLS.ACMEConfig = acme.Manager.ACMETLSConfig
730 l.TLS.Config = tlsconfig
731 l.TLS.ConfigFallback = tlsconfigFallback
732 } else if len(l.TLS.KeyCerts) != 0 {
733 if doLoadTLSKeyCerts {
734 if err := loadTLSKeyCerts(configFile, "listener "+name, l.TLS); err != nil {
739 addErrorf("listener %q: cannot have TLS config without ACME and without static keys/certificates", name)
741 for _, privKeyFile := range l.TLS.HostPrivateKeyFiles {
742 keyPath := configDirPath(configFile, privKeyFile)
743 privKey, err := loadPrivateKeyFile(keyPath)
745 addErrorf("listener %q: parsing host private key for DANE and ACME certificates: %v", name, err)
748 switch k := privKey.(type) {
749 case *rsa.PrivateKey:
750 if k.N.BitLen() != 2048 {
751 log.Error("need rsa key with 2048 bits, for host private key for DANE/ACME certificates, ignoring",
752 slog.String("listener", name),
753 slog.String("file", keyPath),
754 slog.Int("bits", k.N.BitLen()))
757 l.TLS.HostPrivateRSA2048Keys = append(l.TLS.HostPrivateRSA2048Keys, k)
758 case *ecdsa.PrivateKey:
759 if k.Curve != elliptic.P256() {
760 log.Error("unrecognized ecdsa curve for host private key for DANE/ACME certificates, ignoring", slog.String("listener", name), slog.String("file", keyPath))
763 l.TLS.HostPrivateECDSAP256Keys = append(l.TLS.HostPrivateECDSAP256Keys, k)
765 log.Error("unrecognized key type for host private key for DANE/ACME certificates, ignoring",
766 slog.String("listener", name),
767 slog.String("file", keyPath),
768 slog.String("keytype", fmt.Sprintf("%T", privKey)))
772 if l.TLS.ACME != "" && (len(l.TLS.HostPrivateRSA2048Keys) == 0) != (len(l.TLS.HostPrivateECDSAP256Keys) == 0) {
773 log.Error("warning: 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")
777 var minVersion uint16 = tls.VersionTLS12
778 if l.TLS.MinVersion != "" {
779 versions := map[string]uint16{
780 "TLSv1.0": tls.VersionTLS10,
781 "TLSv1.1": tls.VersionTLS11,
782 "TLSv1.2": tls.VersionTLS12,
783 "TLSv1.3": tls.VersionTLS13,
785 v, ok := versions[l.TLS.MinVersion]
787 addErrorf("listener %q: unknown TLS mininum version %q", name, l.TLS.MinVersion)
791 if l.TLS.Config != nil {
792 l.TLS.Config.MinVersion = minVersion
794 if l.TLS.ConfigFallback != nil {
795 l.TLS.ConfigFallback.MinVersion = minVersion
797 if l.TLS.ACMEConfig != nil {
798 l.TLS.ACMEConfig.MinVersion = minVersion
801 var needsTLS []string
802 needtls := func(s string, v bool) {
804 needsTLS = append(needsTLS, s)
807 needtls("IMAPS", l.IMAPS.Enabled)
808 needtls("SMTP", l.SMTP.Enabled && !l.SMTP.NoSTARTTLS)
809 needtls("Submissions", l.Submissions.Enabled)
810 needtls("Submission", l.Submission.Enabled && !l.Submission.NoRequireSTARTTLS)
811 needtls("AccountHTTPS", l.AccountHTTPS.Enabled)
812 needtls("AdminHTTPS", l.AdminHTTPS.Enabled)
813 needtls("AutoconfigHTTPS", l.AutoconfigHTTPS.Enabled && !l.AutoconfigHTTPS.NonTLS)
814 needtls("MTASTSHTTPS", l.MTASTSHTTPS.Enabled && !l.MTASTSHTTPS.NonTLS)
815 needtls("WebserverHTTPS", l.WebserverHTTPS.Enabled)
816 if len(needsTLS) > 0 {
817 addErrorf("listener %q does not specify tls config, but requires tls for %s", name, strings.Join(needsTLS, ", "))
820 if l.AutoconfigHTTPS.Enabled && l.MTASTSHTTPS.Enabled && l.AutoconfigHTTPS.Port == l.MTASTSHTTPS.Port && l.AutoconfigHTTPS.NonTLS != l.MTASTSHTTPS.NonTLS {
821 addErrorf("listener %q tries to enable autoconfig and mta-sts enabled on same port but with both http and https", name)
825 haveUnspecifiedSMTPListener = true
827 for _, ipstr := range l.IPs {
828 ip := net.ParseIP(ipstr)
830 addErrorf("listener %q has invalid IP %q", name, ipstr)
833 if ip.IsUnspecified() {
834 haveUnspecifiedSMTPListener = true
837 if len(c.SpecifiedSMTPListenIPs) >= 2 {
838 haveUnspecifiedSMTPListener = true
839 } else if len(c.SpecifiedSMTPListenIPs) > 0 && (c.SpecifiedSMTPListenIPs[0].To4() == nil) == (ip.To4() == nil) {
840 haveUnspecifiedSMTPListener = true
842 c.SpecifiedSMTPListenIPs = append(c.SpecifiedSMTPListenIPs, ip)
846 for _, s := range l.SMTP.DNSBLs {
847 d, err := dns.ParseDomain(s)
849 addErrorf("listener %q has invalid DNSBL zone %q", name, s)
852 l.SMTP.DNSBLZones = append(l.SMTP.DNSBLZones, d)
854 if l.IPsNATed && len(l.NATIPs) > 0 {
855 addErrorf("listener %q has both IPsNATed and NATIPs (remove deprecated IPsNATed)", name)
857 for _, ipstr := range l.NATIPs {
858 ip := net.ParseIP(ipstr)
860 addErrorf("listener %q has invalid ip %q", name, ipstr)
861 } else if ip.IsUnspecified() || ip.IsLoopback() {
862 addErrorf("listener %q has NAT ip that is the unspecified or loopback address %s", name, ipstr)
865 checkPath := func(kind string, enabled bool, path string) {
866 if enabled && path != "" && !strings.HasPrefix(path, "/") {
867 addErrorf("listener %q has %s with path %q that must start with a slash", name, kind, path)
870 checkPath("AccountHTTP", l.AccountHTTP.Enabled, l.AccountHTTP.Path)
871 checkPath("AccountHTTPS", l.AccountHTTPS.Enabled, l.AccountHTTPS.Path)
872 checkPath("AdminHTTP", l.AdminHTTP.Enabled, l.AdminHTTP.Path)
873 checkPath("AdminHTTPS", l.AdminHTTPS.Enabled, l.AdminHTTPS.Path)
874 c.Listeners[name] = l
876 if haveUnspecifiedSMTPListener {
877 c.SpecifiedSMTPListenIPs = nil
880 var zerouse config.SpecialUseMailboxes
881 if len(c.DefaultMailboxes) > 0 && (c.InitialMailboxes.SpecialUse != zerouse || len(c.InitialMailboxes.Regular) > 0) {
882 addErrorf("cannot have both DefaultMailboxes and InitialMailboxes")
884 // DefaultMailboxes is deprecated.
885 for _, mb := range c.DefaultMailboxes {
886 checkMailboxNormf(mb, "default mailbox")
888 checkSpecialUseMailbox := func(nameOpt string) {
890 checkMailboxNormf(nameOpt, "special-use initial mailbox")
891 if strings.EqualFold(nameOpt, "inbox") {
892 addErrorf("initial mailbox cannot be set to Inbox (Inbox is always created)")
896 checkSpecialUseMailbox(c.InitialMailboxes.SpecialUse.Archive)
897 checkSpecialUseMailbox(c.InitialMailboxes.SpecialUse.Draft)
898 checkSpecialUseMailbox(c.InitialMailboxes.SpecialUse.Junk)
899 checkSpecialUseMailbox(c.InitialMailboxes.SpecialUse.Sent)
900 checkSpecialUseMailbox(c.InitialMailboxes.SpecialUse.Trash)
901 for _, name := range c.InitialMailboxes.Regular {
902 checkMailboxNormf(name, "regular initial mailbox")
903 if strings.EqualFold(name, "inbox") {
904 addErrorf("initial regular mailbox cannot be set to Inbox (Inbox is always created)")
908 checkTransportSMTP := func(name string, isTLS bool, t *config.TransportSMTP) {
910 t.DNSHost, err = dns.ParseDomain(t.Host)
912 addErrorf("transport %s: bad host %s: %v", name, t.Host, err)
915 if isTLS && t.STARTTLSInsecureSkipVerify {
916 addErrorf("transport %s: cannot have STARTTLSInsecureSkipVerify with immediate TLS")
918 if isTLS && t.NoSTARTTLS {
919 addErrorf("transport %s: cannot have NoSTARTTLS with immediate TLS")
925 seen := map[string]bool{}
926 for _, m := range t.Auth.Mechanisms {
928 addErrorf("transport %s: duplicate authentication mechanism %s", name, m)
932 case "SCRAM-SHA-256-PLUS":
933 case "SCRAM-SHA-256":
934 case "SCRAM-SHA-1-PLUS":
939 addErrorf("transport %s: unknown authentication mechanism %s", name, m)
943 t.Auth.EffectiveMechanisms = t.Auth.Mechanisms
944 if len(t.Auth.EffectiveMechanisms) == 0 {
945 t.Auth.EffectiveMechanisms = []string{"SCRAM-SHA-256-PLUS", "SCRAM-SHA-256", "SCRAM-SHA-1-PLUS", "SCRAM-SHA-1", "CRAM-MD5"}
949 checkTransportSocks := func(name string, t *config.TransportSocks) {
950 _, _, err := net.SplitHostPort(t.Address)
952 addErrorf("transport %s: bad address %s: %v", name, t.Address, err)
954 for _, ipstr := range t.RemoteIPs {
955 ip := net.ParseIP(ipstr)
957 addErrorf("transport %s: bad ip %s", name, ipstr)
959 t.IPs = append(t.IPs, ip)
962 t.Hostname, err = dns.ParseDomain(t.RemoteHostname)
964 addErrorf("transport %s: bad hostname %s: %v", name, t.RemoteHostname, err)
968 checkTransportDirect := func(name string, t *config.TransportDirect) {
969 if t.DisableIPv4 && t.DisableIPv6 {
970 addErrorf("transport %s: both IPv4 and IPv6 are disabled, enable at least one", name)
981 for name, t := range c.Transports {
983 if t.Submissions != nil {
985 checkTransportSMTP(name, true, t.Submissions)
987 if t.Submission != nil {
989 checkTransportSMTP(name, false, t.Submission)
993 checkTransportSMTP(name, false, t.SMTP)
997 checkTransportSocks(name, t.Socks)
1001 checkTransportDirect(name, t.Direct)
1004 addErrorf("transport %s: cannot have multiple methods in a transport", name)
1008 // Load CA certificate pool.
1009 if c.TLS.CA != nil {
1010 if c.TLS.CA.AdditionalToSystem {
1012 c.TLS.CertPool, err = x509.SystemCertPool()
1014 addErrorf("fetching system CA cert pool: %v", err)
1017 c.TLS.CertPool = x509.NewCertPool()
1019 for _, certfile := range c.TLS.CA.CertFiles {
1020 p := configDirPath(configFile, certfile)
1021 pemBuf, err := os.ReadFile(p)
1023 addErrorf("reading TLS CA cert file: %v", err)
1025 } else if !c.TLS.CertPool.AppendCertsFromPEM(pemBuf) {
1026 // todo: can we check more fully if we're getting some useful data back?
1027 addErrorf("no CA certs added from %q", p)
1034// PrepareDynamicConfig parses the dynamic config file given a static file.
1035func 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) {
1036 addErrorf := func(format string, args ...any) {
1037 errs = append(errs, fmt.Errorf(format, args...))
1040 f, err := os.Open(dynamicPath)
1042 addErrorf("parsing domains config: %v", err)
1048 addErrorf("stat domains config: %v", err)
1050 if err := sconf.Parse(f, &c); err != nil {
1051 addErrorf("parsing dynamic config file: %v", err)
1055 accDests, aliases, errs = prepareDynamicConfig(ctx, log, dynamicPath, static, &c)
1056 return c, fi.ModTime(), accDests, aliases, errs
1059func 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) {
1060 addErrorf := func(format string, args ...any) {
1061 errs = append(errs, fmt.Errorf(format, args...))
1064 // Check that mailbox is in unicode NFC normalized form.
1065 checkMailboxNormf := func(mailbox string, format string, args ...any) {
1066 s := norm.NFC.String(mailbox)
1068 msg := fmt.Sprintf(format, args...)
1069 addErrorf("%s: mailbox %q is not in NFC normalized form, should be %q", msg, mailbox, s)
1073 // Validate postmaster account exists.
1074 if _, ok := c.Accounts[static.Postmaster.Account]; !ok {
1075 addErrorf("postmaster account %q does not exist", static.Postmaster.Account)
1077 checkMailboxNormf(static.Postmaster.Mailbox, "postmaster mailbox")
1079 accDests = map[string]AccountDestination{}
1080 aliases = map[string]config.Alias{}
1082 // Validate host TLSRPT account/address.
1083 if static.HostTLSRPT.Account != "" {
1084 if _, ok := c.Accounts[static.HostTLSRPT.Account]; !ok {
1085 addErrorf("host tlsrpt account %q does not exist", static.HostTLSRPT.Account)
1087 checkMailboxNormf(static.HostTLSRPT.Mailbox, "host tlsrpt mailbox")
1089 // Localpart has been parsed already.
1091 addrFull := smtp.NewAddress(static.HostTLSRPT.ParsedLocalpart, static.HostnameDomain).String()
1092 dest := config.Destination{
1093 Mailbox: static.HostTLSRPT.Mailbox,
1094 HostTLSReports: true,
1096 accDests[addrFull] = AccountDestination{false, static.HostTLSRPT.ParsedLocalpart, static.HostTLSRPT.Account, dest}
1099 var haveSTSListener, haveWebserverListener bool
1100 for _, l := range static.Listeners {
1101 if l.MTASTSHTTPS.Enabled {
1102 haveSTSListener = true
1104 if l.WebserverHTTP.Enabled || l.WebserverHTTPS.Enabled {
1105 haveWebserverListener = true
1109 checkRoutes := func(descr string, routes []config.Route) {
1110 parseRouteDomains := func(l []string) []string {
1112 for _, e := range l {
1118 if strings.HasPrefix(e, ".") {
1122 d, err := dns.ParseDomain(e)
1124 addErrorf("%s: invalid domain %s: %v", descr, e, err)
1126 r = append(r, prefix+d.ASCII)
1131 for i := range routes {
1132 routes[i].FromDomainASCII = parseRouteDomains(routes[i].FromDomain)
1133 routes[i].ToDomainASCII = parseRouteDomains(routes[i].ToDomain)
1135 routes[i].ResolvedTransport, ok = static.Transports[routes[i].Transport]
1137 addErrorf("%s: route references undefined transport %s", descr, routes[i].Transport)
1142 checkRoutes("global routes", c.Routes)
1144 // Validate domains.
1145 c.ClientSettingDomains = map[dns.Domain]struct{}{}
1146 for d, domain := range c.Domains {
1147 dnsdomain, err := dns.ParseDomain(d)
1149 addErrorf("bad domain %q: %s", d, err)
1150 } else if dnsdomain.Name() != d {
1151 addErrorf("domain %s must be specified in unicode form, %s", d, dnsdomain.Name())
1154 domain.Domain = dnsdomain
1156 if domain.ClientSettingsDomain != "" {
1157 csd, err := dns.ParseDomain(domain.ClientSettingsDomain)
1159 addErrorf("bad client settings domain %q: %s", domain.ClientSettingsDomain, err)
1161 domain.ClientSettingsDNSDomain = csd
1162 c.ClientSettingDomains[csd] = struct{}{}
1165 for _, sign := range domain.DKIM.Sign {
1166 if _, ok := domain.DKIM.Selectors[sign]; !ok {
1167 addErrorf("selector %s for signing is missing in domain %s", sign, d)
1170 for name, sel := range domain.DKIM.Selectors {
1171 seld, err := dns.ParseDomain(name)
1173 addErrorf("bad selector %q: %s", name, err)
1174 } else if seld.Name() != name {
1175 addErrorf("selector %q must be specified in unicode form, %q", name, seld.Name())
1179 if sel.Expiration != "" {
1180 exp, err := time.ParseDuration(sel.Expiration)
1182 addErrorf("selector %q has invalid expiration %q: %v", name, sel.Expiration, err)
1184 sel.ExpirationSeconds = int(exp / time.Second)
1188 sel.HashEffective = sel.Hash
1189 switch sel.HashEffective {
1191 sel.HashEffective = "sha256"
1193 log.Error("using sha1 with DKIM is deprecated as not secure enough, switch to sha256")
1196 addErrorf("unsupported hash %q for selector %q in domain %s", sel.HashEffective, name, d)
1199 pemBuf, err := os.ReadFile(configDirPath(dynamicPath, sel.PrivateKeyFile))
1201 addErrorf("reading private key for selector %s in domain %s: %s", name, d, err)
1204 p, _ := pem.Decode(pemBuf)
1206 addErrorf("private key for selector %s in domain %s has no PEM block", name, d)
1209 key, err := x509.ParsePKCS8PrivateKey(p.Bytes)
1211 addErrorf("parsing private key for selector %s in domain %s: %s", name, d, err)
1214 switch k := key.(type) {
1215 case *rsa.PrivateKey:
1216 if k.N.BitLen() < 1024 {
1218 // Let's help user do the right thing.
1219 addErrorf("rsa keys should be >= 1024 bits")
1222 sel.Algorithm = fmt.Sprintf("rsa-%d", k.N.BitLen())
1223 case ed25519.PrivateKey:
1224 if sel.HashEffective != "sha256" {
1225 addErrorf("hash algorithm %q is not supported with ed25519, only sha256 is", sel.HashEffective)
1228 sel.Algorithm = "ed25519"
1230 addErrorf("private key type %T not yet supported, at selector %s in domain %s", key, name, d)
1233 if len(sel.Headers) == 0 {
1237 // By default we seal signed headers, and we sign user-visible headers to
1238 // prevent/limit reuse of previously signed messages: All addressing fields, date
1239 // and subject, message-referencing fields, parsing instructions (content-type).
1240 sel.HeadersEffective = strings.Split("From,To,Cc,Bcc,Reply-To,References,In-Reply-To,Subject,Date,Message-Id,Content-Type", ",")
1243 for _, h := range sel.Headers {
1244 from = from || strings.EqualFold(h, "From")
1246 if strings.EqualFold(h, "DKIM-Signature") || strings.EqualFold(h, "Received") || strings.EqualFold(h, "Return-Path") {
1247 log.Error("DKIM-signing header %q is recommended against as it may be modified in transit")
1251 addErrorf("From-field must always be DKIM-signed")
1253 sel.HeadersEffective = sel.Headers
1256 domain.DKIM.Selectors[name] = sel
1259 if domain.MTASTS != nil {
1260 if !haveSTSListener {
1261 addErrorf("MTA-STS enabled for domain %q, but there is no listener for MTASTS", d)
1263 sts := domain.MTASTS
1264 if sts.PolicyID == "" {
1265 addErrorf("invalid empty MTA-STS PolicyID")
1268 case mtasts.ModeNone, mtasts.ModeTesting, mtasts.ModeEnforce:
1270 addErrorf("invalid mtasts mode %q", sts.Mode)
1274 checkRoutes("routes for domain", domain.Routes)
1276 c.Domains[d] = domain
1279 // To determine ReportsOnly.
1280 domainHasAddress := map[string]bool{}
1282 // Validate email addresses.
1283 for accName, acc := range c.Accounts {
1285 acc.DNSDomain, err = dns.ParseDomain(acc.Domain)
1287 addErrorf("parsing domain %s for account %q: %s", acc.Domain, accName, err)
1290 if strings.EqualFold(acc.RejectsMailbox, "Inbox") {
1291 addErrorf("account %q: cannot set RejectsMailbox to inbox, messages will be removed automatically from the rejects mailbox", accName)
1293 checkMailboxNormf(acc.RejectsMailbox, "account %q", accName)
1295 if acc.AutomaticJunkFlags.JunkMailboxRegexp != "" {
1296 r, err := regexp.Compile(acc.AutomaticJunkFlags.JunkMailboxRegexp)
1298 addErrorf("invalid JunkMailboxRegexp regular expression: %v", err)
1302 if acc.AutomaticJunkFlags.NeutralMailboxRegexp != "" {
1303 r, err := regexp.Compile(acc.AutomaticJunkFlags.NeutralMailboxRegexp)
1305 addErrorf("invalid NeutralMailboxRegexp regular expression: %v", err)
1307 acc.NeutralMailbox = r
1309 if acc.AutomaticJunkFlags.NotJunkMailboxRegexp != "" {
1310 r, err := regexp.Compile(acc.AutomaticJunkFlags.NotJunkMailboxRegexp)
1312 addErrorf("invalid NotJunkMailboxRegexp regular expression: %v", err)
1314 acc.NotJunkMailbox = r
1317 if acc.JunkFilter != nil {
1318 params := acc.JunkFilter.Params
1319 if params.MaxPower < 0 || params.MaxPower > 0.5 {
1320 addErrorf("junk filter MaxPower must be >= 0 and < 0.5")
1322 if params.TopWords < 0 {
1323 addErrorf("junk filter TopWords must be >= 0")
1325 if params.IgnoreWords < 0 || params.IgnoreWords > 0.5 {
1326 addErrorf("junk filter IgnoreWords must be >= 0 and < 0.5")
1328 if params.RareWords < 0 {
1329 addErrorf("junk filter RareWords must be >= 0")
1333 acc.ParsedFromIDLoginAddresses = make([]smtp.Address, len(acc.FromIDLoginAddresses))
1334 for i, s := range acc.FromIDLoginAddresses {
1335 a, err := smtp.ParseAddress(s)
1337 addErrorf("invalid fromid login address %q in account %q: %v", s, accName, err)
1339 // We check later on if address belongs to account.
1340 dom, ok := c.Domains[a.Domain.Name()]
1342 addErrorf("unknown domain in fromid login address %q for account %q", s, accName)
1343 } else if dom.LocalpartCatchallSeparator == "" {
1344 addErrorf("localpart catchall separator not configured for domain for fromid login address %q for account %q", s, accName)
1346 acc.ParsedFromIDLoginAddresses[i] = a
1349 // Clear any previously derived state.
1352 c.Accounts[accName] = acc
1354 if acc.OutgoingWebhook != nil {
1355 u, err := url.Parse(acc.OutgoingWebhook.URL)
1356 if err == nil && (u.Scheme != "http" && u.Scheme != "https") {
1357 err = errors.New("scheme must be http or https")
1360 addErrorf("parsing outgoing hook url %q in account %q: %v", acc.OutgoingWebhook.URL, accName, err)
1363 // note: outgoing hook events are in ../queue/hooks.go, ../mox-/config.go, ../queue.go and ../webapi/gendoc.sh. keep in sync.
1364 outgoingHookEvents := []string{"delivered", "suppressed", "delayed", "failed", "relayed", "expanded", "canceled", "unrecognized"}
1365 for _, e := range acc.OutgoingWebhook.Events {
1366 if !slices.Contains(outgoingHookEvents, e) {
1367 addErrorf("unknown outgoing hook event %q", e)
1371 if acc.IncomingWebhook != nil {
1372 u, err := url.Parse(acc.IncomingWebhook.URL)
1373 if err == nil && (u.Scheme != "http" && u.Scheme != "https") {
1374 err = errors.New("scheme must be http or https")
1377 addErrorf("parsing incoming hook url %q in account %q: %v", acc.IncomingWebhook.URL, accName, err)
1381 // 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.
1382 replaceLocalparts := map[string]string{}
1384 for addrName, dest := range acc.Destinations {
1385 checkMailboxNormf(dest.Mailbox, "account %q, destination %q", accName, addrName)
1387 for i, rs := range dest.Rulesets {
1388 checkMailboxNormf(rs.Mailbox, "account %q, destination %q, ruleset %d", accName, addrName, i+1)
1392 if rs.SMTPMailFromRegexp != "" {
1394 r, err := regexp.Compile(rs.SMTPMailFromRegexp)
1396 addErrorf("invalid SMTPMailFrom regular expression: %v", err)
1398 c.Accounts[accName].Destinations[addrName].Rulesets[i].SMTPMailFromRegexpCompiled = r
1400 if rs.MsgFromRegexp != "" {
1402 r, err := regexp.Compile(rs.MsgFromRegexp)
1404 addErrorf("invalid MsgFrom regular expression: %v", err)
1406 c.Accounts[accName].Destinations[addrName].Rulesets[i].MsgFromRegexpCompiled = r
1408 if rs.VerifiedDomain != "" {
1410 d, err := dns.ParseDomain(rs.VerifiedDomain)
1412 addErrorf("invalid VerifiedDomain: %v", err)
1414 c.Accounts[accName].Destinations[addrName].Rulesets[i].VerifiedDNSDomain = d
1417 var hdr [][2]*regexp.Regexp
1418 for k, v := range rs.HeadersRegexp {
1420 if strings.ToLower(k) != k {
1421 addErrorf("header field %q must only have lower case characters", k)
1423 if strings.ToLower(v) != v {
1424 addErrorf("header value %q must only have lower case characters", v)
1426 rk, err := regexp.Compile(k)
1428 addErrorf("invalid rule header regexp %q: %v", k, err)
1430 rv, err := regexp.Compile(v)
1432 addErrorf("invalid rule header regexp %q: %v", v, err)
1434 hdr = append(hdr, [...]*regexp.Regexp{rk, rv})
1436 c.Accounts[accName].Destinations[addrName].Rulesets[i].HeadersRegexpCompiled = hdr
1439 addErrorf("ruleset must have at least one rule")
1442 if rs.IsForward && rs.ListAllowDomain != "" {
1443 addErrorf("ruleset cannot have both IsForward and ListAllowDomain")
1446 if rs.SMTPMailFromRegexp == "" || rs.VerifiedDomain == "" {
1447 addErrorf("ruleset with IsForward must have both SMTPMailFromRegexp and VerifiedDomain too")
1450 if rs.ListAllowDomain != "" {
1451 d, err := dns.ParseDomain(rs.ListAllowDomain)
1453 addErrorf("invalid ListAllowDomain %q: %v", rs.ListAllowDomain, err)
1455 c.Accounts[accName].Destinations[addrName].Rulesets[i].ListAllowDNSDomain = d
1458 checkMailboxNormf(rs.AcceptRejectsToMailbox, "account %q, destination %q, ruleset %d, rejects mailbox", accName, addrName, i+1)
1459 if strings.EqualFold(rs.AcceptRejectsToMailbox, "inbox") {
1460 addErrorf("account %q, destination %q, ruleset %d: AcceptRejectsToMailbox cannot be set to Inbox", accName, addrName, i+1)
1464 // Catchall destination for domain.
1465 if strings.HasPrefix(addrName, "@") {
1466 d, err := dns.ParseDomain(addrName[1:])
1468 addErrorf("parsing domain %q in account %q", addrName[1:], accName)
1470 } else if _, ok := c.Domains[d.Name()]; !ok {
1471 addErrorf("unknown domain for address %q in account %q", addrName, accName)
1474 domainHasAddress[d.Name()] = true
1475 addrFull := "@" + d.Name()
1476 if _, ok := accDests[addrFull]; ok {
1477 addErrorf("duplicate canonicalized catchall destination address %s", addrFull)
1479 accDests[addrFull] = AccountDestination{true, "", accName, dest}
1483 // todo deprecated: remove support for parsing destination as just a localpart instead full address.
1484 var address smtp.Address
1485 if localpart, err := smtp.ParseLocalpart(addrName); err != nil && errors.Is(err, smtp.ErrBadLocalpart) {
1486 address, err = smtp.ParseAddress(addrName)
1488 addErrorf("invalid email address %q in account %q", addrName, accName)
1490 } else if _, ok := c.Domains[address.Domain.Name()]; !ok {
1491 addErrorf("unknown domain for address %q in account %q", addrName, accName)
1496 addErrorf("invalid localpart %q in account %q", addrName, accName)
1499 address = smtp.NewAddress(localpart, acc.DNSDomain)
1500 if _, ok := c.Domains[acc.DNSDomain.Name()]; !ok {
1501 addErrorf("unknown domain %s for account %q", acc.DNSDomain.Name(), accName)
1504 replaceLocalparts[addrName] = address.Pack(true)
1507 origLP := address.Localpart
1508 dc := c.Domains[address.Domain.Name()]
1509 domainHasAddress[address.Domain.Name()] = true
1510 lp := CanonicalLocalpart(address.Localpart, dc)
1511 if dc.LocalpartCatchallSeparator != "" && strings.Contains(string(address.Localpart), dc.LocalpartCatchallSeparator) {
1512 addErrorf("localpart of address %s includes domain catchall separator %s", address, dc.LocalpartCatchallSeparator)
1514 address.Localpart = lp
1516 addrFull := address.Pack(true)
1517 if _, ok := accDests[addrFull]; ok {
1518 addErrorf("duplicate canonicalized destination address %s", addrFull)
1520 accDests[addrFull] = AccountDestination{false, origLP, accName, dest}
1523 for lp, addr := range replaceLocalparts {
1524 dest, ok := acc.Destinations[lp]
1526 addErrorf("could not find localpart %q to replace with address in destinations", lp)
1528 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`,
1529 slog.Any("localpart", lp),
1530 slog.Any("address", addr),
1531 slog.String("account", accName))
1532 acc.Destinations[addr] = dest
1533 delete(acc.Destinations, lp)
1537 // Now that all addresses are parsed, check if all fromid login addresses match
1538 // configured addresses.
1539 for i, a := range acc.ParsedFromIDLoginAddresses {
1540 // For domain catchall.
1541 if _, ok := accDests["@"+a.Domain.Name()]; ok {
1544 dc := c.Domains[a.Domain.Name()]
1545 a.Localpart = CanonicalLocalpart(a.Localpart, dc)
1546 if _, ok := accDests[a.Pack(true)]; !ok {
1547 addErrorf("fromid login address %q for account %q does not match its destination addresses", acc.FromIDLoginAddresses[i], accName)
1551 checkRoutes("routes for account", acc.Routes)
1554 // Set DMARC destinations.
1555 for d, domain := range c.Domains {
1556 dmarc := domain.DMARC
1560 if _, ok := c.Accounts[dmarc.Account]; !ok {
1561 addErrorf("DMARC account %q does not exist", dmarc.Account)
1563 lp, err := smtp.ParseLocalpart(dmarc.Localpart)
1565 addErrorf("invalid DMARC localpart %q: %s", dmarc.Localpart, err)
1567 if lp.IsInternational() {
1569 addErrorf("DMARC localpart %q is an internationalized address, only conventional ascii-only address possible for interopability", lp)
1571 addrdom := domain.Domain
1572 if dmarc.Domain != "" {
1573 addrdom, err = dns.ParseDomain(dmarc.Domain)
1575 addErrorf("DMARC domain %q: %s", dmarc.Domain, err)
1576 } else if _, ok := c.Domains[addrdom.Name()]; !ok {
1577 addErrorf("unknown domain %q for DMARC address in domain %q", addrdom, d)
1580 if addrdom == domain.Domain {
1581 domainHasAddress[addrdom.Name()] = true
1584 domain.DMARC.ParsedLocalpart = lp
1585 domain.DMARC.DNSDomain = addrdom
1586 c.Domains[d] = domain
1587 addrFull := smtp.NewAddress(lp, addrdom).String()
1588 dest := config.Destination{
1589 Mailbox: dmarc.Mailbox,
1592 checkMailboxNormf(dmarc.Mailbox, "DMARC mailbox for account %q", dmarc.Account)
1593 accDests[addrFull] = AccountDestination{false, lp, dmarc.Account, dest}
1596 // Set TLSRPT destinations.
1597 for d, domain := range c.Domains {
1598 tlsrpt := domain.TLSRPT
1602 if _, ok := c.Accounts[tlsrpt.Account]; !ok {
1603 addErrorf("TLSRPT account %q does not exist", tlsrpt.Account)
1605 lp, err := smtp.ParseLocalpart(tlsrpt.Localpart)
1607 addErrorf("invalid TLSRPT localpart %q: %s", tlsrpt.Localpart, err)
1609 if lp.IsInternational() {
1610 // Does not appear documented in
../rfc/8460, but similar to DMARC it makes sense
1611 // to keep this ascii-only addresses.
1612 addErrorf("TLSRPT localpart %q is an internationalized address, only conventional ascii-only address allowed for interopability", lp)
1614 addrdom := domain.Domain
1615 if tlsrpt.Domain != "" {
1616 addrdom, err = dns.ParseDomain(tlsrpt.Domain)
1618 addErrorf("TLSRPT domain %q: %s", tlsrpt.Domain, err)
1619 } else if _, ok := c.Domains[addrdom.Name()]; !ok {
1620 addErrorf("unknown domain %q for TLSRPT address in domain %q", tlsrpt.Domain, d)
1623 if addrdom == domain.Domain {
1624 domainHasAddress[addrdom.Name()] = true
1627 domain.TLSRPT.ParsedLocalpart = lp
1628 domain.TLSRPT.DNSDomain = addrdom
1629 c.Domains[d] = domain
1630 addrFull := smtp.NewAddress(lp, addrdom).String()
1631 dest := config.Destination{
1632 Mailbox: tlsrpt.Mailbox,
1633 DomainTLSReports: true,
1635 checkMailboxNormf(tlsrpt.Mailbox, "TLSRPT mailbox for account %q", tlsrpt.Account)
1636 accDests[addrFull] = AccountDestination{false, lp, tlsrpt.Account, dest}
1639 // Set ReportsOnly for domains, based on whether we have seen addresses (possibly
1640 // from DMARC or TLS reporting).
1641 for d, domain := range c.Domains {
1642 domain.ReportsOnly = !domainHasAddress[domain.Domain.Name()]
1643 c.Domains[d] = domain
1646 // Aliases, per domain. Also add references to accounts.
1647 for d, domain := range c.Domains {
1648 for lpstr, a := range domain.Aliases {
1650 a.LocalpartStr = lpstr
1651 var clp smtp.Localpart
1652 lp, err := smtp.ParseLocalpart(lpstr)
1654 addErrorf("domain %q: parsing localpart %q for alias: %v", d, lpstr, err)
1656 } else if domain.LocalpartCatchallSeparator != "" && strings.Contains(string(lp), domain.LocalpartCatchallSeparator) {
1657 addErrorf("domain %q: alias %q contains localpart catchall separator", d, a.LocalpartStr)
1660 clp = CanonicalLocalpart(lp, domain)
1663 addr := smtp.NewAddress(clp, domain.Domain).Pack(true)
1664 if _, ok := aliases[addr]; ok {
1665 addErrorf("domain %q: duplicate alias address %q", d, addr)
1668 if _, ok := accDests[addr]; ok {
1669 addErrorf("domain %q: alias %q already present as regular address", d, addr)
1672 if len(a.Addresses) == 0 {
1673 // Not currently possible, Addresses isn't optional.
1674 addErrorf("domain %q: alias %q needs at least one destination address", d, addr)
1677 a.ParsedAddresses = make([]config.AliasAddress, 0, len(a.Addresses))
1678 seen := map[string]bool{}
1679 for _, destAddr := range a.Addresses {
1680 da, err := smtp.ParseAddress(destAddr)
1682 addErrorf("domain %q: parsing destination address %q in alias %q: %v", d, destAddr, addr, err)
1685 dastr := da.Pack(true)
1686 accDest, ok := accDests[dastr]
1688 addErrorf("domain %q: alias %q references non-existent address %q", d, addr, destAddr)
1692 addErrorf("domain %q: alias %q has duplicate address %q", d, addr, destAddr)
1696 aa := config.AliasAddress{Address: da, AccountName: accDest.Account, Destination: accDest.Destination}
1697 a.ParsedAddresses = append(a.ParsedAddresses, aa)
1699 a.Domain = domain.Domain
1700 c.Domains[d].Aliases[lpstr] = a
1703 for _, aa := range a.ParsedAddresses {
1704 acc := c.Accounts[aa.AccountName]
1707 addrs = make([]string, len(a.ParsedAddresses))
1708 for i := range a.ParsedAddresses {
1709 addrs[i] = a.ParsedAddresses[i].Address.Pack(true)
1712 // Keep the non-sensitive fields.
1713 accAlias := config.Alias{
1714 PostPublic: a.PostPublic,
1715 ListMembers: a.ListMembers,
1716 AllowMsgFrom: a.AllowMsgFrom,
1717 LocalpartStr: a.LocalpartStr,
1720 acc.Aliases = append(acc.Aliases, config.AddressAlias{SubscriptionAddress: aa.Address.Pack(true), Alias: accAlias, MemberAddresses: addrs})
1721 c.Accounts[aa.AccountName] = acc
1726 // Check webserver configs.
1727 if (len(c.WebDomainRedirects) > 0 || len(c.WebHandlers) > 0) && !haveWebserverListener {
1728 addErrorf("WebDomainRedirects or WebHandlers configured but no listener with WebserverHTTP or WebserverHTTPS enabled")
1731 c.WebDNSDomainRedirects = map[dns.Domain]dns.Domain{}
1732 for from, to := range c.WebDomainRedirects {
1733 fromdom, err := dns.ParseDomain(from)
1735 addErrorf("parsing domain for redirect %s: %v", from, err)
1737 todom, err := dns.ParseDomain(to)
1739 addErrorf("parsing domain for redirect %s: %v", to, err)
1740 } else if fromdom == todom {
1741 addErrorf("will not redirect domain %s to itself", todom)
1743 var zerodom dns.Domain
1744 if _, ok := c.WebDNSDomainRedirects[fromdom]; ok && fromdom != zerodom {
1745 addErrorf("duplicate redirect domain %s", from)
1747 c.WebDNSDomainRedirects[fromdom] = todom
1750 for i := range c.WebHandlers {
1751 wh := &c.WebHandlers[i]
1753 if wh.LogName == "" {
1754 wh.Name = fmt.Sprintf("%d", i)
1756 wh.Name = wh.LogName
1759 dom, err := dns.ParseDomain(wh.Domain)
1761 addErrorf("webhandler %s %s: parsing domain: %v", wh.Domain, wh.PathRegexp, err)
1765 if !strings.HasPrefix(wh.PathRegexp, "^") {
1766 addErrorf("webhandler %s %s: path regexp must start with a ^", wh.Domain, wh.PathRegexp)
1768 re, err := regexp.Compile(wh.PathRegexp)
1770 addErrorf("webhandler %s %s: compiling regexp: %v", wh.Domain, wh.PathRegexp, err)
1775 if wh.WebStatic != nil {
1778 if ws.StripPrefix != "" && !strings.HasPrefix(ws.StripPrefix, "/") {
1779 addErrorf("webstatic %s %s: prefix to strip %s must start with a slash", wh.Domain, wh.PathRegexp, ws.StripPrefix)
1781 for k := range ws.ResponseHeaders {
1783 k := strings.TrimSpace(xk)
1784 if k != xk || k == "" {
1785 addErrorf("webstatic %s %s: bad header %q", wh.Domain, wh.PathRegexp, xk)
1789 if wh.WebRedirect != nil {
1791 wr := wh.WebRedirect
1792 if wr.BaseURL != "" {
1793 u, err := url.Parse(wr.BaseURL)
1795 addErrorf("webredirect %s %s: parsing redirect url %s: %v", wh.Domain, wh.PathRegexp, wr.BaseURL, err)
1801 addErrorf("webredirect %s %s: BaseURL must have empty path", wh.Domain, wh.PathRegexp, wr.BaseURL)
1805 if wr.OrigPathRegexp != "" && wr.ReplacePath != "" {
1806 re, err := regexp.Compile(wr.OrigPathRegexp)
1808 addErrorf("webredirect %s %s: compiling regexp %s: %v", wh.Domain, wh.PathRegexp, wr.OrigPathRegexp, err)
1811 } else if wr.OrigPathRegexp != "" || wr.ReplacePath != "" {
1812 addErrorf("webredirect %s %s: must have either both OrigPathRegexp and ReplacePath, or neither", wh.Domain, wh.PathRegexp)
1813 } else if wr.BaseURL == "" {
1814 addErrorf("webredirect %s %s: must at least one of BaseURL and OrigPathRegexp+ReplacePath", wh.Domain, wh.PathRegexp)
1816 if wr.StatusCode != 0 && (wr.StatusCode < 300 || wr.StatusCode >= 400) {
1817 addErrorf("webredirect %s %s: invalid redirect status code %d", wh.Domain, wh.PathRegexp, wr.StatusCode)
1820 if wh.WebForward != nil {
1823 u, err := url.Parse(wf.URL)
1825 addErrorf("webforward %s %s: parsing url %s: %v", wh.Domain, wh.PathRegexp, wf.URL, err)
1829 for k := range wf.ResponseHeaders {
1831 k := strings.TrimSpace(xk)
1832 if k != xk || k == "" {
1833 addErrorf("webforward %s %s: bad header %q", wh.Domain, wh.PathRegexp, xk)
1837 if wh.WebInternal != nil {
1839 wi := wh.WebInternal
1840 if !strings.HasPrefix(wi.BasePath, "/") || !strings.HasSuffix(wi.BasePath, "/") {
1841 addErrorf("webinternal %s %s: base path %q must start and end with /", wh.Domain, wh.PathRegexp, wi.BasePath)
1843 // todo: we could make maxMsgSize and accountPath configurable
1844 const isForwarded = false
1847 wi.Handler = NewWebadminHandler(wi.BasePath, isForwarded)
1849 wi.Handler = NewWebaccountHandler(wi.BasePath, isForwarded)
1852 wi.Handler = NewWebmailHandler(config.DefaultMaxMsgSize, wi.BasePath, isForwarded, accountPath)
1854 wi.Handler = NewWebapiHandler(config.DefaultMaxMsgSize, wi.BasePath, isForwarded)
1856 addErrorf("webinternal %s %s: unknown service %q", wh.Domain, wh.PathRegexp, wi.Service)
1858 wi.Handler = SafeHeaders(http.StripPrefix(wi.BasePath[:len(wi.BasePath)-1], wi.Handler))
1861 addErrorf("webhandler %s %s: must have exactly one handler, not %d", wh.Domain, wh.PathRegexp, n)
1865 c.MonitorDNSBLZones = nil
1866 for _, s := range c.MonitorDNSBLs {
1867 d, err := dns.ParseDomain(s)
1869 addErrorf("invalid monitor dnsbl zone %s: %v", s, err)
1872 if slices.Contains(c.MonitorDNSBLZones, d) {
1873 addErrorf("duplicate zone %s in monitor dnsbl zones", d)
1876 c.MonitorDNSBLZones = append(c.MonitorDNSBLZones, d)
1882func loadPrivateKeyFile(keyPath string) (crypto.Signer, error) {
1883 keyBuf, err := os.ReadFile(keyPath)
1885 return nil, fmt.Errorf("reading host private key: %v", err)
1887 b, _ := pem.Decode(keyBuf)
1889 return nil, fmt.Errorf("parsing pem block for private key: %v", err)
1894 privKey, err = x509.ParsePKCS8PrivateKey(b.Bytes)
1895 case "RSA PRIVATE KEY":
1896 privKey, err = x509.ParsePKCS1PrivateKey(b.Bytes)
1897 case "EC PRIVATE KEY":
1898 privKey, err = x509.ParseECPrivateKey(b.Bytes)
1900 err = fmt.Errorf("unknown pem type %q", b.Type)
1903 return nil, fmt.Errorf("parsing private key: %v", err)
1905 if k, ok := privKey.(crypto.Signer); ok {
1908 return nil, fmt.Errorf("parsed private key not a crypto.Signer, but %T", privKey)
1911func loadTLSKeyCerts(configFile, kind string, ctls *config.TLS) error {
1912 certs := []tls.Certificate{}
1913 for _, kp := range ctls.KeyCerts {
1914 certPath := configDirPath(configFile, kp.CertFile)
1915 keyPath := configDirPath(configFile, kp.KeyFile)
1916 cert, err := loadX509KeyPairPrivileged(certPath, keyPath)
1918 return fmt.Errorf("tls config for %q: parsing x509 key pair: %v", kind, err)
1920 certs = append(certs, cert)
1922 ctls.Config = &tls.Config{
1923 Certificates: certs,
1924 SessionTicketsDisabled: true,
1926 ctls.ConfigFallback = ctls.Config
1930// load x509 key/cert files from file descriptor possibly passed in by privileged
1932func loadX509KeyPairPrivileged(certPath, keyPath string) (tls.Certificate, error) {
1933 certBuf, err := readFilePrivileged(certPath)
1935 return tls.Certificate{}, fmt.Errorf("reading tls certificate: %v", err)
1937 keyBuf, err := readFilePrivileged(keyPath)
1939 return tls.Certificate{}, fmt.Errorf("reading tls key: %v", err)
1941 return tls.X509KeyPair(certBuf, keyBuf)
1944// like os.ReadFile, but open privileged file possibly passed in by root process.
1945func readFilePrivileged(path string) ([]byte, error) {
1946 f, err := OpenPrivileged(path)
1951 return io.ReadAll(f)