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)
118 mox.NewWebadminHandler = func(basePath string, isForwarded bool) http.Handler {
119 return http.HandlerFunc(Handler(basePath, isForwarded))
123// Handler returns a handler for the webadmin endpoints, customized for the
125func Handler(cookiePath string, isForwarded bool) func(w http.ResponseWriter, r *http.Request) {
126 sh, err := makeSherpaHandler(cookiePath, isForwarded)
127 return func(w http.ResponseWriter, r *http.Request) {
129 http.Error(w, "500 - internal server error - cannot handle requests", http.StatusInternalServerError)
132 handle(sh, isForwarded, w, r)
136// Admin exports web API functions for the admin web interface. All its methods are
137// exported under api/. Function calls require valid HTTP Authentication
138// credentials of a user.
140 cookiePath string // From listener, for setting authentication cookies.
141 isForwarded bool // From listener, whether we look at X-Forwarded-* headers.
146var requestInfoCtxKey ctxKey = "requestInfo"
148type requestInfo struct {
149 SessionToken store.SessionToken
150 Response http.ResponseWriter
151 Request *http.Request // For Proto and TLS connection state during message submit.
154func handle(apiHandler http.Handler, isForwarded bool, w http.ResponseWriter, r *http.Request) {
155 ctx := context.WithValue(r.Context(), mlog.CidKey, mox.Cid())
156 log := pkglog.WithContext(ctx).With(slog.String("adminauth", ""))
158 // HTML/JS can be retrieved without authentication.
159 if r.URL.Path == "/" {
162 webadminFile.Serve(ctx, log, w, r)
164 http.Error(w, "405 - method not allowed - use get", http.StatusMethodNotAllowed)
169 isAPI := strings.HasPrefix(r.URL.Path, "/api/")
170 // Only allow POST for calls, they will not work cross-domain without CORS.
171 if isAPI && r.URL.Path != "/api/" && r.Method != "POST" {
172 http.Error(w, "405 - method not allowed - use post", http.StatusMethodNotAllowed)
176 // All other URLs, except the login endpoint require some authentication.
177 var sessionToken store.SessionToken
178 if r.URL.Path != "/api/LoginPrep" && r.URL.Path != "/api/Login" {
180 _, sessionToken, _, ok = webauth.Check(ctx, log, webauth.Admin, "webadmin", isForwarded, w, r, isAPI, isAPI, false)
182 // Response has been written already.
188 reqInfo := requestInfo{sessionToken, w, r}
189 ctx = context.WithValue(ctx, requestInfoCtxKey, reqInfo)
190 apiHandler.ServeHTTP(w, r.WithContext(ctx))
197func xcheckf(ctx context.Context, err error, format string, args ...any) {
201 // If caller tried saving a config that is invalid, or because of a bad request, cause a user error.
202 if errors.Is(err, mox.ErrConfig) || errors.Is(err, mox.ErrRequest) {
203 xcheckuserf(ctx, err, format, args...)
206 msg := fmt.Sprintf(format, args...)
207 errmsg := fmt.Sprintf("%s: %s", msg, err)
208 pkglog.WithContext(ctx).Errorx(msg, err)
209 code := "server:error"
210 if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
213 panic(&sherpa.Error{Code: code, Message: errmsg})
216func xcheckuserf(ctx context.Context, err error, format string, args ...any) {
220 msg := fmt.Sprintf(format, args...)
221 errmsg := fmt.Sprintf("%s: %s", msg, err)
222 pkglog.WithContext(ctx).Errorx(msg, err)
223 panic(&sherpa.Error{Code: "user:error", Message: errmsg})
226func xusererrorf(ctx context.Context, format string, args ...any) {
227 msg := fmt.Sprintf(format, args...)
228 pkglog.WithContext(ctx).Error(msg)
229 panic(&sherpa.Error{Code: "user:error", Message: msg})
232// LoginPrep returns a login token, and also sets it as cookie. Both must be
233// present in the call to Login.
234func (w Admin) LoginPrep(ctx context.Context) string {
235 log := pkglog.WithContext(ctx)
236 reqInfo := ctx.Value(requestInfoCtxKey).(requestInfo)
239 _, err := cryptorand.Read(data[:])
240 xcheckf(ctx, err, "generate token")
241 loginToken := base64.RawURLEncoding.EncodeToString(data[:])
243 webauth.LoginPrep(ctx, log, "webadmin", w.cookiePath, w.isForwarded, reqInfo.Response, reqInfo.Request, loginToken)
248// Login returns a session token for the credentials, or fails with error code
249// "user:badLogin". Call LoginPrep to get a loginToken.
250func (w Admin) Login(ctx context.Context, loginToken, password string) store.CSRFToken {
251 log := pkglog.WithContext(ctx)
252 reqInfo := ctx.Value(requestInfoCtxKey).(requestInfo)
254 csrfToken, err := webauth.Login(ctx, log, webauth.Admin, "webadmin", w.cookiePath, w.isForwarded, reqInfo.Response, reqInfo.Request, loginToken, "", password)
255 if _, ok := err.(*sherpa.Error); ok {
258 xcheckf(ctx, err, "login")
262// Logout invalidates the session token.
263func (w Admin) Logout(ctx context.Context) {
264 log := pkglog.WithContext(ctx)
265 reqInfo := ctx.Value(requestInfoCtxKey).(requestInfo)
267 err := webauth.Logout(ctx, log, webauth.Admin, "webadmin", w.cookiePath, w.isForwarded, reqInfo.Response, reqInfo.Request, "", reqInfo.SessionToken)
268 xcheckf(ctx, err, "logout")
274 Instructions []string
277type DNSSECResult struct {
281type IPRevCheckResult struct {
282 Hostname dns.Domain // This hostname, IPs must resolve back to this.
283 IPNames map[string][]string // IP to names.
293type MXCheckResult struct {
298type TLSCheckResult struct {
302type DANECheckResult struct {
306type SPFRecord struct {
310type SPFCheckResult struct {
312 DomainRecord *SPFRecord
314 HostRecord *SPFRecord
318type DKIMCheckResult struct {
323type DKIMRecord struct {
329type DMARCRecord struct {
333type DMARCCheckResult struct {
340type TLSRPTRecord struct {
344type TLSRPTCheckResult struct {
350type MTASTSRecord struct {
353type MTASTSCheckResult struct {
357 Policy *mtasts.Policy
361type SRVConfCheckResult struct {
362 SRVs map[string][]net.SRV // Service (e.g. "_imaps") to records.
366type AutoconfCheckResult struct {
367 ClientSettingsDomainIPs []string
372type AutodiscoverSRV struct {
377type AutodiscoverCheckResult struct {
378 Records []AutodiscoverSRV
382// CheckResult is the analysis of a domain, its actual configuration (DNS, TLS,
383// connectivity) and the mox configuration. It includes configuration instructions
384// (e.g. DNS records), and warnings and errors encountered.
385type CheckResult struct {
388 IPRev IPRevCheckResult
394 DMARC DMARCCheckResult
395 HostTLSRPT TLSRPTCheckResult
396 DomainTLSRPT TLSRPTCheckResult
397 MTASTS MTASTSCheckResult
398 SRVConf SRVConfCheckResult
399 Autoconf AutoconfCheckResult
400 Autodiscover AutodiscoverCheckResult
403// logPanic can be called with a defer from a goroutine to prevent the entire program from being shutdown in case of a panic.
404func logPanic(ctx context.Context) {
409 pkglog.WithContext(ctx).Error("recover from panic", slog.Any("panic", x))
411 metrics.PanicInc(metrics.Webadmin)
414// return IPs we may be listening on.
415func xlistenIPs(ctx context.Context, receiveOnly bool) []net.IP {
416 ips, err := mox.IPs(ctx, receiveOnly)
417 xcheckf(ctx, err, "listing ips")
421// return IPs from which we may be sending.
422func xsendingIPs(ctx context.Context) []net.IP {
423 ips, err := mox.IPs(ctx, false)
424 xcheckf(ctx, err, "listing ips")
428// CheckDomain checks the configuration for the domain, such as MX, SMTP STARTTLS,
429// SPF, DKIM, DMARC, TLSRPT, MTASTS, autoconfig, autodiscover.
430func (Admin) CheckDomain(ctx context.Context, domainName string) (r CheckResult) {
431 // todo future: should run these checks without a DNS cache so recent changes are picked up.
433 resolver := dns.StrictResolver{Pkg: "check", Log: pkglog.WithContext(ctx).Logger}
434 dialer := &net.Dialer{Timeout: 10 * time.Second}
435 nctx, cancel := context.WithTimeout(ctx, 30*time.Second)
437 return checkDomain(nctx, resolver, dialer, domainName)
440func unptr[T any](l []*T) []T {
444 r := make([]T, len(l))
445 for i, e := range l {
451func checkDomain(ctx context.Context, resolver dns.Resolver, dialer *net.Dialer, domainName string) (r CheckResult) {
452 log := pkglog.WithContext(ctx)
454 domain, err := dns.ParseDomain(domainName)
455 xcheckuserf(ctx, err, "parsing domain")
457 domConf, ok := mox.Conf.Domain(domain)
459 panic(&sherpa.Error{Code: "user:notFound", Message: "domain not found"})
462 listenIPs := xlistenIPs(ctx, true)
463 isListenIP := func(ip net.IP) bool {
464 for _, lip := range listenIPs {
472 addf := func(l *[]string, format string, args ...any) {
473 *l = append(*l, fmt.Sprintf(format, args...))
476 // Host must be an absolute dns name, ending with a dot.
477 lookupIPs := func(errors *[]string, host string) (ips []string, ourIPs, notOurIPs []net.IP, rerr error) {
478 addrs, _, err := resolver.LookupHost(ctx, host)
480 addf(errors, "Looking up %q: %s", host, err)
481 return nil, nil, nil, err
483 for _, addr := range addrs {
484 ip := net.ParseIP(addr)
486 addf(errors, "Bad IP %q", addr)
489 ips = append(ips, ip.String())
491 ourIPs = append(ourIPs, ip)
493 notOurIPs = append(notOurIPs, ip)
496 return ips, ourIPs, notOurIPs, nil
499 checkTLS := func(errors *[]string, host string, ips []string, port string) {
505 RootCAs: mox.Conf.Static.TLS.CertPool,
508 for _, ip := range ips {
509 conn, err := d.DialContext(ctx, "tcp", net.JoinHostPort(ip, port))
511 addf(errors, "TLS connection to hostname %q, IP %q: %s", host, ip, err)
518 // If at least one listener with SMTP enabled has unspecified NATed IPs, we'll skip
519 // some checks related to these IPs.
520 var isNAT, isUnspecifiedNAT bool
521 for _, l := range mox.Conf.Static.Listeners {
526 isUnspecifiedNAT = true
529 if len(l.NATIPs) > 0 {
534 var wg sync.WaitGroup
542 // Some DNSSEC-verifying resolvers return unauthentic data for ".", so we check "com".
543 _, result, err := resolver.LookupNS(ctx, "com.")
545 addf(&r.DNSSEC.Errors, "Looking up NS for DNS root (.) to check support in resolver for DNSSEC-verification: %s", err)
546 } else if !result.Authentic {
547 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.`)
549 _, result, _ := resolver.LookupMX(ctx, domain.ASCII+".")
550 if !result.Authentic {
551 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.`)
555 addf(&r.DNSSEC.Instructions, `Enable DNSSEC-signing of the DNS records of your domain (zone) at your DNS hosting provider.`)
557 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".
559cat <<EOF >/etc/unbound/unbound.conf.d/ede.conf
573 // For each mox.Conf.SpecifiedSMTPListenIPs and all NATIPs, and each IP for
574 // mox.Conf.HostnameDomain, check if they resolve back to the host name.
575 hostIPs := map[dns.Domain][]net.IP{}
576 ips, _, err := resolver.LookupIP(ctx, "ip", mox.Conf.Static.HostnameDomain.ASCII+".")
578 addf(&r.IPRev.Errors, "Looking up IPs for hostname: %s", err)
581 gatherMoreIPs := func(publicIPs []net.IP) {
583 for _, ip := range publicIPs {
584 for _, xip := range ips {
589 ips = append(ips, ip)
593 gatherMoreIPs(mox.Conf.Static.SpecifiedSMTPListenIPs)
595 for _, l := range mox.Conf.Static.Listeners {
600 for _, ip := range l.NATIPs {
601 natips = append(natips, net.ParseIP(ip))
603 gatherMoreIPs(natips)
605 hostIPs[mox.Conf.Static.HostnameDomain] = ips
607 iplist := func(ips []net.IP) string {
609 for _, ip := range ips {
610 ipstrs = append(ipstrs, ip.String())
612 return strings.Join(ipstrs, ", ")
615 r.IPRev.Hostname = mox.Conf.Static.HostnameDomain
616 r.IPRev.Instructions = []string{
617 fmt.Sprintf("Ensure IPs %s have reverse address %s.", iplist(ips), mox.Conf.Static.HostnameDomain.ASCII),
620 // If we have a socks transport, also check its host and IP.
621 for tname, t := range mox.Conf.Static.Transports {
623 hostIPs[t.Socks.Hostname] = append(hostIPs[t.Socks.Hostname], t.Socks.IPs...)
624 instr := fmt.Sprintf("For SOCKS transport %s, ensure IPs %s have reverse address %s.", tname, iplist(t.Socks.IPs), t.Socks.Hostname)
625 r.IPRev.Instructions = append(r.IPRev.Instructions, instr)
635 results := make(chan result)
637 for host, ips := range hostIPs {
638 for _, ip := range ips {
643 addrs, _, err := resolver.LookupAddr(ctx, s)
644 results <- result{host, s, addrs, err}
648 r.IPRev.IPNames = map[string][]string{}
649 for i := 0; i < n; i++ {
651 host, addrs, ip, err := lr.Host, lr.Addrs, lr.IP, lr.Err
653 addf(&r.IPRev.Errors, "Looking up reverse name for %s of %s: %v", ip, host, err)
657 addf(&r.IPRev.Errors, "Expected exactly 1 name for %s of %s, got %d (%v)", ip, host, len(addrs), addrs)
660 for i, a := range addrs {
661 a = strings.TrimRight(a, ".")
663 ad, err := dns.ParseDomain(a)
665 addf(&r.IPRev.Errors, "Parsing reverse name %q for %s: %v", a, ip, err)
672 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)
674 r.IPRev.IPNames[ip] = addrs
677 // Linux machines are often initially set up with a loopback IP for the hostname in
678 // /etc/hosts, presumably because it isn't known if their external IPs are static.
679 // For mail servers, they should certainly be static. The quickstart would also
680 // have warned about this, but could have been missed/ignored.
681 for _, ip := range ips {
683 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())
694 mxs, _, err := resolver.LookupMX(ctx, domain.ASCII+".")
696 addf(&r.MX.Errors, "Looking up MX records for %s: %s", domain, err)
698 r.MX.Records = make([]MX, len(mxs))
699 for i, mx := range mxs {
700 r.MX.Records[i] = MX{mx.Host, int(mx.Pref), nil}
702 if len(mxs) == 1 && mxs[0].Host == "." {
703 addf(&r.MX.Errors, `MX records consists of explicit null mx record (".") indicating that domain does not accept email.`)
706 for i, mx := range mxs {
707 ips, ourIPs, notOurIPs, err := lookupIPs(&r.MX.Errors, mx.Host)
709 addf(&r.MX.Errors, "Looking up IPs for mx host %q: %s", mx.Host, err)
711 r.MX.Records[i].IPs = ips
712 if isUnspecifiedNAT {
715 if len(ourIPs) == 0 {
716 addf(&r.MX.Errors, "None of the IPs that mx %q points to is ours: %v", mx.Host, notOurIPs)
717 } else if len(notOurIPs) > 0 {
718 addf(&r.MX.Errors, "Some of the IPs that mx %q points to are not ours: %v", mx.Host, notOurIPs)
722 r.MX.Instructions = []string{
723 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+"."),
727 // TLS, mostly checking certificate expiration and CA trust.
728 // 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.
734 // MTA-STS, autoconfig, autodiscover are checked in their sections.
736 // Dial a single MX host with given IP and perform STARTTLS handshake.
737 dialSMTPSTARTTLS := func(host, ip string) error {
738 conn, err := dialer.DialContext(ctx, "tcp", net.JoinHostPort(ip, "25"))
748 end := time.Now().Add(10 * time.Second)
749 cctx, cancel := context.WithTimeout(ctx, 10*time.Second)
751 err = conn.SetDeadline(end)
752 log.WithContext(ctx).Check(err, "setting deadline")
754 br := bufio.NewReader(conn)
755 _, err = br.ReadString('\n')
757 return fmt.Errorf("reading SMTP banner from remote: %s", err)
759 if _, err := fmt.Fprintf(conn, "EHLO moxtest\r\n"); err != nil {
760 return fmt.Errorf("writing SMTP EHLO to remote: %s", err)
763 line, err := br.ReadString('\n')
765 return fmt.Errorf("reading SMTP EHLO response from remote: %s", err)
767 if strings.HasPrefix(line, "250-") {
770 if strings.HasPrefix(line, "250 ") {
773 return fmt.Errorf("unexpected response to SMTP EHLO from remote: %q", strings.TrimSuffix(line, "\r\n"))
775 if _, err := fmt.Fprintf(conn, "STARTTLS\r\n"); err != nil {
776 return fmt.Errorf("writing SMTP STARTTLS to remote: %s", err)
778 line, err := br.ReadString('\n')
780 return fmt.Errorf("reading response to SMTP STARTTLS from remote: %s", err)
782 if !strings.HasPrefix(line, "220 ") {
783 return fmt.Errorf("SMTP STARTTLS response from remote not 220 OK: %q", strings.TrimSuffix(line, "\r\n"))
785 config := &tls.Config{
787 RootCAs: mox.Conf.Static.TLS.CertPool,
789 tlsconn := tls.Client(conn, config)
790 if err := tlsconn.HandshakeContext(cctx); err != nil {
791 return fmt.Errorf("TLS handshake after SMTP STARTTLS: %s", err)
799 checkSMTPSTARTTLS := func() {
800 // Initial errors are ignored, will already have been warned about by MX checks.
801 mxs, _, err := resolver.LookupMX(ctx, domain.ASCII+".")
805 if len(mxs) == 1 && mxs[0].Host == "." {
808 for _, mx := range mxs {
809 ips, _, _, err := lookupIPs(&r.MX.Errors, mx.Host)
814 for _, ip := range ips {
815 if err := dialSMTPSTARTTLS(mx.Host, ip); err != nil {
816 addf(&r.TLS.Errors, "SMTP connection with STARTTLS to MX hostname %q IP %s: %s", mx.Host, ip, err)
832 daneRecords := func(l config.Listener) map[string]struct{} {
836 records := map[string]struct{}{}
837 addRecord := func(privKey crypto.Signer) {
838 spkiBuf, err := x509.MarshalPKIXPublicKey(privKey.Public())
840 addf(&r.DANE.Errors, "marshal SubjectPublicKeyInfo for DANE record: %v", err)
843 sum := sha256.Sum256(spkiBuf)
845 Usage: adns.TLSAUsageDANEEE,
846 Selector: adns.TLSASelectorSPKI,
847 MatchType: adns.TLSAMatchTypeSHA256,
850 records[r.Record()] = struct{}{}
852 for _, privKey := range l.TLS.HostPrivateRSA2048Keys {
855 for _, privKey := range l.TLS.HostPrivateECDSAP256Keys {
861 expectedDANERecords := func(host string) map[string]struct{} {
862 for _, l := range mox.Conf.Static.Listeners {
863 if l.HostnameDomain.ASCII == host {
864 return daneRecords(l)
867 public := mox.Conf.Static.Listeners["public"]
868 if mox.Conf.Static.HostnameDomain.ASCII == host && public.HostnameDomain.ASCII == "" {
869 return daneRecords(public)
874 mxl, result, err := resolver.LookupMX(ctx, domain.ASCII+".")
876 addf(&r.DANE.Errors, "Looking up MX hosts to check for DANE records: %s", err)
878 if !result.Authentic {
879 addf(&r.DANE.Warnings, "DANE is inactive because MX records are not DNSSEC-signed.")
881 for _, mx := range mxl {
882 expect := expectedDANERecords(mx.Host)
884 tlsal, tlsaResult, err := resolver.LookupTLSA(ctx, 25, "tcp", mx.Host+".")
885 if dns.IsNotFound(err) {
887 addf(&r.DANE.Errors, "No DANE records for MX host %s, expected: %s.", mx.Host, strings.Join(maps.Keys(expect), "; "))
890 } else if err != nil {
891 addf(&r.DANE.Errors, "Looking up DANE records for MX host %s: %v", mx.Host, err)
893 } else if !tlsaResult.Authentic && len(tlsal) > 0 {
894 addf(&r.DANE.Errors, "DANE records exist for MX host %s, but are not DNSSEC-signed.", mx.Host)
897 extra := map[string]struct{}{}
898 for _, e := range tlsal {
900 if _, ok := expect[s]; ok {
903 extra[s] = struct{}{}
907 l := maps.Keys(expect)
909 addf(&r.DANE.Errors, "Missing DANE records of type TLSA for MX host _25._tcp.%s: %s", mx.Host, strings.Join(l, "; "))
912 l := maps.Keys(extra)
914 addf(&r.DANE.Errors, "Unexpected DANE records of type TLSA for MX host _25._tcp.%s: %s", mx.Host, strings.Join(l, "; "))
919 public := mox.Conf.Static.Listeners["public"]
920 pubDom := public.HostnameDomain
921 if pubDom.ASCII == "" {
922 pubDom = mox.Conf.Static.HostnameDomain
924 records := maps.Keys(daneRecords(public))
925 sort.Strings(records)
926 if len(records) > 0 {
927 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"
928 for _, r := range records {
929 instr += fmt.Sprintf("\t_25._tcp.%s. TLSA %s\n", pubDom.ASCII, r)
931 addf(&r.DANE.Instructions, instr)
936 // 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.
942 // Verify a domain with the configured IPs that do SMTP.
943 verifySPF := func(kind string, domain dns.Domain) (string, *SPFRecord, spf.Record) {
944 _, txt, record, _, err := spf.Lookup(ctx, log.Logger, resolver, domain)
946 addf(&r.SPF.Errors, "Looking up %s SPF record: %s", kind, err)
948 var xrecord *SPFRecord
950 xrecord = &SPFRecord{*record}
957 checkSPFIP := func(ip net.IP) {
962 spfr.Directives = append(spfr.Directives, spf.Directive{Mechanism: mechanism, IP: ip})
970 MailFromLocalpart: "postmaster",
971 MailFromDomain: domain,
972 HelloDomain: dns.IPDomain{Domain: domain},
973 LocalIP: net.ParseIP("127.0.0.1"),
974 LocalHostname: dns.Domain{ASCII: "localhost"},
976 status, mechanism, expl, _, err := spf.Evaluate(ctx, log.Logger, record, resolver, args)
978 addf(&r.SPF.Errors, "Evaluating IP %q against %s SPF record: %s", ip, kind, err)
979 } else if status != spf.StatusPass {
980 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)
984 for _, l := range mox.Conf.Static.Listeners {
985 if !l.SMTP.Enabled || l.IPsNATed {
989 if len(l.NATIPs) > 0 {
992 for _, ipstr := range ips {
993 ip := net.ParseIP(ipstr)
997 for _, t := range mox.Conf.Static.Transports {
999 for _, ip := range t.Socks.IPs {
1005 spfr.Directives = append(spfr.Directives, spf.Directive{Qualifier: "-", Mechanism: "all"})
1006 return txt, xrecord, spfr
1009 // Check SPF record for domain.
1010 var dspfr spf.Record
1011 r.SPF.DomainTXT, r.SPF.DomainRecord, dspfr = verifySPF("domain", domain)
1012 // todo: possibly check all hosts for MX records? assuming they are also sending mail servers.
1013 r.SPF.HostTXT, r.SPF.HostRecord, _ = verifySPF("host", mox.Conf.Static.HostnameDomain)
1015 dtxt, err := dspfr.Record()
1017 addf(&r.SPF.Errors, "Making SPF record for instructions: %s", err)
1019 domainspf := fmt.Sprintf("%s TXT %s", domain.ASCII+".", mox.TXTStrings(dtxt))
1022 hostspf := fmt.Sprintf(`%s TXT "v=spf1 a -all"`, mox.Conf.Static.HostnameDomain.ASCII+".")
1024 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)
1028 // 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.
1034 var missing []string
1035 var haveEd25519 bool
1036 for sel, selc := range domConf.DKIM.Selectors {
1037 if _, ok := selc.Key.(ed25519.PrivateKey); ok {
1041 _, record, txt, _, err := dkim.Lookup(ctx, log.Logger, resolver, selc.Domain, domain)
1043 missing = append(missing, sel)
1044 if errors.Is(err, dkim.ErrNoRecord) {
1045 addf(&r.DKIM.Errors, "No DKIM DNS record for selector %q.", sel)
1046 } else if errors.Is(err, dkim.ErrSyntax) {
1047 addf(&r.DKIM.Errors, "Parsing DKIM DNS record for selector %q: %s", sel, err)
1049 addf(&r.DKIM.Errors, "Fetching DKIM record for selector %q: %s", sel, err)
1053 r.DKIM.Records = append(r.DKIM.Records, DKIMRecord{sel, txt, record})
1054 pubKey := selc.Key.Public()
1056 switch k := pubKey.(type) {
1057 case *rsa.PublicKey:
1059 pk, err = x509.MarshalPKIXPublicKey(k)
1061 addf(&r.DKIM.Errors, "Marshal public key for %q to compare against DNS: %s", sel, err)
1064 case ed25519.PublicKey:
1067 addf(&r.DKIM.Errors, "Internal error: unknown public key type %T.", pubKey)
1071 if record != nil && !bytes.Equal(record.Pubkey, pk) {
1072 addf(&r.DKIM.Errors, "For selector %q, the public key in DKIM DNS TXT record does not match with configured private key.", sel)
1073 missing = append(missing, sel)
1077 if len(domConf.DKIM.Selectors) == 0 {
1078 addf(&r.DKIM.Errors, "No DKIM configuration, add a key to the configuration file, and instructions for DNS records will appear here.")
1079 } else if !haveEd25519 {
1080 addf(&r.DKIM.Warnings, "Consider adding an ed25519 key: the keys are smaller, the cryptography faster and more modern.")
1083 for _, sel := range missing {
1084 dkimr := dkim.Record{
1086 Hashes: []string{"sha256"},
1087 PublicKey: domConf.DKIM.Selectors[sel].Key.Public(),
1089 switch dkimr.PublicKey.(type) {
1090 case *rsa.PublicKey:
1091 case ed25519.PublicKey:
1092 dkimr.Key = "ed25519"
1094 addf(&r.DKIM.Errors, "Internal error: unknown public key type %T.", dkimr.PublicKey)
1096 txt, err := dkimr.Record()
1098 addf(&r.DKIM.Errors, "Making DKIM record for instructions: %s", err)
1101 instr += fmt.Sprintf("\n\t%s._domainkey TXT %s\n", sel, mox.TXTStrings(txt))
1104 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
1105 addf(&r.DKIM.Instructions, "%s", instr)
1115 _, dmarcDomain, record, txt, _, err := dmarc.Lookup(ctx, log.Logger, resolver, domain)
1117 addf(&r.DMARC.Errors, "Looking up DMARC record: %s", err)
1118 } else if record == nil {
1119 addf(&r.DMARC.Errors, "No DMARC record")
1121 r.DMARC.Domain = dmarcDomain.Name()
1124 r.DMARC.Record = &DMARCRecord{*record}
1126 if record != nil && record.Policy == "none" {
1127 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.")
1129 if record != nil && record.SubdomainPolicy == "none" {
1130 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.")
1132 if record != nil && len(record.AggregateReportAddresses) == 0 {
1133 addf(&r.DMARC.Warnings, "It is recommended you specify you would like aggregate reports about delivery success in the DMARC record, see instructions.")
1136 dmarcr := dmarc.DefaultRecord
1137 dmarcr.Policy = "reject"
1140 if domConf.DMARC != nil {
1141 // If the domain is in a different Organizational Domain, the receiving domain
1142 // needs a special DNS record to opt-in to receiving reports. We check for that
1145 orgDom := publicsuffix.Lookup(ctx, log.Logger, domain)
1146 destOrgDom := publicsuffix.Lookup(ctx, log.Logger, domConf.DMARC.DNSDomain)
1147 if orgDom != destOrgDom {
1148 accepts, status, _, _, _, err := dmarc.LookupExternalReportsAccepted(ctx, log.Logger, resolver, domain, domConf.DMARC.DNSDomain)
1149 if status != dmarc.StatusNone {
1150 addf(&r.DMARC.Errors, "Checking if external destination accepts reports: %s", err)
1151 } else if !accepts {
1152 addf(&r.DMARC.Errors, "External destination does not accept reports (%s)", err)
1154 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)
1159 Opaque: smtp.NewAddress(domConf.DMARC.ParsedLocalpart, domConf.DMARC.DNSDomain).Pack(false),
1161 uristr := uri.String()
1162 dmarcr.AggregateReportAddresses = []dmarc.URI{
1163 {Address: uristr, MaxSize: 10, Unit: "m"},
1168 for _, addr := range record.AggregateReportAddresses {
1169 if addr.Address == uristr {
1175 addf(&r.DMARC.Errors, "Configured DMARC reporting address is not present in record.")
1179 addf(&r.DMARC.Instructions, `Configure a DMARC destination in domain in config file.`)
1181 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()))
1182 addf(&r.DMARC.Instructions, instr)
1184 addf(&r.DMARC.Instructions, extInstr)
1188 checkTLSRPT := func(result *TLSRPTCheckResult, dom dns.Domain, address smtp.Address, isHost bool) {
1192 record, txt, err := tlsrpt.Lookup(ctx, log.Logger, resolver, dom)
1194 addf(&result.Errors, "Looking up TLSRPT record: %s", err)
1198 result.Record = &TLSRPTRecord{*record}
1201 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.`
1202 var zeroaddr smtp.Address
1203 if address != zeroaddr {
1204 // TLSRPT does not require validation of reporting addresses outside the domain.
1208 Opaque: address.Pack(false),
1210 rua := tlsrpt.RUA(uri.String())
1211 tlsrptr := &tlsrpt.Record{
1212 Version: "TLSRPTv1",
1213 RUAs: [][]tlsrpt.RUA{{rua}},
1215 instr += fmt.Sprintf(`
1217Ensure a DNS TXT record like the following exists:
1220`, mox.TXTStrings(tlsrptr.String()))
1225 for _, l := range record.RUAs {
1226 for _, e := range l {
1234 addf(&result.Errors, `Configured reporting address is not present in TLSRPT record.`)
1239 addf(&result.Errors, `Configure a host TLSRPT localpart in static mox.conf config file.`)
1241 addf(&result.Errors, `Configure a domain TLSRPT destination in domains.conf config file.`)
1243 addf(&result.Instructions, instr)
1248 var hostTLSRPTAddr smtp.Address
1249 if mox.Conf.Static.HostTLSRPT.Localpart != "" {
1250 hostTLSRPTAddr = smtp.NewAddress(mox.Conf.Static.HostTLSRPT.ParsedLocalpart, mox.Conf.Static.HostnameDomain)
1252 go checkTLSRPT(&r.HostTLSRPT, mox.Conf.Static.HostnameDomain, hostTLSRPTAddr, true)
1256 var domainTLSRPTAddr smtp.Address
1257 if domConf.TLSRPT != nil {
1258 domainTLSRPTAddr = smtp.NewAddress(domConf.TLSRPT.ParsedLocalpart, domain)
1260 go checkTLSRPT(&r.DomainTLSRPT, domain, domainTLSRPTAddr, false)
1268 record, txt, err := mtasts.LookupRecord(ctx, log.Logger, resolver, domain)
1270 addf(&r.MTASTS.Errors, "Looking up MTA-STS record: %s", err)
1274 r.MTASTS.Record = &MTASTSRecord{*record}
1277 policy, text, err := mtasts.FetchPolicy(ctx, log.Logger, domain)
1279 addf(&r.MTASTS.Errors, "Fetching MTA-STS policy: %s", err)
1280 } else if policy.Mode == mtasts.ModeNone {
1281 addf(&r.MTASTS.Warnings, "MTA-STS policy is present, but does not require TLS.")
1282 } else if policy.Mode == mtasts.ModeTesting {
1283 addf(&r.MTASTS.Warnings, "MTA-STS policy is in testing mode, do not forget to change to mode enforce after testing period.")
1285 r.MTASTS.PolicyText = text
1286 r.MTASTS.Policy = policy
1287 if policy != nil && policy.Mode != mtasts.ModeNone {
1288 if !policy.Matches(mox.Conf.Static.HostnameDomain) {
1289 addf(&r.MTASTS.Warnings, "Configured hostname is missing from policy MX list.")
1291 if policy.MaxAgeSeconds <= 24*3600 {
1292 addf(&r.MTASTS.Warnings, "Policy has a MaxAge of less than 1 day. For stable configurations, the recommended period is in weeks.")
1295 mxl, _, _ := resolver.LookupMX(ctx, domain.ASCII+".")
1296 // We do not check for errors, the MX check will complain about mx errors, we assume we will get the same error here.
1297 mxs := map[dns.Domain]struct{}{}
1298 for _, mx := range mxl {
1299 d, err := dns.ParseDomain(strings.TrimSuffix(mx.Host, "."))
1301 addf(&r.MTASTS.Warnings, "MX record %q is invalid: %s", mx.Host, err)
1306 for mx := range mxs {
1307 if !policy.Matches(mx) {
1308 addf(&r.MTASTS.Warnings, "MX record %q does not match MTA-STS policy MX list.", mx)
1311 for _, mx := range policy.MX {
1315 if _, ok := mxs[mx.Domain]; !ok {
1316 addf(&r.MTASTS.Warnings, "MX %q in MTA-STS policy is not in MX record.", mx)
1321 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.
1323After 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.
1325You 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.
1327You 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.
1329The _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.
1331When enabling MTA-STS, or updating a policy, always update the policy first (through a configuration change and reload/restart), and the DNS record second.
1333 addf(&r.MTASTS.Instructions, intro)
1335 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.`)
1337 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+".")
1338 addf(&r.MTASTS.Instructions, host)
1340 mtastsr := mtasts.Record{
1342 ID: time.Now().Format("20060102T150405"),
1344 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())
1345 addf(&r.MTASTS.Instructions, dns)
1354 type srvReq struct {
1362 // We'll assume if any submissions is configured, it is public. Same for imap. And
1363 // if not, that there is a plain option.
1364 var submissions, imaps bool
1365 for _, l := range mox.Conf.Static.Listeners {
1366 if l.TLS != nil && l.Submissions.Enabled {
1369 if l.TLS != nil && l.IMAPS.Enabled {
1373 srvhost := func(ok bool) string {
1375 return mox.Conf.Static.HostnameDomain.ASCII + "."
1379 var reqs = []srvReq{
1380 {name: "_submissions", port: 465, host: srvhost(submissions)},
1381 {name: "_submission", port: 587, host: srvhost(!submissions)},
1382 {name: "_imaps", port: 993, host: srvhost(imaps)},
1383 {name: "_imap", port: 143, host: srvhost(!imaps)},
1384 {name: "_pop3", port: 110, host: "."},
1385 {name: "_pop3s", port: 995, host: "."},
1387 var srvwg sync.WaitGroup
1388 srvwg.Add(len(reqs))
1389 for i := range reqs {
1392 _, reqs[i].srvs, _, reqs[i].err = resolver.LookupSRV(ctx, reqs[i].name[1:], "tcp", domain.ASCII+".")
1397 instr := "Ensure DNS records like the following exist:\n\n"
1398 r.SRVConf.SRVs = map[string][]net.SRV{}
1399 for _, req := range reqs {
1400 name := req.name + "._tcp." + domain.ASCII
1401 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)
1402 r.SRVConf.SRVs[req.name] = unptr(req.srvs)
1404 addf(&r.SRVConf.Errors, "Looking up SRV record %q: %s", name, err)
1405 } else if len(req.srvs) == 0 {
1406 addf(&r.SRVConf.Errors, "Missing SRV record %q", name)
1407 } else if len(req.srvs) != 1 || req.srvs[0].Target != req.host || req.srvs[0].Port != req.port {
1408 addf(&r.SRVConf.Errors, "Unexpected SRV record(s) for %q", name)
1411 addf(&r.SRVConf.Instructions, instr)
1420 if domConf.ClientSettingsDomain != "" {
1421 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+".")
1423 ips, ourIPs, notOurIPs, err := lookupIPs(&r.Autoconf.Errors, domConf.ClientSettingsDNSDomain.ASCII+".")
1425 addf(&r.Autoconf.Errors, "Looking up client settings DNS CNAME: %s", err)
1427 r.Autoconf.ClientSettingsDomainIPs = ips
1428 if !isUnspecifiedNAT {
1429 if len(ourIPs) == 0 {
1430 addf(&r.Autoconf.Errors, "Client settings domain does not point to one of our IPs.")
1431 } else if len(notOurIPs) > 0 {
1432 addf(&r.Autoconf.Errors, "Client settings domain points to some IPs that are not ours: %v", notOurIPs)
1437 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+".")
1439 host := "autoconfig." + domain.ASCII + "."
1440 ips, ourIPs, notOurIPs, err := lookupIPs(&r.Autoconf.Errors, host)
1442 addf(&r.Autoconf.Errors, "Looking up autoconfig host: %s", err)
1446 r.Autoconf.IPs = ips
1447 if !isUnspecifiedNAT {
1448 if len(ourIPs) == 0 {
1449 addf(&r.Autoconf.Errors, "Autoconfig does not point to one of our IPs.")
1450 } else if len(notOurIPs) > 0 {
1451 addf(&r.Autoconf.Errors, "Autoconfig points to some IPs that are not ours: %v", notOurIPs)
1455 checkTLS(&r.Autoconf.Errors, "autoconfig."+domain.ASCII, ips, "443")
1464 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+".")
1466 _, srvs, _, err := resolver.LookupSRV(ctx, "autodiscover", "tcp", domain.ASCII+".")
1468 addf(&r.Autodiscover.Errors, "Looking up SRV record %q: %s", "autodiscover", err)
1472 for _, srv := range srvs {
1473 ips, ourIPs, notOurIPs, err := lookupIPs(&r.Autodiscover.Errors, srv.Target)
1475 addf(&r.Autodiscover.Errors, "Looking up target %q from SRV record: %s", srv.Target, err)
1478 if srv.Port != 443 {
1482 r.Autodiscover.Records = append(r.Autodiscover.Records, AutodiscoverSRV{*srv, ips})
1483 if !isUnspecifiedNAT {
1484 if len(ourIPs) == 0 {
1485 addf(&r.Autodiscover.Errors, "SRV target %q does not point to our IPs.", srv.Target)
1486 } else if len(notOurIPs) > 0 {
1487 addf(&r.Autodiscover.Errors, "SRV target %q points to some IPs that are not ours: %v", srv.Target, notOurIPs)
1491 checkTLS(&r.Autodiscover.Errors, strings.TrimSuffix(srv.Target, "."), ips, "443")
1494 addf(&r.Autodiscover.Errors, "No SRV record for port 443 for https.")
1502// Domains returns all configured domain names, in UTF-8 for IDNA domains.
1503func (Admin) Domains(ctx context.Context) []dns.Domain {
1505 for _, s := range mox.Conf.Domains() {
1506 d, _ := dns.ParseDomain(s)
1512// Domain returns the dns domain for a (potentially unicode as IDNA) domain name.
1513func (Admin) Domain(ctx context.Context, domain string) dns.Domain {
1514 d, err := dns.ParseDomain(domain)
1515 xcheckuserf(ctx, err, "parse domain")
1516 _, ok := mox.Conf.Domain(d)
1518 xcheckuserf(ctx, errors.New("no such domain"), "looking up domain")
1523// ParseDomain parses a domain, possibly an IDNA domain.
1524func (Admin) ParseDomain(ctx context.Context, domain string) dns.Domain {
1525 d, err := dns.ParseDomain(domain)
1526 xcheckuserf(ctx, err, "parse domain")
1530// DomainConfig returns the configuration for a domain.
1531func (Admin) DomainConfig(ctx context.Context, domain string) config.Domain {
1532 d, err := dns.ParseDomain(domain)
1533 xcheckuserf(ctx, err, "parse domain")
1534 conf, ok := mox.Conf.Domain(d)
1536 xcheckuserf(ctx, errors.New("no such domain"), "looking up domain")
1541// DomainLocalparts returns the encoded localparts and accounts configured in domain.
1542func (Admin) DomainLocalparts(ctx context.Context, domain string) (localpartAccounts map[string]string, localpartAliases map[string]config.Alias) {
1543 d, err := dns.ParseDomain(domain)
1544 xcheckuserf(ctx, err, "parsing domain")
1545 _, ok := mox.Conf.Domain(d)
1547 xcheckuserf(ctx, errors.New("no such domain"), "looking up domain")
1549 return mox.Conf.DomainLocalparts(d)
1552// Accounts returns the names of all configured accounts.
1553func (Admin) Accounts(ctx context.Context) []string {
1554 l := mox.Conf.Accounts()
1555 sort.Slice(l, func(i, j int) bool {
1561// Account returns the parsed configuration of an account.
1562func (Admin) Account(ctx context.Context, account string) (accountConfig config.Account, diskUsage int64) {
1563 log := pkglog.WithContext(ctx)
1565 acc, err := store.OpenAccount(log, account)
1566 if err != nil && errors.Is(err, store.ErrAccountUnknown) {
1567 xcheckuserf(ctx, err, "looking up account")
1569 xcheckf(ctx, err, "open account")
1572 log.Check(err, "closing account")
1575 var ac config.Account
1576 acc.WithRLock(func() {
1577 ac, _ = mox.Conf.Account(acc.Name)
1579 err := acc.DB.Read(ctx, func(tx *bstore.Tx) error {
1580 du := store.DiskUsage{ID: 1}
1582 diskUsage = du.MessageSize
1585 xcheckf(ctx, err, "get disk usage")
1588 return ac, diskUsage
1591// ConfigFiles returns the paths and contents of the static and dynamic configuration files.
1592func (Admin) ConfigFiles(ctx context.Context) (staticPath, dynamicPath, static, dynamic string) {
1593 buf0, err := os.ReadFile(mox.ConfigStaticPath)
1594 xcheckf(ctx, err, "read static config file")
1595 buf1, err := os.ReadFile(mox.ConfigDynamicPath)
1596 xcheckf(ctx, err, "read dynamic config file")
1597 return mox.ConfigStaticPath, mox.ConfigDynamicPath, string(buf0), string(buf1)
1600// MTASTSPolicies returns all mtasts policies from the cache.
1601func (Admin) MTASTSPolicies(ctx context.Context) (records []mtastsdb.PolicyRecord) {
1602 records, err := mtastsdb.PolicyRecords(ctx)
1603 xcheckf(ctx, err, "fetching mtasts policies from database")
1607// TLSReports returns TLS reports overlapping with period start/end, for the given
1608// policy domain (or all domains if empty). The reports are sorted first by period
1609// end (most recent first), then by policy domain.
1610func (Admin) TLSReports(ctx context.Context, start, end time.Time, policyDomain string) (reports []tlsrptdb.Record) {
1611 var polDom dns.Domain
1612 if policyDomain != "" {
1614 polDom, err = dns.ParseDomain(policyDomain)
1615 xcheckuserf(ctx, err, "parsing domain %q", policyDomain)
1618 records, err := tlsrptdb.RecordsPeriodDomain(ctx, start, end, polDom)
1619 xcheckf(ctx, err, "fetching tlsrpt report records from database")
1620 sort.Slice(records, func(i, j int) bool {
1621 iend := records[i].Report.DateRange.End
1622 jend := records[j].Report.DateRange.End
1624 return records[i].Domain < records[j].Domain
1626 return iend.After(jend)
1631// TLSReportID returns a single TLS report.
1632func (Admin) TLSReportID(ctx context.Context, domain string, reportID int64) tlsrptdb.Record {
1633 record, err := tlsrptdb.RecordID(ctx, reportID)
1634 if err == nil && record.Domain != domain {
1635 err = bstore.ErrAbsent
1637 if err == bstore.ErrAbsent {
1638 xcheckuserf(ctx, err, "fetching tls report from database")
1640 xcheckf(ctx, err, "fetching tls report from database")
1644// TLSRPTSummary presents TLS reporting statistics for a single domain
1646type TLSRPTSummary struct {
1647 PolicyDomain dns.Domain
1650 ResultTypeCounts map[tlsrpt.ResultType]int64
1653// TLSRPTSummaries returns a summary of received TLS reports overlapping with
1654// period start/end for one or all domains (when domain is empty).
1655// The returned summaries are ordered by domain name.
1656func (Admin) TLSRPTSummaries(ctx context.Context, start, end time.Time, policyDomain string) (domainSummaries []TLSRPTSummary) {
1657 var polDom dns.Domain
1658 if policyDomain != "" {
1660 polDom, err = dns.ParseDomain(policyDomain)
1661 xcheckuserf(ctx, err, "parsing policy domain")
1663 reports, err := tlsrptdb.RecordsPeriodDomain(ctx, start, end, polDom)
1664 xcheckf(ctx, err, "fetching tlsrpt reports from database")
1666 summaries := map[dns.Domain]TLSRPTSummary{}
1667 for _, r := range reports {
1668 dom, err := dns.ParseDomain(r.Domain)
1669 xcheckf(ctx, err, "parsing domain %q", r.Domain)
1671 sum := summaries[dom]
1672 sum.PolicyDomain = dom
1673 for _, result := range r.Report.Policies {
1674 sum.Success += result.Summary.TotalSuccessfulSessionCount
1675 sum.Failure += result.Summary.TotalFailureSessionCount
1676 for _, details := range result.FailureDetails {
1677 if sum.ResultTypeCounts == nil {
1678 sum.ResultTypeCounts = map[tlsrpt.ResultType]int64{}
1680 sum.ResultTypeCounts[details.ResultType] += details.FailedSessionCount
1683 summaries[dom] = sum
1685 sums := make([]TLSRPTSummary, 0, len(summaries))
1686 for _, sum := range summaries {
1687 sums = append(sums, sum)
1689 sort.Slice(sums, func(i, j int) bool {
1690 return sums[i].PolicyDomain.Name() < sums[j].PolicyDomain.Name()
1695// DMARCReports returns DMARC reports overlapping with period start/end, for the
1696// given domain (or all domains if empty). The reports are sorted first by period
1697// end (most recent first), then by domain.
1698func (Admin) DMARCReports(ctx context.Context, start, end time.Time, domain string) (reports []dmarcdb.DomainFeedback) {
1699 reports, err := dmarcdb.RecordsPeriodDomain(ctx, start, end, domain)
1700 xcheckf(ctx, err, "fetching dmarc aggregate reports from database")
1701 sort.Slice(reports, func(i, j int) bool {
1702 iend := reports[i].ReportMetadata.DateRange.End
1703 jend := reports[j].ReportMetadata.DateRange.End
1705 return reports[i].Domain < reports[j].Domain
1712// DMARCReportID returns a single DMARC report.
1713func (Admin) DMARCReportID(ctx context.Context, domain string, reportID int64) (report dmarcdb.DomainFeedback) {
1714 report, err := dmarcdb.RecordID(ctx, reportID)
1715 if err == nil && report.Domain != domain {
1716 err = bstore.ErrAbsent
1718 if err == bstore.ErrAbsent {
1719 xcheckuserf(ctx, err, "fetching dmarc aggregate report from database")
1721 xcheckf(ctx, err, "fetching dmarc aggregate report from database")
1725// DMARCSummary presents DMARC aggregate reporting statistics for a single domain
1727type DMARCSummary struct {
1731 DispositionQuarantine int
1732 DispositionReject int
1735 PolicyOverrides map[dmarcrpt.PolicyOverride]int
1738// DMARCSummaries returns a summary of received DMARC reports overlapping with
1739// period start/end for one or all domains (when domain is empty).
1740// The returned summaries are ordered by domain name.
1741func (Admin) DMARCSummaries(ctx context.Context, start, end time.Time, domain string) (domainSummaries []DMARCSummary) {
1742 reports, err := dmarcdb.RecordsPeriodDomain(ctx, start, end, domain)
1743 xcheckf(ctx, err, "fetching dmarc aggregate reports from database")
1744 summaries := map[string]DMARCSummary{}
1745 for _, r := range reports {
1746 sum := summaries[r.Domain]
1747 sum.Domain = r.Domain
1748 for _, record := range r.Records {
1749 n := record.Row.Count
1753 switch record.Row.PolicyEvaluated.Disposition {
1754 case dmarcrpt.DispositionNone:
1755 sum.DispositionNone += n
1756 case dmarcrpt.DispositionQuarantine:
1757 sum.DispositionQuarantine += n
1758 case dmarcrpt.DispositionReject:
1759 sum.DispositionReject += n
1762 if record.Row.PolicyEvaluated.DKIM == dmarcrpt.DMARCFail {
1765 if record.Row.PolicyEvaluated.SPF == dmarcrpt.DMARCFail {
1769 for _, reason := range record.Row.PolicyEvaluated.Reasons {
1770 if sum.PolicyOverrides == nil {
1771 sum.PolicyOverrides = map[dmarcrpt.PolicyOverride]int{}
1773 sum.PolicyOverrides[reason.Type] += n
1776 summaries[r.Domain] = sum
1778 sums := make([]DMARCSummary, 0, len(summaries))
1779 for _, sum := range summaries {
1780 sums = append(sums, sum)
1782 sort.Slice(sums, func(i, j int) bool {
1783 return sums[i].Domain < sums[j].Domain
1788// Reverse is the result of a reverse lookup.
1789type Reverse struct {
1792 // In the future, we can add a iprev-validated host name, and possibly the IPs of the host names.
1795// LookupIP does a reverse lookup of ip.
1796func (Admin) LookupIP(ctx context.Context, ip string) Reverse {
1797 resolver := dns.StrictResolver{Pkg: "webadmin", Log: pkglog.WithContext(ctx).Logger}
1798 names, _, err := resolver.LookupAddr(ctx, ip)
1799 xcheckuserf(ctx, err, "looking up ip")
1800 return Reverse{names}
1803// DNSBLStatus returns the IPs from which outgoing connections may be made and
1804// their current status in DNSBLs that are configured. The IPs are typically the
1805// configured listen IPs, or otherwise IPs on the machines network interfaces, with
1806// internal/private IPs removed.
1808// The returned value maps IPs to per DNSBL statuses, where "pass" means not listed and
1809// anything else is an error string, e.g. "fail: ..." or "temperror: ...".
1810func (Admin) DNSBLStatus(ctx context.Context) (results map[string]map[string]string, using, monitoring []dns.Domain) {
1811 log := mlog.New("webadmin", nil).WithContext(ctx)
1812 resolver := dns.StrictResolver{Pkg: "check", Log: log.Logger}
1813 return dnsblsStatus(ctx, log, resolver)
1816func dnsblsStatus(ctx context.Context, log mlog.Log, resolver dns.Resolver) (results map[string]map[string]string, using, monitoring []dns.Domain) {
1817 // todo: check health before using dnsbl?
1818 using = mox.Conf.Static.Listeners["public"].SMTP.DNSBLZones
1819 zones := append([]dns.Domain{}, using...)
1820 conf := mox.Conf.DynamicConfig()
1821 for _, zone := range conf.MonitorDNSBLZones {
1822 if !slices.Contains(zones, zone) {
1823 zones = append(zones, zone)
1824 monitoring = append(monitoring, zone)
1828 r := map[string]map[string]string{}
1829 for _, ip := range xsendingIPs(ctx) {
1830 if ip.IsLoopback() || ip.IsPrivate() {
1833 ipstr := ip.String()
1834 r[ipstr] = map[string]string{}
1835 for _, zone := range zones {
1836 status, expl, err := dnsbl.Lookup(ctx, log.Logger, resolver, zone, ip)
1837 result := string(status)
1839 result += ": " + err.Error()
1842 result += ": " + expl
1844 r[ipstr][zone.LogString()] = result
1847 return r, using, monitoring
1850func (Admin) MonitorDNSBLsSave(ctx context.Context, text string) {
1851 var zones []dns.Domain
1852 publicZones := mox.Conf.Static.Listeners["public"].SMTP.DNSBLZones
1853 for _, line := range strings.Split(text, "\n") {
1854 line = strings.TrimSpace(line)
1858 d, err := dns.ParseDomain(line)
1859 xcheckuserf(ctx, err, "parsing dnsbl zone %s", line)
1860 if slices.Contains(zones, d) {
1861 xusererrorf(ctx, "duplicate dnsbl zone %s", line)
1863 if slices.Contains(publicZones, d) {
1864 xusererrorf(ctx, "dnsbl zone %s already present in public listener", line)
1866 zones = append(zones, d)
1869 err := mox.ConfigSave(ctx, func(conf *config.Dynamic) {
1870 conf.MonitorDNSBLs = make([]string, len(zones))
1871 conf.MonitorDNSBLZones = nil
1872 for i, z := range zones {
1873 conf.MonitorDNSBLs[i] = z.Name()
1876 xcheckf(ctx, err, "saving monitoring dnsbl zones")
1879// DomainRecords returns lines describing DNS records that should exist for the
1880// configured domain.
1881func (Admin) DomainRecords(ctx context.Context, domain string) []string {
1882 log := pkglog.WithContext(ctx)
1883 return DomainRecords(ctx, log, domain)
1886// DomainRecords is the implementation of API function Admin.DomainRecords, taking
1888func DomainRecords(ctx context.Context, log mlog.Log, domain string) []string {
1889 d, err := dns.ParseDomain(domain)
1890 xcheckuserf(ctx, err, "parsing domain")
1891 dc, ok := mox.Conf.Domain(d)
1893 xcheckuserf(ctx, errors.New("unknown domain"), "lookup domain")
1895 resolver := dns.StrictResolver{Pkg: "webadmin", Log: pkglog.WithContext(ctx).Logger}
1896 _, result, err := resolver.LookupTXT(ctx, domain+".")
1897 if !dns.IsNotFound(err) {
1898 xcheckf(ctx, err, "looking up record to determine if dnssec is implemented")
1901 var certIssuerDomainName, acmeAccountURI string
1902 public := mox.Conf.Static.Listeners["public"]
1903 if public.TLS != nil && public.TLS.ACME != "" {
1904 acme, ok := mox.Conf.Static.ACME[public.TLS.ACME]
1905 if ok && acme.Manager.Manager.Client != nil {
1906 certIssuerDomainName = acme.IssuerDomainName
1907 acc, err := acme.Manager.Manager.Client.GetReg(ctx, "")
1908 log.Check(err, "get public acme account")
1910 acmeAccountURI = acc.URI
1915 records, err := mox.DomainRecords(dc, d, result.Authentic, certIssuerDomainName, acmeAccountURI)
1916 xcheckf(ctx, err, "dns records")
1920// DomainAdd adds a new domain and reloads the configuration.
1921func (Admin) DomainAdd(ctx context.Context, domain, accountName, localpart string) {
1922 d, err := dns.ParseDomain(domain)
1923 xcheckuserf(ctx, err, "parsing domain")
1925 err = mox.DomainAdd(ctx, d, accountName, smtp.Localpart(norm.NFC.String(localpart)))
1926 xcheckf(ctx, err, "adding domain")
1929// DomainRemove removes an existing domain and reloads the configuration.
1930func (Admin) DomainRemove(ctx context.Context, domain string) {
1931 d, err := dns.ParseDomain(domain)
1932 xcheckuserf(ctx, err, "parsing domain")
1934 err = mox.DomainRemove(ctx, d)
1935 xcheckf(ctx, err, "removing domain")
1938// AccountAdd adds existing a new account, with an initial email address, and
1939// reloads the configuration.
1940func (Admin) AccountAdd(ctx context.Context, accountName, address string) {
1941 err := mox.AccountAdd(ctx, accountName, address)
1942 xcheckf(ctx, err, "adding account")
1945// AccountRemove removes an existing account and reloads the configuration.
1946func (Admin) AccountRemove(ctx context.Context, accountName string) {
1947 err := mox.AccountRemove(ctx, accountName)
1948 xcheckf(ctx, err, "removing account")
1951// AddressAdd adds a new address to the account, which must already exist.
1952func (Admin) AddressAdd(ctx context.Context, address, accountName string) {
1953 err := mox.AddressAdd(ctx, address, accountName)
1954 xcheckf(ctx, err, "adding address")
1957// AddressRemove removes an existing address.
1958func (Admin) AddressRemove(ctx context.Context, address string) {
1959 err := mox.AddressRemove(ctx, address)
1960 xcheckf(ctx, err, "removing address")
1963// SetPassword saves a new password for an account, invalidating the previous password.
1964// Sessions are not interrupted, and will keep working. New login attempts must use the new password.
1965// Password must be at least 8 characters.
1966func (Admin) SetPassword(ctx context.Context, accountName, password string) {
1967 log := pkglog.WithContext(ctx)
1968 if len(password) < 8 {
1969 xusererrorf(ctx, "message must be at least 8 characters")
1971 acc, err := store.OpenAccount(log, accountName)
1972 xcheckf(ctx, err, "open account")
1975 log.WithContext(ctx).Check(err, "closing account")
1977 err = acc.SetPassword(log, password)
1978 xcheckf(ctx, err, "setting password")
1981// AccountSettingsSave set new settings for an account that only an admin can set.
1982func (Admin) AccountSettingsSave(ctx context.Context, accountName string, maxOutgoingMessagesPerDay, maxFirstTimeRecipientsPerDay int, maxMsgSize int64, firstTimeSenderDelay bool) {
1983 err := mox.AccountSave(ctx, accountName, func(acc *config.Account) {
1984 acc.MaxOutgoingMessagesPerDay = maxOutgoingMessagesPerDay
1985 acc.MaxFirstTimeRecipientsPerDay = maxFirstTimeRecipientsPerDay
1986 acc.QuotaMessageSize = maxMsgSize
1987 acc.NoFirstTimeSenderDelay = !firstTimeSenderDelay
1989 xcheckf(ctx, err, "saving account settings")
1992// ClientConfigsDomain returns configurations for email clients, IMAP and
1993// Submission (SMTP) for the domain.
1994func (Admin) ClientConfigsDomain(ctx context.Context, domain string) mox.ClientConfigs {
1995 d, err := dns.ParseDomain(domain)
1996 xcheckuserf(ctx, err, "parsing domain")
1998 cc, err := mox.ClientConfigsDomain(d)
1999 xcheckf(ctx, err, "client config for domain")
2003// QueueSize returns the number of messages currently in the outgoing queue.
2004func (Admin) QueueSize(ctx context.Context) int {
2005 n, err := queue.Count(ctx)
2006 xcheckf(ctx, err, "listing messages in queue")
2010// QueueHoldRuleList lists the hold rules.
2011func (Admin) QueueHoldRuleList(ctx context.Context) []queue.HoldRule {
2012 l, err := queue.HoldRuleList(ctx)
2013 xcheckf(ctx, err, "listing queue hold rules")
2017// QueueHoldRuleAdd adds a hold rule. Newly submitted and existing messages
2018// matching the hold rule will be marked "on hold".
2019func (Admin) QueueHoldRuleAdd(ctx context.Context, hr queue.HoldRule) queue.HoldRule {
2021 hr.SenderDomain, err = dns.ParseDomain(hr.SenderDomainStr)
2022 xcheckuserf(ctx, err, "parsing sender domain %q", hr.SenderDomainStr)
2023 hr.RecipientDomain, err = dns.ParseDomain(hr.RecipientDomainStr)
2024 xcheckuserf(ctx, err, "parsing recipient domain %q", hr.RecipientDomainStr)
2026 log := pkglog.WithContext(ctx)
2027 hr, err = queue.HoldRuleAdd(ctx, log, hr)
2028 xcheckf(ctx, err, "adding queue hold rule")
2032// QueueHoldRuleRemove removes a hold rule. The Hold field of messages in
2033// the queue are not changed.
2034func (Admin) QueueHoldRuleRemove(ctx context.Context, holdRuleID int64) {
2035 log := pkglog.WithContext(ctx)
2036 err := queue.HoldRuleRemove(ctx, log, holdRuleID)
2037 xcheckf(ctx, err, "removing queue hold rule")
2040// QueueList returns the messages currently in the outgoing queue.
2041func (Admin) QueueList(ctx context.Context, filter queue.Filter, sort queue.Sort) []queue.Msg {
2042 l, err := queue.List(ctx, filter, sort)
2043 xcheckf(ctx, err, "listing messages in queue")
2047// QueueNextAttemptSet sets a new time for next delivery attempt of matching
2048// messages from the queue.
2049func (Admin) QueueNextAttemptSet(ctx context.Context, filter queue.Filter, minutes int) (affected int) {
2050 n, err := queue.NextAttemptSet(ctx, filter, time.Now().Add(time.Duration(minutes)*time.Minute))
2051 xcheckf(ctx, err, "setting new next delivery attempt time for matching messages in queue")
2055// QueueNextAttemptAdd adds a duration to the time of next delivery attempt of
2056// matching messages from the queue.
2057func (Admin) QueueNextAttemptAdd(ctx context.Context, filter queue.Filter, minutes int) (affected int) {
2058 n, err := queue.NextAttemptAdd(ctx, filter, time.Duration(minutes)*time.Minute)
2059 xcheckf(ctx, err, "adding duration to next delivery attempt for matching messages in queue")
2063// QueueHoldSet sets the Hold field of matching messages in the queue.
2064func (Admin) QueueHoldSet(ctx context.Context, filter queue.Filter, onHold bool) (affected int) {
2065 n, err := queue.HoldSet(ctx, filter, onHold)
2066 xcheckf(ctx, err, "changing onhold for matching messages in queue")
2070// QueueFail fails delivery for matching messages, causing DSNs to be sent.
2071func (Admin) QueueFail(ctx context.Context, filter queue.Filter) (affected int) {
2072 log := pkglog.WithContext(ctx)
2073 n, err := queue.Fail(ctx, log, filter)
2074 xcheckf(ctx, err, "drop messages from queue")
2078// QueueDrop removes matching messages from the queue.
2079func (Admin) QueueDrop(ctx context.Context, filter queue.Filter) (affected int) {
2080 log := pkglog.WithContext(ctx)
2081 n, err := queue.Drop(ctx, log, filter)
2082 xcheckf(ctx, err, "drop messages from queue")
2086// QueueRequireTLSSet updates the requiretls field for matching messages in the
2087// queue, to be used for the next delivery.
2088func (Admin) QueueRequireTLSSet(ctx context.Context, filter queue.Filter, requireTLS *bool) (affected int) {
2089 n, err := queue.RequireTLSSet(ctx, filter, requireTLS)
2090 xcheckf(ctx, err, "update requiretls for messages in queue")
2094// QueueTransportSet initiates delivery of a message from the queue and sets the transport
2095// to use for delivery.
2096func (Admin) QueueTransportSet(ctx context.Context, filter queue.Filter, transport string) (affected int) {
2097 n, err := queue.TransportSet(ctx, filter, transport)
2098 xcheckf(ctx, err, "changing transport for messages in queue")
2102// RetiredList returns messages retired from the queue (delivery could
2103// have succeeded or failed).
2104func (Admin) RetiredList(ctx context.Context, filter queue.RetiredFilter, sort queue.RetiredSort) []queue.MsgRetired {
2105 l, err := queue.RetiredList(ctx, filter, sort)
2106 xcheckf(ctx, err, "listing retired messages")
2110// HookQueueSize returns the number of webhooks still to be delivered.
2111func (Admin) HookQueueSize(ctx context.Context) int {
2112 n, err := queue.HookQueueSize(ctx)
2113 xcheckf(ctx, err, "get hook queue size")
2117// HookList lists webhooks still to be delivered.
2118func (Admin) HookList(ctx context.Context, filter queue.HookFilter, sort queue.HookSort) []queue.Hook {
2119 l, err := queue.HookList(ctx, filter, sort)
2120 xcheckf(ctx, err, "listing hook queue")
2124// HookNextAttemptSet sets a new time for next delivery attempt of matching
2125// hooks from the queue.
2126func (Admin) HookNextAttemptSet(ctx context.Context, filter queue.HookFilter, minutes int) (affected int) {
2127 n, err := queue.HookNextAttemptSet(ctx, filter, time.Now().Add(time.Duration(minutes)*time.Minute))
2128 xcheckf(ctx, err, "setting new next delivery attempt time for matching webhooks in queue")
2132// HookNextAttemptAdd adds a duration to the time of next delivery attempt of
2133// matching hooks from the queue.
2134func (Admin) HookNextAttemptAdd(ctx context.Context, filter queue.HookFilter, minutes int) (affected int) {
2135 n, err := queue.HookNextAttemptAdd(ctx, filter, time.Duration(minutes)*time.Minute)
2136 xcheckf(ctx, err, "adding duration to next delivery attempt for matching webhooks in queue")
2140// HookRetiredList lists retired webhooks.
2141func (Admin) HookRetiredList(ctx context.Context, filter queue.HookRetiredFilter, sort queue.HookRetiredSort) []queue.HookRetired {
2142 l, err := queue.HookRetiredList(ctx, filter, sort)
2143 xcheckf(ctx, err, "listing retired hooks")
2147// HookCancel prevents further delivery attempts of matching webhooks.
2148func (Admin) HookCancel(ctx context.Context, filter queue.HookFilter) (affected int) {
2149 log := pkglog.WithContext(ctx)
2150 n, err := queue.HookCancel(ctx, log, filter)
2151 xcheckf(ctx, err, "cancel hooks in queue")
2155// LogLevels returns the current log levels.
2156func (Admin) LogLevels(ctx context.Context) map[string]string {
2157 m := map[string]string{}
2158 for pkg, level := range mox.Conf.LogLevels() {
2159 s, ok := mlog.LevelStrings[level]
2168// LogLevelSet sets a log level for a package.
2169func (Admin) LogLevelSet(ctx context.Context, pkg string, levelStr string) {
2170 level, ok := mlog.Levels[levelStr]
2172 xcheckuserf(ctx, errors.New("unknown"), "lookup level")
2174 mox.Conf.LogLevelSet(pkglog.WithContext(ctx), pkg, level)
2177// LogLevelRemove removes a log level for a package, which cannot be the empty string.
2178func (Admin) LogLevelRemove(ctx context.Context, pkg string) {
2179 mox.Conf.LogLevelRemove(pkglog.WithContext(ctx), pkg)
2182// CheckUpdatesEnabled returns whether checking for updates is enabled.
2183func (Admin) CheckUpdatesEnabled(ctx context.Context) bool {
2184 return mox.Conf.Static.CheckUpdates
2187// WebserverConfig is the combination of WebDomainRedirects and WebHandlers
2188// from the domains.conf configuration file.
2189type WebserverConfig struct {
2190 WebDNSDomainRedirects [][2]dns.Domain // From server to frontend.
2191 WebDomainRedirects [][2]string // From frontend to server, it's not convenient to create dns.Domain in the frontend.
2192 WebHandlers []config.WebHandler
2195// WebserverConfig returns the current webserver config
2196func (Admin) WebserverConfig(ctx context.Context) (conf WebserverConfig) {
2197 conf = webserverConfig()
2198 conf.WebDomainRedirects = nil
2202func webserverConfig() WebserverConfig {
2203 conf := mox.Conf.DynamicConfig()
2204 r := conf.WebDNSDomainRedirects
2205 l := conf.WebHandlers
2207 x := make([][2]dns.Domain, 0, len(r))
2208 xs := make([][2]string, 0, len(r))
2209 for k, v := range r {
2210 x = append(x, [2]dns.Domain{k, v})
2211 xs = append(xs, [2]string{k.Name(), v.Name()})
2213 sort.Slice(x, func(i, j int) bool {
2214 return x[i][0].ASCII < x[j][0].ASCII
2216 sort.Slice(xs, func(i, j int) bool {
2217 return xs[i][0] < xs[j][0]
2219 return WebserverConfig{x, xs, l}
2222// WebserverConfigSave saves a new webserver config. If oldConf is not equal to
2223// the current config, an error is returned.
2224func (Admin) WebserverConfigSave(ctx context.Context, oldConf, newConf WebserverConfig) (savedConf WebserverConfig) {
2225 current := webserverConfig()
2226 webhandlersEqual := func() bool {
2227 if len(current.WebHandlers) != len(oldConf.WebHandlers) {
2230 for i, wh := range current.WebHandlers {
2231 if !wh.Equal(oldConf.WebHandlers[i]) {
2237 if !reflect.DeepEqual(oldConf.WebDNSDomainRedirects, current.WebDNSDomainRedirects) || !webhandlersEqual() {
2238 xcheckuserf(ctx, errors.New("config has changed"), "comparing old/current config")
2241 // Convert to map, check that there are no duplicates here. The canonicalized
2242 // dns.Domain are checked again for uniqueness when parsing the config before
2244 domainRedirects := map[string]string{}
2245 for _, x := range newConf.WebDomainRedirects {
2246 if _, ok := domainRedirects[x[0]]; ok {
2247 xcheckuserf(ctx, errors.New("already present"), "checking redirect %s", x[0])
2249 domainRedirects[x[0]] = x[1]
2252 err := mox.ConfigSave(ctx, func(conf *config.Dynamic) {
2253 conf.WebDomainRedirects = domainRedirects
2254 conf.WebHandlers = newConf.WebHandlers
2256 xcheckf(ctx, err, "saving webserver config")
2258 savedConf = webserverConfig()
2259 savedConf.WebDomainRedirects = nil
2263// Transports returns the configured transports, for sending email.
2264func (Admin) Transports(ctx context.Context) map[string]config.Transport {
2265 return mox.Conf.Static.Transports
2268// DMARCEvaluationStats returns a map of all domains with evaluations to a count of
2269// the evaluations and whether those evaluations will cause a report to be sent.
2270func (Admin) DMARCEvaluationStats(ctx context.Context) map[string]dmarcdb.EvaluationStat {
2271 stats, err := dmarcdb.EvaluationStats(ctx)
2272 xcheckf(ctx, err, "get evaluation stats")
2276// DMARCEvaluationsDomain returns all evaluations for aggregate reports for the
2277// domain, sorted from oldest to most recent.
2278func (Admin) DMARCEvaluationsDomain(ctx context.Context, domain string) (dns.Domain, []dmarcdb.Evaluation) {
2279 dom, err := dns.ParseDomain(domain)
2280 xcheckf(ctx, err, "parsing domain")
2282 evals, err := dmarcdb.EvaluationsDomain(ctx, dom)
2283 xcheckf(ctx, err, "get evaluations for domain")
2287// DMARCRemoveEvaluations removes evaluations for a domain.
2288func (Admin) DMARCRemoveEvaluations(ctx context.Context, domain string) {
2289 dom, err := dns.ParseDomain(domain)
2290 xcheckf(ctx, err, "parsing domain")
2292 err = dmarcdb.RemoveEvaluationsDomain(ctx, dom)
2293 xcheckf(ctx, err, "removing evaluations for domain")
2296// DMARCSuppressAdd adds a reporting address to the suppress list. Outgoing
2297// reports will be suppressed for a period.
2298func (Admin) DMARCSuppressAdd(ctx context.Context, reportingAddress string, until time.Time, comment string) {
2299 addr, err := smtp.ParseAddress(reportingAddress)
2300 xcheckuserf(ctx, err, "parsing reporting address")
2302 ba := dmarcdb.SuppressAddress{ReportingAddress: addr.String(), Until: until, Comment: comment}
2303 err = dmarcdb.SuppressAdd(ctx, &ba)
2304 xcheckf(ctx, err, "adding address to suppresslist")
2307// DMARCSuppressList returns all reporting addresses on the suppress list.
2308func (Admin) DMARCSuppressList(ctx context.Context) []dmarcdb.SuppressAddress {
2309 l, err := dmarcdb.SuppressList(ctx)
2310 xcheckf(ctx, err, "listing reporting addresses in suppresslist")
2314// DMARCSuppressRemove removes a reporting address record from the suppress list.
2315func (Admin) DMARCSuppressRemove(ctx context.Context, id int64) {
2316 err := dmarcdb.SuppressRemove(ctx, id)
2317 xcheckf(ctx, err, "removing reporting address from suppresslist")
2320// DMARCSuppressExtend updates the until field of a suppressed reporting address record.
2321func (Admin) DMARCSuppressExtend(ctx context.Context, id int64, until time.Time) {
2322 err := dmarcdb.SuppressUpdate(ctx, id, until)
2323 xcheckf(ctx, err, "updating reporting address in suppresslist")
2326// TLSRPTResults returns all TLSRPT results in the database.
2327func (Admin) TLSRPTResults(ctx context.Context) []tlsrptdb.TLSResult {
2328 results, err := tlsrptdb.Results(ctx)
2329 xcheckf(ctx, err, "get results")
2333// TLSRPTResultsPolicyDomain returns the TLS results for a domain.
2334func (Admin) TLSRPTResultsDomain(ctx context.Context, isRcptDom bool, policyDomain string) (dns.Domain, []tlsrptdb.TLSResult) {
2335 dom, err := dns.ParseDomain(policyDomain)
2336 xcheckf(ctx, err, "parsing domain")
2339 results, err := tlsrptdb.ResultsRecipientDomain(ctx, dom)
2340 xcheckf(ctx, err, "get result for recipient domain")
2343 results, err := tlsrptdb.ResultsPolicyDomain(ctx, dom)
2344 xcheckf(ctx, err, "get result for policy domain")
2348// LookupTLSRPTRecord looks up a TLSRPT record and returns the parsed form, original txt
2349// form from DNS, and error with the TLSRPT record as a string.
2350func (Admin) LookupTLSRPTRecord(ctx context.Context, domain string) (record *TLSRPTRecord, txt string, errstr string) {
2351 log := pkglog.WithContext(ctx)
2352 dom, err := dns.ParseDomain(domain)
2353 xcheckf(ctx, err, "parsing domain")
2355 resolver := dns.StrictResolver{Pkg: "webadmin", Log: log.Logger}
2356 r, txt, err := tlsrpt.Lookup(ctx, log.Logger, resolver, dom)
2357 if err != nil && (errors.Is(err, tlsrpt.ErrNoRecord) || errors.Is(err, tlsrpt.ErrMultipleRecords) || errors.Is(err, tlsrpt.ErrRecordSyntax) || errors.Is(err, tlsrpt.ErrDNS)) {
2358 errstr = err.Error()
2361 xcheckf(ctx, err, "fetching tlsrpt record")
2364 record = &TLSRPTRecord{Record: *r}
2367 return record, txt, errstr
2370// TLSRPTRemoveResults removes the TLS results for a domain for the given day. If
2371// day is empty, all results are removed.
2372func (Admin) TLSRPTRemoveResults(ctx context.Context, isRcptDom bool, domain string, day string) {
2373 dom, err := dns.ParseDomain(domain)
2374 xcheckf(ctx, err, "parsing domain")
2377 err = tlsrptdb.RemoveResultsRecipientDomain(ctx, dom, day)
2378 xcheckf(ctx, err, "removing tls results")
2380 err = tlsrptdb.RemoveResultsPolicyDomain(ctx, dom, day)
2381 xcheckf(ctx, err, "removing tls results")
2385// TLSRPTSuppressAdd adds a reporting address to the suppress list. Outgoing
2386// reports will be suppressed for a period.
2387func (Admin) TLSRPTSuppressAdd(ctx context.Context, reportingAddress string, until time.Time, comment string) {
2388 addr, err := smtp.ParseAddress(reportingAddress)
2389 xcheckuserf(ctx, err, "parsing reporting address")
2391 ba := tlsrptdb.SuppressAddress{ReportingAddress: addr.String(), Until: until, Comment: comment}
2392 err = tlsrptdb.SuppressAdd(ctx, &ba)
2393 xcheckf(ctx, err, "adding address to suppresslist")
2396// TLSRPTSuppressList returns all reporting addresses on the suppress list.
2397func (Admin) TLSRPTSuppressList(ctx context.Context) []tlsrptdb.SuppressAddress {
2398 l, err := tlsrptdb.SuppressList(ctx)
2399 xcheckf(ctx, err, "listing reporting addresses in suppresslist")
2403// TLSRPTSuppressRemove removes a reporting address record from the suppress list.
2404func (Admin) TLSRPTSuppressRemove(ctx context.Context, id int64) {
2405 err := tlsrptdb.SuppressRemove(ctx, id)
2406 xcheckf(ctx, err, "removing reporting address from suppresslist")
2409// TLSRPTSuppressExtend updates the until field of a suppressed reporting address record.
2410func (Admin) TLSRPTSuppressExtend(ctx context.Context, id int64, until time.Time) {
2411 err := tlsrptdb.SuppressUpdate(ctx, id, until)
2412 xcheckf(ctx, err, "updating reporting address in suppresslist")
2415// LookupCid turns an ID from a Received header into a cid as used in logging.
2416func (Admin) LookupCid(ctx context.Context, recvID string) (cid string) {
2417 v, err := mox.ReceivedToCid(recvID)
2418 xcheckf(ctx, err, "received id to cid")
2419 return fmt.Sprintf("%x", v)
2422// Config returns the dynamic config.
2423func (Admin) Config(ctx context.Context) config.Dynamic {
2424 return mox.Conf.DynamicConfig()
2427// AccountRoutesSave saves routes for an account.
2428func (Admin) AccountRoutesSave(ctx context.Context, accountName string, routes []config.Route) {
2429 err := mox.AccountSave(ctx, accountName, func(acc *config.Account) {
2432 xcheckf(ctx, err, "saving account routes")
2435// DomainRoutesSave saves routes for a domain.
2436func (Admin) DomainRoutesSave(ctx context.Context, domainName string, routes []config.Route) {
2437 err := mox.DomainSave(ctx, domainName, func(domain *config.Domain) error {
2438 domain.Routes = routes
2441 xcheckf(ctx, err, "saving domain routes")
2444// RoutesSave saves global routes.
2445func (Admin) RoutesSave(ctx context.Context, routes []config.Route) {
2446 err := mox.ConfigSave(ctx, func(config *config.Dynamic) {
2447 config.Routes = routes
2449 xcheckf(ctx, err, "saving global routes")
2452// DomainDescriptionSave saves the description for a domain.
2453func (Admin) DomainDescriptionSave(ctx context.Context, domainName, descr string) {
2454 err := mox.DomainSave(ctx, domainName, func(domain *config.Domain) error {
2455 domain.Description = descr
2458 xcheckf(ctx, err, "saving domain description")
2461// DomainClientSettingsDomainSave saves the client settings domain for a domain.
2462func (Admin) DomainClientSettingsDomainSave(ctx context.Context, domainName, clientSettingsDomain string) {
2463 err := mox.DomainSave(ctx, domainName, func(domain *config.Domain) error {
2464 domain.ClientSettingsDomain = clientSettingsDomain
2467 xcheckf(ctx, err, "saving client settings domain")
2470// DomainLocalpartConfigSave saves the localpart catchall and case-sensitive
2471// settings for a domain.
2472func (Admin) DomainLocalpartConfigSave(ctx context.Context, domainName, localpartCatchallSeparator string, localpartCaseSensitive bool) {
2473 err := mox.DomainSave(ctx, domainName, func(domain *config.Domain) error {
2474 domain.LocalpartCatchallSeparator = localpartCatchallSeparator
2475 domain.LocalpartCaseSensitive = localpartCaseSensitive
2478 xcheckf(ctx, err, "saving localpart settings for domain")
2481// DomainDMARCAddressSave saves the DMARC reporting address/processing
2482// configuration for a domain. If localpart is empty, processing reports is
2484func (Admin) DomainDMARCAddressSave(ctx context.Context, domainName, localpart, domain, account, mailbox string) {
2485 err := mox.DomainSave(ctx, domainName, func(d *config.Domain) error {
2486 if localpart == "" {
2489 d.DMARC = &config.DMARC{
2490 Localpart: localpart,
2498 xcheckf(ctx, err, "saving dmarc reporting address/settings for domain")
2501// DomainTLSRPTAddressSave saves the TLS reporting address/processing
2502// configuration for a domain. If localpart is empty, processing reports is
2504func (Admin) DomainTLSRPTAddressSave(ctx context.Context, domainName, localpart, domain, account, mailbox string) {
2505 err := mox.DomainSave(ctx, domainName, func(d *config.Domain) error {
2506 if localpart == "" {
2509 d.TLSRPT = &config.TLSRPT{
2510 Localpart: localpart,
2518 xcheckf(ctx, err, "saving tls reporting address/settings for domain")
2521// DomainMTASTSSave saves the MTASTS policy for a domain. If policyID is empty,
2522// no MTASTS policy is served.
2523func (Admin) DomainMTASTSSave(ctx context.Context, domainName, policyID string, mode mtasts.Mode, maxAge time.Duration, mx []string) {
2524 err := mox.DomainSave(ctx, domainName, func(d *config.Domain) error {
2528 d.MTASTS = &config.MTASTS{
2537 xcheckf(ctx, err, "saving mtasts policy for domain")
2540// DomainDKIMAdd adds a DKIM selector for a domain, generating a new private
2541// key. The selector is not enabled for signing.
2542func (Admin) DomainDKIMAdd(ctx context.Context, domainName, selector, algorithm, hash string, headerRelaxed, bodyRelaxed, seal bool, headers []string, lifetime time.Duration) {
2543 d, err := dns.ParseDomain(domainName)
2544 xcheckuserf(ctx, err, "parsing domain")
2545 s, err := dns.ParseDomain(selector)
2546 xcheckuserf(ctx, err, "parsing selector")
2547 err = mox.DKIMAdd(ctx, d, s, algorithm, hash, headerRelaxed, bodyRelaxed, seal, headers, lifetime)
2548 xcheckf(ctx, err, "adding dkim key")
2551// DomainDKIMRemove removes a DKIM selector for a domain.
2552func (Admin) DomainDKIMRemove(ctx context.Context, domainName, selector string) {
2553 d, err := dns.ParseDomain(domainName)
2554 xcheckuserf(ctx, err, "parsing domain")
2555 s, err := dns.ParseDomain(selector)
2556 xcheckuserf(ctx, err, "parsing selector")
2557 err = mox.DKIMRemove(ctx, d, s)
2558 xcheckf(ctx, err, "removing dkim key")
2561// DomainDKIMSave saves the settings of selectors, and which to enable for
2562// signing, for a domain. All currently configured selectors must be present,
2563// selectors cannot be added/removed with this function.
2564func (Admin) DomainDKIMSave(ctx context.Context, domainName string, selectors map[string]config.Selector, sign []string) {
2565 for _, s := range sign {
2566 if _, ok := selectors[s]; !ok {
2567 xcheckuserf(ctx, fmt.Errorf("cannot sign unknown selector %q", s), "checking selectors")
2571 err := mox.DomainSave(ctx, domainName, func(d *config.Domain) error {
2572 if len(selectors) != len(d.DKIM.Selectors) {
2573 xcheckuserf(ctx, fmt.Errorf("cannot add/remove dkim selectors with this function"), "checking selectors")
2575 for s := range selectors {
2576 if _, ok := d.DKIM.Selectors[s]; !ok {
2577 xcheckuserf(ctx, fmt.Errorf("unknown selector %q", s), "checking selectors")
2580 // At least the selectors are the same.
2582 // Build up new selectors.
2583 sels := map[string]config.Selector{}
2584 for name, nsel := range selectors {
2585 osel := d.DKIM.Selectors[name]
2586 xsel := config.Selector{
2588 Canonicalization: nsel.Canonicalization,
2589 DontSealHeaders: nsel.DontSealHeaders,
2590 Expiration: nsel.Expiration,
2592 PrivateKeyFile: osel.PrivateKeyFile,
2594 if !slices.Equal(osel.HeadersEffective, nsel.Headers) {
2595 xsel.Headers = nsel.Headers
2600 // Enable the new selector settings.
2601 d.DKIM = config.DKIM{
2607 xcheckf(ctx, err, "saving dkim selector for domain")
2610func xparseAddress(ctx context.Context, lp, domain string) smtp.Address {
2611 xlp, err := smtp.ParseLocalpart(lp)
2612 xcheckuserf(ctx, err, "parsing localpart")
2613 d, err := dns.ParseDomain(domain)
2614 xcheckuserf(ctx, err, "parsing domain")
2615 return smtp.NewAddress(xlp, d)
2618func (Admin) AliasAdd(ctx context.Context, aliaslp string, domainName string, alias config.Alias) {
2619 addr := xparseAddress(ctx, aliaslp, domainName)
2620 err := mox.AliasAdd(ctx, addr, alias)
2621 xcheckf(ctx, err, "adding alias")
2624func (Admin) AliasUpdate(ctx context.Context, aliaslp string, domainName string, postPublic, listMembers, allowMsgFrom bool) {
2625 addr := xparseAddress(ctx, aliaslp, domainName)
2626 alias := config.Alias{
2627 PostPublic: postPublic,
2628 ListMembers: listMembers,
2629 AllowMsgFrom: allowMsgFrom,
2631 err := mox.AliasUpdate(ctx, addr, alias)
2632 xcheckf(ctx, err, "saving alias")
2635func (Admin) AliasRemove(ctx context.Context, aliaslp string, domainName string) {
2636 addr := xparseAddress(ctx, aliaslp, domainName)
2637 err := mox.AliasRemove(ctx, addr)
2638 xcheckf(ctx, err, "removing alias")
2641func (Admin) AliasAddressesAdd(ctx context.Context, aliaslp string, domainName string, addresses []string) {
2642 addr := xparseAddress(ctx, aliaslp, domainName)
2643 err := mox.AliasAddressesAdd(ctx, addr, addresses)
2644 xcheckf(ctx, err, "adding address to alias")
2647func (Admin) AliasAddressesRemove(ctx context.Context, aliaslp string, domainName string, addresses []string) {
2648 addr := xparseAddress(ctx, aliaslp, domainName)
2649 err := mox.AliasAddressesRemove(ctx, addr, addresses)
2650 xcheckf(ctx, err, "removing address from alias")