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// Config as used in the code, a processed version of what is in the config file.
68// Use methods to lookup a domain/account/address in the dynamic configuration.
70 Static config.Static // Does not change during the lifetime of a running instance.
72 logMutex sync.Mutex // For accessing the log levels.
73 Log map[string]slog.Level
75 dynamicMutex sync.Mutex
76 Dynamic config.Dynamic // Can only be accessed directly by tests. Use methods on Config for locked access.
77 dynamicMtime time.Time
78 DynamicLastCheck time.Time // For use by quickstart only to skip checks.
79 // From canonical full address (localpart@domain, lower-cased when
80 // case-insensitive, stripped of catchall separator) to account and address.
81 // Domains are IDNA names in utf8.
82 accountDestinations map[string]AccountDestination
83 // Like accountDestinations, but for aliases.
84 aliases map[string]config.Alias
87type AccountDestination struct {
88 Catchall bool // If catchall destination for its domain.
89 Localpart smtp.Localpart // In original casing as written in config file.
91 Destination config.Destination
94// LogLevelSet sets a new log level for pkg. An empty pkg sets the default log
95// value that is used if no explicit log level is configured for a package.
96// This change is ephemeral, no config file is changed.
97func (c *Config) LogLevelSet(log mlog.Log, pkg string, level slog.Level) {
99 defer c.logMutex.Unlock()
100 l := c.copyLogLevels()
103 log.Print("log level changed", slog.String("pkg", pkg), slog.Any("level", mlog.LevelStrings[level]))
104 mlog.SetConfig(c.Log)
107// LogLevelRemove removes a configured log level for a package.
108func (c *Config) LogLevelRemove(log mlog.Log, pkg string) {
110 defer c.logMutex.Unlock()
111 l := c.copyLogLevels()
114 log.Print("log level cleared", slog.String("pkg", pkg))
115 mlog.SetConfig(c.Log)
118// copyLogLevels returns a copy of c.Log, for modifications.
119// must be called with log lock held.
120func (c *Config) copyLogLevels() map[string]slog.Level {
121 m := map[string]slog.Level{}
122 for pkg, level := range c.Log {
128// LogLevels returns a copy of the current log levels.
129func (c *Config) LogLevels() map[string]slog.Level {
131 defer c.logMutex.Unlock()
132 return c.copyLogLevels()
135func (c *Config) withDynamicLock(fn func()) {
136 c.dynamicMutex.Lock()
137 defer c.dynamicMutex.Unlock()
139 if now.Sub(c.DynamicLastCheck) > time.Second {
140 c.DynamicLastCheck = now
141 if fi, err := os.Stat(ConfigDynamicPath); err != nil {
142 pkglog.Errorx("stat domains config", err)
143 } else if !fi.ModTime().Equal(c.dynamicMtime) {
144 if errs := c.loadDynamic(); len(errs) > 0 {
145 pkglog.Errorx("loading domains config", errs[0], slog.Any("errors", errs))
147 pkglog.Info("domains config reloaded")
148 c.dynamicMtime = fi.ModTime()
155// must be called with dynamic lock held.
156func (c *Config) loadDynamic() []error {
157 d, mtime, accDests, aliases, err := ParseDynamicConfig(context.Background(), pkglog, ConfigDynamicPath, c.Static)
162 c.dynamicMtime = mtime
163 c.accountDestinations = accDests
165 c.allowACMEHosts(pkglog, true)
169// DynamicConfig returns a shallow copy of the dynamic config. Must not be modified.
170func (c *Config) DynamicConfig() (config config.Dynamic) {
171 c.withDynamicLock(func() {
172 config = c.Dynamic // Shallow copy.
177func (c *Config) Domains() (l []string) {
178 c.withDynamicLock(func() {
179 for name := range c.Dynamic.Domains {
183 sort.Slice(l, func(i, j int) bool {
189func (c *Config) Accounts() (l []string) {
190 c.withDynamicLock(func() {
191 for name := range c.Dynamic.Accounts {
198// DomainLocalparts returns a mapping of encoded localparts to account names for a
199// domain, and encoded localparts to aliases. An empty localpart is a catchall
200// destination for a domain.
201func (c *Config) DomainLocalparts(d dns.Domain) (map[string]string, map[string]config.Alias) {
202 suffix := "@" + d.Name()
203 m := map[string]string{}
204 aliases := map[string]config.Alias{}
205 c.withDynamicLock(func() {
206 for addr, ad := range c.accountDestinations {
207 if strings.HasSuffix(addr, suffix) {
211 m[ad.Localpart.String()] = ad.Account
215 for addr, a := range c.aliases {
216 if strings.HasSuffix(addr, suffix) {
217 aliases[a.LocalpartStr] = a
224func (c *Config) Domain(d dns.Domain) (dom config.Domain, ok bool) {
225 c.withDynamicLock(func() {
226 dom, ok = c.Dynamic.Domains[d.Name()]
231func (c *Config) Account(name string) (acc config.Account, ok bool) {
232 c.withDynamicLock(func() {
233 acc, ok = c.Dynamic.Accounts[name]
238func (c *Config) AccountDestination(addr string) (accDest AccountDestination, alias *config.Alias, ok bool) {
239 c.withDynamicLock(func() {
240 accDest, ok = c.accountDestinations[addr]
243 a, ok = c.aliases[addr]
252func (c *Config) Routes(accountName string, domain dns.Domain) (accountRoutes, domainRoutes, globalRoutes []config.Route) {
253 c.withDynamicLock(func() {
254 acc := c.Dynamic.Accounts[accountName]
255 accountRoutes = acc.Routes
257 dom := c.Dynamic.Domains[domain.Name()]
258 domainRoutes = dom.Routes
260 globalRoutes = c.Dynamic.Routes
265func (c *Config) allowACMEHosts(log mlog.Log, checkACMEHosts bool) {
266 for _, l := range c.Static.Listeners {
267 if l.TLS == nil || l.TLS.ACME == "" {
271 m := c.Static.ACME[l.TLS.ACME].Manager
272 hostnames := map[dns.Domain]struct{}{}
274 hostnames[c.Static.HostnameDomain] = struct{}{}
275 if l.HostnameDomain.ASCII != "" {
276 hostnames[l.HostnameDomain] = struct{}{}
279 for _, dom := range c.Dynamic.Domains {
280 // Do not allow TLS certificates for domains for which we only accept DMARC/TLS
281 // reports as external party.
286 if l.AutoconfigHTTPS.Enabled && !l.AutoconfigHTTPS.NonTLS {
287 if d, err := dns.ParseDomain("autoconfig." + dom.Domain.ASCII); err != nil {
288 log.Errorx("parsing autoconfig domain", err, slog.Any("domain", dom.Domain))
290 hostnames[d] = struct{}{}
294 if l.MTASTSHTTPS.Enabled && dom.MTASTS != nil && !l.MTASTSHTTPS.NonTLS {
295 d, err := dns.ParseDomain("mta-sts." + dom.Domain.ASCII)
297 log.Errorx("parsing mta-sts domain", err, slog.Any("domain", dom.Domain))
299 hostnames[d] = struct{}{}
303 if dom.ClientSettingsDomain != "" {
304 hostnames[dom.ClientSettingsDNSDomain] = struct{}{}
308 if l.WebserverHTTPS.Enabled {
309 for from := range c.Dynamic.WebDNSDomainRedirects {
310 hostnames[from] = struct{}{}
312 for _, wh := range c.Dynamic.WebHandlers {
313 hostnames[wh.DNSDomain] = struct{}{}
317 public := c.Static.Listeners["public"]
319 if len(public.NATIPs) > 0 {
325 m.SetAllowedHostnames(log, dns.StrictResolver{Pkg: "autotls", Log: log.Logger}, hostnames, ips, checkACMEHosts)
329// 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.
331// must be called with lock held.
332// Returns ErrConfig if the configuration is not valid.
333func writeDynamic(ctx context.Context, log mlog.Log, c config.Dynamic) error {
334 accDests, aliases, errs := prepareDynamicConfig(ctx, log, ConfigDynamicPath, Conf.Static, &c)
336 return fmt.Errorf("%w: %v", ErrConfig, errs[0])
340 err := sconf.Write(&b, c)
344 f, err := os.OpenFile(ConfigDynamicPath, os.O_WRONLY, 0660)
351 log.Check(err, "closing file after error")
355 if _, err := f.Write(buf); err != nil {
356 return fmt.Errorf("write domains.conf: %v", err)
358 if err := f.Truncate(int64(len(buf))); err != nil {
359 return fmt.Errorf("truncate domains.conf after write: %v", err)
361 if err := f.Sync(); err != nil {
362 return fmt.Errorf("sync domains.conf after write: %v", err)
364 if err := moxio.SyncDir(log, filepath.Dir(ConfigDynamicPath)); err != nil {
365 return fmt.Errorf("sync dir of domains.conf after write: %v", err)
370 return fmt.Errorf("stat after writing domains.conf: %v", err)
373 if err := f.Close(); err != nil {
374 return fmt.Errorf("close written domains.conf: %v", err)
378 Conf.dynamicMtime = fi.ModTime()
379 Conf.DynamicLastCheck = time.Now()
381 Conf.accountDestinations = accDests
382 Conf.aliases = aliases
384 Conf.allowACMEHosts(log, true)
389// MustLoadConfig loads the config, quitting on errors.
390func MustLoadConfig(doLoadTLSKeyCerts, checkACMEHosts bool) {
391 errs := LoadConfig(context.Background(), pkglog, doLoadTLSKeyCerts, checkACMEHosts)
393 pkglog.Error("loading config file: multiple errors")
394 for _, err := range errs {
395 pkglog.Errorx("config error", err)
397 pkglog.Fatal("stopping after multiple config errors")
398 } else if len(errs) == 1 {
399 pkglog.Fatalx("loading config file", errs[0])
403// LoadConfig attempts to parse and load a config, returning any errors
405func LoadConfig(ctx context.Context, log mlog.Log, doLoadTLSKeyCerts, checkACMEHosts bool) []error {
406 Shutdown, ShutdownCancel = context.WithCancel(context.Background())
407 Context, ContextCancel = context.WithCancel(context.Background())
409 c, errs := ParseConfig(ctx, log, ConfigStaticPath, false, doLoadTLSKeyCerts, checkACMEHosts)
414 mlog.SetConfig(c.Log)
419// SetConfig sets a new config. Not to be used during normal operation.
420func SetConfig(c *Config) {
421 // Cannot just assign *c to Conf, it would copy the mutex.
422 Conf = Config{c.Static, sync.Mutex{}, c.Log, sync.Mutex{}, c.Dynamic, c.dynamicMtime, c.DynamicLastCheck, c.accountDestinations, c.aliases}
424 // If we have non-standard CA roots, use them for all HTTPS requests.
425 if Conf.Static.TLS.CertPool != nil {
426 http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{
427 RootCAs: Conf.Static.TLS.CertPool,
431 SetPedantic(c.Static.Pedantic)
434// Set pedantic in all packages.
435func SetPedantic(p bool) {
443// ParseConfig parses the static config at path p. If checkOnly is true, no changes
444// are made, such as registering ACME identities. If doLoadTLSKeyCerts is true,
445// the TLS KeyCerts configuration is loaded and checked. This is used during the
446// quickstart in the case the user is going to provide their own certificates.
447// If checkACMEHosts is true, the hosts allowed for acme are compared with the
448// explicitly configured ips we are listening on.
449func ParseConfig(ctx context.Context, log mlog.Log, p string, checkOnly, doLoadTLSKeyCerts, checkACMEHosts bool) (c *Config, errs []error) {
451 Static: config.Static{
458 if os.IsNotExist(err) && os.Getenv("MOXCONF") == "" {
459 return nil, []error{fmt.Errorf("open config file: %v (hint: use mox -config ... or set MOXCONF=...)", err)}
461 return nil, []error{fmt.Errorf("open config file: %v", err)}
464 if err := sconf.Parse(f, &c.Static); err != nil {
465 return nil, []error{fmt.Errorf("parsing %s%v", p, err)}
468 if xerrs := PrepareStaticConfig(ctx, log, p, c, checkOnly, doLoadTLSKeyCerts); len(xerrs) > 0 {
472 pp := filepath.Join(filepath.Dir(p), "domains.conf")
473 c.Dynamic, c.dynamicMtime, c.accountDestinations, c.aliases, errs = ParseDynamicConfig(ctx, log, pp, c.Static)
476 c.allowACMEHosts(log, checkACMEHosts)
482// PrepareStaticConfig parses the static config file and prepares data structures
483// for starting mox. If checkOnly is set no substantial changes are made, like
484// creating an ACME registration.
485func PrepareStaticConfig(ctx context.Context, log mlog.Log, configFile string, conf *Config, checkOnly, doLoadTLSKeyCerts bool) (errs []error) {
486 addErrorf := func(format string, args ...any) {
487 errs = append(errs, fmt.Errorf(format, args...))
492 // check that mailbox is in unicode NFC normalized form.
493 checkMailboxNormf := func(mailbox string, format string, args ...any) {
494 s := norm.NFC.String(mailbox)
496 msg := fmt.Sprintf(format, args...)
497 addErrorf("%s: mailbox %q is not in NFC normalized form, should be %q", msg, mailbox, s)
501 // Post-process logging config.
502 if logLevel, ok := mlog.Levels[c.LogLevel]; ok {
503 conf.Log = map[string]slog.Level{"": logLevel}
505 addErrorf("invalid log level %q", c.LogLevel)
507 for pkg, s := range c.PackageLogLevels {
508 if logLevel, ok := mlog.Levels[s]; ok {
509 conf.Log[pkg] = logLevel
511 addErrorf("invalid package log level %q", s)
518 u, err := user.Lookup(c.User)
520 uid, err := strconv.ParseUint(c.User, 10, 32)
522 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)
524 // We assume the same gid as uid.
529 if uid, err := strconv.ParseUint(u.Uid, 10, 32); err != nil {
530 addErrorf("parsing uid %s: %v", u.Uid, err)
534 if gid, err := strconv.ParseUint(u.Gid, 10, 32); err != nil {
535 addErrorf("parsing gid %s: %v", u.Gid, err)
541 hostname, err := dns.ParseDomain(c.Hostname)
543 addErrorf("parsing hostname: %s", err)
544 } else if hostname.Name() != c.Hostname {
545 addErrorf("hostname must be in unicode form %q instead of %q", hostname.Name(), c.Hostname)
547 c.HostnameDomain = hostname
549 if c.HostTLSRPT.Account != "" {
550 tlsrptLocalpart, err := smtp.ParseLocalpart(c.HostTLSRPT.Localpart)
552 addErrorf("invalid localpart %q for host tlsrpt: %v", c.HostTLSRPT.Localpart, err)
553 } else if tlsrptLocalpart.IsInternational() {
554 // Does not appear documented in
../rfc/8460, but similar to DMARC it makes sense
555 // to keep this ascii-only addresses.
556 addErrorf("host TLSRPT localpart %q is an internationalized address, only conventional ascii-only address allowed for interopability", tlsrptLocalpart)
558 c.HostTLSRPT.ParsedLocalpart = tlsrptLocalpart
561 // Return private key for host name for use with an ACME. Used to return the same
562 // private key as pre-generated for use with DANE, with its public key in DNS.
563 // We only use this key for Listener's that have this ACME configured, and for
564 // which the effective listener host name (either specific to the listener, or the
565 // global name) is requested. Other host names can get a fresh private key, they
566 // don't appear in DANE records.
568 // - run 0: only use listener with explicitly matching host name in listener
569 // (default quickstart config does not set it).
570 // - run 1: only look at public listener (and host matching mox host name)
571 // - run 2: all listeners (and host matching mox host name)
572 findACMEHostPrivateKey := func(acmeName, host string, keyType autocert.KeyType, run int) crypto.Signer {
573 for listenerName, l := range Conf.Static.Listeners {
574 if l.TLS == nil || l.TLS.ACME != acmeName {
577 if run == 0 && host != l.HostnameDomain.ASCII {
580 if run == 1 && listenerName != "public" || host != Conf.Static.HostnameDomain.ASCII {
584 case autocert.KeyRSA2048:
585 if len(l.TLS.HostPrivateRSA2048Keys) == 0 {
588 return l.TLS.HostPrivateRSA2048Keys[0]
589 case autocert.KeyECDSAP256:
590 if len(l.TLS.HostPrivateECDSAP256Keys) == 0 {
593 return l.TLS.HostPrivateECDSAP256Keys[0]
600 // Make a function for an autocert.Manager.GetPrivateKey, using findACMEHostPrivateKey.
601 makeGetPrivateKey := func(acmeName string) func(host string, keyType autocert.KeyType) (crypto.Signer, error) {
602 return func(host string, keyType autocert.KeyType) (crypto.Signer, error) {
603 key := findACMEHostPrivateKey(acmeName, host, keyType, 0)
605 key = findACMEHostPrivateKey(acmeName, host, keyType, 1)
608 key = findACMEHostPrivateKey(acmeName, host, keyType, 2)
611 log.Debug("found existing private key for certificate for host",
612 slog.String("acmename", acmeName),
613 slog.String("host", host),
614 slog.Any("keytype", keyType))
617 log.Debug("generating new private key for certificate for host",
618 slog.String("acmename", acmeName),
619 slog.String("host", host),
620 slog.Any("keytype", keyType))
622 case autocert.KeyRSA2048:
623 return rsa.GenerateKey(cryptorand.Reader, 2048)
624 case autocert.KeyECDSAP256:
625 return ecdsa.GenerateKey(elliptic.P256(), cryptorand.Reader)
627 return nil, fmt.Errorf("unrecognized requested key type %v", keyType)
631 for name, acme := range c.ACME {
634 if acme.ExternalAccountBinding != nil {
635 eabKeyID = acme.ExternalAccountBinding.KeyID
636 p := configDirPath(configFile, acme.ExternalAccountBinding.KeyFile)
637 buf, err := os.ReadFile(p)
639 addErrorf("reading external account binding key for acme provider %q: %s", name, err)
641 dec := make([]byte, base64.RawURLEncoding.DecodedLen(len(buf)))
642 n, err := base64.RawURLEncoding.Decode(dec, buf)
644 addErrorf("parsing external account binding key as base64 for acme provider %q: %s", name, err)
655 acmeDir := dataDirPath(configFile, c.DataDir, "acme")
656 os.MkdirAll(acmeDir, 0770)
657 manager, err := autotls.Load(name, acmeDir, acme.ContactEmail, acme.DirectoryURL, eabKeyID, eabKey, makeGetPrivateKey(name), Shutdown.Done())
659 addErrorf("loading ACME identity for %q: %s", name, err)
661 acme.Manager = manager
663 // Help configurations from older quickstarts.
664 if acme.IssuerDomainName == "" && acme.DirectoryURL == "https://acme-v02.api.letsencrypt.org/directory" {
665 acme.IssuerDomainName = "letsencrypt.org"
671 var haveUnspecifiedSMTPListener bool
672 for name, l := range c.Listeners {
673 if l.Hostname != "" {
674 d, err := dns.ParseDomain(l.Hostname)
676 addErrorf("bad listener hostname %q: %s", l.Hostname, err)
681 if l.TLS.ACME != "" && len(l.TLS.KeyCerts) != 0 {
682 addErrorf("listener %q: cannot have ACME and static key/certificates", name)
683 } else if l.TLS.ACME != "" {
684 acme, ok := c.ACME[l.TLS.ACME]
686 addErrorf("listener %q: unknown ACME provider %q", name, l.TLS.ACME)
689 // If only checking or with missing ACME definition, we don't have an acme manager,
690 // so set an empty tls config to continue.
691 var tlsconfig *tls.Config
692 if checkOnly || acme.Manager == nil {
693 tlsconfig = &tls.Config{}
695 tlsconfig = acme.Manager.TLSConfig.Clone()
696 l.TLS.ACMEConfig = acme.Manager.ACMETLSConfig
698 // SMTP STARTTLS connections are commonly made without SNI, because certificates
699 // often aren't verified.
700 hostname := c.HostnameDomain
701 if l.Hostname != "" {
702 hostname = l.HostnameDomain
704 getCert := tlsconfig.GetCertificate
705 tlsconfig.GetCertificate = func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
706 if hello.ServerName == "" {
707 hello.ServerName = hostname.ASCII
709 return getCert(hello)
712 l.TLS.Config = tlsconfig
713 } else if len(l.TLS.KeyCerts) != 0 {
714 if doLoadTLSKeyCerts {
715 if err := loadTLSKeyCerts(configFile, "listener "+name, l.TLS); err != nil {
720 addErrorf("listener %q: cannot have TLS config without ACME and without static keys/certificates", name)
722 for _, privKeyFile := range l.TLS.HostPrivateKeyFiles {
723 keyPath := configDirPath(configFile, privKeyFile)
724 privKey, err := loadPrivateKeyFile(keyPath)
726 addErrorf("listener %q: parsing host private key for DANE and ACME certificates: %v", name, err)
729 switch k := privKey.(type) {
730 case *rsa.PrivateKey:
731 if k.N.BitLen() != 2048 {
732 log.Error("need rsa key with 2048 bits, for host private key for DANE/ACME certificates, ignoring",
733 slog.String("listener", name),
734 slog.String("file", keyPath),
735 slog.Int("bits", k.N.BitLen()))
738 l.TLS.HostPrivateRSA2048Keys = append(l.TLS.HostPrivateRSA2048Keys, k)
739 case *ecdsa.PrivateKey:
740 if k.Curve != elliptic.P256() {
741 log.Error("unrecognized ecdsa curve for host private key for DANE/ACME certificates, ignoring", slog.String("listener", name), slog.String("file", keyPath))
744 l.TLS.HostPrivateECDSAP256Keys = append(l.TLS.HostPrivateECDSAP256Keys, k)
746 log.Error("unrecognized key type for host private key for DANE/ACME certificates, ignoring",
747 slog.String("listener", name),
748 slog.String("file", keyPath),
749 slog.String("keytype", fmt.Sprintf("%T", privKey)))
753 if l.TLS.ACME != "" && (len(l.TLS.HostPrivateRSA2048Keys) == 0) != (len(l.TLS.HostPrivateECDSAP256Keys) == 0) {
754 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")
758 var minVersion uint16 = tls.VersionTLS12
759 if l.TLS.MinVersion != "" {
760 versions := map[string]uint16{
761 "TLSv1.0": tls.VersionTLS10,
762 "TLSv1.1": tls.VersionTLS11,
763 "TLSv1.2": tls.VersionTLS12,
764 "TLSv1.3": tls.VersionTLS13,
766 v, ok := versions[l.TLS.MinVersion]
768 addErrorf("listener %q: unknown TLS mininum version %q", name, l.TLS.MinVersion)
772 if l.TLS.Config != nil {
773 l.TLS.Config.MinVersion = minVersion
775 if l.TLS.ACMEConfig != nil {
776 l.TLS.ACMEConfig.MinVersion = minVersion
779 var needsTLS []string
780 needtls := func(s string, v bool) {
782 needsTLS = append(needsTLS, s)
785 needtls("IMAPS", l.IMAPS.Enabled)
786 needtls("SMTP", l.SMTP.Enabled && !l.SMTP.NoSTARTTLS)
787 needtls("Submissions", l.Submissions.Enabled)
788 needtls("Submission", l.Submission.Enabled && !l.Submission.NoRequireSTARTTLS)
789 needtls("AccountHTTPS", l.AccountHTTPS.Enabled)
790 needtls("AdminHTTPS", l.AdminHTTPS.Enabled)
791 needtls("AutoconfigHTTPS", l.AutoconfigHTTPS.Enabled && !l.AutoconfigHTTPS.NonTLS)
792 needtls("MTASTSHTTPS", l.MTASTSHTTPS.Enabled && !l.MTASTSHTTPS.NonTLS)
793 needtls("WebserverHTTPS", l.WebserverHTTPS.Enabled)
794 if len(needsTLS) > 0 {
795 addErrorf("listener %q does not specify tls config, but requires tls for %s", name, strings.Join(needsTLS, ", "))
798 if l.AutoconfigHTTPS.Enabled && l.MTASTSHTTPS.Enabled && l.AutoconfigHTTPS.Port == l.MTASTSHTTPS.Port && l.AutoconfigHTTPS.NonTLS != l.MTASTSHTTPS.NonTLS {
799 addErrorf("listener %q tries to enable autoconfig and mta-sts enabled on same port but with both http and https", name)
803 haveUnspecifiedSMTPListener = true
805 for _, ipstr := range l.IPs {
806 ip := net.ParseIP(ipstr)
808 addErrorf("listener %q has invalid IP %q", name, ipstr)
811 if ip.IsUnspecified() {
812 haveUnspecifiedSMTPListener = true
815 if len(c.SpecifiedSMTPListenIPs) >= 2 {
816 haveUnspecifiedSMTPListener = true
817 } else if len(c.SpecifiedSMTPListenIPs) > 0 && (c.SpecifiedSMTPListenIPs[0].To4() == nil) == (ip.To4() == nil) {
818 haveUnspecifiedSMTPListener = true
820 c.SpecifiedSMTPListenIPs = append(c.SpecifiedSMTPListenIPs, ip)
824 for _, s := range l.SMTP.DNSBLs {
825 d, err := dns.ParseDomain(s)
827 addErrorf("listener %q has invalid DNSBL zone %q", name, s)
830 l.SMTP.DNSBLZones = append(l.SMTP.DNSBLZones, d)
832 if l.IPsNATed && len(l.NATIPs) > 0 {
833 addErrorf("listener %q has both IPsNATed and NATIPs (remove deprecated IPsNATed)", name)
835 for _, ipstr := range l.NATIPs {
836 ip := net.ParseIP(ipstr)
838 addErrorf("listener %q has invalid ip %q", name, ipstr)
839 } else if ip.IsUnspecified() || ip.IsLoopback() {
840 addErrorf("listener %q has NAT ip that is the unspecified or loopback address %s", name, ipstr)
843 checkPath := func(kind string, enabled bool, path string) {
844 if enabled && path != "" && !strings.HasPrefix(path, "/") {
845 addErrorf("listener %q has %s with path %q that must start with a slash", name, kind, path)
848 checkPath("AccountHTTP", l.AccountHTTP.Enabled, l.AccountHTTP.Path)
849 checkPath("AccountHTTPS", l.AccountHTTPS.Enabled, l.AccountHTTPS.Path)
850 checkPath("AdminHTTP", l.AdminHTTP.Enabled, l.AdminHTTP.Path)
851 checkPath("AdminHTTPS", l.AdminHTTPS.Enabled, l.AdminHTTPS.Path)
852 c.Listeners[name] = l
854 if haveUnspecifiedSMTPListener {
855 c.SpecifiedSMTPListenIPs = nil
858 var zerouse config.SpecialUseMailboxes
859 if len(c.DefaultMailboxes) > 0 && (c.InitialMailboxes.SpecialUse != zerouse || len(c.InitialMailboxes.Regular) > 0) {
860 addErrorf("cannot have both DefaultMailboxes and InitialMailboxes")
862 // DefaultMailboxes is deprecated.
863 for _, mb := range c.DefaultMailboxes {
864 checkMailboxNormf(mb, "default mailbox")
866 checkSpecialUseMailbox := func(nameOpt string) {
868 checkMailboxNormf(nameOpt, "special-use initial mailbox")
869 if strings.EqualFold(nameOpt, "inbox") {
870 addErrorf("initial mailbox cannot be set to Inbox (Inbox is always created)")
874 checkSpecialUseMailbox(c.InitialMailboxes.SpecialUse.Archive)
875 checkSpecialUseMailbox(c.InitialMailboxes.SpecialUse.Draft)
876 checkSpecialUseMailbox(c.InitialMailboxes.SpecialUse.Junk)
877 checkSpecialUseMailbox(c.InitialMailboxes.SpecialUse.Sent)
878 checkSpecialUseMailbox(c.InitialMailboxes.SpecialUse.Trash)
879 for _, name := range c.InitialMailboxes.Regular {
880 checkMailboxNormf(name, "regular initial mailbox")
881 if strings.EqualFold(name, "inbox") {
882 addErrorf("initial regular mailbox cannot be set to Inbox (Inbox is always created)")
886 checkTransportSMTP := func(name string, isTLS bool, t *config.TransportSMTP) {
888 t.DNSHost, err = dns.ParseDomain(t.Host)
890 addErrorf("transport %s: bad host %s: %v", name, t.Host, err)
893 if isTLS && t.STARTTLSInsecureSkipVerify {
894 addErrorf("transport %s: cannot have STARTTLSInsecureSkipVerify with immediate TLS")
896 if isTLS && t.NoSTARTTLS {
897 addErrorf("transport %s: cannot have NoSTARTTLS with immediate TLS")
903 seen := map[string]bool{}
904 for _, m := range t.Auth.Mechanisms {
906 addErrorf("transport %s: duplicate authentication mechanism %s", name, m)
910 case "SCRAM-SHA-256-PLUS":
911 case "SCRAM-SHA-256":
912 case "SCRAM-SHA-1-PLUS":
917 addErrorf("transport %s: unknown authentication mechanism %s", name, m)
921 t.Auth.EffectiveMechanisms = t.Auth.Mechanisms
922 if len(t.Auth.EffectiveMechanisms) == 0 {
923 t.Auth.EffectiveMechanisms = []string{"SCRAM-SHA-256-PLUS", "SCRAM-SHA-256", "SCRAM-SHA-1-PLUS", "SCRAM-SHA-1", "CRAM-MD5"}
927 checkTransportSocks := func(name string, t *config.TransportSocks) {
928 _, _, err := net.SplitHostPort(t.Address)
930 addErrorf("transport %s: bad address %s: %v", name, t.Address, err)
932 for _, ipstr := range t.RemoteIPs {
933 ip := net.ParseIP(ipstr)
935 addErrorf("transport %s: bad ip %s", name, ipstr)
937 t.IPs = append(t.IPs, ip)
940 t.Hostname, err = dns.ParseDomain(t.RemoteHostname)
942 addErrorf("transport %s: bad hostname %s: %v", name, t.RemoteHostname, err)
946 checkTransportDirect := func(name string, t *config.TransportDirect) {
947 if t.DisableIPv4 && t.DisableIPv6 {
948 addErrorf("transport %s: both IPv4 and IPv6 are disabled, enable at least one", name)
959 for name, t := range c.Transports {
961 if t.Submissions != nil {
963 checkTransportSMTP(name, true, t.Submissions)
965 if t.Submission != nil {
967 checkTransportSMTP(name, false, t.Submission)
971 checkTransportSMTP(name, false, t.SMTP)
975 checkTransportSocks(name, t.Socks)
979 checkTransportDirect(name, t.Direct)
982 addErrorf("transport %s: cannot have multiple methods in a transport", name)
986 // Load CA certificate pool.
988 if c.TLS.CA.AdditionalToSystem {
990 c.TLS.CertPool, err = x509.SystemCertPool()
992 addErrorf("fetching system CA cert pool: %v", err)
995 c.TLS.CertPool = x509.NewCertPool()
997 for _, certfile := range c.TLS.CA.CertFiles {
998 p := configDirPath(configFile, certfile)
999 pemBuf, err := os.ReadFile(p)
1001 addErrorf("reading TLS CA cert file: %v", err)
1003 } else if !c.TLS.CertPool.AppendCertsFromPEM(pemBuf) {
1004 // todo: can we check more fully if we're getting some useful data back?
1005 addErrorf("no CA certs added from %q", p)
1012// PrepareDynamicConfig parses the dynamic config file given a static file.
1013func 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) {
1014 addErrorf := func(format string, args ...any) {
1015 errs = append(errs, fmt.Errorf(format, args...))
1018 f, err := os.Open(dynamicPath)
1020 addErrorf("parsing domains config: %v", err)
1026 addErrorf("stat domains config: %v", err)
1028 if err := sconf.Parse(f, &c); err != nil {
1029 addErrorf("parsing dynamic config file: %v", err)
1033 accDests, aliases, errs = prepareDynamicConfig(ctx, log, dynamicPath, static, &c)
1034 return c, fi.ModTime(), accDests, aliases, errs
1037func 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) {
1038 addErrorf := func(format string, args ...any) {
1039 errs = append(errs, fmt.Errorf(format, args...))
1042 // Check that mailbox is in unicode NFC normalized form.
1043 checkMailboxNormf := func(mailbox string, format string, args ...any) {
1044 s := norm.NFC.String(mailbox)
1046 msg := fmt.Sprintf(format, args...)
1047 addErrorf("%s: mailbox %q is not in NFC normalized form, should be %q", msg, mailbox, s)
1051 // Validate postmaster account exists.
1052 if _, ok := c.Accounts[static.Postmaster.Account]; !ok {
1053 addErrorf("postmaster account %q does not exist", static.Postmaster.Account)
1055 checkMailboxNormf(static.Postmaster.Mailbox, "postmaster mailbox")
1057 accDests = map[string]AccountDestination{}
1058 aliases = map[string]config.Alias{}
1060 // Validate host TLSRPT account/address.
1061 if static.HostTLSRPT.Account != "" {
1062 if _, ok := c.Accounts[static.HostTLSRPT.Account]; !ok {
1063 addErrorf("host tlsrpt account %q does not exist", static.HostTLSRPT.Account)
1065 checkMailboxNormf(static.HostTLSRPT.Mailbox, "host tlsrpt mailbox")
1067 // Localpart has been parsed already.
1069 addrFull := smtp.NewAddress(static.HostTLSRPT.ParsedLocalpart, static.HostnameDomain).String()
1070 dest := config.Destination{
1071 Mailbox: static.HostTLSRPT.Mailbox,
1072 HostTLSReports: true,
1074 accDests[addrFull] = AccountDestination{false, static.HostTLSRPT.ParsedLocalpart, static.HostTLSRPT.Account, dest}
1077 var haveSTSListener, haveWebserverListener bool
1078 for _, l := range static.Listeners {
1079 if l.MTASTSHTTPS.Enabled {
1080 haveSTSListener = true
1082 if l.WebserverHTTP.Enabled || l.WebserverHTTPS.Enabled {
1083 haveWebserverListener = true
1087 checkRoutes := func(descr string, routes []config.Route) {
1088 parseRouteDomains := func(l []string) []string {
1090 for _, e := range l {
1096 if strings.HasPrefix(e, ".") {
1100 d, err := dns.ParseDomain(e)
1102 addErrorf("%s: invalid domain %s: %v", descr, e, err)
1104 r = append(r, prefix+d.ASCII)
1109 for i := range routes {
1110 routes[i].FromDomainASCII = parseRouteDomains(routes[i].FromDomain)
1111 routes[i].ToDomainASCII = parseRouteDomains(routes[i].ToDomain)
1113 routes[i].ResolvedTransport, ok = static.Transports[routes[i].Transport]
1115 addErrorf("%s: route references undefined transport %s", descr, routes[i].Transport)
1120 checkRoutes("global routes", c.Routes)
1122 // Validate domains.
1123 for d, domain := range c.Domains {
1124 dnsdomain, err := dns.ParseDomain(d)
1126 addErrorf("bad domain %q: %s", d, err)
1127 } else if dnsdomain.Name() != d {
1128 addErrorf("domain %s must be specified in unicode form, %s", d, dnsdomain.Name())
1131 domain.Domain = dnsdomain
1133 if domain.ClientSettingsDomain != "" {
1134 csd, err := dns.ParseDomain(domain.ClientSettingsDomain)
1136 addErrorf("bad client settings domain %q: %s", domain.ClientSettingsDomain, err)
1138 domain.ClientSettingsDNSDomain = csd
1141 for _, sign := range domain.DKIM.Sign {
1142 if _, ok := domain.DKIM.Selectors[sign]; !ok {
1143 addErrorf("selector %s for signing is missing in domain %s", sign, d)
1146 for name, sel := range domain.DKIM.Selectors {
1147 seld, err := dns.ParseDomain(name)
1149 addErrorf("bad selector %q: %s", name, err)
1150 } else if seld.Name() != name {
1151 addErrorf("selector %q must be specified in unicode form, %q", name, seld.Name())
1155 if sel.Expiration != "" {
1156 exp, err := time.ParseDuration(sel.Expiration)
1158 addErrorf("selector %q has invalid expiration %q: %v", name, sel.Expiration, err)
1160 sel.ExpirationSeconds = int(exp / time.Second)
1164 sel.HashEffective = sel.Hash
1165 switch sel.HashEffective {
1167 sel.HashEffective = "sha256"
1169 log.Error("using sha1 with DKIM is deprecated as not secure enough, switch to sha256")
1172 addErrorf("unsupported hash %q for selector %q in domain %s", sel.HashEffective, name, d)
1175 pemBuf, err := os.ReadFile(configDirPath(dynamicPath, sel.PrivateKeyFile))
1177 addErrorf("reading private key for selector %s in domain %s: %s", name, d, err)
1180 p, _ := pem.Decode(pemBuf)
1182 addErrorf("private key for selector %s in domain %s has no PEM block", name, d)
1185 key, err := x509.ParsePKCS8PrivateKey(p.Bytes)
1187 addErrorf("parsing private key for selector %s in domain %s: %s", name, d, err)
1190 switch k := key.(type) {
1191 case *rsa.PrivateKey:
1192 if k.N.BitLen() < 1024 {
1194 // Let's help user do the right thing.
1195 addErrorf("rsa keys should be >= 1024 bits")
1198 sel.Algorithm = fmt.Sprintf("rsa-%d", k.N.BitLen())
1199 case ed25519.PrivateKey:
1200 if sel.HashEffective != "sha256" {
1201 addErrorf("hash algorithm %q is not supported with ed25519, only sha256 is", sel.HashEffective)
1204 sel.Algorithm = "ed25519"
1206 addErrorf("private key type %T not yet supported, at selector %s in domain %s", key, name, d)
1209 if len(sel.Headers) == 0 {
1213 // By default we seal signed headers, and we sign user-visible headers to
1214 // prevent/limit reuse of previously signed messages: All addressing fields, date
1215 // and subject, message-referencing fields, parsing instructions (content-type).
1216 sel.HeadersEffective = strings.Split("From,To,Cc,Bcc,Reply-To,References,In-Reply-To,Subject,Date,Message-Id,Content-Type", ",")
1219 for _, h := range sel.Headers {
1220 from = from || strings.EqualFold(h, "From")
1222 if strings.EqualFold(h, "DKIM-Signature") || strings.EqualFold(h, "Received") || strings.EqualFold(h, "Return-Path") {
1223 log.Error("DKIM-signing header %q is recommended against as it may be modified in transit")
1227 addErrorf("From-field must always be DKIM-signed")
1229 sel.HeadersEffective = sel.Headers
1232 domain.DKIM.Selectors[name] = sel
1235 if domain.MTASTS != nil {
1236 if !haveSTSListener {
1237 addErrorf("MTA-STS enabled for domain %q, but there is no listener for MTASTS", d)
1239 sts := domain.MTASTS
1240 if sts.PolicyID == "" {
1241 addErrorf("invalid empty MTA-STS PolicyID")
1244 case mtasts.ModeNone, mtasts.ModeTesting, mtasts.ModeEnforce:
1246 addErrorf("invalid mtasts mode %q", sts.Mode)
1250 checkRoutes("routes for domain", domain.Routes)
1252 c.Domains[d] = domain
1255 // To determine ReportsOnly.
1256 domainHasAddress := map[string]bool{}
1258 // Validate email addresses.
1259 for accName, acc := range c.Accounts {
1261 acc.DNSDomain, err = dns.ParseDomain(acc.Domain)
1263 addErrorf("parsing domain %s for account %q: %s", acc.Domain, accName, err)
1266 if strings.EqualFold(acc.RejectsMailbox, "Inbox") {
1267 addErrorf("account %q: cannot set RejectsMailbox to inbox, messages will be removed automatically from the rejects mailbox", accName)
1269 checkMailboxNormf(acc.RejectsMailbox, "account %q", accName)
1271 if acc.AutomaticJunkFlags.JunkMailboxRegexp != "" {
1272 r, err := regexp.Compile(acc.AutomaticJunkFlags.JunkMailboxRegexp)
1274 addErrorf("invalid JunkMailboxRegexp regular expression: %v", err)
1278 if acc.AutomaticJunkFlags.NeutralMailboxRegexp != "" {
1279 r, err := regexp.Compile(acc.AutomaticJunkFlags.NeutralMailboxRegexp)
1281 addErrorf("invalid NeutralMailboxRegexp regular expression: %v", err)
1283 acc.NeutralMailbox = r
1285 if acc.AutomaticJunkFlags.NotJunkMailboxRegexp != "" {
1286 r, err := regexp.Compile(acc.AutomaticJunkFlags.NotJunkMailboxRegexp)
1288 addErrorf("invalid NotJunkMailboxRegexp regular expression: %v", err)
1290 acc.NotJunkMailbox = r
1293 acc.ParsedFromIDLoginAddresses = make([]smtp.Address, len(acc.FromIDLoginAddresses))
1294 for i, s := range acc.FromIDLoginAddresses {
1295 a, err := smtp.ParseAddress(s)
1297 addErrorf("invalid fromid login address %q in account %q: %v", s, accName, err)
1299 // We check later on if address belongs to account.
1300 dom, ok := c.Domains[a.Domain.Name()]
1302 addErrorf("unknown domain in fromid login address %q for account %q", s, accName)
1303 } else if dom.LocalpartCatchallSeparator == "" {
1304 addErrorf("localpart catchall separator not configured for domain for fromid login address %q for account %q", s, accName)
1306 acc.ParsedFromIDLoginAddresses[i] = a
1309 // Clear any previously derived state.
1312 c.Accounts[accName] = acc
1314 if acc.OutgoingWebhook != nil {
1315 u, err := url.Parse(acc.OutgoingWebhook.URL)
1316 if err == nil && (u.Scheme != "http" && u.Scheme != "https") {
1317 err = errors.New("scheme must be http or https")
1320 addErrorf("parsing outgoing hook url %q in account %q: %v", acc.OutgoingWebhook.URL, accName, err)
1323 // note: outgoing hook events are in ../queue/hooks.go, ../mox-/config.go, ../queue.go and ../webapi/gendoc.sh. keep in sync.
1324 outgoingHookEvents := []string{"delivered", "suppressed", "delayed", "failed", "relayed", "expanded", "canceled", "unrecognized"}
1325 for _, e := range acc.OutgoingWebhook.Events {
1326 if !slices.Contains(outgoingHookEvents, e) {
1327 addErrorf("unknown outgoing hook event %q", e)
1331 if acc.IncomingWebhook != nil {
1332 u, err := url.Parse(acc.IncomingWebhook.URL)
1333 if err == nil && (u.Scheme != "http" && u.Scheme != "https") {
1334 err = errors.New("scheme must be http or https")
1337 addErrorf("parsing incoming hook url %q in account %q: %v", acc.IncomingWebhook.URL, accName, err)
1341 // 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.
1342 replaceLocalparts := map[string]string{}
1344 for addrName, dest := range acc.Destinations {
1345 checkMailboxNormf(dest.Mailbox, "account %q, destination %q", accName, addrName)
1347 for i, rs := range dest.Rulesets {
1348 checkMailboxNormf(rs.Mailbox, "account %q, destination %q, ruleset %d", accName, addrName, i+1)
1352 if rs.SMTPMailFromRegexp != "" {
1354 r, err := regexp.Compile(rs.SMTPMailFromRegexp)
1356 addErrorf("invalid SMTPMailFrom regular expression: %v", err)
1358 c.Accounts[accName].Destinations[addrName].Rulesets[i].SMTPMailFromRegexpCompiled = r
1360 if rs.MsgFromRegexp != "" {
1362 r, err := regexp.Compile(rs.MsgFromRegexp)
1364 addErrorf("invalid MsgFrom regular expression: %v", err)
1366 c.Accounts[accName].Destinations[addrName].Rulesets[i].MsgFromRegexpCompiled = r
1368 if rs.VerifiedDomain != "" {
1370 d, err := dns.ParseDomain(rs.VerifiedDomain)
1372 addErrorf("invalid VerifiedDomain: %v", err)
1374 c.Accounts[accName].Destinations[addrName].Rulesets[i].VerifiedDNSDomain = d
1377 var hdr [][2]*regexp.Regexp
1378 for k, v := range rs.HeadersRegexp {
1380 if strings.ToLower(k) != k {
1381 addErrorf("header field %q must only have lower case characters", k)
1383 if strings.ToLower(v) != v {
1384 addErrorf("header value %q must only have lower case characters", v)
1386 rk, err := regexp.Compile(k)
1388 addErrorf("invalid rule header regexp %q: %v", k, err)
1390 rv, err := regexp.Compile(v)
1392 addErrorf("invalid rule header regexp %q: %v", v, err)
1394 hdr = append(hdr, [...]*regexp.Regexp{rk, rv})
1396 c.Accounts[accName].Destinations[addrName].Rulesets[i].HeadersRegexpCompiled = hdr
1399 addErrorf("ruleset must have at least one rule")
1402 if rs.IsForward && rs.ListAllowDomain != "" {
1403 addErrorf("ruleset cannot have both IsForward and ListAllowDomain")
1406 if rs.SMTPMailFromRegexp == "" || rs.VerifiedDomain == "" {
1407 addErrorf("ruleset with IsForward must have both SMTPMailFromRegexp and VerifiedDomain too")
1410 if rs.ListAllowDomain != "" {
1411 d, err := dns.ParseDomain(rs.ListAllowDomain)
1413 addErrorf("invalid ListAllowDomain %q: %v", rs.ListAllowDomain, err)
1415 c.Accounts[accName].Destinations[addrName].Rulesets[i].ListAllowDNSDomain = d
1418 checkMailboxNormf(rs.AcceptRejectsToMailbox, "account %q, destination %q, ruleset %d, rejects mailbox", accName, addrName, i+1)
1419 if strings.EqualFold(rs.AcceptRejectsToMailbox, "inbox") {
1420 addErrorf("account %q, destination %q, ruleset %d: AcceptRejectsToMailbox cannot be set to Inbox", accName, addrName, i+1)
1424 // Catchall destination for domain.
1425 if strings.HasPrefix(addrName, "@") {
1426 d, err := dns.ParseDomain(addrName[1:])
1428 addErrorf("parsing domain %q in account %q", addrName[1:], accName)
1430 } else if _, ok := c.Domains[d.Name()]; !ok {
1431 addErrorf("unknown domain for address %q in account %q", addrName, accName)
1434 domainHasAddress[d.Name()] = true
1435 addrFull := "@" + d.Name()
1436 if _, ok := accDests[addrFull]; ok {
1437 addErrorf("duplicate canonicalized catchall destination address %s", addrFull)
1439 accDests[addrFull] = AccountDestination{true, "", accName, dest}
1443 // todo deprecated: remove support for parsing destination as just a localpart instead full address.
1444 var address smtp.Address
1445 if localpart, err := smtp.ParseLocalpart(addrName); err != nil && errors.Is(err, smtp.ErrBadLocalpart) {
1446 address, err = smtp.ParseAddress(addrName)
1448 addErrorf("invalid email address %q in account %q", addrName, accName)
1450 } else if _, ok := c.Domains[address.Domain.Name()]; !ok {
1451 addErrorf("unknown domain for address %q in account %q", addrName, accName)
1456 addErrorf("invalid localpart %q in account %q", addrName, accName)
1459 address = smtp.NewAddress(localpart, acc.DNSDomain)
1460 if _, ok := c.Domains[acc.DNSDomain.Name()]; !ok {
1461 addErrorf("unknown domain %s for account %q", acc.DNSDomain.Name(), accName)
1464 replaceLocalparts[addrName] = address.Pack(true)
1467 origLP := address.Localpart
1468 dc := c.Domains[address.Domain.Name()]
1469 domainHasAddress[address.Domain.Name()] = true
1470 lp := CanonicalLocalpart(address.Localpart, dc)
1471 if dc.LocalpartCatchallSeparator != "" && strings.Contains(string(address.Localpart), dc.LocalpartCatchallSeparator) {
1472 addErrorf("localpart of address %s includes domain catchall separator %s", address, dc.LocalpartCatchallSeparator)
1474 address.Localpart = lp
1476 addrFull := address.Pack(true)
1477 if _, ok := accDests[addrFull]; ok {
1478 addErrorf("duplicate canonicalized destination address %s", addrFull)
1480 accDests[addrFull] = AccountDestination{false, origLP, accName, dest}
1483 for lp, addr := range replaceLocalparts {
1484 dest, ok := acc.Destinations[lp]
1486 addErrorf("could not find localpart %q to replace with address in destinations", lp)
1488 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`,
1489 slog.Any("localpart", lp),
1490 slog.Any("address", addr),
1491 slog.String("account", accName))
1492 acc.Destinations[addr] = dest
1493 delete(acc.Destinations, lp)
1497 // Now that all addresses are parsed, check if all fromid login addresses match
1498 // configured addresses.
1499 for i, a := range acc.ParsedFromIDLoginAddresses {
1500 // For domain catchall.
1501 if _, ok := accDests["@"+a.Domain.Name()]; ok {
1504 dc := c.Domains[a.Domain.Name()]
1505 a.Localpart = CanonicalLocalpart(a.Localpart, dc)
1506 if _, ok := accDests[a.Pack(true)]; !ok {
1507 addErrorf("fromid login address %q for account %q does not match its destination addresses", acc.FromIDLoginAddresses[i], accName)
1511 checkRoutes("routes for account", acc.Routes)
1514 // Set DMARC destinations.
1515 for d, domain := range c.Domains {
1516 dmarc := domain.DMARC
1520 if _, ok := c.Accounts[dmarc.Account]; !ok {
1521 addErrorf("DMARC account %q does not exist", dmarc.Account)
1523 lp, err := smtp.ParseLocalpart(dmarc.Localpart)
1525 addErrorf("invalid DMARC localpart %q: %s", dmarc.Localpart, err)
1527 if lp.IsInternational() {
1529 addErrorf("DMARC localpart %q is an internationalized address, only conventional ascii-only address possible for interopability", lp)
1531 addrdom := domain.Domain
1532 if dmarc.Domain != "" {
1533 addrdom, err = dns.ParseDomain(dmarc.Domain)
1535 addErrorf("DMARC domain %q: %s", dmarc.Domain, err)
1536 } else if _, ok := c.Domains[addrdom.Name()]; !ok {
1537 addErrorf("unknown domain %q for DMARC address in domain %q", addrdom, d)
1540 if addrdom == domain.Domain {
1541 domainHasAddress[addrdom.Name()] = true
1544 domain.DMARC.ParsedLocalpart = lp
1545 domain.DMARC.DNSDomain = addrdom
1546 c.Domains[d] = domain
1547 addrFull := smtp.NewAddress(lp, addrdom).String()
1548 dest := config.Destination{
1549 Mailbox: dmarc.Mailbox,
1552 checkMailboxNormf(dmarc.Mailbox, "DMARC mailbox for account %q", dmarc.Account)
1553 accDests[addrFull] = AccountDestination{false, lp, dmarc.Account, dest}
1556 // Set TLSRPT destinations.
1557 for d, domain := range c.Domains {
1558 tlsrpt := domain.TLSRPT
1562 if _, ok := c.Accounts[tlsrpt.Account]; !ok {
1563 addErrorf("TLSRPT account %q does not exist", tlsrpt.Account)
1565 lp, err := smtp.ParseLocalpart(tlsrpt.Localpart)
1567 addErrorf("invalid TLSRPT localpart %q: %s", tlsrpt.Localpart, err)
1569 if lp.IsInternational() {
1570 // Does not appear documented in
../rfc/8460, but similar to DMARC it makes sense
1571 // to keep this ascii-only addresses.
1572 addErrorf("TLSRPT localpart %q is an internationalized address, only conventional ascii-only address allowed for interopability", lp)
1574 addrdom := domain.Domain
1575 if tlsrpt.Domain != "" {
1576 addrdom, err = dns.ParseDomain(tlsrpt.Domain)
1578 addErrorf("TLSRPT domain %q: %s", tlsrpt.Domain, err)
1579 } else if _, ok := c.Domains[addrdom.Name()]; !ok {
1580 addErrorf("unknown domain %q for TLSRPT address in domain %q", tlsrpt.Domain, d)
1583 if addrdom == domain.Domain {
1584 domainHasAddress[addrdom.Name()] = true
1587 domain.TLSRPT.ParsedLocalpart = lp
1588 domain.TLSRPT.DNSDomain = addrdom
1589 c.Domains[d] = domain
1590 addrFull := smtp.NewAddress(lp, addrdom).String()
1591 dest := config.Destination{
1592 Mailbox: tlsrpt.Mailbox,
1593 DomainTLSReports: true,
1595 checkMailboxNormf(tlsrpt.Mailbox, "TLSRPT mailbox for account %q", tlsrpt.Account)
1596 accDests[addrFull] = AccountDestination{false, lp, tlsrpt.Account, dest}
1599 // Set ReportsOnly for domains, based on whether we have seen addresses (possibly
1600 // from DMARC or TLS reporting).
1601 for d, domain := range c.Domains {
1602 domain.ReportsOnly = !domainHasAddress[domain.Domain.Name()]
1603 c.Domains[d] = domain
1606 // Aliases, per domain. Also add references to accounts.
1607 for d, domain := range c.Domains {
1608 for lpstr, a := range domain.Aliases {
1610 a.LocalpartStr = lpstr
1611 var clp smtp.Localpart
1612 lp, err := smtp.ParseLocalpart(lpstr)
1614 addErrorf("domain %q: parsing localpart %q for alias: %v", d, lpstr, err)
1616 } else if domain.LocalpartCatchallSeparator != "" && strings.Contains(string(lp), domain.LocalpartCatchallSeparator) {
1617 addErrorf("domain %q: alias %q contains localpart catchall separator", d, a.LocalpartStr)
1620 clp = CanonicalLocalpart(lp, domain)
1623 addr := smtp.NewAddress(clp, domain.Domain).Pack(true)
1624 if _, ok := aliases[addr]; ok {
1625 addErrorf("domain %q: duplicate alias address %q", d, addr)
1628 if _, ok := accDests[addr]; ok {
1629 addErrorf("domain %q: alias %q already present as regular address", d, addr)
1632 if len(a.Addresses) == 0 {
1633 // Not currently possible, Addresses isn't optional.
1634 addErrorf("domain %q: alias %q needs at least one destination address", d, addr)
1637 a.ParsedAddresses = make([]config.AliasAddress, 0, len(a.Addresses))
1638 seen := map[string]bool{}
1639 for _, destAddr := range a.Addresses {
1640 da, err := smtp.ParseAddress(destAddr)
1642 addErrorf("domain %q: parsing destination address %q in alias %q: %v", d, destAddr, addr, err)
1645 dastr := da.Pack(true)
1646 accDest, ok := accDests[dastr]
1648 addErrorf("domain %q: alias %q references non-existent address %q", d, addr, destAddr)
1652 addErrorf("domain %q: alias %q has duplicate address %q", d, addr, destAddr)
1656 aa := config.AliasAddress{Address: da, AccountName: accDest.Account, Destination: accDest.Destination}
1657 a.ParsedAddresses = append(a.ParsedAddresses, aa)
1659 a.Domain = domain.Domain
1660 c.Domains[d].Aliases[lpstr] = a
1663 for _, aa := range a.ParsedAddresses {
1664 acc := c.Accounts[aa.AccountName]
1667 addrs = make([]string, len(a.ParsedAddresses))
1668 for i := range a.ParsedAddresses {
1669 addrs[i] = a.ParsedAddresses[i].Address.Pack(true)
1672 // Keep the non-sensitive fields.
1673 accAlias := config.Alias{
1674 PostPublic: a.PostPublic,
1675 ListMembers: a.ListMembers,
1676 AllowMsgFrom: a.AllowMsgFrom,
1677 LocalpartStr: a.LocalpartStr,
1680 acc.Aliases = append(acc.Aliases, config.AddressAlias{SubscriptionAddress: aa.Address.Pack(true), Alias: accAlias, MemberAddresses: addrs})
1681 c.Accounts[aa.AccountName] = acc
1686 // Check webserver configs.
1687 if (len(c.WebDomainRedirects) > 0 || len(c.WebHandlers) > 0) && !haveWebserverListener {
1688 addErrorf("WebDomainRedirects or WebHandlers configured but no listener with WebserverHTTP or WebserverHTTPS enabled")
1691 c.WebDNSDomainRedirects = map[dns.Domain]dns.Domain{}
1692 for from, to := range c.WebDomainRedirects {
1693 fromdom, err := dns.ParseDomain(from)
1695 addErrorf("parsing domain for redirect %s: %v", from, err)
1697 todom, err := dns.ParseDomain(to)
1699 addErrorf("parsing domain for redirect %s: %v", to, err)
1700 } else if fromdom == todom {
1701 addErrorf("will not redirect domain %s to itself", todom)
1703 var zerodom dns.Domain
1704 if _, ok := c.WebDNSDomainRedirects[fromdom]; ok && fromdom != zerodom {
1705 addErrorf("duplicate redirect domain %s", from)
1707 c.WebDNSDomainRedirects[fromdom] = todom
1710 for i := range c.WebHandlers {
1711 wh := &c.WebHandlers[i]
1713 if wh.LogName == "" {
1714 wh.Name = fmt.Sprintf("%d", i)
1716 wh.Name = wh.LogName
1719 dom, err := dns.ParseDomain(wh.Domain)
1721 addErrorf("webhandler %s %s: parsing domain: %v", wh.Domain, wh.PathRegexp, err)
1725 if !strings.HasPrefix(wh.PathRegexp, "^") {
1726 addErrorf("webhandler %s %s: path regexp must start with a ^", wh.Domain, wh.PathRegexp)
1728 re, err := regexp.Compile(wh.PathRegexp)
1730 addErrorf("webhandler %s %s: compiling regexp: %v", wh.Domain, wh.PathRegexp, err)
1735 if wh.WebStatic != nil {
1738 if ws.StripPrefix != "" && !strings.HasPrefix(ws.StripPrefix, "/") {
1739 addErrorf("webstatic %s %s: prefix to strip %s must start with a slash", wh.Domain, wh.PathRegexp, ws.StripPrefix)
1741 for k := range ws.ResponseHeaders {
1743 k := strings.TrimSpace(xk)
1744 if k != xk || k == "" {
1745 addErrorf("webstatic %s %s: bad header %q", wh.Domain, wh.PathRegexp, xk)
1749 if wh.WebRedirect != nil {
1751 wr := wh.WebRedirect
1752 if wr.BaseURL != "" {
1753 u, err := url.Parse(wr.BaseURL)
1755 addErrorf("webredirect %s %s: parsing redirect url %s: %v", wh.Domain, wh.PathRegexp, wr.BaseURL, err)
1761 addErrorf("webredirect %s %s: BaseURL must have empty path", wh.Domain, wh.PathRegexp, wr.BaseURL)
1765 if wr.OrigPathRegexp != "" && wr.ReplacePath != "" {
1766 re, err := regexp.Compile(wr.OrigPathRegexp)
1768 addErrorf("webredirect %s %s: compiling regexp %s: %v", wh.Domain, wh.PathRegexp, wr.OrigPathRegexp, err)
1771 } else if wr.OrigPathRegexp != "" || wr.ReplacePath != "" {
1772 addErrorf("webredirect %s %s: must have either both OrigPathRegexp and ReplacePath, or neither", wh.Domain, wh.PathRegexp)
1773 } else if wr.BaseURL == "" {
1774 addErrorf("webredirect %s %s: must at least one of BaseURL and OrigPathRegexp+ReplacePath", wh.Domain, wh.PathRegexp)
1776 if wr.StatusCode != 0 && (wr.StatusCode < 300 || wr.StatusCode >= 400) {
1777 addErrorf("webredirect %s %s: invalid redirect status code %d", wh.Domain, wh.PathRegexp, wr.StatusCode)
1780 if wh.WebForward != nil {
1783 u, err := url.Parse(wf.URL)
1785 addErrorf("webforward %s %s: parsing url %s: %v", wh.Domain, wh.PathRegexp, wf.URL, err)
1789 for k := range wf.ResponseHeaders {
1791 k := strings.TrimSpace(xk)
1792 if k != xk || k == "" {
1793 addErrorf("webforward %s %s: bad header %q", wh.Domain, wh.PathRegexp, xk)
1798 addErrorf("webhandler %s %s: must have exactly one handler, not %d", wh.Domain, wh.PathRegexp, n)
1802 c.MonitorDNSBLZones = nil
1803 for _, s := range c.MonitorDNSBLs {
1804 d, err := dns.ParseDomain(s)
1806 addErrorf("invalid monitor dnsbl zone %s: %v", s, err)
1809 if slices.Contains(c.MonitorDNSBLZones, d) {
1810 addErrorf("duplicate zone %s in monitor dnsbl zones", d)
1813 c.MonitorDNSBLZones = append(c.MonitorDNSBLZones, d)
1819func loadPrivateKeyFile(keyPath string) (crypto.Signer, error) {
1820 keyBuf, err := os.ReadFile(keyPath)
1822 return nil, fmt.Errorf("reading host private key: %v", err)
1824 b, _ := pem.Decode(keyBuf)
1826 return nil, fmt.Errorf("parsing pem block for private key: %v", err)
1831 privKey, err = x509.ParsePKCS8PrivateKey(b.Bytes)
1832 case "RSA PRIVATE KEY":
1833 privKey, err = x509.ParsePKCS1PrivateKey(b.Bytes)
1834 case "EC PRIVATE KEY":
1835 privKey, err = x509.ParseECPrivateKey(b.Bytes)
1837 err = fmt.Errorf("unknown pem type %q", b.Type)
1840 return nil, fmt.Errorf("parsing private key: %v", err)
1842 if k, ok := privKey.(crypto.Signer); ok {
1845 return nil, fmt.Errorf("parsed private key not a crypto.Signer, but %T", privKey)
1848func loadTLSKeyCerts(configFile, kind string, ctls *config.TLS) error {
1849 certs := []tls.Certificate{}
1850 for _, kp := range ctls.KeyCerts {
1851 certPath := configDirPath(configFile, kp.CertFile)
1852 keyPath := configDirPath(configFile, kp.KeyFile)
1853 cert, err := loadX509KeyPairPrivileged(certPath, keyPath)
1855 return fmt.Errorf("tls config for %q: parsing x509 key pair: %v", kind, err)
1857 certs = append(certs, cert)
1859 ctls.Config = &tls.Config{
1860 Certificates: certs,
1865// load x509 key/cert files from file descriptor possibly passed in by privileged
1867func loadX509KeyPairPrivileged(certPath, keyPath string) (tls.Certificate, error) {
1868 certBuf, err := readFilePrivileged(certPath)
1870 return tls.Certificate{}, fmt.Errorf("reading tls certificate: %v", err)
1872 keyBuf, err := readFilePrivileged(keyPath)
1874 return tls.Certificate{}, fmt.Errorf("reading tls key: %v", err)
1876 return tls.X509KeyPair(certBuf, keyBuf)
1879// like os.ReadFile, but open privileged file possibly passed in by root process.
1880func readFilePrivileged(path string) ([]byte, error) {
1881 f, err := OpenPrivileged(path)
1886 return io.ReadAll(f)