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