package mox

import (
	"bytes"
	"context"
	"crypto/ed25519"
	"crypto/rsa"
	"crypto/tls"
	"crypto/x509"
	"encoding/pem"
	"errors"
	"fmt"
	"net"
	"os"
	"path/filepath"
	"regexp"
	"sort"
	"strings"
	"sync"
	"time"

	"golang.org/x/text/unicode/norm"

	"github.com/mjl-/sconf"

	"github.com/mjl-/mox/autotls"
	"github.com/mjl-/mox/config"
	"github.com/mjl-/mox/dns"
	"github.com/mjl-/mox/mlog"
	"github.com/mjl-/mox/moxio"
	"github.com/mjl-/mox/mtasts"
	"github.com/mjl-/mox/smtp"
)

var xlog = mlog.New("mox")

// Config paths are set early in program startup. They will point to files in
// the same directory.
var (
	ConfigStaticPath  string
	ConfigDynamicPath string
	Conf              = Config{Log: map[string]mlog.Level{"": mlog.LevelError}}
)

// Config as used in the code, a processed version of what is in the config file.
//
// Use methods to lookup a domain/account/address in the dynamic configuration.
type Config struct {
	Static config.Static // Does not change during the lifetime of a running instance.

	logMutex sync.Mutex // For accessing the log levels.
	Log      map[string]mlog.Level

	dynamicMutex     sync.Mutex
	Dynamic          config.Dynamic // Can only be accessed directly by tests. Use methods on Config for locked access.
	dynamicMtime     time.Time
	DynamicLastCheck time.Time // For use by quickstart only to skip checks.
	// From correctly-cased full address (localpart@domain) to account and
	// address. Domains are IDNA names in utf8.
	accountDestinations map[string]AccountDestination
}

type AccountDestination struct {
	Localpart   smtp.Localpart
	Account     string
	Destination config.Destination
}

// LogLevelSet sets a new log level for pkg. An empty pkg sets the default log
// value that is used if no explicit log level is configured for a package.
// This change is ephemeral, no config file is changed.
func (c *Config) LogLevelSet(pkg string, level mlog.Level) {
	c.logMutex.Lock()
	defer c.logMutex.Unlock()
	l := c.copyLogLevels()
	l[pkg] = level
	c.Log = l
	xlog.Print("log level changed", mlog.Field("pkg", pkg), mlog.Field("level", mlog.LevelStrings[level]))
	mlog.SetConfig(c.Log)
}

// LogLevelRemove removes a configured log level for a package.
func (c *Config) LogLevelRemove(pkg string) {
	c.logMutex.Lock()
	defer c.logMutex.Unlock()
	l := c.copyLogLevels()
	delete(l, pkg)
	c.Log = l
	xlog.Print("log level cleared", mlog.Field("pkg", pkg))
	mlog.SetConfig(c.Log)
}

// copyLogLevels returns a copy of c.Log, for modifications.
// must be called with log lock held.
func (c *Config) copyLogLevels() map[string]mlog.Level {
	m := map[string]mlog.Level{}
	for pkg, level := range c.Log {
		m[pkg] = level
	}
	return m
}

// LogLevels returns a copy of the current log levels.
func (c *Config) LogLevels() map[string]mlog.Level {
	c.logMutex.Lock()
	defer c.logMutex.Unlock()
	return c.copyLogLevels()
}

func (c *Config) withDynamicLock(fn func()) {
	c.dynamicMutex.Lock()
	defer c.dynamicMutex.Unlock()
	now := time.Now()
	if now.Sub(c.DynamicLastCheck) > time.Second {
		c.DynamicLastCheck = now
		if fi, err := os.Stat(ConfigDynamicPath); err != nil {
			xlog.Errorx("stat domains config", err)
		} else if !fi.ModTime().Equal(c.dynamicMtime) {
			if errs := c.loadDynamic(); len(errs) > 0 {
				xlog.Errorx("loading domains config", errs[0], mlog.Field("errors", errs))
			} else {
				xlog.Info("domains config reloaded")
				c.dynamicMtime = fi.ModTime()
			}
		}
	}
	fn()
}

// must be called with dynamic lock held.
func (c *Config) loadDynamic() []error {
	d, mtime, accDests, err := ParseDynamicConfig(context.Background(), ConfigDynamicPath, c.Static)
	if err != nil {
		return err
	}
	c.Dynamic = d
	c.dynamicMtime = mtime
	c.accountDestinations = accDests
	return nil
}

func (c *Config) Domains() (l []string) {
	c.withDynamicLock(func() {
		for name := range c.Dynamic.Domains {
			l = append(l, name)
		}
	})
	sort.Slice(l, func(i, j int) bool {
		return l[i] < l[j]
	})
	return l
}

func (c *Config) Accounts() (l []string) {
	c.withDynamicLock(func() {
		for name := range c.Dynamic.Accounts {
			l = append(l, name)
		}
	})
	return
}

func (c *Config) DomainLocalparts(d dns.Domain) map[smtp.Localpart]string {
	suffix := "@" + d.Name()
	m := map[smtp.Localpart]string{}
	c.withDynamicLock(func() {
		for addr, ad := range c.accountDestinations {
			if strings.HasSuffix(addr, suffix) {
				m[ad.Localpart] = ad.Account
			}
		}
	})
	return m
}

func (c *Config) Domain(d dns.Domain) (dom config.Domain, ok bool) {
	c.withDynamicLock(func() {
		dom, ok = c.Dynamic.Domains[d.Name()]
	})
	return
}

func (c *Config) Account(name string) (acc config.Account, ok bool) {
	c.withDynamicLock(func() {
		acc, ok = c.Dynamic.Accounts[name]
	})
	return
}

func (c *Config) AccountDestination(addr string) (accDests AccountDestination, ok bool) {
	c.withDynamicLock(func() {
		accDests, ok = c.accountDestinations[addr]
	})
	return
}

func (c *Config) allowACMEHosts() {
	// todo future: reset the allowed hosts for autoconfig & mtasts when loading new list.
	for _, l := range c.Static.Listeners {
		if l.TLS == nil || l.TLS.ACME == "" {
			continue
		}
		m := c.Static.ACME[l.TLS.ACME].Manager
		for _, dom := range c.Dynamic.Domains {

			if l.AutoconfigHTTPS.Enabled {
				if d, err := dns.ParseDomain("autoconfig." + dom.Domain.ASCII); err != nil {
					xlog.Errorx("parsing autoconfig domain", err, mlog.Field("domain", dom.Domain))
				} else {
					m.AllowHostname(d)
				}

				if d, err := dns.ParseDomain("autodiscover." + dom.Domain.ASCII); err != nil {
					xlog.Errorx("parsing autodiscover domain", err, mlog.Field("domain", dom.Domain))
				} else {
					m.AllowHostname(d)
				}
			}

			if l.MTASTSHTTPS.Enabled && dom.MTASTS != nil {
				d, err := dns.ParseDomain("mta-sts." + dom.Domain.ASCII)
				if err != nil {
					xlog.Errorx("parsing mta-sts domain", err, mlog.Field("domain", dom.Domain))
					continue
				}
				m.AllowHostname(d)
			}
		}
	}
}

// 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.

// must be called with lock held.
func writeDynamic(ctx context.Context, log *mlog.Log, c config.Dynamic) error {
	accDests, errs := prepareDynamicConfig(ctx, ConfigDynamicPath, Conf.Static, &c)
	if len(errs) > 0 {
		return errs[0]
	}

	var b bytes.Buffer
	err := sconf.Write(&b, c)
	if err != nil {
		return err
	}
	f, err := os.OpenFile(ConfigDynamicPath, os.O_WRONLY, 0660)
	if err != nil {
		return err
	}
	defer func() {
		if f != nil {
			err := f.Close()
			log.Check(err, "closing file after error")
		}
	}()
	buf := b.Bytes()
	if _, err := f.Write(buf); err != nil {
		return fmt.Errorf("write domains.conf: %v", err)
	}
	if err := f.Truncate(int64(len(buf))); err != nil {
		return fmt.Errorf("truncate domains.conf after write: %v", err)
	}
	if err := f.Sync(); err != nil {
		return fmt.Errorf("sync domains.conf after write: %v", err)
	}
	if err := moxio.SyncDir(filepath.Dir(ConfigDynamicPath)); err != nil {
		return fmt.Errorf("sync dir of domains.conf after write: %v", err)
	}

	fi, err := f.Stat()
	if err != nil {
		return fmt.Errorf("stat after writing domains.conf: %v", err)
	}

	if err := f.Close(); err != nil {
		return fmt.Errorf("close written domains.conf: %v", err)
	}
	f = nil

	Conf.dynamicMtime = fi.ModTime()
	Conf.DynamicLastCheck = time.Now()
	Conf.Dynamic = c
	Conf.accountDestinations = accDests

	Conf.allowACMEHosts()

	return nil
}

// MustLoadConfig loads the config, quitting on errors.
func MustLoadConfig() {
	Shutdown, ShutdownCancel = context.WithCancel(context.Background())
	Context, ContextCancel = context.WithCancel(context.Background())

	errs := LoadConfig(context.Background())
	if len(errs) > 1 {
		xlog.Error("loading config file: multiple errors")
		for _, err := range errs {
			xlog.Errorx("config error", err)
		}
		xlog.Fatal("stopping after multiple config errors")
	} else if len(errs) == 1 {
		xlog.Fatalx("loading config file", errs[0])
	}
}

// LoadConfig attempts to parse and load a config, returning any errors
// encountered.
func LoadConfig(ctx context.Context) []error {
	c, errs := ParseConfig(ctx, ConfigStaticPath, false)
	if len(errs) > 0 {
		return errs
	}

	mlog.SetConfig(c.Log)
	SetConfig(c)
	return nil
}

// SetConfig sets a new config. Not to be used during normal operation.
func SetConfig(c *Config) {
	// Cannot just assign *c to Conf, it would copy the mutex.
	Conf = Config{c.Static, sync.Mutex{}, c.Log, sync.Mutex{}, c.Dynamic, c.dynamicMtime, c.DynamicLastCheck, c.accountDestinations}
}

// ParseConfig parses the static config at path p. If checkOnly is true, no
// changes are made, such as registering ACME identities.
func ParseConfig(ctx context.Context, p string, checkOnly bool) (c *Config, errs []error) {
	c = &Config{
		Static: config.Static{
			DataDir: ".",
		},
	}

	f, err := os.Open(p)
	if err != nil {
		if os.IsNotExist(err) && os.Getenv("MOXCONF") == "" {
			return nil, []error{fmt.Errorf("open config file: %v (hint: use mox -config ... or set MOXCONF=...)", err)}
		}
		return nil, []error{fmt.Errorf("open config file: %v", err)}
	}
	defer f.Close()
	if err := sconf.Parse(f, &c.Static); err != nil {
		return nil, []error{fmt.Errorf("parsing %s: %v", p, err)}
	}

	if xerrs := PrepareStaticConfig(ctx, p, c, checkOnly); len(xerrs) > 0 {
		return nil, xerrs
	}

	pp := filepath.Join(filepath.Dir(p), "domains.conf")
	c.Dynamic, c.dynamicMtime, c.accountDestinations, errs = ParseDynamicConfig(ctx, pp, c.Static)

	if !checkOnly {
		c.allowACMEHosts()
	}

	return c, errs
}

// PrepareStaticConfig parses the static config file and prepares data structures
// for starting mox. If checkOnly is set no substantial changes are made, like
// creating an ACME registration.
func PrepareStaticConfig(ctx context.Context, configFile string, config *Config, checkOnly bool) (errs []error) {
	addErrorf := func(format string, args ...any) {
		errs = append(errs, fmt.Errorf(format, args...))
	}

	c := &config.Static

	// check that mailbox is in unicode NFC normalized form.
	checkMailboxNormf := func(mailbox string, format string, args ...any) {
		s := norm.NFC.String(mailbox)
		if mailbox != s {
			msg := fmt.Sprintf(format, args...)
			addErrorf("%s: mailbox %q is not in NFC normalized form, should be %q", msg, mailbox, s)
		}
	}

	// Post-process logging config.
	if logLevel, ok := mlog.Levels[c.LogLevel]; ok {
		config.Log = map[string]mlog.Level{"": logLevel}
	} else {
		addErrorf("invalid log level %q", c.LogLevel)
	}
	for pkg, s := range c.PackageLogLevels {
		if logLevel, ok := mlog.Levels[s]; ok {
			config.Log[pkg] = logLevel
		} else {
			addErrorf("invalid package log level %q", s)
		}
	}

	hostname, err := dns.ParseDomain(c.Hostname)
	if err != nil {
		addErrorf("parsing hostname: %s", err)
	} else if hostname.Name() != c.Hostname {
		addErrorf("hostname must be in IDNA form %q", hostname.Name())
	}
	c.HostnameDomain = hostname

	for name, acme := range c.ACME {
		if checkOnly {
			continue
		}
		acmeDir := dataDirPath(configFile, c.DataDir, "acme")
		os.MkdirAll(acmeDir, 0770)
		manager, err := autotls.Load(name, acmeDir, acme.ContactEmail, acme.DirectoryURL, Shutdown.Done())
		if err != nil {
			addErrorf("loading ACME identity for %q: %s", name, err)
		}
		acme.Manager = manager
		c.ACME[name] = acme
	}

	var haveUnspecifiedSMTPListener bool
	for name, l := range c.Listeners {
		if l.Hostname != "" {
			d, err := dns.ParseDomain(l.Hostname)
			if err != nil {
				addErrorf("bad listener hostname %q: %s", l.Hostname, err)
			}
			l.HostnameDomain = d
		}
		if l.TLS != nil {
			if l.TLS.ACME != "" && len(l.TLS.KeyCerts) != 0 {
				addErrorf("listener %q: cannot have ACME and static key/certificates", name)
			} else if l.TLS.ACME != "" {
				acme, ok := c.ACME[l.TLS.ACME]
				if !ok {
					addErrorf("listener %q: unknown ACME provider %q", name, l.TLS.ACME)
				}

				// If only checking, we don't have an acme manager, so set an empty tls config to
				// continue without errors.
				var tlsconfig *tls.Config
				if checkOnly {
					tlsconfig = &tls.Config{}
				} else {
					tlsconfig = acme.Manager.TLSConfig.Clone()
					l.TLS.ACMEConfig = acme.Manager.ACMETLSConfig

					// SMTP STARTTLS connections are commonly made without SNI, because certificates
					// often aren't validated.
					hostname := c.HostnameDomain
					if l.Hostname != "" {
						hostname = l.HostnameDomain
					}
					getCert := tlsconfig.GetCertificate
					tlsconfig.GetCertificate = func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
						if hello.ServerName == "" {
							hello.ServerName = hostname.ASCII
						}
						return getCert(hello)
					}
				}
				l.TLS.Config = tlsconfig
			} else if len(l.TLS.KeyCerts) != 0 {
				if err := loadTLSKeyCerts(configFile, "listener "+name, l.TLS); err != nil {
					addErrorf("%w", err)
				}
			} else {
				addErrorf("listener %q: cannot have TLS config without ACME and without static keys/certificates", name)
			}

			// TLS 1.2 was introduced in 2008. TLS <1.2 was deprecated by ../rfc/8996:31 and ../rfc/8997:66 in 2021.
			var minVersion uint16 = tls.VersionTLS12
			if l.TLS.MinVersion != "" {
				versions := map[string]uint16{
					"TLSv1.0": tls.VersionTLS10,
					"TLSv1.1": tls.VersionTLS11,
					"TLSv1.2": tls.VersionTLS12,
					"TLSv1.3": tls.VersionTLS13,
				}
				v, ok := versions[l.TLS.MinVersion]
				if !ok {
					addErrorf("listener %q: unknown TLS mininum version %q", name, l.TLS.MinVersion)
				}
				minVersion = v
			}
			if l.TLS.Config != nil {
				l.TLS.Config.MinVersion = minVersion
			}
			if l.TLS.ACMEConfig != nil {
				l.TLS.ACMEConfig.MinVersion = minVersion
			}
		} else if l.IMAPS.Enabled || l.SMTP.Enabled && !l.SMTP.NoSTARTTLS || l.Submissions.Enabled || l.Submission.Enabled && !l.Submission.NoRequireSTARTTLS || l.AccountHTTPS.Enabled || l.AdminHTTPS.Enabled || l.AutoconfigHTTPS.Enabled || l.MTASTSHTTPS.Enabled {
			addErrorf("listener %q requires TLS, but does not specify tls config", name)
		}
		if l.AutoconfigHTTPS.Enabled && (!l.IMAP.Enabled && !l.IMAPS.Enabled || !l.Submission.Enabled && !l.Submissions.Enabled) {
			addErrorf("listener %q with autoconfig enabled must have SMTP submission or submissions and IMAP or IMAPS enabled", name)
		}
		if l.SMTP.Enabled {
			if len(l.IPs) == 0 {
				haveUnspecifiedSMTPListener = true
			}
			for _, ipstr := range l.IPs {
				ip := net.ParseIP(ipstr)
				if ip == nil {
					addErrorf("listener %q has invalid IP %q", name, ipstr)
					continue
				}
				if ip.IsUnspecified() {
					haveUnspecifiedSMTPListener = true
					break
				}
				if len(c.SpecifiedSMTPListenIPs) >= 2 {
					haveUnspecifiedSMTPListener = true
				} else if len(c.SpecifiedSMTPListenIPs) > 0 && (c.SpecifiedSMTPListenIPs[0].To4() == nil) == (ip.To4() == nil) {
					haveUnspecifiedSMTPListener = true
				} else {
					c.SpecifiedSMTPListenIPs = append(c.SpecifiedSMTPListenIPs, ip)
				}
			}
		}
		for _, s := range l.SMTP.DNSBLs {
			d, err := dns.ParseDomain(s)
			if err != nil {
				addErrorf("listener %q has invalid DNSBL zone %q", name, s)
				continue
			}
			l.SMTP.DNSBLZones = append(l.SMTP.DNSBLZones, d)
		}
		c.Listeners[name] = l
	}
	if haveUnspecifiedSMTPListener {
		c.SpecifiedSMTPListenIPs = nil
	}

	for _, mb := range c.DefaultMailboxes {
		checkMailboxNormf(mb, "default mailbox")
	}

	// Load CA certificate pool.
	if c.TLS.CA != nil {
		if c.TLS.CA.AdditionalToSystem {
			var err error
			c.TLS.CertPool, err = x509.SystemCertPool()
			if err != nil {
				addErrorf("fetching system CA cert pool: %v", err)
			}
		} else {
			c.TLS.CertPool = x509.NewCertPool()
		}
		for _, certfile := range c.TLS.CA.CertFiles {
			p := configDirPath(configFile, certfile)
			pemBuf, err := os.ReadFile(p)
			if err != nil {
				addErrorf("reading TLS CA cert file: %v", err)
				continue
			} else if !c.TLS.CertPool.AppendCertsFromPEM(pemBuf) {
				// todo: can we check more fully if we're getting some useful data back?
				addErrorf("no CA certs added from %q", p)
			}
		}
	}
	return
}

// PrepareDynamicConfig parses the dynamic config file given a static file.
func ParseDynamicConfig(ctx context.Context, dynamicPath string, static config.Static) (c config.Dynamic, mtime time.Time, accDests map[string]AccountDestination, errs []error) {
	addErrorf := func(format string, args ...any) {
		errs = append(errs, fmt.Errorf(format, args...))
	}

	f, err := os.Open(dynamicPath)
	if err != nil {
		addErrorf("parsing domains config: %v", err)
		return
	}
	defer f.Close()
	fi, err := f.Stat()
	if err != nil {
		addErrorf("stat domains config: %v", err)
	}
	if err := sconf.Parse(f, &c); err != nil {
		addErrorf("parsing dynamic config file: %v", err)
		return
	}

	accDests, errs = prepareDynamicConfig(ctx, dynamicPath, static, &c)
	return c, fi.ModTime(), accDests, errs
}

func prepareDynamicConfig(ctx context.Context, dynamicPath string, static config.Static, c *config.Dynamic) (accDests map[string]AccountDestination, errs []error) {
	log := xlog.WithContext(ctx)

	addErrorf := func(format string, args ...any) {
		errs = append(errs, fmt.Errorf(format, args...))
	}

	// check that mailbox is in unicode NFC normalized form.
	checkMailboxNormf := func(mailbox string, format string, args ...any) {
		s := norm.NFC.String(mailbox)
		if mailbox != s {
			msg := fmt.Sprintf(format, args...)
			addErrorf("%s: mailbox %q is not in NFC normalized form, should be %q", msg, mailbox, s)
		}
	}

	// Validate postmaster account exists.
	if _, ok := c.Accounts[static.Postmaster.Account]; !ok {
		addErrorf("postmaster account %q does not exist", static.Postmaster.Account)
	}
	checkMailboxNormf(static.Postmaster.Mailbox, "postmaster mailbox")

	var haveSTSListener bool
	for _, l := range static.Listeners {
		if l.MTASTSHTTPS.Enabled {
			haveSTSListener = true
			break
		}
	}

	// Validate domains.
	for d, domain := range c.Domains {
		dnsdomain, err := dns.ParseDomain(d)
		if err != nil {
			addErrorf("bad domain %q: %s", d, err)
		} else if dnsdomain.Name() != d {
			addErrorf("domain %q must be specified in IDNA form, %q", d, dnsdomain.Name())
		}

		domain.Domain = dnsdomain

		for _, sign := range domain.DKIM.Sign {
			if _, ok := domain.DKIM.Selectors[sign]; !ok {
				addErrorf("selector %q for signing is missing in domain %q", sign, d)
			}
		}
		for name, sel := range domain.DKIM.Selectors {
			seld, err := dns.ParseDomain(name)
			if err != nil {
				addErrorf("bad selector %q: %s", name, err)
			} else if seld.Name() != name {
				addErrorf("selector %q must be specified in IDNA form, %q", name, seld.Name())
			}
			sel.Domain = seld

			if sel.Expiration != "" {
				exp, err := time.ParseDuration(sel.Expiration)
				if err != nil {
					addErrorf("selector %q has invalid expiration %q: %v", name, sel.Expiration, err)
				} else {
					sel.ExpirationSeconds = int(exp / time.Second)
				}
			}

			sel.HashEffective = sel.Hash
			switch sel.HashEffective {
			case "":
				sel.HashEffective = "sha256"
			case "sha1":
				log.Error("using sha1 with DKIM is deprecated as not secure enough, switch to sha256")
			case "sha256":
			default:
				addErrorf("unsupported hash %q for selector %q in domain %q", sel.HashEffective, name, d)
			}

			pemBuf, err := os.ReadFile(configDirPath(dynamicPath, sel.PrivateKeyFile))
			if err != nil {
				addErrorf("reading private key for selector %q in domain %q: %s", name, d, err)
				continue
			}
			p, _ := pem.Decode(pemBuf)
			if p == nil {
				addErrorf("private key for selector %q in domain %q has no PEM block", name, d)
				continue
			}
			key, err := x509.ParsePKCS8PrivateKey(p.Bytes)
			if err != nil {
				addErrorf("parsing private key for selector %q in domain %q: %s", name, d, err)
				continue
			}
			switch k := key.(type) {
			case *rsa.PrivateKey:
				if k.N.BitLen() < 1024 {
					// ../rfc/6376:757
					// Let's help user do the right thing.
					addErrorf("rsa keys should be >= 1024 bits")
				}
				sel.Key = k
			case ed25519.PrivateKey:
				if sel.HashEffective != "sha256" {
					addErrorf("hash algorithm %q is not supported with ed25519, only sha256 is", sel.HashEffective)
				}
				sel.Key = k
			default:
				addErrorf("private key type %T not yet supported, at selector %q in domain %q", key, name, d)
			}

			if len(sel.Headers) == 0 {
				// ../rfc/6376:2139
				// ../rfc/6376:2203
				// ../rfc/6376:2212
				// By default we seal signed headers, and we sign user-visible headers to
				// prevent/limit reuse of previously signed messages: All addressing fields, date
				// and subject, message-referencing fields, parsing instructions (content-type).
				sel.HeadersEffective = strings.Split("From,To,Cc,Bcc,Reply-To,References,In-Reply-To,Subject,Date,Message-Id,Content-Type", ",")
			} else {
				var from bool
				for _, h := range sel.Headers {
					from = from || strings.EqualFold(h, "From")
					// ../rfc/6376:2269
					if strings.EqualFold(h, "DKIM-Signature") || strings.EqualFold(h, "Received") || strings.EqualFold(h, "Return-Path") {
						log.Error("DKIM-signing header %q is recommended against as it may be modified in transit")
					}
				}
				if !from {
					addErrorf("From-field must always be DKIM-signed")
				}
				sel.HeadersEffective = sel.Headers
			}

			domain.DKIM.Selectors[name] = sel
		}

		if domain.MTASTS != nil {
			if !haveSTSListener {
				addErrorf("MTA-STS enabled for domain %q, but there is no listener for MTASTS", d)
			}
			sts := domain.MTASTS
			if sts.PolicyID == "" {
				addErrorf("invalid empty MTA-STS PolicyID")
			}
			switch sts.Mode {
			case mtasts.ModeNone, mtasts.ModeTesting, mtasts.ModeEnforce:
			default:
				addErrorf("invalid mtasts mode %q", sts.Mode)
			}
		}

		c.Domains[d] = domain
	}

	// Post-process email addresses for fast lookups.
	accDests = map[string]AccountDestination{}
	for accName, acc := range c.Accounts {
		var err error
		acc.DNSDomain, err = dns.ParseDomain(acc.Domain)
		if err != nil {
			addErrorf("parsing domain %q for account %q: %s", acc.Domain, accName, err)
		}

		if strings.EqualFold(acc.RejectsMailbox, "Inbox") {
			addErrorf("account %q: cannot set RejectsMailbox to inbox, messages will be removed automatically from the rejects mailbox", accName)
		}
		checkMailboxNormf(acc.RejectsMailbox, "account %q", accName)

		if acc.AutomaticJunkFlags.JunkMailboxRegexp != "" {
			r, err := regexp.Compile(acc.AutomaticJunkFlags.JunkMailboxRegexp)
			if err != nil {
				addErrorf("invalid JunkMailboxRegexp regular expression: %v", err)
			}
			acc.JunkMailbox = r
		}
		if acc.AutomaticJunkFlags.NeutralMailboxRegexp != "" {
			r, err := regexp.Compile(acc.AutomaticJunkFlags.NeutralMailboxRegexp)
			if err != nil {
				addErrorf("invalid NeutralMailboxRegexp regular expression: %v", err)
			}
			acc.NeutralMailbox = r
		}
		if acc.AutomaticJunkFlags.NotJunkMailboxRegexp != "" {
			r, err := regexp.Compile(acc.AutomaticJunkFlags.NotJunkMailboxRegexp)
			if err != nil {
				addErrorf("invalid NotJunkMailboxRegexp regular expression: %v", err)
			}
			acc.NotJunkMailbox = r
		}
		c.Accounts[accName] = acc

		for addrName, dest := range acc.Destinations {
			checkMailboxNormf(dest.Mailbox, "account %q, destination %q", accName, addrName)

			for i, rs := range dest.Rulesets {
				checkMailboxNormf(rs.Mailbox, "account %q, destination %q, ruleset %d", accName, addrName, i+1)

				n := 0

				if rs.SMTPMailFromRegexp != "" {
					n++
					r, err := regexp.Compile(rs.SMTPMailFromRegexp)
					if err != nil {
						addErrorf("invalid SMTPMailFrom regular expression: %v", err)
					}
					c.Accounts[accName].Destinations[addrName].Rulesets[i].SMTPMailFromRegexpCompiled = r
				}
				if rs.VerifiedDomain != "" {
					n++
					d, err := dns.ParseDomain(rs.VerifiedDomain)
					if err != nil {
						addErrorf("invalid VerifiedDomain: %v", err)
					}
					c.Accounts[accName].Destinations[addrName].Rulesets[i].VerifiedDNSDomain = d
				}

				var hdr [][2]*regexp.Regexp
				for k, v := range rs.HeadersRegexp {
					n++
					if strings.ToLower(k) != k {
						addErrorf("header field %q must only have lower case characters", k)
					}
					if strings.ToLower(v) != v {
						addErrorf("header value %q must only have lower case characters", v)
					}
					rk, err := regexp.Compile(k)
					if err != nil {
						addErrorf("invalid rule header regexp %q: %v", k, err)
					}
					rv, err := regexp.Compile(v)
					if err != nil {
						addErrorf("invalid rule header regexp %q: %v", v, err)
					}
					hdr = append(hdr, [...]*regexp.Regexp{rk, rv})
				}
				c.Accounts[accName].Destinations[addrName].Rulesets[i].HeadersRegexpCompiled = hdr

				if n == 0 {
					addErrorf("ruleset must have at least one rule")
				}

				if rs.ListAllowDomain != "" {
					d, err := dns.ParseDomain(rs.ListAllowDomain)
					if err != nil {
						addErrorf("invalid ListAllowDomain %q: %v", rs.ListAllowDomain, err)
					}
					c.Accounts[accName].Destinations[addrName].Rulesets[i].ListAllowDNSDomain = d
				}
			}

			var address smtp.Address
			localpart, err := smtp.ParseLocalpart(addrName)
			if err != nil && errors.Is(err, smtp.ErrBadLocalpart) {
				address, err = smtp.ParseAddress(addrName)
				if err != nil {
					addErrorf("invalid email address %q in account %q", addrName, accName)
					continue
				} else if _, ok := c.Domains[address.Domain.Name()]; !ok {
					addErrorf("unknown domain for address %q in account %q", addrName, accName)
					continue
				}
			} else {
				if err != nil {
					addErrorf("invalid localpart %q in account %q", addrName, accName)
					continue
				}
				address = smtp.NewAddress(localpart, acc.DNSDomain)
				if _, ok := c.Domains[acc.DNSDomain.Name()]; !ok {
					addErrorf("unknown domain %q for account %q", acc.DNSDomain.Name(), accName)
					continue
				}
			}
			addrFull := address.Pack(true)
			if _, ok := accDests[addrFull]; ok {
				addErrorf("duplicate destination address %q", addrFull)
			}
			accDests[addrFull] = AccountDestination{address.Localpart, accName, dest}
		}
	}

	// Set DMARC destinations.
	for d, domain := range c.Domains {
		dmarc := domain.DMARC
		if dmarc == nil {
			continue
		}
		if _, ok := c.Accounts[dmarc.Account]; !ok {
			addErrorf("DMARC account %q does not exist", dmarc.Account)
		}
		lp, err := smtp.ParseLocalpart(dmarc.Localpart)
		if err != nil {
			addErrorf("invalid DMARC localpart %q: %s", dmarc.Localpart, err)
		}
		if lp.IsInternational() {
			// ../rfc/8616:234
			addErrorf("DMARC localpart %q is an internationalized address, only conventional ascii-only address possible for interopability", lp)
		}
		domain.DMARC.ParsedLocalpart = lp
		c.Domains[d] = domain
		addrFull := smtp.NewAddress(lp, domain.Domain).String()
		dest := config.Destination{
			Mailbox:      dmarc.Mailbox,
			DMARCReports: true,
		}
		checkMailboxNormf(dmarc.Mailbox, "DMARC mailbox for account %q", dmarc.Account)
		accDests[addrFull] = AccountDestination{lp, dmarc.Account, dest}
	}

	// Set TLSRPT destinations.
	for d, domain := range c.Domains {
		tlsrpt := domain.TLSRPT
		if tlsrpt == nil {
			continue
		}
		if _, ok := c.Accounts[tlsrpt.Account]; !ok {
			addErrorf("TLSRPT account %q does not exist", tlsrpt.Account)
		}
		lp, err := smtp.ParseLocalpart(tlsrpt.Localpart)
		if err != nil {
			addErrorf("invalid TLSRPT localpart %q: %s", tlsrpt.Localpart, err)
		}
		if lp.IsInternational() {
			// Does not appear documented in ../rfc/8460, but similar to DMARC it makes sense
			// to keep this ascii-only addresses.
			addErrorf("TLSRPT localpart %q is an internationalized address, only conventional ascii-only address allowed for interopability", lp)
		}
		domain.TLSRPT.ParsedLocalpart = lp
		c.Domains[d] = domain
		addrFull := smtp.NewAddress(lp, domain.Domain).String()
		dest := config.Destination{
			Mailbox:    tlsrpt.Mailbox,
			TLSReports: true,
		}
		checkMailboxNormf(tlsrpt.Mailbox, "TLSRPT mailbox for account %q", tlsrpt.Account)
		accDests[addrFull] = AccountDestination{lp, tlsrpt.Account, dest}
	}
	return
}

func loadTLSKeyCerts(configFile, kind string, ctls *config.TLS) error {
	certs := []tls.Certificate{}
	for _, kp := range ctls.KeyCerts {
		certPath := configDirPath(configFile, kp.CertFile)
		keyPath := configDirPath(configFile, kp.KeyFile)
		cert, err := tls.LoadX509KeyPair(certPath, keyPath)
		if err != nil {
			return fmt.Errorf("tls config for %q: parsing x509 key pair: %v", kind, err)
		}
		certs = append(certs, cert)
	}
	ctls.Config = &tls.Config{
		Certificates: certs,
	}
	return nil
}
