10 cryptorand "crypto/rand"
34 "golang.org/x/text/unicode/norm"
36 "github.com/mjl-/autocert"
38 "github.com/mjl-/sconf"
40 "github.com/mjl-/mox/autotls"
41 "github.com/mjl-/mox/config"
42 "github.com/mjl-/mox/dkim"
43 "github.com/mjl-/mox/dns"
44 "github.com/mjl-/mox/message"
45 "github.com/mjl-/mox/mlog"
46 "github.com/mjl-/mox/moxio"
47 "github.com/mjl-/mox/mtasts"
48 "github.com/mjl-/mox/smtp"
51var pkglog = mlog.New("mox", nil)
53// Pedantic enables stricter parsing.
56// Config paths are set early in program startup. They will point to files in
59 ConfigStaticPath string
60 ConfigDynamicPath string
61 Conf = Config{Log: map[string]slog.Level{"": slog.LevelError}}
64var ErrConfig = errors.New("config error")
66// Set by packages webadmin, webaccount, webmail, webapisrv to prevent cyclic dependencies.
67var NewWebadminHandler = func(basePath string, isForwarded bool) http.Handler { return nopHandler }
68var NewWebaccountHandler = func(basePath string, isForwarded bool) http.Handler { return nopHandler }
69var NewWebmailHandler = func(maxMsgSize int64, basePath string, isForwarded bool, accountPath string) http.Handler {
72var NewWebapiHandler = func(maxMsgSize int64, basePath string, isForwarded bool) http.Handler { return nopHandler }
74var nopHandler = http.HandlerFunc(nil)
76// Config as used in the code, a processed version of what is in the config file.
78// Use methods to lookup a domain/account/address in the dynamic configuration.
80 Static config.Static // Does not change during the lifetime of a running instance.
82 logMutex sync.Mutex // For accessing the log levels.
83 Log map[string]slog.Level
85 dynamicMutex sync.Mutex
86 Dynamic config.Dynamic // Can only be accessed directly by tests. Use methods on Config for locked access.
87 dynamicMtime time.Time
88 DynamicLastCheck time.Time // For use by quickstart only to skip checks.
90 // From canonical full address (localpart@domain, lower-cased when
91 // case-insensitive, stripped of catchall separator) to account and address.
92 // Domains are IDNA names in utf8. Dynamic config lock must be held when accessing.
93 AccountDestinationsLocked map[string]AccountDestination
95 // Like AccountDestinationsLocked, but for aliases.
96 aliases map[string]config.Alias
99type AccountDestination struct {
100 Catchall bool // If catchall destination for its domain.
101 Localpart smtp.Localpart // In original casing as written in config file.
103 Destination config.Destination
106// LogLevelSet sets a new log level for pkg. An empty pkg sets the default log
107// value that is used if no explicit log level is configured for a package.
108// This change is ephemeral, no config file is changed.
109func (c *Config) LogLevelSet(log mlog.Log, pkg string, level slog.Level) {
111 defer c.logMutex.Unlock()
112 l := c.copyLogLevels()
115 log.Print("log level changed", slog.String("pkg", pkg), slog.Any("level", mlog.LevelStrings[level]))
116 mlog.SetConfig(c.Log)
119// LogLevelRemove removes a configured log level for a package.
120func (c *Config) LogLevelRemove(log mlog.Log, pkg string) {
122 defer c.logMutex.Unlock()
123 l := c.copyLogLevels()
126 log.Print("log level cleared", slog.String("pkg", pkg))
127 mlog.SetConfig(c.Log)
130// copyLogLevels returns a copy of c.Log, for modifications.
131// must be called with log lock held.
132func (c *Config) copyLogLevels() map[string]slog.Level {
133 m := map[string]slog.Level{}
134 for pkg, level := range c.Log {
140// LogLevels returns a copy of the current log levels.
141func (c *Config) LogLevels() map[string]slog.Level {
143 defer c.logMutex.Unlock()
144 return c.copyLogLevels()
147// DynamicLockUnlock locks the dynamic config, will try updating the latest state
148// from disk, and return an unlock function. Should be called as "defer
149// Conf.DynamicLockUnlock()()".
150func (c *Config) DynamicLockUnlock() func() {
151 c.dynamicMutex.Lock()
153 if now.Sub(c.DynamicLastCheck) > time.Second {
154 c.DynamicLastCheck = now
155 if fi, err := os.Stat(ConfigDynamicPath); err != nil {
156 pkglog.Errorx("stat domains config", err)
157 } else if !fi.ModTime().Equal(c.dynamicMtime) {
158 if errs := c.loadDynamic(); len(errs) > 0 {
159 pkglog.Errorx("loading domains config", errs[0], slog.Any("errors", errs))
161 pkglog.Info("domains config reloaded")
162 c.dynamicMtime = fi.ModTime()
166 return c.dynamicMutex.Unlock
169func (c *Config) withDynamicLock(fn func()) {
170 defer c.DynamicLockUnlock()()
174// must be called with dynamic lock held.
175func (c *Config) loadDynamic() []error {
176 d, mtime, accDests, aliases, err := ParseDynamicConfig(context.Background(), pkglog, ConfigDynamicPath, c.Static)
181 c.dynamicMtime = mtime
182 c.AccountDestinationsLocked = accDests
184 c.allowACMEHosts(pkglog, true)
188// DynamicConfig returns a shallow copy of the dynamic config. Must not be modified.
189func (c *Config) DynamicConfig() (config config.Dynamic) {
190 c.withDynamicLock(func() {
191 config = c.Dynamic // Shallow copy.
196func (c *Config) Domains() (l []string) {
197 c.withDynamicLock(func() {
198 for name := range c.Dynamic.Domains {
202 sort.Slice(l, func(i, j int) bool {
208func (c *Config) Accounts() (l []string) {
209 c.withDynamicLock(func() {
210 for name := range c.Dynamic.Accounts {
217// DomainLocalparts returns a mapping of encoded localparts to account names for a
218// domain, and encoded localparts to aliases. An empty localpart is a catchall
219// destination for a domain.
220func (c *Config) DomainLocalparts(d dns.Domain) (map[string]string, map[string]config.Alias) {
221 suffix := "@" + d.Name()
222 m := map[string]string{}
223 aliases := map[string]config.Alias{}
224 c.withDynamicLock(func() {
225 for addr, ad := range c.AccountDestinationsLocked {
226 if strings.HasSuffix(addr, suffix) {
230 m[ad.Localpart.String()] = ad.Account
234 for addr, a := range c.aliases {
235 if strings.HasSuffix(addr, suffix) {
236 aliases[a.LocalpartStr] = a
243func (c *Config) Domain(d dns.Domain) (dom config.Domain, ok bool) {
244 c.withDynamicLock(func() {
245 dom, ok = c.Dynamic.Domains[d.Name()]
250func (c *Config) Account(name string) (acc config.Account, ok bool) {
251 c.withDynamicLock(func() {
252 acc, ok = c.Dynamic.Accounts[name]
257func (c *Config) AccountDestination(addr string) (accDest AccountDestination, alias *config.Alias, ok bool) {
258 c.withDynamicLock(func() {
259 accDest, ok = c.AccountDestinationsLocked[addr]
262 a, ok = c.aliases[addr]
271func (c *Config) Routes(accountName string, domain dns.Domain) (accountRoutes, domainRoutes, globalRoutes []config.Route) {
272 c.withDynamicLock(func() {
273 acc := c.Dynamic.Accounts[accountName]
274 accountRoutes = acc.Routes
276 dom := c.Dynamic.Domains[domain.Name()]
277 domainRoutes = dom.Routes
279 globalRoutes = c.Dynamic.Routes
284func (c *Config) IsClientSettingsDomain(d dns.Domain) (is bool) {
285 c.withDynamicLock(func() {
286 _, is = c.Dynamic.ClientSettingDomains[d]
291func (c *Config) allowACMEHosts(log mlog.Log, checkACMEHosts bool) {
292 for _, l := range c.Static.Listeners {
293 if l.TLS == nil || l.TLS.ACME == "" {
297 m := c.Static.ACME[l.TLS.ACME].Manager
298 hostnames := map[dns.Domain]struct{}{}
300 hostnames[c.Static.HostnameDomain] = struct{}{}
301 if l.HostnameDomain.ASCII != "" {
302 hostnames[l.HostnameDomain] = struct{}{}
305 for _, dom := range c.Dynamic.Domains {
306 // Do not allow TLS certificates for domains for which we only accept DMARC/TLS
307 // reports as external party.
312 if l.AutoconfigHTTPS.Enabled && !l.AutoconfigHTTPS.NonTLS {
313 if d, err := dns.ParseDomain("autoconfig." + dom.Domain.ASCII); err != nil {
314 log.Errorx("parsing autoconfig domain", err, slog.Any("domain", dom.Domain))
316 hostnames[d] = struct{}{}
320 if l.MTASTSHTTPS.Enabled && dom.MTASTS != nil && !l.MTASTSHTTPS.NonTLS {
321 d, err := dns.ParseDomain("mta-sts." + dom.Domain.ASCII)
323 log.Errorx("parsing mta-sts domain", err, slog.Any("domain", dom.Domain))
325 hostnames[d] = struct{}{}
329 if dom.ClientSettingsDomain != "" {
330 hostnames[dom.ClientSettingsDNSDomain] = struct{}{}
334 if l.WebserverHTTPS.Enabled {
335 for from := range c.Dynamic.WebDNSDomainRedirects {
336 hostnames[from] = struct{}{}
338 for _, wh := range c.Dynamic.WebHandlers {
339 hostnames[wh.DNSDomain] = struct{}{}
343 public := c.Static.Listeners["public"]
345 if len(public.NATIPs) > 0 {
351 m.SetAllowedHostnames(log, dns.StrictResolver{Pkg: "autotls", Log: log.Logger}, hostnames, ips, checkACMEHosts)
355// 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.
357// WriteDynamicLocked prepares an updated internal state for the new dynamic
358// config, then writes it to disk and activates it.
360// Returns ErrConfig if the configuration is not valid.
362// Must be called with config lock held.
363func WriteDynamicLocked(ctx context.Context, log mlog.Log, c config.Dynamic) error {
364 accDests, aliases, errs := prepareDynamicConfig(ctx, log, ConfigDynamicPath, Conf.Static, &c)
366 errstrs := make([]string, len(errs))
367 for i, err := range errs {
368 errstrs[i] = err.Error()
370 return fmt.Errorf("%w: %s", ErrConfig, strings.Join(errstrs, "; "))
374 err := sconf.Write(&b, c)
378 f, err := os.OpenFile(ConfigDynamicPath, os.O_WRONLY, 0660)
385 log.Check(err, "closing file after error")
389 if _, err := f.Write(buf); err != nil {
390 return fmt.Errorf("write domains.conf: %v", err)
392 if err := f.Truncate(int64(len(buf))); err != nil {
393 return fmt.Errorf("truncate domains.conf after write: %v", err)
395 if err := f.Sync(); err != nil {
396 return fmt.Errorf("sync domains.conf after write: %v", err)
398 if err := moxio.SyncDir(log, filepath.Dir(ConfigDynamicPath)); err != nil {
399 return fmt.Errorf("sync dir of domains.conf after write: %v", err)
404 return fmt.Errorf("stat after writing domains.conf: %v", err)
407 if err := f.Close(); err != nil {
408 return fmt.Errorf("close written domains.conf: %v", err)
412 Conf.dynamicMtime = fi.ModTime()
413 Conf.DynamicLastCheck = time.Now()
415 Conf.AccountDestinationsLocked = accDests
416 Conf.aliases = aliases
418 Conf.allowACMEHosts(log, true)
423// MustLoadConfig loads the config, quitting on errors.
424func MustLoadConfig(doLoadTLSKeyCerts, checkACMEHosts bool) {
425 errs := LoadConfig(context.Background(), pkglog, doLoadTLSKeyCerts, checkACMEHosts)
427 pkglog.Error("loading config file: multiple errors")
428 for _, err := range errs {
429 pkglog.Errorx("config error", err)
431 pkglog.Fatal("stopping after multiple config errors")
432 } else if len(errs) == 1 {
433 pkglog.Fatalx("loading config file", errs[0])
437// LoadConfig attempts to parse and load a config, returning any errors
439func LoadConfig(ctx context.Context, log mlog.Log, doLoadTLSKeyCerts, checkACMEHosts bool) []error {
440 Shutdown, ShutdownCancel = context.WithCancel(context.Background())
441 Context, ContextCancel = context.WithCancel(context.Background())
443 c, errs := ParseConfig(ctx, log, ConfigStaticPath, false, doLoadTLSKeyCerts, checkACMEHosts)
448 mlog.SetConfig(c.Log)
453// SetConfig sets a new config. Not to be used during normal operation.
454func SetConfig(c *Config) {
455 // Cannot just assign *c to Conf, it would copy the mutex.
456 Conf = Config{c.Static, sync.Mutex{}, c.Log, sync.Mutex{}, c.Dynamic, c.dynamicMtime, c.DynamicLastCheck, c.AccountDestinationsLocked, c.aliases}
458 // If we have non-standard CA roots, use them for all HTTPS requests.
459 if Conf.Static.TLS.CertPool != nil {
460 http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{
461 RootCAs: Conf.Static.TLS.CertPool,
465 SetPedantic(c.Static.Pedantic)
468// Set pedantic in all packages.
469func SetPedantic(p bool) {
477// ParseConfig parses the static config at path p. If checkOnly is true, no changes
478// are made, such as registering ACME identities. If doLoadTLSKeyCerts is true,
479// the TLS KeyCerts configuration is loaded and checked. This is used during the
480// quickstart in the case the user is going to provide their own certificates.
481// If checkACMEHosts is true, the hosts allowed for acme are compared with the
482// explicitly configured ips we are listening on.
483func ParseConfig(ctx context.Context, log mlog.Log, p string, checkOnly, doLoadTLSKeyCerts, checkACMEHosts bool) (c *Config, errs []error) {
485 Static: config.Static{
492 if os.IsNotExist(err) && os.Getenv("MOXCONF") == "" {
493 return nil, []error{fmt.Errorf("open config file: %v (hint: use mox -config ... or set MOXCONF=...)", err)}
495 return nil, []error{fmt.Errorf("open config file: %v", err)}
498 if err := sconf.Parse(f, &c.Static); err != nil {
499 return nil, []error{fmt.Errorf("parsing %s%v", p, err)}
502 if xerrs := PrepareStaticConfig(ctx, log, p, c, checkOnly, doLoadTLSKeyCerts); len(xerrs) > 0 {
506 pp := filepath.Join(filepath.Dir(p), "domains.conf")
507 c.Dynamic, c.dynamicMtime, c.AccountDestinationsLocked, c.aliases, errs = ParseDynamicConfig(ctx, log, pp, c.Static)
510 c.allowACMEHosts(log, checkACMEHosts)
516// PrepareStaticConfig parses the static config file and prepares data structures
517// for starting mox. If checkOnly is set no substantial changes are made, like
518// creating an ACME registration.
519func PrepareStaticConfig(ctx context.Context, log mlog.Log, configFile string, conf *Config, checkOnly, doLoadTLSKeyCerts bool) (errs []error) {
520 addErrorf := func(format string, args ...any) {
521 errs = append(errs, fmt.Errorf(format, args...))
526 // check that mailbox is in unicode NFC normalized form.
527 checkMailboxNormf := func(mailbox string, format string, args ...any) {
528 s := norm.NFC.String(mailbox)
530 msg := fmt.Sprintf(format, args...)
531 addErrorf("%s: mailbox %q is not in NFC normalized form, should be %q", msg, mailbox, s)
535 // Post-process logging config.
536 if logLevel, ok := mlog.Levels[c.LogLevel]; ok {
537 conf.Log = map[string]slog.Level{"": logLevel}
539 addErrorf("invalid log level %q", c.LogLevel)
541 for pkg, s := range c.PackageLogLevels {
542 if logLevel, ok := mlog.Levels[s]; ok {
543 conf.Log[pkg] = logLevel
545 addErrorf("invalid package log level %q", s)
552 u, err := user.Lookup(c.User)
554 uid, err := strconv.ParseUint(c.User, 10, 32)
556 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)
558 // We assume the same gid as uid.
563 if uid, err := strconv.ParseUint(u.Uid, 10, 32); err != nil {
564 addErrorf("parsing uid %s: %v", u.Uid, err)
568 if gid, err := strconv.ParseUint(u.Gid, 10, 32); err != nil {
569 addErrorf("parsing gid %s: %v", u.Gid, err)
575 hostname, err := dns.ParseDomain(c.Hostname)
577 addErrorf("parsing hostname: %s", err)
578 } else if hostname.Name() != c.Hostname {
579 addErrorf("hostname must be in unicode form %q instead of %q", hostname.Name(), c.Hostname)
581 c.HostnameDomain = hostname
583 if c.HostTLSRPT.Account != "" {
584 tlsrptLocalpart, err := smtp.ParseLocalpart(c.HostTLSRPT.Localpart)
586 addErrorf("invalid localpart %q for host tlsrpt: %v", c.HostTLSRPT.Localpart, err)
587 } else if tlsrptLocalpart.IsInternational() {
588 // Does not appear documented in
../rfc/8460, but similar to DMARC it makes sense
589 // to keep this ascii-only addresses.
590 addErrorf("host TLSRPT localpart %q is an internationalized address, only conventional ascii-only address allowed for interopability", tlsrptLocalpart)
592 c.HostTLSRPT.ParsedLocalpart = tlsrptLocalpart
595 // Return private key for host name for use with an ACME. Used to return the same
596 // private key as pre-generated for use with DANE, with its public key in DNS.
597 // We only use this key for Listener's that have this ACME configured, and for
598 // which the effective listener host name (either specific to the listener, or the
599 // global name) is requested. Other host names can get a fresh private key, they
600 // don't appear in DANE records.
602 // - run 0: only use listener with explicitly matching host name in listener
603 // (default quickstart config does not set it).
604 // - run 1: only look at public listener (and host matching mox host name)
605 // - run 2: all listeners (and host matching mox host name)
606 findACMEHostPrivateKey := func(acmeName, host string, keyType autocert.KeyType, run int) crypto.Signer {
607 for listenerName, l := range Conf.Static.Listeners {
608 if l.TLS == nil || l.TLS.ACME != acmeName {
611 if run == 0 && host != l.HostnameDomain.ASCII {
614 if run == 1 && listenerName != "public" || host != Conf.Static.HostnameDomain.ASCII {
618 case autocert.KeyRSA2048:
619 if len(l.TLS.HostPrivateRSA2048Keys) == 0 {
622 return l.TLS.HostPrivateRSA2048Keys[0]
623 case autocert.KeyECDSAP256:
624 if len(l.TLS.HostPrivateECDSAP256Keys) == 0 {
627 return l.TLS.HostPrivateECDSAP256Keys[0]
634 // Make a function for an autocert.Manager.GetPrivateKey, using findACMEHostPrivateKey.
635 makeGetPrivateKey := func(acmeName string) func(host string, keyType autocert.KeyType) (crypto.Signer, error) {
636 return func(host string, keyType autocert.KeyType) (crypto.Signer, error) {
637 key := findACMEHostPrivateKey(acmeName, host, keyType, 0)
639 key = findACMEHostPrivateKey(acmeName, host, keyType, 1)
642 key = findACMEHostPrivateKey(acmeName, host, keyType, 2)
645 log.Debug("found existing private key for certificate for host",
646 slog.String("acmename", acmeName),
647 slog.String("host", host),
648 slog.Any("keytype", keyType))
651 log.Debug("generating new private key for certificate for host",
652 slog.String("acmename", acmeName),
653 slog.String("host", host),
654 slog.Any("keytype", keyType))
656 case autocert.KeyRSA2048:
657 return rsa.GenerateKey(cryptorand.Reader, 2048)
658 case autocert.KeyECDSAP256:
659 return ecdsa.GenerateKey(elliptic.P256(), cryptorand.Reader)
661 return nil, fmt.Errorf("unrecognized requested key type %v", keyType)
665 for name, acme := range c.ACME {
668 if acme.ExternalAccountBinding != nil {
669 eabKeyID = acme.ExternalAccountBinding.KeyID
670 p := configDirPath(configFile, acme.ExternalAccountBinding.KeyFile)
671 buf, err := os.ReadFile(p)
673 addErrorf("reading external account binding key for acme provider %q: %s", name, err)
675 dec := make([]byte, base64.RawURLEncoding.DecodedLen(len(buf)))
676 n, err := base64.RawURLEncoding.Decode(dec, buf)
678 addErrorf("parsing external account binding key as base64 for acme provider %q: %s", name, err)
689 acmeDir := dataDirPath(configFile, c.DataDir, "acme")
690 os.MkdirAll(acmeDir, 0770)
691 manager, err := autotls.Load(name, acmeDir, acme.ContactEmail, acme.DirectoryURL, eabKeyID, eabKey, makeGetPrivateKey(name), Shutdown.Done())
693 addErrorf("loading ACME identity for %q: %s", name, err)
695 acme.Manager = manager
697 // Help configurations from older quickstarts.
698 if acme.IssuerDomainName == "" && acme.DirectoryURL == "https://acme-v02.api.letsencrypt.org/directory" {
699 acme.IssuerDomainName = "letsencrypt.org"
705 var haveUnspecifiedSMTPListener bool
706 for name, l := range c.Listeners {
707 if l.Hostname != "" {
708 d, err := dns.ParseDomain(l.Hostname)
710 addErrorf("bad listener hostname %q: %s", l.Hostname, err)
715 if l.TLS.ACME != "" && len(l.TLS.KeyCerts) != 0 {
716 addErrorf("listener %q: cannot have ACME and static key/certificates", name)
717 } else if l.TLS.ACME != "" {
718 acme, ok := c.ACME[l.TLS.ACME]
720 addErrorf("listener %q: unknown ACME provider %q", name, l.TLS.ACME)
723 // If only checking or with missing ACME definition, we don't have an acme manager,
724 // so set an empty tls config to continue.
725 var tlsconfig, tlsconfigFallback *tls.Config
726 if checkOnly || acme.Manager == nil {
727 tlsconfig = &tls.Config{}
728 tlsconfigFallback = &tls.Config{}
730 hostname := c.HostnameDomain
731 if l.Hostname != "" {
732 hostname = l.HostnameDomain
734 // If SNI is absent, we will use the listener hostname, but reject connections with
735 // an SNI hostname that is not allowlisted.
736 // Incoming SMTP deliveries use tlsconfigFallback for interoperability. TLS
737 // connections for unknown SNI hostnames fall back to a certificate for the
738 // listener hostname instead of causing the TLS connection to fail.
739 tlsconfig = acme.Manager.TLSConfig(hostname, true, false)
740 tlsconfigFallback = acme.Manager.TLSConfig(hostname, true, true)
741 l.TLS.ACMEConfig = acme.Manager.ACMETLSConfig
743 l.TLS.Config = tlsconfig
744 l.TLS.ConfigFallback = tlsconfigFallback
745 } else if len(l.TLS.KeyCerts) != 0 {
746 if doLoadTLSKeyCerts {
747 if err := loadTLSKeyCerts(configFile, "listener "+name, l.TLS); err != nil {
752 addErrorf("listener %q: cannot have TLS config without ACME and without static keys/certificates", name)
754 for _, privKeyFile := range l.TLS.HostPrivateKeyFiles {
755 keyPath := configDirPath(configFile, privKeyFile)
756 privKey, err := loadPrivateKeyFile(keyPath)
758 addErrorf("listener %q: parsing host private key for DANE and ACME certificates: %v", name, err)
761 switch k := privKey.(type) {
762 case *rsa.PrivateKey:
763 if k.N.BitLen() != 2048 {
764 log.Error("need rsa key with 2048 bits, for host private key for DANE/ACME certificates, ignoring",
765 slog.String("listener", name),
766 slog.String("file", keyPath),
767 slog.Int("bits", k.N.BitLen()))
770 l.TLS.HostPrivateRSA2048Keys = append(l.TLS.HostPrivateRSA2048Keys, k)
771 case *ecdsa.PrivateKey:
772 if k.Curve != elliptic.P256() {
773 log.Error("unrecognized ecdsa curve for host private key for DANE/ACME certificates, ignoring", slog.String("listener", name), slog.String("file", keyPath))
776 l.TLS.HostPrivateECDSAP256Keys = append(l.TLS.HostPrivateECDSAP256Keys, k)
778 log.Error("unrecognized key type for host private key for DANE/ACME certificates, ignoring",
779 slog.String("listener", name),
780 slog.String("file", keyPath),
781 slog.String("keytype", fmt.Sprintf("%T", privKey)))
785 if l.TLS.ACME != "" && (len(l.TLS.HostPrivateRSA2048Keys) == 0) != (len(l.TLS.HostPrivateECDSAP256Keys) == 0) {
786 log.Warn("uncommon configuration with either only an RSA 2048 or ECDSA P256 host private key for DANE/ACME certificates; this ACME implementation can retrieve certificates for both type of keys, it is recommended to set either both or none; continuing")
790 var minVersion uint16 = tls.VersionTLS12
791 if l.TLS.MinVersion != "" {
792 versions := map[string]uint16{
793 "TLSv1.0": tls.VersionTLS10,
794 "TLSv1.1": tls.VersionTLS11,
795 "TLSv1.2": tls.VersionTLS12,
796 "TLSv1.3": tls.VersionTLS13,
798 v, ok := versions[l.TLS.MinVersion]
800 addErrorf("listener %q: unknown TLS mininum version %q", name, l.TLS.MinVersion)
804 if l.TLS.Config != nil {
805 l.TLS.Config.MinVersion = minVersion
807 if l.TLS.ConfigFallback != nil {
808 l.TLS.ConfigFallback.MinVersion = minVersion
810 if l.TLS.ACMEConfig != nil {
811 l.TLS.ACMEConfig.MinVersion = minVersion
814 var needsTLS []string
815 needtls := func(s string, v bool) {
817 needsTLS = append(needsTLS, s)
820 needtls("IMAPS", l.IMAPS.Enabled)
821 needtls("SMTP", l.SMTP.Enabled && !l.SMTP.NoSTARTTLS)
822 needtls("Submissions", l.Submissions.Enabled)
823 needtls("Submission", l.Submission.Enabled && !l.Submission.NoRequireSTARTTLS)
824 needtls("AccountHTTPS", l.AccountHTTPS.Enabled)
825 needtls("AdminHTTPS", l.AdminHTTPS.Enabled)
826 needtls("AutoconfigHTTPS", l.AutoconfigHTTPS.Enabled && !l.AutoconfigHTTPS.NonTLS)
827 needtls("MTASTSHTTPS", l.MTASTSHTTPS.Enabled && !l.MTASTSHTTPS.NonTLS)
828 needtls("WebserverHTTPS", l.WebserverHTTPS.Enabled)
829 if len(needsTLS) > 0 {
830 addErrorf("listener %q does not specify tls config, but requires tls for %s", name, strings.Join(needsTLS, ", "))
833 if l.AutoconfigHTTPS.Enabled && l.MTASTSHTTPS.Enabled && l.AutoconfigHTTPS.Port == l.MTASTSHTTPS.Port && l.AutoconfigHTTPS.NonTLS != l.MTASTSHTTPS.NonTLS {
834 addErrorf("listener %q tries to enable autoconfig and mta-sts enabled on same port but with both http and https", name)
838 haveUnspecifiedSMTPListener = true
840 for _, ipstr := range l.IPs {
841 ip := net.ParseIP(ipstr)
843 addErrorf("listener %q has invalid IP %q", name, ipstr)
846 if ip.IsUnspecified() {
847 haveUnspecifiedSMTPListener = true
850 if len(c.SpecifiedSMTPListenIPs) >= 2 {
851 haveUnspecifiedSMTPListener = true
852 } else if len(c.SpecifiedSMTPListenIPs) > 0 && (c.SpecifiedSMTPListenIPs[0].To4() == nil) == (ip.To4() == nil) {
853 haveUnspecifiedSMTPListener = true
855 c.SpecifiedSMTPListenIPs = append(c.SpecifiedSMTPListenIPs, ip)
859 for _, s := range l.SMTP.DNSBLs {
860 d, err := dns.ParseDomain(s)
862 addErrorf("listener %q has invalid DNSBL zone %q", name, s)
865 l.SMTP.DNSBLZones = append(l.SMTP.DNSBLZones, d)
867 if l.IPsNATed && len(l.NATIPs) > 0 {
868 addErrorf("listener %q has both IPsNATed and NATIPs (remove deprecated IPsNATed)", name)
870 for _, ipstr := range l.NATIPs {
871 ip := net.ParseIP(ipstr)
873 addErrorf("listener %q has invalid ip %q", name, ipstr)
874 } else if ip.IsUnspecified() || ip.IsLoopback() {
875 addErrorf("listener %q has NAT ip that is the unspecified or loopback address %s", name, ipstr)
878 checkPath := func(kind string, enabled bool, path string) {
879 if enabled && path != "" && !strings.HasPrefix(path, "/") {
880 addErrorf("listener %q has %s with path %q that must start with a slash", name, kind, path)
883 checkPath("AccountHTTP", l.AccountHTTP.Enabled, l.AccountHTTP.Path)
884 checkPath("AccountHTTPS", l.AccountHTTPS.Enabled, l.AccountHTTPS.Path)
885 checkPath("AdminHTTP", l.AdminHTTP.Enabled, l.AdminHTTP.Path)
886 checkPath("AdminHTTPS", l.AdminHTTPS.Enabled, l.AdminHTTPS.Path)
887 c.Listeners[name] = l
889 if haveUnspecifiedSMTPListener {
890 c.SpecifiedSMTPListenIPs = nil
893 var zerouse config.SpecialUseMailboxes
894 if len(c.DefaultMailboxes) > 0 && (c.InitialMailboxes.SpecialUse != zerouse || len(c.InitialMailboxes.Regular) > 0) {
895 addErrorf("cannot have both DefaultMailboxes and InitialMailboxes")
897 // DefaultMailboxes is deprecated.
898 for _, mb := range c.DefaultMailboxes {
899 checkMailboxNormf(mb, "default mailbox")
901 checkSpecialUseMailbox := func(nameOpt string) {
903 checkMailboxNormf(nameOpt, "special-use initial mailbox")
904 if strings.EqualFold(nameOpt, "inbox") {
905 addErrorf("initial mailbox cannot be set to Inbox (Inbox is always created)")
909 checkSpecialUseMailbox(c.InitialMailboxes.SpecialUse.Archive)
910 checkSpecialUseMailbox(c.InitialMailboxes.SpecialUse.Draft)
911 checkSpecialUseMailbox(c.InitialMailboxes.SpecialUse.Junk)
912 checkSpecialUseMailbox(c.InitialMailboxes.SpecialUse.Sent)
913 checkSpecialUseMailbox(c.InitialMailboxes.SpecialUse.Trash)
914 for _, name := range c.InitialMailboxes.Regular {
915 checkMailboxNormf(name, "regular initial mailbox")
916 if strings.EqualFold(name, "inbox") {
917 addErrorf("initial regular mailbox cannot be set to Inbox (Inbox is always created)")
921 checkTransportSMTP := func(name string, isTLS bool, t *config.TransportSMTP) {
923 t.DNSHost, err = dns.ParseDomain(t.Host)
925 addErrorf("transport %s: bad host %s: %v", name, t.Host, err)
928 if isTLS && t.STARTTLSInsecureSkipVerify {
929 addErrorf("transport %s: cannot have STARTTLSInsecureSkipVerify with immediate TLS")
931 if isTLS && t.NoSTARTTLS {
932 addErrorf("transport %s: cannot have NoSTARTTLS with immediate TLS")
938 seen := map[string]bool{}
939 for _, m := range t.Auth.Mechanisms {
941 addErrorf("transport %s: duplicate authentication mechanism %s", name, m)
945 case "SCRAM-SHA-256-PLUS":
946 case "SCRAM-SHA-256":
947 case "SCRAM-SHA-1-PLUS":
952 addErrorf("transport %s: unknown authentication mechanism %s", name, m)
956 t.Auth.EffectiveMechanisms = t.Auth.Mechanisms
957 if len(t.Auth.EffectiveMechanisms) == 0 {
958 t.Auth.EffectiveMechanisms = []string{"SCRAM-SHA-256-PLUS", "SCRAM-SHA-256", "SCRAM-SHA-1-PLUS", "SCRAM-SHA-1", "CRAM-MD5"}
962 checkTransportSocks := func(name string, t *config.TransportSocks) {
963 _, _, err := net.SplitHostPort(t.Address)
965 addErrorf("transport %s: bad address %s: %v", name, t.Address, err)
967 for _, ipstr := range t.RemoteIPs {
968 ip := net.ParseIP(ipstr)
970 addErrorf("transport %s: bad ip %s", name, ipstr)
972 t.IPs = append(t.IPs, ip)
975 t.Hostname, err = dns.ParseDomain(t.RemoteHostname)
977 addErrorf("transport %s: bad hostname %s: %v", name, t.RemoteHostname, err)
981 checkTransportDirect := func(name string, t *config.TransportDirect) {
982 if t.DisableIPv4 && t.DisableIPv6 {
983 addErrorf("transport %s: both IPv4 and IPv6 are disabled, enable at least one", name)
994 for name, t := range c.Transports {
996 if t.Submissions != nil {
998 checkTransportSMTP(name, true, t.Submissions)
1000 if t.Submission != nil {
1002 checkTransportSMTP(name, false, t.Submission)
1006 checkTransportSMTP(name, false, t.SMTP)
1010 checkTransportSocks(name, t.Socks)
1012 if t.Direct != nil {
1014 checkTransportDirect(name, t.Direct)
1017 addErrorf("transport %s: cannot have multiple methods in a transport", name)
1021 // Load CA certificate pool.
1022 if c.TLS.CA != nil {
1023 if c.TLS.CA.AdditionalToSystem {
1025 c.TLS.CertPool, err = x509.SystemCertPool()
1027 addErrorf("fetching system CA cert pool: %v", err)
1030 c.TLS.CertPool = x509.NewCertPool()
1032 for _, certfile := range c.TLS.CA.CertFiles {
1033 p := configDirPath(configFile, certfile)
1034 pemBuf, err := os.ReadFile(p)
1036 addErrorf("reading TLS CA cert file: %v", err)
1038 } else if !c.TLS.CertPool.AppendCertsFromPEM(pemBuf) {
1039 // todo: can we check more fully if we're getting some useful data back?
1040 addErrorf("no CA certs added from %q", p)
1047// PrepareDynamicConfig parses the dynamic config file given a static file.
1048func 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) {
1049 addErrorf := func(format string, args ...any) {
1050 errs = append(errs, fmt.Errorf(format, args...))
1053 f, err := os.Open(dynamicPath)
1055 addErrorf("parsing domains config: %v", err)
1061 addErrorf("stat domains config: %v", err)
1063 if err := sconf.Parse(f, &c); err != nil {
1064 addErrorf("parsing dynamic config file: %v", err)
1068 accDests, aliases, errs = prepareDynamicConfig(ctx, log, dynamicPath, static, &c)
1069 return c, fi.ModTime(), accDests, aliases, errs
1072func 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) {
1073 addErrorf := func(format string, args ...any) {
1074 errs = append(errs, fmt.Errorf(format, args...))
1077 // Check that mailbox is in unicode NFC normalized form.
1078 checkMailboxNormf := func(mailbox string, format string, args ...any) {
1079 s := norm.NFC.String(mailbox)
1081 msg := fmt.Sprintf(format, args...)
1082 addErrorf("%s: mailbox %q is not in NFC normalized form, should be %q", msg, mailbox, s)
1086 // Validate postmaster account exists.
1087 if _, ok := c.Accounts[static.Postmaster.Account]; !ok {
1088 addErrorf("postmaster account %q does not exist", static.Postmaster.Account)
1090 checkMailboxNormf(static.Postmaster.Mailbox, "postmaster mailbox")
1092 accDests = map[string]AccountDestination{}
1093 aliases = map[string]config.Alias{}
1095 // Validate host TLSRPT account/address.
1096 if static.HostTLSRPT.Account != "" {
1097 if _, ok := c.Accounts[static.HostTLSRPT.Account]; !ok {
1098 addErrorf("host tlsrpt account %q does not exist", static.HostTLSRPT.Account)
1100 checkMailboxNormf(static.HostTLSRPT.Mailbox, "host tlsrpt mailbox")
1102 // Localpart has been parsed already.
1104 addrFull := smtp.NewAddress(static.HostTLSRPT.ParsedLocalpart, static.HostnameDomain).String()
1105 dest := config.Destination{
1106 Mailbox: static.HostTLSRPT.Mailbox,
1107 HostTLSReports: true,
1109 accDests[addrFull] = AccountDestination{false, static.HostTLSRPT.ParsedLocalpart, static.HostTLSRPT.Account, dest}
1112 var haveSTSListener, haveWebserverListener bool
1113 for _, l := range static.Listeners {
1114 if l.MTASTSHTTPS.Enabled {
1115 haveSTSListener = true
1117 if l.WebserverHTTP.Enabled || l.WebserverHTTPS.Enabled {
1118 haveWebserverListener = true
1122 checkRoutes := func(descr string, routes []config.Route) {
1123 parseRouteDomains := func(l []string) []string {
1125 for _, e := range l {
1131 if strings.HasPrefix(e, ".") {
1135 d, err := dns.ParseDomain(e)
1137 addErrorf("%s: invalid domain %s: %v", descr, e, err)
1139 r = append(r, prefix+d.ASCII)
1144 for i := range routes {
1145 routes[i].FromDomainASCII = parseRouteDomains(routes[i].FromDomain)
1146 routes[i].ToDomainASCII = parseRouteDomains(routes[i].ToDomain)
1148 routes[i].ResolvedTransport, ok = static.Transports[routes[i].Transport]
1150 addErrorf("%s: route references undefined transport %s", descr, routes[i].Transport)
1155 checkRoutes("global routes", c.Routes)
1157 // Validate domains.
1158 c.ClientSettingDomains = map[dns.Domain]struct{}{}
1159 for d, domain := range c.Domains {
1160 dnsdomain, err := dns.ParseDomain(d)
1162 addErrorf("bad domain %q: %s", d, err)
1163 } else if dnsdomain.Name() != d {
1164 addErrorf("domain %s must be specified in unicode form, %s", d, dnsdomain.Name())
1167 domain.Domain = dnsdomain
1169 if domain.ClientSettingsDomain != "" {
1170 csd, err := dns.ParseDomain(domain.ClientSettingsDomain)
1172 addErrorf("bad client settings domain %q: %s", domain.ClientSettingsDomain, err)
1174 domain.ClientSettingsDNSDomain = csd
1175 c.ClientSettingDomains[csd] = struct{}{}
1178 for _, sign := range domain.DKIM.Sign {
1179 if _, ok := domain.DKIM.Selectors[sign]; !ok {
1180 addErrorf("selector %s for signing is missing in domain %s", sign, d)
1183 for name, sel := range domain.DKIM.Selectors {
1184 seld, err := dns.ParseDomain(name)
1186 addErrorf("bad selector %q: %s", name, err)
1187 } else if seld.Name() != name {
1188 addErrorf("selector %q must be specified in unicode form, %q", name, seld.Name())
1192 if sel.Expiration != "" {
1193 exp, err := time.ParseDuration(sel.Expiration)
1195 addErrorf("selector %q has invalid expiration %q: %v", name, sel.Expiration, err)
1197 sel.ExpirationSeconds = int(exp / time.Second)
1201 sel.HashEffective = sel.Hash
1202 switch sel.HashEffective {
1204 sel.HashEffective = "sha256"
1206 log.Error("using sha1 with DKIM is deprecated as not secure enough, switch to sha256")
1209 addErrorf("unsupported hash %q for selector %q in domain %s", sel.HashEffective, name, d)
1212 pemBuf, err := os.ReadFile(configDirPath(dynamicPath, sel.PrivateKeyFile))
1214 addErrorf("reading private key for selector %s in domain %s: %s", name, d, err)
1217 p, _ := pem.Decode(pemBuf)
1219 addErrorf("private key for selector %s in domain %s has no PEM block", name, d)
1222 key, err := x509.ParsePKCS8PrivateKey(p.Bytes)
1224 addErrorf("parsing private key for selector %s in domain %s: %s", name, d, err)
1227 switch k := key.(type) {
1228 case *rsa.PrivateKey:
1229 if k.N.BitLen() < 1024 {
1231 // Let's help user do the right thing.
1232 addErrorf("rsa keys should be >= 1024 bits")
1235 sel.Algorithm = fmt.Sprintf("rsa-%d", k.N.BitLen())
1236 case ed25519.PrivateKey:
1237 if sel.HashEffective != "sha256" {
1238 addErrorf("hash algorithm %q is not supported with ed25519, only sha256 is", sel.HashEffective)
1241 sel.Algorithm = "ed25519"
1243 addErrorf("private key type %T not yet supported, at selector %s in domain %s", key, name, d)
1246 if len(sel.Headers) == 0 {
1250 // By default we seal signed headers, and we sign user-visible headers to
1251 // prevent/limit reuse of previously signed messages: All addressing fields, date
1252 // and subject, message-referencing fields, parsing instructions (content-type).
1253 sel.HeadersEffective = strings.Split("From,To,Cc,Bcc,Reply-To,References,In-Reply-To,Subject,Date,Message-Id,Content-Type", ",")
1256 for _, h := range sel.Headers {
1257 from = from || strings.EqualFold(h, "From")
1259 if strings.EqualFold(h, "DKIM-Signature") || strings.EqualFold(h, "Received") || strings.EqualFold(h, "Return-Path") {
1260 log.Error("DKIM-signing header %q is recommended against as it may be modified in transit")
1264 addErrorf("From-field must always be DKIM-signed")
1266 sel.HeadersEffective = sel.Headers
1269 domain.DKIM.Selectors[name] = sel
1272 if domain.MTASTS != nil {
1273 if !haveSTSListener {
1274 addErrorf("MTA-STS enabled for domain %q, but there is no listener for MTASTS", d)
1276 sts := domain.MTASTS
1277 if sts.PolicyID == "" {
1278 addErrorf("invalid empty MTA-STS PolicyID")
1281 case mtasts.ModeNone, mtasts.ModeTesting, mtasts.ModeEnforce:
1283 addErrorf("invalid mtasts mode %q", sts.Mode)
1287 checkRoutes("routes for domain", domain.Routes)
1289 c.Domains[d] = domain
1292 // To determine ReportsOnly.
1293 domainHasAddress := map[string]bool{}
1295 // Validate email addresses.
1296 for accName, acc := range c.Accounts {
1298 acc.DNSDomain, err = dns.ParseDomain(acc.Domain)
1300 addErrorf("parsing domain %s for account %q: %s", acc.Domain, accName, err)
1303 if strings.EqualFold(acc.RejectsMailbox, "Inbox") {
1304 addErrorf("account %q: cannot set RejectsMailbox to inbox, messages will be removed automatically from the rejects mailbox", accName)
1306 checkMailboxNormf(acc.RejectsMailbox, "account %q", accName)
1308 if acc.AutomaticJunkFlags.JunkMailboxRegexp != "" {
1309 r, err := regexp.Compile(acc.AutomaticJunkFlags.JunkMailboxRegexp)
1311 addErrorf("invalid JunkMailboxRegexp regular expression: %v", err)
1315 if acc.AutomaticJunkFlags.NeutralMailboxRegexp != "" {
1316 r, err := regexp.Compile(acc.AutomaticJunkFlags.NeutralMailboxRegexp)
1318 addErrorf("invalid NeutralMailboxRegexp regular expression: %v", err)
1320 acc.NeutralMailbox = r
1322 if acc.AutomaticJunkFlags.NotJunkMailboxRegexp != "" {
1323 r, err := regexp.Compile(acc.AutomaticJunkFlags.NotJunkMailboxRegexp)
1325 addErrorf("invalid NotJunkMailboxRegexp regular expression: %v", err)
1327 acc.NotJunkMailbox = r
1330 if acc.JunkFilter != nil {
1331 params := acc.JunkFilter.Params
1332 if params.MaxPower < 0 || params.MaxPower > 0.5 {
1333 addErrorf("junk filter MaxPower must be >= 0 and < 0.5")
1335 if params.TopWords < 0 {
1336 addErrorf("junk filter TopWords must be >= 0")
1338 if params.IgnoreWords < 0 || params.IgnoreWords > 0.5 {
1339 addErrorf("junk filter IgnoreWords must be >= 0 and < 0.5")
1341 if params.RareWords < 0 {
1342 addErrorf("junk filter RareWords must be >= 0")
1346 acc.ParsedFromIDLoginAddresses = make([]smtp.Address, len(acc.FromIDLoginAddresses))
1347 for i, s := range acc.FromIDLoginAddresses {
1348 a, err := smtp.ParseAddress(s)
1350 addErrorf("invalid fromid login address %q in account %q: %v", s, accName, err)
1352 // We check later on if address belongs to account.
1353 dom, ok := c.Domains[a.Domain.Name()]
1355 addErrorf("unknown domain in fromid login address %q for account %q", s, accName)
1356 } else if dom.LocalpartCatchallSeparator == "" {
1357 addErrorf("localpart catchall separator not configured for domain for fromid login address %q for account %q", s, accName)
1359 acc.ParsedFromIDLoginAddresses[i] = a
1362 // Clear any previously derived state.
1365 c.Accounts[accName] = acc
1367 if acc.OutgoingWebhook != nil {
1368 u, err := url.Parse(acc.OutgoingWebhook.URL)
1369 if err == nil && (u.Scheme != "http" && u.Scheme != "https") {
1370 err = errors.New("scheme must be http or https")
1373 addErrorf("parsing outgoing hook url %q in account %q: %v", acc.OutgoingWebhook.URL, accName, err)
1376 // note: outgoing hook events are in ../queue/hooks.go, ../mox-/config.go, ../queue.go and ../webapi/gendoc.sh. keep in sync.
1377 outgoingHookEvents := []string{"delivered", "suppressed", "delayed", "failed", "relayed", "expanded", "canceled", "unrecognized"}
1378 for _, e := range acc.OutgoingWebhook.Events {
1379 if !slices.Contains(outgoingHookEvents, e) {
1380 addErrorf("unknown outgoing hook event %q", e)
1384 if acc.IncomingWebhook != nil {
1385 u, err := url.Parse(acc.IncomingWebhook.URL)
1386 if err == nil && (u.Scheme != "http" && u.Scheme != "https") {
1387 err = errors.New("scheme must be http or https")
1390 addErrorf("parsing incoming hook url %q in account %q: %v", acc.IncomingWebhook.URL, accName, err)
1394 // 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.
1395 replaceLocalparts := map[string]string{}
1397 for addrName, dest := range acc.Destinations {
1398 checkMailboxNormf(dest.Mailbox, "account %q, destination %q", accName, addrName)
1400 for i, rs := range dest.Rulesets {
1401 checkMailboxNormf(rs.Mailbox, "account %q, destination %q, ruleset %d", accName, addrName, i+1)
1405 if rs.SMTPMailFromRegexp != "" {
1407 r, err := regexp.Compile(rs.SMTPMailFromRegexp)
1409 addErrorf("invalid SMTPMailFrom regular expression: %v", err)
1411 c.Accounts[accName].Destinations[addrName].Rulesets[i].SMTPMailFromRegexpCompiled = r
1413 if rs.MsgFromRegexp != "" {
1415 r, err := regexp.Compile(rs.MsgFromRegexp)
1417 addErrorf("invalid MsgFrom regular expression: %v", err)
1419 c.Accounts[accName].Destinations[addrName].Rulesets[i].MsgFromRegexpCompiled = r
1421 if rs.VerifiedDomain != "" {
1423 d, err := dns.ParseDomain(rs.VerifiedDomain)
1425 addErrorf("invalid VerifiedDomain: %v", err)
1427 c.Accounts[accName].Destinations[addrName].Rulesets[i].VerifiedDNSDomain = d
1430 var hdr [][2]*regexp.Regexp
1431 for k, v := range rs.HeadersRegexp {
1433 if strings.ToLower(k) != k {
1434 addErrorf("header field %q must only have lower case characters", k)
1436 if strings.ToLower(v) != v {
1437 addErrorf("header value %q must only have lower case characters", v)
1439 rk, err := regexp.Compile(k)
1441 addErrorf("invalid rule header regexp %q: %v", k, err)
1443 rv, err := regexp.Compile(v)
1445 addErrorf("invalid rule header regexp %q: %v", v, err)
1447 hdr = append(hdr, [...]*regexp.Regexp{rk, rv})
1449 c.Accounts[accName].Destinations[addrName].Rulesets[i].HeadersRegexpCompiled = hdr
1452 addErrorf("ruleset must have at least one rule")
1455 if rs.IsForward && rs.ListAllowDomain != "" {
1456 addErrorf("ruleset cannot have both IsForward and ListAllowDomain")
1459 if rs.SMTPMailFromRegexp == "" || rs.VerifiedDomain == "" {
1460 addErrorf("ruleset with IsForward must have both SMTPMailFromRegexp and VerifiedDomain too")
1463 if rs.ListAllowDomain != "" {
1464 d, err := dns.ParseDomain(rs.ListAllowDomain)
1466 addErrorf("invalid ListAllowDomain %q: %v", rs.ListAllowDomain, err)
1468 c.Accounts[accName].Destinations[addrName].Rulesets[i].ListAllowDNSDomain = d
1471 checkMailboxNormf(rs.AcceptRejectsToMailbox, "account %q, destination %q, ruleset %d, rejects mailbox", accName, addrName, i+1)
1472 if strings.EqualFold(rs.AcceptRejectsToMailbox, "inbox") {
1473 addErrorf("account %q, destination %q, ruleset %d: AcceptRejectsToMailbox cannot be set to Inbox", accName, addrName, i+1)
1477 // Catchall destination for domain.
1478 if strings.HasPrefix(addrName, "@") {
1479 d, err := dns.ParseDomain(addrName[1:])
1481 addErrorf("parsing domain %q in account %q", addrName[1:], accName)
1483 } else if _, ok := c.Domains[d.Name()]; !ok {
1484 addErrorf("unknown domain for address %q in account %q", addrName, accName)
1487 domainHasAddress[d.Name()] = true
1488 addrFull := "@" + d.Name()
1489 if _, ok := accDests[addrFull]; ok {
1490 addErrorf("duplicate canonicalized catchall destination address %s", addrFull)
1492 accDests[addrFull] = AccountDestination{true, "", accName, dest}
1496 // todo deprecated: remove support for parsing destination as just a localpart instead full address.
1497 var address smtp.Address
1498 if localpart, err := smtp.ParseLocalpart(addrName); err != nil && errors.Is(err, smtp.ErrBadLocalpart) {
1499 address, err = smtp.ParseAddress(addrName)
1501 addErrorf("invalid email address %q in account %q", addrName, accName)
1503 } else if _, ok := c.Domains[address.Domain.Name()]; !ok {
1504 addErrorf("unknown domain for address %q in account %q", addrName, accName)
1509 addErrorf("invalid localpart %q in account %q", addrName, accName)
1512 address = smtp.NewAddress(localpart, acc.DNSDomain)
1513 if _, ok := c.Domains[acc.DNSDomain.Name()]; !ok {
1514 addErrorf("unknown domain %s for account %q", acc.DNSDomain.Name(), accName)
1517 replaceLocalparts[addrName] = address.Pack(true)
1520 origLP := address.Localpart
1521 dc := c.Domains[address.Domain.Name()]
1522 domainHasAddress[address.Domain.Name()] = true
1523 lp := CanonicalLocalpart(address.Localpart, dc)
1524 if dc.LocalpartCatchallSeparator != "" && strings.Contains(string(address.Localpart), dc.LocalpartCatchallSeparator) {
1525 addErrorf("localpart of address %s includes domain catchall separator %s", address, dc.LocalpartCatchallSeparator)
1527 address.Localpart = lp
1529 addrFull := address.Pack(true)
1530 if _, ok := accDests[addrFull]; ok {
1531 addErrorf("duplicate canonicalized destination address %s", addrFull)
1533 accDests[addrFull] = AccountDestination{false, origLP, accName, dest}
1536 for lp, addr := range replaceLocalparts {
1537 dest, ok := acc.Destinations[lp]
1539 addErrorf("could not find localpart %q to replace with address in destinations", lp)
1541 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`,
1542 slog.Any("localpart", lp),
1543 slog.Any("address", addr),
1544 slog.String("account", accName))
1545 acc.Destinations[addr] = dest
1546 delete(acc.Destinations, lp)
1550 // Now that all addresses are parsed, check if all fromid login addresses match
1551 // configured addresses.
1552 for i, a := range acc.ParsedFromIDLoginAddresses {
1553 // For domain catchall.
1554 if _, ok := accDests["@"+a.Domain.Name()]; ok {
1557 dc := c.Domains[a.Domain.Name()]
1558 a.Localpart = CanonicalLocalpart(a.Localpart, dc)
1559 if _, ok := accDests[a.Pack(true)]; !ok {
1560 addErrorf("fromid login address %q for account %q does not match its destination addresses", acc.FromIDLoginAddresses[i], accName)
1564 checkRoutes("routes for account", acc.Routes)
1567 // Set DMARC destinations.
1568 for d, domain := range c.Domains {
1569 dmarc := domain.DMARC
1573 if _, ok := c.Accounts[dmarc.Account]; !ok {
1574 addErrorf("DMARC account %q does not exist", dmarc.Account)
1576 lp, err := smtp.ParseLocalpart(dmarc.Localpart)
1578 addErrorf("invalid DMARC localpart %q: %s", dmarc.Localpart, err)
1580 if lp.IsInternational() {
1582 addErrorf("DMARC localpart %q is an internationalized address, only conventional ascii-only address possible for interopability", lp)
1584 addrdom := domain.Domain
1585 if dmarc.Domain != "" {
1586 addrdom, err = dns.ParseDomain(dmarc.Domain)
1588 addErrorf("DMARC domain %q: %s", dmarc.Domain, err)
1589 } else if _, ok := c.Domains[addrdom.Name()]; !ok {
1590 addErrorf("unknown domain %q for DMARC address in domain %q", addrdom, d)
1593 if addrdom == domain.Domain {
1594 domainHasAddress[addrdom.Name()] = true
1597 domain.DMARC.ParsedLocalpart = lp
1598 domain.DMARC.DNSDomain = addrdom
1599 c.Domains[d] = domain
1600 addrFull := smtp.NewAddress(lp, addrdom).String()
1601 dest := config.Destination{
1602 Mailbox: dmarc.Mailbox,
1605 checkMailboxNormf(dmarc.Mailbox, "DMARC mailbox for account %q", dmarc.Account)
1606 accDests[addrFull] = AccountDestination{false, lp, dmarc.Account, dest}
1609 // Set TLSRPT destinations.
1610 for d, domain := range c.Domains {
1611 tlsrpt := domain.TLSRPT
1615 if _, ok := c.Accounts[tlsrpt.Account]; !ok {
1616 addErrorf("TLSRPT account %q does not exist", tlsrpt.Account)
1618 lp, err := smtp.ParseLocalpart(tlsrpt.Localpart)
1620 addErrorf("invalid TLSRPT localpart %q: %s", tlsrpt.Localpart, err)
1622 if lp.IsInternational() {
1623 // Does not appear documented in
../rfc/8460, but similar to DMARC it makes sense
1624 // to keep this ascii-only addresses.
1625 addErrorf("TLSRPT localpart %q is an internationalized address, only conventional ascii-only address allowed for interopability", lp)
1627 addrdom := domain.Domain
1628 if tlsrpt.Domain != "" {
1629 addrdom, err = dns.ParseDomain(tlsrpt.Domain)
1631 addErrorf("TLSRPT domain %q: %s", tlsrpt.Domain, err)
1632 } else if _, ok := c.Domains[addrdom.Name()]; !ok {
1633 addErrorf("unknown domain %q for TLSRPT address in domain %q", tlsrpt.Domain, d)
1636 if addrdom == domain.Domain {
1637 domainHasAddress[addrdom.Name()] = true
1640 domain.TLSRPT.ParsedLocalpart = lp
1641 domain.TLSRPT.DNSDomain = addrdom
1642 c.Domains[d] = domain
1643 addrFull := smtp.NewAddress(lp, addrdom).String()
1644 dest := config.Destination{
1645 Mailbox: tlsrpt.Mailbox,
1646 DomainTLSReports: true,
1648 checkMailboxNormf(tlsrpt.Mailbox, "TLSRPT mailbox for account %q", tlsrpt.Account)
1649 accDests[addrFull] = AccountDestination{false, lp, tlsrpt.Account, dest}
1652 // Set ReportsOnly for domains, based on whether we have seen addresses (possibly
1653 // from DMARC or TLS reporting).
1654 for d, domain := range c.Domains {
1655 domain.ReportsOnly = !domainHasAddress[domain.Domain.Name()]
1656 c.Domains[d] = domain
1659 // Aliases, per domain. Also add references to accounts.
1660 for d, domain := range c.Domains {
1661 for lpstr, a := range domain.Aliases {
1663 a.LocalpartStr = lpstr
1664 var clp smtp.Localpart
1665 lp, err := smtp.ParseLocalpart(lpstr)
1667 addErrorf("domain %q: parsing localpart %q for alias: %v", d, lpstr, err)
1669 } else if domain.LocalpartCatchallSeparator != "" && strings.Contains(string(lp), domain.LocalpartCatchallSeparator) {
1670 addErrorf("domain %q: alias %q contains localpart catchall separator", d, a.LocalpartStr)
1673 clp = CanonicalLocalpart(lp, domain)
1676 addr := smtp.NewAddress(clp, domain.Domain).Pack(true)
1677 if _, ok := aliases[addr]; ok {
1678 addErrorf("domain %q: duplicate alias address %q", d, addr)
1681 if _, ok := accDests[addr]; ok {
1682 addErrorf("domain %q: alias %q already present as regular address", d, addr)
1685 if len(a.Addresses) == 0 {
1686 // Not currently possible, Addresses isn't optional.
1687 addErrorf("domain %q: alias %q needs at least one destination address", d, addr)
1690 a.ParsedAddresses = make([]config.AliasAddress, 0, len(a.Addresses))
1691 seen := map[string]bool{}
1692 for _, destAddr := range a.Addresses {
1693 da, err := smtp.ParseAddress(destAddr)
1695 addErrorf("domain %q: parsing destination address %q in alias %q: %v", d, destAddr, addr, err)
1698 dastr := da.Pack(true)
1699 accDest, ok := accDests[dastr]
1701 addErrorf("domain %q: alias %q references non-existent address %q", d, addr, destAddr)
1705 addErrorf("domain %q: alias %q has duplicate address %q", d, addr, destAddr)
1709 aa := config.AliasAddress{Address: da, AccountName: accDest.Account, Destination: accDest.Destination}
1710 a.ParsedAddresses = append(a.ParsedAddresses, aa)
1712 a.Domain = domain.Domain
1713 c.Domains[d].Aliases[lpstr] = a
1716 for _, aa := range a.ParsedAddresses {
1717 acc := c.Accounts[aa.AccountName]
1720 addrs = make([]string, len(a.ParsedAddresses))
1721 for i := range a.ParsedAddresses {
1722 addrs[i] = a.ParsedAddresses[i].Address.Pack(true)
1725 // Keep the non-sensitive fields.
1726 accAlias := config.Alias{
1727 PostPublic: a.PostPublic,
1728 ListMembers: a.ListMembers,
1729 AllowMsgFrom: a.AllowMsgFrom,
1730 LocalpartStr: a.LocalpartStr,
1733 acc.Aliases = append(acc.Aliases, config.AddressAlias{SubscriptionAddress: aa.Address.Pack(true), Alias: accAlias, MemberAddresses: addrs})
1734 c.Accounts[aa.AccountName] = acc
1739 // Check webserver configs.
1740 if (len(c.WebDomainRedirects) > 0 || len(c.WebHandlers) > 0) && !haveWebserverListener {
1741 addErrorf("WebDomainRedirects or WebHandlers configured but no listener with WebserverHTTP or WebserverHTTPS enabled")
1744 c.WebDNSDomainRedirects = map[dns.Domain]dns.Domain{}
1745 for from, to := range c.WebDomainRedirects {
1746 fromdom, err := dns.ParseDomain(from)
1748 addErrorf("parsing domain for redirect %s: %v", from, err)
1750 todom, err := dns.ParseDomain(to)
1752 addErrorf("parsing domain for redirect %s: %v", to, err)
1753 } else if fromdom == todom {
1754 addErrorf("will not redirect domain %s to itself", todom)
1756 var zerodom dns.Domain
1757 if _, ok := c.WebDNSDomainRedirects[fromdom]; ok && fromdom != zerodom {
1758 addErrorf("duplicate redirect domain %s", from)
1760 c.WebDNSDomainRedirects[fromdom] = todom
1763 for i := range c.WebHandlers {
1764 wh := &c.WebHandlers[i]
1766 if wh.LogName == "" {
1767 wh.Name = fmt.Sprintf("%d", i)
1769 wh.Name = wh.LogName
1772 dom, err := dns.ParseDomain(wh.Domain)
1774 addErrorf("webhandler %s %s: parsing domain: %v", wh.Domain, wh.PathRegexp, err)
1778 if !strings.HasPrefix(wh.PathRegexp, "^") {
1779 addErrorf("webhandler %s %s: path regexp must start with a ^", wh.Domain, wh.PathRegexp)
1781 re, err := regexp.Compile(wh.PathRegexp)
1783 addErrorf("webhandler %s %s: compiling regexp: %v", wh.Domain, wh.PathRegexp, err)
1788 if wh.WebStatic != nil {
1791 if ws.StripPrefix != "" && !strings.HasPrefix(ws.StripPrefix, "/") {
1792 addErrorf("webstatic %s %s: prefix to strip %s must start with a slash", wh.Domain, wh.PathRegexp, ws.StripPrefix)
1794 for k := range ws.ResponseHeaders {
1796 k := strings.TrimSpace(xk)
1797 if k != xk || k == "" {
1798 addErrorf("webstatic %s %s: bad header %q", wh.Domain, wh.PathRegexp, xk)
1802 if wh.WebRedirect != nil {
1804 wr := wh.WebRedirect
1805 if wr.BaseURL != "" {
1806 u, err := url.Parse(wr.BaseURL)
1808 addErrorf("webredirect %s %s: parsing redirect url %s: %v", wh.Domain, wh.PathRegexp, wr.BaseURL, err)
1814 addErrorf("webredirect %s %s: BaseURL must have empty path", wh.Domain, wh.PathRegexp, wr.BaseURL)
1818 if wr.OrigPathRegexp != "" && wr.ReplacePath != "" {
1819 re, err := regexp.Compile(wr.OrigPathRegexp)
1821 addErrorf("webredirect %s %s: compiling regexp %s: %v", wh.Domain, wh.PathRegexp, wr.OrigPathRegexp, err)
1824 } else if wr.OrigPathRegexp != "" || wr.ReplacePath != "" {
1825 addErrorf("webredirect %s %s: must have either both OrigPathRegexp and ReplacePath, or neither", wh.Domain, wh.PathRegexp)
1826 } else if wr.BaseURL == "" {
1827 addErrorf("webredirect %s %s: must at least one of BaseURL and OrigPathRegexp+ReplacePath", wh.Domain, wh.PathRegexp)
1829 if wr.StatusCode != 0 && (wr.StatusCode < 300 || wr.StatusCode >= 400) {
1830 addErrorf("webredirect %s %s: invalid redirect status code %d", wh.Domain, wh.PathRegexp, wr.StatusCode)
1833 if wh.WebForward != nil {
1836 u, err := url.Parse(wf.URL)
1838 addErrorf("webforward %s %s: parsing url %s: %v", wh.Domain, wh.PathRegexp, wf.URL, err)
1842 for k := range wf.ResponseHeaders {
1844 k := strings.TrimSpace(xk)
1845 if k != xk || k == "" {
1846 addErrorf("webforward %s %s: bad header %q", wh.Domain, wh.PathRegexp, xk)
1850 if wh.WebInternal != nil {
1852 wi := wh.WebInternal
1853 if !strings.HasPrefix(wi.BasePath, "/") || !strings.HasSuffix(wi.BasePath, "/") {
1854 addErrorf("webinternal %s %s: base path %q must start and end with /", wh.Domain, wh.PathRegexp, wi.BasePath)
1856 // todo: we could make maxMsgSize and accountPath configurable
1857 const isForwarded = false
1860 wi.Handler = NewWebadminHandler(wi.BasePath, isForwarded)
1862 wi.Handler = NewWebaccountHandler(wi.BasePath, isForwarded)
1865 wi.Handler = NewWebmailHandler(config.DefaultMaxMsgSize, wi.BasePath, isForwarded, accountPath)
1867 wi.Handler = NewWebapiHandler(config.DefaultMaxMsgSize, wi.BasePath, isForwarded)
1869 addErrorf("webinternal %s %s: unknown service %q", wh.Domain, wh.PathRegexp, wi.Service)
1871 wi.Handler = SafeHeaders(http.StripPrefix(wi.BasePath[:len(wi.BasePath)-1], wi.Handler))
1874 addErrorf("webhandler %s %s: must have exactly one handler, not %d", wh.Domain, wh.PathRegexp, n)
1878 c.MonitorDNSBLZones = nil
1879 for _, s := range c.MonitorDNSBLs {
1880 d, err := dns.ParseDomain(s)
1882 addErrorf("invalid monitor dnsbl zone %s: %v", s, err)
1885 if slices.Contains(c.MonitorDNSBLZones, d) {
1886 addErrorf("duplicate zone %s in monitor dnsbl zones", d)
1889 c.MonitorDNSBLZones = append(c.MonitorDNSBLZones, d)
1895func loadPrivateKeyFile(keyPath string) (crypto.Signer, error) {
1896 keyBuf, err := os.ReadFile(keyPath)
1898 return nil, fmt.Errorf("reading host private key: %v", err)
1900 b, _ := pem.Decode(keyBuf)
1902 return nil, fmt.Errorf("parsing pem block for private key: %v", err)
1907 privKey, err = x509.ParsePKCS8PrivateKey(b.Bytes)
1908 case "RSA PRIVATE KEY":
1909 privKey, err = x509.ParsePKCS1PrivateKey(b.Bytes)
1910 case "EC PRIVATE KEY":
1911 privKey, err = x509.ParseECPrivateKey(b.Bytes)
1913 err = fmt.Errorf("unknown pem type %q", b.Type)
1916 return nil, fmt.Errorf("parsing private key: %v", err)
1918 if k, ok := privKey.(crypto.Signer); ok {
1921 return nil, fmt.Errorf("parsed private key not a crypto.Signer, but %T", privKey)
1924func loadTLSKeyCerts(configFile, kind string, ctls *config.TLS) error {
1925 certs := []tls.Certificate{}
1926 for _, kp := range ctls.KeyCerts {
1927 certPath := configDirPath(configFile, kp.CertFile)
1928 keyPath := configDirPath(configFile, kp.KeyFile)
1929 cert, err := loadX509KeyPairPrivileged(certPath, keyPath)
1931 return fmt.Errorf("tls config for %q: parsing x509 key pair: %v", kind, err)
1933 certs = append(certs, cert)
1935 ctls.Config = &tls.Config{
1936 Certificates: certs,
1938 ctls.ConfigFallback = ctls.Config
1942// load x509 key/cert files from file descriptor possibly passed in by privileged
1944func loadX509KeyPairPrivileged(certPath, keyPath string) (tls.Certificate, error) {
1945 certBuf, err := readFilePrivileged(certPath)
1947 return tls.Certificate{}, fmt.Errorf("reading tls certificate: %v", err)
1949 keyBuf, err := readFilePrivileged(keyPath)
1951 return tls.Certificate{}, fmt.Errorf("reading tls key: %v", err)
1953 return tls.X509KeyPair(certBuf, keyBuf)
1956// like os.ReadFile, but open privileged file possibly passed in by root process.
1957func readFilePrivileged(path string) ([]byte, error) {
1958 f, err := OpenPrivileged(path)
1963 return io.ReadAll(f)