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 managerHosts := map[*autotls.Manager]map[dns.Domain]struct{}{}
316 for _, l := range c.Static.Listeners {
317 if l.TLS == nil || l.TLS.ACME == "" {
321 m := c.Static.ACME[l.TLS.ACME].Manager
322 if managerHosts[m] == nil {
323 managerHosts[m] = map[dns.Domain]struct{}{}
325 hostnames := managerHosts[m]
327 hostnames[c.Static.HostnameDomain] = struct{}{}
328 if l.HostnameDomain.ASCII != "" {
329 hostnames[l.HostnameDomain] = struct{}{}
332 for _, dom := range c.Dynamic.Domains {
333 // Do not allow TLS certificates for domains for which we only accept DMARC/TLS
334 // reports as external party.
339 // Do not fetch TLS certs for disabled domains. The A/AAAA records may not be
340 // configured or still point to a previous machine before a migration.
345 if l.AutoconfigHTTPS.Enabled && !l.AutoconfigHTTPS.NonTLS {
346 if d, err := dns.ParseDomain("autoconfig." + dom.Domain.ASCII); err != nil {
347 log.Errorx("parsing autoconfig domain", err, slog.Any("domain", dom.Domain))
349 hostnames[d] = struct{}{}
353 if l.MTASTSHTTPS.Enabled && dom.MTASTS != nil && !l.MTASTSHTTPS.NonTLS {
354 d, err := dns.ParseDomain("mta-sts." + dom.Domain.ASCII)
356 log.Errorx("parsing mta-sts domain", err, slog.Any("domain", dom.Domain))
358 hostnames[d] = struct{}{}
362 if dom.ClientSettingsDomain != "" {
363 hostnames[dom.ClientSettingsDNSDomain] = struct{}{}
367 if l.WebserverHTTPS.Enabled {
368 for from := range c.Dynamic.WebDNSDomainRedirects {
369 hostnames[from] = struct{}{}
371 for _, wh := range c.Dynamic.WebHandlers {
372 hostnames[wh.DNSDomain] = struct{}{}
378 public := c.Static.Listeners["public"]
380 if len(public.NATIPs) > 0 {
387 for m, hostnames := range managerHosts {
388 m.SetAllowedHostnames(log, dns.StrictResolver{Pkg: "autotls", Log: log.Logger}, hostnames, ips, checkACMEHosts)
392// 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.
394// WriteDynamicLocked prepares an updated internal state for the new dynamic
395// config, then writes it to disk and activates it.
397// Returns ErrConfig if the configuration is not valid.
399// Must be called with config lock held.
400func WriteDynamicLocked(ctx context.Context, log mlog.Log, c config.Dynamic) error {
401 accDests, aliases, errs := prepareDynamicConfig(ctx, log, ConfigDynamicPath, Conf.Static, &c)
403 errstrs := make([]string, len(errs))
404 for i, err := range errs {
405 errstrs[i] = err.Error()
407 return fmt.Errorf("%w: %s", ErrConfig, strings.Join(errstrs, "; "))
411 err := sconf.Write(&b, c)
415 f, err := os.OpenFile(ConfigDynamicPath, os.O_WRONLY, 0660)
422 log.Check(err, "closing file after error")
426 if _, err := f.Write(buf); err != nil {
427 return fmt.Errorf("write domains.conf: %v", err)
429 if err := f.Truncate(int64(len(buf))); err != nil {
430 return fmt.Errorf("truncate domains.conf after write: %v", err)
432 if err := f.Sync(); err != nil {
433 return fmt.Errorf("sync domains.conf after write: %v", err)
435 if err := moxio.SyncDir(log, filepath.Dir(ConfigDynamicPath)); err != nil {
436 return fmt.Errorf("sync dir of domains.conf after write: %v", err)
441 return fmt.Errorf("stat after writing domains.conf: %v", err)
444 if err := f.Close(); err != nil {
445 return fmt.Errorf("close written domains.conf: %v", err)
449 Conf.dynamicMtime = fi.ModTime()
450 Conf.DynamicLastCheck = time.Now()
452 Conf.AccountDestinationsLocked = accDests
453 Conf.aliases = aliases
455 Conf.allowACMEHosts(log, true)
460// MustLoadConfig loads the config, quitting on errors.
461func MustLoadConfig(doLoadTLSKeyCerts, checkACMEHosts bool) {
462 errs := LoadConfig(context.Background(), pkglog, doLoadTLSKeyCerts, checkACMEHosts)
464 pkglog.Error("loading config file: multiple errors")
465 for _, err := range errs {
466 pkglog.Errorx("config error", err)
468 pkglog.Fatal("stopping after multiple config errors")
469 } else if len(errs) == 1 {
470 pkglog.Fatalx("loading config file", errs[0])
474// LoadConfig attempts to parse and load a config, returning any errors
476func LoadConfig(ctx context.Context, log mlog.Log, doLoadTLSKeyCerts, checkACMEHosts bool) []error {
477 Shutdown, ShutdownCancel = context.WithCancel(context.Background())
478 Context, ContextCancel = context.WithCancel(context.Background())
480 c, errs := ParseConfig(ctx, log, ConfigStaticPath, false, doLoadTLSKeyCerts, checkACMEHosts)
485 mlog.SetConfig(c.Log)
490// SetConfig sets a new config. Not to be used during normal operation.
491func SetConfig(c *Config) {
492 // Cannot just assign *c to Conf, it would copy the mutex.
493 Conf = Config{c.Static, sync.Mutex{}, c.Log, sync.Mutex{}, c.Dynamic, c.dynamicMtime, c.DynamicLastCheck, c.AccountDestinationsLocked, c.aliases}
495 // If we have non-standard CA roots, use them for all HTTPS requests.
496 if Conf.Static.TLS.CertPool != nil {
497 http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{
498 RootCAs: Conf.Static.TLS.CertPool,
502 SetPedantic(c.Static.Pedantic)
505// Set pedantic in all packages.
506func SetPedantic(p bool) {
514// ParseConfig parses the static config at path p. If checkOnly is true, no changes
515// are made, such as registering ACME identities. If doLoadTLSKeyCerts is true,
516// the TLS KeyCerts configuration is loaded and checked. This is used during the
517// quickstart in the case the user is going to provide their own certificates.
518// If checkACMEHosts is true, the hosts allowed for acme are compared with the
519// explicitly configured ips we are listening on.
520func ParseConfig(ctx context.Context, log mlog.Log, p string, checkOnly, doLoadTLSKeyCerts, checkACMEHosts bool) (c *Config, errs []error) {
522 Static: config.Static{
529 if os.IsNotExist(err) && os.Getenv("MOXCONF") == "" {
530 return nil, []error{fmt.Errorf("open config file: %v (hint: use mox -config ... or set MOXCONF=...)", err)}
532 return nil, []error{fmt.Errorf("open config file: %v", err)}
535 if err := sconf.Parse(f, &c.Static); err != nil {
536 return nil, []error{fmt.Errorf("parsing %s%v", p, err)}
539 if xerrs := PrepareStaticConfig(ctx, log, p, c, checkOnly, doLoadTLSKeyCerts); len(xerrs) > 0 {
543 pp := filepath.Join(filepath.Dir(p), "domains.conf")
544 c.Dynamic, c.dynamicMtime, c.AccountDestinationsLocked, c.aliases, errs = ParseDynamicConfig(ctx, log, pp, c.Static)
547 c.allowACMEHosts(log, checkACMEHosts)
553// PrepareStaticConfig parses the static config file and prepares data structures
554// for starting mox. If checkOnly is set no substantial changes are made, like
555// creating an ACME registration.
556func PrepareStaticConfig(ctx context.Context, log mlog.Log, configFile string, conf *Config, checkOnly, doLoadTLSKeyCerts bool) (errs []error) {
557 addErrorf := func(format string, args ...any) {
558 errs = append(errs, fmt.Errorf(format, args...))
563 // check that mailbox is in unicode NFC normalized form.
564 checkMailboxNormf := func(mailbox string, format string, args ...any) {
565 s := norm.NFC.String(mailbox)
567 msg := fmt.Sprintf(format, args...)
568 addErrorf("%s: mailbox %q is not in NFC normalized form, should be %q", msg, mailbox, s)
572 // Post-process logging config.
573 if logLevel, ok := mlog.Levels[c.LogLevel]; ok {
574 conf.Log = map[string]slog.Level{"": logLevel}
576 addErrorf("invalid log level %q", c.LogLevel)
578 for pkg, s := range c.PackageLogLevels {
579 if logLevel, ok := mlog.Levels[s]; ok {
580 conf.Log[pkg] = logLevel
582 addErrorf("invalid package log level %q", s)
589 u, err := user.Lookup(c.User)
591 uid, err := strconv.ParseUint(c.User, 10, 32)
593 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)
595 // We assume the same gid as uid.
600 if uid, err := strconv.ParseUint(u.Uid, 10, 32); err != nil {
601 addErrorf("parsing uid %s: %v", u.Uid, err)
605 if gid, err := strconv.ParseUint(u.Gid, 10, 32); err != nil {
606 addErrorf("parsing gid %s: %v", u.Gid, err)
612 hostname, err := dns.ParseDomain(c.Hostname)
614 addErrorf("parsing hostname: %s", err)
615 } else if hostname.Name() != c.Hostname {
616 addErrorf("hostname must be in unicode form %q instead of %q", hostname.Name(), c.Hostname)
618 c.HostnameDomain = hostname
620 if c.HostTLSRPT.Account != "" {
621 tlsrptLocalpart, err := smtp.ParseLocalpart(c.HostTLSRPT.Localpart)
623 addErrorf("invalid localpart %q for host tlsrpt: %v", c.HostTLSRPT.Localpart, err)
624 } else if tlsrptLocalpart.IsInternational() {
625 // Does not appear documented in
../rfc/8460, but similar to DMARC it makes sense
626 // to keep this ascii-only addresses.
627 addErrorf("host TLSRPT localpart %q is an internationalized address, only conventional ascii-only address allowed for interopability", tlsrptLocalpart)
629 c.HostTLSRPT.ParsedLocalpart = tlsrptLocalpart
632 // Return private key for host name for use with an ACME. Used to return the same
633 // private key as pre-generated for use with DANE, with its public key in DNS.
634 // We only use this key for Listener's that have this ACME configured, and for
635 // which the effective listener host name (either specific to the listener, or the
636 // global name) is requested. Other host names can get a fresh private key, they
637 // don't appear in DANE records.
639 // - run 0: only use listener with explicitly matching host name in listener
640 // (default quickstart config does not set it).
641 // - run 1: only look at public listener (and host matching mox host name)
642 // - run 2: all listeners (and host matching mox host name)
643 findACMEHostPrivateKey := func(acmeName, host string, keyType autocert.KeyType, run int) crypto.Signer {
644 for listenerName, l := range Conf.Static.Listeners {
645 if l.TLS == nil || l.TLS.ACME != acmeName {
648 if run == 0 && host != l.HostnameDomain.ASCII {
651 if run == 1 && listenerName != "public" || host != Conf.Static.HostnameDomain.ASCII {
655 case autocert.KeyRSA2048:
656 if len(l.TLS.HostPrivateRSA2048Keys) == 0 {
659 return l.TLS.HostPrivateRSA2048Keys[0]
660 case autocert.KeyECDSAP256:
661 if len(l.TLS.HostPrivateECDSAP256Keys) == 0 {
664 return l.TLS.HostPrivateECDSAP256Keys[0]
671 // Make a function for an autocert.Manager.GetPrivateKey, using findACMEHostPrivateKey.
672 makeGetPrivateKey := func(acmeName string) func(host string, keyType autocert.KeyType) (crypto.Signer, error) {
673 return func(host string, keyType autocert.KeyType) (crypto.Signer, error) {
674 key := findACMEHostPrivateKey(acmeName, host, keyType, 0)
676 key = findACMEHostPrivateKey(acmeName, host, keyType, 1)
679 key = findACMEHostPrivateKey(acmeName, host, keyType, 2)
682 log.Debug("found existing private key for certificate for host",
683 slog.String("acmename", acmeName),
684 slog.String("host", host),
685 slog.Any("keytype", keyType))
688 log.Debug("generating new private key for certificate for host",
689 slog.String("acmename", acmeName),
690 slog.String("host", host),
691 slog.Any("keytype", keyType))
693 case autocert.KeyRSA2048:
694 return rsa.GenerateKey(cryptorand.Reader, 2048)
695 case autocert.KeyECDSAP256:
696 return ecdsa.GenerateKey(elliptic.P256(), cryptorand.Reader)
698 return nil, fmt.Errorf("unrecognized requested key type %v", keyType)
702 for name, acme := range c.ACME {
703 addAcmeErrorf := func(format string, args ...any) {
704 addErrorf("acme provider %s: %s", name, fmt.Sprintf(format, args...))
709 if acme.ExternalAccountBinding != nil {
710 eabKeyID = acme.ExternalAccountBinding.KeyID
711 p := configDirPath(configFile, acme.ExternalAccountBinding.KeyFile)
712 buf, err := os.ReadFile(p)
714 addAcmeErrorf("reading external account binding key: %s", err)
716 dec := make([]byte, base64.RawURLEncoding.DecodedLen(len(buf)))
717 n, err := base64.RawURLEncoding.Decode(dec, buf)
719 addAcmeErrorf("parsing external account binding key as base64: %s", err)
730 acmeDir := dataDirPath(configFile, c.DataDir, "acme")
731 os.MkdirAll(acmeDir, 0770)
732 manager, err := autotls.Load(log, name, acmeDir, acme.ContactEmail, acme.DirectoryURL, eabKeyID, eabKey, makeGetPrivateKey(name), Shutdown.Done())
734 addAcmeErrorf("loading ACME identity: %s", err)
736 acme.Manager = manager
738 // Help configurations from older quickstarts.
739 if acme.IssuerDomainName == "" && acme.DirectoryURL == "https://acme-v02.api.letsencrypt.org/directory" {
740 acme.IssuerDomainName = "letsencrypt.org"
746 var haveUnspecifiedSMTPListener bool
747 for name, l := range c.Listeners {
748 addListenerErrorf := func(format string, args ...any) {
749 addErrorf("listener %s: %w", name, fmt.Errorf(format, args...))
752 if l.Hostname != "" {
753 d, err := dns.ParseDomain(l.Hostname)
755 addListenerErrorf("parsing hostname %q: %s", l.Hostname, err)
760 if l.TLS.ACME != "" && len(l.TLS.KeyCerts) != 0 {
761 addListenerErrorf("cannot have ACME and static key/certificates")
762 } else if l.TLS.ACME != "" {
763 acme, ok := c.ACME[l.TLS.ACME]
765 addListenerErrorf("unknown ACME provider %q", l.TLS.ACME)
768 // If only checking or with missing ACME definition, we don't have an acme manager,
769 // so set an empty tls config to continue.
770 var tlsconfig, tlsconfigFallback *tls.Config
771 if checkOnly || acme.Manager == nil {
772 tlsconfig = &tls.Config{}
773 tlsconfigFallback = &tls.Config{}
775 hostname := c.HostnameDomain
776 if l.Hostname != "" {
777 hostname = l.HostnameDomain
779 // If SNI is absent, we will use the listener hostname, but reject connections with
780 // an SNI hostname that is not allowlisted.
781 // Incoming SMTP deliveries use tlsconfigFallback for interoperability. TLS
782 // connections for unknown SNI hostnames fall back to a certificate for the
783 // listener hostname instead of causing the TLS connection to fail.
784 tlsconfig = acme.Manager.TLSConfig(hostname, true, false)
785 tlsconfigFallback = acme.Manager.TLSConfig(hostname, true, true)
786 l.TLS.ACMEConfig = acme.Manager.ACMETLSConfig
788 l.TLS.Config = tlsconfig
789 l.TLS.ConfigFallback = tlsconfigFallback
790 } else if len(l.TLS.KeyCerts) != 0 {
791 if doLoadTLSKeyCerts {
792 if err := loadTLSKeyCerts(configFile, "listener "+name, l.TLS); err != nil {
793 addListenerErrorf("%w", err)
797 addListenerErrorf("cannot have TLS config without ACME and without static keys/certificates")
799 for _, privKeyFile := range l.TLS.HostPrivateKeyFiles {
800 keyPath := configDirPath(configFile, privKeyFile)
801 privKey, err := loadPrivateKeyFile(keyPath)
803 addListenerErrorf("parsing host private key for DANE and ACME certificates: %v", err)
806 switch k := privKey.(type) {
807 case *rsa.PrivateKey:
808 if k.N.BitLen() != 2048 {
809 log.Error("need rsa key with 2048 bits, for host private key for DANE/ACME certificates, ignoring",
810 slog.String("listener", name),
811 slog.String("file", keyPath),
812 slog.Int("bits", k.N.BitLen()))
815 l.TLS.HostPrivateRSA2048Keys = append(l.TLS.HostPrivateRSA2048Keys, k)
816 case *ecdsa.PrivateKey:
817 if k.Curve != elliptic.P256() {
818 log.Error("unrecognized ecdsa curve for host private key for DANE/ACME certificates, ignoring", slog.String("listener", name), slog.String("file", keyPath))
821 l.TLS.HostPrivateECDSAP256Keys = append(l.TLS.HostPrivateECDSAP256Keys, k)
823 log.Error("unrecognized key type for host private key for DANE/ACME certificates, ignoring",
824 slog.String("listener", name),
825 slog.String("file", keyPath),
826 slog.String("keytype", fmt.Sprintf("%T", privKey)))
830 if l.TLS.ACME != "" && (len(l.TLS.HostPrivateRSA2048Keys) == 0) != (len(l.TLS.HostPrivateECDSAP256Keys) == 0) {
831 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")
835 var minVersion uint16 = tls.VersionTLS12
836 if l.TLS.MinVersion != "" {
837 versions := map[string]uint16{
838 "TLSv1.0": tls.VersionTLS10,
839 "TLSv1.1": tls.VersionTLS11,
840 "TLSv1.2": tls.VersionTLS12,
841 "TLSv1.3": tls.VersionTLS13,
843 v, ok := versions[l.TLS.MinVersion]
845 addListenerErrorf("unknown TLS mininum version %q", l.TLS.MinVersion)
849 if l.TLS.Config != nil {
850 l.TLS.Config.MinVersion = minVersion
852 if l.TLS.ConfigFallback != nil {
853 l.TLS.ConfigFallback.MinVersion = minVersion
855 if l.TLS.ACMEConfig != nil {
856 l.TLS.ACMEConfig.MinVersion = minVersion
859 var needsTLS []string
860 needtls := func(s string, v bool) {
862 needsTLS = append(needsTLS, s)
865 needtls("IMAPS", l.IMAPS.Enabled)
866 needtls("SMTP", l.SMTP.Enabled && !l.SMTP.NoSTARTTLS)
867 needtls("Submissions", l.Submissions.Enabled)
868 needtls("Submission", l.Submission.Enabled && !l.Submission.NoRequireSTARTTLS)
869 needtls("AccountHTTPS", l.AccountHTTPS.Enabled)
870 needtls("AdminHTTPS", l.AdminHTTPS.Enabled)
871 needtls("AutoconfigHTTPS", l.AutoconfigHTTPS.Enabled && !l.AutoconfigHTTPS.NonTLS)
872 needtls("MTASTSHTTPS", l.MTASTSHTTPS.Enabled && !l.MTASTSHTTPS.NonTLS)
873 needtls("WebserverHTTPS", l.WebserverHTTPS.Enabled)
874 if len(needsTLS) > 0 {
875 addListenerErrorf("no tls config specified, but requires tls for %s", strings.Join(needsTLS, ", "))
878 if l.AutoconfigHTTPS.Enabled && l.MTASTSHTTPS.Enabled && l.AutoconfigHTTPS.Port == l.MTASTSHTTPS.Port && l.AutoconfigHTTPS.NonTLS != l.MTASTSHTTPS.NonTLS {
879 addListenerErrorf("autoconfig and mta-sts enabled on same port but with both http and https")
883 haveUnspecifiedSMTPListener = true
885 for _, ipstr := range l.IPs {
886 ip := net.ParseIP(ipstr)
888 addListenerErrorf("invalid IP %q", ipstr)
891 if ip.IsUnspecified() {
892 haveUnspecifiedSMTPListener = true
895 if len(c.SpecifiedSMTPListenIPs) >= 2 {
896 haveUnspecifiedSMTPListener = true
897 } else if len(c.SpecifiedSMTPListenIPs) > 0 && (c.SpecifiedSMTPListenIPs[0].To4() == nil) == (ip.To4() == nil) {
898 haveUnspecifiedSMTPListener = true
900 c.SpecifiedSMTPListenIPs = append(c.SpecifiedSMTPListenIPs, ip)
904 for _, s := range l.SMTP.DNSBLs {
905 d, err := dns.ParseDomain(s)
907 addListenerErrorf("parsing DNSBL zone %q: %s", s, err)
910 l.SMTP.DNSBLZones = append(l.SMTP.DNSBLZones, d)
912 if l.IPsNATed && len(l.NATIPs) > 0 {
913 addListenerErrorf("both IPsNATed and NATIPs configued (remove deprecated IPsNATed)")
915 for _, ipstr := range l.NATIPs {
916 ip := net.ParseIP(ipstr)
918 addListenerErrorf("invalid ip %q", ipstr)
919 } else if ip.IsUnspecified() || ip.IsLoopback() {
920 addListenerErrorf("NAT ip that is the unspecified or loopback address %s", ipstr)
923 cleanPath := func(kind string, enabled bool, path string) string {
927 if path != "" && !strings.HasPrefix(path, "/") {
928 addListenerErrorf("%s with path %q that must start with a slash", kind, path)
929 } else if path != "" && !strings.HasSuffix(path, "/") {
930 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+"/"))
935 l.AccountHTTP.Path = cleanPath("AccountHTTP", l.AccountHTTP.Enabled, l.AccountHTTP.Path)
936 l.AccountHTTPS.Path = cleanPath("AccountHTTPS", l.AccountHTTPS.Enabled, l.AccountHTTPS.Path)
937 l.AdminHTTP.Path = cleanPath("AdminHTTP", l.AdminHTTP.Enabled, l.AdminHTTP.Path)
938 l.AdminHTTPS.Path = cleanPath("AdminHTTPS", l.AdminHTTPS.Enabled, l.AdminHTTPS.Path)
939 l.WebmailHTTP.Path = cleanPath("WebmailHTTP", l.WebmailHTTP.Enabled, l.WebmailHTTP.Path)
940 l.WebmailHTTPS.Path = cleanPath("WebmailHTTPS", l.WebmailHTTPS.Enabled, l.WebmailHTTPS.Path)
941 l.WebAPIHTTP.Path = cleanPath("WebAPIHTTP", l.WebAPIHTTP.Enabled, l.WebAPIHTTP.Path)
942 l.WebAPIHTTPS.Path = cleanPath("WebAPIHTTPS", l.WebAPIHTTPS.Enabled, l.WebAPIHTTPS.Path)
943 c.Listeners[name] = l
945 if haveUnspecifiedSMTPListener {
946 c.SpecifiedSMTPListenIPs = nil
949 var zerouse config.SpecialUseMailboxes
950 if len(c.DefaultMailboxes) > 0 && (c.InitialMailboxes.SpecialUse != zerouse || len(c.InitialMailboxes.Regular) > 0) {
951 addErrorf("cannot have both DefaultMailboxes and InitialMailboxes")
953 // DefaultMailboxes is deprecated.
954 for _, mb := range c.DefaultMailboxes {
955 checkMailboxNormf(mb, "default mailbox")
956 // We don't create parent mailboxes for default mailboxes.
957 if ParentMailboxName(mb) != "" {
958 addErrorf("default mailbox cannot be a child mailbox")
961 checkSpecialUseMailbox := func(nameOpt string) {
963 checkMailboxNormf(nameOpt, "special-use initial mailbox")
964 if strings.EqualFold(nameOpt, "inbox") {
965 addErrorf("initial mailbox cannot be set to Inbox (Inbox is always created)")
967 // We don't currently create parent mailboxes for initial mailboxes.
968 if ParentMailboxName(nameOpt) != "" {
969 addErrorf("initial mailboxes cannot be child mailboxes")
973 checkSpecialUseMailbox(c.InitialMailboxes.SpecialUse.Archive)
974 checkSpecialUseMailbox(c.InitialMailboxes.SpecialUse.Draft)
975 checkSpecialUseMailbox(c.InitialMailboxes.SpecialUse.Junk)
976 checkSpecialUseMailbox(c.InitialMailboxes.SpecialUse.Sent)
977 checkSpecialUseMailbox(c.InitialMailboxes.SpecialUse.Trash)
978 for _, name := range c.InitialMailboxes.Regular {
979 checkMailboxNormf(name, "regular initial mailbox")
980 if strings.EqualFold(name, "inbox") {
981 addErrorf("initial regular mailbox cannot be set to Inbox (Inbox is always created)")
983 if ParentMailboxName(name) != "" {
984 addErrorf("initial mailboxes cannot be child mailboxes")
988 checkTransportSMTP := func(name string, isTLS bool, t *config.TransportSMTP) {
989 addTransportErrorf := func(format string, args ...any) {
990 addErrorf("transport %s: %s", name, fmt.Sprintf(format, args...))
994 t.DNSHost, err = dns.ParseDomain(t.Host)
996 addTransportErrorf("bad host %s: %v", t.Host, err)
999 if isTLS && t.STARTTLSInsecureSkipVerify {
1000 addTransportErrorf("cannot have STARTTLSInsecureSkipVerify with immediate TLS")
1002 if isTLS && t.NoSTARTTLS {
1003 addTransportErrorf("cannot have NoSTARTTLS with immediate TLS")
1009 seen := map[string]bool{}
1010 for _, m := range t.Auth.Mechanisms {
1012 addTransportErrorf("duplicate authentication mechanism %s", m)
1016 case "SCRAM-SHA-256-PLUS":
1017 case "SCRAM-SHA-256":
1018 case "SCRAM-SHA-1-PLUS":
1024 addTransportErrorf("unknown authentication mechanism %s", m)
1028 t.Auth.EffectiveMechanisms = t.Auth.Mechanisms
1029 if len(t.Auth.EffectiveMechanisms) == 0 {
1030 t.Auth.EffectiveMechanisms = []string{"SCRAM-SHA-256-PLUS", "SCRAM-SHA-256", "SCRAM-SHA-1-PLUS", "SCRAM-SHA-1", "CRAM-MD5"}
1034 checkTransportSocks := func(name string, t *config.TransportSocks) {
1035 addTransportErrorf := func(format string, args ...any) {
1036 addErrorf("transport %s: %s", name, fmt.Sprintf(format, args...))
1039 _, _, err := net.SplitHostPort(t.Address)
1041 addTransportErrorf("bad address %s: %v", t.Address, err)
1043 for _, ipstr := range t.RemoteIPs {
1044 ip := net.ParseIP(ipstr)
1046 addTransportErrorf("bad ip %s", ipstr)
1048 t.IPs = append(t.IPs, ip)
1051 t.Hostname, err = dns.ParseDomain(t.RemoteHostname)
1053 addTransportErrorf("bad hostname %s: %v", t.RemoteHostname, err)
1057 checkTransportDirect := func(name string, t *config.TransportDirect) {
1058 addTransportErrorf := func(format string, args ...any) {
1059 addErrorf("transport %s: %s", name, fmt.Sprintf(format, args...))
1062 if t.DisableIPv4 && t.DisableIPv6 {
1063 addTransportErrorf("both IPv4 and IPv6 are disabled, enable at least one")
1074 checkTransportFail := func(name string, t *config.TransportFail) {
1075 addTransportErrorf := func(format string, args ...any) {
1076 addErrorf("transport %s: %s", name, fmt.Sprintf(format, args...))
1079 if t.SMTPCode == 0 {
1080 t.Code = smtp.C554TransactionFailed
1081 } else if t.SMTPCode/100 != 4 && t.SMTPCode/100 != 5 {
1082 addTransportErrorf("smtp code %d must be 4xx or 5xx", t.SMTPCode/100)
1087 if len(t.SMTPMessage) > 256 {
1088 addTransportErrorf("message must be <= 256 characters")
1090 for _, c := range t.SMTPMessage {
1091 if c < ' ' || c >= 0x7f {
1092 addTransportErrorf("message cannot contain control characters including newlines, and must be ascii-only")
1095 t.Message = t.SMTPMessage
1096 if t.Message == "" {
1097 t.Message = "transport fail: explicit immediate delivery failure per configuration"
1101 for name, t := range c.Transports {
1102 addTransportErrorf := func(format string, args ...any) {
1103 addErrorf("transport %s: %s", name, fmt.Sprintf(format, args...))
1107 if t.Submissions != nil {
1109 checkTransportSMTP(name, true, t.Submissions)
1111 if t.Submission != nil {
1113 checkTransportSMTP(name, false, t.Submission)
1117 checkTransportSMTP(name, false, t.SMTP)
1121 checkTransportSocks(name, t.Socks)
1123 if t.Direct != nil {
1125 checkTransportDirect(name, t.Direct)
1129 checkTransportFail(name, t.Fail)
1132 addTransportErrorf("cannot have multiple methods in a transport")
1136 // Load CA certificate pool.
1137 if c.TLS.CA != nil {
1138 if c.TLS.CA.AdditionalToSystem {
1140 c.TLS.CertPool, err = x509.SystemCertPool()
1142 addErrorf("fetching system CA cert pool: %v", err)
1145 c.TLS.CertPool = x509.NewCertPool()
1147 for _, certfile := range c.TLS.CA.CertFiles {
1148 p := configDirPath(configFile, certfile)
1149 pemBuf, err := os.ReadFile(p)
1151 addErrorf("reading TLS CA cert file: %v", err)
1153 } else if !c.TLS.CertPool.AppendCertsFromPEM(pemBuf) {
1154 // todo: can we check more fully if we're getting some useful data back?
1155 addErrorf("no CA certs added from %q", p)
1162// PrepareDynamicConfig parses the dynamic config file given a static file.
1163func 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) {
1164 addErrorf := func(format string, args ...any) {
1165 errs = append(errs, fmt.Errorf(format, args...))
1168 f, err := os.Open(dynamicPath)
1170 addErrorf("parsing domains config: %v", err)
1176 addErrorf("stat domains config: %v", err)
1178 if err := sconf.Parse(f, &c); err != nil {
1179 addErrorf("parsing dynamic config file: %v", err)
1183 accDests, aliases, errs = prepareDynamicConfig(ctx, log, dynamicPath, static, &c)
1184 return c, fi.ModTime(), accDests, aliases, errs
1187func 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) {
1188 addErrorf := func(format string, args ...any) {
1189 errs = append(errs, fmt.Errorf(format, args...))
1192 // Check that mailbox is in unicode NFC normalized form.
1193 checkMailboxNormf := func(mailbox string, what string, errorf func(format string, args ...any)) {
1194 s := norm.NFC.String(mailbox)
1196 errorf("%s: mailbox %q is not in NFC normalized form, should be %q", what, mailbox, s)
1200 // Validate postmaster account exists.
1201 if _, ok := c.Accounts[static.Postmaster.Account]; !ok {
1202 addErrorf("postmaster account %q does not exist", static.Postmaster.Account)
1204 checkMailboxNormf(static.Postmaster.Mailbox, "postmaster mailbox", addErrorf)
1206 accDests = map[string]AccountDestination{}
1207 aliases = map[string]config.Alias{}
1209 // Validate host TLSRPT account/address.
1210 if static.HostTLSRPT.Account != "" {
1211 if _, ok := c.Accounts[static.HostTLSRPT.Account]; !ok {
1212 addErrorf("host tlsrpt account %q does not exist", static.HostTLSRPT.Account)
1214 checkMailboxNormf(static.HostTLSRPT.Mailbox, "host tlsrpt mailbox", addErrorf)
1216 // Localpart has been parsed already.
1218 addrFull := smtp.NewAddress(static.HostTLSRPT.ParsedLocalpart, static.HostnameDomain).String()
1219 dest := config.Destination{
1220 Mailbox: static.HostTLSRPT.Mailbox,
1221 HostTLSReports: true,
1223 accDests[addrFull] = AccountDestination{false, static.HostTLSRPT.ParsedLocalpart, static.HostTLSRPT.Account, dest}
1226 var haveSTSListener, haveWebserverListener bool
1227 for _, l := range static.Listeners {
1228 if l.MTASTSHTTPS.Enabled {
1229 haveSTSListener = true
1231 if l.WebserverHTTP.Enabled || l.WebserverHTTPS.Enabled {
1232 haveWebserverListener = true
1236 checkRoutes := func(descr string, routes []config.Route) {
1237 parseRouteDomains := func(l []string) []string {
1239 for _, e := range l {
1245 if strings.HasPrefix(e, ".") {
1249 d, err := dns.ParseDomain(e)
1251 addErrorf("%s: invalid domain %s: %v", descr, e, err)
1253 r = append(r, prefix+d.ASCII)
1258 for i := range routes {
1259 routes[i].FromDomainASCII = parseRouteDomains(routes[i].FromDomain)
1260 routes[i].ToDomainASCII = parseRouteDomains(routes[i].ToDomain)
1262 routes[i].ResolvedTransport, ok = static.Transports[routes[i].Transport]
1264 addErrorf("%s: route references undefined transport %s", descr, routes[i].Transport)
1269 checkRoutes("global routes", c.Routes)
1271 // Validate domains.
1272 c.ClientSettingDomains = map[dns.Domain]struct{}{}
1273 for d, domain := range c.Domains {
1274 addDomainErrorf := func(format string, args ...any) {
1275 addErrorf("domain %v: %s", d, fmt.Sprintf(format, args...))
1278 dnsdomain, err := dns.ParseDomain(d)
1280 addDomainErrorf("parsing domain: %s", err)
1281 } else if dnsdomain.Name() != d {
1282 addDomainErrorf("must be specified in unicode form, %s", dnsdomain.Name())
1285 domain.Domain = dnsdomain
1287 if domain.ClientSettingsDomain != "" {
1288 csd, err := dns.ParseDomain(domain.ClientSettingsDomain)
1290 addDomainErrorf("bad client settings domain %q: %s", domain.ClientSettingsDomain, err)
1292 domain.ClientSettingsDNSDomain = csd
1293 c.ClientSettingDomains[csd] = struct{}{}
1296 if domain.LocalpartCatchallSeparator != "" && len(domain.LocalpartCatchallSeparators) != 0 {
1297 addDomainErrorf("cannot have both LocalpartCatchallSeparator and LocalpartCatchallSeparators")
1299 domain.LocalpartCatchallSeparatorsEffective = domain.LocalpartCatchallSeparators
1300 if domain.LocalpartCatchallSeparator != "" {
1301 domain.LocalpartCatchallSeparatorsEffective = append(domain.LocalpartCatchallSeparatorsEffective, domain.LocalpartCatchallSeparator)
1303 sepSeen := map[string]bool{}
1304 for _, sep := range domain.LocalpartCatchallSeparatorsEffective {
1306 addDomainErrorf("duplicate localpart catchall separator %q", sep)
1311 for _, sign := range domain.DKIM.Sign {
1312 if _, ok := domain.DKIM.Selectors[sign]; !ok {
1313 addDomainErrorf("unknown selector %s for signing", sign)
1316 for name, sel := range domain.DKIM.Selectors {
1317 addSelectorErrorf := func(format string, args ...any) {
1318 addDomainErrorf("selector %s: %s", name, fmt.Sprintf(format, args...))
1321 seld, err := dns.ParseDomain(name)
1323 addSelectorErrorf("parsing selector: %s", err)
1324 } else if seld.Name() != name {
1325 addSelectorErrorf("must be specified in unicode form, %q", seld.Name())
1329 if sel.Expiration != "" {
1330 exp, err := time.ParseDuration(sel.Expiration)
1332 addSelectorErrorf("invalid expiration %q: %v", sel.Expiration, err)
1334 sel.ExpirationSeconds = int(exp / time.Second)
1338 sel.HashEffective = sel.Hash
1339 switch sel.HashEffective {
1341 sel.HashEffective = "sha256"
1343 log.Error("using sha1 with DKIM is deprecated as not secure enough, switch to sha256")
1346 addSelectorErrorf("unsupported hash %q", sel.HashEffective)
1349 pemBuf, err := os.ReadFile(configDirPath(dynamicPath, sel.PrivateKeyFile))
1351 addSelectorErrorf("reading private key: %s", err)
1354 p, _ := pem.Decode(pemBuf)
1356 addSelectorErrorf("private key has no PEM block")
1359 key, err := x509.ParsePKCS8PrivateKey(p.Bytes)
1361 addSelectorErrorf("parsing private key: %s", err)
1364 switch k := key.(type) {
1365 case *rsa.PrivateKey:
1366 if k.N.BitLen() < 1024 {
1368 // Let's help user do the right thing.
1369 addSelectorErrorf("rsa keys should be >= 1024 bits, is %d bits", k.N.BitLen())
1372 sel.Algorithm = fmt.Sprintf("rsa-%d", k.N.BitLen())
1373 case ed25519.PrivateKey:
1374 if sel.HashEffective != "sha256" {
1375 addSelectorErrorf("hash algorithm %q is not supported with ed25519, only sha256 is", sel.HashEffective)
1378 sel.Algorithm = "ed25519"
1380 addSelectorErrorf("private key type %T not yet supported", key)
1383 if len(sel.Headers) == 0 {
1387 // By default we seal signed headers, and we sign user-visible headers to
1388 // prevent/limit reuse of previously signed messages: All addressing fields, date
1389 // and subject, message-referencing fields, parsing instructions (content-type).
1390 sel.HeadersEffective = strings.Split("From,To,Cc,Bcc,Reply-To,References,In-Reply-To,Subject,Date,Message-Id,Content-Type", ",")
1393 for _, h := range sel.Headers {
1394 from = from || strings.EqualFold(h, "From")
1396 if strings.EqualFold(h, "DKIM-Signature") || strings.EqualFold(h, "Received") || strings.EqualFold(h, "Return-Path") {
1397 log.Error("DKIM-signing header %q is recommended against as it may be modified in transit")
1401 addSelectorErrorf("From-field must always be DKIM-signed")
1403 sel.HeadersEffective = sel.Headers
1406 domain.DKIM.Selectors[name] = sel
1409 if domain.MTASTS != nil {
1410 if !haveSTSListener {
1411 addDomainErrorf("MTA-STS enabled, but there is no listener for MTASTS")
1413 sts := domain.MTASTS
1414 if sts.PolicyID == "" {
1415 addDomainErrorf("invalid empty MTA-STS PolicyID")
1418 case mtasts.ModeNone, mtasts.ModeTesting, mtasts.ModeEnforce:
1420 addDomainErrorf("invalid mtasts mode %q", sts.Mode)
1424 checkRoutes("routes for domain", domain.Routes)
1426 c.Domains[d] = domain
1429 // To determine ReportsOnly.
1430 domainHasAddress := map[string]bool{}
1432 // Validate email addresses.
1433 for accName, acc := range c.Accounts {
1434 addAccountErrorf := func(format string, args ...any) {
1435 addErrorf("account %q: %s", accName, fmt.Sprintf(format, args...))
1439 acc.DNSDomain, err = dns.ParseDomain(acc.Domain)
1441 addAccountErrorf("parsing domain %s: %s", acc.Domain, err)
1444 if strings.EqualFold(acc.RejectsMailbox, "Inbox") {
1445 addAccountErrorf("cannot set RejectsMailbox to inbox, messages will be removed automatically from the rejects mailbox")
1447 if acc.Introbox != "" {
1448 mailbox, _, err := config.CheckMailboxName(acc.Introbox, false)
1450 addAccountErrorf("invalid Introbox mailbox: %v", err)
1452 acc.Introbox = mailbox
1454 if acc.Introbox == acc.RejectsMailbox {
1455 addAccountErrorf("cannot set Introbox and RejectsMailbox to the same mailbox")
1458 checkMailboxNormf(acc.RejectsMailbox, "rejects mailbox", addErrorf)
1460 if len(acc.LoginDisabled) > 256 {
1461 addAccountErrorf("message for disabled login must be <256 characters")
1463 for _, c := range acc.LoginDisabled {
1464 // For IMAP and SMTP. IMAP only allows UTF8 after "ENABLE IMAPrev2".
1465 if c < ' ' || c >= 0x7f {
1466 addAccountErrorf("message cannot contain control characters including newlines, and must be ascii-only")
1470 if acc.AutomaticJunkFlags.JunkMailboxRegexp != "" {
1471 r, err := regexp.Compile(acc.AutomaticJunkFlags.JunkMailboxRegexp)
1473 addAccountErrorf("invalid JunkMailboxRegexp regular expression: %v", err)
1477 if acc.AutomaticJunkFlags.NeutralMailboxRegexp != "" {
1478 r, err := regexp.Compile(acc.AutomaticJunkFlags.NeutralMailboxRegexp)
1480 addAccountErrorf("invalid NeutralMailboxRegexp regular expression: %v", err)
1482 acc.NeutralMailbox = r
1484 if acc.AutomaticJunkFlags.NotJunkMailboxRegexp != "" {
1485 r, err := regexp.Compile(acc.AutomaticJunkFlags.NotJunkMailboxRegexp)
1487 addAccountErrorf("invalid NotJunkMailboxRegexp regular expression: %v", err)
1489 acc.NotJunkMailbox = r
1492 if acc.JunkFilter != nil {
1493 params := acc.JunkFilter.Params
1494 if params.MaxPower < 0 || params.MaxPower > 0.5 {
1495 addAccountErrorf("junk filter MaxPower must be >= 0 and < 0.5")
1497 if params.TopWords < 0 {
1498 addAccountErrorf("junk filter TopWords must be >= 0")
1500 if params.IgnoreWords < 0 || params.IgnoreWords > 0.5 {
1501 addAccountErrorf("junk filter IgnoreWords must be >= 0 and < 0.5")
1503 if params.RareWords < 0 {
1504 addAccountErrorf("junk filter RareWords must be >= 0")
1508 acc.ParsedFromIDLoginAddresses = make([]smtp.Address, len(acc.FromIDLoginAddresses))
1509 for i, s := range acc.FromIDLoginAddresses {
1510 a, err := smtp.ParseAddress(s)
1512 addAccountErrorf("invalid fromid login address %q: %v", s, err)
1514 // We check later on if address belongs to account.
1515 dom, ok := c.Domains[a.Domain.Name()]
1517 addAccountErrorf("unknown domain in fromid login address %q", s)
1518 } else if len(dom.LocalpartCatchallSeparatorsEffective) == 0 {
1519 addAccountErrorf("localpart catchall separator not configured for domain for fromid login address %q", s)
1521 acc.ParsedFromIDLoginAddresses[i] = a
1524 // Clear any previously derived state.
1527 c.Accounts[accName] = acc
1529 if acc.OutgoingWebhook != nil {
1530 u, err := url.Parse(acc.OutgoingWebhook.URL)
1531 if err == nil && (u.Scheme != "http" && u.Scheme != "https") {
1532 err = errors.New("scheme must be http or https")
1535 addAccountErrorf("parsing outgoing hook url %q: %v", acc.OutgoingWebhook.URL, err)
1538 // note: outgoing hook events are in ../queue/hooks.go, ../mox-/config.go, ../queue.go and ../webapi/gendoc.sh. keep in sync.
1539 outgoingHookEvents := []string{"delivered", "suppressed", "delayed", "failed", "relayed", "expanded", "canceled", "unrecognized"}
1540 for _, e := range acc.OutgoingWebhook.Events {
1541 if !slices.Contains(outgoingHookEvents, e) {
1542 addAccountErrorf("unknown outgoing hook event %q", e)
1546 if acc.IncomingWebhook != nil {
1547 u, err := url.Parse(acc.IncomingWebhook.URL)
1548 if err == nil && (u.Scheme != "http" && u.Scheme != "https") {
1549 err = errors.New("scheme must be http or https")
1552 addAccountErrorf("parsing incoming hook url %q: %v", acc.IncomingWebhook.URL, err)
1556 // 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.
1557 replaceLocalparts := map[string]string{}
1559 for addrName, dest := range acc.Destinations {
1560 addDestErrorf := func(format string, args ...any) {
1561 addAccountErrorf("destination %q: %s", addrName, fmt.Sprintf(format, args...))
1564 checkMailboxNormf(dest.Mailbox, "destination mailbox", addDestErrorf)
1566 if dest.SMTPError != "" {
1567 if len(dest.SMTPError) > 256 {
1568 addDestErrorf("smtp error must be smaller than 256 bytes")
1570 for _, c := range dest.SMTPError {
1571 if c < ' ' || c >= 0x7f {
1572 addDestErrorf("smtp error cannot contain contain control characters (including newlines) or non-ascii")
1577 if dest.Mailbox != "" {
1578 addDestErrorf("cannot have both SMTPError and Mailbox")
1580 if len(dest.Rulesets) != 0 {
1581 addDestErrorf("cannot have both SMTPError and Rulesets")
1584 t := strings.SplitN(dest.SMTPError, " ", 2)
1587 addDestErrorf("smtp error must be 421 or 550 (with optional message), not %q", dest.SMTPError)
1590 dest.SMTPErrorCode = smtp.C451LocalErr
1591 dest.SMTPErrorSecode = smtp.SeSys3Other0
1592 dest.SMTPErrorMsg = "error processing"
1594 dest.SMTPErrorCode = smtp.C550MailboxUnavail
1595 dest.SMTPErrorSecode = smtp.SeAddr1UnknownDestMailbox1
1596 dest.SMTPErrorMsg = "no such user(s)"
1599 dest.SMTPErrorMsg = strings.TrimSpace(t[1])
1601 acc.Destinations[addrName] = dest
1604 if dest.MessageAuthRequiredSMTPError != "" {
1605 if len(dest.MessageAuthRequiredSMTPError) > 256 {
1606 addDestErrorf("message authentication required smtp error must be smaller than 256 bytes")
1608 for _, c := range dest.MessageAuthRequiredSMTPError {
1609 if c < ' ' || c >= 0x7f {
1610 addDestErrorf("message authentication required smtp error cannot contain contain control characters (including newlines) or non-ascii")
1616 for i, rs := range dest.Rulesets {
1617 addRulesetErrorf := func(format string, args ...any) {
1618 addDestErrorf("ruleset %d: %s", i+1, fmt.Sprintf(format, args...))
1621 checkMailboxNormf(rs.Mailbox, "ruleset mailbox", addRulesetErrorf)
1625 if rs.SMTPMailFromRegexp != "" {
1627 r, err := regexp.Compile(rs.SMTPMailFromRegexp)
1629 addRulesetErrorf("invalid SMTPMailFrom regular expression: %v", err)
1631 c.Accounts[accName].Destinations[addrName].Rulesets[i].SMTPMailFromRegexpCompiled = r
1633 if rs.MsgFromRegexp != "" {
1635 r, err := regexp.Compile(rs.MsgFromRegexp)
1637 addRulesetErrorf("invalid MsgFrom regular expression: %v", err)
1639 c.Accounts[accName].Destinations[addrName].Rulesets[i].MsgFromRegexpCompiled = r
1641 if rs.VerifiedDomain != "" {
1643 d, err := dns.ParseDomain(rs.VerifiedDomain)
1645 addRulesetErrorf("invalid VerifiedDomain: %v", err)
1647 c.Accounts[accName].Destinations[addrName].Rulesets[i].VerifiedDNSDomain = d
1650 var hdr [][2]*regexp.Regexp
1651 for k, v := range rs.HeadersRegexp {
1653 if strings.ToLower(k) != k {
1654 addRulesetErrorf("header field %q must only have lower case characters", k)
1656 if strings.ToLower(v) != v {
1657 addRulesetErrorf("header value %q must only have lower case characters", v)
1659 rk, err := regexp.Compile(k)
1661 addRulesetErrorf("invalid rule header regexp %q: %v", k, err)
1663 rv, err := regexp.Compile(v)
1665 addRulesetErrorf("invalid rule header regexp %q: %v", v, err)
1667 hdr = append(hdr, [...]*regexp.Regexp{rk, rv})
1669 c.Accounts[accName].Destinations[addrName].Rulesets[i].HeadersRegexpCompiled = hdr
1672 addRulesetErrorf("ruleset must have at least one rule")
1675 if rs.IsForward && rs.ListAllowDomain != "" {
1676 addRulesetErrorf("ruleset cannot have both IsForward and ListAllowDomain")
1679 if rs.SMTPMailFromRegexp == "" || rs.VerifiedDomain == "" {
1680 addRulesetErrorf("ruleset with IsForward must have both SMTPMailFromRegexp and VerifiedDomain too")
1683 if rs.ListAllowDomain != "" {
1684 d, err := dns.ParseDomain(rs.ListAllowDomain)
1686 addRulesetErrorf("invalid ListAllowDomain %q: %v", rs.ListAllowDomain, err)
1688 c.Accounts[accName].Destinations[addrName].Rulesets[i].ListAllowDNSDomain = d
1691 checkMailboxNormf(rs.AcceptRejectsToMailbox, "rejects mailbox", addRulesetErrorf)
1692 if strings.EqualFold(rs.AcceptRejectsToMailbox, "inbox") {
1693 addRulesetErrorf("AcceptRejectsToMailbox cannot be set to Inbox")
1697 // Catchall destination for domain.
1698 if strings.HasPrefix(addrName, "@") {
1699 d, err := dns.ParseDomain(addrName[1:])
1701 addDestErrorf("parsing domain %q", addrName[1:])
1703 } else if _, ok := c.Domains[d.Name()]; !ok {
1704 addDestErrorf("unknown domain for address")
1707 domainHasAddress[d.Name()] = true
1708 addrFull := "@" + d.Name()
1709 if _, ok := accDests[addrFull]; ok {
1710 addDestErrorf("duplicate canonicalized catchall destination address %s", addrFull)
1712 accDests[addrFull] = AccountDestination{true, "", accName, dest}
1716 // todo deprecated: remove support for parsing destination as just a localpart instead full address.
1717 var address smtp.Address
1718 if localpart, err := smtp.ParseLocalpart(addrName); err != nil && errors.Is(err, smtp.ErrBadLocalpart) {
1719 address, err = smtp.ParseAddress(addrName)
1721 addDestErrorf("invalid email address")
1723 } else if _, ok := c.Domains[address.Domain.Name()]; !ok {
1724 addDestErrorf("unknown domain for address")
1729 addDestErrorf("invalid localpart %q", addrName)
1732 address = smtp.NewAddress(localpart, acc.DNSDomain)
1733 if _, ok := c.Domains[acc.DNSDomain.Name()]; !ok {
1734 addDestErrorf("unknown domain %s", acc.DNSDomain.Name())
1737 replaceLocalparts[addrName] = address.Pack(true)
1740 origLP := address.Localpart
1741 dc := c.Domains[address.Domain.Name()]
1742 domainHasAddress[address.Domain.Name()] = true
1743 lp := CanonicalLocalpart(address.Localpart, dc)
1745 for _, sep := range dc.LocalpartCatchallSeparatorsEffective {
1746 if strings.Contains(string(address.Localpart), sep) {
1748 addDestErrorf("localpart of address %s includes domain catchall separator %s", address, sep)
1752 address.Localpart = lp
1754 addrFull := address.Pack(true)
1755 if _, ok := accDests[addrFull]; ok {
1756 addDestErrorf("duplicate canonicalized destination address %s", addrFull)
1758 accDests[addrFull] = AccountDestination{false, origLP, accName, dest}
1761 for lp, addr := range replaceLocalparts {
1762 dest, ok := acc.Destinations[lp]
1764 addAccountErrorf("could not find localpart %q to replace with address in destinations", lp)
1766 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`,
1767 slog.Any("localpart", lp),
1768 slog.Any("address", addr),
1769 slog.String("account", accName))
1770 acc.Destinations[addr] = dest
1771 delete(acc.Destinations, lp)
1775 // Now that all addresses are parsed, check if all fromid login addresses match
1776 // configured addresses.
1777 for i, a := range acc.ParsedFromIDLoginAddresses {
1778 // For domain catchall.
1779 if _, ok := accDests["@"+a.Domain.Name()]; ok {
1782 dc := c.Domains[a.Domain.Name()]
1783 a.Localpart = CanonicalLocalpart(a.Localpart, dc)
1784 if _, ok := accDests[a.Pack(true)]; !ok {
1785 addAccountErrorf("fromid login address %q does not match its destination addresses", acc.FromIDLoginAddresses[i])
1789 checkRoutes("routes for account", acc.Routes)
1792 // Set DMARC destinations.
1793 for d, domain := range c.Domains {
1794 addDomainErrorf := func(format string, args ...any) {
1795 addErrorf("domain %s: %s", d, fmt.Sprintf(format, args...))
1798 dmarc := domain.DMARC
1802 if _, ok := c.Accounts[dmarc.Account]; !ok {
1803 addDomainErrorf("DMARC account %q does not exist", dmarc.Account)
1806 // Note: For backwards compabilitiy, DMARC reporting localparts can contain catchall separators.
1807 lp, err := smtp.ParseLocalpart(dmarc.Localpart)
1809 addDomainErrorf("invalid DMARC localpart %q: %s", dmarc.Localpart, err)
1811 if lp.IsInternational() {
1813 addDomainErrorf("DMARC localpart %q is an internationalized address, only conventional ascii-only address possible for interopability", lp)
1815 addrdom := domain.Domain
1816 if dmarc.Domain != "" {
1817 addrdom, err = dns.ParseDomain(dmarc.Domain)
1819 addDomainErrorf("DMARC domain %q: %s", dmarc.Domain, err)
1820 } else if adomain, ok := c.Domains[addrdom.Name()]; !ok {
1821 addDomainErrorf("unknown domain %q for DMARC address", addrdom)
1822 } else if !adomain.LocalpartCaseSensitive {
1823 lp = smtp.Localpart(strings.ToLower(string(lp)))
1825 } else if !domain.LocalpartCaseSensitive {
1826 lp = smtp.Localpart(strings.ToLower(string(lp)))
1828 if addrdom == domain.Domain {
1829 domainHasAddress[addrdom.Name()] = true
1832 domain.DMARC.ParsedLocalpart = lp
1833 domain.DMARC.DNSDomain = addrdom
1834 c.Domains[d] = domain
1835 addrFull := smtp.NewAddress(lp, addrdom).String()
1836 dest := config.Destination{
1837 Mailbox: dmarc.Mailbox,
1840 checkMailboxNormf(dmarc.Mailbox, "DMARC mailbox for account", addDomainErrorf)
1841 accDests[addrFull] = AccountDestination{false, lp, dmarc.Account, dest}
1844 // Set TLSRPT destinations.
1845 for d, domain := range c.Domains {
1846 addDomainErrorf := func(format string, args ...any) {
1847 addErrorf("domain %s: %s", d, fmt.Sprintf(format, args...))
1850 tlsrpt := domain.TLSRPT
1854 if _, ok := c.Accounts[tlsrpt.Account]; !ok {
1855 addDomainErrorf("TLSRPT account %q does not exist", tlsrpt.Account)
1858 // Note: For backwards compabilitiy, TLS reporting localparts can contain catchall separators.
1859 lp, err := smtp.ParseLocalpart(tlsrpt.Localpart)
1861 addDomainErrorf("invalid TLSRPT localpart %q: %s", tlsrpt.Localpart, err)
1863 if lp.IsInternational() {
1864 // Does not appear documented in
../rfc/8460, but similar to DMARC it makes sense
1865 // to keep this ascii-only addresses.
1866 addDomainErrorf("TLSRPT localpart %q is an internationalized address, only conventional ascii-only address allowed for interopability", lp)
1868 addrdom := domain.Domain
1869 if tlsrpt.Domain != "" {
1870 addrdom, err = dns.ParseDomain(tlsrpt.Domain)
1872 addDomainErrorf("TLSRPT domain %q: %s", tlsrpt.Domain, err)
1873 } else if adomain, ok := c.Domains[addrdom.Name()]; !ok {
1874 addDomainErrorf("unknown domain %q for TLSRPT address", tlsrpt.Domain)
1875 } else if !adomain.LocalpartCaseSensitive {
1876 lp = smtp.Localpart(strings.ToLower(string(lp)))
1878 } else if !domain.LocalpartCaseSensitive {
1879 lp = smtp.Localpart(strings.ToLower(string(lp)))
1881 if addrdom == domain.Domain {
1882 domainHasAddress[addrdom.Name()] = true
1885 domain.TLSRPT.ParsedLocalpart = lp
1886 domain.TLSRPT.DNSDomain = addrdom
1887 c.Domains[d] = domain
1888 addrFull := smtp.NewAddress(lp, addrdom).String()
1889 dest := config.Destination{
1890 Mailbox: tlsrpt.Mailbox,
1891 DomainTLSReports: true,
1893 checkMailboxNormf(tlsrpt.Mailbox, "TLSRPT mailbox", addDomainErrorf)
1894 accDests[addrFull] = AccountDestination{false, lp, tlsrpt.Account, dest}
1897 // Set ReportsOnly for domains, based on whether we have seen addresses (possibly
1898 // from DMARC or TLS reporting).
1899 for d, domain := range c.Domains {
1900 domain.ReportsOnly = !domainHasAddress[domain.Domain.Name()]
1901 c.Domains[d] = domain
1904 // Aliases, per domain. Also add references to accounts.
1905 for d, domain := range c.Domains {
1906 for lpstr, a := range domain.Aliases {
1907 addAliasErrorf := func(format string, args ...any) {
1908 addErrorf("domain %s: alias %s: %s", d, lpstr, fmt.Sprintf(format, args...))
1912 a.LocalpartStr = lpstr
1913 var clp smtp.Localpart
1914 lp, err := smtp.ParseLocalpart(lpstr)
1916 addAliasErrorf("parsing alias: %v", err)
1920 for _, sep := range domain.LocalpartCatchallSeparatorsEffective {
1921 if strings.Contains(string(lp), sep) {
1922 addAliasErrorf("alias contains localpart catchall separator")
1929 clp = CanonicalLocalpart(lp, domain)
1932 addr := smtp.NewAddress(clp, domain.Domain).Pack(true)
1933 if _, ok := aliases[addr]; ok {
1934 addAliasErrorf("duplicate alias address %q", addr)
1937 if _, ok := accDests[addr]; ok {
1938 addAliasErrorf("alias %q already present as regular address", addr)
1941 if len(a.Addresses) == 0 {
1942 // Not currently possible, Addresses isn't optional.
1943 addAliasErrorf("alias %q needs at least one destination address", addr)
1946 a.ParsedAddresses = make([]config.AliasAddress, 0, len(a.Addresses))
1947 seen := map[string]bool{}
1948 for _, destAddr := range a.Addresses {
1949 da, err := smtp.ParseAddress(destAddr)
1951 addAliasErrorf("parsing destination address %q: %v", destAddr, err)
1954 dastr := da.Pack(true)
1955 accDest, ok := accDests[dastr]
1957 addAliasErrorf("references non-existent address %q", destAddr)
1961 addAliasErrorf("duplicate address %q", destAddr)
1965 aa := config.AliasAddress{Address: da, AccountName: accDest.Account, Destination: accDest.Destination}
1966 a.ParsedAddresses = append(a.ParsedAddresses, aa)
1968 a.Domain = domain.Domain
1969 c.Domains[d].Aliases[lpstr] = a
1972 for _, aa := range a.ParsedAddresses {
1973 acc := c.Accounts[aa.AccountName]
1976 addrs = make([]string, len(a.ParsedAddresses))
1977 for i := range a.ParsedAddresses {
1978 addrs[i] = a.ParsedAddresses[i].Address.Pack(true)
1981 // Keep the non-sensitive fields.
1982 accAlias := config.Alias{
1983 PostPublic: a.PostPublic,
1984 ListMembers: a.ListMembers,
1985 AllowMsgFrom: a.AllowMsgFrom,
1986 LocalpartStr: a.LocalpartStr,
1989 acc.Aliases = append(acc.Aliases, config.AddressAlias{SubscriptionAddress: aa.Address.Pack(true), Alias: accAlias, MemberAddresses: addrs})
1990 c.Accounts[aa.AccountName] = acc
1995 // Check webserver configs.
1996 if (len(c.WebDomainRedirects) > 0 || len(c.WebHandlers) > 0) && !haveWebserverListener {
1997 addErrorf("WebDomainRedirects or WebHandlers configured but no listener with WebserverHTTP or WebserverHTTPS enabled")
2000 c.WebDNSDomainRedirects = map[dns.Domain]dns.Domain{}
2001 for from, to := range c.WebDomainRedirects {
2002 addRedirectErrorf := func(format string, args ...any) {
2003 addErrorf("web redirect %s to %s: %s", from, to, fmt.Sprintf(format, args...))
2006 fromdom, err := dns.ParseDomain(from)
2008 addRedirectErrorf("parsing domain for redirect %s: %v", from, err)
2010 todom, err := dns.ParseDomain(to)
2012 addRedirectErrorf("parsing domain for redirect %s: %v", to, err)
2013 } else if fromdom == todom {
2014 addRedirectErrorf("will not redirect domain %s to itself", todom)
2016 var zerodom dns.Domain
2017 if _, ok := c.WebDNSDomainRedirects[fromdom]; ok && fromdom != zerodom {
2018 addRedirectErrorf("duplicate redirect domain %s", from)
2020 c.WebDNSDomainRedirects[fromdom] = todom
2023 for i := range c.WebHandlers {
2024 wh := &c.WebHandlers[i]
2026 addHandlerErrorf := func(format string, args ...any) {
2027 addErrorf("webhandler %s %s: %s", wh.Domain, wh.PathRegexp, fmt.Sprintf(format, args...))
2030 if wh.LogName == "" {
2031 wh.Name = fmt.Sprintf("%d", i)
2033 wh.Name = wh.LogName
2036 dom, err := dns.ParseDomain(wh.Domain)
2038 addHandlerErrorf("parsing domain: %v", err)
2042 if !strings.HasPrefix(wh.PathRegexp, "^") {
2043 addHandlerErrorf("path regexp must start with a ^")
2045 re, err := regexp.Compile(wh.PathRegexp)
2047 addHandlerErrorf("compiling regexp: %v", err)
2052 if wh.WebStatic != nil {
2055 if ws.StripPrefix != "" && !strings.HasPrefix(ws.StripPrefix, "/") {
2056 addHandlerErrorf("static: prefix to strip %s must start with a slash", ws.StripPrefix)
2058 for k := range ws.ResponseHeaders {
2060 k := strings.TrimSpace(xk)
2061 if k != xk || k == "" {
2062 addHandlerErrorf("static: bad header %q", xk)
2066 if wh.WebRedirect != nil {
2068 wr := wh.WebRedirect
2069 if wr.BaseURL != "" {
2070 u, err := url.Parse(wr.BaseURL)
2072 addHandlerErrorf("redirect: parsing redirect url %s: %v", wr.BaseURL, err)
2078 addHandlerErrorf("redirect: BaseURL must have empty path: %s", wr.BaseURL)
2082 if wr.OrigPathRegexp != "" && wr.ReplacePath != "" {
2083 re, err := regexp.Compile(wr.OrigPathRegexp)
2085 addHandlerErrorf("compiling regexp %s: %v", wr.OrigPathRegexp, err)
2088 } else if wr.OrigPathRegexp != "" || wr.ReplacePath != "" {
2089 addHandlerErrorf("redirect: must have either both OrigPathRegexp and ReplacePath, or neither")
2090 } else if wr.BaseURL == "" {
2091 addHandlerErrorf("must at least one of BaseURL and OrigPathRegexp+ReplacePath")
2093 if wr.StatusCode != 0 && (wr.StatusCode < 300 || wr.StatusCode >= 400) {
2094 addHandlerErrorf("redirect: invalid redirect status code %d", wr.StatusCode)
2097 if wh.WebForward != nil {
2100 u, err := url.Parse(wf.URL)
2102 addHandlerErrorf("forward: parsing url %s: %v", wf.URL, err)
2106 for k := range wf.ResponseHeaders {
2108 k := strings.TrimSpace(xk)
2109 if k != xk || k == "" {
2110 addHandlerErrorf("forrward: bad header %q", xk)
2114 if wh.WebInternal != nil {
2116 wi := wh.WebInternal
2117 if !strings.HasPrefix(wi.BasePath, "/") || !strings.HasSuffix(wi.BasePath, "/") {
2118 addHandlerErrorf("internal service: base path %q must start and end with /", wi.BasePath)
2120 // todo: we could make maxMsgSize and accountPath configurable
2121 const isForwarded = false
2124 wi.Handler = NewWebadminHandler(wi.BasePath, isForwarded)
2126 wi.Handler = NewWebaccountHandler(wi.BasePath, isForwarded)
2129 wi.Handler = NewWebmailHandler(config.DefaultMaxMsgSize, wi.BasePath, isForwarded, accountPath)
2131 wi.Handler = NewWebapiHandler(config.DefaultMaxMsgSize, wi.BasePath, isForwarded)
2133 addHandlerErrorf("internal service: unknown service %q", wi.Service)
2135 wi.Handler = SafeHeaders(http.StripPrefix(wi.BasePath[:len(wi.BasePath)-1], wi.Handler))
2138 addHandlerErrorf("must have exactly one handler, not %d", n)
2142 c.MonitorDNSBLZones = nil
2143 for _, s := range c.MonitorDNSBLs {
2144 d, err := dns.ParseDomain(s)
2146 addErrorf("dnsbl %s: parsing dnsbl zone: %v", s, err)
2149 if slices.Contains(c.MonitorDNSBLZones, d) {
2150 addErrorf("dnsbl %s: duplicate zone", s)
2153 c.MonitorDNSBLZones = append(c.MonitorDNSBLZones, d)
2159func loadPrivateKeyFile(keyPath string) (crypto.Signer, error) {
2160 keyBuf, err := os.ReadFile(keyPath)
2162 return nil, fmt.Errorf("reading host private key: %v", err)
2164 b, _ := pem.Decode(keyBuf)
2166 return nil, fmt.Errorf("parsing pem block for private key: %v", err)
2171 privKey, err = x509.ParsePKCS8PrivateKey(b.Bytes)
2172 case "RSA PRIVATE KEY":
2173 privKey, err = x509.ParsePKCS1PrivateKey(b.Bytes)
2174 case "EC PRIVATE KEY":
2175 privKey, err = x509.ParseECPrivateKey(b.Bytes)
2177 err = fmt.Errorf("unknown pem type %q", b.Type)
2180 return nil, fmt.Errorf("parsing private key: %v", err)
2182 if k, ok := privKey.(crypto.Signer); ok {
2185 return nil, fmt.Errorf("parsed private key not a crypto.Signer, but %T", privKey)
2188func loadTLSKeyCerts(configFile, kind string, ctls *config.TLS) error {
2189 certs := []tls.Certificate{}
2190 for _, kp := range ctls.KeyCerts {
2191 certPath := configDirPath(configFile, kp.CertFile)
2192 keyPath := configDirPath(configFile, kp.KeyFile)
2193 cert, err := loadX509KeyPairPrivileged(certPath, keyPath)
2195 return fmt.Errorf("tls config for %q: parsing x509 key pair: %v", kind, err)
2197 certs = append(certs, cert)
2199 ctls.Config = &tls.Config{
2200 Certificates: certs,
2202 ctls.ConfigFallback = ctls.Config
2206// load x509 key/cert files from file descriptor possibly passed in by privileged
2208func loadX509KeyPairPrivileged(certPath, keyPath string) (tls.Certificate, error) {
2209 certBuf, err := readFilePrivileged(certPath)
2211 return tls.Certificate{}, fmt.Errorf("reading tls certificate: %v", err)
2213 keyBuf, err := readFilePrivileged(keyPath)
2215 return tls.Certificate{}, fmt.Errorf("reading tls key: %v", err)
2217 return tls.X509KeyPair(certBuf, keyBuf)
2220// like os.ReadFile, but open privileged file possibly passed in by root process.
2221func readFilePrivileged(path string) ([]byte, error) {
2222 f, err := OpenPrivileged(path)
2227 return io.ReadAll(f)