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