1// Package webadmin is a web app for the mox administrator for viewing and changing
2// the configuration, like creating/removing accounts, viewing DMARC and TLS
3// reports, check DNS records for a domain, change the webserver configuration,
13 cryptorand "crypto/rand"
40 "golang.org/x/text/unicode/norm"
42 "github.com/mjl-/adns"
44 "github.com/mjl-/bstore"
45 "github.com/mjl-/sherpa"
46 "github.com/mjl-/sherpadoc"
47 "github.com/mjl-/sherpaprom"
49 "github.com/mjl-/mox/admin"
50 "github.com/mjl-/mox/config"
51 "github.com/mjl-/mox/dkim"
52 "github.com/mjl-/mox/dmarc"
53 "github.com/mjl-/mox/dmarcdb"
54 "github.com/mjl-/mox/dmarcrpt"
55 "github.com/mjl-/mox/dns"
56 "github.com/mjl-/mox/dnsbl"
57 "github.com/mjl-/mox/metrics"
58 "github.com/mjl-/mox/mlog"
59 mox "github.com/mjl-/mox/mox-"
60 "github.com/mjl-/mox/moxvar"
61 "github.com/mjl-/mox/mtasts"
62 "github.com/mjl-/mox/mtastsdb"
63 "github.com/mjl-/mox/publicsuffix"
64 "github.com/mjl-/mox/queue"
65 "github.com/mjl-/mox/smtp"
66 "github.com/mjl-/mox/spf"
67 "github.com/mjl-/mox/store"
68 "github.com/mjl-/mox/tlsrpt"
69 "github.com/mjl-/mox/tlsrptdb"
70 "github.com/mjl-/mox/webauth"
73var pkglog = mlog.New("webadmin", nil)
76var adminapiJSON []byte
84var webadminFile = &mox.WebappFile{
87 HTMLPath: filepath.FromSlash("webadmin/admin.html"),
88 JSPath: filepath.FromSlash("webadmin/admin.js"),
89 CustomStem: "webadmin",
92var adminDoc = mustParseAPI("admin", adminapiJSON)
94func mustParseAPI(api string, buf []byte) (doc sherpadoc.Section) {
95 err := json.Unmarshal(buf, &doc)
97 pkglog.Fatalx("parsing webadmin api docs", err, slog.String("api", api))
102var sherpaHandlerOpts *sherpa.HandlerOpts
104func makeSherpaHandler(cookiePath string, isForwarded bool) (http.Handler, error) {
105 return sherpa.NewHandler("/api/", moxvar.Version, Admin{cookiePath, isForwarded}, &adminDoc, sherpaHandlerOpts)
109 collector, err := sherpaprom.NewCollector("moxadmin", nil)
111 pkglog.Fatalx("creating sherpa prometheus collector", err)
114 sherpaHandlerOpts = &sherpa.HandlerOpts{Collector: collector, AdjustFunctionNames: "none", NoCORS: true}
116 _, err = makeSherpaHandler("", false)
118 pkglog.Fatalx("sherpa handler", err)
121 mox.NewWebadminHandler = func(basePath string, isForwarded bool) http.Handler {
122 return http.HandlerFunc(Handler(basePath, isForwarded))
126// Handler returns a handler for the webadmin endpoints, customized for the
128func Handler(cookiePath string, isForwarded bool) func(w http.ResponseWriter, r *http.Request) {
129 sh, err := makeSherpaHandler(cookiePath, isForwarded)
130 return func(w http.ResponseWriter, r *http.Request) {
132 http.Error(w, "500 - internal server error - cannot handle requests", http.StatusInternalServerError)
135 handle(sh, isForwarded, w, r)
139// Admin exports web API functions for the admin web interface. All its methods are
140// exported under api/. Function calls require valid HTTP Authentication
141// credentials of a user.
143 cookiePath string // From listener, for setting authentication cookies.
144 isForwarded bool // From listener, whether we look at X-Forwarded-* headers.
149var requestInfoCtxKey ctxKey = "requestInfo"
151type requestInfo struct {
152 SessionToken store.SessionToken
153 Response http.ResponseWriter
154 Request *http.Request // For Proto and TLS connection state during message submit.
157func handle(apiHandler http.Handler, isForwarded bool, w http.ResponseWriter, r *http.Request) {
158 ctx := context.WithValue(r.Context(), mlog.CidKey, mox.Cid())
159 log := pkglog.WithContext(ctx).With(slog.String("adminauth", ""))
161 // HTML/JS can be retrieved without authentication.
162 if r.URL.Path == "/" {
165 webadminFile.Serve(ctx, log, w, r)
167 http.Error(w, "405 - method not allowed - use get", http.StatusMethodNotAllowed)
170 } else if r.URL.Path == "/licenses.txt" {
173 w.Header().Set("Content-Type", "text/plain; charset=utf-8")
176 http.Error(w, "405 - method not allowed - use get", http.StatusMethodNotAllowed)
181 isAPI := strings.HasPrefix(r.URL.Path, "/api/")
182 // Only allow POST for calls, they will not work cross-domain without CORS.
183 if isAPI && r.URL.Path != "/api/" && r.Method != "POST" {
184 http.Error(w, "405 - method not allowed - use post", http.StatusMethodNotAllowed)
188 // All other URLs, except the login endpoint require some authentication.
189 var sessionToken store.SessionToken
190 if r.URL.Path != "/api/LoginPrep" && r.URL.Path != "/api/Login" {
192 _, sessionToken, _, ok = webauth.Check(ctx, log, webauth.Admin, "webadmin", isForwarded, w, r, isAPI, isAPI, false)
194 // Response has been written already.
200 reqInfo := requestInfo{sessionToken, w, r}
201 ctx = context.WithValue(ctx, requestInfoCtxKey, reqInfo)
202 apiHandler.ServeHTTP(w, r.WithContext(ctx))
209func xcheckf(ctx context.Context, err error, format string, args ...any) {
213 // If caller tried saving a config that is invalid, or because of a bad request, cause a user error.
214 if errors.Is(err, mox.ErrConfig) || errors.Is(err, admin.ErrRequest) {
215 xcheckuserf(ctx, err, format, args...)
218 msg := fmt.Sprintf(format, args...)
219 errmsg := fmt.Sprintf("%s: %s", msg, err)
220 pkglog.WithContext(ctx).Errorx(msg, err)
221 code := "server:error"
222 if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
225 panic(&sherpa.Error{Code: code, Message: errmsg})
228func xcheckuserf(ctx context.Context, err error, format string, args ...any) {
232 msg := fmt.Sprintf(format, args...)
233 errmsg := fmt.Sprintf("%s: %s", msg, err)
234 pkglog.WithContext(ctx).Errorx(msg, err)
235 panic(&sherpa.Error{Code: "user:error", Message: errmsg})
238func xusererrorf(ctx context.Context, format string, args ...any) {
239 msg := fmt.Sprintf(format, args...)
240 pkglog.WithContext(ctx).Error(msg)
241 panic(&sherpa.Error{Code: "user:error", Message: msg})
244// LoginPrep returns a login token, and also sets it as cookie. Both must be
245// present in the call to Login.
246func (w Admin) LoginPrep(ctx context.Context) string {
247 log := pkglog.WithContext(ctx)
248 reqInfo := ctx.Value(requestInfoCtxKey).(requestInfo)
251 _, err := cryptorand.Read(data[:])
252 xcheckf(ctx, err, "generate token")
253 loginToken := base64.RawURLEncoding.EncodeToString(data[:])
255 webauth.LoginPrep(ctx, log, "webadmin", w.cookiePath, w.isForwarded, reqInfo.Response, reqInfo.Request, loginToken)
260// Login returns a session token for the credentials, or fails with error code
261// "user:badLogin". Call LoginPrep to get a loginToken.
262func (w Admin) Login(ctx context.Context, loginToken, password string) store.CSRFToken {
263 log := pkglog.WithContext(ctx)
264 reqInfo := ctx.Value(requestInfoCtxKey).(requestInfo)
266 csrfToken, err := webauth.Login(ctx, log, webauth.Admin, "webadmin", w.cookiePath, w.isForwarded, reqInfo.Response, reqInfo.Request, loginToken, "", password)
267 if _, ok := err.(*sherpa.Error); ok {
270 xcheckf(ctx, err, "login")
274// Logout invalidates the session token.
275func (w Admin) Logout(ctx context.Context) {
276 log := pkglog.WithContext(ctx)
277 reqInfo := ctx.Value(requestInfoCtxKey).(requestInfo)
279 err := webauth.Logout(ctx, log, webauth.Admin, "webadmin", w.cookiePath, w.isForwarded, reqInfo.Response, reqInfo.Request, "", reqInfo.SessionToken)
280 xcheckf(ctx, err, "logout")
283// Version returns the version, goos and goarch.
284func (w Admin) Version(ctx context.Context) (version, goos, goarch string) {
285 return moxvar.Version, runtime.GOOS, runtime.GOARCH
291 Instructions []string
294type DNSSECResult struct {
298type IPRevCheckResult struct {
299 Hostname dns.Domain // This hostname, IPs must resolve back to this.
300 IPNames map[string][]string // IP to names.
310type MXCheckResult struct {
315type TLSCheckResult struct {
319type DANECheckResult struct {
323type SPFRecord struct {
327type SPFCheckResult struct {
329 DomainRecord *SPFRecord
331 HostRecord *SPFRecord
335type DKIMCheckResult struct {
340type DKIMRecord struct {
346type DMARCRecord struct {
350type DMARCCheckResult struct {
357type TLSRPTRecord struct {
361type TLSRPTCheckResult struct {
367type MTASTSRecord struct {
370type MTASTSCheckResult struct {
374 Policy *mtasts.Policy
378type SRVConfCheckResult struct {
379 SRVs map[string][]net.SRV // Service (e.g. "_imaps") to records.
383type AutoconfCheckResult struct {
384 ClientSettingsDomainIPs []string
389type AutodiscoverSRV struct {
394type AutodiscoverCheckResult struct {
395 Records []AutodiscoverSRV
399// CheckResult is the analysis of a domain, its actual configuration (DNS, TLS,
400// connectivity) and the mox configuration. It includes configuration instructions
401// (e.g. DNS records), and warnings and errors encountered.
402type CheckResult struct {
405 IPRev IPRevCheckResult
411 DMARC DMARCCheckResult
412 HostTLSRPT TLSRPTCheckResult
413 DomainTLSRPT TLSRPTCheckResult
414 MTASTS MTASTSCheckResult
415 SRVConf SRVConfCheckResult
416 Autoconf AutoconfCheckResult
417 Autodiscover AutodiscoverCheckResult
420// logPanic can be called with a defer from a goroutine to prevent the entire program from being shutdown in case of a panic.
421func logPanic(ctx context.Context) {
426 pkglog.WithContext(ctx).Error("recover from panic", slog.Any("panic", x))
428 metrics.PanicInc(metrics.Webadmin)
431// return IPs we may be listening on.
432func xlistenIPs(ctx context.Context, receiveOnly bool) []net.IP {
433 ips, err := mox.IPs(ctx, receiveOnly)
434 xcheckf(ctx, err, "listing ips")
438// return IPs from which we may be sending.
439func xsendingIPs(ctx context.Context) []net.IP {
440 ips, err := mox.IPs(ctx, false)
441 xcheckf(ctx, err, "listing ips")
445// CheckDomain checks the configuration for the domain, such as MX, SMTP STARTTLS,
446// SPF, DKIM, DMARC, TLSRPT, MTASTS, autoconfig, autodiscover.
447func (Admin) CheckDomain(ctx context.Context, domainName string) (r CheckResult) {
448 // todo future: should run these checks without a DNS cache so recent changes are picked up.
450 resolver := dns.StrictResolver{Pkg: "check", Log: pkglog.WithContext(ctx).Logger}
451 dialer := &net.Dialer{Timeout: 10 * time.Second}
452 nctx, cancel := context.WithTimeout(ctx, 30*time.Second)
454 return checkDomain(nctx, resolver, dialer, domainName)
457func unptr[T any](l []*T) []T {
461 r := make([]T, len(l))
462 for i, e := range l {
468func checkDomain(ctx context.Context, resolver dns.Resolver, dialer *net.Dialer, domainName string) (r CheckResult) {
469 log := pkglog.WithContext(ctx)
471 domain, xerr := dns.ParseDomain(domainName)
472 xcheckuserf(ctx, xerr, "parsing domain")
474 domConf, ok := mox.Conf.Domain(domain)
476 panic(&sherpa.Error{Code: "user:notFound", Message: "domain not found"})
479 listenIPs := xlistenIPs(ctx, true)
480 isListenIP := func(ip net.IP) bool {
481 for _, lip := range listenIPs {
489 addf := func(l *[]string, format string, args ...any) {
490 *l = append(*l, fmt.Sprintf(format, args...))
493 // Host must be an absolute dns name, ending with a dot.
494 lookupIPs := func(errors *[]string, host string) (ips []string, ourIPs, notOurIPs []net.IP, rerr error) {
495 addrs, _, err := resolver.LookupHost(ctx, host)
497 addf(errors, "Looking up %q: %s", host, err)
498 return nil, nil, nil, err
500 for _, addr := range addrs {
501 ip := net.ParseIP(addr)
503 addf(errors, "Bad IP %q", addr)
506 ips = append(ips, ip.String())
508 ourIPs = append(ourIPs, ip)
510 notOurIPs = append(notOurIPs, ip)
513 return ips, ourIPs, notOurIPs, nil
516 checkTLS := func(errors *[]string, host string, ips []string, port string) {
522 RootCAs: mox.Conf.Static.TLS.CertPool,
525 for _, ip := range ips {
526 conn, err := d.DialContext(ctx, "tcp", net.JoinHostPort(ip, port))
528 addf(errors, "TLS connection to hostname %q, IP %q: %s", host, ip, err)
531 log.Check(err, "closing tcp connection")
536 // If at least one listener with SMTP enabled has unspecified NATed IPs, we'll skip
537 // some checks related to these IPs.
538 var isNAT, isUnspecifiedNAT bool
539 for _, l := range mox.Conf.Static.Listeners {
544 isUnspecifiedNAT = true
547 if len(l.NATIPs) > 0 {
552 var wg sync.WaitGroup
560 // Some DNSSEC-verifying resolvers return unauthentic data for ".", so we check "com".
561 _, result, err := resolver.LookupNS(ctx, "com.")
563 addf(&r.DNSSEC.Errors, "Looking up NS for DNS root (.) to check support in resolver for DNSSEC-verification: %s", err)
564 } else if !result.Authentic {
565 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.`)
567 _, result, _ := resolver.LookupMX(ctx, domain.ASCII+".")
568 if !result.Authentic {
569 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.`)
573 addf(&r.DNSSEC.Instructions, `Enable DNSSEC-signing of the DNS records of your domain (zone) at your DNS hosting provider.`)
575 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".
577cat <<EOF >/etc/unbound/unbound.conf.d/ede.conf
591 // For each mox.Conf.SpecifiedSMTPListenIPs and all NATIPs, and each IP for
592 // mox.Conf.HostnameDomain, check if they resolve back to the host name.
593 hostIPs := map[dns.Domain][]net.IP{}
594 ips, _, err := resolver.LookupIP(ctx, "ip", mox.Conf.Static.HostnameDomain.ASCII+".")
596 addf(&r.IPRev.Errors, "Looking up IPs for hostname: %s", err)
599 gatherMoreIPs := func(publicIPs []net.IP) {
601 for _, ip := range publicIPs {
602 for _, xip := range ips {
607 ips = append(ips, ip)
611 gatherMoreIPs(mox.Conf.Static.SpecifiedSMTPListenIPs)
613 for _, l := range mox.Conf.Static.Listeners {
618 for _, ip := range l.NATIPs {
619 natips = append(natips, net.ParseIP(ip))
621 gatherMoreIPs(natips)
623 hostIPs[mox.Conf.Static.HostnameDomain] = ips
625 iplist := func(ips []net.IP) string {
627 for _, ip := range ips {
628 ipstrs = append(ipstrs, ip.String())
630 return strings.Join(ipstrs, ", ")
633 r.IPRev.Hostname = mox.Conf.Static.HostnameDomain
634 r.IPRev.Instructions = []string{
635 fmt.Sprintf("Ensure IPs %s have reverse address %s.", iplist(ips), mox.Conf.Static.HostnameDomain.ASCII),
638 // If we have a socks transport, also check its host and IP.
639 for tname, t := range mox.Conf.Static.Transports {
641 hostIPs[t.Socks.Hostname] = append(hostIPs[t.Socks.Hostname], t.Socks.IPs...)
642 instr := fmt.Sprintf("For SOCKS transport %s, ensure IPs %s have reverse address %s.", tname, iplist(t.Socks.IPs), t.Socks.Hostname)
643 r.IPRev.Instructions = append(r.IPRev.Instructions, instr)
653 results := make(chan result)
655 for host, ips := range hostIPs {
656 for _, ip := range ips {
661 addrs, _, err := resolver.LookupAddr(ctx, s)
662 results <- result{host, s, addrs, err}
666 r.IPRev.IPNames = map[string][]string{}
669 host, addrs, ip, err := lr.Host, lr.Addrs, lr.IP, lr.Err
671 addf(&r.IPRev.Errors, "Looking up reverse name for %s of %s: %v", ip, host, err)
675 for i, a := range addrs {
676 a = strings.TrimRight(a, ".")
678 ad, err := dns.ParseDomain(a)
680 addf(&r.IPRev.Errors, "Parsing reverse name %q for %s: %v", a, ip, err)
686 if !match && !isNAT && host == mox.Conf.Static.HostnameDomain {
687 addf(&r.IPRev.Warnings, "IP %s with name(s) %s is forward confirmed, but does not match hostname %s.", ip, strings.Join(addrs, ","), host)
689 r.IPRev.IPNames[ip] = addrs
692 // Linux machines are often initially set up with a loopback IP for the hostname in
693 // /etc/hosts, presumably because it isn't known if their external IPs are static.
694 // For mail servers, they should certainly be static. The quickstart would also
695 // have warned about this, but could have been missed/ignored.
696 for _, ip := range ips {
698 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())
709 mxs, _, err := resolver.LookupMX(ctx, domain.ASCII+".")
711 addf(&r.MX.Errors, "Looking up MX records for %s: %s", domain, err)
713 r.MX.Records = make([]MX, len(mxs))
714 for i, mx := range mxs {
715 r.MX.Records[i] = MX{mx.Host, int(mx.Pref), nil}
717 if len(mxs) == 1 && mxs[0].Host == "." {
718 addf(&r.MX.Errors, `MX records consists of explicit null mx record (".") indicating that domain does not accept email.`)
721 for i, mx := range mxs {
722 ips, ourIPs, notOurIPs, err := lookupIPs(&r.MX.Errors, mx.Host)
724 addf(&r.MX.Errors, "Looking up IPs for mx host %q: %s", mx.Host, err)
726 r.MX.Records[i].IPs = ips
727 if isUnspecifiedNAT {
730 if len(ourIPs) == 0 {
731 addf(&r.MX.Errors, "None of the IPs that mx %q points to is ours: %v", mx.Host, notOurIPs)
732 } else if len(notOurIPs) > 0 {
733 addf(&r.MX.Errors, "Some of the IPs that mx %q points to are not ours: %v", mx.Host, notOurIPs)
737 r.MX.Instructions = []string{
738 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+"."),
742 // TLS, mostly checking certificate expiration and CA trust.
743 // 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.
749 // MTA-STS, autoconfig, autodiscover are checked in their sections.
751 // Dial a single MX host with given IP and perform STARTTLS handshake.
752 dialSMTPSTARTTLS := func(host, ip string) error {
753 conn, err := dialer.DialContext(ctx, "tcp", net.JoinHostPort(ip, "25"))
760 log.Check(err, "closing tcp connection")
764 end := time.Now().Add(10 * time.Second)
765 cctx, cancel := context.WithTimeout(ctx, 10*time.Second)
767 err = conn.SetDeadline(end)
768 log.WithContext(ctx).Check(err, "setting deadline")
770 br := bufio.NewReader(conn)
771 _, err = br.ReadString('\n')
773 return fmt.Errorf("reading SMTP banner from remote: %s", err)
775 if _, err := fmt.Fprintf(conn, "EHLO moxtest\r\n"); err != nil {
776 return fmt.Errorf("writing SMTP EHLO to remote: %s", err)
779 line, err := br.ReadString('\n')
781 return fmt.Errorf("reading SMTP EHLO response from remote: %s", err)
783 if strings.HasPrefix(line, "250-") {
786 if strings.HasPrefix(line, "250 ") {
789 return fmt.Errorf("unexpected response to SMTP EHLO from remote: %q", strings.TrimSuffix(line, "\r\n"))
791 if _, err := fmt.Fprintf(conn, "STARTTLS\r\n"); err != nil {
792 return fmt.Errorf("writing SMTP STARTTLS to remote: %s", err)
794 line, err := br.ReadString('\n')
796 return fmt.Errorf("reading response to SMTP STARTTLS from remote: %s", err)
798 if !strings.HasPrefix(line, "220 ") {
799 return fmt.Errorf("SMTP STARTTLS response from remote not 220 OK: %q", strings.TrimSuffix(line, "\r\n"))
801 config := &tls.Config{
803 RootCAs: mox.Conf.Static.TLS.CertPool,
805 tlsconn := tls.Client(conn, config)
806 if err := tlsconn.HandshakeContext(cctx); err != nil {
807 return fmt.Errorf("TLS handshake after SMTP STARTTLS: %s", err)
811 log.Check(err, "closing smtp connection")
816 checkSMTPSTARTTLS := func() {
817 // Initial errors are ignored, will already have been warned about by MX checks.
818 mxs, _, err := resolver.LookupMX(ctx, domain.ASCII+".")
822 if len(mxs) == 1 && mxs[0].Host == "." {
825 for _, mx := range mxs {
826 ips, _, _, err := lookupIPs(&r.MX.Errors, mx.Host)
831 for _, ip := range ips {
832 if err := dialSMTPSTARTTLS(mx.Host, ip); err != nil {
833 addf(&r.TLS.Errors, "SMTP connection with STARTTLS to MX hostname %q IP %s: %s", mx.Host, ip, err)
849 daneRecords := func(l config.Listener) map[string]struct{} {
853 records := map[string]struct{}{}
854 addRecord := func(privKey crypto.Signer) {
855 spkiBuf, err := x509.MarshalPKIXPublicKey(privKey.Public())
857 addf(&r.DANE.Errors, "marshal SubjectPublicKeyInfo for DANE record: %v", err)
860 sum := sha256.Sum256(spkiBuf)
862 Usage: adns.TLSAUsageDANEEE,
863 Selector: adns.TLSASelectorSPKI,
864 MatchType: adns.TLSAMatchTypeSHA256,
867 records[r.Record()] = struct{}{}
869 for _, privKey := range l.TLS.HostPrivateRSA2048Keys {
872 for _, privKey := range l.TLS.HostPrivateECDSAP256Keys {
878 expectedDANERecords := func(host string) map[string]struct{} {
879 for _, l := range mox.Conf.Static.Listeners {
880 if l.HostnameDomain.ASCII == host {
881 return daneRecords(l)
884 public := mox.Conf.Static.Listeners["public"]
885 if mox.Conf.Static.HostnameDomain.ASCII == host && public.HostnameDomain.ASCII == "" {
886 return daneRecords(public)
891 mxl, result, err := resolver.LookupMX(ctx, domain.ASCII+".")
893 addf(&r.DANE.Errors, "Looking up MX hosts to check for DANE records: %s", err)
895 if !result.Authentic {
896 addf(&r.DANE.Warnings, "DANE is inactive because MX records are not DNSSEC-signed.")
898 for _, mx := range mxl {
899 expect := expectedDANERecords(mx.Host)
901 tlsal, tlsaResult, err := resolver.LookupTLSA(ctx, 25, "tcp", mx.Host+".")
902 if dns.IsNotFound(err) {
904 addf(&r.DANE.Errors, "No DANE records for MX host %s, expected: %s.", mx.Host, strings.Join(slices.Collect(maps.Keys(expect)), "; "))
907 } else if err != nil {
908 addf(&r.DANE.Errors, "Looking up DANE records for MX host %s: %v", mx.Host, err)
910 } else if !tlsaResult.Authentic && len(tlsal) > 0 {
911 addf(&r.DANE.Errors, "DANE records exist for MX host %s, but are not DNSSEC-signed.", mx.Host)
914 extra := map[string]struct{}{}
915 for _, e := range tlsal {
917 if _, ok := expect[s]; ok {
920 extra[s] = struct{}{}
924 l := slices.Sorted(maps.Keys(expect))
925 addf(&r.DANE.Errors, "Missing DANE records of type TLSA for MX host _25._tcp.%s: %s", mx.Host, strings.Join(l, "; "))
928 l := slices.Sorted(maps.Keys(extra))
929 addf(&r.DANE.Errors, "Unexpected DANE records of type TLSA for MX host _25._tcp.%s: %s", mx.Host, strings.Join(l, "; "))
934 public := mox.Conf.Static.Listeners["public"]
935 pubDom := public.HostnameDomain
936 if pubDom.ASCII == "" {
937 pubDom = mox.Conf.Static.HostnameDomain
939 records := slices.Sorted(maps.Keys(daneRecords(public)))
940 if len(records) > 0 {
941 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"
942 for _, r := range records {
943 instr += fmt.Sprintf("\t_25._tcp.%s. TLSA %s\n", pubDom.ASCII, r)
945 addf(&r.DANE.Instructions, instr)
947 addf(&r.DANE.Warnings, "DANE not configured: no static TLS host keys.")
949 instr := "Add static TLS keys for use with DANE to mox.conf under: Listeners, public, TLS, HostPrivateKeyFiles.\n\nIf automatic TLS certificate management with ACME is configured, run \"mox config ensureacmehostprivatekeys\" to generate static TLS keys and to print a snippet for \"HostPrivateKeyFiles\" for inclusion in mox.conf.\n\nIf TLS keys and certificates are managed externally, configure the TLS keys manually under \"HostPrivateKeyFiles\" in mox.conf, and make sure new TLS keys are not generated for each new certificate (look for an option to \"reuse private keys\" when doing ACME). Important: Before using new TLS keys, corresponding new DANE (TLSA) DNS records must be published (taking TTL into account to let the previous records expire). Using new TLS keys without updating DANE (TLSA) DNS records will cause DANE verification failures, breaking incoming deliveries.\n\nWith \"HostPrivateKeyFiles\" configured, DNS records for DANE based on those TLS keys will be suggested, and future DNS checks will look for those DNS records. Once those DNS records are published, DANE is active for all domains with an MX record pointing to the host."
950 addf(&r.DANE.Instructions, instr)
955 // 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.
961 ips := mox.DomainSPFIPs()
963 // Verify a domain with the configured IPs that do SMTP.
964 verifySPF := func(isHost bool, domain dns.Domain) (string, *SPFRecord, spf.Record) {
970 _, txt, record, _, err := spf.Lookup(ctx, log.Logger, resolver, domain)
972 addf(&r.SPF.Errors, "Looking up %s SPF record: %s", kind, err)
974 var xrecord *SPFRecord
976 xrecord = &SPFRecord{*record}
983 checkSPFIP := func(ip net.IP) {
988 spfr.Directives = append(spfr.Directives, spf.Directive{Mechanism: mechanism, IP: ip})
996 MailFromLocalpart: "postmaster",
997 MailFromDomain: domain,
998 HelloDomain: dns.IPDomain{Domain: domain},
999 LocalIP: net.ParseIP("127.0.0.1"),
1000 LocalHostname: dns.Domain{ASCII: "localhost"},
1002 status, mechanism, expl, _, err := spf.Evaluate(ctx, log.Logger, record, resolver, args)
1004 addf(&r.SPF.Errors, "Evaluating IP %q against %s SPF record: %s", ip, kind, err)
1005 } else if status != spf.StatusPass {
1006 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)
1010 for _, ip := range ips {
1014 spfr.Directives = append(spfr.Directives, spf.Directive{Mechanism: "mx"})
1021 spfr.Directives = append(spfr.Directives, spf.Directive{Qualifier: qual, Mechanism: "all"})
1022 return txt, xrecord, spfr
1025 // Check SPF record for domain.
1026 var dspfr spf.Record
1027 r.SPF.DomainTXT, r.SPF.DomainRecord, dspfr = verifySPF(false, domain)
1028 // todo: possibly check all hosts for MX records? assuming they are also sending mail servers.
1029 r.SPF.HostTXT, r.SPF.HostRecord, _ = verifySPF(true, mox.Conf.Static.HostnameDomain)
1032 addf(&r.SPF.Warnings, `No explicitly configured IPs found to check SPF policy against. Consider configuring public IPs instead of unspecified addresses (0.0.0.0 and/or ::) in the "public" listener in mox.conf, or NATIPs in case of NAT.`)
1035 dtxt, err := dspfr.Record()
1037 addf(&r.SPF.Errors, "Making SPF record for instructions: %s", err)
1039 domainspf := fmt.Sprintf("%s TXT %s", domain.ASCII+".", mox.TXTStrings(dtxt))
1042 hostspf := fmt.Sprintf(`%s TXT "v=spf1 a -all"`, mox.Conf.Static.HostnameDomain.ASCII+".")
1044 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)
1048 // 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.
1054 var missing []string
1055 for sel, selc := range domConf.DKIM.Selectors {
1056 _, record, txt, _, err := dkim.Lookup(ctx, log.Logger, resolver, selc.Domain, domain)
1058 missing = append(missing, sel)
1059 if errors.Is(err, dkim.ErrNoRecord) {
1060 addf(&r.DKIM.Errors, "No DKIM DNS record for selector %q.", sel)
1061 } else if errors.Is(err, dkim.ErrSyntax) {
1062 addf(&r.DKIM.Errors, "Parsing DKIM DNS record for selector %q: %s", sel, err)
1064 addf(&r.DKIM.Errors, "Fetching DKIM record for selector %q: %s", sel, err)
1068 r.DKIM.Records = append(r.DKIM.Records, DKIMRecord{sel, txt, record})
1069 pubKey := selc.Key.Public()
1071 switch k := pubKey.(type) {
1072 case *rsa.PublicKey:
1074 pk, err = x509.MarshalPKIXPublicKey(k)
1076 addf(&r.DKIM.Errors, "Marshal public key for %q to compare against DNS: %s", sel, err)
1079 case ed25519.PublicKey:
1082 addf(&r.DKIM.Errors, "Internal error: unknown public key type %T.", pubKey)
1086 if record != nil && !bytes.Equal(record.Pubkey, pk) {
1087 addf(&r.DKIM.Errors, "For selector %q, the public key in DKIM DNS TXT record does not match with configured private key.", sel)
1088 missing = append(missing, sel)
1092 if len(domConf.DKIM.Selectors) == 0 {
1093 addf(&r.DKIM.Errors, "No DKIM configuration, add a key to the configuration file, and instructions for DNS records will appear here.")
1096 for _, sel := range missing {
1097 dkimr := dkim.Record{
1099 Hashes: []string{"sha256"},
1100 PublicKey: domConf.DKIM.Selectors[sel].Key.Public(),
1102 switch dkimr.PublicKey.(type) {
1103 case *rsa.PublicKey:
1104 case ed25519.PublicKey:
1105 dkimr.Key = "ed25519"
1107 addf(&r.DKIM.Errors, "Internal error: unknown public key type %T.", dkimr.PublicKey)
1109 txt, err := dkimr.Record()
1111 addf(&r.DKIM.Errors, "Making DKIM record for instructions: %s", err)
1114 instr += fmt.Sprintf("\n\t%s._domainkey.%s TXT %s\n", sel, domain.ASCII+".", mox.TXTStrings(txt))
1117 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
1118 addf(&r.DKIM.Instructions, "%s", instr)
1128 _, dmarcDomain, record, txt, _, err := dmarc.Lookup(ctx, log.Logger, resolver, domain)
1130 addf(&r.DMARC.Errors, "Looking up DMARC record: %s", err)
1131 } else if record == nil {
1132 addf(&r.DMARC.Errors, "No DMARC record")
1134 r.DMARC.Domain = dmarcDomain.Name()
1137 r.DMARC.Record = &DMARCRecord{*record}
1139 if record != nil && record.Policy == "none" {
1140 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.")
1142 if record != nil && record.SubdomainPolicy == "none" {
1143 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.")
1145 if record != nil && len(record.AggregateReportAddresses) == 0 {
1146 addf(&r.DMARC.Warnings, "It is recommended you specify you would like aggregate reports about delivery success in the DMARC record, see instructions.")
1149 dmarcr := dmarc.DefaultRecord
1150 dmarcr.Policy = "reject"
1153 if domConf.DMARC != nil {
1154 // If the domain is in a different Organizational Domain, the receiving domain
1155 // needs a special DNS record to opt-in to receiving reports. We check for that
1158 orgDom := publicsuffix.Lookup(ctx, log.Logger, domain)
1159 destOrgDom := publicsuffix.Lookup(ctx, log.Logger, domConf.DMARC.DNSDomain)
1160 if orgDom != destOrgDom {
1161 accepts, status, _, _, _, err := dmarc.LookupExternalReportsAccepted(ctx, log.Logger, resolver, domain, domConf.DMARC.DNSDomain)
1162 if status != dmarc.StatusNone {
1163 addf(&r.DMARC.Errors, "Checking if external destination accepts reports: %s", err)
1164 } else if !accepts {
1165 addf(&r.DMARC.Errors, "External destination does not accept reports (%s)", err)
1167 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)
1172 Opaque: smtp.NewAddress(domConf.DMARC.ParsedLocalpart, domConf.DMARC.DNSDomain).Pack(false),
1174 uristr := uri.String()
1175 dmarcr.AggregateReportAddresses = []dmarc.URI{
1176 {Address: uristr, MaxSize: 10, Unit: "m"},
1181 for _, addr := range record.AggregateReportAddresses {
1182 if addr.Address == uristr {
1188 addf(&r.DMARC.Errors, "Configured DMARC reporting address is not present in record.")
1192 addf(&r.DMARC.Instructions, `Configure a DMARC destination in domain in config file.`)
1194 instr := fmt.Sprintf("Ensure a DNS TXT record like the following exists:\n\n\t_dmarc.%s TXT %s\n\nYou can start with testing mode by replacing p=reject with p=none. You can also request for the policy to be applied to a percentage of emails instead of all, by adding pct=X, with X between 0 and 100. Keep in mind that receiving mail servers will apply some anti-spam assessment regardless of the policy and whether it is applied to the message. The ruf= part requests daily aggregate reports to be sent to the specified address, which is automatically configured and reports automatically analyzed.", domain.ASCII+".", mox.TXTStrings(dmarcr.String()))
1195 addf(&r.DMARC.Instructions, instr)
1197 addf(&r.DMARC.Instructions, extInstr)
1201 checkTLSRPT := func(result *TLSRPTCheckResult, dom dns.Domain, address smtp.Address, isHost bool) {
1205 record, txt, err := tlsrpt.Lookup(ctx, log.Logger, resolver, dom)
1207 addf(&result.Errors, "Looking up TLSRPT record for domain %s: %s", dom, err)
1211 result.Record = &TLSRPTRecord{*record}
1214 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.`
1215 var zeroaddr smtp.Address
1216 if address != zeroaddr {
1217 // TLSRPT does not require validation of reporting addresses outside the domain.
1221 Opaque: address.Pack(false),
1223 rua := tlsrpt.RUA(uri.String())
1224 tlsrptr := &tlsrpt.Record{
1225 Version: "TLSRPTv1",
1226 RUAs: [][]tlsrpt.RUA{{rua}},
1228 instr += fmt.Sprintf(`
1230Ensure a DNS TXT record like the following exists:
1232 _smtp._tls.%s TXT %s
1234`, dom.ASCII+".", mox.TXTStrings(tlsrptr.String()))
1239 for _, l := range record.RUAs {
1240 for _, e := range l {
1248 addf(&result.Errors, `Configured reporting address is not present in TLSRPT record.`)
1253 instr += fmt.Sprintf(`
1255Ensure the following snippet is present in mox.conf (ensure tabs are used for indenting, not spaces):
1262`, mox.Conf.Static.Postmaster.Account)
1263 addf(&result.Errors, `Configure a HostTLSRPT section in the static mox.conf config file, restart mox and check again for instructions for the TLSRPT DNS record.`)
1265 addf(&result.Errors, `Configure a TLSRPT destination for the domain (through the admin web interface or by editing the domains.conf config file, adding a TLSRPT section) and check again for instructions for the TLSRPT DNS record.`)
1267 addf(&result.Instructions, instr)
1272 var hostTLSRPTAddr smtp.Address
1273 if mox.Conf.Static.HostTLSRPT.Localpart != "" {
1274 hostTLSRPTAddr = smtp.NewAddress(mox.Conf.Static.HostTLSRPT.ParsedLocalpart, mox.Conf.Static.HostnameDomain)
1276 go checkTLSRPT(&r.HostTLSRPT, mox.Conf.Static.HostnameDomain, hostTLSRPTAddr, true)
1280 var domainTLSRPTAddr smtp.Address
1281 if domConf.TLSRPT != nil {
1282 domainTLSRPTAddr = smtp.NewAddress(domConf.TLSRPT.ParsedLocalpart, domain)
1284 go checkTLSRPT(&r.DomainTLSRPT, domain, domainTLSRPTAddr, false)
1292 // The admin has explicitly disabled mta-sts, keep warning about it.
1293 if domConf.MTASTS == nil {
1294 addf(&r.MTASTS.Warnings, "MTA-STS is not configured for this domain.")
1297 record, txt, err := mtasts.LookupRecord(ctx, log.Logger, resolver, domain)
1298 if err != nil && !(domConf.MTASTS == nil && errors.Is(err, mtasts.ErrNoRecord)) {
1299 addf(&r.MTASTS.Errors, "Looking up MTA-STS record: %s", err)
1303 r.MTASTS.Record = &MTASTSRecord{*record}
1306 policy, text, err := mtasts.FetchPolicy(ctx, log.Logger, domain)
1308 if !(domConf.MTASTS == nil && errors.Is(err, mtasts.ErrNoPolicy)) {
1309 addf(&r.MTASTS.Errors, "Fetching MTA-STS policy: %s", err)
1311 } else if policy.Mode == mtasts.ModeNone {
1312 addf(&r.MTASTS.Warnings, "MTA-STS policy is present, but does not require TLS.")
1313 } else if policy.Mode == mtasts.ModeTesting {
1314 addf(&r.MTASTS.Warnings, "MTA-STS policy is in testing mode, do not forget to change to mode enforce after testing period.")
1316 r.MTASTS.PolicyText = text
1317 r.MTASTS.Policy = policy
1318 if policy != nil && policy.Mode != mtasts.ModeNone {
1319 if !policy.Matches(mox.Conf.Static.HostnameDomain) {
1320 addf(&r.MTASTS.Warnings, "Configured hostname is missing from policy MX list.")
1322 if policy.MaxAgeSeconds <= 24*3600 {
1323 addf(&r.MTASTS.Warnings, "Policy has a MaxAge of less than 1 day. For stable configurations, the recommended period is in weeks.")
1326 mxl, _, _ := resolver.LookupMX(ctx, domain.ASCII+".")
1327 // We do not check for errors, the MX check will complain about mx errors, we assume we will get the same error here.
1328 mxs := map[dns.Domain]struct{}{}
1329 for _, mx := range mxl {
1330 d, err := dns.ParseDomain(strings.TrimSuffix(mx.Host, "."))
1332 addf(&r.MTASTS.Warnings, "MX record %q is invalid: %s", mx.Host, err)
1337 for mx := range mxs {
1338 if !policy.Matches(mx) {
1339 addf(&r.MTASTS.Warnings, "MX record %q does not match MTA-STS policy MX list.", mx)
1342 for _, mx := range policy.MX {
1346 if _, ok := mxs[mx.Domain]; !ok {
1347 addf(&r.MTASTS.Warnings, "MX %q in MTA-STS policy is not in MX record.", mx)
1352 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.
1354After 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.
1356You 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.
1358You 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.
1360The _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.
1362When enabling MTA-STS, or updating a policy, always update the policy first (through a configuration change and reload/restart), and the DNS record second.
1364 addf(&r.MTASTS.Instructions, intro)
1366 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.`)
1368 host := fmt.Sprintf("Ensure DNS CNAME/A/AAAA records exist that resolves mta-sts.%s to this mail server. For example:\n\n\tmta-sts.%s CNAME %s\n\n", domain.ASCII, domain.ASCII+".", mox.Conf.Static.HostnameDomain.ASCII+".")
1369 addf(&r.MTASTS.Instructions, host)
1371 mtastsr := mtasts.Record{
1373 ID: time.Now().Format("20060102T150405"),
1375 dns := fmt.Sprintf("Ensure a DNS TXT record like the following exists:\n\n\t_mta-sts.%s TXT %s\n\nConfigure the ID in the configuration file, it must be of the form [a-zA-Z0-9]{1,31}. It represents the version of the policy. For each policy change, you must change the ID to a new unique value. You could use a timestamp like 20220621T123000. When this field exists, an SMTP server will fetch a policy at https://mta-sts.%s/.well-known/mta-sts.txt. This policy is served by mox.", domain.ASCII+".", mox.TXTStrings(mtastsr.String()), domain.Name())
1376 addf(&r.MTASTS.Instructions, dns)
1385 type srvReq struct {
1393 // We'll assume if any submissions is configured, it is public. Same for imap. And
1394 // if not, that there is a plain option.
1395 var submissions, imaps bool
1396 for _, l := range mox.Conf.Static.Listeners {
1397 if l.TLS != nil && l.Submissions.Enabled {
1400 if l.TLS != nil && l.IMAPS.Enabled {
1404 srvhost := func(ok bool) string {
1406 return mox.Conf.Static.HostnameDomain.ASCII + "."
1410 var reqs = []srvReq{
1411 {name: "_submissions", port: 465, host: srvhost(submissions)},
1412 {name: "_submission", port: 587, host: srvhost(!submissions)},
1413 {name: "_imaps", port: 993, host: srvhost(imaps)},
1414 {name: "_imap", port: 143, host: srvhost(!imaps)},
1415 {name: "_pop3", port: 110, host: "."},
1416 {name: "_pop3s", port: 995, host: "."},
1418 // Host "." indicates the service is not available. We suggested in the DNS records
1420 for i := range reqs {
1421 if reqs[i].host == "." {
1425 var srvwg sync.WaitGroup
1426 srvwg.Add(len(reqs))
1427 for i := range reqs {
1430 _, reqs[i].srvs, _, reqs[i].err = resolver.LookupSRV(ctx, reqs[i].name[1:], "tcp", domain.ASCII+".")
1435 instr := "Ensure DNS records like the following exist:\n\n"
1436 r.SRVConf.SRVs = map[string][]net.SRV{}
1437 for _, req := range reqs {
1438 name := req.name + "._tcp." + domain.ASCII
1440 if req.host == "." {
1443 instr += fmt.Sprintf("\t%s._tcp.%-*s SRV 0 %d %d %s\n", req.name, len("_submissions")-len(req.name)+len(domain.ASCII+"."), domain.ASCII+".", weight, req.port, req.host)
1444 r.SRVConf.SRVs[req.name] = unptr(req.srvs)
1446 addf(&r.SRVConf.Errors, "Looking up SRV record %q: %s", name, req.err)
1447 } else if len(req.srvs) == 0 {
1448 if req.host == "." {
1449 addf(&r.SRVConf.Warnings, "Missing optional SRV record %q", name)
1451 addf(&r.SRVConf.Errors, "Missing SRV record %q", name)
1453 } else if len(req.srvs) != 1 || req.srvs[0].Target != req.host || req.srvs[0].Port != req.port {
1455 for _, srv := range req.srvs {
1456 srvs = append(srvs, fmt.Sprintf("%d %d %d %s", srv.Priority, srv.Weight, srv.Port, srv.Target))
1458 addf(&r.SRVConf.Errors, "Unexpected SRV record(s) for %q: %s", name, strings.Join(srvs, ", "))
1461 addf(&r.SRVConf.Instructions, instr)
1470 if domConf.ClientSettingsDomain != "" {
1471 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+".")
1473 ips, ourIPs, notOurIPs, err := lookupIPs(&r.Autoconf.Errors, domConf.ClientSettingsDNSDomain.ASCII+".")
1475 addf(&r.Autoconf.Errors, "Looking up client settings DNS CNAME: %s", err)
1477 r.Autoconf.ClientSettingsDomainIPs = ips
1478 if !isUnspecifiedNAT {
1479 if len(ourIPs) == 0 {
1480 addf(&r.Autoconf.Errors, "Client settings domain does not point to one of our IPs.")
1481 } else if len(notOurIPs) > 0 {
1482 addf(&r.Autoconf.Errors, "Client settings domain points to some IPs that are not ours: %v", notOurIPs)
1487 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+".")
1489 host := "autoconfig." + domain.ASCII + "."
1490 ips, ourIPs, notOurIPs, err := lookupIPs(&r.Autoconf.Errors, host)
1492 addf(&r.Autoconf.Errors, "Looking up autoconfig host: %s", err)
1496 r.Autoconf.IPs = ips
1497 if !isUnspecifiedNAT {
1498 if len(ourIPs) == 0 {
1499 addf(&r.Autoconf.Errors, "Autoconfig does not point to one of our IPs.")
1500 } else if len(notOurIPs) > 0 {
1501 addf(&r.Autoconf.Errors, "Autoconfig points to some IPs that are not ours: %v", notOurIPs)
1505 checkTLS(&r.Autoconf.Errors, "autoconfig."+domain.ASCII, ips, "443")
1514 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+".")
1516 _, srvs, _, err := resolver.LookupSRV(ctx, "autodiscover", "tcp", domain.ASCII+".")
1518 addf(&r.Autodiscover.Errors, "Looking up SRV record %q: %s", "autodiscover", err)
1522 for _, srv := range srvs {
1523 ips, ourIPs, notOurIPs, err := lookupIPs(&r.Autodiscover.Errors, srv.Target)
1525 addf(&r.Autodiscover.Errors, "Looking up target %q from SRV record: %s", srv.Target, err)
1528 if srv.Port != 443 {
1532 r.Autodiscover.Records = append(r.Autodiscover.Records, AutodiscoverSRV{*srv, ips})
1533 if !isUnspecifiedNAT {
1534 if len(ourIPs) == 0 {
1535 addf(&r.Autodiscover.Errors, "SRV target %q does not point to our IPs.", srv.Target)
1536 } else if len(notOurIPs) > 0 {
1537 addf(&r.Autodiscover.Errors, "SRV target %q points to some IPs that are not ours: %v", srv.Target, notOurIPs)
1541 checkTLS(&r.Autodiscover.Errors, strings.TrimSuffix(srv.Target, "."), ips, "443")
1544 addf(&r.Autodiscover.Errors, "No SRV record for port 443 for https.")
1552// Domains returns all configured domain names.
1553func (Admin) Domains(ctx context.Context) []config.Domain {
1554 return mox.Conf.DomainConfigs()
1557// Domain returns the dns domain for a (potentially unicode as IDNA) domain name.
1558func (Admin) Domain(ctx context.Context, domain string) dns.Domain {
1559 d, err := dns.ParseDomain(domain)
1560 xcheckuserf(ctx, err, "parse domain")
1561 _, ok := mox.Conf.Domain(d)
1563 xcheckuserf(ctx, errors.New("no such domain"), "looking up domain")
1568// ParseDomain parses a domain, possibly an IDNA domain.
1569func (Admin) ParseDomain(ctx context.Context, domain string) dns.Domain {
1570 d, err := dns.ParseDomain(domain)
1571 xcheckuserf(ctx, err, "parse domain")
1575// DomainConfig returns the configuration for a domain.
1576func (Admin) DomainConfig(ctx context.Context, domain string) config.Domain {
1577 d, err := dns.ParseDomain(domain)
1578 xcheckuserf(ctx, err, "parse domain")
1579 conf, ok := mox.Conf.Domain(d)
1581 xcheckuserf(ctx, errors.New("no such domain"), "looking up domain")
1586// DomainLocalparts returns the encoded localparts and accounts configured in domain.
1587func (Admin) DomainLocalparts(ctx context.Context, domain string) (localpartAccounts map[string]string, localpartAliases map[string]config.Alias) {
1588 d, err := dns.ParseDomain(domain)
1589 xcheckuserf(ctx, err, "parsing domain")
1590 _, ok := mox.Conf.Domain(d)
1592 xcheckuserf(ctx, errors.New("no such domain"), "looking up domain")
1594 return mox.Conf.DomainLocalparts(d)
1597// Accounts returns the names of all configured and all disabled accounts.
1598func (Admin) Accounts(ctx context.Context) (all, disabled []string) {
1599 all, disabled = mox.Conf.AccountsDisabled()
1604// Account returns the parsed configuration of an account.
1605func (Admin) Account(ctx context.Context, account string) (accountConfig config.Account, diskUsage int64) {
1606 log := pkglog.WithContext(ctx)
1608 acc, err := store.OpenAccount(log, account, false)
1609 if err != nil && errors.Is(err, store.ErrAccountUnknown) {
1610 xcheckuserf(ctx, err, "looking up account")
1612 xcheckf(ctx, err, "open account")
1615 log.Check(err, "closing account")
1618 var ac config.Account
1619 acc.WithRLock(func() {
1620 ac, _ = mox.Conf.Account(acc.Name)
1622 err := acc.DB.Read(ctx, func(tx *bstore.Tx) error {
1623 du := store.DiskUsage{ID: 1}
1625 diskUsage = du.MessageSize
1628 xcheckf(ctx, err, "get disk usage")
1631 return ac, diskUsage
1634// ConfigFiles returns the paths and contents of the static and dynamic configuration files.
1635func (Admin) ConfigFiles(ctx context.Context) (staticPath, dynamicPath, static, dynamic string) {
1636 buf0, err := os.ReadFile(mox.ConfigStaticPath)
1637 xcheckf(ctx, err, "read static config file")
1638 buf1, err := os.ReadFile(mox.ConfigDynamicPath)
1639 xcheckf(ctx, err, "read dynamic config file")
1640 return mox.ConfigStaticPath, mox.ConfigDynamicPath, string(buf0), string(buf1)
1643// MTASTSPolicies returns all mtasts policies from the cache.
1644func (Admin) MTASTSPolicies(ctx context.Context) (records []mtastsdb.PolicyRecord) {
1645 records, err := mtastsdb.PolicyRecords(ctx)
1646 xcheckf(ctx, err, "fetching mtasts policies from database")
1650// TLSReports returns TLS reports overlapping with period start/end, for the given
1651// policy domain (or all domains if empty). The reports are sorted first by period
1652// end (most recent first), then by policy domain.
1653func (Admin) TLSReports(ctx context.Context, start, end time.Time, policyDomain string) (reports []tlsrptdb.Record) {
1654 var polDom dns.Domain
1655 if policyDomain != "" {
1657 polDom, err = dns.ParseDomain(policyDomain)
1658 xcheckuserf(ctx, err, "parsing domain %q", policyDomain)
1661 records, err := tlsrptdb.RecordsPeriodDomain(ctx, start, end, polDom)
1662 xcheckf(ctx, err, "fetching tlsrpt report records from database")
1663 sort.Slice(records, func(i, j int) bool {
1664 iend := records[i].Report.DateRange.End
1665 jend := records[j].Report.DateRange.End
1667 return records[i].Domain < records[j].Domain
1669 return iend.After(jend)
1674// TLSReportID returns a single TLS report.
1675func (Admin) TLSReportID(ctx context.Context, domain string, reportID int64) tlsrptdb.Record {
1676 record, err := tlsrptdb.RecordID(ctx, reportID)
1677 if err == nil && record.Domain != domain {
1678 err = bstore.ErrAbsent
1680 if err == bstore.ErrAbsent {
1681 xcheckuserf(ctx, err, "fetching tls report from database")
1683 xcheckf(ctx, err, "fetching tls report from database")
1687// TLSRPTSummary presents TLS reporting statistics for a single domain
1689type TLSRPTSummary struct {
1690 PolicyDomain dns.Domain
1693 ResultTypeCounts map[tlsrpt.ResultType]int64
1696// TLSRPTSummaries returns a summary of received TLS reports overlapping with
1697// period start/end for one or all domains (when domain is empty).
1698// The returned summaries are ordered by domain name.
1699func (Admin) TLSRPTSummaries(ctx context.Context, start, end time.Time, policyDomain string) (domainSummaries []TLSRPTSummary) {
1700 var polDom dns.Domain
1701 if policyDomain != "" {
1703 polDom, err = dns.ParseDomain(policyDomain)
1704 xcheckuserf(ctx, err, "parsing policy domain")
1706 reports, err := tlsrptdb.RecordsPeriodDomain(ctx, start, end, polDom)
1707 xcheckf(ctx, err, "fetching tlsrpt reports from database")
1709 summaries := map[dns.Domain]TLSRPTSummary{}
1710 for _, r := range reports {
1711 dom, err := dns.ParseDomain(r.Domain)
1712 xcheckf(ctx, err, "parsing domain %q", r.Domain)
1714 sum := summaries[dom]
1715 sum.PolicyDomain = dom
1716 for _, result := range r.Report.Policies {
1717 sum.Success += result.Summary.TotalSuccessfulSessionCount
1718 sum.Failure += result.Summary.TotalFailureSessionCount
1719 for _, details := range result.FailureDetails {
1720 if sum.ResultTypeCounts == nil {
1721 sum.ResultTypeCounts = map[tlsrpt.ResultType]int64{}
1723 sum.ResultTypeCounts[details.ResultType] += details.FailedSessionCount
1726 summaries[dom] = sum
1728 sums := make([]TLSRPTSummary, 0, len(summaries))
1729 for _, sum := range summaries {
1730 sums = append(sums, sum)
1732 sort.Slice(sums, func(i, j int) bool {
1733 return sums[i].PolicyDomain.Name() < sums[j].PolicyDomain.Name()
1738// DMARCReports returns DMARC reports overlapping with period start/end, for the
1739// given domain (or all domains if empty). The reports are sorted first by period
1740// end (most recent first), then by domain.
1741func (Admin) DMARCReports(ctx context.Context, start, end time.Time, domain string) (reports []dmarcdb.DomainFeedback) {
1742 reports, err := dmarcdb.RecordsPeriodDomain(ctx, start, end, domain)
1743 xcheckf(ctx, err, "fetching dmarc aggregate reports from database")
1744 sort.Slice(reports, func(i, j int) bool {
1745 iend := reports[i].ReportMetadata.DateRange.End
1746 jend := reports[j].ReportMetadata.DateRange.End
1748 return reports[i].Domain < reports[j].Domain
1755// DMARCReportID returns a single DMARC report.
1756func (Admin) DMARCReportID(ctx context.Context, domain string, reportID int64) (report dmarcdb.DomainFeedback) {
1757 report, err := dmarcdb.RecordID(ctx, reportID)
1758 if err == nil && report.Domain != domain {
1759 err = bstore.ErrAbsent
1761 if err == bstore.ErrAbsent {
1762 xcheckuserf(ctx, err, "fetching dmarc aggregate report from database")
1764 xcheckf(ctx, err, "fetching dmarc aggregate report from database")
1768// DMARCSummary presents DMARC aggregate reporting statistics for a single domain
1770type DMARCSummary struct {
1774 DispositionQuarantine int
1775 DispositionReject int
1778 PolicyOverrides map[dmarcrpt.PolicyOverride]int
1781// DMARCSummaries returns a summary of received DMARC reports overlapping with
1782// period start/end for one or all domains (when domain is empty).
1783// The returned summaries are ordered by domain name.
1784func (Admin) DMARCSummaries(ctx context.Context, start, end time.Time, domain string) (domainSummaries []DMARCSummary) {
1785 reports, err := dmarcdb.RecordsPeriodDomain(ctx, start, end, domain)
1786 xcheckf(ctx, err, "fetching dmarc aggregate reports from database")
1787 summaries := map[string]DMARCSummary{}
1788 for _, r := range reports {
1789 sum := summaries[r.Domain]
1790 sum.Domain = r.Domain
1791 for _, record := range r.Records {
1792 n := record.Row.Count
1796 switch record.Row.PolicyEvaluated.Disposition {
1797 case dmarcrpt.DispositionNone:
1798 sum.DispositionNone += n
1799 case dmarcrpt.DispositionQuarantine:
1800 sum.DispositionQuarantine += n
1801 case dmarcrpt.DispositionReject:
1802 sum.DispositionReject += n
1805 if record.Row.PolicyEvaluated.DKIM == dmarcrpt.DMARCFail {
1808 if record.Row.PolicyEvaluated.SPF == dmarcrpt.DMARCFail {
1812 for _, reason := range record.Row.PolicyEvaluated.Reasons {
1813 if sum.PolicyOverrides == nil {
1814 sum.PolicyOverrides = map[dmarcrpt.PolicyOverride]int{}
1816 sum.PolicyOverrides[reason.Type] += n
1819 summaries[r.Domain] = sum
1821 sums := make([]DMARCSummary, 0, len(summaries))
1822 for _, sum := range summaries {
1823 sums = append(sums, sum)
1825 sort.Slice(sums, func(i, j int) bool {
1826 return sums[i].Domain < sums[j].Domain
1831// Reverse is the result of a reverse lookup.
1832type Reverse struct {
1835 // In the future, we can add a iprev-validated host name, and possibly the IPs of the host names.
1838// LookupIP does a reverse lookup of ip.
1839func (Admin) LookupIP(ctx context.Context, ip string) Reverse {
1840 resolver := dns.StrictResolver{Pkg: "webadmin", Log: pkglog.WithContext(ctx).Logger}
1841 names, _, err := resolver.LookupAddr(ctx, ip)
1842 xcheckuserf(ctx, err, "looking up ip")
1843 return Reverse{names}
1846// DNSBLStatus returns the IPs from which outgoing connections may be made and
1847// their current status in DNSBLs that are configured. The IPs are typically the
1848// configured listen IPs, or otherwise IPs on the machines network interfaces, with
1849// internal/private IPs removed.
1851// The returned value maps IPs to per DNSBL statuses, where "pass" means not listed and
1852// anything else is an error string, e.g. "fail: ..." or "temperror: ...".
1853func (Admin) DNSBLStatus(ctx context.Context) (results map[string]map[string]string, using, monitoring []dns.Domain) {
1854 log := mlog.New("webadmin", nil).WithContext(ctx)
1855 resolver := dns.StrictResolver{Pkg: "check", Log: log.Logger}
1856 return dnsblsStatus(ctx, log, resolver)
1859func dnsblsStatus(ctx context.Context, log mlog.Log, resolver dns.Resolver) (results map[string]map[string]string, using, monitoring []dns.Domain) {
1860 // todo: check health before using dnsbl?
1861 using = mox.Conf.Static.Listeners["public"].SMTP.DNSBLZones
1862 zones := slices.Clone(using)
1863 conf := mox.Conf.DynamicConfig()
1864 for _, zone := range conf.MonitorDNSBLZones {
1865 if !slices.Contains(zones, zone) {
1866 zones = append(zones, zone)
1867 monitoring = append(monitoring, zone)
1871 r := map[string]map[string]string{}
1872 for _, ip := range xsendingIPs(ctx) {
1873 if ip.IsLoopback() || ip.IsPrivate() {
1876 ipstr := ip.String()
1877 r[ipstr] = map[string]string{}
1878 for _, zone := range zones {
1879 status, expl, err := dnsbl.Lookup(ctx, log.Logger, resolver, zone, ip)
1880 result := string(status)
1882 result += ": " + err.Error()
1885 result += ": " + expl
1887 r[ipstr][zone.LogString()] = result
1890 return r, using, monitoring
1893func (Admin) MonitorDNSBLsSave(ctx context.Context, text string) {
1894 var zones []dns.Domain
1895 publicZones := mox.Conf.Static.Listeners["public"].SMTP.DNSBLZones
1896 for _, line := range strings.Split(text, "\n") {
1897 line = strings.TrimSpace(line)
1901 d, err := dns.ParseDomain(line)
1902 xcheckuserf(ctx, err, "parsing dnsbl zone %s", line)
1903 if slices.Contains(zones, d) {
1904 xusererrorf(ctx, "duplicate dnsbl zone %s", line)
1906 if slices.Contains(publicZones, d) {
1907 xusererrorf(ctx, "dnsbl zone %s already present in public listener", line)
1909 zones = append(zones, d)
1912 err := admin.ConfigSave(ctx, func(conf *config.Dynamic) {
1913 conf.MonitorDNSBLs = make([]string, len(zones))
1914 conf.MonitorDNSBLZones = nil
1915 for i, z := range zones {
1916 conf.MonitorDNSBLs[i] = z.Name()
1919 xcheckf(ctx, err, "saving monitoring dnsbl zones")
1922// DomainRecords returns lines describing DNS records that should exist for the
1923// configured domain.
1924func (Admin) DomainRecords(ctx context.Context, domain string) []string {
1925 log := pkglog.WithContext(ctx)
1926 return DomainRecords(ctx, log, domain)
1929// DomainRecords is the implementation of API function Admin.DomainRecords, taking
1931func DomainRecords(ctx context.Context, log mlog.Log, domain string) []string {
1932 d, err := dns.ParseDomain(domain)
1933 xcheckuserf(ctx, err, "parsing domain")
1934 dc, ok := mox.Conf.Domain(d)
1936 xcheckuserf(ctx, errors.New("unknown domain"), "lookup domain")
1938 resolver := dns.StrictResolver{Pkg: "webadmin", Log: pkglog.WithContext(ctx).Logger}
1939 _, result, err := resolver.LookupTXT(ctx, domain+".")
1940 if !dns.IsNotFound(err) {
1941 xcheckf(ctx, err, "looking up record to determine if dnssec is implemented")
1944 var certIssuerDomainName, acmeAccountURI string
1945 public := mox.Conf.Static.Listeners["public"]
1946 if public.TLS != nil && public.TLS.ACME != "" {
1947 acme, ok := mox.Conf.Static.ACME[public.TLS.ACME]
1948 if ok && acme.Manager.Manager.Client != nil {
1949 certIssuerDomainName = acme.IssuerDomainName
1950 acc, err := acme.Manager.Manager.Client.GetReg(ctx, "")
1951 log.Check(err, "get public acme account")
1953 acmeAccountURI = acc.URI
1958 records, err := admin.DomainRecords(dc, d, result.Authentic, certIssuerDomainName, acmeAccountURI)
1959 xcheckf(ctx, err, "dns records")
1963// DomainAdd adds a new domain and reloads the configuration.
1964func (Admin) DomainAdd(ctx context.Context, disabled bool, domain, accountName, localpart string) {
1965 d, err := dns.ParseDomain(domain)
1966 xcheckuserf(ctx, err, "parsing domain")
1968 err = admin.DomainAdd(ctx, disabled, d, accountName, smtp.Localpart(norm.NFC.String(localpart)))
1969 xcheckf(ctx, err, "adding domain")
1972// DomainRemove removes an existing domain and reloads the configuration.
1973func (Admin) DomainRemove(ctx context.Context, domain string) {
1974 d, err := dns.ParseDomain(domain)
1975 xcheckuserf(ctx, err, "parsing domain")
1977 err = admin.DomainRemove(ctx, d)
1978 xcheckf(ctx, err, "removing domain")
1981// AccountAdd adds existing a new account, with an initial email address, and
1982// reloads the configuration.
1983func (Admin) AccountAdd(ctx context.Context, accountName, address string) {
1984 err := admin.AccountAdd(ctx, accountName, address)
1985 xcheckf(ctx, err, "adding account")
1988// AccountRemove removes an existing account and reloads the configuration.
1989func (Admin) AccountRemove(ctx context.Context, accountName string) {
1990 err := admin.AccountRemove(ctx, accountName)
1991 xcheckf(ctx, err, "removing account")
1994// AddressAdd adds a new address to the account, which must already exist.
1995func (Admin) AddressAdd(ctx context.Context, address, accountName string) {
1996 err := admin.AddressAdd(ctx, address, accountName)
1997 xcheckf(ctx, err, "adding address")
2000// AddressRemove removes an existing address.
2001func (Admin) AddressRemove(ctx context.Context, address string) {
2002 err := admin.AddressRemove(ctx, address)
2003 xcheckf(ctx, err, "removing address")
2006// SetPassword saves a new password for an account, invalidating the previous password.
2007// Sessions are not interrupted, and will keep working. New login attempts must use the new password.
2008// Password must be at least 8 characters.
2009func (Admin) SetPassword(ctx context.Context, accountName, password string) {
2010 log := pkglog.WithContext(ctx)
2011 if len(password) < 8 {
2012 xusererrorf(ctx, "message must be at least 8 characters")
2014 acc, err := store.OpenAccount(log, accountName, false)
2015 xcheckf(ctx, err, "open account")
2018 log.WithContext(ctx).Check(err, "closing account")
2020 err = acc.SetPassword(log, password)
2021 xcheckf(ctx, err, "setting password")
2024// AccountSettingsSave set new settings for an account that only an admin can set.
2025func (Admin) AccountSettingsSave(ctx context.Context, accountName string, maxOutgoingMessagesPerDay, maxFirstTimeRecipientsPerDay int, maxMsgSize int64, firstTimeSenderDelay, noCustomPassword bool) {
2026 err := admin.AccountSave(ctx, accountName, func(acc *config.Account) {
2027 acc.MaxOutgoingMessagesPerDay = maxOutgoingMessagesPerDay
2028 acc.MaxFirstTimeRecipientsPerDay = maxFirstTimeRecipientsPerDay
2029 acc.QuotaMessageSize = maxMsgSize
2030 acc.NoFirstTimeSenderDelay = !firstTimeSenderDelay
2031 acc.NoCustomPassword = noCustomPassword
2033 xcheckf(ctx, err, "saving account settings")
2036// AccountLoginDisabledSave saves the LoginDisabled field of an account.
2037func (Admin) AccountLoginDisabledSave(ctx context.Context, accountName string, loginDisabled string) {
2038 log := pkglog.WithContext(ctx)
2040 acc, err := store.OpenAccount(log, accountName, false)
2041 xcheckf(ctx, err, "open account")
2044 log.Check(err, "closing account")
2047 err = admin.AccountSave(ctx, accountName, func(acc *config.Account) {
2048 acc.LoginDisabled = loginDisabled
2050 xcheckf(ctx, err, "saving login disabled account")
2052 err = acc.SessionsClear(ctx, log)
2053 xcheckf(ctx, err, "removing current sessions")
2056// ClientConfigsDomain returns configurations for email clients, IMAP and
2057// Submission (SMTP) for the domain.
2058func (Admin) ClientConfigsDomain(ctx context.Context, domain string) admin.ClientConfigs {
2059 d, err := dns.ParseDomain(domain)
2060 xcheckuserf(ctx, err, "parsing domain")
2062 cc, err := admin.ClientConfigsDomain(d)
2063 xcheckf(ctx, err, "client config for domain")
2067// QueueSize returns the number of messages currently in the outgoing queue.
2068func (Admin) QueueSize(ctx context.Context) int {
2069 n, err := queue.Count(ctx)
2070 xcheckf(ctx, err, "listing messages in queue")
2074// QueueHoldRuleList lists the hold rules.
2075func (Admin) QueueHoldRuleList(ctx context.Context) []queue.HoldRule {
2076 l, err := queue.HoldRuleList(ctx)
2077 xcheckf(ctx, err, "listing queue hold rules")
2081// QueueHoldRuleAdd adds a hold rule. Newly submitted and existing messages
2082// matching the hold rule will be marked "on hold".
2083func (Admin) QueueHoldRuleAdd(ctx context.Context, hr queue.HoldRule) queue.HoldRule {
2085 hr.SenderDomain, err = dns.ParseDomain(hr.SenderDomainStr)
2086 xcheckuserf(ctx, err, "parsing sender domain %q", hr.SenderDomainStr)
2087 hr.RecipientDomain, err = dns.ParseDomain(hr.RecipientDomainStr)
2088 xcheckuserf(ctx, err, "parsing recipient domain %q", hr.RecipientDomainStr)
2090 log := pkglog.WithContext(ctx)
2091 hr, err = queue.HoldRuleAdd(ctx, log, hr)
2092 xcheckf(ctx, err, "adding queue hold rule")
2096// QueueHoldRuleRemove removes a hold rule. The Hold field of messages in
2097// the queue are not changed.
2098func (Admin) QueueHoldRuleRemove(ctx context.Context, holdRuleID int64) {
2099 log := pkglog.WithContext(ctx)
2100 err := queue.HoldRuleRemove(ctx, log, holdRuleID)
2101 xcheckf(ctx, err, "removing queue hold rule")
2104// QueueList returns the messages currently in the outgoing queue.
2105func (Admin) QueueList(ctx context.Context, filter queue.Filter, sort queue.Sort) []queue.Msg {
2106 l, err := queue.List(ctx, filter, sort)
2107 xcheckf(ctx, err, "listing messages in queue")
2111// QueueNextAttemptSet sets a new time for next delivery attempt of matching
2112// messages from the queue.
2113func (Admin) QueueNextAttemptSet(ctx context.Context, filter queue.Filter, minutes int) (affected int) {
2114 n, err := queue.NextAttemptSet(ctx, filter, time.Now().Add(time.Duration(minutes)*time.Minute))
2115 xcheckf(ctx, err, "setting new next delivery attempt time for matching messages in queue")
2119// QueueNextAttemptAdd adds a duration to the time of next delivery attempt of
2120// matching messages from the queue.
2121func (Admin) QueueNextAttemptAdd(ctx context.Context, filter queue.Filter, minutes int) (affected int) {
2122 n, err := queue.NextAttemptAdd(ctx, filter, time.Duration(minutes)*time.Minute)
2123 xcheckf(ctx, err, "adding duration to next delivery attempt for matching messages in queue")
2127// QueueHoldSet sets the Hold field of matching messages in the queue.
2128func (Admin) QueueHoldSet(ctx context.Context, filter queue.Filter, onHold bool) (affected int) {
2129 n, err := queue.HoldSet(ctx, filter, onHold)
2130 xcheckf(ctx, err, "changing onhold for matching messages in queue")
2134// QueueFail fails delivery for matching messages, causing DSNs to be sent.
2135func (Admin) QueueFail(ctx context.Context, filter queue.Filter) (affected int) {
2136 log := pkglog.WithContext(ctx)
2137 n, err := queue.Fail(ctx, log, filter)
2138 xcheckf(ctx, err, "drop messages from queue")
2142// QueueDrop removes matching messages from the queue.
2143func (Admin) QueueDrop(ctx context.Context, filter queue.Filter) (affected int) {
2144 log := pkglog.WithContext(ctx)
2145 n, err := queue.Drop(ctx, log, filter)
2146 xcheckf(ctx, err, "drop messages from queue")
2150// QueueRequireTLSSet updates the requiretls field for matching messages in the
2151// queue, to be used for the next delivery.
2152func (Admin) QueueRequireTLSSet(ctx context.Context, filter queue.Filter, requireTLS *bool) (affected int) {
2153 n, err := queue.RequireTLSSet(ctx, filter, requireTLS)
2154 xcheckf(ctx, err, "update requiretls for messages in queue")
2158// QueueTransportSet initiates delivery of a message from the queue and sets the transport
2159// to use for delivery.
2160func (Admin) QueueTransportSet(ctx context.Context, filter queue.Filter, transport string) (affected int) {
2161 n, err := queue.TransportSet(ctx, filter, transport)
2162 xcheckf(ctx, err, "changing transport for messages in queue")
2166// RetiredList returns messages retired from the queue (delivery could
2167// have succeeded or failed).
2168func (Admin) RetiredList(ctx context.Context, filter queue.RetiredFilter, sort queue.RetiredSort) []queue.MsgRetired {
2169 l, err := queue.RetiredList(ctx, filter, sort)
2170 xcheckf(ctx, err, "listing retired messages")
2174// HookQueueSize returns the number of webhooks still to be delivered.
2175func (Admin) HookQueueSize(ctx context.Context) int {
2176 n, err := queue.HookQueueSize(ctx)
2177 xcheckf(ctx, err, "get hook queue size")
2181// HookList lists webhooks still to be delivered.
2182func (Admin) HookList(ctx context.Context, filter queue.HookFilter, sort queue.HookSort) []queue.Hook {
2183 l, err := queue.HookList(ctx, filter, sort)
2184 xcheckf(ctx, err, "listing hook queue")
2188// HookNextAttemptSet sets a new time for next delivery attempt of matching
2189// hooks from the queue.
2190func (Admin) HookNextAttemptSet(ctx context.Context, filter queue.HookFilter, minutes int) (affected int) {
2191 n, err := queue.HookNextAttemptSet(ctx, filter, time.Now().Add(time.Duration(minutes)*time.Minute))
2192 xcheckf(ctx, err, "setting new next delivery attempt time for matching webhooks in queue")
2196// HookNextAttemptAdd adds a duration to the time of next delivery attempt of
2197// matching hooks from the queue.
2198func (Admin) HookNextAttemptAdd(ctx context.Context, filter queue.HookFilter, minutes int) (affected int) {
2199 n, err := queue.HookNextAttemptAdd(ctx, filter, time.Duration(minutes)*time.Minute)
2200 xcheckf(ctx, err, "adding duration to next delivery attempt for matching webhooks in queue")
2204// HookRetiredList lists retired webhooks.
2205func (Admin) HookRetiredList(ctx context.Context, filter queue.HookRetiredFilter, sort queue.HookRetiredSort) []queue.HookRetired {
2206 l, err := queue.HookRetiredList(ctx, filter, sort)
2207 xcheckf(ctx, err, "listing retired hooks")
2211// HookCancel prevents further delivery attempts of matching webhooks.
2212func (Admin) HookCancel(ctx context.Context, filter queue.HookFilter) (affected int) {
2213 log := pkglog.WithContext(ctx)
2214 n, err := queue.HookCancel(ctx, log, filter)
2215 xcheckf(ctx, err, "cancel hooks in queue")
2219// LogLevels returns the current log levels.
2220func (Admin) LogLevels(ctx context.Context) map[string]string {
2221 m := map[string]string{}
2222 for pkg, level := range mox.Conf.LogLevels() {
2223 s, ok := mlog.LevelStrings[level]
2232// LogLevelSet sets a log level for a package.
2233func (Admin) LogLevelSet(ctx context.Context, pkg string, levelStr string) {
2234 level, ok := mlog.Levels[levelStr]
2236 xcheckuserf(ctx, errors.New("unknown"), "lookup level")
2238 mox.Conf.LogLevelSet(pkglog.WithContext(ctx), pkg, level)
2241// LogLevelRemove removes a log level for a package, which cannot be the empty string.
2242func (Admin) LogLevelRemove(ctx context.Context, pkg string) {
2243 mox.Conf.LogLevelRemove(pkglog.WithContext(ctx), pkg)
2246// CheckUpdatesEnabled returns whether checking for updates is enabled.
2247func (Admin) CheckUpdatesEnabled(ctx context.Context) bool {
2248 return mox.Conf.Static.CheckUpdates
2251// WebserverConfig is the combination of WebDomainRedirects and WebHandlers
2252// from the domains.conf configuration file.
2253type WebserverConfig struct {
2254 WebDNSDomainRedirects [][2]dns.Domain // From server to frontend.
2255 WebDomainRedirects [][2]string // From frontend to server, it's not convenient to create dns.Domain in the frontend.
2256 WebHandlers []config.WebHandler
2259// WebserverConfig returns the current webserver config
2260func (Admin) WebserverConfig(ctx context.Context) (conf WebserverConfig) {
2261 conf = webserverConfig()
2262 conf.WebDomainRedirects = nil
2266func webserverConfig() WebserverConfig {
2267 conf := mox.Conf.DynamicConfig()
2268 r := conf.WebDNSDomainRedirects
2269 l := conf.WebHandlers
2271 x := make([][2]dns.Domain, 0, len(r))
2272 xs := make([][2]string, 0, len(r))
2273 for k, v := range r {
2274 x = append(x, [2]dns.Domain{k, v})
2275 xs = append(xs, [2]string{k.Name(), v.Name()})
2277 sort.Slice(x, func(i, j int) bool {
2278 return x[i][0].ASCII < x[j][0].ASCII
2280 sort.Slice(xs, func(i, j int) bool {
2281 return xs[i][0] < xs[j][0]
2283 return WebserverConfig{x, xs, l}
2286// WebserverConfigSave saves a new webserver config. If oldConf is not equal to
2287// the current config, an error is returned.
2288func (Admin) WebserverConfigSave(ctx context.Context, oldConf, newConf WebserverConfig) (savedConf WebserverConfig) {
2289 current := webserverConfig()
2290 webhandlersEqual := func() bool {
2291 if len(current.WebHandlers) != len(oldConf.WebHandlers) {
2294 for i, wh := range current.WebHandlers {
2295 if !wh.Equal(oldConf.WebHandlers[i]) {
2301 if !reflect.DeepEqual(oldConf.WebDNSDomainRedirects, current.WebDNSDomainRedirects) || !webhandlersEqual() {
2302 xcheckuserf(ctx, errors.New("config has changed"), "comparing old/current config")
2305 // Convert to map, check that there are no duplicates here. The canonicalized
2306 // dns.Domain are checked again for uniqueness when parsing the config before
2308 domainRedirects := map[string]string{}
2309 for _, x := range newConf.WebDomainRedirects {
2310 if _, ok := domainRedirects[x[0]]; ok {
2311 xcheckuserf(ctx, errors.New("already present"), "checking redirect %s", x[0])
2313 domainRedirects[x[0]] = x[1]
2316 err := admin.ConfigSave(ctx, func(conf *config.Dynamic) {
2317 conf.WebDomainRedirects = domainRedirects
2318 conf.WebHandlers = newConf.WebHandlers
2320 xcheckf(ctx, err, "saving webserver config")
2322 savedConf = webserverConfig()
2323 savedConf.WebDomainRedirects = nil
2327// Transports returns the configured transports, for sending email.
2328func (Admin) Transports(ctx context.Context) map[string]config.Transport {
2329 return mox.Conf.Static.Transports
2332// DMARCEvaluationStats returns a map of all domains with evaluations to a count of
2333// the evaluations and whether those evaluations will cause a report to be sent.
2334func (Admin) DMARCEvaluationStats(ctx context.Context) map[string]dmarcdb.EvaluationStat {
2335 stats, err := dmarcdb.EvaluationStats(ctx)
2336 xcheckf(ctx, err, "get evaluation stats")
2340// DMARCEvaluationsDomain returns all evaluations for aggregate reports for the
2341// domain, sorted from oldest to most recent.
2342func (Admin) DMARCEvaluationsDomain(ctx context.Context, domain string) (dns.Domain, []dmarcdb.Evaluation) {
2343 dom, err := dns.ParseDomain(domain)
2344 xcheckf(ctx, err, "parsing domain")
2346 evals, err := dmarcdb.EvaluationsDomain(ctx, dom)
2347 xcheckf(ctx, err, "get evaluations for domain")
2351// DMARCRemoveEvaluations removes evaluations for a domain.
2352func (Admin) DMARCRemoveEvaluations(ctx context.Context, domain string) {
2353 dom, err := dns.ParseDomain(domain)
2354 xcheckf(ctx, err, "parsing domain")
2356 err = dmarcdb.RemoveEvaluationsDomain(ctx, dom)
2357 xcheckf(ctx, err, "removing evaluations for domain")
2360// DMARCSuppressAdd adds a reporting address to the suppress list. Outgoing
2361// reports will be suppressed for a period.
2362func (Admin) DMARCSuppressAdd(ctx context.Context, reportingAddress string, until time.Time, comment string) {
2363 addr, err := smtp.ParseAddress(reportingAddress)
2364 xcheckuserf(ctx, err, "parsing reporting address")
2366 ba := dmarcdb.SuppressAddress{ReportingAddress: addr.String(), Until: until, Comment: comment}
2367 err = dmarcdb.SuppressAdd(ctx, &ba)
2368 xcheckf(ctx, err, "adding address to suppresslist")
2371// DMARCSuppressList returns all reporting addresses on the suppress list.
2372func (Admin) DMARCSuppressList(ctx context.Context) []dmarcdb.SuppressAddress {
2373 l, err := dmarcdb.SuppressList(ctx)
2374 xcheckf(ctx, err, "listing reporting addresses in suppresslist")
2378// DMARCSuppressRemove removes a reporting address record from the suppress list.
2379func (Admin) DMARCSuppressRemove(ctx context.Context, id int64) {
2380 err := dmarcdb.SuppressRemove(ctx, id)
2381 xcheckf(ctx, err, "removing reporting address from suppresslist")
2384// DMARCSuppressExtend updates the until field of a suppressed reporting address record.
2385func (Admin) DMARCSuppressExtend(ctx context.Context, id int64, until time.Time) {
2386 err := dmarcdb.SuppressUpdate(ctx, id, until)
2387 xcheckf(ctx, err, "updating reporting address in suppresslist")
2390// TLSRPTResults returns all TLSRPT results in the database.
2391func (Admin) TLSRPTResults(ctx context.Context) []tlsrptdb.TLSResult {
2392 results, err := tlsrptdb.Results(ctx)
2393 xcheckf(ctx, err, "get results")
2397// TLSRPTResultsPolicyDomain returns the TLS results for a domain.
2398func (Admin) TLSRPTResultsDomain(ctx context.Context, isRcptDom bool, policyDomain string) (dns.Domain, []tlsrptdb.TLSResult) {
2399 dom, err := dns.ParseDomain(policyDomain)
2400 xcheckf(ctx, err, "parsing domain")
2403 results, err := tlsrptdb.ResultsRecipientDomain(ctx, dom)
2404 xcheckf(ctx, err, "get result for recipient domain")
2407 results, err := tlsrptdb.ResultsPolicyDomain(ctx, dom)
2408 xcheckf(ctx, err, "get result for policy domain")
2412// LookupTLSRPTRecord looks up a TLSRPT record and returns the parsed form, original txt
2413// form from DNS, and error with the TLSRPT record as a string.
2414func (Admin) LookupTLSRPTRecord(ctx context.Context, domain string) (record *TLSRPTRecord, txt string, errstr string) {
2415 log := pkglog.WithContext(ctx)
2416 dom, err := dns.ParseDomain(domain)
2417 xcheckf(ctx, err, "parsing domain")
2419 resolver := dns.StrictResolver{Pkg: "webadmin", Log: log.Logger}
2420 r, txt, err := tlsrpt.Lookup(ctx, log.Logger, resolver, dom)
2421 if err != nil && (errors.Is(err, tlsrpt.ErrNoRecord) || errors.Is(err, tlsrpt.ErrMultipleRecords) || errors.Is(err, tlsrpt.ErrRecordSyntax) || errors.Is(err, tlsrpt.ErrDNS)) {
2422 errstr = err.Error()
2425 xcheckf(ctx, err, "fetching tlsrpt record")
2428 record = &TLSRPTRecord{Record: *r}
2431 return record, txt, errstr
2434// TLSRPTRemoveResults removes the TLS results for a domain for the given day. If
2435// day is empty, all results are removed.
2436func (Admin) TLSRPTRemoveResults(ctx context.Context, isRcptDom bool, domain string, day string) {
2437 dom, err := dns.ParseDomain(domain)
2438 xcheckf(ctx, err, "parsing domain")
2441 err = tlsrptdb.RemoveResultsRecipientDomain(ctx, dom, day)
2442 xcheckf(ctx, err, "removing tls results")
2444 err = tlsrptdb.RemoveResultsPolicyDomain(ctx, dom, day)
2445 xcheckf(ctx, err, "removing tls results")
2449// TLSRPTSuppressAdd adds a reporting address to the suppress list. Outgoing
2450// reports will be suppressed for a period.
2451func (Admin) TLSRPTSuppressAdd(ctx context.Context, reportingAddress string, until time.Time, comment string) {
2452 addr, err := smtp.ParseAddress(reportingAddress)
2453 xcheckuserf(ctx, err, "parsing reporting address")
2455 ba := tlsrptdb.SuppressAddress{ReportingAddress: addr.String(), Until: until, Comment: comment}
2456 err = tlsrptdb.SuppressAdd(ctx, &ba)
2457 xcheckf(ctx, err, "adding address to suppresslist")
2460// TLSRPTSuppressList returns all reporting addresses on the suppress list.
2461func (Admin) TLSRPTSuppressList(ctx context.Context) []tlsrptdb.SuppressAddress {
2462 l, err := tlsrptdb.SuppressList(ctx)
2463 xcheckf(ctx, err, "listing reporting addresses in suppresslist")
2467// TLSRPTSuppressRemove removes a reporting address record from the suppress list.
2468func (Admin) TLSRPTSuppressRemove(ctx context.Context, id int64) {
2469 err := tlsrptdb.SuppressRemove(ctx, id)
2470 xcheckf(ctx, err, "removing reporting address from suppresslist")
2473// TLSRPTSuppressExtend updates the until field of a suppressed reporting address record.
2474func (Admin) TLSRPTSuppressExtend(ctx context.Context, id int64, until time.Time) {
2475 err := tlsrptdb.SuppressUpdate(ctx, id, until)
2476 xcheckf(ctx, err, "updating reporting address in suppresslist")
2479// LookupCid turns an ID from a Received header into a cid as used in logging.
2480func (Admin) LookupCid(ctx context.Context, recvID string) (cid string) {
2481 v, err := mox.ReceivedToCid(recvID)
2482 xcheckf(ctx, err, "received id to cid")
2483 return fmt.Sprintf("%x", v)
2486// Config returns the dynamic config.
2487func (Admin) Config(ctx context.Context) config.Dynamic {
2488 return mox.Conf.DynamicConfig()
2491// AccountRoutesSave saves routes for an account.
2492func (Admin) AccountRoutesSave(ctx context.Context, accountName string, routes []config.Route) {
2493 err := admin.AccountSave(ctx, accountName, func(acc *config.Account) {
2496 xcheckf(ctx, err, "saving account routes")
2499// DomainRoutesSave saves routes for a domain.
2500func (Admin) DomainRoutesSave(ctx context.Context, domainName string, routes []config.Route) {
2501 err := admin.DomainSave(ctx, domainName, func(domain *config.Domain) error {
2502 domain.Routes = routes
2505 xcheckf(ctx, err, "saving domain routes")
2508// RoutesSave saves global routes.
2509func (Admin) RoutesSave(ctx context.Context, routes []config.Route) {
2510 err := admin.ConfigSave(ctx, func(config *config.Dynamic) {
2511 config.Routes = routes
2513 xcheckf(ctx, err, "saving global routes")
2516// DomainDescriptionSave saves the description for a domain.
2517func (Admin) DomainDescriptionSave(ctx context.Context, domainName, descr string) {
2518 err := admin.DomainSave(ctx, domainName, func(domain *config.Domain) error {
2519 domain.Description = descr
2522 xcheckf(ctx, err, "saving domain description")
2525// DomainClientSettingsDomainSave saves the client settings domain for a domain.
2526func (Admin) DomainClientSettingsDomainSave(ctx context.Context, domainName, clientSettingsDomain string) {
2527 err := admin.DomainSave(ctx, domainName, func(domain *config.Domain) error {
2528 domain.ClientSettingsDomain = clientSettingsDomain
2531 xcheckf(ctx, err, "saving client settings domain")
2534// DomainLocalpartConfigSave saves the localpart catchall and case-sensitive
2535// settings for a domain.
2536func (Admin) DomainLocalpartConfigSave(ctx context.Context, domainName string, localpartCatchallSeparators []string, localpartCaseSensitive bool) {
2537 err := admin.DomainSave(ctx, domainName, func(domain *config.Domain) error {
2538 domain.LocalpartCatchallSeparatorsEffective = localpartCatchallSeparators
2539 // If there is a single separator, we prefer the non-list form, it's easier to
2540 // read/edit and should suffice for most setups.
2541 domain.LocalpartCatchallSeparator = ""
2542 domain.LocalpartCatchallSeparators = nil
2543 if len(localpartCatchallSeparators) == 1 {
2544 domain.LocalpartCatchallSeparator = localpartCatchallSeparators[0]
2546 domain.LocalpartCatchallSeparators = localpartCatchallSeparators
2549 domain.LocalpartCaseSensitive = localpartCaseSensitive
2552 xcheckf(ctx, err, "saving localpart settings for domain")
2555// DomainDMARCAddressSave saves the DMARC reporting address/processing
2556// configuration for a domain. If localpart is empty, processing reports is
2558func (Admin) DomainDMARCAddressSave(ctx context.Context, domainName, localpart, domain, account, mailbox string) {
2559 err := admin.DomainSave(ctx, domainName, func(d *config.Domain) error {
2560 if localpart == "" {
2563 d.DMARC = &config.DMARC{
2564 Localpart: localpart,
2572 xcheckf(ctx, err, "saving dmarc reporting address/settings for domain")
2575// DomainTLSRPTAddressSave saves the TLS reporting address/processing
2576// configuration for a domain. If localpart is empty, processing reports is
2578func (Admin) DomainTLSRPTAddressSave(ctx context.Context, domainName, localpart, domain, account, mailbox string) {
2579 err := admin.DomainSave(ctx, domainName, func(d *config.Domain) error {
2580 if localpart == "" {
2583 d.TLSRPT = &config.TLSRPT{
2584 Localpart: localpart,
2592 xcheckf(ctx, err, "saving tls reporting address/settings for domain")
2595// DomainMTASTSSave saves the MTASTS policy for a domain. If policyID is empty,
2596// no MTASTS policy is served.
2597func (Admin) DomainMTASTSSave(ctx context.Context, domainName, policyID string, mode mtasts.Mode, maxAge time.Duration, mx []string) {
2598 err := admin.DomainSave(ctx, domainName, func(d *config.Domain) error {
2602 d.MTASTS = &config.MTASTS{
2611 xcheckf(ctx, err, "saving mtasts policy for domain")
2614// DomainDKIMAdd adds a DKIM selector for a domain, generating a new private
2615// key. The selector is not enabled for signing.
2616func (Admin) DomainDKIMAdd(ctx context.Context, domainName, selector, algorithm, hash string, headerRelaxed, bodyRelaxed, seal bool, headers []string, lifetime time.Duration) {
2617 d, err := dns.ParseDomain(domainName)
2618 xcheckuserf(ctx, err, "parsing domain")
2619 s, err := dns.ParseDomain(selector)
2620 xcheckuserf(ctx, err, "parsing selector")
2621 err = admin.DKIMAdd(ctx, d, s, algorithm, hash, headerRelaxed, bodyRelaxed, seal, headers, lifetime)
2622 xcheckf(ctx, err, "adding dkim key")
2625// DomainDKIMRemove removes a DKIM selector for a domain.
2626func (Admin) DomainDKIMRemove(ctx context.Context, domainName, selector string) {
2627 d, err := dns.ParseDomain(domainName)
2628 xcheckuserf(ctx, err, "parsing domain")
2629 s, err := dns.ParseDomain(selector)
2630 xcheckuserf(ctx, err, "parsing selector")
2631 err = admin.DKIMRemove(ctx, d, s)
2632 xcheckf(ctx, err, "removing dkim key")
2635// DomainDKIMSave saves the settings of selectors, and which to enable for
2636// signing, for a domain. All currently configured selectors must be present,
2637// selectors cannot be added/removed with this function.
2638func (Admin) DomainDKIMSave(ctx context.Context, domainName string, selectors map[string]config.Selector, sign []string) {
2639 for _, s := range sign {
2640 if _, ok := selectors[s]; !ok {
2641 xcheckuserf(ctx, fmt.Errorf("cannot sign unknown selector %q", s), "checking selectors")
2645 err := admin.DomainSave(ctx, domainName, func(d *config.Domain) error {
2646 if len(selectors) != len(d.DKIM.Selectors) {
2647 xcheckuserf(ctx, fmt.Errorf("cannot add/remove dkim selectors with this function"), "checking selectors")
2649 for s := range selectors {
2650 if _, ok := d.DKIM.Selectors[s]; !ok {
2651 xcheckuserf(ctx, fmt.Errorf("unknown selector %q", s), "checking selectors")
2654 // At least the selectors are the same.
2656 // Build up new selectors.
2657 sels := map[string]config.Selector{}
2658 for name, nsel := range selectors {
2659 osel := d.DKIM.Selectors[name]
2660 xsel := config.Selector{
2662 Canonicalization: nsel.Canonicalization,
2663 DontSealHeaders: nsel.DontSealHeaders,
2664 Expiration: nsel.Expiration,
2666 PrivateKeyFile: osel.PrivateKeyFile,
2668 if !slices.Equal(osel.HeadersEffective, nsel.Headers) {
2669 xsel.Headers = nsel.Headers
2674 // Enable the new selector settings.
2675 d.DKIM = config.DKIM{
2681 xcheckf(ctx, err, "saving dkim selector for domain")
2684// DomainDisabledSave saves the Disabled field of a domain. A disabled domain
2685// rejects incoming/outgoing messages involving the domain and does not request new
2686// TLS certificats with ACME.
2687func (Admin) DomainDisabledSave(ctx context.Context, domainName string, disabled bool) {
2688 err := admin.DomainSave(ctx, domainName, func(d *config.Domain) error {
2689 d.Disabled = disabled
2692 xcheckf(ctx, err, "saving disabled setting for domain")
2695func xparseAddress(ctx context.Context, lp, domain string) smtp.Address {
2696 xlp, err := smtp.ParseLocalpart(lp)
2697 xcheckuserf(ctx, err, "parsing localpart")
2698 d, err := dns.ParseDomain(domain)
2699 xcheckuserf(ctx, err, "parsing domain")
2700 return smtp.NewAddress(xlp, d)
2703func (Admin) AliasAdd(ctx context.Context, aliaslp string, domainName string, alias config.Alias) {
2704 addr := xparseAddress(ctx, aliaslp, domainName)
2705 err := admin.AliasAdd(ctx, addr, alias)
2706 xcheckf(ctx, err, "adding alias")
2709func (Admin) AliasUpdate(ctx context.Context, aliaslp string, domainName string, postPublic, listMembers, allowMsgFrom bool) {
2710 addr := xparseAddress(ctx, aliaslp, domainName)
2711 alias := config.Alias{
2712 PostPublic: postPublic,
2713 ListMembers: listMembers,
2714 AllowMsgFrom: allowMsgFrom,
2716 err := admin.AliasUpdate(ctx, addr, alias)
2717 xcheckf(ctx, err, "saving alias")
2720func (Admin) AliasRemove(ctx context.Context, aliaslp string, domainName string) {
2721 addr := xparseAddress(ctx, aliaslp, domainName)
2722 err := admin.AliasRemove(ctx, addr)
2723 xcheckf(ctx, err, "removing alias")
2726func (Admin) AliasAddressesAdd(ctx context.Context, aliaslp string, domainName string, addresses []string) {
2727 addr := xparseAddress(ctx, aliaslp, domainName)
2728 err := admin.AliasAddressesAdd(ctx, addr, addresses)
2729 xcheckf(ctx, err, "adding address to alias")
2732func (Admin) AliasAddressesRemove(ctx context.Context, aliaslp string, domainName string, addresses []string) {
2733 addr := xparseAddress(ctx, aliaslp, domainName)
2734 err := admin.AliasAddressesRemove(ctx, addr, addresses)
2735 xcheckf(ctx, err, "removing address from alias")
2738func (Admin) TLSPublicKeys(ctx context.Context, accountOpt string) ([]store.TLSPublicKey, error) {
2739 return store.TLSPublicKeyList(ctx, accountOpt)
2742func (Admin) LoginAttempts(ctx context.Context, accountName string, limit int) []store.LoginAttempt {
2743 l, err := store.LoginAttemptList(ctx, accountName, limit)
2744 xcheckf(ctx, err, "listing login attempts")