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 // If caller tried saving a config that is invalid, or because of a bad request, cause a user error.
198 if errors.Is(err, mox.ErrConfig) || errors.Is(err, mox.ErrRequest) {
199 xcheckuserf(ctx, err, format, args...)
202 msg := fmt.Sprintf(format, args...)
203 errmsg := fmt.Sprintf("%s: %s", msg, err)
204 pkglog.WithContext(ctx).Errorx(msg, err)
205 code := "server:error"
206 if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
209 panic(&sherpa.Error{Code: code, Message: errmsg})
212func xcheckuserf(ctx context.Context, err error, format string, args ...any) {
216 msg := fmt.Sprintf(format, args...)
217 errmsg := fmt.Sprintf("%s: %s", msg, err)
218 pkglog.WithContext(ctx).Errorx(msg, err)
219 panic(&sherpa.Error{Code: "user:error", Message: errmsg})
222func xusererrorf(ctx context.Context, format string, args ...any) {
223 msg := fmt.Sprintf(format, args...)
224 pkglog.WithContext(ctx).Error(msg)
225 panic(&sherpa.Error{Code: "user:error", Message: msg})
228// LoginPrep returns a login token, and also sets it as cookie. Both must be
229// present in the call to Login.
230func (w Admin) LoginPrep(ctx context.Context) string {
231 log := pkglog.WithContext(ctx)
232 reqInfo := ctx.Value(requestInfoCtxKey).(requestInfo)
235 _, err := cryptorand.Read(data[:])
236 xcheckf(ctx, err, "generate token")
237 loginToken := base64.RawURLEncoding.EncodeToString(data[:])
239 webauth.LoginPrep(ctx, log, "webadmin", w.cookiePath, w.isForwarded, reqInfo.Response, reqInfo.Request, loginToken)
244// Login returns a session token for the credentials, or fails with error code
245// "user:badLogin". Call LoginPrep to get a loginToken.
246func (w Admin) Login(ctx context.Context, loginToken, password string) store.CSRFToken {
247 log := pkglog.WithContext(ctx)
248 reqInfo := ctx.Value(requestInfoCtxKey).(requestInfo)
250 csrfToken, err := webauth.Login(ctx, log, webauth.Admin, "webadmin", w.cookiePath, w.isForwarded, reqInfo.Response, reqInfo.Request, loginToken, "", password)
251 if _, ok := err.(*sherpa.Error); ok {
254 xcheckf(ctx, err, "login")
258// Logout invalidates the session token.
259func (w Admin) Logout(ctx context.Context) {
260 log := pkglog.WithContext(ctx)
261 reqInfo := ctx.Value(requestInfoCtxKey).(requestInfo)
263 err := webauth.Logout(ctx, log, webauth.Admin, "webadmin", w.cookiePath, w.isForwarded, reqInfo.Response, reqInfo.Request, "", reqInfo.SessionToken)
264 xcheckf(ctx, err, "logout")
270 Instructions []string
273type DNSSECResult struct {
277type IPRevCheckResult struct {
278 Hostname dns.Domain // This hostname, IPs must resolve back to this.
279 IPNames map[string][]string // IP to names.
289type MXCheckResult struct {
294type TLSCheckResult struct {
298type DANECheckResult struct {
302type SPFRecord struct {
306type SPFCheckResult struct {
308 DomainRecord *SPFRecord
310 HostRecord *SPFRecord
314type DKIMCheckResult struct {
319type DKIMRecord struct {
325type DMARCRecord struct {
329type DMARCCheckResult struct {
336type TLSRPTRecord struct {
340type TLSRPTCheckResult struct {
346type MTASTSRecord struct {
349type MTASTSCheckResult struct {
353 Policy *mtasts.Policy
357type SRVConfCheckResult struct {
358 SRVs map[string][]net.SRV // Service (e.g. "_imaps") to records.
362type AutoconfCheckResult struct {
363 ClientSettingsDomainIPs []string
368type AutodiscoverSRV struct {
373type AutodiscoverCheckResult struct {
374 Records []AutodiscoverSRV
378// CheckResult is the analysis of a domain, its actual configuration (DNS, TLS,
379// connectivity) and the mox configuration. It includes configuration instructions
380// (e.g. DNS records), and warnings and errors encountered.
381type CheckResult struct {
384 IPRev IPRevCheckResult
390 DMARC DMARCCheckResult
391 HostTLSRPT TLSRPTCheckResult
392 DomainTLSRPT TLSRPTCheckResult
393 MTASTS MTASTSCheckResult
394 SRVConf SRVConfCheckResult
395 Autoconf AutoconfCheckResult
396 Autodiscover AutodiscoverCheckResult
399// logPanic can be called with a defer from a goroutine to prevent the entire program from being shutdown in case of a panic.
400func logPanic(ctx context.Context) {
405 pkglog.WithContext(ctx).Error("recover from panic", slog.Any("panic", x))
407 metrics.PanicInc(metrics.Webadmin)
410// return IPs we may be listening on.
411func xlistenIPs(ctx context.Context, receiveOnly bool) []net.IP {
412 ips, err := mox.IPs(ctx, receiveOnly)
413 xcheckf(ctx, err, "listing ips")
417// return IPs from which we may be sending.
418func xsendingIPs(ctx context.Context) []net.IP {
419 ips, err := mox.IPs(ctx, false)
420 xcheckf(ctx, err, "listing ips")
424// CheckDomain checks the configuration for the domain, such as MX, SMTP STARTTLS,
425// SPF, DKIM, DMARC, TLSRPT, MTASTS, autoconfig, autodiscover.
426func (Admin) CheckDomain(ctx context.Context, domainName string) (r CheckResult) {
427 // todo future: should run these checks without a DNS cache so recent changes are picked up.
429 resolver := dns.StrictResolver{Pkg: "check", Log: pkglog.WithContext(ctx).Logger}
430 dialer := &net.Dialer{Timeout: 10 * time.Second}
431 nctx, cancel := context.WithTimeout(ctx, 30*time.Second)
433 return checkDomain(nctx, resolver, dialer, domainName)
436func unptr[T any](l []*T) []T {
440 r := make([]T, len(l))
441 for i, e := range l {
447func checkDomain(ctx context.Context, resolver dns.Resolver, dialer *net.Dialer, domainName string) (r CheckResult) {
448 log := pkglog.WithContext(ctx)
450 domain, err := dns.ParseDomain(domainName)
451 xcheckuserf(ctx, err, "parsing domain")
453 domConf, ok := mox.Conf.Domain(domain)
455 panic(&sherpa.Error{Code: "user:notFound", Message: "domain not found"})
458 listenIPs := xlistenIPs(ctx, true)
459 isListenIP := func(ip net.IP) bool {
460 for _, lip := range listenIPs {
468 addf := func(l *[]string, format string, args ...any) {
469 *l = append(*l, fmt.Sprintf(format, args...))
472 // Host must be an absolute dns name, ending with a dot.
473 lookupIPs := func(errors *[]string, host string) (ips []string, ourIPs, notOurIPs []net.IP, rerr error) {
474 addrs, _, err := resolver.LookupHost(ctx, host)
476 addf(errors, "Looking up %q: %s", host, err)
477 return nil, nil, nil, err
479 for _, addr := range addrs {
480 ip := net.ParseIP(addr)
482 addf(errors, "Bad IP %q", addr)
485 ips = append(ips, ip.String())
487 ourIPs = append(ourIPs, ip)
489 notOurIPs = append(notOurIPs, ip)
492 return ips, ourIPs, notOurIPs, nil
495 checkTLS := func(errors *[]string, host string, ips []string, port string) {
501 RootCAs: mox.Conf.Static.TLS.CertPool,
504 for _, ip := range ips {
505 conn, err := d.DialContext(ctx, "tcp", net.JoinHostPort(ip, port))
507 addf(errors, "TLS connection to hostname %q, IP %q: %s", host, ip, err)
514 // If at least one listener with SMTP enabled has unspecified NATed IPs, we'll skip
515 // some checks related to these IPs.
516 var isNAT, isUnspecifiedNAT bool
517 for _, l := range mox.Conf.Static.Listeners {
522 isUnspecifiedNAT = true
525 if len(l.NATIPs) > 0 {
530 var wg sync.WaitGroup
538 // Some DNSSEC-verifying resolvers return unauthentic data for ".", so we check "com".
539 _, result, err := resolver.LookupNS(ctx, "com.")
541 addf(&r.DNSSEC.Errors, "Looking up NS for DNS root (.) to check support in resolver for DNSSEC-verification: %s", err)
542 } else if !result.Authentic {
543 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.`)
545 _, result, _ := resolver.LookupMX(ctx, domain.ASCII+".")
546 if !result.Authentic {
547 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.`)
551 addf(&r.DNSSEC.Instructions, `Enable DNSSEC-signing of the DNS records of your domain (zone) at your DNS hosting provider.`)
553 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".
555cat <<EOF >/etc/unbound/unbound.conf.d/ede.conf
569 // For each mox.Conf.SpecifiedSMTPListenIPs and all NATIPs, and each IP for
570 // mox.Conf.HostnameDomain, check if they resolve back to the host name.
571 hostIPs := map[dns.Domain][]net.IP{}
572 ips, _, err := resolver.LookupIP(ctx, "ip", mox.Conf.Static.HostnameDomain.ASCII+".")
574 addf(&r.IPRev.Errors, "Looking up IPs for hostname: %s", err)
577 gatherMoreIPs := func(publicIPs []net.IP) {
579 for _, ip := range publicIPs {
580 for _, xip := range ips {
585 ips = append(ips, ip)
589 gatherMoreIPs(mox.Conf.Static.SpecifiedSMTPListenIPs)
591 for _, l := range mox.Conf.Static.Listeners {
596 for _, ip := range l.NATIPs {
597 natips = append(natips, net.ParseIP(ip))
599 gatherMoreIPs(natips)
601 hostIPs[mox.Conf.Static.HostnameDomain] = ips
603 iplist := func(ips []net.IP) string {
605 for _, ip := range ips {
606 ipstrs = append(ipstrs, ip.String())
608 return strings.Join(ipstrs, ", ")
611 r.IPRev.Hostname = mox.Conf.Static.HostnameDomain
612 r.IPRev.Instructions = []string{
613 fmt.Sprintf("Ensure IPs %s have reverse address %s.", iplist(ips), mox.Conf.Static.HostnameDomain.ASCII),
616 // If we have a socks transport, also check its host and IP.
617 for tname, t := range mox.Conf.Static.Transports {
619 hostIPs[t.Socks.Hostname] = append(hostIPs[t.Socks.Hostname], t.Socks.IPs...)
620 instr := fmt.Sprintf("For SOCKS transport %s, ensure IPs %s have reverse address %s.", tname, iplist(t.Socks.IPs), t.Socks.Hostname)
621 r.IPRev.Instructions = append(r.IPRev.Instructions, instr)
631 results := make(chan result)
633 for host, ips := range hostIPs {
634 for _, ip := range ips {
639 addrs, _, err := resolver.LookupAddr(ctx, s)
640 results <- result{host, s, addrs, err}
644 r.IPRev.IPNames = map[string][]string{}
645 for i := 0; i < n; i++ {
647 host, addrs, ip, err := lr.Host, lr.Addrs, lr.IP, lr.Err
649 addf(&r.IPRev.Errors, "Looking up reverse name for %s of %s: %v", ip, host, err)
653 addf(&r.IPRev.Errors, "Expected exactly 1 name for %s of %s, got %d (%v)", ip, host, len(addrs), addrs)
656 for i, a := range addrs {
657 a = strings.TrimRight(a, ".")
659 ad, err := dns.ParseDomain(a)
661 addf(&r.IPRev.Errors, "Parsing reverse name %q for %s: %v", a, ip, err)
668 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)
670 r.IPRev.IPNames[ip] = addrs
673 // Linux machines are often initially set up with a loopback IP for the hostname in
674 // /etc/hosts, presumably because it isn't known if their external IPs are static.
675 // For mail servers, they should certainly be static. The quickstart would also
676 // have warned about this, but could have been missed/ignored.
677 for _, ip := range ips {
679 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())
690 mxs, _, err := resolver.LookupMX(ctx, domain.ASCII+".")
692 addf(&r.MX.Errors, "Looking up MX records for %s: %s", domain, err)
694 r.MX.Records = make([]MX, len(mxs))
695 for i, mx := range mxs {
696 r.MX.Records[i] = MX{mx.Host, int(mx.Pref), nil}
698 if len(mxs) == 1 && mxs[0].Host == "." {
699 addf(&r.MX.Errors, `MX records consists of explicit null mx record (".") indicating that domain does not accept email.`)
702 for i, mx := range mxs {
703 ips, ourIPs, notOurIPs, err := lookupIPs(&r.MX.Errors, mx.Host)
705 addf(&r.MX.Errors, "Looking up IPs for mx host %q: %s", mx.Host, err)
707 r.MX.Records[i].IPs = ips
708 if isUnspecifiedNAT {
711 if len(ourIPs) == 0 {
712 addf(&r.MX.Errors, "None of the IPs that mx %q points to is ours: %v", mx.Host, notOurIPs)
713 } else if len(notOurIPs) > 0 {
714 addf(&r.MX.Errors, "Some of the IPs that mx %q points to are not ours: %v", mx.Host, notOurIPs)
718 r.MX.Instructions = []string{
719 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+"."),
723 // TLS, mostly checking certificate expiration and CA trust.
724 // 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.
730 // MTA-STS, autoconfig, autodiscover are checked in their sections.
732 // Dial a single MX host with given IP and perform STARTTLS handshake.
733 dialSMTPSTARTTLS := func(host, ip string) error {
734 conn, err := dialer.DialContext(ctx, "tcp", net.JoinHostPort(ip, "25"))
744 end := time.Now().Add(10 * time.Second)
745 cctx, cancel := context.WithTimeout(ctx, 10*time.Second)
747 err = conn.SetDeadline(end)
748 log.WithContext(ctx).Check(err, "setting deadline")
750 br := bufio.NewReader(conn)
751 _, err = br.ReadString('\n')
753 return fmt.Errorf("reading SMTP banner from remote: %s", err)
755 if _, err := fmt.Fprintf(conn, "EHLO moxtest\r\n"); err != nil {
756 return fmt.Errorf("writing SMTP EHLO to remote: %s", err)
759 line, err := br.ReadString('\n')
761 return fmt.Errorf("reading SMTP EHLO response from remote: %s", err)
763 if strings.HasPrefix(line, "250-") {
766 if strings.HasPrefix(line, "250 ") {
769 return fmt.Errorf("unexpected response to SMTP EHLO from remote: %q", strings.TrimSuffix(line, "\r\n"))
771 if _, err := fmt.Fprintf(conn, "STARTTLS\r\n"); err != nil {
772 return fmt.Errorf("writing SMTP STARTTLS to remote: %s", err)
774 line, err := br.ReadString('\n')
776 return fmt.Errorf("reading response to SMTP STARTTLS from remote: %s", err)
778 if !strings.HasPrefix(line, "220 ") {
779 return fmt.Errorf("SMTP STARTTLS response from remote not 220 OK: %q", strings.TrimSuffix(line, "\r\n"))
781 config := &tls.Config{
783 RootCAs: mox.Conf.Static.TLS.CertPool,
785 tlsconn := tls.Client(conn, config)
786 if err := tlsconn.HandshakeContext(cctx); err != nil {
787 return fmt.Errorf("TLS handshake after SMTP STARTTLS: %s", err)
795 checkSMTPSTARTTLS := func() {
796 // Initial errors are ignored, will already have been warned about by MX checks.
797 mxs, _, err := resolver.LookupMX(ctx, domain.ASCII+".")
801 if len(mxs) == 1 && mxs[0].Host == "." {
804 for _, mx := range mxs {
805 ips, _, _, err := lookupIPs(&r.MX.Errors, mx.Host)
810 for _, ip := range ips {
811 if err := dialSMTPSTARTTLS(mx.Host, ip); err != nil {
812 addf(&r.TLS.Errors, "SMTP connection with STARTTLS to MX hostname %q IP %s: %s", mx.Host, ip, err)
828 daneRecords := func(l config.Listener) map[string]struct{} {
832 records := map[string]struct{}{}
833 addRecord := func(privKey crypto.Signer) {
834 spkiBuf, err := x509.MarshalPKIXPublicKey(privKey.Public())
836 addf(&r.DANE.Errors, "marshal SubjectPublicKeyInfo for DANE record: %v", err)
839 sum := sha256.Sum256(spkiBuf)
841 Usage: adns.TLSAUsageDANEEE,
842 Selector: adns.TLSASelectorSPKI,
843 MatchType: adns.TLSAMatchTypeSHA256,
846 records[r.Record()] = struct{}{}
848 for _, privKey := range l.TLS.HostPrivateRSA2048Keys {
851 for _, privKey := range l.TLS.HostPrivateECDSAP256Keys {
857 expectedDANERecords := func(host string) map[string]struct{} {
858 for _, l := range mox.Conf.Static.Listeners {
859 if l.HostnameDomain.ASCII == host {
860 return daneRecords(l)
863 public := mox.Conf.Static.Listeners["public"]
864 if mox.Conf.Static.HostnameDomain.ASCII == host && public.HostnameDomain.ASCII == "" {
865 return daneRecords(public)
870 mxl, result, err := resolver.LookupMX(ctx, domain.ASCII+".")
872 addf(&r.DANE.Errors, "Looking up MX hosts to check for DANE records: %s", err)
874 if !result.Authentic {
875 addf(&r.DANE.Warnings, "DANE is inactive because MX records are not DNSSEC-signed.")
877 for _, mx := range mxl {
878 expect := expectedDANERecords(mx.Host)
880 tlsal, tlsaResult, err := resolver.LookupTLSA(ctx, 25, "tcp", mx.Host+".")
881 if dns.IsNotFound(err) {
883 addf(&r.DANE.Errors, "No DANE records for MX host %s, expected: %s.", mx.Host, strings.Join(maps.Keys(expect), "; "))
886 } else if err != nil {
887 addf(&r.DANE.Errors, "Looking up DANE records for MX host %s: %v", mx.Host, err)
889 } else if !tlsaResult.Authentic && len(tlsal) > 0 {
890 addf(&r.DANE.Errors, "DANE records exist for MX host %s, but are not DNSSEC-signed.", mx.Host)
893 extra := map[string]struct{}{}
894 for _, e := range tlsal {
896 if _, ok := expect[s]; ok {
899 extra[s] = struct{}{}
903 l := maps.Keys(expect)
905 addf(&r.DANE.Errors, "Missing DANE records of type TLSA for MX host _25._tcp.%s: %s", mx.Host, strings.Join(l, "; "))
908 l := maps.Keys(extra)
910 addf(&r.DANE.Errors, "Unexpected DANE records of type TLSA for MX host _25._tcp.%s: %s", mx.Host, strings.Join(l, "; "))
915 public := mox.Conf.Static.Listeners["public"]
916 pubDom := public.HostnameDomain
917 if pubDom.ASCII == "" {
918 pubDom = mox.Conf.Static.HostnameDomain
920 records := maps.Keys(daneRecords(public))
921 sort.Strings(records)
922 if len(records) > 0 {
923 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"
924 for _, r := range records {
925 instr += fmt.Sprintf("\t_25._tcp.%s. TLSA %s\n", pubDom.ASCII, r)
927 addf(&r.DANE.Instructions, instr)
932 // 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.
938 // Verify a domain with the configured IPs that do SMTP.
939 verifySPF := func(kind string, domain dns.Domain) (string, *SPFRecord, spf.Record) {
940 _, txt, record, _, err := spf.Lookup(ctx, log.Logger, resolver, domain)
942 addf(&r.SPF.Errors, "Looking up %s SPF record: %s", kind, err)
944 var xrecord *SPFRecord
946 xrecord = &SPFRecord{*record}
953 checkSPFIP := func(ip net.IP) {
958 spfr.Directives = append(spfr.Directives, spf.Directive{Mechanism: mechanism, IP: ip})
966 MailFromLocalpart: "postmaster",
967 MailFromDomain: domain,
968 HelloDomain: dns.IPDomain{Domain: domain},
969 LocalIP: net.ParseIP("127.0.0.1"),
970 LocalHostname: dns.Domain{ASCII: "localhost"},
972 status, mechanism, expl, _, err := spf.Evaluate(ctx, log.Logger, record, resolver, args)
974 addf(&r.SPF.Errors, "Evaluating IP %q against %s SPF record: %s", ip, kind, err)
975 } else if status != spf.StatusPass {
976 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)
980 for _, l := range mox.Conf.Static.Listeners {
981 if !l.SMTP.Enabled || l.IPsNATed {
985 if len(l.NATIPs) > 0 {
988 for _, ipstr := range ips {
989 ip := net.ParseIP(ipstr)
993 for _, t := range mox.Conf.Static.Transports {
995 for _, ip := range t.Socks.IPs {
1001 spfr.Directives = append(spfr.Directives, spf.Directive{Qualifier: "-", Mechanism: "all"})
1002 return txt, xrecord, spfr
1005 // Check SPF record for domain.
1006 var dspfr spf.Record
1007 r.SPF.DomainTXT, r.SPF.DomainRecord, dspfr = verifySPF("domain", domain)
1008 // todo: possibly check all hosts for MX records? assuming they are also sending mail servers.
1009 r.SPF.HostTXT, r.SPF.HostRecord, _ = verifySPF("host", mox.Conf.Static.HostnameDomain)
1011 dtxt, err := dspfr.Record()
1013 addf(&r.SPF.Errors, "Making SPF record for instructions: %s", err)
1015 domainspf := fmt.Sprintf("%s TXT %s", domain.ASCII+".", mox.TXTStrings(dtxt))
1018 hostspf := fmt.Sprintf(`%s TXT "v=spf1 a -all"`, mox.Conf.Static.HostnameDomain.ASCII+".")
1020 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)
1024 // 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.
1030 var missing []string
1031 var haveEd25519 bool
1032 for sel, selc := range domConf.DKIM.Selectors {
1033 if _, ok := selc.Key.(ed25519.PrivateKey); ok {
1037 _, record, txt, _, err := dkim.Lookup(ctx, log.Logger, resolver, selc.Domain, domain)
1039 missing = append(missing, sel)
1040 if errors.Is(err, dkim.ErrNoRecord) {
1041 addf(&r.DKIM.Errors, "No DKIM DNS record for selector %q.", sel)
1042 } else if errors.Is(err, dkim.ErrSyntax) {
1043 addf(&r.DKIM.Errors, "Parsing DKIM DNS record for selector %q: %s", sel, err)
1045 addf(&r.DKIM.Errors, "Fetching DKIM record for selector %q: %s", sel, err)
1049 r.DKIM.Records = append(r.DKIM.Records, DKIMRecord{sel, txt, record})
1050 pubKey := selc.Key.Public()
1052 switch k := pubKey.(type) {
1053 case *rsa.PublicKey:
1055 pk, err = x509.MarshalPKIXPublicKey(k)
1057 addf(&r.DKIM.Errors, "Marshal public key for %q to compare against DNS: %s", sel, err)
1060 case ed25519.PublicKey:
1063 addf(&r.DKIM.Errors, "Internal error: unknown public key type %T.", pubKey)
1067 if record != nil && !bytes.Equal(record.Pubkey, pk) {
1068 addf(&r.DKIM.Errors, "For selector %q, the public key in DKIM DNS TXT record does not match with configured private key.", sel)
1069 missing = append(missing, sel)
1073 if len(domConf.DKIM.Selectors) == 0 {
1074 addf(&r.DKIM.Errors, "No DKIM configuration, add a key to the configuration file, and instructions for DNS records will appear here.")
1075 } else if !haveEd25519 {
1076 addf(&r.DKIM.Warnings, "Consider adding an ed25519 key: the keys are smaller, the cryptography faster and more modern.")
1079 for _, sel := range missing {
1080 dkimr := dkim.Record{
1082 Hashes: []string{"sha256"},
1083 PublicKey: domConf.DKIM.Selectors[sel].Key.Public(),
1085 switch dkimr.PublicKey.(type) {
1086 case *rsa.PublicKey:
1087 case ed25519.PublicKey:
1088 dkimr.Key = "ed25519"
1090 addf(&r.DKIM.Errors, "Internal error: unknown public key type %T.", dkimr.PublicKey)
1092 txt, err := dkimr.Record()
1094 addf(&r.DKIM.Errors, "Making DKIM record for instructions: %s", err)
1097 instr += fmt.Sprintf("\n\t%s._domainkey TXT %s\n", sel, mox.TXTStrings(txt))
1100 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
1101 addf(&r.DKIM.Instructions, "%s", instr)
1111 _, dmarcDomain, record, txt, _, err := dmarc.Lookup(ctx, log.Logger, resolver, domain)
1113 addf(&r.DMARC.Errors, "Looking up DMARC record: %s", err)
1114 } else if record == nil {
1115 addf(&r.DMARC.Errors, "No DMARC record")
1117 r.DMARC.Domain = dmarcDomain.Name()
1120 r.DMARC.Record = &DMARCRecord{*record}
1122 if record != nil && record.Policy == "none" {
1123 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.")
1125 if record != nil && record.SubdomainPolicy == "none" {
1126 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.")
1128 if record != nil && len(record.AggregateReportAddresses) == 0 {
1129 addf(&r.DMARC.Warnings, "It is recommended you specify you would like aggregate reports about delivery success in the DMARC record, see instructions.")
1132 dmarcr := dmarc.DefaultRecord
1133 dmarcr.Policy = "reject"
1136 if domConf.DMARC != nil {
1137 // If the domain is in a different Organizational Domain, the receiving domain
1138 // needs a special DNS record to opt-in to receiving reports. We check for that
1141 orgDom := publicsuffix.Lookup(ctx, log.Logger, domain)
1142 destOrgDom := publicsuffix.Lookup(ctx, log.Logger, domConf.DMARC.DNSDomain)
1143 if orgDom != destOrgDom {
1144 accepts, status, _, _, _, err := dmarc.LookupExternalReportsAccepted(ctx, log.Logger, resolver, domain, domConf.DMARC.DNSDomain)
1145 if status != dmarc.StatusNone {
1146 addf(&r.DMARC.Errors, "Checking if external destination accepts reports: %s", err)
1147 } else if !accepts {
1148 addf(&r.DMARC.Errors, "External destination does not accept reports (%s)", err)
1150 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)
1155 Opaque: smtp.NewAddress(domConf.DMARC.ParsedLocalpart, domConf.DMARC.DNSDomain).Pack(false),
1157 uristr := uri.String()
1158 dmarcr.AggregateReportAddresses = []dmarc.URI{
1159 {Address: uristr, MaxSize: 10, Unit: "m"},
1164 for _, addr := range record.AggregateReportAddresses {
1165 if addr.Address == uristr {
1171 addf(&r.DMARC.Errors, "Configured DMARC reporting address is not present in record.")
1175 addf(&r.DMARC.Instructions, `Configure a DMARC destination in domain in config file.`)
1177 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()))
1178 addf(&r.DMARC.Instructions, instr)
1180 addf(&r.DMARC.Instructions, extInstr)
1184 checkTLSRPT := func(result *TLSRPTCheckResult, dom dns.Domain, address smtp.Address, isHost bool) {
1188 record, txt, err := tlsrpt.Lookup(ctx, log.Logger, resolver, dom)
1190 addf(&result.Errors, "Looking up TLSRPT record: %s", err)
1194 result.Record = &TLSRPTRecord{*record}
1197 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.`
1198 var zeroaddr smtp.Address
1199 if address != zeroaddr {
1200 // TLSRPT does not require validation of reporting addresses outside the domain.
1204 Opaque: address.Pack(false),
1206 rua := tlsrpt.RUA(uri.String())
1207 tlsrptr := &tlsrpt.Record{
1208 Version: "TLSRPTv1",
1209 RUAs: [][]tlsrpt.RUA{{rua}},
1211 instr += fmt.Sprintf(`
1213Ensure a DNS TXT record like the following exists:
1216`, mox.TXTStrings(tlsrptr.String()))
1221 for _, l := range record.RUAs {
1222 for _, e := range l {
1230 addf(&result.Errors, `Configured reporting address is not present in TLSRPT record.`)
1235 addf(&result.Errors, `Configure a host TLSRPT localpart in static mox.conf config file.`)
1237 addf(&result.Errors, `Configure a domain TLSRPT destination in domains.conf config file.`)
1239 addf(&result.Instructions, instr)
1244 var hostTLSRPTAddr smtp.Address
1245 if mox.Conf.Static.HostTLSRPT.Localpart != "" {
1246 hostTLSRPTAddr = smtp.NewAddress(mox.Conf.Static.HostTLSRPT.ParsedLocalpart, mox.Conf.Static.HostnameDomain)
1248 go checkTLSRPT(&r.HostTLSRPT, mox.Conf.Static.HostnameDomain, hostTLSRPTAddr, true)
1252 var domainTLSRPTAddr smtp.Address
1253 if domConf.TLSRPT != nil {
1254 domainTLSRPTAddr = smtp.NewAddress(domConf.TLSRPT.ParsedLocalpart, domain)
1256 go checkTLSRPT(&r.DomainTLSRPT, domain, domainTLSRPTAddr, false)
1264 record, txt, err := mtasts.LookupRecord(ctx, log.Logger, resolver, domain)
1266 addf(&r.MTASTS.Errors, "Looking up MTA-STS record: %s", err)
1270 r.MTASTS.Record = &MTASTSRecord{*record}
1273 policy, text, err := mtasts.FetchPolicy(ctx, log.Logger, domain)
1275 addf(&r.MTASTS.Errors, "Fetching MTA-STS policy: %s", err)
1276 } else if policy.Mode == mtasts.ModeNone {
1277 addf(&r.MTASTS.Warnings, "MTA-STS policy is present, but does not require TLS.")
1278 } else if policy.Mode == mtasts.ModeTesting {
1279 addf(&r.MTASTS.Warnings, "MTA-STS policy is in testing mode, do not forget to change to mode enforce after testing period.")
1281 r.MTASTS.PolicyText = text
1282 r.MTASTS.Policy = policy
1283 if policy != nil && policy.Mode != mtasts.ModeNone {
1284 if !policy.Matches(mox.Conf.Static.HostnameDomain) {
1285 addf(&r.MTASTS.Warnings, "Configured hostname is missing from policy MX list.")
1287 if policy.MaxAgeSeconds <= 24*3600 {
1288 addf(&r.MTASTS.Warnings, "Policy has a MaxAge of less than 1 day. For stable configurations, the recommended period is in weeks.")
1291 mxl, _, _ := resolver.LookupMX(ctx, domain.ASCII+".")
1292 // We do not check for errors, the MX check will complain about mx errors, we assume we will get the same error here.
1293 mxs := map[dns.Domain]struct{}{}
1294 for _, mx := range mxl {
1295 d, err := dns.ParseDomain(strings.TrimSuffix(mx.Host, "."))
1297 addf(&r.MTASTS.Warnings, "MX record %q is invalid: %s", mx.Host, err)
1302 for mx := range mxs {
1303 if !policy.Matches(mx) {
1304 addf(&r.MTASTS.Warnings, "MX record %q does not match MTA-STS policy MX list.", mx)
1307 for _, mx := range policy.MX {
1311 if _, ok := mxs[mx.Domain]; !ok {
1312 addf(&r.MTASTS.Warnings, "MX %q in MTA-STS policy is not in MX record.", mx)
1317 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.
1319After 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.
1321You 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.
1323You 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.
1325The _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.
1327When enabling MTA-STS, or updating a policy, always update the policy first (through a configuration change and reload/restart), and the DNS record second.
1329 addf(&r.MTASTS.Instructions, intro)
1331 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.`)
1333 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+".")
1334 addf(&r.MTASTS.Instructions, host)
1336 mtastsr := mtasts.Record{
1338 ID: time.Now().Format("20060102T150405"),
1340 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())
1341 addf(&r.MTASTS.Instructions, dns)
1350 type srvReq struct {
1358 // We'll assume if any submissions is configured, it is public. Same for imap. And
1359 // if not, that there is a plain option.
1360 var submissions, imaps bool
1361 for _, l := range mox.Conf.Static.Listeners {
1362 if l.TLS != nil && l.Submissions.Enabled {
1365 if l.TLS != nil && l.IMAPS.Enabled {
1369 srvhost := func(ok bool) string {
1371 return mox.Conf.Static.HostnameDomain.ASCII + "."
1375 var reqs = []srvReq{
1376 {name: "_submissions", port: 465, host: srvhost(submissions)},
1377 {name: "_submission", port: 587, host: srvhost(!submissions)},
1378 {name: "_imaps", port: 993, host: srvhost(imaps)},
1379 {name: "_imap", port: 143, host: srvhost(!imaps)},
1380 {name: "_pop3", port: 110, host: "."},
1381 {name: "_pop3s", port: 995, host: "."},
1383 var srvwg sync.WaitGroup
1384 srvwg.Add(len(reqs))
1385 for i := range reqs {
1388 _, reqs[i].srvs, _, reqs[i].err = resolver.LookupSRV(ctx, reqs[i].name[1:], "tcp", domain.ASCII+".")
1393 instr := "Ensure DNS records like the following exist:\n\n"
1394 r.SRVConf.SRVs = map[string][]net.SRV{}
1395 for _, req := range reqs {
1396 name := req.name + "_.tcp." + domain.ASCII
1397 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)
1398 r.SRVConf.SRVs[req.name] = unptr(req.srvs)
1400 addf(&r.SRVConf.Errors, "Looking up SRV record %q: %s", name, err)
1401 } else if len(req.srvs) == 0 {
1402 addf(&r.SRVConf.Errors, "Missing SRV record %q", name)
1403 } else if len(req.srvs) != 1 || req.srvs[0].Target != req.host || req.srvs[0].Port != req.port {
1404 addf(&r.SRVConf.Errors, "Unexpected SRV record(s) for %q", name)
1407 addf(&r.SRVConf.Instructions, instr)
1416 if domConf.ClientSettingsDomain != "" {
1417 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+".")
1419 ips, ourIPs, notOurIPs, err := lookupIPs(&r.Autoconf.Errors, domConf.ClientSettingsDNSDomain.ASCII+".")
1421 addf(&r.Autoconf.Errors, "Looking up client settings DNS CNAME: %s", err)
1423 r.Autoconf.ClientSettingsDomainIPs = ips
1424 if !isUnspecifiedNAT {
1425 if len(ourIPs) == 0 {
1426 addf(&r.Autoconf.Errors, "Client settings domain does not point to one of our IPs.")
1427 } else if len(notOurIPs) > 0 {
1428 addf(&r.Autoconf.Errors, "Client settings domain points to some IPs that are not ours: %v", notOurIPs)
1433 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+".")
1435 host := "autoconfig." + domain.ASCII + "."
1436 ips, ourIPs, notOurIPs, err := lookupIPs(&r.Autoconf.Errors, host)
1438 addf(&r.Autoconf.Errors, "Looking up autoconfig host: %s", err)
1442 r.Autoconf.IPs = ips
1443 if !isUnspecifiedNAT {
1444 if len(ourIPs) == 0 {
1445 addf(&r.Autoconf.Errors, "Autoconfig does not point to one of our IPs.")
1446 } else if len(notOurIPs) > 0 {
1447 addf(&r.Autoconf.Errors, "Autoconfig points to some IPs that are not ours: %v", notOurIPs)
1451 checkTLS(&r.Autoconf.Errors, "autoconfig."+domain.ASCII, ips, "443")
1460 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+".")
1462 _, srvs, _, err := resolver.LookupSRV(ctx, "autodiscover", "tcp", domain.ASCII+".")
1464 addf(&r.Autodiscover.Errors, "Looking up SRV record %q: %s", "autodiscover", err)
1468 for _, srv := range srvs {
1469 ips, ourIPs, notOurIPs, err := lookupIPs(&r.Autodiscover.Errors, srv.Target)
1471 addf(&r.Autodiscover.Errors, "Looking up target %q from SRV record: %s", srv.Target, err)
1474 if srv.Port != 443 {
1478 r.Autodiscover.Records = append(r.Autodiscover.Records, AutodiscoverSRV{*srv, ips})
1479 if !isUnspecifiedNAT {
1480 if len(ourIPs) == 0 {
1481 addf(&r.Autodiscover.Errors, "SRV target %q does not point to our IPs.", srv.Target)
1482 } else if len(notOurIPs) > 0 {
1483 addf(&r.Autodiscover.Errors, "SRV target %q points to some IPs that are not ours: %v", srv.Target, notOurIPs)
1487 checkTLS(&r.Autodiscover.Errors, strings.TrimSuffix(srv.Target, "."), ips, "443")
1490 addf(&r.Autodiscover.Errors, "No SRV record for port 443 for https.")
1498// Domains returns all configured domain names, in UTF-8 for IDNA domains.
1499func (Admin) Domains(ctx context.Context) []dns.Domain {
1501 for _, s := range mox.Conf.Domains() {
1502 d, _ := dns.ParseDomain(s)
1508// Domain returns the dns domain for a (potentially unicode as IDNA) domain name.
1509func (Admin) Domain(ctx context.Context, domain string) dns.Domain {
1510 d, err := dns.ParseDomain(domain)
1511 xcheckuserf(ctx, err, "parse domain")
1512 _, ok := mox.Conf.Domain(d)
1514 xcheckuserf(ctx, errors.New("no such domain"), "looking up domain")
1519// ParseDomain parses a domain, possibly an IDNA domain.
1520func (Admin) ParseDomain(ctx context.Context, domain string) dns.Domain {
1521 d, err := dns.ParseDomain(domain)
1522 xcheckuserf(ctx, err, "parse domain")
1526// DomainConfig returns the configuration for a domain.
1527func (Admin) DomainConfig(ctx context.Context, domain string) config.Domain {
1528 d, err := dns.ParseDomain(domain)
1529 xcheckuserf(ctx, err, "parse domain")
1530 conf, ok := mox.Conf.Domain(d)
1532 xcheckuserf(ctx, errors.New("no such domain"), "looking up domain")
1537// DomainLocalparts returns the encoded localparts and accounts configured in domain.
1538func (Admin) DomainLocalparts(ctx context.Context, domain string) (localpartAccounts map[string]string, localpartAliases map[string]config.Alias) {
1539 d, err := dns.ParseDomain(domain)
1540 xcheckuserf(ctx, err, "parsing domain")
1541 _, ok := mox.Conf.Domain(d)
1543 xcheckuserf(ctx, errors.New("no such domain"), "looking up domain")
1545 return mox.Conf.DomainLocalparts(d)
1548// Accounts returns the names of all configured accounts.
1549func (Admin) Accounts(ctx context.Context) []string {
1550 l := mox.Conf.Accounts()
1551 sort.Slice(l, func(i, j int) bool {
1557// Account returns the parsed configuration of an account.
1558func (Admin) Account(ctx context.Context, account string) (accountConfig config.Account, diskUsage int64) {
1559 log := pkglog.WithContext(ctx)
1561 acc, err := store.OpenAccount(log, account)
1562 if err != nil && errors.Is(err, store.ErrAccountUnknown) {
1563 xcheckuserf(ctx, err, "looking up account")
1565 xcheckf(ctx, err, "open account")
1568 log.Check(err, "closing account")
1571 var ac config.Account
1572 acc.WithRLock(func() {
1573 ac, _ = mox.Conf.Account(acc.Name)
1575 err := acc.DB.Read(ctx, func(tx *bstore.Tx) error {
1576 du := store.DiskUsage{ID: 1}
1578 diskUsage = du.MessageSize
1581 xcheckf(ctx, err, "get disk usage")
1584 return ac, diskUsage
1587// ConfigFiles returns the paths and contents of the static and dynamic configuration files.
1588func (Admin) ConfigFiles(ctx context.Context) (staticPath, dynamicPath, static, dynamic string) {
1589 buf0, err := os.ReadFile(mox.ConfigStaticPath)
1590 xcheckf(ctx, err, "read static config file")
1591 buf1, err := os.ReadFile(mox.ConfigDynamicPath)
1592 xcheckf(ctx, err, "read dynamic config file")
1593 return mox.ConfigStaticPath, mox.ConfigDynamicPath, string(buf0), string(buf1)
1596// MTASTSPolicies returns all mtasts policies from the cache.
1597func (Admin) MTASTSPolicies(ctx context.Context) (records []mtastsdb.PolicyRecord) {
1598 records, err := mtastsdb.PolicyRecords(ctx)
1599 xcheckf(ctx, err, "fetching mtasts policies from database")
1603// TLSReports returns TLS reports overlapping with period start/end, for the given
1604// policy domain (or all domains if empty). The reports are sorted first by period
1605// end (most recent first), then by policy domain.
1606func (Admin) TLSReports(ctx context.Context, start, end time.Time, policyDomain string) (reports []tlsrptdb.Record) {
1607 var polDom dns.Domain
1608 if policyDomain != "" {
1610 polDom, err = dns.ParseDomain(policyDomain)
1611 xcheckuserf(ctx, err, "parsing domain %q", policyDomain)
1614 records, err := tlsrptdb.RecordsPeriodDomain(ctx, start, end, polDom)
1615 xcheckf(ctx, err, "fetching tlsrpt report records from database")
1616 sort.Slice(records, func(i, j int) bool {
1617 iend := records[i].Report.DateRange.End
1618 jend := records[j].Report.DateRange.End
1620 return records[i].Domain < records[j].Domain
1622 return iend.After(jend)
1627// TLSReportID returns a single TLS report.
1628func (Admin) TLSReportID(ctx context.Context, domain string, reportID int64) tlsrptdb.Record {
1629 record, err := tlsrptdb.RecordID(ctx, reportID)
1630 if err == nil && record.Domain != domain {
1631 err = bstore.ErrAbsent
1633 if err == bstore.ErrAbsent {
1634 xcheckuserf(ctx, err, "fetching tls report from database")
1636 xcheckf(ctx, err, "fetching tls report from database")
1640// TLSRPTSummary presents TLS reporting statistics for a single domain
1642type TLSRPTSummary struct {
1643 PolicyDomain dns.Domain
1646 ResultTypeCounts map[tlsrpt.ResultType]int64
1649// TLSRPTSummaries returns a summary of received TLS reports overlapping with
1650// period start/end for one or all domains (when domain is empty).
1651// The returned summaries are ordered by domain name.
1652func (Admin) TLSRPTSummaries(ctx context.Context, start, end time.Time, policyDomain string) (domainSummaries []TLSRPTSummary) {
1653 var polDom dns.Domain
1654 if policyDomain != "" {
1656 polDom, err = dns.ParseDomain(policyDomain)
1657 xcheckuserf(ctx, err, "parsing policy domain")
1659 reports, err := tlsrptdb.RecordsPeriodDomain(ctx, start, end, polDom)
1660 xcheckf(ctx, err, "fetching tlsrpt reports from database")
1662 summaries := map[dns.Domain]TLSRPTSummary{}
1663 for _, r := range reports {
1664 dom, err := dns.ParseDomain(r.Domain)
1665 xcheckf(ctx, err, "parsing domain %q", r.Domain)
1667 sum := summaries[dom]
1668 sum.PolicyDomain = dom
1669 for _, result := range r.Report.Policies {
1670 sum.Success += result.Summary.TotalSuccessfulSessionCount
1671 sum.Failure += result.Summary.TotalFailureSessionCount
1672 for _, details := range result.FailureDetails {
1673 if sum.ResultTypeCounts == nil {
1674 sum.ResultTypeCounts = map[tlsrpt.ResultType]int64{}
1676 sum.ResultTypeCounts[details.ResultType] += details.FailedSessionCount
1679 summaries[dom] = sum
1681 sums := make([]TLSRPTSummary, 0, len(summaries))
1682 for _, sum := range summaries {
1683 sums = append(sums, sum)
1685 sort.Slice(sums, func(i, j int) bool {
1686 return sums[i].PolicyDomain.Name() < sums[j].PolicyDomain.Name()
1691// DMARCReports returns DMARC reports overlapping with period start/end, for the
1692// given domain (or all domains if empty). The reports are sorted first by period
1693// end (most recent first), then by domain.
1694func (Admin) DMARCReports(ctx context.Context, start, end time.Time, domain string) (reports []dmarcdb.DomainFeedback) {
1695 reports, err := dmarcdb.RecordsPeriodDomain(ctx, start, end, domain)
1696 xcheckf(ctx, err, "fetching dmarc aggregate reports from database")
1697 sort.Slice(reports, func(i, j int) bool {
1698 iend := reports[i].ReportMetadata.DateRange.End
1699 jend := reports[j].ReportMetadata.DateRange.End
1701 return reports[i].Domain < reports[j].Domain
1708// DMARCReportID returns a single DMARC report.
1709func (Admin) DMARCReportID(ctx context.Context, domain string, reportID int64) (report dmarcdb.DomainFeedback) {
1710 report, err := dmarcdb.RecordID(ctx, reportID)
1711 if err == nil && report.Domain != domain {
1712 err = bstore.ErrAbsent
1714 if err == bstore.ErrAbsent {
1715 xcheckuserf(ctx, err, "fetching dmarc aggregate report from database")
1717 xcheckf(ctx, err, "fetching dmarc aggregate report from database")
1721// DMARCSummary presents DMARC aggregate reporting statistics for a single domain
1723type DMARCSummary struct {
1727 DispositionQuarantine int
1728 DispositionReject int
1731 PolicyOverrides map[dmarcrpt.PolicyOverride]int
1734// DMARCSummaries returns a summary of received DMARC reports overlapping with
1735// period start/end for one or all domains (when domain is empty).
1736// The returned summaries are ordered by domain name.
1737func (Admin) DMARCSummaries(ctx context.Context, start, end time.Time, domain string) (domainSummaries []DMARCSummary) {
1738 reports, err := dmarcdb.RecordsPeriodDomain(ctx, start, end, domain)
1739 xcheckf(ctx, err, "fetching dmarc aggregate reports from database")
1740 summaries := map[string]DMARCSummary{}
1741 for _, r := range reports {
1742 sum := summaries[r.Domain]
1743 sum.Domain = r.Domain
1744 for _, record := range r.Records {
1745 n := record.Row.Count
1749 switch record.Row.PolicyEvaluated.Disposition {
1750 case dmarcrpt.DispositionNone:
1751 sum.DispositionNone += n
1752 case dmarcrpt.DispositionQuarantine:
1753 sum.DispositionQuarantine += n
1754 case dmarcrpt.DispositionReject:
1755 sum.DispositionReject += n
1758 if record.Row.PolicyEvaluated.DKIM == dmarcrpt.DMARCFail {
1761 if record.Row.PolicyEvaluated.SPF == dmarcrpt.DMARCFail {
1765 for _, reason := range record.Row.PolicyEvaluated.Reasons {
1766 if sum.PolicyOverrides == nil {
1767 sum.PolicyOverrides = map[dmarcrpt.PolicyOverride]int{}
1769 sum.PolicyOverrides[reason.Type] += n
1772 summaries[r.Domain] = sum
1774 sums := make([]DMARCSummary, 0, len(summaries))
1775 for _, sum := range summaries {
1776 sums = append(sums, sum)
1778 sort.Slice(sums, func(i, j int) bool {
1779 return sums[i].Domain < sums[j].Domain
1784// Reverse is the result of a reverse lookup.
1785type Reverse struct {
1788 // In the future, we can add a iprev-validated host name, and possibly the IPs of the host names.
1791// LookupIP does a reverse lookup of ip.
1792func (Admin) LookupIP(ctx context.Context, ip string) Reverse {
1793 resolver := dns.StrictResolver{Pkg: "webadmin", Log: pkglog.WithContext(ctx).Logger}
1794 names, _, err := resolver.LookupAddr(ctx, ip)
1795 xcheckuserf(ctx, err, "looking up ip")
1796 return Reverse{names}
1799// DNSBLStatus returns the IPs from which outgoing connections may be made and
1800// their current status in DNSBLs that are configured. The IPs are typically the
1801// configured listen IPs, or otherwise IPs on the machines network interfaces, with
1802// internal/private IPs removed.
1804// The returned value maps IPs to per DNSBL statuses, where "pass" means not listed and
1805// anything else is an error string, e.g. "fail: ..." or "temperror: ...".
1806func (Admin) DNSBLStatus(ctx context.Context) (results map[string]map[string]string, using, monitoring []dns.Domain) {
1807 log := mlog.New("webadmin", nil).WithContext(ctx)
1808 resolver := dns.StrictResolver{Pkg: "check", Log: log.Logger}
1809 return dnsblsStatus(ctx, log, resolver)
1812func dnsblsStatus(ctx context.Context, log mlog.Log, resolver dns.Resolver) (results map[string]map[string]string, using, monitoring []dns.Domain) {
1813 // todo: check health before using dnsbl?
1814 using = mox.Conf.Static.Listeners["public"].SMTP.DNSBLZones
1815 zones := append([]dns.Domain{}, using...)
1816 conf := mox.Conf.DynamicConfig()
1817 for _, zone := range conf.MonitorDNSBLZones {
1818 if !slices.Contains(zones, zone) {
1819 zones = append(zones, zone)
1820 monitoring = append(monitoring, zone)
1824 r := map[string]map[string]string{}
1825 for _, ip := range xsendingIPs(ctx) {
1826 if ip.IsLoopback() || ip.IsPrivate() {
1829 ipstr := ip.String()
1830 r[ipstr] = map[string]string{}
1831 for _, zone := range zones {
1832 status, expl, err := dnsbl.Lookup(ctx, log.Logger, resolver, zone, ip)
1833 result := string(status)
1835 result += ": " + err.Error()
1838 result += ": " + expl
1840 r[ipstr][zone.LogString()] = result
1843 return r, using, monitoring
1846func (Admin) MonitorDNSBLsSave(ctx context.Context, text string) {
1847 var zones []dns.Domain
1848 publicZones := mox.Conf.Static.Listeners["public"].SMTP.DNSBLZones
1849 for _, line := range strings.Split(text, "\n") {
1850 line = strings.TrimSpace(line)
1854 d, err := dns.ParseDomain(line)
1855 xcheckuserf(ctx, err, "parsing dnsbl zone %s", line)
1856 if slices.Contains(zones, d) {
1857 xusererrorf(ctx, "duplicate dnsbl zone %s", line)
1859 if slices.Contains(publicZones, d) {
1860 xusererrorf(ctx, "dnsbl zone %s already present in public listener", line)
1862 zones = append(zones, d)
1865 err := mox.ConfigSave(ctx, func(conf *config.Dynamic) {
1866 conf.MonitorDNSBLs = make([]string, len(zones))
1867 conf.MonitorDNSBLZones = nil
1868 for i, z := range zones {
1869 conf.MonitorDNSBLs[i] = z.Name()
1872 xcheckf(ctx, err, "saving monitoring dnsbl zones")
1875// DomainRecords returns lines describing DNS records that should exist for the
1876// configured domain.
1877func (Admin) DomainRecords(ctx context.Context, domain string) []string {
1878 log := pkglog.WithContext(ctx)
1879 return DomainRecords(ctx, log, domain)
1882// DomainRecords is the implementation of API function Admin.DomainRecords, taking
1884func DomainRecords(ctx context.Context, log mlog.Log, domain string) []string {
1885 d, err := dns.ParseDomain(domain)
1886 xcheckuserf(ctx, err, "parsing domain")
1887 dc, ok := mox.Conf.Domain(d)
1889 xcheckuserf(ctx, errors.New("unknown domain"), "lookup domain")
1891 resolver := dns.StrictResolver{Pkg: "webadmin", Log: pkglog.WithContext(ctx).Logger}
1892 _, result, err := resolver.LookupTXT(ctx, domain+".")
1893 if !dns.IsNotFound(err) {
1894 xcheckf(ctx, err, "looking up record to determine if dnssec is implemented")
1897 var certIssuerDomainName, acmeAccountURI string
1898 public := mox.Conf.Static.Listeners["public"]
1899 if public.TLS != nil && public.TLS.ACME != "" {
1900 acme, ok := mox.Conf.Static.ACME[public.TLS.ACME]
1901 if ok && acme.Manager.Manager.Client != nil {
1902 certIssuerDomainName = acme.IssuerDomainName
1903 acc, err := acme.Manager.Manager.Client.GetReg(ctx, "")
1904 log.Check(err, "get public acme account")
1906 acmeAccountURI = acc.URI
1911 records, err := mox.DomainRecords(dc, d, result.Authentic, certIssuerDomainName, acmeAccountURI)
1912 xcheckf(ctx, err, "dns records")
1916// DomainAdd adds a new domain and reloads the configuration.
1917func (Admin) DomainAdd(ctx context.Context, domain, accountName, localpart string) {
1918 d, err := dns.ParseDomain(domain)
1919 xcheckuserf(ctx, err, "parsing domain")
1921 err = mox.DomainAdd(ctx, d, accountName, smtp.Localpart(norm.NFC.String(localpart)))
1922 xcheckf(ctx, err, "adding domain")
1925// DomainRemove removes an existing domain and reloads the configuration.
1926func (Admin) DomainRemove(ctx context.Context, domain string) {
1927 d, err := dns.ParseDomain(domain)
1928 xcheckuserf(ctx, err, "parsing domain")
1930 err = mox.DomainRemove(ctx, d)
1931 xcheckf(ctx, err, "removing domain")
1934// AccountAdd adds existing a new account, with an initial email address, and
1935// reloads the configuration.
1936func (Admin) AccountAdd(ctx context.Context, accountName, address string) {
1937 err := mox.AccountAdd(ctx, accountName, address)
1938 xcheckf(ctx, err, "adding account")
1941// AccountRemove removes an existing account and reloads the configuration.
1942func (Admin) AccountRemove(ctx context.Context, accountName string) {
1943 err := mox.AccountRemove(ctx, accountName)
1944 xcheckf(ctx, err, "removing account")
1947// AddressAdd adds a new address to the account, which must already exist.
1948func (Admin) AddressAdd(ctx context.Context, address, accountName string) {
1949 err := mox.AddressAdd(ctx, address, accountName)
1950 xcheckf(ctx, err, "adding address")
1953// AddressRemove removes an existing address.
1954func (Admin) AddressRemove(ctx context.Context, address string) {
1955 err := mox.AddressRemove(ctx, address)
1956 xcheckf(ctx, err, "removing address")
1959// SetPassword saves a new password for an account, invalidating the previous password.
1960// Sessions are not interrupted, and will keep working. New login attempts must use the new password.
1961// Password must be at least 8 characters.
1962func (Admin) SetPassword(ctx context.Context, accountName, password string) {
1963 log := pkglog.WithContext(ctx)
1964 if len(password) < 8 {
1965 xusererrorf(ctx, "message must be at least 8 characters")
1967 acc, err := store.OpenAccount(log, accountName)
1968 xcheckf(ctx, err, "open account")
1971 log.WithContext(ctx).Check(err, "closing account")
1973 err = acc.SetPassword(log, password)
1974 xcheckf(ctx, err, "setting password")
1977// AccountSettingsSave set new settings for an account that only an admin can set.
1978func (Admin) AccountSettingsSave(ctx context.Context, accountName string, maxOutgoingMessagesPerDay, maxFirstTimeRecipientsPerDay int, maxMsgSize int64, firstTimeSenderDelay bool) {
1979 err := mox.AccountSave(ctx, accountName, func(acc *config.Account) {
1980 acc.MaxOutgoingMessagesPerDay = maxOutgoingMessagesPerDay
1981 acc.MaxFirstTimeRecipientsPerDay = maxFirstTimeRecipientsPerDay
1982 acc.QuotaMessageSize = maxMsgSize
1983 acc.NoFirstTimeSenderDelay = !firstTimeSenderDelay
1985 xcheckf(ctx, err, "saving account settings")
1988// ClientConfigsDomain returns configurations for email clients, IMAP and
1989// Submission (SMTP) for the domain.
1990func (Admin) ClientConfigsDomain(ctx context.Context, domain string) mox.ClientConfigs {
1991 d, err := dns.ParseDomain(domain)
1992 xcheckuserf(ctx, err, "parsing domain")
1994 cc, err := mox.ClientConfigsDomain(d)
1995 xcheckf(ctx, err, "client config for domain")
1999// QueueSize returns the number of messages currently in the outgoing queue.
2000func (Admin) QueueSize(ctx context.Context) int {
2001 n, err := queue.Count(ctx)
2002 xcheckf(ctx, err, "listing messages in queue")
2006// QueueHoldRuleList lists the hold rules.
2007func (Admin) QueueHoldRuleList(ctx context.Context) []queue.HoldRule {
2008 l, err := queue.HoldRuleList(ctx)
2009 xcheckf(ctx, err, "listing queue hold rules")
2013// QueueHoldRuleAdd adds a hold rule. Newly submitted and existing messages
2014// matching the hold rule will be marked "on hold".
2015func (Admin) QueueHoldRuleAdd(ctx context.Context, hr queue.HoldRule) queue.HoldRule {
2017 hr.SenderDomain, err = dns.ParseDomain(hr.SenderDomainStr)
2018 xcheckuserf(ctx, err, "parsing sender domain %q", hr.SenderDomainStr)
2019 hr.RecipientDomain, err = dns.ParseDomain(hr.RecipientDomainStr)
2020 xcheckuserf(ctx, err, "parsing recipient domain %q", hr.RecipientDomainStr)
2022 log := pkglog.WithContext(ctx)
2023 hr, err = queue.HoldRuleAdd(ctx, log, hr)
2024 xcheckf(ctx, err, "adding queue hold rule")
2028// QueueHoldRuleRemove removes a hold rule. The Hold field of messages in
2029// the queue are not changed.
2030func (Admin) QueueHoldRuleRemove(ctx context.Context, holdRuleID int64) {
2031 log := pkglog.WithContext(ctx)
2032 err := queue.HoldRuleRemove(ctx, log, holdRuleID)
2033 xcheckf(ctx, err, "removing queue hold rule")
2036// QueueList returns the messages currently in the outgoing queue.
2037func (Admin) QueueList(ctx context.Context, filter queue.Filter, sort queue.Sort) []queue.Msg {
2038 l, err := queue.List(ctx, filter, sort)
2039 xcheckf(ctx, err, "listing messages in queue")
2043// QueueNextAttemptSet sets a new time for next delivery attempt of matching
2044// messages from the queue.
2045func (Admin) QueueNextAttemptSet(ctx context.Context, filter queue.Filter, minutes int) (affected int) {
2046 n, err := queue.NextAttemptSet(ctx, filter, time.Now().Add(time.Duration(minutes)*time.Minute))
2047 xcheckf(ctx, err, "setting new next delivery attempt time for matching messages in queue")
2051// QueueNextAttemptAdd adds a duration to the time of next delivery attempt of
2052// matching messages from the queue.
2053func (Admin) QueueNextAttemptAdd(ctx context.Context, filter queue.Filter, minutes int) (affected int) {
2054 n, err := queue.NextAttemptAdd(ctx, filter, time.Duration(minutes)*time.Minute)
2055 xcheckf(ctx, err, "adding duration to next delivery attempt for matching messages in queue")
2059// QueueHoldSet sets the Hold field of matching messages in the queue.
2060func (Admin) QueueHoldSet(ctx context.Context, filter queue.Filter, onHold bool) (affected int) {
2061 n, err := queue.HoldSet(ctx, filter, onHold)
2062 xcheckf(ctx, err, "changing onhold for matching messages in queue")
2066// QueueFail fails delivery for matching messages, causing DSNs to be sent.
2067func (Admin) QueueFail(ctx context.Context, filter queue.Filter) (affected int) {
2068 log := pkglog.WithContext(ctx)
2069 n, err := queue.Fail(ctx, log, filter)
2070 xcheckf(ctx, err, "drop messages from queue")
2074// QueueDrop removes matching messages from the queue.
2075func (Admin) QueueDrop(ctx context.Context, filter queue.Filter) (affected int) {
2076 log := pkglog.WithContext(ctx)
2077 n, err := queue.Drop(ctx, log, filter)
2078 xcheckf(ctx, err, "drop messages from queue")
2082// QueueRequireTLSSet updates the requiretls field for matching messages in the
2083// queue, to be used for the next delivery.
2084func (Admin) QueueRequireTLSSet(ctx context.Context, filter queue.Filter, requireTLS *bool) (affected int) {
2085 n, err := queue.RequireTLSSet(ctx, filter, requireTLS)
2086 xcheckf(ctx, err, "update requiretls for messages in queue")
2090// QueueTransportSet initiates delivery of a message from the queue and sets the transport
2091// to use for delivery.
2092func (Admin) QueueTransportSet(ctx context.Context, filter queue.Filter, transport string) (affected int) {
2093 n, err := queue.TransportSet(ctx, filter, transport)
2094 xcheckf(ctx, err, "changing transport for messages in queue")
2098// RetiredList returns messages retired from the queue (delivery could
2099// have succeeded or failed).
2100func (Admin) RetiredList(ctx context.Context, filter queue.RetiredFilter, sort queue.RetiredSort) []queue.MsgRetired {
2101 l, err := queue.RetiredList(ctx, filter, sort)
2102 xcheckf(ctx, err, "listing retired messages")
2106// HookQueueSize returns the number of webhooks still to be delivered.
2107func (Admin) HookQueueSize(ctx context.Context) int {
2108 n, err := queue.HookQueueSize(ctx)
2109 xcheckf(ctx, err, "get hook queue size")
2113// HookList lists webhooks still to be delivered.
2114func (Admin) HookList(ctx context.Context, filter queue.HookFilter, sort queue.HookSort) []queue.Hook {
2115 l, err := queue.HookList(ctx, filter, sort)
2116 xcheckf(ctx, err, "listing hook queue")
2120// HookNextAttemptSet sets a new time for next delivery attempt of matching
2121// hooks from the queue.
2122func (Admin) HookNextAttemptSet(ctx context.Context, filter queue.HookFilter, minutes int) (affected int) {
2123 n, err := queue.HookNextAttemptSet(ctx, filter, time.Now().Add(time.Duration(minutes)*time.Minute))
2124 xcheckf(ctx, err, "setting new next delivery attempt time for matching webhooks in queue")
2128// HookNextAttemptAdd adds a duration to the time of next delivery attempt of
2129// matching hooks from the queue.
2130func (Admin) HookNextAttemptAdd(ctx context.Context, filter queue.HookFilter, minutes int) (affected int) {
2131 n, err := queue.HookNextAttemptAdd(ctx, filter, time.Duration(minutes)*time.Minute)
2132 xcheckf(ctx, err, "adding duration to next delivery attempt for matching webhooks in queue")
2136// HookRetiredList lists retired webhooks.
2137func (Admin) HookRetiredList(ctx context.Context, filter queue.HookRetiredFilter, sort queue.HookRetiredSort) []queue.HookRetired {
2138 l, err := queue.HookRetiredList(ctx, filter, sort)
2139 xcheckf(ctx, err, "listing retired hooks")
2143// HookCancel prevents further delivery attempts of matching webhooks.
2144func (Admin) HookCancel(ctx context.Context, filter queue.HookFilter) (affected int) {
2145 log := pkglog.WithContext(ctx)
2146 n, err := queue.HookCancel(ctx, log, filter)
2147 xcheckf(ctx, err, "cancel hooks in queue")
2151// LogLevels returns the current log levels.
2152func (Admin) LogLevels(ctx context.Context) map[string]string {
2153 m := map[string]string{}
2154 for pkg, level := range mox.Conf.LogLevels() {
2155 s, ok := mlog.LevelStrings[level]
2164// LogLevelSet sets a log level for a package.
2165func (Admin) LogLevelSet(ctx context.Context, pkg string, levelStr string) {
2166 level, ok := mlog.Levels[levelStr]
2168 xcheckuserf(ctx, errors.New("unknown"), "lookup level")
2170 mox.Conf.LogLevelSet(pkglog.WithContext(ctx), pkg, level)
2173// LogLevelRemove removes a log level for a package, which cannot be the empty string.
2174func (Admin) LogLevelRemove(ctx context.Context, pkg string) {
2175 mox.Conf.LogLevelRemove(pkglog.WithContext(ctx), pkg)
2178// CheckUpdatesEnabled returns whether checking for updates is enabled.
2179func (Admin) CheckUpdatesEnabled(ctx context.Context) bool {
2180 return mox.Conf.Static.CheckUpdates
2183// WebserverConfig is the combination of WebDomainRedirects and WebHandlers
2184// from the domains.conf configuration file.
2185type WebserverConfig struct {
2186 WebDNSDomainRedirects [][2]dns.Domain // From server to frontend.
2187 WebDomainRedirects [][2]string // From frontend to server, it's not convenient to create dns.Domain in the frontend.
2188 WebHandlers []config.WebHandler
2191// WebserverConfig returns the current webserver config
2192func (Admin) WebserverConfig(ctx context.Context) (conf WebserverConfig) {
2193 conf = webserverConfig()
2194 conf.WebDomainRedirects = nil
2198func webserverConfig() WebserverConfig {
2199 conf := mox.Conf.DynamicConfig()
2200 r := conf.WebDNSDomainRedirects
2201 l := conf.WebHandlers
2203 x := make([][2]dns.Domain, 0, len(r))
2204 xs := make([][2]string, 0, len(r))
2205 for k, v := range r {
2206 x = append(x, [2]dns.Domain{k, v})
2207 xs = append(xs, [2]string{k.Name(), v.Name()})
2209 sort.Slice(x, func(i, j int) bool {
2210 return x[i][0].ASCII < x[j][0].ASCII
2212 sort.Slice(xs, func(i, j int) bool {
2213 return xs[i][0] < xs[j][0]
2215 return WebserverConfig{x, xs, l}
2218// WebserverConfigSave saves a new webserver config. If oldConf is not equal to
2219// the current config, an error is returned.
2220func (Admin) WebserverConfigSave(ctx context.Context, oldConf, newConf WebserverConfig) (savedConf WebserverConfig) {
2221 current := webserverConfig()
2222 webhandlersEqual := func() bool {
2223 if len(current.WebHandlers) != len(oldConf.WebHandlers) {
2226 for i, wh := range current.WebHandlers {
2227 if !wh.Equal(oldConf.WebHandlers[i]) {
2233 if !reflect.DeepEqual(oldConf.WebDNSDomainRedirects, current.WebDNSDomainRedirects) || !webhandlersEqual() {
2234 xcheckuserf(ctx, errors.New("config has changed"), "comparing old/current config")
2237 // Convert to map, check that there are no duplicates here. The canonicalized
2238 // dns.Domain are checked again for uniqueness when parsing the config before
2240 domainRedirects := map[string]string{}
2241 for _, x := range newConf.WebDomainRedirects {
2242 if _, ok := domainRedirects[x[0]]; ok {
2243 xcheckuserf(ctx, errors.New("already present"), "checking redirect %s", x[0])
2245 domainRedirects[x[0]] = x[1]
2248 err := mox.ConfigSave(ctx, func(conf *config.Dynamic) {
2249 conf.WebDomainRedirects = domainRedirects
2250 conf.WebHandlers = newConf.WebHandlers
2252 xcheckf(ctx, err, "saving webserver config")
2254 savedConf = webserverConfig()
2255 savedConf.WebDomainRedirects = nil
2259// Transports returns the configured transports, for sending email.
2260func (Admin) Transports(ctx context.Context) map[string]config.Transport {
2261 return mox.Conf.Static.Transports
2264// DMARCEvaluationStats returns a map of all domains with evaluations to a count of
2265// the evaluations and whether those evaluations will cause a report to be sent.
2266func (Admin) DMARCEvaluationStats(ctx context.Context) map[string]dmarcdb.EvaluationStat {
2267 stats, err := dmarcdb.EvaluationStats(ctx)
2268 xcheckf(ctx, err, "get evaluation stats")
2272// DMARCEvaluationsDomain returns all evaluations for aggregate reports for the
2273// domain, sorted from oldest to most recent.
2274func (Admin) DMARCEvaluationsDomain(ctx context.Context, domain string) (dns.Domain, []dmarcdb.Evaluation) {
2275 dom, err := dns.ParseDomain(domain)
2276 xcheckf(ctx, err, "parsing domain")
2278 evals, err := dmarcdb.EvaluationsDomain(ctx, dom)
2279 xcheckf(ctx, err, "get evaluations for domain")
2283// DMARCRemoveEvaluations removes evaluations for a domain.
2284func (Admin) DMARCRemoveEvaluations(ctx context.Context, domain string) {
2285 dom, err := dns.ParseDomain(domain)
2286 xcheckf(ctx, err, "parsing domain")
2288 err = dmarcdb.RemoveEvaluationsDomain(ctx, dom)
2289 xcheckf(ctx, err, "removing evaluations for domain")
2292// DMARCSuppressAdd adds a reporting address to the suppress list. Outgoing
2293// reports will be suppressed for a period.
2294func (Admin) DMARCSuppressAdd(ctx context.Context, reportingAddress string, until time.Time, comment string) {
2295 addr, err := smtp.ParseAddress(reportingAddress)
2296 xcheckuserf(ctx, err, "parsing reporting address")
2298 ba := dmarcdb.SuppressAddress{ReportingAddress: addr.String(), Until: until, Comment: comment}
2299 err = dmarcdb.SuppressAdd(ctx, &ba)
2300 xcheckf(ctx, err, "adding address to suppresslist")
2303// DMARCSuppressList returns all reporting addresses on the suppress list.
2304func (Admin) DMARCSuppressList(ctx context.Context) []dmarcdb.SuppressAddress {
2305 l, err := dmarcdb.SuppressList(ctx)
2306 xcheckf(ctx, err, "listing reporting addresses in suppresslist")
2310// DMARCSuppressRemove removes a reporting address record from the suppress list.
2311func (Admin) DMARCSuppressRemove(ctx context.Context, id int64) {
2312 err := dmarcdb.SuppressRemove(ctx, id)
2313 xcheckf(ctx, err, "removing reporting address from suppresslist")
2316// DMARCSuppressExtend updates the until field of a suppressed reporting address record.
2317func (Admin) DMARCSuppressExtend(ctx context.Context, id int64, until time.Time) {
2318 err := dmarcdb.SuppressUpdate(ctx, id, until)
2319 xcheckf(ctx, err, "updating reporting address in suppresslist")
2322// TLSRPTResults returns all TLSRPT results in the database.
2323func (Admin) TLSRPTResults(ctx context.Context) []tlsrptdb.TLSResult {
2324 results, err := tlsrptdb.Results(ctx)
2325 xcheckf(ctx, err, "get results")
2329// TLSRPTResultsPolicyDomain returns the TLS results for a domain.
2330func (Admin) TLSRPTResultsDomain(ctx context.Context, isRcptDom bool, policyDomain string) (dns.Domain, []tlsrptdb.TLSResult) {
2331 dom, err := dns.ParseDomain(policyDomain)
2332 xcheckf(ctx, err, "parsing domain")
2335 results, err := tlsrptdb.ResultsRecipientDomain(ctx, dom)
2336 xcheckf(ctx, err, "get result for recipient domain")
2339 results, err := tlsrptdb.ResultsPolicyDomain(ctx, dom)
2340 xcheckf(ctx, err, "get result for policy domain")
2344// LookupTLSRPTRecord looks up a TLSRPT record and returns the parsed form, original txt
2345// form from DNS, and error with the TLSRPT record as a string.
2346func (Admin) LookupTLSRPTRecord(ctx context.Context, domain string) (record *TLSRPTRecord, txt string, errstr string) {
2347 log := pkglog.WithContext(ctx)
2348 dom, err := dns.ParseDomain(domain)
2349 xcheckf(ctx, err, "parsing domain")
2351 resolver := dns.StrictResolver{Pkg: "webadmin", Log: log.Logger}
2352 r, txt, err := tlsrpt.Lookup(ctx, log.Logger, resolver, dom)
2353 if err != nil && (errors.Is(err, tlsrpt.ErrNoRecord) || errors.Is(err, tlsrpt.ErrMultipleRecords) || errors.Is(err, tlsrpt.ErrRecordSyntax) || errors.Is(err, tlsrpt.ErrDNS)) {
2354 errstr = err.Error()
2357 xcheckf(ctx, err, "fetching tlsrpt record")
2360 record = &TLSRPTRecord{Record: *r}
2363 return record, txt, errstr
2366// TLSRPTRemoveResults removes the TLS results for a domain for the given day. If
2367// day is empty, all results are removed.
2368func (Admin) TLSRPTRemoveResults(ctx context.Context, isRcptDom bool, domain string, day string) {
2369 dom, err := dns.ParseDomain(domain)
2370 xcheckf(ctx, err, "parsing domain")
2373 err = tlsrptdb.RemoveResultsRecipientDomain(ctx, dom, day)
2374 xcheckf(ctx, err, "removing tls results")
2376 err = tlsrptdb.RemoveResultsPolicyDomain(ctx, dom, day)
2377 xcheckf(ctx, err, "removing tls results")
2381// TLSRPTSuppressAdd adds a reporting address to the suppress list. Outgoing
2382// reports will be suppressed for a period.
2383func (Admin) TLSRPTSuppressAdd(ctx context.Context, reportingAddress string, until time.Time, comment string) {
2384 addr, err := smtp.ParseAddress(reportingAddress)
2385 xcheckuserf(ctx, err, "parsing reporting address")
2387 ba := tlsrptdb.SuppressAddress{ReportingAddress: addr.String(), Until: until, Comment: comment}
2388 err = tlsrptdb.SuppressAdd(ctx, &ba)
2389 xcheckf(ctx, err, "adding address to suppresslist")
2392// TLSRPTSuppressList returns all reporting addresses on the suppress list.
2393func (Admin) TLSRPTSuppressList(ctx context.Context) []tlsrptdb.SuppressAddress {
2394 l, err := tlsrptdb.SuppressList(ctx)
2395 xcheckf(ctx, err, "listing reporting addresses in suppresslist")
2399// TLSRPTSuppressRemove removes a reporting address record from the suppress list.
2400func (Admin) TLSRPTSuppressRemove(ctx context.Context, id int64) {
2401 err := tlsrptdb.SuppressRemove(ctx, id)
2402 xcheckf(ctx, err, "removing reporting address from suppresslist")
2405// TLSRPTSuppressExtend updates the until field of a suppressed reporting address record.
2406func (Admin) TLSRPTSuppressExtend(ctx context.Context, id int64, until time.Time) {
2407 err := tlsrptdb.SuppressUpdate(ctx, id, until)
2408 xcheckf(ctx, err, "updating reporting address in suppresslist")
2411// LookupCid turns an ID from a Received header into a cid as used in logging.
2412func (Admin) LookupCid(ctx context.Context, recvID string) (cid string) {
2413 v, err := mox.ReceivedToCid(recvID)
2414 xcheckf(ctx, err, "received id to cid")
2415 return fmt.Sprintf("%x", v)
2418// Config returns the dynamic config.
2419func (Admin) Config(ctx context.Context) config.Dynamic {
2420 return mox.Conf.DynamicConfig()
2423// AccountRoutesSave saves routes for an account.
2424func (Admin) AccountRoutesSave(ctx context.Context, accountName string, routes []config.Route) {
2425 err := mox.AccountSave(ctx, accountName, func(acc *config.Account) {
2428 xcheckf(ctx, err, "saving account routes")
2431// DomainRoutesSave saves routes for a domain.
2432func (Admin) DomainRoutesSave(ctx context.Context, domainName string, routes []config.Route) {
2433 err := mox.DomainSave(ctx, domainName, func(domain *config.Domain) error {
2434 domain.Routes = routes
2437 xcheckf(ctx, err, "saving domain routes")
2440// RoutesSave saves global routes.
2441func (Admin) RoutesSave(ctx context.Context, routes []config.Route) {
2442 err := mox.ConfigSave(ctx, func(config *config.Dynamic) {
2443 config.Routes = routes
2445 xcheckf(ctx, err, "saving global routes")
2448// DomainDescriptionSave saves the description for a domain.
2449func (Admin) DomainDescriptionSave(ctx context.Context, domainName, descr string) {
2450 err := mox.DomainSave(ctx, domainName, func(domain *config.Domain) error {
2451 domain.Description = descr
2454 xcheckf(ctx, err, "saving domain description")
2457// DomainClientSettingsDomainSave saves the client settings domain for a domain.
2458func (Admin) DomainClientSettingsDomainSave(ctx context.Context, domainName, clientSettingsDomain string) {
2459 err := mox.DomainSave(ctx, domainName, func(domain *config.Domain) error {
2460 domain.ClientSettingsDomain = clientSettingsDomain
2463 xcheckf(ctx, err, "saving client settings domain")
2466// DomainLocalpartConfigSave saves the localpart catchall and case-sensitive
2467// settings for a domain.
2468func (Admin) DomainLocalpartConfigSave(ctx context.Context, domainName, localpartCatchallSeparator string, localpartCaseSensitive bool) {
2469 err := mox.DomainSave(ctx, domainName, func(domain *config.Domain) error {
2470 domain.LocalpartCatchallSeparator = localpartCatchallSeparator
2471 domain.LocalpartCaseSensitive = localpartCaseSensitive
2474 xcheckf(ctx, err, "saving localpart settings for domain")
2477// DomainDMARCAddressSave saves the DMARC reporting address/processing
2478// configuration for a domain. If localpart is empty, processing reports is
2480func (Admin) DomainDMARCAddressSave(ctx context.Context, domainName, localpart, domain, account, mailbox string) {
2481 err := mox.DomainSave(ctx, domainName, func(d *config.Domain) error {
2482 if localpart == "" {
2485 d.DMARC = &config.DMARC{
2486 Localpart: localpart,
2494 xcheckf(ctx, err, "saving dmarc reporting address/settings for domain")
2497// DomainTLSRPTAddressSave saves the TLS reporting address/processing
2498// configuration for a domain. If localpart is empty, processing reports is
2500func (Admin) DomainTLSRPTAddressSave(ctx context.Context, domainName, localpart, domain, account, mailbox string) {
2501 err := mox.DomainSave(ctx, domainName, func(d *config.Domain) error {
2502 if localpart == "" {
2505 d.TLSRPT = &config.TLSRPT{
2506 Localpart: localpart,
2514 xcheckf(ctx, err, "saving tls reporting address/settings for domain")
2517// DomainMTASTSSave saves the MTASTS policy for a domain. If policyID is empty,
2518// no MTASTS policy is served.
2519func (Admin) DomainMTASTSSave(ctx context.Context, domainName, policyID string, mode mtasts.Mode, maxAge time.Duration, mx []string) {
2520 err := mox.DomainSave(ctx, domainName, func(d *config.Domain) error {
2524 d.MTASTS = &config.MTASTS{
2533 xcheckf(ctx, err, "saving mtasts policy for domain")
2536// DomainDKIMAdd adds a DKIM selector for a domain, generating a new private
2537// key. The selector is not enabled for signing.
2538func (Admin) DomainDKIMAdd(ctx context.Context, domainName, selector, algorithm, hash string, headerRelaxed, bodyRelaxed, seal bool, headers []string, lifetime time.Duration) {
2539 d, err := dns.ParseDomain(domainName)
2540 xcheckuserf(ctx, err, "parsing domain")
2541 s, err := dns.ParseDomain(selector)
2542 xcheckuserf(ctx, err, "parsing selector")
2543 err = mox.DKIMAdd(ctx, d, s, algorithm, hash, headerRelaxed, bodyRelaxed, seal, headers, lifetime)
2544 xcheckf(ctx, err, "adding dkim key")
2547// DomainDKIMRemove removes a DKIM selector for a domain.
2548func (Admin) DomainDKIMRemove(ctx context.Context, domainName, selector string) {
2549 d, err := dns.ParseDomain(domainName)
2550 xcheckuserf(ctx, err, "parsing domain")
2551 s, err := dns.ParseDomain(selector)
2552 xcheckuserf(ctx, err, "parsing selector")
2553 err = mox.DKIMRemove(ctx, d, s)
2554 xcheckf(ctx, err, "removing dkim key")
2557// DomainDKIMSave saves the settings of selectors, and which to enable for
2558// signing, for a domain. All currently configured selectors must be present,
2559// selectors cannot be added/removed with this function.
2560func (Admin) DomainDKIMSave(ctx context.Context, domainName string, selectors map[string]config.Selector, sign []string) {
2561 for _, s := range sign {
2562 if _, ok := selectors[s]; !ok {
2563 xcheckuserf(ctx, fmt.Errorf("cannot sign unknown selector %q", s), "checking selectors")
2567 err := mox.DomainSave(ctx, domainName, func(d *config.Domain) error {
2568 if len(selectors) != len(d.DKIM.Selectors) {
2569 xcheckuserf(ctx, fmt.Errorf("cannot add/remove dkim selectors with this function"), "checking selectors")
2571 for s := range selectors {
2572 if _, ok := d.DKIM.Selectors[s]; !ok {
2573 xcheckuserf(ctx, fmt.Errorf("unknown selector %q", s), "checking selectors")
2576 // At least the selectors are the same.
2578 // Build up new selectors.
2579 sels := map[string]config.Selector{}
2580 for name, nsel := range selectors {
2581 osel := d.DKIM.Selectors[name]
2582 xsel := config.Selector{
2584 Canonicalization: nsel.Canonicalization,
2585 DontSealHeaders: nsel.DontSealHeaders,
2586 Expiration: nsel.Expiration,
2588 PrivateKeyFile: osel.PrivateKeyFile,
2590 if !slices.Equal(osel.HeadersEffective, nsel.Headers) {
2591 xsel.Headers = nsel.Headers
2596 // Enable the new selector settings.
2597 d.DKIM = config.DKIM{
2603 xcheckf(ctx, err, "saving dkim selector for domain")
2606func xparseAddress(ctx context.Context, lp, domain string) smtp.Address {
2607 xlp, err := smtp.ParseLocalpart(lp)
2608 xcheckuserf(ctx, err, "parsing localpart")
2609 d, err := dns.ParseDomain(domain)
2610 xcheckuserf(ctx, err, "parsing domain")
2611 return smtp.NewAddress(xlp, d)
2614func (Admin) AliasAdd(ctx context.Context, aliaslp string, domainName string, alias config.Alias) {
2615 addr := xparseAddress(ctx, aliaslp, domainName)
2616 err := mox.AliasAdd(ctx, addr, alias)
2617 xcheckf(ctx, err, "adding alias")
2620func (Admin) AliasUpdate(ctx context.Context, aliaslp string, domainName string, postPublic, listMembers, allowMsgFrom bool) {
2621 addr := xparseAddress(ctx, aliaslp, domainName)
2622 alias := config.Alias{
2623 PostPublic: postPublic,
2624 ListMembers: listMembers,
2625 AllowMsgFrom: allowMsgFrom,
2627 err := mox.AliasUpdate(ctx, addr, alias)
2628 xcheckf(ctx, err, "saving alias")
2631func (Admin) AliasRemove(ctx context.Context, aliaslp string, domainName string) {
2632 addr := xparseAddress(ctx, aliaslp, domainName)
2633 err := mox.AliasRemove(ctx, addr)
2634 xcheckf(ctx, err, "removing alias")
2637func (Admin) AliasAddressesAdd(ctx context.Context, aliaslp string, domainName string, addresses []string) {
2638 addr := xparseAddress(ctx, aliaslp, domainName)
2639 err := mox.AliasAddressesAdd(ctx, addr, addresses)
2640 xcheckf(ctx, err, "adding address to alias")
2643func (Admin) AliasAddressesRemove(ctx context.Context, aliaslp string, domainName string, addresses []string) {
2644 addr := xparseAddress(ctx, aliaslp, domainName)
2645 err := mox.AliasAddressesRemove(ctx, addr, addresses)
2646 xcheckf(ctx, err, "removing address from alias")