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"
38 "golang.org/x/exp/maps"
39 "golang.org/x/text/unicode/norm"
41 "github.com/mjl-/adns"
43 "github.com/mjl-/bstore"
44 "github.com/mjl-/sherpa"
45 "github.com/mjl-/sherpadoc"
46 "github.com/mjl-/sherpaprom"
48 "github.com/mjl-/mox/config"
49 "github.com/mjl-/mox/dkim"
50 "github.com/mjl-/mox/dmarc"
51 "github.com/mjl-/mox/dmarcdb"
52 "github.com/mjl-/mox/dmarcrpt"
53 "github.com/mjl-/mox/dns"
54 "github.com/mjl-/mox/dnsbl"
55 "github.com/mjl-/mox/metrics"
56 "github.com/mjl-/mox/mlog"
57 mox "github.com/mjl-/mox/mox-"
58 "github.com/mjl-/mox/moxvar"
59 "github.com/mjl-/mox/mtasts"
60 "github.com/mjl-/mox/mtastsdb"
61 "github.com/mjl-/mox/publicsuffix"
62 "github.com/mjl-/mox/queue"
63 "github.com/mjl-/mox/smtp"
64 "github.com/mjl-/mox/spf"
65 "github.com/mjl-/mox/store"
66 "github.com/mjl-/mox/tlsrpt"
67 "github.com/mjl-/mox/tlsrptdb"
68 "github.com/mjl-/mox/webauth"
71var pkglog = mlog.New("webadmin", nil)
74var adminapiJSON []byte
82var webadminFile = &mox.WebappFile{
85 HTMLPath: filepath.FromSlash("webadmin/admin.html"),
86 JSPath: filepath.FromSlash("webadmin/admin.js"),
89var adminDoc = mustParseAPI("admin", adminapiJSON)
91func mustParseAPI(api string, buf []byte) (doc sherpadoc.Section) {
92 err := json.Unmarshal(buf, &doc)
94 pkglog.Fatalx("parsing webadmin api docs", err, slog.String("api", api))
99var sherpaHandlerOpts *sherpa.HandlerOpts
101func makeSherpaHandler(cookiePath string, isForwarded bool) (http.Handler, error) {
102 return sherpa.NewHandler("/api/", moxvar.Version, Admin{cookiePath, isForwarded}, &adminDoc, sherpaHandlerOpts)
106 collector, err := sherpaprom.NewCollector("moxadmin", nil)
108 pkglog.Fatalx("creating sherpa prometheus collector", err)
111 sherpaHandlerOpts = &sherpa.HandlerOpts{Collector: collector, AdjustFunctionNames: "none", NoCORS: true}
113 _, err = makeSherpaHandler("", false)
115 pkglog.Fatalx("sherpa handler", err)
119// Handler returns a handler for the webadmin endpoints, customized for the
121func Handler(cookiePath string, isForwarded bool) func(w http.ResponseWriter, r *http.Request) {
122 sh, err := makeSherpaHandler(cookiePath, isForwarded)
123 return func(w http.ResponseWriter, r *http.Request) {
125 http.Error(w, "500 - internal server error - cannot handle requests", http.StatusInternalServerError)
128 handle(sh, isForwarded, w, r)
132// Admin exports web API functions for the admin web interface. All its methods are
133// exported under api/. Function calls require valid HTTP Authentication
134// credentials of a user.
136 cookiePath string // From listener, for setting authentication cookies.
137 isForwarded bool // From listener, whether we look at X-Forwarded-* headers.
142var requestInfoCtxKey ctxKey = "requestInfo"
144type requestInfo struct {
145 SessionToken store.SessionToken
146 Response http.ResponseWriter
147 Request *http.Request // For Proto and TLS connection state during message submit.
150func handle(apiHandler http.Handler, isForwarded bool, w http.ResponseWriter, r *http.Request) {
151 ctx := context.WithValue(r.Context(), mlog.CidKey, mox.Cid())
152 log := pkglog.WithContext(ctx).With(slog.String("adminauth", ""))
154 // HTML/JS can be retrieved without authentication.
155 if r.URL.Path == "/" {
158 webadminFile.Serve(ctx, log, w, r)
160 http.Error(w, "405 - method not allowed - use get", http.StatusMethodNotAllowed)
165 isAPI := strings.HasPrefix(r.URL.Path, "/api/")
166 // Only allow POST for calls, they will not work cross-domain without CORS.
167 if isAPI && r.URL.Path != "/api/" && r.Method != "POST" {
168 http.Error(w, "405 - method not allowed - use post", http.StatusMethodNotAllowed)
172 // All other URLs, except the login endpoint require some authentication.
173 var sessionToken store.SessionToken
174 if r.URL.Path != "/api/LoginPrep" && r.URL.Path != "/api/Login" {
176 _, sessionToken, _, ok = webauth.Check(ctx, log, webauth.Admin, "webadmin", isForwarded, w, r, isAPI, isAPI, false)
178 // Response has been written already.
184 reqInfo := requestInfo{sessionToken, w, r}
185 ctx = context.WithValue(ctx, requestInfoCtxKey, reqInfo)
186 apiHandler.ServeHTTP(w, r.WithContext(ctx))
193func xcheckf(ctx context.Context, err error, format string, args ...any) {
197 msg := fmt.Sprintf(format, args...)
198 errmsg := fmt.Sprintf("%s: %s", msg, err)
199 pkglog.WithContext(ctx).Errorx(msg, err)
200 code := "server:error"
201 if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
204 panic(&sherpa.Error{Code: code, Message: errmsg})
207func xcheckuserf(ctx context.Context, err error, format string, args ...any) {
211 msg := fmt.Sprintf(format, args...)
212 errmsg := fmt.Sprintf("%s: %s", msg, err)
213 pkglog.WithContext(ctx).Errorx(msg, err)
214 panic(&sherpa.Error{Code: "user:error", Message: errmsg})
217func xusererrorf(ctx context.Context, format string, args ...any) {
218 msg := fmt.Sprintf(format, args...)
219 pkglog.WithContext(ctx).Error(msg)
220 panic(&sherpa.Error{Code: "user:error", Message: msg})
223// LoginPrep returns a login token, and also sets it as cookie. Both must be
224// present in the call to Login.
225func (w Admin) LoginPrep(ctx context.Context) string {
226 log := pkglog.WithContext(ctx)
227 reqInfo := ctx.Value(requestInfoCtxKey).(requestInfo)
230 _, err := cryptorand.Read(data[:])
231 xcheckf(ctx, err, "generate token")
232 loginToken := base64.RawURLEncoding.EncodeToString(data[:])
234 webauth.LoginPrep(ctx, log, "webadmin", w.cookiePath, w.isForwarded, reqInfo.Response, reqInfo.Request, loginToken)
239// Login returns a session token for the credentials, or fails with error code
240// "user:badLogin". Call LoginPrep to get a loginToken.
241func (w Admin) Login(ctx context.Context, loginToken, password string) store.CSRFToken {
242 log := pkglog.WithContext(ctx)
243 reqInfo := ctx.Value(requestInfoCtxKey).(requestInfo)
245 csrfToken, err := webauth.Login(ctx, log, webauth.Admin, "webadmin", w.cookiePath, w.isForwarded, reqInfo.Response, reqInfo.Request, loginToken, "", password)
246 if _, ok := err.(*sherpa.Error); ok {
249 xcheckf(ctx, err, "login")
253// Logout invalidates the session token.
254func (w Admin) Logout(ctx context.Context) {
255 log := pkglog.WithContext(ctx)
256 reqInfo := ctx.Value(requestInfoCtxKey).(requestInfo)
258 err := webauth.Logout(ctx, log, webauth.Admin, "webadmin", w.cookiePath, w.isForwarded, reqInfo.Response, reqInfo.Request, "", reqInfo.SessionToken)
259 xcheckf(ctx, err, "logout")
265 Instructions []string
268type DNSSECResult struct {
272type IPRevCheckResult struct {
273 Hostname dns.Domain // This hostname, IPs must resolve back to this.
274 IPNames map[string][]string // IP to names.
284type MXCheckResult struct {
289type TLSCheckResult struct {
293type DANECheckResult struct {
297type SPFRecord struct {
301type SPFCheckResult struct {
303 DomainRecord *SPFRecord
305 HostRecord *SPFRecord
309type DKIMCheckResult struct {
314type DKIMRecord struct {
320type DMARCRecord struct {
324type DMARCCheckResult struct {
331type TLSRPTRecord struct {
335type TLSRPTCheckResult struct {
341type MTASTSRecord struct {
344type MTASTSCheckResult struct {
348 Policy *mtasts.Policy
352type SRVConfCheckResult struct {
353 SRVs map[string][]net.SRV // Service (e.g. "_imaps") to records.
357type AutoconfCheckResult struct {
358 ClientSettingsDomainIPs []string
363type AutodiscoverSRV struct {
368type AutodiscoverCheckResult struct {
369 Records []AutodiscoverSRV
373// CheckResult is the analysis of a domain, its actual configuration (DNS, TLS,
374// connectivity) and the mox configuration. It includes configuration instructions
375// (e.g. DNS records), and warnings and errors encountered.
376type CheckResult struct {
379 IPRev IPRevCheckResult
385 DMARC DMARCCheckResult
386 HostTLSRPT TLSRPTCheckResult
387 DomainTLSRPT TLSRPTCheckResult
388 MTASTS MTASTSCheckResult
389 SRVConf SRVConfCheckResult
390 Autoconf AutoconfCheckResult
391 Autodiscover AutodiscoverCheckResult
394// logPanic can be called with a defer from a goroutine to prevent the entire program from being shutdown in case of a panic.
395func logPanic(ctx context.Context) {
400 pkglog.WithContext(ctx).Error("recover from panic", slog.Any("panic", x))
402 metrics.PanicInc(metrics.Webadmin)
405// return IPs we may be listening on.
406func xlistenIPs(ctx context.Context, receiveOnly bool) []net.IP {
407 ips, err := mox.IPs(ctx, receiveOnly)
408 xcheckf(ctx, err, "listing ips")
412// return IPs from which we may be sending.
413func xsendingIPs(ctx context.Context) []net.IP {
414 ips, err := mox.IPs(ctx, false)
415 xcheckf(ctx, err, "listing ips")
419// CheckDomain checks the configuration for the domain, such as MX, SMTP STARTTLS,
420// SPF, DKIM, DMARC, TLSRPT, MTASTS, autoconfig, autodiscover.
421func (Admin) CheckDomain(ctx context.Context, domainName string) (r CheckResult) {
422 // todo future: should run these checks without a DNS cache so recent changes are picked up.
424 resolver := dns.StrictResolver{Pkg: "check", Log: pkglog.WithContext(ctx).Logger}
425 dialer := &net.Dialer{Timeout: 10 * time.Second}
426 nctx, cancel := context.WithTimeout(ctx, 30*time.Second)
428 return checkDomain(nctx, resolver, dialer, domainName)
431func unptr[T any](l []*T) []T {
435 r := make([]T, len(l))
436 for i, e := range l {
442func checkDomain(ctx context.Context, resolver dns.Resolver, dialer *net.Dialer, domainName string) (r CheckResult) {
443 log := pkglog.WithContext(ctx)
445 domain, err := dns.ParseDomain(domainName)
446 xcheckuserf(ctx, err, "parsing domain")
448 domConf, ok := mox.Conf.Domain(domain)
450 panic(&sherpa.Error{Code: "user:notFound", Message: "domain not found"})
453 listenIPs := xlistenIPs(ctx, true)
454 isListenIP := func(ip net.IP) bool {
455 for _, lip := range listenIPs {
463 addf := func(l *[]string, format string, args ...any) {
464 *l = append(*l, fmt.Sprintf(format, args...))
467 // Host must be an absolute dns name, ending with a dot.
468 lookupIPs := func(errors *[]string, host string) (ips []string, ourIPs, notOurIPs []net.IP, rerr error) {
469 addrs, _, err := resolver.LookupHost(ctx, host)
471 addf(errors, "Looking up %q: %s", host, err)
472 return nil, nil, nil, err
474 for _, addr := range addrs {
475 ip := net.ParseIP(addr)
477 addf(errors, "Bad IP %q", addr)
480 ips = append(ips, ip.String())
482 ourIPs = append(ourIPs, ip)
484 notOurIPs = append(notOurIPs, ip)
487 return ips, ourIPs, notOurIPs, nil
490 checkTLS := func(errors *[]string, host string, ips []string, port string) {
496 RootCAs: mox.Conf.Static.TLS.CertPool,
499 for _, ip := range ips {
500 conn, err := d.DialContext(ctx, "tcp", net.JoinHostPort(ip, port))
502 addf(errors, "TLS connection to hostname %q, IP %q: %s", host, ip, err)
509 // If at least one listener with SMTP enabled has unspecified NATed IPs, we'll skip
510 // some checks related to these IPs.
511 var isNAT, isUnspecifiedNAT bool
512 for _, l := range mox.Conf.Static.Listeners {
517 isUnspecifiedNAT = true
520 if len(l.NATIPs) > 0 {
525 var wg sync.WaitGroup
533 // Some DNSSEC-verifying resolvers return unauthentic data for ".", so we check "com".
534 _, result, err := resolver.LookupNS(ctx, "com.")
536 addf(&r.DNSSEC.Errors, "Looking up NS for DNS root (.) to check support in resolver for DNSSEC-verification: %s", err)
537 } else if !result.Authentic {
538 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.`)
540 _, result, _ := resolver.LookupMX(ctx, domain.ASCII+".")
541 if !result.Authentic {
542 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.`)
546 addf(&r.DNSSEC.Instructions, `Enable DNSSEC-signing of the DNS records of your domain (zone) at your DNS hosting provider.`)
548 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".
550cat <<EOF >/etc/unbound/unbound.conf.d/ede.conf
564 // For each mox.Conf.SpecifiedSMTPListenIPs and all NATIPs, and each IP for
565 // mox.Conf.HostnameDomain, check if they resolve back to the host name.
566 hostIPs := map[dns.Domain][]net.IP{}
567 ips, _, err := resolver.LookupIP(ctx, "ip", mox.Conf.Static.HostnameDomain.ASCII+".")
569 addf(&r.IPRev.Errors, "Looking up IPs for hostname: %s", err)
572 gatherMoreIPs := func(publicIPs []net.IP) {
574 for _, ip := range publicIPs {
575 for _, xip := range ips {
580 ips = append(ips, ip)
584 gatherMoreIPs(mox.Conf.Static.SpecifiedSMTPListenIPs)
586 for _, l := range mox.Conf.Static.Listeners {
591 for _, ip := range l.NATIPs {
592 natips = append(natips, net.ParseIP(ip))
594 gatherMoreIPs(natips)
596 hostIPs[mox.Conf.Static.HostnameDomain] = ips
598 iplist := func(ips []net.IP) string {
600 for _, ip := range ips {
601 ipstrs = append(ipstrs, ip.String())
603 return strings.Join(ipstrs, ", ")
606 r.IPRev.Hostname = mox.Conf.Static.HostnameDomain
607 r.IPRev.Instructions = []string{
608 fmt.Sprintf("Ensure IPs %s have reverse address %s.", iplist(ips), mox.Conf.Static.HostnameDomain.ASCII),
611 // If we have a socks transport, also check its host and IP.
612 for tname, t := range mox.Conf.Static.Transports {
614 hostIPs[t.Socks.Hostname] = append(hostIPs[t.Socks.Hostname], t.Socks.IPs...)
615 instr := fmt.Sprintf("For SOCKS transport %s, ensure IPs %s have reverse address %s.", tname, iplist(t.Socks.IPs), t.Socks.Hostname)
616 r.IPRev.Instructions = append(r.IPRev.Instructions, instr)
626 results := make(chan result)
628 for host, ips := range hostIPs {
629 for _, ip := range ips {
634 addrs, _, err := resolver.LookupAddr(ctx, s)
635 results <- result{host, s, addrs, err}
639 r.IPRev.IPNames = map[string][]string{}
640 for i := 0; i < n; i++ {
642 host, addrs, ip, err := lr.Host, lr.Addrs, lr.IP, lr.Err
644 addf(&r.IPRev.Errors, "Looking up reverse name for %s of %s: %v", ip, host, err)
648 addf(&r.IPRev.Errors, "Expected exactly 1 name for %s of %s, got %d (%v)", ip, host, len(addrs), addrs)
651 for i, a := range addrs {
652 a = strings.TrimRight(a, ".")
654 ad, err := dns.ParseDomain(a)
656 addf(&r.IPRev.Errors, "Parsing reverse name %q for %s: %v", a, ip, err)
663 addf(&r.IPRev.Errors, "Reverse name(s) %s for ip %s do not match hostname %s, which will cause other mail servers to reject incoming messages from this IP.", strings.Join(addrs, ","), ip, host)
665 r.IPRev.IPNames[ip] = addrs
668 // Linux machines are often initially set up with a loopback IP for the hostname in
669 // /etc/hosts, presumably because it isn't known if their external IPs are static.
670 // For mail servers, they should certainly be static. The quickstart would also
671 // have warned about this, but could have been missed/ignored.
672 for _, ip := range ips {
674 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())
685 mxs, _, err := resolver.LookupMX(ctx, domain.ASCII+".")
687 addf(&r.MX.Errors, "Looking up MX records for %s: %s", domain, err)
689 r.MX.Records = make([]MX, len(mxs))
690 for i, mx := range mxs {
691 r.MX.Records[i] = MX{mx.Host, int(mx.Pref), nil}
693 if len(mxs) == 1 && mxs[0].Host == "." {
694 addf(&r.MX.Errors, `MX records consists of explicit null mx record (".") indicating that domain does not accept email.`)
697 for i, mx := range mxs {
698 ips, ourIPs, notOurIPs, err := lookupIPs(&r.MX.Errors, mx.Host)
700 addf(&r.MX.Errors, "Looking up IPs for mx host %q: %s", mx.Host, err)
702 r.MX.Records[i].IPs = ips
703 if isUnspecifiedNAT {
706 if len(ourIPs) == 0 {
707 addf(&r.MX.Errors, "None of the IPs that mx %q points to is ours: %v", mx.Host, notOurIPs)
708 } else if len(notOurIPs) > 0 {
709 addf(&r.MX.Errors, "Some of the IPs that mx %q points to are not ours: %v", mx.Host, notOurIPs)
713 r.MX.Instructions = []string{
714 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+"."),
718 // TLS, mostly checking certificate expiration and CA trust.
719 // 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.
725 // MTA-STS, autoconfig, autodiscover are checked in their sections.
727 // Dial a single MX host with given IP and perform STARTTLS handshake.
728 dialSMTPSTARTTLS := func(host, ip string) error {
729 conn, err := dialer.DialContext(ctx, "tcp", net.JoinHostPort(ip, "25"))
739 end := time.Now().Add(10 * time.Second)
740 cctx, cancel := context.WithTimeout(ctx, 10*time.Second)
742 err = conn.SetDeadline(end)
743 log.WithContext(ctx).Check(err, "setting deadline")
745 br := bufio.NewReader(conn)
746 _, err = br.ReadString('\n')
748 return fmt.Errorf("reading SMTP banner from remote: %s", err)
750 if _, err := fmt.Fprintf(conn, "EHLO moxtest\r\n"); err != nil {
751 return fmt.Errorf("writing SMTP EHLO to remote: %s", err)
754 line, err := br.ReadString('\n')
756 return fmt.Errorf("reading SMTP EHLO response from remote: %s", err)
758 if strings.HasPrefix(line, "250-") {
761 if strings.HasPrefix(line, "250 ") {
764 return fmt.Errorf("unexpected response to SMTP EHLO from remote: %q", strings.TrimSuffix(line, "\r\n"))
766 if _, err := fmt.Fprintf(conn, "STARTTLS\r\n"); err != nil {
767 return fmt.Errorf("writing SMTP STARTTLS to remote: %s", err)
769 line, err := br.ReadString('\n')
771 return fmt.Errorf("reading response to SMTP STARTTLS from remote: %s", err)
773 if !strings.HasPrefix(line, "220 ") {
774 return fmt.Errorf("SMTP STARTTLS response from remote not 220 OK: %q", strings.TrimSuffix(line, "\r\n"))
776 config := &tls.Config{
778 RootCAs: mox.Conf.Static.TLS.CertPool,
780 tlsconn := tls.Client(conn, config)
781 if err := tlsconn.HandshakeContext(cctx); err != nil {
782 return fmt.Errorf("TLS handshake after SMTP STARTTLS: %s", err)
790 checkSMTPSTARTTLS := func() {
791 // Initial errors are ignored, will already have been warned about by MX checks.
792 mxs, _, err := resolver.LookupMX(ctx, domain.ASCII+".")
796 if len(mxs) == 1 && mxs[0].Host == "." {
799 for _, mx := range mxs {
800 ips, _, _, err := lookupIPs(&r.MX.Errors, mx.Host)
805 for _, ip := range ips {
806 if err := dialSMTPSTARTTLS(mx.Host, ip); err != nil {
807 addf(&r.TLS.Errors, "SMTP connection with STARTTLS to MX hostname %q IP %s: %s", mx.Host, ip, err)
823 daneRecords := func(l config.Listener) map[string]struct{} {
827 records := map[string]struct{}{}
828 addRecord := func(privKey crypto.Signer) {
829 spkiBuf, err := x509.MarshalPKIXPublicKey(privKey.Public())
831 addf(&r.DANE.Errors, "marshal SubjectPublicKeyInfo for DANE record: %v", err)
834 sum := sha256.Sum256(spkiBuf)
836 Usage: adns.TLSAUsageDANEEE,
837 Selector: adns.TLSASelectorSPKI,
838 MatchType: adns.TLSAMatchTypeSHA256,
841 records[r.Record()] = struct{}{}
843 for _, privKey := range l.TLS.HostPrivateRSA2048Keys {
846 for _, privKey := range l.TLS.HostPrivateECDSAP256Keys {
852 expectedDANERecords := func(host string) map[string]struct{} {
853 for _, l := range mox.Conf.Static.Listeners {
854 if l.HostnameDomain.ASCII == host {
855 return daneRecords(l)
858 public := mox.Conf.Static.Listeners["public"]
859 if mox.Conf.Static.HostnameDomain.ASCII == host && public.HostnameDomain.ASCII == "" {
860 return daneRecords(public)
865 mxl, result, err := resolver.LookupMX(ctx, domain.ASCII+".")
867 addf(&r.DANE.Errors, "Looking up MX hosts to check for DANE records: %s", err)
869 if !result.Authentic {
870 addf(&r.DANE.Warnings, "DANE is inactive because MX records are not DNSSEC-signed.")
872 for _, mx := range mxl {
873 expect := expectedDANERecords(mx.Host)
875 tlsal, tlsaResult, err := resolver.LookupTLSA(ctx, 25, "tcp", mx.Host+".")
876 if dns.IsNotFound(err) {
878 addf(&r.DANE.Errors, "No DANE records for MX host %s, expected: %s.", mx.Host, strings.Join(maps.Keys(expect), "; "))
881 } else if err != nil {
882 addf(&r.DANE.Errors, "Looking up DANE records for MX host %s: %v", mx.Host, err)
884 } else if !tlsaResult.Authentic && len(tlsal) > 0 {
885 addf(&r.DANE.Errors, "DANE records exist for MX host %s, but are not DNSSEC-signed.", mx.Host)
888 extra := map[string]struct{}{}
889 for _, e := range tlsal {
891 if _, ok := expect[s]; ok {
894 extra[s] = struct{}{}
898 l := maps.Keys(expect)
900 addf(&r.DANE.Errors, "Missing DANE records of type TLSA for MX host _25._tcp.%s: %s", mx.Host, strings.Join(l, "; "))
903 l := maps.Keys(extra)
905 addf(&r.DANE.Errors, "Unexpected DANE records of type TLSA for MX host _25._tcp.%s: %s", mx.Host, strings.Join(l, "; "))
910 public := mox.Conf.Static.Listeners["public"]
911 pubDom := public.HostnameDomain
912 if pubDom.ASCII == "" {
913 pubDom = mox.Conf.Static.HostnameDomain
915 records := maps.Keys(daneRecords(public))
916 sort.Strings(records)
917 if len(records) > 0 {
918 instr := "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"
919 for _, r := range records {
920 instr += fmt.Sprintf("\t_25._tcp.%s. TLSA %s\n", pubDom.ASCII, r)
922 addf(&r.DANE.Instructions, instr)
927 // 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.
933 // Verify a domain with the configured IPs that do SMTP.
934 verifySPF := func(kind string, domain dns.Domain) (string, *SPFRecord, spf.Record) {
935 _, txt, record, _, err := spf.Lookup(ctx, log.Logger, resolver, domain)
937 addf(&r.SPF.Errors, "Looking up %s SPF record: %s", kind, err)
939 var xrecord *SPFRecord
941 xrecord = &SPFRecord{*record}
948 checkSPFIP := func(ip net.IP) {
953 spfr.Directives = append(spfr.Directives, spf.Directive{Mechanism: mechanism, IP: ip})
961 MailFromLocalpart: "postmaster",
962 MailFromDomain: domain,
963 HelloDomain: dns.IPDomain{Domain: domain},
964 LocalIP: net.ParseIP("127.0.0.1"),
965 LocalHostname: dns.Domain{ASCII: "localhost"},
967 status, mechanism, expl, _, err := spf.Evaluate(ctx, log.Logger, record, resolver, args)
969 addf(&r.SPF.Errors, "Evaluating IP %q against %s SPF record: %s", ip, kind, err)
970 } else if status != spf.StatusPass {
971 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)
975 for _, l := range mox.Conf.Static.Listeners {
976 if !l.SMTP.Enabled || l.IPsNATed {
980 if len(l.NATIPs) > 0 {
983 for _, ipstr := range ips {
984 ip := net.ParseIP(ipstr)
988 for _, t := range mox.Conf.Static.Transports {
990 for _, ip := range t.Socks.IPs {
996 spfr.Directives = append(spfr.Directives, spf.Directive{Qualifier: "-", Mechanism: "all"})
997 return txt, xrecord, spfr
1000 // Check SPF record for domain.
1001 var dspfr spf.Record
1002 r.SPF.DomainTXT, r.SPF.DomainRecord, dspfr = verifySPF("domain", domain)
1003 // todo: possibly check all hosts for MX records? assuming they are also sending mail servers.
1004 r.SPF.HostTXT, r.SPF.HostRecord, _ = verifySPF("host", mox.Conf.Static.HostnameDomain)
1006 dtxt, err := dspfr.Record()
1008 addf(&r.SPF.Errors, "Making SPF record for instructions: %s", err)
1010 domainspf := fmt.Sprintf("%s TXT %s", domain.ASCII+".", mox.TXTStrings(dtxt))
1013 hostspf := fmt.Sprintf(`%s TXT "v=spf1 a -all"`, mox.Conf.Static.HostnameDomain.ASCII+".")
1015 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)
1019 // 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.
1025 var missing []string
1026 var haveEd25519 bool
1027 for sel, selc := range domConf.DKIM.Selectors {
1028 if _, ok := selc.Key.(ed25519.PrivateKey); ok {
1032 _, record, txt, _, err := dkim.Lookup(ctx, log.Logger, resolver, selc.Domain, domain)
1034 missing = append(missing, sel)
1035 if errors.Is(err, dkim.ErrNoRecord) {
1036 addf(&r.DKIM.Errors, "No DKIM DNS record for selector %q.", sel)
1037 } else if errors.Is(err, dkim.ErrSyntax) {
1038 addf(&r.DKIM.Errors, "Parsing DKIM DNS record for selector %q: %s", sel, err)
1040 addf(&r.DKIM.Errors, "Fetching DKIM record for selector %q: %s", sel, err)
1044 r.DKIM.Records = append(r.DKIM.Records, DKIMRecord{sel, txt, record})
1045 pubKey := selc.Key.Public()
1047 switch k := pubKey.(type) {
1048 case *rsa.PublicKey:
1050 pk, err = x509.MarshalPKIXPublicKey(k)
1052 addf(&r.DKIM.Errors, "Marshal public key for %q to compare against DNS: %s", sel, err)
1055 case ed25519.PublicKey:
1058 addf(&r.DKIM.Errors, "Internal error: unknown public key type %T.", pubKey)
1062 if record != nil && !bytes.Equal(record.Pubkey, pk) {
1063 addf(&r.DKIM.Errors, "For selector %q, the public key in DKIM DNS TXT record does not match with configured private key.", sel)
1064 missing = append(missing, sel)
1068 if len(domConf.DKIM.Selectors) == 0 {
1069 addf(&r.DKIM.Errors, "No DKIM configuration, add a key to the configuration file, and instructions for DNS records will appear here.")
1070 } else if !haveEd25519 {
1071 addf(&r.DKIM.Warnings, "Consider adding an ed25519 key: the keys are smaller, the cryptography faster and more modern.")
1074 for _, sel := range missing {
1075 dkimr := dkim.Record{
1077 Hashes: []string{"sha256"},
1078 PublicKey: domConf.DKIM.Selectors[sel].Key.Public(),
1080 switch dkimr.PublicKey.(type) {
1081 case *rsa.PublicKey:
1082 case ed25519.PublicKey:
1083 dkimr.Key = "ed25519"
1085 addf(&r.DKIM.Errors, "Internal error: unknown public key type %T.", dkimr.PublicKey)
1087 txt, err := dkimr.Record()
1089 addf(&r.DKIM.Errors, "Making DKIM record for instructions: %s", err)
1092 instr += fmt.Sprintf("\n\t%s._domainkey TXT %s\n", sel, mox.TXTStrings(txt))
1095 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
1096 addf(&r.DKIM.Instructions, "%s", instr)
1106 _, dmarcDomain, record, txt, _, err := dmarc.Lookup(ctx, log.Logger, resolver, domain)
1108 addf(&r.DMARC.Errors, "Looking up DMARC record: %s", err)
1109 } else if record == nil {
1110 addf(&r.DMARC.Errors, "No DMARC record")
1112 r.DMARC.Domain = dmarcDomain.Name()
1115 r.DMARC.Record = &DMARCRecord{*record}
1117 if record != nil && record.Policy == "none" {
1118 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.")
1120 if record != nil && record.SubdomainPolicy == "none" {
1121 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.")
1123 if record != nil && len(record.AggregateReportAddresses) == 0 {
1124 addf(&r.DMARC.Warnings, "It is recommended you specify you would like aggregate reports about delivery success in the DMARC record, see instructions.")
1127 dmarcr := dmarc.DefaultRecord
1128 dmarcr.Policy = "reject"
1131 if domConf.DMARC != nil {
1132 // If the domain is in a different Organizational Domain, the receiving domain
1133 // needs a special DNS record to opt-in to receiving reports. We check for that
1136 orgDom := publicsuffix.Lookup(ctx, log.Logger, domain)
1137 destOrgDom := publicsuffix.Lookup(ctx, log.Logger, domConf.DMARC.DNSDomain)
1138 if orgDom != destOrgDom {
1139 accepts, status, _, _, _, err := dmarc.LookupExternalReportsAccepted(ctx, log.Logger, resolver, domain, domConf.DMARC.DNSDomain)
1140 if status != dmarc.StatusNone {
1141 addf(&r.DMARC.Errors, "Checking if external destination accepts reports: %s", err)
1142 } else if !accepts {
1143 addf(&r.DMARC.Errors, "External destination does not accept reports (%s)", err)
1145 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)
1150 Opaque: smtp.NewAddress(domConf.DMARC.ParsedLocalpart, domConf.DMARC.DNSDomain).Pack(false),
1152 uristr := uri.String()
1153 dmarcr.AggregateReportAddresses = []dmarc.URI{
1154 {Address: uristr, MaxSize: 10, Unit: "m"},
1159 for _, addr := range record.AggregateReportAddresses {
1160 if addr.Address == uristr {
1166 addf(&r.DMARC.Errors, "Configured DMARC reporting address is not present in record.")
1170 addf(&r.DMARC.Instructions, `Configure a DMARC destination in domain in config file.`)
1172 instr := fmt.Sprintf("Ensure a DNS TXT record like the following exists:\n\n\t_dmarc 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.", mox.TXTStrings(dmarcr.String()))
1173 addf(&r.DMARC.Instructions, instr)
1175 addf(&r.DMARC.Instructions, extInstr)
1179 checkTLSRPT := func(result *TLSRPTCheckResult, dom dns.Domain, address smtp.Address, isHost bool) {
1183 record, txt, err := tlsrpt.Lookup(ctx, log.Logger, resolver, dom)
1185 addf(&result.Errors, "Looking up TLSRPT record: %s", err)
1189 result.Record = &TLSRPTRecord{*record}
1192 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.`
1193 var zeroaddr smtp.Address
1194 if address != zeroaddr {
1195 // TLSRPT does not require validation of reporting addresses outside the domain.
1199 Opaque: address.Pack(false),
1201 rua := tlsrpt.RUA(uri.String())
1202 tlsrptr := &tlsrpt.Record{
1203 Version: "TLSRPTv1",
1204 RUAs: [][]tlsrpt.RUA{{rua}},
1206 instr += fmt.Sprintf(`
1208Ensure a DNS TXT record like the following exists:
1211`, mox.TXTStrings(tlsrptr.String()))
1216 for _, l := range record.RUAs {
1217 for _, e := range l {
1225 addf(&result.Errors, `Configured reporting address is not present in TLSRPT record.`)
1230 addf(&result.Errors, `Configure a host TLSRPT localpart in static mox.conf config file.`)
1232 addf(&result.Errors, `Configure a domain TLSRPT destination in domains.conf config file.`)
1234 addf(&result.Instructions, instr)
1239 var hostTLSRPTAddr smtp.Address
1240 if mox.Conf.Static.HostTLSRPT.Localpart != "" {
1241 hostTLSRPTAddr = smtp.NewAddress(mox.Conf.Static.HostTLSRPT.ParsedLocalpart, mox.Conf.Static.HostnameDomain)
1243 go checkTLSRPT(&r.HostTLSRPT, mox.Conf.Static.HostnameDomain, hostTLSRPTAddr, true)
1247 var domainTLSRPTAddr smtp.Address
1248 if domConf.TLSRPT != nil {
1249 domainTLSRPTAddr = smtp.NewAddress(domConf.TLSRPT.ParsedLocalpart, domain)
1251 go checkTLSRPT(&r.DomainTLSRPT, domain, domainTLSRPTAddr, false)
1259 record, txt, err := mtasts.LookupRecord(ctx, log.Logger, resolver, domain)
1261 addf(&r.MTASTS.Errors, "Looking up MTA-STS record: %s", err)
1265 r.MTASTS.Record = &MTASTSRecord{*record}
1268 policy, text, err := mtasts.FetchPolicy(ctx, log.Logger, domain)
1270 addf(&r.MTASTS.Errors, "Fetching MTA-STS policy: %s", err)
1271 } else if policy.Mode == mtasts.ModeNone {
1272 addf(&r.MTASTS.Warnings, "MTA-STS policy is present, but does not require TLS.")
1273 } else if policy.Mode == mtasts.ModeTesting {
1274 addf(&r.MTASTS.Warnings, "MTA-STS policy is in testing mode, do not forget to change to mode enforce after testing period.")
1276 r.MTASTS.PolicyText = text
1277 r.MTASTS.Policy = policy
1278 if policy != nil && policy.Mode != mtasts.ModeNone {
1279 if !policy.Matches(mox.Conf.Static.HostnameDomain) {
1280 addf(&r.MTASTS.Warnings, "Configured hostname is missing from policy MX list.")
1282 if policy.MaxAgeSeconds <= 24*3600 {
1283 addf(&r.MTASTS.Warnings, "Policy has a MaxAge of less than 1 day. For stable configurations, the recommended period is in weeks.")
1286 mxl, _, _ := resolver.LookupMX(ctx, domain.ASCII+".")
1287 // We do not check for errors, the MX check will complain about mx errors, we assume we will get the same error here.
1288 mxs := map[dns.Domain]struct{}{}
1289 for _, mx := range mxl {
1290 d, err := dns.ParseDomain(strings.TrimSuffix(mx.Host, "."))
1292 addf(&r.MTASTS.Warnings, "MX record %q is invalid: %s", mx.Host, err)
1297 for mx := range mxs {
1298 if !policy.Matches(mx) {
1299 addf(&r.MTASTS.Warnings, "MX record %q does not match MTA-STS policy MX list.", mx)
1302 for _, mx := range policy.MX {
1306 if _, ok := mxs[mx.Domain]; !ok {
1307 addf(&r.MTASTS.Warnings, "MX %q in MTA-STS policy is not in MX record.", mx)
1312 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.
1314After 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.
1316You 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.
1318You 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.
1320The _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.
1322When enabling MTA-STS, or updating a policy, always update the policy first (through a configuration change and reload/restart), and the DNS record second.
1324 addf(&r.MTASTS.Instructions, intro)
1326 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.`)
1328 host := fmt.Sprintf("Ensure DNS CNAME/A/AAAA records exist that resolve mta-sts.%s to this mail server. For example:\n\n\t%s CNAME %s\n\n", domain.ASCII, "mta-sts."+domain.ASCII+".", mox.Conf.Static.HostnameDomain.ASCII+".")
1329 addf(&r.MTASTS.Instructions, host)
1331 mtastsr := mtasts.Record{
1333 ID: time.Now().Format("20060102T150405"),
1335 dns := fmt.Sprintf("Ensure a DNS TXT record like the following exists:\n\n\t_mta-sts 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.", mox.TXTStrings(mtastsr.String()), domain.Name())
1336 addf(&r.MTASTS.Instructions, dns)
1345 type srvReq struct {
1353 // We'll assume if any submissions is configured, it is public. Same for imap. And
1354 // if not, that there is a plain option.
1355 var submissions, imaps bool
1356 for _, l := range mox.Conf.Static.Listeners {
1357 if l.TLS != nil && l.Submissions.Enabled {
1360 if l.TLS != nil && l.IMAPS.Enabled {
1364 srvhost := func(ok bool) string {
1366 return mox.Conf.Static.HostnameDomain.ASCII + "."
1370 var reqs = []srvReq{
1371 {name: "_submissions", port: 465, host: srvhost(submissions)},
1372 {name: "_submission", port: 587, host: srvhost(!submissions)},
1373 {name: "_imaps", port: 993, host: srvhost(imaps)},
1374 {name: "_imap", port: 143, host: srvhost(!imaps)},
1375 {name: "_pop3", port: 110, host: "."},
1376 {name: "_pop3s", port: 995, host: "."},
1378 var srvwg sync.WaitGroup
1379 srvwg.Add(len(reqs))
1380 for i := range reqs {
1383 _, reqs[i].srvs, _, reqs[i].err = resolver.LookupSRV(ctx, reqs[i].name[1:], "tcp", domain.ASCII+".")
1388 instr := "Ensure DNS records like the following exist:\n\n"
1389 r.SRVConf.SRVs = map[string][]net.SRV{}
1390 for _, req := range reqs {
1391 name := req.name + "_.tcp." + domain.ASCII
1392 instr += fmt.Sprintf("\t%s._tcp.%-*s SRV 0 1 %d %s\n", req.name, len("_submissions")-len(req.name)+len(domain.ASCII+"."), domain.ASCII+".", req.port, req.host)
1393 r.SRVConf.SRVs[req.name] = unptr(req.srvs)
1395 addf(&r.SRVConf.Errors, "Looking up SRV record %q: %s", name, err)
1396 } else if len(req.srvs) == 0 {
1397 addf(&r.SRVConf.Errors, "Missing SRV record %q", name)
1398 } else if len(req.srvs) != 1 || req.srvs[0].Target != req.host || req.srvs[0].Port != req.port {
1399 addf(&r.SRVConf.Errors, "Unexpected SRV record(s) for %q", name)
1402 addf(&r.SRVConf.Instructions, instr)
1411 if domConf.ClientSettingsDomain != "" {
1412 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+".")
1414 ips, ourIPs, notOurIPs, err := lookupIPs(&r.Autoconf.Errors, domConf.ClientSettingsDNSDomain.ASCII+".")
1416 addf(&r.Autoconf.Errors, "Looking up client settings DNS CNAME: %s", err)
1418 r.Autoconf.ClientSettingsDomainIPs = ips
1419 if !isUnspecifiedNAT {
1420 if len(ourIPs) == 0 {
1421 addf(&r.Autoconf.Errors, "Client settings domain does not point to one of our IPs.")
1422 } else if len(notOurIPs) > 0 {
1423 addf(&r.Autoconf.Errors, "Client settings domain points to some IPs that are not ours: %v", notOurIPs)
1428 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+".")
1430 host := "autoconfig." + domain.ASCII + "."
1431 ips, ourIPs, notOurIPs, err := lookupIPs(&r.Autoconf.Errors, host)
1433 addf(&r.Autoconf.Errors, "Looking up autoconfig host: %s", err)
1437 r.Autoconf.IPs = ips
1438 if !isUnspecifiedNAT {
1439 if len(ourIPs) == 0 {
1440 addf(&r.Autoconf.Errors, "Autoconfig does not point to one of our IPs.")
1441 } else if len(notOurIPs) > 0 {
1442 addf(&r.Autoconf.Errors, "Autoconfig points to some IPs that are not ours: %v", notOurIPs)
1446 checkTLS(&r.Autoconf.Errors, "autoconfig."+domain.ASCII, ips, "443")
1455 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+".")
1457 _, srvs, _, err := resolver.LookupSRV(ctx, "autodiscover", "tcp", domain.ASCII+".")
1459 addf(&r.Autodiscover.Errors, "Looking up SRV record %q: %s", "autodiscover", err)
1463 for _, srv := range srvs {
1464 ips, ourIPs, notOurIPs, err := lookupIPs(&r.Autodiscover.Errors, srv.Target)
1466 addf(&r.Autodiscover.Errors, "Looking up target %q from SRV record: %s", srv.Target, err)
1469 if srv.Port != 443 {
1473 r.Autodiscover.Records = append(r.Autodiscover.Records, AutodiscoverSRV{*srv, ips})
1474 if !isUnspecifiedNAT {
1475 if len(ourIPs) == 0 {
1476 addf(&r.Autodiscover.Errors, "SRV target %q does not point to our IPs.", srv.Target)
1477 } else if len(notOurIPs) > 0 {
1478 addf(&r.Autodiscover.Errors, "SRV target %q points to some IPs that are not ours: %v", srv.Target, notOurIPs)
1482 checkTLS(&r.Autodiscover.Errors, strings.TrimSuffix(srv.Target, "."), ips, "443")
1485 addf(&r.Autodiscover.Errors, "No SRV record for port 443 for https.")
1493// Domains returns all configured domain names, in UTF-8 for IDNA domains.
1494func (Admin) Domains(ctx context.Context) []dns.Domain {
1496 for _, s := range mox.Conf.Domains() {
1497 d, _ := dns.ParseDomain(s)
1503// Domain returns the dns domain for a (potentially unicode as IDNA) domain name.
1504func (Admin) Domain(ctx context.Context, domain string) dns.Domain {
1505 d, err := dns.ParseDomain(domain)
1506 xcheckuserf(ctx, err, "parse domain")
1507 _, ok := mox.Conf.Domain(d)
1509 xcheckuserf(ctx, errors.New("no such domain"), "looking up domain")
1514// ParseDomain parses a domain, possibly an IDNA domain.
1515func (Admin) ParseDomain(ctx context.Context, domain string) dns.Domain {
1516 d, err := dns.ParseDomain(domain)
1517 xcheckuserf(ctx, err, "parse domain")
1521// DomainLocalparts returns the encoded localparts and accounts configured in domain.
1522func (Admin) DomainLocalparts(ctx context.Context, domain string) (localpartAccounts map[string]string) {
1523 d, err := dns.ParseDomain(domain)
1524 xcheckuserf(ctx, err, "parsing domain")
1525 _, ok := mox.Conf.Domain(d)
1527 xcheckuserf(ctx, errors.New("no such domain"), "looking up domain")
1529 return mox.Conf.DomainLocalparts(d)
1532// Accounts returns the names of all configured accounts.
1533func (Admin) Accounts(ctx context.Context) []string {
1534 l := mox.Conf.Accounts()
1535 sort.Slice(l, func(i, j int) bool {
1541// Account returns the parsed configuration of an account.
1542func (Admin) Account(ctx context.Context, account string) map[string]any {
1543 ac, ok := mox.Conf.Account(account)
1545 xcheckuserf(ctx, errors.New("no such account"), "looking up account")
1548 // todo: should change sherpa to understand config.Account directly, with its anonymous structs.
1549 buf, err := json.Marshal(ac)
1550 xcheckf(ctx, err, "marshal to json")
1551 r := map[string]any{}
1552 err = json.Unmarshal(buf, &r)
1553 xcheckf(ctx, err, "unmarshal from json")
1558// ConfigFiles returns the paths and contents of the static and dynamic configuration files.
1559func (Admin) ConfigFiles(ctx context.Context) (staticPath, dynamicPath, static, dynamic string) {
1560 buf0, err := os.ReadFile(mox.ConfigStaticPath)
1561 xcheckf(ctx, err, "read static config file")
1562 buf1, err := os.ReadFile(mox.ConfigDynamicPath)
1563 xcheckf(ctx, err, "read dynamic config file")
1564 return mox.ConfigStaticPath, mox.ConfigDynamicPath, string(buf0), string(buf1)
1567// MTASTSPolicies returns all mtasts policies from the cache.
1568func (Admin) MTASTSPolicies(ctx context.Context) (records []mtastsdb.PolicyRecord) {
1569 records, err := mtastsdb.PolicyRecords(ctx)
1570 xcheckf(ctx, err, "fetching mtasts policies from database")
1574// TLSReports returns TLS reports overlapping with period start/end, for the given
1575// policy domain (or all domains if empty). The reports are sorted first by period
1576// end (most recent first), then by policy domain.
1577func (Admin) TLSReports(ctx context.Context, start, end time.Time, policyDomain string) (reports []tlsrptdb.TLSReportRecord) {
1578 var polDom dns.Domain
1579 if policyDomain != "" {
1581 polDom, err = dns.ParseDomain(policyDomain)
1582 xcheckuserf(ctx, err, "parsing domain %q", policyDomain)
1585 records, err := tlsrptdb.RecordsPeriodDomain(ctx, start, end, polDom)
1586 xcheckf(ctx, err, "fetching tlsrpt report records from database")
1587 sort.Slice(records, func(i, j int) bool {
1588 iend := records[i].Report.DateRange.End
1589 jend := records[j].Report.DateRange.End
1591 return records[i].Domain < records[j].Domain
1593 return iend.After(jend)
1598// TLSReportID returns a single TLS report.
1599func (Admin) TLSReportID(ctx context.Context, domain string, reportID int64) tlsrptdb.TLSReportRecord {
1600 record, err := tlsrptdb.RecordID(ctx, reportID)
1601 if err == nil && record.Domain != domain {
1602 err = bstore.ErrAbsent
1604 if err == bstore.ErrAbsent {
1605 xcheckuserf(ctx, err, "fetching tls report from database")
1607 xcheckf(ctx, err, "fetching tls report from database")
1611// TLSRPTSummary presents TLS reporting statistics for a single domain
1613type TLSRPTSummary struct {
1614 PolicyDomain dns.Domain
1617 ResultTypeCounts map[tlsrpt.ResultType]int64
1620// TLSRPTSummaries returns a summary of received TLS reports overlapping with
1621// period start/end for one or all domains (when domain is empty).
1622// The returned summaries are ordered by domain name.
1623func (Admin) TLSRPTSummaries(ctx context.Context, start, end time.Time, policyDomain string) (domainSummaries []TLSRPTSummary) {
1624 var polDom dns.Domain
1625 if policyDomain != "" {
1627 polDom, err = dns.ParseDomain(policyDomain)
1628 xcheckuserf(ctx, err, "parsing policy domain")
1630 reports, err := tlsrptdb.RecordsPeriodDomain(ctx, start, end, polDom)
1631 xcheckf(ctx, err, "fetching tlsrpt reports from database")
1633 summaries := map[dns.Domain]TLSRPTSummary{}
1634 for _, r := range reports {
1635 dom, err := dns.ParseDomain(r.Domain)
1636 xcheckf(ctx, err, "parsing domain %q", r.Domain)
1638 sum := summaries[dom]
1639 sum.PolicyDomain = dom
1640 for _, result := range r.Report.Policies {
1641 sum.Success += result.Summary.TotalSuccessfulSessionCount
1642 sum.Failure += result.Summary.TotalFailureSessionCount
1643 for _, details := range result.FailureDetails {
1644 if sum.ResultTypeCounts == nil {
1645 sum.ResultTypeCounts = map[tlsrpt.ResultType]int64{}
1647 sum.ResultTypeCounts[details.ResultType] += details.FailedSessionCount
1650 summaries[dom] = sum
1652 sums := make([]TLSRPTSummary, 0, len(summaries))
1653 for _, sum := range summaries {
1654 sums = append(sums, sum)
1656 sort.Slice(sums, func(i, j int) bool {
1657 return sums[i].PolicyDomain.Name() < sums[j].PolicyDomain.Name()
1662// DMARCReports returns DMARC reports overlapping with period start/end, for the
1663// given domain (or all domains if empty). The reports are sorted first by period
1664// end (most recent first), then by domain.
1665func (Admin) DMARCReports(ctx context.Context, start, end time.Time, domain string) (reports []dmarcdb.DomainFeedback) {
1666 reports, err := dmarcdb.RecordsPeriodDomain(ctx, start, end, domain)
1667 xcheckf(ctx, err, "fetching dmarc aggregate reports from database")
1668 sort.Slice(reports, func(i, j int) bool {
1669 iend := reports[i].ReportMetadata.DateRange.End
1670 jend := reports[j].ReportMetadata.DateRange.End
1672 return reports[i].Domain < reports[j].Domain
1679// DMARCReportID returns a single DMARC report.
1680func (Admin) DMARCReportID(ctx context.Context, domain string, reportID int64) (report dmarcdb.DomainFeedback) {
1681 report, err := dmarcdb.RecordID(ctx, reportID)
1682 if err == nil && report.Domain != domain {
1683 err = bstore.ErrAbsent
1685 if err == bstore.ErrAbsent {
1686 xcheckuserf(ctx, err, "fetching dmarc aggregate report from database")
1688 xcheckf(ctx, err, "fetching dmarc aggregate report from database")
1692// DMARCSummary presents DMARC aggregate reporting statistics for a single domain
1694type DMARCSummary struct {
1698 DispositionQuarantine int
1699 DispositionReject int
1702 PolicyOverrides map[dmarcrpt.PolicyOverride]int
1705// DMARCSummaries returns a summary of received DMARC reports overlapping with
1706// period start/end for one or all domains (when domain is empty).
1707// The returned summaries are ordered by domain name.
1708func (Admin) DMARCSummaries(ctx context.Context, start, end time.Time, domain string) (domainSummaries []DMARCSummary) {
1709 reports, err := dmarcdb.RecordsPeriodDomain(ctx, start, end, domain)
1710 xcheckf(ctx, err, "fetching dmarc aggregate reports from database")
1711 summaries := map[string]DMARCSummary{}
1712 for _, r := range reports {
1713 sum := summaries[r.Domain]
1714 sum.Domain = r.Domain
1715 for _, record := range r.Records {
1716 n := record.Row.Count
1720 switch record.Row.PolicyEvaluated.Disposition {
1721 case dmarcrpt.DispositionNone:
1722 sum.DispositionNone += n
1723 case dmarcrpt.DispositionQuarantine:
1724 sum.DispositionQuarantine += n
1725 case dmarcrpt.DispositionReject:
1726 sum.DispositionReject += n
1729 if record.Row.PolicyEvaluated.DKIM == dmarcrpt.DMARCFail {
1732 if record.Row.PolicyEvaluated.SPF == dmarcrpt.DMARCFail {
1736 for _, reason := range record.Row.PolicyEvaluated.Reasons {
1737 if sum.PolicyOverrides == nil {
1738 sum.PolicyOverrides = map[dmarcrpt.PolicyOverride]int{}
1740 sum.PolicyOverrides[reason.Type] += n
1743 summaries[r.Domain] = sum
1745 sums := make([]DMARCSummary, 0, len(summaries))
1746 for _, sum := range summaries {
1747 sums = append(sums, sum)
1749 sort.Slice(sums, func(i, j int) bool {
1750 return sums[i].Domain < sums[j].Domain
1755// Reverse is the result of a reverse lookup.
1756type Reverse struct {
1759 // In the future, we can add a iprev-validated host name, and possibly the IPs of the host names.
1762// LookupIP does a reverse lookup of ip.
1763func (Admin) LookupIP(ctx context.Context, ip string) Reverse {
1764 resolver := dns.StrictResolver{Pkg: "webadmin", Log: pkglog.WithContext(ctx).Logger}
1765 names, _, err := resolver.LookupAddr(ctx, ip)
1766 xcheckuserf(ctx, err, "looking up ip")
1767 return Reverse{names}
1770// DNSBLStatus returns the IPs from which outgoing connections may be made and
1771// their current status in DNSBLs that are configured. The IPs are typically the
1772// configured listen IPs, or otherwise IPs on the machines network interfaces, with
1773// internal/private IPs removed.
1775// The returned value maps IPs to per DNSBL statuses, where "pass" means not listed and
1776// anything else is an error string, e.g. "fail: ..." or "temperror: ...".
1777func (Admin) DNSBLStatus(ctx context.Context) (results map[string]map[string]string, using, monitoring []dns.Domain) {
1778 log := mlog.New("webadmin", nil).WithContext(ctx)
1779 resolver := dns.StrictResolver{Pkg: "check", Log: log.Logger}
1780 return dnsblsStatus(ctx, log, resolver)
1783func dnsblsStatus(ctx context.Context, log mlog.Log, resolver dns.Resolver) (results map[string]map[string]string, using, monitoring []dns.Domain) {
1784 // todo: check health before using dnsbl?
1785 using = mox.Conf.Static.Listeners["public"].SMTP.DNSBLZones
1786 zones := append([]dns.Domain{}, using...)
1787 for _, zone := range mox.Conf.MonitorDNSBLs() {
1788 if !slices.Contains(zones, zone) {
1789 zones = append(zones, zone)
1790 monitoring = append(monitoring, zone)
1794 r := map[string]map[string]string{}
1795 for _, ip := range xsendingIPs(ctx) {
1796 if ip.IsLoopback() || ip.IsPrivate() {
1799 ipstr := ip.String()
1800 r[ipstr] = map[string]string{}
1801 for _, zone := range zones {
1802 status, expl, err := dnsbl.Lookup(ctx, log.Logger, resolver, zone, ip)
1803 result := string(status)
1805 result += ": " + err.Error()
1808 result += ": " + expl
1810 r[ipstr][zone.LogString()] = result
1813 return r, using, monitoring
1816func (Admin) MonitorDNSBLsSave(ctx context.Context, text string) {
1817 var zones []dns.Domain
1818 publicZones := mox.Conf.Static.Listeners["public"].SMTP.DNSBLZones
1819 for _, line := range strings.Split(text, "\n") {
1820 line = strings.TrimSpace(line)
1824 d, err := dns.ParseDomain(line)
1825 xcheckuserf(ctx, err, "parsing dnsbl zone %s", line)
1826 if slices.Contains(zones, d) {
1827 xusererrorf(ctx, "duplicate dnsbl zone %s", line)
1829 if slices.Contains(publicZones, d) {
1830 xusererrorf(ctx, "dnsbl zone %s already present in public listener", line)
1832 zones = append(zones, d)
1834 err := mox.MonitorDNSBLsSave(ctx, zones)
1835 xcheckf(ctx, err, "saving monitoring dnsbl zones")
1838// DomainRecords returns lines describing DNS records that should exist for the
1839// configured domain.
1840func (Admin) DomainRecords(ctx context.Context, domain string) []string {
1841 log := pkglog.WithContext(ctx)
1842 return DomainRecords(ctx, log, domain)
1845// DomainRecords is the implementation of API function Admin.DomainRecords, taking
1847func DomainRecords(ctx context.Context, log mlog.Log, domain string) []string {
1848 d, err := dns.ParseDomain(domain)
1849 xcheckuserf(ctx, err, "parsing domain")
1850 dc, ok := mox.Conf.Domain(d)
1852 xcheckuserf(ctx, errors.New("unknown domain"), "lookup domain")
1854 resolver := dns.StrictResolver{Pkg: "webadmin", Log: pkglog.WithContext(ctx).Logger}
1855 _, result, err := resolver.LookupTXT(ctx, domain+".")
1856 if !dns.IsNotFound(err) {
1857 xcheckf(ctx, err, "looking up record to determine if dnssec is implemented")
1860 var certIssuerDomainName, acmeAccountURI string
1861 public := mox.Conf.Static.Listeners["public"]
1862 if public.TLS != nil && public.TLS.ACME != "" {
1863 acme, ok := mox.Conf.Static.ACME[public.TLS.ACME]
1864 if ok && acme.Manager.Manager.Client != nil {
1865 certIssuerDomainName = acme.IssuerDomainName
1866 acc, err := acme.Manager.Manager.Client.GetReg(ctx, "")
1867 log.Check(err, "get public acme account")
1869 acmeAccountURI = acc.URI
1874 records, err := mox.DomainRecords(dc, d, result.Authentic, certIssuerDomainName, acmeAccountURI)
1875 xcheckf(ctx, err, "dns records")
1879// DomainAdd adds a new domain and reloads the configuration.
1880func (Admin) DomainAdd(ctx context.Context, domain, accountName, localpart string) {
1881 d, err := dns.ParseDomain(domain)
1882 xcheckuserf(ctx, err, "parsing domain")
1884 err = mox.DomainAdd(ctx, d, accountName, smtp.Localpart(norm.NFC.String(localpart)))
1885 xcheckf(ctx, err, "adding domain")
1888// DomainRemove removes an existing domain and reloads the configuration.
1889func (Admin) DomainRemove(ctx context.Context, domain string) {
1890 d, err := dns.ParseDomain(domain)
1891 xcheckuserf(ctx, err, "parsing domain")
1893 err = mox.DomainRemove(ctx, d)
1894 xcheckf(ctx, err, "removing domain")
1897// AccountAdd adds existing a new account, with an initial email address, and
1898// reloads the configuration.
1899func (Admin) AccountAdd(ctx context.Context, accountName, address string) {
1900 err := mox.AccountAdd(ctx, accountName, address)
1901 xcheckf(ctx, err, "adding account")
1904// AccountRemove removes an existing account and reloads the configuration.
1905func (Admin) AccountRemove(ctx context.Context, accountName string) {
1906 err := mox.AccountRemove(ctx, accountName)
1907 xcheckf(ctx, err, "removing account")
1910// AddressAdd adds a new address to the account, which must already exist.
1911func (Admin) AddressAdd(ctx context.Context, address, accountName string) {
1912 err := mox.AddressAdd(ctx, address, accountName)
1913 xcheckf(ctx, err, "adding address")
1916// AddressRemove removes an existing address.
1917func (Admin) AddressRemove(ctx context.Context, address string) {
1918 err := mox.AddressRemove(ctx, address)
1919 xcheckf(ctx, err, "removing address")
1922// SetPassword saves a new password for an account, invalidating the previous password.
1923// Sessions are not interrupted, and will keep working. New login attempts must use the new password.
1924// Password must be at least 8 characters.
1925func (Admin) SetPassword(ctx context.Context, accountName, password string) {
1926 log := pkglog.WithContext(ctx)
1927 if len(password) < 8 {
1928 xusererrorf(ctx, "message must be at least 8 characters")
1930 acc, err := store.OpenAccount(log, accountName)
1931 xcheckf(ctx, err, "open account")
1934 log.WithContext(ctx).Check(err, "closing account")
1936 err = acc.SetPassword(log, password)
1937 xcheckf(ctx, err, "setting password")
1940// SetAccountLimits set new limits on outgoing messages for an account.
1941func (Admin) SetAccountLimits(ctx context.Context, accountName string, maxOutgoingMessagesPerDay, maxFirstTimeRecipientsPerDay int, maxMsgSize int64) {
1942 err := mox.AccountLimitsSave(ctx, accountName, maxOutgoingMessagesPerDay, maxFirstTimeRecipientsPerDay, maxMsgSize)
1943 xcheckf(ctx, err, "saving account limits")
1946// ClientConfigsDomain returns configurations for email clients, IMAP and
1947// Submission (SMTP) for the domain.
1948func (Admin) ClientConfigsDomain(ctx context.Context, domain string) mox.ClientConfigs {
1949 d, err := dns.ParseDomain(domain)
1950 xcheckuserf(ctx, err, "parsing domain")
1952 cc, err := mox.ClientConfigsDomain(d)
1953 xcheckf(ctx, err, "client config for domain")
1957// QueueList returns the messages currently in the outgoing queue.
1958func (Admin) QueueList(ctx context.Context) []queue.Msg {
1959 l, err := queue.List(ctx)
1960 xcheckf(ctx, err, "listing messages in queue")
1964// QueueSize returns the number of messages currently in the outgoing queue.
1965func (Admin) QueueSize(ctx context.Context) int {
1966 n, err := queue.Count(ctx)
1967 xcheckf(ctx, err, "listing messages in queue")
1971// QueueKick initiates delivery of a message from the queue and sets the transport
1972// to use for delivery.
1973func (Admin) QueueKick(ctx context.Context, id int64, transport string) {
1974 n, err := queue.Kick(ctx, id, "", "", &transport)
1975 if err == nil && n == 0 {
1976 err = errors.New("message not found")
1978 xcheckf(ctx, err, "kick message in queue")
1981// QueueDrop removes a message from the queue.
1982func (Admin) QueueDrop(ctx context.Context, id int64) {
1983 log := pkglog.WithContext(ctx)
1984 n, err := queue.Drop(ctx, log, id, "", "")
1985 if err == nil && n == 0 {
1986 err = errors.New("message not found")
1988 xcheckf(ctx, err, "drop message from queue")
1991// QueueSaveRequireTLS updates the requiretls field for a message in the queue,
1992// to be used for the next delivery.
1993func (Admin) QueueSaveRequireTLS(ctx context.Context, id int64, requireTLS *bool) {
1994 err := queue.SaveRequireTLS(ctx, id, requireTLS)
1995 xcheckf(ctx, err, "update requiretls for message in queue")
1998// LogLevels returns the current log levels.
1999func (Admin) LogLevels(ctx context.Context) map[string]string {
2000 m := map[string]string{}
2001 for pkg, level := range mox.Conf.LogLevels() {
2002 s, ok := mlog.LevelStrings[level]
2011// LogLevelSet sets a log level for a package.
2012func (Admin) LogLevelSet(ctx context.Context, pkg string, levelStr string) {
2013 level, ok := mlog.Levels[levelStr]
2015 xcheckuserf(ctx, errors.New("unknown"), "lookup level")
2017 mox.Conf.LogLevelSet(pkglog.WithContext(ctx), pkg, level)
2020// LogLevelRemove removes a log level for a package, which cannot be the empty string.
2021func (Admin) LogLevelRemove(ctx context.Context, pkg string) {
2022 mox.Conf.LogLevelRemove(pkglog.WithContext(ctx), pkg)
2025// CheckUpdatesEnabled returns whether checking for updates is enabled.
2026func (Admin) CheckUpdatesEnabled(ctx context.Context) bool {
2027 return mox.Conf.Static.CheckUpdates
2030// WebserverConfig is the combination of WebDomainRedirects and WebHandlers
2031// from the domains.conf configuration file.
2032type WebserverConfig struct {
2033 WebDNSDomainRedirects [][2]dns.Domain // From server to frontend.
2034 WebDomainRedirects [][2]string // From frontend to server, it's not convenient to create dns.Domain in the frontend.
2035 WebHandlers []config.WebHandler
2038// WebserverConfig returns the current webserver config
2039func (Admin) WebserverConfig(ctx context.Context) (conf WebserverConfig) {
2040 conf = webserverConfig()
2041 conf.WebDomainRedirects = nil
2045func webserverConfig() WebserverConfig {
2046 r, l := mox.Conf.WebServer()
2047 x := make([][2]dns.Domain, 0, len(r))
2048 xs := make([][2]string, 0, len(r))
2049 for k, v := range r {
2050 x = append(x, [2]dns.Domain{k, v})
2051 xs = append(xs, [2]string{k.Name(), v.Name()})
2053 sort.Slice(x, func(i, j int) bool {
2054 return x[i][0].ASCII < x[j][0].ASCII
2056 sort.Slice(xs, func(i, j int) bool {
2057 return xs[i][0] < xs[j][0]
2059 return WebserverConfig{x, xs, l}
2062// WebserverConfigSave saves a new webserver config. If oldConf is not equal to
2063// the current config, an error is returned.
2064func (Admin) WebserverConfigSave(ctx context.Context, oldConf, newConf WebserverConfig) (savedConf WebserverConfig) {
2065 current := webserverConfig()
2066 webhandlersEqual := func() bool {
2067 if len(current.WebHandlers) != len(oldConf.WebHandlers) {
2070 for i, wh := range current.WebHandlers {
2071 if !wh.Equal(oldConf.WebHandlers[i]) {
2077 if !reflect.DeepEqual(oldConf.WebDNSDomainRedirects, current.WebDNSDomainRedirects) || !webhandlersEqual() {
2078 xcheckuserf(ctx, errors.New("config has changed"), "comparing old/current config")
2081 // Convert to map, check that there are no duplicates here. The canonicalized
2082 // dns.Domain are checked again for uniqueness when parsing the config before
2084 domainRedirects := map[string]string{}
2085 for _, x := range newConf.WebDomainRedirects {
2086 if _, ok := domainRedirects[x[0]]; ok {
2087 xcheckuserf(ctx, errors.New("already present"), "checking redirect %s", x[0])
2089 domainRedirects[x[0]] = x[1]
2092 err := mox.WebserverConfigSet(ctx, domainRedirects, newConf.WebHandlers)
2093 xcheckf(ctx, err, "saving webserver config")
2095 savedConf = webserverConfig()
2096 savedConf.WebDomainRedirects = nil
2100// Transports returns the configured transports, for sending email.
2101func (Admin) Transports(ctx context.Context) map[string]config.Transport {
2102 return mox.Conf.Static.Transports
2105// DMARCEvaluationStats returns a map of all domains with evaluations to a count of
2106// the evaluations and whether those evaluations will cause a report to be sent.
2107func (Admin) DMARCEvaluationStats(ctx context.Context) map[string]dmarcdb.EvaluationStat {
2108 stats, err := dmarcdb.EvaluationStats(ctx)
2109 xcheckf(ctx, err, "get evaluation stats")
2113// DMARCEvaluationsDomain returns all evaluations for aggregate reports for the
2114// domain, sorted from oldest to most recent.
2115func (Admin) DMARCEvaluationsDomain(ctx context.Context, domain string) (dns.Domain, []dmarcdb.Evaluation) {
2116 dom, err := dns.ParseDomain(domain)
2117 xcheckf(ctx, err, "parsing domain")
2119 evals, err := dmarcdb.EvaluationsDomain(ctx, dom)
2120 xcheckf(ctx, err, "get evaluations for domain")
2124// DMARCRemoveEvaluations removes evaluations for a domain.
2125func (Admin) DMARCRemoveEvaluations(ctx context.Context, domain string) {
2126 dom, err := dns.ParseDomain(domain)
2127 xcheckf(ctx, err, "parsing domain")
2129 err = dmarcdb.RemoveEvaluationsDomain(ctx, dom)
2130 xcheckf(ctx, err, "removing evaluations for domain")
2133// DMARCSuppressAdd adds a reporting address to the suppress list. Outgoing
2134// reports will be suppressed for a period.
2135func (Admin) DMARCSuppressAdd(ctx context.Context, reportingAddress string, until time.Time, comment string) {
2136 addr, err := smtp.ParseAddress(reportingAddress)
2137 xcheckuserf(ctx, err, "parsing reporting address")
2139 ba := dmarcdb.SuppressAddress{ReportingAddress: addr.String(), Until: until, Comment: comment}
2140 err = dmarcdb.SuppressAdd(ctx, &ba)
2141 xcheckf(ctx, err, "adding address to suppresslist")
2144// DMARCSuppressList returns all reporting addresses on the suppress list.
2145func (Admin) DMARCSuppressList(ctx context.Context) []dmarcdb.SuppressAddress {
2146 l, err := dmarcdb.SuppressList(ctx)
2147 xcheckf(ctx, err, "listing reporting addresses in suppresslist")
2151// DMARCSuppressRemove removes a reporting address record from the suppress list.
2152func (Admin) DMARCSuppressRemove(ctx context.Context, id int64) {
2153 err := dmarcdb.SuppressRemove(ctx, id)
2154 xcheckf(ctx, err, "removing reporting address from suppresslist")
2157// DMARCSuppressExtend updates the until field of a suppressed reporting address record.
2158func (Admin) DMARCSuppressExtend(ctx context.Context, id int64, until time.Time) {
2159 err := dmarcdb.SuppressUpdate(ctx, id, until)
2160 xcheckf(ctx, err, "updating reporting address in suppresslist")
2163// TLSRPTResults returns all TLSRPT results in the database.
2164func (Admin) TLSRPTResults(ctx context.Context) []tlsrptdb.TLSResult {
2165 results, err := tlsrptdb.Results(ctx)
2166 xcheckf(ctx, err, "get results")
2170// TLSRPTResultsPolicyDomain returns the TLS results for a domain.
2171func (Admin) TLSRPTResultsDomain(ctx context.Context, isRcptDom bool, policyDomain string) (dns.Domain, []tlsrptdb.TLSResult) {
2172 dom, err := dns.ParseDomain(policyDomain)
2173 xcheckf(ctx, err, "parsing domain")
2176 results, err := tlsrptdb.ResultsRecipientDomain(ctx, dom)
2177 xcheckf(ctx, err, "get result for recipient domain")
2180 results, err := tlsrptdb.ResultsPolicyDomain(ctx, dom)
2181 xcheckf(ctx, err, "get result for policy domain")
2185// LookupTLSRPTRecord looks up a TLSRPT record and returns the parsed form, original txt
2186// form from DNS, and error with the TLSRPT record as a string.
2187func (Admin) LookupTLSRPTRecord(ctx context.Context, domain string) (record *TLSRPTRecord, txt string, errstr string) {
2188 log := pkglog.WithContext(ctx)
2189 dom, err := dns.ParseDomain(domain)
2190 xcheckf(ctx, err, "parsing domain")
2192 resolver := dns.StrictResolver{Pkg: "webadmin", Log: log.Logger}
2193 r, txt, err := tlsrpt.Lookup(ctx, log.Logger, resolver, dom)
2194 if err != nil && (errors.Is(err, tlsrpt.ErrNoRecord) || errors.Is(err, tlsrpt.ErrMultipleRecords) || errors.Is(err, tlsrpt.ErrRecordSyntax) || errors.Is(err, tlsrpt.ErrDNS)) {
2195 errstr = err.Error()
2198 xcheckf(ctx, err, "fetching tlsrpt record")
2201 record = &TLSRPTRecord{Record: *r}
2204 return record, txt, errstr
2207// TLSRPTRemoveResults removes the TLS results for a domain for the given day. If
2208// day is empty, all results are removed.
2209func (Admin) TLSRPTRemoveResults(ctx context.Context, isRcptDom bool, domain string, day string) {
2210 dom, err := dns.ParseDomain(domain)
2211 xcheckf(ctx, err, "parsing domain")
2214 err = tlsrptdb.RemoveResultsRecipientDomain(ctx, dom, day)
2215 xcheckf(ctx, err, "removing tls results")
2217 err = tlsrptdb.RemoveResultsPolicyDomain(ctx, dom, day)
2218 xcheckf(ctx, err, "removing tls results")
2222// TLSRPTSuppressAdd adds a reporting address to the suppress list. Outgoing
2223// reports will be suppressed for a period.
2224func (Admin) TLSRPTSuppressAdd(ctx context.Context, reportingAddress string, until time.Time, comment string) {
2225 addr, err := smtp.ParseAddress(reportingAddress)
2226 xcheckuserf(ctx, err, "parsing reporting address")
2228 ba := tlsrptdb.TLSRPTSuppressAddress{ReportingAddress: addr.String(), Until: until, Comment: comment}
2229 err = tlsrptdb.SuppressAdd(ctx, &ba)
2230 xcheckf(ctx, err, "adding address to suppresslist")
2233// TLSRPTSuppressList returns all reporting addresses on the suppress list.
2234func (Admin) TLSRPTSuppressList(ctx context.Context) []tlsrptdb.TLSRPTSuppressAddress {
2235 l, err := tlsrptdb.SuppressList(ctx)
2236 xcheckf(ctx, err, "listing reporting addresses in suppresslist")
2240// TLSRPTSuppressRemove removes a reporting address record from the suppress list.
2241func (Admin) TLSRPTSuppressRemove(ctx context.Context, id int64) {
2242 err := tlsrptdb.SuppressRemove(ctx, id)
2243 xcheckf(ctx, err, "removing reporting address from suppresslist")
2246// TLSRPTSuppressExtend updates the until field of a suppressed reporting address record.
2247func (Admin) TLSRPTSuppressExtend(ctx context.Context, id int64, until time.Time) {
2248 err := tlsrptdb.SuppressUpdate(ctx, id, until)
2249 xcheckf(ctx, err, "updating reporting address in suppresslist")
2252// LookupCid turns an ID from a Received header into a cid as used in logging.
2253func (Admin) LookupCid(ctx context.Context, recvID string) (cid string) {
2254 v, err := mox.ReceivedToCid(recvID)
2255 xcheckf(ctx, err, "received id to cid")
2256 return fmt.Sprintf("%x", v)