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
64// Config as used in the code, a processed version of what is in the config file.
65//
66// Use methods to lookup a domain/account/address in the dynamic configuration.
67type Config struct {
68 Static config.Static // Does not change during the lifetime of a running instance.
69
70 logMutex sync.Mutex // For accessing the log levels.
71 Log map[string]slog.Level
72
73 dynamicMutex sync.Mutex
74 Dynamic config.Dynamic // Can only be accessed directly by tests. Use methods on Config for locked access.
75 dynamicMtime time.Time
76 DynamicLastCheck time.Time // For use by quickstart only to skip checks.
77 // From canonical full address (localpart@domain, lower-cased when
78 // case-insensitive, stripped of catchall separator) to account and address.
79 // Domains are IDNA names in utf8.
80 accountDestinations map[string]AccountDestination
81}
82
83type AccountDestination struct {
84 Catchall bool // If catchall destination for its domain.
85 Localpart smtp.Localpart // In original casing as written in config file.
86 Account string
87 Destination config.Destination
88}
89
90// LogLevelSet sets a new log level for pkg. An empty pkg sets the default log
91// value that is used if no explicit log level is configured for a package.
92// This change is ephemeral, no config file is changed.
93func (c *Config) LogLevelSet(log mlog.Log, pkg string, level slog.Level) {
94 c.logMutex.Lock()
95 defer c.logMutex.Unlock()
96 l := c.copyLogLevels()
97 l[pkg] = level
98 c.Log = l
99 log.Print("log level changed", slog.String("pkg", pkg), slog.Any("level", mlog.LevelStrings[level]))
100 mlog.SetConfig(c.Log)
101}
102
103// LogLevelRemove removes a configured log level for a package.
104func (c *Config) LogLevelRemove(log mlog.Log, pkg string) {
105 c.logMutex.Lock()
106 defer c.logMutex.Unlock()
107 l := c.copyLogLevels()
108 delete(l, pkg)
109 c.Log = l
110 log.Print("log level cleared", slog.String("pkg", pkg))
111 mlog.SetConfig(c.Log)
112}
113
114// copyLogLevels returns a copy of c.Log, for modifications.
115// must be called with log lock held.
116func (c *Config) copyLogLevels() map[string]slog.Level {
117 m := map[string]slog.Level{}
118 for pkg, level := range c.Log {
119 m[pkg] = level
120 }
121 return m
122}
123
124// LogLevels returns a copy of the current log levels.
125func (c *Config) LogLevels() map[string]slog.Level {
126 c.logMutex.Lock()
127 defer c.logMutex.Unlock()
128 return c.copyLogLevels()
129}
130
131func (c *Config) withDynamicLock(fn func()) {
132 c.dynamicMutex.Lock()
133 defer c.dynamicMutex.Unlock()
134 now := time.Now()
135 if now.Sub(c.DynamicLastCheck) > time.Second {
136 c.DynamicLastCheck = now
137 if fi, err := os.Stat(ConfigDynamicPath); err != nil {
138 pkglog.Errorx("stat domains config", err)
139 } else if !fi.ModTime().Equal(c.dynamicMtime) {
140 if errs := c.loadDynamic(); len(errs) > 0 {
141 pkglog.Errorx("loading domains config", errs[0], slog.Any("errors", errs))
142 } else {
143 pkglog.Info("domains config reloaded")
144 c.dynamicMtime = fi.ModTime()
145 }
146 }
147 }
148 fn()
149}
150
151// must be called with dynamic lock held.
152func (c *Config) loadDynamic() []error {
153 d, mtime, accDests, err := ParseDynamicConfig(context.Background(), pkglog, ConfigDynamicPath, c.Static)
154 if err != nil {
155 return err
156 }
157 c.Dynamic = d
158 c.dynamicMtime = mtime
159 c.accountDestinations = accDests
160 c.allowACMEHosts(pkglog, true)
161 return nil
162}
163
164func (c *Config) Domains() (l []string) {
165 c.withDynamicLock(func() {
166 for name := range c.Dynamic.Domains {
167 l = append(l, name)
168 }
169 })
170 sort.Slice(l, func(i, j int) bool {
171 return l[i] < l[j]
172 })
173 return l
174}
175
176func (c *Config) Accounts() (l []string) {
177 c.withDynamicLock(func() {
178 for name := range c.Dynamic.Accounts {
179 l = append(l, name)
180 }
181 })
182 return
183}
184
185// DomainLocalparts returns a mapping of encoded localparts to account names for a
186// domain. An empty localpart is a catchall destination for a domain.
187func (c *Config) DomainLocalparts(d dns.Domain) map[string]string {
188 suffix := "@" + d.Name()
189 m := map[string]string{}
190 c.withDynamicLock(func() {
191 for addr, ad := range c.accountDestinations {
192 if strings.HasSuffix(addr, suffix) {
193 if ad.Catchall {
194 m[""] = ad.Account
195 } else {
196 m[ad.Localpart.String()] = ad.Account
197 }
198 }
199 }
200 })
201 return m
202}
203
204func (c *Config) Domain(d dns.Domain) (dom config.Domain, ok bool) {
205 c.withDynamicLock(func() {
206 dom, ok = c.Dynamic.Domains[d.Name()]
207 })
208 return
209}
210
211func (c *Config) Account(name string) (acc config.Account, ok bool) {
212 c.withDynamicLock(func() {
213 acc, ok = c.Dynamic.Accounts[name]
214 })
215 return
216}
217
218func (c *Config) AccountDestination(addr string) (accDests AccountDestination, ok bool) {
219 c.withDynamicLock(func() {
220 accDests, ok = c.accountDestinations[addr]
221 })
222 return
223}
224
225func (c *Config) WebServer() (r map[dns.Domain]dns.Domain, l []config.WebHandler) {
226 c.withDynamicLock(func() {
227 r = c.Dynamic.WebDNSDomainRedirects
228 l = c.Dynamic.WebHandlers
229 })
230 return r, l
231}
232
233func (c *Config) Routes(accountName string, domain dns.Domain) (accountRoutes, domainRoutes, globalRoutes []config.Route) {
234 c.withDynamicLock(func() {
235 acc := c.Dynamic.Accounts[accountName]
236 accountRoutes = acc.Routes
237
238 dom := c.Dynamic.Domains[domain.Name()]
239 domainRoutes = dom.Routes
240
241 globalRoutes = c.Dynamic.Routes
242 })
243 return
244}
245
246func (c *Config) MonitorDNSBLs() (zones []dns.Domain) {
247 c.withDynamicLock(func() {
248 zones = c.Dynamic.MonitorDNSBLZones
249 })
250 return
251}
252
253func (c *Config) allowACMEHosts(log mlog.Log, checkACMEHosts bool) {
254 for _, l := range c.Static.Listeners {
255 if l.TLS == nil || l.TLS.ACME == "" {
256 continue
257 }
258
259 m := c.Static.ACME[l.TLS.ACME].Manager
260 hostnames := map[dns.Domain]struct{}{}
261
262 hostnames[c.Static.HostnameDomain] = struct{}{}
263 if l.HostnameDomain.ASCII != "" {
264 hostnames[l.HostnameDomain] = struct{}{}
265 }
266
267 for _, dom := range c.Dynamic.Domains {
268 // Do not allow TLS certificates for domains for which we only accept DMARC/TLS
269 // reports as external party.
270 if dom.ReportsOnly {
271 continue
272 }
273
274 if l.AutoconfigHTTPS.Enabled && !l.AutoconfigHTTPS.NonTLS {
275 if d, err := dns.ParseDomain("autoconfig." + dom.Domain.ASCII); err != nil {
276 log.Errorx("parsing autoconfig domain", err, slog.Any("domain", dom.Domain))
277 } else {
278 hostnames[d] = struct{}{}
279 }
280 }
281
282 if l.MTASTSHTTPS.Enabled && dom.MTASTS != nil && !l.MTASTSHTTPS.NonTLS {
283 d, err := dns.ParseDomain("mta-sts." + dom.Domain.ASCII)
284 if err != nil {
285 log.Errorx("parsing mta-sts domain", err, slog.Any("domain", dom.Domain))
286 } else {
287 hostnames[d] = struct{}{}
288 }
289 }
290
291 if dom.ClientSettingsDomain != "" {
292 hostnames[dom.ClientSettingsDNSDomain] = struct{}{}
293 }
294 }
295
296 if l.WebserverHTTPS.Enabled {
297 for from := range c.Dynamic.WebDNSDomainRedirects {
298 hostnames[from] = struct{}{}
299 }
300 for _, wh := range c.Dynamic.WebHandlers {
301 hostnames[wh.DNSDomain] = struct{}{}
302 }
303 }
304
305 public := c.Static.Listeners["public"]
306 ips := public.IPs
307 if len(public.NATIPs) > 0 {
308 ips = public.NATIPs
309 }
310 if public.IPsNATed {
311 ips = nil
312 }
313 m.SetAllowedHostnames(log, dns.StrictResolver{Pkg: "autotls", Log: log.Logger}, hostnames, ips, checkACMEHosts)
314 }
315}
316
317// todo future: write config parsing & writing code that can read a config and remembers the exact tokens including newlines and comments, and can write back a modified file. the goal is to be able to write a config file automatically (after changing fields through the ui), but not loose comments and whitespace, to still get useful diffs for storing the config in a version control system.
318
319// must be called with lock held.
320func writeDynamic(ctx context.Context, log mlog.Log, c config.Dynamic) error {
321 accDests, errs := prepareDynamicConfig(ctx, log, ConfigDynamicPath, Conf.Static, &c)
322 if len(errs) > 0 {
323 return errs[0]
324 }
325
326 var b bytes.Buffer
327 err := sconf.Write(&b, c)
328 if err != nil {
329 return err
330 }
331 f, err := os.OpenFile(ConfigDynamicPath, os.O_WRONLY, 0660)
332 if err != nil {
333 return err
334 }
335 defer func() {
336 if f != nil {
337 err := f.Close()
338 log.Check(err, "closing file after error")
339 }
340 }()
341 buf := b.Bytes()
342 if _, err := f.Write(buf); err != nil {
343 return fmt.Errorf("write domains.conf: %v", err)
344 }
345 if err := f.Truncate(int64(len(buf))); err != nil {
346 return fmt.Errorf("truncate domains.conf after write: %v", err)
347 }
348 if err := f.Sync(); err != nil {
349 return fmt.Errorf("sync domains.conf after write: %v", err)
350 }
351 if err := moxio.SyncDir(log, filepath.Dir(ConfigDynamicPath)); err != nil {
352 return fmt.Errorf("sync dir of domains.conf after write: %v", err)
353 }
354
355 fi, err := f.Stat()
356 if err != nil {
357 return fmt.Errorf("stat after writing domains.conf: %v", err)
358 }
359
360 if err := f.Close(); err != nil {
361 return fmt.Errorf("close written domains.conf: %v", err)
362 }
363 f = nil
364
365 Conf.dynamicMtime = fi.ModTime()
366 Conf.DynamicLastCheck = time.Now()
367 Conf.Dynamic = c
368 Conf.accountDestinations = accDests
369
370 Conf.allowACMEHosts(log, true)
371
372 return nil
373}
374
375// MustLoadConfig loads the config, quitting on errors.
376func MustLoadConfig(doLoadTLSKeyCerts, checkACMEHosts bool) {
377 errs := LoadConfig(context.Background(), pkglog, doLoadTLSKeyCerts, checkACMEHosts)
378 if len(errs) > 1 {
379 pkglog.Error("loading config file: multiple errors")
380 for _, err := range errs {
381 pkglog.Errorx("config error", err)
382 }
383 pkglog.Fatal("stopping after multiple config errors")
384 } else if len(errs) == 1 {
385 pkglog.Fatalx("loading config file", errs[0])
386 }
387}
388
389// LoadConfig attempts to parse and load a config, returning any errors
390// encountered.
391func LoadConfig(ctx context.Context, log mlog.Log, doLoadTLSKeyCerts, checkACMEHosts bool) []error {
392 Shutdown, ShutdownCancel = context.WithCancel(context.Background())
393 Context, ContextCancel = context.WithCancel(context.Background())
394
395 c, errs := ParseConfig(ctx, log, ConfigStaticPath, false, doLoadTLSKeyCerts, checkACMEHosts)
396 if len(errs) > 0 {
397 return errs
398 }
399
400 mlog.SetConfig(c.Log)
401 SetConfig(c)
402 return nil
403}
404
405// SetConfig sets a new config. Not to be used during normal operation.
406func SetConfig(c *Config) {
407 // Cannot just assign *c to Conf, it would copy the mutex.
408 Conf = Config{c.Static, sync.Mutex{}, c.Log, sync.Mutex{}, c.Dynamic, c.dynamicMtime, c.DynamicLastCheck, c.accountDestinations}
409
410 // If we have non-standard CA roots, use them for all HTTPS requests.
411 if Conf.Static.TLS.CertPool != nil {
412 http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{
413 RootCAs: Conf.Static.TLS.CertPool,
414 }
415 }
416
417 SetPedantic(c.Static.Pedantic)
418}
419
420// Set pedantic in all packages.
421func SetPedantic(p bool) {
422 dkim.Pedantic = p
423 dns.Pedantic = p
424 message.Pedantic = p
425 smtp.Pedantic = p
426 Pedantic = p
427}
428
429// ParseConfig parses the static config at path p. If checkOnly is true, no changes
430// are made, such as registering ACME identities. If doLoadTLSKeyCerts is true,
431// the TLS KeyCerts configuration is loaded and checked. This is used during the
432// quickstart in the case the user is going to provide their own certificates.
433// If checkACMEHosts is true, the hosts allowed for acme are compared with the
434// explicitly configured ips we are listening on.
435func ParseConfig(ctx context.Context, log mlog.Log, p string, checkOnly, doLoadTLSKeyCerts, checkACMEHosts bool) (c *Config, errs []error) {
436 c = &Config{
437 Static: config.Static{
438 DataDir: ".",
439 },
440 }
441
442 f, err := os.Open(p)
443 if err != nil {
444 if os.IsNotExist(err) && os.Getenv("MOXCONF") == "" {
445 return nil, []error{fmt.Errorf("open config file: %v (hint: use mox -config ... or set MOXCONF=...)", err)}
446 }
447 return nil, []error{fmt.Errorf("open config file: %v", err)}
448 }
449 defer f.Close()
450 if err := sconf.Parse(f, &c.Static); err != nil {
451 return nil, []error{fmt.Errorf("parsing %s%v", p, err)}
452 }
453
454 if xerrs := PrepareStaticConfig(ctx, log, p, c, checkOnly, doLoadTLSKeyCerts); len(xerrs) > 0 {
455 return nil, xerrs
456 }
457
458 pp := filepath.Join(filepath.Dir(p), "domains.conf")
459 c.Dynamic, c.dynamicMtime, c.accountDestinations, errs = ParseDynamicConfig(ctx, log, pp, c.Static)
460
461 if !checkOnly {
462 c.allowACMEHosts(log, checkACMEHosts)
463 }
464
465 return c, errs
466}
467
468// PrepareStaticConfig parses the static config file and prepares data structures
469// for starting mox. If checkOnly is set no substantial changes are made, like
470// creating an ACME registration.
471func PrepareStaticConfig(ctx context.Context, log mlog.Log, configFile string, conf *Config, checkOnly, doLoadTLSKeyCerts bool) (errs []error) {
472 addErrorf := func(format string, args ...any) {
473 errs = append(errs, fmt.Errorf(format, args...))
474 }
475
476 c := &conf.Static
477
478 // check that mailbox is in unicode NFC normalized form.
479 checkMailboxNormf := func(mailbox string, format string, args ...any) {
480 s := norm.NFC.String(mailbox)
481 if mailbox != s {
482 msg := fmt.Sprintf(format, args...)
483 addErrorf("%s: mailbox %q is not in NFC normalized form, should be %q", msg, mailbox, s)
484 }
485 }
486
487 // Post-process logging config.
488 if logLevel, ok := mlog.Levels[c.LogLevel]; ok {
489 conf.Log = map[string]slog.Level{"": logLevel}
490 } else {
491 addErrorf("invalid log level %q", c.LogLevel)
492 }
493 for pkg, s := range c.PackageLogLevels {
494 if logLevel, ok := mlog.Levels[s]; ok {
495 conf.Log[pkg] = logLevel
496 } else {
497 addErrorf("invalid package log level %q", s)
498 }
499 }
500
501 if c.User == "" {
502 c.User = "mox"
503 }
504 u, err := user.Lookup(c.User)
505 if err != nil {
506 uid, err := strconv.ParseUint(c.User, 10, 32)
507 if err != nil {
508 addErrorf("parsing unknown user %s as uid: %v (hint: add user mox with \"useradd -d $PWD mox\" or specify a different username on the quickstart command-line)", c.User, err)
509 } else {
510 // We assume the same gid as uid.
511 c.UID = uint32(uid)
512 c.GID = uint32(uid)
513 }
514 } else {
515 if uid, err := strconv.ParseUint(u.Uid, 10, 32); err != nil {
516 addErrorf("parsing uid %s: %v", u.Uid, err)
517 } else {
518 c.UID = uint32(uid)
519 }
520 if gid, err := strconv.ParseUint(u.Gid, 10, 32); err != nil {
521 addErrorf("parsing gid %s: %v", u.Gid, err)
522 } else {
523 c.GID = uint32(gid)
524 }
525 }
526
527 hostname, err := dns.ParseDomain(c.Hostname)
528 if err != nil {
529 addErrorf("parsing hostname: %s", err)
530 } else if hostname.Name() != c.Hostname {
531 addErrorf("hostname must be in unicode form %q instead of %q", hostname.Name(), c.Hostname)
532 }
533 c.HostnameDomain = hostname
534
535 if c.HostTLSRPT.Account != "" {
536 tlsrptLocalpart, err := smtp.ParseLocalpart(c.HostTLSRPT.Localpart)
537 if err != nil {
538 addErrorf("invalid localpart %q for host tlsrpt: %v", c.HostTLSRPT.Localpart, err)
539 } else if tlsrptLocalpart.IsInternational() {
540 // Does not appear documented in ../rfc/8460, but similar to DMARC it makes sense
541 // to keep this ascii-only addresses.
542 addErrorf("host TLSRPT localpart %q is an internationalized address, only conventional ascii-only address allowed for interopability", tlsrptLocalpart)
543 }
544 c.HostTLSRPT.ParsedLocalpart = tlsrptLocalpart
545 }
546
547 // Return private key for host name for use with an ACME. Used to return the same
548 // private key as pre-generated for use with DANE, with its public key in DNS.
549 // We only use this key for Listener's that have this ACME configured, and for
550 // which the effective listener host name (either specific to the listener, or the
551 // global name) is requested. Other host names can get a fresh private key, they
552 // don't appear in DANE records.
553 //
554 // - run 0: only use listener with explicitly matching host name in listener
555 // (default quickstart config does not set it).
556 // - run 1: only look at public listener (and host matching mox host name)
557 // - run 2: all listeners (and host matching mox host name)
558 findACMEHostPrivateKey := func(acmeName, host string, keyType autocert.KeyType, run int) crypto.Signer {
559 for listenerName, l := range Conf.Static.Listeners {
560 if l.TLS == nil || l.TLS.ACME != acmeName {
561 continue
562 }
563 if run == 0 && host != l.HostnameDomain.ASCII {
564 continue
565 }
566 if run == 1 && listenerName != "public" || host != Conf.Static.HostnameDomain.ASCII {
567 continue
568 }
569 switch keyType {
570 case autocert.KeyRSA2048:
571 if len(l.TLS.HostPrivateRSA2048Keys) == 0 {
572 continue
573 }
574 return l.TLS.HostPrivateRSA2048Keys[0]
575 case autocert.KeyECDSAP256:
576 if len(l.TLS.HostPrivateECDSAP256Keys) == 0 {
577 continue
578 }
579 return l.TLS.HostPrivateECDSAP256Keys[0]
580 default:
581 return nil
582 }
583 }
584 return nil
585 }
586 // Make a function for an autocert.Manager.GetPrivateKey, using findACMEHostPrivateKey.
587 makeGetPrivateKey := func(acmeName string) func(host string, keyType autocert.KeyType) (crypto.Signer, error) {
588 return func(host string, keyType autocert.KeyType) (crypto.Signer, error) {
589 key := findACMEHostPrivateKey(acmeName, host, keyType, 0)
590 if key == nil {
591 key = findACMEHostPrivateKey(acmeName, host, keyType, 1)
592 }
593 if key == nil {
594 key = findACMEHostPrivateKey(acmeName, host, keyType, 2)
595 }
596 if key != nil {
597 log.Debug("found existing private key for certificate for host",
598 slog.String("acmename", acmeName),
599 slog.String("host", host),
600 slog.Any("keytype", keyType))
601 return key, nil
602 }
603 log.Debug("generating new private key for certificate for host",
604 slog.String("acmename", acmeName),
605 slog.String("host", host),
606 slog.Any("keytype", keyType))
607 switch keyType {
608 case autocert.KeyRSA2048:
609 return rsa.GenerateKey(cryptorand.Reader, 2048)
610 case autocert.KeyECDSAP256:
611 return ecdsa.GenerateKey(elliptic.P256(), cryptorand.Reader)
612 default:
613 return nil, fmt.Errorf("unrecognized requested key type %v", keyType)
614 }
615 }
616 }
617 for name, acme := range c.ACME {
618 var eabKeyID string
619 var eabKey []byte
620 if acme.ExternalAccountBinding != nil {
621 eabKeyID = acme.ExternalAccountBinding.KeyID
622 p := configDirPath(configFile, acme.ExternalAccountBinding.KeyFile)
623 buf, err := os.ReadFile(p)
624 if err != nil {
625 addErrorf("reading external account binding key for acme provider %q: %s", name, err)
626 } else {
627 dec := make([]byte, base64.RawURLEncoding.DecodedLen(len(buf)))
628 n, err := base64.RawURLEncoding.Decode(dec, buf)
629 if err != nil {
630 addErrorf("parsing external account binding key as base64 for acme provider %q: %s", name, err)
631 } else {
632 eabKey = dec[:n]
633 }
634 }
635 }
636
637 if checkOnly {
638 continue
639 }
640
641 acmeDir := dataDirPath(configFile, c.DataDir, "acme")
642 os.MkdirAll(acmeDir, 0770)
643 manager, err := autotls.Load(name, acmeDir, acme.ContactEmail, acme.DirectoryURL, eabKeyID, eabKey, makeGetPrivateKey(name), Shutdown.Done())
644 if err != nil {
645 addErrorf("loading ACME identity for %q: %s", name, err)
646 }
647 acme.Manager = manager
648
649 // Help configurations from older quickstarts.
650 if acme.IssuerDomainName == "" && acme.DirectoryURL == "https://acme-v02.api.letsencrypt.org/directory" {
651 acme.IssuerDomainName = "letsencrypt.org"
652 }
653
654 c.ACME[name] = acme
655 }
656
657 var haveUnspecifiedSMTPListener bool
658 for name, l := range c.Listeners {
659 if l.Hostname != "" {
660 d, err := dns.ParseDomain(l.Hostname)
661 if err != nil {
662 addErrorf("bad listener hostname %q: %s", l.Hostname, err)
663 }
664 l.HostnameDomain = d
665 }
666 if l.TLS != nil {
667 if l.TLS.ACME != "" && len(l.TLS.KeyCerts) != 0 {
668 addErrorf("listener %q: cannot have ACME and static key/certificates", name)
669 } else if l.TLS.ACME != "" {
670 acme, ok := c.ACME[l.TLS.ACME]
671 if !ok {
672 addErrorf("listener %q: unknown ACME provider %q", name, l.TLS.ACME)
673 }
674
675 // If only checking or with missing ACME definition, we don't have an acme manager,
676 // so set an empty tls config to continue.
677 var tlsconfig *tls.Config
678 if checkOnly || acme.Manager == nil {
679 tlsconfig = &tls.Config{}
680 } else {
681 tlsconfig = acme.Manager.TLSConfig.Clone()
682 l.TLS.ACMEConfig = acme.Manager.ACMETLSConfig
683
684 // SMTP STARTTLS connections are commonly made without SNI, because certificates
685 // often aren't verified.
686 hostname := c.HostnameDomain
687 if l.Hostname != "" {
688 hostname = l.HostnameDomain
689 }
690 getCert := tlsconfig.GetCertificate
691 tlsconfig.GetCertificate = func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
692 if hello.ServerName == "" {
693 hello.ServerName = hostname.ASCII
694 }
695 return getCert(hello)
696 }
697 }
698 l.TLS.Config = tlsconfig
699 } else if len(l.TLS.KeyCerts) != 0 {
700 if doLoadTLSKeyCerts {
701 if err := loadTLSKeyCerts(configFile, "listener "+name, l.TLS); err != nil {
702 addErrorf("%w", err)
703 }
704 }
705 } else {
706 addErrorf("listener %q: cannot have TLS config without ACME and without static keys/certificates", name)
707 }
708 for _, privKeyFile := range l.TLS.HostPrivateKeyFiles {
709 keyPath := configDirPath(configFile, privKeyFile)
710 privKey, err := loadPrivateKeyFile(keyPath)
711 if err != nil {
712 addErrorf("listener %q: parsing host private key for DANE and ACME certificates: %v", name, err)
713 continue
714 }
715 switch k := privKey.(type) {
716 case *rsa.PrivateKey:
717 if k.N.BitLen() != 2048 {
718 log.Error("need rsa key with 2048 bits, for host private key for DANE/ACME certificates, ignoring",
719 slog.String("listener", name),
720 slog.String("file", keyPath),
721 slog.Int("bits", k.N.BitLen()))
722 continue
723 }
724 l.TLS.HostPrivateRSA2048Keys = append(l.TLS.HostPrivateRSA2048Keys, k)
725 case *ecdsa.PrivateKey:
726 if k.Curve != elliptic.P256() {
727 log.Error("unrecognized ecdsa curve for host private key for DANE/ACME certificates, ignoring", slog.String("listener", name), slog.String("file", keyPath))
728 continue
729 }
730 l.TLS.HostPrivateECDSAP256Keys = append(l.TLS.HostPrivateECDSAP256Keys, k)
731 default:
732 log.Error("unrecognized key type for host private key for DANE/ACME certificates, ignoring",
733 slog.String("listener", name),
734 slog.String("file", keyPath),
735 slog.String("keytype", fmt.Sprintf("%T", privKey)))
736 continue
737 }
738 }
739 if l.TLS.ACME != "" && (len(l.TLS.HostPrivateRSA2048Keys) == 0) != (len(l.TLS.HostPrivateECDSAP256Keys) == 0) {
740 log.Error("warning: uncommon configuration with either only an RSA 2048 or ECDSA P256 host private key for DANE/ACME certificates; this ACME implementation can retrieve certificates for both type of keys, it is recommended to set either both or none; continuing")
741 }
742
743 // TLS 1.2 was introduced in 2008. TLS <1.2 was deprecated by ../rfc/8996:31 and ../rfc/8997:66 in 2021.
744 var minVersion uint16 = tls.VersionTLS12
745 if l.TLS.MinVersion != "" {
746 versions := map[string]uint16{
747 "TLSv1.0": tls.VersionTLS10,
748 "TLSv1.1": tls.VersionTLS11,
749 "TLSv1.2": tls.VersionTLS12,
750 "TLSv1.3": tls.VersionTLS13,
751 }
752 v, ok := versions[l.TLS.MinVersion]
753 if !ok {
754 addErrorf("listener %q: unknown TLS mininum version %q", name, l.TLS.MinVersion)
755 }
756 minVersion = v
757 }
758 if l.TLS.Config != nil {
759 l.TLS.Config.MinVersion = minVersion
760 }
761 if l.TLS.ACMEConfig != nil {
762 l.TLS.ACMEConfig.MinVersion = minVersion
763 }
764 } else {
765 var needsTLS []string
766 needtls := func(s string, v bool) {
767 if v {
768 needsTLS = append(needsTLS, s)
769 }
770 }
771 needtls("IMAPS", l.IMAPS.Enabled)
772 needtls("SMTP", l.SMTP.Enabled && !l.SMTP.NoSTARTTLS)
773 needtls("Submissions", l.Submissions.Enabled)
774 needtls("Submission", l.Submission.Enabled && !l.Submission.NoRequireSTARTTLS)
775 needtls("AccountHTTPS", l.AccountHTTPS.Enabled)
776 needtls("AdminHTTPS", l.AdminHTTPS.Enabled)
777 needtls("AutoconfigHTTPS", l.AutoconfigHTTPS.Enabled && !l.AutoconfigHTTPS.NonTLS)
778 needtls("MTASTSHTTPS", l.MTASTSHTTPS.Enabled && !l.MTASTSHTTPS.NonTLS)
779 needtls("WebserverHTTPS", l.WebserverHTTPS.Enabled)
780 if len(needsTLS) > 0 {
781 addErrorf("listener %q does not specify tls config, but requires tls for %s", name, strings.Join(needsTLS, ", "))
782 }
783 }
784 if l.AutoconfigHTTPS.Enabled && l.MTASTSHTTPS.Enabled && l.AutoconfigHTTPS.Port == l.MTASTSHTTPS.Port && l.AutoconfigHTTPS.NonTLS != l.MTASTSHTTPS.NonTLS {
785 addErrorf("listener %q tries to enable autoconfig and mta-sts enabled on same port but with both http and https", name)
786 }
787 if l.SMTP.Enabled {
788 if len(l.IPs) == 0 {
789 haveUnspecifiedSMTPListener = true
790 }
791 for _, ipstr := range l.IPs {
792 ip := net.ParseIP(ipstr)
793 if ip == nil {
794 addErrorf("listener %q has invalid IP %q", name, ipstr)
795 continue
796 }
797 if ip.IsUnspecified() {
798 haveUnspecifiedSMTPListener = true
799 break
800 }
801 if len(c.SpecifiedSMTPListenIPs) >= 2 {
802 haveUnspecifiedSMTPListener = true
803 } else if len(c.SpecifiedSMTPListenIPs) > 0 && (c.SpecifiedSMTPListenIPs[0].To4() == nil) == (ip.To4() == nil) {
804 haveUnspecifiedSMTPListener = true
805 } else {
806 c.SpecifiedSMTPListenIPs = append(c.SpecifiedSMTPListenIPs, ip)
807 }
808 }
809 }
810 for _, s := range l.SMTP.DNSBLs {
811 d, err := dns.ParseDomain(s)
812 if err != nil {
813 addErrorf("listener %q has invalid DNSBL zone %q", name, s)
814 continue
815 }
816 l.SMTP.DNSBLZones = append(l.SMTP.DNSBLZones, d)
817 }
818 if l.IPsNATed && len(l.NATIPs) > 0 {
819 addErrorf("listener %q has both IPsNATed and NATIPs (remove deprecated IPsNATed)", name)
820 }
821 for _, ipstr := range l.NATIPs {
822 ip := net.ParseIP(ipstr)
823 if ip == nil {
824 addErrorf("listener %q has invalid ip %q", name, ipstr)
825 } else if ip.IsUnspecified() || ip.IsLoopback() {
826 addErrorf("listener %q has NAT ip that is the unspecified or loopback address %s", name, ipstr)
827 }
828 }
829 checkPath := func(kind string, enabled bool, path string) {
830 if enabled && path != "" && !strings.HasPrefix(path, "/") {
831 addErrorf("listener %q has %s with path %q that must start with a slash", name, kind, path)
832 }
833 }
834 checkPath("AccountHTTP", l.AccountHTTP.Enabled, l.AccountHTTP.Path)
835 checkPath("AccountHTTPS", l.AccountHTTPS.Enabled, l.AccountHTTPS.Path)
836 checkPath("AdminHTTP", l.AdminHTTP.Enabled, l.AdminHTTP.Path)
837 checkPath("AdminHTTPS", l.AdminHTTPS.Enabled, l.AdminHTTPS.Path)
838 c.Listeners[name] = l
839 }
840 if haveUnspecifiedSMTPListener {
841 c.SpecifiedSMTPListenIPs = nil
842 }
843
844 var zerouse config.SpecialUseMailboxes
845 if len(c.DefaultMailboxes) > 0 && (c.InitialMailboxes.SpecialUse != zerouse || len(c.InitialMailboxes.Regular) > 0) {
846 addErrorf("cannot have both DefaultMailboxes and InitialMailboxes")
847 }
848 // DefaultMailboxes is deprecated.
849 for _, mb := range c.DefaultMailboxes {
850 checkMailboxNormf(mb, "default mailbox")
851 }
852 checkSpecialUseMailbox := func(nameOpt string) {
853 if nameOpt != "" {
854 checkMailboxNormf(nameOpt, "special-use initial mailbox")
855 if strings.EqualFold(nameOpt, "inbox") {
856 addErrorf("initial mailbox cannot be set to Inbox (Inbox is always created)")
857 }
858 }
859 }
860 checkSpecialUseMailbox(c.InitialMailboxes.SpecialUse.Archive)
861 checkSpecialUseMailbox(c.InitialMailboxes.SpecialUse.Draft)
862 checkSpecialUseMailbox(c.InitialMailboxes.SpecialUse.Junk)
863 checkSpecialUseMailbox(c.InitialMailboxes.SpecialUse.Sent)
864 checkSpecialUseMailbox(c.InitialMailboxes.SpecialUse.Trash)
865 for _, name := range c.InitialMailboxes.Regular {
866 checkMailboxNormf(name, "regular initial mailbox")
867 if strings.EqualFold(name, "inbox") {
868 addErrorf("initial regular mailbox cannot be set to Inbox (Inbox is always created)")
869 }
870 }
871
872 checkTransportSMTP := func(name string, isTLS bool, t *config.TransportSMTP) {
873 var err error
874 t.DNSHost, err = dns.ParseDomain(t.Host)
875 if err != nil {
876 addErrorf("transport %s: bad host %s: %v", name, t.Host, err)
877 }
878
879 if isTLS && t.STARTTLSInsecureSkipVerify {
880 addErrorf("transport %s: cannot have STARTTLSInsecureSkipVerify with immediate TLS")
881 }
882 if isTLS && t.NoSTARTTLS {
883 addErrorf("transport %s: cannot have NoSTARTTLS with immediate TLS")
884 }
885
886 if t.Auth == nil {
887 return
888 }
889 seen := map[string]bool{}
890 for _, m := range t.Auth.Mechanisms {
891 if seen[m] {
892 addErrorf("transport %s: duplicate authentication mechanism %s", name, m)
893 }
894 seen[m] = true
895 switch m {
896 case "SCRAM-SHA-256-PLUS":
897 case "SCRAM-SHA-256":
898 case "SCRAM-SHA-1-PLUS":
899 case "SCRAM-SHA-1":
900 case "CRAM-MD5":
901 case "PLAIN":
902 default:
903 addErrorf("transport %s: unknown authentication mechanism %s", name, m)
904 }
905 }
906
907 t.Auth.EffectiveMechanisms = t.Auth.Mechanisms
908 if len(t.Auth.EffectiveMechanisms) == 0 {
909 t.Auth.EffectiveMechanisms = []string{"SCRAM-SHA-256-PLUS", "SCRAM-SHA-256", "SCRAM-SHA-1-PLUS", "SCRAM-SHA-1", "CRAM-MD5"}
910 }
911 }
912
913 checkTransportSocks := func(name string, t *config.TransportSocks) {
914 _, _, err := net.SplitHostPort(t.Address)
915 if err != nil {
916 addErrorf("transport %s: bad address %s: %v", name, t.Address, err)
917 }
918 for _, ipstr := range t.RemoteIPs {
919 ip := net.ParseIP(ipstr)
920 if ip == nil {
921 addErrorf("transport %s: bad ip %s", name, ipstr)
922 } else {
923 t.IPs = append(t.IPs, ip)
924 }
925 }
926 t.Hostname, err = dns.ParseDomain(t.RemoteHostname)
927 if err != nil {
928 addErrorf("transport %s: bad hostname %s: %v", name, t.RemoteHostname, err)
929 }
930 }
931
932 for name, t := range c.Transports {
933 n := 0
934 if t.Submissions != nil {
935 n++
936 checkTransportSMTP(name, true, t.Submissions)
937 }
938 if t.Submission != nil {
939 n++
940 checkTransportSMTP(name, false, t.Submission)
941 }
942 if t.SMTP != nil {
943 n++
944 checkTransportSMTP(name, false, t.SMTP)
945 }
946 if t.Socks != nil {
947 n++
948 checkTransportSocks(name, t.Socks)
949 }
950 if n > 1 {
951 addErrorf("transport %s: cannot have multiple methods in a transport", name)
952 }
953 }
954
955 // Load CA certificate pool.
956 if c.TLS.CA != nil {
957 if c.TLS.CA.AdditionalToSystem {
958 var err error
959 c.TLS.CertPool, err = x509.SystemCertPool()
960 if err != nil {
961 addErrorf("fetching system CA cert pool: %v", err)
962 }
963 } else {
964 c.TLS.CertPool = x509.NewCertPool()
965 }
966 for _, certfile := range c.TLS.CA.CertFiles {
967 p := configDirPath(configFile, certfile)
968 pemBuf, err := os.ReadFile(p)
969 if err != nil {
970 addErrorf("reading TLS CA cert file: %v", err)
971 continue
972 } else if !c.TLS.CertPool.AppendCertsFromPEM(pemBuf) {
973 // todo: can we check more fully if we're getting some useful data back?
974 addErrorf("no CA certs added from %q", p)
975 }
976 }
977 }
978 return
979}
980
981// PrepareDynamicConfig parses the dynamic config file given a static file.
982func ParseDynamicConfig(ctx context.Context, log mlog.Log, dynamicPath string, static config.Static) (c config.Dynamic, mtime time.Time, accDests map[string]AccountDestination, errs []error) {
983 addErrorf := func(format string, args ...any) {
984 errs = append(errs, fmt.Errorf(format, args...))
985 }
986
987 f, err := os.Open(dynamicPath)
988 if err != nil {
989 addErrorf("parsing domains config: %v", err)
990 return
991 }
992 defer f.Close()
993 fi, err := f.Stat()
994 if err != nil {
995 addErrorf("stat domains config: %v", err)
996 }
997 if err := sconf.Parse(f, &c); err != nil {
998 addErrorf("parsing dynamic config file: %v", err)
999 return
1000 }
1001
1002 accDests, errs = prepareDynamicConfig(ctx, log, dynamicPath, static, &c)
1003 return c, fi.ModTime(), accDests, errs
1004}
1005
1006func prepareDynamicConfig(ctx context.Context, log mlog.Log, dynamicPath string, static config.Static, c *config.Dynamic) (accDests map[string]AccountDestination, errs []error) {
1007 addErrorf := func(format string, args ...any) {
1008 errs = append(errs, fmt.Errorf(format, args...))
1009 }
1010
1011 // Check that mailbox is in unicode NFC normalized form.
1012 checkMailboxNormf := func(mailbox string, format string, args ...any) {
1013 s := norm.NFC.String(mailbox)
1014 if mailbox != s {
1015 msg := fmt.Sprintf(format, args...)
1016 addErrorf("%s: mailbox %q is not in NFC normalized form, should be %q", msg, mailbox, s)
1017 }
1018 }
1019
1020 // Validate postmaster account exists.
1021 if _, ok := c.Accounts[static.Postmaster.Account]; !ok {
1022 addErrorf("postmaster account %q does not exist", static.Postmaster.Account)
1023 }
1024 checkMailboxNormf(static.Postmaster.Mailbox, "postmaster mailbox")
1025
1026 accDests = map[string]AccountDestination{}
1027
1028 // Validate host TLSRPT account/address.
1029 if static.HostTLSRPT.Account != "" {
1030 if _, ok := c.Accounts[static.HostTLSRPT.Account]; !ok {
1031 addErrorf("host tlsrpt account %q does not exist", static.HostTLSRPT.Account)
1032 }
1033 checkMailboxNormf(static.HostTLSRPT.Mailbox, "host tlsrpt mailbox")
1034
1035 // Localpart has been parsed already.
1036
1037 addrFull := smtp.NewAddress(static.HostTLSRPT.ParsedLocalpart, static.HostnameDomain).String()
1038 dest := config.Destination{
1039 Mailbox: static.HostTLSRPT.Mailbox,
1040 HostTLSReports: true,
1041 }
1042 accDests[addrFull] = AccountDestination{false, static.HostTLSRPT.ParsedLocalpart, static.HostTLSRPT.Account, dest}
1043 }
1044
1045 var haveSTSListener, haveWebserverListener bool
1046 for _, l := range static.Listeners {
1047 if l.MTASTSHTTPS.Enabled {
1048 haveSTSListener = true
1049 }
1050 if l.WebserverHTTP.Enabled || l.WebserverHTTPS.Enabled {
1051 haveWebserverListener = true
1052 }
1053 }
1054
1055 checkRoutes := func(descr string, routes []config.Route) {
1056 parseRouteDomains := func(l []string) []string {
1057 var r []string
1058 for _, e := range l {
1059 if e == "." {
1060 r = append(r, e)
1061 continue
1062 }
1063 prefix := ""
1064 if strings.HasPrefix(e, ".") {
1065 prefix = "."
1066 e = e[1:]
1067 }
1068 d, err := dns.ParseDomain(e)
1069 if err != nil {
1070 addErrorf("%s: invalid domain %s: %v", descr, e, err)
1071 }
1072 r = append(r, prefix+d.ASCII)
1073 }
1074 return r
1075 }
1076
1077 for i := range routes {
1078 routes[i].FromDomainASCII = parseRouteDomains(routes[i].FromDomain)
1079 routes[i].ToDomainASCII = parseRouteDomains(routes[i].ToDomain)
1080 var ok bool
1081 routes[i].ResolvedTransport, ok = static.Transports[routes[i].Transport]
1082 if !ok {
1083 addErrorf("%s: route references undefined transport %s", descr, routes[i].Transport)
1084 }
1085 }
1086 }
1087
1088 checkRoutes("global routes", c.Routes)
1089
1090 // Validate domains.
1091 for d, domain := range c.Domains {
1092 dnsdomain, err := dns.ParseDomain(d)
1093 if err != nil {
1094 addErrorf("bad domain %q: %s", d, err)
1095 } else if dnsdomain.Name() != d {
1096 addErrorf("domain %s must be specified in unicode form, %s", d, dnsdomain.Name())
1097 }
1098
1099 domain.Domain = dnsdomain
1100
1101 if domain.ClientSettingsDomain != "" {
1102 csd, err := dns.ParseDomain(domain.ClientSettingsDomain)
1103 if err != nil {
1104 addErrorf("bad client settings domain %q: %s", domain.ClientSettingsDomain, err)
1105 }
1106 domain.ClientSettingsDNSDomain = csd
1107 }
1108
1109 for _, sign := range domain.DKIM.Sign {
1110 if _, ok := domain.DKIM.Selectors[sign]; !ok {
1111 addErrorf("selector %s for signing is missing in domain %s", sign, d)
1112 }
1113 }
1114 for name, sel := range domain.DKIM.Selectors {
1115 seld, err := dns.ParseDomain(name)
1116 if err != nil {
1117 addErrorf("bad selector %q: %s", name, err)
1118 } else if seld.Name() != name {
1119 addErrorf("selector %q must be specified in unicode form, %q", name, seld.Name())
1120 }
1121 sel.Domain = seld
1122
1123 if sel.Expiration != "" {
1124 exp, err := time.ParseDuration(sel.Expiration)
1125 if err != nil {
1126 addErrorf("selector %q has invalid expiration %q: %v", name, sel.Expiration, err)
1127 } else {
1128 sel.ExpirationSeconds = int(exp / time.Second)
1129 }
1130 }
1131
1132 sel.HashEffective = sel.Hash
1133 switch sel.HashEffective {
1134 case "":
1135 sel.HashEffective = "sha256"
1136 case "sha1":
1137 log.Error("using sha1 with DKIM is deprecated as not secure enough, switch to sha256")
1138 case "sha256":
1139 default:
1140 addErrorf("unsupported hash %q for selector %q in domain %s", sel.HashEffective, name, d)
1141 }
1142
1143 pemBuf, err := os.ReadFile(configDirPath(dynamicPath, sel.PrivateKeyFile))
1144 if err != nil {
1145 addErrorf("reading private key for selector %s in domain %s: %s", name, d, err)
1146 continue
1147 }
1148 p, _ := pem.Decode(pemBuf)
1149 if p == nil {
1150 addErrorf("private key for selector %s in domain %s has no PEM block", name, d)
1151 continue
1152 }
1153 key, err := x509.ParsePKCS8PrivateKey(p.Bytes)
1154 if err != nil {
1155 addErrorf("parsing private key for selector %s in domain %s: %s", name, d, err)
1156 continue
1157 }
1158 switch k := key.(type) {
1159 case *rsa.PrivateKey:
1160 if k.N.BitLen() < 1024 {
1161 // ../rfc/6376:757
1162 // Let's help user do the right thing.
1163 addErrorf("rsa keys should be >= 1024 bits")
1164 }
1165 sel.Key = k
1166 case ed25519.PrivateKey:
1167 if sel.HashEffective != "sha256" {
1168 addErrorf("hash algorithm %q is not supported with ed25519, only sha256 is", sel.HashEffective)
1169 }
1170 sel.Key = k
1171 default:
1172 addErrorf("private key type %T not yet supported, at selector %s in domain %s", key, name, d)
1173 }
1174
1175 if len(sel.Headers) == 0 {
1176 // ../rfc/6376:2139
1177 // ../rfc/6376:2203
1178 // ../rfc/6376:2212
1179 // By default we seal signed headers, and we sign user-visible headers to
1180 // prevent/limit reuse of previously signed messages: All addressing fields, date
1181 // and subject, message-referencing fields, parsing instructions (content-type).
1182 sel.HeadersEffective = strings.Split("From,To,Cc,Bcc,Reply-To,References,In-Reply-To,Subject,Date,Message-Id,Content-Type", ",")
1183 } else {
1184 var from bool
1185 for _, h := range sel.Headers {
1186 from = from || strings.EqualFold(h, "From")
1187 // ../rfc/6376:2269
1188 if strings.EqualFold(h, "DKIM-Signature") || strings.EqualFold(h, "Received") || strings.EqualFold(h, "Return-Path") {
1189 log.Error("DKIM-signing header %q is recommended against as it may be modified in transit")
1190 }
1191 }
1192 if !from {
1193 addErrorf("From-field must always be DKIM-signed")
1194 }
1195 sel.HeadersEffective = sel.Headers
1196 }
1197
1198 domain.DKIM.Selectors[name] = sel
1199 }
1200
1201 if domain.MTASTS != nil {
1202 if !haveSTSListener {
1203 addErrorf("MTA-STS enabled for domain %q, but there is no listener for MTASTS", d)
1204 }
1205 sts := domain.MTASTS
1206 if sts.PolicyID == "" {
1207 addErrorf("invalid empty MTA-STS PolicyID")
1208 }
1209 switch sts.Mode {
1210 case mtasts.ModeNone, mtasts.ModeTesting, mtasts.ModeEnforce:
1211 default:
1212 addErrorf("invalid mtasts mode %q", sts.Mode)
1213 }
1214 }
1215
1216 checkRoutes("routes for domain", domain.Routes)
1217
1218 c.Domains[d] = domain
1219 }
1220
1221 // To determine ReportsOnly.
1222 domainHasAddress := map[string]bool{}
1223
1224 // Validate email addresses.
1225 for accName, acc := range c.Accounts {
1226 var err error
1227 acc.DNSDomain, err = dns.ParseDomain(acc.Domain)
1228 if err != nil {
1229 addErrorf("parsing domain %s for account %q: %s", acc.Domain, accName, err)
1230 }
1231
1232 if strings.EqualFold(acc.RejectsMailbox, "Inbox") {
1233 addErrorf("account %q: cannot set RejectsMailbox to inbox, messages will be removed automatically from the rejects mailbox", accName)
1234 }
1235 checkMailboxNormf(acc.RejectsMailbox, "account %q", accName)
1236
1237 if acc.AutomaticJunkFlags.JunkMailboxRegexp != "" {
1238 r, err := regexp.Compile(acc.AutomaticJunkFlags.JunkMailboxRegexp)
1239 if err != nil {
1240 addErrorf("invalid JunkMailboxRegexp regular expression: %v", err)
1241 }
1242 acc.JunkMailbox = r
1243 }
1244 if acc.AutomaticJunkFlags.NeutralMailboxRegexp != "" {
1245 r, err := regexp.Compile(acc.AutomaticJunkFlags.NeutralMailboxRegexp)
1246 if err != nil {
1247 addErrorf("invalid NeutralMailboxRegexp regular expression: %v", err)
1248 }
1249 acc.NeutralMailbox = r
1250 }
1251 if acc.AutomaticJunkFlags.NotJunkMailboxRegexp != "" {
1252 r, err := regexp.Compile(acc.AutomaticJunkFlags.NotJunkMailboxRegexp)
1253 if err != nil {
1254 addErrorf("invalid NotJunkMailboxRegexp regular expression: %v", err)
1255 }
1256 acc.NotJunkMailbox = r
1257 }
1258 c.Accounts[accName] = acc
1259
1260 // todo deprecated: only localpart as keys for Destinations, we are replacing them with full addresses. if domains.conf is written, we won't have to do this again.
1261 replaceLocalparts := map[string]string{}
1262
1263 for addrName, dest := range acc.Destinations {
1264 checkMailboxNormf(dest.Mailbox, "account %q, destination %q", accName, addrName)
1265
1266 for i, rs := range dest.Rulesets {
1267 checkMailboxNormf(rs.Mailbox, "account %q, destination %q, ruleset %d", accName, addrName, i+1)
1268
1269 n := 0
1270
1271 if rs.SMTPMailFromRegexp != "" {
1272 n++
1273 r, err := regexp.Compile(rs.SMTPMailFromRegexp)
1274 if err != nil {
1275 addErrorf("invalid SMTPMailFrom regular expression: %v", err)
1276 }
1277 c.Accounts[accName].Destinations[addrName].Rulesets[i].SMTPMailFromRegexpCompiled = r
1278 }
1279 if rs.VerifiedDomain != "" {
1280 n++
1281 d, err := dns.ParseDomain(rs.VerifiedDomain)
1282 if err != nil {
1283 addErrorf("invalid VerifiedDomain: %v", err)
1284 }
1285 c.Accounts[accName].Destinations[addrName].Rulesets[i].VerifiedDNSDomain = d
1286 }
1287
1288 var hdr [][2]*regexp.Regexp
1289 for k, v := range rs.HeadersRegexp {
1290 n++
1291 if strings.ToLower(k) != k {
1292 addErrorf("header field %q must only have lower case characters", k)
1293 }
1294 if strings.ToLower(v) != v {
1295 addErrorf("header value %q must only have lower case characters", v)
1296 }
1297 rk, err := regexp.Compile(k)
1298 if err != nil {
1299 addErrorf("invalid rule header regexp %q: %v", k, err)
1300 }
1301 rv, err := regexp.Compile(v)
1302 if err != nil {
1303 addErrorf("invalid rule header regexp %q: %v", v, err)
1304 }
1305 hdr = append(hdr, [...]*regexp.Regexp{rk, rv})
1306 }
1307 c.Accounts[accName].Destinations[addrName].Rulesets[i].HeadersRegexpCompiled = hdr
1308
1309 if n == 0 {
1310 addErrorf("ruleset must have at least one rule")
1311 }
1312
1313 if rs.IsForward && rs.ListAllowDomain != "" {
1314 addErrorf("ruleset cannot have both IsForward and ListAllowDomain")
1315 }
1316 if rs.IsForward {
1317 if rs.SMTPMailFromRegexp == "" || rs.VerifiedDomain == "" {
1318 addErrorf("ruleset with IsForward must have both SMTPMailFromRegexp and VerifiedDomain too")
1319 }
1320 }
1321 if rs.ListAllowDomain != "" {
1322 d, err := dns.ParseDomain(rs.ListAllowDomain)
1323 if err != nil {
1324 addErrorf("invalid ListAllowDomain %q: %v", rs.ListAllowDomain, err)
1325 }
1326 c.Accounts[accName].Destinations[addrName].Rulesets[i].ListAllowDNSDomain = d
1327 }
1328
1329 checkMailboxNormf(rs.AcceptRejectsToMailbox, "account %q, destination %q, ruleset %d, rejects mailbox", accName, addrName, i+1)
1330 if strings.EqualFold(rs.AcceptRejectsToMailbox, "inbox") {
1331 addErrorf("account %q, destination %q, ruleset %d: AcceptRejectsToMailbox cannot be set to Inbox", accName, addrName, i+1)
1332 }
1333 }
1334
1335 // Catchall destination for domain.
1336 if strings.HasPrefix(addrName, "@") {
1337 d, err := dns.ParseDomain(addrName[1:])
1338 if err != nil {
1339 addErrorf("parsing domain %q in account %q", addrName[1:], accName)
1340 continue
1341 } else if _, ok := c.Domains[d.Name()]; !ok {
1342 addErrorf("unknown domain for address %q in account %q", addrName, accName)
1343 continue
1344 }
1345 domainHasAddress[d.Name()] = true
1346 addrFull := "@" + d.Name()
1347 if _, ok := accDests[addrFull]; ok {
1348 addErrorf("duplicate canonicalized catchall destination address %s", addrFull)
1349 }
1350 accDests[addrFull] = AccountDestination{true, "", accName, dest}
1351 continue
1352 }
1353
1354 // todo deprecated: remove support for parsing destination as just a localpart instead full address.
1355 var address smtp.Address
1356 if localpart, err := smtp.ParseLocalpart(addrName); err != nil && errors.Is(err, smtp.ErrBadLocalpart) {
1357 address, err = smtp.ParseAddress(addrName)
1358 if err != nil {
1359 addErrorf("invalid email address %q in account %q", addrName, accName)
1360 continue
1361 } else if _, ok := c.Domains[address.Domain.Name()]; !ok {
1362 addErrorf("unknown domain for address %q in account %q", addrName, accName)
1363 continue
1364 }
1365 } else {
1366 if err != nil {
1367 addErrorf("invalid localpart %q in account %q", addrName, accName)
1368 continue
1369 }
1370 address = smtp.NewAddress(localpart, acc.DNSDomain)
1371 if _, ok := c.Domains[acc.DNSDomain.Name()]; !ok {
1372 addErrorf("unknown domain %s for account %q", acc.DNSDomain.Name(), accName)
1373 continue
1374 }
1375 replaceLocalparts[addrName] = address.Pack(true)
1376 }
1377
1378 origLP := address.Localpart
1379 dc := c.Domains[address.Domain.Name()]
1380 domainHasAddress[address.Domain.Name()] = true
1381 if lp, err := CanonicalLocalpart(address.Localpart, dc); err != nil {
1382 addErrorf("canonicalizing localpart %s: %v", address.Localpart, err)
1383 } else if dc.LocalpartCatchallSeparator != "" && strings.Contains(string(address.Localpart), dc.LocalpartCatchallSeparator) {
1384 addErrorf("localpart of address %s includes domain catchall separator %s", address, dc.LocalpartCatchallSeparator)
1385 } else {
1386 address.Localpart = lp
1387 }
1388 addrFull := address.Pack(true)
1389 if _, ok := accDests[addrFull]; ok {
1390 addErrorf("duplicate canonicalized destination address %s", addrFull)
1391 }
1392 accDests[addrFull] = AccountDestination{false, origLP, accName, dest}
1393 }
1394
1395 for lp, addr := range replaceLocalparts {
1396 dest, ok := acc.Destinations[lp]
1397 if !ok {
1398 addErrorf("could not find localpart %q to replace with address in destinations", lp)
1399 } else {
1400 log.Warn(`deprecation warning: support for account destination addresses specified as just localpart ("username") instead of full email address will be removed in the future; update domains.conf, for each Account, for each Destination, ensure each key is an email address by appending "@" and the default domain for the account`,
1401 slog.Any("localpart", lp),
1402 slog.Any("address", addr),
1403 slog.String("account", accName))
1404 acc.Destinations[addr] = dest
1405 delete(acc.Destinations, lp)
1406 }
1407 }
1408
1409 checkRoutes("routes for account", acc.Routes)
1410 }
1411
1412 // Set DMARC destinations.
1413 for d, domain := range c.Domains {
1414 dmarc := domain.DMARC
1415 if dmarc == nil {
1416 continue
1417 }
1418 if _, ok := c.Accounts[dmarc.Account]; !ok {
1419 addErrorf("DMARC account %q does not exist", dmarc.Account)
1420 }
1421 lp, err := smtp.ParseLocalpart(dmarc.Localpart)
1422 if err != nil {
1423 addErrorf("invalid DMARC localpart %q: %s", dmarc.Localpart, err)
1424 }
1425 if lp.IsInternational() {
1426 // ../rfc/8616:234
1427 addErrorf("DMARC localpart %q is an internationalized address, only conventional ascii-only address possible for interopability", lp)
1428 }
1429 addrdom := domain.Domain
1430 if dmarc.Domain != "" {
1431 addrdom, err = dns.ParseDomain(dmarc.Domain)
1432 if err != nil {
1433 addErrorf("DMARC domain %q: %s", dmarc.Domain, err)
1434 } else if _, ok := c.Domains[addrdom.Name()]; !ok {
1435 addErrorf("unknown domain %q for DMARC address in domain %q", addrdom, d)
1436 }
1437 }
1438 if addrdom == domain.Domain {
1439 domainHasAddress[addrdom.Name()] = true
1440 }
1441
1442 domain.DMARC.ParsedLocalpart = lp
1443 domain.DMARC.DNSDomain = addrdom
1444 c.Domains[d] = domain
1445 addrFull := smtp.NewAddress(lp, addrdom).String()
1446 dest := config.Destination{
1447 Mailbox: dmarc.Mailbox,
1448 DMARCReports: true,
1449 }
1450 checkMailboxNormf(dmarc.Mailbox, "DMARC mailbox for account %q", dmarc.Account)
1451 accDests[addrFull] = AccountDestination{false, lp, dmarc.Account, dest}
1452 }
1453
1454 // Set TLSRPT destinations.
1455 for d, domain := range c.Domains {
1456 tlsrpt := domain.TLSRPT
1457 if tlsrpt == nil {
1458 continue
1459 }
1460 if _, ok := c.Accounts[tlsrpt.Account]; !ok {
1461 addErrorf("TLSRPT account %q does not exist", tlsrpt.Account)
1462 }
1463 lp, err := smtp.ParseLocalpart(tlsrpt.Localpart)
1464 if err != nil {
1465 addErrorf("invalid TLSRPT localpart %q: %s", tlsrpt.Localpart, err)
1466 }
1467 if lp.IsInternational() {
1468 // Does not appear documented in ../rfc/8460, but similar to DMARC it makes sense
1469 // to keep this ascii-only addresses.
1470 addErrorf("TLSRPT localpart %q is an internationalized address, only conventional ascii-only address allowed for interopability", lp)
1471 }
1472 addrdom := domain.Domain
1473 if tlsrpt.Domain != "" {
1474 addrdom, err = dns.ParseDomain(tlsrpt.Domain)
1475 if err != nil {
1476 addErrorf("TLSRPT domain %q: %s", tlsrpt.Domain, err)
1477 } else if _, ok := c.Domains[addrdom.Name()]; !ok {
1478 addErrorf("unknown domain %q for TLSRPT address in domain %q", tlsrpt.Domain, d)
1479 }
1480 }
1481 if addrdom == domain.Domain {
1482 domainHasAddress[addrdom.Name()] = true
1483 }
1484
1485 domain.TLSRPT.ParsedLocalpart = lp
1486 domain.TLSRPT.DNSDomain = addrdom
1487 c.Domains[d] = domain
1488 addrFull := smtp.NewAddress(lp, addrdom).String()
1489 dest := config.Destination{
1490 Mailbox: tlsrpt.Mailbox,
1491 DomainTLSReports: true,
1492 }
1493 checkMailboxNormf(tlsrpt.Mailbox, "TLSRPT mailbox for account %q", tlsrpt.Account)
1494 accDests[addrFull] = AccountDestination{false, lp, tlsrpt.Account, dest}
1495 }
1496
1497 // Set ReportsOnly for domains, based on whether we have seen addresses (possibly
1498 // from DMARC or TLS reporting).
1499 for d, domain := range c.Domains {
1500 domain.ReportsOnly = !domainHasAddress[domain.Domain.Name()]
1501 c.Domains[d] = domain
1502 }
1503
1504 // Check webserver configs.
1505 if (len(c.WebDomainRedirects) > 0 || len(c.WebHandlers) > 0) && !haveWebserverListener {
1506 addErrorf("WebDomainRedirects or WebHandlers configured but no listener with WebserverHTTP or WebserverHTTPS enabled")
1507 }
1508
1509 c.WebDNSDomainRedirects = map[dns.Domain]dns.Domain{}
1510 for from, to := range c.WebDomainRedirects {
1511 fromdom, err := dns.ParseDomain(from)
1512 if err != nil {
1513 addErrorf("parsing domain for redirect %s: %v", from, err)
1514 }
1515 todom, err := dns.ParseDomain(to)
1516 if err != nil {
1517 addErrorf("parsing domain for redirect %s: %v", to, err)
1518 } else if fromdom == todom {
1519 addErrorf("will not redirect domain %s to itself", todom)
1520 }
1521 var zerodom dns.Domain
1522 if _, ok := c.WebDNSDomainRedirects[fromdom]; ok && fromdom != zerodom {
1523 addErrorf("duplicate redirect domain %s", from)
1524 }
1525 c.WebDNSDomainRedirects[fromdom] = todom
1526 }
1527
1528 for i := range c.WebHandlers {
1529 wh := &c.WebHandlers[i]
1530
1531 if wh.LogName == "" {
1532 wh.Name = fmt.Sprintf("%d", i)
1533 } else {
1534 wh.Name = wh.LogName
1535 }
1536
1537 dom, err := dns.ParseDomain(wh.Domain)
1538 if err != nil {
1539 addErrorf("webhandler %s %s: parsing domain: %v", wh.Domain, wh.PathRegexp, err)
1540 }
1541 wh.DNSDomain = dom
1542
1543 if !strings.HasPrefix(wh.PathRegexp, "^") {
1544 addErrorf("webhandler %s %s: path regexp must start with a ^", wh.Domain, wh.PathRegexp)
1545 }
1546 re, err := regexp.Compile(wh.PathRegexp)
1547 if err != nil {
1548 addErrorf("webhandler %s %s: compiling regexp: %v", wh.Domain, wh.PathRegexp, err)
1549 }
1550 wh.Path = re
1551
1552 var n int
1553 if wh.WebStatic != nil {
1554 n++
1555 ws := wh.WebStatic
1556 if ws.StripPrefix != "" && !strings.HasPrefix(ws.StripPrefix, "/") {
1557 addErrorf("webstatic %s %s: prefix to strip %s must start with a slash", wh.Domain, wh.PathRegexp, ws.StripPrefix)
1558 }
1559 for k := range ws.ResponseHeaders {
1560 xk := k
1561 k := strings.TrimSpace(xk)
1562 if k != xk || k == "" {
1563 addErrorf("webstatic %s %s: bad header %q", wh.Domain, wh.PathRegexp, xk)
1564 }
1565 }
1566 }
1567 if wh.WebRedirect != nil {
1568 n++
1569 wr := wh.WebRedirect
1570 if wr.BaseURL != "" {
1571 u, err := url.Parse(wr.BaseURL)
1572 if err != nil {
1573 addErrorf("webredirect %s %s: parsing redirect url %s: %v", wh.Domain, wh.PathRegexp, wr.BaseURL, err)
1574 }
1575 switch u.Path {
1576 case "", "/":
1577 u.Path = "/"
1578 default:
1579 addErrorf("webredirect %s %s: BaseURL must have empty path", wh.Domain, wh.PathRegexp, wr.BaseURL)
1580 }
1581 wr.URL = u
1582 }
1583 if wr.OrigPathRegexp != "" && wr.ReplacePath != "" {
1584 re, err := regexp.Compile(wr.OrigPathRegexp)
1585 if err != nil {
1586 addErrorf("webredirect %s %s: compiling regexp %s: %v", wh.Domain, wh.PathRegexp, wr.OrigPathRegexp, err)
1587 }
1588 wr.OrigPath = re
1589 } else if wr.OrigPathRegexp != "" || wr.ReplacePath != "" {
1590 addErrorf("webredirect %s %s: must have either both OrigPathRegexp and ReplacePath, or neither", wh.Domain, wh.PathRegexp)
1591 } else if wr.BaseURL == "" {
1592 addErrorf("webredirect %s %s: must at least one of BaseURL and OrigPathRegexp+ReplacePath", wh.Domain, wh.PathRegexp)
1593 }
1594 if wr.StatusCode != 0 && (wr.StatusCode < 300 || wr.StatusCode >= 400) {
1595 addErrorf("webredirect %s %s: invalid redirect status code %d", wh.Domain, wh.PathRegexp, wr.StatusCode)
1596 }
1597 }
1598 if wh.WebForward != nil {
1599 n++
1600 wf := wh.WebForward
1601 u, err := url.Parse(wf.URL)
1602 if err != nil {
1603 addErrorf("webforward %s %s: parsing url %s: %v", wh.Domain, wh.PathRegexp, wf.URL, err)
1604 }
1605 wf.TargetURL = u
1606
1607 for k := range wf.ResponseHeaders {
1608 xk := k
1609 k := strings.TrimSpace(xk)
1610 if k != xk || k == "" {
1611 addErrorf("webforward %s %s: bad header %q", wh.Domain, wh.PathRegexp, xk)
1612 }
1613 }
1614 }
1615 if n != 1 {
1616 addErrorf("webhandler %s %s: must have exactly one handler, not %d", wh.Domain, wh.PathRegexp, n)
1617 }
1618 }
1619
1620 c.MonitorDNSBLZones = nil
1621 for _, s := range c.MonitorDNSBLs {
1622 d, err := dns.ParseDomain(s)
1623 if err != nil {
1624 addErrorf("invalid monitor dnsbl zone %s: %v", s, err)
1625 continue
1626 }
1627 if slices.Contains(c.MonitorDNSBLZones, d) {
1628 addErrorf("duplicate zone %s in monitor dnsbl zones", d)
1629 continue
1630 }
1631 c.MonitorDNSBLZones = append(c.MonitorDNSBLZones, d)
1632 }
1633
1634 return
1635}
1636
1637func loadPrivateKeyFile(keyPath string) (crypto.Signer, error) {
1638 keyBuf, err := os.ReadFile(keyPath)
1639 if err != nil {
1640 return nil, fmt.Errorf("reading host private key: %v", err)
1641 }
1642 b, _ := pem.Decode(keyBuf)
1643 if b == nil {
1644 return nil, fmt.Errorf("parsing pem block for private key: %v", err)
1645 }
1646 var privKey any
1647 switch b.Type {
1648 case "PRIVATE KEY":
1649 privKey, err = x509.ParsePKCS8PrivateKey(b.Bytes)
1650 case "RSA PRIVATE KEY":
1651 privKey, err = x509.ParsePKCS1PrivateKey(b.Bytes)
1652 case "EC PRIVATE KEY":
1653 privKey, err = x509.ParseECPrivateKey(b.Bytes)
1654 default:
1655 err = fmt.Errorf("unknown pem type %q", b.Type)
1656 }
1657 if err != nil {
1658 return nil, fmt.Errorf("parsing private key: %v", err)
1659 }
1660 if k, ok := privKey.(crypto.Signer); ok {
1661 return k, nil
1662 }
1663 return nil, fmt.Errorf("parsed private key not a crypto.Signer, but %T", privKey)
1664}
1665
1666func loadTLSKeyCerts(configFile, kind string, ctls *config.TLS) error {
1667 certs := []tls.Certificate{}
1668 for _, kp := range ctls.KeyCerts {
1669 certPath := configDirPath(configFile, kp.CertFile)
1670 keyPath := configDirPath(configFile, kp.KeyFile)
1671 cert, err := loadX509KeyPairPrivileged(certPath, keyPath)
1672 if err != nil {
1673 return fmt.Errorf("tls config for %q: parsing x509 key pair: %v", kind, err)
1674 }
1675 certs = append(certs, cert)
1676 }
1677 ctls.Config = &tls.Config{
1678 Certificates: certs,
1679 }
1680 return nil
1681}
1682
1683// load x509 key/cert files from file descriptor possibly passed in by privileged
1684// process.
1685func loadX509KeyPairPrivileged(certPath, keyPath string) (tls.Certificate, error) {
1686 certBuf, err := readFilePrivileged(certPath)
1687 if err != nil {
1688 return tls.Certificate{}, fmt.Errorf("reading tls certificate: %v", err)
1689 }
1690 keyBuf, err := readFilePrivileged(keyPath)
1691 if err != nil {
1692 return tls.Certificate{}, fmt.Errorf("reading tls key: %v", err)
1693 }
1694 return tls.X509KeyPair(certBuf, keyBuf)
1695}
1696
1697// like os.ReadFile, but open privileged file possibly passed in by root process.
1698func readFilePrivileged(path string) ([]byte, error) {
1699 f, err := OpenPrivileged(path)
1700 if err != nil {
1701 return nil, err
1702 }
1703 defer f.Close()
1704 return io.ReadAll(f)
1705}
1706