1// Package webadmin is a web app for the mox administrator for viewing and changing
2// the configuration, like creating/removing accounts, viewing DMARC and TLS
3// reports, check DNS records for a domain, change the webserver configuration,
13 cryptorand "crypto/rand"
40 "golang.org/x/text/unicode/norm"
42 "github.com/mjl-/adns"
44 "github.com/mjl-/bstore"
45 "github.com/mjl-/sherpa"
46 "github.com/mjl-/sherpadoc"
47 "github.com/mjl-/sherpaprom"
49 "github.com/mjl-/mox/admin"
50 "github.com/mjl-/mox/config"
51 "github.com/mjl-/mox/dkim"
52 "github.com/mjl-/mox/dmarc"
53 "github.com/mjl-/mox/dmarcdb"
54 "github.com/mjl-/mox/dmarcrpt"
55 "github.com/mjl-/mox/dns"
56 "github.com/mjl-/mox/dnsbl"
57 "github.com/mjl-/mox/metrics"
58 "github.com/mjl-/mox/mlog"
59 mox "github.com/mjl-/mox/mox-"
60 "github.com/mjl-/mox/moxvar"
61 "github.com/mjl-/mox/mtasts"
62 "github.com/mjl-/mox/mtastsdb"
63 "github.com/mjl-/mox/publicsuffix"
64 "github.com/mjl-/mox/queue"
65 "github.com/mjl-/mox/smtp"
66 "github.com/mjl-/mox/spf"
67 "github.com/mjl-/mox/store"
68 "github.com/mjl-/mox/tlsrpt"
69 "github.com/mjl-/mox/tlsrptdb"
70 "github.com/mjl-/mox/webauth"
73var pkglog = mlog.New("webadmin", nil)
76var adminapiJSON []byte
84var webadminFile = &mox.WebappFile{
87 HTMLPath: filepath.FromSlash("webadmin/admin.html"),
88 JSPath: filepath.FromSlash("webadmin/admin.js"),
89 CustomStem: "webadmin",
92var adminDoc = mustParseAPI("admin", adminapiJSON)
94func mustParseAPI(api string, buf []byte) (doc sherpadoc.Section) {
95 err := json.Unmarshal(buf, &doc)
97 pkglog.Fatalx("parsing webadmin api docs", err, slog.String("api", api))
102var sherpaHandlerOpts *sherpa.HandlerOpts
104func makeSherpaHandler(cookiePath string, isForwarded bool) (http.Handler, error) {
105 return sherpa.NewHandler("/api/", moxvar.Version, Admin{cookiePath, isForwarded}, &adminDoc, sherpaHandlerOpts)
109 collector, err := sherpaprom.NewCollector("moxadmin", nil)
111 pkglog.Fatalx("creating sherpa prometheus collector", err)
114 sherpaHandlerOpts = &sherpa.HandlerOpts{Collector: collector, AdjustFunctionNames: "none", NoCORS: true}
116 _, err = makeSherpaHandler("", false)
118 pkglog.Fatalx("sherpa handler", err)
121 mox.NewWebadminHandler = func(basePath string, isForwarded bool) http.Handler {
122 return http.HandlerFunc(Handler(basePath, isForwarded))
126// Handler returns a handler for the webadmin endpoints, customized for the
128func Handler(cookiePath string, isForwarded bool) func(w http.ResponseWriter, r *http.Request) {
129 sh, err := makeSherpaHandler(cookiePath, isForwarded)
130 return func(w http.ResponseWriter, r *http.Request) {
132 http.Error(w, "500 - internal server error - cannot handle requests", http.StatusInternalServerError)
135 handle(sh, isForwarded, w, r)
139// Admin exports web API functions for the admin web interface. All its methods are
140// exported under api/. Function calls require valid HTTP Authentication
141// credentials of a user.
143 cookiePath string // From listener, for setting authentication cookies.
144 isForwarded bool // From listener, whether we look at X-Forwarded-* headers.
149var requestInfoCtxKey ctxKey = "requestInfo"
151type requestInfo struct {
152 SessionToken store.SessionToken
153 Response http.ResponseWriter
154 Request *http.Request // For Proto and TLS connection state during message submit.
157func handle(apiHandler http.Handler, isForwarded bool, w http.ResponseWriter, r *http.Request) {
158 ctx := context.WithValue(r.Context(), mlog.CidKey, mox.Cid())
159 log := pkglog.WithContext(ctx).With(slog.String("adminauth", ""))
161 // HTML/JS can be retrieved without authentication.
162 if r.URL.Path == "/" {
165 webadminFile.Serve(ctx, log, w, r)
167 http.Error(w, "405 - method not allowed - use get", http.StatusMethodNotAllowed)
170 } else if r.URL.Path == "/licenses.txt" {
173 w.Header().Set("Content-Type", "text/plain; charset=utf-8")
176 http.Error(w, "405 - method not allowed - use get", http.StatusMethodNotAllowed)
181 isAPI := strings.HasPrefix(r.URL.Path, "/api/")
182 // Only allow POST for calls, they will not work cross-domain without CORS.
183 if isAPI && r.URL.Path != "/api/" && r.Method != "POST" {
184 http.Error(w, "405 - method not allowed - use post", http.StatusMethodNotAllowed)
188 // All other URLs, except the login endpoint require some authentication.
189 var sessionToken store.SessionToken
190 if r.URL.Path != "/api/LoginPrep" && r.URL.Path != "/api/Login" {
192 _, sessionToken, _, ok = webauth.Check(ctx, log, webauth.Admin, "webadmin", isForwarded, w, r, isAPI, isAPI, false)
194 // Response has been written already.
200 reqInfo := requestInfo{sessionToken, w, r}
201 ctx = context.WithValue(ctx, requestInfoCtxKey, reqInfo)
202 apiHandler.ServeHTTP(w, r.WithContext(ctx))
209func xcheckf(ctx context.Context, err error, format string, args ...any) {
213 // If caller tried saving a config that is invalid, or because of a bad request, cause a user error.
214 if errors.Is(err, mox.ErrConfig) || errors.Is(err, admin.ErrRequest) {
215 xcheckuserf(ctx, err, format, args...)
218 msg := fmt.Sprintf(format, args...)
219 errmsg := fmt.Sprintf("%s: %s", msg, err)
220 pkglog.WithContext(ctx).Errorx(msg, err)
221 code := "server:error"
222 if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
225 panic(&sherpa.Error{Code: code, Message: errmsg})
228func xcheckuserf(ctx context.Context, err error, format string, args ...any) {
232 msg := fmt.Sprintf(format, args...)
233 errmsg := fmt.Sprintf("%s: %s", msg, err)
234 pkglog.WithContext(ctx).Errorx(msg, err)
235 panic(&sherpa.Error{Code: "user:error", Message: errmsg})
238func xusererrorf(ctx context.Context, format string, args ...any) {
239 msg := fmt.Sprintf(format, args...)
240 pkglog.WithContext(ctx).Error(msg)
241 panic(&sherpa.Error{Code: "user:error", Message: msg})
244// LoginPrep returns a login token, and also sets it as cookie. Both must be
245// present in the call to Login.
246func (w Admin) LoginPrep(ctx context.Context) string {
247 log := pkglog.WithContext(ctx)
248 reqInfo := ctx.Value(requestInfoCtxKey).(requestInfo)
251 cryptorand.Read(data[:])
252 loginToken := base64.RawURLEncoding.EncodeToString(data[:])
254 webauth.LoginPrep(ctx, log, "webadmin", w.cookiePath, w.isForwarded, reqInfo.Response, reqInfo.Request, loginToken)
259// Login returns a session token for the credentials, or fails with error code
260// "user:badLogin". Call LoginPrep to get a loginToken.
261func (w Admin) Login(ctx context.Context, loginToken, password string) store.CSRFToken {
262 log := pkglog.WithContext(ctx)
263 reqInfo := ctx.Value(requestInfoCtxKey).(requestInfo)
265 csrfToken, err := webauth.Login(ctx, log, webauth.Admin, "webadmin", w.cookiePath, w.isForwarded, reqInfo.Response, reqInfo.Request, loginToken, "", password)
266 if _, ok := err.(*sherpa.Error); ok {
269 xcheckf(ctx, err, "login")
273// Logout invalidates the session token.
274func (w Admin) Logout(ctx context.Context) {
275 log := pkglog.WithContext(ctx)
276 reqInfo := ctx.Value(requestInfoCtxKey).(requestInfo)
278 err := webauth.Logout(ctx, log, webauth.Admin, "webadmin", w.cookiePath, w.isForwarded, reqInfo.Response, reqInfo.Request, "", reqInfo.SessionToken)
279 xcheckf(ctx, err, "logout")
282// Version returns the version, goos and goarch.
283func (w Admin) Version(ctx context.Context) (version, goos, goarch string) {
284 return moxvar.Version, runtime.GOOS, runtime.GOARCH
290 Instructions []string
293type DNSSECResult struct {
297type IPRevCheckResult struct {
298 Hostname dns.Domain // This hostname, IPs must resolve back to this.
299 IPNames map[string][]string // IP to names.
309type MXCheckResult struct {
314type TLSCheckResult struct {
318type DANECheckResult struct {
322type SPFRecord struct {
326type SPFCheckResult struct {
328 DomainRecord *SPFRecord
330 HostRecord *SPFRecord
334type DKIMCheckResult struct {
339type DKIMRecord struct {
345type DMARCRecord struct {
349type DMARCCheckResult struct {
356type TLSRPTRecord struct {
360type TLSRPTCheckResult struct {
366type MTASTSRecord struct {
369type MTASTSCheckResult struct {
373 Policy *mtasts.Policy
377type SRVConfCheckResult struct {
378 SRVs map[string][]net.SRV // Service (e.g. "_imaps") to records.
382type AutoconfCheckResult struct {
383 ClientSettingsDomainIPs []string
388type AutodiscoverSRV struct {
393type AutodiscoverCheckResult struct {
394 Records []AutodiscoverSRV
398// CheckResult is the analysis of a domain, its actual configuration (DNS, TLS,
399// connectivity) and the mox configuration. It includes configuration instructions
400// (e.g. DNS records), and warnings and errors encountered.
401type CheckResult struct {
404 IPRev IPRevCheckResult
410 DMARC DMARCCheckResult
411 HostTLSRPT TLSRPTCheckResult
412 DomainTLSRPT TLSRPTCheckResult
413 MTASTS MTASTSCheckResult
414 SRVConf SRVConfCheckResult
415 Autoconf AutoconfCheckResult
416 Autodiscover AutodiscoverCheckResult
419// logPanic can be called with a defer from a goroutine to prevent the entire program from being shutdown in case of a panic.
420func logPanic(ctx context.Context) {
425 pkglog.WithContext(ctx).Error("recover from panic", slog.Any("panic", x))
427 metrics.PanicInc(metrics.Webadmin)
430// return IPs we may be listening on.
431func xlistenIPs(ctx context.Context, receiveOnly bool) []net.IP {
432 ips, err := mox.IPs(ctx, receiveOnly)
433 xcheckf(ctx, err, "listing ips")
437// return IPs from which we may be sending.
438func xsendingIPs(ctx context.Context) []net.IP {
439 ips, err := mox.IPs(ctx, false)
440 xcheckf(ctx, err, "listing ips")
444// CheckDomain checks the configuration for the domain, such as MX, SMTP STARTTLS,
445// SPF, DKIM, DMARC, TLSRPT, MTASTS, autoconfig, autodiscover.
446func (Admin) CheckDomain(ctx context.Context, domainName string) (r CheckResult) {
447 // todo future: should run these checks without a DNS cache so recent changes are picked up.
449 resolver := dns.StrictResolver{Pkg: "check", Log: pkglog.WithContext(ctx).Logger}
450 dialer := &net.Dialer{Timeout: 10 * time.Second}
451 nctx, cancel := context.WithTimeout(ctx, 30*time.Second)
453 return checkDomain(nctx, resolver, dialer, domainName)
456func unptr[T any](l []*T) []T {
460 r := make([]T, len(l))
461 for i, e := range l {
467func checkDomain(ctx context.Context, resolver dns.Resolver, dialer *net.Dialer, domainName string) (r CheckResult) {
468 log := pkglog.WithContext(ctx)
470 domain, xerr := dns.ParseDomain(domainName)
471 xcheckuserf(ctx, xerr, "parsing domain")
473 domConf, ok := mox.Conf.Domain(domain)
475 panic(&sherpa.Error{Code: "user:notFound", Message: "domain not found"})
478 listenIPs := xlistenIPs(ctx, true)
479 isListenIP := func(ip net.IP) bool {
480 for _, lip := range listenIPs {
488 addf := func(l *[]string, format string, args ...any) {
489 *l = append(*l, fmt.Sprintf(format, args...))
492 // Host must be an absolute dns name, ending with a dot.
493 lookupIPs := func(errors *[]string, host string) (ips []string, ourIPs, notOurIPs []net.IP, rerr error) {
494 addrs, _, err := resolver.LookupHost(ctx, host)
496 addf(errors, "Looking up %q: %s", host, err)
497 return nil, nil, nil, err
499 for _, addr := range addrs {
500 ip := net.ParseIP(addr)
502 addf(errors, "Bad IP %q", addr)
505 ips = append(ips, ip.String())
507 ourIPs = append(ourIPs, ip)
509 notOurIPs = append(notOurIPs, ip)
512 return ips, ourIPs, notOurIPs, nil
515 checkTLS := func(errors *[]string, host string, ips []string, port string) {
521 RootCAs: mox.Conf.Static.TLS.CertPool,
524 for _, ip := range ips {
525 conn, err := d.DialContext(ctx, "tcp", net.JoinHostPort(ip, port))
527 addf(errors, "TLS connection to hostname %q, IP %q: %s", host, ip, err)
530 log.Check(err, "closing tcp connection")
535 // If at least one listener with SMTP enabled has unspecified NATed IPs, we'll skip
536 // some checks related to these IPs.
537 var isNAT, isUnspecifiedNAT bool
538 for _, l := range mox.Conf.Static.Listeners {
543 isUnspecifiedNAT = true
546 if len(l.NATIPs) > 0 {
551 var wg sync.WaitGroup
559 // Some DNSSEC-verifying resolvers return unauthentic data for ".", so we check "com".
560 _, result, err := resolver.LookupNS(ctx, "com.")
562 addf(&r.DNSSEC.Errors, "Looking up NS for DNS root (.) to check support in resolver for DNSSEC-verification: %s", err)
563 } else if !result.Authentic {
564 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.`)
566 _, result, _ := resolver.LookupMX(ctx, domain.ASCII+".")
567 if !result.Authentic {
568 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.`)
572 addf(&r.DNSSEC.Instructions, `Enable DNSSEC-signing of the DNS records of your domain (zone) at your DNS hosting provider.`)
574 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".
576cat <<EOF >/etc/unbound/unbound.conf.d/ede.conf
590 // For each mox.Conf.SpecifiedSMTPListenIPs and all NATIPs, and each IP for
591 // mox.Conf.HostnameDomain, check if they resolve back to the host name.
592 hostIPs := map[dns.Domain][]net.IP{}
593 ips, _, err := resolver.LookupIP(ctx, "ip", mox.Conf.Static.HostnameDomain.ASCII+".")
595 addf(&r.IPRev.Errors, "Looking up IPs for hostname: %s", err)
598 gatherMoreIPs := func(publicIPs []net.IP) {
600 for _, ip := range publicIPs {
601 for _, xip := range ips {
606 ips = append(ips, ip)
610 gatherMoreIPs(mox.Conf.Static.SpecifiedSMTPListenIPs)
612 for _, l := range mox.Conf.Static.Listeners {
617 for _, ip := range l.NATIPs {
618 natips = append(natips, net.ParseIP(ip))
620 gatherMoreIPs(natips)
622 hostIPs[mox.Conf.Static.HostnameDomain] = ips
624 iplist := func(ips []net.IP) string {
626 for _, ip := range ips {
627 ipstrs = append(ipstrs, ip.String())
629 return strings.Join(ipstrs, ", ")
632 r.IPRev.Hostname = mox.Conf.Static.HostnameDomain
633 r.IPRev.Instructions = []string{
634 fmt.Sprintf("Ensure IPs %s have reverse address %s.", iplist(ips), mox.Conf.Static.HostnameDomain.ASCII),
637 // If we have a socks transport, also check its host and IP.
638 for tname, t := range mox.Conf.Static.Transports {
640 hostIPs[t.Socks.Hostname] = append(hostIPs[t.Socks.Hostname], t.Socks.IPs...)
641 instr := fmt.Sprintf("For SOCKS transport %s, ensure IPs %s have reverse address %s.", tname, iplist(t.Socks.IPs), t.Socks.Hostname)
642 r.IPRev.Instructions = append(r.IPRev.Instructions, instr)
652 results := make(chan result)
654 for host, ips := range hostIPs {
655 for _, ip := range ips {
660 addrs, _, err := resolver.LookupAddr(ctx, s)
661 results <- result{host, s, addrs, err}
665 r.IPRev.IPNames = map[string][]string{}
668 host, addrs, ip, err := lr.Host, lr.Addrs, lr.IP, lr.Err
670 addf(&r.IPRev.Errors, "Looking up reverse name for %s of %s: %v", ip, host, err)
674 for i, a := range addrs {
675 a = strings.TrimRight(a, ".")
677 ad, err := dns.ParseDomain(a)
679 addf(&r.IPRev.Errors, "Parsing reverse name %q for %s: %v", a, ip, err)
685 if !match && !isNAT && host == mox.Conf.Static.HostnameDomain {
686 addf(&r.IPRev.Warnings, "IP %s with name(s) %s is forward confirmed, but does not match hostname %s.", ip, strings.Join(addrs, ","), host)
688 r.IPRev.IPNames[ip] = addrs
691 // Linux machines are often initially set up with a loopback IP for the hostname in
692 // /etc/hosts, presumably because it isn't known if their external IPs are static.
693 // For mail servers, they should certainly be static. The quickstart would also
694 // have warned about this, but could have been missed/ignored.
695 for _, ip := range ips {
697 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())
708 mxs, _, err := resolver.LookupMX(ctx, domain.ASCII+".")
710 addf(&r.MX.Errors, "Looking up MX records for %s: %s", domain, err)
712 r.MX.Records = make([]MX, len(mxs))
713 for i, mx := range mxs {
714 r.MX.Records[i] = MX{mx.Host, int(mx.Pref), nil}
716 if len(mxs) == 1 && mxs[0].Host == "." {
717 addf(&r.MX.Errors, `MX records consists of explicit null mx record (".") indicating that domain does not accept email.`)
720 for i, mx := range mxs {
721 ips, ourIPs, notOurIPs, err := lookupIPs(&r.MX.Errors, mx.Host)
723 addf(&r.MX.Errors, "Looking up IPs for mx host %q: %s", mx.Host, err)
725 r.MX.Records[i].IPs = ips
726 if isUnspecifiedNAT {
729 if len(ourIPs) == 0 {
730 addf(&r.MX.Errors, "None of the IPs that mx %q points to is ours: %v", mx.Host, notOurIPs)
731 } else if len(notOurIPs) > 0 {
732 addf(&r.MX.Errors, "Some of the IPs that mx %q points to are not ours: %v", mx.Host, notOurIPs)
736 r.MX.Instructions = []string{
737 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+"."),
741 // TLS, mostly checking certificate expiration and CA trust.
742 // 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.
748 // MTA-STS, autoconfig, autodiscover are checked in their sections.
750 // Dial a single MX host with given IP and perform STARTTLS handshake.
751 dialSMTPSTARTTLS := func(host, ip string) error {
752 conn, err := dialer.DialContext(ctx, "tcp", net.JoinHostPort(ip, "25"))
759 log.Check(err, "closing tcp connection")
763 end := time.Now().Add(10 * time.Second)
764 cctx, cancel := context.WithTimeout(ctx, 10*time.Second)
766 err = conn.SetDeadline(end)
767 log.WithContext(ctx).Check(err, "setting deadline")
769 br := bufio.NewReader(conn)
770 _, err = br.ReadString('\n')
772 return fmt.Errorf("reading SMTP banner from remote: %s", err)
774 if _, err := fmt.Fprintf(conn, "EHLO moxtest\r\n"); err != nil {
775 return fmt.Errorf("writing SMTP EHLO to remote: %s", err)
778 line, err := br.ReadString('\n')
780 return fmt.Errorf("reading SMTP EHLO response from remote: %s", err)
782 if strings.HasPrefix(line, "250-") {
785 if strings.HasPrefix(line, "250 ") {
788 return fmt.Errorf("unexpected response to SMTP EHLO from remote: %q", strings.TrimSuffix(line, "\r\n"))
790 if _, err := fmt.Fprintf(conn, "STARTTLS\r\n"); err != nil {
791 return fmt.Errorf("writing SMTP STARTTLS to remote: %s", err)
793 line, err := br.ReadString('\n')
795 return fmt.Errorf("reading response to SMTP STARTTLS from remote: %s", err)
797 if !strings.HasPrefix(line, "220 ") {
798 return fmt.Errorf("SMTP STARTTLS response from remote not 220 OK: %q", strings.TrimSuffix(line, "\r\n"))
800 config := &tls.Config{
802 RootCAs: mox.Conf.Static.TLS.CertPool,
804 tlsconn := tls.Client(conn, config)
805 if err := tlsconn.HandshakeContext(cctx); err != nil {
806 return fmt.Errorf("TLS handshake after SMTP STARTTLS: %s", err)
810 log.Check(err, "closing smtp connection")
815 checkSMTPSTARTTLS := func() {
816 // Initial errors are ignored, will already have been warned about by MX checks.
817 mxs, _, err := resolver.LookupMX(ctx, domain.ASCII+".")
821 if len(mxs) == 1 && mxs[0].Host == "." {
824 for _, mx := range mxs {
825 ips, _, _, err := lookupIPs(&r.MX.Errors, mx.Host)
830 for _, ip := range ips {
831 if err := dialSMTPSTARTTLS(mx.Host, ip); err != nil {
832 addf(&r.TLS.Errors, "SMTP connection with STARTTLS to MX hostname %q IP %s: %s", mx.Host, ip, err)
848 daneRecords := func(l config.Listener) map[string]struct{} {
852 records := map[string]struct{}{}
853 addRecord := func(privKey crypto.Signer) {
854 spkiBuf, err := x509.MarshalPKIXPublicKey(privKey.Public())
856 addf(&r.DANE.Errors, "marshal SubjectPublicKeyInfo for DANE record: %v", err)
859 sum := sha256.Sum256(spkiBuf)
861 Usage: adns.TLSAUsageDANEEE,
862 Selector: adns.TLSASelectorSPKI,
863 MatchType: adns.TLSAMatchTypeSHA256,
866 records[r.Record()] = struct{}{}
868 for _, privKey := range l.TLS.HostPrivateRSA2048Keys {
871 for _, privKey := range l.TLS.HostPrivateECDSAP256Keys {
877 expectedDANERecords := func(host string) map[string]struct{} {
878 for _, l := range mox.Conf.Static.Listeners {
879 if l.HostnameDomain.ASCII == host {
880 return daneRecords(l)
883 public := mox.Conf.Static.Listeners["public"]
884 if mox.Conf.Static.HostnameDomain.ASCII == host && public.HostnameDomain.ASCII == "" {
885 return daneRecords(public)
890 mxl, result, err := resolver.LookupMX(ctx, domain.ASCII+".")
892 addf(&r.DANE.Errors, "Looking up MX hosts to check for DANE records: %s", err)
894 if !result.Authentic {
895 addf(&r.DANE.Warnings, "DANE is inactive because MX records are not DNSSEC-signed.")
897 for _, mx := range mxl {
898 expect := expectedDANERecords(mx.Host)
900 tlsal, tlsaResult, err := resolver.LookupTLSA(ctx, 25, "tcp", mx.Host+".")
901 if dns.IsNotFound(err) {
903 addf(&r.DANE.Errors, "No DANE records for MX host %s, expected: %s.", mx.Host, strings.Join(slices.Collect(maps.Keys(expect)), "; "))
906 } else if err != nil {
907 addf(&r.DANE.Errors, "Looking up DANE records for MX host %s: %v", mx.Host, err)
909 } else if !tlsaResult.Authentic && len(tlsal) > 0 {
910 addf(&r.DANE.Errors, "DANE records exist for MX host %s, but are not DNSSEC-signed.", mx.Host)
913 extra := map[string]struct{}{}
914 for _, e := range tlsal {
916 if _, ok := expect[s]; ok {
919 extra[s] = struct{}{}
923 l := slices.Sorted(maps.Keys(expect))
924 addf(&r.DANE.Errors, "Missing DANE records of type TLSA for MX host _25._tcp.%s: %s", mx.Host, strings.Join(l, "; "))
927 l := slices.Sorted(maps.Keys(extra))
928 addf(&r.DANE.Errors, "Unexpected DANE records of type TLSA for MX host _25._tcp.%s: %s", mx.Host, strings.Join(l, "; "))
933 public := mox.Conf.Static.Listeners["public"]
934 pubDom := public.HostnameDomain
935 if pubDom.ASCII == "" {
936 pubDom = mox.Conf.Static.HostnameDomain
938 records := slices.Sorted(maps.Keys(daneRecords(public)))
939 if len(records) > 0 {
940 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"
941 for _, r := range records {
942 instr += fmt.Sprintf("\t_25._tcp.%s. TLSA %s\n", pubDom.ASCII, r)
944 addf(&r.DANE.Instructions, instr)
946 addf(&r.DANE.Warnings, "DANE not configured: no static TLS host keys.")
948 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."
949 addf(&r.DANE.Instructions, instr)
954 // 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.
960 ips := mox.DomainSPFIPs()
962 // Verify a domain with the configured IPs that do SMTP.
963 verifySPF := func(isHost bool, domain dns.Domain) (string, *SPFRecord, spf.Record) {
969 _, txt, record, _, err := spf.Lookup(ctx, log.Logger, resolver, domain)
971 addf(&r.SPF.Errors, "Looking up %s SPF record: %s", kind, err)
973 var xrecord *SPFRecord
975 xrecord = &SPFRecord{*record}
982 checkSPFIP := func(ip net.IP) {
987 spfr.Directives = append(spfr.Directives, spf.Directive{Mechanism: mechanism, IP: ip})
995 MailFromLocalpart: "postmaster",
996 MailFromDomain: domain,
997 HelloDomain: dns.IPDomain{Domain: domain},
998 LocalIP: net.ParseIP("127.0.0.1"),
999 LocalHostname: dns.Domain{ASCII: "localhost"},
1001 status, mechanism, expl, _, err := spf.Evaluate(ctx, log.Logger, record, resolver, args)
1003 addf(&r.SPF.Errors, "Evaluating IP %q against %s SPF record: %s", ip, kind, err)
1004 } else if status != spf.StatusPass {
1005 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)
1009 for _, ip := range ips {
1013 spfr.Directives = append(spfr.Directives, spf.Directive{Mechanism: "mx"})
1020 spfr.Directives = append(spfr.Directives, spf.Directive{Qualifier: qual, Mechanism: "all"})
1021 return txt, xrecord, spfr
1024 // Check SPF record for domain.
1025 var dspfr spf.Record
1026 r.SPF.DomainTXT, r.SPF.DomainRecord, dspfr = verifySPF(false, domain)
1027 // todo: possibly check all hosts for MX records? assuming they are also sending mail servers.
1028 r.SPF.HostTXT, r.SPF.HostRecord, _ = verifySPF(true, mox.Conf.Static.HostnameDomain)
1031 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.`)
1034 dtxt, err := dspfr.Record()
1036 addf(&r.SPF.Errors, "Making SPF record for instructions: %s", err)
1038 domainspf := fmt.Sprintf("%s TXT %s", domain.ASCII+".", mox.TXTStrings(dtxt))
1041 hostspf := fmt.Sprintf(`%s TXT "v=spf1 a -all"`, mox.Conf.Static.HostnameDomain.ASCII+".")
1043 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)
1047 // 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.
1053 var missing []string
1054 for sel, selc := range domConf.DKIM.Selectors {
1055 _, record, txt, _, err := dkim.Lookup(ctx, log.Logger, resolver, selc.Domain, domain)
1057 missing = append(missing, sel)
1058 if errors.Is(err, dkim.ErrNoRecord) {
1059 addf(&r.DKIM.Errors, "No DKIM DNS record for selector %q.", sel)
1060 } else if errors.Is(err, dkim.ErrSyntax) {
1061 addf(&r.DKIM.Errors, "Parsing DKIM DNS record for selector %q: %s", sel, err)
1063 addf(&r.DKIM.Errors, "Fetching DKIM record for selector %q: %s", sel, err)
1067 r.DKIM.Records = append(r.DKIM.Records, DKIMRecord{sel, txt, record})
1068 pubKey := selc.Key.Public()
1070 switch k := pubKey.(type) {
1071 case *rsa.PublicKey:
1073 pk, err = x509.MarshalPKIXPublicKey(k)
1075 addf(&r.DKIM.Errors, "Marshal public key for %q to compare against DNS: %s", sel, err)
1078 case ed25519.PublicKey:
1081 addf(&r.DKIM.Errors, "Internal error: unknown public key type %T.", pubKey)
1085 if record != nil && !bytes.Equal(record.Pubkey, pk) {
1086 addf(&r.DKIM.Errors, "For selector %q, the public key in DKIM DNS TXT record does not match with configured private key.", sel)
1087 missing = append(missing, sel)
1091 if len(domConf.DKIM.Selectors) == 0 {
1092 addf(&r.DKIM.Errors, "No DKIM configuration, add a key to the configuration file, and instructions for DNS records will appear here.")
1095 for _, sel := range missing {
1096 dkimr := dkim.Record{
1098 Hashes: []string{"sha256"},
1099 PublicKey: domConf.DKIM.Selectors[sel].Key.Public(),
1101 switch dkimr.PublicKey.(type) {
1102 case *rsa.PublicKey:
1103 case ed25519.PublicKey:
1104 dkimr.Key = "ed25519"
1106 addf(&r.DKIM.Errors, "Internal error: unknown public key type %T.", dkimr.PublicKey)
1108 txt, err := dkimr.Record()
1110 addf(&r.DKIM.Errors, "Making DKIM record for instructions: %s", err)
1113 instr += fmt.Sprintf("\n\t%s._domainkey.%s TXT %s\n", sel, domain.ASCII+".", mox.TXTStrings(txt))
1116 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
1117 addf(&r.DKIM.Instructions, "%s", instr)
1127 _, dmarcDomain, record, txt, _, err := dmarc.Lookup(ctx, log.Logger, resolver, domain)
1129 addf(&r.DMARC.Errors, "Looking up DMARC record: %s", err)
1130 } else if record == nil {
1131 addf(&r.DMARC.Errors, "No DMARC record")
1133 r.DMARC.Domain = dmarcDomain.Name()
1136 r.DMARC.Record = &DMARCRecord{*record}
1138 if record != nil && record.Policy == "none" {
1139 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.")
1141 if record != nil && record.SubdomainPolicy == "none" {
1142 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.")
1144 if record != nil && len(record.AggregateReportAddresses) == 0 {
1145 addf(&r.DMARC.Warnings, "It is recommended you specify you would like aggregate reports about delivery success in the DMARC record, see instructions.")
1148 dmarcr := dmarc.DefaultRecord
1149 dmarcr.Policy = "reject"
1152 if domConf.DMARC != nil {
1153 // If the domain is in a different Organizational Domain, the receiving domain
1154 // needs a special DNS record to opt-in to receiving reports. We check for that
1157 orgDom := publicsuffix.Lookup(ctx, log.Logger, domain)
1158 destOrgDom := publicsuffix.Lookup(ctx, log.Logger, domConf.DMARC.DNSDomain)
1159 if orgDom != destOrgDom {
1160 accepts, status, _, _, _, err := dmarc.LookupExternalReportsAccepted(ctx, log.Logger, resolver, domain, domConf.DMARC.DNSDomain)
1161 if status != dmarc.StatusNone {
1162 addf(&r.DMARC.Errors, "Checking if external destination accepts reports: %s", err)
1163 } else if !accepts {
1164 addf(&r.DMARC.Errors, "External destination does not accept reports (%s)", err)
1166 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)
1171 Opaque: smtp.NewAddress(domConf.DMARC.ParsedLocalpart, domConf.DMARC.DNSDomain).Pack(false),
1173 uristr := uri.String()
1174 dmarcr.AggregateReportAddresses = []dmarc.URI{
1175 {Address: uristr, MaxSize: 10, Unit: "m"},
1180 for _, addr := range record.AggregateReportAddresses {
1181 if addr.Address == uristr {
1187 addf(&r.DMARC.Errors, "Configured DMARC reporting address is not present in record.")
1191 addf(&r.DMARC.Instructions, `Configure a DMARC destination in domain in config file.`)
1193 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()))
1194 addf(&r.DMARC.Instructions, instr)
1196 addf(&r.DMARC.Instructions, extInstr)
1200 checkTLSRPT := func(result *TLSRPTCheckResult, dom dns.Domain, address smtp.Address, isHost bool) {
1204 record, txt, err := tlsrpt.Lookup(ctx, log.Logger, resolver, dom)
1206 addf(&result.Errors, "Looking up TLSRPT record for domain %s: %s", dom, err)
1210 result.Record = &TLSRPTRecord{*record}
1213 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.`
1214 var zeroaddr smtp.Address
1215 if address != zeroaddr {
1216 // TLSRPT does not require validation of reporting addresses outside the domain.
1220 Opaque: address.Pack(false),
1222 rua := tlsrpt.RUA(uri.String())
1223 tlsrptr := &tlsrpt.Record{
1224 Version: "TLSRPTv1",
1225 RUAs: [][]tlsrpt.RUA{{rua}},
1227 instr += fmt.Sprintf(`
1229Ensure a DNS TXT record like the following exists:
1231 _smtp._tls.%s TXT %s
1233`, dom.ASCII+".", mox.TXTStrings(tlsrptr.String()))
1238 for _, l := range record.RUAs {
1239 for _, e := range l {
1247 addf(&result.Errors, `Configured reporting address is not present in TLSRPT record.`)
1252 instr += fmt.Sprintf(`
1254Ensure the following snippet is present in mox.conf (ensure tabs are used for indenting, not spaces):
1261`, mox.Conf.Static.Postmaster.Account)
1262 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.`)
1264 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.`)
1266 addf(&result.Instructions, instr)
1271 var hostTLSRPTAddr smtp.Address
1272 if mox.Conf.Static.HostTLSRPT.Localpart != "" {
1273 hostTLSRPTAddr = smtp.NewAddress(mox.Conf.Static.HostTLSRPT.ParsedLocalpart, mox.Conf.Static.HostnameDomain)
1275 go checkTLSRPT(&r.HostTLSRPT, mox.Conf.Static.HostnameDomain, hostTLSRPTAddr, true)
1279 var domainTLSRPTAddr smtp.Address
1280 if domConf.TLSRPT != nil {
1281 domainTLSRPTAddr = smtp.NewAddress(domConf.TLSRPT.ParsedLocalpart, domain)
1283 go checkTLSRPT(&r.DomainTLSRPT, domain, domainTLSRPTAddr, false)
1291 // The admin has explicitly disabled mta-sts, keep warning about it.
1292 if domConf.MTASTS == nil {
1293 addf(&r.MTASTS.Warnings, "MTA-STS is not configured for this domain.")
1296 record, txt, err := mtasts.LookupRecord(ctx, log.Logger, resolver, domain)
1297 if err != nil && !(domConf.MTASTS == nil && errors.Is(err, mtasts.ErrNoRecord)) {
1298 addf(&r.MTASTS.Errors, "Looking up MTA-STS record: %s", err)
1302 r.MTASTS.Record = &MTASTSRecord{*record}
1305 policy, text, err := mtasts.FetchPolicy(ctx, log.Logger, domain)
1307 if !(domConf.MTASTS == nil && errors.Is(err, mtasts.ErrNoPolicy)) {
1308 addf(&r.MTASTS.Errors, "Fetching MTA-STS policy: %s", err)
1310 } else if policy.Mode == mtasts.ModeNone {
1311 addf(&r.MTASTS.Warnings, "MTA-STS policy is present, but does not require TLS.")
1312 } else if policy.Mode == mtasts.ModeTesting {
1313 addf(&r.MTASTS.Warnings, "MTA-STS policy is in testing mode, do not forget to change to mode enforce after testing period.")
1315 r.MTASTS.PolicyText = text
1316 r.MTASTS.Policy = policy
1317 if policy != nil && policy.Mode != mtasts.ModeNone {
1318 if !policy.Matches(mox.Conf.Static.HostnameDomain) {
1319 addf(&r.MTASTS.Warnings, "Configured hostname is missing from policy MX list.")
1321 if policy.MaxAgeSeconds <= 24*3600 {
1322 addf(&r.MTASTS.Warnings, "Policy has a MaxAge of less than 1 day. For stable configurations, the recommended period is in weeks.")
1325 mxl, _, _ := resolver.LookupMX(ctx, domain.ASCII+".")
1326 // We do not check for errors, the MX check will complain about mx errors, we assume we will get the same error here.
1327 mxs := map[dns.Domain]struct{}{}
1328 for _, mx := range mxl {
1329 d, err := dns.ParseDomain(strings.TrimSuffix(mx.Host, "."))
1331 addf(&r.MTASTS.Warnings, "MX record %q is invalid: %s", mx.Host, err)
1336 for mx := range mxs {
1337 if !policy.Matches(mx) {
1338 addf(&r.MTASTS.Warnings, "MX record %q does not match MTA-STS policy MX list.", mx)
1341 for _, mx := range policy.MX {
1345 if _, ok := mxs[mx.Domain]; !ok {
1346 addf(&r.MTASTS.Warnings, "MX %q in MTA-STS policy is not in MX record.", mx.LogString())
1351 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.
1353After 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.
1355You 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.
1357You 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.
1359The _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.
1361When enabling MTA-STS, or updating a policy, always update the policy first (through a configuration change and reload/restart), and the DNS record second.
1363 addf(&r.MTASTS.Instructions, intro)
1365 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.`)
1367 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+".")
1368 addf(&r.MTASTS.Instructions, host)
1370 mtastsr := mtasts.Record{
1372 ID: time.Now().Format("20060102T150405"),
1374 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())
1375 addf(&r.MTASTS.Instructions, dns)
1384 type srvReq struct {
1387 // First entry is host we suggest and prefer, but we won't complain if the current
1388 // value is one of the later values, to account for historic values we suggested
1389 // that aren't wrong and we don't want to bother admins with.
1395 // We'll assume if any submissions is configured, it is public. Same for imap. And
1396 // if not, that there is a plain option.
1397 var submissions, imaps bool
1398 for _, l := range mox.Conf.Static.Listeners {
1399 if l.TLS != nil && l.Submissions.Enabled {
1402 if l.TLS != nil && l.IMAPS.Enabled {
1406 srvhost := func(ok bool) []string {
1408 return []string{"."}
1410 if domConf.ClientSettingsDomain != "" {
1412 domConf.ClientSettingsDNSDomain.ASCII + ".",
1413 mox.Conf.Static.HostnameDomain.ASCII + ".",
1416 return []string{mox.Conf.Static.HostnameDomain.ASCII + "."}
1418 var reqs = []srvReq{
1419 {name: "_submissions", port: 465, host: srvhost(submissions)},
1420 {name: "_submission", port: 587, host: srvhost(!submissions)},
1421 {name: "_imaps", port: 993, host: srvhost(imaps)},
1422 {name: "_imap", port: 143, host: srvhost(!imaps)},
1423 {name: "_pop3", port: 110, host: []string{"."}},
1424 {name: "_pop3s", port: 995, host: []string{"."}},
1426 // Host "." indicates the service is not available. We suggested in the DNS records
1428 for i := range reqs {
1429 if reqs[i].host[0] == "." {
1433 var srvwg sync.WaitGroup
1434 srvwg.Add(len(reqs))
1435 for i := range reqs {
1438 _, reqs[i].srvs, _, reqs[i].err = resolver.LookupSRV(ctx, reqs[i].name[1:], "tcp", domain.ASCII+".")
1443 instr := "Ensure DNS records like the following exist:\n\n"
1444 r.SRVConf.SRVs = map[string][]net.SRV{}
1445 for _, req := range reqs {
1446 name := req.name + "._tcp." + domain.ASCII
1448 if req.host[0] == "." {
1451 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[0])
1452 r.SRVConf.SRVs[req.name] = unptr(req.srvs)
1454 addf(&r.SRVConf.Errors, "Looking up SRV record %q: %s", name, req.err)
1455 } else if len(req.srvs) == 0 {
1456 if req.host[0] == "." {
1457 addf(&r.SRVConf.Warnings, "Missing optional SRV record %q", name)
1459 addf(&r.SRVConf.Errors, "Missing SRV record %q", name)
1461 } else if len(req.srvs) != 1 || !slices.Contains(req.host, req.srvs[0].Target) || req.srvs[0].Port != req.port {
1463 for _, srv := range req.srvs {
1464 srvs = append(srvs, fmt.Sprintf("%d %d %d %s", srv.Priority, srv.Weight, srv.Port, srv.Target))
1466 addf(&r.SRVConf.Errors, "Unexpected SRV record(s) for %q: %s", name, strings.Join(srvs, ", "))
1469 addf(&r.SRVConf.Instructions, instr)
1478 if domConf.ClientSettingsDomain != "" {
1479 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+".")
1481 ips, ourIPs, notOurIPs, err := lookupIPs(&r.Autoconf.Errors, domConf.ClientSettingsDNSDomain.ASCII+".")
1483 addf(&r.Autoconf.Errors, "Looking up client settings DNS CNAME: %s", err)
1485 r.Autoconf.ClientSettingsDomainIPs = ips
1486 if !isUnspecifiedNAT {
1487 if len(ourIPs) == 0 {
1488 addf(&r.Autoconf.Errors, "Client settings domain does not point to one of our IPs.")
1489 } else if len(notOurIPs) > 0 {
1490 addf(&r.Autoconf.Errors, "Client settings domain points to some IPs that are not ours: %v", notOurIPs)
1495 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+".")
1497 host := "autoconfig." + domain.ASCII + "."
1498 ips, ourIPs, notOurIPs, err := lookupIPs(&r.Autoconf.Errors, host)
1500 addf(&r.Autoconf.Errors, "Looking up autoconfig host: %s", err)
1504 r.Autoconf.IPs = ips
1505 if !isUnspecifiedNAT {
1506 if len(ourIPs) == 0 {
1507 addf(&r.Autoconf.Errors, "Autoconfig does not point to one of our IPs.")
1508 } else if len(notOurIPs) > 0 {
1509 addf(&r.Autoconf.Errors, "Autoconfig points to some IPs that are not ours: %v", notOurIPs)
1513 checkTLS(&r.Autoconf.Errors, "autoconfig."+domain.ASCII, ips, "443")
1522 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+".")
1524 _, srvs, _, err := resolver.LookupSRV(ctx, "autodiscover", "tcp", domain.ASCII+".")
1526 addf(&r.Autodiscover.Errors, "Looking up SRV record %q: %s", "autodiscover", err)
1530 for _, srv := range srvs {
1531 ips, ourIPs, notOurIPs, err := lookupIPs(&r.Autodiscover.Errors, srv.Target)
1533 addf(&r.Autodiscover.Errors, "Looking up target %q from SRV record: %s", srv.Target, err)
1536 if srv.Port != 443 {
1540 r.Autodiscover.Records = append(r.Autodiscover.Records, AutodiscoverSRV{*srv, ips})
1541 if !isUnspecifiedNAT {
1542 if len(ourIPs) == 0 {
1543 addf(&r.Autodiscover.Errors, "SRV target %q does not point to our IPs.", srv.Target)
1544 } else if len(notOurIPs) > 0 {
1545 addf(&r.Autodiscover.Errors, "SRV target %q points to some IPs that are not ours: %v", srv.Target, notOurIPs)
1549 checkTLS(&r.Autodiscover.Errors, strings.TrimSuffix(srv.Target, "."), ips, "443")
1552 addf(&r.Autodiscover.Errors, "No SRV record for port 443 for https.")
1560// Domains returns all configured domain names.
1561func (Admin) Domains(ctx context.Context) []config.Domain {
1562 return mox.Conf.DomainConfigs()
1565// Domain returns the dns domain for a (potentially unicode as IDNA) domain name.
1566func (Admin) Domain(ctx context.Context, domain string) dns.Domain {
1567 d, err := dns.ParseDomain(domain)
1568 xcheckuserf(ctx, err, "parse domain")
1569 _, ok := mox.Conf.Domain(d)
1571 xcheckuserf(ctx, errors.New("no such domain"), "looking up domain")
1576// ParseDomain parses a domain, possibly an IDNA domain.
1577func (Admin) ParseDomain(ctx context.Context, domain string) dns.Domain {
1578 d, err := dns.ParseDomain(domain)
1579 xcheckuserf(ctx, err, "parse domain")
1583// DomainConfig returns the configuration for a domain.
1584func (Admin) DomainConfig(ctx context.Context, domain string) config.Domain {
1585 d, err := dns.ParseDomain(domain)
1586 xcheckuserf(ctx, err, "parse domain")
1587 conf, ok := mox.Conf.Domain(d)
1589 xcheckuserf(ctx, errors.New("no such domain"), "looking up domain")
1594// DomainLocalparts returns the encoded localparts and accounts configured in domain.
1595func (Admin) DomainLocalparts(ctx context.Context, domain string) (localpartAccounts map[string]string, localpartAliases map[string]config.Alias) {
1596 d, err := dns.ParseDomain(domain)
1597 xcheckuserf(ctx, err, "parsing domain")
1598 _, ok := mox.Conf.Domain(d)
1600 xcheckuserf(ctx, errors.New("no such domain"), "looking up domain")
1602 return mox.Conf.DomainLocalparts(d)
1605// Accounts returns the names of all configured and all disabled accounts.
1606func (Admin) Accounts(ctx context.Context) (all, disabled []string) {
1607 all, disabled = mox.Conf.AccountsDisabled()
1612// Account returns the parsed configuration of an account.
1613func (Admin) Account(ctx context.Context, account string) (accountConfig config.Account, diskUsage int64) {
1614 log := pkglog.WithContext(ctx)
1616 acc, err := store.OpenAccount(log, account, false)
1617 if err != nil && errors.Is(err, store.ErrAccountUnknown) {
1618 xcheckuserf(ctx, err, "looking up account")
1620 xcheckf(ctx, err, "open account")
1623 log.Check(err, "closing account")
1626 var ac config.Account
1627 acc.WithRLock(func() {
1628 ac, _ = mox.Conf.Account(acc.Name)
1630 err := acc.DB.Read(ctx, func(tx *bstore.Tx) error {
1631 du := store.DiskUsage{ID: 1}
1633 diskUsage = du.MessageSize
1636 xcheckf(ctx, err, "get disk usage")
1639 return ac, diskUsage
1642// ConfigFiles returns the paths and contents of the static and dynamic configuration files.
1643func (Admin) ConfigFiles(ctx context.Context) (staticPath, dynamicPath, static, dynamic string) {
1644 buf0, err := os.ReadFile(mox.ConfigStaticPath)
1645 xcheckf(ctx, err, "read static config file")
1646 buf1, err := os.ReadFile(mox.ConfigDynamicPath)
1647 xcheckf(ctx, err, "read dynamic config file")
1648 return mox.ConfigStaticPath, mox.ConfigDynamicPath, string(buf0), string(buf1)
1651// MTASTSPolicies returns all mtasts policies from the cache.
1652func (Admin) MTASTSPolicies(ctx context.Context) (records []mtastsdb.PolicyRecord) {
1653 records, err := mtastsdb.PolicyRecords(ctx)
1654 xcheckf(ctx, err, "fetching mtasts policies from database")
1658// TLSReports returns TLS reports overlapping with period start/end, for the given
1659// policy domain (or all domains if empty). The reports are sorted first by period
1660// end (most recent first), then by policy domain.
1661func (Admin) TLSReports(ctx context.Context, start, end time.Time, policyDomain string) (reports []tlsrptdb.Record) {
1662 var polDom dns.Domain
1663 if policyDomain != "" {
1665 polDom, err = dns.ParseDomain(policyDomain)
1666 xcheckuserf(ctx, err, "parsing domain %q", policyDomain)
1669 records, err := tlsrptdb.RecordsPeriodDomain(ctx, start, end, polDom)
1670 xcheckf(ctx, err, "fetching tlsrpt report records from database")
1671 sort.Slice(records, func(i, j int) bool {
1672 iend := records[i].Report.DateRange.End
1673 jend := records[j].Report.DateRange.End
1675 return records[i].Domain < records[j].Domain
1677 return iend.After(jend)
1682// TLSReportID returns a single TLS report.
1683func (Admin) TLSReportID(ctx context.Context, domain string, reportID int64) tlsrptdb.Record {
1684 record, err := tlsrptdb.RecordID(ctx, reportID)
1685 if err == nil && record.Domain != domain {
1686 err = bstore.ErrAbsent
1688 if err == bstore.ErrAbsent {
1689 xcheckuserf(ctx, err, "fetching tls report from database")
1691 xcheckf(ctx, err, "fetching tls report from database")
1695// TLSRPTSummary presents TLS reporting statistics for a single domain
1697type TLSRPTSummary struct {
1698 PolicyDomain dns.Domain
1701 ResultTypeCounts map[tlsrpt.ResultType]int64
1704// TLSRPTSummaries returns a summary of received TLS reports overlapping with
1705// period start/end for one or all domains (when domain is empty).
1706// The returned summaries are ordered by domain name.
1707func (Admin) TLSRPTSummaries(ctx context.Context, start, end time.Time, policyDomain string) (domainSummaries []TLSRPTSummary) {
1708 var polDom dns.Domain
1709 if policyDomain != "" {
1711 polDom, err = dns.ParseDomain(policyDomain)
1712 xcheckuserf(ctx, err, "parsing policy domain")
1714 reports, err := tlsrptdb.RecordsPeriodDomain(ctx, start, end, polDom)
1715 xcheckf(ctx, err, "fetching tlsrpt reports from database")
1717 summaries := map[dns.Domain]TLSRPTSummary{}
1718 for _, r := range reports {
1719 dom, err := dns.ParseDomain(r.Domain)
1720 xcheckf(ctx, err, "parsing domain %q", r.Domain)
1722 sum := summaries[dom]
1723 sum.PolicyDomain = dom
1724 for _, result := range r.Report.Policies {
1725 sum.Success += result.Summary.TotalSuccessfulSessionCount
1726 sum.Failure += result.Summary.TotalFailureSessionCount
1727 for _, details := range result.FailureDetails {
1728 if sum.ResultTypeCounts == nil {
1729 sum.ResultTypeCounts = map[tlsrpt.ResultType]int64{}
1731 sum.ResultTypeCounts[details.ResultType] += details.FailedSessionCount
1734 summaries[dom] = sum
1736 sums := make([]TLSRPTSummary, 0, len(summaries))
1737 for _, sum := range summaries {
1738 sums = append(sums, sum)
1740 sort.Slice(sums, func(i, j int) bool {
1741 return sums[i].PolicyDomain.Name() < sums[j].PolicyDomain.Name()
1746// DMARCReports returns DMARC reports overlapping with period start/end, for the
1747// given domain (or all domains if empty). The reports are sorted first by period
1748// end (most recent first), then by domain.
1749func (Admin) DMARCReports(ctx context.Context, start, end time.Time, domain string) (reports []dmarcdb.DomainFeedback) {
1750 reports, err := dmarcdb.RecordsPeriodDomain(ctx, start, end, domain)
1751 xcheckf(ctx, err, "fetching dmarc aggregate reports from database")
1752 sort.Slice(reports, func(i, j int) bool {
1753 iend := reports[i].ReportMetadata.DateRange.End
1754 jend := reports[j].ReportMetadata.DateRange.End
1756 return reports[i].Domain < reports[j].Domain
1763// DMARCReportID returns a single DMARC report.
1764func (Admin) DMARCReportID(ctx context.Context, domain string, reportID int64) (report dmarcdb.DomainFeedback) {
1765 report, err := dmarcdb.RecordID(ctx, reportID)
1766 if err == nil && report.Domain != domain {
1767 err = bstore.ErrAbsent
1769 if err == bstore.ErrAbsent {
1770 xcheckuserf(ctx, err, "fetching dmarc aggregate report from database")
1772 xcheckf(ctx, err, "fetching dmarc aggregate report from database")
1776// DMARCSummary presents DMARC aggregate reporting statistics for a single domain
1778type DMARCSummary struct {
1782 DispositionQuarantine int
1783 DispositionReject int
1786 PolicyOverrides map[dmarcrpt.PolicyOverride]int
1789// DMARCSummaries returns a summary of received DMARC reports overlapping with
1790// period start/end for one or all domains (when domain is empty).
1791// The returned summaries are ordered by domain name.
1792func (Admin) DMARCSummaries(ctx context.Context, start, end time.Time, domain string) (domainSummaries []DMARCSummary) {
1793 reports, err := dmarcdb.RecordsPeriodDomain(ctx, start, end, domain)
1794 xcheckf(ctx, err, "fetching dmarc aggregate reports from database")
1795 summaries := map[string]DMARCSummary{}
1796 for _, r := range reports {
1797 sum := summaries[r.Domain]
1798 sum.Domain = r.Domain
1799 for _, record := range r.Records {
1800 n := record.Row.Count
1804 switch record.Row.PolicyEvaluated.Disposition {
1805 case dmarcrpt.DispositionNone:
1806 sum.DispositionNone += n
1807 case dmarcrpt.DispositionQuarantine:
1808 sum.DispositionQuarantine += n
1809 case dmarcrpt.DispositionReject:
1810 sum.DispositionReject += n
1813 if record.Row.PolicyEvaluated.DKIM == dmarcrpt.DMARCFail {
1816 if record.Row.PolicyEvaluated.SPF == dmarcrpt.DMARCFail {
1820 for _, reason := range record.Row.PolicyEvaluated.Reasons {
1821 if sum.PolicyOverrides == nil {
1822 sum.PolicyOverrides = map[dmarcrpt.PolicyOverride]int{}
1824 sum.PolicyOverrides[reason.Type] += n
1827 summaries[r.Domain] = sum
1829 sums := make([]DMARCSummary, 0, len(summaries))
1830 for _, sum := range summaries {
1831 sums = append(sums, sum)
1833 sort.Slice(sums, func(i, j int) bool {
1834 return sums[i].Domain < sums[j].Domain
1839// Reverse is the result of a reverse lookup.
1840type Reverse struct {
1843 // In the future, we can add a iprev-validated host name, and possibly the IPs of the host names.
1846// LookupIP does a reverse lookup of ip.
1847func (Admin) LookupIP(ctx context.Context, ip string) Reverse {
1848 resolver := dns.StrictResolver{Pkg: "webadmin", Log: pkglog.WithContext(ctx).Logger}
1849 names, _, err := resolver.LookupAddr(ctx, ip)
1850 xcheckuserf(ctx, err, "looking up ip")
1851 return Reverse{names}
1854// DNSBLStatus returns the IPs from which outgoing connections may be made and
1855// their current status in DNSBLs that are configured. The IPs are typically the
1856// configured listen IPs, or otherwise IPs on the machines network interfaces, with
1857// internal/private IPs removed.
1859// The returned value maps IPs to per DNSBL statuses, where "pass" means not listed and
1860// anything else is an error string, e.g. "fail: ..." or "temperror: ...".
1861func (Admin) DNSBLStatus(ctx context.Context) (results map[string]map[string]string, using, monitoring []dns.Domain) {
1862 log := mlog.New("webadmin", nil).WithContext(ctx)
1863 resolver := dns.StrictResolver{Pkg: "check", Log: log.Logger}
1864 return dnsblsStatus(ctx, log, resolver)
1867func dnsblsStatus(ctx context.Context, log mlog.Log, resolver dns.Resolver) (results map[string]map[string]string, using, monitoring []dns.Domain) {
1868 // todo: check health before using dnsbl?
1869 using = mox.Conf.Static.Listeners["public"].SMTP.DNSBLZones
1870 zones := slices.Clone(using)
1871 conf := mox.Conf.DynamicConfig()
1872 for _, zone := range conf.MonitorDNSBLZones {
1873 if !slices.Contains(zones, zone) {
1874 zones = append(zones, zone)
1875 monitoring = append(monitoring, zone)
1879 r := map[string]map[string]string{}
1880 for _, ip := range xsendingIPs(ctx) {
1881 if ip.IsLoopback() || ip.IsPrivate() {
1884 ipstr := ip.String()
1885 r[ipstr] = map[string]string{}
1886 for _, zone := range zones {
1887 status, expl, err := dnsbl.Lookup(ctx, log.Logger, resolver, zone, ip)
1888 result := string(status)
1890 result += ": " + err.Error()
1893 result += ": " + expl
1895 r[ipstr][zone.LogString()] = result
1898 return r, using, monitoring
1901func (Admin) MonitorDNSBLsSave(ctx context.Context, text string) {
1902 var zones []dns.Domain
1903 publicZones := mox.Conf.Static.Listeners["public"].SMTP.DNSBLZones
1904 for line := range strings.SplitSeq(text, "\n") {
1905 line = strings.TrimSpace(line)
1909 d, err := dns.ParseDomain(line)
1910 xcheckuserf(ctx, err, "parsing dnsbl zone %s", line)
1911 if slices.Contains(zones, d) {
1912 xusererrorf(ctx, "duplicate dnsbl zone %s", line)
1914 if slices.Contains(publicZones, d) {
1915 xusererrorf(ctx, "dnsbl zone %s already present in public listener", line)
1917 zones = append(zones, d)
1920 err := admin.ConfigSave(ctx, func(conf *config.Dynamic) {
1921 conf.MonitorDNSBLs = make([]string, len(zones))
1922 conf.MonitorDNSBLZones = nil
1923 for i, z := range zones {
1924 conf.MonitorDNSBLs[i] = z.Name()
1927 xcheckf(ctx, err, "saving monitoring dnsbl zones")
1930// DomainRecords returns lines describing DNS records that should exist for the
1931// configured domain.
1932func (Admin) DomainRecords(ctx context.Context, domain string) []string {
1933 log := pkglog.WithContext(ctx)
1934 return DomainRecords(ctx, log, domain)
1937// DomainRecords is the implementation of API function Admin.DomainRecords, taking
1939func DomainRecords(ctx context.Context, log mlog.Log, domain string) []string {
1940 d, err := dns.ParseDomain(domain)
1941 xcheckuserf(ctx, err, "parsing domain")
1942 dc, ok := mox.Conf.Domain(d)
1944 xcheckuserf(ctx, errors.New("unknown domain"), "lookup domain")
1946 resolver := dns.StrictResolver{Pkg: "webadmin", Log: pkglog.WithContext(ctx).Logger}
1947 _, result, err := resolver.LookupTXT(ctx, domain+".")
1948 if !dns.IsNotFound(err) {
1949 xcheckf(ctx, err, "looking up record to determine if dnssec is implemented")
1952 var certIssuerDomainName, acmeAccountURI string
1953 public := mox.Conf.Static.Listeners["public"]
1954 if public.TLS != nil && public.TLS.ACME != "" {
1955 acme, ok := mox.Conf.Static.ACME[public.TLS.ACME]
1956 if ok && acme.Manager.Manager.Client != nil {
1957 certIssuerDomainName = acme.IssuerDomainName
1958 acc, err := acme.Manager.Manager.Client.GetReg(ctx, "")
1959 log.Check(err, "get public acme account")
1961 acmeAccountURI = acc.URI
1966 records, err := admin.DomainRecords(dc, d, result.Authentic, certIssuerDomainName, acmeAccountURI)
1967 xcheckf(ctx, err, "dns records")
1971// DomainAdd adds a new domain and reloads the configuration.
1972func (Admin) DomainAdd(ctx context.Context, disabled bool, domain, accountName, localpart string) {
1973 d, err := dns.ParseDomain(domain)
1974 xcheckuserf(ctx, err, "parsing domain")
1976 err = admin.DomainAdd(ctx, disabled, d, accountName, smtp.Localpart(norm.NFC.String(localpart)))
1977 xcheckf(ctx, err, "adding domain")
1980// DomainRemove removes an existing domain and reloads the configuration.
1981func (Admin) DomainRemove(ctx context.Context, domain string) {
1982 d, err := dns.ParseDomain(domain)
1983 xcheckuserf(ctx, err, "parsing domain")
1985 err = admin.DomainRemove(ctx, d)
1986 xcheckf(ctx, err, "removing domain")
1989// AccountAdd adds existing a new account, with an initial email address, and
1990// reloads the configuration.
1991func (Admin) AccountAdd(ctx context.Context, accountName, address string) {
1992 err := admin.AccountAdd(ctx, accountName, address)
1993 xcheckf(ctx, err, "adding account")
1996// AccountRemove removes an existing account and reloads the configuration.
1997func (Admin) AccountRemove(ctx context.Context, accountName string) {
1998 err := admin.AccountRemove(ctx, accountName)
1999 xcheckf(ctx, err, "removing account")
2002// AddressAdd adds a new address to the account, which must already exist.
2003func (Admin) AddressAdd(ctx context.Context, address, accountName string) {
2004 err := admin.AddressAdd(ctx, address, accountName)
2005 xcheckf(ctx, err, "adding address")
2008// AddressRemove removes an existing address.
2009func (Admin) AddressRemove(ctx context.Context, address string) {
2010 err := admin.AddressRemove(ctx, address)
2011 xcheckf(ctx, err, "removing address")
2014// SetPassword saves a new password for an account, invalidating the previous password.
2015// Sessions are not interrupted, and will keep working. New login attempts must use the new password.
2016// Password must be at least 8 characters.
2017func (Admin) SetPassword(ctx context.Context, accountName, password string) {
2018 log := pkglog.WithContext(ctx)
2019 if len(password) < 8 {
2020 xusererrorf(ctx, "message must be at least 8 characters")
2022 acc, err := store.OpenAccount(log, accountName, false)
2023 xcheckf(ctx, err, "open account")
2026 log.WithContext(ctx).Check(err, "closing account")
2028 err = acc.SetPassword(log, password)
2029 xcheckf(ctx, err, "setting password")
2032// AccountSettingsSave set new settings for an account that only an admin can set.
2033func (Admin) AccountSettingsSave(ctx context.Context, accountName string, maxOutgoingMessagesPerDay, maxFirstTimeRecipientsPerDay int, maxMsgSize int64, firstTimeSenderDelay, noCustomPassword bool) {
2034 err := admin.AccountSave(ctx, accountName, func(acc *config.Account) {
2035 acc.MaxOutgoingMessagesPerDay = maxOutgoingMessagesPerDay
2036 acc.MaxFirstTimeRecipientsPerDay = maxFirstTimeRecipientsPerDay
2037 acc.QuotaMessageSize = maxMsgSize
2038 acc.NoFirstTimeSenderDelay = !firstTimeSenderDelay
2039 acc.NoCustomPassword = noCustomPassword
2041 xcheckf(ctx, err, "saving account settings")
2044// AccountLoginDisabledSave saves the LoginDisabled field of an account.
2045func (Admin) AccountLoginDisabledSave(ctx context.Context, accountName string, loginDisabled string) {
2046 log := pkglog.WithContext(ctx)
2048 acc, err := store.OpenAccount(log, accountName, false)
2049 xcheckf(ctx, err, "open account")
2052 log.Check(err, "closing account")
2055 err = admin.AccountSave(ctx, accountName, func(acc *config.Account) {
2056 acc.LoginDisabled = loginDisabled
2058 xcheckf(ctx, err, "saving login disabled account")
2060 err = acc.SessionsClear(ctx, log)
2061 xcheckf(ctx, err, "removing current sessions")
2064// ClientConfigsDomain returns configurations for email clients, IMAP and
2065// Submission (SMTP) for the domain.
2066func (Admin) ClientConfigsDomain(ctx context.Context, domain string) admin.ClientConfigs {
2067 d, err := dns.ParseDomain(domain)
2068 xcheckuserf(ctx, err, "parsing domain")
2070 cc, err := admin.ClientConfigsDomain(d)
2071 xcheckf(ctx, err, "client config for domain")
2075// QueueSize returns the number of messages currently in the outgoing queue.
2076func (Admin) QueueSize(ctx context.Context) int {
2077 n, err := queue.Count(ctx)
2078 xcheckf(ctx, err, "listing messages in queue")
2082// QueueHoldRuleList lists the hold rules.
2083func (Admin) QueueHoldRuleList(ctx context.Context) []queue.HoldRule {
2084 l, err := queue.HoldRuleList(ctx)
2085 xcheckf(ctx, err, "listing queue hold rules")
2089// QueueHoldRuleAdd adds a hold rule. Newly submitted and existing messages
2090// matching the hold rule will be marked "on hold".
2091func (Admin) QueueHoldRuleAdd(ctx context.Context, hr queue.HoldRule) queue.HoldRule {
2093 hr.SenderDomain, err = dns.ParseDomain(hr.SenderDomainStr)
2094 xcheckuserf(ctx, err, "parsing sender domain %q", hr.SenderDomainStr)
2095 hr.RecipientDomain, err = dns.ParseDomain(hr.RecipientDomainStr)
2096 xcheckuserf(ctx, err, "parsing recipient domain %q", hr.RecipientDomainStr)
2098 log := pkglog.WithContext(ctx)
2099 hr, err = queue.HoldRuleAdd(ctx, log, hr)
2100 xcheckf(ctx, err, "adding queue hold rule")
2104// QueueHoldRuleRemove removes a hold rule. The Hold field of messages in
2105// the queue are not changed.
2106func (Admin) QueueHoldRuleRemove(ctx context.Context, holdRuleID int64) {
2107 log := pkglog.WithContext(ctx)
2108 err := queue.HoldRuleRemove(ctx, log, holdRuleID)
2109 xcheckf(ctx, err, "removing queue hold rule")
2112// QueueList returns the messages currently in the outgoing queue.
2113func (Admin) QueueList(ctx context.Context, filter queue.Filter, sort queue.Sort) []queue.Msg {
2114 l, err := queue.List(ctx, filter, sort)
2115 xcheckf(ctx, err, "listing messages in queue")
2119// QueueNextAttemptSet sets a new time for next delivery attempt of matching
2120// messages from the queue.
2121func (Admin) QueueNextAttemptSet(ctx context.Context, filter queue.Filter, minutes int) (affected int) {
2122 n, err := queue.NextAttemptSet(ctx, filter, time.Now().Add(time.Duration(minutes)*time.Minute))
2123 xcheckf(ctx, err, "setting new next delivery attempt time for matching messages in queue")
2127// QueueNextAttemptAdd adds a duration to the time of next delivery attempt of
2128// matching messages from the queue.
2129func (Admin) QueueNextAttemptAdd(ctx context.Context, filter queue.Filter, minutes int) (affected int) {
2130 n, err := queue.NextAttemptAdd(ctx, filter, time.Duration(minutes)*time.Minute)
2131 xcheckf(ctx, err, "adding duration to next delivery attempt for matching messages in queue")
2135// QueueHoldSet sets the Hold field of matching messages in the queue.
2136func (Admin) QueueHoldSet(ctx context.Context, filter queue.Filter, onHold bool) (affected int) {
2137 n, err := queue.HoldSet(ctx, filter, onHold)
2138 xcheckf(ctx, err, "changing onhold for matching messages in queue")
2142// QueueFail fails delivery for matching messages, causing DSNs to be sent.
2143func (Admin) QueueFail(ctx context.Context, filter queue.Filter) (affected int) {
2144 log := pkglog.WithContext(ctx)
2145 n, err := queue.Fail(ctx, log, filter)
2146 xcheckf(ctx, err, "drop messages from queue")
2150// QueueDrop removes matching messages from the queue.
2151func (Admin) QueueDrop(ctx context.Context, filter queue.Filter) (affected int) {
2152 log := pkglog.WithContext(ctx)
2153 n, err := queue.Drop(ctx, log, filter)
2154 xcheckf(ctx, err, "drop messages from queue")
2158// QueueRequireTLSSet updates the requiretls field for matching messages in the
2159// queue, to be used for the next delivery.
2160func (Admin) QueueRequireTLSSet(ctx context.Context, filter queue.Filter, requireTLS *bool) (affected int) {
2161 n, err := queue.RequireTLSSet(ctx, filter, requireTLS)
2162 xcheckf(ctx, err, "update requiretls for messages in queue")
2166// QueueTransportSet initiates delivery of a message from the queue and sets the transport
2167// to use for delivery.
2168func (Admin) QueueTransportSet(ctx context.Context, filter queue.Filter, transport string) (affected int) {
2169 n, err := queue.TransportSet(ctx, filter, transport)
2170 xcheckf(ctx, err, "changing transport for messages in queue")
2174// RetiredList returns messages retired from the queue (delivery could
2175// have succeeded or failed).
2176func (Admin) RetiredList(ctx context.Context, filter queue.RetiredFilter, sort queue.RetiredSort) []queue.MsgRetired {
2177 l, err := queue.RetiredList(ctx, filter, sort)
2178 xcheckf(ctx, err, "listing retired messages")
2182// HookQueueSize returns the number of webhooks still to be delivered.
2183func (Admin) HookQueueSize(ctx context.Context) int {
2184 n, err := queue.HookQueueSize(ctx)
2185 xcheckf(ctx, err, "get hook queue size")
2189// HookList lists webhooks still to be delivered.
2190func (Admin) HookList(ctx context.Context, filter queue.HookFilter, sort queue.HookSort) []queue.Hook {
2191 l, err := queue.HookList(ctx, filter, sort)
2192 xcheckf(ctx, err, "listing hook queue")
2196// HookNextAttemptSet sets a new time for next delivery attempt of matching
2197// hooks from the queue.
2198func (Admin) HookNextAttemptSet(ctx context.Context, filter queue.HookFilter, minutes int) (affected int) {
2199 n, err := queue.HookNextAttemptSet(ctx, filter, time.Now().Add(time.Duration(minutes)*time.Minute))
2200 xcheckf(ctx, err, "setting new next delivery attempt time for matching webhooks in queue")
2204// HookNextAttemptAdd adds a duration to the time of next delivery attempt of
2205// matching hooks from the queue.
2206func (Admin) HookNextAttemptAdd(ctx context.Context, filter queue.HookFilter, minutes int) (affected int) {
2207 n, err := queue.HookNextAttemptAdd(ctx, filter, time.Duration(minutes)*time.Minute)
2208 xcheckf(ctx, err, "adding duration to next delivery attempt for matching webhooks in queue")
2212// HookRetiredList lists retired webhooks.
2213func (Admin) HookRetiredList(ctx context.Context, filter queue.HookRetiredFilter, sort queue.HookRetiredSort) []queue.HookRetired {
2214 l, err := queue.HookRetiredList(ctx, filter, sort)
2215 xcheckf(ctx, err, "listing retired hooks")
2219// HookCancel prevents further delivery attempts of matching webhooks.
2220func (Admin) HookCancel(ctx context.Context, filter queue.HookFilter) (affected int) {
2221 log := pkglog.WithContext(ctx)
2222 n, err := queue.HookCancel(ctx, log, filter)
2223 xcheckf(ctx, err, "cancel hooks in queue")
2227// LogLevels returns the current log levels.
2228func (Admin) LogLevels(ctx context.Context) map[string]string {
2229 m := map[string]string{}
2230 for pkg, level := range mox.Conf.LogLevels() {
2231 s, ok := mlog.LevelStrings[level]
2240// LogLevelSet sets a log level for a package.
2241func (Admin) LogLevelSet(ctx context.Context, pkg string, levelStr string) {
2242 level, ok := mlog.Levels[levelStr]
2244 xcheckuserf(ctx, errors.New("unknown"), "lookup level")
2246 mox.Conf.LogLevelSet(pkglog.WithContext(ctx), pkg, level)
2249// LogLevelRemove removes a log level for a package, which cannot be the empty string.
2250func (Admin) LogLevelRemove(ctx context.Context, pkg string) {
2251 mox.Conf.LogLevelRemove(pkglog.WithContext(ctx), pkg)
2254// CheckUpdatesEnabled returns whether checking for updates is enabled.
2255func (Admin) CheckUpdatesEnabled(ctx context.Context) bool {
2256 return mox.Conf.Static.CheckUpdates
2259// WebserverConfig is the combination of WebDomainRedirects and WebHandlers
2260// from the domains.conf configuration file.
2261type WebserverConfig struct {
2262 WebDNSDomainRedirects [][2]dns.Domain // From server to frontend.
2263 WebDomainRedirects [][2]string // From frontend to server, it's not convenient to create dns.Domain in the frontend.
2264 WebHandlers []config.WebHandler
2267// WebserverConfig returns the current webserver config
2268func (Admin) WebserverConfig(ctx context.Context) (conf WebserverConfig) {
2269 conf = webserverConfig()
2270 conf.WebDomainRedirects = nil
2274func webserverConfig() WebserverConfig {
2275 conf := mox.Conf.DynamicConfig()
2276 r := conf.WebDNSDomainRedirects
2277 l := conf.WebHandlers
2279 x := make([][2]dns.Domain, 0, len(r))
2280 xs := make([][2]string, 0, len(r))
2281 for k, v := range r {
2282 x = append(x, [2]dns.Domain{k, v})
2283 xs = append(xs, [2]string{k.Name(), v.Name()})
2285 sort.Slice(x, func(i, j int) bool {
2286 return x[i][0].ASCII < x[j][0].ASCII
2288 sort.Slice(xs, func(i, j int) bool {
2289 return xs[i][0] < xs[j][0]
2291 return WebserverConfig{x, xs, l}
2294// WebserverConfigSave saves a new webserver config. If oldConf is not equal to
2295// the current config, an error is returned.
2296func (Admin) WebserverConfigSave(ctx context.Context, oldConf, newConf WebserverConfig) (savedConf WebserverConfig) {
2297 current := webserverConfig()
2298 webhandlersEqual := func() bool {
2299 if len(current.WebHandlers) != len(oldConf.WebHandlers) {
2302 for i, wh := range current.WebHandlers {
2303 if !wh.Equal(oldConf.WebHandlers[i]) {
2309 if !reflect.DeepEqual(oldConf.WebDNSDomainRedirects, current.WebDNSDomainRedirects) || !webhandlersEqual() {
2310 xcheckuserf(ctx, errors.New("config has changed"), "comparing old/current config")
2313 // Convert to map, check that there are no duplicates here. The canonicalized
2314 // dns.Domain are checked again for uniqueness when parsing the config before
2316 domainRedirects := map[string]string{}
2317 for _, x := range newConf.WebDomainRedirects {
2318 if _, ok := domainRedirects[x[0]]; ok {
2319 xcheckuserf(ctx, errors.New("already present"), "checking redirect %s", x[0])
2321 domainRedirects[x[0]] = x[1]
2324 err := admin.ConfigSave(ctx, func(conf *config.Dynamic) {
2325 conf.WebDomainRedirects = domainRedirects
2326 conf.WebHandlers = newConf.WebHandlers
2328 xcheckf(ctx, err, "saving webserver config")
2330 savedConf = webserverConfig()
2331 savedConf.WebDomainRedirects = nil
2335// Transports returns the configured transports, for sending email.
2336func (Admin) Transports(ctx context.Context) map[string]config.Transport {
2337 return mox.Conf.Static.Transports
2340// DMARCEvaluationStats returns a map of all domains with evaluations to a count of
2341// the evaluations and whether those evaluations will cause a report to be sent.
2342func (Admin) DMARCEvaluationStats(ctx context.Context) map[string]dmarcdb.EvaluationStat {
2343 stats, err := dmarcdb.EvaluationStats(ctx)
2344 xcheckf(ctx, err, "get evaluation stats")
2348// DMARCEvaluationsDomain returns all evaluations for aggregate reports for the
2349// domain, sorted from oldest to most recent.
2350func (Admin) DMARCEvaluationsDomain(ctx context.Context, domain string) (dns.Domain, []dmarcdb.Evaluation) {
2351 dom, err := dns.ParseDomain(domain)
2352 xcheckf(ctx, err, "parsing domain")
2354 evals, err := dmarcdb.EvaluationsDomain(ctx, dom)
2355 xcheckf(ctx, err, "get evaluations for domain")
2359// DMARCRemoveEvaluations removes evaluations for a domain.
2360func (Admin) DMARCRemoveEvaluations(ctx context.Context, domain string) {
2361 dom, err := dns.ParseDomain(domain)
2362 xcheckf(ctx, err, "parsing domain")
2364 err = dmarcdb.RemoveEvaluationsDomain(ctx, dom)
2365 xcheckf(ctx, err, "removing evaluations for domain")
2368// DMARCSuppressAdd adds a reporting address to the suppress list. Outgoing
2369// reports will be suppressed for a period.
2370func (Admin) DMARCSuppressAdd(ctx context.Context, reportingAddress string, until time.Time, comment string) {
2371 addr, err := smtp.ParseAddress(reportingAddress)
2372 xcheckuserf(ctx, err, "parsing reporting address")
2374 ba := dmarcdb.SuppressAddress{ReportingAddress: addr.String(), Until: until, Comment: comment}
2375 err = dmarcdb.SuppressAdd(ctx, &ba)
2376 xcheckf(ctx, err, "adding address to suppresslist")
2379// DMARCSuppressList returns all reporting addresses on the suppress list.
2380func (Admin) DMARCSuppressList(ctx context.Context) []dmarcdb.SuppressAddress {
2381 l, err := dmarcdb.SuppressList(ctx)
2382 xcheckf(ctx, err, "listing reporting addresses in suppresslist")
2386// DMARCSuppressRemove removes a reporting address record from the suppress list.
2387func (Admin) DMARCSuppressRemove(ctx context.Context, id int64) {
2388 err := dmarcdb.SuppressRemove(ctx, id)
2389 xcheckf(ctx, err, "removing reporting address from suppresslist")
2392// DMARCSuppressExtend updates the until field of a suppressed reporting address record.
2393func (Admin) DMARCSuppressExtend(ctx context.Context, id int64, until time.Time) {
2394 err := dmarcdb.SuppressUpdate(ctx, id, until)
2395 xcheckf(ctx, err, "updating reporting address in suppresslist")
2398// TLSRPTResults returns all TLSRPT results in the database.
2399func (Admin) TLSRPTResults(ctx context.Context) []tlsrptdb.TLSResult {
2400 results, err := tlsrptdb.Results(ctx)
2401 xcheckf(ctx, err, "get results")
2405// TLSRPTResultsPolicyDomain returns the TLS results for a domain.
2406func (Admin) TLSRPTResultsDomain(ctx context.Context, isRcptDom bool, policyDomain string) (dns.Domain, []tlsrptdb.TLSResult) {
2407 dom, err := dns.ParseDomain(policyDomain)
2408 xcheckf(ctx, err, "parsing domain")
2411 results, err := tlsrptdb.ResultsRecipientDomain(ctx, dom)
2412 xcheckf(ctx, err, "get result for recipient domain")
2415 results, err := tlsrptdb.ResultsPolicyDomain(ctx, dom)
2416 xcheckf(ctx, err, "get result for policy domain")
2420// LookupTLSRPTRecord looks up a TLSRPT record and returns the parsed form, original txt
2421// form from DNS, and error with the TLSRPT record as a string.
2422func (Admin) LookupTLSRPTRecord(ctx context.Context, domain string) (record *TLSRPTRecord, txt string, errstr string) {
2423 log := pkglog.WithContext(ctx)
2424 dom, err := dns.ParseDomain(domain)
2425 xcheckf(ctx, err, "parsing domain")
2427 resolver := dns.StrictResolver{Pkg: "webadmin", Log: log.Logger}
2428 r, txt, err := tlsrpt.Lookup(ctx, log.Logger, resolver, dom)
2429 if err != nil && (errors.Is(err, tlsrpt.ErrNoRecord) || errors.Is(err, tlsrpt.ErrMultipleRecords) || errors.Is(err, tlsrpt.ErrRecordSyntax) || errors.Is(err, tlsrpt.ErrDNS)) {
2430 errstr = err.Error()
2433 xcheckf(ctx, err, "fetching tlsrpt record")
2436 record = &TLSRPTRecord{Record: *r}
2439 return record, txt, errstr
2442// TLSRPTRemoveResults removes the TLS results for a domain for the given day. If
2443// day is empty, all results are removed.
2444func (Admin) TLSRPTRemoveResults(ctx context.Context, isRcptDom bool, domain string, day string) {
2445 dom, err := dns.ParseDomain(domain)
2446 xcheckf(ctx, err, "parsing domain")
2449 err = tlsrptdb.RemoveResultsRecipientDomain(ctx, dom, day)
2450 xcheckf(ctx, err, "removing tls results")
2452 err = tlsrptdb.RemoveResultsPolicyDomain(ctx, dom, day)
2453 xcheckf(ctx, err, "removing tls results")
2457// TLSRPTSuppressAdd adds a reporting address to the suppress list. Outgoing
2458// reports will be suppressed for a period.
2459func (Admin) TLSRPTSuppressAdd(ctx context.Context, reportingAddress string, until time.Time, comment string) {
2460 addr, err := smtp.ParseAddress(reportingAddress)
2461 xcheckuserf(ctx, err, "parsing reporting address")
2463 ba := tlsrptdb.SuppressAddress{ReportingAddress: addr.String(), Until: until, Comment: comment}
2464 err = tlsrptdb.SuppressAdd(ctx, &ba)
2465 xcheckf(ctx, err, "adding address to suppresslist")
2468// TLSRPTSuppressList returns all reporting addresses on the suppress list.
2469func (Admin) TLSRPTSuppressList(ctx context.Context) []tlsrptdb.SuppressAddress {
2470 l, err := tlsrptdb.SuppressList(ctx)
2471 xcheckf(ctx, err, "listing reporting addresses in suppresslist")
2475// TLSRPTSuppressRemove removes a reporting address record from the suppress list.
2476func (Admin) TLSRPTSuppressRemove(ctx context.Context, id int64) {
2477 err := tlsrptdb.SuppressRemove(ctx, id)
2478 xcheckf(ctx, err, "removing reporting address from suppresslist")
2481// TLSRPTSuppressExtend updates the until field of a suppressed reporting address record.
2482func (Admin) TLSRPTSuppressExtend(ctx context.Context, id int64, until time.Time) {
2483 err := tlsrptdb.SuppressUpdate(ctx, id, until)
2484 xcheckf(ctx, err, "updating reporting address in suppresslist")
2487// LookupCid turns an ID from a Received header into a cid as used in logging.
2488func (Admin) LookupCid(ctx context.Context, recvID string) (cid string) {
2489 v, err := mox.ReceivedToCid(recvID)
2490 xcheckf(ctx, err, "received id to cid")
2491 return fmt.Sprintf("%x", v)
2494// Config returns the dynamic config.
2495func (Admin) Config(ctx context.Context) config.Dynamic {
2496 return mox.Conf.DynamicConfig()
2499// AccountRoutesSave saves routes for an account.
2500func (Admin) AccountRoutesSave(ctx context.Context, accountName string, routes []config.Route) {
2501 err := admin.AccountSave(ctx, accountName, func(acc *config.Account) {
2504 xcheckf(ctx, err, "saving account routes")
2507// DomainRoutesSave saves routes for a domain.
2508func (Admin) DomainRoutesSave(ctx context.Context, domainName string, routes []config.Route) {
2509 err := admin.DomainSave(ctx, domainName, func(domain *config.Domain) error {
2510 domain.Routes = routes
2513 xcheckf(ctx, err, "saving domain routes")
2516// RoutesSave saves global routes.
2517func (Admin) RoutesSave(ctx context.Context, routes []config.Route) {
2518 err := admin.ConfigSave(ctx, func(config *config.Dynamic) {
2519 config.Routes = routes
2521 xcheckf(ctx, err, "saving global routes")
2524// DomainDescriptionSave saves the description for a domain.
2525func (Admin) DomainDescriptionSave(ctx context.Context, domainName, descr string) {
2526 err := admin.DomainSave(ctx, domainName, func(domain *config.Domain) error {
2527 domain.Description = descr
2530 xcheckf(ctx, err, "saving domain description")
2533// DomainClientSettingsDomainSave saves the client settings domain for a domain.
2534func (Admin) DomainClientSettingsDomainSave(ctx context.Context, domainName, clientSettingsDomain string) {
2535 err := admin.DomainSave(ctx, domainName, func(domain *config.Domain) error {
2536 domain.ClientSettingsDomain = clientSettingsDomain
2539 xcheckf(ctx, err, "saving client settings domain")
2542// DomainLocalpartConfigSave saves the localpart catchall and case-sensitive
2543// settings for a domain.
2544func (Admin) DomainLocalpartConfigSave(ctx context.Context, domainName string, localpartCatchallSeparators []string, localpartCaseSensitive bool) {
2545 err := admin.DomainSave(ctx, domainName, func(domain *config.Domain) error {
2546 // We don't allow introducing new catchall separators that are used in DMARC/TLS
2547 // reporting. Can occur in existing configs for backwards compatibility.
2548 containsSep := func(seps []string) bool {
2549 for _, sep := range seps {
2550 if domain.DMARC != nil && strings.Contains(domain.DMARC.Localpart, sep) {
2553 if domain.TLSRPT != nil && strings.Contains(domain.TLSRPT.Localpart, sep) {
2559 if !containsSep(domain.LocalpartCatchallSeparatorsEffective) && containsSep(localpartCatchallSeparators) {
2560 xusererrorf(ctx, "cannot add localpart catchall separators that are used in dmarc and/or tls reporting addresses, change reporting addresses first")
2563 domain.LocalpartCatchallSeparatorsEffective = localpartCatchallSeparators
2564 // If there is a single separator, we prefer the non-list form, it's easier to
2565 // read/edit and should suffice for most setups.
2566 domain.LocalpartCatchallSeparator = ""
2567 domain.LocalpartCatchallSeparators = nil
2568 if len(localpartCatchallSeparators) == 1 {
2569 domain.LocalpartCatchallSeparator = localpartCatchallSeparators[0]
2571 domain.LocalpartCatchallSeparators = localpartCatchallSeparators
2574 domain.LocalpartCaseSensitive = localpartCaseSensitive
2577 xcheckf(ctx, err, "saving localpart settings for domain")
2580// DomainDMARCAddressSave saves the DMARC reporting address/processing
2581// configuration for a domain. If localpart is empty, processing reports is
2583func (Admin) DomainDMARCAddressSave(ctx context.Context, domainName, localpart, domain, account, mailbox string) {
2584 err := admin.DomainSave(ctx, domainName, func(d *config.Domain) error {
2585 // DMARC reporting addresses can contain the localpart catchall separator(s) for
2586 // backwards compability (hence not enforced when parsing the config files), but we
2587 // don't allow creating them.
2588 if d.DMARC == nil || d.DMARC.Localpart != localpart {
2589 for _, sep := range d.LocalpartCatchallSeparatorsEffective {
2590 if strings.Contains(localpart, sep) {
2591 xusererrorf(ctx, "dmarc reporting address cannot contain catchall separator %q in localpart (%q)", sep, localpart)
2596 if localpart == "" {
2599 d.DMARC = &config.DMARC{
2600 Localpart: localpart,
2608 xcheckf(ctx, err, "saving dmarc reporting address/settings for domain")
2611// DomainTLSRPTAddressSave saves the TLS reporting address/processing
2612// configuration for a domain. If localpart is empty, processing reports is
2614func (Admin) DomainTLSRPTAddressSave(ctx context.Context, domainName, localpart, domain, account, mailbox string) {
2615 err := admin.DomainSave(ctx, domainName, func(d *config.Domain) error {
2616 // TLS reporting addresses can contain the localpart catchall separator(s) for
2617 // backwards compability (hence not enforced when parsing the config files), but we
2618 // don't allow creating them.
2619 if d.TLSRPT == nil || d.TLSRPT.Localpart != localpart {
2620 for _, sep := range d.LocalpartCatchallSeparatorsEffective {
2621 if strings.Contains(localpart, sep) {
2622 xusererrorf(ctx, "tls reporting address cannot contain catchall separator %q in localpart (%q)", sep, localpart)
2627 if localpart == "" {
2630 d.TLSRPT = &config.TLSRPT{
2631 Localpart: localpart,
2639 xcheckf(ctx, err, "saving tls reporting address/settings for domain")
2642// DomainMTASTSSave saves the MTASTS policy for a domain. If policyID is empty,
2643// no MTASTS policy is served.
2644func (Admin) DomainMTASTSSave(ctx context.Context, domainName, policyID string, mode mtasts.Mode, maxAge time.Duration, mx []string) {
2645 err := admin.DomainSave(ctx, domainName, func(d *config.Domain) error {
2649 d.MTASTS = &config.MTASTS{
2658 xcheckf(ctx, err, "saving mtasts policy for domain")
2661// DomainDKIMAdd adds a DKIM selector for a domain, generating a new private
2662// key. The selector is not enabled for signing.
2663func (Admin) DomainDKIMAdd(ctx context.Context, domainName, selector, algorithm, hash string, headerRelaxed, bodyRelaxed, seal bool, headers []string, lifetime time.Duration) {
2664 d, err := dns.ParseDomain(domainName)
2665 xcheckuserf(ctx, err, "parsing domain")
2666 s, err := dns.ParseDomain(selector)
2667 xcheckuserf(ctx, err, "parsing selector")
2668 err = admin.DKIMAdd(ctx, d, s, algorithm, hash, headerRelaxed, bodyRelaxed, seal, headers, lifetime)
2669 xcheckf(ctx, err, "adding dkim key")
2672// DomainDKIMRemove removes a DKIM selector for a domain.
2673func (Admin) DomainDKIMRemove(ctx context.Context, domainName, selector string) {
2674 d, err := dns.ParseDomain(domainName)
2675 xcheckuserf(ctx, err, "parsing domain")
2676 s, err := dns.ParseDomain(selector)
2677 xcheckuserf(ctx, err, "parsing selector")
2678 err = admin.DKIMRemove(ctx, d, s)
2679 xcheckf(ctx, err, "removing dkim key")
2682// DomainDKIMSave saves the settings of selectors, and which to enable for
2683// signing, for a domain. All currently configured selectors must be present,
2684// selectors cannot be added/removed with this function.
2685func (Admin) DomainDKIMSave(ctx context.Context, domainName string, selectors map[string]config.Selector, sign []string) {
2686 for _, s := range sign {
2687 if _, ok := selectors[s]; !ok {
2688 xcheckuserf(ctx, fmt.Errorf("cannot sign unknown selector %q", s), "checking selectors")
2692 err := admin.DomainSave(ctx, domainName, func(d *config.Domain) error {
2693 if len(selectors) != len(d.DKIM.Selectors) {
2694 xcheckuserf(ctx, fmt.Errorf("cannot add/remove dkim selectors with this function"), "checking selectors")
2696 for s := range selectors {
2697 if _, ok := d.DKIM.Selectors[s]; !ok {
2698 xcheckuserf(ctx, fmt.Errorf("unknown selector %q", s), "checking selectors")
2701 // At least the selectors are the same.
2703 // Build up new selectors.
2704 sels := map[string]config.Selector{}
2705 for name, nsel := range selectors {
2706 osel := d.DKIM.Selectors[name]
2707 xsel := config.Selector{
2709 Canonicalization: nsel.Canonicalization,
2710 DontSealHeaders: nsel.DontSealHeaders,
2711 Expiration: nsel.Expiration,
2713 PrivateKeyFile: osel.PrivateKeyFile,
2715 if !slices.Equal(osel.HeadersEffective, nsel.Headers) {
2716 xsel.Headers = nsel.Headers
2721 // Enable the new selector settings.
2722 d.DKIM = config.DKIM{
2728 xcheckf(ctx, err, "saving dkim selector for domain")
2731// DomainDisabledSave saves the Disabled field of a domain. A disabled domain
2732// rejects incoming/outgoing messages involving the domain and does not request new
2733// TLS certificats with ACME.
2734func (Admin) DomainDisabledSave(ctx context.Context, domainName string, disabled bool) {
2735 err := admin.DomainSave(ctx, domainName, func(d *config.Domain) error {
2736 d.Disabled = disabled
2739 xcheckf(ctx, err, "saving disabled setting for domain")
2742func xparseAddress(ctx context.Context, lp, domain string) smtp.Address {
2743 xlp, err := smtp.ParseLocalpart(lp)
2744 xcheckuserf(ctx, err, "parsing localpart")
2745 d, err := dns.ParseDomain(domain)
2746 xcheckuserf(ctx, err, "parsing domain")
2747 return smtp.NewAddress(xlp, d)
2750func (Admin) AliasAdd(ctx context.Context, aliaslp string, domainName string, alias config.Alias) {
2751 addr := xparseAddress(ctx, aliaslp, domainName)
2752 err := admin.AliasAdd(ctx, addr, alias)
2753 xcheckf(ctx, err, "adding alias")
2756func (Admin) AliasUpdate(ctx context.Context, aliaslp string, domainName string, postPublic, listMembers, allowMsgFrom bool) {
2757 addr := xparseAddress(ctx, aliaslp, domainName)
2758 alias := config.Alias{
2759 PostPublic: postPublic,
2760 ListMembers: listMembers,
2761 AllowMsgFrom: allowMsgFrom,
2763 err := admin.AliasUpdate(ctx, addr, alias)
2764 xcheckf(ctx, err, "saving alias")
2767func (Admin) AliasRemove(ctx context.Context, aliaslp string, domainName string) {
2768 addr := xparseAddress(ctx, aliaslp, domainName)
2769 err := admin.AliasRemove(ctx, addr)
2770 xcheckf(ctx, err, "removing alias")
2773func (Admin) AliasAddressesAdd(ctx context.Context, aliaslp string, domainName string, addresses []string) {
2774 addr := xparseAddress(ctx, aliaslp, domainName)
2775 err := admin.AliasAddressesAdd(ctx, addr, addresses)
2776 xcheckf(ctx, err, "adding address to alias")
2779func (Admin) AliasAddressesRemove(ctx context.Context, aliaslp string, domainName string, addresses []string) {
2780 addr := xparseAddress(ctx, aliaslp, domainName)
2781 err := admin.AliasAddressesRemove(ctx, addr, addresses)
2782 xcheckf(ctx, err, "removing address from alias")
2785func (Admin) TLSPublicKeys(ctx context.Context, accountOpt string) ([]store.TLSPublicKey, error) {
2786 return store.TLSPublicKeyList(ctx, accountOpt)
2789func (Admin) LoginAttempts(ctx context.Context, accountName string, limit int) []store.LoginAttempt {
2790 l, err := store.LoginAttemptList(ctx, accountName, limit)
2791 xcheckf(ctx, err, "listing login attempts")