10 cryptorand "crypto/rand"
32 "golang.org/x/exp/slog"
33 "golang.org/x/text/unicode/norm"
35 "github.com/mjl-/autocert"
37 "github.com/mjl-/sconf"
39 "github.com/mjl-/mox/autotls"
40 "github.com/mjl-/mox/config"
41 "github.com/mjl-/mox/dkim"
42 "github.com/mjl-/mox/dns"
43 "github.com/mjl-/mox/message"
44 "github.com/mjl-/mox/mlog"
45 "github.com/mjl-/mox/moxio"
46 "github.com/mjl-/mox/mtasts"
47 "github.com/mjl-/mox/smtp"
50var pkglog = mlog.New("mox", nil)
52// Pedantic enables stricter parsing.
55// Config paths are set early in program startup. They will point to files in
58 ConfigStaticPath string
59 ConfigDynamicPath string
60 Conf = Config{Log: map[string]slog.Level{"": slog.LevelError}}
63// Config as used in the code, a processed version of what is in the config file.
65// Use methods to lookup a domain/account/address in the dynamic configuration.
67 Static config.Static // Does not change during the lifetime of a running instance.
69 logMutex sync.Mutex // For accessing the log levels.
70 Log map[string]slog.Level
72 dynamicMutex sync.Mutex
73 Dynamic config.Dynamic // Can only be accessed directly by tests. Use methods on Config for locked access.
74 dynamicMtime time.Time
75 DynamicLastCheck time.Time // For use by quickstart only to skip checks.
76 // From canonical full address (localpart@domain, lower-cased when
77 // case-insensitive, stripped of catchall separator) to account and address.
78 // Domains are IDNA names in utf8.
79 accountDestinations map[string]AccountDestination
82type AccountDestination struct {
83 Catchall bool // If catchall destination for its domain.
84 Localpart smtp.Localpart // In original casing as written in config file.
86 Destination config.Destination
89// LogLevelSet sets a new log level for pkg. An empty pkg sets the default log
90// value that is used if no explicit log level is configured for a package.
91// This change is ephemeral, no config file is changed.
92func (c *Config) LogLevelSet(log mlog.Log, pkg string, level slog.Level) {
94 defer c.logMutex.Unlock()
95 l := c.copyLogLevels()
98 log.Print("log level changed", slog.String("pkg", pkg), slog.Any("level", mlog.LevelStrings[level]))
102// LogLevelRemove removes a configured log level for a package.
103func (c *Config) LogLevelRemove(log mlog.Log, pkg string) {
105 defer c.logMutex.Unlock()
106 l := c.copyLogLevels()
109 log.Print("log level cleared", slog.String("pkg", pkg))
110 mlog.SetConfig(c.Log)
113// copyLogLevels returns a copy of c.Log, for modifications.
114// must be called with log lock held.
115func (c *Config) copyLogLevels() map[string]slog.Level {
116 m := map[string]slog.Level{}
117 for pkg, level := range c.Log {
123// LogLevels returns a copy of the current log levels.
124func (c *Config) LogLevels() map[string]slog.Level {
126 defer c.logMutex.Unlock()
127 return c.copyLogLevels()
130func (c *Config) withDynamicLock(fn func()) {
131 c.dynamicMutex.Lock()
132 defer c.dynamicMutex.Unlock()
134 if now.Sub(c.DynamicLastCheck) > time.Second {
135 c.DynamicLastCheck = now
136 if fi, err := os.Stat(ConfigDynamicPath); err != nil {
137 pkglog.Errorx("stat domains config", err)
138 } else if !fi.ModTime().Equal(c.dynamicMtime) {
139 if errs := c.loadDynamic(); len(errs) > 0 {
140 pkglog.Errorx("loading domains config", errs[0], slog.Any("errors", errs))
142 pkglog.Info("domains config reloaded")
143 c.dynamicMtime = fi.ModTime()
150// must be called with dynamic lock held.
151func (c *Config) loadDynamic() []error {
152 d, mtime, accDests, err := ParseDynamicConfig(context.Background(), pkglog, ConfigDynamicPath, c.Static)
157 c.dynamicMtime = mtime
158 c.accountDestinations = accDests
159 c.allowACMEHosts(pkglog, true)
163func (c *Config) Domains() (l []string) {
164 c.withDynamicLock(func() {
165 for name := range c.Dynamic.Domains {
169 sort.Slice(l, func(i, j int) bool {
175func (c *Config) Accounts() (l []string) {
176 c.withDynamicLock(func() {
177 for name := range c.Dynamic.Accounts {
184// DomainLocalparts returns a mapping of encoded localparts to account names for a
185// domain. An empty localpart is a catchall destination for a domain.
186func (c *Config) DomainLocalparts(d dns.Domain) map[string]string {
187 suffix := "@" + d.Name()
188 m := map[string]string{}
189 c.withDynamicLock(func() {
190 for addr, ad := range c.accountDestinations {
191 if strings.HasSuffix(addr, suffix) {
195 m[ad.Localpart.String()] = ad.Account
203func (c *Config) Domain(d dns.Domain) (dom config.Domain, ok bool) {
204 c.withDynamicLock(func() {
205 dom, ok = c.Dynamic.Domains[d.Name()]
210func (c *Config) Account(name string) (acc config.Account, ok bool) {
211 c.withDynamicLock(func() {
212 acc, ok = c.Dynamic.Accounts[name]
217func (c *Config) AccountDestination(addr string) (accDests AccountDestination, ok bool) {
218 c.withDynamicLock(func() {
219 accDests, ok = c.accountDestinations[addr]
224func (c *Config) WebServer() (r map[dns.Domain]dns.Domain, l []config.WebHandler) {
225 c.withDynamicLock(func() {
226 r = c.Dynamic.WebDNSDomainRedirects
227 l = c.Dynamic.WebHandlers
232func (c *Config) Routes(accountName string, domain dns.Domain) (accountRoutes, domainRoutes, globalRoutes []config.Route) {
233 c.withDynamicLock(func() {
234 acc := c.Dynamic.Accounts[accountName]
235 accountRoutes = acc.Routes
237 dom := c.Dynamic.Domains[domain.Name()]
238 domainRoutes = dom.Routes
240 globalRoutes = c.Dynamic.Routes
245func (c *Config) allowACMEHosts(log mlog.Log, checkACMEHosts bool) {
246 for _, l := range c.Static.Listeners {
247 if l.TLS == nil || l.TLS.ACME == "" {
251 m := c.Static.ACME[l.TLS.ACME].Manager
252 hostnames := map[dns.Domain]struct{}{}
254 hostnames[c.Static.HostnameDomain] = struct{}{}
255 if l.HostnameDomain.ASCII != "" {
256 hostnames[l.HostnameDomain] = struct{}{}
259 for _, dom := range c.Dynamic.Domains {
260 if dom.DMARC != nil && dom.DMARC.Domain != "" && dom.DMARC.DNSDomain != dom.Domain {
261 // Do not allow TLS certificates for domains for which we only accept DMARC reports
262 // as external party.
266 if l.AutoconfigHTTPS.Enabled && !l.AutoconfigHTTPS.NonTLS {
267 if d, err := dns.ParseDomain("autoconfig." + dom.Domain.ASCII); err != nil {
268 log.Errorx("parsing autoconfig domain", err, slog.Any("domain", dom.Domain))
270 hostnames[d] = struct{}{}
274 if l.MTASTSHTTPS.Enabled && dom.MTASTS != nil && !l.MTASTSHTTPS.NonTLS {
275 d, err := dns.ParseDomain("mta-sts." + dom.Domain.ASCII)
277 log.Errorx("parsing mta-sts domain", err, slog.Any("domain", dom.Domain))
279 hostnames[d] = struct{}{}
283 if dom.ClientSettingsDomain != "" {
284 hostnames[dom.ClientSettingsDNSDomain] = struct{}{}
288 if l.WebserverHTTPS.Enabled {
289 for from := range c.Dynamic.WebDNSDomainRedirects {
290 hostnames[from] = struct{}{}
292 for _, wh := range c.Dynamic.WebHandlers {
293 hostnames[wh.DNSDomain] = struct{}{}
297 public := c.Static.Listeners["public"]
299 if len(public.NATIPs) > 0 {
305 m.SetAllowedHostnames(log, dns.StrictResolver{Pkg: "autotls", Log: log.Logger}, hostnames, ips, checkACMEHosts)
309// 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.
311// must be called with lock held.
312func writeDynamic(ctx context.Context, log mlog.Log, c config.Dynamic) error {
313 accDests, errs := prepareDynamicConfig(ctx, log, ConfigDynamicPath, Conf.Static, &c)
319 err := sconf.Write(&b, c)
323 f, err := os.OpenFile(ConfigDynamicPath, os.O_WRONLY, 0660)
330 log.Check(err, "closing file after error")
334 if _, err := f.Write(buf); err != nil {
335 return fmt.Errorf("write domains.conf: %v", err)
337 if err := f.Truncate(int64(len(buf))); err != nil {
338 return fmt.Errorf("truncate domains.conf after write: %v", err)
340 if err := f.Sync(); err != nil {
341 return fmt.Errorf("sync domains.conf after write: %v", err)
343 if err := moxio.SyncDir(log, filepath.Dir(ConfigDynamicPath)); err != nil {
344 return fmt.Errorf("sync dir of domains.conf after write: %v", err)
349 return fmt.Errorf("stat after writing domains.conf: %v", err)
352 if err := f.Close(); err != nil {
353 return fmt.Errorf("close written domains.conf: %v", err)
357 Conf.dynamicMtime = fi.ModTime()
358 Conf.DynamicLastCheck = time.Now()
360 Conf.accountDestinations = accDests
362 Conf.allowACMEHosts(log, true)
367// MustLoadConfig loads the config, quitting on errors.
368func MustLoadConfig(doLoadTLSKeyCerts, checkACMEHosts bool) {
369 errs := LoadConfig(context.Background(), pkglog, doLoadTLSKeyCerts, checkACMEHosts)
371 pkglog.Error("loading config file: multiple errors")
372 for _, err := range errs {
373 pkglog.Errorx("config error", err)
375 pkglog.Fatal("stopping after multiple config errors")
376 } else if len(errs) == 1 {
377 pkglog.Fatalx("loading config file", errs[0])
381// LoadConfig attempts to parse and load a config, returning any errors
383func LoadConfig(ctx context.Context, log mlog.Log, doLoadTLSKeyCerts, checkACMEHosts bool) []error {
384 Shutdown, ShutdownCancel = context.WithCancel(context.Background())
385 Context, ContextCancel = context.WithCancel(context.Background())
387 c, errs := ParseConfig(ctx, log, ConfigStaticPath, false, doLoadTLSKeyCerts, checkACMEHosts)
392 mlog.SetConfig(c.Log)
397// SetConfig sets a new config. Not to be used during normal operation.
398func SetConfig(c *Config) {
399 // Cannot just assign *c to Conf, it would copy the mutex.
400 Conf = Config{c.Static, sync.Mutex{}, c.Log, sync.Mutex{}, c.Dynamic, c.dynamicMtime, c.DynamicLastCheck, c.accountDestinations}
402 // If we have non-standard CA roots, use them for all HTTPS requests.
403 if Conf.Static.TLS.CertPool != nil {
404 http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{
405 RootCAs: Conf.Static.TLS.CertPool,
409 SetPedantic(c.Static.Pedantic)
412// Set pedantic in all packages.
413func SetPedantic(p bool) {
421// ParseConfig parses the static config at path p. If checkOnly is true, no changes
422// are made, such as registering ACME identities. If doLoadTLSKeyCerts is true,
423// the TLS KeyCerts configuration is loaded and checked. This is used during the
424// quickstart in the case the user is going to provide their own certificates.
425// If checkACMEHosts is true, the hosts allowed for acme are compared with the
426// explicitly configured ips we are listening on.
427func ParseConfig(ctx context.Context, log mlog.Log, p string, checkOnly, doLoadTLSKeyCerts, checkACMEHosts bool) (c *Config, errs []error) {
429 Static: config.Static{
436 if os.IsNotExist(err) && os.Getenv("MOXCONF") == "" {
437 return nil, []error{fmt.Errorf("open config file: %v (hint: use mox -config ... or set MOXCONF=...)", err)}
439 return nil, []error{fmt.Errorf("open config file: %v", err)}
442 if err := sconf.Parse(f, &c.Static); err != nil {
443 return nil, []error{fmt.Errorf("parsing %s%v", p, err)}
446 if xerrs := PrepareStaticConfig(ctx, log, p, c, checkOnly, doLoadTLSKeyCerts); len(xerrs) > 0 {
450 pp := filepath.Join(filepath.Dir(p), "domains.conf")
451 c.Dynamic, c.dynamicMtime, c.accountDestinations, errs = ParseDynamicConfig(ctx, log, pp, c.Static)
454 c.allowACMEHosts(log, checkACMEHosts)
460// PrepareStaticConfig parses the static config file and prepares data structures
461// for starting mox. If checkOnly is set no substantial changes are made, like
462// creating an ACME registration.
463func PrepareStaticConfig(ctx context.Context, log mlog.Log, configFile string, conf *Config, checkOnly, doLoadTLSKeyCerts bool) (errs []error) {
464 addErrorf := func(format string, args ...any) {
465 errs = append(errs, fmt.Errorf(format, args...))
470 // check that mailbox is in unicode NFC normalized form.
471 checkMailboxNormf := func(mailbox string, format string, args ...any) {
472 s := norm.NFC.String(mailbox)
474 msg := fmt.Sprintf(format, args...)
475 addErrorf("%s: mailbox %q is not in NFC normalized form, should be %q", msg, mailbox, s)
479 // Post-process logging config.
480 if logLevel, ok := mlog.Levels[c.LogLevel]; ok {
481 conf.Log = map[string]slog.Level{"": logLevel}
483 addErrorf("invalid log level %q", c.LogLevel)
485 for pkg, s := range c.PackageLogLevels {
486 if logLevel, ok := mlog.Levels[s]; ok {
487 conf.Log[pkg] = logLevel
489 addErrorf("invalid package log level %q", s)
496 u, err := user.Lookup(c.User)
498 uid, err := strconv.ParseUint(c.User, 10, 32)
500 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)
502 // We assume the same gid as uid.
507 if uid, err := strconv.ParseUint(u.Uid, 10, 32); err != nil {
508 addErrorf("parsing uid %s: %v", u.Uid, err)
512 if gid, err := strconv.ParseUint(u.Gid, 10, 32); err != nil {
513 addErrorf("parsing gid %s: %v", u.Gid, err)
519 hostname, err := dns.ParseDomain(c.Hostname)
521 addErrorf("parsing hostname: %s", err)
522 } else if hostname.Name() != c.Hostname {
523 addErrorf("hostname must be in IDNA form %q", hostname.Name())
525 c.HostnameDomain = hostname
527 if c.HostTLSRPT.Account != "" {
528 tlsrptLocalpart, err := smtp.ParseLocalpart(c.HostTLSRPT.Localpart)
530 addErrorf("invalid localpart %q for host tlsrpt: %v", c.HostTLSRPT.Localpart, err)
531 } else if tlsrptLocalpart.IsInternational() {
532 // Does not appear documented in
../rfc/8460, but similar to DMARC it makes sense
533 // to keep this ascii-only addresses.
534 addErrorf("host TLSRPT localpart %q is an internationalized address, only conventional ascii-only address allowed for interopability", tlsrptLocalpart)
536 c.HostTLSRPT.ParsedLocalpart = tlsrptLocalpart
539 // Return private key for host name for use with an ACME. Used to return the same
540 // private key as pre-generated for use with DANE, with its public key in DNS.
541 // We only use this key for Listener's that have this ACME configured, and for
542 // which the effective listener host name (either specific to the listener, or the
543 // global name) is requested. Other host names can get a fresh private key, they
544 // don't appear in DANE records.
546 // - run 0: only use listener with explicitly matching host name in listener
547 // (default quickstart config does not set it).
548 // - run 1: only look at public listener (and host matching mox host name)
549 // - run 2: all listeners (and host matching mox host name)
550 findACMEHostPrivateKey := func(acmeName, host string, keyType autocert.KeyType, run int) crypto.Signer {
551 for listenerName, l := range Conf.Static.Listeners {
552 if l.TLS == nil || l.TLS.ACME != acmeName {
555 if run == 0 && host != l.HostnameDomain.ASCII {
558 if run == 1 && listenerName != "public" || host != Conf.Static.HostnameDomain.ASCII {
562 case autocert.KeyRSA2048:
563 if len(l.TLS.HostPrivateRSA2048Keys) == 0 {
566 return l.TLS.HostPrivateRSA2048Keys[0]
567 case autocert.KeyECDSAP256:
568 if len(l.TLS.HostPrivateECDSAP256Keys) == 0 {
571 return l.TLS.HostPrivateECDSAP256Keys[0]
578 // Make a function for an autocert.Manager.GetPrivateKey, using findACMEHostPrivateKey.
579 makeGetPrivateKey := func(acmeName string) func(host string, keyType autocert.KeyType) (crypto.Signer, error) {
580 return func(host string, keyType autocert.KeyType) (crypto.Signer, error) {
581 key := findACMEHostPrivateKey(acmeName, host, keyType, 0)
583 key = findACMEHostPrivateKey(acmeName, host, keyType, 1)
586 key = findACMEHostPrivateKey(acmeName, host, keyType, 2)
589 log.Debug("found existing private key for certificate for host",
590 slog.String("acmename", acmeName),
591 slog.String("host", host),
592 slog.Any("keytype", keyType))
595 log.Debug("generating new private key for certificate for host",
596 slog.String("acmename", acmeName),
597 slog.String("host", host),
598 slog.Any("keytype", keyType))
600 case autocert.KeyRSA2048:
601 return rsa.GenerateKey(cryptorand.Reader, 2048)
602 case autocert.KeyECDSAP256:
603 return ecdsa.GenerateKey(elliptic.P256(), cryptorand.Reader)
605 return nil, fmt.Errorf("unrecognized requested key type %v", keyType)
609 for name, acme := range c.ACME {
612 if acme.ExternalAccountBinding != nil {
613 eabKeyID = acme.ExternalAccountBinding.KeyID
614 p := configDirPath(configFile, acme.ExternalAccountBinding.KeyFile)
615 buf, err := os.ReadFile(p)
617 addErrorf("reading external account binding key for acme provider %q: %s", name, err)
619 dec := make([]byte, base64.RawURLEncoding.DecodedLen(len(buf)))
620 n, err := base64.RawURLEncoding.Decode(dec, buf)
622 addErrorf("parsing external account binding key as base64 for acme provider %q: %s", name, err)
633 acmeDir := dataDirPath(configFile, c.DataDir, "acme")
634 os.MkdirAll(acmeDir, 0770)
635 manager, err := autotls.Load(name, acmeDir, acme.ContactEmail, acme.DirectoryURL, eabKeyID, eabKey, makeGetPrivateKey(name), Shutdown.Done())
637 addErrorf("loading ACME identity for %q: %s", name, err)
639 acme.Manager = manager
641 // Help configurations from older quickstarts.
642 if acme.IssuerDomainName == "" && acme.DirectoryURL == "https://acme-v02.api.letsencrypt.org/directory" {
643 acme.IssuerDomainName = "letsencrypt.org"
649 var haveUnspecifiedSMTPListener bool
650 for name, l := range c.Listeners {
651 if l.Hostname != "" {
652 d, err := dns.ParseDomain(l.Hostname)
654 addErrorf("bad listener hostname %q: %s", l.Hostname, err)
659 if l.TLS.ACME != "" && len(l.TLS.KeyCerts) != 0 {
660 addErrorf("listener %q: cannot have ACME and static key/certificates", name)
661 } else if l.TLS.ACME != "" {
662 acme, ok := c.ACME[l.TLS.ACME]
664 addErrorf("listener %q: unknown ACME provider %q", name, l.TLS.ACME)
667 // If only checking or with missing ACME definition, we don't have an acme manager,
668 // so set an empty tls config to continue.
669 var tlsconfig *tls.Config
670 if checkOnly || acme.Manager == nil {
671 tlsconfig = &tls.Config{}
673 tlsconfig = acme.Manager.TLSConfig.Clone()
674 l.TLS.ACMEConfig = acme.Manager.ACMETLSConfig
676 // SMTP STARTTLS connections are commonly made without SNI, because certificates
677 // often aren't verified.
678 hostname := c.HostnameDomain
679 if l.Hostname != "" {
680 hostname = l.HostnameDomain
682 getCert := tlsconfig.GetCertificate
683 tlsconfig.GetCertificate = func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
684 if hello.ServerName == "" {
685 hello.ServerName = hostname.ASCII
687 return getCert(hello)
690 l.TLS.Config = tlsconfig
691 } else if len(l.TLS.KeyCerts) != 0 {
692 if doLoadTLSKeyCerts {
693 if err := loadTLSKeyCerts(configFile, "listener "+name, l.TLS); err != nil {
698 addErrorf("listener %q: cannot have TLS config without ACME and without static keys/certificates", name)
700 for _, privKeyFile := range l.TLS.HostPrivateKeyFiles {
701 keyPath := configDirPath(configFile, privKeyFile)
702 privKey, err := loadPrivateKeyFile(keyPath)
704 addErrorf("listener %q: parsing host private key for DANE and ACME certificates: %v", name, err)
707 switch k := privKey.(type) {
708 case *rsa.PrivateKey:
709 if k.N.BitLen() != 2048 {
710 log.Error("need rsa key with 2048 bits, for host private key for DANE/ACME certificates, ignoring",
711 slog.String("listener", name),
712 slog.String("file", keyPath),
713 slog.Int("bits", k.N.BitLen()))
716 l.TLS.HostPrivateRSA2048Keys = append(l.TLS.HostPrivateRSA2048Keys, k)
717 case *ecdsa.PrivateKey:
718 if k.Curve != elliptic.P256() {
719 log.Error("unrecognized ecdsa curve for host private key for DANE/ACME certificates, ignoring", slog.String("listener", name), slog.String("file", keyPath))
722 l.TLS.HostPrivateECDSAP256Keys = append(l.TLS.HostPrivateECDSAP256Keys, k)
724 log.Error("unrecognized key type for host private key for DANE/ACME certificates, ignoring",
725 slog.String("listener", name),
726 slog.String("file", keyPath),
727 slog.String("keytype", fmt.Sprintf("%T", privKey)))
731 if l.TLS.ACME != "" && (len(l.TLS.HostPrivateRSA2048Keys) == 0) != (len(l.TLS.HostPrivateECDSAP256Keys) == 0) {
732 log.Error("warning: uncommon configuration with either only an RSA 2048 or ECDSA P256 host private key for DANE/ACME certificates; this ACME implementation can retrieve certificates for both type of keys, it is recommended to set either both or none; continuing")
736 var minVersion uint16 = tls.VersionTLS12
737 if l.TLS.MinVersion != "" {
738 versions := map[string]uint16{
739 "TLSv1.0": tls.VersionTLS10,
740 "TLSv1.1": tls.VersionTLS11,
741 "TLSv1.2": tls.VersionTLS12,
742 "TLSv1.3": tls.VersionTLS13,
744 v, ok := versions[l.TLS.MinVersion]
746 addErrorf("listener %q: unknown TLS mininum version %q", name, l.TLS.MinVersion)
750 if l.TLS.Config != nil {
751 l.TLS.Config.MinVersion = minVersion
753 if l.TLS.ACMEConfig != nil {
754 l.TLS.ACMEConfig.MinVersion = minVersion
757 var needsTLS []string
758 needtls := func(s string, v bool) {
760 needsTLS = append(needsTLS, s)
763 needtls("IMAPS", l.IMAPS.Enabled)
764 needtls("SMTP", l.SMTP.Enabled && !l.SMTP.NoSTARTTLS)
765 needtls("Submissions", l.Submissions.Enabled)
766 needtls("Submission", l.Submission.Enabled && !l.Submission.NoRequireSTARTTLS)
767 needtls("AccountHTTPS", l.AccountHTTPS.Enabled)
768 needtls("AdminHTTPS", l.AdminHTTPS.Enabled)
769 needtls("AutoconfigHTTPS", l.AutoconfigHTTPS.Enabled && !l.AutoconfigHTTPS.NonTLS)
770 needtls("MTASTSHTTPS", l.MTASTSHTTPS.Enabled && !l.MTASTSHTTPS.NonTLS)
771 needtls("WebserverHTTPS", l.WebserverHTTPS.Enabled)
772 if len(needsTLS) > 0 {
773 addErrorf("listener %q does not specify tls config, but requires tls for %s", name, strings.Join(needsTLS, ", "))
776 if l.AutoconfigHTTPS.Enabled && l.MTASTSHTTPS.Enabled && l.AutoconfigHTTPS.Port == l.MTASTSHTTPS.Port && l.AutoconfigHTTPS.NonTLS != l.MTASTSHTTPS.NonTLS {
777 addErrorf("listener %q tries to enable autoconfig and mta-sts enabled on same port but with both http and https", name)
781 haveUnspecifiedSMTPListener = true
783 for _, ipstr := range l.IPs {
784 ip := net.ParseIP(ipstr)
786 addErrorf("listener %q has invalid IP %q", name, ipstr)
789 if ip.IsUnspecified() {
790 haveUnspecifiedSMTPListener = true
793 if len(c.SpecifiedSMTPListenIPs) >= 2 {
794 haveUnspecifiedSMTPListener = true
795 } else if len(c.SpecifiedSMTPListenIPs) > 0 && (c.SpecifiedSMTPListenIPs[0].To4() == nil) == (ip.To4() == nil) {
796 haveUnspecifiedSMTPListener = true
798 c.SpecifiedSMTPListenIPs = append(c.SpecifiedSMTPListenIPs, ip)
802 for _, s := range l.SMTP.DNSBLs {
803 d, err := dns.ParseDomain(s)
805 addErrorf("listener %q has invalid DNSBL zone %q", name, s)
808 l.SMTP.DNSBLZones = append(l.SMTP.DNSBLZones, d)
810 if l.IPsNATed && len(l.NATIPs) > 0 {
811 addErrorf("listener %q has both IPsNATed and NATIPs (remove deprecated IPsNATed)", name)
813 for _, ipstr := range l.NATIPs {
814 ip := net.ParseIP(ipstr)
816 addErrorf("listener %q has invalid ip %q", name, ipstr)
817 } else if ip.IsUnspecified() || ip.IsLoopback() {
818 addErrorf("listener %q has NAT ip that is the unspecified or loopback address %s", name, ipstr)
821 checkPath := func(kind string, enabled bool, path string) {
822 if enabled && path != "" && !strings.HasPrefix(path, "/") {
823 addErrorf("listener %q has %s with path %q that must start with a slash", name, kind, path)
826 checkPath("AccountHTTP", l.AccountHTTP.Enabled, l.AccountHTTP.Path)
827 checkPath("AccountHTTPS", l.AccountHTTPS.Enabled, l.AccountHTTPS.Path)
828 checkPath("AdminHTTP", l.AdminHTTP.Enabled, l.AdminHTTP.Path)
829 checkPath("AdminHTTPS", l.AdminHTTPS.Enabled, l.AdminHTTPS.Path)
830 c.Listeners[name] = l
832 if haveUnspecifiedSMTPListener {
833 c.SpecifiedSMTPListenIPs = nil
836 var zerouse config.SpecialUseMailboxes
837 if len(c.DefaultMailboxes) > 0 && (c.InitialMailboxes.SpecialUse != zerouse || len(c.InitialMailboxes.Regular) > 0) {
838 addErrorf("cannot have both DefaultMailboxes and InitialMailboxes")
840 // DefaultMailboxes is deprecated.
841 for _, mb := range c.DefaultMailboxes {
842 checkMailboxNormf(mb, "default mailbox")
844 checkSpecialUseMailbox := func(nameOpt string) {
846 checkMailboxNormf(nameOpt, "special-use initial mailbox")
847 if strings.EqualFold(nameOpt, "inbox") {
848 addErrorf("initial mailbox cannot be set to Inbox (Inbox is always created)")
852 checkSpecialUseMailbox(c.InitialMailboxes.SpecialUse.Archive)
853 checkSpecialUseMailbox(c.InitialMailboxes.SpecialUse.Draft)
854 checkSpecialUseMailbox(c.InitialMailboxes.SpecialUse.Junk)
855 checkSpecialUseMailbox(c.InitialMailboxes.SpecialUse.Sent)
856 checkSpecialUseMailbox(c.InitialMailboxes.SpecialUse.Trash)
857 for _, name := range c.InitialMailboxes.Regular {
858 checkMailboxNormf(name, "regular initial mailbox")
859 if strings.EqualFold(name, "inbox") {
860 addErrorf("initial regular mailbox cannot be set to Inbox (Inbox is always created)")
864 checkTransportSMTP := func(name string, isTLS bool, t *config.TransportSMTP) {
866 t.DNSHost, err = dns.ParseDomain(t.Host)
868 addErrorf("transport %s: bad host %s: %v", name, t.Host, err)
871 if isTLS && t.STARTTLSInsecureSkipVerify {
872 addErrorf("transport %s: cannot have STARTTLSInsecureSkipVerify with immediate TLS")
874 if isTLS && t.NoSTARTTLS {
875 addErrorf("transport %s: cannot have NoSTARTTLS with immediate TLS")
881 seen := map[string]bool{}
882 for _, m := range t.Auth.Mechanisms {
884 addErrorf("transport %s: duplicate authentication mechanism %s", name, m)
888 case "SCRAM-SHA-256-PLUS":
889 case "SCRAM-SHA-256":
890 case "SCRAM-SHA-1-PLUS":
895 addErrorf("transport %s: unknown authentication mechanism %s", name, m)
899 t.Auth.EffectiveMechanisms = t.Auth.Mechanisms
900 if len(t.Auth.EffectiveMechanisms) == 0 {
901 t.Auth.EffectiveMechanisms = []string{"SCRAM-SHA-256-PLUS", "SCRAM-SHA-256", "SCRAM-SHA-1-PLUS", "SCRAM-SHA-1", "CRAM-MD5"}
905 checkTransportSocks := func(name string, t *config.TransportSocks) {
906 _, _, err := net.SplitHostPort(t.Address)
908 addErrorf("transport %s: bad address %s: %v", name, t.Address, err)
910 for _, ipstr := range t.RemoteIPs {
911 ip := net.ParseIP(ipstr)
913 addErrorf("transport %s: bad ip %s", name, ipstr)
915 t.IPs = append(t.IPs, ip)
918 t.Hostname, err = dns.ParseDomain(t.RemoteHostname)
920 addErrorf("transport %s: bad hostname %s: %v", name, t.RemoteHostname, err)
924 for name, t := range c.Transports {
926 if t.Submissions != nil {
928 checkTransportSMTP(name, true, t.Submissions)
930 if t.Submission != nil {
932 checkTransportSMTP(name, false, t.Submission)
936 checkTransportSMTP(name, false, t.SMTP)
940 checkTransportSocks(name, t.Socks)
943 addErrorf("transport %s: cannot have multiple methods in a transport", name)
947 // Load CA certificate pool.
949 if c.TLS.CA.AdditionalToSystem {
951 c.TLS.CertPool, err = x509.SystemCertPool()
953 addErrorf("fetching system CA cert pool: %v", err)
956 c.TLS.CertPool = x509.NewCertPool()
958 for _, certfile := range c.TLS.CA.CertFiles {
959 p := configDirPath(configFile, certfile)
960 pemBuf, err := os.ReadFile(p)
962 addErrorf("reading TLS CA cert file: %v", err)
964 } else if !c.TLS.CertPool.AppendCertsFromPEM(pemBuf) {
965 // todo: can we check more fully if we're getting some useful data back?
966 addErrorf("no CA certs added from %q", p)
973// PrepareDynamicConfig parses the dynamic config file given a static file.
974func ParseDynamicConfig(ctx context.Context, log mlog.Log, dynamicPath string, static config.Static) (c config.Dynamic, mtime time.Time, accDests map[string]AccountDestination, errs []error) {
975 addErrorf := func(format string, args ...any) {
976 errs = append(errs, fmt.Errorf(format, args...))
979 f, err := os.Open(dynamicPath)
981 addErrorf("parsing domains config: %v", err)
987 addErrorf("stat domains config: %v", err)
989 if err := sconf.Parse(f, &c); err != nil {
990 addErrorf("parsing dynamic config file: %v", err)
994 accDests, errs = prepareDynamicConfig(ctx, log, dynamicPath, static, &c)
995 return c, fi.ModTime(), accDests, errs
998func prepareDynamicConfig(ctx context.Context, log mlog.Log, dynamicPath string, static config.Static, c *config.Dynamic) (accDests map[string]AccountDestination, errs []error) {
999 addErrorf := func(format string, args ...any) {
1000 errs = append(errs, fmt.Errorf(format, args...))
1003 // Check that mailbox is in unicode NFC normalized form.
1004 checkMailboxNormf := func(mailbox string, format string, args ...any) {
1005 s := norm.NFC.String(mailbox)
1007 msg := fmt.Sprintf(format, args...)
1008 addErrorf("%s: mailbox %q is not in NFC normalized form, should be %q", msg, mailbox, s)
1012 // Validate postmaster account exists.
1013 if _, ok := c.Accounts[static.Postmaster.Account]; !ok {
1014 addErrorf("postmaster account %q does not exist", static.Postmaster.Account)
1016 checkMailboxNormf(static.Postmaster.Mailbox, "postmaster mailbox")
1018 accDests = map[string]AccountDestination{}
1020 // Validate host TLSRPT account/address.
1021 if static.HostTLSRPT.Account != "" {
1022 if _, ok := c.Accounts[static.HostTLSRPT.Account]; !ok {
1023 addErrorf("host tlsrpt account %q does not exist", static.HostTLSRPT.Account)
1025 checkMailboxNormf(static.HostTLSRPT.Mailbox, "host tlsrpt mailbox")
1027 // Localpart has been parsed already.
1029 addrFull := smtp.NewAddress(static.HostTLSRPT.ParsedLocalpart, static.HostnameDomain).String()
1030 dest := config.Destination{
1031 Mailbox: static.HostTLSRPT.Mailbox,
1032 HostTLSReports: true,
1034 accDests[addrFull] = AccountDestination{false, static.HostTLSRPT.ParsedLocalpart, static.HostTLSRPT.Account, dest}
1037 var haveSTSListener, haveWebserverListener bool
1038 for _, l := range static.Listeners {
1039 if l.MTASTSHTTPS.Enabled {
1040 haveSTSListener = true
1042 if l.WebserverHTTP.Enabled || l.WebserverHTTPS.Enabled {
1043 haveWebserverListener = true
1047 checkRoutes := func(descr string, routes []config.Route) {
1048 parseRouteDomains := func(l []string) []string {
1050 for _, e := range l {
1056 if strings.HasPrefix(e, ".") {
1060 d, err := dns.ParseDomain(e)
1062 addErrorf("%s: invalid domain %s: %v", descr, e, err)
1064 r = append(r, prefix+d.ASCII)
1069 for i := range routes {
1070 routes[i].FromDomainASCII = parseRouteDomains(routes[i].FromDomain)
1071 routes[i].ToDomainASCII = parseRouteDomains(routes[i].ToDomain)
1073 routes[i].ResolvedTransport, ok = static.Transports[routes[i].Transport]
1075 addErrorf("%s: route references undefined transport %s", descr, routes[i].Transport)
1080 checkRoutes("global routes", c.Routes)
1082 // Validate domains.
1083 for d, domain := range c.Domains {
1084 dnsdomain, err := dns.ParseDomain(d)
1086 addErrorf("bad domain %q: %s", d, err)
1087 } else if dnsdomain.Name() != d {
1088 addErrorf("domain %s must be specified in IDNA form, %s", d, dnsdomain.Name())
1091 domain.Domain = dnsdomain
1093 if domain.ClientSettingsDomain != "" {
1094 csd, err := dns.ParseDomain(domain.ClientSettingsDomain)
1096 addErrorf("bad client settings domain %q: %s", domain.ClientSettingsDomain, err)
1098 domain.ClientSettingsDNSDomain = csd
1101 for _, sign := range domain.DKIM.Sign {
1102 if _, ok := domain.DKIM.Selectors[sign]; !ok {
1103 addErrorf("selector %s for signing is missing in domain %s", sign, d)
1106 for name, sel := range domain.DKIM.Selectors {
1107 seld, err := dns.ParseDomain(name)
1109 addErrorf("bad selector %q: %s", name, err)
1110 } else if seld.Name() != name {
1111 addErrorf("selector %q must be specified in IDNA form, %q", name, seld.Name())
1115 if sel.Expiration != "" {
1116 exp, err := time.ParseDuration(sel.Expiration)
1118 addErrorf("selector %q has invalid expiration %q: %v", name, sel.Expiration, err)
1120 sel.ExpirationSeconds = int(exp / time.Second)
1124 sel.HashEffective = sel.Hash
1125 switch sel.HashEffective {
1127 sel.HashEffective = "sha256"
1129 log.Error("using sha1 with DKIM is deprecated as not secure enough, switch to sha256")
1132 addErrorf("unsupported hash %q for selector %q in domain %s", sel.HashEffective, name, d)
1135 pemBuf, err := os.ReadFile(configDirPath(dynamicPath, sel.PrivateKeyFile))
1137 addErrorf("reading private key for selector %s in domain %s: %s", name, d, err)
1140 p, _ := pem.Decode(pemBuf)
1142 addErrorf("private key for selector %s in domain %s has no PEM block", name, d)
1145 key, err := x509.ParsePKCS8PrivateKey(p.Bytes)
1147 addErrorf("parsing private key for selector %s in domain %s: %s", name, d, err)
1150 switch k := key.(type) {
1151 case *rsa.PrivateKey:
1152 if k.N.BitLen() < 1024 {
1154 // Let's help user do the right thing.
1155 addErrorf("rsa keys should be >= 1024 bits")
1158 case ed25519.PrivateKey:
1159 if sel.HashEffective != "sha256" {
1160 addErrorf("hash algorithm %q is not supported with ed25519, only sha256 is", sel.HashEffective)
1164 addErrorf("private key type %T not yet supported, at selector %s in domain %s", key, name, d)
1167 if len(sel.Headers) == 0 {
1171 // By default we seal signed headers, and we sign user-visible headers to
1172 // prevent/limit reuse of previously signed messages: All addressing fields, date
1173 // and subject, message-referencing fields, parsing instructions (content-type).
1174 sel.HeadersEffective = strings.Split("From,To,Cc,Bcc,Reply-To,References,In-Reply-To,Subject,Date,Message-Id,Content-Type", ",")
1177 for _, h := range sel.Headers {
1178 from = from || strings.EqualFold(h, "From")
1180 if strings.EqualFold(h, "DKIM-Signature") || strings.EqualFold(h, "Received") || strings.EqualFold(h, "Return-Path") {
1181 log.Error("DKIM-signing header %q is recommended against as it may be modified in transit")
1185 addErrorf("From-field must always be DKIM-signed")
1187 sel.HeadersEffective = sel.Headers
1190 domain.DKIM.Selectors[name] = sel
1193 if domain.MTASTS != nil {
1194 if !haveSTSListener {
1195 addErrorf("MTA-STS enabled for domain %q, but there is no listener for MTASTS", d)
1197 sts := domain.MTASTS
1198 if sts.PolicyID == "" {
1199 addErrorf("invalid empty MTA-STS PolicyID")
1202 case mtasts.ModeNone, mtasts.ModeTesting, mtasts.ModeEnforce:
1204 addErrorf("invalid mtasts mode %q", sts.Mode)
1208 checkRoutes("routes for domain", domain.Routes)
1210 c.Domains[d] = domain
1213 // Validate email addresses.
1214 for accName, acc := range c.Accounts {
1216 acc.DNSDomain, err = dns.ParseDomain(acc.Domain)
1218 addErrorf("parsing domain %s for account %q: %s", acc.Domain, accName, err)
1221 if strings.EqualFold(acc.RejectsMailbox, "Inbox") {
1222 addErrorf("account %q: cannot set RejectsMailbox to inbox, messages will be removed automatically from the rejects mailbox", accName)
1224 checkMailboxNormf(acc.RejectsMailbox, "account %q", accName)
1226 if acc.AutomaticJunkFlags.JunkMailboxRegexp != "" {
1227 r, err := regexp.Compile(acc.AutomaticJunkFlags.JunkMailboxRegexp)
1229 addErrorf("invalid JunkMailboxRegexp regular expression: %v", err)
1233 if acc.AutomaticJunkFlags.NeutralMailboxRegexp != "" {
1234 r, err := regexp.Compile(acc.AutomaticJunkFlags.NeutralMailboxRegexp)
1236 addErrorf("invalid NeutralMailboxRegexp regular expression: %v", err)
1238 acc.NeutralMailbox = r
1240 if acc.AutomaticJunkFlags.NotJunkMailboxRegexp != "" {
1241 r, err := regexp.Compile(acc.AutomaticJunkFlags.NotJunkMailboxRegexp)
1243 addErrorf("invalid NotJunkMailboxRegexp regular expression: %v", err)
1245 acc.NotJunkMailbox = r
1247 c.Accounts[accName] = acc
1249 // 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.
1250 replaceLocalparts := map[string]string{}
1252 for addrName, dest := range acc.Destinations {
1253 checkMailboxNormf(dest.Mailbox, "account %q, destination %q", accName, addrName)
1255 for i, rs := range dest.Rulesets {
1256 checkMailboxNormf(rs.Mailbox, "account %q, destination %q, ruleset %d", accName, addrName, i+1)
1260 if rs.SMTPMailFromRegexp != "" {
1262 r, err := regexp.Compile(rs.SMTPMailFromRegexp)
1264 addErrorf("invalid SMTPMailFrom regular expression: %v", err)
1266 c.Accounts[accName].Destinations[addrName].Rulesets[i].SMTPMailFromRegexpCompiled = r
1268 if rs.VerifiedDomain != "" {
1270 d, err := dns.ParseDomain(rs.VerifiedDomain)
1272 addErrorf("invalid VerifiedDomain: %v", err)
1274 c.Accounts[accName].Destinations[addrName].Rulesets[i].VerifiedDNSDomain = d
1277 var hdr [][2]*regexp.Regexp
1278 for k, v := range rs.HeadersRegexp {
1280 if strings.ToLower(k) != k {
1281 addErrorf("header field %q must only have lower case characters", k)
1283 if strings.ToLower(v) != v {
1284 addErrorf("header value %q must only have lower case characters", v)
1286 rk, err := regexp.Compile(k)
1288 addErrorf("invalid rule header regexp %q: %v", k, err)
1290 rv, err := regexp.Compile(v)
1292 addErrorf("invalid rule header regexp %q: %v", v, err)
1294 hdr = append(hdr, [...]*regexp.Regexp{rk, rv})
1296 c.Accounts[accName].Destinations[addrName].Rulesets[i].HeadersRegexpCompiled = hdr
1299 addErrorf("ruleset must have at least one rule")
1302 if rs.IsForward && rs.ListAllowDomain != "" {
1303 addErrorf("ruleset cannot have both IsForward and ListAllowDomain")
1306 if rs.SMTPMailFromRegexp == "" || rs.VerifiedDomain == "" {
1307 addErrorf("ruleset with IsForward must have both SMTPMailFromRegexp and VerifiedDomain too")
1310 if rs.ListAllowDomain != "" {
1311 d, err := dns.ParseDomain(rs.ListAllowDomain)
1313 addErrorf("invalid ListAllowDomain %q: %v", rs.ListAllowDomain, err)
1315 c.Accounts[accName].Destinations[addrName].Rulesets[i].ListAllowDNSDomain = d
1318 checkMailboxNormf(rs.AcceptRejectsToMailbox, "account %q, destination %q, ruleset %d, rejects mailbox", accName, addrName, i+1)
1319 if strings.EqualFold(rs.AcceptRejectsToMailbox, "inbox") {
1320 addErrorf("account %q, destination %q, ruleset %d: AcceptRejectsToMailbox cannot be set to Inbox", accName, addrName, i+1)
1324 // Catchall destination for domain.
1325 if strings.HasPrefix(addrName, "@") {
1326 d, err := dns.ParseDomain(addrName[1:])
1328 addErrorf("parsing domain %q in account %q", addrName[1:], accName)
1330 } else if _, ok := c.Domains[d.Name()]; !ok {
1331 addErrorf("unknown domain for address %q in account %q", addrName, accName)
1334 addrFull := "@" + d.Name()
1335 if _, ok := accDests[addrFull]; ok {
1336 addErrorf("duplicate canonicalized catchall destination address %s", addrFull)
1338 accDests[addrFull] = AccountDestination{true, "", accName, dest}
1342 // todo deprecated: remove support for parsing destination as just a localpart instead full address.
1343 var address smtp.Address
1344 if localpart, err := smtp.ParseLocalpart(addrName); err != nil && errors.Is(err, smtp.ErrBadLocalpart) {
1345 address, err = smtp.ParseAddress(addrName)
1347 addErrorf("invalid email address %q in account %q", addrName, accName)
1349 } else if _, ok := c.Domains[address.Domain.Name()]; !ok {
1350 addErrorf("unknown domain for address %q in account %q", addrName, accName)
1355 addErrorf("invalid localpart %q in account %q", addrName, accName)
1358 address = smtp.NewAddress(localpart, acc.DNSDomain)
1359 if _, ok := c.Domains[acc.DNSDomain.Name()]; !ok {
1360 addErrorf("unknown domain %s for account %q", acc.DNSDomain.Name(), accName)
1363 replaceLocalparts[addrName] = address.Pack(true)
1366 origLP := address.Localpart
1367 dc := c.Domains[address.Domain.Name()]
1368 if lp, err := CanonicalLocalpart(address.Localpart, dc); err != nil {
1369 addErrorf("canonicalizing localpart %s: %v", address.Localpart, err)
1370 } else if dc.LocalpartCatchallSeparator != "" && strings.Contains(string(address.Localpart), dc.LocalpartCatchallSeparator) {
1371 addErrorf("localpart of address %s includes domain catchall separator %s", address, dc.LocalpartCatchallSeparator)
1373 address.Localpart = lp
1375 addrFull := address.Pack(true)
1376 if _, ok := accDests[addrFull]; ok {
1377 addErrorf("duplicate canonicalized destination address %s", addrFull)
1379 accDests[addrFull] = AccountDestination{false, origLP, accName, dest}
1382 for lp, addr := range replaceLocalparts {
1383 dest, ok := acc.Destinations[lp]
1385 addErrorf("could not find localpart %q to replace with address in destinations", lp)
1387 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`,
1388 slog.Any("localpart", lp),
1389 slog.Any("address", addr),
1390 slog.String("account", accName))
1391 acc.Destinations[addr] = dest
1392 delete(acc.Destinations, lp)
1396 checkRoutes("routes for account", acc.Routes)
1399 // Set DMARC destinations.
1400 for d, domain := range c.Domains {
1401 dmarc := domain.DMARC
1405 if _, ok := c.Accounts[dmarc.Account]; !ok {
1406 addErrorf("DMARC account %q does not exist", dmarc.Account)
1408 lp, err := smtp.ParseLocalpart(dmarc.Localpart)
1410 addErrorf("invalid DMARC localpart %q: %s", dmarc.Localpart, err)
1412 if lp.IsInternational() {
1414 addErrorf("DMARC localpart %q is an internationalized address, only conventional ascii-only address possible for interopability", lp)
1416 addrdom := domain.Domain
1417 if dmarc.Domain != "" {
1418 addrdom, err = dns.ParseDomain(dmarc.Domain)
1420 addErrorf("DMARC domain %q: %s", dmarc.Domain, err)
1421 } else if _, ok := c.Domains[addrdom.Name()]; !ok {
1422 addErrorf("unknown domain %q for DMARC address in domain %q", dmarc.Domain, d)
1426 domain.DMARC.ParsedLocalpart = lp
1427 domain.DMARC.DNSDomain = addrdom
1428 c.Domains[d] = domain
1429 addrFull := smtp.NewAddress(lp, addrdom).String()
1430 dest := config.Destination{
1431 Mailbox: dmarc.Mailbox,
1434 checkMailboxNormf(dmarc.Mailbox, "DMARC mailbox for account %q", dmarc.Account)
1435 accDests[addrFull] = AccountDestination{false, lp, dmarc.Account, dest}
1438 // Set TLSRPT destinations.
1439 for d, domain := range c.Domains {
1440 tlsrpt := domain.TLSRPT
1444 if _, ok := c.Accounts[tlsrpt.Account]; !ok {
1445 addErrorf("TLSRPT account %q does not exist", tlsrpt.Account)
1447 lp, err := smtp.ParseLocalpart(tlsrpt.Localpart)
1449 addErrorf("invalid TLSRPT localpart %q: %s", tlsrpt.Localpart, err)
1451 if lp.IsInternational() {
1452 // Does not appear documented in
../rfc/8460, but similar to DMARC it makes sense
1453 // to keep this ascii-only addresses.
1454 addErrorf("TLSRPT localpart %q is an internationalized address, only conventional ascii-only address allowed for interopability", lp)
1456 addrdom := domain.Domain
1457 if tlsrpt.Domain != "" {
1458 addrdom, err = dns.ParseDomain(tlsrpt.Domain)
1460 addErrorf("TLSRPT domain %q: %s", tlsrpt.Domain, err)
1461 } else if _, ok := c.Domains[addrdom.Name()]; !ok {
1462 addErrorf("unknown domain %q for TLSRPT address in domain %q", tlsrpt.Domain, d)
1466 domain.TLSRPT.ParsedLocalpart = lp
1467 domain.TLSRPT.DNSDomain = addrdom
1468 c.Domains[d] = domain
1469 addrFull := smtp.NewAddress(lp, addrdom).String()
1470 dest := config.Destination{
1471 Mailbox: tlsrpt.Mailbox,
1472 DomainTLSReports: true,
1474 checkMailboxNormf(tlsrpt.Mailbox, "TLSRPT mailbox for account %q", tlsrpt.Account)
1475 accDests[addrFull] = AccountDestination{false, lp, tlsrpt.Account, dest}
1478 // Check webserver configs.
1479 if (len(c.WebDomainRedirects) > 0 || len(c.WebHandlers) > 0) && !haveWebserverListener {
1480 addErrorf("WebDomainRedirects or WebHandlers configured but no listener with WebserverHTTP or WebserverHTTPS enabled")
1483 c.WebDNSDomainRedirects = map[dns.Domain]dns.Domain{}
1484 for from, to := range c.WebDomainRedirects {
1485 fromdom, err := dns.ParseDomain(from)
1487 addErrorf("parsing domain for redirect %s: %v", from, err)
1489 todom, err := dns.ParseDomain(to)
1491 addErrorf("parsing domain for redirect %s: %v", to, err)
1492 } else if fromdom == todom {
1493 addErrorf("will not redirect domain %s to itself", todom)
1495 var zerodom dns.Domain
1496 if _, ok := c.WebDNSDomainRedirects[fromdom]; ok && fromdom != zerodom {
1497 addErrorf("duplicate redirect domain %s", from)
1499 c.WebDNSDomainRedirects[fromdom] = todom
1502 for i := range c.WebHandlers {
1503 wh := &c.WebHandlers[i]
1505 if wh.LogName == "" {
1506 wh.Name = fmt.Sprintf("%d", i)
1508 wh.Name = wh.LogName
1511 dom, err := dns.ParseDomain(wh.Domain)
1513 addErrorf("webhandler %s %s: parsing domain: %v", wh.Domain, wh.PathRegexp, err)
1517 if !strings.HasPrefix(wh.PathRegexp, "^") {
1518 addErrorf("webhandler %s %s: path regexp must start with a ^", wh.Domain, wh.PathRegexp)
1520 re, err := regexp.Compile(wh.PathRegexp)
1522 addErrorf("webhandler %s %s: compiling regexp: %v", wh.Domain, wh.PathRegexp, err)
1527 if wh.WebStatic != nil {
1530 if ws.StripPrefix != "" && !strings.HasPrefix(ws.StripPrefix, "/") {
1531 addErrorf("webstatic %s %s: prefix to strip %s must start with a slash", wh.Domain, wh.PathRegexp, ws.StripPrefix)
1533 for k := range ws.ResponseHeaders {
1535 k := strings.TrimSpace(xk)
1536 if k != xk || k == "" {
1537 addErrorf("webstatic %s %s: bad header %q", wh.Domain, wh.PathRegexp, xk)
1541 if wh.WebRedirect != nil {
1543 wr := wh.WebRedirect
1544 if wr.BaseURL != "" {
1545 u, err := url.Parse(wr.BaseURL)
1547 addErrorf("webredirect %s %s: parsing redirect url %s: %v", wh.Domain, wh.PathRegexp, wr.BaseURL, err)
1553 addErrorf("webredirect %s %s: BaseURL must have empty path", wh.Domain, wh.PathRegexp, wr.BaseURL)
1557 if wr.OrigPathRegexp != "" && wr.ReplacePath != "" {
1558 re, err := regexp.Compile(wr.OrigPathRegexp)
1560 addErrorf("webredirect %s %s: compiling regexp %s: %v", wh.Domain, wh.PathRegexp, wr.OrigPathRegexp, err)
1563 } else if wr.OrigPathRegexp != "" || wr.ReplacePath != "" {
1564 addErrorf("webredirect %s %s: must have either both OrigPathRegexp and ReplacePath, or neither", wh.Domain, wh.PathRegexp)
1565 } else if wr.BaseURL == "" {
1566 addErrorf("webredirect %s %s: must at least one of BaseURL and OrigPathRegexp+ReplacePath", wh.Domain, wh.PathRegexp)
1568 if wr.StatusCode != 0 && (wr.StatusCode < 300 || wr.StatusCode >= 400) {
1569 addErrorf("webredirect %s %s: invalid redirect status code %d", wh.Domain, wh.PathRegexp, wr.StatusCode)
1572 if wh.WebForward != nil {
1575 u, err := url.Parse(wf.URL)
1577 addErrorf("webforward %s %s: parsing url %s: %v", wh.Domain, wh.PathRegexp, wf.URL, err)
1581 for k := range wf.ResponseHeaders {
1583 k := strings.TrimSpace(xk)
1584 if k != xk || k == "" {
1585 addErrorf("webforward %s %s: bad header %q", wh.Domain, wh.PathRegexp, xk)
1590 addErrorf("webhandler %s %s: must have exactly one handler, not %d", wh.Domain, wh.PathRegexp, n)
1597func loadPrivateKeyFile(keyPath string) (crypto.Signer, error) {
1598 keyBuf, err := os.ReadFile(keyPath)
1600 return nil, fmt.Errorf("reading host private key: %v", err)
1602 b, _ := pem.Decode(keyBuf)
1604 return nil, fmt.Errorf("parsing pem block for private key: %v", err)
1609 privKey, err = x509.ParsePKCS8PrivateKey(b.Bytes)
1610 case "RSA PRIVATE KEY":
1611 privKey, err = x509.ParsePKCS1PrivateKey(b.Bytes)
1612 case "EC PRIVATE KEY":
1613 privKey, err = x509.ParseECPrivateKey(b.Bytes)
1615 err = fmt.Errorf("unknown pem type %q", b.Type)
1618 return nil, fmt.Errorf("parsing private key: %v", err)
1620 if k, ok := privKey.(crypto.Signer); ok {
1623 return nil, fmt.Errorf("parsed private key not a crypto.Signer, but %T", privKey)
1626func loadTLSKeyCerts(configFile, kind string, ctls *config.TLS) error {
1627 certs := []tls.Certificate{}
1628 for _, kp := range ctls.KeyCerts {
1629 certPath := configDirPath(configFile, kp.CertFile)
1630 keyPath := configDirPath(configFile, kp.KeyFile)
1631 cert, err := loadX509KeyPairPrivileged(certPath, keyPath)
1633 return fmt.Errorf("tls config for %q: parsing x509 key pair: %v", kind, err)
1635 certs = append(certs, cert)
1637 ctls.Config = &tls.Config{
1638 Certificates: certs,
1643// load x509 key/cert files from file descriptor possibly passed in by privileged
1645func loadX509KeyPairPrivileged(certPath, keyPath string) (tls.Certificate, error) {
1646 certBuf, err := readFilePrivileged(certPath)
1648 return tls.Certificate{}, fmt.Errorf("reading tls certificate: %v", err)
1650 keyBuf, err := readFilePrivileged(keyPath)
1652 return tls.Certificate{}, fmt.Errorf("reading tls key: %v", err)
1654 return tls.X509KeyPair(certBuf, keyBuf)
1657// like os.ReadFile, but open privileged file possibly passed in by root process.
1658func readFilePrivileged(path string) ([]byte, error) {
1659 f, err := OpenPrivileged(path)
1664 return io.ReadAll(f)