10 cryptorand "crypto/rand"
34 "golang.org/x/text/unicode/norm"
36 "github.com/mjl-/autocert"
38 "github.com/mjl-/sconf"
40 "github.com/mjl-/mox/autotls"
41 "github.com/mjl-/mox/config"
42 "github.com/mjl-/mox/dkim"
43 "github.com/mjl-/mox/dns"
44 "github.com/mjl-/mox/message"
45 "github.com/mjl-/mox/mlog"
46 "github.com/mjl-/mox/moxio"
47 "github.com/mjl-/mox/mtasts"
48 "github.com/mjl-/mox/smtp"
51var pkglog = mlog.New("mox", nil)
53// Pedantic enables stricter parsing.
56// Config paths are set early in program startup. They will point to files in
59 ConfigStaticPath string
60 ConfigDynamicPath string
61 Conf = Config{Log: map[string]slog.Level{"": slog.LevelError}}
64// Config as used in the code, a processed version of what is in the config file.
66// Use methods to lookup a domain/account/address in the dynamic configuration.
68 Static config.Static // Does not change during the lifetime of a running instance.
70 logMutex sync.Mutex // For accessing the log levels.
71 Log map[string]slog.Level
73 dynamicMutex sync.Mutex
74 Dynamic config.Dynamic // Can only be accessed directly by tests. Use methods on Config for locked access.
75 dynamicMtime time.Time
76 DynamicLastCheck time.Time // For use by quickstart only to skip checks.
77 // From canonical full address (localpart@domain, lower-cased when
78 // case-insensitive, stripped of catchall separator) to account and address.
79 // Domains are IDNA names in utf8.
80 accountDestinations map[string]AccountDestination
83type AccountDestination struct {
84 Catchall bool // If catchall destination for its domain.
85 Localpart smtp.Localpart // In original casing as written in config file.
87 Destination config.Destination
90// LogLevelSet sets a new log level for pkg. An empty pkg sets the default log
91// value that is used if no explicit log level is configured for a package.
92// This change is ephemeral, no config file is changed.
93func (c *Config) LogLevelSet(log mlog.Log, pkg string, level slog.Level) {
95 defer c.logMutex.Unlock()
96 l := c.copyLogLevels()
99 log.Print("log level changed", slog.String("pkg", pkg), slog.Any("level", mlog.LevelStrings[level]))
100 mlog.SetConfig(c.Log)
103// LogLevelRemove removes a configured log level for a package.
104func (c *Config) LogLevelRemove(log mlog.Log, pkg string) {
106 defer c.logMutex.Unlock()
107 l := c.copyLogLevels()
110 log.Print("log level cleared", slog.String("pkg", pkg))
111 mlog.SetConfig(c.Log)
114// copyLogLevels returns a copy of c.Log, for modifications.
115// must be called with log lock held.
116func (c *Config) copyLogLevels() map[string]slog.Level {
117 m := map[string]slog.Level{}
118 for pkg, level := range c.Log {
124// LogLevels returns a copy of the current log levels.
125func (c *Config) LogLevels() map[string]slog.Level {
127 defer c.logMutex.Unlock()
128 return c.copyLogLevels()
131func (c *Config) withDynamicLock(fn func()) {
132 c.dynamicMutex.Lock()
133 defer c.dynamicMutex.Unlock()
135 if now.Sub(c.DynamicLastCheck) > time.Second {
136 c.DynamicLastCheck = now
137 if fi, err := os.Stat(ConfigDynamicPath); err != nil {
138 pkglog.Errorx("stat domains config", err)
139 } else if !fi.ModTime().Equal(c.dynamicMtime) {
140 if errs := c.loadDynamic(); len(errs) > 0 {
141 pkglog.Errorx("loading domains config", errs[0], slog.Any("errors", errs))
143 pkglog.Info("domains config reloaded")
144 c.dynamicMtime = fi.ModTime()
151// must be called with dynamic lock held.
152func (c *Config) loadDynamic() []error {
153 d, mtime, accDests, err := ParseDynamicConfig(context.Background(), pkglog, ConfigDynamicPath, c.Static)
158 c.dynamicMtime = mtime
159 c.accountDestinations = accDests
160 c.allowACMEHosts(pkglog, true)
164func (c *Config) Domains() (l []string) {
165 c.withDynamicLock(func() {
166 for name := range c.Dynamic.Domains {
170 sort.Slice(l, func(i, j int) bool {
176func (c *Config) Accounts() (l []string) {
177 c.withDynamicLock(func() {
178 for name := range c.Dynamic.Accounts {
185// DomainLocalparts returns a mapping of encoded localparts to account names for a
186// domain. An empty localpart is a catchall destination for a domain.
187func (c *Config) DomainLocalparts(d dns.Domain) map[string]string {
188 suffix := "@" + d.Name()
189 m := map[string]string{}
190 c.withDynamicLock(func() {
191 for addr, ad := range c.accountDestinations {
192 if strings.HasSuffix(addr, suffix) {
196 m[ad.Localpart.String()] = ad.Account
204func (c *Config) Domain(d dns.Domain) (dom config.Domain, ok bool) {
205 c.withDynamicLock(func() {
206 dom, ok = c.Dynamic.Domains[d.Name()]
211func (c *Config) Account(name string) (acc config.Account, ok bool) {
212 c.withDynamicLock(func() {
213 acc, ok = c.Dynamic.Accounts[name]
218func (c *Config) AccountDestination(addr string) (accDests AccountDestination, ok bool) {
219 c.withDynamicLock(func() {
220 accDests, ok = c.accountDestinations[addr]
225func (c *Config) WebServer() (r map[dns.Domain]dns.Domain, l []config.WebHandler) {
226 c.withDynamicLock(func() {
227 r = c.Dynamic.WebDNSDomainRedirects
228 l = c.Dynamic.WebHandlers
233func (c *Config) Routes(accountName string, domain dns.Domain) (accountRoutes, domainRoutes, globalRoutes []config.Route) {
234 c.withDynamicLock(func() {
235 acc := c.Dynamic.Accounts[accountName]
236 accountRoutes = acc.Routes
238 dom := c.Dynamic.Domains[domain.Name()]
239 domainRoutes = dom.Routes
241 globalRoutes = c.Dynamic.Routes
246func (c *Config) MonitorDNSBLs() (zones []dns.Domain) {
247 c.withDynamicLock(func() {
248 zones = c.Dynamic.MonitorDNSBLZones
253func (c *Config) allowACMEHosts(log mlog.Log, checkACMEHosts bool) {
254 for _, l := range c.Static.Listeners {
255 if l.TLS == nil || l.TLS.ACME == "" {
259 m := c.Static.ACME[l.TLS.ACME].Manager
260 hostnames := map[dns.Domain]struct{}{}
262 hostnames[c.Static.HostnameDomain] = struct{}{}
263 if l.HostnameDomain.ASCII != "" {
264 hostnames[l.HostnameDomain] = struct{}{}
267 for _, dom := range c.Dynamic.Domains {
268 // Do not allow TLS certificates for domains for which we only accept DMARC/TLS
269 // reports as external party.
274 if l.AutoconfigHTTPS.Enabled && !l.AutoconfigHTTPS.NonTLS {
275 if d, err := dns.ParseDomain("autoconfig." + dom.Domain.ASCII); err != nil {
276 log.Errorx("parsing autoconfig domain", err, slog.Any("domain", dom.Domain))
278 hostnames[d] = struct{}{}
282 if l.MTASTSHTTPS.Enabled && dom.MTASTS != nil && !l.MTASTSHTTPS.NonTLS {
283 d, err := dns.ParseDomain("mta-sts." + dom.Domain.ASCII)
285 log.Errorx("parsing mta-sts domain", err, slog.Any("domain", dom.Domain))
287 hostnames[d] = struct{}{}
291 if dom.ClientSettingsDomain != "" {
292 hostnames[dom.ClientSettingsDNSDomain] = struct{}{}
296 if l.WebserverHTTPS.Enabled {
297 for from := range c.Dynamic.WebDNSDomainRedirects {
298 hostnames[from] = struct{}{}
300 for _, wh := range c.Dynamic.WebHandlers {
301 hostnames[wh.DNSDomain] = struct{}{}
305 public := c.Static.Listeners["public"]
307 if len(public.NATIPs) > 0 {
313 m.SetAllowedHostnames(log, dns.StrictResolver{Pkg: "autotls", Log: log.Logger}, hostnames, ips, checkACMEHosts)
317// 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.
319// must be called with lock held.
320func writeDynamic(ctx context.Context, log mlog.Log, c config.Dynamic) error {
321 accDests, errs := prepareDynamicConfig(ctx, log, ConfigDynamicPath, Conf.Static, &c)
327 err := sconf.Write(&b, c)
331 f, err := os.OpenFile(ConfigDynamicPath, os.O_WRONLY, 0660)
338 log.Check(err, "closing file after error")
342 if _, err := f.Write(buf); err != nil {
343 return fmt.Errorf("write domains.conf: %v", err)
345 if err := f.Truncate(int64(len(buf))); err != nil {
346 return fmt.Errorf("truncate domains.conf after write: %v", err)
348 if err := f.Sync(); err != nil {
349 return fmt.Errorf("sync domains.conf after write: %v", err)
351 if err := moxio.SyncDir(log, filepath.Dir(ConfigDynamicPath)); err != nil {
352 return fmt.Errorf("sync dir of domains.conf after write: %v", err)
357 return fmt.Errorf("stat after writing domains.conf: %v", err)
360 if err := f.Close(); err != nil {
361 return fmt.Errorf("close written domains.conf: %v", err)
365 Conf.dynamicMtime = fi.ModTime()
366 Conf.DynamicLastCheck = time.Now()
368 Conf.accountDestinations = accDests
370 Conf.allowACMEHosts(log, true)
375// MustLoadConfig loads the config, quitting on errors.
376func MustLoadConfig(doLoadTLSKeyCerts, checkACMEHosts bool) {
377 errs := LoadConfig(context.Background(), pkglog, doLoadTLSKeyCerts, checkACMEHosts)
379 pkglog.Error("loading config file: multiple errors")
380 for _, err := range errs {
381 pkglog.Errorx("config error", err)
383 pkglog.Fatal("stopping after multiple config errors")
384 } else if len(errs) == 1 {
385 pkglog.Fatalx("loading config file", errs[0])
389// LoadConfig attempts to parse and load a config, returning any errors
391func LoadConfig(ctx context.Context, log mlog.Log, doLoadTLSKeyCerts, checkACMEHosts bool) []error {
392 Shutdown, ShutdownCancel = context.WithCancel(context.Background())
393 Context, ContextCancel = context.WithCancel(context.Background())
395 c, errs := ParseConfig(ctx, log, ConfigStaticPath, false, doLoadTLSKeyCerts, checkACMEHosts)
400 mlog.SetConfig(c.Log)
405// SetConfig sets a new config. Not to be used during normal operation.
406func SetConfig(c *Config) {
407 // Cannot just assign *c to Conf, it would copy the mutex.
408 Conf = Config{c.Static, sync.Mutex{}, c.Log, sync.Mutex{}, c.Dynamic, c.dynamicMtime, c.DynamicLastCheck, c.accountDestinations}
410 // If we have non-standard CA roots, use them for all HTTPS requests.
411 if Conf.Static.TLS.CertPool != nil {
412 http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{
413 RootCAs: Conf.Static.TLS.CertPool,
417 SetPedantic(c.Static.Pedantic)
420// Set pedantic in all packages.
421func SetPedantic(p bool) {
429// ParseConfig parses the static config at path p. If checkOnly is true, no changes
430// are made, such as registering ACME identities. If doLoadTLSKeyCerts is true,
431// the TLS KeyCerts configuration is loaded and checked. This is used during the
432// quickstart in the case the user is going to provide their own certificates.
433// If checkACMEHosts is true, the hosts allowed for acme are compared with the
434// explicitly configured ips we are listening on.
435func ParseConfig(ctx context.Context, log mlog.Log, p string, checkOnly, doLoadTLSKeyCerts, checkACMEHosts bool) (c *Config, errs []error) {
437 Static: config.Static{
444 if os.IsNotExist(err) && os.Getenv("MOXCONF") == "" {
445 return nil, []error{fmt.Errorf("open config file: %v (hint: use mox -config ... or set MOXCONF=...)", err)}
447 return nil, []error{fmt.Errorf("open config file: %v", err)}
450 if err := sconf.Parse(f, &c.Static); err != nil {
451 return nil, []error{fmt.Errorf("parsing %s%v", p, err)}
454 if xerrs := PrepareStaticConfig(ctx, log, p, c, checkOnly, doLoadTLSKeyCerts); len(xerrs) > 0 {
458 pp := filepath.Join(filepath.Dir(p), "domains.conf")
459 c.Dynamic, c.dynamicMtime, c.accountDestinations, errs = ParseDynamicConfig(ctx, log, pp, c.Static)
462 c.allowACMEHosts(log, checkACMEHosts)
468// PrepareStaticConfig parses the static config file and prepares data structures
469// for starting mox. If checkOnly is set no substantial changes are made, like
470// creating an ACME registration.
471func PrepareStaticConfig(ctx context.Context, log mlog.Log, configFile string, conf *Config, checkOnly, doLoadTLSKeyCerts bool) (errs []error) {
472 addErrorf := func(format string, args ...any) {
473 errs = append(errs, fmt.Errorf(format, args...))
478 // check that mailbox is in unicode NFC normalized form.
479 checkMailboxNormf := func(mailbox string, format string, args ...any) {
480 s := norm.NFC.String(mailbox)
482 msg := fmt.Sprintf(format, args...)
483 addErrorf("%s: mailbox %q is not in NFC normalized form, should be %q", msg, mailbox, s)
487 // Post-process logging config.
488 if logLevel, ok := mlog.Levels[c.LogLevel]; ok {
489 conf.Log = map[string]slog.Level{"": logLevel}
491 addErrorf("invalid log level %q", c.LogLevel)
493 for pkg, s := range c.PackageLogLevels {
494 if logLevel, ok := mlog.Levels[s]; ok {
495 conf.Log[pkg] = logLevel
497 addErrorf("invalid package log level %q", s)
504 u, err := user.Lookup(c.User)
506 uid, err := strconv.ParseUint(c.User, 10, 32)
508 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)
510 // We assume the same gid as uid.
515 if uid, err := strconv.ParseUint(u.Uid, 10, 32); err != nil {
516 addErrorf("parsing uid %s: %v", u.Uid, err)
520 if gid, err := strconv.ParseUint(u.Gid, 10, 32); err != nil {
521 addErrorf("parsing gid %s: %v", u.Gid, err)
527 hostname, err := dns.ParseDomain(c.Hostname)
529 addErrorf("parsing hostname: %s", err)
530 } else if hostname.Name() != c.Hostname {
531 addErrorf("hostname must be in unicode form %q instead of %q", hostname.Name(), c.Hostname)
533 c.HostnameDomain = hostname
535 if c.HostTLSRPT.Account != "" {
536 tlsrptLocalpart, err := smtp.ParseLocalpart(c.HostTLSRPT.Localpart)
538 addErrorf("invalid localpart %q for host tlsrpt: %v", c.HostTLSRPT.Localpart, err)
539 } else if tlsrptLocalpart.IsInternational() {
540 // Does not appear documented in
../rfc/8460, but similar to DMARC it makes sense
541 // to keep this ascii-only addresses.
542 addErrorf("host TLSRPT localpart %q is an internationalized address, only conventional ascii-only address allowed for interopability", tlsrptLocalpart)
544 c.HostTLSRPT.ParsedLocalpart = tlsrptLocalpart
547 // Return private key for host name for use with an ACME. Used to return the same
548 // private key as pre-generated for use with DANE, with its public key in DNS.
549 // We only use this key for Listener's that have this ACME configured, and for
550 // which the effective listener host name (either specific to the listener, or the
551 // global name) is requested. Other host names can get a fresh private key, they
552 // don't appear in DANE records.
554 // - run 0: only use listener with explicitly matching host name in listener
555 // (default quickstart config does not set it).
556 // - run 1: only look at public listener (and host matching mox host name)
557 // - run 2: all listeners (and host matching mox host name)
558 findACMEHostPrivateKey := func(acmeName, host string, keyType autocert.KeyType, run int) crypto.Signer {
559 for listenerName, l := range Conf.Static.Listeners {
560 if l.TLS == nil || l.TLS.ACME != acmeName {
563 if run == 0 && host != l.HostnameDomain.ASCII {
566 if run == 1 && listenerName != "public" || host != Conf.Static.HostnameDomain.ASCII {
570 case autocert.KeyRSA2048:
571 if len(l.TLS.HostPrivateRSA2048Keys) == 0 {
574 return l.TLS.HostPrivateRSA2048Keys[0]
575 case autocert.KeyECDSAP256:
576 if len(l.TLS.HostPrivateECDSAP256Keys) == 0 {
579 return l.TLS.HostPrivateECDSAP256Keys[0]
586 // Make a function for an autocert.Manager.GetPrivateKey, using findACMEHostPrivateKey.
587 makeGetPrivateKey := func(acmeName string) func(host string, keyType autocert.KeyType) (crypto.Signer, error) {
588 return func(host string, keyType autocert.KeyType) (crypto.Signer, error) {
589 key := findACMEHostPrivateKey(acmeName, host, keyType, 0)
591 key = findACMEHostPrivateKey(acmeName, host, keyType, 1)
594 key = findACMEHostPrivateKey(acmeName, host, keyType, 2)
597 log.Debug("found existing private key for certificate for host",
598 slog.String("acmename", acmeName),
599 slog.String("host", host),
600 slog.Any("keytype", keyType))
603 log.Debug("generating new private key for certificate for host",
604 slog.String("acmename", acmeName),
605 slog.String("host", host),
606 slog.Any("keytype", keyType))
608 case autocert.KeyRSA2048:
609 return rsa.GenerateKey(cryptorand.Reader, 2048)
610 case autocert.KeyECDSAP256:
611 return ecdsa.GenerateKey(elliptic.P256(), cryptorand.Reader)
613 return nil, fmt.Errorf("unrecognized requested key type %v", keyType)
617 for name, acme := range c.ACME {
620 if acme.ExternalAccountBinding != nil {
621 eabKeyID = acme.ExternalAccountBinding.KeyID
622 p := configDirPath(configFile, acme.ExternalAccountBinding.KeyFile)
623 buf, err := os.ReadFile(p)
625 addErrorf("reading external account binding key for acme provider %q: %s", name, err)
627 dec := make([]byte, base64.RawURLEncoding.DecodedLen(len(buf)))
628 n, err := base64.RawURLEncoding.Decode(dec, buf)
630 addErrorf("parsing external account binding key as base64 for acme provider %q: %s", name, err)
641 acmeDir := dataDirPath(configFile, c.DataDir, "acme")
642 os.MkdirAll(acmeDir, 0770)
643 manager, err := autotls.Load(name, acmeDir, acme.ContactEmail, acme.DirectoryURL, eabKeyID, eabKey, makeGetPrivateKey(name), Shutdown.Done())
645 addErrorf("loading ACME identity for %q: %s", name, err)
647 acme.Manager = manager
649 // Help configurations from older quickstarts.
650 if acme.IssuerDomainName == "" && acme.DirectoryURL == "https://acme-v02.api.letsencrypt.org/directory" {
651 acme.IssuerDomainName = "letsencrypt.org"
657 var haveUnspecifiedSMTPListener bool
658 for name, l := range c.Listeners {
659 if l.Hostname != "" {
660 d, err := dns.ParseDomain(l.Hostname)
662 addErrorf("bad listener hostname %q: %s", l.Hostname, err)
667 if l.TLS.ACME != "" && len(l.TLS.KeyCerts) != 0 {
668 addErrorf("listener %q: cannot have ACME and static key/certificates", name)
669 } else if l.TLS.ACME != "" {
670 acme, ok := c.ACME[l.TLS.ACME]
672 addErrorf("listener %q: unknown ACME provider %q", name, l.TLS.ACME)
675 // If only checking or with missing ACME definition, we don't have an acme manager,
676 // so set an empty tls config to continue.
677 var tlsconfig *tls.Config
678 if checkOnly || acme.Manager == nil {
679 tlsconfig = &tls.Config{}
681 tlsconfig = acme.Manager.TLSConfig.Clone()
682 l.TLS.ACMEConfig = acme.Manager.ACMETLSConfig
684 // SMTP STARTTLS connections are commonly made without SNI, because certificates
685 // often aren't verified.
686 hostname := c.HostnameDomain
687 if l.Hostname != "" {
688 hostname = l.HostnameDomain
690 getCert := tlsconfig.GetCertificate
691 tlsconfig.GetCertificate = func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
692 if hello.ServerName == "" {
693 hello.ServerName = hostname.ASCII
695 return getCert(hello)
698 l.TLS.Config = tlsconfig
699 } else if len(l.TLS.KeyCerts) != 0 {
700 if doLoadTLSKeyCerts {
701 if err := loadTLSKeyCerts(configFile, "listener "+name, l.TLS); err != nil {
706 addErrorf("listener %q: cannot have TLS config without ACME and without static keys/certificates", name)
708 for _, privKeyFile := range l.TLS.HostPrivateKeyFiles {
709 keyPath := configDirPath(configFile, privKeyFile)
710 privKey, err := loadPrivateKeyFile(keyPath)
712 addErrorf("listener %q: parsing host private key for DANE and ACME certificates: %v", name, err)
715 switch k := privKey.(type) {
716 case *rsa.PrivateKey:
717 if k.N.BitLen() != 2048 {
718 log.Error("need rsa key with 2048 bits, for host private key for DANE/ACME certificates, ignoring",
719 slog.String("listener", name),
720 slog.String("file", keyPath),
721 slog.Int("bits", k.N.BitLen()))
724 l.TLS.HostPrivateRSA2048Keys = append(l.TLS.HostPrivateRSA2048Keys, k)
725 case *ecdsa.PrivateKey:
726 if k.Curve != elliptic.P256() {
727 log.Error("unrecognized ecdsa curve for host private key for DANE/ACME certificates, ignoring", slog.String("listener", name), slog.String("file", keyPath))
730 l.TLS.HostPrivateECDSAP256Keys = append(l.TLS.HostPrivateECDSAP256Keys, k)
732 log.Error("unrecognized key type for host private key for DANE/ACME certificates, ignoring",
733 slog.String("listener", name),
734 slog.String("file", keyPath),
735 slog.String("keytype", fmt.Sprintf("%T", privKey)))
739 if l.TLS.ACME != "" && (len(l.TLS.HostPrivateRSA2048Keys) == 0) != (len(l.TLS.HostPrivateECDSAP256Keys) == 0) {
740 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")
744 var minVersion uint16 = tls.VersionTLS12
745 if l.TLS.MinVersion != "" {
746 versions := map[string]uint16{
747 "TLSv1.0": tls.VersionTLS10,
748 "TLSv1.1": tls.VersionTLS11,
749 "TLSv1.2": tls.VersionTLS12,
750 "TLSv1.3": tls.VersionTLS13,
752 v, ok := versions[l.TLS.MinVersion]
754 addErrorf("listener %q: unknown TLS mininum version %q", name, l.TLS.MinVersion)
758 if l.TLS.Config != nil {
759 l.TLS.Config.MinVersion = minVersion
761 if l.TLS.ACMEConfig != nil {
762 l.TLS.ACMEConfig.MinVersion = minVersion
765 var needsTLS []string
766 needtls := func(s string, v bool) {
768 needsTLS = append(needsTLS, s)
771 needtls("IMAPS", l.IMAPS.Enabled)
772 needtls("SMTP", l.SMTP.Enabled && !l.SMTP.NoSTARTTLS)
773 needtls("Submissions", l.Submissions.Enabled)
774 needtls("Submission", l.Submission.Enabled && !l.Submission.NoRequireSTARTTLS)
775 needtls("AccountHTTPS", l.AccountHTTPS.Enabled)
776 needtls("AdminHTTPS", l.AdminHTTPS.Enabled)
777 needtls("AutoconfigHTTPS", l.AutoconfigHTTPS.Enabled && !l.AutoconfigHTTPS.NonTLS)
778 needtls("MTASTSHTTPS", l.MTASTSHTTPS.Enabled && !l.MTASTSHTTPS.NonTLS)
779 needtls("WebserverHTTPS", l.WebserverHTTPS.Enabled)
780 if len(needsTLS) > 0 {
781 addErrorf("listener %q does not specify tls config, but requires tls for %s", name, strings.Join(needsTLS, ", "))
784 if l.AutoconfigHTTPS.Enabled && l.MTASTSHTTPS.Enabled && l.AutoconfigHTTPS.Port == l.MTASTSHTTPS.Port && l.AutoconfigHTTPS.NonTLS != l.MTASTSHTTPS.NonTLS {
785 addErrorf("listener %q tries to enable autoconfig and mta-sts enabled on same port but with both http and https", name)
789 haveUnspecifiedSMTPListener = true
791 for _, ipstr := range l.IPs {
792 ip := net.ParseIP(ipstr)
794 addErrorf("listener %q has invalid IP %q", name, ipstr)
797 if ip.IsUnspecified() {
798 haveUnspecifiedSMTPListener = true
801 if len(c.SpecifiedSMTPListenIPs) >= 2 {
802 haveUnspecifiedSMTPListener = true
803 } else if len(c.SpecifiedSMTPListenIPs) > 0 && (c.SpecifiedSMTPListenIPs[0].To4() == nil) == (ip.To4() == nil) {
804 haveUnspecifiedSMTPListener = true
806 c.SpecifiedSMTPListenIPs = append(c.SpecifiedSMTPListenIPs, ip)
810 for _, s := range l.SMTP.DNSBLs {
811 d, err := dns.ParseDomain(s)
813 addErrorf("listener %q has invalid DNSBL zone %q", name, s)
816 l.SMTP.DNSBLZones = append(l.SMTP.DNSBLZones, d)
818 if l.IPsNATed && len(l.NATIPs) > 0 {
819 addErrorf("listener %q has both IPsNATed and NATIPs (remove deprecated IPsNATed)", name)
821 for _, ipstr := range l.NATIPs {
822 ip := net.ParseIP(ipstr)
824 addErrorf("listener %q has invalid ip %q", name, ipstr)
825 } else if ip.IsUnspecified() || ip.IsLoopback() {
826 addErrorf("listener %q has NAT ip that is the unspecified or loopback address %s", name, ipstr)
829 checkPath := func(kind string, enabled bool, path string) {
830 if enabled && path != "" && !strings.HasPrefix(path, "/") {
831 addErrorf("listener %q has %s with path %q that must start with a slash", name, kind, path)
834 checkPath("AccountHTTP", l.AccountHTTP.Enabled, l.AccountHTTP.Path)
835 checkPath("AccountHTTPS", l.AccountHTTPS.Enabled, l.AccountHTTPS.Path)
836 checkPath("AdminHTTP", l.AdminHTTP.Enabled, l.AdminHTTP.Path)
837 checkPath("AdminHTTPS", l.AdminHTTPS.Enabled, l.AdminHTTPS.Path)
838 c.Listeners[name] = l
840 if haveUnspecifiedSMTPListener {
841 c.SpecifiedSMTPListenIPs = nil
844 var zerouse config.SpecialUseMailboxes
845 if len(c.DefaultMailboxes) > 0 && (c.InitialMailboxes.SpecialUse != zerouse || len(c.InitialMailboxes.Regular) > 0) {
846 addErrorf("cannot have both DefaultMailboxes and InitialMailboxes")
848 // DefaultMailboxes is deprecated.
849 for _, mb := range c.DefaultMailboxes {
850 checkMailboxNormf(mb, "default mailbox")
852 checkSpecialUseMailbox := func(nameOpt string) {
854 checkMailboxNormf(nameOpt, "special-use initial mailbox")
855 if strings.EqualFold(nameOpt, "inbox") {
856 addErrorf("initial mailbox cannot be set to Inbox (Inbox is always created)")
860 checkSpecialUseMailbox(c.InitialMailboxes.SpecialUse.Archive)
861 checkSpecialUseMailbox(c.InitialMailboxes.SpecialUse.Draft)
862 checkSpecialUseMailbox(c.InitialMailboxes.SpecialUse.Junk)
863 checkSpecialUseMailbox(c.InitialMailboxes.SpecialUse.Sent)
864 checkSpecialUseMailbox(c.InitialMailboxes.SpecialUse.Trash)
865 for _, name := range c.InitialMailboxes.Regular {
866 checkMailboxNormf(name, "regular initial mailbox")
867 if strings.EqualFold(name, "inbox") {
868 addErrorf("initial regular mailbox cannot be set to Inbox (Inbox is always created)")
872 checkTransportSMTP := func(name string, isTLS bool, t *config.TransportSMTP) {
874 t.DNSHost, err = dns.ParseDomain(t.Host)
876 addErrorf("transport %s: bad host %s: %v", name, t.Host, err)
879 if isTLS && t.STARTTLSInsecureSkipVerify {
880 addErrorf("transport %s: cannot have STARTTLSInsecureSkipVerify with immediate TLS")
882 if isTLS && t.NoSTARTTLS {
883 addErrorf("transport %s: cannot have NoSTARTTLS with immediate TLS")
889 seen := map[string]bool{}
890 for _, m := range t.Auth.Mechanisms {
892 addErrorf("transport %s: duplicate authentication mechanism %s", name, m)
896 case "SCRAM-SHA-256-PLUS":
897 case "SCRAM-SHA-256":
898 case "SCRAM-SHA-1-PLUS":
903 addErrorf("transport %s: unknown authentication mechanism %s", name, m)
907 t.Auth.EffectiveMechanisms = t.Auth.Mechanisms
908 if len(t.Auth.EffectiveMechanisms) == 0 {
909 t.Auth.EffectiveMechanisms = []string{"SCRAM-SHA-256-PLUS", "SCRAM-SHA-256", "SCRAM-SHA-1-PLUS", "SCRAM-SHA-1", "CRAM-MD5"}
913 checkTransportSocks := func(name string, t *config.TransportSocks) {
914 _, _, err := net.SplitHostPort(t.Address)
916 addErrorf("transport %s: bad address %s: %v", name, t.Address, err)
918 for _, ipstr := range t.RemoteIPs {
919 ip := net.ParseIP(ipstr)
921 addErrorf("transport %s: bad ip %s", name, ipstr)
923 t.IPs = append(t.IPs, ip)
926 t.Hostname, err = dns.ParseDomain(t.RemoteHostname)
928 addErrorf("transport %s: bad hostname %s: %v", name, t.RemoteHostname, err)
932 for name, t := range c.Transports {
934 if t.Submissions != nil {
936 checkTransportSMTP(name, true, t.Submissions)
938 if t.Submission != nil {
940 checkTransportSMTP(name, false, t.Submission)
944 checkTransportSMTP(name, false, t.SMTP)
948 checkTransportSocks(name, t.Socks)
951 addErrorf("transport %s: cannot have multiple methods in a transport", name)
955 // Load CA certificate pool.
957 if c.TLS.CA.AdditionalToSystem {
959 c.TLS.CertPool, err = x509.SystemCertPool()
961 addErrorf("fetching system CA cert pool: %v", err)
964 c.TLS.CertPool = x509.NewCertPool()
966 for _, certfile := range c.TLS.CA.CertFiles {
967 p := configDirPath(configFile, certfile)
968 pemBuf, err := os.ReadFile(p)
970 addErrorf("reading TLS CA cert file: %v", err)
972 } else if !c.TLS.CertPool.AppendCertsFromPEM(pemBuf) {
973 // todo: can we check more fully if we're getting some useful data back?
974 addErrorf("no CA certs added from %q", p)
981// PrepareDynamicConfig parses the dynamic config file given a static file.
982func ParseDynamicConfig(ctx context.Context, log mlog.Log, dynamicPath string, static config.Static) (c config.Dynamic, mtime time.Time, accDests map[string]AccountDestination, errs []error) {
983 addErrorf := func(format string, args ...any) {
984 errs = append(errs, fmt.Errorf(format, args...))
987 f, err := os.Open(dynamicPath)
989 addErrorf("parsing domains config: %v", err)
995 addErrorf("stat domains config: %v", err)
997 if err := sconf.Parse(f, &c); err != nil {
998 addErrorf("parsing dynamic config file: %v", err)
1002 accDests, errs = prepareDynamicConfig(ctx, log, dynamicPath, static, &c)
1003 return c, fi.ModTime(), accDests, errs
1006func prepareDynamicConfig(ctx context.Context, log mlog.Log, dynamicPath string, static config.Static, c *config.Dynamic) (accDests map[string]AccountDestination, errs []error) {
1007 addErrorf := func(format string, args ...any) {
1008 errs = append(errs, fmt.Errorf(format, args...))
1011 // Check that mailbox is in unicode NFC normalized form.
1012 checkMailboxNormf := func(mailbox string, format string, args ...any) {
1013 s := norm.NFC.String(mailbox)
1015 msg := fmt.Sprintf(format, args...)
1016 addErrorf("%s: mailbox %q is not in NFC normalized form, should be %q", msg, mailbox, s)
1020 // Validate postmaster account exists.
1021 if _, ok := c.Accounts[static.Postmaster.Account]; !ok {
1022 addErrorf("postmaster account %q does not exist", static.Postmaster.Account)
1024 checkMailboxNormf(static.Postmaster.Mailbox, "postmaster mailbox")
1026 accDests = map[string]AccountDestination{}
1028 // Validate host TLSRPT account/address.
1029 if static.HostTLSRPT.Account != "" {
1030 if _, ok := c.Accounts[static.HostTLSRPT.Account]; !ok {
1031 addErrorf("host tlsrpt account %q does not exist", static.HostTLSRPT.Account)
1033 checkMailboxNormf(static.HostTLSRPT.Mailbox, "host tlsrpt mailbox")
1035 // Localpart has been parsed already.
1037 addrFull := smtp.NewAddress(static.HostTLSRPT.ParsedLocalpart, static.HostnameDomain).String()
1038 dest := config.Destination{
1039 Mailbox: static.HostTLSRPT.Mailbox,
1040 HostTLSReports: true,
1042 accDests[addrFull] = AccountDestination{false, static.HostTLSRPT.ParsedLocalpart, static.HostTLSRPT.Account, dest}
1045 var haveSTSListener, haveWebserverListener bool
1046 for _, l := range static.Listeners {
1047 if l.MTASTSHTTPS.Enabled {
1048 haveSTSListener = true
1050 if l.WebserverHTTP.Enabled || l.WebserverHTTPS.Enabled {
1051 haveWebserverListener = true
1055 checkRoutes := func(descr string, routes []config.Route) {
1056 parseRouteDomains := func(l []string) []string {
1058 for _, e := range l {
1064 if strings.HasPrefix(e, ".") {
1068 d, err := dns.ParseDomain(e)
1070 addErrorf("%s: invalid domain %s: %v", descr, e, err)
1072 r = append(r, prefix+d.ASCII)
1077 for i := range routes {
1078 routes[i].FromDomainASCII = parseRouteDomains(routes[i].FromDomain)
1079 routes[i].ToDomainASCII = parseRouteDomains(routes[i].ToDomain)
1081 routes[i].ResolvedTransport, ok = static.Transports[routes[i].Transport]
1083 addErrorf("%s: route references undefined transport %s", descr, routes[i].Transport)
1088 checkRoutes("global routes", c.Routes)
1090 // Validate domains.
1091 for d, domain := range c.Domains {
1092 dnsdomain, err := dns.ParseDomain(d)
1094 addErrorf("bad domain %q: %s", d, err)
1095 } else if dnsdomain.Name() != d {
1096 addErrorf("domain %s must be specified in unicode form, %s", d, dnsdomain.Name())
1099 domain.Domain = dnsdomain
1101 if domain.ClientSettingsDomain != "" {
1102 csd, err := dns.ParseDomain(domain.ClientSettingsDomain)
1104 addErrorf("bad client settings domain %q: %s", domain.ClientSettingsDomain, err)
1106 domain.ClientSettingsDNSDomain = csd
1109 for _, sign := range domain.DKIM.Sign {
1110 if _, ok := domain.DKIM.Selectors[sign]; !ok {
1111 addErrorf("selector %s for signing is missing in domain %s", sign, d)
1114 for name, sel := range domain.DKIM.Selectors {
1115 seld, err := dns.ParseDomain(name)
1117 addErrorf("bad selector %q: %s", name, err)
1118 } else if seld.Name() != name {
1119 addErrorf("selector %q must be specified in unicode form, %q", name, seld.Name())
1123 if sel.Expiration != "" {
1124 exp, err := time.ParseDuration(sel.Expiration)
1126 addErrorf("selector %q has invalid expiration %q: %v", name, sel.Expiration, err)
1128 sel.ExpirationSeconds = int(exp / time.Second)
1132 sel.HashEffective = sel.Hash
1133 switch sel.HashEffective {
1135 sel.HashEffective = "sha256"
1137 log.Error("using sha1 with DKIM is deprecated as not secure enough, switch to sha256")
1140 addErrorf("unsupported hash %q for selector %q in domain %s", sel.HashEffective, name, d)
1143 pemBuf, err := os.ReadFile(configDirPath(dynamicPath, sel.PrivateKeyFile))
1145 addErrorf("reading private key for selector %s in domain %s: %s", name, d, err)
1148 p, _ := pem.Decode(pemBuf)
1150 addErrorf("private key for selector %s in domain %s has no PEM block", name, d)
1153 key, err := x509.ParsePKCS8PrivateKey(p.Bytes)
1155 addErrorf("parsing private key for selector %s in domain %s: %s", name, d, err)
1158 switch k := key.(type) {
1159 case *rsa.PrivateKey:
1160 if k.N.BitLen() < 1024 {
1162 // Let's help user do the right thing.
1163 addErrorf("rsa keys should be >= 1024 bits")
1166 case ed25519.PrivateKey:
1167 if sel.HashEffective != "sha256" {
1168 addErrorf("hash algorithm %q is not supported with ed25519, only sha256 is", sel.HashEffective)
1172 addErrorf("private key type %T not yet supported, at selector %s in domain %s", key, name, d)
1175 if len(sel.Headers) == 0 {
1179 // By default we seal signed headers, and we sign user-visible headers to
1180 // prevent/limit reuse of previously signed messages: All addressing fields, date
1181 // and subject, message-referencing fields, parsing instructions (content-type).
1182 sel.HeadersEffective = strings.Split("From,To,Cc,Bcc,Reply-To,References,In-Reply-To,Subject,Date,Message-Id,Content-Type", ",")
1185 for _, h := range sel.Headers {
1186 from = from || strings.EqualFold(h, "From")
1188 if strings.EqualFold(h, "DKIM-Signature") || strings.EqualFold(h, "Received") || strings.EqualFold(h, "Return-Path") {
1189 log.Error("DKIM-signing header %q is recommended against as it may be modified in transit")
1193 addErrorf("From-field must always be DKIM-signed")
1195 sel.HeadersEffective = sel.Headers
1198 domain.DKIM.Selectors[name] = sel
1201 if domain.MTASTS != nil {
1202 if !haveSTSListener {
1203 addErrorf("MTA-STS enabled for domain %q, but there is no listener for MTASTS", d)
1205 sts := domain.MTASTS
1206 if sts.PolicyID == "" {
1207 addErrorf("invalid empty MTA-STS PolicyID")
1210 case mtasts.ModeNone, mtasts.ModeTesting, mtasts.ModeEnforce:
1212 addErrorf("invalid mtasts mode %q", sts.Mode)
1216 checkRoutes("routes for domain", domain.Routes)
1218 c.Domains[d] = domain
1221 // To determine ReportsOnly.
1222 domainHasAddress := map[string]bool{}
1224 // Validate email addresses.
1225 for accName, acc := range c.Accounts {
1227 acc.DNSDomain, err = dns.ParseDomain(acc.Domain)
1229 addErrorf("parsing domain %s for account %q: %s", acc.Domain, accName, err)
1232 if strings.EqualFold(acc.RejectsMailbox, "Inbox") {
1233 addErrorf("account %q: cannot set RejectsMailbox to inbox, messages will be removed automatically from the rejects mailbox", accName)
1235 checkMailboxNormf(acc.RejectsMailbox, "account %q", accName)
1237 if acc.AutomaticJunkFlags.JunkMailboxRegexp != "" {
1238 r, err := regexp.Compile(acc.AutomaticJunkFlags.JunkMailboxRegexp)
1240 addErrorf("invalid JunkMailboxRegexp regular expression: %v", err)
1244 if acc.AutomaticJunkFlags.NeutralMailboxRegexp != "" {
1245 r, err := regexp.Compile(acc.AutomaticJunkFlags.NeutralMailboxRegexp)
1247 addErrorf("invalid NeutralMailboxRegexp regular expression: %v", err)
1249 acc.NeutralMailbox = r
1251 if acc.AutomaticJunkFlags.NotJunkMailboxRegexp != "" {
1252 r, err := regexp.Compile(acc.AutomaticJunkFlags.NotJunkMailboxRegexp)
1254 addErrorf("invalid NotJunkMailboxRegexp regular expression: %v", err)
1256 acc.NotJunkMailbox = r
1258 c.Accounts[accName] = acc
1260 // 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.
1261 replaceLocalparts := map[string]string{}
1263 for addrName, dest := range acc.Destinations {
1264 checkMailboxNormf(dest.Mailbox, "account %q, destination %q", accName, addrName)
1266 for i, rs := range dest.Rulesets {
1267 checkMailboxNormf(rs.Mailbox, "account %q, destination %q, ruleset %d", accName, addrName, i+1)
1271 if rs.SMTPMailFromRegexp != "" {
1273 r, err := regexp.Compile(rs.SMTPMailFromRegexp)
1275 addErrorf("invalid SMTPMailFrom regular expression: %v", err)
1277 c.Accounts[accName].Destinations[addrName].Rulesets[i].SMTPMailFromRegexpCompiled = r
1279 if rs.VerifiedDomain != "" {
1281 d, err := dns.ParseDomain(rs.VerifiedDomain)
1283 addErrorf("invalid VerifiedDomain: %v", err)
1285 c.Accounts[accName].Destinations[addrName].Rulesets[i].VerifiedDNSDomain = d
1288 var hdr [][2]*regexp.Regexp
1289 for k, v := range rs.HeadersRegexp {
1291 if strings.ToLower(k) != k {
1292 addErrorf("header field %q must only have lower case characters", k)
1294 if strings.ToLower(v) != v {
1295 addErrorf("header value %q must only have lower case characters", v)
1297 rk, err := regexp.Compile(k)
1299 addErrorf("invalid rule header regexp %q: %v", k, err)
1301 rv, err := regexp.Compile(v)
1303 addErrorf("invalid rule header regexp %q: %v", v, err)
1305 hdr = append(hdr, [...]*regexp.Regexp{rk, rv})
1307 c.Accounts[accName].Destinations[addrName].Rulesets[i].HeadersRegexpCompiled = hdr
1310 addErrorf("ruleset must have at least one rule")
1313 if rs.IsForward && rs.ListAllowDomain != "" {
1314 addErrorf("ruleset cannot have both IsForward and ListAllowDomain")
1317 if rs.SMTPMailFromRegexp == "" || rs.VerifiedDomain == "" {
1318 addErrorf("ruleset with IsForward must have both SMTPMailFromRegexp and VerifiedDomain too")
1321 if rs.ListAllowDomain != "" {
1322 d, err := dns.ParseDomain(rs.ListAllowDomain)
1324 addErrorf("invalid ListAllowDomain %q: %v", rs.ListAllowDomain, err)
1326 c.Accounts[accName].Destinations[addrName].Rulesets[i].ListAllowDNSDomain = d
1329 checkMailboxNormf(rs.AcceptRejectsToMailbox, "account %q, destination %q, ruleset %d, rejects mailbox", accName, addrName, i+1)
1330 if strings.EqualFold(rs.AcceptRejectsToMailbox, "inbox") {
1331 addErrorf("account %q, destination %q, ruleset %d: AcceptRejectsToMailbox cannot be set to Inbox", accName, addrName, i+1)
1335 // Catchall destination for domain.
1336 if strings.HasPrefix(addrName, "@") {
1337 d, err := dns.ParseDomain(addrName[1:])
1339 addErrorf("parsing domain %q in account %q", addrName[1:], accName)
1341 } else if _, ok := c.Domains[d.Name()]; !ok {
1342 addErrorf("unknown domain for address %q in account %q", addrName, accName)
1345 domainHasAddress[d.Name()] = true
1346 addrFull := "@" + d.Name()
1347 if _, ok := accDests[addrFull]; ok {
1348 addErrorf("duplicate canonicalized catchall destination address %s", addrFull)
1350 accDests[addrFull] = AccountDestination{true, "", accName, dest}
1354 // todo deprecated: remove support for parsing destination as just a localpart instead full address.
1355 var address smtp.Address
1356 if localpart, err := smtp.ParseLocalpart(addrName); err != nil && errors.Is(err, smtp.ErrBadLocalpart) {
1357 address, err = smtp.ParseAddress(addrName)
1359 addErrorf("invalid email address %q in account %q", addrName, accName)
1361 } else if _, ok := c.Domains[address.Domain.Name()]; !ok {
1362 addErrorf("unknown domain for address %q in account %q", addrName, accName)
1367 addErrorf("invalid localpart %q in account %q", addrName, accName)
1370 address = smtp.NewAddress(localpart, acc.DNSDomain)
1371 if _, ok := c.Domains[acc.DNSDomain.Name()]; !ok {
1372 addErrorf("unknown domain %s for account %q", acc.DNSDomain.Name(), accName)
1375 replaceLocalparts[addrName] = address.Pack(true)
1378 origLP := address.Localpart
1379 dc := c.Domains[address.Domain.Name()]
1380 domainHasAddress[address.Domain.Name()] = true
1381 if lp, err := CanonicalLocalpart(address.Localpart, dc); err != nil {
1382 addErrorf("canonicalizing localpart %s: %v", address.Localpart, err)
1383 } else if dc.LocalpartCatchallSeparator != "" && strings.Contains(string(address.Localpart), dc.LocalpartCatchallSeparator) {
1384 addErrorf("localpart of address %s includes domain catchall separator %s", address, dc.LocalpartCatchallSeparator)
1386 address.Localpart = lp
1388 addrFull := address.Pack(true)
1389 if _, ok := accDests[addrFull]; ok {
1390 addErrorf("duplicate canonicalized destination address %s", addrFull)
1392 accDests[addrFull] = AccountDestination{false, origLP, accName, dest}
1395 for lp, addr := range replaceLocalparts {
1396 dest, ok := acc.Destinations[lp]
1398 addErrorf("could not find localpart %q to replace with address in destinations", lp)
1400 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`,
1401 slog.Any("localpart", lp),
1402 slog.Any("address", addr),
1403 slog.String("account", accName))
1404 acc.Destinations[addr] = dest
1405 delete(acc.Destinations, lp)
1409 checkRoutes("routes for account", acc.Routes)
1412 // Set DMARC destinations.
1413 for d, domain := range c.Domains {
1414 dmarc := domain.DMARC
1418 if _, ok := c.Accounts[dmarc.Account]; !ok {
1419 addErrorf("DMARC account %q does not exist", dmarc.Account)
1421 lp, err := smtp.ParseLocalpart(dmarc.Localpart)
1423 addErrorf("invalid DMARC localpart %q: %s", dmarc.Localpart, err)
1425 if lp.IsInternational() {
1427 addErrorf("DMARC localpart %q is an internationalized address, only conventional ascii-only address possible for interopability", lp)
1429 addrdom := domain.Domain
1430 if dmarc.Domain != "" {
1431 addrdom, err = dns.ParseDomain(dmarc.Domain)
1433 addErrorf("DMARC domain %q: %s", dmarc.Domain, err)
1434 } else if _, ok := c.Domains[addrdom.Name()]; !ok {
1435 addErrorf("unknown domain %q for DMARC address in domain %q", addrdom, d)
1438 if addrdom == domain.Domain {
1439 domainHasAddress[addrdom.Name()] = true
1442 domain.DMARC.ParsedLocalpart = lp
1443 domain.DMARC.DNSDomain = addrdom
1444 c.Domains[d] = domain
1445 addrFull := smtp.NewAddress(lp, addrdom).String()
1446 dest := config.Destination{
1447 Mailbox: dmarc.Mailbox,
1450 checkMailboxNormf(dmarc.Mailbox, "DMARC mailbox for account %q", dmarc.Account)
1451 accDests[addrFull] = AccountDestination{false, lp, dmarc.Account, dest}
1454 // Set TLSRPT destinations.
1455 for d, domain := range c.Domains {
1456 tlsrpt := domain.TLSRPT
1460 if _, ok := c.Accounts[tlsrpt.Account]; !ok {
1461 addErrorf("TLSRPT account %q does not exist", tlsrpt.Account)
1463 lp, err := smtp.ParseLocalpart(tlsrpt.Localpart)
1465 addErrorf("invalid TLSRPT localpart %q: %s", tlsrpt.Localpart, err)
1467 if lp.IsInternational() {
1468 // Does not appear documented in
../rfc/8460, but similar to DMARC it makes sense
1469 // to keep this ascii-only addresses.
1470 addErrorf("TLSRPT localpart %q is an internationalized address, only conventional ascii-only address allowed for interopability", lp)
1472 addrdom := domain.Domain
1473 if tlsrpt.Domain != "" {
1474 addrdom, err = dns.ParseDomain(tlsrpt.Domain)
1476 addErrorf("TLSRPT domain %q: %s", tlsrpt.Domain, err)
1477 } else if _, ok := c.Domains[addrdom.Name()]; !ok {
1478 addErrorf("unknown domain %q for TLSRPT address in domain %q", tlsrpt.Domain, d)
1481 if addrdom == domain.Domain {
1482 domainHasAddress[addrdom.Name()] = true
1485 domain.TLSRPT.ParsedLocalpart = lp
1486 domain.TLSRPT.DNSDomain = addrdom
1487 c.Domains[d] = domain
1488 addrFull := smtp.NewAddress(lp, addrdom).String()
1489 dest := config.Destination{
1490 Mailbox: tlsrpt.Mailbox,
1491 DomainTLSReports: true,
1493 checkMailboxNormf(tlsrpt.Mailbox, "TLSRPT mailbox for account %q", tlsrpt.Account)
1494 accDests[addrFull] = AccountDestination{false, lp, tlsrpt.Account, dest}
1497 // Set ReportsOnly for domains, based on whether we have seen addresses (possibly
1498 // from DMARC or TLS reporting).
1499 for d, domain := range c.Domains {
1500 domain.ReportsOnly = !domainHasAddress[domain.Domain.Name()]
1501 c.Domains[d] = domain
1504 // Check webserver configs.
1505 if (len(c.WebDomainRedirects) > 0 || len(c.WebHandlers) > 0) && !haveWebserverListener {
1506 addErrorf("WebDomainRedirects or WebHandlers configured but no listener with WebserverHTTP or WebserverHTTPS enabled")
1509 c.WebDNSDomainRedirects = map[dns.Domain]dns.Domain{}
1510 for from, to := range c.WebDomainRedirects {
1511 fromdom, err := dns.ParseDomain(from)
1513 addErrorf("parsing domain for redirect %s: %v", from, err)
1515 todom, err := dns.ParseDomain(to)
1517 addErrorf("parsing domain for redirect %s: %v", to, err)
1518 } else if fromdom == todom {
1519 addErrorf("will not redirect domain %s to itself", todom)
1521 var zerodom dns.Domain
1522 if _, ok := c.WebDNSDomainRedirects[fromdom]; ok && fromdom != zerodom {
1523 addErrorf("duplicate redirect domain %s", from)
1525 c.WebDNSDomainRedirects[fromdom] = todom
1528 for i := range c.WebHandlers {
1529 wh := &c.WebHandlers[i]
1531 if wh.LogName == "" {
1532 wh.Name = fmt.Sprintf("%d", i)
1534 wh.Name = wh.LogName
1537 dom, err := dns.ParseDomain(wh.Domain)
1539 addErrorf("webhandler %s %s: parsing domain: %v", wh.Domain, wh.PathRegexp, err)
1543 if !strings.HasPrefix(wh.PathRegexp, "^") {
1544 addErrorf("webhandler %s %s: path regexp must start with a ^", wh.Domain, wh.PathRegexp)
1546 re, err := regexp.Compile(wh.PathRegexp)
1548 addErrorf("webhandler %s %s: compiling regexp: %v", wh.Domain, wh.PathRegexp, err)
1553 if wh.WebStatic != nil {
1556 if ws.StripPrefix != "" && !strings.HasPrefix(ws.StripPrefix, "/") {
1557 addErrorf("webstatic %s %s: prefix to strip %s must start with a slash", wh.Domain, wh.PathRegexp, ws.StripPrefix)
1559 for k := range ws.ResponseHeaders {
1561 k := strings.TrimSpace(xk)
1562 if k != xk || k == "" {
1563 addErrorf("webstatic %s %s: bad header %q", wh.Domain, wh.PathRegexp, xk)
1567 if wh.WebRedirect != nil {
1569 wr := wh.WebRedirect
1570 if wr.BaseURL != "" {
1571 u, err := url.Parse(wr.BaseURL)
1573 addErrorf("webredirect %s %s: parsing redirect url %s: %v", wh.Domain, wh.PathRegexp, wr.BaseURL, err)
1579 addErrorf("webredirect %s %s: BaseURL must have empty path", wh.Domain, wh.PathRegexp, wr.BaseURL)
1583 if wr.OrigPathRegexp != "" && wr.ReplacePath != "" {
1584 re, err := regexp.Compile(wr.OrigPathRegexp)
1586 addErrorf("webredirect %s %s: compiling regexp %s: %v", wh.Domain, wh.PathRegexp, wr.OrigPathRegexp, err)
1589 } else if wr.OrigPathRegexp != "" || wr.ReplacePath != "" {
1590 addErrorf("webredirect %s %s: must have either both OrigPathRegexp and ReplacePath, or neither", wh.Domain, wh.PathRegexp)
1591 } else if wr.BaseURL == "" {
1592 addErrorf("webredirect %s %s: must at least one of BaseURL and OrigPathRegexp+ReplacePath", wh.Domain, wh.PathRegexp)
1594 if wr.StatusCode != 0 && (wr.StatusCode < 300 || wr.StatusCode >= 400) {
1595 addErrorf("webredirect %s %s: invalid redirect status code %d", wh.Domain, wh.PathRegexp, wr.StatusCode)
1598 if wh.WebForward != nil {
1601 u, err := url.Parse(wf.URL)
1603 addErrorf("webforward %s %s: parsing url %s: %v", wh.Domain, wh.PathRegexp, wf.URL, err)
1607 for k := range wf.ResponseHeaders {
1609 k := strings.TrimSpace(xk)
1610 if k != xk || k == "" {
1611 addErrorf("webforward %s %s: bad header %q", wh.Domain, wh.PathRegexp, xk)
1616 addErrorf("webhandler %s %s: must have exactly one handler, not %d", wh.Domain, wh.PathRegexp, n)
1620 c.MonitorDNSBLZones = nil
1621 for _, s := range c.MonitorDNSBLs {
1622 d, err := dns.ParseDomain(s)
1624 addErrorf("invalid monitor dnsbl zone %s: %v", s, err)
1627 if slices.Contains(c.MonitorDNSBLZones, d) {
1628 addErrorf("duplicate zone %s in monitor dnsbl zones", d)
1631 c.MonitorDNSBLZones = append(c.MonitorDNSBLZones, d)
1637func loadPrivateKeyFile(keyPath string) (crypto.Signer, error) {
1638 keyBuf, err := os.ReadFile(keyPath)
1640 return nil, fmt.Errorf("reading host private key: %v", err)
1642 b, _ := pem.Decode(keyBuf)
1644 return nil, fmt.Errorf("parsing pem block for private key: %v", err)
1649 privKey, err = x509.ParsePKCS8PrivateKey(b.Bytes)
1650 case "RSA PRIVATE KEY":
1651 privKey, err = x509.ParsePKCS1PrivateKey(b.Bytes)
1652 case "EC PRIVATE KEY":
1653 privKey, err = x509.ParseECPrivateKey(b.Bytes)
1655 err = fmt.Errorf("unknown pem type %q", b.Type)
1658 return nil, fmt.Errorf("parsing private key: %v", err)
1660 if k, ok := privKey.(crypto.Signer); ok {
1663 return nil, fmt.Errorf("parsed private key not a crypto.Signer, but %T", privKey)
1666func loadTLSKeyCerts(configFile, kind string, ctls *config.TLS) error {
1667 certs := []tls.Certificate{}
1668 for _, kp := range ctls.KeyCerts {
1669 certPath := configDirPath(configFile, kp.CertFile)
1670 keyPath := configDirPath(configFile, kp.KeyFile)
1671 cert, err := loadX509KeyPairPrivileged(certPath, keyPath)
1673 return fmt.Errorf("tls config for %q: parsing x509 key pair: %v", kind, err)
1675 certs = append(certs, cert)
1677 ctls.Config = &tls.Config{
1678 Certificates: certs,
1683// load x509 key/cert files from file descriptor possibly passed in by privileged
1685func loadX509KeyPairPrivileged(certPath, keyPath string) (tls.Certificate, error) {
1686 certBuf, err := readFilePrivileged(certPath)
1688 return tls.Certificate{}, fmt.Errorf("reading tls certificate: %v", err)
1690 keyBuf, err := readFilePrivileged(keyPath)
1692 return tls.Certificate{}, fmt.Errorf("reading tls key: %v", err)
1694 return tls.X509KeyPair(certBuf, keyBuf)
1697// like os.ReadFile, but open privileged file possibly passed in by root process.
1698func readFilePrivileged(path string) ([]byte, error) {
1699 f, err := OpenPrivileged(path)
1704 return io.ReadAll(f)