1package mox
2
3import (
4 "bytes"
5 "cmp"
6 "context"
7 "crypto"
8 "crypto/ecdsa"
9 "crypto/ed25519"
10 "crypto/elliptic"
11 cryptorand "crypto/rand"
12 "crypto/rsa"
13 "crypto/tls"
14 "crypto/x509"
15 "encoding/base64"
16 "encoding/pem"
17 "errors"
18 "fmt"
19 "io"
20 "log/slog"
21 "maps"
22 "net"
23 "net/http"
24 "net/url"
25 "os"
26 "os/user"
27 "path/filepath"
28 "regexp"
29 "slices"
30 "strconv"
31 "strings"
32 "sync"
33 "time"
34
35 "golang.org/x/text/unicode/norm"
36
37 "github.com/mjl-/autocert"
38
39 "github.com/mjl-/sconf"
40
41 "github.com/mjl-/mox/autotls"
42 "github.com/mjl-/mox/config"
43 "github.com/mjl-/mox/dkim"
44 "github.com/mjl-/mox/dns"
45 "github.com/mjl-/mox/message"
46 "github.com/mjl-/mox/mlog"
47 "github.com/mjl-/mox/moxio"
48 "github.com/mjl-/mox/mtasts"
49 "github.com/mjl-/mox/smtp"
50)
51
52var pkglog = mlog.New("mox", nil)
53
54// Pedantic enables stricter parsing.
55var Pedantic bool
56
57// Config paths are set early in program startup. They will point to files in
58// the same directory.
59var (
60 ConfigStaticPath string
61 ConfigDynamicPath string
62 Conf = Config{Log: map[string]slog.Level{"": slog.LevelError}}
63)
64
65var ErrConfig = errors.New("config error")
66
67// Set by packages webadmin, webaccount, webmail, webapisrv to prevent cyclic dependencies.
68var NewWebadminHandler = func(basePath string, isForwarded bool) http.Handler { return nopHandler }
69var NewWebaccountHandler = func(basePath string, isForwarded bool) http.Handler { return nopHandler }
70var NewWebmailHandler = func(maxMsgSize int64, basePath string, isForwarded bool, accountPath string) http.Handler {
71 return nopHandler
72}
73var NewWebapiHandler = func(maxMsgSize int64, basePath string, isForwarded bool) http.Handler { return nopHandler }
74
75var nopHandler = http.HandlerFunc(nil)
76
77// Config as used in the code, a processed version of what is in the config file.
78//
79// Use methods to lookup a domain/account/address in the dynamic configuration.
80type Config struct {
81 Static config.Static // Does not change during the lifetime of a running instance.
82
83 logMutex sync.Mutex // For accessing the log levels.
84 Log map[string]slog.Level
85
86 dynamicMutex sync.Mutex
87 Dynamic config.Dynamic // Can only be accessed directly by tests. Use methods on Config for locked access.
88 dynamicMtime time.Time
89 DynamicLastCheck time.Time // For use by quickstart only to skip checks.
90
91 // From canonical full address (localpart@domain, lower-cased when
92 // case-insensitive, stripped of catchall separator) to account and address.
93 // Domains are IDNA names in utf8. Dynamic config lock must be held when accessing.
94 AccountDestinationsLocked map[string]AccountDestination
95
96 // Like AccountDestinationsLocked, but for aliases.
97 aliases map[string]config.Alias
98}
99
100type AccountDestination struct {
101 Catchall bool // If catchall destination for its domain.
102 Localpart smtp.Localpart // In original casing as written in config file.
103 Account string
104 Destination config.Destination
105}
106
107// LogLevelSet sets a new log level for pkg. An empty pkg sets the default log
108// value that is used if no explicit log level is configured for a package.
109// This change is ephemeral, no config file is changed.
110func (c *Config) LogLevelSet(log mlog.Log, pkg string, level slog.Level) {
111 c.logMutex.Lock()
112 defer c.logMutex.Unlock()
113 l := c.copyLogLevels()
114 l[pkg] = level
115 c.Log = l
116 log.Print("log level changed", slog.String("pkg", pkg), slog.Any("level", mlog.LevelStrings[level]))
117 mlog.SetConfig(c.Log)
118}
119
120// LogLevelRemove removes a configured log level for a package.
121func (c *Config) LogLevelRemove(log mlog.Log, pkg string) {
122 c.logMutex.Lock()
123 defer c.logMutex.Unlock()
124 l := c.copyLogLevels()
125 delete(l, pkg)
126 c.Log = l
127 log.Print("log level cleared", slog.String("pkg", pkg))
128 mlog.SetConfig(c.Log)
129}
130
131// copyLogLevels returns a copy of c.Log, for modifications.
132// must be called with log lock held.
133func (c *Config) copyLogLevels() map[string]slog.Level {
134 m := map[string]slog.Level{}
135 maps.Copy(m, c.Log)
136 return m
137}
138
139// LogLevels returns a copy of the current log levels.
140func (c *Config) LogLevels() map[string]slog.Level {
141 c.logMutex.Lock()
142 defer c.logMutex.Unlock()
143 return c.copyLogLevels()
144}
145
146// DynamicLockUnlock locks the dynamic config, will try updating the latest state
147// from disk, and return an unlock function. Should be called as "defer
148// Conf.DynamicLockUnlock()()".
149func (c *Config) DynamicLockUnlock() func() {
150 c.dynamicMutex.Lock()
151 now := time.Now()
152 if now.Sub(c.DynamicLastCheck) > time.Second {
153 c.DynamicLastCheck = now
154 if fi, err := os.Stat(ConfigDynamicPath); err != nil {
155 pkglog.Errorx("stat domains config", err)
156 } else if !fi.ModTime().Equal(c.dynamicMtime) {
157 if errs := c.loadDynamic(); len(errs) > 0 {
158 pkglog.Errorx("loading domains config", errs[0], slog.Any("errors", errs))
159 } else {
160 pkglog.Info("domains config reloaded")
161 c.dynamicMtime = fi.ModTime()
162 }
163 }
164 }
165 return c.dynamicMutex.Unlock
166}
167
168func (c *Config) withDynamicLock(fn func()) {
169 defer c.DynamicLockUnlock()()
170 fn()
171}
172
173// must be called with dynamic lock held.
174func (c *Config) loadDynamic() []error {
175 d, mtime, accDests, aliases, err := ParseDynamicConfig(context.Background(), pkglog, ConfigDynamicPath, c.Static)
176 if err != nil {
177 return err
178 }
179 c.Dynamic = d
180 c.dynamicMtime = mtime
181 c.AccountDestinationsLocked = accDests
182 c.aliases = aliases
183 c.allowACMEHosts(pkglog, true)
184 return nil
185}
186
187// DynamicConfig returns a shallow copy of the dynamic config. Must not be modified.
188func (c *Config) DynamicConfig() (config config.Dynamic) {
189 c.withDynamicLock(func() {
190 config = c.Dynamic // Shallow copy.
191 })
192 return
193}
194
195func (c *Config) Domains() (l []string) {
196 c.withDynamicLock(func() {
197 for name := range c.Dynamic.Domains {
198 l = append(l, name)
199 }
200 })
201 slices.Sort(l)
202 return l
203}
204
205func (c *Config) Accounts() (l []string) {
206 c.withDynamicLock(func() {
207 for name := range c.Dynamic.Accounts {
208 l = append(l, name)
209 }
210 })
211 return
212}
213
214func (c *Config) AccountsDisabled() (all, disabled []string) {
215 c.withDynamicLock(func() {
216 for name, conf := range c.Dynamic.Accounts {
217 all = append(all, name)
218 if conf.LoginDisabled != "" {
219 disabled = append(disabled, name)
220 }
221 }
222 })
223 return
224}
225
226// DomainLocalparts returns a mapping of encoded localparts to account names for a
227// domain, and encoded localparts to aliases. An empty localpart is a catchall
228// destination for a domain.
229func (c *Config) DomainLocalparts(d dns.Domain) (map[string]string, map[string]config.Alias) {
230 suffix := "@" + d.Name()
231 m := map[string]string{}
232 aliases := map[string]config.Alias{}
233 c.withDynamicLock(func() {
234 for addr, ad := range c.AccountDestinationsLocked {
235 if strings.HasSuffix(addr, suffix) {
236 if ad.Catchall {
237 m[""] = ad.Account
238 } else {
239 m[ad.Localpart.String()] = ad.Account
240 }
241 }
242 }
243 for addr, a := range c.aliases {
244 if strings.HasSuffix(addr, suffix) {
245 aliases[a.LocalpartStr] = a
246 }
247 }
248 })
249 return m, aliases
250}
251
252func (c *Config) Domain(d dns.Domain) (dom config.Domain, ok bool) {
253 c.withDynamicLock(func() {
254 dom, ok = c.Dynamic.Domains[d.Name()]
255 })
256 return
257}
258
259func (c *Config) DomainConfigs() (doms []config.Domain) {
260 c.withDynamicLock(func() {
261 doms = make([]config.Domain, 0, len(c.Dynamic.Domains))
262 for _, d := range c.Dynamic.Domains {
263 doms = append(doms, d)
264 }
265 })
266 slices.SortFunc(doms, func(a, b config.Domain) int {
267 return cmp.Compare(a.Domain.Name(), b.Domain.Name())
268 })
269 return
270}
271
272func (c *Config) Account(name string) (acc config.Account, ok bool) {
273 c.withDynamicLock(func() {
274 acc, ok = c.Dynamic.Accounts[name]
275 })
276 return
277}
278
279func (c *Config) AccountDestination(addr string) (accDest AccountDestination, alias *config.Alias, ok bool) {
280 c.withDynamicLock(func() {
281 accDest, ok = c.AccountDestinationsLocked[addr]
282 if !ok {
283 var a config.Alias
284 a, ok = c.aliases[addr]
285 if ok {
286 alias = &a
287 }
288 }
289 })
290 return
291}
292
293func (c *Config) Routes(accountName string, domain dns.Domain) (accountRoutes, domainRoutes, globalRoutes []config.Route) {
294 c.withDynamicLock(func() {
295 acc := c.Dynamic.Accounts[accountName]
296 accountRoutes = acc.Routes
297
298 dom := c.Dynamic.Domains[domain.Name()]
299 domainRoutes = dom.Routes
300
301 globalRoutes = c.Dynamic.Routes
302 })
303 return
304}
305
306func (c *Config) IsClientSettingsDomain(d dns.Domain) (is bool) {
307 c.withDynamicLock(func() {
308 _, is = c.Dynamic.ClientSettingDomains[d]
309 })
310 return
311}
312
313func (c *Config) allowACMEHosts(log mlog.Log, checkACMEHosts bool) {
314 managerHosts := map[*autotls.Manager]map[dns.Domain]struct{}{}
315
316 for _, l := range c.Static.Listeners {
317 if l.TLS == nil || l.TLS.ACME == "" {
318 continue
319 }
320
321 m := c.Static.ACME[l.TLS.ACME].Manager
322 if managerHosts[m] == nil {
323 managerHosts[m] = map[dns.Domain]struct{}{}
324 }
325 hostnames := managerHosts[m]
326
327 hostnames[c.Static.HostnameDomain] = struct{}{}
328 if l.HostnameDomain.ASCII != "" {
329 hostnames[l.HostnameDomain] = struct{}{}
330 }
331
332 for _, dom := range c.Dynamic.Domains {
333 // Do not allow TLS certificates for domains for which we only accept DMARC/TLS
334 // reports as external party.
335 if dom.ReportsOnly {
336 continue
337 }
338
339 // Do not fetch TLS certs for disabled domains. The A/AAAA records may not be
340 // configured or still point to a previous machine before a migration.
341 if dom.Disabled {
342 continue
343 }
344
345 if l.AutoconfigHTTPS.Enabled && !l.AutoconfigHTTPS.NonTLS {
346 if d, err := dns.ParseDomain("autoconfig." + dom.Domain.ASCII); err != nil {
347 log.Errorx("parsing autoconfig domain", err, slog.Any("domain", dom.Domain))
348 } else {
349 hostnames[d] = struct{}{}
350 }
351 }
352
353 if l.MTASTSHTTPS.Enabled && dom.MTASTS != nil && !l.MTASTSHTTPS.NonTLS {
354 d, err := dns.ParseDomain("mta-sts." + dom.Domain.ASCII)
355 if err != nil {
356 log.Errorx("parsing mta-sts domain", err, slog.Any("domain", dom.Domain))
357 } else {
358 hostnames[d] = struct{}{}
359 }
360 }
361
362 if dom.ClientSettingsDomain != "" {
363 hostnames[dom.ClientSettingsDNSDomain] = struct{}{}
364 }
365 }
366
367 if l.WebserverHTTPS.Enabled {
368 for from := range c.Dynamic.WebDNSDomainRedirects {
369 hostnames[from] = struct{}{}
370 }
371 for _, wh := range c.Dynamic.WebHandlers {
372 hostnames[wh.DNSDomain] = struct{}{}
373 }
374 }
375
376 }
377
378 public := c.Static.Listeners["public"]
379 ips := public.IPs
380 if len(public.NATIPs) > 0 {
381 ips = public.NATIPs
382 }
383 if public.IPsNATed {
384 ips = nil
385 }
386
387 for m, hostnames := range managerHosts {
388 m.SetAllowedHostnames(log, dns.StrictResolver{Pkg: "autotls", Log: log.Logger}, hostnames, ips, checkACMEHosts)
389 }
390}
391
392// todo future: write config parsing & writing code that can read a config and remembers the exact tokens including newlines and comments, and can write back a modified file. the goal is to be able to write a config file automatically (after changing fields through the ui), but not loose comments and whitespace, to still get useful diffs for storing the config in a version control system.
393
394// WriteDynamicLocked prepares an updated internal state for the new dynamic
395// config, then writes it to disk and activates it.
396//
397// Returns ErrConfig if the configuration is not valid.
398//
399// Must be called with config lock held.
400func WriteDynamicLocked(ctx context.Context, log mlog.Log, c config.Dynamic) error {
401 accDests, aliases, errs := prepareDynamicConfig(ctx, log, ConfigDynamicPath, Conf.Static, &c)
402 if len(errs) > 0 {
403 errstrs := make([]string, len(errs))
404 for i, err := range errs {
405 errstrs[i] = err.Error()
406 }
407 return fmt.Errorf("%w: %s", ErrConfig, strings.Join(errstrs, "; "))
408 }
409
410 var b bytes.Buffer
411 err := sconf.Write(&b, c)
412 if err != nil {
413 return err
414 }
415 f, err := os.OpenFile(ConfigDynamicPath, os.O_WRONLY, 0660)
416 if err != nil {
417 return err
418 }
419 defer func() {
420 if f != nil {
421 err := f.Close()
422 log.Check(err, "closing file after error")
423 }
424 }()
425 buf := b.Bytes()
426 if _, err := f.Write(buf); err != nil {
427 return fmt.Errorf("write domains.conf: %v", err)
428 }
429 if err := f.Truncate(int64(len(buf))); err != nil {
430 return fmt.Errorf("truncate domains.conf after write: %v", err)
431 }
432 if err := f.Sync(); err != nil {
433 return fmt.Errorf("sync domains.conf after write: %v", err)
434 }
435 if err := moxio.SyncDir(log, filepath.Dir(ConfigDynamicPath)); err != nil {
436 return fmt.Errorf("sync dir of domains.conf after write: %v", err)
437 }
438
439 fi, err := f.Stat()
440 if err != nil {
441 return fmt.Errorf("stat after writing domains.conf: %v", err)
442 }
443
444 if err := f.Close(); err != nil {
445 return fmt.Errorf("close written domains.conf: %v", err)
446 }
447 f = nil
448
449 Conf.dynamicMtime = fi.ModTime()
450 Conf.DynamicLastCheck = time.Now()
451 Conf.Dynamic = c
452 Conf.AccountDestinationsLocked = accDests
453 Conf.aliases = aliases
454
455 Conf.allowACMEHosts(log, true)
456
457 return nil
458}
459
460// MustLoadConfig loads the config, quitting on errors.
461func MustLoadConfig(doLoadTLSKeyCerts, checkACMEHosts bool) {
462 errs := LoadConfig(context.Background(), pkglog, doLoadTLSKeyCerts, checkACMEHosts)
463 if len(errs) > 1 {
464 pkglog.Error("loading config file: multiple errors")
465 for _, err := range errs {
466 pkglog.Errorx("config error", err)
467 }
468 pkglog.Fatal("stopping after multiple config errors")
469 } else if len(errs) == 1 {
470 pkglog.Fatalx("loading config file", errs[0])
471 }
472}
473
474// LoadConfig attempts to parse and load a config, returning any errors
475// encountered.
476func LoadConfig(ctx context.Context, log mlog.Log, doLoadTLSKeyCerts, checkACMEHosts bool) []error {
477 Shutdown, ShutdownCancel = context.WithCancel(context.Background())
478 Context, ContextCancel = context.WithCancel(context.Background())
479
480 c, errs := ParseConfig(ctx, log, ConfigStaticPath, false, doLoadTLSKeyCerts, checkACMEHosts)
481 if len(errs) > 0 {
482 return errs
483 }
484
485 mlog.SetConfig(c.Log)
486 SetConfig(c)
487 return nil
488}
489
490// SetConfig sets a new config. Not to be used during normal operation.
491func SetConfig(c *Config) {
492 // Cannot just assign *c to Conf, it would copy the mutex.
493 Conf = Config{c.Static, sync.Mutex{}, c.Log, sync.Mutex{}, c.Dynamic, c.dynamicMtime, c.DynamicLastCheck, c.AccountDestinationsLocked, c.aliases}
494
495 // If we have non-standard CA roots, use them for all HTTPS requests.
496 if Conf.Static.TLS.CertPool != nil {
497 http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{
498 RootCAs: Conf.Static.TLS.CertPool,
499 }
500 }
501
502 SetPedantic(c.Static.Pedantic)
503}
504
505// Set pedantic in all packages.
506func SetPedantic(p bool) {
507 dkim.Pedantic = p
508 dns.Pedantic = p
509 message.Pedantic = p
510 smtp.Pedantic = p
511 Pedantic = p
512}
513
514// ParseConfig parses the static config at path p. If checkOnly is true, no changes
515// are made, such as registering ACME identities. If doLoadTLSKeyCerts is true,
516// the TLS KeyCerts configuration is loaded and checked. This is used during the
517// quickstart in the case the user is going to provide their own certificates.
518// If checkACMEHosts is true, the hosts allowed for acme are compared with the
519// explicitly configured ips we are listening on.
520func ParseConfig(ctx context.Context, log mlog.Log, p string, checkOnly, doLoadTLSKeyCerts, checkACMEHosts bool) (c *Config, errs []error) {
521 c = &Config{
522 Static: config.Static{
523 DataDir: ".",
524 },
525 }
526
527 f, err := os.Open(p)
528 if err != nil {
529 if os.IsNotExist(err) && os.Getenv("MOXCONF") == "" {
530 return nil, []error{fmt.Errorf("open config file: %v (hint: use mox -config ... or set MOXCONF=...)", err)}
531 }
532 return nil, []error{fmt.Errorf("open config file: %v", err)}
533 }
534 defer f.Close()
535 if err := sconf.Parse(f, &c.Static); err != nil {
536 return nil, []error{fmt.Errorf("parsing %s%v", p, err)}
537 }
538
539 if xerrs := PrepareStaticConfig(ctx, log, p, c, checkOnly, doLoadTLSKeyCerts); len(xerrs) > 0 {
540 return nil, xerrs
541 }
542
543 pp := filepath.Join(filepath.Dir(p), "domains.conf")
544 c.Dynamic, c.dynamicMtime, c.AccountDestinationsLocked, c.aliases, errs = ParseDynamicConfig(ctx, log, pp, c.Static)
545
546 if !checkOnly {
547 c.allowACMEHosts(log, checkACMEHosts)
548 }
549
550 return c, errs
551}
552
553// PrepareStaticConfig parses the static config file and prepares data structures
554// for starting mox. If checkOnly is set no substantial changes are made, like
555// creating an ACME registration.
556func PrepareStaticConfig(ctx context.Context, log mlog.Log, configFile string, conf *Config, checkOnly, doLoadTLSKeyCerts bool) (errs []error) {
557 addErrorf := func(format string, args ...any) {
558 errs = append(errs, fmt.Errorf(format, args...))
559 }
560
561 c := &conf.Static
562
563 // check that mailbox is in unicode NFC normalized form.
564 checkMailboxNormf := func(mailbox string, format string, args ...any) {
565 s := norm.NFC.String(mailbox)
566 if mailbox != s {
567 msg := fmt.Sprintf(format, args...)
568 addErrorf("%s: mailbox %q is not in NFC normalized form, should be %q", msg, mailbox, s)
569 }
570 }
571
572 // Post-process logging config.
573 if logLevel, ok := mlog.Levels[c.LogLevel]; ok {
574 conf.Log = map[string]slog.Level{"": logLevel}
575 } else {
576 addErrorf("invalid log level %q", c.LogLevel)
577 }
578 for pkg, s := range c.PackageLogLevels {
579 if logLevel, ok := mlog.Levels[s]; ok {
580 conf.Log[pkg] = logLevel
581 } else {
582 addErrorf("invalid package log level %q", s)
583 }
584 }
585
586 if c.User == "" {
587 c.User = "mox"
588 }
589 u, err := user.Lookup(c.User)
590 if err != nil {
591 uid, err := strconv.ParseUint(c.User, 10, 32)
592 if err != nil {
593 addErrorf("parsing unknown user %s as uid: %v (hint: add user mox with \"useradd -d $PWD mox\" or specify a different username on the quickstart command-line)", c.User, err)
594 } else {
595 // We assume the same gid as uid.
596 c.UID = uint32(uid)
597 c.GID = uint32(uid)
598 }
599 } else {
600 if uid, err := strconv.ParseUint(u.Uid, 10, 32); err != nil {
601 addErrorf("parsing uid %s: %v", u.Uid, err)
602 } else {
603 c.UID = uint32(uid)
604 }
605 if gid, err := strconv.ParseUint(u.Gid, 10, 32); err != nil {
606 addErrorf("parsing gid %s: %v", u.Gid, err)
607 } else {
608 c.GID = uint32(gid)
609 }
610 }
611
612 hostname, err := dns.ParseDomain(c.Hostname)
613 if err != nil {
614 addErrorf("parsing hostname: %s", err)
615 } else if hostname.Name() != c.Hostname {
616 addErrorf("hostname must be in unicode form %q instead of %q", hostname.Name(), c.Hostname)
617 }
618 c.HostnameDomain = hostname
619
620 if c.HostTLSRPT.Account != "" {
621 tlsrptLocalpart, err := smtp.ParseLocalpart(c.HostTLSRPT.Localpart)
622 if err != nil {
623 addErrorf("invalid localpart %q for host tlsrpt: %v", c.HostTLSRPT.Localpart, err)
624 } else if tlsrptLocalpart.IsInternational() {
625 // Does not appear documented in ../rfc/8460, but similar to DMARC it makes sense
626 // to keep this ascii-only addresses.
627 addErrorf("host TLSRPT localpart %q is an internationalized address, only conventional ascii-only address allowed for interopability", tlsrptLocalpart)
628 }
629 c.HostTLSRPT.ParsedLocalpart = tlsrptLocalpart
630 }
631
632 // Return private key for host name for use with an ACME. Used to return the same
633 // private key as pre-generated for use with DANE, with its public key in DNS.
634 // We only use this key for Listener's that have this ACME configured, and for
635 // which the effective listener host name (either specific to the listener, or the
636 // global name) is requested. Other host names can get a fresh private key, they
637 // don't appear in DANE records.
638 //
639 // - run 0: only use listener with explicitly matching host name in listener
640 // (default quickstart config does not set it).
641 // - run 1: only look at public listener (and host matching mox host name)
642 // - run 2: all listeners (and host matching mox host name)
643 findACMEHostPrivateKey := func(acmeName, host string, keyType autocert.KeyType, run int) crypto.Signer {
644 for listenerName, l := range Conf.Static.Listeners {
645 if l.TLS == nil || l.TLS.ACME != acmeName {
646 continue
647 }
648 if run == 0 && host != l.HostnameDomain.ASCII {
649 continue
650 }
651 if run == 1 && listenerName != "public" || host != Conf.Static.HostnameDomain.ASCII {
652 continue
653 }
654 switch keyType {
655 case autocert.KeyRSA2048:
656 if len(l.TLS.HostPrivateRSA2048Keys) == 0 {
657 continue
658 }
659 return l.TLS.HostPrivateRSA2048Keys[0]
660 case autocert.KeyECDSAP256:
661 if len(l.TLS.HostPrivateECDSAP256Keys) == 0 {
662 continue
663 }
664 return l.TLS.HostPrivateECDSAP256Keys[0]
665 default:
666 return nil
667 }
668 }
669 return nil
670 }
671 // Make a function for an autocert.Manager.GetPrivateKey, using findACMEHostPrivateKey.
672 makeGetPrivateKey := func(acmeName string) func(host string, keyType autocert.KeyType) (crypto.Signer, error) {
673 return func(host string, keyType autocert.KeyType) (crypto.Signer, error) {
674 key := findACMEHostPrivateKey(acmeName, host, keyType, 0)
675 if key == nil {
676 key = findACMEHostPrivateKey(acmeName, host, keyType, 1)
677 }
678 if key == nil {
679 key = findACMEHostPrivateKey(acmeName, host, keyType, 2)
680 }
681 if key != nil {
682 log.Debug("found existing private key for certificate for host",
683 slog.String("acmename", acmeName),
684 slog.String("host", host),
685 slog.Any("keytype", keyType))
686 return key, nil
687 }
688 log.Debug("generating new private key for certificate for host",
689 slog.String("acmename", acmeName),
690 slog.String("host", host),
691 slog.Any("keytype", keyType))
692 switch keyType {
693 case autocert.KeyRSA2048:
694 return rsa.GenerateKey(cryptorand.Reader, 2048)
695 case autocert.KeyECDSAP256:
696 return ecdsa.GenerateKey(elliptic.P256(), cryptorand.Reader)
697 default:
698 return nil, fmt.Errorf("unrecognized requested key type %v", keyType)
699 }
700 }
701 }
702 for name, acme := range c.ACME {
703 addAcmeErrorf := func(format string, args ...any) {
704 addErrorf("acme provider %s: %s", name, fmt.Sprintf(format, args...))
705 }
706
707 var eabKeyID string
708 var eabKey []byte
709 if acme.ExternalAccountBinding != nil {
710 eabKeyID = acme.ExternalAccountBinding.KeyID
711 p := configDirPath(configFile, acme.ExternalAccountBinding.KeyFile)
712 buf, err := os.ReadFile(p)
713 if err != nil {
714 addAcmeErrorf("reading external account binding key: %s", err)
715 } else {
716 dec := make([]byte, base64.RawURLEncoding.DecodedLen(len(buf)))
717 n, err := base64.RawURLEncoding.Decode(dec, buf)
718 if err != nil {
719 addAcmeErrorf("parsing external account binding key as base64: %s", err)
720 } else {
721 eabKey = dec[:n]
722 }
723 }
724 }
725
726 if checkOnly {
727 continue
728 }
729
730 acmeDir := dataDirPath(configFile, c.DataDir, "acme")
731 os.MkdirAll(acmeDir, 0770)
732 manager, err := autotls.Load(log, name, acmeDir, acme.ContactEmail, acme.DirectoryURL, eabKeyID, eabKey, makeGetPrivateKey(name), Shutdown.Done())
733 if err != nil {
734 addAcmeErrorf("loading ACME identity: %s", err)
735 }
736 acme.Manager = manager
737
738 // Help configurations from older quickstarts.
739 if acme.IssuerDomainName == "" && acme.DirectoryURL == "https://acme-v02.api.letsencrypt.org/directory" {
740 acme.IssuerDomainName = "letsencrypt.org"
741 }
742
743 c.ACME[name] = acme
744 }
745
746 var haveUnspecifiedSMTPListener bool
747 for name, l := range c.Listeners {
748 addListenerErrorf := func(format string, args ...any) {
749 addErrorf("listener %s: %w", name, fmt.Errorf(format, args...))
750 }
751
752 if l.Hostname != "" {
753 d, err := dns.ParseDomain(l.Hostname)
754 if err != nil {
755 addListenerErrorf("parsing hostname %q: %s", l.Hostname, err)
756 }
757 l.HostnameDomain = d
758 }
759 if l.TLS != nil {
760 if l.TLS.ACME != "" && len(l.TLS.KeyCerts) != 0 {
761 addListenerErrorf("cannot have ACME and static key/certificates")
762 } else if l.TLS.ACME != "" {
763 acme, ok := c.ACME[l.TLS.ACME]
764 if !ok {
765 addListenerErrorf("unknown ACME provider %q", l.TLS.ACME)
766 }
767
768 // If only checking or with missing ACME definition, we don't have an acme manager,
769 // so set an empty tls config to continue.
770 var tlsconfig, tlsconfigFallback *tls.Config
771 if checkOnly || acme.Manager == nil {
772 tlsconfig = &tls.Config{}
773 tlsconfigFallback = &tls.Config{}
774 } else {
775 hostname := c.HostnameDomain
776 if l.Hostname != "" {
777 hostname = l.HostnameDomain
778 }
779 // If SNI is absent, we will use the listener hostname, but reject connections with
780 // an SNI hostname that is not allowlisted.
781 // Incoming SMTP deliveries use tlsconfigFallback for interoperability. TLS
782 // connections for unknown SNI hostnames fall back to a certificate for the
783 // listener hostname instead of causing the TLS connection to fail.
784 tlsconfig = acme.Manager.TLSConfig(hostname, true, false)
785 tlsconfigFallback = acme.Manager.TLSConfig(hostname, true, true)
786 l.TLS.ACMEConfig = acme.Manager.ACMETLSConfig
787 }
788 l.TLS.Config = tlsconfig
789 l.TLS.ConfigFallback = tlsconfigFallback
790 } else if len(l.TLS.KeyCerts) != 0 {
791 if doLoadTLSKeyCerts {
792 if err := loadTLSKeyCerts(configFile, "listener "+name, l.TLS); err != nil {
793 addListenerErrorf("%w", err)
794 }
795 }
796 } else {
797 addListenerErrorf("cannot have TLS config without ACME and without static keys/certificates")
798 }
799 for _, privKeyFile := range l.TLS.HostPrivateKeyFiles {
800 keyPath := configDirPath(configFile, privKeyFile)
801 privKey, err := loadPrivateKeyFile(keyPath)
802 if err != nil {
803 addListenerErrorf("parsing host private key for DANE and ACME certificates: %v", err)
804 continue
805 }
806 switch k := privKey.(type) {
807 case *rsa.PrivateKey:
808 if k.N.BitLen() != 2048 {
809 log.Error("need rsa key with 2048 bits, for host private key for DANE/ACME certificates, ignoring",
810 slog.String("listener", name),
811 slog.String("file", keyPath),
812 slog.Int("bits", k.N.BitLen()))
813 continue
814 }
815 l.TLS.HostPrivateRSA2048Keys = append(l.TLS.HostPrivateRSA2048Keys, k)
816 case *ecdsa.PrivateKey:
817 if k.Curve != elliptic.P256() {
818 log.Error("unrecognized ecdsa curve for host private key for DANE/ACME certificates, ignoring", slog.String("listener", name), slog.String("file", keyPath))
819 continue
820 }
821 l.TLS.HostPrivateECDSAP256Keys = append(l.TLS.HostPrivateECDSAP256Keys, k)
822 default:
823 log.Error("unrecognized key type for host private key for DANE/ACME certificates, ignoring",
824 slog.String("listener", name),
825 slog.String("file", keyPath),
826 slog.String("keytype", fmt.Sprintf("%T", privKey)))
827 continue
828 }
829 }
830 if l.TLS.ACME != "" && (len(l.TLS.HostPrivateRSA2048Keys) == 0) != (len(l.TLS.HostPrivateECDSAP256Keys) == 0) {
831 log.Warn("uncommon configuration with either only an RSA 2048 or ECDSA P256 host private key for DANE/ACME certificates; this ACME implementation can retrieve certificates for both type of keys, it is recommended to set either both or none; continuing")
832 }
833
834 // TLS 1.2 was introduced in 2008. TLS <1.2 was deprecated by ../rfc/8996:31 and ../rfc/8997:66 in 2021.
835 var minVersion uint16 = tls.VersionTLS12
836 if l.TLS.MinVersion != "" {
837 versions := map[string]uint16{
838 "TLSv1.0": tls.VersionTLS10,
839 "TLSv1.1": tls.VersionTLS11,
840 "TLSv1.2": tls.VersionTLS12,
841 "TLSv1.3": tls.VersionTLS13,
842 }
843 v, ok := versions[l.TLS.MinVersion]
844 if !ok {
845 addListenerErrorf("unknown TLS mininum version %q", l.TLS.MinVersion)
846 }
847 minVersion = v
848 }
849 if l.TLS.Config != nil {
850 l.TLS.Config.MinVersion = minVersion
851 }
852 if l.TLS.ConfigFallback != nil {
853 l.TLS.ConfigFallback.MinVersion = minVersion
854 }
855 if l.TLS.ACMEConfig != nil {
856 l.TLS.ACMEConfig.MinVersion = minVersion
857 }
858 } else {
859 var needsTLS []string
860 needtls := func(s string, v bool) {
861 if v {
862 needsTLS = append(needsTLS, s)
863 }
864 }
865 needtls("IMAPS", l.IMAPS.Enabled)
866 needtls("SMTP", l.SMTP.Enabled && !l.SMTP.NoSTARTTLS)
867 needtls("Submissions", l.Submissions.Enabled)
868 needtls("Submission", l.Submission.Enabled && !l.Submission.NoRequireSTARTTLS)
869 needtls("AccountHTTPS", l.AccountHTTPS.Enabled)
870 needtls("AdminHTTPS", l.AdminHTTPS.Enabled)
871 needtls("AutoconfigHTTPS", l.AutoconfigHTTPS.Enabled && !l.AutoconfigHTTPS.NonTLS)
872 needtls("MTASTSHTTPS", l.MTASTSHTTPS.Enabled && !l.MTASTSHTTPS.NonTLS)
873 needtls("WebserverHTTPS", l.WebserverHTTPS.Enabled)
874 if len(needsTLS) > 0 {
875 addListenerErrorf("no tls config specified, but requires tls for %s", strings.Join(needsTLS, ", "))
876 }
877 }
878 if l.AutoconfigHTTPS.Enabled && l.MTASTSHTTPS.Enabled && l.AutoconfigHTTPS.Port == l.MTASTSHTTPS.Port && l.AutoconfigHTTPS.NonTLS != l.MTASTSHTTPS.NonTLS {
879 addListenerErrorf("autoconfig and mta-sts enabled on same port but with both http and https")
880 }
881 if l.SMTP.Enabled {
882 if len(l.IPs) == 0 {
883 haveUnspecifiedSMTPListener = true
884 }
885 for _, ipstr := range l.IPs {
886 ip := net.ParseIP(ipstr)
887 if ip == nil {
888 addListenerErrorf("invalid IP %q", ipstr)
889 continue
890 }
891 if ip.IsUnspecified() {
892 haveUnspecifiedSMTPListener = true
893 break
894 }
895 if len(c.SpecifiedSMTPListenIPs) >= 2 {
896 haveUnspecifiedSMTPListener = true
897 } else if len(c.SpecifiedSMTPListenIPs) > 0 && (c.SpecifiedSMTPListenIPs[0].To4() == nil) == (ip.To4() == nil) {
898 haveUnspecifiedSMTPListener = true
899 } else {
900 c.SpecifiedSMTPListenIPs = append(c.SpecifiedSMTPListenIPs, ip)
901 }
902 }
903 }
904 for _, s := range l.SMTP.DNSBLs {
905 d, err := dns.ParseDomain(s)
906 if err != nil {
907 addListenerErrorf("parsing DNSBL zone %q: %s", s, err)
908 continue
909 }
910 l.SMTP.DNSBLZones = append(l.SMTP.DNSBLZones, d)
911 }
912 if l.IPsNATed && len(l.NATIPs) > 0 {
913 addListenerErrorf("both IPsNATed and NATIPs configued (remove deprecated IPsNATed)")
914 }
915 for _, ipstr := range l.NATIPs {
916 ip := net.ParseIP(ipstr)
917 if ip == nil {
918 addListenerErrorf("invalid ip %q", ipstr)
919 } else if ip.IsUnspecified() || ip.IsLoopback() {
920 addListenerErrorf("NAT ip that is the unspecified or loopback address %s", ipstr)
921 }
922 }
923 cleanPath := func(kind string, enabled bool, path string) string {
924 if !enabled {
925 return path
926 }
927 if path != "" && !strings.HasPrefix(path, "/") {
928 addListenerErrorf("%s with path %q that must start with a slash", kind, path)
929 } else if path != "" && !strings.HasSuffix(path, "/") {
930 log.Warn("http service path should end with a slash, using effective path with slash", slog.String("kind", kind), slog.String("path", path), slog.String("effectivepath", path+"/"))
931 path += "/"
932 }
933 return path
934 }
935 l.AccountHTTP.Path = cleanPath("AccountHTTP", l.AccountHTTP.Enabled, l.AccountHTTP.Path)
936 l.AccountHTTPS.Path = cleanPath("AccountHTTPS", l.AccountHTTPS.Enabled, l.AccountHTTPS.Path)
937 l.AdminHTTP.Path = cleanPath("AdminHTTP", l.AdminHTTP.Enabled, l.AdminHTTP.Path)
938 l.AdminHTTPS.Path = cleanPath("AdminHTTPS", l.AdminHTTPS.Enabled, l.AdminHTTPS.Path)
939 l.WebmailHTTP.Path = cleanPath("WebmailHTTP", l.WebmailHTTP.Enabled, l.WebmailHTTP.Path)
940 l.WebmailHTTPS.Path = cleanPath("WebmailHTTPS", l.WebmailHTTPS.Enabled, l.WebmailHTTPS.Path)
941 l.WebAPIHTTP.Path = cleanPath("WebAPIHTTP", l.WebAPIHTTP.Enabled, l.WebAPIHTTP.Path)
942 l.WebAPIHTTPS.Path = cleanPath("WebAPIHTTPS", l.WebAPIHTTPS.Enabled, l.WebAPIHTTPS.Path)
943 c.Listeners[name] = l
944 }
945 if haveUnspecifiedSMTPListener {
946 c.SpecifiedSMTPListenIPs = nil
947 }
948
949 var zerouse config.SpecialUseMailboxes
950 if len(c.DefaultMailboxes) > 0 && (c.InitialMailboxes.SpecialUse != zerouse || len(c.InitialMailboxes.Regular) > 0) {
951 addErrorf("cannot have both DefaultMailboxes and InitialMailboxes")
952 }
953 // DefaultMailboxes is deprecated.
954 for _, mb := range c.DefaultMailboxes {
955 checkMailboxNormf(mb, "default mailbox")
956 // We don't create parent mailboxes for default mailboxes.
957 if ParentMailboxName(mb) != "" {
958 addErrorf("default mailbox cannot be a child mailbox")
959 }
960 }
961 checkSpecialUseMailbox := func(nameOpt string) {
962 if nameOpt != "" {
963 checkMailboxNormf(nameOpt, "special-use initial mailbox")
964 if strings.EqualFold(nameOpt, "inbox") {
965 addErrorf("initial mailbox cannot be set to Inbox (Inbox is always created)")
966 }
967 // We don't currently create parent mailboxes for initial mailboxes.
968 if ParentMailboxName(nameOpt) != "" {
969 addErrorf("initial mailboxes cannot be child mailboxes")
970 }
971 }
972 }
973 checkSpecialUseMailbox(c.InitialMailboxes.SpecialUse.Archive)
974 checkSpecialUseMailbox(c.InitialMailboxes.SpecialUse.Draft)
975 checkSpecialUseMailbox(c.InitialMailboxes.SpecialUse.Junk)
976 checkSpecialUseMailbox(c.InitialMailboxes.SpecialUse.Sent)
977 checkSpecialUseMailbox(c.InitialMailboxes.SpecialUse.Trash)
978 for _, name := range c.InitialMailboxes.Regular {
979 checkMailboxNormf(name, "regular initial mailbox")
980 if strings.EqualFold(name, "inbox") {
981 addErrorf("initial regular mailbox cannot be set to Inbox (Inbox is always created)")
982 }
983 if ParentMailboxName(name) != "" {
984 addErrorf("initial mailboxes cannot be child mailboxes")
985 }
986 }
987
988 checkTransportSMTP := func(name string, isTLS bool, t *config.TransportSMTP) {
989 addTransportErrorf := func(format string, args ...any) {
990 addErrorf("transport %s: %s", name, fmt.Sprintf(format, args...))
991 }
992
993 var err error
994 t.DNSHost, err = dns.ParseDomain(t.Host)
995 if err != nil {
996 addTransportErrorf("bad host %s: %v", t.Host, err)
997 }
998
999 if isTLS && t.STARTTLSInsecureSkipVerify {
1000 addTransportErrorf("cannot have STARTTLSInsecureSkipVerify with immediate TLS")
1001 }
1002 if isTLS && t.NoSTARTTLS {
1003 addTransportErrorf("cannot have NoSTARTTLS with immediate TLS")
1004 }
1005
1006 if t.Auth == nil {
1007 return
1008 }
1009 seen := map[string]bool{}
1010 for _, m := range t.Auth.Mechanisms {
1011 if seen[m] {
1012 addTransportErrorf("duplicate authentication mechanism %s", m)
1013 }
1014 seen[m] = true
1015 switch m {
1016 case "SCRAM-SHA-256-PLUS":
1017 case "SCRAM-SHA-256":
1018 case "SCRAM-SHA-1-PLUS":
1019 case "SCRAM-SHA-1":
1020 case "CRAM-MD5":
1021 case "PLAIN":
1022 case "LOGIN":
1023 default:
1024 addTransportErrorf("unknown authentication mechanism %s", m)
1025 }
1026 }
1027
1028 t.Auth.EffectiveMechanisms = t.Auth.Mechanisms
1029 if len(t.Auth.EffectiveMechanisms) == 0 {
1030 t.Auth.EffectiveMechanisms = []string{"SCRAM-SHA-256-PLUS", "SCRAM-SHA-256", "SCRAM-SHA-1-PLUS", "SCRAM-SHA-1", "CRAM-MD5"}
1031 }
1032 }
1033
1034 checkTransportSocks := func(name string, t *config.TransportSocks) {
1035 addTransportErrorf := func(format string, args ...any) {
1036 addErrorf("transport %s: %s", name, fmt.Sprintf(format, args...))
1037 }
1038
1039 _, _, err := net.SplitHostPort(t.Address)
1040 if err != nil {
1041 addTransportErrorf("bad address %s: %v", t.Address, err)
1042 }
1043 for _, ipstr := range t.RemoteIPs {
1044 ip := net.ParseIP(ipstr)
1045 if ip == nil {
1046 addTransportErrorf("bad ip %s", ipstr)
1047 } else {
1048 t.IPs = append(t.IPs, ip)
1049 }
1050 }
1051 t.Hostname, err = dns.ParseDomain(t.RemoteHostname)
1052 if err != nil {
1053 addTransportErrorf("bad hostname %s: %v", t.RemoteHostname, err)
1054 }
1055 }
1056
1057 checkTransportDirect := func(name string, t *config.TransportDirect) {
1058 addTransportErrorf := func(format string, args ...any) {
1059 addErrorf("transport %s: %s", name, fmt.Sprintf(format, args...))
1060 }
1061
1062 if t.DisableIPv4 && t.DisableIPv6 {
1063 addTransportErrorf("both IPv4 and IPv6 are disabled, enable at least one")
1064 }
1065 t.IPFamily = "ip"
1066 if t.DisableIPv4 {
1067 t.IPFamily = "ip6"
1068 }
1069 if t.DisableIPv6 {
1070 t.IPFamily = "ip4"
1071 }
1072 }
1073
1074 checkTransportFail := func(name string, t *config.TransportFail) {
1075 addTransportErrorf := func(format string, args ...any) {
1076 addErrorf("transport %s: %s", name, fmt.Sprintf(format, args...))
1077 }
1078
1079 if t.SMTPCode == 0 {
1080 t.Code = smtp.C554TransactionFailed
1081 } else if t.SMTPCode/100 != 4 && t.SMTPCode/100 != 5 {
1082 addTransportErrorf("smtp code %d must be 4xx or 5xx", t.SMTPCode/100)
1083 } else {
1084 t.Code = t.SMTPCode
1085 }
1086
1087 if len(t.SMTPMessage) > 256 {
1088 addTransportErrorf("message must be <= 256 characters")
1089 }
1090 for _, c := range t.SMTPMessage {
1091 if c < ' ' || c >= 0x7f {
1092 addTransportErrorf("message cannot contain control characters including newlines, and must be ascii-only")
1093 }
1094 }
1095 t.Message = t.SMTPMessage
1096 if t.Message == "" {
1097 t.Message = "transport fail: explicit immediate delivery failure per configuration"
1098 }
1099 }
1100
1101 for name, t := range c.Transports {
1102 addTransportErrorf := func(format string, args ...any) {
1103 addErrorf("transport %s: %s", name, fmt.Sprintf(format, args...))
1104 }
1105
1106 n := 0
1107 if t.Submissions != nil {
1108 n++
1109 checkTransportSMTP(name, true, t.Submissions)
1110 }
1111 if t.Submission != nil {
1112 n++
1113 checkTransportSMTP(name, false, t.Submission)
1114 }
1115 if t.SMTP != nil {
1116 n++
1117 checkTransportSMTP(name, false, t.SMTP)
1118 }
1119 if t.Socks != nil {
1120 n++
1121 checkTransportSocks(name, t.Socks)
1122 }
1123 if t.Direct != nil {
1124 n++
1125 checkTransportDirect(name, t.Direct)
1126 }
1127 if t.Fail != nil {
1128 n++
1129 checkTransportFail(name, t.Fail)
1130 }
1131 if n > 1 {
1132 addTransportErrorf("cannot have multiple methods in a transport")
1133 }
1134 }
1135
1136 // Load CA certificate pool.
1137 if c.TLS.CA != nil {
1138 if c.TLS.CA.AdditionalToSystem {
1139 var err error
1140 c.TLS.CertPool, err = x509.SystemCertPool()
1141 if err != nil {
1142 addErrorf("fetching system CA cert pool: %v", err)
1143 }
1144 } else {
1145 c.TLS.CertPool = x509.NewCertPool()
1146 }
1147 for _, certfile := range c.TLS.CA.CertFiles {
1148 p := configDirPath(configFile, certfile)
1149 pemBuf, err := os.ReadFile(p)
1150 if err != nil {
1151 addErrorf("reading TLS CA cert file: %v", err)
1152 continue
1153 } else if !c.TLS.CertPool.AppendCertsFromPEM(pemBuf) {
1154 // todo: can we check more fully if we're getting some useful data back?
1155 addErrorf("no CA certs added from %q", p)
1156 }
1157 }
1158 }
1159 return
1160}
1161
1162// PrepareDynamicConfig parses the dynamic config file given a static file.
1163func ParseDynamicConfig(ctx context.Context, log mlog.Log, dynamicPath string, static config.Static) (c config.Dynamic, mtime time.Time, accDests map[string]AccountDestination, aliases map[string]config.Alias, errs []error) {
1164 addErrorf := func(format string, args ...any) {
1165 errs = append(errs, fmt.Errorf(format, args...))
1166 }
1167
1168 f, err := os.Open(dynamicPath)
1169 if err != nil {
1170 addErrorf("parsing domains config: %v", err)
1171 return
1172 }
1173 defer f.Close()
1174 fi, err := f.Stat()
1175 if err != nil {
1176 addErrorf("stat domains config: %v", err)
1177 }
1178 if err := sconf.Parse(f, &c); err != nil {
1179 addErrorf("parsing dynamic config file: %v", err)
1180 return
1181 }
1182
1183 accDests, aliases, errs = prepareDynamicConfig(ctx, log, dynamicPath, static, &c)
1184 return c, fi.ModTime(), accDests, aliases, errs
1185}
1186
1187func prepareDynamicConfig(ctx context.Context, log mlog.Log, dynamicPath string, static config.Static, c *config.Dynamic) (accDests map[string]AccountDestination, aliases map[string]config.Alias, errs []error) {
1188 addErrorf := func(format string, args ...any) {
1189 errs = append(errs, fmt.Errorf(format, args...))
1190 }
1191
1192 // Check that mailbox is in unicode NFC normalized form.
1193 checkMailboxNormf := func(mailbox string, what string, errorf func(format string, args ...any)) {
1194 s := norm.NFC.String(mailbox)
1195 if mailbox != s {
1196 errorf("%s: mailbox %q is not in NFC normalized form, should be %q", what, mailbox, s)
1197 }
1198 }
1199
1200 // Validate postmaster account exists.
1201 if _, ok := c.Accounts[static.Postmaster.Account]; !ok {
1202 addErrorf("postmaster account %q does not exist", static.Postmaster.Account)
1203 }
1204 checkMailboxNormf(static.Postmaster.Mailbox, "postmaster mailbox", addErrorf)
1205
1206 accDests = map[string]AccountDestination{}
1207 aliases = map[string]config.Alias{}
1208
1209 // Validate host TLSRPT account/address.
1210 if static.HostTLSRPT.Account != "" {
1211 if _, ok := c.Accounts[static.HostTLSRPT.Account]; !ok {
1212 addErrorf("host tlsrpt account %q does not exist", static.HostTLSRPT.Account)
1213 }
1214 checkMailboxNormf(static.HostTLSRPT.Mailbox, "host tlsrpt mailbox", addErrorf)
1215
1216 // Localpart has been parsed already.
1217
1218 addrFull := smtp.NewAddress(static.HostTLSRPT.ParsedLocalpart, static.HostnameDomain).String()
1219 dest := config.Destination{
1220 Mailbox: static.HostTLSRPT.Mailbox,
1221 HostTLSReports: true,
1222 }
1223 accDests[addrFull] = AccountDestination{false, static.HostTLSRPT.ParsedLocalpart, static.HostTLSRPT.Account, dest}
1224 }
1225
1226 var haveSTSListener, haveWebserverListener bool
1227 for _, l := range static.Listeners {
1228 if l.MTASTSHTTPS.Enabled {
1229 haveSTSListener = true
1230 }
1231 if l.WebserverHTTP.Enabled || l.WebserverHTTPS.Enabled {
1232 haveWebserverListener = true
1233 }
1234 }
1235
1236 checkRoutes := func(descr string, routes []config.Route) {
1237 parseRouteDomains := func(l []string) []string {
1238 var r []string
1239 for _, e := range l {
1240 if e == "." {
1241 r = append(r, e)
1242 continue
1243 }
1244 prefix := ""
1245 if strings.HasPrefix(e, ".") {
1246 prefix = "."
1247 e = e[1:]
1248 }
1249 d, err := dns.ParseDomain(e)
1250 if err != nil {
1251 addErrorf("%s: invalid domain %s: %v", descr, e, err)
1252 }
1253 r = append(r, prefix+d.ASCII)
1254 }
1255 return r
1256 }
1257
1258 for i := range routes {
1259 routes[i].FromDomainASCII = parseRouteDomains(routes[i].FromDomain)
1260 routes[i].ToDomainASCII = parseRouteDomains(routes[i].ToDomain)
1261 var ok bool
1262 routes[i].ResolvedTransport, ok = static.Transports[routes[i].Transport]
1263 if !ok {
1264 addErrorf("%s: route references undefined transport %s", descr, routes[i].Transport)
1265 }
1266 }
1267 }
1268
1269 checkRoutes("global routes", c.Routes)
1270
1271 // Validate domains.
1272 c.ClientSettingDomains = map[dns.Domain]struct{}{}
1273 for d, domain := range c.Domains {
1274 addDomainErrorf := func(format string, args ...any) {
1275 addErrorf("domain %v: %s", d, fmt.Sprintf(format, args...))
1276 }
1277
1278 dnsdomain, err := dns.ParseDomain(d)
1279 if err != nil {
1280 addDomainErrorf("parsing domain: %s", err)
1281 } else if dnsdomain.Name() != d {
1282 addDomainErrorf("must be specified in unicode form, %s", dnsdomain.Name())
1283 }
1284
1285 domain.Domain = dnsdomain
1286
1287 if domain.ClientSettingsDomain != "" {
1288 csd, err := dns.ParseDomain(domain.ClientSettingsDomain)
1289 if err != nil {
1290 addDomainErrorf("bad client settings domain %q: %s", domain.ClientSettingsDomain, err)
1291 }
1292 domain.ClientSettingsDNSDomain = csd
1293 c.ClientSettingDomains[csd] = struct{}{}
1294 }
1295
1296 if domain.LocalpartCatchallSeparator != "" && len(domain.LocalpartCatchallSeparators) != 0 {
1297 addDomainErrorf("cannot have both LocalpartCatchallSeparator and LocalpartCatchallSeparators")
1298 }
1299 domain.LocalpartCatchallSeparatorsEffective = domain.LocalpartCatchallSeparators
1300 if domain.LocalpartCatchallSeparator != "" {
1301 domain.LocalpartCatchallSeparatorsEffective = append(domain.LocalpartCatchallSeparatorsEffective, domain.LocalpartCatchallSeparator)
1302 }
1303 sepSeen := map[string]bool{}
1304 for _, sep := range domain.LocalpartCatchallSeparatorsEffective {
1305 if sepSeen[sep] {
1306 addDomainErrorf("duplicate localpart catchall separator %q", sep)
1307 }
1308 sepSeen[sep] = true
1309 }
1310
1311 for _, sign := range domain.DKIM.Sign {
1312 if _, ok := domain.DKIM.Selectors[sign]; !ok {
1313 addDomainErrorf("unknown selector %s for signing", sign)
1314 }
1315 }
1316 for name, sel := range domain.DKIM.Selectors {
1317 addSelectorErrorf := func(format string, args ...any) {
1318 addDomainErrorf("selector %s: %s", name, fmt.Sprintf(format, args...))
1319 }
1320
1321 seld, err := dns.ParseDomain(name)
1322 if err != nil {
1323 addSelectorErrorf("parsing selector: %s", err)
1324 } else if seld.Name() != name {
1325 addSelectorErrorf("must be specified in unicode form, %q", seld.Name())
1326 }
1327 sel.Domain = seld
1328
1329 if sel.Expiration != "" {
1330 exp, err := time.ParseDuration(sel.Expiration)
1331 if err != nil {
1332 addSelectorErrorf("invalid expiration %q: %v", sel.Expiration, err)
1333 } else {
1334 sel.ExpirationSeconds = int(exp / time.Second)
1335 }
1336 }
1337
1338 sel.HashEffective = sel.Hash
1339 switch sel.HashEffective {
1340 case "":
1341 sel.HashEffective = "sha256"
1342 case "sha1":
1343 log.Error("using sha1 with DKIM is deprecated as not secure enough, switch to sha256")
1344 case "sha256":
1345 default:
1346 addSelectorErrorf("unsupported hash %q", sel.HashEffective)
1347 }
1348
1349 pemBuf, err := os.ReadFile(configDirPath(dynamicPath, sel.PrivateKeyFile))
1350 if err != nil {
1351 addSelectorErrorf("reading private key: %s", err)
1352 continue
1353 }
1354 p, _ := pem.Decode(pemBuf)
1355 if p == nil {
1356 addSelectorErrorf("private key has no PEM block")
1357 continue
1358 }
1359 key, err := x509.ParsePKCS8PrivateKey(p.Bytes)
1360 if err != nil {
1361 addSelectorErrorf("parsing private key: %s", err)
1362 continue
1363 }
1364 switch k := key.(type) {
1365 case *rsa.PrivateKey:
1366 if k.N.BitLen() < 1024 {
1367 // ../rfc/6376:757
1368 // Let's help user do the right thing.
1369 addSelectorErrorf("rsa keys should be >= 1024 bits, is %d bits", k.N.BitLen())
1370 }
1371 sel.Key = k
1372 sel.Algorithm = fmt.Sprintf("rsa-%d", k.N.BitLen())
1373 case ed25519.PrivateKey:
1374 if sel.HashEffective != "sha256" {
1375 addSelectorErrorf("hash algorithm %q is not supported with ed25519, only sha256 is", sel.HashEffective)
1376 }
1377 sel.Key = k
1378 sel.Algorithm = "ed25519"
1379 default:
1380 addSelectorErrorf("private key type %T not yet supported", key)
1381 }
1382
1383 if len(sel.Headers) == 0 {
1384 // ../rfc/6376:2139
1385 // ../rfc/6376:2203
1386 // ../rfc/6376:2212
1387 // By default we seal signed headers, and we sign user-visible headers to
1388 // prevent/limit reuse of previously signed messages: All addressing fields, date
1389 // and subject, message-referencing fields, parsing instructions (content-type).
1390 sel.HeadersEffective = strings.Split("From,To,Cc,Bcc,Reply-To,References,In-Reply-To,Subject,Date,Message-Id,Content-Type", ",")
1391 } else {
1392 var from bool
1393 for _, h := range sel.Headers {
1394 from = from || strings.EqualFold(h, "From")
1395 // ../rfc/6376:2269
1396 if strings.EqualFold(h, "DKIM-Signature") || strings.EqualFold(h, "Received") || strings.EqualFold(h, "Return-Path") {
1397 log.Error("DKIM-signing header %q is recommended against as it may be modified in transit")
1398 }
1399 }
1400 if !from {
1401 addSelectorErrorf("From-field must always be DKIM-signed")
1402 }
1403 sel.HeadersEffective = sel.Headers
1404 }
1405
1406 domain.DKIM.Selectors[name] = sel
1407 }
1408
1409 if domain.MTASTS != nil {
1410 if !haveSTSListener {
1411 addDomainErrorf("MTA-STS enabled, but there is no listener for MTASTS")
1412 }
1413 sts := domain.MTASTS
1414 if sts.PolicyID == "" {
1415 addDomainErrorf("invalid empty MTA-STS PolicyID")
1416 }
1417 switch sts.Mode {
1418 case mtasts.ModeNone, mtasts.ModeTesting, mtasts.ModeEnforce:
1419 default:
1420 addDomainErrorf("invalid mtasts mode %q", sts.Mode)
1421 }
1422 }
1423
1424 checkRoutes("routes for domain", domain.Routes)
1425
1426 c.Domains[d] = domain
1427 }
1428
1429 // To determine ReportsOnly.
1430 domainHasAddress := map[string]bool{}
1431
1432 // Validate email addresses.
1433 for accName, acc := range c.Accounts {
1434 addAccountErrorf := func(format string, args ...any) {
1435 addErrorf("account %q: %s", accName, fmt.Sprintf(format, args...))
1436 }
1437
1438 var err error
1439 acc.DNSDomain, err = dns.ParseDomain(acc.Domain)
1440 if err != nil {
1441 addAccountErrorf("parsing domain %s: %s", acc.Domain, err)
1442 }
1443
1444 if strings.EqualFold(acc.RejectsMailbox, "Inbox") {
1445 addAccountErrorf("cannot set RejectsMailbox to inbox, messages will be removed automatically from the rejects mailbox")
1446 }
1447 if acc.Introbox != "" {
1448 mailbox, _, err := config.CheckMailboxName(acc.Introbox, false)
1449 if err != nil {
1450 addAccountErrorf("invalid Introbox mailbox: %v", err)
1451 } else {
1452 acc.Introbox = mailbox
1453 }
1454 if acc.Introbox == acc.RejectsMailbox {
1455 addAccountErrorf("cannot set Introbox and RejectsMailbox to the same mailbox")
1456 }
1457 }
1458 checkMailboxNormf(acc.RejectsMailbox, "rejects mailbox", addErrorf)
1459
1460 if len(acc.LoginDisabled) > 256 {
1461 addAccountErrorf("message for disabled login must be <256 characters")
1462 }
1463 for _, c := range acc.LoginDisabled {
1464 // For IMAP and SMTP. IMAP only allows UTF8 after "ENABLE IMAPrev2".
1465 if c < ' ' || c >= 0x7f {
1466 addAccountErrorf("message cannot contain control characters including newlines, and must be ascii-only")
1467 }
1468 }
1469
1470 if acc.AutomaticJunkFlags.JunkMailboxRegexp != "" {
1471 r, err := regexp.Compile(acc.AutomaticJunkFlags.JunkMailboxRegexp)
1472 if err != nil {
1473 addAccountErrorf("invalid JunkMailboxRegexp regular expression: %v", err)
1474 }
1475 acc.JunkMailbox = r
1476 }
1477 if acc.AutomaticJunkFlags.NeutralMailboxRegexp != "" {
1478 r, err := regexp.Compile(acc.AutomaticJunkFlags.NeutralMailboxRegexp)
1479 if err != nil {
1480 addAccountErrorf("invalid NeutralMailboxRegexp regular expression: %v", err)
1481 }
1482 acc.NeutralMailbox = r
1483 }
1484 if acc.AutomaticJunkFlags.NotJunkMailboxRegexp != "" {
1485 r, err := regexp.Compile(acc.AutomaticJunkFlags.NotJunkMailboxRegexp)
1486 if err != nil {
1487 addAccountErrorf("invalid NotJunkMailboxRegexp regular expression: %v", err)
1488 }
1489 acc.NotJunkMailbox = r
1490 }
1491
1492 if acc.JunkFilter != nil {
1493 params := acc.JunkFilter.Params
1494 if params.MaxPower < 0 || params.MaxPower > 0.5 {
1495 addAccountErrorf("junk filter MaxPower must be >= 0 and < 0.5")
1496 }
1497 if params.TopWords < 0 {
1498 addAccountErrorf("junk filter TopWords must be >= 0")
1499 }
1500 if params.IgnoreWords < 0 || params.IgnoreWords > 0.5 {
1501 addAccountErrorf("junk filter IgnoreWords must be >= 0 and < 0.5")
1502 }
1503 if params.RareWords < 0 {
1504 addAccountErrorf("junk filter RareWords must be >= 0")
1505 }
1506 }
1507
1508 acc.ParsedFromIDLoginAddresses = make([]smtp.Address, len(acc.FromIDLoginAddresses))
1509 for i, s := range acc.FromIDLoginAddresses {
1510 a, err := smtp.ParseAddress(s)
1511 if err != nil {
1512 addAccountErrorf("invalid fromid login address %q: %v", s, err)
1513 }
1514 // We check later on if address belongs to account.
1515 dom, ok := c.Domains[a.Domain.Name()]
1516 if !ok {
1517 addAccountErrorf("unknown domain in fromid login address %q", s)
1518 } else if len(dom.LocalpartCatchallSeparatorsEffective) == 0 {
1519 addAccountErrorf("localpart catchall separator not configured for domain for fromid login address %q", s)
1520 }
1521 acc.ParsedFromIDLoginAddresses[i] = a
1522 }
1523
1524 // Clear any previously derived state.
1525 acc.Aliases = nil
1526
1527 c.Accounts[accName] = acc
1528
1529 if acc.OutgoingWebhook != nil {
1530 u, err := url.Parse(acc.OutgoingWebhook.URL)
1531 if err == nil && (u.Scheme != "http" && u.Scheme != "https") {
1532 err = errors.New("scheme must be http or https")
1533 }
1534 if err != nil {
1535 addAccountErrorf("parsing outgoing hook url %q: %v", acc.OutgoingWebhook.URL, err)
1536 }
1537
1538 // note: outgoing hook events are in ../queue/hooks.go, ../mox-/config.go, ../queue.go and ../webapi/gendoc.sh. keep in sync.
1539 outgoingHookEvents := []string{"delivered", "suppressed", "delayed", "failed", "relayed", "expanded", "canceled", "unrecognized"}
1540 for _, e := range acc.OutgoingWebhook.Events {
1541 if !slices.Contains(outgoingHookEvents, e) {
1542 addAccountErrorf("unknown outgoing hook event %q", e)
1543 }
1544 }
1545 }
1546 if acc.IncomingWebhook != nil {
1547 u, err := url.Parse(acc.IncomingWebhook.URL)
1548 if err == nil && (u.Scheme != "http" && u.Scheme != "https") {
1549 err = errors.New("scheme must be http or https")
1550 }
1551 if err != nil {
1552 addAccountErrorf("parsing incoming hook url %q: %v", acc.IncomingWebhook.URL, err)
1553 }
1554 }
1555
1556 // todo deprecated: only localpart as keys for Destinations, we are replacing them with full addresses. if domains.conf is written, we won't have to do this again.
1557 replaceLocalparts := map[string]string{}
1558
1559 for addrName, dest := range acc.Destinations {
1560 addDestErrorf := func(format string, args ...any) {
1561 addAccountErrorf("destination %q: %s", addrName, fmt.Sprintf(format, args...))
1562 }
1563
1564 checkMailboxNormf(dest.Mailbox, "destination mailbox", addDestErrorf)
1565
1566 if dest.SMTPError != "" {
1567 if len(dest.SMTPError) > 256 {
1568 addDestErrorf("smtp error must be smaller than 256 bytes")
1569 }
1570 for _, c := range dest.SMTPError {
1571 if c < ' ' || c >= 0x7f {
1572 addDestErrorf("smtp error cannot contain contain control characters (including newlines) or non-ascii")
1573 break
1574 }
1575 }
1576
1577 if dest.Mailbox != "" {
1578 addDestErrorf("cannot have both SMTPError and Mailbox")
1579 }
1580 if len(dest.Rulesets) != 0 {
1581 addDestErrorf("cannot have both SMTPError and Rulesets")
1582 }
1583
1584 t := strings.SplitN(dest.SMTPError, " ", 2)
1585 switch t[0] {
1586 default:
1587 addDestErrorf("smtp error must be 421 or 550 (with optional message), not %q", dest.SMTPError)
1588
1589 case "421":
1590 dest.SMTPErrorCode = smtp.C451LocalErr
1591 dest.SMTPErrorSecode = smtp.SeSys3Other0
1592 dest.SMTPErrorMsg = "error processing"
1593 case "550":
1594 dest.SMTPErrorCode = smtp.C550MailboxUnavail
1595 dest.SMTPErrorSecode = smtp.SeAddr1UnknownDestMailbox1
1596 dest.SMTPErrorMsg = "no such user(s)"
1597 }
1598 if len(t) > 1 {
1599 dest.SMTPErrorMsg = strings.TrimSpace(t[1])
1600 }
1601 acc.Destinations[addrName] = dest
1602 }
1603
1604 if dest.MessageAuthRequiredSMTPError != "" {
1605 if len(dest.MessageAuthRequiredSMTPError) > 256 {
1606 addDestErrorf("message authentication required smtp error must be smaller than 256 bytes")
1607 }
1608 for _, c := range dest.MessageAuthRequiredSMTPError {
1609 if c < ' ' || c >= 0x7f {
1610 addDestErrorf("message authentication required smtp error cannot contain contain control characters (including newlines) or non-ascii")
1611 break
1612 }
1613 }
1614 }
1615
1616 for i, rs := range dest.Rulesets {
1617 addRulesetErrorf := func(format string, args ...any) {
1618 addDestErrorf("ruleset %d: %s", i+1, fmt.Sprintf(format, args...))
1619 }
1620
1621 checkMailboxNormf(rs.Mailbox, "ruleset mailbox", addRulesetErrorf)
1622
1623 n := 0
1624
1625 if rs.SMTPMailFromRegexp != "" {
1626 n++
1627 r, err := regexp.Compile(rs.SMTPMailFromRegexp)
1628 if err != nil {
1629 addRulesetErrorf("invalid SMTPMailFrom regular expression: %v", err)
1630 }
1631 c.Accounts[accName].Destinations[addrName].Rulesets[i].SMTPMailFromRegexpCompiled = r
1632 }
1633 if rs.MsgFromRegexp != "" {
1634 n++
1635 r, err := regexp.Compile(rs.MsgFromRegexp)
1636 if err != nil {
1637 addRulesetErrorf("invalid MsgFrom regular expression: %v", err)
1638 }
1639 c.Accounts[accName].Destinations[addrName].Rulesets[i].MsgFromRegexpCompiled = r
1640 }
1641 if rs.VerifiedDomain != "" {
1642 n++
1643 d, err := dns.ParseDomain(rs.VerifiedDomain)
1644 if err != nil {
1645 addRulesetErrorf("invalid VerifiedDomain: %v", err)
1646 }
1647 c.Accounts[accName].Destinations[addrName].Rulesets[i].VerifiedDNSDomain = d
1648 }
1649
1650 var hdr [][2]*regexp.Regexp
1651 for k, v := range rs.HeadersRegexp {
1652 n++
1653 if strings.ToLower(k) != k {
1654 addRulesetErrorf("header field %q must only have lower case characters", k)
1655 }
1656 if strings.ToLower(v) != v {
1657 addRulesetErrorf("header value %q must only have lower case characters", v)
1658 }
1659 rk, err := regexp.Compile(k)
1660 if err != nil {
1661 addRulesetErrorf("invalid rule header regexp %q: %v", k, err)
1662 }
1663 rv, err := regexp.Compile(v)
1664 if err != nil {
1665 addRulesetErrorf("invalid rule header regexp %q: %v", v, err)
1666 }
1667 hdr = append(hdr, [...]*regexp.Regexp{rk, rv})
1668 }
1669 c.Accounts[accName].Destinations[addrName].Rulesets[i].HeadersRegexpCompiled = hdr
1670
1671 if n == 0 {
1672 addRulesetErrorf("ruleset must have at least one rule")
1673 }
1674
1675 if rs.IsForward && rs.ListAllowDomain != "" {
1676 addRulesetErrorf("ruleset cannot have both IsForward and ListAllowDomain")
1677 }
1678 if rs.IsForward {
1679 if rs.SMTPMailFromRegexp == "" || rs.VerifiedDomain == "" {
1680 addRulesetErrorf("ruleset with IsForward must have both SMTPMailFromRegexp and VerifiedDomain too")
1681 }
1682 }
1683 if rs.ListAllowDomain != "" {
1684 d, err := dns.ParseDomain(rs.ListAllowDomain)
1685 if err != nil {
1686 addRulesetErrorf("invalid ListAllowDomain %q: %v", rs.ListAllowDomain, err)
1687 }
1688 c.Accounts[accName].Destinations[addrName].Rulesets[i].ListAllowDNSDomain = d
1689 }
1690
1691 checkMailboxNormf(rs.AcceptRejectsToMailbox, "rejects mailbox", addRulesetErrorf)
1692 if strings.EqualFold(rs.AcceptRejectsToMailbox, "inbox") {
1693 addRulesetErrorf("AcceptRejectsToMailbox cannot be set to Inbox")
1694 }
1695 }
1696
1697 // Catchall destination for domain.
1698 if strings.HasPrefix(addrName, "@") {
1699 d, err := dns.ParseDomain(addrName[1:])
1700 if err != nil {
1701 addDestErrorf("parsing domain %q", addrName[1:])
1702 continue
1703 } else if _, ok := c.Domains[d.Name()]; !ok {
1704 addDestErrorf("unknown domain for address")
1705 continue
1706 }
1707 domainHasAddress[d.Name()] = true
1708 addrFull := "@" + d.Name()
1709 if _, ok := accDests[addrFull]; ok {
1710 addDestErrorf("duplicate canonicalized catchall destination address %s", addrFull)
1711 }
1712 accDests[addrFull] = AccountDestination{true, "", accName, dest}
1713 continue
1714 }
1715
1716 // todo deprecated: remove support for parsing destination as just a localpart instead full address.
1717 var address smtp.Address
1718 if localpart, err := smtp.ParseLocalpart(addrName); err != nil && errors.Is(err, smtp.ErrBadLocalpart) {
1719 address, err = smtp.ParseAddress(addrName)
1720 if err != nil {
1721 addDestErrorf("invalid email address")
1722 continue
1723 } else if _, ok := c.Domains[address.Domain.Name()]; !ok {
1724 addDestErrorf("unknown domain for address")
1725 continue
1726 }
1727 } else {
1728 if err != nil {
1729 addDestErrorf("invalid localpart %q", addrName)
1730 continue
1731 }
1732 address = smtp.NewAddress(localpart, acc.DNSDomain)
1733 if _, ok := c.Domains[acc.DNSDomain.Name()]; !ok {
1734 addDestErrorf("unknown domain %s", acc.DNSDomain.Name())
1735 continue
1736 }
1737 replaceLocalparts[addrName] = address.Pack(true)
1738 }
1739
1740 origLP := address.Localpart
1741 dc := c.Domains[address.Domain.Name()]
1742 domainHasAddress[address.Domain.Name()] = true
1743 lp := CanonicalLocalpart(address.Localpart, dc)
1744 var hasSep bool
1745 for _, sep := range dc.LocalpartCatchallSeparatorsEffective {
1746 if strings.Contains(string(address.Localpart), sep) {
1747 hasSep = true
1748 addDestErrorf("localpart of address %s includes domain catchall separator %s", address, sep)
1749 }
1750 }
1751 if !hasSep {
1752 address.Localpart = lp
1753 }
1754 addrFull := address.Pack(true)
1755 if _, ok := accDests[addrFull]; ok {
1756 addDestErrorf("duplicate canonicalized destination address %s", addrFull)
1757 }
1758 accDests[addrFull] = AccountDestination{false, origLP, accName, dest}
1759 }
1760
1761 for lp, addr := range replaceLocalparts {
1762 dest, ok := acc.Destinations[lp]
1763 if !ok {
1764 addAccountErrorf("could not find localpart %q to replace with address in destinations", lp)
1765 } else {
1766 log.Warn(`deprecation warning: support for account destination addresses specified as just localpart ("username") instead of full email address will be removed in the future; update domains.conf, for each Account, for each Destination, ensure each key is an email address by appending "@" and the default domain for the account`,
1767 slog.Any("localpart", lp),
1768 slog.Any("address", addr),
1769 slog.String("account", accName))
1770 acc.Destinations[addr] = dest
1771 delete(acc.Destinations, lp)
1772 }
1773 }
1774
1775 // Now that all addresses are parsed, check if all fromid login addresses match
1776 // configured addresses.
1777 for i, a := range acc.ParsedFromIDLoginAddresses {
1778 // For domain catchall.
1779 if _, ok := accDests["@"+a.Domain.Name()]; ok {
1780 continue
1781 }
1782 dc := c.Domains[a.Domain.Name()]
1783 a.Localpart = CanonicalLocalpart(a.Localpart, dc)
1784 if _, ok := accDests[a.Pack(true)]; !ok {
1785 addAccountErrorf("fromid login address %q does not match its destination addresses", acc.FromIDLoginAddresses[i])
1786 }
1787 }
1788
1789 checkRoutes("routes for account", acc.Routes)
1790 }
1791
1792 // Set DMARC destinations.
1793 for d, domain := range c.Domains {
1794 addDomainErrorf := func(format string, args ...any) {
1795 addErrorf("domain %s: %s", d, fmt.Sprintf(format, args...))
1796 }
1797
1798 dmarc := domain.DMARC
1799 if dmarc == nil {
1800 continue
1801 }
1802 if _, ok := c.Accounts[dmarc.Account]; !ok {
1803 addDomainErrorf("DMARC account %q does not exist", dmarc.Account)
1804 }
1805
1806 // Note: For backwards compabilitiy, DMARC reporting localparts can contain catchall separators.
1807 lp, err := smtp.ParseLocalpart(dmarc.Localpart)
1808 if err != nil {
1809 addDomainErrorf("invalid DMARC localpart %q: %s", dmarc.Localpart, err)
1810 }
1811 if lp.IsInternational() {
1812 // ../rfc/8616:234
1813 addDomainErrorf("DMARC localpart %q is an internationalized address, only conventional ascii-only address possible for interopability", lp)
1814 }
1815 addrdom := domain.Domain
1816 if dmarc.Domain != "" {
1817 addrdom, err = dns.ParseDomain(dmarc.Domain)
1818 if err != nil {
1819 addDomainErrorf("DMARC domain %q: %s", dmarc.Domain, err)
1820 } else if adomain, ok := c.Domains[addrdom.Name()]; !ok {
1821 addDomainErrorf("unknown domain %q for DMARC address", addrdom)
1822 } else if !adomain.LocalpartCaseSensitive {
1823 lp = smtp.Localpart(strings.ToLower(string(lp)))
1824 }
1825 } else if !domain.LocalpartCaseSensitive {
1826 lp = smtp.Localpart(strings.ToLower(string(lp)))
1827 }
1828 if addrdom == domain.Domain {
1829 domainHasAddress[addrdom.Name()] = true
1830 }
1831
1832 domain.DMARC.ParsedLocalpart = lp
1833 domain.DMARC.DNSDomain = addrdom
1834 c.Domains[d] = domain
1835 addrFull := smtp.NewAddress(lp, addrdom).String()
1836 dest := config.Destination{
1837 Mailbox: dmarc.Mailbox,
1838 DMARCReports: true,
1839 }
1840 checkMailboxNormf(dmarc.Mailbox, "DMARC mailbox for account", addDomainErrorf)
1841 accDests[addrFull] = AccountDestination{false, lp, dmarc.Account, dest}
1842 }
1843
1844 // Set TLSRPT destinations.
1845 for d, domain := range c.Domains {
1846 addDomainErrorf := func(format string, args ...any) {
1847 addErrorf("domain %s: %s", d, fmt.Sprintf(format, args...))
1848 }
1849
1850 tlsrpt := domain.TLSRPT
1851 if tlsrpt == nil {
1852 continue
1853 }
1854 if _, ok := c.Accounts[tlsrpt.Account]; !ok {
1855 addDomainErrorf("TLSRPT account %q does not exist", tlsrpt.Account)
1856 }
1857
1858 // Note: For backwards compabilitiy, TLS reporting localparts can contain catchall separators.
1859 lp, err := smtp.ParseLocalpart(tlsrpt.Localpart)
1860 if err != nil {
1861 addDomainErrorf("invalid TLSRPT localpart %q: %s", tlsrpt.Localpart, err)
1862 }
1863 if lp.IsInternational() {
1864 // Does not appear documented in ../rfc/8460, but similar to DMARC it makes sense
1865 // to keep this ascii-only addresses.
1866 addDomainErrorf("TLSRPT localpart %q is an internationalized address, only conventional ascii-only address allowed for interopability", lp)
1867 }
1868 addrdom := domain.Domain
1869 if tlsrpt.Domain != "" {
1870 addrdom, err = dns.ParseDomain(tlsrpt.Domain)
1871 if err != nil {
1872 addDomainErrorf("TLSRPT domain %q: %s", tlsrpt.Domain, err)
1873 } else if adomain, ok := c.Domains[addrdom.Name()]; !ok {
1874 addDomainErrorf("unknown domain %q for TLSRPT address", tlsrpt.Domain)
1875 } else if !adomain.LocalpartCaseSensitive {
1876 lp = smtp.Localpart(strings.ToLower(string(lp)))
1877 }
1878 } else if !domain.LocalpartCaseSensitive {
1879 lp = smtp.Localpart(strings.ToLower(string(lp)))
1880 }
1881 if addrdom == domain.Domain {
1882 domainHasAddress[addrdom.Name()] = true
1883 }
1884
1885 domain.TLSRPT.ParsedLocalpart = lp
1886 domain.TLSRPT.DNSDomain = addrdom
1887 c.Domains[d] = domain
1888 addrFull := smtp.NewAddress(lp, addrdom).String()
1889 dest := config.Destination{
1890 Mailbox: tlsrpt.Mailbox,
1891 DomainTLSReports: true,
1892 }
1893 checkMailboxNormf(tlsrpt.Mailbox, "TLSRPT mailbox", addDomainErrorf)
1894 accDests[addrFull] = AccountDestination{false, lp, tlsrpt.Account, dest}
1895 }
1896
1897 // Set ReportsOnly for domains, based on whether we have seen addresses (possibly
1898 // from DMARC or TLS reporting).
1899 for d, domain := range c.Domains {
1900 domain.ReportsOnly = !domainHasAddress[domain.Domain.Name()]
1901 c.Domains[d] = domain
1902 }
1903
1904 // Aliases, per domain. Also add references to accounts.
1905 for d, domain := range c.Domains {
1906 for lpstr, a := range domain.Aliases {
1907 addAliasErrorf := func(format string, args ...any) {
1908 addErrorf("domain %s: alias %s: %s", d, lpstr, fmt.Sprintf(format, args...))
1909 }
1910
1911 var err error
1912 a.LocalpartStr = lpstr
1913 var clp smtp.Localpart
1914 lp, err := smtp.ParseLocalpart(lpstr)
1915 if err != nil {
1916 addAliasErrorf("parsing alias: %v", err)
1917 continue
1918 } else {
1919 var hasSep bool
1920 for _, sep := range domain.LocalpartCatchallSeparatorsEffective {
1921 if strings.Contains(string(lp), sep) {
1922 addAliasErrorf("alias contains localpart catchall separator")
1923 hasSep = true
1924 }
1925 }
1926 if hasSep {
1927 continue
1928 }
1929 clp = CanonicalLocalpart(lp, domain)
1930 }
1931
1932 addr := smtp.NewAddress(clp, domain.Domain).Pack(true)
1933 if _, ok := aliases[addr]; ok {
1934 addAliasErrorf("duplicate alias address %q", addr)
1935 continue
1936 }
1937 if _, ok := accDests[addr]; ok {
1938 addAliasErrorf("alias %q already present as regular address", addr)
1939 continue
1940 }
1941 if len(a.Addresses) == 0 {
1942 // Not currently possible, Addresses isn't optional.
1943 addAliasErrorf("alias %q needs at least one destination address", addr)
1944 continue
1945 }
1946 a.ParsedAddresses = make([]config.AliasAddress, 0, len(a.Addresses))
1947 seen := map[string]bool{}
1948 for _, destAddr := range a.Addresses {
1949 da, err := smtp.ParseAddress(destAddr)
1950 if err != nil {
1951 addAliasErrorf("parsing destination address %q: %v", destAddr, err)
1952 continue
1953 }
1954 dastr := da.Pack(true)
1955 accDest, ok := accDests[dastr]
1956 if !ok {
1957 addAliasErrorf("references non-existent address %q", destAddr)
1958 continue
1959 }
1960 if seen[dastr] {
1961 addAliasErrorf("duplicate address %q", destAddr)
1962 continue
1963 }
1964 seen[dastr] = true
1965 aa := config.AliasAddress{Address: da, AccountName: accDest.Account, Destination: accDest.Destination}
1966 a.ParsedAddresses = append(a.ParsedAddresses, aa)
1967 }
1968 a.Domain = domain.Domain
1969 c.Domains[d].Aliases[lpstr] = a
1970 aliases[addr] = a
1971
1972 for _, aa := range a.ParsedAddresses {
1973 acc := c.Accounts[aa.AccountName]
1974 var addrs []string
1975 if a.ListMembers {
1976 addrs = make([]string, len(a.ParsedAddresses))
1977 for i := range a.ParsedAddresses {
1978 addrs[i] = a.ParsedAddresses[i].Address.Pack(true)
1979 }
1980 }
1981 // Keep the non-sensitive fields.
1982 accAlias := config.Alias{
1983 PostPublic: a.PostPublic,
1984 ListMembers: a.ListMembers,
1985 AllowMsgFrom: a.AllowMsgFrom,
1986 LocalpartStr: a.LocalpartStr,
1987 Domain: a.Domain,
1988 }
1989 acc.Aliases = append(acc.Aliases, config.AddressAlias{SubscriptionAddress: aa.Address.Pack(true), Alias: accAlias, MemberAddresses: addrs})
1990 c.Accounts[aa.AccountName] = acc
1991 }
1992 }
1993 }
1994
1995 // Check webserver configs.
1996 if (len(c.WebDomainRedirects) > 0 || len(c.WebHandlers) > 0) && !haveWebserverListener {
1997 addErrorf("WebDomainRedirects or WebHandlers configured but no listener with WebserverHTTP or WebserverHTTPS enabled")
1998 }
1999
2000 c.WebDNSDomainRedirects = map[dns.Domain]dns.Domain{}
2001 for from, to := range c.WebDomainRedirects {
2002 addRedirectErrorf := func(format string, args ...any) {
2003 addErrorf("web redirect %s to %s: %s", from, to, fmt.Sprintf(format, args...))
2004 }
2005
2006 fromdom, err := dns.ParseDomain(from)
2007 if err != nil {
2008 addRedirectErrorf("parsing domain for redirect %s: %v", from, err)
2009 }
2010 todom, err := dns.ParseDomain(to)
2011 if err != nil {
2012 addRedirectErrorf("parsing domain for redirect %s: %v", to, err)
2013 } else if fromdom == todom {
2014 addRedirectErrorf("will not redirect domain %s to itself", todom)
2015 }
2016 var zerodom dns.Domain
2017 if _, ok := c.WebDNSDomainRedirects[fromdom]; ok && fromdom != zerodom {
2018 addRedirectErrorf("duplicate redirect domain %s", from)
2019 }
2020 c.WebDNSDomainRedirects[fromdom] = todom
2021 }
2022
2023 for i := range c.WebHandlers {
2024 wh := &c.WebHandlers[i]
2025
2026 addHandlerErrorf := func(format string, args ...any) {
2027 addErrorf("webhandler %s %s: %s", wh.Domain, wh.PathRegexp, fmt.Sprintf(format, args...))
2028 }
2029
2030 if wh.LogName == "" {
2031 wh.Name = fmt.Sprintf("%d", i)
2032 } else {
2033 wh.Name = wh.LogName
2034 }
2035
2036 dom, err := dns.ParseDomain(wh.Domain)
2037 if err != nil {
2038 addHandlerErrorf("parsing domain: %v", err)
2039 }
2040 wh.DNSDomain = dom
2041
2042 if !strings.HasPrefix(wh.PathRegexp, "^") {
2043 addHandlerErrorf("path regexp must start with a ^")
2044 }
2045 re, err := regexp.Compile(wh.PathRegexp)
2046 if err != nil {
2047 addHandlerErrorf("compiling regexp: %v", err)
2048 }
2049 wh.Path = re
2050
2051 var n int
2052 if wh.WebStatic != nil {
2053 n++
2054 ws := wh.WebStatic
2055 if ws.StripPrefix != "" && !strings.HasPrefix(ws.StripPrefix, "/") {
2056 addHandlerErrorf("static: prefix to strip %s must start with a slash", ws.StripPrefix)
2057 }
2058 for k := range ws.ResponseHeaders {
2059 xk := k
2060 k := strings.TrimSpace(xk)
2061 if k != xk || k == "" {
2062 addHandlerErrorf("static: bad header %q", xk)
2063 }
2064 }
2065 }
2066 if wh.WebRedirect != nil {
2067 n++
2068 wr := wh.WebRedirect
2069 if wr.BaseURL != "" {
2070 u, err := url.Parse(wr.BaseURL)
2071 if err != nil {
2072 addHandlerErrorf("redirect: parsing redirect url %s: %v", wr.BaseURL, err)
2073 }
2074 switch u.Path {
2075 case "", "/":
2076 u.Path = "/"
2077 default:
2078 addHandlerErrorf("redirect: BaseURL must have empty path: %s", wr.BaseURL)
2079 }
2080 wr.URL = u
2081 }
2082 if wr.OrigPathRegexp != "" && wr.ReplacePath != "" {
2083 re, err := regexp.Compile(wr.OrigPathRegexp)
2084 if err != nil {
2085 addHandlerErrorf("compiling regexp %s: %v", wr.OrigPathRegexp, err)
2086 }
2087 wr.OrigPath = re
2088 } else if wr.OrigPathRegexp != "" || wr.ReplacePath != "" {
2089 addHandlerErrorf("redirect: must have either both OrigPathRegexp and ReplacePath, or neither")
2090 } else if wr.BaseURL == "" {
2091 addHandlerErrorf("must at least one of BaseURL and OrigPathRegexp+ReplacePath")
2092 }
2093 if wr.StatusCode != 0 && (wr.StatusCode < 300 || wr.StatusCode >= 400) {
2094 addHandlerErrorf("redirect: invalid redirect status code %d", wr.StatusCode)
2095 }
2096 }
2097 if wh.WebForward != nil {
2098 n++
2099 wf := wh.WebForward
2100 u, err := url.Parse(wf.URL)
2101 if err != nil {
2102 addHandlerErrorf("forward: parsing url %s: %v", wf.URL, err)
2103 }
2104 wf.TargetURL = u
2105
2106 for k := range wf.ResponseHeaders {
2107 xk := k
2108 k := strings.TrimSpace(xk)
2109 if k != xk || k == "" {
2110 addHandlerErrorf("forrward: bad header %q", xk)
2111 }
2112 }
2113 }
2114 if wh.WebInternal != nil {
2115 n++
2116 wi := wh.WebInternal
2117 if !strings.HasPrefix(wi.BasePath, "/") || !strings.HasSuffix(wi.BasePath, "/") {
2118 addHandlerErrorf("internal service: base path %q must start and end with /", wi.BasePath)
2119 }
2120 // todo: we could make maxMsgSize and accountPath configurable
2121 const isForwarded = false
2122 switch wi.Service {
2123 case "admin":
2124 wi.Handler = NewWebadminHandler(wi.BasePath, isForwarded)
2125 case "account":
2126 wi.Handler = NewWebaccountHandler(wi.BasePath, isForwarded)
2127 case "webmail":
2128 accountPath := ""
2129 wi.Handler = NewWebmailHandler(config.DefaultMaxMsgSize, wi.BasePath, isForwarded, accountPath)
2130 case "webapi":
2131 wi.Handler = NewWebapiHandler(config.DefaultMaxMsgSize, wi.BasePath, isForwarded)
2132 default:
2133 addHandlerErrorf("internal service: unknown service %q", wi.Service)
2134 }
2135 wi.Handler = SafeHeaders(http.StripPrefix(wi.BasePath[:len(wi.BasePath)-1], wi.Handler))
2136 }
2137 if n != 1 {
2138 addHandlerErrorf("must have exactly one handler, not %d", n)
2139 }
2140 }
2141
2142 c.MonitorDNSBLZones = nil
2143 for _, s := range c.MonitorDNSBLs {
2144 d, err := dns.ParseDomain(s)
2145 if err != nil {
2146 addErrorf("dnsbl %s: parsing dnsbl zone: %v", s, err)
2147 continue
2148 }
2149 if slices.Contains(c.MonitorDNSBLZones, d) {
2150 addErrorf("dnsbl %s: duplicate zone", s)
2151 continue
2152 }
2153 c.MonitorDNSBLZones = append(c.MonitorDNSBLZones, d)
2154 }
2155
2156 return
2157}
2158
2159func loadPrivateKeyFile(keyPath string) (crypto.Signer, error) {
2160 keyBuf, err := os.ReadFile(keyPath)
2161 if err != nil {
2162 return nil, fmt.Errorf("reading host private key: %v", err)
2163 }
2164 b, _ := pem.Decode(keyBuf)
2165 if b == nil {
2166 return nil, fmt.Errorf("parsing pem block for private key: %v", err)
2167 }
2168 var privKey any
2169 switch b.Type {
2170 case "PRIVATE KEY":
2171 privKey, err = x509.ParsePKCS8PrivateKey(b.Bytes)
2172 case "RSA PRIVATE KEY":
2173 privKey, err = x509.ParsePKCS1PrivateKey(b.Bytes)
2174 case "EC PRIVATE KEY":
2175 privKey, err = x509.ParseECPrivateKey(b.Bytes)
2176 default:
2177 err = fmt.Errorf("unknown pem type %q", b.Type)
2178 }
2179 if err != nil {
2180 return nil, fmt.Errorf("parsing private key: %v", err)
2181 }
2182 if k, ok := privKey.(crypto.Signer); ok {
2183 return k, nil
2184 }
2185 return nil, fmt.Errorf("parsed private key not a crypto.Signer, but %T", privKey)
2186}
2187
2188func loadTLSKeyCerts(configFile, kind string, ctls *config.TLS) error {
2189 certs := []tls.Certificate{}
2190 for _, kp := range ctls.KeyCerts {
2191 certPath := configDirPath(configFile, kp.CertFile)
2192 keyPath := configDirPath(configFile, kp.KeyFile)
2193 cert, err := loadX509KeyPairPrivileged(certPath, keyPath)
2194 if err != nil {
2195 return fmt.Errorf("tls config for %q: parsing x509 key pair: %v", kind, err)
2196 }
2197 certs = append(certs, cert)
2198 }
2199 ctls.Config = &tls.Config{
2200 Certificates: certs,
2201 }
2202 ctls.ConfigFallback = ctls.Config
2203 return nil
2204}
2205
2206// load x509 key/cert files from file descriptor possibly passed in by privileged
2207// process.
2208func loadX509KeyPairPrivileged(certPath, keyPath string) (tls.Certificate, error) {
2209 certBuf, err := readFilePrivileged(certPath)
2210 if err != nil {
2211 return tls.Certificate{}, fmt.Errorf("reading tls certificate: %v", err)
2212 }
2213 keyBuf, err := readFilePrivileged(keyPath)
2214 if err != nil {
2215 return tls.Certificate{}, fmt.Errorf("reading tls key: %v", err)
2216 }
2217 return tls.X509KeyPair(certBuf, keyBuf)
2218}
2219
2220// like os.ReadFile, but open privileged file possibly passed in by root process.
2221func readFilePrivileged(path string) ([]byte, error) {
2222 f, err := OpenPrivileged(path)
2223 if err != nil {
2224 return nil, err
2225 }
2226 defer f.Close()
2227 return io.ReadAll(f)
2228}
2229