8	cryptorand "crypto/rand"
 
25	"golang.org/x/exp/maps"
 
27	"github.com/mjl-/adns"
 
29	"github.com/mjl-/mox/config"
 
30	"github.com/mjl-/mox/dkim"
 
31	"github.com/mjl-/mox/dmarc"
 
32	"github.com/mjl-/mox/dns"
 
33	"github.com/mjl-/mox/junk"
 
34	"github.com/mjl-/mox/mlog"
 
35	"github.com/mjl-/mox/mtasts"
 
36	"github.com/mjl-/mox/smtp"
 
37	"github.com/mjl-/mox/tlsrpt"
 
40var ErrRequest = errors.New("bad request")
 
42// TXTStrings returns a TXT record value as one or more quoted strings, each max
 
43// 100 characters. In case of multiple strings, a multi-line record is returned.
 
44func TXTStrings(s string) string {
 
58		r += "\t\t\"" + s[:n] + "\"\n"
 
65// MakeDKIMEd25519Key returns a PEM buffer containing an ed25519 key for use
 
67// selector and domain can be empty. If not, they are used in the note.
 
68func MakeDKIMEd25519Key(selector, domain dns.Domain) ([]byte, error) {
 
69	_, privKey, err := ed25519.GenerateKey(cryptorand.Reader)
 
71		return nil, fmt.Errorf("generating key: %w", err)
 
74	pkcs8, err := x509.MarshalPKCS8PrivateKey(privKey)
 
76		return nil, fmt.Errorf("marshal key: %w", err)
 
81		Headers: map[string]string{
 
82			"Note": dkimKeyNote("ed25519", selector, domain),
 
87	if err := pem.Encode(b, block); err != nil {
 
88		return nil, fmt.Errorf("encoding pem: %w", err)
 
93func dkimKeyNote(kind string, selector, domain dns.Domain) string {
 
94	s := kind + " dkim private key"
 
96	if selector != zero && domain != zero {
 
97		s += fmt.Sprintf(" for %s._domainkey.%s", selector.ASCII, domain.ASCII)
 
99	s += fmt.Sprintf(", generated by mox on %s", time.Now().Format(time.RFC3339))
 
103// MakeDKIMRSAKey returns a PEM buffer containing an rsa key for use with
 
105// selector and domain can be empty. If not, they are used in the note.
 
106func MakeDKIMRSAKey(selector, domain dns.Domain) ([]byte, error) {
 
107	// 2048 bits seems reasonable in 2022, 1024 is on the low side, larger
 
108	// keys may not fit in UDP DNS response.
 
109	privKey, err := rsa.GenerateKey(cryptorand.Reader, 2048)
 
111		return nil, fmt.Errorf("generating key: %w", err)
 
114	pkcs8, err := x509.MarshalPKCS8PrivateKey(privKey)
 
116		return nil, fmt.Errorf("marshal key: %w", err)
 
121		Headers: map[string]string{
 
122			"Note": dkimKeyNote("rsa-2048", selector, domain),
 
127	if err := pem.Encode(b, block); err != nil {
 
128		return nil, fmt.Errorf("encoding pem: %w", err)
 
130	return b.Bytes(), nil
 
133// MakeAccountConfig returns a new account configuration for an email address.
 
134func MakeAccountConfig(addr smtp.Address) config.Account {
 
135	account := config.Account{
 
136		Domain: addr.Domain.Name(),
 
137		Destinations: map[string]config.Destination{
 
140		RejectsMailbox: "Rejects",
 
141		JunkFilter: &config.JunkFilter{
 
152	account.AutomaticJunkFlags.Enabled = true
 
153	account.AutomaticJunkFlags.JunkMailboxRegexp = "^(junk|spam)"
 
154	account.AutomaticJunkFlags.NeutralMailboxRegexp = "^(inbox|neutral|postmaster|dmarc|tlsrpt|rejects)"
 
155	account.SubjectPass.Period = 12 * time.Hour
 
159func writeFile(log mlog.Log, path string, data []byte) error {
 
160	os.MkdirAll(filepath.Dir(path), 0770)
 
162	f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0660)
 
164		return fmt.Errorf("creating file %s: %s", path, err)
 
169			log.Check(err, "closing file after error")
 
170			err = os.Remove(path)
 
171			log.Check(err, "removing file after error", slog.String("path", path))
 
174	if _, err := f.Write(data); err != nil {
 
175		return fmt.Errorf("writing file %s: %s", path, err)
 
177	if err := f.Close(); err != nil {
 
178		return fmt.Errorf("close file: %v", err)
 
184// MakeDomainConfig makes a new config for a domain, creating DKIM keys, using
 
185// accountName for DMARC and TLS reports.
 
186func MakeDomainConfig(ctx context.Context, domain, hostname dns.Domain, accountName string, withMTASTS bool) (config.Domain, []string, error) {
 
187	log := pkglog.WithContext(ctx)
 
190	year := now.Format("2006")
 
191	timestamp := now.Format("20060102T150405")
 
195		for _, p := range paths {
 
197			log.Check(err, "removing path for domain config", slog.String("path", p))
 
201	confDKIM := config.DKIM{
 
202		Selectors: map[string]config.Selector{},
 
205	addSelector := func(kind, name string, privKey []byte) error {
 
206		record := fmt.Sprintf("%s._domainkey.%s", name, domain.ASCII)
 
207		keyPath := filepath.Join("dkim", fmt.Sprintf("%s.%s.%s.privatekey.pkcs8.pem", record, timestamp, kind))
 
208		p := configDirPath(ConfigDynamicPath, keyPath)
 
209		if err := writeFile(log, p, privKey); err != nil {
 
212		paths = append(paths, p)
 
213		confDKIM.Selectors[name] = config.Selector{
 
216			// Messages in the wild have been observed with 2 hours and 1 year expiration.
 
218			PrivateKeyFile: keyPath,
 
223	addEd25519 := func(name string) error {
 
224		key, err := MakeDKIMEd25519Key(dns.Domain{ASCII: name}, domain)
 
226			return fmt.Errorf("making dkim ed25519 private key: %s", err)
 
228		return addSelector("ed25519", name, key)
 
231	addRSA := func(name string) error {
 
232		key, err := MakeDKIMRSAKey(dns.Domain{ASCII: name}, domain)
 
234			return fmt.Errorf("making dkim rsa private key: %s", err)
 
236		return addSelector("rsa2048", name, key)
 
239	if err := addEd25519(year + "a"); err != nil {
 
240		return config.Domain{}, nil, err
 
242	if err := addRSA(year + "b"); err != nil {
 
243		return config.Domain{}, nil, err
 
245	if err := addEd25519(year + "c"); err != nil {
 
246		return config.Domain{}, nil, err
 
248	if err := addRSA(year + "d"); err != nil {
 
249		return config.Domain{}, nil, err
 
252	// We sign with the first two. In case they are misused, the switch to the other
 
253	// keys is easy, just change the config. Operators should make the public key field
 
254	// of the misused keys empty in the DNS records to disable the misused keys.
 
255	confDKIM.Sign = []string{year + "a", year + "b"}
 
257	confDomain := config.Domain{
 
258		ClientSettingsDomain:       "mail." + domain.Name(),
 
259		LocalpartCatchallSeparator: "+",
 
261		DMARC: &config.DMARC{
 
262			Account:   accountName,
 
263			Localpart: "dmarc-reports",
 
266		TLSRPT: &config.TLSRPT{
 
267			Account:   accountName,
 
268			Localpart: "tls-reports",
 
274		confDomain.MTASTS = &config.MTASTS{
 
275			PolicyID: time.Now().UTC().Format("20060102T150405"),
 
276			Mode:     mtasts.ModeEnforce,
 
277			// We start out with 24 hour, and warn in the admin interface that users should
 
278			// increase it to weeks once the setup works.
 
279			MaxAge: 24 * time.Hour,
 
280			MX:     []string{hostname.ASCII},
 
287	return confDomain, rpaths, nil
 
290// DKIMAdd adds a DKIM selector for a domain, generating a key and writing it to disk.
 
291func DKIMAdd(ctx context.Context, domain, selector dns.Domain, algorithm, hash string, headerRelaxed, bodyRelaxed, seal bool, headers []string, lifetime time.Duration) (rerr error) {
 
292	log := pkglog.WithContext(ctx)
 
295			log.Errorx("adding dkim key", rerr,
 
296				slog.Any("domain", domain),
 
297				slog.Any("selector", selector))
 
302	case "sha256", "sha1":
 
304		return fmt.Errorf("%w: unknown hash algorithm %q", ErrRequest, hash)
 
312		privKey, err = MakeDKIMRSAKey(selector, domain)
 
315		privKey, err = MakeDKIMEd25519Key(selector, domain)
 
318		err = fmt.Errorf("unknown algorithm")
 
321		return fmt.Errorf("%w: making dkim key: %v", ErrRequest, err)
 
324	// Only take lock now, we don't want to hold it while generating a key.
 
325	Conf.dynamicMutex.Lock()
 
326	defer Conf.dynamicMutex.Unlock()
 
329	d, ok := c.Domains[domain.Name()]
 
331		return fmt.Errorf("%w: domain does not exist", ErrRequest)
 
334	if _, ok := d.DKIM.Selectors[selector.Name()]; ok {
 
335		return fmt.Errorf("%w: selector already exists for domain", ErrRequest)
 
338	record := fmt.Sprintf("%s._domainkey.%s", selector.ASCII, domain.ASCII)
 
339	timestamp := time.Now().Format("20060102T150405")
 
340	keyPath := filepath.Join("dkim", fmt.Sprintf("%s.%s.%s.privatekey.pkcs8.pem", record, timestamp, kind))
 
341	p := configDirPath(ConfigDynamicPath, keyPath)
 
342	if err := writeFile(log, p, privKey); err != nil {
 
343		return fmt.Errorf("writing key file: %v", err)
 
347		if removePath != "" {
 
348			err := os.Remove(removePath)
 
349			log.Check(err, "removing path for dkim key", slog.String("path", removePath))
 
353	nsel := config.Selector{
 
355		Canonicalization: config.Canonicalization{
 
356			HeaderRelaxed: headerRelaxed,
 
357			BodyRelaxed:   bodyRelaxed,
 
360		DontSealHeaders: !seal,
 
361		Expiration:      lifetime.String(),
 
362		PrivateKeyFile:  keyPath,
 
365	// All good, time to update the config.
 
367	nd.DKIM.Selectors = map[string]config.Selector{}
 
368	for name, osel := range d.DKIM.Selectors {
 
369		nd.DKIM.Selectors[name] = osel
 
371	nd.DKIM.Selectors[selector.Name()] = nsel
 
373	nc.Domains = map[string]config.Domain{}
 
374	for name, dom := range c.Domains {
 
375		nc.Domains[name] = dom
 
377	nc.Domains[domain.Name()] = nd
 
379	if err := writeDynamic(ctx, log, nc); err != nil {
 
380		return fmt.Errorf("writing domains.conf: %w", err)
 
383	log.Info("dkim key added", slog.Any("domain", domain), slog.Any("selector", selector))
 
384	removePath = "" // Prevent cleanup of key file.
 
388// DKIMRemove removes the selector from the domain, moving the key file out of the way.
 
389func DKIMRemove(ctx context.Context, domain, selector dns.Domain) (rerr error) {
 
390	log := pkglog.WithContext(ctx)
 
393			log.Errorx("removing dkim key", rerr,
 
394				slog.Any("domain", domain),
 
395				slog.Any("selector", selector))
 
399	Conf.dynamicMutex.Lock()
 
400	defer Conf.dynamicMutex.Unlock()
 
403	d, ok := c.Domains[domain.Name()]
 
405		return fmt.Errorf("%w: domain does not exist", ErrRequest)
 
408	sel, ok := d.DKIM.Selectors[selector.Name()]
 
410		return fmt.Errorf("%w: selector does not exist for domain", ErrRequest)
 
413	nsels := map[string]config.Selector{}
 
414	for name, sel := range d.DKIM.Selectors {
 
415		if name != selector.Name() {
 
419	nsign := make([]string, 0, len(d.DKIM.Sign))
 
420	for _, name := range d.DKIM.Sign {
 
421		if name != selector.Name() {
 
422			nsign = append(nsign, name)
 
427	nd.DKIM = config.DKIM{Selectors: nsels, Sign: nsign}
 
429	nc.Domains = map[string]config.Domain{}
 
430	for name, dom := range c.Domains {
 
431		nc.Domains[name] = dom
 
433	nc.Domains[domain.Name()] = nd
 
435	if err := writeDynamic(ctx, log, nc); err != nil {
 
436		return fmt.Errorf("writing domains.conf: %w", err)
 
439	// Move away a DKIM private key to a subdirectory "old". But only if
 
440	// not in use by other domains.
 
441	usedKeyPaths := gatherUsedKeysPaths(nc)
 
442	moveAwayKeys(log, map[string]config.Selector{selector.Name(): sel}, usedKeyPaths)
 
444	log.Info("dkim key removed", slog.Any("domain", domain), slog.Any("selector", selector))
 
448// DomainAdd adds the domain to the domains config, rewriting domains.conf and
 
451// accountName is used for DMARC/TLS report and potentially for the postmaster address.
 
452// If the account does not exist, it is created with localpart. Localpart must be
 
453// set only if the account does not yet exist.
 
454func DomainAdd(ctx context.Context, domain dns.Domain, accountName string, localpart smtp.Localpart) (rerr error) {
 
455	log := pkglog.WithContext(ctx)
 
458			log.Errorx("adding domain", rerr,
 
459				slog.Any("domain", domain),
 
460				slog.String("account", accountName),
 
461				slog.Any("localpart", localpart))
 
465	Conf.dynamicMutex.Lock()
 
466	defer Conf.dynamicMutex.Unlock()
 
469	if _, ok := c.Domains[domain.Name()]; ok {
 
470		return fmt.Errorf("%w: domain already present", ErrRequest)
 
473	// Compose new config without modifying existing data structures. If we fail, we
 
476	nc.Domains = map[string]config.Domain{}
 
477	for name, d := range c.Domains {
 
481	// Only enable mta-sts for domain if there is a listener with mta-sts.
 
483	for _, l := range Conf.Static.Listeners {
 
484		if l.MTASTSHTTPS.Enabled {
 
490	confDomain, cleanupFiles, err := MakeDomainConfig(ctx, domain, Conf.Static.HostnameDomain, accountName, withMTASTS)
 
492		return fmt.Errorf("preparing domain config: %v", err)
 
495		for _, f := range cleanupFiles {
 
497			log.Check(err, "cleaning up file after error", slog.String("path", f))
 
501	if _, ok := c.Accounts[accountName]; ok && localpart != "" {
 
502		return fmt.Errorf("%w: account already exists (leave localpart empty when using an existing account)", ErrRequest)
 
503	} else if !ok && localpart == "" {
 
504		return fmt.Errorf("%w: account does not yet exist (specify a localpart)", ErrRequest)
 
505	} else if accountName == "" {
 
506		return fmt.Errorf("%w: account name is empty", ErrRequest)
 
508		nc.Accounts[accountName] = MakeAccountConfig(smtp.Address{Localpart: localpart, Domain: domain})
 
509	} else if accountName != Conf.Static.Postmaster.Account {
 
510		nacc := nc.Accounts[accountName]
 
511		nd := map[string]config.Destination{}
 
512		for k, v := range nacc.Destinations {
 
515		pmaddr := smtp.Address{Localpart: "postmaster", Domain: domain}
 
516		nd[pmaddr.String()] = config.Destination{}
 
517		nacc.Destinations = nd
 
518		nc.Accounts[accountName] = nacc
 
521	nc.Domains[domain.Name()] = confDomain
 
523	if err := writeDynamic(ctx, log, nc); err != nil {
 
524		return fmt.Errorf("writing domains.conf: %w", err)
 
526	log.Info("domain added", slog.Any("domain", domain))
 
527	cleanupFiles = nil // All good, don't cleanup.
 
531// DomainRemove removes domain from the config, rewriting domains.conf.
 
533// No accounts are removed, also not when they still reference this domain.
 
534func DomainRemove(ctx context.Context, domain dns.Domain) (rerr error) {
 
535	log := pkglog.WithContext(ctx)
 
538			log.Errorx("removing domain", rerr, slog.Any("domain", domain))
 
542	Conf.dynamicMutex.Lock()
 
543	defer Conf.dynamicMutex.Unlock()
 
546	domConf, ok := c.Domains[domain.Name()]
 
548		return fmt.Errorf("%w: domain does not exist", ErrRequest)
 
551	// Compose new config without modifying existing data structures. If we fail, we
 
554	nc.Domains = map[string]config.Domain{}
 
556	for name, d := range c.Domains {
 
562	if err := writeDynamic(ctx, log, nc); err != nil {
 
563		return fmt.Errorf("writing domains.conf: %w", err)
 
566	// Move away any DKIM private keys to a subdirectory "old". But only if
 
567	// they are not in use by other domains.
 
568	usedKeyPaths := gatherUsedKeysPaths(nc)
 
569	moveAwayKeys(log, domConf.DKIM.Selectors, usedKeyPaths)
 
571	log.Info("domain removed", slog.Any("domain", domain))
 
575func gatherUsedKeysPaths(nc config.Dynamic) map[string]bool {
 
576	usedKeyPaths := map[string]bool{}
 
577	for _, dc := range nc.Domains {
 
578		for _, sel := range dc.DKIM.Selectors {
 
579			usedKeyPaths[filepath.Clean(sel.PrivateKeyFile)] = true
 
585func moveAwayKeys(log mlog.Log, sels map[string]config.Selector, usedKeyPaths map[string]bool) {
 
586	for _, sel := range sels {
 
587		if sel.PrivateKeyFile == "" || usedKeyPaths[filepath.Clean(sel.PrivateKeyFile)] {
 
590		src := ConfigDirPath(sel.PrivateKeyFile)
 
591		dst := ConfigDirPath(filepath.Join(filepath.Dir(sel.PrivateKeyFile), "old", filepath.Base(sel.PrivateKeyFile)))
 
592		_, err := os.Stat(dst)
 
594			err = fmt.Errorf("destination already exists")
 
595		} else if os.IsNotExist(err) {
 
596			os.MkdirAll(filepath.Dir(dst), 0770)
 
597			err = os.Rename(src, dst)
 
600			log.Errorx("renaming dkim private key file for removed domain", err, slog.String("src", src), slog.String("dst", dst))
 
605// DomainSave calls xmodify with a shallow copy of the domain config. xmodify
 
606// can modify the config, but must clone all referencing data it changes.
 
607// xmodify may employ panic-based error handling. After xmodify returns, the
 
608// modified config is verified, saved and takes effect.
 
609func DomainSave(ctx context.Context, domainName string, xmodify func(config *config.Domain) error) (rerr error) {
 
610	log := pkglog.WithContext(ctx)
 
613			log.Errorx("saving domain config", rerr)
 
617	Conf.dynamicMutex.Lock()
 
618	defer Conf.dynamicMutex.Unlock()
 
620	nc := Conf.Dynamic                // Shallow copy.
 
621	dom, ok := nc.Domains[domainName] // dom is a shallow copy.
 
623		return fmt.Errorf("%w: domain not present", ErrRequest)
 
626	if err := xmodify(&dom); err != nil {
 
630	// Compose new config without modifying existing data structures. If we fail, we
 
632	nc.Domains = map[string]config.Domain{}
 
633	for name, d := range Conf.Dynamic.Domains {
 
636	nc.Domains[domainName] = dom
 
638	if err := writeDynamic(ctx, log, nc); err != nil {
 
639		return fmt.Errorf("writing domains.conf: %w", err)
 
642	log.Info("domain saved")
 
646// ConfigSave calls xmodify with a shallow copy of the dynamic config. xmodify
 
647// can modify the config, but must clone all referencing data it changes.
 
648// xmodify may employ panic-based error handling. After xmodify returns, the
 
649// modified config is verified, saved and takes effect.
 
650func ConfigSave(ctx context.Context, xmodify func(config *config.Dynamic)) (rerr error) {
 
651	log := pkglog.WithContext(ctx)
 
654			log.Errorx("saving config", rerr)
 
658	Conf.dynamicMutex.Lock()
 
659	defer Conf.dynamicMutex.Unlock()
 
661	nc := Conf.Dynamic // Shallow copy.
 
664	if err := writeDynamic(ctx, log, nc); err != nil {
 
665		return fmt.Errorf("writing domains.conf: %w", err)
 
668	log.Info("config saved")
 
672// todo: find a way to automatically create the dns records as it would greatly simplify setting up email for a domain. we could also dynamically make changes, e.g. providing grace periods after disabling a dkim key, only automatically removing the dkim dns key after a few days. but this requires some kind of api and authentication to the dns server. there doesn't appear to be a single commonly used api for dns management. each of the numerous cloud providers have their own APIs and rather large SKDs to use them. we don't want to link all of them in.
 
674// DomainRecords returns text lines describing DNS records required for configuring
 
677// If certIssuerDomainName is set, CAA records to limit TLS certificate issuance to
 
678// that caID will be suggested. If acmeAccountURI is also set, CAA records also
 
679// restricting issuance to that account ID will be suggested.
 
680func DomainRecords(domConf config.Domain, domain dns.Domain, hasDNSSEC bool, certIssuerDomainName, acmeAccountURI string) ([]string, error) {
 
682	h := Conf.Static.HostnameDomain.ASCII
 
684	// The first line with ";" is used by ../testdata/integration/moxacmepebble.sh and
 
685	// ../testdata/integration/moxmail2.sh for selecting DNS records
 
687		"; Time To Live of 5 minutes, may be recognized if importing as a zone file.",
 
688		"; Once your setup is working, you may want to increase the TTL.",
 
693	if public, ok := Conf.Static.Listeners["public"]; ok && public.TLS != nil && (len(public.TLS.HostPrivateRSA2048Keys) > 0 || len(public.TLS.HostPrivateECDSAP256Keys) > 0) {
 
694		records = append(records,
 
695			`; DANE: These records indicate that a remote mail server trying to deliver email`,
 
696			`; with SMTP (TCP port 25) must verify the TLS certificate with DANE-EE (3), based`,
 
697			`; on the certificate public key ("SPKI", 1) that is SHA2-256-hashed (1) to the`,
 
698			`; hexadecimal hash. DANE-EE verification means only the certificate or public`,
 
699			`; key is verified, not whether the certificate is signed by a (centralized)`,
 
700			`; certificate authority (CA), is expired, or matches the host name.`,
 
702			`; NOTE: Create the records below only once: They are for the machine, and apply`,
 
703			`; to all hosted domains.`,
 
706			records = append(records,
 
708				"; WARNING: Domain does not appear to be DNSSEC-signed. To enable DANE, first",
 
709				"; enable DNSSEC on your domain, then add the TLSA records. Records below have been",
 
713		addTLSA := func(privKey crypto.Signer) error {
 
714			spkiBuf, err := x509.MarshalPKIXPublicKey(privKey.Public())
 
716				return fmt.Errorf("marshal SubjectPublicKeyInfo for DANE record: %v", err)
 
718			sum := sha256.Sum256(spkiBuf)
 
719			tlsaRecord := adns.TLSA{
 
720				Usage:     adns.TLSAUsageDANEEE,
 
721				Selector:  adns.TLSASelectorSPKI,
 
722				MatchType: adns.TLSAMatchTypeSHA256,
 
727				s = fmt.Sprintf("_25._tcp.%-*s TLSA %s", 20+len(d)-len("_25._tcp."), h+".", tlsaRecord.Record())
 
729				s = fmt.Sprintf(";; _25._tcp.%-*s TLSA %s", 20+len(d)-len(";; _25._tcp."), h+".", tlsaRecord.Record())
 
731			records = append(records, s)
 
734		for _, privKey := range public.TLS.HostPrivateECDSAP256Keys {
 
735			if err := addTLSA(privKey); err != nil {
 
739		for _, privKey := range public.TLS.HostPrivateRSA2048Keys {
 
740			if err := addTLSA(privKey); err != nil {
 
744		records = append(records, "")
 
748		records = append(records,
 
749			"; For the machine, only needs to be created once, for the first domain added:",
 
751			"; SPF-allow host for itself, resulting in relaxed DMARC pass for (postmaster)",
 
752			"; messages (DSNs) sent from host:",
 
757	if d != h && Conf.Static.HostTLSRPT.ParsedLocalpart != "" {
 
760			Opaque: smtp.NewAddress(Conf.Static.HostTLSRPT.ParsedLocalpart, Conf.Static.HostnameDomain).Pack(false),
 
762		tlsrptr := tlsrpt.Record{Version: "TLSRPTv1", RUAs: [][]tlsrpt.RUA{{tlsrpt.RUA(uri.String())}}}
 
763		records = append(records,
 
764			"; For the machine, only needs to be created once, for the first domain added:",
 
766			"; Request reporting about success/failures of TLS connections to (MX) host, for DANE.",
 
767			fmt.Sprintf(`_smtp._tls.%-*s         TXT "%s"`, 20+len(d)-len("_smtp._tls."), h+".", tlsrptr.String()),
 
772	records = append(records,
 
773		"; Deliver email for the domain to this host.",
 
774		fmt.Sprintf("%s.                    MX 10 %s.", d, h),
 
777		"; Outgoing messages will be signed with the first two DKIM keys. The other two",
 
778		"; configured for backup, switching to them is just a config change.",
 
780	var selectors []string
 
781	for name := range domConf.DKIM.Selectors {
 
782		selectors = append(selectors, name)
 
784	sort.Slice(selectors, func(i, j int) bool {
 
785		return selectors[i] < selectors[j]
 
787	for _, name := range selectors {
 
788		sel := domConf.DKIM.Selectors[name]
 
789		dkimr := dkim.Record{
 
791			Hashes:    []string{"sha256"},
 
792			PublicKey: sel.Key.Public(),
 
794		if _, ok := sel.Key.(ed25519.PrivateKey); ok {
 
795			dkimr.Key = "ed25519"
 
796		} else if _, ok := sel.Key.(*rsa.PrivateKey); !ok {
 
797			return nil, fmt.Errorf("unrecognized private key for DKIM selector %q: %T", name, sel.Key)
 
799		txt, err := dkimr.Record()
 
801			return nil, fmt.Errorf("making DKIM DNS TXT record: %v", err)
 
805			records = append(records,
 
806				"; NOTE: The following strings must be added to DNS as single record.",
 
809		s := fmt.Sprintf("%s._domainkey.%s.   TXT %s", name, d, TXTStrings(txt))
 
810		records = append(records, s)
 
813	dmarcr := dmarc.DefaultRecord
 
814	dmarcr.Policy = "reject"
 
815	if domConf.DMARC != nil {
 
818			Opaque: smtp.NewAddress(domConf.DMARC.ParsedLocalpart, domConf.DMARC.DNSDomain).Pack(false),
 
820		dmarcr.AggregateReportAddresses = []dmarc.URI{
 
821			{Address: uri.String(), MaxSize: 10, Unit: "m"},
 
824	records = append(records,
 
827		"; Specify the MX host is allowed to send for our domain and for itself (for DSNs).",
 
828		"; ~all means softfail for anything else, which is done instead of -all to prevent older",
 
829		"; mail servers from rejecting the message because they never get to looking for a dkim/dmarc pass.",
 
830		fmt.Sprintf(`%s.                    TXT "v=spf1 mx ~all"`, d),
 
833		"; Emails that fail the DMARC check (without aligned DKIM and without aligned SPF)",
 
834		"; should be rejected, and request reports. If you email through mailing lists that",
 
835		"; strip DKIM-Signature headers and don't rewrite the From header, you may want to",
 
836		"; set the policy to p=none.",
 
837		fmt.Sprintf(`_dmarc.%s.             TXT "%s"`, d, dmarcr.String()),
 
841	if sts := domConf.MTASTS; sts != nil {
 
842		records = append(records,
 
843			"; Remote servers can use MTA-STS to verify our TLS certificate with the",
 
844			"; WebPKI pool of CA's (certificate authorities) when delivering over SMTP with",
 
846			fmt.Sprintf(`mta-sts.%s.            CNAME %s.`, d, h),
 
847			fmt.Sprintf(`_mta-sts.%s.           TXT "v=STSv1; id=%s"`, d, sts.PolicyID),
 
851		records = append(records,
 
852			"; Note: No MTA-STS to indicate TLS should be used. Either because disabled for the",
 
853			"; domain or because mox.conf does not have a listener with MTA-STS configured.",
 
858	if domConf.TLSRPT != nil {
 
861			Opaque: smtp.NewAddress(domConf.TLSRPT.ParsedLocalpart, domConf.TLSRPT.DNSDomain).Pack(false),
 
863		tlsrptr := tlsrpt.Record{Version: "TLSRPTv1", RUAs: [][]tlsrpt.RUA{{tlsrpt.RUA(uri.String())}}}
 
864		records = append(records,
 
865			"; Request reporting about TLS failures.",
 
866			fmt.Sprintf(`_smtp._tls.%s.         TXT "%s"`, d, tlsrptr.String()),
 
871	if domConf.ClientSettingsDomain != "" && domConf.ClientSettingsDNSDomain != Conf.Static.HostnameDomain {
 
872		records = append(records,
 
873			"; Client settings will reference a subdomain of the hosted domain, making it",
 
874			"; easier to migrate to a different server in the future by not requiring settings",
 
875			"; in all clients to be updated.",
 
876			fmt.Sprintf(`%-*s CNAME %s.`, 20+len(d), domConf.ClientSettingsDNSDomain.ASCII+".", h),
 
881	records = append(records,
 
882		"; Autoconfig is used by Thunderbird. Autodiscover is (in theory) used by Microsoft.",
 
883		fmt.Sprintf(`autoconfig.%s.         CNAME %s.`, d, h),
 
884		fmt.Sprintf(`_autodiscover._tcp.%s. SRV 0 1 443 %s.`, d, h),
 
888		"; For secure IMAP and submission autoconfig, point to mail host.",
 
889		fmt.Sprintf(`_imaps._tcp.%s.        SRV 0 1 993 %s.`, d, h),
 
890		fmt.Sprintf(`_submissions._tcp.%s.  SRV 0 1 465 %s.`, d, h),
 
893		"; Next records specify POP3 and non-TLS ports are not to be used.",
 
894		"; These are optional and safe to leave out (e.g. if you have to click a lot in a",
 
895		"; DNS admin web interface).",
 
896		fmt.Sprintf(`_imap._tcp.%s.         SRV 0 1 143 .`, d),
 
897		fmt.Sprintf(`_submission._tcp.%s.   SRV 0 1 587 .`, d),
 
898		fmt.Sprintf(`_pop3._tcp.%s.         SRV 0 1 110 .`, d),
 
899		fmt.Sprintf(`_pop3s._tcp.%s.        SRV 0 1 995 .`, d),
 
902	if certIssuerDomainName != "" {
 
904		records = append(records,
 
907			"; You could mark Let's Encrypt as the only Certificate Authority allowed to",
 
908			"; sign TLS certificates for your domain.",
 
909			fmt.Sprintf(`%s.                    CAA 0 issue "%s"`, d, certIssuerDomainName),
 
911		if acmeAccountURI != "" {
 
914			records = append(records,
 
916				"; Optionally limit certificates for this domain to the account ID and methods used by mox.",
 
917				fmt.Sprintf(`;; %s.                 CAA 0 issue "%s; accounturi=%s; validationmethods=tls-alpn-01,http-01"`, d, certIssuerDomainName, acmeAccountURI),
 
919				"; Or alternatively only limit for email-specific subdomains, so you can use",
 
920				"; other accounts/methods for other subdomains.",
 
921				fmt.Sprintf(`;; autoconfig.%s.      CAA 0 issue "%s; accounturi=%s; validationmethods=tls-alpn-01,http-01"`, d, certIssuerDomainName, acmeAccountURI),
 
922				fmt.Sprintf(`;; mta-sts.%s.         CAA 0 issue "%s; accounturi=%s; validationmethods=tls-alpn-01,http-01"`, d, certIssuerDomainName, acmeAccountURI),
 
924			if domConf.ClientSettingsDomain != "" && domConf.ClientSettingsDNSDomain != Conf.Static.HostnameDomain {
 
925				records = append(records,
 
926					fmt.Sprintf(`;; %-*s CAA 0 issue "%s; accounturi=%s; validationmethods=tls-alpn-01,http-01"`, 20-3+len(d), domConf.ClientSettingsDNSDomain.ASCII, certIssuerDomainName, acmeAccountURI),
 
929			if strings.HasSuffix(h, "."+d) {
 
930				records = append(records,
 
932					"; And the mail hostname.",
 
933					fmt.Sprintf(`;; %-*s CAA 0 issue "%s; accounturi=%s; validationmethods=tls-alpn-01,http-01"`, 20-3+len(d), h+".", certIssuerDomainName, acmeAccountURI),
 
937			// The string "will be suggested" is used by
 
938			// ../testdata/integration/moxacmepebble.sh and ../testdata/integration/moxmail2.sh
 
939			// as end of DNS records.
 
940			records = append(records,
 
942				"; Note: After starting up, once an ACME account has been created, CAA records",
 
943				"; that restrict issuance to the account will be suggested.",
 
950// AccountAdd adds an account and an initial address and reloads the configuration.
 
952// The new account does not have a password, so cannot yet log in. Email can be
 
955// Catchall addresses are not supported for AccountAdd. Add separately with AddressAdd.
 
956func AccountAdd(ctx context.Context, account, address string) (rerr error) {
 
957	log := pkglog.WithContext(ctx)
 
960			log.Errorx("adding account", rerr, slog.String("account", account), slog.String("address", address))
 
964	addr, err := smtp.ParseAddress(address)
 
966		return fmt.Errorf("%w: parsing email address: %v", ErrRequest, err)
 
969	Conf.dynamicMutex.Lock()
 
970	defer Conf.dynamicMutex.Unlock()
 
973	if _, ok := c.Accounts[account]; ok {
 
974		return fmt.Errorf("%w: account already present", ErrRequest)
 
977	if err := checkAddressAvailable(addr); err != nil {
 
978		return fmt.Errorf("%w: address not available: %v", ErrRequest, err)
 
981	// Compose new config without modifying existing data structures. If we fail, we
 
984	nc.Accounts = map[string]config.Account{}
 
985	for name, a := range c.Accounts {
 
986		nc.Accounts[name] = a
 
988	nc.Accounts[account] = MakeAccountConfig(addr)
 
990	if err := writeDynamic(ctx, log, nc); err != nil {
 
991		return fmt.Errorf("writing domains.conf: %w", err)
 
993	log.Info("account added", slog.String("account", account), slog.Any("address", addr))
 
997// AccountRemove removes an account and reloads the configuration.
 
998func AccountRemove(ctx context.Context, account string) (rerr error) {
 
999	log := pkglog.WithContext(ctx)
 
1002			log.Errorx("adding account", rerr, slog.String("account", account))
 
1006	Conf.dynamicMutex.Lock()
 
1007	defer Conf.dynamicMutex.Unlock()
 
1010	if _, ok := c.Accounts[account]; !ok {
 
1011		return fmt.Errorf("%w: account does not exist", ErrRequest)
 
1014	// Compose new config without modifying existing data structures. If we fail, we
 
1017	nc.Accounts = map[string]config.Account{}
 
1018	for name, a := range c.Accounts {
 
1019		if name != account {
 
1020			nc.Accounts[name] = a
 
1024	if err := writeDynamic(ctx, log, nc); err != nil {
 
1025		return fmt.Errorf("writing domains.conf: %w", err)
 
1027	log.Info("account removed", slog.String("account", account))
 
1031// checkAddressAvailable checks that the address after canonicalization is not
 
1032// already configured, and that its localpart does not contain the catchall
 
1033// localpart separator.
 
1035// Must be called with config lock held.
 
1036func checkAddressAvailable(addr smtp.Address) error {
 
1037	dc, ok := Conf.Dynamic.Domains[addr.Domain.Name()]
 
1039		return fmt.Errorf("domain does not exist")
 
1041	lp := CanonicalLocalpart(addr.Localpart, dc)
 
1042	if _, ok := Conf.accountDestinations[smtp.NewAddress(lp, addr.Domain).String()]; ok {
 
1043		return fmt.Errorf("canonicalized address %s already configured", smtp.NewAddress(lp, addr.Domain))
 
1044	} else if dc.LocalpartCatchallSeparator != "" && strings.Contains(string(addr.Localpart), dc.LocalpartCatchallSeparator) {
 
1045		return fmt.Errorf("localpart cannot include domain catchall separator %s", dc.LocalpartCatchallSeparator)
 
1046	} else if _, ok := dc.Aliases[lp.String()]; ok {
 
1047		return fmt.Errorf("address in use as alias")
 
1052// AddressAdd adds an email address to an account and reloads the configuration. If
 
1053// address starts with an @ it is treated as a catchall address for the domain.
 
1054func AddressAdd(ctx context.Context, address, account string) (rerr error) {
 
1055	log := pkglog.WithContext(ctx)
 
1058			log.Errorx("adding address", rerr, slog.String("address", address), slog.String("account", account))
 
1062	Conf.dynamicMutex.Lock()
 
1063	defer Conf.dynamicMutex.Unlock()
 
1066	a, ok := c.Accounts[account]
 
1068		return fmt.Errorf("%w: account does not exist", ErrRequest)
 
1072	if strings.HasPrefix(address, "@") {
 
1073		d, err := dns.ParseDomain(address[1:])
 
1075			return fmt.Errorf("%w: parsing domain: %v", ErrRequest, err)
 
1078		destAddr = "@" + dname
 
1079		if _, ok := Conf.Dynamic.Domains[dname]; !ok {
 
1080			return fmt.Errorf("%w: domain does not exist", ErrRequest)
 
1081		} else if _, ok := Conf.accountDestinations[destAddr]; ok {
 
1082			return fmt.Errorf("%w: catchall address already configured for domain", ErrRequest)
 
1085		addr, err := smtp.ParseAddress(address)
 
1087			return fmt.Errorf("%w: parsing email address: %v", ErrRequest, err)
 
1090		if err := checkAddressAvailable(addr); err != nil {
 
1091			return fmt.Errorf("%w: address not available: %v", ErrRequest, err)
 
1093		destAddr = addr.String()
 
1096	// Compose new config without modifying existing data structures. If we fail, we
 
1099	nc.Accounts = map[string]config.Account{}
 
1100	for name, a := range c.Accounts {
 
1101		nc.Accounts[name] = a
 
1103	nd := map[string]config.Destination{}
 
1104	for name, d := range a.Destinations {
 
1107	nd[destAddr] = config.Destination{}
 
1109	nc.Accounts[account] = a
 
1111	if err := writeDynamic(ctx, log, nc); err != nil {
 
1112		return fmt.Errorf("writing domains.conf: %w", err)
 
1114	log.Info("address added", slog.String("address", address), slog.String("account", account))
 
1118// AddressRemove removes an email address and reloads the configuration.
 
1119// Address can be a catchall address for the domain of the form "@<domain>".
 
1121// If the address is member of an alias, remove it from from the alias, unless it
 
1122// is the last member.
 
1123func AddressRemove(ctx context.Context, address string) (rerr error) {
 
1124	log := pkglog.WithContext(ctx)
 
1127			log.Errorx("removing address", rerr, slog.String("address", address))
 
1131	Conf.dynamicMutex.Lock()
 
1132	defer Conf.dynamicMutex.Unlock()
 
1134	ad, ok := Conf.accountDestinations[address]
 
1136		return fmt.Errorf("%w: address does not exists", ErrRequest)
 
1139	// Compose new config without modifying existing data structures. If we fail, we
 
1141	a, ok := Conf.Dynamic.Accounts[ad.Account]
 
1143		return fmt.Errorf("internal error: cannot find account")
 
1146	na.Destinations = map[string]config.Destination{}
 
1148	for destAddr, d := range a.Destinations {
 
1149		if destAddr != address {
 
1150			na.Destinations[destAddr] = d
 
1156		return fmt.Errorf("%w: address not removed, likely a postmaster/reporting address", ErrRequest)
 
1159	// Also remove matching address from FromIDLoginAddresses, composing a new slice.
 
1160	var fromIDLoginAddresses []string
 
1162	var pa smtp.Address // For non-catchall addresses (most).
 
1164	if strings.HasPrefix(address, "@") {
 
1165		dom, err = dns.ParseDomain(address[1:])
 
1167			return fmt.Errorf("%w: parsing domain for catchall address: %v", ErrRequest, err)
 
1170		pa, err = smtp.ParseAddress(address)
 
1172			return fmt.Errorf("%w: parsing address: %v", ErrRequest, err)
 
1176	for i, fa := range a.ParsedFromIDLoginAddresses {
 
1177		if fa.Domain != dom {
 
1178			// Keep for different domain.
 
1179			fromIDLoginAddresses = append(fromIDLoginAddresses, a.FromIDLoginAddresses[i])
 
1182		if strings.HasPrefix(address, "@") {
 
1185		dc, ok := Conf.Dynamic.Domains[dom.Name()]
 
1187			return fmt.Errorf("%w: unknown domain in fromid login address %q", ErrRequest, fa.Pack(true))
 
1189		flp := CanonicalLocalpart(fa.Localpart, dc)
 
1190		alp := CanonicalLocalpart(pa.Localpart, dc)
 
1192			// Keep for different localpart.
 
1193			fromIDLoginAddresses = append(fromIDLoginAddresses, a.FromIDLoginAddresses[i])
 
1196	na.FromIDLoginAddresses = fromIDLoginAddresses
 
1198	// And remove as member from aliases configured in domains.
 
1199	domains := maps.Clone(Conf.Dynamic.Domains)
 
1200	for _, aa := range na.Aliases {
 
1201		if aa.SubscriptionAddress != address {
 
1205		aliasAddr := fmt.Sprintf("%s@%s", aa.Alias.LocalpartStr, aa.Alias.Domain.Name())
 
1207		dom, ok := Conf.Dynamic.Domains[aa.Alias.Domain.Name()]
 
1209			return fmt.Errorf("cannot find domain for alias %s", aliasAddr)
 
1211		a, ok := dom.Aliases[aa.Alias.LocalpartStr]
 
1213			return fmt.Errorf("cannot find alias %s", aliasAddr)
 
1215		a.Addresses = slices.Clone(a.Addresses)
 
1216		a.Addresses = slices.DeleteFunc(a.Addresses, func(v string) bool { return v == address })
 
1217		if len(a.Addresses) == 0 {
 
1218			return fmt.Errorf("address is last member of alias %s, add new members or remove alias first", aliasAddr)
 
1220		a.ParsedAddresses = nil // Filled when parsing config.
 
1221		dom.Aliases = maps.Clone(dom.Aliases)
 
1222		dom.Aliases[aa.Alias.LocalpartStr] = a
 
1223		domains[aa.Alias.Domain.Name()] = dom
 
1225	na.Aliases = nil // Filled when parsing config.
 
1228	nc.Accounts = map[string]config.Account{}
 
1229	for name, a := range Conf.Dynamic.Accounts {
 
1230		nc.Accounts[name] = a
 
1232	nc.Accounts[ad.Account] = na
 
1233	nc.Domains = domains
 
1235	if err := writeDynamic(ctx, log, nc); err != nil {
 
1236		return fmt.Errorf("writing domains.conf: %w", err)
 
1238	log.Info("address removed", slog.String("address", address), slog.String("account", ad.Account))
 
1242func AliasAdd(ctx context.Context, addr smtp.Address, alias config.Alias) error {
 
1243	return DomainSave(ctx, addr.Domain.Name(), func(d *config.Domain) error {
 
1244		if _, ok := d.Aliases[addr.Localpart.String()]; ok {
 
1245			return fmt.Errorf("%w: alias already present", ErrRequest)
 
1247		if d.Aliases == nil {
 
1248			d.Aliases = map[string]config.Alias{}
 
1250		d.Aliases = maps.Clone(d.Aliases)
 
1251		d.Aliases[addr.Localpart.String()] = alias
 
1256func AliasUpdate(ctx context.Context, addr smtp.Address, alias config.Alias) error {
 
1257	return DomainSave(ctx, addr.Domain.Name(), func(d *config.Domain) error {
 
1258		a, ok := d.Aliases[addr.Localpart.String()]
 
1260			return fmt.Errorf("%w: alias does not exist", ErrRequest)
 
1262		a.PostPublic = alias.PostPublic
 
1263		a.ListMembers = alias.ListMembers
 
1264		a.AllowMsgFrom = alias.AllowMsgFrom
 
1265		d.Aliases = maps.Clone(d.Aliases)
 
1266		d.Aliases[addr.Localpart.String()] = a
 
1271func AliasRemove(ctx context.Context, addr smtp.Address) error {
 
1272	return DomainSave(ctx, addr.Domain.Name(), func(d *config.Domain) error {
 
1273		_, ok := d.Aliases[addr.Localpart.String()]
 
1275			return fmt.Errorf("%w: alias does not exist", ErrRequest)
 
1277		d.Aliases = maps.Clone(d.Aliases)
 
1278		delete(d.Aliases, addr.Localpart.String())
 
1283func AliasAddressesAdd(ctx context.Context, addr smtp.Address, addresses []string) error {
 
1284	if len(addresses) == 0 {
 
1285		return fmt.Errorf("%w: at least one address required", ErrRequest)
 
1287	return DomainSave(ctx, addr.Domain.Name(), func(d *config.Domain) error {
 
1288		alias, ok := d.Aliases[addr.Localpart.String()]
 
1290			return fmt.Errorf("%w: no such alias", ErrRequest)
 
1292		alias.Addresses = append(slices.Clone(alias.Addresses), addresses...)
 
1293		alias.ParsedAddresses = nil
 
1294		d.Aliases = maps.Clone(d.Aliases)
 
1295		d.Aliases[addr.Localpart.String()] = alias
 
1300func AliasAddressesRemove(ctx context.Context, addr smtp.Address, addresses []string) error {
 
1301	if len(addresses) == 0 {
 
1302		return fmt.Errorf("%w: need at least one address", ErrRequest)
 
1304	return DomainSave(ctx, addr.Domain.Name(), func(d *config.Domain) error {
 
1305		alias, ok := d.Aliases[addr.Localpart.String()]
 
1307			return fmt.Errorf("%w: no such alias", ErrRequest)
 
1309		alias.Addresses = slices.DeleteFunc(slices.Clone(alias.Addresses), func(addr string) bool {
 
1311			addresses = slices.DeleteFunc(addresses, func(a string) bool { return a == addr })
 
1312			return n > len(addresses)
 
1314		if len(addresses) > 0 {
 
1315			return fmt.Errorf("%w: address not found: %s", ErrRequest, strings.Join(addresses, ", "))
 
1317		alias.ParsedAddresses = nil
 
1318		d.Aliases = maps.Clone(d.Aliases)
 
1319		d.Aliases[addr.Localpart.String()] = alias
 
1324// AccountSave updates the configuration of an account. Function xmodify is called
 
1325// with a shallow copy of the current configuration of the account. It must not
 
1326// change referencing fields (e.g. existing slice/map/pointer), they may still be
 
1327// in use, and the change may be rolled back. Referencing values must be copied and
 
1328// replaced by the modify. The function may raise a panic for error handling.
 
1329func AccountSave(ctx context.Context, account string, xmodify func(acc *config.Account)) (rerr error) {
 
1330	log := pkglog.WithContext(ctx)
 
1333			log.Errorx("saving account fields", rerr, slog.String("account", account))
 
1337	Conf.dynamicMutex.Lock()
 
1338	defer Conf.dynamicMutex.Unlock()
 
1341	acc, ok := c.Accounts[account]
 
1343		return fmt.Errorf("%w: account not present", ErrRequest)
 
1348	// Compose new config without modifying existing data structures. If we fail, we
 
1351	nc.Accounts = map[string]config.Account{}
 
1352	for name, a := range c.Accounts {
 
1353		nc.Accounts[name] = a
 
1355	nc.Accounts[account] = acc
 
1357	if err := writeDynamic(ctx, log, nc); err != nil {
 
1358		return fmt.Errorf("writing domains.conf: %w", err)
 
1360	log.Info("account fields saved", slog.String("account", account))
 
1367	TLSModeImmediate TLSMode = 0
 
1368	TLSModeSTARTTLS  TLSMode = 1
 
1369	TLSModeNone      TLSMode = 2
 
1372type ProtocolConfig struct {
 
1378type ClientConfig struct {
 
1380	Submission ProtocolConfig
 
1383// ClientConfigDomain returns a single IMAP and Submission client configuration for
 
1385func ClientConfigDomain(d dns.Domain) (rconfig ClientConfig, rerr error) {
 
1386	var haveIMAP, haveSubmission bool
 
1388	domConf, ok := Conf.Domain(d)
 
1390		return ClientConfig{}, fmt.Errorf("%w: unknown domain", ErrRequest)
 
1393	gather := func(l config.Listener) (done bool) {
 
1394		host := Conf.Static.HostnameDomain
 
1395		if l.Hostname != "" {
 
1396			host = l.HostnameDomain
 
1398		if domConf.ClientSettingsDomain != "" {
 
1399			host = domConf.ClientSettingsDNSDomain
 
1401		if !haveIMAP && l.IMAPS.Enabled {
 
1402			rconfig.IMAP.Host = host
 
1403			rconfig.IMAP.Port = config.Port(l.IMAPS.Port, 993)
 
1404			rconfig.IMAP.TLSMode = TLSModeImmediate
 
1407		if !haveIMAP && l.IMAP.Enabled {
 
1408			rconfig.IMAP.Host = host
 
1409			rconfig.IMAP.Port = config.Port(l.IMAP.Port, 143)
 
1410			rconfig.IMAP.TLSMode = TLSModeSTARTTLS
 
1412				rconfig.IMAP.TLSMode = TLSModeNone
 
1416		if !haveSubmission && l.Submissions.Enabled {
 
1417			rconfig.Submission.Host = host
 
1418			rconfig.Submission.Port = config.Port(l.Submissions.Port, 465)
 
1419			rconfig.Submission.TLSMode = TLSModeImmediate
 
1420			haveSubmission = true
 
1422		if !haveSubmission && l.Submission.Enabled {
 
1423			rconfig.Submission.Host = host
 
1424			rconfig.Submission.Port = config.Port(l.Submission.Port, 587)
 
1425			rconfig.Submission.TLSMode = TLSModeSTARTTLS
 
1427				rconfig.Submission.TLSMode = TLSModeNone
 
1429			haveSubmission = true
 
1431		return haveIMAP && haveSubmission
 
1434	// Look at the public listener first. Most likely the intended configuration.
 
1435	if public, ok := Conf.Static.Listeners["public"]; ok {
 
1440	// Go through the other listeners in consistent order.
 
1441	names := maps.Keys(Conf.Static.Listeners)
 
1443	for _, name := range names {
 
1444		if gather(Conf.Static.Listeners[name]) {
 
1448	return ClientConfig{}, fmt.Errorf("%w: no listeners found for imap and/or submission", ErrRequest)
 
1451// ClientConfigs holds the client configuration for IMAP/Submission for a
 
1453type ClientConfigs struct {
 
1454	Entries []ClientConfigsEntry
 
1457type ClientConfigsEntry struct {
 
1465// ClientConfigsDomain returns the client configs for IMAP/Submission for a
 
1467func ClientConfigsDomain(d dns.Domain) (ClientConfigs, error) {
 
1468	domConf, ok := Conf.Domain(d)
 
1470		return ClientConfigs{}, fmt.Errorf("%w: unknown domain", ErrRequest)
 
1473	c := ClientConfigs{}
 
1474	c.Entries = []ClientConfigsEntry{}
 
1475	var listeners []string
 
1477	for name := range Conf.Static.Listeners {
 
1478		listeners = append(listeners, name)
 
1480	sort.Slice(listeners, func(i, j int) bool {
 
1481		return listeners[i] < listeners[j]
 
1484	note := func(tls bool, requiretls bool) string {
 
1486			return "plain text, no STARTTLS configured"
 
1489			return "STARTTLS required"
 
1491		return "STARTTLS optional"
 
1494	for _, name := range listeners {
 
1495		l := Conf.Static.Listeners[name]
 
1496		host := Conf.Static.HostnameDomain
 
1497		if l.Hostname != "" {
 
1498			host = l.HostnameDomain
 
1500		if domConf.ClientSettingsDomain != "" {
 
1501			host = domConf.ClientSettingsDNSDomain
 
1503		if l.Submissions.Enabled {
 
1504			c.Entries = append(c.Entries, ClientConfigsEntry{"Submission (SMTP)", host, config.Port(l.Submissions.Port, 465), name, "with TLS"})
 
1506		if l.IMAPS.Enabled {
 
1507			c.Entries = append(c.Entries, ClientConfigsEntry{"IMAP", host, config.Port(l.IMAPS.Port, 993), name, "with TLS"})
 
1509		if l.Submission.Enabled {
 
1510			c.Entries = append(c.Entries, ClientConfigsEntry{"Submission (SMTP)", host, config.Port(l.Submission.Port, 587), name, note(l.TLS != nil, !l.Submission.NoRequireSTARTTLS)})
 
1513			c.Entries = append(c.Entries, ClientConfigsEntry{"IMAP", host, config.Port(l.IMAPS.Port, 143), name, note(l.TLS != nil, !l.IMAP.NoRequireSTARTTLS)})
 
1520// IPs returns ip addresses we may be listening/receiving mail on or
 
1521// connecting/sending from to the outside.
 
1522func IPs(ctx context.Context, receiveOnly bool) ([]net.IP, error) {
 
1523	log := pkglog.WithContext(ctx)
 
1525	// Try to gather all IPs we are listening on by going through the config.
 
1526	// If we encounter 0.0.0.0 or ::, we'll gather all local IPs afterwards.
 
1528	var ipv4all, ipv6all bool
 
1529	for _, l := range Conf.Static.Listeners {
 
1530		// If NATed, we don't know our external IPs.
 
1535		if len(l.NATIPs) > 0 {
 
1538		for _, s := range check {
 
1539			ip := net.ParseIP(s)
 
1540			if ip.IsUnspecified() {
 
1541				if ip.To4() != nil {
 
1548			ips = append(ips, ip)
 
1552	// We'll list the IPs on the interfaces. How useful is this? There is a good chance
 
1553	// we're listening on all addresses because of a load balancer/firewall.
 
1554	if ipv4all || ipv6all {
 
1555		ifaces, err := net.Interfaces()
 
1557			return nil, fmt.Errorf("listing network interfaces: %v", err)
 
1559		for _, iface := range ifaces {
 
1560			if iface.Flags&net.FlagUp == 0 {
 
1563			addrs, err := iface.Addrs()
 
1565				return nil, fmt.Errorf("listing addresses for network interface: %v", err)
 
1567			if len(addrs) == 0 {
 
1571			for _, addr := range addrs {
 
1572				ip, _, err := net.ParseCIDR(addr.String())
 
1574					log.Errorx("bad interface addr", err, slog.Any("address", addr))
 
1577				v4 := ip.To4() != nil
 
1578				if ipv4all && v4 || ipv6all && !v4 {
 
1579					ips = append(ips, ip)
 
1589	for _, t := range Conf.Static.Transports {
 
1591			ips = append(ips, t.Socks.IPs...)