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