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 *tls.Config
713 if checkOnly || acme.Manager == nil {
714 tlsconfig = &tls.Config{}
716 tlsconfig = acme.Manager.TLSConfig.Clone()
717 l.TLS.ACMEConfig = acme.Manager.ACMETLSConfig
719 // SMTP STARTTLS connections are commonly made without SNI, because certificates
720 // often aren't verified.
721 hostname := c.HostnameDomain
722 if l.Hostname != "" {
723 hostname = l.HostnameDomain
725 getCert := tlsconfig.GetCertificate
726 tlsconfig.GetCertificate = func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
727 if hello.ServerName == "" {
728 hello.ServerName = hostname.ASCII
730 return getCert(hello)
733 l.TLS.Config = tlsconfig
734 } else if len(l.TLS.KeyCerts) != 0 {
735 if doLoadTLSKeyCerts {
736 if err := loadTLSKeyCerts(configFile, "listener "+name, l.TLS); err != nil {
741 addErrorf("listener %q: cannot have TLS config without ACME and without static keys/certificates", name)
743 for _, privKeyFile := range l.TLS.HostPrivateKeyFiles {
744 keyPath := configDirPath(configFile, privKeyFile)
745 privKey, err := loadPrivateKeyFile(keyPath)
747 addErrorf("listener %q: parsing host private key for DANE and ACME certificates: %v", name, err)
750 switch k := privKey.(type) {
751 case *rsa.PrivateKey:
752 if k.N.BitLen() != 2048 {
753 log.Error("need rsa key with 2048 bits, for host private key for DANE/ACME certificates, ignoring",
754 slog.String("listener", name),
755 slog.String("file", keyPath),
756 slog.Int("bits", k.N.BitLen()))
759 l.TLS.HostPrivateRSA2048Keys = append(l.TLS.HostPrivateRSA2048Keys, k)
760 case *ecdsa.PrivateKey:
761 if k.Curve != elliptic.P256() {
762 log.Error("unrecognized ecdsa curve for host private key for DANE/ACME certificates, ignoring", slog.String("listener", name), slog.String("file", keyPath))
765 l.TLS.HostPrivateECDSAP256Keys = append(l.TLS.HostPrivateECDSAP256Keys, k)
767 log.Error("unrecognized key type for host private key for DANE/ACME certificates, ignoring",
768 slog.String("listener", name),
769 slog.String("file", keyPath),
770 slog.String("keytype", fmt.Sprintf("%T", privKey)))
774 if l.TLS.ACME != "" && (len(l.TLS.HostPrivateRSA2048Keys) == 0) != (len(l.TLS.HostPrivateECDSAP256Keys) == 0) {
775 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")
779 var minVersion uint16 = tls.VersionTLS12
780 if l.TLS.MinVersion != "" {
781 versions := map[string]uint16{
782 "TLSv1.0": tls.VersionTLS10,
783 "TLSv1.1": tls.VersionTLS11,
784 "TLSv1.2": tls.VersionTLS12,
785 "TLSv1.3": tls.VersionTLS13,
787 v, ok := versions[l.TLS.MinVersion]
789 addErrorf("listener %q: unknown TLS mininum version %q", name, l.TLS.MinVersion)
793 if l.TLS.Config != nil {
794 l.TLS.Config.MinVersion = minVersion
796 if l.TLS.ACMEConfig != nil {
797 l.TLS.ACMEConfig.MinVersion = minVersion
800 var needsTLS []string
801 needtls := func(s string, v bool) {
803 needsTLS = append(needsTLS, s)
806 needtls("IMAPS", l.IMAPS.Enabled)
807 needtls("SMTP", l.SMTP.Enabled && !l.SMTP.NoSTARTTLS)
808 needtls("Submissions", l.Submissions.Enabled)
809 needtls("Submission", l.Submission.Enabled && !l.Submission.NoRequireSTARTTLS)
810 needtls("AccountHTTPS", l.AccountHTTPS.Enabled)
811 needtls("AdminHTTPS", l.AdminHTTPS.Enabled)
812 needtls("AutoconfigHTTPS", l.AutoconfigHTTPS.Enabled && !l.AutoconfigHTTPS.NonTLS)
813 needtls("MTASTSHTTPS", l.MTASTSHTTPS.Enabled && !l.MTASTSHTTPS.NonTLS)
814 needtls("WebserverHTTPS", l.WebserverHTTPS.Enabled)
815 if len(needsTLS) > 0 {
816 addErrorf("listener %q does not specify tls config, but requires tls for %s", name, strings.Join(needsTLS, ", "))
819 if l.AutoconfigHTTPS.Enabled && l.MTASTSHTTPS.Enabled && l.AutoconfigHTTPS.Port == l.MTASTSHTTPS.Port && l.AutoconfigHTTPS.NonTLS != l.MTASTSHTTPS.NonTLS {
820 addErrorf("listener %q tries to enable autoconfig and mta-sts enabled on same port but with both http and https", name)
824 haveUnspecifiedSMTPListener = true
826 for _, ipstr := range l.IPs {
827 ip := net.ParseIP(ipstr)
829 addErrorf("listener %q has invalid IP %q", name, ipstr)
832 if ip.IsUnspecified() {
833 haveUnspecifiedSMTPListener = true
836 if len(c.SpecifiedSMTPListenIPs) >= 2 {
837 haveUnspecifiedSMTPListener = true
838 } else if len(c.SpecifiedSMTPListenIPs) > 0 && (c.SpecifiedSMTPListenIPs[0].To4() == nil) == (ip.To4() == nil) {
839 haveUnspecifiedSMTPListener = true
841 c.SpecifiedSMTPListenIPs = append(c.SpecifiedSMTPListenIPs, ip)
845 for _, s := range l.SMTP.DNSBLs {
846 d, err := dns.ParseDomain(s)
848 addErrorf("listener %q has invalid DNSBL zone %q", name, s)
851 l.SMTP.DNSBLZones = append(l.SMTP.DNSBLZones, d)
853 if l.IPsNATed && len(l.NATIPs) > 0 {
854 addErrorf("listener %q has both IPsNATed and NATIPs (remove deprecated IPsNATed)", name)
856 for _, ipstr := range l.NATIPs {
857 ip := net.ParseIP(ipstr)
859 addErrorf("listener %q has invalid ip %q", name, ipstr)
860 } else if ip.IsUnspecified() || ip.IsLoopback() {
861 addErrorf("listener %q has NAT ip that is the unspecified or loopback address %s", name, ipstr)
864 checkPath := func(kind string, enabled bool, path string) {
865 if enabled && path != "" && !strings.HasPrefix(path, "/") {
866 addErrorf("listener %q has %s with path %q that must start with a slash", name, kind, path)
869 checkPath("AccountHTTP", l.AccountHTTP.Enabled, l.AccountHTTP.Path)
870 checkPath("AccountHTTPS", l.AccountHTTPS.Enabled, l.AccountHTTPS.Path)
871 checkPath("AdminHTTP", l.AdminHTTP.Enabled, l.AdminHTTP.Path)
872 checkPath("AdminHTTPS", l.AdminHTTPS.Enabled, l.AdminHTTPS.Path)
873 c.Listeners[name] = l
875 if haveUnspecifiedSMTPListener {
876 c.SpecifiedSMTPListenIPs = nil
879 var zerouse config.SpecialUseMailboxes
880 if len(c.DefaultMailboxes) > 0 && (c.InitialMailboxes.SpecialUse != zerouse || len(c.InitialMailboxes.Regular) > 0) {
881 addErrorf("cannot have both DefaultMailboxes and InitialMailboxes")
883 // DefaultMailboxes is deprecated.
884 for _, mb := range c.DefaultMailboxes {
885 checkMailboxNormf(mb, "default mailbox")
887 checkSpecialUseMailbox := func(nameOpt string) {
889 checkMailboxNormf(nameOpt, "special-use initial mailbox")
890 if strings.EqualFold(nameOpt, "inbox") {
891 addErrorf("initial mailbox cannot be set to Inbox (Inbox is always created)")
895 checkSpecialUseMailbox(c.InitialMailboxes.SpecialUse.Archive)
896 checkSpecialUseMailbox(c.InitialMailboxes.SpecialUse.Draft)
897 checkSpecialUseMailbox(c.InitialMailboxes.SpecialUse.Junk)
898 checkSpecialUseMailbox(c.InitialMailboxes.SpecialUse.Sent)
899 checkSpecialUseMailbox(c.InitialMailboxes.SpecialUse.Trash)
900 for _, name := range c.InitialMailboxes.Regular {
901 checkMailboxNormf(name, "regular initial mailbox")
902 if strings.EqualFold(name, "inbox") {
903 addErrorf("initial regular mailbox cannot be set to Inbox (Inbox is always created)")
907 checkTransportSMTP := func(name string, isTLS bool, t *config.TransportSMTP) {
909 t.DNSHost, err = dns.ParseDomain(t.Host)
911 addErrorf("transport %s: bad host %s: %v", name, t.Host, err)
914 if isTLS && t.STARTTLSInsecureSkipVerify {
915 addErrorf("transport %s: cannot have STARTTLSInsecureSkipVerify with immediate TLS")
917 if isTLS && t.NoSTARTTLS {
918 addErrorf("transport %s: cannot have NoSTARTTLS with immediate TLS")
924 seen := map[string]bool{}
925 for _, m := range t.Auth.Mechanisms {
927 addErrorf("transport %s: duplicate authentication mechanism %s", name, m)
931 case "SCRAM-SHA-256-PLUS":
932 case "SCRAM-SHA-256":
933 case "SCRAM-SHA-1-PLUS":
938 addErrorf("transport %s: unknown authentication mechanism %s", name, m)
942 t.Auth.EffectiveMechanisms = t.Auth.Mechanisms
943 if len(t.Auth.EffectiveMechanisms) == 0 {
944 t.Auth.EffectiveMechanisms = []string{"SCRAM-SHA-256-PLUS", "SCRAM-SHA-256", "SCRAM-SHA-1-PLUS", "SCRAM-SHA-1", "CRAM-MD5"}
948 checkTransportSocks := func(name string, t *config.TransportSocks) {
949 _, _, err := net.SplitHostPort(t.Address)
951 addErrorf("transport %s: bad address %s: %v", name, t.Address, err)
953 for _, ipstr := range t.RemoteIPs {
954 ip := net.ParseIP(ipstr)
956 addErrorf("transport %s: bad ip %s", name, ipstr)
958 t.IPs = append(t.IPs, ip)
961 t.Hostname, err = dns.ParseDomain(t.RemoteHostname)
963 addErrorf("transport %s: bad hostname %s: %v", name, t.RemoteHostname, err)
967 checkTransportDirect := func(name string, t *config.TransportDirect) {
968 if t.DisableIPv4 && t.DisableIPv6 {
969 addErrorf("transport %s: both IPv4 and IPv6 are disabled, enable at least one", name)
980 for name, t := range c.Transports {
982 if t.Submissions != nil {
984 checkTransportSMTP(name, true, t.Submissions)
986 if t.Submission != nil {
988 checkTransportSMTP(name, false, t.Submission)
992 checkTransportSMTP(name, false, t.SMTP)
996 checkTransportSocks(name, t.Socks)
1000 checkTransportDirect(name, t.Direct)
1003 addErrorf("transport %s: cannot have multiple methods in a transport", name)
1007 // Load CA certificate pool.
1008 if c.TLS.CA != nil {
1009 if c.TLS.CA.AdditionalToSystem {
1011 c.TLS.CertPool, err = x509.SystemCertPool()
1013 addErrorf("fetching system CA cert pool: %v", err)
1016 c.TLS.CertPool = x509.NewCertPool()
1018 for _, certfile := range c.TLS.CA.CertFiles {
1019 p := configDirPath(configFile, certfile)
1020 pemBuf, err := os.ReadFile(p)
1022 addErrorf("reading TLS CA cert file: %v", err)
1024 } else if !c.TLS.CertPool.AppendCertsFromPEM(pemBuf) {
1025 // todo: can we check more fully if we're getting some useful data back?
1026 addErrorf("no CA certs added from %q", p)
1033// PrepareDynamicConfig parses the dynamic config file given a static file.
1034func 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) {
1035 addErrorf := func(format string, args ...any) {
1036 errs = append(errs, fmt.Errorf(format, args...))
1039 f, err := os.Open(dynamicPath)
1041 addErrorf("parsing domains config: %v", err)
1047 addErrorf("stat domains config: %v", err)
1049 if err := sconf.Parse(f, &c); err != nil {
1050 addErrorf("parsing dynamic config file: %v", err)
1054 accDests, aliases, errs = prepareDynamicConfig(ctx, log, dynamicPath, static, &c)
1055 return c, fi.ModTime(), accDests, aliases, errs
1058func 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) {
1059 addErrorf := func(format string, args ...any) {
1060 errs = append(errs, fmt.Errorf(format, args...))
1063 // Check that mailbox is in unicode NFC normalized form.
1064 checkMailboxNormf := func(mailbox string, format string, args ...any) {
1065 s := norm.NFC.String(mailbox)
1067 msg := fmt.Sprintf(format, args...)
1068 addErrorf("%s: mailbox %q is not in NFC normalized form, should be %q", msg, mailbox, s)
1072 // Validate postmaster account exists.
1073 if _, ok := c.Accounts[static.Postmaster.Account]; !ok {
1074 addErrorf("postmaster account %q does not exist", static.Postmaster.Account)
1076 checkMailboxNormf(static.Postmaster.Mailbox, "postmaster mailbox")
1078 accDests = map[string]AccountDestination{}
1079 aliases = map[string]config.Alias{}
1081 // Validate host TLSRPT account/address.
1082 if static.HostTLSRPT.Account != "" {
1083 if _, ok := c.Accounts[static.HostTLSRPT.Account]; !ok {
1084 addErrorf("host tlsrpt account %q does not exist", static.HostTLSRPT.Account)
1086 checkMailboxNormf(static.HostTLSRPT.Mailbox, "host tlsrpt mailbox")
1088 // Localpart has been parsed already.
1090 addrFull := smtp.NewAddress(static.HostTLSRPT.ParsedLocalpart, static.HostnameDomain).String()
1091 dest := config.Destination{
1092 Mailbox: static.HostTLSRPT.Mailbox,
1093 HostTLSReports: true,
1095 accDests[addrFull] = AccountDestination{false, static.HostTLSRPT.ParsedLocalpart, static.HostTLSRPT.Account, dest}
1098 var haveSTSListener, haveWebserverListener bool
1099 for _, l := range static.Listeners {
1100 if l.MTASTSHTTPS.Enabled {
1101 haveSTSListener = true
1103 if l.WebserverHTTP.Enabled || l.WebserverHTTPS.Enabled {
1104 haveWebserverListener = true
1108 checkRoutes := func(descr string, routes []config.Route) {
1109 parseRouteDomains := func(l []string) []string {
1111 for _, e := range l {
1117 if strings.HasPrefix(e, ".") {
1121 d, err := dns.ParseDomain(e)
1123 addErrorf("%s: invalid domain %s: %v", descr, e, err)
1125 r = append(r, prefix+d.ASCII)
1130 for i := range routes {
1131 routes[i].FromDomainASCII = parseRouteDomains(routes[i].FromDomain)
1132 routes[i].ToDomainASCII = parseRouteDomains(routes[i].ToDomain)
1134 routes[i].ResolvedTransport, ok = static.Transports[routes[i].Transport]
1136 addErrorf("%s: route references undefined transport %s", descr, routes[i].Transport)
1141 checkRoutes("global routes", c.Routes)
1143 // Validate domains.
1144 c.ClientSettingDomains = map[dns.Domain]struct{}{}
1145 for d, domain := range c.Domains {
1146 dnsdomain, err := dns.ParseDomain(d)
1148 addErrorf("bad domain %q: %s", d, err)
1149 } else if dnsdomain.Name() != d {
1150 addErrorf("domain %s must be specified in unicode form, %s", d, dnsdomain.Name())
1153 domain.Domain = dnsdomain
1155 if domain.ClientSettingsDomain != "" {
1156 csd, err := dns.ParseDomain(domain.ClientSettingsDomain)
1158 addErrorf("bad client settings domain %q: %s", domain.ClientSettingsDomain, err)
1160 domain.ClientSettingsDNSDomain = csd
1161 c.ClientSettingDomains[csd] = struct{}{}
1164 for _, sign := range domain.DKIM.Sign {
1165 if _, ok := domain.DKIM.Selectors[sign]; !ok {
1166 addErrorf("selector %s for signing is missing in domain %s", sign, d)
1169 for name, sel := range domain.DKIM.Selectors {
1170 seld, err := dns.ParseDomain(name)
1172 addErrorf("bad selector %q: %s", name, err)
1173 } else if seld.Name() != name {
1174 addErrorf("selector %q must be specified in unicode form, %q", name, seld.Name())
1178 if sel.Expiration != "" {
1179 exp, err := time.ParseDuration(sel.Expiration)
1181 addErrorf("selector %q has invalid expiration %q: %v", name, sel.Expiration, err)
1183 sel.ExpirationSeconds = int(exp / time.Second)
1187 sel.HashEffective = sel.Hash
1188 switch sel.HashEffective {
1190 sel.HashEffective = "sha256"
1192 log.Error("using sha1 with DKIM is deprecated as not secure enough, switch to sha256")
1195 addErrorf("unsupported hash %q for selector %q in domain %s", sel.HashEffective, name, d)
1198 pemBuf, err := os.ReadFile(configDirPath(dynamicPath, sel.PrivateKeyFile))
1200 addErrorf("reading private key for selector %s in domain %s: %s", name, d, err)
1203 p, _ := pem.Decode(pemBuf)
1205 addErrorf("private key for selector %s in domain %s has no PEM block", name, d)
1208 key, err := x509.ParsePKCS8PrivateKey(p.Bytes)
1210 addErrorf("parsing private key for selector %s in domain %s: %s", name, d, err)
1213 switch k := key.(type) {
1214 case *rsa.PrivateKey:
1215 if k.N.BitLen() < 1024 {
1217 // Let's help user do the right thing.
1218 addErrorf("rsa keys should be >= 1024 bits")
1221 sel.Algorithm = fmt.Sprintf("rsa-%d", k.N.BitLen())
1222 case ed25519.PrivateKey:
1223 if sel.HashEffective != "sha256" {
1224 addErrorf("hash algorithm %q is not supported with ed25519, only sha256 is", sel.HashEffective)
1227 sel.Algorithm = "ed25519"
1229 addErrorf("private key type %T not yet supported, at selector %s in domain %s", key, name, d)
1232 if len(sel.Headers) == 0 {
1236 // By default we seal signed headers, and we sign user-visible headers to
1237 // prevent/limit reuse of previously signed messages: All addressing fields, date
1238 // and subject, message-referencing fields, parsing instructions (content-type).
1239 sel.HeadersEffective = strings.Split("From,To,Cc,Bcc,Reply-To,References,In-Reply-To,Subject,Date,Message-Id,Content-Type", ",")
1242 for _, h := range sel.Headers {
1243 from = from || strings.EqualFold(h, "From")
1245 if strings.EqualFold(h, "DKIM-Signature") || strings.EqualFold(h, "Received") || strings.EqualFold(h, "Return-Path") {
1246 log.Error("DKIM-signing header %q is recommended against as it may be modified in transit")
1250 addErrorf("From-field must always be DKIM-signed")
1252 sel.HeadersEffective = sel.Headers
1255 domain.DKIM.Selectors[name] = sel
1258 if domain.MTASTS != nil {
1259 if !haveSTSListener {
1260 addErrorf("MTA-STS enabled for domain %q, but there is no listener for MTASTS", d)
1262 sts := domain.MTASTS
1263 if sts.PolicyID == "" {
1264 addErrorf("invalid empty MTA-STS PolicyID")
1267 case mtasts.ModeNone, mtasts.ModeTesting, mtasts.ModeEnforce:
1269 addErrorf("invalid mtasts mode %q", sts.Mode)
1273 checkRoutes("routes for domain", domain.Routes)
1275 c.Domains[d] = domain
1278 // To determine ReportsOnly.
1279 domainHasAddress := map[string]bool{}
1281 // Validate email addresses.
1282 for accName, acc := range c.Accounts {
1284 acc.DNSDomain, err = dns.ParseDomain(acc.Domain)
1286 addErrorf("parsing domain %s for account %q: %s", acc.Domain, accName, err)
1289 if strings.EqualFold(acc.RejectsMailbox, "Inbox") {
1290 addErrorf("account %q: cannot set RejectsMailbox to inbox, messages will be removed automatically from the rejects mailbox", accName)
1292 checkMailboxNormf(acc.RejectsMailbox, "account %q", accName)
1294 if acc.AutomaticJunkFlags.JunkMailboxRegexp != "" {
1295 r, err := regexp.Compile(acc.AutomaticJunkFlags.JunkMailboxRegexp)
1297 addErrorf("invalid JunkMailboxRegexp regular expression: %v", err)
1301 if acc.AutomaticJunkFlags.NeutralMailboxRegexp != "" {
1302 r, err := regexp.Compile(acc.AutomaticJunkFlags.NeutralMailboxRegexp)
1304 addErrorf("invalid NeutralMailboxRegexp regular expression: %v", err)
1306 acc.NeutralMailbox = r
1308 if acc.AutomaticJunkFlags.NotJunkMailboxRegexp != "" {
1309 r, err := regexp.Compile(acc.AutomaticJunkFlags.NotJunkMailboxRegexp)
1311 addErrorf("invalid NotJunkMailboxRegexp regular expression: %v", err)
1313 acc.NotJunkMailbox = r
1316 if acc.JunkFilter != nil {
1317 params := acc.JunkFilter.Params
1318 if params.MaxPower < 0 || params.MaxPower > 0.5 {
1319 addErrorf("junk filter MaxPower must be >= 0 and < 0.5")
1321 if params.TopWords < 0 {
1322 addErrorf("junk filter TopWords must be >= 0")
1324 if params.IgnoreWords < 0 || params.IgnoreWords > 0.5 {
1325 addErrorf("junk filter IgnoreWords must be >= 0 and < 0.5")
1327 if params.RareWords < 0 {
1328 addErrorf("junk filter RareWords must be >= 0")
1332 acc.ParsedFromIDLoginAddresses = make([]smtp.Address, len(acc.FromIDLoginAddresses))
1333 for i, s := range acc.FromIDLoginAddresses {
1334 a, err := smtp.ParseAddress(s)
1336 addErrorf("invalid fromid login address %q in account %q: %v", s, accName, err)
1338 // We check later on if address belongs to account.
1339 dom, ok := c.Domains[a.Domain.Name()]
1341 addErrorf("unknown domain in fromid login address %q for account %q", s, accName)
1342 } else if dom.LocalpartCatchallSeparator == "" {
1343 addErrorf("localpart catchall separator not configured for domain for fromid login address %q for account %q", s, accName)
1345 acc.ParsedFromIDLoginAddresses[i] = a
1348 // Clear any previously derived state.
1351 c.Accounts[accName] = acc
1353 if acc.OutgoingWebhook != nil {
1354 u, err := url.Parse(acc.OutgoingWebhook.URL)
1355 if err == nil && (u.Scheme != "http" && u.Scheme != "https") {
1356 err = errors.New("scheme must be http or https")
1359 addErrorf("parsing outgoing hook url %q in account %q: %v", acc.OutgoingWebhook.URL, accName, err)
1362 // note: outgoing hook events are in ../queue/hooks.go, ../mox-/config.go, ../queue.go and ../webapi/gendoc.sh. keep in sync.
1363 outgoingHookEvents := []string{"delivered", "suppressed", "delayed", "failed", "relayed", "expanded", "canceled", "unrecognized"}
1364 for _, e := range acc.OutgoingWebhook.Events {
1365 if !slices.Contains(outgoingHookEvents, e) {
1366 addErrorf("unknown outgoing hook event %q", e)
1370 if acc.IncomingWebhook != nil {
1371 u, err := url.Parse(acc.IncomingWebhook.URL)
1372 if err == nil && (u.Scheme != "http" && u.Scheme != "https") {
1373 err = errors.New("scheme must be http or https")
1376 addErrorf("parsing incoming hook url %q in account %q: %v", acc.IncomingWebhook.URL, accName, err)
1380 // 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.
1381 replaceLocalparts := map[string]string{}
1383 for addrName, dest := range acc.Destinations {
1384 checkMailboxNormf(dest.Mailbox, "account %q, destination %q", accName, addrName)
1386 for i, rs := range dest.Rulesets {
1387 checkMailboxNormf(rs.Mailbox, "account %q, destination %q, ruleset %d", accName, addrName, i+1)
1391 if rs.SMTPMailFromRegexp != "" {
1393 r, err := regexp.Compile(rs.SMTPMailFromRegexp)
1395 addErrorf("invalid SMTPMailFrom regular expression: %v", err)
1397 c.Accounts[accName].Destinations[addrName].Rulesets[i].SMTPMailFromRegexpCompiled = r
1399 if rs.MsgFromRegexp != "" {
1401 r, err := regexp.Compile(rs.MsgFromRegexp)
1403 addErrorf("invalid MsgFrom regular expression: %v", err)
1405 c.Accounts[accName].Destinations[addrName].Rulesets[i].MsgFromRegexpCompiled = r
1407 if rs.VerifiedDomain != "" {
1409 d, err := dns.ParseDomain(rs.VerifiedDomain)
1411 addErrorf("invalid VerifiedDomain: %v", err)
1413 c.Accounts[accName].Destinations[addrName].Rulesets[i].VerifiedDNSDomain = d
1416 var hdr [][2]*regexp.Regexp
1417 for k, v := range rs.HeadersRegexp {
1419 if strings.ToLower(k) != k {
1420 addErrorf("header field %q must only have lower case characters", k)
1422 if strings.ToLower(v) != v {
1423 addErrorf("header value %q must only have lower case characters", v)
1425 rk, err := regexp.Compile(k)
1427 addErrorf("invalid rule header regexp %q: %v", k, err)
1429 rv, err := regexp.Compile(v)
1431 addErrorf("invalid rule header regexp %q: %v", v, err)
1433 hdr = append(hdr, [...]*regexp.Regexp{rk, rv})
1435 c.Accounts[accName].Destinations[addrName].Rulesets[i].HeadersRegexpCompiled = hdr
1438 addErrorf("ruleset must have at least one rule")
1441 if rs.IsForward && rs.ListAllowDomain != "" {
1442 addErrorf("ruleset cannot have both IsForward and ListAllowDomain")
1445 if rs.SMTPMailFromRegexp == "" || rs.VerifiedDomain == "" {
1446 addErrorf("ruleset with IsForward must have both SMTPMailFromRegexp and VerifiedDomain too")
1449 if rs.ListAllowDomain != "" {
1450 d, err := dns.ParseDomain(rs.ListAllowDomain)
1452 addErrorf("invalid ListAllowDomain %q: %v", rs.ListAllowDomain, err)
1454 c.Accounts[accName].Destinations[addrName].Rulesets[i].ListAllowDNSDomain = d
1457 checkMailboxNormf(rs.AcceptRejectsToMailbox, "account %q, destination %q, ruleset %d, rejects mailbox", accName, addrName, i+1)
1458 if strings.EqualFold(rs.AcceptRejectsToMailbox, "inbox") {
1459 addErrorf("account %q, destination %q, ruleset %d: AcceptRejectsToMailbox cannot be set to Inbox", accName, addrName, i+1)
1463 // Catchall destination for domain.
1464 if strings.HasPrefix(addrName, "@") {
1465 d, err := dns.ParseDomain(addrName[1:])
1467 addErrorf("parsing domain %q in account %q", addrName[1:], accName)
1469 } else if _, ok := c.Domains[d.Name()]; !ok {
1470 addErrorf("unknown domain for address %q in account %q", addrName, accName)
1473 domainHasAddress[d.Name()] = true
1474 addrFull := "@" + d.Name()
1475 if _, ok := accDests[addrFull]; ok {
1476 addErrorf("duplicate canonicalized catchall destination address %s", addrFull)
1478 accDests[addrFull] = AccountDestination{true, "", accName, dest}
1482 // todo deprecated: remove support for parsing destination as just a localpart instead full address.
1483 var address smtp.Address
1484 if localpart, err := smtp.ParseLocalpart(addrName); err != nil && errors.Is(err, smtp.ErrBadLocalpart) {
1485 address, err = smtp.ParseAddress(addrName)
1487 addErrorf("invalid email address %q in account %q", addrName, accName)
1489 } else if _, ok := c.Domains[address.Domain.Name()]; !ok {
1490 addErrorf("unknown domain for address %q in account %q", addrName, accName)
1495 addErrorf("invalid localpart %q in account %q", addrName, accName)
1498 address = smtp.NewAddress(localpart, acc.DNSDomain)
1499 if _, ok := c.Domains[acc.DNSDomain.Name()]; !ok {
1500 addErrorf("unknown domain %s for account %q", acc.DNSDomain.Name(), accName)
1503 replaceLocalparts[addrName] = address.Pack(true)
1506 origLP := address.Localpart
1507 dc := c.Domains[address.Domain.Name()]
1508 domainHasAddress[address.Domain.Name()] = true
1509 lp := CanonicalLocalpart(address.Localpart, dc)
1510 if dc.LocalpartCatchallSeparator != "" && strings.Contains(string(address.Localpart), dc.LocalpartCatchallSeparator) {
1511 addErrorf("localpart of address %s includes domain catchall separator %s", address, dc.LocalpartCatchallSeparator)
1513 address.Localpart = lp
1515 addrFull := address.Pack(true)
1516 if _, ok := accDests[addrFull]; ok {
1517 addErrorf("duplicate canonicalized destination address %s", addrFull)
1519 accDests[addrFull] = AccountDestination{false, origLP, accName, dest}
1522 for lp, addr := range replaceLocalparts {
1523 dest, ok := acc.Destinations[lp]
1525 addErrorf("could not find localpart %q to replace with address in destinations", lp)
1527 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`,
1528 slog.Any("localpart", lp),
1529 slog.Any("address", addr),
1530 slog.String("account", accName))
1531 acc.Destinations[addr] = dest
1532 delete(acc.Destinations, lp)
1536 // Now that all addresses are parsed, check if all fromid login addresses match
1537 // configured addresses.
1538 for i, a := range acc.ParsedFromIDLoginAddresses {
1539 // For domain catchall.
1540 if _, ok := accDests["@"+a.Domain.Name()]; ok {
1543 dc := c.Domains[a.Domain.Name()]
1544 a.Localpart = CanonicalLocalpart(a.Localpart, dc)
1545 if _, ok := accDests[a.Pack(true)]; !ok {
1546 addErrorf("fromid login address %q for account %q does not match its destination addresses", acc.FromIDLoginAddresses[i], accName)
1550 checkRoutes("routes for account", acc.Routes)
1553 // Set DMARC destinations.
1554 for d, domain := range c.Domains {
1555 dmarc := domain.DMARC
1559 if _, ok := c.Accounts[dmarc.Account]; !ok {
1560 addErrorf("DMARC account %q does not exist", dmarc.Account)
1562 lp, err := smtp.ParseLocalpart(dmarc.Localpart)
1564 addErrorf("invalid DMARC localpart %q: %s", dmarc.Localpart, err)
1566 if lp.IsInternational() {
1568 addErrorf("DMARC localpart %q is an internationalized address, only conventional ascii-only address possible for interopability", lp)
1570 addrdom := domain.Domain
1571 if dmarc.Domain != "" {
1572 addrdom, err = dns.ParseDomain(dmarc.Domain)
1574 addErrorf("DMARC domain %q: %s", dmarc.Domain, err)
1575 } else if _, ok := c.Domains[addrdom.Name()]; !ok {
1576 addErrorf("unknown domain %q for DMARC address in domain %q", addrdom, d)
1579 if addrdom == domain.Domain {
1580 domainHasAddress[addrdom.Name()] = true
1583 domain.DMARC.ParsedLocalpart = lp
1584 domain.DMARC.DNSDomain = addrdom
1585 c.Domains[d] = domain
1586 addrFull := smtp.NewAddress(lp, addrdom).String()
1587 dest := config.Destination{
1588 Mailbox: dmarc.Mailbox,
1591 checkMailboxNormf(dmarc.Mailbox, "DMARC mailbox for account %q", dmarc.Account)
1592 accDests[addrFull] = AccountDestination{false, lp, dmarc.Account, dest}
1595 // Set TLSRPT destinations.
1596 for d, domain := range c.Domains {
1597 tlsrpt := domain.TLSRPT
1601 if _, ok := c.Accounts[tlsrpt.Account]; !ok {
1602 addErrorf("TLSRPT account %q does not exist", tlsrpt.Account)
1604 lp, err := smtp.ParseLocalpart(tlsrpt.Localpart)
1606 addErrorf("invalid TLSRPT localpart %q: %s", tlsrpt.Localpart, err)
1608 if lp.IsInternational() {
1609 // Does not appear documented in
../rfc/8460, but similar to DMARC it makes sense
1610 // to keep this ascii-only addresses.
1611 addErrorf("TLSRPT localpart %q is an internationalized address, only conventional ascii-only address allowed for interopability", lp)
1613 addrdom := domain.Domain
1614 if tlsrpt.Domain != "" {
1615 addrdom, err = dns.ParseDomain(tlsrpt.Domain)
1617 addErrorf("TLSRPT domain %q: %s", tlsrpt.Domain, err)
1618 } else if _, ok := c.Domains[addrdom.Name()]; !ok {
1619 addErrorf("unknown domain %q for TLSRPT address in domain %q", tlsrpt.Domain, d)
1622 if addrdom == domain.Domain {
1623 domainHasAddress[addrdom.Name()] = true
1626 domain.TLSRPT.ParsedLocalpart = lp
1627 domain.TLSRPT.DNSDomain = addrdom
1628 c.Domains[d] = domain
1629 addrFull := smtp.NewAddress(lp, addrdom).String()
1630 dest := config.Destination{
1631 Mailbox: tlsrpt.Mailbox,
1632 DomainTLSReports: true,
1634 checkMailboxNormf(tlsrpt.Mailbox, "TLSRPT mailbox for account %q", tlsrpt.Account)
1635 accDests[addrFull] = AccountDestination{false, lp, tlsrpt.Account, dest}
1638 // Set ReportsOnly for domains, based on whether we have seen addresses (possibly
1639 // from DMARC or TLS reporting).
1640 for d, domain := range c.Domains {
1641 domain.ReportsOnly = !domainHasAddress[domain.Domain.Name()]
1642 c.Domains[d] = domain
1645 // Aliases, per domain. Also add references to accounts.
1646 for d, domain := range c.Domains {
1647 for lpstr, a := range domain.Aliases {
1649 a.LocalpartStr = lpstr
1650 var clp smtp.Localpart
1651 lp, err := smtp.ParseLocalpart(lpstr)
1653 addErrorf("domain %q: parsing localpart %q for alias: %v", d, lpstr, err)
1655 } else if domain.LocalpartCatchallSeparator != "" && strings.Contains(string(lp), domain.LocalpartCatchallSeparator) {
1656 addErrorf("domain %q: alias %q contains localpart catchall separator", d, a.LocalpartStr)
1659 clp = CanonicalLocalpart(lp, domain)
1662 addr := smtp.NewAddress(clp, domain.Domain).Pack(true)
1663 if _, ok := aliases[addr]; ok {
1664 addErrorf("domain %q: duplicate alias address %q", d, addr)
1667 if _, ok := accDests[addr]; ok {
1668 addErrorf("domain %q: alias %q already present as regular address", d, addr)
1671 if len(a.Addresses) == 0 {
1672 // Not currently possible, Addresses isn't optional.
1673 addErrorf("domain %q: alias %q needs at least one destination address", d, addr)
1676 a.ParsedAddresses = make([]config.AliasAddress, 0, len(a.Addresses))
1677 seen := map[string]bool{}
1678 for _, destAddr := range a.Addresses {
1679 da, err := smtp.ParseAddress(destAddr)
1681 addErrorf("domain %q: parsing destination address %q in alias %q: %v", d, destAddr, addr, err)
1684 dastr := da.Pack(true)
1685 accDest, ok := accDests[dastr]
1687 addErrorf("domain %q: alias %q references non-existent address %q", d, addr, destAddr)
1691 addErrorf("domain %q: alias %q has duplicate address %q", d, addr, destAddr)
1695 aa := config.AliasAddress{Address: da, AccountName: accDest.Account, Destination: accDest.Destination}
1696 a.ParsedAddresses = append(a.ParsedAddresses, aa)
1698 a.Domain = domain.Domain
1699 c.Domains[d].Aliases[lpstr] = a
1702 for _, aa := range a.ParsedAddresses {
1703 acc := c.Accounts[aa.AccountName]
1706 addrs = make([]string, len(a.ParsedAddresses))
1707 for i := range a.ParsedAddresses {
1708 addrs[i] = a.ParsedAddresses[i].Address.Pack(true)
1711 // Keep the non-sensitive fields.
1712 accAlias := config.Alias{
1713 PostPublic: a.PostPublic,
1714 ListMembers: a.ListMembers,
1715 AllowMsgFrom: a.AllowMsgFrom,
1716 LocalpartStr: a.LocalpartStr,
1719 acc.Aliases = append(acc.Aliases, config.AddressAlias{SubscriptionAddress: aa.Address.Pack(true), Alias: accAlias, MemberAddresses: addrs})
1720 c.Accounts[aa.AccountName] = acc
1725 // Check webserver configs.
1726 if (len(c.WebDomainRedirects) > 0 || len(c.WebHandlers) > 0) && !haveWebserverListener {
1727 addErrorf("WebDomainRedirects or WebHandlers configured but no listener with WebserverHTTP or WebserverHTTPS enabled")
1730 c.WebDNSDomainRedirects = map[dns.Domain]dns.Domain{}
1731 for from, to := range c.WebDomainRedirects {
1732 fromdom, err := dns.ParseDomain(from)
1734 addErrorf("parsing domain for redirect %s: %v", from, err)
1736 todom, err := dns.ParseDomain(to)
1738 addErrorf("parsing domain for redirect %s: %v", to, err)
1739 } else if fromdom == todom {
1740 addErrorf("will not redirect domain %s to itself", todom)
1742 var zerodom dns.Domain
1743 if _, ok := c.WebDNSDomainRedirects[fromdom]; ok && fromdom != zerodom {
1744 addErrorf("duplicate redirect domain %s", from)
1746 c.WebDNSDomainRedirects[fromdom] = todom
1749 for i := range c.WebHandlers {
1750 wh := &c.WebHandlers[i]
1752 if wh.LogName == "" {
1753 wh.Name = fmt.Sprintf("%d", i)
1755 wh.Name = wh.LogName
1758 dom, err := dns.ParseDomain(wh.Domain)
1760 addErrorf("webhandler %s %s: parsing domain: %v", wh.Domain, wh.PathRegexp, err)
1764 if !strings.HasPrefix(wh.PathRegexp, "^") {
1765 addErrorf("webhandler %s %s: path regexp must start with a ^", wh.Domain, wh.PathRegexp)
1767 re, err := regexp.Compile(wh.PathRegexp)
1769 addErrorf("webhandler %s %s: compiling regexp: %v", wh.Domain, wh.PathRegexp, err)
1774 if wh.WebStatic != nil {
1777 if ws.StripPrefix != "" && !strings.HasPrefix(ws.StripPrefix, "/") {
1778 addErrorf("webstatic %s %s: prefix to strip %s must start with a slash", wh.Domain, wh.PathRegexp, ws.StripPrefix)
1780 for k := range ws.ResponseHeaders {
1782 k := strings.TrimSpace(xk)
1783 if k != xk || k == "" {
1784 addErrorf("webstatic %s %s: bad header %q", wh.Domain, wh.PathRegexp, xk)
1788 if wh.WebRedirect != nil {
1790 wr := wh.WebRedirect
1791 if wr.BaseURL != "" {
1792 u, err := url.Parse(wr.BaseURL)
1794 addErrorf("webredirect %s %s: parsing redirect url %s: %v", wh.Domain, wh.PathRegexp, wr.BaseURL, err)
1800 addErrorf("webredirect %s %s: BaseURL must have empty path", wh.Domain, wh.PathRegexp, wr.BaseURL)
1804 if wr.OrigPathRegexp != "" && wr.ReplacePath != "" {
1805 re, err := regexp.Compile(wr.OrigPathRegexp)
1807 addErrorf("webredirect %s %s: compiling regexp %s: %v", wh.Domain, wh.PathRegexp, wr.OrigPathRegexp, err)
1810 } else if wr.OrigPathRegexp != "" || wr.ReplacePath != "" {
1811 addErrorf("webredirect %s %s: must have either both OrigPathRegexp and ReplacePath, or neither", wh.Domain, wh.PathRegexp)
1812 } else if wr.BaseURL == "" {
1813 addErrorf("webredirect %s %s: must at least one of BaseURL and OrigPathRegexp+ReplacePath", wh.Domain, wh.PathRegexp)
1815 if wr.StatusCode != 0 && (wr.StatusCode < 300 || wr.StatusCode >= 400) {
1816 addErrorf("webredirect %s %s: invalid redirect status code %d", wh.Domain, wh.PathRegexp, wr.StatusCode)
1819 if wh.WebForward != nil {
1822 u, err := url.Parse(wf.URL)
1824 addErrorf("webforward %s %s: parsing url %s: %v", wh.Domain, wh.PathRegexp, wf.URL, err)
1828 for k := range wf.ResponseHeaders {
1830 k := strings.TrimSpace(xk)
1831 if k != xk || k == "" {
1832 addErrorf("webforward %s %s: bad header %q", wh.Domain, wh.PathRegexp, xk)
1836 if wh.WebInternal != nil {
1838 wi := wh.WebInternal
1839 if !strings.HasPrefix(wi.BasePath, "/") || !strings.HasSuffix(wi.BasePath, "/") {
1840 addErrorf("webinternal %s %s: base path %q must start and end with /", wh.Domain, wh.PathRegexp, wi.BasePath)
1842 // todo: we could make maxMsgSize and accountPath configurable
1843 const isForwarded = false
1846 wi.Handler = NewWebadminHandler(wi.BasePath, isForwarded)
1848 wi.Handler = NewWebaccountHandler(wi.BasePath, isForwarded)
1851 wi.Handler = NewWebmailHandler(config.DefaultMaxMsgSize, wi.BasePath, isForwarded, accountPath)
1853 wi.Handler = NewWebapiHandler(config.DefaultMaxMsgSize, wi.BasePath, isForwarded)
1855 addErrorf("webinternal %s %s: unknown service %q", wh.Domain, wh.PathRegexp, wi.Service)
1857 wi.Handler = SafeHeaders(http.StripPrefix(wi.BasePath[:len(wi.BasePath)-1], wi.Handler))
1860 addErrorf("webhandler %s %s: must have exactly one handler, not %d", wh.Domain, wh.PathRegexp, n)
1864 c.MonitorDNSBLZones = nil
1865 for _, s := range c.MonitorDNSBLs {
1866 d, err := dns.ParseDomain(s)
1868 addErrorf("invalid monitor dnsbl zone %s: %v", s, err)
1871 if slices.Contains(c.MonitorDNSBLZones, d) {
1872 addErrorf("duplicate zone %s in monitor dnsbl zones", d)
1875 c.MonitorDNSBLZones = append(c.MonitorDNSBLZones, d)
1881func loadPrivateKeyFile(keyPath string) (crypto.Signer, error) {
1882 keyBuf, err := os.ReadFile(keyPath)
1884 return nil, fmt.Errorf("reading host private key: %v", err)
1886 b, _ := pem.Decode(keyBuf)
1888 return nil, fmt.Errorf("parsing pem block for private key: %v", err)
1893 privKey, err = x509.ParsePKCS8PrivateKey(b.Bytes)
1894 case "RSA PRIVATE KEY":
1895 privKey, err = x509.ParsePKCS1PrivateKey(b.Bytes)
1896 case "EC PRIVATE KEY":
1897 privKey, err = x509.ParseECPrivateKey(b.Bytes)
1899 err = fmt.Errorf("unknown pem type %q", b.Type)
1902 return nil, fmt.Errorf("parsing private key: %v", err)
1904 if k, ok := privKey.(crypto.Signer); ok {
1907 return nil, fmt.Errorf("parsed private key not a crypto.Signer, but %T", privKey)
1910func loadTLSKeyCerts(configFile, kind string, ctls *config.TLS) error {
1911 certs := []tls.Certificate{}
1912 for _, kp := range ctls.KeyCerts {
1913 certPath := configDirPath(configFile, kp.CertFile)
1914 keyPath := configDirPath(configFile, kp.KeyFile)
1915 cert, err := loadX509KeyPairPrivileged(certPath, keyPath)
1917 return fmt.Errorf("tls config for %q: parsing x509 key pair: %v", kind, err)
1919 certs = append(certs, cert)
1921 ctls.Config = &tls.Config{
1922 Certificates: certs,
1927// load x509 key/cert files from file descriptor possibly passed in by privileged
1929func loadX509KeyPairPrivileged(certPath, keyPath string) (tls.Certificate, error) {
1930 certBuf, err := readFilePrivileged(certPath)
1932 return tls.Certificate{}, fmt.Errorf("reading tls certificate: %v", err)
1934 keyBuf, err := readFilePrivileged(keyPath)
1936 return tls.Certificate{}, fmt.Errorf("reading tls key: %v", err)
1938 return tls.X509KeyPair(certBuf, keyBuf)
1941// like os.ReadFile, but open privileged file possibly passed in by root process.
1942func readFilePrivileged(path string) ([]byte, error) {
1943 f, err := OpenPrivileged(path)
1948 return io.ReadAll(f)