10 cryptorand "crypto/rand"
33 "golang.org/x/text/unicode/norm"
35 "github.com/mjl-/autocert"
37 "github.com/mjl-/sconf"
39 "github.com/mjl-/mox/autotls"
40 "github.com/mjl-/mox/config"
41 "github.com/mjl-/mox/dkim"
42 "github.com/mjl-/mox/dns"
43 "github.com/mjl-/mox/message"
44 "github.com/mjl-/mox/mlog"
45 "github.com/mjl-/mox/moxio"
46 "github.com/mjl-/mox/mtasts"
47 "github.com/mjl-/mox/smtp"
50var pkglog = mlog.New("mox", nil)
52// Pedantic enables stricter parsing.
55// Config paths are set early in program startup. They will point to files in
58 ConfigStaticPath string
59 ConfigDynamicPath string
60 Conf = Config{Log: map[string]slog.Level{"": slog.LevelError}}
63var ErrConfig = errors.New("config error")
65// Set by packages webadmin, webaccount, webmail, webapisrv to prevent cyclic dependencies.
66var NewWebadminHandler = func(basePath string, isForwarded bool) http.Handler { return nopHandler }
67var NewWebaccountHandler = func(basePath string, isForwarded bool) http.Handler { return nopHandler }
68var NewWebmailHandler = func(maxMsgSize int64, basePath string, isForwarded bool, accountPath string) http.Handler {
71var NewWebapiHandler = func(maxMsgSize int64, basePath string, isForwarded bool) http.Handler { return nopHandler }
73var nopHandler = http.HandlerFunc(nil)
75// Config as used in the code, a processed version of what is in the config file.
77// Use methods to lookup a domain/account/address in the dynamic configuration.
79 Static config.Static // Does not change during the lifetime of a running instance.
81 logMutex sync.Mutex // For accessing the log levels.
82 Log map[string]slog.Level
84 dynamicMutex sync.Mutex
85 Dynamic config.Dynamic // Can only be accessed directly by tests. Use methods on Config for locked access.
86 dynamicMtime time.Time
87 DynamicLastCheck time.Time // For use by quickstart only to skip checks.
89 // From canonical full address (localpart@domain, lower-cased when
90 // case-insensitive, stripped of catchall separator) to account and address.
91 // Domains are IDNA names in utf8. Dynamic config lock must be held when accessing.
92 AccountDestinationsLocked map[string]AccountDestination
94 // Like AccountDestinationsLocked, but for aliases.
95 aliases map[string]config.Alias
98type AccountDestination struct {
99 Catchall bool // If catchall destination for its domain.
100 Localpart smtp.Localpart // In original casing as written in config file.
102 Destination config.Destination
105// LogLevelSet sets a new log level for pkg. An empty pkg sets the default log
106// value that is used if no explicit log level is configured for a package.
107// This change is ephemeral, no config file is changed.
108func (c *Config) LogLevelSet(log mlog.Log, pkg string, level slog.Level) {
110 defer c.logMutex.Unlock()
111 l := c.copyLogLevels()
114 log.Print("log level changed", slog.String("pkg", pkg), slog.Any("level", mlog.LevelStrings[level]))
115 mlog.SetConfig(c.Log)
118// LogLevelRemove removes a configured log level for a package.
119func (c *Config) LogLevelRemove(log mlog.Log, pkg string) {
121 defer c.logMutex.Unlock()
122 l := c.copyLogLevels()
125 log.Print("log level cleared", slog.String("pkg", pkg))
126 mlog.SetConfig(c.Log)
129// copyLogLevels returns a copy of c.Log, for modifications.
130// must be called with log lock held.
131func (c *Config) copyLogLevels() map[string]slog.Level {
132 m := map[string]slog.Level{}
133 for pkg, level := range c.Log {
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)
269func (c *Config) Account(name string) (acc config.Account, ok bool) {
270 c.withDynamicLock(func() {
271 acc, ok = c.Dynamic.Accounts[name]
276func (c *Config) AccountDestination(addr string) (accDest AccountDestination, alias *config.Alias, ok bool) {
277 c.withDynamicLock(func() {
278 accDest, ok = c.AccountDestinationsLocked[addr]
281 a, ok = c.aliases[addr]
290func (c *Config) Routes(accountName string, domain dns.Domain) (accountRoutes, domainRoutes, globalRoutes []config.Route) {
291 c.withDynamicLock(func() {
292 acc := c.Dynamic.Accounts[accountName]
293 accountRoutes = acc.Routes
295 dom := c.Dynamic.Domains[domain.Name()]
296 domainRoutes = dom.Routes
298 globalRoutes = c.Dynamic.Routes
303func (c *Config) IsClientSettingsDomain(d dns.Domain) (is bool) {
304 c.withDynamicLock(func() {
305 _, is = c.Dynamic.ClientSettingDomains[d]
310func (c *Config) allowACMEHosts(log mlog.Log, checkACMEHosts bool) {
311 for _, l := range c.Static.Listeners {
312 if l.TLS == nil || l.TLS.ACME == "" {
316 m := c.Static.ACME[l.TLS.ACME].Manager
317 hostnames := map[dns.Domain]struct{}{}
319 hostnames[c.Static.HostnameDomain] = struct{}{}
320 if l.HostnameDomain.ASCII != "" {
321 hostnames[l.HostnameDomain] = struct{}{}
324 for _, dom := range c.Dynamic.Domains {
325 // Do not allow TLS certificates for domains for which we only accept DMARC/TLS
326 // reports as external party.
331 // Do not fetch TLS certs for disabled domains. The A/AAAA records may not be
332 // configured or still point to a previous machine before a migration.
337 if l.AutoconfigHTTPS.Enabled && !l.AutoconfigHTTPS.NonTLS {
338 if d, err := dns.ParseDomain("autoconfig." + dom.Domain.ASCII); err != nil {
339 log.Errorx("parsing autoconfig domain", err, slog.Any("domain", dom.Domain))
341 hostnames[d] = struct{}{}
345 if l.MTASTSHTTPS.Enabled && dom.MTASTS != nil && !l.MTASTSHTTPS.NonTLS {
346 d, err := dns.ParseDomain("mta-sts." + dom.Domain.ASCII)
348 log.Errorx("parsing mta-sts domain", err, slog.Any("domain", dom.Domain))
350 hostnames[d] = struct{}{}
354 if dom.ClientSettingsDomain != "" {
355 hostnames[dom.ClientSettingsDNSDomain] = struct{}{}
359 if l.WebserverHTTPS.Enabled {
360 for from := range c.Dynamic.WebDNSDomainRedirects {
361 hostnames[from] = struct{}{}
363 for _, wh := range c.Dynamic.WebHandlers {
364 hostnames[wh.DNSDomain] = struct{}{}
368 public := c.Static.Listeners["public"]
370 if len(public.NATIPs) > 0 {
376 m.SetAllowedHostnames(log, dns.StrictResolver{Pkg: "autotls", Log: log.Logger}, hostnames, ips, checkACMEHosts)
380// 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.
382// WriteDynamicLocked prepares an updated internal state for the new dynamic
383// config, then writes it to disk and activates it.
385// Returns ErrConfig if the configuration is not valid.
387// Must be called with config lock held.
388func WriteDynamicLocked(ctx context.Context, log mlog.Log, c config.Dynamic) error {
389 accDests, aliases, errs := prepareDynamicConfig(ctx, log, ConfigDynamicPath, Conf.Static, &c)
391 errstrs := make([]string, len(errs))
392 for i, err := range errs {
393 errstrs[i] = err.Error()
395 return fmt.Errorf("%w: %s", ErrConfig, strings.Join(errstrs, "; "))
399 err := sconf.Write(&b, c)
403 f, err := os.OpenFile(ConfigDynamicPath, os.O_WRONLY, 0660)
410 log.Check(err, "closing file after error")
414 if _, err := f.Write(buf); err != nil {
415 return fmt.Errorf("write domains.conf: %v", err)
417 if err := f.Truncate(int64(len(buf))); err != nil {
418 return fmt.Errorf("truncate domains.conf after write: %v", err)
420 if err := f.Sync(); err != nil {
421 return fmt.Errorf("sync domains.conf after write: %v", err)
423 if err := moxio.SyncDir(log, filepath.Dir(ConfigDynamicPath)); err != nil {
424 return fmt.Errorf("sync dir of domains.conf after write: %v", err)
429 return fmt.Errorf("stat after writing domains.conf: %v", err)
432 if err := f.Close(); err != nil {
433 return fmt.Errorf("close written domains.conf: %v", err)
437 Conf.dynamicMtime = fi.ModTime()
438 Conf.DynamicLastCheck = time.Now()
440 Conf.AccountDestinationsLocked = accDests
441 Conf.aliases = aliases
443 Conf.allowACMEHosts(log, true)
448// MustLoadConfig loads the config, quitting on errors.
449func MustLoadConfig(doLoadTLSKeyCerts, checkACMEHosts bool) {
450 errs := LoadConfig(context.Background(), pkglog, doLoadTLSKeyCerts, checkACMEHosts)
452 pkglog.Error("loading config file: multiple errors")
453 for _, err := range errs {
454 pkglog.Errorx("config error", err)
456 pkglog.Fatal("stopping after multiple config errors")
457 } else if len(errs) == 1 {
458 pkglog.Fatalx("loading config file", errs[0])
462// LoadConfig attempts to parse and load a config, returning any errors
464func LoadConfig(ctx context.Context, log mlog.Log, doLoadTLSKeyCerts, checkACMEHosts bool) []error {
465 Shutdown, ShutdownCancel = context.WithCancel(context.Background())
466 Context, ContextCancel = context.WithCancel(context.Background())
468 c, errs := ParseConfig(ctx, log, ConfigStaticPath, false, doLoadTLSKeyCerts, checkACMEHosts)
473 mlog.SetConfig(c.Log)
478// SetConfig sets a new config. Not to be used during normal operation.
479func SetConfig(c *Config) {
480 // Cannot just assign *c to Conf, it would copy the mutex.
481 Conf = Config{c.Static, sync.Mutex{}, c.Log, sync.Mutex{}, c.Dynamic, c.dynamicMtime, c.DynamicLastCheck, c.AccountDestinationsLocked, c.aliases}
483 // If we have non-standard CA roots, use them for all HTTPS requests.
484 if Conf.Static.TLS.CertPool != nil {
485 http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{
486 RootCAs: Conf.Static.TLS.CertPool,
490 SetPedantic(c.Static.Pedantic)
493// Set pedantic in all packages.
494func SetPedantic(p bool) {
502// ParseConfig parses the static config at path p. If checkOnly is true, no changes
503// are made, such as registering ACME identities. If doLoadTLSKeyCerts is true,
504// the TLS KeyCerts configuration is loaded and checked. This is used during the
505// quickstart in the case the user is going to provide their own certificates.
506// If checkACMEHosts is true, the hosts allowed for acme are compared with the
507// explicitly configured ips we are listening on.
508func ParseConfig(ctx context.Context, log mlog.Log, p string, checkOnly, doLoadTLSKeyCerts, checkACMEHosts bool) (c *Config, errs []error) {
510 Static: config.Static{
517 if os.IsNotExist(err) && os.Getenv("MOXCONF") == "" {
518 return nil, []error{fmt.Errorf("open config file: %v (hint: use mox -config ... or set MOXCONF=...)", err)}
520 return nil, []error{fmt.Errorf("open config file: %v", err)}
523 if err := sconf.Parse(f, &c.Static); err != nil {
524 return nil, []error{fmt.Errorf("parsing %s%v", p, err)}
527 if xerrs := PrepareStaticConfig(ctx, log, p, c, checkOnly, doLoadTLSKeyCerts); len(xerrs) > 0 {
531 pp := filepath.Join(filepath.Dir(p), "domains.conf")
532 c.Dynamic, c.dynamicMtime, c.AccountDestinationsLocked, c.aliases, errs = ParseDynamicConfig(ctx, log, pp, c.Static)
535 c.allowACMEHosts(log, checkACMEHosts)
541// PrepareStaticConfig parses the static config file and prepares data structures
542// for starting mox. If checkOnly is set no substantial changes are made, like
543// creating an ACME registration.
544func PrepareStaticConfig(ctx context.Context, log mlog.Log, configFile string, conf *Config, checkOnly, doLoadTLSKeyCerts bool) (errs []error) {
545 addErrorf := func(format string, args ...any) {
546 errs = append(errs, fmt.Errorf(format, args...))
551 // check that mailbox is in unicode NFC normalized form.
552 checkMailboxNormf := func(mailbox string, format string, args ...any) {
553 s := norm.NFC.String(mailbox)
555 msg := fmt.Sprintf(format, args...)
556 addErrorf("%s: mailbox %q is not in NFC normalized form, should be %q", msg, mailbox, s)
560 // Post-process logging config.
561 if logLevel, ok := mlog.Levels[c.LogLevel]; ok {
562 conf.Log = map[string]slog.Level{"": logLevel}
564 addErrorf("invalid log level %q", c.LogLevel)
566 for pkg, s := range c.PackageLogLevels {
567 if logLevel, ok := mlog.Levels[s]; ok {
568 conf.Log[pkg] = logLevel
570 addErrorf("invalid package log level %q", s)
577 u, err := user.Lookup(c.User)
579 uid, err := strconv.ParseUint(c.User, 10, 32)
581 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)
583 // We assume the same gid as uid.
588 if uid, err := strconv.ParseUint(u.Uid, 10, 32); err != nil {
589 addErrorf("parsing uid %s: %v", u.Uid, err)
593 if gid, err := strconv.ParseUint(u.Gid, 10, 32); err != nil {
594 addErrorf("parsing gid %s: %v", u.Gid, err)
600 hostname, err := dns.ParseDomain(c.Hostname)
602 addErrorf("parsing hostname: %s", err)
603 } else if hostname.Name() != c.Hostname {
604 addErrorf("hostname must be in unicode form %q instead of %q", hostname.Name(), c.Hostname)
606 c.HostnameDomain = hostname
608 if c.HostTLSRPT.Account != "" {
609 tlsrptLocalpart, err := smtp.ParseLocalpart(c.HostTLSRPT.Localpart)
611 addErrorf("invalid localpart %q for host tlsrpt: %v", c.HostTLSRPT.Localpart, err)
612 } else if tlsrptLocalpart.IsInternational() {
613 // Does not appear documented in
../rfc/8460, but similar to DMARC it makes sense
614 // to keep this ascii-only addresses.
615 addErrorf("host TLSRPT localpart %q is an internationalized address, only conventional ascii-only address allowed for interopability", tlsrptLocalpart)
617 c.HostTLSRPT.ParsedLocalpart = tlsrptLocalpart
620 // Return private key for host name for use with an ACME. Used to return the same
621 // private key as pre-generated for use with DANE, with its public key in DNS.
622 // We only use this key for Listener's that have this ACME configured, and for
623 // which the effective listener host name (either specific to the listener, or the
624 // global name) is requested. Other host names can get a fresh private key, they
625 // don't appear in DANE records.
627 // - run 0: only use listener with explicitly matching host name in listener
628 // (default quickstart config does not set it).
629 // - run 1: only look at public listener (and host matching mox host name)
630 // - run 2: all listeners (and host matching mox host name)
631 findACMEHostPrivateKey := func(acmeName, host string, keyType autocert.KeyType, run int) crypto.Signer {
632 for listenerName, l := range Conf.Static.Listeners {
633 if l.TLS == nil || l.TLS.ACME != acmeName {
636 if run == 0 && host != l.HostnameDomain.ASCII {
639 if run == 1 && listenerName != "public" || host != Conf.Static.HostnameDomain.ASCII {
643 case autocert.KeyRSA2048:
644 if len(l.TLS.HostPrivateRSA2048Keys) == 0 {
647 return l.TLS.HostPrivateRSA2048Keys[0]
648 case autocert.KeyECDSAP256:
649 if len(l.TLS.HostPrivateECDSAP256Keys) == 0 {
652 return l.TLS.HostPrivateECDSAP256Keys[0]
659 // Make a function for an autocert.Manager.GetPrivateKey, using findACMEHostPrivateKey.
660 makeGetPrivateKey := func(acmeName string) func(host string, keyType autocert.KeyType) (crypto.Signer, error) {
661 return func(host string, keyType autocert.KeyType) (crypto.Signer, error) {
662 key := findACMEHostPrivateKey(acmeName, host, keyType, 0)
664 key = findACMEHostPrivateKey(acmeName, host, keyType, 1)
667 key = findACMEHostPrivateKey(acmeName, host, keyType, 2)
670 log.Debug("found existing private key for certificate for host",
671 slog.String("acmename", acmeName),
672 slog.String("host", host),
673 slog.Any("keytype", keyType))
676 log.Debug("generating new private key for certificate for host",
677 slog.String("acmename", acmeName),
678 slog.String("host", host),
679 slog.Any("keytype", keyType))
681 case autocert.KeyRSA2048:
682 return rsa.GenerateKey(cryptorand.Reader, 2048)
683 case autocert.KeyECDSAP256:
684 return ecdsa.GenerateKey(elliptic.P256(), cryptorand.Reader)
686 return nil, fmt.Errorf("unrecognized requested key type %v", keyType)
690 for name, acme := range c.ACME {
691 addAcmeErrorf := func(format string, args ...any) {
692 addErrorf("acme provider %s: %s", name, fmt.Sprintf(format, args...))
697 if acme.ExternalAccountBinding != nil {
698 eabKeyID = acme.ExternalAccountBinding.KeyID
699 p := configDirPath(configFile, acme.ExternalAccountBinding.KeyFile)
700 buf, err := os.ReadFile(p)
702 addAcmeErrorf("reading external account binding key: %s", err)
704 dec := make([]byte, base64.RawURLEncoding.DecodedLen(len(buf)))
705 n, err := base64.RawURLEncoding.Decode(dec, buf)
707 addAcmeErrorf("parsing external account binding key as base64: %s", err)
718 acmeDir := dataDirPath(configFile, c.DataDir, "acme")
719 os.MkdirAll(acmeDir, 0770)
720 manager, err := autotls.Load(log, name, acmeDir, acme.ContactEmail, acme.DirectoryURL, eabKeyID, eabKey, makeGetPrivateKey(name), Shutdown.Done())
722 addAcmeErrorf("loading ACME identity: %s", err)
724 acme.Manager = manager
726 // Help configurations from older quickstarts.
727 if acme.IssuerDomainName == "" && acme.DirectoryURL == "https://acme-v02.api.letsencrypt.org/directory" {
728 acme.IssuerDomainName = "letsencrypt.org"
734 var haveUnspecifiedSMTPListener bool
735 for name, l := range c.Listeners {
736 addListenerErrorf := func(format string, args ...any) {
737 addErrorf("listener %s: %s", name, fmt.Sprintf(format, args...))
740 if l.Hostname != "" {
741 d, err := dns.ParseDomain(l.Hostname)
743 addListenerErrorf("parsing hostname %q: %s", l.Hostname, err)
748 if l.TLS.ACME != "" && len(l.TLS.KeyCerts) != 0 {
749 addListenerErrorf("cannot have ACME and static key/certificates")
750 } else if l.TLS.ACME != "" {
751 acme, ok := c.ACME[l.TLS.ACME]
753 addListenerErrorf("unknown ACME provider %q", l.TLS.ACME)
756 // If only checking or with missing ACME definition, we don't have an acme manager,
757 // so set an empty tls config to continue.
758 var tlsconfig, tlsconfigFallback *tls.Config
759 if checkOnly || acme.Manager == nil {
760 tlsconfig = &tls.Config{}
761 tlsconfigFallback = &tls.Config{}
763 hostname := c.HostnameDomain
764 if l.Hostname != "" {
765 hostname = l.HostnameDomain
767 // If SNI is absent, we will use the listener hostname, but reject connections with
768 // an SNI hostname that is not allowlisted.
769 // Incoming SMTP deliveries use tlsconfigFallback for interoperability. TLS
770 // connections for unknown SNI hostnames fall back to a certificate for the
771 // listener hostname instead of causing the TLS connection to fail.
772 tlsconfig = acme.Manager.TLSConfig(hostname, true, false)
773 tlsconfigFallback = acme.Manager.TLSConfig(hostname, true, true)
774 l.TLS.ACMEConfig = acme.Manager.ACMETLSConfig
776 l.TLS.Config = tlsconfig
777 l.TLS.ConfigFallback = tlsconfigFallback
778 } else if len(l.TLS.KeyCerts) != 0 {
779 if doLoadTLSKeyCerts {
780 if err := loadTLSKeyCerts(configFile, "listener "+name, l.TLS); err != nil {
781 addListenerErrorf("%w", err)
785 addListenerErrorf("cannot have TLS config without ACME and without static keys/certificates")
787 for _, privKeyFile := range l.TLS.HostPrivateKeyFiles {
788 keyPath := configDirPath(configFile, privKeyFile)
789 privKey, err := loadPrivateKeyFile(keyPath)
791 addListenerErrorf("parsing host private key for DANE and ACME certificates: %v", err)
794 switch k := privKey.(type) {
795 case *rsa.PrivateKey:
796 if k.N.BitLen() != 2048 {
797 log.Error("need rsa key with 2048 bits, for host private key for DANE/ACME certificates, ignoring",
798 slog.String("listener", name),
799 slog.String("file", keyPath),
800 slog.Int("bits", k.N.BitLen()))
803 l.TLS.HostPrivateRSA2048Keys = append(l.TLS.HostPrivateRSA2048Keys, k)
804 case *ecdsa.PrivateKey:
805 if k.Curve != elliptic.P256() {
806 log.Error("unrecognized ecdsa curve for host private key for DANE/ACME certificates, ignoring", slog.String("listener", name), slog.String("file", keyPath))
809 l.TLS.HostPrivateECDSAP256Keys = append(l.TLS.HostPrivateECDSAP256Keys, k)
811 log.Error("unrecognized key type for host private key for DANE/ACME certificates, ignoring",
812 slog.String("listener", name),
813 slog.String("file", keyPath),
814 slog.String("keytype", fmt.Sprintf("%T", privKey)))
818 if l.TLS.ACME != "" && (len(l.TLS.HostPrivateRSA2048Keys) == 0) != (len(l.TLS.HostPrivateECDSAP256Keys) == 0) {
819 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")
823 var minVersion uint16 = tls.VersionTLS12
824 if l.TLS.MinVersion != "" {
825 versions := map[string]uint16{
826 "TLSv1.0": tls.VersionTLS10,
827 "TLSv1.1": tls.VersionTLS11,
828 "TLSv1.2": tls.VersionTLS12,
829 "TLSv1.3": tls.VersionTLS13,
831 v, ok := versions[l.TLS.MinVersion]
833 addListenerErrorf("unknown TLS mininum version %q", l.TLS.MinVersion)
837 if l.TLS.Config != nil {
838 l.TLS.Config.MinVersion = minVersion
840 if l.TLS.ConfigFallback != nil {
841 l.TLS.ConfigFallback.MinVersion = minVersion
843 if l.TLS.ACMEConfig != nil {
844 l.TLS.ACMEConfig.MinVersion = minVersion
847 var needsTLS []string
848 needtls := func(s string, v bool) {
850 needsTLS = append(needsTLS, s)
853 needtls("IMAPS", l.IMAPS.Enabled)
854 needtls("SMTP", l.SMTP.Enabled && !l.SMTP.NoSTARTTLS)
855 needtls("Submissions", l.Submissions.Enabled)
856 needtls("Submission", l.Submission.Enabled && !l.Submission.NoRequireSTARTTLS)
857 needtls("AccountHTTPS", l.AccountHTTPS.Enabled)
858 needtls("AdminHTTPS", l.AdminHTTPS.Enabled)
859 needtls("AutoconfigHTTPS", l.AutoconfigHTTPS.Enabled && !l.AutoconfigHTTPS.NonTLS)
860 needtls("MTASTSHTTPS", l.MTASTSHTTPS.Enabled && !l.MTASTSHTTPS.NonTLS)
861 needtls("WebserverHTTPS", l.WebserverHTTPS.Enabled)
862 if len(needsTLS) > 0 {
863 addListenerErrorf("no tls config specified, but requires tls for %s", strings.Join(needsTLS, ", "))
866 if l.AutoconfigHTTPS.Enabled && l.MTASTSHTTPS.Enabled && l.AutoconfigHTTPS.Port == l.MTASTSHTTPS.Port && l.AutoconfigHTTPS.NonTLS != l.MTASTSHTTPS.NonTLS {
867 addListenerErrorf("autoconfig and mta-sts enabled on same port but with both http and https")
871 haveUnspecifiedSMTPListener = true
873 for _, ipstr := range l.IPs {
874 ip := net.ParseIP(ipstr)
876 addListenerErrorf("invalid IP %q", ipstr)
879 if ip.IsUnspecified() {
880 haveUnspecifiedSMTPListener = true
883 if len(c.SpecifiedSMTPListenIPs) >= 2 {
884 haveUnspecifiedSMTPListener = true
885 } else if len(c.SpecifiedSMTPListenIPs) > 0 && (c.SpecifiedSMTPListenIPs[0].To4() == nil) == (ip.To4() == nil) {
886 haveUnspecifiedSMTPListener = true
888 c.SpecifiedSMTPListenIPs = append(c.SpecifiedSMTPListenIPs, ip)
892 for _, s := range l.SMTP.DNSBLs {
893 d, err := dns.ParseDomain(s)
895 addListenerErrorf("parsing DNSBL zone %q: %s", s, err)
898 l.SMTP.DNSBLZones = append(l.SMTP.DNSBLZones, d)
900 if l.IPsNATed && len(l.NATIPs) > 0 {
901 addListenerErrorf("both IPsNATed and NATIPs configued (remove deprecated IPsNATed)")
903 for _, ipstr := range l.NATIPs {
904 ip := net.ParseIP(ipstr)
906 addListenerErrorf("invalid ip %q", ipstr)
907 } else if ip.IsUnspecified() || ip.IsLoopback() {
908 addListenerErrorf("NAT ip that is the unspecified or loopback address %s", ipstr)
911 cleanPath := func(kind string, enabled bool, path string) string {
915 if path != "" && !strings.HasPrefix(path, "/") {
916 addListenerErrorf("%s with path %q that must start with a slash", kind, path)
917 } else if path != "" && !strings.HasSuffix(path, "/") {
918 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+"/"))
923 l.AccountHTTP.Path = cleanPath("AccountHTTP", l.AccountHTTP.Enabled, l.AccountHTTP.Path)
924 l.AccountHTTPS.Path = cleanPath("AccountHTTPS", l.AccountHTTPS.Enabled, l.AccountHTTPS.Path)
925 l.AdminHTTP.Path = cleanPath("AdminHTTP", l.AdminHTTP.Enabled, l.AdminHTTP.Path)
926 l.AdminHTTPS.Path = cleanPath("AdminHTTPS", l.AdminHTTPS.Enabled, l.AdminHTTPS.Path)
927 l.WebmailHTTP.Path = cleanPath("WebmailHTTP", l.WebmailHTTP.Enabled, l.WebmailHTTP.Path)
928 l.WebmailHTTPS.Path = cleanPath("WebmailHTTPS", l.WebmailHTTPS.Enabled, l.WebmailHTTPS.Path)
929 l.WebAPIHTTP.Path = cleanPath("WebAPIHTTP", l.WebAPIHTTP.Enabled, l.WebAPIHTTP.Path)
930 l.WebAPIHTTPS.Path = cleanPath("WebAPIHTTPS", l.WebAPIHTTPS.Enabled, l.WebAPIHTTPS.Path)
931 c.Listeners[name] = l
933 if haveUnspecifiedSMTPListener {
934 c.SpecifiedSMTPListenIPs = nil
937 var zerouse config.SpecialUseMailboxes
938 if len(c.DefaultMailboxes) > 0 && (c.InitialMailboxes.SpecialUse != zerouse || len(c.InitialMailboxes.Regular) > 0) {
939 addErrorf("cannot have both DefaultMailboxes and InitialMailboxes")
941 // DefaultMailboxes is deprecated.
942 for _, mb := range c.DefaultMailboxes {
943 checkMailboxNormf(mb, "default mailbox")
944 // We don't create parent mailboxes for default mailboxes.
945 if ParentMailboxName(mb) != "" {
946 addErrorf("default mailbox cannot be a child mailbox")
949 checkSpecialUseMailbox := func(nameOpt string) {
951 checkMailboxNormf(nameOpt, "special-use initial mailbox")
952 if strings.EqualFold(nameOpt, "inbox") {
953 addErrorf("initial mailbox cannot be set to Inbox (Inbox is always created)")
955 // We don't currently create parent mailboxes for initial mailboxes.
956 if ParentMailboxName(nameOpt) != "" {
957 addErrorf("initial mailboxes cannot be child mailboxes")
961 checkSpecialUseMailbox(c.InitialMailboxes.SpecialUse.Archive)
962 checkSpecialUseMailbox(c.InitialMailboxes.SpecialUse.Draft)
963 checkSpecialUseMailbox(c.InitialMailboxes.SpecialUse.Junk)
964 checkSpecialUseMailbox(c.InitialMailboxes.SpecialUse.Sent)
965 checkSpecialUseMailbox(c.InitialMailboxes.SpecialUse.Trash)
966 for _, name := range c.InitialMailboxes.Regular {
967 checkMailboxNormf(name, "regular initial mailbox")
968 if strings.EqualFold(name, "inbox") {
969 addErrorf("initial regular mailbox cannot be set to Inbox (Inbox is always created)")
971 if ParentMailboxName(name) != "" {
972 addErrorf("initial mailboxes cannot be child mailboxes")
976 checkTransportSMTP := func(name string, isTLS bool, t *config.TransportSMTP) {
977 addTransportErrorf := func(format string, args ...any) {
978 addErrorf("transport %s: %s", name, fmt.Sprintf(format, args...))
982 t.DNSHost, err = dns.ParseDomain(t.Host)
984 addTransportErrorf("bad host %s: %v", t.Host, err)
987 if isTLS && t.STARTTLSInsecureSkipVerify {
988 addTransportErrorf("cannot have STARTTLSInsecureSkipVerify with immediate TLS")
990 if isTLS && t.NoSTARTTLS {
991 addTransportErrorf("cannot have NoSTARTTLS with immediate TLS")
997 seen := map[string]bool{}
998 for _, m := range t.Auth.Mechanisms {
1000 addTransportErrorf("duplicate authentication mechanism %s", m)
1004 case "SCRAM-SHA-256-PLUS":
1005 case "SCRAM-SHA-256":
1006 case "SCRAM-SHA-1-PLUS":
1011 addTransportErrorf("unknown authentication mechanism %s", m)
1015 t.Auth.EffectiveMechanisms = t.Auth.Mechanisms
1016 if len(t.Auth.EffectiveMechanisms) == 0 {
1017 t.Auth.EffectiveMechanisms = []string{"SCRAM-SHA-256-PLUS", "SCRAM-SHA-256", "SCRAM-SHA-1-PLUS", "SCRAM-SHA-1", "CRAM-MD5"}
1021 checkTransportSocks := func(name string, t *config.TransportSocks) {
1022 addTransportErrorf := func(format string, args ...any) {
1023 addErrorf("transport %s: %s", name, fmt.Sprintf(format, args...))
1026 _, _, err := net.SplitHostPort(t.Address)
1028 addTransportErrorf("bad address %s: %v", t.Address, err)
1030 for _, ipstr := range t.RemoteIPs {
1031 ip := net.ParseIP(ipstr)
1033 addTransportErrorf("bad ip %s", ipstr)
1035 t.IPs = append(t.IPs, ip)
1038 t.Hostname, err = dns.ParseDomain(t.RemoteHostname)
1040 addTransportErrorf("bad hostname %s: %v", t.RemoteHostname, err)
1044 checkTransportDirect := func(name string, t *config.TransportDirect) {
1045 addTransportErrorf := func(format string, args ...any) {
1046 addErrorf("transport %s: %s", name, fmt.Sprintf(format, args...))
1049 if t.DisableIPv4 && t.DisableIPv6 {
1050 addTransportErrorf("both IPv4 and IPv6 are disabled, enable at least one")
1061 checkTransportFail := func(name string, t *config.TransportFail) {
1062 addTransportErrorf := func(format string, args ...any) {
1063 addErrorf("transport %s: %s", name, fmt.Sprintf(format, args...))
1066 if t.SMTPCode == 0 {
1067 t.Code = smtp.C554TransactionFailed
1068 } else if t.SMTPCode/100 != 4 && t.SMTPCode/100 != 5 {
1069 addTransportErrorf("smtp code %d must be 4xx or 5xx", t.SMTPCode/100)
1074 if len(t.SMTPMessage) > 256 {
1075 addTransportErrorf("message must be <= 256 characters")
1077 for _, c := range t.SMTPMessage {
1078 if c < ' ' || c >= 0x7f {
1079 addTransportErrorf("message cannot contain control characters including newlines, and must be ascii-only")
1082 t.Message = t.SMTPMessage
1083 if t.Message == "" {
1084 t.Message = "transport fail: explicit immediate delivery failure per configuration"
1088 for name, t := range c.Transports {
1089 addTransportErrorf := func(format string, args ...any) {
1090 addErrorf("transport %s: %s", name, fmt.Sprintf(format, args...))
1094 if t.Submissions != nil {
1096 checkTransportSMTP(name, true, t.Submissions)
1098 if t.Submission != nil {
1100 checkTransportSMTP(name, false, t.Submission)
1104 checkTransportSMTP(name, false, t.SMTP)
1108 checkTransportSocks(name, t.Socks)
1110 if t.Direct != nil {
1112 checkTransportDirect(name, t.Direct)
1116 checkTransportFail(name, t.Fail)
1119 addTransportErrorf("cannot have multiple methods in a transport")
1123 // Load CA certificate pool.
1124 if c.TLS.CA != nil {
1125 if c.TLS.CA.AdditionalToSystem {
1127 c.TLS.CertPool, err = x509.SystemCertPool()
1129 addErrorf("fetching system CA cert pool: %v", err)
1132 c.TLS.CertPool = x509.NewCertPool()
1134 for _, certfile := range c.TLS.CA.CertFiles {
1135 p := configDirPath(configFile, certfile)
1136 pemBuf, err := os.ReadFile(p)
1138 addErrorf("reading TLS CA cert file: %v", err)
1140 } else if !c.TLS.CertPool.AppendCertsFromPEM(pemBuf) {
1141 // todo: can we check more fully if we're getting some useful data back?
1142 addErrorf("no CA certs added from %q", p)
1149// PrepareDynamicConfig parses the dynamic config file given a static file.
1150func 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) {
1151 addErrorf := func(format string, args ...any) {
1152 errs = append(errs, fmt.Errorf(format, args...))
1155 f, err := os.Open(dynamicPath)
1157 addErrorf("parsing domains config: %v", err)
1163 addErrorf("stat domains config: %v", err)
1165 if err := sconf.Parse(f, &c); err != nil {
1166 addErrorf("parsing dynamic config file: %v", err)
1170 accDests, aliases, errs = prepareDynamicConfig(ctx, log, dynamicPath, static, &c)
1171 return c, fi.ModTime(), accDests, aliases, errs
1174func 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) {
1175 addErrorf := func(format string, args ...any) {
1176 errs = append(errs, fmt.Errorf(format, args...))
1179 // Check that mailbox is in unicode NFC normalized form.
1180 checkMailboxNormf := func(mailbox string, what string, errorf func(format string, args ...any)) {
1181 s := norm.NFC.String(mailbox)
1183 errorf("%s: mailbox %q is not in NFC normalized form, should be %q", what, mailbox, s)
1187 // Validate postmaster account exists.
1188 if _, ok := c.Accounts[static.Postmaster.Account]; !ok {
1189 addErrorf("postmaster account %q does not exist", static.Postmaster.Account)
1191 checkMailboxNormf(static.Postmaster.Mailbox, "postmaster mailbox", addErrorf)
1193 accDests = map[string]AccountDestination{}
1194 aliases = map[string]config.Alias{}
1196 // Validate host TLSRPT account/address.
1197 if static.HostTLSRPT.Account != "" {
1198 if _, ok := c.Accounts[static.HostTLSRPT.Account]; !ok {
1199 addErrorf("host tlsrpt account %q does not exist", static.HostTLSRPT.Account)
1201 checkMailboxNormf(static.HostTLSRPT.Mailbox, "host tlsrpt mailbox", addErrorf)
1203 // Localpart has been parsed already.
1205 addrFull := smtp.NewAddress(static.HostTLSRPT.ParsedLocalpart, static.HostnameDomain).String()
1206 dest := config.Destination{
1207 Mailbox: static.HostTLSRPT.Mailbox,
1208 HostTLSReports: true,
1210 accDests[addrFull] = AccountDestination{false, static.HostTLSRPT.ParsedLocalpart, static.HostTLSRPT.Account, dest}
1213 var haveSTSListener, haveWebserverListener bool
1214 for _, l := range static.Listeners {
1215 if l.MTASTSHTTPS.Enabled {
1216 haveSTSListener = true
1218 if l.WebserverHTTP.Enabled || l.WebserverHTTPS.Enabled {
1219 haveWebserverListener = true
1223 checkRoutes := func(descr string, routes []config.Route) {
1224 parseRouteDomains := func(l []string) []string {
1226 for _, e := range l {
1232 if strings.HasPrefix(e, ".") {
1236 d, err := dns.ParseDomain(e)
1238 addErrorf("%s: invalid domain %s: %v", descr, e, err)
1240 r = append(r, prefix+d.ASCII)
1245 for i := range routes {
1246 routes[i].FromDomainASCII = parseRouteDomains(routes[i].FromDomain)
1247 routes[i].ToDomainASCII = parseRouteDomains(routes[i].ToDomain)
1249 routes[i].ResolvedTransport, ok = static.Transports[routes[i].Transport]
1251 addErrorf("%s: route references undefined transport %s", descr, routes[i].Transport)
1256 checkRoutes("global routes", c.Routes)
1258 // Validate domains.
1259 c.ClientSettingDomains = map[dns.Domain]struct{}{}
1260 for d, domain := range c.Domains {
1261 addDomainErrorf := func(format string, args ...any) {
1262 addErrorf(fmt.Sprintf("domain %v: %s", d, fmt.Sprintf(format, args...)))
1265 dnsdomain, err := dns.ParseDomain(d)
1267 addDomainErrorf("parsing domain: %s", err)
1268 } else if dnsdomain.Name() != d {
1269 addDomainErrorf("must be specified in unicode form, %s", dnsdomain.Name())
1272 domain.Domain = dnsdomain
1274 if domain.ClientSettingsDomain != "" {
1275 csd, err := dns.ParseDomain(domain.ClientSettingsDomain)
1277 addDomainErrorf("bad client settings domain %q: %s", domain.ClientSettingsDomain, err)
1279 domain.ClientSettingsDNSDomain = csd
1280 c.ClientSettingDomains[csd] = struct{}{}
1283 if domain.LocalpartCatchallSeparator != "" && len(domain.LocalpartCatchallSeparators) != 0 {
1284 addDomainErrorf("cannot have both LocalpartCatchallSeparator and LocalpartCatchallSeparators")
1286 domain.LocalpartCatchallSeparatorsEffective = domain.LocalpartCatchallSeparators
1287 if domain.LocalpartCatchallSeparator != "" {
1288 domain.LocalpartCatchallSeparatorsEffective = append(domain.LocalpartCatchallSeparatorsEffective, domain.LocalpartCatchallSeparator)
1290 sepSeen := map[string]bool{}
1291 for _, sep := range domain.LocalpartCatchallSeparatorsEffective {
1293 addDomainErrorf("duplicate localpart catchall separator %q", sep)
1298 for _, sign := range domain.DKIM.Sign {
1299 if _, ok := domain.DKIM.Selectors[sign]; !ok {
1300 addDomainErrorf("unknown selector %s for signing", sign)
1303 for name, sel := range domain.DKIM.Selectors {
1304 addSelectorErrorf := func(format string, args ...any) {
1305 addDomainErrorf("selector %s: %s", name, fmt.Sprintf(format, args...))
1308 seld, err := dns.ParseDomain(name)
1310 addSelectorErrorf("parsing selector: %s", err)
1311 } else if seld.Name() != name {
1312 addSelectorErrorf("must be specified in unicode form, %q", seld.Name())
1316 if sel.Expiration != "" {
1317 exp, err := time.ParseDuration(sel.Expiration)
1319 addSelectorErrorf("invalid expiration %q: %v", sel.Expiration, err)
1321 sel.ExpirationSeconds = int(exp / time.Second)
1325 sel.HashEffective = sel.Hash
1326 switch sel.HashEffective {
1328 sel.HashEffective = "sha256"
1330 log.Error("using sha1 with DKIM is deprecated as not secure enough, switch to sha256")
1333 addSelectorErrorf("unsupported hash %q", sel.HashEffective)
1336 pemBuf, err := os.ReadFile(configDirPath(dynamicPath, sel.PrivateKeyFile))
1338 addSelectorErrorf("reading private key: %s", err)
1341 p, _ := pem.Decode(pemBuf)
1343 addSelectorErrorf("private key has no PEM block")
1346 key, err := x509.ParsePKCS8PrivateKey(p.Bytes)
1348 addSelectorErrorf("parsing private key: %s", err)
1351 switch k := key.(type) {
1352 case *rsa.PrivateKey:
1353 if k.N.BitLen() < 1024 {
1355 // Let's help user do the right thing.
1356 addSelectorErrorf("rsa keys should be >= 1024 bits, is %d bits", k.N.BitLen())
1359 sel.Algorithm = fmt.Sprintf("rsa-%d", k.N.BitLen())
1360 case ed25519.PrivateKey:
1361 if sel.HashEffective != "sha256" {
1362 addSelectorErrorf("hash algorithm %q is not supported with ed25519, only sha256 is", sel.HashEffective)
1365 sel.Algorithm = "ed25519"
1367 addSelectorErrorf("private key type %T not yet supported", key)
1370 if len(sel.Headers) == 0 {
1374 // By default we seal signed headers, and we sign user-visible headers to
1375 // prevent/limit reuse of previously signed messages: All addressing fields, date
1376 // and subject, message-referencing fields, parsing instructions (content-type).
1377 sel.HeadersEffective = strings.Split("From,To,Cc,Bcc,Reply-To,References,In-Reply-To,Subject,Date,Message-Id,Content-Type", ",")
1380 for _, h := range sel.Headers {
1381 from = from || strings.EqualFold(h, "From")
1383 if strings.EqualFold(h, "DKIM-Signature") || strings.EqualFold(h, "Received") || strings.EqualFold(h, "Return-Path") {
1384 log.Error("DKIM-signing header %q is recommended against as it may be modified in transit")
1388 addSelectorErrorf("From-field must always be DKIM-signed")
1390 sel.HeadersEffective = sel.Headers
1393 domain.DKIM.Selectors[name] = sel
1396 if domain.MTASTS != nil {
1397 if !haveSTSListener {
1398 addDomainErrorf("MTA-STS enabled, but there is no listener for MTASTS", d)
1400 sts := domain.MTASTS
1401 if sts.PolicyID == "" {
1402 addDomainErrorf("invalid empty MTA-STS PolicyID")
1405 case mtasts.ModeNone, mtasts.ModeTesting, mtasts.ModeEnforce:
1407 addDomainErrorf("invalid mtasts mode %q", sts.Mode)
1411 checkRoutes("routes for domain", domain.Routes)
1413 c.Domains[d] = domain
1416 // To determine ReportsOnly.
1417 domainHasAddress := map[string]bool{}
1419 // Validate email addresses.
1420 for accName, acc := range c.Accounts {
1421 addAccountErrorf := func(format string, args ...any) {
1422 addErrorf("account %q: %s", accName, fmt.Sprintf(format, args...))
1426 acc.DNSDomain, err = dns.ParseDomain(acc.Domain)
1428 addAccountErrorf("parsing domain %s: %s", acc.Domain, err)
1431 if strings.EqualFold(acc.RejectsMailbox, "Inbox") {
1432 addAccountErrorf("cannot set RejectsMailbox to inbox, messages will be removed automatically from the rejects mailbox")
1434 checkMailboxNormf(acc.RejectsMailbox, "rejects mailbox", addErrorf)
1436 if len(acc.LoginDisabled) > 256 {
1437 addAccountErrorf("message for disabled login must be <256 characters")
1439 for _, c := range acc.LoginDisabled {
1440 // For IMAP and SMTP. IMAP only allows UTF8 after "ENABLE IMAPrev2".
1441 if c < ' ' || c >= 0x7f {
1442 addAccountErrorf("message cannot contain control characters including newlines, and must be ascii-only")
1446 if acc.AutomaticJunkFlags.JunkMailboxRegexp != "" {
1447 r, err := regexp.Compile(acc.AutomaticJunkFlags.JunkMailboxRegexp)
1449 addAccountErrorf("invalid JunkMailboxRegexp regular expression: %v", err)
1453 if acc.AutomaticJunkFlags.NeutralMailboxRegexp != "" {
1454 r, err := regexp.Compile(acc.AutomaticJunkFlags.NeutralMailboxRegexp)
1456 addAccountErrorf("invalid NeutralMailboxRegexp regular expression: %v", err)
1458 acc.NeutralMailbox = r
1460 if acc.AutomaticJunkFlags.NotJunkMailboxRegexp != "" {
1461 r, err := regexp.Compile(acc.AutomaticJunkFlags.NotJunkMailboxRegexp)
1463 addAccountErrorf("invalid NotJunkMailboxRegexp regular expression: %v", err)
1465 acc.NotJunkMailbox = r
1468 if acc.JunkFilter != nil {
1469 params := acc.JunkFilter.Params
1470 if params.MaxPower < 0 || params.MaxPower > 0.5 {
1471 addAccountErrorf("junk filter MaxPower must be >= 0 and < 0.5")
1473 if params.TopWords < 0 {
1474 addAccountErrorf("junk filter TopWords must be >= 0")
1476 if params.IgnoreWords < 0 || params.IgnoreWords > 0.5 {
1477 addAccountErrorf("junk filter IgnoreWords must be >= 0 and < 0.5")
1479 if params.RareWords < 0 {
1480 addAccountErrorf("junk filter RareWords must be >= 0")
1484 acc.ParsedFromIDLoginAddresses = make([]smtp.Address, len(acc.FromIDLoginAddresses))
1485 for i, s := range acc.FromIDLoginAddresses {
1486 a, err := smtp.ParseAddress(s)
1488 addAccountErrorf("invalid fromid login address %q: %v", s, err)
1490 // We check later on if address belongs to account.
1491 dom, ok := c.Domains[a.Domain.Name()]
1493 addAccountErrorf("unknown domain in fromid login address %q", s)
1494 } else if len(dom.LocalpartCatchallSeparatorsEffective) == 0 {
1495 addAccountErrorf("localpart catchall separator not configured for domain for fromid login address %q", s)
1497 acc.ParsedFromIDLoginAddresses[i] = a
1500 // Clear any previously derived state.
1503 c.Accounts[accName] = acc
1505 if acc.OutgoingWebhook != nil {
1506 u, err := url.Parse(acc.OutgoingWebhook.URL)
1507 if err == nil && (u.Scheme != "http" && u.Scheme != "https") {
1508 err = errors.New("scheme must be http or https")
1511 addAccountErrorf("parsing outgoing hook url %q: %v", acc.OutgoingWebhook.URL, err)
1514 // note: outgoing hook events are in ../queue/hooks.go, ../mox-/config.go, ../queue.go and ../webapi/gendoc.sh. keep in sync.
1515 outgoingHookEvents := []string{"delivered", "suppressed", "delayed", "failed", "relayed", "expanded", "canceled", "unrecognized"}
1516 for _, e := range acc.OutgoingWebhook.Events {
1517 if !slices.Contains(outgoingHookEvents, e) {
1518 addAccountErrorf("unknown outgoing hook event %q", e)
1522 if acc.IncomingWebhook != nil {
1523 u, err := url.Parse(acc.IncomingWebhook.URL)
1524 if err == nil && (u.Scheme != "http" && u.Scheme != "https") {
1525 err = errors.New("scheme must be http or https")
1528 addAccountErrorf("parsing incoming hook url %q: %v", acc.IncomingWebhook.URL, err)
1532 // 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.
1533 replaceLocalparts := map[string]string{}
1535 for addrName, dest := range acc.Destinations {
1536 addDestErrorf := func(format string, args ...any) {
1537 addAccountErrorf("destination %q: %s", addrName, fmt.Sprintf(format, args...))
1540 checkMailboxNormf(dest.Mailbox, "destination mailbox", addDestErrorf)
1542 if dest.SMTPError != "" {
1543 if len(dest.SMTPError) > 256 {
1544 addDestErrorf("smtp error must be smaller than 256 bytes")
1546 for _, c := range dest.SMTPError {
1547 if c < ' ' || c >= 0x7f {
1548 addDestErrorf("smtp error cannot contain contain control characters (including newlines) or non-ascii")
1553 if dest.Mailbox != "" {
1554 addDestErrorf("cannot have both SMTPError and Mailbox")
1556 if len(dest.Rulesets) != 0 {
1557 addDestErrorf("cannot have both SMTPError and Rulesets")
1560 t := strings.SplitN(dest.SMTPError, " ", 2)
1563 addDestErrorf("smtp error must be 421 or 550 (with optional message), not %q", dest.SMTPError)
1566 dest.SMTPErrorCode = smtp.C451LocalErr
1567 dest.SMTPErrorSecode = smtp.SeSys3Other0
1568 dest.SMTPErrorMsg = "error processing"
1570 dest.SMTPErrorCode = smtp.C550MailboxUnavail
1571 dest.SMTPErrorSecode = smtp.SeAddr1UnknownDestMailbox1
1572 dest.SMTPErrorMsg = "no such user(s)"
1575 dest.SMTPErrorMsg = strings.TrimSpace(t[1])
1577 acc.Destinations[addrName] = dest
1580 if dest.MessageAuthRequiredSMTPError != "" {
1581 if len(dest.MessageAuthRequiredSMTPError) > 256 {
1582 addDestErrorf("message authentication required smtp error must be smaller than 256 bytes")
1584 for _, c := range dest.MessageAuthRequiredSMTPError {
1585 if c < ' ' || c >= 0x7f {
1586 addDestErrorf("message authentication required smtp error cannot contain contain control characters (including newlines) or non-ascii")
1592 for i, rs := range dest.Rulesets {
1593 addRulesetErrorf := func(format string, args ...any) {
1594 addDestErrorf("ruleset %d: %s", i+1, fmt.Sprintf(format, args...))
1597 checkMailboxNormf(rs.Mailbox, "ruleset mailbox", addRulesetErrorf)
1601 if rs.SMTPMailFromRegexp != "" {
1603 r, err := regexp.Compile(rs.SMTPMailFromRegexp)
1605 addRulesetErrorf("invalid SMTPMailFrom regular expression: %v", err)
1607 c.Accounts[accName].Destinations[addrName].Rulesets[i].SMTPMailFromRegexpCompiled = r
1609 if rs.MsgFromRegexp != "" {
1611 r, err := regexp.Compile(rs.MsgFromRegexp)
1613 addRulesetErrorf("invalid MsgFrom regular expression: %v", err)
1615 c.Accounts[accName].Destinations[addrName].Rulesets[i].MsgFromRegexpCompiled = r
1617 if rs.VerifiedDomain != "" {
1619 d, err := dns.ParseDomain(rs.VerifiedDomain)
1621 addRulesetErrorf("invalid VerifiedDomain: %v", err)
1623 c.Accounts[accName].Destinations[addrName].Rulesets[i].VerifiedDNSDomain = d
1626 var hdr [][2]*regexp.Regexp
1627 for k, v := range rs.HeadersRegexp {
1629 if strings.ToLower(k) != k {
1630 addRulesetErrorf("header field %q must only have lower case characters", k)
1632 if strings.ToLower(v) != v {
1633 addRulesetErrorf("header value %q must only have lower case characters", v)
1635 rk, err := regexp.Compile(k)
1637 addRulesetErrorf("invalid rule header regexp %q: %v", k, err)
1639 rv, err := regexp.Compile(v)
1641 addRulesetErrorf("invalid rule header regexp %q: %v", v, err)
1643 hdr = append(hdr, [...]*regexp.Regexp{rk, rv})
1645 c.Accounts[accName].Destinations[addrName].Rulesets[i].HeadersRegexpCompiled = hdr
1648 addRulesetErrorf("ruleset must have at least one rule")
1651 if rs.IsForward && rs.ListAllowDomain != "" {
1652 addRulesetErrorf("ruleset cannot have both IsForward and ListAllowDomain")
1655 if rs.SMTPMailFromRegexp == "" || rs.VerifiedDomain == "" {
1656 addRulesetErrorf("ruleset with IsForward must have both SMTPMailFromRegexp and VerifiedDomain too")
1659 if rs.ListAllowDomain != "" {
1660 d, err := dns.ParseDomain(rs.ListAllowDomain)
1662 addRulesetErrorf("invalid ListAllowDomain %q: %v", rs.ListAllowDomain, err)
1664 c.Accounts[accName].Destinations[addrName].Rulesets[i].ListAllowDNSDomain = d
1667 checkMailboxNormf(rs.AcceptRejectsToMailbox, "rejects mailbox", addRulesetErrorf)
1668 if strings.EqualFold(rs.AcceptRejectsToMailbox, "inbox") {
1669 addRulesetErrorf("AcceptRejectsToMailbox cannot be set to Inbox")
1673 // Catchall destination for domain.
1674 if strings.HasPrefix(addrName, "@") {
1675 d, err := dns.ParseDomain(addrName[1:])
1677 addDestErrorf("parsing domain %q", addrName[1:])
1679 } else if _, ok := c.Domains[d.Name()]; !ok {
1680 addDestErrorf("unknown domain for address")
1683 domainHasAddress[d.Name()] = true
1684 addrFull := "@" + d.Name()
1685 if _, ok := accDests[addrFull]; ok {
1686 addDestErrorf("duplicate canonicalized catchall destination address %s", addrFull)
1688 accDests[addrFull] = AccountDestination{true, "", accName, dest}
1692 // todo deprecated: remove support for parsing destination as just a localpart instead full address.
1693 var address smtp.Address
1694 if localpart, err := smtp.ParseLocalpart(addrName); err != nil && errors.Is(err, smtp.ErrBadLocalpart) {
1695 address, err = smtp.ParseAddress(addrName)
1697 addDestErrorf("invalid email address")
1699 } else if _, ok := c.Domains[address.Domain.Name()]; !ok {
1700 addDestErrorf("unknown domain for address")
1705 addDestErrorf("invalid localpart %q", addrName)
1708 address = smtp.NewAddress(localpart, acc.DNSDomain)
1709 if _, ok := c.Domains[acc.DNSDomain.Name()]; !ok {
1710 addDestErrorf("unknown domain %s", acc.DNSDomain.Name())
1713 replaceLocalparts[addrName] = address.Pack(true)
1716 origLP := address.Localpart
1717 dc := c.Domains[address.Domain.Name()]
1718 domainHasAddress[address.Domain.Name()] = true
1719 lp := CanonicalLocalpart(address.Localpart, dc)
1721 for _, sep := range dc.LocalpartCatchallSeparatorsEffective {
1722 if strings.Contains(string(address.Localpart), sep) {
1724 addDestErrorf("localpart of address %s includes domain catchall separator %s", address, sep)
1728 address.Localpart = lp
1730 addrFull := address.Pack(true)
1731 if _, ok := accDests[addrFull]; ok {
1732 addDestErrorf("duplicate canonicalized destination address %s", addrFull)
1734 accDests[addrFull] = AccountDestination{false, origLP, accName, dest}
1737 for lp, addr := range replaceLocalparts {
1738 dest, ok := acc.Destinations[lp]
1740 addAccountErrorf("could not find localpart %q to replace with address in destinations", lp)
1742 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`,
1743 slog.Any("localpart", lp),
1744 slog.Any("address", addr),
1745 slog.String("account", accName))
1746 acc.Destinations[addr] = dest
1747 delete(acc.Destinations, lp)
1751 // Now that all addresses are parsed, check if all fromid login addresses match
1752 // configured addresses.
1753 for i, a := range acc.ParsedFromIDLoginAddresses {
1754 // For domain catchall.
1755 if _, ok := accDests["@"+a.Domain.Name()]; ok {
1758 dc := c.Domains[a.Domain.Name()]
1759 a.Localpart = CanonicalLocalpart(a.Localpart, dc)
1760 if _, ok := accDests[a.Pack(true)]; !ok {
1761 addAccountErrorf("fromid login address %q does not match its destination addresses", acc.FromIDLoginAddresses[i])
1765 checkRoutes("routes for account", acc.Routes)
1768 // Set DMARC destinations.
1769 for d, domain := range c.Domains {
1770 addDomainErrorf := func(format string, args ...any) {
1771 addErrorf("domain %s: %s", d, fmt.Sprintf(format, args...))
1774 dmarc := domain.DMARC
1778 if _, ok := c.Accounts[dmarc.Account]; !ok {
1779 addDomainErrorf("DMARC account %q does not exist", dmarc.Account)
1782 // Note: For backwards compabilitiy, DMARC reporting localparts can contain catchall separators.
1783 lp, err := smtp.ParseLocalpart(dmarc.Localpart)
1785 addDomainErrorf("invalid DMARC localpart %q: %s", dmarc.Localpart, err)
1787 if lp.IsInternational() {
1789 addDomainErrorf("DMARC localpart %q is an internationalized address, only conventional ascii-only address possible for interopability", lp)
1791 addrdom := domain.Domain
1792 if dmarc.Domain != "" {
1793 addrdom, err = dns.ParseDomain(dmarc.Domain)
1795 addDomainErrorf("DMARC domain %q: %s", dmarc.Domain, err)
1796 } else if adomain, ok := c.Domains[addrdom.Name()]; !ok {
1797 addDomainErrorf("unknown domain %q for DMARC address", addrdom)
1798 } else if !adomain.LocalpartCaseSensitive {
1799 lp = smtp.Localpart(strings.ToLower(string(lp)))
1801 } else if !domain.LocalpartCaseSensitive {
1802 lp = smtp.Localpart(strings.ToLower(string(lp)))
1804 if addrdom == domain.Domain {
1805 domainHasAddress[addrdom.Name()] = true
1808 domain.DMARC.ParsedLocalpart = lp
1809 domain.DMARC.DNSDomain = addrdom
1810 c.Domains[d] = domain
1811 addrFull := smtp.NewAddress(lp, addrdom).String()
1812 dest := config.Destination{
1813 Mailbox: dmarc.Mailbox,
1816 checkMailboxNormf(dmarc.Mailbox, "DMARC mailbox for account", addDomainErrorf)
1817 accDests[addrFull] = AccountDestination{false, lp, dmarc.Account, dest}
1820 // Set TLSRPT destinations.
1821 for d, domain := range c.Domains {
1822 addDomainErrorf := func(format string, args ...any) {
1823 addErrorf("domain %s: %s", d, fmt.Sprintf(format, args...))
1826 tlsrpt := domain.TLSRPT
1830 if _, ok := c.Accounts[tlsrpt.Account]; !ok {
1831 addDomainErrorf("TLSRPT account %q does not exist", tlsrpt.Account)
1834 // Note: For backwards compabilitiy, TLS reporting localparts can contain catchall separators.
1835 lp, err := smtp.ParseLocalpart(tlsrpt.Localpart)
1837 addDomainErrorf("invalid TLSRPT localpart %q: %s", tlsrpt.Localpart, err)
1839 if lp.IsInternational() {
1840 // Does not appear documented in
../rfc/8460, but similar to DMARC it makes sense
1841 // to keep this ascii-only addresses.
1842 addDomainErrorf("TLSRPT localpart %q is an internationalized address, only conventional ascii-only address allowed for interopability", lp)
1844 addrdom := domain.Domain
1845 if tlsrpt.Domain != "" {
1846 addrdom, err = dns.ParseDomain(tlsrpt.Domain)
1848 addDomainErrorf("TLSRPT domain %q: %s", tlsrpt.Domain, err)
1849 } else if adomain, ok := c.Domains[addrdom.Name()]; !ok {
1850 addDomainErrorf("unknown domain %q for TLSRPT address", tlsrpt.Domain)
1851 } else if !adomain.LocalpartCaseSensitive {
1852 lp = smtp.Localpart(strings.ToLower(string(lp)))
1854 } else if !domain.LocalpartCaseSensitive {
1855 lp = smtp.Localpart(strings.ToLower(string(lp)))
1857 if addrdom == domain.Domain {
1858 domainHasAddress[addrdom.Name()] = true
1861 domain.TLSRPT.ParsedLocalpart = lp
1862 domain.TLSRPT.DNSDomain = addrdom
1863 c.Domains[d] = domain
1864 addrFull := smtp.NewAddress(lp, addrdom).String()
1865 dest := config.Destination{
1866 Mailbox: tlsrpt.Mailbox,
1867 DomainTLSReports: true,
1869 checkMailboxNormf(tlsrpt.Mailbox, "TLSRPT mailbox", addDomainErrorf)
1870 accDests[addrFull] = AccountDestination{false, lp, tlsrpt.Account, dest}
1873 // Set ReportsOnly for domains, based on whether we have seen addresses (possibly
1874 // from DMARC or TLS reporting).
1875 for d, domain := range c.Domains {
1876 domain.ReportsOnly = !domainHasAddress[domain.Domain.Name()]
1877 c.Domains[d] = domain
1880 // Aliases, per domain. Also add references to accounts.
1881 for d, domain := range c.Domains {
1882 for lpstr, a := range domain.Aliases {
1883 addAliasErrorf := func(format string, args ...any) {
1884 addErrorf("domain %s: alias %s: %s", d, lpstr, fmt.Sprintf(format, args...))
1888 a.LocalpartStr = lpstr
1889 var clp smtp.Localpart
1890 lp, err := smtp.ParseLocalpart(lpstr)
1892 addAliasErrorf("parsing alias: %v", err)
1896 for _, sep := range domain.LocalpartCatchallSeparatorsEffective {
1897 if strings.Contains(string(lp), sep) {
1898 addAliasErrorf("alias contains localpart catchall separator")
1905 clp = CanonicalLocalpart(lp, domain)
1908 addr := smtp.NewAddress(clp, domain.Domain).Pack(true)
1909 if _, ok := aliases[addr]; ok {
1910 addAliasErrorf("duplicate alias address %q", addr)
1913 if _, ok := accDests[addr]; ok {
1914 addAliasErrorf("alias %q already present as regular address", addr)
1917 if len(a.Addresses) == 0 {
1918 // Not currently possible, Addresses isn't optional.
1919 addAliasErrorf("alias %q needs at least one destination address", addr)
1922 a.ParsedAddresses = make([]config.AliasAddress, 0, len(a.Addresses))
1923 seen := map[string]bool{}
1924 for _, destAddr := range a.Addresses {
1925 da, err := smtp.ParseAddress(destAddr)
1927 addAliasErrorf("parsing destination address %q: %v", destAddr, err)
1930 dastr := da.Pack(true)
1931 accDest, ok := accDests[dastr]
1933 addAliasErrorf("references non-existent address %q", destAddr)
1937 addAliasErrorf("duplicate address %q", destAddr)
1941 aa := config.AliasAddress{Address: da, AccountName: accDest.Account, Destination: accDest.Destination}
1942 a.ParsedAddresses = append(a.ParsedAddresses, aa)
1944 a.Domain = domain.Domain
1945 c.Domains[d].Aliases[lpstr] = a
1948 for _, aa := range a.ParsedAddresses {
1949 acc := c.Accounts[aa.AccountName]
1952 addrs = make([]string, len(a.ParsedAddresses))
1953 for i := range a.ParsedAddresses {
1954 addrs[i] = a.ParsedAddresses[i].Address.Pack(true)
1957 // Keep the non-sensitive fields.
1958 accAlias := config.Alias{
1959 PostPublic: a.PostPublic,
1960 ListMembers: a.ListMembers,
1961 AllowMsgFrom: a.AllowMsgFrom,
1962 LocalpartStr: a.LocalpartStr,
1965 acc.Aliases = append(acc.Aliases, config.AddressAlias{SubscriptionAddress: aa.Address.Pack(true), Alias: accAlias, MemberAddresses: addrs})
1966 c.Accounts[aa.AccountName] = acc
1971 // Check webserver configs.
1972 if (len(c.WebDomainRedirects) > 0 || len(c.WebHandlers) > 0) && !haveWebserverListener {
1973 addErrorf("WebDomainRedirects or WebHandlers configured but no listener with WebserverHTTP or WebserverHTTPS enabled")
1976 c.WebDNSDomainRedirects = map[dns.Domain]dns.Domain{}
1977 for from, to := range c.WebDomainRedirects {
1978 addRedirectErrorf := func(format string, args ...any) {
1979 addErrorf("web redirect %s to %s: %s", from, to, fmt.Sprintf(format, args...))
1982 fromdom, err := dns.ParseDomain(from)
1984 addRedirectErrorf("parsing domain for redirect %s: %v", from, err)
1986 todom, err := dns.ParseDomain(to)
1988 addRedirectErrorf("parsing domain for redirect %s: %v", to, err)
1989 } else if fromdom == todom {
1990 addRedirectErrorf("will not redirect domain %s to itself", todom)
1992 var zerodom dns.Domain
1993 if _, ok := c.WebDNSDomainRedirects[fromdom]; ok && fromdom != zerodom {
1994 addRedirectErrorf("duplicate redirect domain %s", from)
1996 c.WebDNSDomainRedirects[fromdom] = todom
1999 for i := range c.WebHandlers {
2000 wh := &c.WebHandlers[i]
2002 addHandlerErrorf := func(format string, args ...any) {
2003 addErrorf("webhandler %s %s: %s", wh.Domain, wh.PathRegexp, fmt.Sprintf(format, args...))
2006 if wh.LogName == "" {
2007 wh.Name = fmt.Sprintf("%d", i)
2009 wh.Name = wh.LogName
2012 dom, err := dns.ParseDomain(wh.Domain)
2014 addHandlerErrorf("parsing domain: %v", err)
2018 if !strings.HasPrefix(wh.PathRegexp, "^") {
2019 addHandlerErrorf("path regexp must start with a ^")
2021 re, err := regexp.Compile(wh.PathRegexp)
2023 addHandlerErrorf("compiling regexp: %v", err)
2028 if wh.WebStatic != nil {
2031 if ws.StripPrefix != "" && !strings.HasPrefix(ws.StripPrefix, "/") {
2032 addHandlerErrorf("static: prefix to strip %s must start with a slash", ws.StripPrefix)
2034 for k := range ws.ResponseHeaders {
2036 k := strings.TrimSpace(xk)
2037 if k != xk || k == "" {
2038 addHandlerErrorf("static: bad header %q", xk)
2042 if wh.WebRedirect != nil {
2044 wr := wh.WebRedirect
2045 if wr.BaseURL != "" {
2046 u, err := url.Parse(wr.BaseURL)
2048 addHandlerErrorf("redirect: parsing redirect url %s: %v", wr.BaseURL, err)
2054 addHandlerErrorf("redirect: BaseURL must have empty path", wr.BaseURL)
2058 if wr.OrigPathRegexp != "" && wr.ReplacePath != "" {
2059 re, err := regexp.Compile(wr.OrigPathRegexp)
2061 addHandlerErrorf("compiling regexp %s: %v", wr.OrigPathRegexp, err)
2064 } else if wr.OrigPathRegexp != "" || wr.ReplacePath != "" {
2065 addHandlerErrorf("redirect: must have either both OrigPathRegexp and ReplacePath, or neither")
2066 } else if wr.BaseURL == "" {
2067 addHandlerErrorf("must at least one of BaseURL and OrigPathRegexp+ReplacePath")
2069 if wr.StatusCode != 0 && (wr.StatusCode < 300 || wr.StatusCode >= 400) {
2070 addHandlerErrorf("redirect: invalid redirect status code %d", wr.StatusCode)
2073 if wh.WebForward != nil {
2076 u, err := url.Parse(wf.URL)
2078 addHandlerErrorf("forward: parsing url %s: %v", wf.URL, err)
2082 for k := range wf.ResponseHeaders {
2084 k := strings.TrimSpace(xk)
2085 if k != xk || k == "" {
2086 addHandlerErrorf("forrward: bad header %q", xk)
2090 if wh.WebInternal != nil {
2092 wi := wh.WebInternal
2093 if !strings.HasPrefix(wi.BasePath, "/") || !strings.HasSuffix(wi.BasePath, "/") {
2094 addHandlerErrorf("internal service: base path %q must start and end with /", wi.BasePath)
2096 // todo: we could make maxMsgSize and accountPath configurable
2097 const isForwarded = false
2100 wi.Handler = NewWebadminHandler(wi.BasePath, isForwarded)
2102 wi.Handler = NewWebaccountHandler(wi.BasePath, isForwarded)
2105 wi.Handler = NewWebmailHandler(config.DefaultMaxMsgSize, wi.BasePath, isForwarded, accountPath)
2107 wi.Handler = NewWebapiHandler(config.DefaultMaxMsgSize, wi.BasePath, isForwarded)
2109 addHandlerErrorf("internal service: unknown service %q", wi.Service)
2111 wi.Handler = SafeHeaders(http.StripPrefix(wi.BasePath[:len(wi.BasePath)-1], wi.Handler))
2114 addHandlerErrorf("must have exactly one handler, not %d", n)
2118 c.MonitorDNSBLZones = nil
2119 for _, s := range c.MonitorDNSBLs {
2120 d, err := dns.ParseDomain(s)
2122 addErrorf("dnsbl %s: parsing dnsbl zone: %v", s, err)
2125 if slices.Contains(c.MonitorDNSBLZones, d) {
2126 addErrorf("dnsbl %s: duplicate zone", s)
2129 c.MonitorDNSBLZones = append(c.MonitorDNSBLZones, d)
2135func loadPrivateKeyFile(keyPath string) (crypto.Signer, error) {
2136 keyBuf, err := os.ReadFile(keyPath)
2138 return nil, fmt.Errorf("reading host private key: %v", err)
2140 b, _ := pem.Decode(keyBuf)
2142 return nil, fmt.Errorf("parsing pem block for private key: %v", err)
2147 privKey, err = x509.ParsePKCS8PrivateKey(b.Bytes)
2148 case "RSA PRIVATE KEY":
2149 privKey, err = x509.ParsePKCS1PrivateKey(b.Bytes)
2150 case "EC PRIVATE KEY":
2151 privKey, err = x509.ParseECPrivateKey(b.Bytes)
2153 err = fmt.Errorf("unknown pem type %q", b.Type)
2156 return nil, fmt.Errorf("parsing private key: %v", err)
2158 if k, ok := privKey.(crypto.Signer); ok {
2161 return nil, fmt.Errorf("parsed private key not a crypto.Signer, but %T", privKey)
2164func loadTLSKeyCerts(configFile, kind string, ctls *config.TLS) error {
2165 certs := []tls.Certificate{}
2166 for _, kp := range ctls.KeyCerts {
2167 certPath := configDirPath(configFile, kp.CertFile)
2168 keyPath := configDirPath(configFile, kp.KeyFile)
2169 cert, err := loadX509KeyPairPrivileged(certPath, keyPath)
2171 return fmt.Errorf("tls config for %q: parsing x509 key pair: %v", kind, err)
2173 certs = append(certs, cert)
2175 ctls.Config = &tls.Config{
2176 Certificates: certs,
2178 ctls.ConfigFallback = ctls.Config
2182// load x509 key/cert files from file descriptor possibly passed in by privileged
2184func loadX509KeyPairPrivileged(certPath, keyPath string) (tls.Certificate, error) {
2185 certBuf, err := readFilePrivileged(certPath)
2187 return tls.Certificate{}, fmt.Errorf("reading tls certificate: %v", err)
2189 keyBuf, err := readFilePrivileged(keyPath)
2191 return tls.Certificate{}, fmt.Errorf("reading tls key: %v", err)
2193 return tls.X509KeyPair(certBuf, keyBuf)
2196// like os.ReadFile, but open privileged file possibly passed in by root process.
2197func readFilePrivileged(path string) ([]byte, error) {
2198 f, err := OpenPrivileged(path)
2203 return io.ReadAll(f)