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