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":
1023 addTransportErrorf("unknown authentication mechanism %s", m)
1027 t.Auth.EffectiveMechanisms = t.Auth.Mechanisms
1028 if len(t.Auth.EffectiveMechanisms) == 0 {
1029 t.Auth.EffectiveMechanisms = []string{"SCRAM-SHA-256-PLUS", "SCRAM-SHA-256", "SCRAM-SHA-1-PLUS", "SCRAM-SHA-1", "CRAM-MD5"}
1033 checkTransportSocks := func(name string, t *config.TransportSocks) {
1034 addTransportErrorf := func(format string, args ...any) {
1035 addErrorf("transport %s: %s", name, fmt.Sprintf(format, args...))
1038 _, _, err := net.SplitHostPort(t.Address)
1040 addTransportErrorf("bad address %s: %v", t.Address, err)
1042 for _, ipstr := range t.RemoteIPs {
1043 ip := net.ParseIP(ipstr)
1045 addTransportErrorf("bad ip %s", ipstr)
1047 t.IPs = append(t.IPs, ip)
1050 t.Hostname, err = dns.ParseDomain(t.RemoteHostname)
1052 addTransportErrorf("bad hostname %s: %v", t.RemoteHostname, err)
1056 checkTransportDirect := func(name string, t *config.TransportDirect) {
1057 addTransportErrorf := func(format string, args ...any) {
1058 addErrorf("transport %s: %s", name, fmt.Sprintf(format, args...))
1061 if t.DisableIPv4 && t.DisableIPv6 {
1062 addTransportErrorf("both IPv4 and IPv6 are disabled, enable at least one")
1073 checkTransportFail := func(name string, t *config.TransportFail) {
1074 addTransportErrorf := func(format string, args ...any) {
1075 addErrorf("transport %s: %s", name, fmt.Sprintf(format, args...))
1078 if t.SMTPCode == 0 {
1079 t.Code = smtp.C554TransactionFailed
1080 } else if t.SMTPCode/100 != 4 && t.SMTPCode/100 != 5 {
1081 addTransportErrorf("smtp code %d must be 4xx or 5xx", t.SMTPCode/100)
1086 if len(t.SMTPMessage) > 256 {
1087 addTransportErrorf("message must be <= 256 characters")
1089 for _, c := range t.SMTPMessage {
1090 if c < ' ' || c >= 0x7f {
1091 addTransportErrorf("message cannot contain control characters including newlines, and must be ascii-only")
1094 t.Message = t.SMTPMessage
1095 if t.Message == "" {
1096 t.Message = "transport fail: explicit immediate delivery failure per configuration"
1100 for name, t := range c.Transports {
1101 addTransportErrorf := func(format string, args ...any) {
1102 addErrorf("transport %s: %s", name, fmt.Sprintf(format, args...))
1106 if t.Submissions != nil {
1108 checkTransportSMTP(name, true, t.Submissions)
1110 if t.Submission != nil {
1112 checkTransportSMTP(name, false, t.Submission)
1116 checkTransportSMTP(name, false, t.SMTP)
1120 checkTransportSocks(name, t.Socks)
1122 if t.Direct != nil {
1124 checkTransportDirect(name, t.Direct)
1128 checkTransportFail(name, t.Fail)
1131 addTransportErrorf("cannot have multiple methods in a transport")
1135 // Load CA certificate pool.
1136 if c.TLS.CA != nil {
1137 if c.TLS.CA.AdditionalToSystem {
1139 c.TLS.CertPool, err = x509.SystemCertPool()
1141 addErrorf("fetching system CA cert pool: %v", err)
1144 c.TLS.CertPool = x509.NewCertPool()
1146 for _, certfile := range c.TLS.CA.CertFiles {
1147 p := configDirPath(configFile, certfile)
1148 pemBuf, err := os.ReadFile(p)
1150 addErrorf("reading TLS CA cert file: %v", err)
1152 } else if !c.TLS.CertPool.AppendCertsFromPEM(pemBuf) {
1153 // todo: can we check more fully if we're getting some useful data back?
1154 addErrorf("no CA certs added from %q", p)
1161// PrepareDynamicConfig parses the dynamic config file given a static file.
1162func 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) {
1163 addErrorf := func(format string, args ...any) {
1164 errs = append(errs, fmt.Errorf(format, args...))
1167 f, err := os.Open(dynamicPath)
1169 addErrorf("parsing domains config: %v", err)
1175 addErrorf("stat domains config: %v", err)
1177 if err := sconf.Parse(f, &c); err != nil {
1178 addErrorf("parsing dynamic config file: %v", err)
1182 accDests, aliases, errs = prepareDynamicConfig(ctx, log, dynamicPath, static, &c)
1183 return c, fi.ModTime(), accDests, aliases, errs
1186func 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) {
1187 addErrorf := func(format string, args ...any) {
1188 errs = append(errs, fmt.Errorf(format, args...))
1191 // Check that mailbox is in unicode NFC normalized form.
1192 checkMailboxNormf := func(mailbox string, what string, errorf func(format string, args ...any)) {
1193 s := norm.NFC.String(mailbox)
1195 errorf("%s: mailbox %q is not in NFC normalized form, should be %q", what, mailbox, s)
1199 // Validate postmaster account exists.
1200 if _, ok := c.Accounts[static.Postmaster.Account]; !ok {
1201 addErrorf("postmaster account %q does not exist", static.Postmaster.Account)
1203 checkMailboxNormf(static.Postmaster.Mailbox, "postmaster mailbox", addErrorf)
1205 accDests = map[string]AccountDestination{}
1206 aliases = map[string]config.Alias{}
1208 // Validate host TLSRPT account/address.
1209 if static.HostTLSRPT.Account != "" {
1210 if _, ok := c.Accounts[static.HostTLSRPT.Account]; !ok {
1211 addErrorf("host tlsrpt account %q does not exist", static.HostTLSRPT.Account)
1213 checkMailboxNormf(static.HostTLSRPT.Mailbox, "host tlsrpt mailbox", addErrorf)
1215 // Localpart has been parsed already.
1217 addrFull := smtp.NewAddress(static.HostTLSRPT.ParsedLocalpart, static.HostnameDomain).String()
1218 dest := config.Destination{
1219 Mailbox: static.HostTLSRPT.Mailbox,
1220 HostTLSReports: true,
1222 accDests[addrFull] = AccountDestination{false, static.HostTLSRPT.ParsedLocalpart, static.HostTLSRPT.Account, dest}
1225 var haveSTSListener, haveWebserverListener bool
1226 for _, l := range static.Listeners {
1227 if l.MTASTSHTTPS.Enabled {
1228 haveSTSListener = true
1230 if l.WebserverHTTP.Enabled || l.WebserverHTTPS.Enabled {
1231 haveWebserverListener = true
1235 checkRoutes := func(descr string, routes []config.Route) {
1236 parseRouteDomains := func(l []string) []string {
1238 for _, e := range l {
1244 if strings.HasPrefix(e, ".") {
1248 d, err := dns.ParseDomain(e)
1250 addErrorf("%s: invalid domain %s: %v", descr, e, err)
1252 r = append(r, prefix+d.ASCII)
1257 for i := range routes {
1258 routes[i].FromDomainASCII = parseRouteDomains(routes[i].FromDomain)
1259 routes[i].ToDomainASCII = parseRouteDomains(routes[i].ToDomain)
1261 routes[i].ResolvedTransport, ok = static.Transports[routes[i].Transport]
1263 addErrorf("%s: route references undefined transport %s", descr, routes[i].Transport)
1268 checkRoutes("global routes", c.Routes)
1270 // Validate domains.
1271 c.ClientSettingDomains = map[dns.Domain]struct{}{}
1272 for d, domain := range c.Domains {
1273 addDomainErrorf := func(format string, args ...any) {
1274 addErrorf("domain %v: %s", d, fmt.Sprintf(format, args...))
1277 dnsdomain, err := dns.ParseDomain(d)
1279 addDomainErrorf("parsing domain: %s", err)
1280 } else if dnsdomain.Name() != d {
1281 addDomainErrorf("must be specified in unicode form, %s", dnsdomain.Name())
1284 domain.Domain = dnsdomain
1286 if domain.ClientSettingsDomain != "" {
1287 csd, err := dns.ParseDomain(domain.ClientSettingsDomain)
1289 addDomainErrorf("bad client settings domain %q: %s", domain.ClientSettingsDomain, err)
1291 domain.ClientSettingsDNSDomain = csd
1292 c.ClientSettingDomains[csd] = struct{}{}
1295 if domain.LocalpartCatchallSeparator != "" && len(domain.LocalpartCatchallSeparators) != 0 {
1296 addDomainErrorf("cannot have both LocalpartCatchallSeparator and LocalpartCatchallSeparators")
1298 domain.LocalpartCatchallSeparatorsEffective = domain.LocalpartCatchallSeparators
1299 if domain.LocalpartCatchallSeparator != "" {
1300 domain.LocalpartCatchallSeparatorsEffective = append(domain.LocalpartCatchallSeparatorsEffective, domain.LocalpartCatchallSeparator)
1302 sepSeen := map[string]bool{}
1303 for _, sep := range domain.LocalpartCatchallSeparatorsEffective {
1305 addDomainErrorf("duplicate localpart catchall separator %q", sep)
1310 for _, sign := range domain.DKIM.Sign {
1311 if _, ok := domain.DKIM.Selectors[sign]; !ok {
1312 addDomainErrorf("unknown selector %s for signing", sign)
1315 for name, sel := range domain.DKIM.Selectors {
1316 addSelectorErrorf := func(format string, args ...any) {
1317 addDomainErrorf("selector %s: %s", name, fmt.Sprintf(format, args...))
1320 seld, err := dns.ParseDomain(name)
1322 addSelectorErrorf("parsing selector: %s", err)
1323 } else if seld.Name() != name {
1324 addSelectorErrorf("must be specified in unicode form, %q", seld.Name())
1328 if sel.Expiration != "" {
1329 exp, err := time.ParseDuration(sel.Expiration)
1331 addSelectorErrorf("invalid expiration %q: %v", sel.Expiration, err)
1333 sel.ExpirationSeconds = int(exp / time.Second)
1337 sel.HashEffective = sel.Hash
1338 switch sel.HashEffective {
1340 sel.HashEffective = "sha256"
1342 log.Error("using sha1 with DKIM is deprecated as not secure enough, switch to sha256")
1345 addSelectorErrorf("unsupported hash %q", sel.HashEffective)
1348 pemBuf, err := os.ReadFile(configDirPath(dynamicPath, sel.PrivateKeyFile))
1350 addSelectorErrorf("reading private key: %s", err)
1353 p, _ := pem.Decode(pemBuf)
1355 addSelectorErrorf("private key has no PEM block")
1358 key, err := x509.ParsePKCS8PrivateKey(p.Bytes)
1360 addSelectorErrorf("parsing private key: %s", err)
1363 switch k := key.(type) {
1364 case *rsa.PrivateKey:
1365 if k.N.BitLen() < 1024 {
1367 // Let's help user do the right thing.
1368 addSelectorErrorf("rsa keys should be >= 1024 bits, is %d bits", k.N.BitLen())
1371 sel.Algorithm = fmt.Sprintf("rsa-%d", k.N.BitLen())
1372 case ed25519.PrivateKey:
1373 if sel.HashEffective != "sha256" {
1374 addSelectorErrorf("hash algorithm %q is not supported with ed25519, only sha256 is", sel.HashEffective)
1377 sel.Algorithm = "ed25519"
1379 addSelectorErrorf("private key type %T not yet supported", key)
1382 if len(sel.Headers) == 0 {
1386 // By default we seal signed headers, and we sign user-visible headers to
1387 // prevent/limit reuse of previously signed messages: All addressing fields, date
1388 // and subject, message-referencing fields, parsing instructions (content-type).
1389 sel.HeadersEffective = strings.Split("From,To,Cc,Bcc,Reply-To,References,In-Reply-To,Subject,Date,Message-Id,Content-Type", ",")
1392 for _, h := range sel.Headers {
1393 from = from || strings.EqualFold(h, "From")
1395 if strings.EqualFold(h, "DKIM-Signature") || strings.EqualFold(h, "Received") || strings.EqualFold(h, "Return-Path") {
1396 log.Error("DKIM-signing header %q is recommended against as it may be modified in transit")
1400 addSelectorErrorf("From-field must always be DKIM-signed")
1402 sel.HeadersEffective = sel.Headers
1405 domain.DKIM.Selectors[name] = sel
1408 if domain.MTASTS != nil {
1409 if !haveSTSListener {
1410 addDomainErrorf("MTA-STS enabled, but there is no listener for MTASTS")
1412 sts := domain.MTASTS
1413 if sts.PolicyID == "" {
1414 addDomainErrorf("invalid empty MTA-STS PolicyID")
1417 case mtasts.ModeNone, mtasts.ModeTesting, mtasts.ModeEnforce:
1419 addDomainErrorf("invalid mtasts mode %q", sts.Mode)
1423 checkRoutes("routes for domain", domain.Routes)
1425 c.Domains[d] = domain
1428 // To determine ReportsOnly.
1429 domainHasAddress := map[string]bool{}
1431 // Validate email addresses.
1432 for accName, acc := range c.Accounts {
1433 addAccountErrorf := func(format string, args ...any) {
1434 addErrorf("account %q: %s", accName, fmt.Sprintf(format, args...))
1438 acc.DNSDomain, err = dns.ParseDomain(acc.Domain)
1440 addAccountErrorf("parsing domain %s: %s", acc.Domain, err)
1443 if strings.EqualFold(acc.RejectsMailbox, "Inbox") {
1444 addAccountErrorf("cannot set RejectsMailbox to inbox, messages will be removed automatically from the rejects mailbox")
1446 if acc.Introbox != "" {
1447 mailbox, _, err := config.CheckMailboxName(acc.Introbox, false)
1449 addAccountErrorf("invalid Introbox mailbox: %v", err)
1451 acc.Introbox = mailbox
1453 if acc.Introbox == acc.RejectsMailbox {
1454 addAccountErrorf("cannot set Introbox and RejectsMailbox to the same mailbox")
1457 checkMailboxNormf(acc.RejectsMailbox, "rejects mailbox", addErrorf)
1459 if len(acc.LoginDisabled) > 256 {
1460 addAccountErrorf("message for disabled login must be <256 characters")
1462 for _, c := range acc.LoginDisabled {
1463 // For IMAP and SMTP. IMAP only allows UTF8 after "ENABLE IMAPrev2".
1464 if c < ' ' || c >= 0x7f {
1465 addAccountErrorf("message cannot contain control characters including newlines, and must be ascii-only")
1469 if acc.AutomaticJunkFlags.JunkMailboxRegexp != "" {
1470 r, err := regexp.Compile(acc.AutomaticJunkFlags.JunkMailboxRegexp)
1472 addAccountErrorf("invalid JunkMailboxRegexp regular expression: %v", err)
1476 if acc.AutomaticJunkFlags.NeutralMailboxRegexp != "" {
1477 r, err := regexp.Compile(acc.AutomaticJunkFlags.NeutralMailboxRegexp)
1479 addAccountErrorf("invalid NeutralMailboxRegexp regular expression: %v", err)
1481 acc.NeutralMailbox = r
1483 if acc.AutomaticJunkFlags.NotJunkMailboxRegexp != "" {
1484 r, err := regexp.Compile(acc.AutomaticJunkFlags.NotJunkMailboxRegexp)
1486 addAccountErrorf("invalid NotJunkMailboxRegexp regular expression: %v", err)
1488 acc.NotJunkMailbox = r
1491 if acc.JunkFilter != nil {
1492 params := acc.JunkFilter.Params
1493 if params.MaxPower < 0 || params.MaxPower > 0.5 {
1494 addAccountErrorf("junk filter MaxPower must be >= 0 and < 0.5")
1496 if params.TopWords < 0 {
1497 addAccountErrorf("junk filter TopWords must be >= 0")
1499 if params.IgnoreWords < 0 || params.IgnoreWords > 0.5 {
1500 addAccountErrorf("junk filter IgnoreWords must be >= 0 and < 0.5")
1502 if params.RareWords < 0 {
1503 addAccountErrorf("junk filter RareWords must be >= 0")
1507 acc.ParsedFromIDLoginAddresses = make([]smtp.Address, len(acc.FromIDLoginAddresses))
1508 for i, s := range acc.FromIDLoginAddresses {
1509 a, err := smtp.ParseAddress(s)
1511 addAccountErrorf("invalid fromid login address %q: %v", s, err)
1513 // We check later on if address belongs to account.
1514 dom, ok := c.Domains[a.Domain.Name()]
1516 addAccountErrorf("unknown domain in fromid login address %q", s)
1517 } else if len(dom.LocalpartCatchallSeparatorsEffective) == 0 {
1518 addAccountErrorf("localpart catchall separator not configured for domain for fromid login address %q", s)
1520 acc.ParsedFromIDLoginAddresses[i] = a
1523 // Clear any previously derived state.
1526 c.Accounts[accName] = acc
1528 if acc.OutgoingWebhook != nil {
1529 u, err := url.Parse(acc.OutgoingWebhook.URL)
1530 if err == nil && (u.Scheme != "http" && u.Scheme != "https") {
1531 err = errors.New("scheme must be http or https")
1534 addAccountErrorf("parsing outgoing hook url %q: %v", acc.OutgoingWebhook.URL, err)
1537 // note: outgoing hook events are in ../queue/hooks.go, ../mox-/config.go, ../queue.go and ../webapi/gendoc.sh. keep in sync.
1538 outgoingHookEvents := []string{"delivered", "suppressed", "delayed", "failed", "relayed", "expanded", "canceled", "unrecognized"}
1539 for _, e := range acc.OutgoingWebhook.Events {
1540 if !slices.Contains(outgoingHookEvents, e) {
1541 addAccountErrorf("unknown outgoing hook event %q", e)
1545 if acc.IncomingWebhook != nil {
1546 u, err := url.Parse(acc.IncomingWebhook.URL)
1547 if err == nil && (u.Scheme != "http" && u.Scheme != "https") {
1548 err = errors.New("scheme must be http or https")
1551 addAccountErrorf("parsing incoming hook url %q: %v", acc.IncomingWebhook.URL, err)
1555 // 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.
1556 replaceLocalparts := map[string]string{}
1558 for addrName, dest := range acc.Destinations {
1559 addDestErrorf := func(format string, args ...any) {
1560 addAccountErrorf("destination %q: %s", addrName, fmt.Sprintf(format, args...))
1563 checkMailboxNormf(dest.Mailbox, "destination mailbox", addDestErrorf)
1565 if dest.SMTPError != "" {
1566 if len(dest.SMTPError) > 256 {
1567 addDestErrorf("smtp error must be smaller than 256 bytes")
1569 for _, c := range dest.SMTPError {
1570 if c < ' ' || c >= 0x7f {
1571 addDestErrorf("smtp error cannot contain contain control characters (including newlines) or non-ascii")
1576 if dest.Mailbox != "" {
1577 addDestErrorf("cannot have both SMTPError and Mailbox")
1579 if len(dest.Rulesets) != 0 {
1580 addDestErrorf("cannot have both SMTPError and Rulesets")
1583 t := strings.SplitN(dest.SMTPError, " ", 2)
1586 addDestErrorf("smtp error must be 421 or 550 (with optional message), not %q", dest.SMTPError)
1589 dest.SMTPErrorCode = smtp.C451LocalErr
1590 dest.SMTPErrorSecode = smtp.SeSys3Other0
1591 dest.SMTPErrorMsg = "error processing"
1593 dest.SMTPErrorCode = smtp.C550MailboxUnavail
1594 dest.SMTPErrorSecode = smtp.SeAddr1UnknownDestMailbox1
1595 dest.SMTPErrorMsg = "no such user(s)"
1598 dest.SMTPErrorMsg = strings.TrimSpace(t[1])
1600 acc.Destinations[addrName] = dest
1603 if dest.MessageAuthRequiredSMTPError != "" {
1604 if len(dest.MessageAuthRequiredSMTPError) > 256 {
1605 addDestErrorf("message authentication required smtp error must be smaller than 256 bytes")
1607 for _, c := range dest.MessageAuthRequiredSMTPError {
1608 if c < ' ' || c >= 0x7f {
1609 addDestErrorf("message authentication required smtp error cannot contain contain control characters (including newlines) or non-ascii")
1615 for i, rs := range dest.Rulesets {
1616 addRulesetErrorf := func(format string, args ...any) {
1617 addDestErrorf("ruleset %d: %s", i+1, fmt.Sprintf(format, args...))
1620 checkMailboxNormf(rs.Mailbox, "ruleset mailbox", addRulesetErrorf)
1624 if rs.SMTPMailFromRegexp != "" {
1626 r, err := regexp.Compile(rs.SMTPMailFromRegexp)
1628 addRulesetErrorf("invalid SMTPMailFrom regular expression: %v", err)
1630 c.Accounts[accName].Destinations[addrName].Rulesets[i].SMTPMailFromRegexpCompiled = r
1632 if rs.MsgFromRegexp != "" {
1634 r, err := regexp.Compile(rs.MsgFromRegexp)
1636 addRulesetErrorf("invalid MsgFrom regular expression: %v", err)
1638 c.Accounts[accName].Destinations[addrName].Rulesets[i].MsgFromRegexpCompiled = r
1640 if rs.VerifiedDomain != "" {
1642 d, err := dns.ParseDomain(rs.VerifiedDomain)
1644 addRulesetErrorf("invalid VerifiedDomain: %v", err)
1646 c.Accounts[accName].Destinations[addrName].Rulesets[i].VerifiedDNSDomain = d
1649 var hdr [][2]*regexp.Regexp
1650 for k, v := range rs.HeadersRegexp {
1652 if strings.ToLower(k) != k {
1653 addRulesetErrorf("header field %q must only have lower case characters", k)
1655 if strings.ToLower(v) != v {
1656 addRulesetErrorf("header value %q must only have lower case characters", v)
1658 rk, err := regexp.Compile(k)
1660 addRulesetErrorf("invalid rule header regexp %q: %v", k, err)
1662 rv, err := regexp.Compile(v)
1664 addRulesetErrorf("invalid rule header regexp %q: %v", v, err)
1666 hdr = append(hdr, [...]*regexp.Regexp{rk, rv})
1668 c.Accounts[accName].Destinations[addrName].Rulesets[i].HeadersRegexpCompiled = hdr
1671 addRulesetErrorf("ruleset must have at least one rule")
1674 if rs.IsForward && rs.ListAllowDomain != "" {
1675 addRulesetErrorf("ruleset cannot have both IsForward and ListAllowDomain")
1678 if rs.SMTPMailFromRegexp == "" || rs.VerifiedDomain == "" {
1679 addRulesetErrorf("ruleset with IsForward must have both SMTPMailFromRegexp and VerifiedDomain too")
1682 if rs.ListAllowDomain != "" {
1683 d, err := dns.ParseDomain(rs.ListAllowDomain)
1685 addRulesetErrorf("invalid ListAllowDomain %q: %v", rs.ListAllowDomain, err)
1687 c.Accounts[accName].Destinations[addrName].Rulesets[i].ListAllowDNSDomain = d
1690 checkMailboxNormf(rs.AcceptRejectsToMailbox, "rejects mailbox", addRulesetErrorf)
1691 if strings.EqualFold(rs.AcceptRejectsToMailbox, "inbox") {
1692 addRulesetErrorf("AcceptRejectsToMailbox cannot be set to Inbox")
1696 // Catchall destination for domain.
1697 if strings.HasPrefix(addrName, "@") {
1698 d, err := dns.ParseDomain(addrName[1:])
1700 addDestErrorf("parsing domain %q", addrName[1:])
1702 } else if _, ok := c.Domains[d.Name()]; !ok {
1703 addDestErrorf("unknown domain for address")
1706 domainHasAddress[d.Name()] = true
1707 addrFull := "@" + d.Name()
1708 if _, ok := accDests[addrFull]; ok {
1709 addDestErrorf("duplicate canonicalized catchall destination address %s", addrFull)
1711 accDests[addrFull] = AccountDestination{true, "", accName, dest}
1715 // todo deprecated: remove support for parsing destination as just a localpart instead full address.
1716 var address smtp.Address
1717 if localpart, err := smtp.ParseLocalpart(addrName); err != nil && errors.Is(err, smtp.ErrBadLocalpart) {
1718 address, err = smtp.ParseAddress(addrName)
1720 addDestErrorf("invalid email address")
1722 } else if _, ok := c.Domains[address.Domain.Name()]; !ok {
1723 addDestErrorf("unknown domain for address")
1728 addDestErrorf("invalid localpart %q", addrName)
1731 address = smtp.NewAddress(localpart, acc.DNSDomain)
1732 if _, ok := c.Domains[acc.DNSDomain.Name()]; !ok {
1733 addDestErrorf("unknown domain %s", acc.DNSDomain.Name())
1736 replaceLocalparts[addrName] = address.Pack(true)
1739 origLP := address.Localpart
1740 dc := c.Domains[address.Domain.Name()]
1741 domainHasAddress[address.Domain.Name()] = true
1742 lp := CanonicalLocalpart(address.Localpart, dc)
1744 for _, sep := range dc.LocalpartCatchallSeparatorsEffective {
1745 if strings.Contains(string(address.Localpart), sep) {
1747 addDestErrorf("localpart of address %s includes domain catchall separator %s", address, sep)
1751 address.Localpart = lp
1753 addrFull := address.Pack(true)
1754 if _, ok := accDests[addrFull]; ok {
1755 addDestErrorf("duplicate canonicalized destination address %s", addrFull)
1757 accDests[addrFull] = AccountDestination{false, origLP, accName, dest}
1760 for lp, addr := range replaceLocalparts {
1761 dest, ok := acc.Destinations[lp]
1763 addAccountErrorf("could not find localpart %q to replace with address in destinations", lp)
1765 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`,
1766 slog.Any("localpart", lp),
1767 slog.Any("address", addr),
1768 slog.String("account", accName))
1769 acc.Destinations[addr] = dest
1770 delete(acc.Destinations, lp)
1774 // Now that all addresses are parsed, check if all fromid login addresses match
1775 // configured addresses.
1776 for i, a := range acc.ParsedFromIDLoginAddresses {
1777 // For domain catchall.
1778 if _, ok := accDests["@"+a.Domain.Name()]; ok {
1781 dc := c.Domains[a.Domain.Name()]
1782 a.Localpart = CanonicalLocalpart(a.Localpart, dc)
1783 if _, ok := accDests[a.Pack(true)]; !ok {
1784 addAccountErrorf("fromid login address %q does not match its destination addresses", acc.FromIDLoginAddresses[i])
1788 checkRoutes("routes for account", acc.Routes)
1791 // Set DMARC destinations.
1792 for d, domain := range c.Domains {
1793 addDomainErrorf := func(format string, args ...any) {
1794 addErrorf("domain %s: %s", d, fmt.Sprintf(format, args...))
1797 dmarc := domain.DMARC
1801 if _, ok := c.Accounts[dmarc.Account]; !ok {
1802 addDomainErrorf("DMARC account %q does not exist", dmarc.Account)
1805 // Note: For backwards compabilitiy, DMARC reporting localparts can contain catchall separators.
1806 lp, err := smtp.ParseLocalpart(dmarc.Localpart)
1808 addDomainErrorf("invalid DMARC localpart %q: %s", dmarc.Localpart, err)
1810 if lp.IsInternational() {
1812 addDomainErrorf("DMARC localpart %q is an internationalized address, only conventional ascii-only address possible for interopability", lp)
1814 addrdom := domain.Domain
1815 if dmarc.Domain != "" {
1816 addrdom, err = dns.ParseDomain(dmarc.Domain)
1818 addDomainErrorf("DMARC domain %q: %s", dmarc.Domain, err)
1819 } else if adomain, ok := c.Domains[addrdom.Name()]; !ok {
1820 addDomainErrorf("unknown domain %q for DMARC address", addrdom)
1821 } else if !adomain.LocalpartCaseSensitive {
1822 lp = smtp.Localpart(strings.ToLower(string(lp)))
1824 } else if !domain.LocalpartCaseSensitive {
1825 lp = smtp.Localpart(strings.ToLower(string(lp)))
1827 if addrdom == domain.Domain {
1828 domainHasAddress[addrdom.Name()] = true
1831 domain.DMARC.ParsedLocalpart = lp
1832 domain.DMARC.DNSDomain = addrdom
1833 c.Domains[d] = domain
1834 addrFull := smtp.NewAddress(lp, addrdom).String()
1835 dest := config.Destination{
1836 Mailbox: dmarc.Mailbox,
1839 checkMailboxNormf(dmarc.Mailbox, "DMARC mailbox for account", addDomainErrorf)
1840 accDests[addrFull] = AccountDestination{false, lp, dmarc.Account, dest}
1843 // Set TLSRPT destinations.
1844 for d, domain := range c.Domains {
1845 addDomainErrorf := func(format string, args ...any) {
1846 addErrorf("domain %s: %s", d, fmt.Sprintf(format, args...))
1849 tlsrpt := domain.TLSRPT
1853 if _, ok := c.Accounts[tlsrpt.Account]; !ok {
1854 addDomainErrorf("TLSRPT account %q does not exist", tlsrpt.Account)
1857 // Note: For backwards compabilitiy, TLS reporting localparts can contain catchall separators.
1858 lp, err := smtp.ParseLocalpart(tlsrpt.Localpart)
1860 addDomainErrorf("invalid TLSRPT localpart %q: %s", tlsrpt.Localpart, err)
1862 if lp.IsInternational() {
1863 // Does not appear documented in
../rfc/8460, but similar to DMARC it makes sense
1864 // to keep this ascii-only addresses.
1865 addDomainErrorf("TLSRPT localpart %q is an internationalized address, only conventional ascii-only address allowed for interopability", lp)
1867 addrdom := domain.Domain
1868 if tlsrpt.Domain != "" {
1869 addrdom, err = dns.ParseDomain(tlsrpt.Domain)
1871 addDomainErrorf("TLSRPT domain %q: %s", tlsrpt.Domain, err)
1872 } else if adomain, ok := c.Domains[addrdom.Name()]; !ok {
1873 addDomainErrorf("unknown domain %q for TLSRPT address", tlsrpt.Domain)
1874 } else if !adomain.LocalpartCaseSensitive {
1875 lp = smtp.Localpart(strings.ToLower(string(lp)))
1877 } else if !domain.LocalpartCaseSensitive {
1878 lp = smtp.Localpart(strings.ToLower(string(lp)))
1880 if addrdom == domain.Domain {
1881 domainHasAddress[addrdom.Name()] = true
1884 domain.TLSRPT.ParsedLocalpart = lp
1885 domain.TLSRPT.DNSDomain = addrdom
1886 c.Domains[d] = domain
1887 addrFull := smtp.NewAddress(lp, addrdom).String()
1888 dest := config.Destination{
1889 Mailbox: tlsrpt.Mailbox,
1890 DomainTLSReports: true,
1892 checkMailboxNormf(tlsrpt.Mailbox, "TLSRPT mailbox", addDomainErrorf)
1893 accDests[addrFull] = AccountDestination{false, lp, tlsrpt.Account, dest}
1896 // Set ReportsOnly for domains, based on whether we have seen addresses (possibly
1897 // from DMARC or TLS reporting).
1898 for d, domain := range c.Domains {
1899 domain.ReportsOnly = !domainHasAddress[domain.Domain.Name()]
1900 c.Domains[d] = domain
1903 // Aliases, per domain. Also add references to accounts.
1904 for d, domain := range c.Domains {
1905 for lpstr, a := range domain.Aliases {
1906 addAliasErrorf := func(format string, args ...any) {
1907 addErrorf("domain %s: alias %s: %s", d, lpstr, fmt.Sprintf(format, args...))
1911 a.LocalpartStr = lpstr
1912 var clp smtp.Localpart
1913 lp, err := smtp.ParseLocalpart(lpstr)
1915 addAliasErrorf("parsing alias: %v", err)
1919 for _, sep := range domain.LocalpartCatchallSeparatorsEffective {
1920 if strings.Contains(string(lp), sep) {
1921 addAliasErrorf("alias contains localpart catchall separator")
1928 clp = CanonicalLocalpart(lp, domain)
1931 addr := smtp.NewAddress(clp, domain.Domain).Pack(true)
1932 if _, ok := aliases[addr]; ok {
1933 addAliasErrorf("duplicate alias address %q", addr)
1936 if _, ok := accDests[addr]; ok {
1937 addAliasErrorf("alias %q already present as regular address", addr)
1940 if len(a.Addresses) == 0 {
1941 // Not currently possible, Addresses isn't optional.
1942 addAliasErrorf("alias %q needs at least one destination address", addr)
1945 a.ParsedAddresses = make([]config.AliasAddress, 0, len(a.Addresses))
1946 seen := map[string]bool{}
1947 for _, destAddr := range a.Addresses {
1948 da, err := smtp.ParseAddress(destAddr)
1950 addAliasErrorf("parsing destination address %q: %v", destAddr, err)
1953 dastr := da.Pack(true)
1954 accDest, ok := accDests[dastr]
1956 addAliasErrorf("references non-existent address %q", destAddr)
1960 addAliasErrorf("duplicate address %q", destAddr)
1964 aa := config.AliasAddress{Address: da, AccountName: accDest.Account, Destination: accDest.Destination}
1965 a.ParsedAddresses = append(a.ParsedAddresses, aa)
1967 a.Domain = domain.Domain
1968 c.Domains[d].Aliases[lpstr] = a
1971 for _, aa := range a.ParsedAddresses {
1972 acc := c.Accounts[aa.AccountName]
1975 addrs = make([]string, len(a.ParsedAddresses))
1976 for i := range a.ParsedAddresses {
1977 addrs[i] = a.ParsedAddresses[i].Address.Pack(true)
1980 // Keep the non-sensitive fields.
1981 accAlias := config.Alias{
1982 PostPublic: a.PostPublic,
1983 ListMembers: a.ListMembers,
1984 AllowMsgFrom: a.AllowMsgFrom,
1985 LocalpartStr: a.LocalpartStr,
1988 acc.Aliases = append(acc.Aliases, config.AddressAlias{SubscriptionAddress: aa.Address.Pack(true), Alias: accAlias, MemberAddresses: addrs})
1989 c.Accounts[aa.AccountName] = acc
1994 // Check webserver configs.
1995 if (len(c.WebDomainRedirects) > 0 || len(c.WebHandlers) > 0) && !haveWebserverListener {
1996 addErrorf("WebDomainRedirects or WebHandlers configured but no listener with WebserverHTTP or WebserverHTTPS enabled")
1999 c.WebDNSDomainRedirects = map[dns.Domain]dns.Domain{}
2000 for from, to := range c.WebDomainRedirects {
2001 addRedirectErrorf := func(format string, args ...any) {
2002 addErrorf("web redirect %s to %s: %s", from, to, fmt.Sprintf(format, args...))
2005 fromdom, err := dns.ParseDomain(from)
2007 addRedirectErrorf("parsing domain for redirect %s: %v", from, err)
2009 todom, err := dns.ParseDomain(to)
2011 addRedirectErrorf("parsing domain for redirect %s: %v", to, err)
2012 } else if fromdom == todom {
2013 addRedirectErrorf("will not redirect domain %s to itself", todom)
2015 var zerodom dns.Domain
2016 if _, ok := c.WebDNSDomainRedirects[fromdom]; ok && fromdom != zerodom {
2017 addRedirectErrorf("duplicate redirect domain %s", from)
2019 c.WebDNSDomainRedirects[fromdom] = todom
2022 for i := range c.WebHandlers {
2023 wh := &c.WebHandlers[i]
2025 addHandlerErrorf := func(format string, args ...any) {
2026 addErrorf("webhandler %s %s: %s", wh.Domain, wh.PathRegexp, fmt.Sprintf(format, args...))
2029 if wh.LogName == "" {
2030 wh.Name = fmt.Sprintf("%d", i)
2032 wh.Name = wh.LogName
2035 dom, err := dns.ParseDomain(wh.Domain)
2037 addHandlerErrorf("parsing domain: %v", err)
2041 if !strings.HasPrefix(wh.PathRegexp, "^") {
2042 addHandlerErrorf("path regexp must start with a ^")
2044 re, err := regexp.Compile(wh.PathRegexp)
2046 addHandlerErrorf("compiling regexp: %v", err)
2051 if wh.WebStatic != nil {
2054 if ws.StripPrefix != "" && !strings.HasPrefix(ws.StripPrefix, "/") {
2055 addHandlerErrorf("static: prefix to strip %s must start with a slash", ws.StripPrefix)
2057 for k := range ws.ResponseHeaders {
2059 k := strings.TrimSpace(xk)
2060 if k != xk || k == "" {
2061 addHandlerErrorf("static: bad header %q", xk)
2065 if wh.WebRedirect != nil {
2067 wr := wh.WebRedirect
2068 if wr.BaseURL != "" {
2069 u, err := url.Parse(wr.BaseURL)
2071 addHandlerErrorf("redirect: parsing redirect url %s: %v", wr.BaseURL, err)
2077 addHandlerErrorf("redirect: BaseURL must have empty path: %s", wr.BaseURL)
2081 if wr.OrigPathRegexp != "" && wr.ReplacePath != "" {
2082 re, err := regexp.Compile(wr.OrigPathRegexp)
2084 addHandlerErrorf("compiling regexp %s: %v", wr.OrigPathRegexp, err)
2087 } else if wr.OrigPathRegexp != "" || wr.ReplacePath != "" {
2088 addHandlerErrorf("redirect: must have either both OrigPathRegexp and ReplacePath, or neither")
2089 } else if wr.BaseURL == "" {
2090 addHandlerErrorf("must at least one of BaseURL and OrigPathRegexp+ReplacePath")
2092 if wr.StatusCode != 0 && (wr.StatusCode < 300 || wr.StatusCode >= 400) {
2093 addHandlerErrorf("redirect: invalid redirect status code %d", wr.StatusCode)
2096 if wh.WebForward != nil {
2099 u, err := url.Parse(wf.URL)
2101 addHandlerErrorf("forward: parsing url %s: %v", wf.URL, err)
2105 for k := range wf.ResponseHeaders {
2107 k := strings.TrimSpace(xk)
2108 if k != xk || k == "" {
2109 addHandlerErrorf("forrward: bad header %q", xk)
2113 if wh.WebInternal != nil {
2115 wi := wh.WebInternal
2116 if !strings.HasPrefix(wi.BasePath, "/") || !strings.HasSuffix(wi.BasePath, "/") {
2117 addHandlerErrorf("internal service: base path %q must start and end with /", wi.BasePath)
2119 // todo: we could make maxMsgSize and accountPath configurable
2120 const isForwarded = false
2123 wi.Handler = NewWebadminHandler(wi.BasePath, isForwarded)
2125 wi.Handler = NewWebaccountHandler(wi.BasePath, isForwarded)
2128 wi.Handler = NewWebmailHandler(config.DefaultMaxMsgSize, wi.BasePath, isForwarded, accountPath)
2130 wi.Handler = NewWebapiHandler(config.DefaultMaxMsgSize, wi.BasePath, isForwarded)
2132 addHandlerErrorf("internal service: unknown service %q", wi.Service)
2134 wi.Handler = SafeHeaders(http.StripPrefix(wi.BasePath[:len(wi.BasePath)-1], wi.Handler))
2137 addHandlerErrorf("must have exactly one handler, not %d", n)
2141 c.MonitorDNSBLZones = nil
2142 for _, s := range c.MonitorDNSBLs {
2143 d, err := dns.ParseDomain(s)
2145 addErrorf("dnsbl %s: parsing dnsbl zone: %v", s, err)
2148 if slices.Contains(c.MonitorDNSBLZones, d) {
2149 addErrorf("dnsbl %s: duplicate zone", s)
2152 c.MonitorDNSBLZones = append(c.MonitorDNSBLZones, d)
2158func loadPrivateKeyFile(keyPath string) (crypto.Signer, error) {
2159 keyBuf, err := os.ReadFile(keyPath)
2161 return nil, fmt.Errorf("reading host private key: %v", err)
2163 b, _ := pem.Decode(keyBuf)
2165 return nil, fmt.Errorf("parsing pem block for private key: %v", err)
2170 privKey, err = x509.ParsePKCS8PrivateKey(b.Bytes)
2171 case "RSA PRIVATE KEY":
2172 privKey, err = x509.ParsePKCS1PrivateKey(b.Bytes)
2173 case "EC PRIVATE KEY":
2174 privKey, err = x509.ParseECPrivateKey(b.Bytes)
2176 err = fmt.Errorf("unknown pem type %q", b.Type)
2179 return nil, fmt.Errorf("parsing private key: %v", err)
2181 if k, ok := privKey.(crypto.Signer); ok {
2184 return nil, fmt.Errorf("parsed private key not a crypto.Signer, but %T", privKey)
2187func loadTLSKeyCerts(configFile, kind string, ctls *config.TLS) error {
2188 certs := []tls.Certificate{}
2189 for _, kp := range ctls.KeyCerts {
2190 certPath := configDirPath(configFile, kp.CertFile)
2191 keyPath := configDirPath(configFile, kp.KeyFile)
2192 cert, err := loadX509KeyPairPrivileged(certPath, keyPath)
2194 return fmt.Errorf("tls config for %q: parsing x509 key pair: %v", kind, err)
2196 certs = append(certs, cert)
2198 ctls.Config = &tls.Config{
2199 Certificates: certs,
2201 ctls.ConfigFallback = ctls.Config
2205// load x509 key/cert files from file descriptor possibly passed in by privileged
2207func loadX509KeyPairPrivileged(certPath, keyPath string) (tls.Certificate, error) {
2208 certBuf, err := readFilePrivileged(certPath)
2210 return tls.Certificate{}, fmt.Errorf("reading tls certificate: %v", err)
2212 keyBuf, err := readFilePrivileged(keyPath)
2214 return tls.Certificate{}, fmt.Errorf("reading tls key: %v", err)
2216 return tls.X509KeyPair(certBuf, keyBuf)
2219// like os.ReadFile, but open privileged file possibly passed in by root process.
2220func readFilePrivileged(path string) ([]byte, error) {
2221 f, err := OpenPrivileged(path)
2226 return io.ReadAll(f)