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