1// Package webadmin is a web app for the mox administrator for viewing and changing
2// the configuration, like creating/removing accounts, viewing DMARC and TLS
3// reports, check DNS records for a domain, change the webserver configuration,
13 cryptorand "crypto/rand"
40 "golang.org/x/text/unicode/norm"
42 "github.com/mjl-/adns"
44 "github.com/mjl-/bstore"
45 "github.com/mjl-/sherpa"
46 "github.com/mjl-/sherpadoc"
47 "github.com/mjl-/sherpaprom"
49 "github.com/mjl-/mox/admin"
50 "github.com/mjl-/mox/config"
51 "github.com/mjl-/mox/dkim"
52 "github.com/mjl-/mox/dmarc"
53 "github.com/mjl-/mox/dmarcdb"
54 "github.com/mjl-/mox/dmarcrpt"
55 "github.com/mjl-/mox/dns"
56 "github.com/mjl-/mox/dnsbl"
57 "github.com/mjl-/mox/metrics"
58 "github.com/mjl-/mox/mlog"
59 mox "github.com/mjl-/mox/mox-"
60 "github.com/mjl-/mox/moxvar"
61 "github.com/mjl-/mox/mtasts"
62 "github.com/mjl-/mox/mtastsdb"
63 "github.com/mjl-/mox/publicsuffix"
64 "github.com/mjl-/mox/queue"
65 "github.com/mjl-/mox/smtp"
66 "github.com/mjl-/mox/spf"
67 "github.com/mjl-/mox/store"
68 "github.com/mjl-/mox/tlsrpt"
69 "github.com/mjl-/mox/tlsrptdb"
70 "github.com/mjl-/mox/webauth"
73var pkglog = mlog.New("webadmin", nil)
76var adminapiJSON []byte
84var webadminFile = &mox.WebappFile{
87 HTMLPath: filepath.FromSlash("webadmin/admin.html"),
88 JSPath: filepath.FromSlash("webadmin/admin.js"),
89 CustomStem: "webadmin",
92var adminDoc = mustParseAPI("admin", adminapiJSON)
94func mustParseAPI(api string, buf []byte) (doc sherpadoc.Section) {
95 err := json.Unmarshal(buf, &doc)
97 pkglog.Fatalx("parsing webadmin api docs", err, slog.String("api", api))
102var sherpaHandlerOpts *sherpa.HandlerOpts
104func makeSherpaHandler(cookiePath string, isForwarded bool) (http.Handler, error) {
105 return sherpa.NewHandler("/api/", moxvar.Version, Admin{cookiePath, isForwarded}, &adminDoc, sherpaHandlerOpts)
109 collector, err := sherpaprom.NewCollector("moxadmin", nil)
111 pkglog.Fatalx("creating sherpa prometheus collector", err)
114 sherpaHandlerOpts = &sherpa.HandlerOpts{Collector: collector, AdjustFunctionNames: "none", NoCORS: true}
116 _, err = makeSherpaHandler("", false)
118 pkglog.Fatalx("sherpa handler", err)
121 mox.NewWebadminHandler = func(basePath string, isForwarded bool) http.Handler {
122 return http.HandlerFunc(Handler(basePath, isForwarded))
126// Handler returns a handler for the webadmin endpoints, customized for the
128func Handler(cookiePath string, isForwarded bool) func(w http.ResponseWriter, r *http.Request) {
129 sh, err := makeSherpaHandler(cookiePath, isForwarded)
130 return func(w http.ResponseWriter, r *http.Request) {
132 http.Error(w, "500 - internal server error - cannot handle requests", http.StatusInternalServerError)
135 handle(sh, isForwarded, w, r)
139// Admin exports web API functions for the admin web interface. All its methods are
140// exported under api/. Function calls require valid HTTP Authentication
141// credentials of a user.
143 cookiePath string // From listener, for setting authentication cookies.
144 isForwarded bool // From listener, whether we look at X-Forwarded-* headers.
149var requestInfoCtxKey ctxKey = "requestInfo"
151type requestInfo struct {
152 SessionToken store.SessionToken
153 Response http.ResponseWriter
154 Request *http.Request // For Proto and TLS connection state during message submit.
157func handle(apiHandler http.Handler, isForwarded bool, w http.ResponseWriter, r *http.Request) {
158 ctx := context.WithValue(r.Context(), mlog.CidKey, mox.Cid())
159 log := pkglog.WithContext(ctx).With(slog.String("adminauth", ""))
161 // HTML/JS can be retrieved without authentication.
162 if r.URL.Path == "/" {
165 webadminFile.Serve(ctx, log, w, r)
167 http.Error(w, "405 - method not allowed - use get", http.StatusMethodNotAllowed)
170 } else if r.URL.Path == "/licenses.txt" {
173 w.Header().Set("Content-Type", "text/plain; charset=utf-8")
176 http.Error(w, "405 - method not allowed - use get", http.StatusMethodNotAllowed)
181 isAPI := strings.HasPrefix(r.URL.Path, "/api/")
182 // Only allow POST for calls, they will not work cross-domain without CORS.
183 if isAPI && r.URL.Path != "/api/" && r.Method != "POST" {
184 http.Error(w, "405 - method not allowed - use post", http.StatusMethodNotAllowed)
188 // All other URLs, except the login endpoint require some authentication.
189 var sessionToken store.SessionToken
190 if r.URL.Path != "/api/LoginPrep" && r.URL.Path != "/api/Login" {
192 _, sessionToken, _, ok = webauth.Check(ctx, log, webauth.Admin, "webadmin", isForwarded, w, r, isAPI, isAPI, false)
194 // Response has been written already.
200 reqInfo := requestInfo{sessionToken, w, r}
201 ctx = context.WithValue(ctx, requestInfoCtxKey, reqInfo)
202 apiHandler.ServeHTTP(w, r.WithContext(ctx))
209func xcheckf(ctx context.Context, err error, format string, args ...any) {
213 // If caller tried saving a config that is invalid, or because of a bad request, cause a user error.
214 if errors.Is(err, mox.ErrConfig) || errors.Is(err, admin.ErrRequest) {
215 xcheckuserf(ctx, err, format, args...)
218 msg := fmt.Sprintf(format, args...)
219 errmsg := fmt.Sprintf("%s: %s", msg, err)
220 pkglog.WithContext(ctx).Errorx(msg, err)
221 code := "server:error"
222 if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
225 panic(&sherpa.Error{Code: code, Message: errmsg})
228func xcheckuserf(ctx context.Context, err error, format string, args ...any) {
232 msg := fmt.Sprintf(format, args...)
233 errmsg := fmt.Sprintf("%s: %s", msg, err)
234 pkglog.WithContext(ctx).Errorx(msg, err)
235 panic(&sherpa.Error{Code: "user:error", Message: errmsg})
238func xusererrorf(ctx context.Context, format string, args ...any) {
239 msg := fmt.Sprintf(format, args...)
240 pkglog.WithContext(ctx).Error(msg)
241 panic(&sherpa.Error{Code: "user:error", Message: msg})
244// LoginPrep returns a login token, and also sets it as cookie. Both must be
245// present in the call to Login.
246func (w Admin) LoginPrep(ctx context.Context) string {
247 log := pkglog.WithContext(ctx)
248 reqInfo := ctx.Value(requestInfoCtxKey).(requestInfo)
251 cryptorand.Read(data[:])
252 loginToken := base64.RawURLEncoding.EncodeToString(data[:])
254 webauth.LoginPrep(ctx, log, "webadmin", w.cookiePath, w.isForwarded, reqInfo.Response, reqInfo.Request, loginToken)
259// Login returns a session token for the credentials, or fails with error code
260// "user:badLogin". Call LoginPrep to get a loginToken.
261func (w Admin) Login(ctx context.Context, loginToken, password string) store.CSRFToken {
262 log := pkglog.WithContext(ctx)
263 reqInfo := ctx.Value(requestInfoCtxKey).(requestInfo)
265 csrfToken, err := webauth.Login(ctx, log, webauth.Admin, "webadmin", w.cookiePath, w.isForwarded, reqInfo.Response, reqInfo.Request, loginToken, "", password)
266 if _, ok := err.(*sherpa.Error); ok {
269 xcheckf(ctx, err, "login")
273// Logout invalidates the session token.
274func (w Admin) Logout(ctx context.Context) {
275 log := pkglog.WithContext(ctx)
276 reqInfo := ctx.Value(requestInfoCtxKey).(requestInfo)
278 err := webauth.Logout(ctx, log, webauth.Admin, "webadmin", w.cookiePath, w.isForwarded, reqInfo.Response, reqInfo.Request, "", reqInfo.SessionToken)
279 xcheckf(ctx, err, "logout")
282// Version returns the version, goos and goarch.
283func (w Admin) Version(ctx context.Context) (version, goos, goarch string) {
284 return moxvar.Version, runtime.GOOS, runtime.GOARCH
290 Instructions []string
293type DNSSECResult struct {
297type IPRevCheckResult struct {
298 Hostname dns.Domain // This hostname, IPs must resolve back to this.
299 IPNames map[string][]string // IP to names.
309type MXCheckResult struct {
314type TLSCheckResult struct {
318type DANECheckResult struct {
322type SPFRecord struct {
326type SPFCheckResult struct {
328 DomainRecord *SPFRecord
330 HostRecord *SPFRecord
334type DKIMCheckResult struct {
339type DKIMRecord struct {
345type DMARCRecord struct {
349type DMARCCheckResult struct {
356type TLSRPTRecord struct {
360type TLSRPTCheckResult struct {
366type MTASTSRecord struct {
369type MTASTSCheckResult struct {
373 Policy *mtasts.Policy
377type SRVConfCheckResult struct {
378 SRVs map[string][]net.SRV // Service (e.g. "_imaps") to records.
382type AutoconfCheckResult struct {
383 ClientSettingsDomainIPs []string
388type AutodiscoverSRV struct {
393type AutodiscoverCheckResult struct {
394 Records []AutodiscoverSRV
398// CheckResult is the analysis of a domain, its actual configuration (DNS, TLS,
399// connectivity) and the mox configuration. It includes configuration instructions
400// (e.g. DNS records), and warnings and errors encountered.
401type CheckResult struct {
404 IPRev IPRevCheckResult
410 DMARC DMARCCheckResult
411 HostTLSRPT TLSRPTCheckResult
412 DomainTLSRPT TLSRPTCheckResult
413 MTASTS MTASTSCheckResult
414 SRVConf SRVConfCheckResult
415 Autoconf AutoconfCheckResult
416 Autodiscover AutodiscoverCheckResult
419// logPanic can be called with a defer from a goroutine to prevent the entire program from being shutdown in case of a panic.
420func logPanic(ctx context.Context) {
425 pkglog.WithContext(ctx).Error("recover from panic", slog.Any("panic", x))
427 metrics.PanicInc(metrics.Webadmin)
430// return IPs we may be listening on.
431func xlistenIPs(ctx context.Context, receiveOnly bool) []net.IP {
432 ips, err := mox.IPs(ctx, receiveOnly)
433 xcheckf(ctx, err, "listing ips")
437// return IPs from which we may be sending.
438func xsendingIPs(ctx context.Context) []net.IP {
439 ips, err := mox.IPs(ctx, false)
440 xcheckf(ctx, err, "listing ips")
444// CheckDomain checks the configuration for the domain, such as MX, SMTP STARTTLS,
445// SPF, DKIM, DMARC, TLSRPT, MTASTS, autoconfig, autodiscover.
446func (Admin) CheckDomain(ctx context.Context, domainName string) (r CheckResult) {
447 // todo future: should run these checks without a DNS cache so recent changes are picked up.
449 resolver := dns.StrictResolver{Pkg: "check", Log: pkglog.WithContext(ctx).Logger}
450 dialer := &net.Dialer{Timeout: 10 * time.Second}
451 nctx, cancel := context.WithTimeout(ctx, 30*time.Second)
453 return checkDomain(nctx, resolver, dialer, domainName)
456func unptr[T any](l []*T) []T {
460 r := make([]T, len(l))
461 for i, e := range l {
467func checkDomain(ctx context.Context, resolver dns.Resolver, dialer *net.Dialer, domainName string) (r CheckResult) {
468 log := pkglog.WithContext(ctx)
470 domain, xerr := dns.ParseDomain(domainName)
471 xcheckuserf(ctx, xerr, "parsing domain")
473 domConf, ok := mox.Conf.Domain(domain)
475 panic(&sherpa.Error{Code: "user:notFound", Message: "domain not found"})
478 listenIPs := xlistenIPs(ctx, true)
479 isListenIP := func(ip net.IP) bool {
480 return slices.ContainsFunc(listenIPs, ip.Equal)
483 addf := func(l *[]string, format string, args ...any) {
484 *l = append(*l, fmt.Sprintf(format, args...))
487 // Host must be an absolute dns name, ending with a dot.
488 lookupIPs := func(errors *[]string, host string) (ips []string, ourIPs, notOurIPs []net.IP, rerr error) {
489 addrs, _, err := resolver.LookupHost(ctx, host)
491 addf(errors, "Looking up %q: %s", host, err)
492 return nil, nil, nil, err
494 for _, addr := range addrs {
495 ip := net.ParseIP(addr)
497 addf(errors, "Bad IP %q", addr)
500 ips = append(ips, ip.String())
502 ourIPs = append(ourIPs, ip)
504 notOurIPs = append(notOurIPs, ip)
507 return ips, ourIPs, notOurIPs, nil
510 checkTLS := func(errors *[]string, host string, ips []string, port string) {
516 RootCAs: mox.Conf.Static.TLS.CertPool,
519 for _, ip := range ips {
520 conn, err := d.DialContext(ctx, "tcp", net.JoinHostPort(ip, port))
522 addf(errors, "TLS connection to hostname %q, IP %q: %s", host, ip, err)
525 log.Check(err, "closing tcp connection")
530 // If at least one listener with SMTP enabled has unspecified NATed IPs, we'll skip
531 // some checks related to these IPs.
532 var isNAT, isUnspecifiedNAT bool
533 for _, l := range mox.Conf.Static.Listeners {
538 isUnspecifiedNAT = true
541 if len(l.NATIPs) > 0 {
546 var wg sync.WaitGroup
554 // Some DNSSEC-verifying resolvers return unauthentic data for ".", so we check "com".
555 _, result, err := resolver.LookupNS(ctx, "com.")
557 addf(&r.DNSSEC.Errors, "Looking up NS for DNS root (.) to check support in resolver for DNSSEC-verification: %s", err)
558 } else if !result.Authentic {
559 addf(&r.DNSSEC.Warnings, `It looks like the DNS resolvers configured on your system do not verify DNSSEC, or aren't trusted (by having loopback IPs or through "options trust-ad" in /etc/resolv.conf). Without DNSSEC, outbound delivery with SMTP uses unprotected MX records, and SMTP STARTTLS connections cannot verify the TLS certificate with DANE (based on public keys in DNS), and will fall back to either MTA-STS for verification, or use "opportunistic TLS" with no certificate verification.`)
561 _, result, _ := resolver.LookupMX(ctx, domain.ASCII+".")
562 if !result.Authentic {
563 addf(&r.DNSSEC.Warnings, `DNS records for this domain (zone) are not DNSSEC-signed. Mail servers sending email to your domain, or receiving email from your domain, cannot verify that the MX/SPF/DKIM/DMARC/MTA-STS records they see are authentic.`)
567 addf(&r.DNSSEC.Instructions, `Enable DNSSEC-signing of the DNS records of your domain (zone) at your DNS hosting provider.`)
569 addf(&r.DNSSEC.Instructions, `If your DNS records are already DNSSEC-signed, you may not have a DNSSEC-verifying recursive resolver configured. Install unbound, ensure it has DNSSEC root keys (see unbound-anchor), and enable support for "extended dns errors" (EDE, available since unbound v1.16.0). Test with "dig com. ns" and look for "ad" (authentic data) in response "flags".
571cat <<EOF >/etc/unbound/unbound.conf.d/ede.conf
585 // For each mox.Conf.SpecifiedSMTPListenIPs and all NATIPs, and each IP for
586 // mox.Conf.HostnameDomain, check if they resolve back to the host name.
587 hostIPs := map[dns.Domain][]net.IP{}
588 ips, _, err := resolver.LookupIP(ctx, "ip", mox.Conf.Static.HostnameDomain.ASCII+".")
590 addf(&r.IPRev.Errors, "Looking up IPs for hostname: %s", err)
593 gatherMoreIPs := func(publicIPs []net.IP) {
595 for _, ip := range publicIPs {
596 for _, xip := range ips {
601 ips = append(ips, ip)
605 gatherMoreIPs(mox.Conf.Static.SpecifiedSMTPListenIPs)
607 for _, l := range mox.Conf.Static.Listeners {
612 for _, ip := range l.NATIPs {
613 natips = append(natips, net.ParseIP(ip))
615 gatherMoreIPs(natips)
617 hostIPs[mox.Conf.Static.HostnameDomain] = ips
619 iplist := func(ips []net.IP) string {
621 for _, ip := range ips {
622 ipstrs = append(ipstrs, ip.String())
624 return strings.Join(ipstrs, ", ")
627 r.IPRev.Hostname = mox.Conf.Static.HostnameDomain
628 r.IPRev.Instructions = []string{
629 fmt.Sprintf("Ensure IPs %s have reverse address %s.", iplist(ips), mox.Conf.Static.HostnameDomain.ASCII),
632 // If we have a socks transport, also check its host and IP.
633 for tname, t := range mox.Conf.Static.Transports {
635 hostIPs[t.Socks.Hostname] = append(hostIPs[t.Socks.Hostname], t.Socks.IPs...)
636 instr := fmt.Sprintf("For SOCKS transport %s, ensure IPs %s have reverse address %s.", tname, iplist(t.Socks.IPs), t.Socks.Hostname)
637 r.IPRev.Instructions = append(r.IPRev.Instructions, instr)
647 results := make(chan result)
649 for host, ips := range hostIPs {
650 for _, ip := range ips {
655 addrs, _, err := resolver.LookupAddr(ctx, s)
656 results <- result{host, s, addrs, err}
660 r.IPRev.IPNames = map[string][]string{}
663 host, addrs, ip, err := lr.Host, lr.Addrs, lr.IP, lr.Err
665 addf(&r.IPRev.Errors, "Looking up reverse name for %s of %s: %v", ip, host, err)
669 for i, a := range addrs {
670 a = strings.TrimRight(a, ".")
672 ad, err := dns.ParseDomain(a)
674 addf(&r.IPRev.Errors, "Parsing reverse name %q for %s: %v", a, ip, err)
680 if !match && !isNAT && host == mox.Conf.Static.HostnameDomain {
681 addf(&r.IPRev.Warnings, "IP %s with name(s) %s is forward confirmed, but does not match hostname %s.", ip, strings.Join(addrs, ","), host)
683 r.IPRev.IPNames[ip] = addrs
686 // Linux machines are often initially set up with a loopback IP for the hostname in
687 // /etc/hosts, presumably because it isn't known if their external IPs are static.
688 // For mail servers, they should certainly be static. The quickstart would also
689 // have warned about this, but could have been missed/ignored.
690 for _, ip := range ips {
692 addf(&r.IPRev.Errors, "Hostname %s resolves to loopback IP %s, this will likely prevent email delivery to local accounts from working. The loopback IP was probably configured in /etc/hosts at system installation time. Replace the loopback IP with your actual external IPs in /etc/hosts.", mox.Conf.Static.HostnameDomain, ip.String())
703 mxs, _, err := resolver.LookupMX(ctx, domain.ASCII+".")
705 addf(&r.MX.Errors, "Looking up MX records for %s: %s", domain, err)
707 r.MX.Records = make([]MX, len(mxs))
708 for i, mx := range mxs {
709 r.MX.Records[i] = MX{mx.Host, int(mx.Pref), nil}
711 if len(mxs) == 1 && mxs[0].Host == "." {
712 addf(&r.MX.Errors, `MX records consists of explicit null mx record (".") indicating that domain does not accept email.`)
715 for i, mx := range mxs {
716 ips, ourIPs, notOurIPs, err := lookupIPs(&r.MX.Errors, mx.Host)
718 addf(&r.MX.Errors, "Looking up IPs for mx host %q: %s", mx.Host, err)
720 r.MX.Records[i].IPs = ips
721 if isUnspecifiedNAT {
724 if len(ourIPs) == 0 {
725 addf(&r.MX.Errors, "None of the IPs that mx %q points to is ours: %v", mx.Host, notOurIPs)
726 } else if len(notOurIPs) > 0 {
727 addf(&r.MX.Errors, "Some of the IPs that mx %q points to are not ours: %v", mx.Host, notOurIPs)
731 r.MX.Instructions = []string{
732 fmt.Sprintf("Ensure a DNS MX record like the following exists:\n\n\t%s MX 10 %s\n\nWithout the trailing dot, the name would be interpreted as relative to the domain.", domain.ASCII+".", mox.Conf.Static.HostnameDomain.ASCII+"."),
736 // TLS, mostly checking certificate expiration and CA trust.
737 // todo: should add checks about the listeners (which aren't specific to domains) somewhere else, not on the domain page with this checkDomain call. i.e. submissions, imap starttls, imaps.
743 // MTA-STS, autoconfig, autodiscover are checked in their sections.
745 // Dial a single MX host with given IP and perform STARTTLS handshake.
746 dialSMTPSTARTTLS := func(host, ip string) error {
747 conn, err := dialer.DialContext(ctx, "tcp", net.JoinHostPort(ip, "25"))
754 log.Check(err, "closing tcp connection")
758 end := time.Now().Add(10 * time.Second)
759 cctx, cancel := context.WithTimeout(ctx, 10*time.Second)
761 err = conn.SetDeadline(end)
762 log.WithContext(ctx).Check(err, "setting deadline")
764 br := bufio.NewReader(conn)
765 _, err = br.ReadString('\n')
767 return fmt.Errorf("reading SMTP banner from remote: %s", err)
769 if _, err := fmt.Fprintf(conn, "EHLO moxtest\r\n"); err != nil {
770 return fmt.Errorf("writing SMTP EHLO to remote: %s", err)
773 line, err := br.ReadString('\n')
775 return fmt.Errorf("reading SMTP EHLO response from remote: %s", err)
777 if strings.HasPrefix(line, "250-") {
780 if strings.HasPrefix(line, "250 ") {
783 return fmt.Errorf("unexpected response to SMTP EHLO from remote: %q", strings.TrimSuffix(line, "\r\n"))
785 if _, err := fmt.Fprintf(conn, "STARTTLS\r\n"); err != nil {
786 return fmt.Errorf("writing SMTP STARTTLS to remote: %s", err)
788 line, err := br.ReadString('\n')
790 return fmt.Errorf("reading response to SMTP STARTTLS from remote: %s", err)
792 if !strings.HasPrefix(line, "220 ") {
793 return fmt.Errorf("SMTP STARTTLS response from remote not 220 OK: %q", strings.TrimSuffix(line, "\r\n"))
795 config := &tls.Config{
797 RootCAs: mox.Conf.Static.TLS.CertPool,
799 tlsconn := tls.Client(conn, config)
800 if err := tlsconn.HandshakeContext(cctx); err != nil {
801 return fmt.Errorf("TLS handshake after SMTP STARTTLS: %s", err)
805 log.Check(err, "closing smtp connection")
810 checkSMTPSTARTTLS := func() {
811 // Initial errors are ignored, will already have been warned about by MX checks.
812 mxs, _, err := resolver.LookupMX(ctx, domain.ASCII+".")
816 if len(mxs) == 1 && mxs[0].Host == "." {
819 for _, mx := range mxs {
820 ips, _, _, err := lookupIPs(&r.MX.Errors, mx.Host)
825 for _, ip := range ips {
826 if err := dialSMTPSTARTTLS(mx.Host, ip); err != nil {
827 addf(&r.TLS.Errors, "SMTP connection with STARTTLS to MX hostname %q IP %s: %s", mx.Host, ip, err)
843 daneRecords := func(l config.Listener) map[string]struct{} {
847 records := map[string]struct{}{}
848 addRecord := func(privKey crypto.Signer) {
849 spkiBuf, err := x509.MarshalPKIXPublicKey(privKey.Public())
851 addf(&r.DANE.Errors, "marshal SubjectPublicKeyInfo for DANE record: %v", err)
854 sum := sha256.Sum256(spkiBuf)
856 Usage: adns.TLSAUsageDANEEE,
857 Selector: adns.TLSASelectorSPKI,
858 MatchType: adns.TLSAMatchTypeSHA256,
861 records[r.Record()] = struct{}{}
863 for _, privKey := range l.TLS.HostPrivateRSA2048Keys {
866 for _, privKey := range l.TLS.HostPrivateECDSAP256Keys {
872 expectedDANERecords := func(host string) map[string]struct{} {
873 for _, l := range mox.Conf.Static.Listeners {
874 if l.HostnameDomain.ASCII == host {
875 return daneRecords(l)
878 public := mox.Conf.Static.Listeners["public"]
879 if mox.Conf.Static.HostnameDomain.ASCII == host && public.HostnameDomain.ASCII == "" {
880 return daneRecords(public)
885 mxl, result, err := resolver.LookupMX(ctx, domain.ASCII+".")
887 addf(&r.DANE.Errors, "Looking up MX hosts to check for DANE records: %s", err)
889 if !result.Authentic {
890 addf(&r.DANE.Warnings, "DANE is inactive because MX records are not DNSSEC-signed.")
892 for _, mx := range mxl {
893 expect := expectedDANERecords(mx.Host)
895 tlsal, tlsaResult, err := resolver.LookupTLSA(ctx, 25, "tcp", mx.Host+".")
896 if dns.IsNotFound(err) {
898 addf(&r.DANE.Errors, "No DANE records for MX host %s, expected: %s.", mx.Host, strings.Join(slices.Collect(maps.Keys(expect)), "; "))
901 } else if err != nil {
902 addf(&r.DANE.Errors, "Looking up DANE records for MX host %s: %v", mx.Host, err)
904 } else if !tlsaResult.Authentic && len(tlsal) > 0 {
905 addf(&r.DANE.Errors, "DANE records exist for MX host %s, but are not DNSSEC-signed.", mx.Host)
908 extra := map[string]struct{}{}
909 for _, e := range tlsal {
911 if _, ok := expect[s]; ok {
914 extra[s] = struct{}{}
918 l := slices.Sorted(maps.Keys(expect))
919 addf(&r.DANE.Errors, "Missing DANE records of type TLSA for MX host _25._tcp.%s: %s", mx.Host, strings.Join(l, "; "))
922 l := slices.Sorted(maps.Keys(extra))
923 addf(&r.DANE.Errors, "Unexpected DANE records of type TLSA for MX host _25._tcp.%s: %s", mx.Host, strings.Join(l, "; "))
928 public := mox.Conf.Static.Listeners["public"]
929 pubDom := public.HostnameDomain
930 if pubDom.ASCII == "" {
931 pubDom = mox.Conf.Static.HostnameDomain
933 records := slices.Sorted(maps.Keys(daneRecords(public)))
934 if len(records) > 0 {
935 var instr strings.Builder
936 instr.WriteString("Ensure the DNS records below exist. These records are for the whole machine, not per domain, so create them only once. Make sure DNSSEC is enabled, otherwise the records have no effect. The records indicate that a remote mail server trying to deliver email with SMTP (TCP port 25) must verify the TLS certificate with DANE-EE (3), based on the certificate public key (\"SPKI\", 1) that is SHA2-256-hashed (1) to the hexadecimal hash. DANE-EE verification means only the certificate or public key is verified, not whether the certificate is signed by a (centralized) certificate authority (CA), is expired, or matches the host name.\n\n")
937 for _, r := range records {
938 instr.WriteString(fmt.Sprintf("\t_25._tcp.%s. TLSA %s\n", pubDom.ASCII, r))
940 addf(&r.DANE.Instructions, "%s", instr.String())
942 addf(&r.DANE.Warnings, "DANE not configured: no static TLS host keys.")
944 const instr = "Add static TLS keys for use with DANE to mox.conf under: Listeners, public, TLS, HostPrivateKeyFiles.\n\nIf automatic TLS certificate management with ACME is configured, run \"mox config ensureacmehostprivatekeys\" to generate static TLS keys and to print a snippet for \"HostPrivateKeyFiles\" for inclusion in mox.conf.\n\nIf TLS keys and certificates are managed externally, configure the TLS keys manually under \"HostPrivateKeyFiles\" in mox.conf, and make sure new TLS keys are not generated for each new certificate (look for an option to \"reuse private keys\" when doing ACME). Important: Before using new TLS keys, corresponding new DANE (TLSA) DNS records must be published (taking TTL into account to let the previous records expire). Using new TLS keys without updating DANE (TLSA) DNS records will cause DANE verification failures, breaking incoming deliveries.\n\nWith \"HostPrivateKeyFiles\" configured, DNS records for DANE based on those TLS keys will be suggested, and future DNS checks will look for those DNS records. Once those DNS records are published, DANE is active for all domains with an MX record pointing to the host."
945 addf(&r.DANE.Instructions, instr)
950 // todo: add warnings if we have Transports with submission? admin should ensure their IPs are in the SPF record. it may be an IP(net), or an include. that means we cannot easily check for it. and should we first check the transport can be used from this domain (or an account that has this domain?). also see DKIM.
956 ips := mox.DomainSPFIPs()
958 // Verify a domain with the configured IPs that do SMTP.
959 verifySPF := func(isHost bool, domain dns.Domain) (string, *SPFRecord, spf.Record) {
965 _, txt, record, _, err := spf.Lookup(ctx, log.Logger, resolver, domain)
967 addf(&r.SPF.Errors, "Looking up %s SPF record: %s", kind, err)
969 var xrecord *SPFRecord
971 xrecord = &SPFRecord{*record}
978 checkSPFIP := func(ip net.IP) {
983 spfr.Directives = append(spfr.Directives, spf.Directive{Mechanism: mechanism, IP: ip})
991 MailFromLocalpart: "postmaster",
992 MailFromDomain: domain,
993 HelloDomain: dns.IPDomain{Domain: domain},
994 LocalIP: net.ParseIP("127.0.0.1"),
995 LocalHostname: dns.Domain{ASCII: "localhost"},
997 status, mechanism, expl, _, err := spf.Evaluate(ctx, log.Logger, record, resolver, args)
999 addf(&r.SPF.Errors, "Evaluating IP %q against %s SPF record: %s", ip, kind, err)
1000 } else if status != spf.StatusPass {
1001 addf(&r.SPF.Errors, "IP %q does not pass %s SPF evaluation, status not \"pass\" but %q (mechanism %q, explanation %q)", ip, kind, status, mechanism, expl)
1005 for _, ip := range ips {
1009 spfr.Directives = append(spfr.Directives, spf.Directive{Mechanism: "mx"})
1016 spfr.Directives = append(spfr.Directives, spf.Directive{Qualifier: qual, Mechanism: "all"})
1017 return txt, xrecord, spfr
1020 // Check SPF record for domain.
1021 var dspfr spf.Record
1022 r.SPF.DomainTXT, r.SPF.DomainRecord, dspfr = verifySPF(false, domain)
1023 // todo: possibly check all hosts for MX records? assuming they are also sending mail servers.
1024 r.SPF.HostTXT, r.SPF.HostRecord, _ = verifySPF(true, mox.Conf.Static.HostnameDomain)
1027 addf(&r.SPF.Warnings, `No explicitly configured IPs found to check SPF policy against. Consider configuring public IPs instead of unspecified addresses (0.0.0.0 and/or ::) in the "public" listener in mox.conf, or NATIPs in case of NAT.`)
1030 dtxt, err := dspfr.Record()
1032 addf(&r.SPF.Errors, "Making SPF record for instructions: %s", err)
1034 domainspf := fmt.Sprintf("%s TXT %s", domain.ASCII+".", mox.TXTStrings(dtxt))
1037 hostspf := fmt.Sprintf(`%s TXT "v=spf1 a -all"`, mox.Conf.Static.HostnameDomain.ASCII+".")
1039 addf(&r.SPF.Instructions, "Ensure DNS TXT records like the following exists:\n\n\t%s\n\t%s\n\nIf you have an existing mail setup, with other hosts also sending mail for you domain, you should add those IPs as well. You could replace \"-all\" with \"~all\" to treat mail sent from unlisted IPs as \"softfail\", or with \"?all\" for \"neutral\".", domainspf, hostspf)
1043 // todo: add warnings if we have Transports with submission? admin should ensure DKIM records exist. we cannot easily check if they actually exist though. and should we first check the transport can be used from this domain (or an account that has this domain?). also see SPF.
1049 var missing []string
1050 for sel, selc := range domConf.DKIM.Selectors {
1051 _, record, txt, _, err := dkim.Lookup(ctx, log.Logger, resolver, selc.Domain, domain)
1053 missing = append(missing, sel)
1054 if errors.Is(err, dkim.ErrNoRecord) {
1055 addf(&r.DKIM.Errors, "No DKIM DNS record for selector %q.", sel)
1056 } else if errors.Is(err, dkim.ErrSyntax) {
1057 addf(&r.DKIM.Errors, "Parsing DKIM DNS record for selector %q: %s", sel, err)
1059 addf(&r.DKIM.Errors, "Fetching DKIM record for selector %q: %s", sel, err)
1063 r.DKIM.Records = append(r.DKIM.Records, DKIMRecord{sel, txt, record})
1064 pubKey := selc.Key.Public()
1066 switch k := pubKey.(type) {
1067 case *rsa.PublicKey:
1069 pk, err = x509.MarshalPKIXPublicKey(k)
1071 addf(&r.DKIM.Errors, "Marshal public key for %q to compare against DNS: %s", sel, err)
1074 case ed25519.PublicKey:
1077 addf(&r.DKIM.Errors, "Internal error: unknown public key type %T.", pubKey)
1081 if record != nil && !bytes.Equal(record.Pubkey, pk) {
1082 addf(&r.DKIM.Errors, "For selector %q, the public key in DKIM DNS TXT record does not match with configured private key.", sel)
1083 missing = append(missing, sel)
1087 if len(domConf.DKIM.Selectors) == 0 {
1088 addf(&r.DKIM.Errors, "No DKIM configuration, add a key to the configuration file, and instructions for DNS records will appear here.")
1091 for _, sel := range missing {
1092 dkimr := dkim.Record{
1094 Hashes: []string{"sha256"},
1095 PublicKey: domConf.DKIM.Selectors[sel].Key.Public(),
1097 switch dkimr.PublicKey.(type) {
1098 case *rsa.PublicKey:
1099 case ed25519.PublicKey:
1100 dkimr.Key = "ed25519"
1102 addf(&r.DKIM.Errors, "Internal error: unknown public key type %T.", dkimr.PublicKey)
1104 txt, err := dkimr.Record()
1106 addf(&r.DKIM.Errors, "Making DKIM record for instructions: %s", err)
1109 instr += fmt.Sprintf("\n\t%s._domainkey.%s TXT %s\n", sel, domain.ASCII+".", mox.TXTStrings(txt))
1112 instr = "Ensure the following DNS record(s) exists, so mail servers receiving emails from this domain can verify the signatures in the mail headers:\n" + instr
1113 addf(&r.DKIM.Instructions, "%s", instr)
1123 _, dmarcDomain, record, txt, _, err := dmarc.Lookup(ctx, log.Logger, resolver, domain)
1125 addf(&r.DMARC.Errors, "Looking up DMARC record: %s", err)
1126 } else if record == nil {
1127 addf(&r.DMARC.Errors, "No DMARC record")
1129 r.DMARC.Domain = dmarcDomain.Name()
1132 r.DMARC.Record = &DMARCRecord{*record}
1134 if record != nil && record.Policy == "none" {
1135 addf(&r.DMARC.Warnings, "DMARC policy is in test mode (p=none), do not forget to change to p=reject or p=quarantine after test period has been completed.")
1137 if record != nil && record.SubdomainPolicy == "none" {
1138 addf(&r.DMARC.Warnings, "DMARC subdomain policy is in test mode (sp=none), do not forget to change to sp=reject or sp=quarantine after test period has been completed.")
1140 if record != nil && len(record.AggregateReportAddresses) == 0 {
1141 addf(&r.DMARC.Warnings, "It is recommended you specify you would like aggregate reports about delivery success in the DMARC record, see instructions.")
1144 dmarcr := dmarc.DefaultRecord
1145 dmarcr.Policy = "reject"
1148 if domConf.DMARC != nil {
1149 // If the domain is in a different Organizational Domain, the receiving domain
1150 // needs a special DNS record to opt-in to receiving reports. We check for that
1153 orgDom := publicsuffix.Lookup(ctx, log.Logger, domain)
1154 destOrgDom := publicsuffix.Lookup(ctx, log.Logger, domConf.DMARC.DNSDomain)
1155 if orgDom != destOrgDom {
1156 accepts, status, _, _, _, err := dmarc.LookupExternalReportsAccepted(ctx, log.Logger, resolver, domain, domConf.DMARC.DNSDomain)
1157 if status != dmarc.StatusNone {
1158 addf(&r.DMARC.Errors, "Checking if external destination accepts reports: %s", err)
1159 } else if !accepts {
1160 addf(&r.DMARC.Errors, "External destination does not accept reports (%s)", err)
1162 extInstr = fmt.Sprintf("Ensure a DNS TXT record exists in the domain of the destination address to opt-in to receiving reports from this domain:\n\n\t%s._report._dmarc.%s. TXT \"v=DMARC1;\"\n\n", domain.ASCII, domConf.DMARC.DNSDomain.ASCII)
1167 Opaque: smtp.NewAddress(domConf.DMARC.ParsedLocalpart, domConf.DMARC.DNSDomain).Pack(false),
1169 uristr := uri.String()
1170 dmarcr.AggregateReportAddresses = []dmarc.URI{
1171 {Address: uristr, MaxSize: 10, Unit: "m"},
1176 for _, addr := range record.AggregateReportAddresses {
1177 if addr.Address == uristr {
1183 addf(&r.DMARC.Errors, "Configured DMARC reporting address is not present in record.")
1187 addf(&r.DMARC.Instructions, `Configure a DMARC destination in domain in config file.`)
1189 instr := fmt.Sprintf("Ensure a DNS TXT record like the following exists:\n\n\t_dmarc.%s TXT %s\n\nYou can start with testing mode by replacing p=reject with p=none. You can also request for the policy to be applied to a percentage of emails instead of all, by adding pct=X, with X between 0 and 100. Keep in mind that receiving mail servers will apply some anti-spam assessment regardless of the policy and whether it is applied to the message. The ruf= part requests daily aggregate reports to be sent to the specified address, which is automatically configured and reports automatically analyzed.", domain.ASCII+".", mox.TXTStrings(dmarcr.String()))
1190 addf(&r.DMARC.Instructions, "%s", instr)
1192 addf(&r.DMARC.Instructions, "%s", extInstr)
1196 checkTLSRPT := func(result *TLSRPTCheckResult, dom dns.Domain, address smtp.Address, isHost bool) {
1200 record, txt, err := tlsrpt.Lookup(ctx, log.Logger, resolver, dom)
1202 addf(&result.Errors, "Looking up TLSRPT record for domain %s: %s", dom, err)
1206 result.Record = &TLSRPTRecord{*record}
1209 instr := `TLSRPT is an opt-in mechanism to request feedback about TLS connectivity from remote SMTP servers when they connect to us. It allows detecting delivery problems and unwanted downgrades to plaintext SMTP connections. With TLSRPT you configure an email address to which reports should be sent. Remote SMTP servers will send a report once a day with the number of successful connections, and the number of failed connections including details that should help debugging/resolving any issues. Both the mail host (e.g. mail.domain.example) and a recipient domain (e.g. domain.example, with an MX record pointing to mail.domain.example) can have a TLSRPT record. The TLSRPT record for the hosts is for reporting about DANE, the TLSRPT record for the domain is for MTA-STS.`
1210 var zeroaddr smtp.Address
1211 if address != zeroaddr {
1212 // TLSRPT does not require validation of reporting addresses outside the domain.
1216 Opaque: address.Pack(false),
1218 rua := tlsrpt.RUA(uri.String())
1219 tlsrptr := &tlsrpt.Record{
1220 Version: "TLSRPTv1",
1221 RUAs: [][]tlsrpt.RUA{{rua}},
1223 instr += fmt.Sprintf(`
1225Ensure a DNS TXT record like the following exists:
1227 _smtp._tls.%s TXT %s
1229`, dom.ASCII+".", mox.TXTStrings(tlsrptr.String()))
1234 for _, l := range record.RUAs {
1235 for _, e := range l {
1243 addf(&result.Errors, `Configured reporting address is not present in TLSRPT record.`)
1248 instr += fmt.Sprintf(`
1250Ensure the following snippet is present in mox.conf (ensure tabs are used for indenting, not spaces):
1257`, mox.Conf.Static.Postmaster.Account)
1258 addf(&result.Errors, `Configure a HostTLSRPT section in the static mox.conf config file, restart mox and check again for instructions for the TLSRPT DNS record.`)
1260 addf(&result.Errors, `Configure a TLSRPT destination for the domain (through the admin web interface or by editing the domains.conf config file, adding a TLSRPT section) and check again for instructions for the TLSRPT DNS record.`)
1262 addf(&result.Instructions, "%s", instr)
1267 var hostTLSRPTAddr smtp.Address
1268 if mox.Conf.Static.HostTLSRPT.Localpart != "" {
1269 hostTLSRPTAddr = smtp.NewAddress(mox.Conf.Static.HostTLSRPT.ParsedLocalpart, mox.Conf.Static.HostnameDomain)
1271 go checkTLSRPT(&r.HostTLSRPT, mox.Conf.Static.HostnameDomain, hostTLSRPTAddr, true)
1275 var domainTLSRPTAddr smtp.Address
1276 if domConf.TLSRPT != nil {
1277 domainTLSRPTAddr = smtp.NewAddress(domConf.TLSRPT.ParsedLocalpart, domain)
1279 go checkTLSRPT(&r.DomainTLSRPT, domain, domainTLSRPTAddr, false)
1287 // The admin has explicitly disabled mta-sts, keep warning about it.
1288 if domConf.MTASTS == nil {
1289 addf(&r.MTASTS.Warnings, "MTA-STS is not configured for this domain.")
1292 record, txt, err := mtasts.LookupRecord(ctx, log.Logger, resolver, domain)
1293 if err != nil && !(domConf.MTASTS == nil && errors.Is(err, mtasts.ErrNoRecord)) {
1294 addf(&r.MTASTS.Errors, "Looking up MTA-STS record: %s", err)
1298 r.MTASTS.Record = &MTASTSRecord{*record}
1301 policy, text, err := mtasts.FetchPolicy(ctx, log.Logger, domain)
1303 if !(domConf.MTASTS == nil && errors.Is(err, mtasts.ErrNoPolicy)) {
1304 addf(&r.MTASTS.Errors, "Fetching MTA-STS policy: %s", err)
1306 } else if policy.Mode == mtasts.ModeNone {
1307 addf(&r.MTASTS.Warnings, "MTA-STS policy is present, but does not require TLS.")
1308 } else if policy.Mode == mtasts.ModeTesting {
1309 addf(&r.MTASTS.Warnings, "MTA-STS policy is in testing mode, do not forget to change to mode enforce after testing period.")
1311 r.MTASTS.PolicyText = text
1312 r.MTASTS.Policy = policy
1313 if policy != nil && policy.Mode != mtasts.ModeNone {
1314 if !policy.Matches(mox.Conf.Static.HostnameDomain) {
1315 addf(&r.MTASTS.Warnings, "Configured hostname is missing from policy MX list.")
1317 if policy.MaxAgeSeconds <= 24*3600 {
1318 addf(&r.MTASTS.Warnings, "Policy has a MaxAge of less than 1 day. For stable configurations, the recommended period is in weeks.")
1321 mxl, _, _ := resolver.LookupMX(ctx, domain.ASCII+".")
1322 // We do not check for errors, the MX check will complain about mx errors, we assume we will get the same error here.
1323 mxs := map[dns.Domain]struct{}{}
1324 for _, mx := range mxl {
1325 d, err := dns.ParseDomain(strings.TrimSuffix(mx.Host, "."))
1327 addf(&r.MTASTS.Warnings, "MX record %q is invalid: %s", mx.Host, err)
1332 for mx := range mxs {
1333 if !policy.Matches(mx) {
1334 addf(&r.MTASTS.Warnings, "MX record %q does not match MTA-STS policy MX list.", mx)
1337 for _, mx := range policy.MX {
1341 if _, ok := mxs[mx.Domain]; !ok {
1342 addf(&r.MTASTS.Warnings, "MX %q in MTA-STS policy is not in MX record.", mx.LogString())
1347 intro := `MTA-STS is an opt-in mechanism to signal to remote SMTP servers which MX records are valid and that they must use the STARTTLS command and verify the TLS connection. Email servers should already be using STARTTLS to protect communication, but active attackers can, and have in the past, removed the indication of support for the optional STARTTLS support from SMTP sessions, or added additional MX records in DNS responses. MTA-STS protects against compromised DNS and compromised plaintext SMTP sessions, but not against compromised internet PKI infrastructure. If an attacker controls a certificate authority, and is willing to use it, MTA-STS does not prevent an attack. MTA-STS does not protect against attackers on first contact with a domain. Only on subsequent contacts, with MTA-STS policies in the cache, can attacks can be detected.
1349After enabling MTA-STS for this domain, remote SMTP servers may still deliver in plain text, without TLS-protection. MTA-STS is an opt-in mechanism, not all servers support it yet.
1351You can opt-in to MTA-STS by creating a DNS record, _mta-sts.<domain>, and serving a policy at https://mta-sts.<domain>/.well-known/mta-sts.txt. Mox will serve the policy, you must create the DNS records.
1353You can start with a policy in "testing" mode. Remote SMTP servers will apply the MTA-STS policy, but not abort delivery in case of failure. Instead, you will receive a report if you have TLSRPT configured. By starting in testing mode for a representative period, verifying all mail can be deliverd, you can safely switch to "enforce" mode. While in enforce mode, plaintext deliveries to mox are refused.
1355The _mta-sts DNS TXT record has an "id" field. The id serves as a version of the policy. A policy specifies the mode: none, testing, enforce. For "none", no TLS is required. A policy has a "max age", indicating how long the policy can be cached. Allowing the policy to be cached for a long time provides stronger counter measures to active attackers, but reduces configuration change agility. After enabling "enforce" mode, remote SMTP servers may and will cache your policy for as long as "max age" was configured. Keep this in mind when enabling/disabling MTA-STS. To disable MTA-STS after having it enabled, publish a new record with mode "none" until all past policy expiration times have passed.
1357When enabling MTA-STS, or updating a policy, always update the policy first (through a configuration change and reload/restart), and the DNS record second.
1359 addf(&r.MTASTS.Instructions, "%s", intro)
1361 addf(&r.MTASTS.Instructions, `Enable a policy through the configuration file. For new deployments, it is best to start with mode "testing" while enabling TLSRPT. Start with a short "max_age", so updates to your policy are picked up quickly. When confidence in the deployment is high enough, switch to "enforce" mode and a longer "max age". A max age in the order of weeks is recommended. If you foresee a change to your setup in the future, requiring different policies or MX records, you may want to dial back the "max age" ahead of time, similar to how you would handle TTL's in DNS record updates.`)
1363 host := fmt.Sprintf("Ensure DNS CNAME/A/AAAA records exist that resolves mta-sts.%s to this mail server. For example:\n\n\tmta-sts.%s CNAME %s\n\n", domain.ASCII, domain.ASCII+".", mox.Conf.Static.HostnameDomain.ASCII+".")
1364 addf(&r.MTASTS.Instructions, "%s", host)
1366 mtastsr := mtasts.Record{
1368 ID: time.Now().Format("20060102T150405"),
1370 dns := fmt.Sprintf("Ensure a DNS TXT record like the following exists:\n\n\t_mta-sts.%s TXT %s\n\nConfigure the ID in the configuration file, it must be of the form [a-zA-Z0-9]{1,31}. It represents the version of the policy. For each policy change, you must change the ID to a new unique value. You could use a timestamp like 20220621T123000. When this field exists, an SMTP server will fetch a policy at https://mta-sts.%s/.well-known/mta-sts.txt. This policy is served by mox.", domain.ASCII+".", mox.TXTStrings(mtastsr.String()), domain.Name())
1371 addf(&r.MTASTS.Instructions, "%s", dns)
1380 type srvReq struct {
1383 // First entry is host we suggest and prefer, but we won't complain if the current
1384 // value is one of the later values, to account for historic values we suggested
1385 // that aren't wrong and we don't want to bother admins with.
1391 // We'll assume if any submissions is configured, it is public. Same for imap. And
1392 // if not, that there is a plain option.
1393 var submissions, imaps bool
1394 for _, l := range mox.Conf.Static.Listeners {
1395 if l.TLS != nil && l.Submissions.Enabled {
1398 if l.TLS != nil && l.IMAPS.Enabled {
1402 srvhost := func(ok bool) []string {
1404 return []string{"."}
1406 if domConf.ClientSettingsDomain != "" {
1408 domConf.ClientSettingsDNSDomain.ASCII + ".",
1409 mox.Conf.Static.HostnameDomain.ASCII + ".",
1412 return []string{mox.Conf.Static.HostnameDomain.ASCII + "."}
1414 var reqs = []srvReq{
1415 {name: "_submissions", port: 465, host: srvhost(submissions)},
1416 {name: "_submission", port: 587, host: srvhost(!submissions)},
1417 {name: "_imaps", port: 993, host: srvhost(imaps)},
1418 {name: "_imap", port: 143, host: srvhost(!imaps)},
1419 {name: "_pop3", port: 110, host: []string{"."}},
1420 {name: "_pop3s", port: 995, host: []string{"."}},
1422 // Host "." indicates the service is not available. We suggested in the DNS records
1424 for i := range reqs {
1425 if reqs[i].host[0] == "." {
1429 var srvwg sync.WaitGroup
1430 srvwg.Add(len(reqs))
1431 for i := range reqs {
1434 _, reqs[i].srvs, _, reqs[i].err = resolver.LookupSRV(ctx, reqs[i].name[1:], "tcp", domain.ASCII+".")
1439 var instr strings.Builder
1440 instr.WriteString("Ensure DNS records like the following exist:\n\n")
1441 r.SRVConf.SRVs = map[string][]net.SRV{}
1442 for _, req := range reqs {
1443 name := req.name + "._tcp." + domain.ASCII
1445 if req.host[0] == "." {
1448 instr.WriteString(fmt.Sprintf("\t%s._tcp.%-*s SRV 0 %d %d %s\n", req.name, len("_submissions")-len(req.name)+len(domain.ASCII+"."), domain.ASCII+".", weight, req.port, req.host[0]))
1449 r.SRVConf.SRVs[req.name] = unptr(req.srvs)
1451 addf(&r.SRVConf.Errors, "Looking up SRV record %q: %s", name, req.err)
1452 } else if len(req.srvs) == 0 {
1453 if req.host[0] == "." {
1454 addf(&r.SRVConf.Warnings, "Missing optional SRV record %q", name)
1456 addf(&r.SRVConf.Errors, "Missing SRV record %q", name)
1458 } else if len(req.srvs) != 1 || !slices.Contains(req.host, req.srvs[0].Target) || req.srvs[0].Port != req.port {
1460 for _, srv := range req.srvs {
1461 srvs = append(srvs, fmt.Sprintf("%d %d %d %s", srv.Priority, srv.Weight, srv.Port, srv.Target))
1463 addf(&r.SRVConf.Errors, "Unexpected SRV record(s) for %q: %s", name, strings.Join(srvs, ", "))
1466 addf(&r.SRVConf.Instructions, "%s", instr.String())
1475 if domConf.ClientSettingsDomain != "" {
1476 addf(&r.Autoconf.Instructions, "Ensure a DNS CNAME record like the following exists:\n\n\t%s CNAME %s\n\nNote: the trailing dot is relevant, it makes the host name absolute instead of relative to the domain name.", domConf.ClientSettingsDNSDomain.ASCII+".", mox.Conf.Static.HostnameDomain.ASCII+".")
1478 ips, ourIPs, notOurIPs, err := lookupIPs(&r.Autoconf.Errors, domConf.ClientSettingsDNSDomain.ASCII+".")
1480 addf(&r.Autoconf.Errors, "Looking up client settings DNS CNAME: %s", err)
1482 r.Autoconf.ClientSettingsDomainIPs = ips
1483 if !isUnspecifiedNAT {
1484 if len(ourIPs) == 0 {
1485 addf(&r.Autoconf.Errors, "Client settings domain does not point to one of our IPs.")
1486 } else if len(notOurIPs) > 0 {
1487 addf(&r.Autoconf.Errors, "Client settings domain points to some IPs that are not ours: %v", notOurIPs)
1492 addf(&r.Autoconf.Instructions, "Ensure a DNS CNAME record like the following exists:\n\n\tautoconfig.%s CNAME %s\n\nNote: the trailing dot is relevant, it makes the host name absolute instead of relative to the domain name.", domain.ASCII+".", mox.Conf.Static.HostnameDomain.ASCII+".")
1494 host := "autoconfig." + domain.ASCII + "."
1495 ips, ourIPs, notOurIPs, err := lookupIPs(&r.Autoconf.Errors, host)
1497 addf(&r.Autoconf.Errors, "Looking up autoconfig host: %s", err)
1501 r.Autoconf.IPs = ips
1502 if !isUnspecifiedNAT {
1503 if len(ourIPs) == 0 {
1504 addf(&r.Autoconf.Errors, "Autoconfig does not point to one of our IPs.")
1505 } else if len(notOurIPs) > 0 {
1506 addf(&r.Autoconf.Errors, "Autoconfig points to some IPs that are not ours: %v", notOurIPs)
1510 checkTLS(&r.Autoconf.Errors, "autoconfig."+domain.ASCII, ips, "443")
1519 addf(&r.Autodiscover.Instructions, "Ensure DNS records like the following exist:\n\n\t_autodiscover._tcp.%s SRV 0 1 443 %s\n\tautoconfig.%s CNAME %s\n\nNote: the trailing dots are relevant, it makes the host names absolute instead of relative to the domain name.", domain.ASCII+".", mox.Conf.Static.HostnameDomain.ASCII+".", domain.ASCII+".", mox.Conf.Static.HostnameDomain.ASCII+".")
1521 _, srvs, _, err := resolver.LookupSRV(ctx, "autodiscover", "tcp", domain.ASCII+".")
1523 addf(&r.Autodiscover.Errors, "Looking up SRV record %q: %s", "autodiscover", err)
1527 for _, srv := range srvs {
1528 ips, ourIPs, notOurIPs, err := lookupIPs(&r.Autodiscover.Errors, srv.Target)
1530 addf(&r.Autodiscover.Errors, "Looking up target %q from SRV record: %s", srv.Target, err)
1533 if srv.Port != 443 {
1537 r.Autodiscover.Records = append(r.Autodiscover.Records, AutodiscoverSRV{*srv, ips})
1538 if !isUnspecifiedNAT {
1539 if len(ourIPs) == 0 {
1540 addf(&r.Autodiscover.Errors, "SRV target %q does not point to our IPs.", srv.Target)
1541 } else if len(notOurIPs) > 0 {
1542 addf(&r.Autodiscover.Errors, "SRV target %q points to some IPs that are not ours: %v", srv.Target, notOurIPs)
1546 checkTLS(&r.Autodiscover.Errors, strings.TrimSuffix(srv.Target, "."), ips, "443")
1549 addf(&r.Autodiscover.Errors, "No SRV record for port 443 for https.")
1557// Domains returns all configured domain names.
1558func (Admin) Domains(ctx context.Context) []config.Domain {
1559 return mox.Conf.DomainConfigs()
1562// Domain returns the dns domain for a (potentially unicode as IDNA) domain name.
1563func (Admin) Domain(ctx context.Context, domain string) dns.Domain {
1564 d, err := dns.ParseDomain(domain)
1565 xcheckuserf(ctx, err, "parse domain")
1566 _, ok := mox.Conf.Domain(d)
1568 xcheckuserf(ctx, errors.New("no such domain"), "looking up domain")
1573// ParseDomain parses a domain, possibly an IDNA domain.
1574func (Admin) ParseDomain(ctx context.Context, domain string) dns.Domain {
1575 d, err := dns.ParseDomain(domain)
1576 xcheckuserf(ctx, err, "parse domain")
1580// DomainConfig returns the configuration for a domain.
1581func (Admin) DomainConfig(ctx context.Context, domain string) config.Domain {
1582 d, err := dns.ParseDomain(domain)
1583 xcheckuserf(ctx, err, "parse domain")
1584 conf, ok := mox.Conf.Domain(d)
1586 xcheckuserf(ctx, errors.New("no such domain"), "looking up domain")
1591// DomainLocalparts returns the encoded localparts and accounts configured in domain.
1592func (Admin) DomainLocalparts(ctx context.Context, domain string) (localpartAccounts map[string]string, localpartAliases map[string]config.Alias) {
1593 d, err := dns.ParseDomain(domain)
1594 xcheckuserf(ctx, err, "parsing domain")
1595 _, ok := mox.Conf.Domain(d)
1597 xcheckuserf(ctx, errors.New("no such domain"), "looking up domain")
1599 return mox.Conf.DomainLocalparts(d)
1602// Accounts returns the names of all configured and all disabled accounts.
1603func (Admin) Accounts(ctx context.Context) (all, disabled []string) {
1604 all, disabled = mox.Conf.AccountsDisabled()
1609// Account returns the parsed configuration of an account.
1610func (Admin) Account(ctx context.Context, account string) (accountConfig config.Account, diskUsage int64) {
1611 log := pkglog.WithContext(ctx)
1613 acc, err := store.OpenAccount(log, account, false)
1614 if err != nil && errors.Is(err, store.ErrAccountUnknown) {
1615 xcheckuserf(ctx, err, "looking up account")
1617 xcheckf(ctx, err, "open account")
1620 log.Check(err, "closing account")
1623 var ac config.Account
1624 acc.WithRLock(func() {
1625 ac, _ = mox.Conf.Account(acc.Name)
1627 err := acc.DB.Read(ctx, func(tx *bstore.Tx) error {
1628 du := store.DiskUsage{ID: 1}
1630 diskUsage = du.MessageSize
1633 xcheckf(ctx, err, "get disk usage")
1636 return ac, diskUsage
1639// ConfigFiles returns the paths and contents of the static and dynamic configuration files.
1640func (Admin) ConfigFiles(ctx context.Context) (staticPath, dynamicPath, static, dynamic string) {
1641 buf0, err := os.ReadFile(mox.ConfigStaticPath)
1642 xcheckf(ctx, err, "read static config file")
1643 buf1, err := os.ReadFile(mox.ConfigDynamicPath)
1644 xcheckf(ctx, err, "read dynamic config file")
1645 return mox.ConfigStaticPath, mox.ConfigDynamicPath, string(buf0), string(buf1)
1648// MTASTSPolicies returns all mtasts policies from the cache.
1649func (Admin) MTASTSPolicies(ctx context.Context) (records []mtastsdb.PolicyRecord) {
1650 records, err := mtastsdb.PolicyRecords(ctx)
1651 xcheckf(ctx, err, "fetching mtasts policies from database")
1655// TLSReports returns TLS reports overlapping with period start/end, for the given
1656// policy domain (or all domains if empty). The reports are sorted first by period
1657// end (most recent first), then by policy domain.
1658func (Admin) TLSReports(ctx context.Context, start, end time.Time, policyDomain string) (reports []tlsrptdb.Record) {
1659 var polDom dns.Domain
1660 if policyDomain != "" {
1662 polDom, err = dns.ParseDomain(policyDomain)
1663 xcheckuserf(ctx, err, "parsing domain %q", policyDomain)
1666 records, err := tlsrptdb.RecordsPeriodDomain(ctx, start, end, polDom)
1667 xcheckf(ctx, err, "fetching tlsrpt report records from database")
1668 sort.Slice(records, func(i, j int) bool {
1669 iend := records[i].Report.DateRange.End
1670 jend := records[j].Report.DateRange.End
1672 return records[i].Domain < records[j].Domain
1674 return iend.After(jend)
1679// TLSReportID returns a single TLS report.
1680func (Admin) TLSReportID(ctx context.Context, domain string, reportID int64) tlsrptdb.Record {
1681 record, err := tlsrptdb.RecordID(ctx, reportID)
1682 if err == nil && record.Domain != domain {
1683 err = bstore.ErrAbsent
1685 if err == bstore.ErrAbsent {
1686 xcheckuserf(ctx, err, "fetching tls report from database")
1688 xcheckf(ctx, err, "fetching tls report from database")
1692// TLSRPTSummary presents TLS reporting statistics for a single domain
1694type TLSRPTSummary struct {
1695 PolicyDomain dns.Domain
1698 ResultTypeCounts map[tlsrpt.ResultType]int64
1701// TLSRPTSummaries returns a summary of received TLS reports overlapping with
1702// period start/end for one or all domains (when domain is empty).
1703// The returned summaries are ordered by domain name.
1704func (Admin) TLSRPTSummaries(ctx context.Context, start, end time.Time, policyDomain string) (domainSummaries []TLSRPTSummary) {
1705 var polDom dns.Domain
1706 if policyDomain != "" {
1708 polDom, err = dns.ParseDomain(policyDomain)
1709 xcheckuserf(ctx, err, "parsing policy domain")
1711 reports, err := tlsrptdb.RecordsPeriodDomain(ctx, start, end, polDom)
1712 xcheckf(ctx, err, "fetching tlsrpt reports from database")
1714 summaries := map[dns.Domain]TLSRPTSummary{}
1715 for _, r := range reports {
1716 dom, err := dns.ParseDomain(r.Domain)
1717 xcheckf(ctx, err, "parsing domain %q", r.Domain)
1719 sum := summaries[dom]
1720 sum.PolicyDomain = dom
1721 for _, result := range r.Report.Policies {
1722 sum.Success += result.Summary.TotalSuccessfulSessionCount
1723 sum.Failure += result.Summary.TotalFailureSessionCount
1724 for _, details := range result.FailureDetails {
1725 if sum.ResultTypeCounts == nil {
1726 sum.ResultTypeCounts = map[tlsrpt.ResultType]int64{}
1728 sum.ResultTypeCounts[details.ResultType] += details.FailedSessionCount
1731 summaries[dom] = sum
1733 sums := make([]TLSRPTSummary, 0, len(summaries))
1734 for _, sum := range summaries {
1735 sums = append(sums, sum)
1737 sort.Slice(sums, func(i, j int) bool {
1738 return sums[i].PolicyDomain.Name() < sums[j].PolicyDomain.Name()
1743// DMARCReports returns DMARC reports overlapping with period start/end, for the
1744// given domain (or all domains if empty). The reports are sorted first by period
1745// end (most recent first), then by domain.
1746func (Admin) DMARCReports(ctx context.Context, start, end time.Time, domain string) (reports []dmarcdb.DomainFeedback) {
1747 reports, err := dmarcdb.RecordsPeriodDomain(ctx, start, end, domain)
1748 xcheckf(ctx, err, "fetching dmarc aggregate reports from database")
1749 sort.Slice(reports, func(i, j int) bool {
1750 iend := reports[i].ReportMetadata.DateRange.End
1751 jend := reports[j].ReportMetadata.DateRange.End
1753 return reports[i].Domain < reports[j].Domain
1760// DMARCReportID returns a single DMARC report.
1761func (Admin) DMARCReportID(ctx context.Context, domain string, reportID int64) (report dmarcdb.DomainFeedback) {
1762 report, err := dmarcdb.RecordID(ctx, reportID)
1763 if err == nil && report.Domain != domain {
1764 err = bstore.ErrAbsent
1766 if err == bstore.ErrAbsent {
1767 xcheckuserf(ctx, err, "fetching dmarc aggregate report from database")
1769 xcheckf(ctx, err, "fetching dmarc aggregate report from database")
1773// DMARCSummary presents DMARC aggregate reporting statistics for a single domain
1775type DMARCSummary struct {
1779 DispositionQuarantine int
1780 DispositionReject int
1783 PolicyOverrides map[dmarcrpt.PolicyOverride]int
1786// DMARCSummaries returns a summary of received DMARC reports overlapping with
1787// period start/end for one or all domains (when domain is empty).
1788// The returned summaries are ordered by domain name.
1789func (Admin) DMARCSummaries(ctx context.Context, start, end time.Time, domain string) (domainSummaries []DMARCSummary) {
1790 reports, err := dmarcdb.RecordsPeriodDomain(ctx, start, end, domain)
1791 xcheckf(ctx, err, "fetching dmarc aggregate reports from database")
1792 summaries := map[string]DMARCSummary{}
1793 for _, r := range reports {
1794 sum := summaries[r.Domain]
1795 sum.Domain = r.Domain
1796 for _, record := range r.Records {
1797 n := record.Row.Count
1801 switch record.Row.PolicyEvaluated.Disposition {
1802 case dmarcrpt.DispositionNone:
1803 sum.DispositionNone += n
1804 case dmarcrpt.DispositionQuarantine:
1805 sum.DispositionQuarantine += n
1806 case dmarcrpt.DispositionReject:
1807 sum.DispositionReject += n
1810 if record.Row.PolicyEvaluated.DKIM == dmarcrpt.DMARCFail {
1813 if record.Row.PolicyEvaluated.SPF == dmarcrpt.DMARCFail {
1817 for _, reason := range record.Row.PolicyEvaluated.Reasons {
1818 if sum.PolicyOverrides == nil {
1819 sum.PolicyOverrides = map[dmarcrpt.PolicyOverride]int{}
1821 sum.PolicyOverrides[reason.Type] += n
1824 summaries[r.Domain] = sum
1826 sums := make([]DMARCSummary, 0, len(summaries))
1827 for _, sum := range summaries {
1828 sums = append(sums, sum)
1830 sort.Slice(sums, func(i, j int) bool {
1831 return sums[i].Domain < sums[j].Domain
1836// Reverse is the result of a reverse lookup.
1837type Reverse struct {
1840 // In the future, we can add a iprev-validated host name, and possibly the IPs of the host names.
1843// LookupIP does a reverse lookup of ip.
1844func (Admin) LookupIP(ctx context.Context, ip string) Reverse {
1845 resolver := dns.StrictResolver{Pkg: "webadmin", Log: pkglog.WithContext(ctx).Logger}
1846 names, _, err := resolver.LookupAddr(ctx, ip)
1847 xcheckuserf(ctx, err, "looking up ip")
1848 return Reverse{names}
1851// DNSBLStatus returns the IPs from which outgoing connections may be made and
1852// their current status in DNSBLs that are configured. The IPs are typically the
1853// configured listen IPs, or otherwise IPs on the machines network interfaces, with
1854// internal/private IPs removed.
1856// The returned value maps IPs to per DNSBL statuses, where "pass" means not listed and
1857// anything else is an error string, e.g. "fail: ..." or "temperror: ...".
1858func (Admin) DNSBLStatus(ctx context.Context) (results map[string]map[string]string, using, monitoring []dns.Domain) {
1859 log := mlog.New("webadmin", nil).WithContext(ctx)
1860 resolver := dns.StrictResolver{Pkg: "check", Log: log.Logger}
1861 return dnsblsStatus(ctx, log, resolver)
1864func dnsblsStatus(ctx context.Context, log mlog.Log, resolver dns.Resolver) (results map[string]map[string]string, using, monitoring []dns.Domain) {
1865 // todo: check health before using dnsbl?
1866 using = mox.Conf.Static.Listeners["public"].SMTP.DNSBLZones
1867 zones := slices.Clone(using)
1868 conf := mox.Conf.DynamicConfig()
1869 for _, zone := range conf.MonitorDNSBLZones {
1870 if !slices.Contains(zones, zone) {
1871 zones = append(zones, zone)
1872 monitoring = append(monitoring, zone)
1876 r := map[string]map[string]string{}
1877 for _, ip := range xsendingIPs(ctx) {
1878 if ip.IsLoopback() || ip.IsPrivate() {
1881 ipstr := ip.String()
1882 r[ipstr] = map[string]string{}
1883 for _, zone := range zones {
1884 status, expl, err := dnsbl.Lookup(ctx, log.Logger, resolver, zone, ip)
1885 result := string(status)
1887 result += ": " + err.Error()
1890 result += ": " + expl
1892 r[ipstr][zone.LogString()] = result
1895 return r, using, monitoring
1898func (Admin) MonitorDNSBLsSave(ctx context.Context, text string) {
1899 var zones []dns.Domain
1900 publicZones := mox.Conf.Static.Listeners["public"].SMTP.DNSBLZones
1901 for line := range strings.SplitSeq(text, "\n") {
1902 line = strings.TrimSpace(line)
1906 d, err := dns.ParseDomain(line)
1907 xcheckuserf(ctx, err, "parsing dnsbl zone %s", line)
1908 if slices.Contains(zones, d) {
1909 xusererrorf(ctx, "duplicate dnsbl zone %s", line)
1911 if slices.Contains(publicZones, d) {
1912 xusererrorf(ctx, "dnsbl zone %s already present in public listener", line)
1914 zones = append(zones, d)
1917 err := admin.ConfigSave(ctx, func(conf *config.Dynamic) {
1918 conf.MonitorDNSBLs = make([]string, len(zones))
1919 conf.MonitorDNSBLZones = nil
1920 for i, z := range zones {
1921 conf.MonitorDNSBLs[i] = z.Name()
1924 xcheckf(ctx, err, "saving monitoring dnsbl zones")
1927// DomainRecords returns lines describing DNS records that should exist for the
1928// configured domain.
1929func (Admin) DomainRecords(ctx context.Context, domain string) []string {
1930 log := pkglog.WithContext(ctx)
1931 return DomainRecords(ctx, log, domain)
1934// DomainRecords is the implementation of API function Admin.DomainRecords, taking
1936func DomainRecords(ctx context.Context, log mlog.Log, domain string) []string {
1937 d, err := dns.ParseDomain(domain)
1938 xcheckuserf(ctx, err, "parsing domain")
1939 dc, ok := mox.Conf.Domain(d)
1941 xcheckuserf(ctx, errors.New("unknown domain"), "lookup domain")
1943 resolver := dns.StrictResolver{Pkg: "webadmin", Log: pkglog.WithContext(ctx).Logger}
1944 _, result, err := resolver.LookupTXT(ctx, domain+".")
1945 if !dns.IsNotFound(err) {
1946 xcheckf(ctx, err, "looking up record to determine if dnssec is implemented")
1949 var certIssuerDomainName, acmeAccountURI string
1950 public := mox.Conf.Static.Listeners["public"]
1951 if public.TLS != nil && public.TLS.ACME != "" {
1952 acme, ok := mox.Conf.Static.ACME[public.TLS.ACME]
1953 if ok && acme.Manager.Manager.Client != nil {
1954 certIssuerDomainName = acme.IssuerDomainName
1955 acc, err := acme.Manager.Manager.Client.GetReg(ctx, "")
1956 log.Check(err, "get public acme account")
1958 acmeAccountURI = acc.URI
1963 records, err := admin.DomainRecords(dc, d, result.Authentic, certIssuerDomainName, acmeAccountURI)
1964 xcheckf(ctx, err, "dns records")
1968// DomainAdd adds a new domain and reloads the configuration.
1969func (Admin) DomainAdd(ctx context.Context, disabled bool, domain, accountName, localpart string) {
1970 d, err := dns.ParseDomain(domain)
1971 xcheckuserf(ctx, err, "parsing domain")
1973 err = admin.DomainAdd(ctx, disabled, d, accountName, smtp.Localpart(norm.NFC.String(localpart)))
1974 xcheckf(ctx, err, "adding domain")
1977// DomainRemove removes an existing domain and reloads the configuration.
1978func (Admin) DomainRemove(ctx context.Context, domain string) {
1979 d, err := dns.ParseDomain(domain)
1980 xcheckuserf(ctx, err, "parsing domain")
1982 err = admin.DomainRemove(ctx, d)
1983 xcheckf(ctx, err, "removing domain")
1986// AccountAdd adds existing a new account, with an initial email address, and
1987// reloads the configuration.
1988func (Admin) AccountAdd(ctx context.Context, accountName, address string) {
1989 err := admin.AccountAdd(ctx, accountName, address)
1990 xcheckf(ctx, err, "adding account")
1993// AccountRemove removes an existing account and reloads the configuration.
1994func (Admin) AccountRemove(ctx context.Context, accountName string) {
1995 err := admin.AccountRemove(ctx, accountName)
1996 xcheckf(ctx, err, "removing account")
1999// AddressAdd adds a new address to the account, which must already exist.
2000func (Admin) AddressAdd(ctx context.Context, address, accountName string) {
2001 err := admin.AddressAdd(ctx, address, accountName)
2002 xcheckf(ctx, err, "adding address")
2005// AddressRemove removes an existing address.
2006func (Admin) AddressRemove(ctx context.Context, address string) {
2007 err := admin.AddressRemove(ctx, address)
2008 xcheckf(ctx, err, "removing address")
2011// SetPassword saves a new password for an account, invalidating the previous password.
2012// Sessions are not interrupted, and will keep working. New login attempts must use the new password.
2013// Password must be at least 8 characters.
2014func (Admin) SetPassword(ctx context.Context, accountName, password string) {
2015 log := pkglog.WithContext(ctx)
2016 if len(password) < 8 {
2017 xusererrorf(ctx, "message must be at least 8 characters")
2019 acc, err := store.OpenAccount(log, accountName, false)
2020 xcheckf(ctx, err, "open account")
2023 log.WithContext(ctx).Check(err, "closing account")
2025 err = acc.SetPassword(log, password)
2026 xcheckf(ctx, err, "setting password")
2029// AccountSettingsSave set new settings for an account that only an admin can set.
2030func (Admin) AccountSettingsSave(ctx context.Context, accountName string, maxOutgoingMessagesPerDay, maxFirstTimeRecipientsPerDay int, maxMsgSize int64, firstTimeSenderDelay, noCustomPassword bool) {
2031 err := admin.AccountSave(ctx, accountName, func(acc *config.Account) {
2032 acc.MaxOutgoingMessagesPerDay = maxOutgoingMessagesPerDay
2033 acc.MaxFirstTimeRecipientsPerDay = maxFirstTimeRecipientsPerDay
2034 acc.QuotaMessageSize = maxMsgSize
2035 acc.NoFirstTimeSenderDelay = !firstTimeSenderDelay
2036 acc.NoCustomPassword = noCustomPassword
2038 xcheckf(ctx, err, "saving account settings")
2041// AccountLoginDisabledSave saves the LoginDisabled field of an account.
2042func (Admin) AccountLoginDisabledSave(ctx context.Context, accountName string, loginDisabled string) {
2043 log := pkglog.WithContext(ctx)
2045 acc, err := store.OpenAccount(log, accountName, false)
2046 xcheckf(ctx, err, "open account")
2049 log.Check(err, "closing account")
2052 err = admin.AccountSave(ctx, accountName, func(acc *config.Account) {
2053 acc.LoginDisabled = loginDisabled
2055 xcheckf(ctx, err, "saving login disabled account")
2057 err = acc.SessionsClear(ctx, log)
2058 xcheckf(ctx, err, "removing current sessions")
2061// ClientConfigsDomain returns configurations for email clients, IMAP and
2062// Submission (SMTP) for the domain.
2063func (Admin) ClientConfigsDomain(ctx context.Context, domain string) admin.ClientConfigs {
2064 d, err := dns.ParseDomain(domain)
2065 xcheckuserf(ctx, err, "parsing domain")
2067 cc, err := admin.ClientConfigsDomain(d)
2068 xcheckf(ctx, err, "client config for domain")
2072// QueueSize returns the number of messages currently in the outgoing queue.
2073func (Admin) QueueSize(ctx context.Context) int {
2074 n, err := queue.Count(ctx)
2075 xcheckf(ctx, err, "listing messages in queue")
2079// QueueHoldRuleList lists the hold rules.
2080func (Admin) QueueHoldRuleList(ctx context.Context) []queue.HoldRule {
2081 l, err := queue.HoldRuleList(ctx)
2082 xcheckf(ctx, err, "listing queue hold rules")
2086// QueueHoldRuleAdd adds a hold rule. Newly submitted and existing messages
2087// matching the hold rule will be marked "on hold".
2088func (Admin) QueueHoldRuleAdd(ctx context.Context, hr queue.HoldRule) queue.HoldRule {
2090 hr.SenderDomain, err = dns.ParseDomain(hr.SenderDomainStr)
2091 xcheckuserf(ctx, err, "parsing sender domain %q", hr.SenderDomainStr)
2092 hr.RecipientDomain, err = dns.ParseDomain(hr.RecipientDomainStr)
2093 xcheckuserf(ctx, err, "parsing recipient domain %q", hr.RecipientDomainStr)
2095 log := pkglog.WithContext(ctx)
2096 hr, err = queue.HoldRuleAdd(ctx, log, hr)
2097 xcheckf(ctx, err, "adding queue hold rule")
2101// QueueHoldRuleRemove removes a hold rule. The Hold field of messages in
2102// the queue are not changed.
2103func (Admin) QueueHoldRuleRemove(ctx context.Context, holdRuleID int64) {
2104 log := pkglog.WithContext(ctx)
2105 err := queue.HoldRuleRemove(ctx, log, holdRuleID)
2106 xcheckf(ctx, err, "removing queue hold rule")
2109// QueueList returns the messages currently in the outgoing queue.
2110func (Admin) QueueList(ctx context.Context, filter queue.Filter, sort queue.Sort) []queue.Msg {
2111 l, err := queue.List(ctx, filter, sort)
2112 xcheckf(ctx, err, "listing messages in queue")
2116// QueueNextAttemptSet sets a new time for next delivery attempt of matching
2117// messages from the queue.
2118func (Admin) QueueNextAttemptSet(ctx context.Context, filter queue.Filter, minutes int) (affected int) {
2119 n, err := queue.NextAttemptSet(ctx, filter, time.Now().Add(time.Duration(minutes)*time.Minute))
2120 xcheckf(ctx, err, "setting new next delivery attempt time for matching messages in queue")
2124// QueueNextAttemptAdd adds a duration to the time of next delivery attempt of
2125// matching messages from the queue.
2126func (Admin) QueueNextAttemptAdd(ctx context.Context, filter queue.Filter, minutes int) (affected int) {
2127 n, err := queue.NextAttemptAdd(ctx, filter, time.Duration(minutes)*time.Minute)
2128 xcheckf(ctx, err, "adding duration to next delivery attempt for matching messages in queue")
2132// QueueHoldSet sets the Hold field of matching messages in the queue.
2133func (Admin) QueueHoldSet(ctx context.Context, filter queue.Filter, onHold bool) (affected int) {
2134 n, err := queue.HoldSet(ctx, filter, onHold)
2135 xcheckf(ctx, err, "changing onhold for matching messages in queue")
2139// QueueFail fails delivery for matching messages, causing DSNs to be sent.
2140func (Admin) QueueFail(ctx context.Context, filter queue.Filter) (affected int) {
2141 log := pkglog.WithContext(ctx)
2142 n, err := queue.Fail(ctx, log, filter)
2143 xcheckf(ctx, err, "drop messages from queue")
2147// QueueDrop removes matching messages from the queue.
2148func (Admin) QueueDrop(ctx context.Context, filter queue.Filter) (affected int) {
2149 log := pkglog.WithContext(ctx)
2150 n, err := queue.Drop(ctx, log, filter)
2151 xcheckf(ctx, err, "drop messages from queue")
2155// QueueRequireTLSSet updates the requiretls field for matching messages in the
2156// queue, to be used for the next delivery.
2157func (Admin) QueueRequireTLSSet(ctx context.Context, filter queue.Filter, requireTLS *bool) (affected int) {
2158 n, err := queue.RequireTLSSet(ctx, filter, requireTLS)
2159 xcheckf(ctx, err, "update requiretls for messages in queue")
2163// QueueTransportSet initiates delivery of a message from the queue and sets the transport
2164// to use for delivery.
2165func (Admin) QueueTransportSet(ctx context.Context, filter queue.Filter, transport string) (affected int) {
2166 n, err := queue.TransportSet(ctx, filter, transport)
2167 xcheckf(ctx, err, "changing transport for messages in queue")
2171// RetiredList returns messages retired from the queue (delivery could
2172// have succeeded or failed).
2173func (Admin) RetiredList(ctx context.Context, filter queue.RetiredFilter, sort queue.RetiredSort) []queue.MsgRetired {
2174 l, err := queue.RetiredList(ctx, filter, sort)
2175 xcheckf(ctx, err, "listing retired messages")
2179// HookQueueSize returns the number of webhooks still to be delivered.
2180func (Admin) HookQueueSize(ctx context.Context) int {
2181 n, err := queue.HookQueueSize(ctx)
2182 xcheckf(ctx, err, "get hook queue size")
2186// HookList lists webhooks still to be delivered.
2187func (Admin) HookList(ctx context.Context, filter queue.HookFilter, sort queue.HookSort) []queue.Hook {
2188 l, err := queue.HookList(ctx, filter, sort)
2189 xcheckf(ctx, err, "listing hook queue")
2193// HookNextAttemptSet sets a new time for next delivery attempt of matching
2194// hooks from the queue.
2195func (Admin) HookNextAttemptSet(ctx context.Context, filter queue.HookFilter, minutes int) (affected int) {
2196 n, err := queue.HookNextAttemptSet(ctx, filter, time.Now().Add(time.Duration(minutes)*time.Minute))
2197 xcheckf(ctx, err, "setting new next delivery attempt time for matching webhooks in queue")
2201// HookNextAttemptAdd adds a duration to the time of next delivery attempt of
2202// matching hooks from the queue.
2203func (Admin) HookNextAttemptAdd(ctx context.Context, filter queue.HookFilter, minutes int) (affected int) {
2204 n, err := queue.HookNextAttemptAdd(ctx, filter, time.Duration(minutes)*time.Minute)
2205 xcheckf(ctx, err, "adding duration to next delivery attempt for matching webhooks in queue")
2209// HookRetiredList lists retired webhooks.
2210func (Admin) HookRetiredList(ctx context.Context, filter queue.HookRetiredFilter, sort queue.HookRetiredSort) []queue.HookRetired {
2211 l, err := queue.HookRetiredList(ctx, filter, sort)
2212 xcheckf(ctx, err, "listing retired hooks")
2216// HookCancel prevents further delivery attempts of matching webhooks.
2217func (Admin) HookCancel(ctx context.Context, filter queue.HookFilter) (affected int) {
2218 log := pkglog.WithContext(ctx)
2219 n, err := queue.HookCancel(ctx, log, filter)
2220 xcheckf(ctx, err, "cancel hooks in queue")
2224// LogLevels returns the current log levels.
2225func (Admin) LogLevels(ctx context.Context) map[string]string {
2226 m := map[string]string{}
2227 for pkg, level := range mox.Conf.LogLevels() {
2228 s, ok := mlog.LevelStrings[level]
2237// LogLevelSet sets a log level for a package.
2238func (Admin) LogLevelSet(ctx context.Context, pkg string, levelStr string) {
2239 level, ok := mlog.Levels[levelStr]
2241 xcheckuserf(ctx, errors.New("unknown"), "lookup level")
2243 mox.Conf.LogLevelSet(pkglog.WithContext(ctx), pkg, level)
2246// LogLevelRemove removes a log level for a package, which cannot be the empty string.
2247func (Admin) LogLevelRemove(ctx context.Context, pkg string) {
2248 mox.Conf.LogLevelRemove(pkglog.WithContext(ctx), pkg)
2251// CheckUpdatesEnabled returns whether checking for updates is enabled.
2252func (Admin) CheckUpdatesEnabled(ctx context.Context) bool {
2253 return mox.Conf.Static.CheckUpdates
2256// WebserverConfig is the combination of WebDomainRedirects and WebHandlers
2257// from the domains.conf configuration file.
2258type WebserverConfig struct {
2259 WebDNSDomainRedirects [][2]dns.Domain // From server to frontend.
2260 WebDomainRedirects [][2]string // From frontend to server, it's not convenient to create dns.Domain in the frontend.
2261 WebHandlers []config.WebHandler
2264// WebserverConfig returns the current webserver config
2265func (Admin) WebserverConfig(ctx context.Context) (conf WebserverConfig) {
2266 conf = webserverConfig()
2267 conf.WebDomainRedirects = nil
2271func webserverConfig() WebserverConfig {
2272 conf := mox.Conf.DynamicConfig()
2273 r := conf.WebDNSDomainRedirects
2274 l := conf.WebHandlers
2276 x := make([][2]dns.Domain, 0, len(r))
2277 xs := make([][2]string, 0, len(r))
2278 for k, v := range r {
2279 x = append(x, [2]dns.Domain{k, v})
2280 xs = append(xs, [2]string{k.Name(), v.Name()})
2282 sort.Slice(x, func(i, j int) bool {
2283 return x[i][0].ASCII < x[j][0].ASCII
2285 sort.Slice(xs, func(i, j int) bool {
2286 return xs[i][0] < xs[j][0]
2288 return WebserverConfig{x, xs, l}
2291// WebserverConfigSave saves a new webserver config. If oldConf is not equal to
2292// the current config, an error is returned.
2293func (Admin) WebserverConfigSave(ctx context.Context, oldConf, newConf WebserverConfig) (savedConf WebserverConfig) {
2294 current := webserverConfig()
2295 webhandlersEqual := func() bool {
2296 if len(current.WebHandlers) != len(oldConf.WebHandlers) {
2299 for i, wh := range current.WebHandlers {
2300 if !wh.Equal(oldConf.WebHandlers[i]) {
2306 if !reflect.DeepEqual(oldConf.WebDNSDomainRedirects, current.WebDNSDomainRedirects) || !webhandlersEqual() {
2307 xcheckuserf(ctx, errors.New("config has changed"), "comparing old/current config")
2310 // Convert to map, check that there are no duplicates here. The canonicalized
2311 // dns.Domain are checked again for uniqueness when parsing the config before
2313 domainRedirects := map[string]string{}
2314 for _, x := range newConf.WebDomainRedirects {
2315 if _, ok := domainRedirects[x[0]]; ok {
2316 xcheckuserf(ctx, errors.New("already present"), "checking redirect %s", x[0])
2318 domainRedirects[x[0]] = x[1]
2321 err := admin.ConfigSave(ctx, func(conf *config.Dynamic) {
2322 conf.WebDomainRedirects = domainRedirects
2323 conf.WebHandlers = newConf.WebHandlers
2325 xcheckf(ctx, err, "saving webserver config")
2327 savedConf = webserverConfig()
2328 savedConf.WebDomainRedirects = nil
2332// Transports returns the configured transports, for sending email.
2333func (Admin) Transports(ctx context.Context) map[string]config.Transport {
2334 return mox.Conf.Static.Transports
2337// DMARCEvaluationStats returns a map of all domains with evaluations to a count of
2338// the evaluations and whether those evaluations will cause a report to be sent.
2339func (Admin) DMARCEvaluationStats(ctx context.Context) map[string]dmarcdb.EvaluationStat {
2340 stats, err := dmarcdb.EvaluationStats(ctx)
2341 xcheckf(ctx, err, "get evaluation stats")
2345// DMARCEvaluationsDomain returns all evaluations for aggregate reports for the
2346// domain, sorted from oldest to most recent.
2347func (Admin) DMARCEvaluationsDomain(ctx context.Context, domain string) (dns.Domain, []dmarcdb.Evaluation) {
2348 dom, err := dns.ParseDomain(domain)
2349 xcheckf(ctx, err, "parsing domain")
2351 evals, err := dmarcdb.EvaluationsDomain(ctx, dom)
2352 xcheckf(ctx, err, "get evaluations for domain")
2356// DMARCRemoveEvaluations removes evaluations for a domain.
2357func (Admin) DMARCRemoveEvaluations(ctx context.Context, domain string) {
2358 dom, err := dns.ParseDomain(domain)
2359 xcheckf(ctx, err, "parsing domain")
2361 err = dmarcdb.RemoveEvaluationsDomain(ctx, dom)
2362 xcheckf(ctx, err, "removing evaluations for domain")
2365// DMARCSuppressAdd adds a reporting address to the suppress list. Outgoing
2366// reports will be suppressed for a period.
2367func (Admin) DMARCSuppressAdd(ctx context.Context, reportingAddress string, until time.Time, comment string) {
2368 addr, err := smtp.ParseAddress(reportingAddress)
2369 xcheckuserf(ctx, err, "parsing reporting address")
2371 ba := dmarcdb.SuppressAddress{ReportingAddress: addr.String(), Until: until, Comment: comment}
2372 err = dmarcdb.SuppressAdd(ctx, &ba)
2373 xcheckf(ctx, err, "adding address to suppresslist")
2376// DMARCSuppressList returns all reporting addresses on the suppress list.
2377func (Admin) DMARCSuppressList(ctx context.Context) []dmarcdb.SuppressAddress {
2378 l, err := dmarcdb.SuppressList(ctx)
2379 xcheckf(ctx, err, "listing reporting addresses in suppresslist")
2383// DMARCSuppressRemove removes a reporting address record from the suppress list.
2384func (Admin) DMARCSuppressRemove(ctx context.Context, id int64) {
2385 err := dmarcdb.SuppressRemove(ctx, id)
2386 xcheckf(ctx, err, "removing reporting address from suppresslist")
2389// DMARCSuppressExtend updates the until field of a suppressed reporting address record.
2390func (Admin) DMARCSuppressExtend(ctx context.Context, id int64, until time.Time) {
2391 err := dmarcdb.SuppressUpdate(ctx, id, until)
2392 xcheckf(ctx, err, "updating reporting address in suppresslist")
2395// TLSRPTResults returns all TLSRPT results in the database.
2396func (Admin) TLSRPTResults(ctx context.Context) []tlsrptdb.TLSResult {
2397 results, err := tlsrptdb.Results(ctx)
2398 xcheckf(ctx, err, "get results")
2402// TLSRPTResultsPolicyDomain returns the TLS results for a domain.
2403func (Admin) TLSRPTResultsDomain(ctx context.Context, isRcptDom bool, policyDomain string) (dns.Domain, []tlsrptdb.TLSResult) {
2404 dom, err := dns.ParseDomain(policyDomain)
2405 xcheckf(ctx, err, "parsing domain")
2408 results, err := tlsrptdb.ResultsRecipientDomain(ctx, dom)
2409 xcheckf(ctx, err, "get result for recipient domain")
2412 results, err := tlsrptdb.ResultsPolicyDomain(ctx, dom)
2413 xcheckf(ctx, err, "get result for policy domain")
2417// LookupTLSRPTRecord looks up a TLSRPT record and returns the parsed form, original txt
2418// form from DNS, and error with the TLSRPT record as a string.
2419func (Admin) LookupTLSRPTRecord(ctx context.Context, domain string) (record *TLSRPTRecord, txt string, errstr string) {
2420 log := pkglog.WithContext(ctx)
2421 dom, err := dns.ParseDomain(domain)
2422 xcheckf(ctx, err, "parsing domain")
2424 resolver := dns.StrictResolver{Pkg: "webadmin", Log: log.Logger}
2425 r, txt, err := tlsrpt.Lookup(ctx, log.Logger, resolver, dom)
2426 if err != nil && (errors.Is(err, tlsrpt.ErrNoRecord) || errors.Is(err, tlsrpt.ErrMultipleRecords) || errors.Is(err, tlsrpt.ErrRecordSyntax) || errors.Is(err, tlsrpt.ErrDNS)) {
2427 errstr = err.Error()
2430 xcheckf(ctx, err, "fetching tlsrpt record")
2433 record = &TLSRPTRecord{Record: *r}
2436 return record, txt, errstr
2439// TLSRPTRemoveResults removes the TLS results for a domain for the given day. If
2440// day is empty, all results are removed.
2441func (Admin) TLSRPTRemoveResults(ctx context.Context, isRcptDom bool, domain string, day string) {
2442 dom, err := dns.ParseDomain(domain)
2443 xcheckf(ctx, err, "parsing domain")
2446 err = tlsrptdb.RemoveResultsRecipientDomain(ctx, dom, day)
2447 xcheckf(ctx, err, "removing tls results")
2449 err = tlsrptdb.RemoveResultsPolicyDomain(ctx, dom, day)
2450 xcheckf(ctx, err, "removing tls results")
2454// TLSRPTSuppressAdd adds a reporting address to the suppress list. Outgoing
2455// reports will be suppressed for a period.
2456func (Admin) TLSRPTSuppressAdd(ctx context.Context, reportingAddress string, until time.Time, comment string) {
2457 addr, err := smtp.ParseAddress(reportingAddress)
2458 xcheckuserf(ctx, err, "parsing reporting address")
2460 ba := tlsrptdb.SuppressAddress{ReportingAddress: addr.String(), Until: until, Comment: comment}
2461 err = tlsrptdb.SuppressAdd(ctx, &ba)
2462 xcheckf(ctx, err, "adding address to suppresslist")
2465// TLSRPTSuppressList returns all reporting addresses on the suppress list.
2466func (Admin) TLSRPTSuppressList(ctx context.Context) []tlsrptdb.SuppressAddress {
2467 l, err := tlsrptdb.SuppressList(ctx)
2468 xcheckf(ctx, err, "listing reporting addresses in suppresslist")
2472// TLSRPTSuppressRemove removes a reporting address record from the suppress list.
2473func (Admin) TLSRPTSuppressRemove(ctx context.Context, id int64) {
2474 err := tlsrptdb.SuppressRemove(ctx, id)
2475 xcheckf(ctx, err, "removing reporting address from suppresslist")
2478// TLSRPTSuppressExtend updates the until field of a suppressed reporting address record.
2479func (Admin) TLSRPTSuppressExtend(ctx context.Context, id int64, until time.Time) {
2480 err := tlsrptdb.SuppressUpdate(ctx, id, until)
2481 xcheckf(ctx, err, "updating reporting address in suppresslist")
2484// LookupCid turns an ID from a Received header into a cid as used in logging.
2485func (Admin) LookupCid(ctx context.Context, recvID string) (cid string) {
2486 v, err := mox.ReceivedToCid(recvID)
2487 xcheckf(ctx, err, "received id to cid")
2488 return fmt.Sprintf("%x", v)
2491// Config returns the dynamic config.
2492func (Admin) Config(ctx context.Context) config.Dynamic {
2493 return mox.Conf.DynamicConfig()
2496// AccountRoutesSave saves routes for an account.
2497func (Admin) AccountRoutesSave(ctx context.Context, accountName string, routes []config.Route) {
2498 err := admin.AccountSave(ctx, accountName, func(acc *config.Account) {
2501 xcheckf(ctx, err, "saving account routes")
2504// DomainRoutesSave saves routes for a domain.
2505func (Admin) DomainRoutesSave(ctx context.Context, domainName string, routes []config.Route) {
2506 err := admin.DomainSave(ctx, domainName, func(domain *config.Domain) error {
2507 domain.Routes = routes
2510 xcheckf(ctx, err, "saving domain routes")
2513// RoutesSave saves global routes.
2514func (Admin) RoutesSave(ctx context.Context, routes []config.Route) {
2515 err := admin.ConfigSave(ctx, func(config *config.Dynamic) {
2516 config.Routes = routes
2518 xcheckf(ctx, err, "saving global routes")
2521// DomainDescriptionSave saves the description for a domain.
2522func (Admin) DomainDescriptionSave(ctx context.Context, domainName, descr string) {
2523 err := admin.DomainSave(ctx, domainName, func(domain *config.Domain) error {
2524 domain.Description = descr
2527 xcheckf(ctx, err, "saving domain description")
2530// DomainClientSettingsDomainSave saves the client settings domain for a domain.
2531func (Admin) DomainClientSettingsDomainSave(ctx context.Context, domainName, clientSettingsDomain string) {
2532 err := admin.DomainSave(ctx, domainName, func(domain *config.Domain) error {
2533 domain.ClientSettingsDomain = clientSettingsDomain
2536 xcheckf(ctx, err, "saving client settings domain")
2539// DomainLocalpartConfigSave saves the localpart catchall and case-sensitive
2540// settings for a domain.
2541func (Admin) DomainLocalpartConfigSave(ctx context.Context, domainName string, localpartCatchallSeparators []string, localpartCaseSensitive bool) {
2542 err := admin.DomainSave(ctx, domainName, func(domain *config.Domain) error {
2543 // We don't allow introducing new catchall separators that are used in DMARC/TLS
2544 // reporting. Can occur in existing configs for backwards compatibility.
2545 containsSep := func(seps []string) bool {
2546 for _, sep := range seps {
2547 if domain.DMARC != nil && strings.Contains(domain.DMARC.Localpart, sep) {
2550 if domain.TLSRPT != nil && strings.Contains(domain.TLSRPT.Localpart, sep) {
2556 if !containsSep(domain.LocalpartCatchallSeparatorsEffective) && containsSep(localpartCatchallSeparators) {
2557 xusererrorf(ctx, "cannot add localpart catchall separators that are used in dmarc and/or tls reporting addresses, change reporting addresses first")
2560 domain.LocalpartCatchallSeparatorsEffective = localpartCatchallSeparators
2561 // If there is a single separator, we prefer the non-list form, it's easier to
2562 // read/edit and should suffice for most setups.
2563 domain.LocalpartCatchallSeparator = ""
2564 domain.LocalpartCatchallSeparators = nil
2565 if len(localpartCatchallSeparators) == 1 {
2566 domain.LocalpartCatchallSeparator = localpartCatchallSeparators[0]
2568 domain.LocalpartCatchallSeparators = localpartCatchallSeparators
2571 domain.LocalpartCaseSensitive = localpartCaseSensitive
2574 xcheckf(ctx, err, "saving localpart settings for domain")
2577// DomainDMARCAddressSave saves the DMARC reporting address/processing
2578// configuration for a domain. If localpart is empty, processing reports is
2580func (Admin) DomainDMARCAddressSave(ctx context.Context, domainName, localpart, domain, account, mailbox string) {
2581 err := admin.DomainSave(ctx, domainName, func(d *config.Domain) error {
2582 // DMARC reporting addresses can contain the localpart catchall separator(s) for
2583 // backwards compability (hence not enforced when parsing the config files), but we
2584 // don't allow creating them.
2585 if d.DMARC == nil || d.DMARC.Localpart != localpart {
2586 for _, sep := range d.LocalpartCatchallSeparatorsEffective {
2587 if strings.Contains(localpart, sep) {
2588 xusererrorf(ctx, "dmarc reporting address cannot contain catchall separator %q in localpart (%q)", sep, localpart)
2593 if localpart == "" {
2596 d.DMARC = &config.DMARC{
2597 Localpart: localpart,
2605 xcheckf(ctx, err, "saving dmarc reporting address/settings for domain")
2608// DomainTLSRPTAddressSave saves the TLS reporting address/processing
2609// configuration for a domain. If localpart is empty, processing reports is
2611func (Admin) DomainTLSRPTAddressSave(ctx context.Context, domainName, localpart, domain, account, mailbox string) {
2612 err := admin.DomainSave(ctx, domainName, func(d *config.Domain) error {
2613 // TLS reporting addresses can contain the localpart catchall separator(s) for
2614 // backwards compability (hence not enforced when parsing the config files), but we
2615 // don't allow creating them.
2616 if d.TLSRPT == nil || d.TLSRPT.Localpart != localpart {
2617 for _, sep := range d.LocalpartCatchallSeparatorsEffective {
2618 if strings.Contains(localpart, sep) {
2619 xusererrorf(ctx, "tls reporting address cannot contain catchall separator %q in localpart (%q)", sep, localpart)
2624 if localpart == "" {
2627 d.TLSRPT = &config.TLSRPT{
2628 Localpart: localpart,
2636 xcheckf(ctx, err, "saving tls reporting address/settings for domain")
2639// DomainMTASTSSave saves the MTASTS policy for a domain. If policyID is empty,
2640// no MTASTS policy is served.
2641func (Admin) DomainMTASTSSave(ctx context.Context, domainName, policyID string, mode mtasts.Mode, maxAge time.Duration, mx []string) {
2642 err := admin.DomainSave(ctx, domainName, func(d *config.Domain) error {
2646 d.MTASTS = &config.MTASTS{
2655 xcheckf(ctx, err, "saving mtasts policy for domain")
2658// DomainDKIMAdd adds a DKIM selector for a domain, generating a new private
2659// key. The selector is not enabled for signing.
2660func (Admin) DomainDKIMAdd(ctx context.Context, domainName, selector, algorithm, hash string, headerRelaxed, bodyRelaxed, seal bool, headers []string, lifetime time.Duration) {
2661 d, err := dns.ParseDomain(domainName)
2662 xcheckuserf(ctx, err, "parsing domain")
2663 s, err := dns.ParseDomain(selector)
2664 xcheckuserf(ctx, err, "parsing selector")
2665 err = admin.DKIMAdd(ctx, d, s, algorithm, hash, headerRelaxed, bodyRelaxed, seal, headers, lifetime)
2666 xcheckf(ctx, err, "adding dkim key")
2669// DomainDKIMRemove removes a DKIM selector for a domain.
2670func (Admin) DomainDKIMRemove(ctx context.Context, domainName, selector string) {
2671 d, err := dns.ParseDomain(domainName)
2672 xcheckuserf(ctx, err, "parsing domain")
2673 s, err := dns.ParseDomain(selector)
2674 xcheckuserf(ctx, err, "parsing selector")
2675 err = admin.DKIMRemove(ctx, d, s)
2676 xcheckf(ctx, err, "removing dkim key")
2679// DomainDKIMSave saves the settings of selectors, and which to enable for
2680// signing, for a domain. All currently configured selectors must be present,
2681// selectors cannot be added/removed with this function.
2682func (Admin) DomainDKIMSave(ctx context.Context, domainName string, selectors map[string]config.Selector, sign []string) {
2683 for _, s := range sign {
2684 if _, ok := selectors[s]; !ok {
2685 xcheckuserf(ctx, fmt.Errorf("cannot sign unknown selector %q", s), "checking selectors")
2689 err := admin.DomainSave(ctx, domainName, func(d *config.Domain) error {
2690 if len(selectors) != len(d.DKIM.Selectors) {
2691 xcheckuserf(ctx, fmt.Errorf("cannot add/remove dkim selectors with this function"), "checking selectors")
2693 for s := range selectors {
2694 if _, ok := d.DKIM.Selectors[s]; !ok {
2695 xcheckuserf(ctx, fmt.Errorf("unknown selector %q", s), "checking selectors")
2698 // At least the selectors are the same.
2700 // Build up new selectors.
2701 sels := map[string]config.Selector{}
2702 for name, nsel := range selectors {
2703 osel := d.DKIM.Selectors[name]
2704 xsel := config.Selector{
2706 Canonicalization: nsel.Canonicalization,
2707 DontSealHeaders: nsel.DontSealHeaders,
2708 Expiration: nsel.Expiration,
2710 PrivateKeyFile: osel.PrivateKeyFile,
2712 if !slices.Equal(osel.HeadersEffective, nsel.Headers) {
2713 xsel.Headers = nsel.Headers
2718 // Enable the new selector settings.
2719 d.DKIM = config.DKIM{
2725 xcheckf(ctx, err, "saving dkim selector for domain")
2728// DomainDisabledSave saves the Disabled field of a domain. A disabled domain
2729// rejects incoming/outgoing messages involving the domain and does not request new
2730// TLS certificats with ACME.
2731func (Admin) DomainDisabledSave(ctx context.Context, domainName string, disabled bool) {
2732 err := admin.DomainSave(ctx, domainName, func(d *config.Domain) error {
2733 d.Disabled = disabled
2736 xcheckf(ctx, err, "saving disabled setting for domain")
2739func xparseAddress(ctx context.Context, lp, domain string) smtp.Address {
2740 xlp, err := smtp.ParseLocalpart(lp)
2741 xcheckuserf(ctx, err, "parsing localpart")
2742 d, err := dns.ParseDomain(domain)
2743 xcheckuserf(ctx, err, "parsing domain")
2744 return smtp.NewAddress(xlp, d)
2747func (Admin) AliasAdd(ctx context.Context, aliaslp string, domainName string, alias config.Alias) {
2748 addr := xparseAddress(ctx, aliaslp, domainName)
2749 err := admin.AliasAdd(ctx, addr, alias)
2750 xcheckf(ctx, err, "adding alias")
2753func (Admin) AliasUpdate(ctx context.Context, aliaslp string, domainName string, postPublic, listMembers, allowMsgFrom bool) {
2754 addr := xparseAddress(ctx, aliaslp, domainName)
2755 alias := config.Alias{
2756 PostPublic: postPublic,
2757 ListMembers: listMembers,
2758 AllowMsgFrom: allowMsgFrom,
2760 err := admin.AliasUpdate(ctx, addr, alias)
2761 xcheckf(ctx, err, "saving alias")
2764func (Admin) AliasRemove(ctx context.Context, aliaslp string, domainName string) {
2765 addr := xparseAddress(ctx, aliaslp, domainName)
2766 err := admin.AliasRemove(ctx, addr)
2767 xcheckf(ctx, err, "removing alias")
2770func (Admin) AliasAddressesAdd(ctx context.Context, aliaslp string, domainName string, addresses []string) {
2771 addr := xparseAddress(ctx, aliaslp, domainName)
2772 err := admin.AliasAddressesAdd(ctx, addr, addresses)
2773 xcheckf(ctx, err, "adding address to alias")
2776func (Admin) AliasAddressesRemove(ctx context.Context, aliaslp string, domainName string, addresses []string) {
2777 addr := xparseAddress(ctx, aliaslp, domainName)
2778 err := admin.AliasAddressesRemove(ctx, addr, addresses)
2779 xcheckf(ctx, err, "removing address from alias")
2782func (Admin) TLSPublicKeys(ctx context.Context, accountOpt string) ([]store.TLSPublicKey, error) {
2783 return store.TLSPublicKeyList(ctx, accountOpt)
2786func (Admin) LoginAttempts(ctx context.Context, accountName string, limit int) []store.LoginAttempt {
2787 l, err := store.LoginAttemptList(ctx, accountName, limit)
2788 xcheckf(ctx, err, "listing login attempts")