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