11 cryptorand "crypto/rand"
35 "golang.org/x/text/unicode/norm"
37 "github.com/mjl-/autocert"
39 "github.com/mjl-/sconf"
41 "github.com/mjl-/mox/autotls"
42 "github.com/mjl-/mox/config"
43 "github.com/mjl-/mox/dkim"
44 "github.com/mjl-/mox/dns"
45 "github.com/mjl-/mox/message"
46 "github.com/mjl-/mox/mlog"
47 "github.com/mjl-/mox/moxio"
48 "github.com/mjl-/mox/mtasts"
49 "github.com/mjl-/mox/smtp"
52var pkglog = mlog.New("mox", nil)
54// Pedantic enables stricter parsing.
57// Config paths are set early in program startup. They will point to files in
60 ConfigStaticPath string
61 ConfigDynamicPath string
62 Conf = Config{Log: map[string]slog.Level{"": slog.LevelError}}
65var ErrConfig = errors.New("config error")
67// Set by packages webadmin, webaccount, webmail, webapisrv to prevent cyclic dependencies.
68var NewWebadminHandler = func(basePath string, isForwarded bool) http.Handler { return nopHandler }
69var NewWebaccountHandler = func(basePath string, isForwarded bool) http.Handler { return nopHandler }
70var NewWebmailHandler = func(maxMsgSize int64, basePath string, isForwarded bool, accountPath string) http.Handler {
73var NewWebapiHandler = func(maxMsgSize int64, basePath string, isForwarded bool) http.Handler { return nopHandler }
75var nopHandler = http.HandlerFunc(nil)
77// Config as used in the code, a processed version of what is in the config file.
79// Use methods to lookup a domain/account/address in the dynamic configuration.
81 Static config.Static // Does not change during the lifetime of a running instance.
83 logMutex sync.Mutex // For accessing the log levels.
84 Log map[string]slog.Level
86 dynamicMutex sync.Mutex
87 Dynamic config.Dynamic // Can only be accessed directly by tests. Use methods on Config for locked access.
88 dynamicMtime time.Time
89 DynamicLastCheck time.Time // For use by quickstart only to skip checks.
91 // From canonical full address (localpart@domain, lower-cased when
92 // case-insensitive, stripped of catchall separator) to account and address.
93 // Domains are IDNA names in utf8. Dynamic config lock must be held when accessing.
94 AccountDestinationsLocked map[string]AccountDestination
96 // Like AccountDestinationsLocked, but for aliases.
97 aliases map[string]config.Alias
100type AccountDestination struct {
101 Catchall bool // If catchall destination for its domain.
102 Localpart smtp.Localpart // In original casing as written in config file.
104 Destination config.Destination
107// LogLevelSet sets a new log level for pkg. An empty pkg sets the default log
108// value that is used if no explicit log level is configured for a package.
109// This change is ephemeral, no config file is changed.
110func (c *Config) LogLevelSet(log mlog.Log, pkg string, level slog.Level) {
112 defer c.logMutex.Unlock()
113 l := c.copyLogLevels()
116 log.Print("log level changed", slog.String("pkg", pkg), slog.Any("level", mlog.LevelStrings[level]))
117 mlog.SetConfig(c.Log)
120// LogLevelRemove removes a configured log level for a package.
121func (c *Config) LogLevelRemove(log mlog.Log, pkg string) {
123 defer c.logMutex.Unlock()
124 l := c.copyLogLevels()
127 log.Print("log level cleared", slog.String("pkg", pkg))
128 mlog.SetConfig(c.Log)
131// copyLogLevels returns a copy of c.Log, for modifications.
132// must be called with log lock held.
133func (c *Config) copyLogLevels() map[string]slog.Level {
134 m := map[string]slog.Level{}
139// LogLevels returns a copy of the current log levels.
140func (c *Config) LogLevels() map[string]slog.Level {
142 defer c.logMutex.Unlock()
143 return c.copyLogLevels()
146// DynamicLockUnlock locks the dynamic config, will try updating the latest state
147// from disk, and return an unlock function. Should be called as "defer
148// Conf.DynamicLockUnlock()()".
149func (c *Config) DynamicLockUnlock() func() {
150 c.dynamicMutex.Lock()
152 if now.Sub(c.DynamicLastCheck) > time.Second {
153 c.DynamicLastCheck = now
154 if fi, err := os.Stat(ConfigDynamicPath); err != nil {
155 pkglog.Errorx("stat domains config", err)
156 } else if !fi.ModTime().Equal(c.dynamicMtime) {
157 if errs := c.loadDynamic(); len(errs) > 0 {
158 pkglog.Errorx("loading domains config", errs[0], slog.Any("errors", errs))
160 pkglog.Info("domains config reloaded")
161 c.dynamicMtime = fi.ModTime()
165 return c.dynamicMutex.Unlock
168func (c *Config) withDynamicLock(fn func()) {
169 defer c.DynamicLockUnlock()()
173// must be called with dynamic lock held.
174func (c *Config) loadDynamic() []error {
175 d, mtime, accDests, aliases, err := ParseDynamicConfig(context.Background(), pkglog, ConfigDynamicPath, c.Static)
180 c.dynamicMtime = mtime
181 c.AccountDestinationsLocked = accDests
183 c.allowACMEHosts(pkglog, true)
187// DynamicConfig returns a shallow copy of the dynamic config. Must not be modified.
188func (c *Config) DynamicConfig() (config config.Dynamic) {
189 c.withDynamicLock(func() {
190 config = c.Dynamic // Shallow copy.
195func (c *Config) Domains() (l []string) {
196 c.withDynamicLock(func() {
197 for name := range c.Dynamic.Domains {
205func (c *Config) Accounts() (l []string) {
206 c.withDynamicLock(func() {
207 for name := range c.Dynamic.Accounts {
214func (c *Config) AccountsDisabled() (all, disabled []string) {
215 c.withDynamicLock(func() {
216 for name, conf := range c.Dynamic.Accounts {
217 all = append(all, name)
218 if conf.LoginDisabled != "" {
219 disabled = append(disabled, name)
226// DomainLocalparts returns a mapping of encoded localparts to account names for a
227// domain, and encoded localparts to aliases. An empty localpart is a catchall
228// destination for a domain.
229func (c *Config) DomainLocalparts(d dns.Domain) (map[string]string, map[string]config.Alias) {
230 suffix := "@" + d.Name()
231 m := map[string]string{}
232 aliases := map[string]config.Alias{}
233 c.withDynamicLock(func() {
234 for addr, ad := range c.AccountDestinationsLocked {
235 if strings.HasSuffix(addr, suffix) {
239 m[ad.Localpart.String()] = ad.Account
243 for addr, a := range c.aliases {
244 if strings.HasSuffix(addr, suffix) {
245 aliases[a.LocalpartStr] = a
252func (c *Config) Domain(d dns.Domain) (dom config.Domain, ok bool) {
253 c.withDynamicLock(func() {
254 dom, ok = c.Dynamic.Domains[d.Name()]
259func (c *Config) DomainConfigs() (doms []config.Domain) {
260 c.withDynamicLock(func() {
261 doms = make([]config.Domain, 0, len(c.Dynamic.Domains))
262 for _, d := range c.Dynamic.Domains {
263 doms = append(doms, d)
266 slices.SortFunc(doms, func(a, b config.Domain) int {
267 return cmp.Compare(a.Domain.Name(), b.Domain.Name())
272func (c *Config) Account(name string) (acc config.Account, ok bool) {
273 c.withDynamicLock(func() {
274 acc, ok = c.Dynamic.Accounts[name]
279func (c *Config) AccountDestination(addr string) (accDest AccountDestination, alias *config.Alias, ok bool) {
280 c.withDynamicLock(func() {
281 accDest, ok = c.AccountDestinationsLocked[addr]
284 a, ok = c.aliases[addr]
293func (c *Config) Routes(accountName string, domain dns.Domain) (accountRoutes, domainRoutes, globalRoutes []config.Route) {
294 c.withDynamicLock(func() {
295 acc := c.Dynamic.Accounts[accountName]
296 accountRoutes = acc.Routes
298 dom := c.Dynamic.Domains[domain.Name()]
299 domainRoutes = dom.Routes
301 globalRoutes = c.Dynamic.Routes
306func (c *Config) IsClientSettingsDomain(d dns.Domain) (is bool) {
307 c.withDynamicLock(func() {
308 _, is = c.Dynamic.ClientSettingDomains[d]
313func (c *Config) allowACMEHosts(log mlog.Log, checkACMEHosts bool) {
314 for _, l := range c.Static.Listeners {
315 if l.TLS == nil || l.TLS.ACME == "" {
319 m := c.Static.ACME[l.TLS.ACME].Manager
320 hostnames := map[dns.Domain]struct{}{}
322 hostnames[c.Static.HostnameDomain] = struct{}{}
323 if l.HostnameDomain.ASCII != "" {
324 hostnames[l.HostnameDomain] = struct{}{}
327 for _, dom := range c.Dynamic.Domains {
328 // Do not allow TLS certificates for domains for which we only accept DMARC/TLS
329 // reports as external party.
334 // Do not fetch TLS certs for disabled domains. The A/AAAA records may not be
335 // configured or still point to a previous machine before a migration.
340 if l.AutoconfigHTTPS.Enabled && !l.AutoconfigHTTPS.NonTLS {
341 if d, err := dns.ParseDomain("autoconfig." + dom.Domain.ASCII); err != nil {
342 log.Errorx("parsing autoconfig domain", err, slog.Any("domain", dom.Domain))
344 hostnames[d] = struct{}{}
348 if l.MTASTSHTTPS.Enabled && dom.MTASTS != nil && !l.MTASTSHTTPS.NonTLS {
349 d, err := dns.ParseDomain("mta-sts." + dom.Domain.ASCII)
351 log.Errorx("parsing mta-sts domain", err, slog.Any("domain", dom.Domain))
353 hostnames[d] = struct{}{}
357 if dom.ClientSettingsDomain != "" {
358 hostnames[dom.ClientSettingsDNSDomain] = struct{}{}
362 if l.WebserverHTTPS.Enabled {
363 for from := range c.Dynamic.WebDNSDomainRedirects {
364 hostnames[from] = struct{}{}
366 for _, wh := range c.Dynamic.WebHandlers {
367 hostnames[wh.DNSDomain] = struct{}{}
371 public := c.Static.Listeners["public"]
373 if len(public.NATIPs) > 0 {
379 m.SetAllowedHostnames(log, dns.StrictResolver{Pkg: "autotls", Log: log.Logger}, hostnames, ips, checkACMEHosts)
383// todo future: write config parsing & writing code that can read a config and remembers the exact tokens including newlines and comments, and can write back a modified file. the goal is to be able to write a config file automatically (after changing fields through the ui), but not loose comments and whitespace, to still get useful diffs for storing the config in a version control system.
385// WriteDynamicLocked prepares an updated internal state for the new dynamic
386// config, then writes it to disk and activates it.
388// Returns ErrConfig if the configuration is not valid.
390// Must be called with config lock held.
391func WriteDynamicLocked(ctx context.Context, log mlog.Log, c config.Dynamic) error {
392 accDests, aliases, errs := prepareDynamicConfig(ctx, log, ConfigDynamicPath, Conf.Static, &c)
394 errstrs := make([]string, len(errs))
395 for i, err := range errs {
396 errstrs[i] = err.Error()
398 return fmt.Errorf("%w: %s", ErrConfig, strings.Join(errstrs, "; "))
402 err := sconf.Write(&b, c)
406 f, err := os.OpenFile(ConfigDynamicPath, os.O_WRONLY, 0660)
413 log.Check(err, "closing file after error")
417 if _, err := f.Write(buf); err != nil {
418 return fmt.Errorf("write domains.conf: %v", err)
420 if err := f.Truncate(int64(len(buf))); err != nil {
421 return fmt.Errorf("truncate domains.conf after write: %v", err)
423 if err := f.Sync(); err != nil {
424 return fmt.Errorf("sync domains.conf after write: %v", err)
426 if err := moxio.SyncDir(log, filepath.Dir(ConfigDynamicPath)); err != nil {
427 return fmt.Errorf("sync dir of domains.conf after write: %v", err)
432 return fmt.Errorf("stat after writing domains.conf: %v", err)
435 if err := f.Close(); err != nil {
436 return fmt.Errorf("close written domains.conf: %v", err)
440 Conf.dynamicMtime = fi.ModTime()
441 Conf.DynamicLastCheck = time.Now()
443 Conf.AccountDestinationsLocked = accDests
444 Conf.aliases = aliases
446 Conf.allowACMEHosts(log, true)
451// MustLoadConfig loads the config, quitting on errors.
452func MustLoadConfig(doLoadTLSKeyCerts, checkACMEHosts bool) {
453 errs := LoadConfig(context.Background(), pkglog, doLoadTLSKeyCerts, checkACMEHosts)
455 pkglog.Error("loading config file: multiple errors")
456 for _, err := range errs {
457 pkglog.Errorx("config error", err)
459 pkglog.Fatal("stopping after multiple config errors")
460 } else if len(errs) == 1 {
461 pkglog.Fatalx("loading config file", errs[0])
465// LoadConfig attempts to parse and load a config, returning any errors
467func LoadConfig(ctx context.Context, log mlog.Log, doLoadTLSKeyCerts, checkACMEHosts bool) []error {
468 Shutdown, ShutdownCancel = context.WithCancel(context.Background())
469 Context, ContextCancel = context.WithCancel(context.Background())
471 c, errs := ParseConfig(ctx, log, ConfigStaticPath, false, doLoadTLSKeyCerts, checkACMEHosts)
476 mlog.SetConfig(c.Log)
481// SetConfig sets a new config. Not to be used during normal operation.
482func SetConfig(c *Config) {
483 // Cannot just assign *c to Conf, it would copy the mutex.
484 Conf = Config{c.Static, sync.Mutex{}, c.Log, sync.Mutex{}, c.Dynamic, c.dynamicMtime, c.DynamicLastCheck, c.AccountDestinationsLocked, c.aliases}
486 // If we have non-standard CA roots, use them for all HTTPS requests.
487 if Conf.Static.TLS.CertPool != nil {
488 http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{
489 RootCAs: Conf.Static.TLS.CertPool,
493 SetPedantic(c.Static.Pedantic)
496// Set pedantic in all packages.
497func SetPedantic(p bool) {
505// ParseConfig parses the static config at path p. If checkOnly is true, no changes
506// are made, such as registering ACME identities. If doLoadTLSKeyCerts is true,
507// the TLS KeyCerts configuration is loaded and checked. This is used during the
508// quickstart in the case the user is going to provide their own certificates.
509// If checkACMEHosts is true, the hosts allowed for acme are compared with the
510// explicitly configured ips we are listening on.
511func ParseConfig(ctx context.Context, log mlog.Log, p string, checkOnly, doLoadTLSKeyCerts, checkACMEHosts bool) (c *Config, errs []error) {
513 Static: config.Static{
520 if os.IsNotExist(err) && os.Getenv("MOXCONF") == "" {
521 return nil, []error{fmt.Errorf("open config file: %v (hint: use mox -config ... or set MOXCONF=...)", err)}
523 return nil, []error{fmt.Errorf("open config file: %v", err)}
526 if err := sconf.Parse(f, &c.Static); err != nil {
527 return nil, []error{fmt.Errorf("parsing %s%v", p, err)}
530 if xerrs := PrepareStaticConfig(ctx, log, p, c, checkOnly, doLoadTLSKeyCerts); len(xerrs) > 0 {
534 pp := filepath.Join(filepath.Dir(p), "domains.conf")
535 c.Dynamic, c.dynamicMtime, c.AccountDestinationsLocked, c.aliases, errs = ParseDynamicConfig(ctx, log, pp, c.Static)
538 c.allowACMEHosts(log, checkACMEHosts)
544// PrepareStaticConfig parses the static config file and prepares data structures
545// for starting mox. If checkOnly is set no substantial changes are made, like
546// creating an ACME registration.
547func PrepareStaticConfig(ctx context.Context, log mlog.Log, configFile string, conf *Config, checkOnly, doLoadTLSKeyCerts bool) (errs []error) {
548 addErrorf := func(format string, args ...any) {
549 errs = append(errs, fmt.Errorf(format, args...))
554 // check that mailbox is in unicode NFC normalized form.
555 checkMailboxNormf := func(mailbox string, format string, args ...any) {
556 s := norm.NFC.String(mailbox)
558 msg := fmt.Sprintf(format, args...)
559 addErrorf("%s: mailbox %q is not in NFC normalized form, should be %q", msg, mailbox, s)
563 // Post-process logging config.
564 if logLevel, ok := mlog.Levels[c.LogLevel]; ok {
565 conf.Log = map[string]slog.Level{"": logLevel}
567 addErrorf("invalid log level %q", c.LogLevel)
569 for pkg, s := range c.PackageLogLevels {
570 if logLevel, ok := mlog.Levels[s]; ok {
571 conf.Log[pkg] = logLevel
573 addErrorf("invalid package log level %q", s)
580 u, err := user.Lookup(c.User)
582 uid, err := strconv.ParseUint(c.User, 10, 32)
584 addErrorf("parsing unknown user %s as uid: %v (hint: add user mox with \"useradd -d $PWD mox\" or specify a different username on the quickstart command-line)", c.User, err)
586 // We assume the same gid as uid.
591 if uid, err := strconv.ParseUint(u.Uid, 10, 32); err != nil {
592 addErrorf("parsing uid %s: %v", u.Uid, err)
596 if gid, err := strconv.ParseUint(u.Gid, 10, 32); err != nil {
597 addErrorf("parsing gid %s: %v", u.Gid, err)
603 hostname, err := dns.ParseDomain(c.Hostname)
605 addErrorf("parsing hostname: %s", err)
606 } else if hostname.Name() != c.Hostname {
607 addErrorf("hostname must be in unicode form %q instead of %q", hostname.Name(), c.Hostname)
609 c.HostnameDomain = hostname
611 if c.HostTLSRPT.Account != "" {
612 tlsrptLocalpart, err := smtp.ParseLocalpart(c.HostTLSRPT.Localpart)
614 addErrorf("invalid localpart %q for host tlsrpt: %v", c.HostTLSRPT.Localpart, err)
615 } else if tlsrptLocalpart.IsInternational() {
616 // Does not appear documented in
../rfc/8460, but similar to DMARC it makes sense
617 // to keep this ascii-only addresses.
618 addErrorf("host TLSRPT localpart %q is an internationalized address, only conventional ascii-only address allowed for interopability", tlsrptLocalpart)
620 c.HostTLSRPT.ParsedLocalpart = tlsrptLocalpart
623 // Return private key for host name for use with an ACME. Used to return the same
624 // private key as pre-generated for use with DANE, with its public key in DNS.
625 // We only use this key for Listener's that have this ACME configured, and for
626 // which the effective listener host name (either specific to the listener, or the
627 // global name) is requested. Other host names can get a fresh private key, they
628 // don't appear in DANE records.
630 // - run 0: only use listener with explicitly matching host name in listener
631 // (default quickstart config does not set it).
632 // - run 1: only look at public listener (and host matching mox host name)
633 // - run 2: all listeners (and host matching mox host name)
634 findACMEHostPrivateKey := func(acmeName, host string, keyType autocert.KeyType, run int) crypto.Signer {
635 for listenerName, l := range Conf.Static.Listeners {
636 if l.TLS == nil || l.TLS.ACME != acmeName {
639 if run == 0 && host != l.HostnameDomain.ASCII {
642 if run == 1 && listenerName != "public" || host != Conf.Static.HostnameDomain.ASCII {
646 case autocert.KeyRSA2048:
647 if len(l.TLS.HostPrivateRSA2048Keys) == 0 {
650 return l.TLS.HostPrivateRSA2048Keys[0]
651 case autocert.KeyECDSAP256:
652 if len(l.TLS.HostPrivateECDSAP256Keys) == 0 {
655 return l.TLS.HostPrivateECDSAP256Keys[0]
662 // Make a function for an autocert.Manager.GetPrivateKey, using findACMEHostPrivateKey.
663 makeGetPrivateKey := func(acmeName string) func(host string, keyType autocert.KeyType) (crypto.Signer, error) {
664 return func(host string, keyType autocert.KeyType) (crypto.Signer, error) {
665 key := findACMEHostPrivateKey(acmeName, host, keyType, 0)
667 key = findACMEHostPrivateKey(acmeName, host, keyType, 1)
670 key = findACMEHostPrivateKey(acmeName, host, keyType, 2)
673 log.Debug("found existing private key for certificate for host",
674 slog.String("acmename", acmeName),
675 slog.String("host", host),
676 slog.Any("keytype", keyType))
679 log.Debug("generating new private key for certificate for host",
680 slog.String("acmename", acmeName),
681 slog.String("host", host),
682 slog.Any("keytype", keyType))
684 case autocert.KeyRSA2048:
685 return rsa.GenerateKey(cryptorand.Reader, 2048)
686 case autocert.KeyECDSAP256:
687 return ecdsa.GenerateKey(elliptic.P256(), cryptorand.Reader)
689 return nil, fmt.Errorf("unrecognized requested key type %v", keyType)
693 for name, acme := range c.ACME {
694 addAcmeErrorf := func(format string, args ...any) {
695 addErrorf("acme provider %s: %s", name, fmt.Sprintf(format, args...))
700 if acme.ExternalAccountBinding != nil {
701 eabKeyID = acme.ExternalAccountBinding.KeyID
702 p := configDirPath(configFile, acme.ExternalAccountBinding.KeyFile)
703 buf, err := os.ReadFile(p)
705 addAcmeErrorf("reading external account binding key: %s", err)
707 dec := make([]byte, base64.RawURLEncoding.DecodedLen(len(buf)))
708 n, err := base64.RawURLEncoding.Decode(dec, buf)
710 addAcmeErrorf("parsing external account binding key as base64: %s", err)
721 acmeDir := dataDirPath(configFile, c.DataDir, "acme")
722 os.MkdirAll(acmeDir, 0770)
723 manager, err := autotls.Load(log, name, acmeDir, acme.ContactEmail, acme.DirectoryURL, eabKeyID, eabKey, makeGetPrivateKey(name), Shutdown.Done())
725 addAcmeErrorf("loading ACME identity: %s", err)
727 acme.Manager = manager
729 // Help configurations from older quickstarts.
730 if acme.IssuerDomainName == "" && acme.DirectoryURL == "https://acme-v02.api.letsencrypt.org/directory" {
731 acme.IssuerDomainName = "letsencrypt.org"
737 var haveUnspecifiedSMTPListener bool
738 for name, l := range c.Listeners {
739 addListenerErrorf := func(format string, args ...any) {
740 addErrorf("listener %s: %s", name, fmt.Sprintf(format, args...))
743 if l.Hostname != "" {
744 d, err := dns.ParseDomain(l.Hostname)
746 addListenerErrorf("parsing hostname %q: %s", l.Hostname, err)
751 if l.TLS.ACME != "" && len(l.TLS.KeyCerts) != 0 {
752 addListenerErrorf("cannot have ACME and static key/certificates")
753 } else if l.TLS.ACME != "" {
754 acme, ok := c.ACME[l.TLS.ACME]
756 addListenerErrorf("unknown ACME provider %q", l.TLS.ACME)
759 // If only checking or with missing ACME definition, we don't have an acme manager,
760 // so set an empty tls config to continue.
761 var tlsconfig, tlsconfigFallback *tls.Config
762 if checkOnly || acme.Manager == nil {
763 tlsconfig = &tls.Config{}
764 tlsconfigFallback = &tls.Config{}
766 hostname := c.HostnameDomain
767 if l.Hostname != "" {
768 hostname = l.HostnameDomain
770 // If SNI is absent, we will use the listener hostname, but reject connections with
771 // an SNI hostname that is not allowlisted.
772 // Incoming SMTP deliveries use tlsconfigFallback for interoperability. TLS
773 // connections for unknown SNI hostnames fall back to a certificate for the
774 // listener hostname instead of causing the TLS connection to fail.
775 tlsconfig = acme.Manager.TLSConfig(hostname, true, false)
776 tlsconfigFallback = acme.Manager.TLSConfig(hostname, true, true)
777 l.TLS.ACMEConfig = acme.Manager.ACMETLSConfig
779 l.TLS.Config = tlsconfig
780 l.TLS.ConfigFallback = tlsconfigFallback
781 } else if len(l.TLS.KeyCerts) != 0 {
782 if doLoadTLSKeyCerts {
783 if err := loadTLSKeyCerts(configFile, "listener "+name, l.TLS); err != nil {
784 addListenerErrorf("%w", err)
788 addListenerErrorf("cannot have TLS config without ACME and without static keys/certificates")
790 for _, privKeyFile := range l.TLS.HostPrivateKeyFiles {
791 keyPath := configDirPath(configFile, privKeyFile)
792 privKey, err := loadPrivateKeyFile(keyPath)
794 addListenerErrorf("parsing host private key for DANE and ACME certificates: %v", err)
797 switch k := privKey.(type) {
798 case *rsa.PrivateKey:
799 if k.N.BitLen() != 2048 {
800 log.Error("need rsa key with 2048 bits, for host private key for DANE/ACME certificates, ignoring",
801 slog.String("listener", name),
802 slog.String("file", keyPath),
803 slog.Int("bits", k.N.BitLen()))
806 l.TLS.HostPrivateRSA2048Keys = append(l.TLS.HostPrivateRSA2048Keys, k)
807 case *ecdsa.PrivateKey:
808 if k.Curve != elliptic.P256() {
809 log.Error("unrecognized ecdsa curve for host private key for DANE/ACME certificates, ignoring", slog.String("listener", name), slog.String("file", keyPath))
812 l.TLS.HostPrivateECDSAP256Keys = append(l.TLS.HostPrivateECDSAP256Keys, k)
814 log.Error("unrecognized key type for host private key for DANE/ACME certificates, ignoring",
815 slog.String("listener", name),
816 slog.String("file", keyPath),
817 slog.String("keytype", fmt.Sprintf("%T", privKey)))
821 if l.TLS.ACME != "" && (len(l.TLS.HostPrivateRSA2048Keys) == 0) != (len(l.TLS.HostPrivateECDSAP256Keys) == 0) {
822 log.Warn("uncommon configuration with either only an RSA 2048 or ECDSA P256 host private key for DANE/ACME certificates; this ACME implementation can retrieve certificates for both type of keys, it is recommended to set either both or none; continuing")
826 var minVersion uint16 = tls.VersionTLS12
827 if l.TLS.MinVersion != "" {
828 versions := map[string]uint16{
829 "TLSv1.0": tls.VersionTLS10,
830 "TLSv1.1": tls.VersionTLS11,
831 "TLSv1.2": tls.VersionTLS12,
832 "TLSv1.3": tls.VersionTLS13,
834 v, ok := versions[l.TLS.MinVersion]
836 addListenerErrorf("unknown TLS mininum version %q", l.TLS.MinVersion)
840 if l.TLS.Config != nil {
841 l.TLS.Config.MinVersion = minVersion
843 if l.TLS.ConfigFallback != nil {
844 l.TLS.ConfigFallback.MinVersion = minVersion
846 if l.TLS.ACMEConfig != nil {
847 l.TLS.ACMEConfig.MinVersion = minVersion
850 var needsTLS []string
851 needtls := func(s string, v bool) {
853 needsTLS = append(needsTLS, s)
856 needtls("IMAPS", l.IMAPS.Enabled)
857 needtls("SMTP", l.SMTP.Enabled && !l.SMTP.NoSTARTTLS)
858 needtls("Submissions", l.Submissions.Enabled)
859 needtls("Submission", l.Submission.Enabled && !l.Submission.NoRequireSTARTTLS)
860 needtls("AccountHTTPS", l.AccountHTTPS.Enabled)
861 needtls("AdminHTTPS", l.AdminHTTPS.Enabled)
862 needtls("AutoconfigHTTPS", l.AutoconfigHTTPS.Enabled && !l.AutoconfigHTTPS.NonTLS)
863 needtls("MTASTSHTTPS", l.MTASTSHTTPS.Enabled && !l.MTASTSHTTPS.NonTLS)
864 needtls("WebserverHTTPS", l.WebserverHTTPS.Enabled)
865 if len(needsTLS) > 0 {
866 addListenerErrorf("no tls config specified, but requires tls for %s", strings.Join(needsTLS, ", "))
869 if l.AutoconfigHTTPS.Enabled && l.MTASTSHTTPS.Enabled && l.AutoconfigHTTPS.Port == l.MTASTSHTTPS.Port && l.AutoconfigHTTPS.NonTLS != l.MTASTSHTTPS.NonTLS {
870 addListenerErrorf("autoconfig and mta-sts enabled on same port but with both http and https")
874 haveUnspecifiedSMTPListener = true
876 for _, ipstr := range l.IPs {
877 ip := net.ParseIP(ipstr)
879 addListenerErrorf("invalid IP %q", ipstr)
882 if ip.IsUnspecified() {
883 haveUnspecifiedSMTPListener = true
886 if len(c.SpecifiedSMTPListenIPs) >= 2 {
887 haveUnspecifiedSMTPListener = true
888 } else if len(c.SpecifiedSMTPListenIPs) > 0 && (c.SpecifiedSMTPListenIPs[0].To4() == nil) == (ip.To4() == nil) {
889 haveUnspecifiedSMTPListener = true
891 c.SpecifiedSMTPListenIPs = append(c.SpecifiedSMTPListenIPs, ip)
895 for _, s := range l.SMTP.DNSBLs {
896 d, err := dns.ParseDomain(s)
898 addListenerErrorf("parsing DNSBL zone %q: %s", s, err)
901 l.SMTP.DNSBLZones = append(l.SMTP.DNSBLZones, d)
903 if l.IPsNATed && len(l.NATIPs) > 0 {
904 addListenerErrorf("both IPsNATed and NATIPs configued (remove deprecated IPsNATed)")
906 for _, ipstr := range l.NATIPs {
907 ip := net.ParseIP(ipstr)
909 addListenerErrorf("invalid ip %q", ipstr)
910 } else if ip.IsUnspecified() || ip.IsLoopback() {
911 addListenerErrorf("NAT ip that is the unspecified or loopback address %s", ipstr)
914 cleanPath := func(kind string, enabled bool, path string) string {
918 if path != "" && !strings.HasPrefix(path, "/") {
919 addListenerErrorf("%s with path %q that must start with a slash", kind, path)
920 } else if path != "" && !strings.HasSuffix(path, "/") {
921 log.Warn("http service path should end with a slash, using effective path with slash", slog.String("kind", kind), slog.String("path", path), slog.String("effectivepath", path+"/"))
926 l.AccountHTTP.Path = cleanPath("AccountHTTP", l.AccountHTTP.Enabled, l.AccountHTTP.Path)
927 l.AccountHTTPS.Path = cleanPath("AccountHTTPS", l.AccountHTTPS.Enabled, l.AccountHTTPS.Path)
928 l.AdminHTTP.Path = cleanPath("AdminHTTP", l.AdminHTTP.Enabled, l.AdminHTTP.Path)
929 l.AdminHTTPS.Path = cleanPath("AdminHTTPS", l.AdminHTTPS.Enabled, l.AdminHTTPS.Path)
930 l.WebmailHTTP.Path = cleanPath("WebmailHTTP", l.WebmailHTTP.Enabled, l.WebmailHTTP.Path)
931 l.WebmailHTTPS.Path = cleanPath("WebmailHTTPS", l.WebmailHTTPS.Enabled, l.WebmailHTTPS.Path)
932 l.WebAPIHTTP.Path = cleanPath("WebAPIHTTP", l.WebAPIHTTP.Enabled, l.WebAPIHTTP.Path)
933 l.WebAPIHTTPS.Path = cleanPath("WebAPIHTTPS", l.WebAPIHTTPS.Enabled, l.WebAPIHTTPS.Path)
934 c.Listeners[name] = l
936 if haveUnspecifiedSMTPListener {
937 c.SpecifiedSMTPListenIPs = nil
940 var zerouse config.SpecialUseMailboxes
941 if len(c.DefaultMailboxes) > 0 && (c.InitialMailboxes.SpecialUse != zerouse || len(c.InitialMailboxes.Regular) > 0) {
942 addErrorf("cannot have both DefaultMailboxes and InitialMailboxes")
944 // DefaultMailboxes is deprecated.
945 for _, mb := range c.DefaultMailboxes {
946 checkMailboxNormf(mb, "default mailbox")
947 // We don't create parent mailboxes for default mailboxes.
948 if ParentMailboxName(mb) != "" {
949 addErrorf("default mailbox cannot be a child mailbox")
952 checkSpecialUseMailbox := func(nameOpt string) {
954 checkMailboxNormf(nameOpt, "special-use initial mailbox")
955 if strings.EqualFold(nameOpt, "inbox") {
956 addErrorf("initial mailbox cannot be set to Inbox (Inbox is always created)")
958 // We don't currently create parent mailboxes for initial mailboxes.
959 if ParentMailboxName(nameOpt) != "" {
960 addErrorf("initial mailboxes cannot be child mailboxes")
964 checkSpecialUseMailbox(c.InitialMailboxes.SpecialUse.Archive)
965 checkSpecialUseMailbox(c.InitialMailboxes.SpecialUse.Draft)
966 checkSpecialUseMailbox(c.InitialMailboxes.SpecialUse.Junk)
967 checkSpecialUseMailbox(c.InitialMailboxes.SpecialUse.Sent)
968 checkSpecialUseMailbox(c.InitialMailboxes.SpecialUse.Trash)
969 for _, name := range c.InitialMailboxes.Regular {
970 checkMailboxNormf(name, "regular initial mailbox")
971 if strings.EqualFold(name, "inbox") {
972 addErrorf("initial regular mailbox cannot be set to Inbox (Inbox is always created)")
974 if ParentMailboxName(name) != "" {
975 addErrorf("initial mailboxes cannot be child mailboxes")
979 checkTransportSMTP := func(name string, isTLS bool, t *config.TransportSMTP) {
980 addTransportErrorf := func(format string, args ...any) {
981 addErrorf("transport %s: %s", name, fmt.Sprintf(format, args...))
985 t.DNSHost, err = dns.ParseDomain(t.Host)
987 addTransportErrorf("bad host %s: %v", t.Host, err)
990 if isTLS && t.STARTTLSInsecureSkipVerify {
991 addTransportErrorf("cannot have STARTTLSInsecureSkipVerify with immediate TLS")
993 if isTLS && t.NoSTARTTLS {
994 addTransportErrorf("cannot have NoSTARTTLS with immediate TLS")
1000 seen := map[string]bool{}
1001 for _, m := range t.Auth.Mechanisms {
1003 addTransportErrorf("duplicate authentication mechanism %s", m)
1007 case "SCRAM-SHA-256-PLUS":
1008 case "SCRAM-SHA-256":
1009 case "SCRAM-SHA-1-PLUS":
1014 addTransportErrorf("unknown authentication mechanism %s", m)
1018 t.Auth.EffectiveMechanisms = t.Auth.Mechanisms
1019 if len(t.Auth.EffectiveMechanisms) == 0 {
1020 t.Auth.EffectiveMechanisms = []string{"SCRAM-SHA-256-PLUS", "SCRAM-SHA-256", "SCRAM-SHA-1-PLUS", "SCRAM-SHA-1", "CRAM-MD5"}
1024 checkTransportSocks := func(name string, t *config.TransportSocks) {
1025 addTransportErrorf := func(format string, args ...any) {
1026 addErrorf("transport %s: %s", name, fmt.Sprintf(format, args...))
1029 _, _, err := net.SplitHostPort(t.Address)
1031 addTransportErrorf("bad address %s: %v", t.Address, err)
1033 for _, ipstr := range t.RemoteIPs {
1034 ip := net.ParseIP(ipstr)
1036 addTransportErrorf("bad ip %s", ipstr)
1038 t.IPs = append(t.IPs, ip)
1041 t.Hostname, err = dns.ParseDomain(t.RemoteHostname)
1043 addTransportErrorf("bad hostname %s: %v", t.RemoteHostname, err)
1047 checkTransportDirect := func(name string, t *config.TransportDirect) {
1048 addTransportErrorf := func(format string, args ...any) {
1049 addErrorf("transport %s: %s", name, fmt.Sprintf(format, args...))
1052 if t.DisableIPv4 && t.DisableIPv6 {
1053 addTransportErrorf("both IPv4 and IPv6 are disabled, enable at least one")
1064 checkTransportFail := func(name string, t *config.TransportFail) {
1065 addTransportErrorf := func(format string, args ...any) {
1066 addErrorf("transport %s: %s", name, fmt.Sprintf(format, args...))
1069 if t.SMTPCode == 0 {
1070 t.Code = smtp.C554TransactionFailed
1071 } else if t.SMTPCode/100 != 4 && t.SMTPCode/100 != 5 {
1072 addTransportErrorf("smtp code %d must be 4xx or 5xx", t.SMTPCode/100)
1077 if len(t.SMTPMessage) > 256 {
1078 addTransportErrorf("message must be <= 256 characters")
1080 for _, c := range t.SMTPMessage {
1081 if c < ' ' || c >= 0x7f {
1082 addTransportErrorf("message cannot contain control characters including newlines, and must be ascii-only")
1085 t.Message = t.SMTPMessage
1086 if t.Message == "" {
1087 t.Message = "transport fail: explicit immediate delivery failure per configuration"
1091 for name, t := range c.Transports {
1092 addTransportErrorf := func(format string, args ...any) {
1093 addErrorf("transport %s: %s", name, fmt.Sprintf(format, args...))
1097 if t.Submissions != nil {
1099 checkTransportSMTP(name, true, t.Submissions)
1101 if t.Submission != nil {
1103 checkTransportSMTP(name, false, t.Submission)
1107 checkTransportSMTP(name, false, t.SMTP)
1111 checkTransportSocks(name, t.Socks)
1113 if t.Direct != nil {
1115 checkTransportDirect(name, t.Direct)
1119 checkTransportFail(name, t.Fail)
1122 addTransportErrorf("cannot have multiple methods in a transport")
1126 // Load CA certificate pool.
1127 if c.TLS.CA != nil {
1128 if c.TLS.CA.AdditionalToSystem {
1130 c.TLS.CertPool, err = x509.SystemCertPool()
1132 addErrorf("fetching system CA cert pool: %v", err)
1135 c.TLS.CertPool = x509.NewCertPool()
1137 for _, certfile := range c.TLS.CA.CertFiles {
1138 p := configDirPath(configFile, certfile)
1139 pemBuf, err := os.ReadFile(p)
1141 addErrorf("reading TLS CA cert file: %v", err)
1143 } else if !c.TLS.CertPool.AppendCertsFromPEM(pemBuf) {
1144 // todo: can we check more fully if we're getting some useful data back?
1145 addErrorf("no CA certs added from %q", p)
1152// PrepareDynamicConfig parses the dynamic config file given a static file.
1153func 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) {
1154 addErrorf := func(format string, args ...any) {
1155 errs = append(errs, fmt.Errorf(format, args...))
1158 f, err := os.Open(dynamicPath)
1160 addErrorf("parsing domains config: %v", err)
1166 addErrorf("stat domains config: %v", err)
1168 if err := sconf.Parse(f, &c); err != nil {
1169 addErrorf("parsing dynamic config file: %v", err)
1173 accDests, aliases, errs = prepareDynamicConfig(ctx, log, dynamicPath, static, &c)
1174 return c, fi.ModTime(), accDests, aliases, errs
1177func 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) {
1178 addErrorf := func(format string, args ...any) {
1179 errs = append(errs, fmt.Errorf(format, args...))
1182 // Check that mailbox is in unicode NFC normalized form.
1183 checkMailboxNormf := func(mailbox string, what string, errorf func(format string, args ...any)) {
1184 s := norm.NFC.String(mailbox)
1186 errorf("%s: mailbox %q is not in NFC normalized form, should be %q", what, mailbox, s)
1190 // Validate postmaster account exists.
1191 if _, ok := c.Accounts[static.Postmaster.Account]; !ok {
1192 addErrorf("postmaster account %q does not exist", static.Postmaster.Account)
1194 checkMailboxNormf(static.Postmaster.Mailbox, "postmaster mailbox", addErrorf)
1196 accDests = map[string]AccountDestination{}
1197 aliases = map[string]config.Alias{}
1199 // Validate host TLSRPT account/address.
1200 if static.HostTLSRPT.Account != "" {
1201 if _, ok := c.Accounts[static.HostTLSRPT.Account]; !ok {
1202 addErrorf("host tlsrpt account %q does not exist", static.HostTLSRPT.Account)
1204 checkMailboxNormf(static.HostTLSRPT.Mailbox, "host tlsrpt mailbox", addErrorf)
1206 // Localpart has been parsed already.
1208 addrFull := smtp.NewAddress(static.HostTLSRPT.ParsedLocalpart, static.HostnameDomain).String()
1209 dest := config.Destination{
1210 Mailbox: static.HostTLSRPT.Mailbox,
1211 HostTLSReports: true,
1213 accDests[addrFull] = AccountDestination{false, static.HostTLSRPT.ParsedLocalpart, static.HostTLSRPT.Account, dest}
1216 var haveSTSListener, haveWebserverListener bool
1217 for _, l := range static.Listeners {
1218 if l.MTASTSHTTPS.Enabled {
1219 haveSTSListener = true
1221 if l.WebserverHTTP.Enabled || l.WebserverHTTPS.Enabled {
1222 haveWebserverListener = true
1226 checkRoutes := func(descr string, routes []config.Route) {
1227 parseRouteDomains := func(l []string) []string {
1229 for _, e := range l {
1235 if strings.HasPrefix(e, ".") {
1239 d, err := dns.ParseDomain(e)
1241 addErrorf("%s: invalid domain %s: %v", descr, e, err)
1243 r = append(r, prefix+d.ASCII)
1248 for i := range routes {
1249 routes[i].FromDomainASCII = parseRouteDomains(routes[i].FromDomain)
1250 routes[i].ToDomainASCII = parseRouteDomains(routes[i].ToDomain)
1252 routes[i].ResolvedTransport, ok = static.Transports[routes[i].Transport]
1254 addErrorf("%s: route references undefined transport %s", descr, routes[i].Transport)
1259 checkRoutes("global routes", c.Routes)
1261 // Validate domains.
1262 c.ClientSettingDomains = map[dns.Domain]struct{}{}
1263 for d, domain := range c.Domains {
1264 addDomainErrorf := func(format string, args ...any) {
1265 addErrorf(fmt.Sprintf("domain %v: %s", d, fmt.Sprintf(format, args...)))
1268 dnsdomain, err := dns.ParseDomain(d)
1270 addDomainErrorf("parsing domain: %s", err)
1271 } else if dnsdomain.Name() != d {
1272 addDomainErrorf("must be specified in unicode form, %s", dnsdomain.Name())
1275 domain.Domain = dnsdomain
1277 if domain.ClientSettingsDomain != "" {
1278 csd, err := dns.ParseDomain(domain.ClientSettingsDomain)
1280 addDomainErrorf("bad client settings domain %q: %s", domain.ClientSettingsDomain, err)
1282 domain.ClientSettingsDNSDomain = csd
1283 c.ClientSettingDomains[csd] = struct{}{}
1286 if domain.LocalpartCatchallSeparator != "" && len(domain.LocalpartCatchallSeparators) != 0 {
1287 addDomainErrorf("cannot have both LocalpartCatchallSeparator and LocalpartCatchallSeparators")
1289 domain.LocalpartCatchallSeparatorsEffective = domain.LocalpartCatchallSeparators
1290 if domain.LocalpartCatchallSeparator != "" {
1291 domain.LocalpartCatchallSeparatorsEffective = append(domain.LocalpartCatchallSeparatorsEffective, domain.LocalpartCatchallSeparator)
1293 sepSeen := map[string]bool{}
1294 for _, sep := range domain.LocalpartCatchallSeparatorsEffective {
1296 addDomainErrorf("duplicate localpart catchall separator %q", sep)
1301 for _, sign := range domain.DKIM.Sign {
1302 if _, ok := domain.DKIM.Selectors[sign]; !ok {
1303 addDomainErrorf("unknown selector %s for signing", sign)
1306 for name, sel := range domain.DKIM.Selectors {
1307 addSelectorErrorf := func(format string, args ...any) {
1308 addDomainErrorf("selector %s: %s", name, fmt.Sprintf(format, args...))
1311 seld, err := dns.ParseDomain(name)
1313 addSelectorErrorf("parsing selector: %s", err)
1314 } else if seld.Name() != name {
1315 addSelectorErrorf("must be specified in unicode form, %q", seld.Name())
1319 if sel.Expiration != "" {
1320 exp, err := time.ParseDuration(sel.Expiration)
1322 addSelectorErrorf("invalid expiration %q: %v", sel.Expiration, err)
1324 sel.ExpirationSeconds = int(exp / time.Second)
1328 sel.HashEffective = sel.Hash
1329 switch sel.HashEffective {
1331 sel.HashEffective = "sha256"
1333 log.Error("using sha1 with DKIM is deprecated as not secure enough, switch to sha256")
1336 addSelectorErrorf("unsupported hash %q", sel.HashEffective)
1339 pemBuf, err := os.ReadFile(configDirPath(dynamicPath, sel.PrivateKeyFile))
1341 addSelectorErrorf("reading private key: %s", err)
1344 p, _ := pem.Decode(pemBuf)
1346 addSelectorErrorf("private key has no PEM block")
1349 key, err := x509.ParsePKCS8PrivateKey(p.Bytes)
1351 addSelectorErrorf("parsing private key: %s", err)
1354 switch k := key.(type) {
1355 case *rsa.PrivateKey:
1356 if k.N.BitLen() < 1024 {
1358 // Let's help user do the right thing.
1359 addSelectorErrorf("rsa keys should be >= 1024 bits, is %d bits", k.N.BitLen())
1362 sel.Algorithm = fmt.Sprintf("rsa-%d", k.N.BitLen())
1363 case ed25519.PrivateKey:
1364 if sel.HashEffective != "sha256" {
1365 addSelectorErrorf("hash algorithm %q is not supported with ed25519, only sha256 is", sel.HashEffective)
1368 sel.Algorithm = "ed25519"
1370 addSelectorErrorf("private key type %T not yet supported", key)
1373 if len(sel.Headers) == 0 {
1377 // By default we seal signed headers, and we sign user-visible headers to
1378 // prevent/limit reuse of previously signed messages: All addressing fields, date
1379 // and subject, message-referencing fields, parsing instructions (content-type).
1380 sel.HeadersEffective = strings.Split("From,To,Cc,Bcc,Reply-To,References,In-Reply-To,Subject,Date,Message-Id,Content-Type", ",")
1383 for _, h := range sel.Headers {
1384 from = from || strings.EqualFold(h, "From")
1386 if strings.EqualFold(h, "DKIM-Signature") || strings.EqualFold(h, "Received") || strings.EqualFold(h, "Return-Path") {
1387 log.Error("DKIM-signing header %q is recommended against as it may be modified in transit")
1391 addSelectorErrorf("From-field must always be DKIM-signed")
1393 sel.HeadersEffective = sel.Headers
1396 domain.DKIM.Selectors[name] = sel
1399 if domain.MTASTS != nil {
1400 if !haveSTSListener {
1401 addDomainErrorf("MTA-STS enabled, but there is no listener for MTASTS", d)
1403 sts := domain.MTASTS
1404 if sts.PolicyID == "" {
1405 addDomainErrorf("invalid empty MTA-STS PolicyID")
1408 case mtasts.ModeNone, mtasts.ModeTesting, mtasts.ModeEnforce:
1410 addDomainErrorf("invalid mtasts mode %q", sts.Mode)
1414 checkRoutes("routes for domain", domain.Routes)
1416 c.Domains[d] = domain
1419 // To determine ReportsOnly.
1420 domainHasAddress := map[string]bool{}
1422 // Validate email addresses.
1423 for accName, acc := range c.Accounts {
1424 addAccountErrorf := func(format string, args ...any) {
1425 addErrorf("account %q: %s", accName, fmt.Sprintf(format, args...))
1429 acc.DNSDomain, err = dns.ParseDomain(acc.Domain)
1431 addAccountErrorf("parsing domain %s: %s", acc.Domain, err)
1434 if strings.EqualFold(acc.RejectsMailbox, "Inbox") {
1435 addAccountErrorf("cannot set RejectsMailbox to inbox, messages will be removed automatically from the rejects mailbox")
1437 checkMailboxNormf(acc.RejectsMailbox, "rejects mailbox", addErrorf)
1439 if len(acc.LoginDisabled) > 256 {
1440 addAccountErrorf("message for disabled login must be <256 characters")
1442 for _, c := range acc.LoginDisabled {
1443 // For IMAP and SMTP. IMAP only allows UTF8 after "ENABLE IMAPrev2".
1444 if c < ' ' || c >= 0x7f {
1445 addAccountErrorf("message cannot contain control characters including newlines, and must be ascii-only")
1449 if acc.AutomaticJunkFlags.JunkMailboxRegexp != "" {
1450 r, err := regexp.Compile(acc.AutomaticJunkFlags.JunkMailboxRegexp)
1452 addAccountErrorf("invalid JunkMailboxRegexp regular expression: %v", err)
1456 if acc.AutomaticJunkFlags.NeutralMailboxRegexp != "" {
1457 r, err := regexp.Compile(acc.AutomaticJunkFlags.NeutralMailboxRegexp)
1459 addAccountErrorf("invalid NeutralMailboxRegexp regular expression: %v", err)
1461 acc.NeutralMailbox = r
1463 if acc.AutomaticJunkFlags.NotJunkMailboxRegexp != "" {
1464 r, err := regexp.Compile(acc.AutomaticJunkFlags.NotJunkMailboxRegexp)
1466 addAccountErrorf("invalid NotJunkMailboxRegexp regular expression: %v", err)
1468 acc.NotJunkMailbox = r
1471 if acc.JunkFilter != nil {
1472 params := acc.JunkFilter.Params
1473 if params.MaxPower < 0 || params.MaxPower > 0.5 {
1474 addAccountErrorf("junk filter MaxPower must be >= 0 and < 0.5")
1476 if params.TopWords < 0 {
1477 addAccountErrorf("junk filter TopWords must be >= 0")
1479 if params.IgnoreWords < 0 || params.IgnoreWords > 0.5 {
1480 addAccountErrorf("junk filter IgnoreWords must be >= 0 and < 0.5")
1482 if params.RareWords < 0 {
1483 addAccountErrorf("junk filter RareWords must be >= 0")
1487 acc.ParsedFromIDLoginAddresses = make([]smtp.Address, len(acc.FromIDLoginAddresses))
1488 for i, s := range acc.FromIDLoginAddresses {
1489 a, err := smtp.ParseAddress(s)
1491 addAccountErrorf("invalid fromid login address %q: %v", s, err)
1493 // We check later on if address belongs to account.
1494 dom, ok := c.Domains[a.Domain.Name()]
1496 addAccountErrorf("unknown domain in fromid login address %q", s)
1497 } else if len(dom.LocalpartCatchallSeparatorsEffective) == 0 {
1498 addAccountErrorf("localpart catchall separator not configured for domain for fromid login address %q", s)
1500 acc.ParsedFromIDLoginAddresses[i] = a
1503 // Clear any previously derived state.
1506 c.Accounts[accName] = acc
1508 if acc.OutgoingWebhook != nil {
1509 u, err := url.Parse(acc.OutgoingWebhook.URL)
1510 if err == nil && (u.Scheme != "http" && u.Scheme != "https") {
1511 err = errors.New("scheme must be http or https")
1514 addAccountErrorf("parsing outgoing hook url %q: %v", acc.OutgoingWebhook.URL, err)
1517 // note: outgoing hook events are in ../queue/hooks.go, ../mox-/config.go, ../queue.go and ../webapi/gendoc.sh. keep in sync.
1518 outgoingHookEvents := []string{"delivered", "suppressed", "delayed", "failed", "relayed", "expanded", "canceled", "unrecognized"}
1519 for _, e := range acc.OutgoingWebhook.Events {
1520 if !slices.Contains(outgoingHookEvents, e) {
1521 addAccountErrorf("unknown outgoing hook event %q", e)
1525 if acc.IncomingWebhook != nil {
1526 u, err := url.Parse(acc.IncomingWebhook.URL)
1527 if err == nil && (u.Scheme != "http" && u.Scheme != "https") {
1528 err = errors.New("scheme must be http or https")
1531 addAccountErrorf("parsing incoming hook url %q: %v", acc.IncomingWebhook.URL, err)
1535 // 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.
1536 replaceLocalparts := map[string]string{}
1538 for addrName, dest := range acc.Destinations {
1539 addDestErrorf := func(format string, args ...any) {
1540 addAccountErrorf("destination %q: %s", addrName, fmt.Sprintf(format, args...))
1543 checkMailboxNormf(dest.Mailbox, "destination mailbox", addDestErrorf)
1545 if dest.SMTPError != "" {
1546 if len(dest.SMTPError) > 256 {
1547 addDestErrorf("smtp error must be smaller than 256 bytes")
1549 for _, c := range dest.SMTPError {
1550 if c < ' ' || c >= 0x7f {
1551 addDestErrorf("smtp error cannot contain contain control characters (including newlines) or non-ascii")
1556 if dest.Mailbox != "" {
1557 addDestErrorf("cannot have both SMTPError and Mailbox")
1559 if len(dest.Rulesets) != 0 {
1560 addDestErrorf("cannot have both SMTPError and Rulesets")
1563 t := strings.SplitN(dest.SMTPError, " ", 2)
1566 addDestErrorf("smtp error must be 421 or 550 (with optional message), not %q", dest.SMTPError)
1569 dest.SMTPErrorCode = smtp.C451LocalErr
1570 dest.SMTPErrorSecode = smtp.SeSys3Other0
1571 dest.SMTPErrorMsg = "error processing"
1573 dest.SMTPErrorCode = smtp.C550MailboxUnavail
1574 dest.SMTPErrorSecode = smtp.SeAddr1UnknownDestMailbox1
1575 dest.SMTPErrorMsg = "no such user(s)"
1578 dest.SMTPErrorMsg = strings.TrimSpace(t[1])
1580 acc.Destinations[addrName] = dest
1583 if dest.MessageAuthRequiredSMTPError != "" {
1584 if len(dest.MessageAuthRequiredSMTPError) > 256 {
1585 addDestErrorf("message authentication required smtp error must be smaller than 256 bytes")
1587 for _, c := range dest.MessageAuthRequiredSMTPError {
1588 if c < ' ' || c >= 0x7f {
1589 addDestErrorf("message authentication required smtp error cannot contain contain control characters (including newlines) or non-ascii")
1595 for i, rs := range dest.Rulesets {
1596 addRulesetErrorf := func(format string, args ...any) {
1597 addDestErrorf("ruleset %d: %s", i+1, fmt.Sprintf(format, args...))
1600 checkMailboxNormf(rs.Mailbox, "ruleset mailbox", addRulesetErrorf)
1604 if rs.SMTPMailFromRegexp != "" {
1606 r, err := regexp.Compile(rs.SMTPMailFromRegexp)
1608 addRulesetErrorf("invalid SMTPMailFrom regular expression: %v", err)
1610 c.Accounts[accName].Destinations[addrName].Rulesets[i].SMTPMailFromRegexpCompiled = r
1612 if rs.MsgFromRegexp != "" {
1614 r, err := regexp.Compile(rs.MsgFromRegexp)
1616 addRulesetErrorf("invalid MsgFrom regular expression: %v", err)
1618 c.Accounts[accName].Destinations[addrName].Rulesets[i].MsgFromRegexpCompiled = r
1620 if rs.VerifiedDomain != "" {
1622 d, err := dns.ParseDomain(rs.VerifiedDomain)
1624 addRulesetErrorf("invalid VerifiedDomain: %v", err)
1626 c.Accounts[accName].Destinations[addrName].Rulesets[i].VerifiedDNSDomain = d
1629 var hdr [][2]*regexp.Regexp
1630 for k, v := range rs.HeadersRegexp {
1632 if strings.ToLower(k) != k {
1633 addRulesetErrorf("header field %q must only have lower case characters", k)
1635 if strings.ToLower(v) != v {
1636 addRulesetErrorf("header value %q must only have lower case characters", v)
1638 rk, err := regexp.Compile(k)
1640 addRulesetErrorf("invalid rule header regexp %q: %v", k, err)
1642 rv, err := regexp.Compile(v)
1644 addRulesetErrorf("invalid rule header regexp %q: %v", v, err)
1646 hdr = append(hdr, [...]*regexp.Regexp{rk, rv})
1648 c.Accounts[accName].Destinations[addrName].Rulesets[i].HeadersRegexpCompiled = hdr
1651 addRulesetErrorf("ruleset must have at least one rule")
1654 if rs.IsForward && rs.ListAllowDomain != "" {
1655 addRulesetErrorf("ruleset cannot have both IsForward and ListAllowDomain")
1658 if rs.SMTPMailFromRegexp == "" || rs.VerifiedDomain == "" {
1659 addRulesetErrorf("ruleset with IsForward must have both SMTPMailFromRegexp and VerifiedDomain too")
1662 if rs.ListAllowDomain != "" {
1663 d, err := dns.ParseDomain(rs.ListAllowDomain)
1665 addRulesetErrorf("invalid ListAllowDomain %q: %v", rs.ListAllowDomain, err)
1667 c.Accounts[accName].Destinations[addrName].Rulesets[i].ListAllowDNSDomain = d
1670 checkMailboxNormf(rs.AcceptRejectsToMailbox, "rejects mailbox", addRulesetErrorf)
1671 if strings.EqualFold(rs.AcceptRejectsToMailbox, "inbox") {
1672 addRulesetErrorf("AcceptRejectsToMailbox cannot be set to Inbox")
1676 // Catchall destination for domain.
1677 if strings.HasPrefix(addrName, "@") {
1678 d, err := dns.ParseDomain(addrName[1:])
1680 addDestErrorf("parsing domain %q", addrName[1:])
1682 } else if _, ok := c.Domains[d.Name()]; !ok {
1683 addDestErrorf("unknown domain for address")
1686 domainHasAddress[d.Name()] = true
1687 addrFull := "@" + d.Name()
1688 if _, ok := accDests[addrFull]; ok {
1689 addDestErrorf("duplicate canonicalized catchall destination address %s", addrFull)
1691 accDests[addrFull] = AccountDestination{true, "", accName, dest}
1695 // todo deprecated: remove support for parsing destination as just a localpart instead full address.
1696 var address smtp.Address
1697 if localpart, err := smtp.ParseLocalpart(addrName); err != nil && errors.Is(err, smtp.ErrBadLocalpart) {
1698 address, err = smtp.ParseAddress(addrName)
1700 addDestErrorf("invalid email address")
1702 } else if _, ok := c.Domains[address.Domain.Name()]; !ok {
1703 addDestErrorf("unknown domain for address")
1708 addDestErrorf("invalid localpart %q", addrName)
1711 address = smtp.NewAddress(localpart, acc.DNSDomain)
1712 if _, ok := c.Domains[acc.DNSDomain.Name()]; !ok {
1713 addDestErrorf("unknown domain %s", acc.DNSDomain.Name())
1716 replaceLocalparts[addrName] = address.Pack(true)
1719 origLP := address.Localpart
1720 dc := c.Domains[address.Domain.Name()]
1721 domainHasAddress[address.Domain.Name()] = true
1722 lp := CanonicalLocalpart(address.Localpart, dc)
1724 for _, sep := range dc.LocalpartCatchallSeparatorsEffective {
1725 if strings.Contains(string(address.Localpart), sep) {
1727 addDestErrorf("localpart of address %s includes domain catchall separator %s", address, sep)
1731 address.Localpart = lp
1733 addrFull := address.Pack(true)
1734 if _, ok := accDests[addrFull]; ok {
1735 addDestErrorf("duplicate canonicalized destination address %s", addrFull)
1737 accDests[addrFull] = AccountDestination{false, origLP, accName, dest}
1740 for lp, addr := range replaceLocalparts {
1741 dest, ok := acc.Destinations[lp]
1743 addAccountErrorf("could not find localpart %q to replace with address in destinations", lp)
1745 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`,
1746 slog.Any("localpart", lp),
1747 slog.Any("address", addr),
1748 slog.String("account", accName))
1749 acc.Destinations[addr] = dest
1750 delete(acc.Destinations, lp)
1754 // Now that all addresses are parsed, check if all fromid login addresses match
1755 // configured addresses.
1756 for i, a := range acc.ParsedFromIDLoginAddresses {
1757 // For domain catchall.
1758 if _, ok := accDests["@"+a.Domain.Name()]; ok {
1761 dc := c.Domains[a.Domain.Name()]
1762 a.Localpart = CanonicalLocalpart(a.Localpart, dc)
1763 if _, ok := accDests[a.Pack(true)]; !ok {
1764 addAccountErrorf("fromid login address %q does not match its destination addresses", acc.FromIDLoginAddresses[i])
1768 checkRoutes("routes for account", acc.Routes)
1771 // Set DMARC destinations.
1772 for d, domain := range c.Domains {
1773 addDomainErrorf := func(format string, args ...any) {
1774 addErrorf("domain %s: %s", d, fmt.Sprintf(format, args...))
1777 dmarc := domain.DMARC
1781 if _, ok := c.Accounts[dmarc.Account]; !ok {
1782 addDomainErrorf("DMARC account %q does not exist", dmarc.Account)
1785 // Note: For backwards compabilitiy, DMARC reporting localparts can contain catchall separators.
1786 lp, err := smtp.ParseLocalpart(dmarc.Localpart)
1788 addDomainErrorf("invalid DMARC localpart %q: %s", dmarc.Localpart, err)
1790 if lp.IsInternational() {
1792 addDomainErrorf("DMARC localpart %q is an internationalized address, only conventional ascii-only address possible for interopability", lp)
1794 addrdom := domain.Domain
1795 if dmarc.Domain != "" {
1796 addrdom, err = dns.ParseDomain(dmarc.Domain)
1798 addDomainErrorf("DMARC domain %q: %s", dmarc.Domain, err)
1799 } else if adomain, ok := c.Domains[addrdom.Name()]; !ok {
1800 addDomainErrorf("unknown domain %q for DMARC address", addrdom)
1801 } else if !adomain.LocalpartCaseSensitive {
1802 lp = smtp.Localpart(strings.ToLower(string(lp)))
1804 } else if !domain.LocalpartCaseSensitive {
1805 lp = smtp.Localpart(strings.ToLower(string(lp)))
1807 if addrdom == domain.Domain {
1808 domainHasAddress[addrdom.Name()] = true
1811 domain.DMARC.ParsedLocalpart = lp
1812 domain.DMARC.DNSDomain = addrdom
1813 c.Domains[d] = domain
1814 addrFull := smtp.NewAddress(lp, addrdom).String()
1815 dest := config.Destination{
1816 Mailbox: dmarc.Mailbox,
1819 checkMailboxNormf(dmarc.Mailbox, "DMARC mailbox for account", addDomainErrorf)
1820 accDests[addrFull] = AccountDestination{false, lp, dmarc.Account, dest}
1823 // Set TLSRPT destinations.
1824 for d, domain := range c.Domains {
1825 addDomainErrorf := func(format string, args ...any) {
1826 addErrorf("domain %s: %s", d, fmt.Sprintf(format, args...))
1829 tlsrpt := domain.TLSRPT
1833 if _, ok := c.Accounts[tlsrpt.Account]; !ok {
1834 addDomainErrorf("TLSRPT account %q does not exist", tlsrpt.Account)
1837 // Note: For backwards compabilitiy, TLS reporting localparts can contain catchall separators.
1838 lp, err := smtp.ParseLocalpart(tlsrpt.Localpart)
1840 addDomainErrorf("invalid TLSRPT localpart %q: %s", tlsrpt.Localpart, err)
1842 if lp.IsInternational() {
1843 // Does not appear documented in
../rfc/8460, but similar to DMARC it makes sense
1844 // to keep this ascii-only addresses.
1845 addDomainErrorf("TLSRPT localpart %q is an internationalized address, only conventional ascii-only address allowed for interopability", lp)
1847 addrdom := domain.Domain
1848 if tlsrpt.Domain != "" {
1849 addrdom, err = dns.ParseDomain(tlsrpt.Domain)
1851 addDomainErrorf("TLSRPT domain %q: %s", tlsrpt.Domain, err)
1852 } else if adomain, ok := c.Domains[addrdom.Name()]; !ok {
1853 addDomainErrorf("unknown domain %q for TLSRPT address", tlsrpt.Domain)
1854 } else if !adomain.LocalpartCaseSensitive {
1855 lp = smtp.Localpart(strings.ToLower(string(lp)))
1857 } else if !domain.LocalpartCaseSensitive {
1858 lp = smtp.Localpart(strings.ToLower(string(lp)))
1860 if addrdom == domain.Domain {
1861 domainHasAddress[addrdom.Name()] = true
1864 domain.TLSRPT.ParsedLocalpart = lp
1865 domain.TLSRPT.DNSDomain = addrdom
1866 c.Domains[d] = domain
1867 addrFull := smtp.NewAddress(lp, addrdom).String()
1868 dest := config.Destination{
1869 Mailbox: tlsrpt.Mailbox,
1870 DomainTLSReports: true,
1872 checkMailboxNormf(tlsrpt.Mailbox, "TLSRPT mailbox", addDomainErrorf)
1873 accDests[addrFull] = AccountDestination{false, lp, tlsrpt.Account, dest}
1876 // Set ReportsOnly for domains, based on whether we have seen addresses (possibly
1877 // from DMARC or TLS reporting).
1878 for d, domain := range c.Domains {
1879 domain.ReportsOnly = !domainHasAddress[domain.Domain.Name()]
1880 c.Domains[d] = domain
1883 // Aliases, per domain. Also add references to accounts.
1884 for d, domain := range c.Domains {
1885 for lpstr, a := range domain.Aliases {
1886 addAliasErrorf := func(format string, args ...any) {
1887 addErrorf("domain %s: alias %s: %s", d, lpstr, fmt.Sprintf(format, args...))
1891 a.LocalpartStr = lpstr
1892 var clp smtp.Localpart
1893 lp, err := smtp.ParseLocalpart(lpstr)
1895 addAliasErrorf("parsing alias: %v", err)
1899 for _, sep := range domain.LocalpartCatchallSeparatorsEffective {
1900 if strings.Contains(string(lp), sep) {
1901 addAliasErrorf("alias contains localpart catchall separator")
1908 clp = CanonicalLocalpart(lp, domain)
1911 addr := smtp.NewAddress(clp, domain.Domain).Pack(true)
1912 if _, ok := aliases[addr]; ok {
1913 addAliasErrorf("duplicate alias address %q", addr)
1916 if _, ok := accDests[addr]; ok {
1917 addAliasErrorf("alias %q already present as regular address", addr)
1920 if len(a.Addresses) == 0 {
1921 // Not currently possible, Addresses isn't optional.
1922 addAliasErrorf("alias %q needs at least one destination address", addr)
1925 a.ParsedAddresses = make([]config.AliasAddress, 0, len(a.Addresses))
1926 seen := map[string]bool{}
1927 for _, destAddr := range a.Addresses {
1928 da, err := smtp.ParseAddress(destAddr)
1930 addAliasErrorf("parsing destination address %q: %v", destAddr, err)
1933 dastr := da.Pack(true)
1934 accDest, ok := accDests[dastr]
1936 addAliasErrorf("references non-existent address %q", destAddr)
1940 addAliasErrorf("duplicate address %q", destAddr)
1944 aa := config.AliasAddress{Address: da, AccountName: accDest.Account, Destination: accDest.Destination}
1945 a.ParsedAddresses = append(a.ParsedAddresses, aa)
1947 a.Domain = domain.Domain
1948 c.Domains[d].Aliases[lpstr] = a
1951 for _, aa := range a.ParsedAddresses {
1952 acc := c.Accounts[aa.AccountName]
1955 addrs = make([]string, len(a.ParsedAddresses))
1956 for i := range a.ParsedAddresses {
1957 addrs[i] = a.ParsedAddresses[i].Address.Pack(true)
1960 // Keep the non-sensitive fields.
1961 accAlias := config.Alias{
1962 PostPublic: a.PostPublic,
1963 ListMembers: a.ListMembers,
1964 AllowMsgFrom: a.AllowMsgFrom,
1965 LocalpartStr: a.LocalpartStr,
1968 acc.Aliases = append(acc.Aliases, config.AddressAlias{SubscriptionAddress: aa.Address.Pack(true), Alias: accAlias, MemberAddresses: addrs})
1969 c.Accounts[aa.AccountName] = acc
1974 // Check webserver configs.
1975 if (len(c.WebDomainRedirects) > 0 || len(c.WebHandlers) > 0) && !haveWebserverListener {
1976 addErrorf("WebDomainRedirects or WebHandlers configured but no listener with WebserverHTTP or WebserverHTTPS enabled")
1979 c.WebDNSDomainRedirects = map[dns.Domain]dns.Domain{}
1980 for from, to := range c.WebDomainRedirects {
1981 addRedirectErrorf := func(format string, args ...any) {
1982 addErrorf("web redirect %s to %s: %s", from, to, fmt.Sprintf(format, args...))
1985 fromdom, err := dns.ParseDomain(from)
1987 addRedirectErrorf("parsing domain for redirect %s: %v", from, err)
1989 todom, err := dns.ParseDomain(to)
1991 addRedirectErrorf("parsing domain for redirect %s: %v", to, err)
1992 } else if fromdom == todom {
1993 addRedirectErrorf("will not redirect domain %s to itself", todom)
1995 var zerodom dns.Domain
1996 if _, ok := c.WebDNSDomainRedirects[fromdom]; ok && fromdom != zerodom {
1997 addRedirectErrorf("duplicate redirect domain %s", from)
1999 c.WebDNSDomainRedirects[fromdom] = todom
2002 for i := range c.WebHandlers {
2003 wh := &c.WebHandlers[i]
2005 addHandlerErrorf := func(format string, args ...any) {
2006 addErrorf("webhandler %s %s: %s", wh.Domain, wh.PathRegexp, fmt.Sprintf(format, args...))
2009 if wh.LogName == "" {
2010 wh.Name = fmt.Sprintf("%d", i)
2012 wh.Name = wh.LogName
2015 dom, err := dns.ParseDomain(wh.Domain)
2017 addHandlerErrorf("parsing domain: %v", err)
2021 if !strings.HasPrefix(wh.PathRegexp, "^") {
2022 addHandlerErrorf("path regexp must start with a ^")
2024 re, err := regexp.Compile(wh.PathRegexp)
2026 addHandlerErrorf("compiling regexp: %v", err)
2031 if wh.WebStatic != nil {
2034 if ws.StripPrefix != "" && !strings.HasPrefix(ws.StripPrefix, "/") {
2035 addHandlerErrorf("static: prefix to strip %s must start with a slash", ws.StripPrefix)
2037 for k := range ws.ResponseHeaders {
2039 k := strings.TrimSpace(xk)
2040 if k != xk || k == "" {
2041 addHandlerErrorf("static: bad header %q", xk)
2045 if wh.WebRedirect != nil {
2047 wr := wh.WebRedirect
2048 if wr.BaseURL != "" {
2049 u, err := url.Parse(wr.BaseURL)
2051 addHandlerErrorf("redirect: parsing redirect url %s: %v", wr.BaseURL, err)
2057 addHandlerErrorf("redirect: BaseURL must have empty path", wr.BaseURL)
2061 if wr.OrigPathRegexp != "" && wr.ReplacePath != "" {
2062 re, err := regexp.Compile(wr.OrigPathRegexp)
2064 addHandlerErrorf("compiling regexp %s: %v", wr.OrigPathRegexp, err)
2067 } else if wr.OrigPathRegexp != "" || wr.ReplacePath != "" {
2068 addHandlerErrorf("redirect: must have either both OrigPathRegexp and ReplacePath, or neither")
2069 } else if wr.BaseURL == "" {
2070 addHandlerErrorf("must at least one of BaseURL and OrigPathRegexp+ReplacePath")
2072 if wr.StatusCode != 0 && (wr.StatusCode < 300 || wr.StatusCode >= 400) {
2073 addHandlerErrorf("redirect: invalid redirect status code %d", wr.StatusCode)
2076 if wh.WebForward != nil {
2079 u, err := url.Parse(wf.URL)
2081 addHandlerErrorf("forward: parsing url %s: %v", wf.URL, err)
2085 for k := range wf.ResponseHeaders {
2087 k := strings.TrimSpace(xk)
2088 if k != xk || k == "" {
2089 addHandlerErrorf("forrward: bad header %q", xk)
2093 if wh.WebInternal != nil {
2095 wi := wh.WebInternal
2096 if !strings.HasPrefix(wi.BasePath, "/") || !strings.HasSuffix(wi.BasePath, "/") {
2097 addHandlerErrorf("internal service: base path %q must start and end with /", wi.BasePath)
2099 // todo: we could make maxMsgSize and accountPath configurable
2100 const isForwarded = false
2103 wi.Handler = NewWebadminHandler(wi.BasePath, isForwarded)
2105 wi.Handler = NewWebaccountHandler(wi.BasePath, isForwarded)
2108 wi.Handler = NewWebmailHandler(config.DefaultMaxMsgSize, wi.BasePath, isForwarded, accountPath)
2110 wi.Handler = NewWebapiHandler(config.DefaultMaxMsgSize, wi.BasePath, isForwarded)
2112 addHandlerErrorf("internal service: unknown service %q", wi.Service)
2114 wi.Handler = SafeHeaders(http.StripPrefix(wi.BasePath[:len(wi.BasePath)-1], wi.Handler))
2117 addHandlerErrorf("must have exactly one handler, not %d", n)
2121 c.MonitorDNSBLZones = nil
2122 for _, s := range c.MonitorDNSBLs {
2123 d, err := dns.ParseDomain(s)
2125 addErrorf("dnsbl %s: parsing dnsbl zone: %v", s, err)
2128 if slices.Contains(c.MonitorDNSBLZones, d) {
2129 addErrorf("dnsbl %s: duplicate zone", s)
2132 c.MonitorDNSBLZones = append(c.MonitorDNSBLZones, d)
2138func loadPrivateKeyFile(keyPath string) (crypto.Signer, error) {
2139 keyBuf, err := os.ReadFile(keyPath)
2141 return nil, fmt.Errorf("reading host private key: %v", err)
2143 b, _ := pem.Decode(keyBuf)
2145 return nil, fmt.Errorf("parsing pem block for private key: %v", err)
2150 privKey, err = x509.ParsePKCS8PrivateKey(b.Bytes)
2151 case "RSA PRIVATE KEY":
2152 privKey, err = x509.ParsePKCS1PrivateKey(b.Bytes)
2153 case "EC PRIVATE KEY":
2154 privKey, err = x509.ParseECPrivateKey(b.Bytes)
2156 err = fmt.Errorf("unknown pem type %q", b.Type)
2159 return nil, fmt.Errorf("parsing private key: %v", err)
2161 if k, ok := privKey.(crypto.Signer); ok {
2164 return nil, fmt.Errorf("parsed private key not a crypto.Signer, but %T", privKey)
2167func loadTLSKeyCerts(configFile, kind string, ctls *config.TLS) error {
2168 certs := []tls.Certificate{}
2169 for _, kp := range ctls.KeyCerts {
2170 certPath := configDirPath(configFile, kp.CertFile)
2171 keyPath := configDirPath(configFile, kp.KeyFile)
2172 cert, err := loadX509KeyPairPrivileged(certPath, keyPath)
2174 return fmt.Errorf("tls config for %q: parsing x509 key pair: %v", kind, err)
2176 certs = append(certs, cert)
2178 ctls.Config = &tls.Config{
2179 Certificates: certs,
2181 ctls.ConfigFallback = ctls.Config
2185// load x509 key/cert files from file descriptor possibly passed in by privileged
2187func loadX509KeyPairPrivileged(certPath, keyPath string) (tls.Certificate, error) {
2188 certBuf, err := readFilePrivileged(certPath)
2190 return tls.Certificate{}, fmt.Errorf("reading tls certificate: %v", err)
2192 keyBuf, err := readFilePrivileged(keyPath)
2194 return tls.Certificate{}, fmt.Errorf("reading tls key: %v", err)
2196 return tls.X509KeyPair(certBuf, keyBuf)
2199// like os.ReadFile, but open privileged file possibly passed in by root process.
2200func readFilePrivileged(path string) ([]byte, error) {
2201 f, err := OpenPrivileged(path)
2206 return io.ReadAll(f)