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, tlsconfigFallback *tls.Config
713 if checkOnly || acme.Manager == nil {
714 tlsconfig = &tls.Config{}
715 tlsconfigFallback = &tls.Config{}
716 } else {
717 hostname := c.HostnameDomain
718 if l.Hostname != "" {
719 hostname = l.HostnameDomain
720 }
721 // If SNI is absent, we will use the listener hostname, but reject connections with
722 // an SNI hostname that is not allowlisted.
723 // Incoming SMTP deliveries use tlsconfigFallback for interoperability. TLS
724 // connections for unknown SNI hostnames fall back to a certificate for the
725 // listener hostname instead of causing the TLS connection to fail.
726 tlsconfig = acme.Manager.TLSConfig(hostname, true, false)
727 tlsconfigFallback = acme.Manager.TLSConfig(hostname, true, true)
728 l.TLS.ACMEConfig = acme.Manager.ACMETLSConfig
729 }
730 l.TLS.Config = tlsconfig
731 l.TLS.ConfigFallback = tlsconfigFallback
732 } else if len(l.TLS.KeyCerts) != 0 {
733 if doLoadTLSKeyCerts {
734 if err := loadTLSKeyCerts(configFile, "listener "+name, l.TLS); err != nil {
735 addErrorf("%w", err)
736 }
737 }
738 } else {
739 addErrorf("listener %q: cannot have TLS config without ACME and without static keys/certificates", name)
740 }
741 for _, privKeyFile := range l.TLS.HostPrivateKeyFiles {
742 keyPath := configDirPath(configFile, privKeyFile)
743 privKey, err := loadPrivateKeyFile(keyPath)
744 if err != nil {
745 addErrorf("listener %q: parsing host private key for DANE and ACME certificates: %v", name, err)
746 continue
747 }
748 switch k := privKey.(type) {
749 case *rsa.PrivateKey:
750 if k.N.BitLen() != 2048 {
751 log.Error("need rsa key with 2048 bits, for host private key for DANE/ACME certificates, ignoring",
752 slog.String("listener", name),
753 slog.String("file", keyPath),
754 slog.Int("bits", k.N.BitLen()))
755 continue
756 }
757 l.TLS.HostPrivateRSA2048Keys = append(l.TLS.HostPrivateRSA2048Keys, k)
758 case *ecdsa.PrivateKey:
759 if k.Curve != elliptic.P256() {
760 log.Error("unrecognized ecdsa curve for host private key for DANE/ACME certificates, ignoring", slog.String("listener", name), slog.String("file", keyPath))
761 continue
762 }
763 l.TLS.HostPrivateECDSAP256Keys = append(l.TLS.HostPrivateECDSAP256Keys, k)
764 default:
765 log.Error("unrecognized key type for host private key for DANE/ACME certificates, ignoring",
766 slog.String("listener", name),
767 slog.String("file", keyPath),
768 slog.String("keytype", fmt.Sprintf("%T", privKey)))
769 continue
770 }
771 }
772 if l.TLS.ACME != "" && (len(l.TLS.HostPrivateRSA2048Keys) == 0) != (len(l.TLS.HostPrivateECDSAP256Keys) == 0) {
773 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")
774 }
775
776 // TLS 1.2 was introduced in 2008. TLS <1.2 was deprecated by ../rfc/8996:31 and ../rfc/8997:66 in 2021.
777 var minVersion uint16 = tls.VersionTLS12
778 if l.TLS.MinVersion != "" {
779 versions := map[string]uint16{
780 "TLSv1.0": tls.VersionTLS10,
781 "TLSv1.1": tls.VersionTLS11,
782 "TLSv1.2": tls.VersionTLS12,
783 "TLSv1.3": tls.VersionTLS13,
784 }
785 v, ok := versions[l.TLS.MinVersion]
786 if !ok {
787 addErrorf("listener %q: unknown TLS mininum version %q", name, l.TLS.MinVersion)
788 }
789 minVersion = v
790 }
791 if l.TLS.Config != nil {
792 l.TLS.Config.MinVersion = minVersion
793 }
794 if l.TLS.ConfigFallback != nil {
795 l.TLS.ConfigFallback.MinVersion = minVersion
796 }
797 if l.TLS.ACMEConfig != nil {
798 l.TLS.ACMEConfig.MinVersion = minVersion
799 }
800 } else {
801 var needsTLS []string
802 needtls := func(s string, v bool) {
803 if v {
804 needsTLS = append(needsTLS, s)
805 }
806 }
807 needtls("IMAPS", l.IMAPS.Enabled)
808 needtls("SMTP", l.SMTP.Enabled && !l.SMTP.NoSTARTTLS)
809 needtls("Submissions", l.Submissions.Enabled)
810 needtls("Submission", l.Submission.Enabled && !l.Submission.NoRequireSTARTTLS)
811 needtls("AccountHTTPS", l.AccountHTTPS.Enabled)
812 needtls("AdminHTTPS", l.AdminHTTPS.Enabled)
813 needtls("AutoconfigHTTPS", l.AutoconfigHTTPS.Enabled && !l.AutoconfigHTTPS.NonTLS)
814 needtls("MTASTSHTTPS", l.MTASTSHTTPS.Enabled && !l.MTASTSHTTPS.NonTLS)
815 needtls("WebserverHTTPS", l.WebserverHTTPS.Enabled)
816 if len(needsTLS) > 0 {
817 addErrorf("listener %q does not specify tls config, but requires tls for %s", name, strings.Join(needsTLS, ", "))
818 }
819 }
820 if l.AutoconfigHTTPS.Enabled && l.MTASTSHTTPS.Enabled && l.AutoconfigHTTPS.Port == l.MTASTSHTTPS.Port && l.AutoconfigHTTPS.NonTLS != l.MTASTSHTTPS.NonTLS {
821 addErrorf("listener %q tries to enable autoconfig and mta-sts enabled on same port but with both http and https", name)
822 }
823 if l.SMTP.Enabled {
824 if len(l.IPs) == 0 {
825 haveUnspecifiedSMTPListener = true
826 }
827 for _, ipstr := range l.IPs {
828 ip := net.ParseIP(ipstr)
829 if ip == nil {
830 addErrorf("listener %q has invalid IP %q", name, ipstr)
831 continue
832 }
833 if ip.IsUnspecified() {
834 haveUnspecifiedSMTPListener = true
835 break
836 }
837 if len(c.SpecifiedSMTPListenIPs) >= 2 {
838 haveUnspecifiedSMTPListener = true
839 } else if len(c.SpecifiedSMTPListenIPs) > 0 && (c.SpecifiedSMTPListenIPs[0].To4() == nil) == (ip.To4() == nil) {
840 haveUnspecifiedSMTPListener = true
841 } else {
842 c.SpecifiedSMTPListenIPs = append(c.SpecifiedSMTPListenIPs, ip)
843 }
844 }
845 }
846 for _, s := range l.SMTP.DNSBLs {
847 d, err := dns.ParseDomain(s)
848 if err != nil {
849 addErrorf("listener %q has invalid DNSBL zone %q", name, s)
850 continue
851 }
852 l.SMTP.DNSBLZones = append(l.SMTP.DNSBLZones, d)
853 }
854 if l.IPsNATed && len(l.NATIPs) > 0 {
855 addErrorf("listener %q has both IPsNATed and NATIPs (remove deprecated IPsNATed)", name)
856 }
857 for _, ipstr := range l.NATIPs {
858 ip := net.ParseIP(ipstr)
859 if ip == nil {
860 addErrorf("listener %q has invalid ip %q", name, ipstr)
861 } else if ip.IsUnspecified() || ip.IsLoopback() {
862 addErrorf("listener %q has NAT ip that is the unspecified or loopback address %s", name, ipstr)
863 }
864 }
865 checkPath := func(kind string, enabled bool, path string) {
866 if enabled && path != "" && !strings.HasPrefix(path, "/") {
867 addErrorf("listener %q has %s with path %q that must start with a slash", name, kind, path)
868 }
869 }
870 checkPath("AccountHTTP", l.AccountHTTP.Enabled, l.AccountHTTP.Path)
871 checkPath("AccountHTTPS", l.AccountHTTPS.Enabled, l.AccountHTTPS.Path)
872 checkPath("AdminHTTP", l.AdminHTTP.Enabled, l.AdminHTTP.Path)
873 checkPath("AdminHTTPS", l.AdminHTTPS.Enabled, l.AdminHTTPS.Path)
874 c.Listeners[name] = l
875 }
876 if haveUnspecifiedSMTPListener {
877 c.SpecifiedSMTPListenIPs = nil
878 }
879
880 var zerouse config.SpecialUseMailboxes
881 if len(c.DefaultMailboxes) > 0 && (c.InitialMailboxes.SpecialUse != zerouse || len(c.InitialMailboxes.Regular) > 0) {
882 addErrorf("cannot have both DefaultMailboxes and InitialMailboxes")
883 }
884 // DefaultMailboxes is deprecated.
885 for _, mb := range c.DefaultMailboxes {
886 checkMailboxNormf(mb, "default mailbox")
887 }
888 checkSpecialUseMailbox := func(nameOpt string) {
889 if nameOpt != "" {
890 checkMailboxNormf(nameOpt, "special-use initial mailbox")
891 if strings.EqualFold(nameOpt, "inbox") {
892 addErrorf("initial mailbox cannot be set to Inbox (Inbox is always created)")
893 }
894 }
895 }
896 checkSpecialUseMailbox(c.InitialMailboxes.SpecialUse.Archive)
897 checkSpecialUseMailbox(c.InitialMailboxes.SpecialUse.Draft)
898 checkSpecialUseMailbox(c.InitialMailboxes.SpecialUse.Junk)
899 checkSpecialUseMailbox(c.InitialMailboxes.SpecialUse.Sent)
900 checkSpecialUseMailbox(c.InitialMailboxes.SpecialUse.Trash)
901 for _, name := range c.InitialMailboxes.Regular {
902 checkMailboxNormf(name, "regular initial mailbox")
903 if strings.EqualFold(name, "inbox") {
904 addErrorf("initial regular mailbox cannot be set to Inbox (Inbox is always created)")
905 }
906 }
907
908 checkTransportSMTP := func(name string, isTLS bool, t *config.TransportSMTP) {
909 var err error
910 t.DNSHost, err = dns.ParseDomain(t.Host)
911 if err != nil {
912 addErrorf("transport %s: bad host %s: %v", name, t.Host, err)
913 }
914
915 if isTLS && t.STARTTLSInsecureSkipVerify {
916 addErrorf("transport %s: cannot have STARTTLSInsecureSkipVerify with immediate TLS")
917 }
918 if isTLS && t.NoSTARTTLS {
919 addErrorf("transport %s: cannot have NoSTARTTLS with immediate TLS")
920 }
921
922 if t.Auth == nil {
923 return
924 }
925 seen := map[string]bool{}
926 for _, m := range t.Auth.Mechanisms {
927 if seen[m] {
928 addErrorf("transport %s: duplicate authentication mechanism %s", name, m)
929 }
930 seen[m] = true
931 switch m {
932 case "SCRAM-SHA-256-PLUS":
933 case "SCRAM-SHA-256":
934 case "SCRAM-SHA-1-PLUS":
935 case "SCRAM-SHA-1":
936 case "CRAM-MD5":
937 case "PLAIN":
938 default:
939 addErrorf("transport %s: unknown authentication mechanism %s", name, m)
940 }
941 }
942
943 t.Auth.EffectiveMechanisms = t.Auth.Mechanisms
944 if len(t.Auth.EffectiveMechanisms) == 0 {
945 t.Auth.EffectiveMechanisms = []string{"SCRAM-SHA-256-PLUS", "SCRAM-SHA-256", "SCRAM-SHA-1-PLUS", "SCRAM-SHA-1", "CRAM-MD5"}
946 }
947 }
948
949 checkTransportSocks := func(name string, t *config.TransportSocks) {
950 _, _, err := net.SplitHostPort(t.Address)
951 if err != nil {
952 addErrorf("transport %s: bad address %s: %v", name, t.Address, err)
953 }
954 for _, ipstr := range t.RemoteIPs {
955 ip := net.ParseIP(ipstr)
956 if ip == nil {
957 addErrorf("transport %s: bad ip %s", name, ipstr)
958 } else {
959 t.IPs = append(t.IPs, ip)
960 }
961 }
962 t.Hostname, err = dns.ParseDomain(t.RemoteHostname)
963 if err != nil {
964 addErrorf("transport %s: bad hostname %s: %v", name, t.RemoteHostname, err)
965 }
966 }
967
968 checkTransportDirect := func(name string, t *config.TransportDirect) {
969 if t.DisableIPv4 && t.DisableIPv6 {
970 addErrorf("transport %s: both IPv4 and IPv6 are disabled, enable at least one", name)
971 }
972 t.IPFamily = "ip"
973 if t.DisableIPv4 {
974 t.IPFamily = "ip6"
975 }
976 if t.DisableIPv6 {
977 t.IPFamily = "ip4"
978 }
979 }
980
981 for name, t := range c.Transports {
982 n := 0
983 if t.Submissions != nil {
984 n++
985 checkTransportSMTP(name, true, t.Submissions)
986 }
987 if t.Submission != nil {
988 n++
989 checkTransportSMTP(name, false, t.Submission)
990 }
991 if t.SMTP != nil {
992 n++
993 checkTransportSMTP(name, false, t.SMTP)
994 }
995 if t.Socks != nil {
996 n++
997 checkTransportSocks(name, t.Socks)
998 }
999 if t.Direct != nil {
1000 n++
1001 checkTransportDirect(name, t.Direct)
1002 }
1003 if n > 1 {
1004 addErrorf("transport %s: cannot have multiple methods in a transport", name)
1005 }
1006 }
1007
1008 // Load CA certificate pool.
1009 if c.TLS.CA != nil {
1010 if c.TLS.CA.AdditionalToSystem {
1011 var err error
1012 c.TLS.CertPool, err = x509.SystemCertPool()
1013 if err != nil {
1014 addErrorf("fetching system CA cert pool: %v", err)
1015 }
1016 } else {
1017 c.TLS.CertPool = x509.NewCertPool()
1018 }
1019 for _, certfile := range c.TLS.CA.CertFiles {
1020 p := configDirPath(configFile, certfile)
1021 pemBuf, err := os.ReadFile(p)
1022 if err != nil {
1023 addErrorf("reading TLS CA cert file: %v", err)
1024 continue
1025 } else if !c.TLS.CertPool.AppendCertsFromPEM(pemBuf) {
1026 // todo: can we check more fully if we're getting some useful data back?
1027 addErrorf("no CA certs added from %q", p)
1028 }
1029 }
1030 }
1031 return
1032}
1033
1034// PrepareDynamicConfig parses the dynamic config file given a static file.
1035func 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) {
1036 addErrorf := func(format string, args ...any) {
1037 errs = append(errs, fmt.Errorf(format, args...))
1038 }
1039
1040 f, err := os.Open(dynamicPath)
1041 if err != nil {
1042 addErrorf("parsing domains config: %v", err)
1043 return
1044 }
1045 defer f.Close()
1046 fi, err := f.Stat()
1047 if err != nil {
1048 addErrorf("stat domains config: %v", err)
1049 }
1050 if err := sconf.Parse(f, &c); err != nil {
1051 addErrorf("parsing dynamic config file: %v", err)
1052 return
1053 }
1054
1055 accDests, aliases, errs = prepareDynamicConfig(ctx, log, dynamicPath, static, &c)
1056 return c, fi.ModTime(), accDests, aliases, errs
1057}
1058
1059func 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) {
1060 addErrorf := func(format string, args ...any) {
1061 errs = append(errs, fmt.Errorf(format, args...))
1062 }
1063
1064 // Check that mailbox is in unicode NFC normalized form.
1065 checkMailboxNormf := func(mailbox string, format string, args ...any) {
1066 s := norm.NFC.String(mailbox)
1067 if mailbox != s {
1068 msg := fmt.Sprintf(format, args...)
1069 addErrorf("%s: mailbox %q is not in NFC normalized form, should be %q", msg, mailbox, s)
1070 }
1071 }
1072
1073 // Validate postmaster account exists.
1074 if _, ok := c.Accounts[static.Postmaster.Account]; !ok {
1075 addErrorf("postmaster account %q does not exist", static.Postmaster.Account)
1076 }
1077 checkMailboxNormf(static.Postmaster.Mailbox, "postmaster mailbox")
1078
1079 accDests = map[string]AccountDestination{}
1080 aliases = map[string]config.Alias{}
1081
1082 // Validate host TLSRPT account/address.
1083 if static.HostTLSRPT.Account != "" {
1084 if _, ok := c.Accounts[static.HostTLSRPT.Account]; !ok {
1085 addErrorf("host tlsrpt account %q does not exist", static.HostTLSRPT.Account)
1086 }
1087 checkMailboxNormf(static.HostTLSRPT.Mailbox, "host tlsrpt mailbox")
1088
1089 // Localpart has been parsed already.
1090
1091 addrFull := smtp.NewAddress(static.HostTLSRPT.ParsedLocalpart, static.HostnameDomain).String()
1092 dest := config.Destination{
1093 Mailbox: static.HostTLSRPT.Mailbox,
1094 HostTLSReports: true,
1095 }
1096 accDests[addrFull] = AccountDestination{false, static.HostTLSRPT.ParsedLocalpart, static.HostTLSRPT.Account, dest}
1097 }
1098
1099 var haveSTSListener, haveWebserverListener bool
1100 for _, l := range static.Listeners {
1101 if l.MTASTSHTTPS.Enabled {
1102 haveSTSListener = true
1103 }
1104 if l.WebserverHTTP.Enabled || l.WebserverHTTPS.Enabled {
1105 haveWebserverListener = true
1106 }
1107 }
1108
1109 checkRoutes := func(descr string, routes []config.Route) {
1110 parseRouteDomains := func(l []string) []string {
1111 var r []string
1112 for _, e := range l {
1113 if e == "." {
1114 r = append(r, e)
1115 continue
1116 }
1117 prefix := ""
1118 if strings.HasPrefix(e, ".") {
1119 prefix = "."
1120 e = e[1:]
1121 }
1122 d, err := dns.ParseDomain(e)
1123 if err != nil {
1124 addErrorf("%s: invalid domain %s: %v", descr, e, err)
1125 }
1126 r = append(r, prefix+d.ASCII)
1127 }
1128 return r
1129 }
1130
1131 for i := range routes {
1132 routes[i].FromDomainASCII = parseRouteDomains(routes[i].FromDomain)
1133 routes[i].ToDomainASCII = parseRouteDomains(routes[i].ToDomain)
1134 var ok bool
1135 routes[i].ResolvedTransport, ok = static.Transports[routes[i].Transport]
1136 if !ok {
1137 addErrorf("%s: route references undefined transport %s", descr, routes[i].Transport)
1138 }
1139 }
1140 }
1141
1142 checkRoutes("global routes", c.Routes)
1143
1144 // Validate domains.
1145 c.ClientSettingDomains = map[dns.Domain]struct{}{}
1146 for d, domain := range c.Domains {
1147 dnsdomain, err := dns.ParseDomain(d)
1148 if err != nil {
1149 addErrorf("bad domain %q: %s", d, err)
1150 } else if dnsdomain.Name() != d {
1151 addErrorf("domain %s must be specified in unicode form, %s", d, dnsdomain.Name())
1152 }
1153
1154 domain.Domain = dnsdomain
1155
1156 if domain.ClientSettingsDomain != "" {
1157 csd, err := dns.ParseDomain(domain.ClientSettingsDomain)
1158 if err != nil {
1159 addErrorf("bad client settings domain %q: %s", domain.ClientSettingsDomain, err)
1160 }
1161 domain.ClientSettingsDNSDomain = csd
1162 c.ClientSettingDomains[csd] = struct{}{}
1163 }
1164
1165 for _, sign := range domain.DKIM.Sign {
1166 if _, ok := domain.DKIM.Selectors[sign]; !ok {
1167 addErrorf("selector %s for signing is missing in domain %s", sign, d)
1168 }
1169 }
1170 for name, sel := range domain.DKIM.Selectors {
1171 seld, err := dns.ParseDomain(name)
1172 if err != nil {
1173 addErrorf("bad selector %q: %s", name, err)
1174 } else if seld.Name() != name {
1175 addErrorf("selector %q must be specified in unicode form, %q", name, seld.Name())
1176 }
1177 sel.Domain = seld
1178
1179 if sel.Expiration != "" {
1180 exp, err := time.ParseDuration(sel.Expiration)
1181 if err != nil {
1182 addErrorf("selector %q has invalid expiration %q: %v", name, sel.Expiration, err)
1183 } else {
1184 sel.ExpirationSeconds = int(exp / time.Second)
1185 }
1186 }
1187
1188 sel.HashEffective = sel.Hash
1189 switch sel.HashEffective {
1190 case "":
1191 sel.HashEffective = "sha256"
1192 case "sha1":
1193 log.Error("using sha1 with DKIM is deprecated as not secure enough, switch to sha256")
1194 case "sha256":
1195 default:
1196 addErrorf("unsupported hash %q for selector %q in domain %s", sel.HashEffective, name, d)
1197 }
1198
1199 pemBuf, err := os.ReadFile(configDirPath(dynamicPath, sel.PrivateKeyFile))
1200 if err != nil {
1201 addErrorf("reading private key for selector %s in domain %s: %s", name, d, err)
1202 continue
1203 }
1204 p, _ := pem.Decode(pemBuf)
1205 if p == nil {
1206 addErrorf("private key for selector %s in domain %s has no PEM block", name, d)
1207 continue
1208 }
1209 key, err := x509.ParsePKCS8PrivateKey(p.Bytes)
1210 if err != nil {
1211 addErrorf("parsing private key for selector %s in domain %s: %s", name, d, err)
1212 continue
1213 }
1214 switch k := key.(type) {
1215 case *rsa.PrivateKey:
1216 if k.N.BitLen() < 1024 {
1217 // ../rfc/6376:757
1218 // Let's help user do the right thing.
1219 addErrorf("rsa keys should be >= 1024 bits")
1220 }
1221 sel.Key = k
1222 sel.Algorithm = fmt.Sprintf("rsa-%d", k.N.BitLen())
1223 case ed25519.PrivateKey:
1224 if sel.HashEffective != "sha256" {
1225 addErrorf("hash algorithm %q is not supported with ed25519, only sha256 is", sel.HashEffective)
1226 }
1227 sel.Key = k
1228 sel.Algorithm = "ed25519"
1229 default:
1230 addErrorf("private key type %T not yet supported, at selector %s in domain %s", key, name, d)
1231 }
1232
1233 if len(sel.Headers) == 0 {
1234 // ../rfc/6376:2139
1235 // ../rfc/6376:2203
1236 // ../rfc/6376:2212
1237 // By default we seal signed headers, and we sign user-visible headers to
1238 // prevent/limit reuse of previously signed messages: All addressing fields, date
1239 // and subject, message-referencing fields, parsing instructions (content-type).
1240 sel.HeadersEffective = strings.Split("From,To,Cc,Bcc,Reply-To,References,In-Reply-To,Subject,Date,Message-Id,Content-Type", ",")
1241 } else {
1242 var from bool
1243 for _, h := range sel.Headers {
1244 from = from || strings.EqualFold(h, "From")
1245 // ../rfc/6376:2269
1246 if strings.EqualFold(h, "DKIM-Signature") || strings.EqualFold(h, "Received") || strings.EqualFold(h, "Return-Path") {
1247 log.Error("DKIM-signing header %q is recommended against as it may be modified in transit")
1248 }
1249 }
1250 if !from {
1251 addErrorf("From-field must always be DKIM-signed")
1252 }
1253 sel.HeadersEffective = sel.Headers
1254 }
1255
1256 domain.DKIM.Selectors[name] = sel
1257 }
1258
1259 if domain.MTASTS != nil {
1260 if !haveSTSListener {
1261 addErrorf("MTA-STS enabled for domain %q, but there is no listener for MTASTS", d)
1262 }
1263 sts := domain.MTASTS
1264 if sts.PolicyID == "" {
1265 addErrorf("invalid empty MTA-STS PolicyID")
1266 }
1267 switch sts.Mode {
1268 case mtasts.ModeNone, mtasts.ModeTesting, mtasts.ModeEnforce:
1269 default:
1270 addErrorf("invalid mtasts mode %q", sts.Mode)
1271 }
1272 }
1273
1274 checkRoutes("routes for domain", domain.Routes)
1275
1276 c.Domains[d] = domain
1277 }
1278
1279 // To determine ReportsOnly.
1280 domainHasAddress := map[string]bool{}
1281
1282 // Validate email addresses.
1283 for accName, acc := range c.Accounts {
1284 var err error
1285 acc.DNSDomain, err = dns.ParseDomain(acc.Domain)
1286 if err != nil {
1287 addErrorf("parsing domain %s for account %q: %s", acc.Domain, accName, err)
1288 }
1289
1290 if strings.EqualFold(acc.RejectsMailbox, "Inbox") {
1291 addErrorf("account %q: cannot set RejectsMailbox to inbox, messages will be removed automatically from the rejects mailbox", accName)
1292 }
1293 checkMailboxNormf(acc.RejectsMailbox, "account %q", accName)
1294
1295 if acc.AutomaticJunkFlags.JunkMailboxRegexp != "" {
1296 r, err := regexp.Compile(acc.AutomaticJunkFlags.JunkMailboxRegexp)
1297 if err != nil {
1298 addErrorf("invalid JunkMailboxRegexp regular expression: %v", err)
1299 }
1300 acc.JunkMailbox = r
1301 }
1302 if acc.AutomaticJunkFlags.NeutralMailboxRegexp != "" {
1303 r, err := regexp.Compile(acc.AutomaticJunkFlags.NeutralMailboxRegexp)
1304 if err != nil {
1305 addErrorf("invalid NeutralMailboxRegexp regular expression: %v", err)
1306 }
1307 acc.NeutralMailbox = r
1308 }
1309 if acc.AutomaticJunkFlags.NotJunkMailboxRegexp != "" {
1310 r, err := regexp.Compile(acc.AutomaticJunkFlags.NotJunkMailboxRegexp)
1311 if err != nil {
1312 addErrorf("invalid NotJunkMailboxRegexp regular expression: %v", err)
1313 }
1314 acc.NotJunkMailbox = r
1315 }
1316
1317 if acc.JunkFilter != nil {
1318 params := acc.JunkFilter.Params
1319 if params.MaxPower < 0 || params.MaxPower > 0.5 {
1320 addErrorf("junk filter MaxPower must be >= 0 and < 0.5")
1321 }
1322 if params.TopWords < 0 {
1323 addErrorf("junk filter TopWords must be >= 0")
1324 }
1325 if params.IgnoreWords < 0 || params.IgnoreWords > 0.5 {
1326 addErrorf("junk filter IgnoreWords must be >= 0 and < 0.5")
1327 }
1328 if params.RareWords < 0 {
1329 addErrorf("junk filter RareWords must be >= 0")
1330 }
1331 }
1332
1333 acc.ParsedFromIDLoginAddresses = make([]smtp.Address, len(acc.FromIDLoginAddresses))
1334 for i, s := range acc.FromIDLoginAddresses {
1335 a, err := smtp.ParseAddress(s)
1336 if err != nil {
1337 addErrorf("invalid fromid login address %q in account %q: %v", s, accName, err)
1338 }
1339 // We check later on if address belongs to account.
1340 dom, ok := c.Domains[a.Domain.Name()]
1341 if !ok {
1342 addErrorf("unknown domain in fromid login address %q for account %q", s, accName)
1343 } else if dom.LocalpartCatchallSeparator == "" {
1344 addErrorf("localpart catchall separator not configured for domain for fromid login address %q for account %q", s, accName)
1345 }
1346 acc.ParsedFromIDLoginAddresses[i] = a
1347 }
1348
1349 // Clear any previously derived state.
1350 acc.Aliases = nil
1351
1352 c.Accounts[accName] = acc
1353
1354 if acc.OutgoingWebhook != nil {
1355 u, err := url.Parse(acc.OutgoingWebhook.URL)
1356 if err == nil && (u.Scheme != "http" && u.Scheme != "https") {
1357 err = errors.New("scheme must be http or https")
1358 }
1359 if err != nil {
1360 addErrorf("parsing outgoing hook url %q in account %q: %v", acc.OutgoingWebhook.URL, accName, err)
1361 }
1362
1363 // note: outgoing hook events are in ../queue/hooks.go, ../mox-/config.go, ../queue.go and ../webapi/gendoc.sh. keep in sync.
1364 outgoingHookEvents := []string{"delivered", "suppressed", "delayed", "failed", "relayed", "expanded", "canceled", "unrecognized"}
1365 for _, e := range acc.OutgoingWebhook.Events {
1366 if !slices.Contains(outgoingHookEvents, e) {
1367 addErrorf("unknown outgoing hook event %q", e)
1368 }
1369 }
1370 }
1371 if acc.IncomingWebhook != nil {
1372 u, err := url.Parse(acc.IncomingWebhook.URL)
1373 if err == nil && (u.Scheme != "http" && u.Scheme != "https") {
1374 err = errors.New("scheme must be http or https")
1375 }
1376 if err != nil {
1377 addErrorf("parsing incoming hook url %q in account %q: %v", acc.IncomingWebhook.URL, accName, err)
1378 }
1379 }
1380
1381 // 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.
1382 replaceLocalparts := map[string]string{}
1383
1384 for addrName, dest := range acc.Destinations {
1385 checkMailboxNormf(dest.Mailbox, "account %q, destination %q", accName, addrName)
1386
1387 for i, rs := range dest.Rulesets {
1388 checkMailboxNormf(rs.Mailbox, "account %q, destination %q, ruleset %d", accName, addrName, i+1)
1389
1390 n := 0
1391
1392 if rs.SMTPMailFromRegexp != "" {
1393 n++
1394 r, err := regexp.Compile(rs.SMTPMailFromRegexp)
1395 if err != nil {
1396 addErrorf("invalid SMTPMailFrom regular expression: %v", err)
1397 }
1398 c.Accounts[accName].Destinations[addrName].Rulesets[i].SMTPMailFromRegexpCompiled = r
1399 }
1400 if rs.MsgFromRegexp != "" {
1401 n++
1402 r, err := regexp.Compile(rs.MsgFromRegexp)
1403 if err != nil {
1404 addErrorf("invalid MsgFrom regular expression: %v", err)
1405 }
1406 c.Accounts[accName].Destinations[addrName].Rulesets[i].MsgFromRegexpCompiled = r
1407 }
1408 if rs.VerifiedDomain != "" {
1409 n++
1410 d, err := dns.ParseDomain(rs.VerifiedDomain)
1411 if err != nil {
1412 addErrorf("invalid VerifiedDomain: %v", err)
1413 }
1414 c.Accounts[accName].Destinations[addrName].Rulesets[i].VerifiedDNSDomain = d
1415 }
1416
1417 var hdr [][2]*regexp.Regexp
1418 for k, v := range rs.HeadersRegexp {
1419 n++
1420 if strings.ToLower(k) != k {
1421 addErrorf("header field %q must only have lower case characters", k)
1422 }
1423 if strings.ToLower(v) != v {
1424 addErrorf("header value %q must only have lower case characters", v)
1425 }
1426 rk, err := regexp.Compile(k)
1427 if err != nil {
1428 addErrorf("invalid rule header regexp %q: %v", k, err)
1429 }
1430 rv, err := regexp.Compile(v)
1431 if err != nil {
1432 addErrorf("invalid rule header regexp %q: %v", v, err)
1433 }
1434 hdr = append(hdr, [...]*regexp.Regexp{rk, rv})
1435 }
1436 c.Accounts[accName].Destinations[addrName].Rulesets[i].HeadersRegexpCompiled = hdr
1437
1438 if n == 0 {
1439 addErrorf("ruleset must have at least one rule")
1440 }
1441
1442 if rs.IsForward && rs.ListAllowDomain != "" {
1443 addErrorf("ruleset cannot have both IsForward and ListAllowDomain")
1444 }
1445 if rs.IsForward {
1446 if rs.SMTPMailFromRegexp == "" || rs.VerifiedDomain == "" {
1447 addErrorf("ruleset with IsForward must have both SMTPMailFromRegexp and VerifiedDomain too")
1448 }
1449 }
1450 if rs.ListAllowDomain != "" {
1451 d, err := dns.ParseDomain(rs.ListAllowDomain)
1452 if err != nil {
1453 addErrorf("invalid ListAllowDomain %q: %v", rs.ListAllowDomain, err)
1454 }
1455 c.Accounts[accName].Destinations[addrName].Rulesets[i].ListAllowDNSDomain = d
1456 }
1457
1458 checkMailboxNormf(rs.AcceptRejectsToMailbox, "account %q, destination %q, ruleset %d, rejects mailbox", accName, addrName, i+1)
1459 if strings.EqualFold(rs.AcceptRejectsToMailbox, "inbox") {
1460 addErrorf("account %q, destination %q, ruleset %d: AcceptRejectsToMailbox cannot be set to Inbox", accName, addrName, i+1)
1461 }
1462 }
1463
1464 // Catchall destination for domain.
1465 if strings.HasPrefix(addrName, "@") {
1466 d, err := dns.ParseDomain(addrName[1:])
1467 if err != nil {
1468 addErrorf("parsing domain %q in account %q", addrName[1:], accName)
1469 continue
1470 } else if _, ok := c.Domains[d.Name()]; !ok {
1471 addErrorf("unknown domain for address %q in account %q", addrName, accName)
1472 continue
1473 }
1474 domainHasAddress[d.Name()] = true
1475 addrFull := "@" + d.Name()
1476 if _, ok := accDests[addrFull]; ok {
1477 addErrorf("duplicate canonicalized catchall destination address %s", addrFull)
1478 }
1479 accDests[addrFull] = AccountDestination{true, "", accName, dest}
1480 continue
1481 }
1482
1483 // todo deprecated: remove support for parsing destination as just a localpart instead full address.
1484 var address smtp.Address
1485 if localpart, err := smtp.ParseLocalpart(addrName); err != nil && errors.Is(err, smtp.ErrBadLocalpart) {
1486 address, err = smtp.ParseAddress(addrName)
1487 if err != nil {
1488 addErrorf("invalid email address %q in account %q", addrName, accName)
1489 continue
1490 } else if _, ok := c.Domains[address.Domain.Name()]; !ok {
1491 addErrorf("unknown domain for address %q in account %q", addrName, accName)
1492 continue
1493 }
1494 } else {
1495 if err != nil {
1496 addErrorf("invalid localpart %q in account %q", addrName, accName)
1497 continue
1498 }
1499 address = smtp.NewAddress(localpart, acc.DNSDomain)
1500 if _, ok := c.Domains[acc.DNSDomain.Name()]; !ok {
1501 addErrorf("unknown domain %s for account %q", acc.DNSDomain.Name(), accName)
1502 continue
1503 }
1504 replaceLocalparts[addrName] = address.Pack(true)
1505 }
1506
1507 origLP := address.Localpart
1508 dc := c.Domains[address.Domain.Name()]
1509 domainHasAddress[address.Domain.Name()] = true
1510 lp := CanonicalLocalpart(address.Localpart, dc)
1511 if dc.LocalpartCatchallSeparator != "" && strings.Contains(string(address.Localpart), dc.LocalpartCatchallSeparator) {
1512 addErrorf("localpart of address %s includes domain catchall separator %s", address, dc.LocalpartCatchallSeparator)
1513 } else {
1514 address.Localpart = lp
1515 }
1516 addrFull := address.Pack(true)
1517 if _, ok := accDests[addrFull]; ok {
1518 addErrorf("duplicate canonicalized destination address %s", addrFull)
1519 }
1520 accDests[addrFull] = AccountDestination{false, origLP, accName, dest}
1521 }
1522
1523 for lp, addr := range replaceLocalparts {
1524 dest, ok := acc.Destinations[lp]
1525 if !ok {
1526 addErrorf("could not find localpart %q to replace with address in destinations", lp)
1527 } else {
1528 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`,
1529 slog.Any("localpart", lp),
1530 slog.Any("address", addr),
1531 slog.String("account", accName))
1532 acc.Destinations[addr] = dest
1533 delete(acc.Destinations, lp)
1534 }
1535 }
1536
1537 // Now that all addresses are parsed, check if all fromid login addresses match
1538 // configured addresses.
1539 for i, a := range acc.ParsedFromIDLoginAddresses {
1540 // For domain catchall.
1541 if _, ok := accDests["@"+a.Domain.Name()]; ok {
1542 continue
1543 }
1544 dc := c.Domains[a.Domain.Name()]
1545 a.Localpart = CanonicalLocalpart(a.Localpart, dc)
1546 if _, ok := accDests[a.Pack(true)]; !ok {
1547 addErrorf("fromid login address %q for account %q does not match its destination addresses", acc.FromIDLoginAddresses[i], accName)
1548 }
1549 }
1550
1551 checkRoutes("routes for account", acc.Routes)
1552 }
1553
1554 // Set DMARC destinations.
1555 for d, domain := range c.Domains {
1556 dmarc := domain.DMARC
1557 if dmarc == nil {
1558 continue
1559 }
1560 if _, ok := c.Accounts[dmarc.Account]; !ok {
1561 addErrorf("DMARC account %q does not exist", dmarc.Account)
1562 }
1563 lp, err := smtp.ParseLocalpart(dmarc.Localpart)
1564 if err != nil {
1565 addErrorf("invalid DMARC localpart %q: %s", dmarc.Localpart, err)
1566 }
1567 if lp.IsInternational() {
1568 // ../rfc/8616:234
1569 addErrorf("DMARC localpart %q is an internationalized address, only conventional ascii-only address possible for interopability", lp)
1570 }
1571 addrdom := domain.Domain
1572 if dmarc.Domain != "" {
1573 addrdom, err = dns.ParseDomain(dmarc.Domain)
1574 if err != nil {
1575 addErrorf("DMARC domain %q: %s", dmarc.Domain, err)
1576 } else if _, ok := c.Domains[addrdom.Name()]; !ok {
1577 addErrorf("unknown domain %q for DMARC address in domain %q", addrdom, d)
1578 }
1579 }
1580 if addrdom == domain.Domain {
1581 domainHasAddress[addrdom.Name()] = true
1582 }
1583
1584 domain.DMARC.ParsedLocalpart = lp
1585 domain.DMARC.DNSDomain = addrdom
1586 c.Domains[d] = domain
1587 addrFull := smtp.NewAddress(lp, addrdom).String()
1588 dest := config.Destination{
1589 Mailbox: dmarc.Mailbox,
1590 DMARCReports: true,
1591 }
1592 checkMailboxNormf(dmarc.Mailbox, "DMARC mailbox for account %q", dmarc.Account)
1593 accDests[addrFull] = AccountDestination{false, lp, dmarc.Account, dest}
1594 }
1595
1596 // Set TLSRPT destinations.
1597 for d, domain := range c.Domains {
1598 tlsrpt := domain.TLSRPT
1599 if tlsrpt == nil {
1600 continue
1601 }
1602 if _, ok := c.Accounts[tlsrpt.Account]; !ok {
1603 addErrorf("TLSRPT account %q does not exist", tlsrpt.Account)
1604 }
1605 lp, err := smtp.ParseLocalpart(tlsrpt.Localpart)
1606 if err != nil {
1607 addErrorf("invalid TLSRPT localpart %q: %s", tlsrpt.Localpart, err)
1608 }
1609 if lp.IsInternational() {
1610 // Does not appear documented in ../rfc/8460, but similar to DMARC it makes sense
1611 // to keep this ascii-only addresses.
1612 addErrorf("TLSRPT localpart %q is an internationalized address, only conventional ascii-only address allowed for interopability", lp)
1613 }
1614 addrdom := domain.Domain
1615 if tlsrpt.Domain != "" {
1616 addrdom, err = dns.ParseDomain(tlsrpt.Domain)
1617 if err != nil {
1618 addErrorf("TLSRPT domain %q: %s", tlsrpt.Domain, err)
1619 } else if _, ok := c.Domains[addrdom.Name()]; !ok {
1620 addErrorf("unknown domain %q for TLSRPT address in domain %q", tlsrpt.Domain, d)
1621 }
1622 }
1623 if addrdom == domain.Domain {
1624 domainHasAddress[addrdom.Name()] = true
1625 }
1626
1627 domain.TLSRPT.ParsedLocalpart = lp
1628 domain.TLSRPT.DNSDomain = addrdom
1629 c.Domains[d] = domain
1630 addrFull := smtp.NewAddress(lp, addrdom).String()
1631 dest := config.Destination{
1632 Mailbox: tlsrpt.Mailbox,
1633 DomainTLSReports: true,
1634 }
1635 checkMailboxNormf(tlsrpt.Mailbox, "TLSRPT mailbox for account %q", tlsrpt.Account)
1636 accDests[addrFull] = AccountDestination{false, lp, tlsrpt.Account, dest}
1637 }
1638
1639 // Set ReportsOnly for domains, based on whether we have seen addresses (possibly
1640 // from DMARC or TLS reporting).
1641 for d, domain := range c.Domains {
1642 domain.ReportsOnly = !domainHasAddress[domain.Domain.Name()]
1643 c.Domains[d] = domain
1644 }
1645
1646 // Aliases, per domain. Also add references to accounts.
1647 for d, domain := range c.Domains {
1648 for lpstr, a := range domain.Aliases {
1649 var err error
1650 a.LocalpartStr = lpstr
1651 var clp smtp.Localpart
1652 lp, err := smtp.ParseLocalpart(lpstr)
1653 if err != nil {
1654 addErrorf("domain %q: parsing localpart %q for alias: %v", d, lpstr, err)
1655 continue
1656 } else if domain.LocalpartCatchallSeparator != "" && strings.Contains(string(lp), domain.LocalpartCatchallSeparator) {
1657 addErrorf("domain %q: alias %q contains localpart catchall separator", d, a.LocalpartStr)
1658 continue
1659 } else {
1660 clp = CanonicalLocalpart(lp, domain)
1661 }
1662
1663 addr := smtp.NewAddress(clp, domain.Domain).Pack(true)
1664 if _, ok := aliases[addr]; ok {
1665 addErrorf("domain %q: duplicate alias address %q", d, addr)
1666 continue
1667 }
1668 if _, ok := accDests[addr]; ok {
1669 addErrorf("domain %q: alias %q already present as regular address", d, addr)
1670 continue
1671 }
1672 if len(a.Addresses) == 0 {
1673 // Not currently possible, Addresses isn't optional.
1674 addErrorf("domain %q: alias %q needs at least one destination address", d, addr)
1675 continue
1676 }
1677 a.ParsedAddresses = make([]config.AliasAddress, 0, len(a.Addresses))
1678 seen := map[string]bool{}
1679 for _, destAddr := range a.Addresses {
1680 da, err := smtp.ParseAddress(destAddr)
1681 if err != nil {
1682 addErrorf("domain %q: parsing destination address %q in alias %q: %v", d, destAddr, addr, err)
1683 continue
1684 }
1685 dastr := da.Pack(true)
1686 accDest, ok := accDests[dastr]
1687 if !ok {
1688 addErrorf("domain %q: alias %q references non-existent address %q", d, addr, destAddr)
1689 continue
1690 }
1691 if seen[dastr] {
1692 addErrorf("domain %q: alias %q has duplicate address %q", d, addr, destAddr)
1693 continue
1694 }
1695 seen[dastr] = true
1696 aa := config.AliasAddress{Address: da, AccountName: accDest.Account, Destination: accDest.Destination}
1697 a.ParsedAddresses = append(a.ParsedAddresses, aa)
1698 }
1699 a.Domain = domain.Domain
1700 c.Domains[d].Aliases[lpstr] = a
1701 aliases[addr] = a
1702
1703 for _, aa := range a.ParsedAddresses {
1704 acc := c.Accounts[aa.AccountName]
1705 var addrs []string
1706 if a.ListMembers {
1707 addrs = make([]string, len(a.ParsedAddresses))
1708 for i := range a.ParsedAddresses {
1709 addrs[i] = a.ParsedAddresses[i].Address.Pack(true)
1710 }
1711 }
1712 // Keep the non-sensitive fields.
1713 accAlias := config.Alias{
1714 PostPublic: a.PostPublic,
1715 ListMembers: a.ListMembers,
1716 AllowMsgFrom: a.AllowMsgFrom,
1717 LocalpartStr: a.LocalpartStr,
1718 Domain: a.Domain,
1719 }
1720 acc.Aliases = append(acc.Aliases, config.AddressAlias{SubscriptionAddress: aa.Address.Pack(true), Alias: accAlias, MemberAddresses: addrs})
1721 c.Accounts[aa.AccountName] = acc
1722 }
1723 }
1724 }
1725
1726 // Check webserver configs.
1727 if (len(c.WebDomainRedirects) > 0 || len(c.WebHandlers) > 0) && !haveWebserverListener {
1728 addErrorf("WebDomainRedirects or WebHandlers configured but no listener with WebserverHTTP or WebserverHTTPS enabled")
1729 }
1730
1731 c.WebDNSDomainRedirects = map[dns.Domain]dns.Domain{}
1732 for from, to := range c.WebDomainRedirects {
1733 fromdom, err := dns.ParseDomain(from)
1734 if err != nil {
1735 addErrorf("parsing domain for redirect %s: %v", from, err)
1736 }
1737 todom, err := dns.ParseDomain(to)
1738 if err != nil {
1739 addErrorf("parsing domain for redirect %s: %v", to, err)
1740 } else if fromdom == todom {
1741 addErrorf("will not redirect domain %s to itself", todom)
1742 }
1743 var zerodom dns.Domain
1744 if _, ok := c.WebDNSDomainRedirects[fromdom]; ok && fromdom != zerodom {
1745 addErrorf("duplicate redirect domain %s", from)
1746 }
1747 c.WebDNSDomainRedirects[fromdom] = todom
1748 }
1749
1750 for i := range c.WebHandlers {
1751 wh := &c.WebHandlers[i]
1752
1753 if wh.LogName == "" {
1754 wh.Name = fmt.Sprintf("%d", i)
1755 } else {
1756 wh.Name = wh.LogName
1757 }
1758
1759 dom, err := dns.ParseDomain(wh.Domain)
1760 if err != nil {
1761 addErrorf("webhandler %s %s: parsing domain: %v", wh.Domain, wh.PathRegexp, err)
1762 }
1763 wh.DNSDomain = dom
1764
1765 if !strings.HasPrefix(wh.PathRegexp, "^") {
1766 addErrorf("webhandler %s %s: path regexp must start with a ^", wh.Domain, wh.PathRegexp)
1767 }
1768 re, err := regexp.Compile(wh.PathRegexp)
1769 if err != nil {
1770 addErrorf("webhandler %s %s: compiling regexp: %v", wh.Domain, wh.PathRegexp, err)
1771 }
1772 wh.Path = re
1773
1774 var n int
1775 if wh.WebStatic != nil {
1776 n++
1777 ws := wh.WebStatic
1778 if ws.StripPrefix != "" && !strings.HasPrefix(ws.StripPrefix, "/") {
1779 addErrorf("webstatic %s %s: prefix to strip %s must start with a slash", wh.Domain, wh.PathRegexp, ws.StripPrefix)
1780 }
1781 for k := range ws.ResponseHeaders {
1782 xk := k
1783 k := strings.TrimSpace(xk)
1784 if k != xk || k == "" {
1785 addErrorf("webstatic %s %s: bad header %q", wh.Domain, wh.PathRegexp, xk)
1786 }
1787 }
1788 }
1789 if wh.WebRedirect != nil {
1790 n++
1791 wr := wh.WebRedirect
1792 if wr.BaseURL != "" {
1793 u, err := url.Parse(wr.BaseURL)
1794 if err != nil {
1795 addErrorf("webredirect %s %s: parsing redirect url %s: %v", wh.Domain, wh.PathRegexp, wr.BaseURL, err)
1796 }
1797 switch u.Path {
1798 case "", "/":
1799 u.Path = "/"
1800 default:
1801 addErrorf("webredirect %s %s: BaseURL must have empty path", wh.Domain, wh.PathRegexp, wr.BaseURL)
1802 }
1803 wr.URL = u
1804 }
1805 if wr.OrigPathRegexp != "" && wr.ReplacePath != "" {
1806 re, err := regexp.Compile(wr.OrigPathRegexp)
1807 if err != nil {
1808 addErrorf("webredirect %s %s: compiling regexp %s: %v", wh.Domain, wh.PathRegexp, wr.OrigPathRegexp, err)
1809 }
1810 wr.OrigPath = re
1811 } else if wr.OrigPathRegexp != "" || wr.ReplacePath != "" {
1812 addErrorf("webredirect %s %s: must have either both OrigPathRegexp and ReplacePath, or neither", wh.Domain, wh.PathRegexp)
1813 } else if wr.BaseURL == "" {
1814 addErrorf("webredirect %s %s: must at least one of BaseURL and OrigPathRegexp+ReplacePath", wh.Domain, wh.PathRegexp)
1815 }
1816 if wr.StatusCode != 0 && (wr.StatusCode < 300 || wr.StatusCode >= 400) {
1817 addErrorf("webredirect %s %s: invalid redirect status code %d", wh.Domain, wh.PathRegexp, wr.StatusCode)
1818 }
1819 }
1820 if wh.WebForward != nil {
1821 n++
1822 wf := wh.WebForward
1823 u, err := url.Parse(wf.URL)
1824 if err != nil {
1825 addErrorf("webforward %s %s: parsing url %s: %v", wh.Domain, wh.PathRegexp, wf.URL, err)
1826 }
1827 wf.TargetURL = u
1828
1829 for k := range wf.ResponseHeaders {
1830 xk := k
1831 k := strings.TrimSpace(xk)
1832 if k != xk || k == "" {
1833 addErrorf("webforward %s %s: bad header %q", wh.Domain, wh.PathRegexp, xk)
1834 }
1835 }
1836 }
1837 if wh.WebInternal != nil {
1838 n++
1839 wi := wh.WebInternal
1840 if !strings.HasPrefix(wi.BasePath, "/") || !strings.HasSuffix(wi.BasePath, "/") {
1841 addErrorf("webinternal %s %s: base path %q must start and end with /", wh.Domain, wh.PathRegexp, wi.BasePath)
1842 }
1843 // todo: we could make maxMsgSize and accountPath configurable
1844 const isForwarded = false
1845 switch wi.Service {
1846 case "admin":
1847 wi.Handler = NewWebadminHandler(wi.BasePath, isForwarded)
1848 case "account":
1849 wi.Handler = NewWebaccountHandler(wi.BasePath, isForwarded)
1850 case "webmail":
1851 accountPath := ""
1852 wi.Handler = NewWebmailHandler(config.DefaultMaxMsgSize, wi.BasePath, isForwarded, accountPath)
1853 case "webapi":
1854 wi.Handler = NewWebapiHandler(config.DefaultMaxMsgSize, wi.BasePath, isForwarded)
1855 default:
1856 addErrorf("webinternal %s %s: unknown service %q", wh.Domain, wh.PathRegexp, wi.Service)
1857 }
1858 wi.Handler = SafeHeaders(http.StripPrefix(wi.BasePath[:len(wi.BasePath)-1], wi.Handler))
1859 }
1860 if n != 1 {
1861 addErrorf("webhandler %s %s: must have exactly one handler, not %d", wh.Domain, wh.PathRegexp, n)
1862 }
1863 }
1864
1865 c.MonitorDNSBLZones = nil
1866 for _, s := range c.MonitorDNSBLs {
1867 d, err := dns.ParseDomain(s)
1868 if err != nil {
1869 addErrorf("invalid monitor dnsbl zone %s: %v", s, err)
1870 continue
1871 }
1872 if slices.Contains(c.MonitorDNSBLZones, d) {
1873 addErrorf("duplicate zone %s in monitor dnsbl zones", d)
1874 continue
1875 }
1876 c.MonitorDNSBLZones = append(c.MonitorDNSBLZones, d)
1877 }
1878
1879 return
1880}
1881
1882func loadPrivateKeyFile(keyPath string) (crypto.Signer, error) {
1883 keyBuf, err := os.ReadFile(keyPath)
1884 if err != nil {
1885 return nil, fmt.Errorf("reading host private key: %v", err)
1886 }
1887 b, _ := pem.Decode(keyBuf)
1888 if b == nil {
1889 return nil, fmt.Errorf("parsing pem block for private key: %v", err)
1890 }
1891 var privKey any
1892 switch b.Type {
1893 case "PRIVATE KEY":
1894 privKey, err = x509.ParsePKCS8PrivateKey(b.Bytes)
1895 case "RSA PRIVATE KEY":
1896 privKey, err = x509.ParsePKCS1PrivateKey(b.Bytes)
1897 case "EC PRIVATE KEY":
1898 privKey, err = x509.ParseECPrivateKey(b.Bytes)
1899 default:
1900 err = fmt.Errorf("unknown pem type %q", b.Type)
1901 }
1902 if err != nil {
1903 return nil, fmt.Errorf("parsing private key: %v", err)
1904 }
1905 if k, ok := privKey.(crypto.Signer); ok {
1906 return k, nil
1907 }
1908 return nil, fmt.Errorf("parsed private key not a crypto.Signer, but %T", privKey)
1909}
1910
1911func loadTLSKeyCerts(configFile, kind string, ctls *config.TLS) error {
1912 certs := []tls.Certificate{}
1913 for _, kp := range ctls.KeyCerts {
1914 certPath := configDirPath(configFile, kp.CertFile)
1915 keyPath := configDirPath(configFile, kp.KeyFile)
1916 cert, err := loadX509KeyPairPrivileged(certPath, keyPath)
1917 if err != nil {
1918 return fmt.Errorf("tls config for %q: parsing x509 key pair: %v", kind, err)
1919 }
1920 certs = append(certs, cert)
1921 }
1922 ctls.Config = &tls.Config{
1923 Certificates: certs,
1924 SessionTicketsDisabled: true,
1925 }
1926 ctls.ConfigFallback = ctls.Config
1927 return nil
1928}
1929
1930// load x509 key/cert files from file descriptor possibly passed in by privileged
1931// process.
1932func loadX509KeyPairPrivileged(certPath, keyPath string) (tls.Certificate, error) {
1933 certBuf, err := readFilePrivileged(certPath)
1934 if err != nil {
1935 return tls.Certificate{}, fmt.Errorf("reading tls certificate: %v", err)
1936 }
1937 keyBuf, err := readFilePrivileged(keyPath)
1938 if err != nil {
1939 return tls.Certificate{}, fmt.Errorf("reading tls key: %v", err)
1940 }
1941 return tls.X509KeyPair(certBuf, keyBuf)
1942}
1943
1944// like os.ReadFile, but open privileged file possibly passed in by root process.
1945func readFilePrivileged(path string) ([]byte, error) {
1946 f, err := OpenPrivileged(path)
1947 if err != nil {
1948 return nil, err
1949 }
1950 defer f.Close()
1951 return io.ReadAll(f)
1952}
1953