1// Package webadmin is a web app for the mox administrator for viewing and changing
2// the configuration, like creating/removing accounts, viewing DMARC and TLS
3// reports, check DNS records for a domain, change the webserver configuration,
13 cryptorand "crypto/rand"
38 "golang.org/x/exp/maps"
39 "golang.org/x/text/unicode/norm"
41 "github.com/mjl-/adns"
43 "github.com/mjl-/bstore"
44 "github.com/mjl-/sherpa"
45 "github.com/mjl-/sherpadoc"
46 "github.com/mjl-/sherpaprom"
48 "github.com/mjl-/mox/config"
49 "github.com/mjl-/mox/dkim"
50 "github.com/mjl-/mox/dmarc"
51 "github.com/mjl-/mox/dmarcdb"
52 "github.com/mjl-/mox/dmarcrpt"
53 "github.com/mjl-/mox/dns"
54 "github.com/mjl-/mox/dnsbl"
55 "github.com/mjl-/mox/metrics"
56 "github.com/mjl-/mox/mlog"
57 mox "github.com/mjl-/mox/mox-"
58 "github.com/mjl-/mox/moxvar"
59 "github.com/mjl-/mox/mtasts"
60 "github.com/mjl-/mox/mtastsdb"
61 "github.com/mjl-/mox/publicsuffix"
62 "github.com/mjl-/mox/queue"
63 "github.com/mjl-/mox/smtp"
64 "github.com/mjl-/mox/spf"
65 "github.com/mjl-/mox/store"
66 "github.com/mjl-/mox/tlsrpt"
67 "github.com/mjl-/mox/tlsrptdb"
68 "github.com/mjl-/mox/webauth"
71var pkglog = mlog.New("webadmin", nil)
74var adminapiJSON []byte
82var webadminFile = &mox.WebappFile{
85 HTMLPath: filepath.FromSlash("webadmin/admin.html"),
86 JSPath: filepath.FromSlash("webadmin/admin.js"),
89var adminDoc = mustParseAPI("admin", adminapiJSON)
91func mustParseAPI(api string, buf []byte) (doc sherpadoc.Section) {
92 err := json.Unmarshal(buf, &doc)
94 pkglog.Fatalx("parsing webadmin api docs", err, slog.String("api", api))
99var sherpaHandlerOpts *sherpa.HandlerOpts
101func makeSherpaHandler(cookiePath string, isForwarded bool) (http.Handler, error) {
102 return sherpa.NewHandler("/api/", moxvar.Version, Admin{cookiePath, isForwarded}, &adminDoc, sherpaHandlerOpts)
106 collector, err := sherpaprom.NewCollector("moxadmin", nil)
108 pkglog.Fatalx("creating sherpa prometheus collector", err)
111 sherpaHandlerOpts = &sherpa.HandlerOpts{Collector: collector, AdjustFunctionNames: "none", NoCORS: true}
113 _, err = makeSherpaHandler("", false)
115 pkglog.Fatalx("sherpa handler", err)
118 mox.NewWebadminHandler = func(basePath string, isForwarded bool) http.Handler {
119 return http.HandlerFunc(Handler(basePath, isForwarded))
123// Handler returns a handler for the webadmin endpoints, customized for the
125func Handler(cookiePath string, isForwarded bool) func(w http.ResponseWriter, r *http.Request) {
126 sh, err := makeSherpaHandler(cookiePath, isForwarded)
127 return func(w http.ResponseWriter, r *http.Request) {
129 http.Error(w, "500 - internal server error - cannot handle requests", http.StatusInternalServerError)
132 handle(sh, isForwarded, w, r)
136// Admin exports web API functions for the admin web interface. All its methods are
137// exported under api/. Function calls require valid HTTP Authentication
138// credentials of a user.
140 cookiePath string // From listener, for setting authentication cookies.
141 isForwarded bool // From listener, whether we look at X-Forwarded-* headers.
146var requestInfoCtxKey ctxKey = "requestInfo"
148type requestInfo struct {
149 SessionToken store.SessionToken
150 Response http.ResponseWriter
151 Request *http.Request // For Proto and TLS connection state during message submit.
154func handle(apiHandler http.Handler, isForwarded bool, w http.ResponseWriter, r *http.Request) {
155 ctx := context.WithValue(r.Context(), mlog.CidKey, mox.Cid())
156 log := pkglog.WithContext(ctx).With(slog.String("adminauth", ""))
158 // HTML/JS can be retrieved without authentication.
159 if r.URL.Path == "/" {
162 webadminFile.Serve(ctx, log, w, r)
164 http.Error(w, "405 - method not allowed - use get", http.StatusMethodNotAllowed)
167 } else if r.URL.Path == "/licenses.txt" {
170 w.Header().Set("Content-Type", "text/plain; charset=utf-8")
173 http.Error(w, "405 - method not allowed - use get", http.StatusMethodNotAllowed)
178 isAPI := strings.HasPrefix(r.URL.Path, "/api/")
179 // Only allow POST for calls, they will not work cross-domain without CORS.
180 if isAPI && r.URL.Path != "/api/" && r.Method != "POST" {
181 http.Error(w, "405 - method not allowed - use post", http.StatusMethodNotAllowed)
185 // All other URLs, except the login endpoint require some authentication.
186 var sessionToken store.SessionToken
187 if r.URL.Path != "/api/LoginPrep" && r.URL.Path != "/api/Login" {
189 _, sessionToken, _, ok = webauth.Check(ctx, log, webauth.Admin, "webadmin", isForwarded, w, r, isAPI, isAPI, false)
191 // Response has been written already.
197 reqInfo := requestInfo{sessionToken, w, r}
198 ctx = context.WithValue(ctx, requestInfoCtxKey, reqInfo)
199 apiHandler.ServeHTTP(w, r.WithContext(ctx))
206func xcheckf(ctx context.Context, err error, format string, args ...any) {
210 // If caller tried saving a config that is invalid, or because of a bad request, cause a user error.
211 if errors.Is(err, mox.ErrConfig) || errors.Is(err, mox.ErrRequest) {
212 xcheckuserf(ctx, err, format, args...)
215 msg := fmt.Sprintf(format, args...)
216 errmsg := fmt.Sprintf("%s: %s", msg, err)
217 pkglog.WithContext(ctx).Errorx(msg, err)
218 code := "server:error"
219 if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
222 panic(&sherpa.Error{Code: code, Message: errmsg})
225func xcheckuserf(ctx context.Context, err error, format string, args ...any) {
229 msg := fmt.Sprintf(format, args...)
230 errmsg := fmt.Sprintf("%s: %s", msg, err)
231 pkglog.WithContext(ctx).Errorx(msg, err)
232 panic(&sherpa.Error{Code: "user:error", Message: errmsg})
235func xusererrorf(ctx context.Context, format string, args ...any) {
236 msg := fmt.Sprintf(format, args...)
237 pkglog.WithContext(ctx).Error(msg)
238 panic(&sherpa.Error{Code: "user:error", Message: msg})
241// LoginPrep returns a login token, and also sets it as cookie. Both must be
242// present in the call to Login.
243func (w Admin) LoginPrep(ctx context.Context) string {
244 log := pkglog.WithContext(ctx)
245 reqInfo := ctx.Value(requestInfoCtxKey).(requestInfo)
248 _, err := cryptorand.Read(data[:])
249 xcheckf(ctx, err, "generate token")
250 loginToken := base64.RawURLEncoding.EncodeToString(data[:])
252 webauth.LoginPrep(ctx, log, "webadmin", w.cookiePath, w.isForwarded, reqInfo.Response, reqInfo.Request, loginToken)
257// Login returns a session token for the credentials, or fails with error code
258// "user:badLogin". Call LoginPrep to get a loginToken.
259func (w Admin) Login(ctx context.Context, loginToken, password string) store.CSRFToken {
260 log := pkglog.WithContext(ctx)
261 reqInfo := ctx.Value(requestInfoCtxKey).(requestInfo)
263 csrfToken, err := webauth.Login(ctx, log, webauth.Admin, "webadmin", w.cookiePath, w.isForwarded, reqInfo.Response, reqInfo.Request, loginToken, "", password)
264 if _, ok := err.(*sherpa.Error); ok {
267 xcheckf(ctx, err, "login")
271// Logout invalidates the session token.
272func (w Admin) Logout(ctx context.Context) {
273 log := pkglog.WithContext(ctx)
274 reqInfo := ctx.Value(requestInfoCtxKey).(requestInfo)
276 err := webauth.Logout(ctx, log, webauth.Admin, "webadmin", w.cookiePath, w.isForwarded, reqInfo.Response, reqInfo.Request, "", reqInfo.SessionToken)
277 xcheckf(ctx, err, "logout")
283 Instructions []string
286type DNSSECResult struct {
290type IPRevCheckResult struct {
291 Hostname dns.Domain // This hostname, IPs must resolve back to this.
292 IPNames map[string][]string // IP to names.
302type MXCheckResult struct {
307type TLSCheckResult struct {
311type DANECheckResult struct {
315type SPFRecord struct {
319type SPFCheckResult struct {
321 DomainRecord *SPFRecord
323 HostRecord *SPFRecord
327type DKIMCheckResult struct {
332type DKIMRecord struct {
338type DMARCRecord struct {
342type DMARCCheckResult struct {
349type TLSRPTRecord struct {
353type TLSRPTCheckResult struct {
359type MTASTSRecord struct {
362type MTASTSCheckResult struct {
366 Policy *mtasts.Policy
370type SRVConfCheckResult struct {
371 SRVs map[string][]net.SRV // Service (e.g. "_imaps") to records.
375type AutoconfCheckResult struct {
376 ClientSettingsDomainIPs []string
381type AutodiscoverSRV struct {
386type AutodiscoverCheckResult struct {
387 Records []AutodiscoverSRV
391// CheckResult is the analysis of a domain, its actual configuration (DNS, TLS,
392// connectivity) and the mox configuration. It includes configuration instructions
393// (e.g. DNS records), and warnings and errors encountered.
394type CheckResult struct {
397 IPRev IPRevCheckResult
403 DMARC DMARCCheckResult
404 HostTLSRPT TLSRPTCheckResult
405 DomainTLSRPT TLSRPTCheckResult
406 MTASTS MTASTSCheckResult
407 SRVConf SRVConfCheckResult
408 Autoconf AutoconfCheckResult
409 Autodiscover AutodiscoverCheckResult
412// logPanic can be called with a defer from a goroutine to prevent the entire program from being shutdown in case of a panic.
413func logPanic(ctx context.Context) {
418 pkglog.WithContext(ctx).Error("recover from panic", slog.Any("panic", x))
420 metrics.PanicInc(metrics.Webadmin)
423// return IPs we may be listening on.
424func xlistenIPs(ctx context.Context, receiveOnly bool) []net.IP {
425 ips, err := mox.IPs(ctx, receiveOnly)
426 xcheckf(ctx, err, "listing ips")
430// return IPs from which we may be sending.
431func xsendingIPs(ctx context.Context) []net.IP {
432 ips, err := mox.IPs(ctx, false)
433 xcheckf(ctx, err, "listing ips")
437// CheckDomain checks the configuration for the domain, such as MX, SMTP STARTTLS,
438// SPF, DKIM, DMARC, TLSRPT, MTASTS, autoconfig, autodiscover.
439func (Admin) CheckDomain(ctx context.Context, domainName string) (r CheckResult) {
440 // todo future: should run these checks without a DNS cache so recent changes are picked up.
442 resolver := dns.StrictResolver{Pkg: "check", Log: pkglog.WithContext(ctx).Logger}
443 dialer := &net.Dialer{Timeout: 10 * time.Second}
444 nctx, cancel := context.WithTimeout(ctx, 30*time.Second)
446 return checkDomain(nctx, resolver, dialer, domainName)
449func unptr[T any](l []*T) []T {
453 r := make([]T, len(l))
454 for i, e := range l {
460func checkDomain(ctx context.Context, resolver dns.Resolver, dialer *net.Dialer, domainName string) (r CheckResult) {
461 log := pkglog.WithContext(ctx)
463 domain, err := dns.ParseDomain(domainName)
464 xcheckuserf(ctx, err, "parsing domain")
466 domConf, ok := mox.Conf.Domain(domain)
468 panic(&sherpa.Error{Code: "user:notFound", Message: "domain not found"})
471 listenIPs := xlistenIPs(ctx, true)
472 isListenIP := func(ip net.IP) bool {
473 for _, lip := range listenIPs {
481 addf := func(l *[]string, format string, args ...any) {
482 *l = append(*l, fmt.Sprintf(format, args...))
485 // Host must be an absolute dns name, ending with a dot.
486 lookupIPs := func(errors *[]string, host string) (ips []string, ourIPs, notOurIPs []net.IP, rerr error) {
487 addrs, _, err := resolver.LookupHost(ctx, host)
489 addf(errors, "Looking up %q: %s", host, err)
490 return nil, nil, nil, err
492 for _, addr := range addrs {
493 ip := net.ParseIP(addr)
495 addf(errors, "Bad IP %q", addr)
498 ips = append(ips, ip.String())
500 ourIPs = append(ourIPs, ip)
502 notOurIPs = append(notOurIPs, ip)
505 return ips, ourIPs, notOurIPs, nil
508 checkTLS := func(errors *[]string, host string, ips []string, port string) {
514 RootCAs: mox.Conf.Static.TLS.CertPool,
517 for _, ip := range ips {
518 conn, err := d.DialContext(ctx, "tcp", net.JoinHostPort(ip, port))
520 addf(errors, "TLS connection to hostname %q, IP %q: %s", host, ip, err)
527 // If at least one listener with SMTP enabled has unspecified NATed IPs, we'll skip
528 // some checks related to these IPs.
529 var isNAT, isUnspecifiedNAT bool
530 for _, l := range mox.Conf.Static.Listeners {
535 isUnspecifiedNAT = true
538 if len(l.NATIPs) > 0 {
543 var wg sync.WaitGroup
551 // Some DNSSEC-verifying resolvers return unauthentic data for ".", so we check "com".
552 _, result, err := resolver.LookupNS(ctx, "com.")
554 addf(&r.DNSSEC.Errors, "Looking up NS for DNS root (.) to check support in resolver for DNSSEC-verification: %s", err)
555 } else if !result.Authentic {
556 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.`)
558 _, result, _ := resolver.LookupMX(ctx, domain.ASCII+".")
559 if !result.Authentic {
560 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.`)
564 addf(&r.DNSSEC.Instructions, `Enable DNSSEC-signing of the DNS records of your domain (zone) at your DNS hosting provider.`)
566 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".
568cat <<EOF >/etc/unbound/unbound.conf.d/ede.conf
582 // For each mox.Conf.SpecifiedSMTPListenIPs and all NATIPs, and each IP for
583 // mox.Conf.HostnameDomain, check if they resolve back to the host name.
584 hostIPs := map[dns.Domain][]net.IP{}
585 ips, _, err := resolver.LookupIP(ctx, "ip", mox.Conf.Static.HostnameDomain.ASCII+".")
587 addf(&r.IPRev.Errors, "Looking up IPs for hostname: %s", err)
590 gatherMoreIPs := func(publicIPs []net.IP) {
592 for _, ip := range publicIPs {
593 for _, xip := range ips {
598 ips = append(ips, ip)
602 gatherMoreIPs(mox.Conf.Static.SpecifiedSMTPListenIPs)
604 for _, l := range mox.Conf.Static.Listeners {
609 for _, ip := range l.NATIPs {
610 natips = append(natips, net.ParseIP(ip))
612 gatherMoreIPs(natips)
614 hostIPs[mox.Conf.Static.HostnameDomain] = ips
616 iplist := func(ips []net.IP) string {
618 for _, ip := range ips {
619 ipstrs = append(ipstrs, ip.String())
621 return strings.Join(ipstrs, ", ")
624 r.IPRev.Hostname = mox.Conf.Static.HostnameDomain
625 r.IPRev.Instructions = []string{
626 fmt.Sprintf("Ensure IPs %s have reverse address %s.", iplist(ips), mox.Conf.Static.HostnameDomain.ASCII),
629 // If we have a socks transport, also check its host and IP.
630 for tname, t := range mox.Conf.Static.Transports {
632 hostIPs[t.Socks.Hostname] = append(hostIPs[t.Socks.Hostname], t.Socks.IPs...)
633 instr := fmt.Sprintf("For SOCKS transport %s, ensure IPs %s have reverse address %s.", tname, iplist(t.Socks.IPs), t.Socks.Hostname)
634 r.IPRev.Instructions = append(r.IPRev.Instructions, instr)
644 results := make(chan result)
646 for host, ips := range hostIPs {
647 for _, ip := range ips {
652 addrs, _, err := resolver.LookupAddr(ctx, s)
653 results <- result{host, s, addrs, err}
657 r.IPRev.IPNames = map[string][]string{}
658 for i := 0; i < n; i++ {
660 host, addrs, ip, err := lr.Host, lr.Addrs, lr.IP, lr.Err
662 addf(&r.IPRev.Errors, "Looking up reverse name for %s of %s: %v", ip, host, err)
666 addf(&r.IPRev.Errors, "Expected exactly 1 name for %s of %s, got %d (%v)", ip, host, len(addrs), addrs)
669 for i, a := range addrs {
670 a = strings.TrimRight(a, ".")
672 ad, err := dns.ParseDomain(a)
674 addf(&r.IPRev.Errors, "Parsing reverse name %q for %s: %v", a, ip, err)
681 addf(&r.IPRev.Errors, "Reverse name(s) %s for ip %s do not match hostname %s, which will cause other mail servers to reject incoming messages from this IP.", strings.Join(addrs, ","), ip, host)
683 r.IPRev.IPNames[ip] = addrs
686 // Linux machines are often initially set up with a loopback IP for the hostname in
687 // /etc/hosts, presumably because it isn't known if their external IPs are static.
688 // For mail servers, they should certainly be static. The quickstart would also
689 // have warned about this, but could have been missed/ignored.
690 for _, ip := range ips {
692 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())
703 mxs, _, err := resolver.LookupMX(ctx, domain.ASCII+".")
705 addf(&r.MX.Errors, "Looking up MX records for %s: %s", domain, err)
707 r.MX.Records = make([]MX, len(mxs))
708 for i, mx := range mxs {
709 r.MX.Records[i] = MX{mx.Host, int(mx.Pref), nil}
711 if len(mxs) == 1 && mxs[0].Host == "." {
712 addf(&r.MX.Errors, `MX records consists of explicit null mx record (".") indicating that domain does not accept email.`)
715 for i, mx := range mxs {
716 ips, ourIPs, notOurIPs, err := lookupIPs(&r.MX.Errors, mx.Host)
718 addf(&r.MX.Errors, "Looking up IPs for mx host %q: %s", mx.Host, err)
720 r.MX.Records[i].IPs = ips
721 if isUnspecifiedNAT {
724 if len(ourIPs) == 0 {
725 addf(&r.MX.Errors, "None of the IPs that mx %q points to is ours: %v", mx.Host, notOurIPs)
726 } else if len(notOurIPs) > 0 {
727 addf(&r.MX.Errors, "Some of the IPs that mx %q points to are not ours: %v", mx.Host, notOurIPs)
731 r.MX.Instructions = []string{
732 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+"."),
736 // TLS, mostly checking certificate expiration and CA trust.
737 // 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.
743 // MTA-STS, autoconfig, autodiscover are checked in their sections.
745 // Dial a single MX host with given IP and perform STARTTLS handshake.
746 dialSMTPSTARTTLS := func(host, ip string) error {
747 conn, err := dialer.DialContext(ctx, "tcp", net.JoinHostPort(ip, "25"))
757 end := time.Now().Add(10 * time.Second)
758 cctx, cancel := context.WithTimeout(ctx, 10*time.Second)
760 err = conn.SetDeadline(end)
761 log.WithContext(ctx).Check(err, "setting deadline")
763 br := bufio.NewReader(conn)
764 _, err = br.ReadString('\n')
766 return fmt.Errorf("reading SMTP banner from remote: %s", err)
768 if _, err := fmt.Fprintf(conn, "EHLO moxtest\r\n"); err != nil {
769 return fmt.Errorf("writing SMTP EHLO to remote: %s", err)
772 line, err := br.ReadString('\n')
774 return fmt.Errorf("reading SMTP EHLO response from remote: %s", err)
776 if strings.HasPrefix(line, "250-") {
779 if strings.HasPrefix(line, "250 ") {
782 return fmt.Errorf("unexpected response to SMTP EHLO from remote: %q", strings.TrimSuffix(line, "\r\n"))
784 if _, err := fmt.Fprintf(conn, "STARTTLS\r\n"); err != nil {
785 return fmt.Errorf("writing SMTP STARTTLS to remote: %s", err)
787 line, err := br.ReadString('\n')
789 return fmt.Errorf("reading response to SMTP STARTTLS from remote: %s", err)
791 if !strings.HasPrefix(line, "220 ") {
792 return fmt.Errorf("SMTP STARTTLS response from remote not 220 OK: %q", strings.TrimSuffix(line, "\r\n"))
794 config := &tls.Config{
796 RootCAs: mox.Conf.Static.TLS.CertPool,
798 tlsconn := tls.Client(conn, config)
799 if err := tlsconn.HandshakeContext(cctx); err != nil {
800 return fmt.Errorf("TLS handshake after SMTP STARTTLS: %s", err)
808 checkSMTPSTARTTLS := func() {
809 // Initial errors are ignored, will already have been warned about by MX checks.
810 mxs, _, err := resolver.LookupMX(ctx, domain.ASCII+".")
814 if len(mxs) == 1 && mxs[0].Host == "." {
817 for _, mx := range mxs {
818 ips, _, _, err := lookupIPs(&r.MX.Errors, mx.Host)
823 for _, ip := range ips {
824 if err := dialSMTPSTARTTLS(mx.Host, ip); err != nil {
825 addf(&r.TLS.Errors, "SMTP connection with STARTTLS to MX hostname %q IP %s: %s", mx.Host, ip, err)
841 daneRecords := func(l config.Listener) map[string]struct{} {
845 records := map[string]struct{}{}
846 addRecord := func(privKey crypto.Signer) {
847 spkiBuf, err := x509.MarshalPKIXPublicKey(privKey.Public())
849 addf(&r.DANE.Errors, "marshal SubjectPublicKeyInfo for DANE record: %v", err)
852 sum := sha256.Sum256(spkiBuf)
854 Usage: adns.TLSAUsageDANEEE,
855 Selector: adns.TLSASelectorSPKI,
856 MatchType: adns.TLSAMatchTypeSHA256,
859 records[r.Record()] = struct{}{}
861 for _, privKey := range l.TLS.HostPrivateRSA2048Keys {
864 for _, privKey := range l.TLS.HostPrivateECDSAP256Keys {
870 expectedDANERecords := func(host string) map[string]struct{} {
871 for _, l := range mox.Conf.Static.Listeners {
872 if l.HostnameDomain.ASCII == host {
873 return daneRecords(l)
876 public := mox.Conf.Static.Listeners["public"]
877 if mox.Conf.Static.HostnameDomain.ASCII == host && public.HostnameDomain.ASCII == "" {
878 return daneRecords(public)
883 mxl, result, err := resolver.LookupMX(ctx, domain.ASCII+".")
885 addf(&r.DANE.Errors, "Looking up MX hosts to check for DANE records: %s", err)
887 if !result.Authentic {
888 addf(&r.DANE.Warnings, "DANE is inactive because MX records are not DNSSEC-signed.")
890 for _, mx := range mxl {
891 expect := expectedDANERecords(mx.Host)
893 tlsal, tlsaResult, err := resolver.LookupTLSA(ctx, 25, "tcp", mx.Host+".")
894 if dns.IsNotFound(err) {
896 addf(&r.DANE.Errors, "No DANE records for MX host %s, expected: %s.", mx.Host, strings.Join(maps.Keys(expect), "; "))
899 } else if err != nil {
900 addf(&r.DANE.Errors, "Looking up DANE records for MX host %s: %v", mx.Host, err)
902 } else if !tlsaResult.Authentic && len(tlsal) > 0 {
903 addf(&r.DANE.Errors, "DANE records exist for MX host %s, but are not DNSSEC-signed.", mx.Host)
906 extra := map[string]struct{}{}
907 for _, e := range tlsal {
909 if _, ok := expect[s]; ok {
912 extra[s] = struct{}{}
916 l := maps.Keys(expect)
918 addf(&r.DANE.Errors, "Missing DANE records of type TLSA for MX host _25._tcp.%s: %s", mx.Host, strings.Join(l, "; "))
921 l := maps.Keys(extra)
923 addf(&r.DANE.Errors, "Unexpected DANE records of type TLSA for MX host _25._tcp.%s: %s", mx.Host, strings.Join(l, "; "))
928 public := mox.Conf.Static.Listeners["public"]
929 pubDom := public.HostnameDomain
930 if pubDom.ASCII == "" {
931 pubDom = mox.Conf.Static.HostnameDomain
933 records := maps.Keys(daneRecords(public))
934 sort.Strings(records)
935 if len(records) > 0 {
936 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"
937 for _, r := range records {
938 instr += fmt.Sprintf("\t_25._tcp.%s. TLSA %s\n", pubDom.ASCII, r)
940 addf(&r.DANE.Instructions, instr)
942 addf(&r.DANE.Warnings, "DANE not configured: no static TLS host keys.")
944 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."
945 addf(&r.DANE.Instructions, instr)
950 // 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.
956 // Verify a domain with the configured IPs that do SMTP.
957 verifySPF := func(isHost bool, domain dns.Domain) (string, *SPFRecord, spf.Record) {
963 _, txt, record, _, err := spf.Lookup(ctx, log.Logger, resolver, domain)
965 addf(&r.SPF.Errors, "Looking up %s SPF record: %s", kind, err)
967 var xrecord *SPFRecord
969 xrecord = &SPFRecord{*record}
976 checkSPFIP := func(ip net.IP) {
981 spfr.Directives = append(spfr.Directives, spf.Directive{Mechanism: mechanism, IP: ip})
989 MailFromLocalpart: "postmaster",
990 MailFromDomain: domain,
991 HelloDomain: dns.IPDomain{Domain: domain},
992 LocalIP: net.ParseIP("127.0.0.1"),
993 LocalHostname: dns.Domain{ASCII: "localhost"},
995 status, mechanism, expl, _, err := spf.Evaluate(ctx, log.Logger, record, resolver, args)
997 addf(&r.SPF.Errors, "Evaluating IP %q against %s SPF record: %s", ip, kind, err)
998 } else if status != spf.StatusPass {
999 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)
1003 for _, ip := range mox.DomainSPFIPs() {
1008 spfr.Directives = append(spfr.Directives, spf.Directive{Mechanism: "mx"})
1015 spfr.Directives = append(spfr.Directives, spf.Directive{Qualifier: qual, Mechanism: "all"})
1016 return txt, xrecord, spfr
1019 // Check SPF record for domain.
1020 var dspfr spf.Record
1021 r.SPF.DomainTXT, r.SPF.DomainRecord, dspfr = verifySPF(false, domain)
1022 // todo: possibly check all hosts for MX records? assuming they are also sending mail servers.
1023 r.SPF.HostTXT, r.SPF.HostRecord, _ = verifySPF(true, mox.Conf.Static.HostnameDomain)
1025 dtxt, err := dspfr.Record()
1027 addf(&r.SPF.Errors, "Making SPF record for instructions: %s", err)
1029 domainspf := fmt.Sprintf("%s TXT %s", domain.ASCII+".", mox.TXTStrings(dtxt))
1032 hostspf := fmt.Sprintf(`%s TXT "v=spf1 a -all"`, mox.Conf.Static.HostnameDomain.ASCII+".")
1034 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)
1038 // 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.
1044 var missing []string
1045 var haveEd25519 bool
1046 for sel, selc := range domConf.DKIM.Selectors {
1047 if _, ok := selc.Key.(ed25519.PrivateKey); ok {
1051 _, record, txt, _, err := dkim.Lookup(ctx, log.Logger, resolver, selc.Domain, domain)
1053 missing = append(missing, sel)
1054 if errors.Is(err, dkim.ErrNoRecord) {
1055 addf(&r.DKIM.Errors, "No DKIM DNS record for selector %q.", sel)
1056 } else if errors.Is(err, dkim.ErrSyntax) {
1057 addf(&r.DKIM.Errors, "Parsing DKIM DNS record for selector %q: %s", sel, err)
1059 addf(&r.DKIM.Errors, "Fetching DKIM record for selector %q: %s", sel, err)
1063 r.DKIM.Records = append(r.DKIM.Records, DKIMRecord{sel, txt, record})
1064 pubKey := selc.Key.Public()
1066 switch k := pubKey.(type) {
1067 case *rsa.PublicKey:
1069 pk, err = x509.MarshalPKIXPublicKey(k)
1071 addf(&r.DKIM.Errors, "Marshal public key for %q to compare against DNS: %s", sel, err)
1074 case ed25519.PublicKey:
1077 addf(&r.DKIM.Errors, "Internal error: unknown public key type %T.", pubKey)
1081 if record != nil && !bytes.Equal(record.Pubkey, pk) {
1082 addf(&r.DKIM.Errors, "For selector %q, the public key in DKIM DNS TXT record does not match with configured private key.", sel)
1083 missing = append(missing, sel)
1087 if len(domConf.DKIM.Selectors) == 0 {
1088 addf(&r.DKIM.Errors, "No DKIM configuration, add a key to the configuration file, and instructions for DNS records will appear here.")
1089 } else if !haveEd25519 {
1090 addf(&r.DKIM.Warnings, "Consider adding an ed25519 key: the keys are smaller, the cryptography faster and more modern.")
1093 for _, sel := range missing {
1094 dkimr := dkim.Record{
1096 Hashes: []string{"sha256"},
1097 PublicKey: domConf.DKIM.Selectors[sel].Key.Public(),
1099 switch dkimr.PublicKey.(type) {
1100 case *rsa.PublicKey:
1101 case ed25519.PublicKey:
1102 dkimr.Key = "ed25519"
1104 addf(&r.DKIM.Errors, "Internal error: unknown public key type %T.", dkimr.PublicKey)
1106 txt, err := dkimr.Record()
1108 addf(&r.DKIM.Errors, "Making DKIM record for instructions: %s", err)
1111 instr += fmt.Sprintf("\n\t%s._domainkey.%s TXT %s\n", sel, domain.ASCII+".", mox.TXTStrings(txt))
1114 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
1115 addf(&r.DKIM.Instructions, "%s", instr)
1125 _, dmarcDomain, record, txt, _, err := dmarc.Lookup(ctx, log.Logger, resolver, domain)
1127 addf(&r.DMARC.Errors, "Looking up DMARC record: %s", err)
1128 } else if record == nil {
1129 addf(&r.DMARC.Errors, "No DMARC record")
1131 r.DMARC.Domain = dmarcDomain.Name()
1134 r.DMARC.Record = &DMARCRecord{*record}
1136 if record != nil && record.Policy == "none" {
1137 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.")
1139 if record != nil && record.SubdomainPolicy == "none" {
1140 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.")
1142 if record != nil && len(record.AggregateReportAddresses) == 0 {
1143 addf(&r.DMARC.Warnings, "It is recommended you specify you would like aggregate reports about delivery success in the DMARC record, see instructions.")
1146 dmarcr := dmarc.DefaultRecord
1147 dmarcr.Policy = "reject"
1150 if domConf.DMARC != nil {
1151 // If the domain is in a different Organizational Domain, the receiving domain
1152 // needs a special DNS record to opt-in to receiving reports. We check for that
1155 orgDom := publicsuffix.Lookup(ctx, log.Logger, domain)
1156 destOrgDom := publicsuffix.Lookup(ctx, log.Logger, domConf.DMARC.DNSDomain)
1157 if orgDom != destOrgDom {
1158 accepts, status, _, _, _, err := dmarc.LookupExternalReportsAccepted(ctx, log.Logger, resolver, domain, domConf.DMARC.DNSDomain)
1159 if status != dmarc.StatusNone {
1160 addf(&r.DMARC.Errors, "Checking if external destination accepts reports: %s", err)
1161 } else if !accepts {
1162 addf(&r.DMARC.Errors, "External destination does not accept reports (%s)", err)
1164 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)
1169 Opaque: smtp.NewAddress(domConf.DMARC.ParsedLocalpart, domConf.DMARC.DNSDomain).Pack(false),
1171 uristr := uri.String()
1172 dmarcr.AggregateReportAddresses = []dmarc.URI{
1173 {Address: uristr, MaxSize: 10, Unit: "m"},
1178 for _, addr := range record.AggregateReportAddresses {
1179 if addr.Address == uristr {
1185 addf(&r.DMARC.Errors, "Configured DMARC reporting address is not present in record.")
1189 addf(&r.DMARC.Instructions, `Configure a DMARC destination in domain in config file.`)
1191 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()))
1192 addf(&r.DMARC.Instructions, instr)
1194 addf(&r.DMARC.Instructions, extInstr)
1198 checkTLSRPT := func(result *TLSRPTCheckResult, dom dns.Domain, address smtp.Address, isHost bool) {
1202 record, txt, err := tlsrpt.Lookup(ctx, log.Logger, resolver, dom)
1204 addf(&result.Errors, "Looking up TLSRPT record: %s", err)
1208 result.Record = &TLSRPTRecord{*record}
1211 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.`
1212 var zeroaddr smtp.Address
1213 if address != zeroaddr {
1214 // TLSRPT does not require validation of reporting addresses outside the domain.
1218 Opaque: address.Pack(false),
1220 rua := tlsrpt.RUA(uri.String())
1221 tlsrptr := &tlsrpt.Record{
1222 Version: "TLSRPTv1",
1223 RUAs: [][]tlsrpt.RUA{{rua}},
1225 instr += fmt.Sprintf(`
1227Ensure a DNS TXT record like the following exists:
1229 _smtp._tls.%s TXT %s
1231`, dom.ASCII+".", mox.TXTStrings(tlsrptr.String()))
1236 for _, l := range record.RUAs {
1237 for _, e := range l {
1245 addf(&result.Errors, `Configured reporting address is not present in TLSRPT record.`)
1250 addf(&result.Errors, `Configure a host TLSRPT localpart in static mox.conf config file.`)
1252 addf(&result.Errors, `Configure a domain TLSRPT destination in domains.conf config file.`)
1254 addf(&result.Instructions, instr)
1259 var hostTLSRPTAddr smtp.Address
1260 if mox.Conf.Static.HostTLSRPT.Localpart != "" {
1261 hostTLSRPTAddr = smtp.NewAddress(mox.Conf.Static.HostTLSRPT.ParsedLocalpart, mox.Conf.Static.HostnameDomain)
1263 go checkTLSRPT(&r.HostTLSRPT, mox.Conf.Static.HostnameDomain, hostTLSRPTAddr, true)
1267 var domainTLSRPTAddr smtp.Address
1268 if domConf.TLSRPT != nil {
1269 domainTLSRPTAddr = smtp.NewAddress(domConf.TLSRPT.ParsedLocalpart, domain)
1271 go checkTLSRPT(&r.DomainTLSRPT, domain, domainTLSRPTAddr, false)
1279 record, txt, err := mtasts.LookupRecord(ctx, log.Logger, resolver, domain)
1281 addf(&r.MTASTS.Errors, "Looking up MTA-STS record: %s", err)
1285 r.MTASTS.Record = &MTASTSRecord{*record}
1288 policy, text, err := mtasts.FetchPolicy(ctx, log.Logger, domain)
1290 addf(&r.MTASTS.Errors, "Fetching MTA-STS policy: %s", err)
1291 } else if policy.Mode == mtasts.ModeNone {
1292 addf(&r.MTASTS.Warnings, "MTA-STS policy is present, but does not require TLS.")
1293 } else if policy.Mode == mtasts.ModeTesting {
1294 addf(&r.MTASTS.Warnings, "MTA-STS policy is in testing mode, do not forget to change to mode enforce after testing period.")
1296 r.MTASTS.PolicyText = text
1297 r.MTASTS.Policy = policy
1298 if policy != nil && policy.Mode != mtasts.ModeNone {
1299 if !policy.Matches(mox.Conf.Static.HostnameDomain) {
1300 addf(&r.MTASTS.Warnings, "Configured hostname is missing from policy MX list.")
1302 if policy.MaxAgeSeconds <= 24*3600 {
1303 addf(&r.MTASTS.Warnings, "Policy has a MaxAge of less than 1 day. For stable configurations, the recommended period is in weeks.")
1306 mxl, _, _ := resolver.LookupMX(ctx, domain.ASCII+".")
1307 // We do not check for errors, the MX check will complain about mx errors, we assume we will get the same error here.
1308 mxs := map[dns.Domain]struct{}{}
1309 for _, mx := range mxl {
1310 d, err := dns.ParseDomain(strings.TrimSuffix(mx.Host, "."))
1312 addf(&r.MTASTS.Warnings, "MX record %q is invalid: %s", mx.Host, err)
1317 for mx := range mxs {
1318 if !policy.Matches(mx) {
1319 addf(&r.MTASTS.Warnings, "MX record %q does not match MTA-STS policy MX list.", mx)
1322 for _, mx := range policy.MX {
1326 if _, ok := mxs[mx.Domain]; !ok {
1327 addf(&r.MTASTS.Warnings, "MX %q in MTA-STS policy is not in MX record.", mx)
1332 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.
1334After 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.
1336You 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.
1338You 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.
1340The _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.
1342When enabling MTA-STS, or updating a policy, always update the policy first (through a configuration change and reload/restart), and the DNS record second.
1344 addf(&r.MTASTS.Instructions, intro)
1346 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.`)
1348 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+".")
1349 addf(&r.MTASTS.Instructions, host)
1351 mtastsr := mtasts.Record{
1353 ID: time.Now().Format("20060102T150405"),
1355 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())
1356 addf(&r.MTASTS.Instructions, dns)
1365 type srvReq struct {
1373 // We'll assume if any submissions is configured, it is public. Same for imap. And
1374 // if not, that there is a plain option.
1375 var submissions, imaps bool
1376 for _, l := range mox.Conf.Static.Listeners {
1377 if l.TLS != nil && l.Submissions.Enabled {
1380 if l.TLS != nil && l.IMAPS.Enabled {
1384 srvhost := func(ok bool) string {
1386 return mox.Conf.Static.HostnameDomain.ASCII + "."
1390 var reqs = []srvReq{
1391 {name: "_submissions", port: 465, host: srvhost(submissions)},
1392 {name: "_submission", port: 587, host: srvhost(!submissions)},
1393 {name: "_imaps", port: 993, host: srvhost(imaps)},
1394 {name: "_imap", port: 143, host: srvhost(!imaps)},
1395 {name: "_pop3", port: 110, host: "."},
1396 {name: "_pop3s", port: 995, host: "."},
1398 var srvwg sync.WaitGroup
1399 srvwg.Add(len(reqs))
1400 for i := range reqs {
1403 _, reqs[i].srvs, _, reqs[i].err = resolver.LookupSRV(ctx, reqs[i].name[1:], "tcp", domain.ASCII+".")
1408 instr := "Ensure DNS records like the following exist:\n\n"
1409 r.SRVConf.SRVs = map[string][]net.SRV{}
1410 for _, req := range reqs {
1411 name := req.name + "._tcp." + domain.ASCII
1412 instr += fmt.Sprintf("\t%s._tcp.%-*s SRV 0 1 %d %s\n", req.name, len("_submissions")-len(req.name)+len(domain.ASCII+"."), domain.ASCII+".", req.port, req.host)
1413 r.SRVConf.SRVs[req.name] = unptr(req.srvs)
1415 addf(&r.SRVConf.Errors, "Looking up SRV record %q: %s", name, err)
1416 } else if len(req.srvs) == 0 {
1417 if req.host == "." {
1418 addf(&r.SRVConf.Warnings, "Missing optional SRV record %q", name)
1420 addf(&r.SRVConf.Errors, "Missing SRV record %q", name)
1422 } else if len(req.srvs) != 1 || req.srvs[0].Target != req.host || req.srvs[0].Port != req.port {
1423 addf(&r.SRVConf.Errors, "Unexpected SRV record(s) for %q", name)
1426 addf(&r.SRVConf.Instructions, instr)
1435 if domConf.ClientSettingsDomain != "" {
1436 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+".")
1438 ips, ourIPs, notOurIPs, err := lookupIPs(&r.Autoconf.Errors, domConf.ClientSettingsDNSDomain.ASCII+".")
1440 addf(&r.Autoconf.Errors, "Looking up client settings DNS CNAME: %s", err)
1442 r.Autoconf.ClientSettingsDomainIPs = ips
1443 if !isUnspecifiedNAT {
1444 if len(ourIPs) == 0 {
1445 addf(&r.Autoconf.Errors, "Client settings domain does not point to one of our IPs.")
1446 } else if len(notOurIPs) > 0 {
1447 addf(&r.Autoconf.Errors, "Client settings domain points to some IPs that are not ours: %v", notOurIPs)
1452 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+".")
1454 host := "autoconfig." + domain.ASCII + "."
1455 ips, ourIPs, notOurIPs, err := lookupIPs(&r.Autoconf.Errors, host)
1457 addf(&r.Autoconf.Errors, "Looking up autoconfig host: %s", err)
1461 r.Autoconf.IPs = ips
1462 if !isUnspecifiedNAT {
1463 if len(ourIPs) == 0 {
1464 addf(&r.Autoconf.Errors, "Autoconfig does not point to one of our IPs.")
1465 } else if len(notOurIPs) > 0 {
1466 addf(&r.Autoconf.Errors, "Autoconfig points to some IPs that are not ours: %v", notOurIPs)
1470 checkTLS(&r.Autoconf.Errors, "autoconfig."+domain.ASCII, ips, "443")
1479 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+".")
1481 _, srvs, _, err := resolver.LookupSRV(ctx, "autodiscover", "tcp", domain.ASCII+".")
1483 addf(&r.Autodiscover.Errors, "Looking up SRV record %q: %s", "autodiscover", err)
1487 for _, srv := range srvs {
1488 ips, ourIPs, notOurIPs, err := lookupIPs(&r.Autodiscover.Errors, srv.Target)
1490 addf(&r.Autodiscover.Errors, "Looking up target %q from SRV record: %s", srv.Target, err)
1493 if srv.Port != 443 {
1497 r.Autodiscover.Records = append(r.Autodiscover.Records, AutodiscoverSRV{*srv, ips})
1498 if !isUnspecifiedNAT {
1499 if len(ourIPs) == 0 {
1500 addf(&r.Autodiscover.Errors, "SRV target %q does not point to our IPs.", srv.Target)
1501 } else if len(notOurIPs) > 0 {
1502 addf(&r.Autodiscover.Errors, "SRV target %q points to some IPs that are not ours: %v", srv.Target, notOurIPs)
1506 checkTLS(&r.Autodiscover.Errors, strings.TrimSuffix(srv.Target, "."), ips, "443")
1509 addf(&r.Autodiscover.Errors, "No SRV record for port 443 for https.")
1517// Domains returns all configured domain names, in UTF-8 for IDNA domains.
1518func (Admin) Domains(ctx context.Context) []dns.Domain {
1520 for _, s := range mox.Conf.Domains() {
1521 d, _ := dns.ParseDomain(s)
1527// Domain returns the dns domain for a (potentially unicode as IDNA) domain name.
1528func (Admin) Domain(ctx context.Context, domain string) dns.Domain {
1529 d, err := dns.ParseDomain(domain)
1530 xcheckuserf(ctx, err, "parse domain")
1531 _, ok := mox.Conf.Domain(d)
1533 xcheckuserf(ctx, errors.New("no such domain"), "looking up domain")
1538// ParseDomain parses a domain, possibly an IDNA domain.
1539func (Admin) ParseDomain(ctx context.Context, domain string) dns.Domain {
1540 d, err := dns.ParseDomain(domain)
1541 xcheckuserf(ctx, err, "parse domain")
1545// DomainConfig returns the configuration for a domain.
1546func (Admin) DomainConfig(ctx context.Context, domain string) config.Domain {
1547 d, err := dns.ParseDomain(domain)
1548 xcheckuserf(ctx, err, "parse domain")
1549 conf, ok := mox.Conf.Domain(d)
1551 xcheckuserf(ctx, errors.New("no such domain"), "looking up domain")
1556// DomainLocalparts returns the encoded localparts and accounts configured in domain.
1557func (Admin) DomainLocalparts(ctx context.Context, domain string) (localpartAccounts map[string]string, localpartAliases map[string]config.Alias) {
1558 d, err := dns.ParseDomain(domain)
1559 xcheckuserf(ctx, err, "parsing domain")
1560 _, ok := mox.Conf.Domain(d)
1562 xcheckuserf(ctx, errors.New("no such domain"), "looking up domain")
1564 return mox.Conf.DomainLocalparts(d)
1567// Accounts returns the names of all configured accounts.
1568func (Admin) Accounts(ctx context.Context) []string {
1569 l := mox.Conf.Accounts()
1570 sort.Slice(l, func(i, j int) bool {
1576// Account returns the parsed configuration of an account.
1577func (Admin) Account(ctx context.Context, account string) (accountConfig config.Account, diskUsage int64) {
1578 log := pkglog.WithContext(ctx)
1580 acc, err := store.OpenAccount(log, account)
1581 if err != nil && errors.Is(err, store.ErrAccountUnknown) {
1582 xcheckuserf(ctx, err, "looking up account")
1584 xcheckf(ctx, err, "open account")
1587 log.Check(err, "closing account")
1590 var ac config.Account
1591 acc.WithRLock(func() {
1592 ac, _ = mox.Conf.Account(acc.Name)
1594 err := acc.DB.Read(ctx, func(tx *bstore.Tx) error {
1595 du := store.DiskUsage{ID: 1}
1597 diskUsage = du.MessageSize
1600 xcheckf(ctx, err, "get disk usage")
1603 return ac, diskUsage
1606// ConfigFiles returns the paths and contents of the static and dynamic configuration files.
1607func (Admin) ConfigFiles(ctx context.Context) (staticPath, dynamicPath, static, dynamic string) {
1608 buf0, err := os.ReadFile(mox.ConfigStaticPath)
1609 xcheckf(ctx, err, "read static config file")
1610 buf1, err := os.ReadFile(mox.ConfigDynamicPath)
1611 xcheckf(ctx, err, "read dynamic config file")
1612 return mox.ConfigStaticPath, mox.ConfigDynamicPath, string(buf0), string(buf1)
1615// MTASTSPolicies returns all mtasts policies from the cache.
1616func (Admin) MTASTSPolicies(ctx context.Context) (records []mtastsdb.PolicyRecord) {
1617 records, err := mtastsdb.PolicyRecords(ctx)
1618 xcheckf(ctx, err, "fetching mtasts policies from database")
1622// TLSReports returns TLS reports overlapping with period start/end, for the given
1623// policy domain (or all domains if empty). The reports are sorted first by period
1624// end (most recent first), then by policy domain.
1625func (Admin) TLSReports(ctx context.Context, start, end time.Time, policyDomain string) (reports []tlsrptdb.Record) {
1626 var polDom dns.Domain
1627 if policyDomain != "" {
1629 polDom, err = dns.ParseDomain(policyDomain)
1630 xcheckuserf(ctx, err, "parsing domain %q", policyDomain)
1633 records, err := tlsrptdb.RecordsPeriodDomain(ctx, start, end, polDom)
1634 xcheckf(ctx, err, "fetching tlsrpt report records from database")
1635 sort.Slice(records, func(i, j int) bool {
1636 iend := records[i].Report.DateRange.End
1637 jend := records[j].Report.DateRange.End
1639 return records[i].Domain < records[j].Domain
1641 return iend.After(jend)
1646// TLSReportID returns a single TLS report.
1647func (Admin) TLSReportID(ctx context.Context, domain string, reportID int64) tlsrptdb.Record {
1648 record, err := tlsrptdb.RecordID(ctx, reportID)
1649 if err == nil && record.Domain != domain {
1650 err = bstore.ErrAbsent
1652 if err == bstore.ErrAbsent {
1653 xcheckuserf(ctx, err, "fetching tls report from database")
1655 xcheckf(ctx, err, "fetching tls report from database")
1659// TLSRPTSummary presents TLS reporting statistics for a single domain
1661type TLSRPTSummary struct {
1662 PolicyDomain dns.Domain
1665 ResultTypeCounts map[tlsrpt.ResultType]int64
1668// TLSRPTSummaries returns a summary of received TLS reports overlapping with
1669// period start/end for one or all domains (when domain is empty).
1670// The returned summaries are ordered by domain name.
1671func (Admin) TLSRPTSummaries(ctx context.Context, start, end time.Time, policyDomain string) (domainSummaries []TLSRPTSummary) {
1672 var polDom dns.Domain
1673 if policyDomain != "" {
1675 polDom, err = dns.ParseDomain(policyDomain)
1676 xcheckuserf(ctx, err, "parsing policy domain")
1678 reports, err := tlsrptdb.RecordsPeriodDomain(ctx, start, end, polDom)
1679 xcheckf(ctx, err, "fetching tlsrpt reports from database")
1681 summaries := map[dns.Domain]TLSRPTSummary{}
1682 for _, r := range reports {
1683 dom, err := dns.ParseDomain(r.Domain)
1684 xcheckf(ctx, err, "parsing domain %q", r.Domain)
1686 sum := summaries[dom]
1687 sum.PolicyDomain = dom
1688 for _, result := range r.Report.Policies {
1689 sum.Success += result.Summary.TotalSuccessfulSessionCount
1690 sum.Failure += result.Summary.TotalFailureSessionCount
1691 for _, details := range result.FailureDetails {
1692 if sum.ResultTypeCounts == nil {
1693 sum.ResultTypeCounts = map[tlsrpt.ResultType]int64{}
1695 sum.ResultTypeCounts[details.ResultType] += details.FailedSessionCount
1698 summaries[dom] = sum
1700 sums := make([]TLSRPTSummary, 0, len(summaries))
1701 for _, sum := range summaries {
1702 sums = append(sums, sum)
1704 sort.Slice(sums, func(i, j int) bool {
1705 return sums[i].PolicyDomain.Name() < sums[j].PolicyDomain.Name()
1710// DMARCReports returns DMARC reports overlapping with period start/end, for the
1711// given domain (or all domains if empty). The reports are sorted first by period
1712// end (most recent first), then by domain.
1713func (Admin) DMARCReports(ctx context.Context, start, end time.Time, domain string) (reports []dmarcdb.DomainFeedback) {
1714 reports, err := dmarcdb.RecordsPeriodDomain(ctx, start, end, domain)
1715 xcheckf(ctx, err, "fetching dmarc aggregate reports from database")
1716 sort.Slice(reports, func(i, j int) bool {
1717 iend := reports[i].ReportMetadata.DateRange.End
1718 jend := reports[j].ReportMetadata.DateRange.End
1720 return reports[i].Domain < reports[j].Domain
1727// DMARCReportID returns a single DMARC report.
1728func (Admin) DMARCReportID(ctx context.Context, domain string, reportID int64) (report dmarcdb.DomainFeedback) {
1729 report, err := dmarcdb.RecordID(ctx, reportID)
1730 if err == nil && report.Domain != domain {
1731 err = bstore.ErrAbsent
1733 if err == bstore.ErrAbsent {
1734 xcheckuserf(ctx, err, "fetching dmarc aggregate report from database")
1736 xcheckf(ctx, err, "fetching dmarc aggregate report from database")
1740// DMARCSummary presents DMARC aggregate reporting statistics for a single domain
1742type DMARCSummary struct {
1746 DispositionQuarantine int
1747 DispositionReject int
1750 PolicyOverrides map[dmarcrpt.PolicyOverride]int
1753// DMARCSummaries returns a summary of received DMARC reports overlapping with
1754// period start/end for one or all domains (when domain is empty).
1755// The returned summaries are ordered by domain name.
1756func (Admin) DMARCSummaries(ctx context.Context, start, end time.Time, domain string) (domainSummaries []DMARCSummary) {
1757 reports, err := dmarcdb.RecordsPeriodDomain(ctx, start, end, domain)
1758 xcheckf(ctx, err, "fetching dmarc aggregate reports from database")
1759 summaries := map[string]DMARCSummary{}
1760 for _, r := range reports {
1761 sum := summaries[r.Domain]
1762 sum.Domain = r.Domain
1763 for _, record := range r.Records {
1764 n := record.Row.Count
1768 switch record.Row.PolicyEvaluated.Disposition {
1769 case dmarcrpt.DispositionNone:
1770 sum.DispositionNone += n
1771 case dmarcrpt.DispositionQuarantine:
1772 sum.DispositionQuarantine += n
1773 case dmarcrpt.DispositionReject:
1774 sum.DispositionReject += n
1777 if record.Row.PolicyEvaluated.DKIM == dmarcrpt.DMARCFail {
1780 if record.Row.PolicyEvaluated.SPF == dmarcrpt.DMARCFail {
1784 for _, reason := range record.Row.PolicyEvaluated.Reasons {
1785 if sum.PolicyOverrides == nil {
1786 sum.PolicyOverrides = map[dmarcrpt.PolicyOverride]int{}
1788 sum.PolicyOverrides[reason.Type] += n
1791 summaries[r.Domain] = sum
1793 sums := make([]DMARCSummary, 0, len(summaries))
1794 for _, sum := range summaries {
1795 sums = append(sums, sum)
1797 sort.Slice(sums, func(i, j int) bool {
1798 return sums[i].Domain < sums[j].Domain
1803// Reverse is the result of a reverse lookup.
1804type Reverse struct {
1807 // In the future, we can add a iprev-validated host name, and possibly the IPs of the host names.
1810// LookupIP does a reverse lookup of ip.
1811func (Admin) LookupIP(ctx context.Context, ip string) Reverse {
1812 resolver := dns.StrictResolver{Pkg: "webadmin", Log: pkglog.WithContext(ctx).Logger}
1813 names, _, err := resolver.LookupAddr(ctx, ip)
1814 xcheckuserf(ctx, err, "looking up ip")
1815 return Reverse{names}
1818// DNSBLStatus returns the IPs from which outgoing connections may be made and
1819// their current status in DNSBLs that are configured. The IPs are typically the
1820// configured listen IPs, or otherwise IPs on the machines network interfaces, with
1821// internal/private IPs removed.
1823// The returned value maps IPs to per DNSBL statuses, where "pass" means not listed and
1824// anything else is an error string, e.g. "fail: ..." or "temperror: ...".
1825func (Admin) DNSBLStatus(ctx context.Context) (results map[string]map[string]string, using, monitoring []dns.Domain) {
1826 log := mlog.New("webadmin", nil).WithContext(ctx)
1827 resolver := dns.StrictResolver{Pkg: "check", Log: log.Logger}
1828 return dnsblsStatus(ctx, log, resolver)
1831func dnsblsStatus(ctx context.Context, log mlog.Log, resolver dns.Resolver) (results map[string]map[string]string, using, monitoring []dns.Domain) {
1832 // todo: check health before using dnsbl?
1833 using = mox.Conf.Static.Listeners["public"].SMTP.DNSBLZones
1834 zones := append([]dns.Domain{}, using...)
1835 conf := mox.Conf.DynamicConfig()
1836 for _, zone := range conf.MonitorDNSBLZones {
1837 if !slices.Contains(zones, zone) {
1838 zones = append(zones, zone)
1839 monitoring = append(monitoring, zone)
1843 r := map[string]map[string]string{}
1844 for _, ip := range xsendingIPs(ctx) {
1845 if ip.IsLoopback() || ip.IsPrivate() {
1848 ipstr := ip.String()
1849 r[ipstr] = map[string]string{}
1850 for _, zone := range zones {
1851 status, expl, err := dnsbl.Lookup(ctx, log.Logger, resolver, zone, ip)
1852 result := string(status)
1854 result += ": " + err.Error()
1857 result += ": " + expl
1859 r[ipstr][zone.LogString()] = result
1862 return r, using, monitoring
1865func (Admin) MonitorDNSBLsSave(ctx context.Context, text string) {
1866 var zones []dns.Domain
1867 publicZones := mox.Conf.Static.Listeners["public"].SMTP.DNSBLZones
1868 for _, line := range strings.Split(text, "\n") {
1869 line = strings.TrimSpace(line)
1873 d, err := dns.ParseDomain(line)
1874 xcheckuserf(ctx, err, "parsing dnsbl zone %s", line)
1875 if slices.Contains(zones, d) {
1876 xusererrorf(ctx, "duplicate dnsbl zone %s", line)
1878 if slices.Contains(publicZones, d) {
1879 xusererrorf(ctx, "dnsbl zone %s already present in public listener", line)
1881 zones = append(zones, d)
1884 err := mox.ConfigSave(ctx, func(conf *config.Dynamic) {
1885 conf.MonitorDNSBLs = make([]string, len(zones))
1886 conf.MonitorDNSBLZones = nil
1887 for i, z := range zones {
1888 conf.MonitorDNSBLs[i] = z.Name()
1891 xcheckf(ctx, err, "saving monitoring dnsbl zones")
1894// DomainRecords returns lines describing DNS records that should exist for the
1895// configured domain.
1896func (Admin) DomainRecords(ctx context.Context, domain string) []string {
1897 log := pkglog.WithContext(ctx)
1898 return DomainRecords(ctx, log, domain)
1901// DomainRecords is the implementation of API function Admin.DomainRecords, taking
1903func DomainRecords(ctx context.Context, log mlog.Log, domain string) []string {
1904 d, err := dns.ParseDomain(domain)
1905 xcheckuserf(ctx, err, "parsing domain")
1906 dc, ok := mox.Conf.Domain(d)
1908 xcheckuserf(ctx, errors.New("unknown domain"), "lookup domain")
1910 resolver := dns.StrictResolver{Pkg: "webadmin", Log: pkglog.WithContext(ctx).Logger}
1911 _, result, err := resolver.LookupTXT(ctx, domain+".")
1912 if !dns.IsNotFound(err) {
1913 xcheckf(ctx, err, "looking up record to determine if dnssec is implemented")
1916 var certIssuerDomainName, acmeAccountURI string
1917 public := mox.Conf.Static.Listeners["public"]
1918 if public.TLS != nil && public.TLS.ACME != "" {
1919 acme, ok := mox.Conf.Static.ACME[public.TLS.ACME]
1920 if ok && acme.Manager.Manager.Client != nil {
1921 certIssuerDomainName = acme.IssuerDomainName
1922 acc, err := acme.Manager.Manager.Client.GetReg(ctx, "")
1923 log.Check(err, "get public acme account")
1925 acmeAccountURI = acc.URI
1930 records, err := mox.DomainRecords(dc, d, result.Authentic, certIssuerDomainName, acmeAccountURI)
1931 xcheckf(ctx, err, "dns records")
1935// DomainAdd adds a new domain and reloads the configuration.
1936func (Admin) DomainAdd(ctx context.Context, domain, accountName, localpart string) {
1937 d, err := dns.ParseDomain(domain)
1938 xcheckuserf(ctx, err, "parsing domain")
1940 err = mox.DomainAdd(ctx, d, accountName, smtp.Localpart(norm.NFC.String(localpart)))
1941 xcheckf(ctx, err, "adding domain")
1944// DomainRemove removes an existing domain and reloads the configuration.
1945func (Admin) DomainRemove(ctx context.Context, domain string) {
1946 d, err := dns.ParseDomain(domain)
1947 xcheckuserf(ctx, err, "parsing domain")
1949 err = mox.DomainRemove(ctx, d)
1950 xcheckf(ctx, err, "removing domain")
1953// AccountAdd adds existing a new account, with an initial email address, and
1954// reloads the configuration.
1955func (Admin) AccountAdd(ctx context.Context, accountName, address string) {
1956 err := mox.AccountAdd(ctx, accountName, address)
1957 xcheckf(ctx, err, "adding account")
1960// AccountRemove removes an existing account and reloads the configuration.
1961func (Admin) AccountRemove(ctx context.Context, accountName string) {
1962 err := mox.AccountRemove(ctx, accountName)
1963 xcheckf(ctx, err, "removing account")
1966// AddressAdd adds a new address to the account, which must already exist.
1967func (Admin) AddressAdd(ctx context.Context, address, accountName string) {
1968 err := mox.AddressAdd(ctx, address, accountName)
1969 xcheckf(ctx, err, "adding address")
1972// AddressRemove removes an existing address.
1973func (Admin) AddressRemove(ctx context.Context, address string) {
1974 err := mox.AddressRemove(ctx, address)
1975 xcheckf(ctx, err, "removing address")
1978// SetPassword saves a new password for an account, invalidating the previous password.
1979// Sessions are not interrupted, and will keep working. New login attempts must use the new password.
1980// Password must be at least 8 characters.
1981func (Admin) SetPassword(ctx context.Context, accountName, password string) {
1982 log := pkglog.WithContext(ctx)
1983 if len(password) < 8 {
1984 xusererrorf(ctx, "message must be at least 8 characters")
1986 acc, err := store.OpenAccount(log, accountName)
1987 xcheckf(ctx, err, "open account")
1990 log.WithContext(ctx).Check(err, "closing account")
1992 err = acc.SetPassword(log, password)
1993 xcheckf(ctx, err, "setting password")
1996// AccountSettingsSave set new settings for an account that only an admin can set.
1997func (Admin) AccountSettingsSave(ctx context.Context, accountName string, maxOutgoingMessagesPerDay, maxFirstTimeRecipientsPerDay int, maxMsgSize int64, firstTimeSenderDelay bool) {
1998 err := mox.AccountSave(ctx, accountName, func(acc *config.Account) {
1999 acc.MaxOutgoingMessagesPerDay = maxOutgoingMessagesPerDay
2000 acc.MaxFirstTimeRecipientsPerDay = maxFirstTimeRecipientsPerDay
2001 acc.QuotaMessageSize = maxMsgSize
2002 acc.NoFirstTimeSenderDelay = !firstTimeSenderDelay
2004 xcheckf(ctx, err, "saving account settings")
2007// ClientConfigsDomain returns configurations for email clients, IMAP and
2008// Submission (SMTP) for the domain.
2009func (Admin) ClientConfigsDomain(ctx context.Context, domain string) mox.ClientConfigs {
2010 d, err := dns.ParseDomain(domain)
2011 xcheckuserf(ctx, err, "parsing domain")
2013 cc, err := mox.ClientConfigsDomain(d)
2014 xcheckf(ctx, err, "client config for domain")
2018// QueueSize returns the number of messages currently in the outgoing queue.
2019func (Admin) QueueSize(ctx context.Context) int {
2020 n, err := queue.Count(ctx)
2021 xcheckf(ctx, err, "listing messages in queue")
2025// QueueHoldRuleList lists the hold rules.
2026func (Admin) QueueHoldRuleList(ctx context.Context) []queue.HoldRule {
2027 l, err := queue.HoldRuleList(ctx)
2028 xcheckf(ctx, err, "listing queue hold rules")
2032// QueueHoldRuleAdd adds a hold rule. Newly submitted and existing messages
2033// matching the hold rule will be marked "on hold".
2034func (Admin) QueueHoldRuleAdd(ctx context.Context, hr queue.HoldRule) queue.HoldRule {
2036 hr.SenderDomain, err = dns.ParseDomain(hr.SenderDomainStr)
2037 xcheckuserf(ctx, err, "parsing sender domain %q", hr.SenderDomainStr)
2038 hr.RecipientDomain, err = dns.ParseDomain(hr.RecipientDomainStr)
2039 xcheckuserf(ctx, err, "parsing recipient domain %q", hr.RecipientDomainStr)
2041 log := pkglog.WithContext(ctx)
2042 hr, err = queue.HoldRuleAdd(ctx, log, hr)
2043 xcheckf(ctx, err, "adding queue hold rule")
2047// QueueHoldRuleRemove removes a hold rule. The Hold field of messages in
2048// the queue are not changed.
2049func (Admin) QueueHoldRuleRemove(ctx context.Context, holdRuleID int64) {
2050 log := pkglog.WithContext(ctx)
2051 err := queue.HoldRuleRemove(ctx, log, holdRuleID)
2052 xcheckf(ctx, err, "removing queue hold rule")
2055// QueueList returns the messages currently in the outgoing queue.
2056func (Admin) QueueList(ctx context.Context, filter queue.Filter, sort queue.Sort) []queue.Msg {
2057 l, err := queue.List(ctx, filter, sort)
2058 xcheckf(ctx, err, "listing messages in queue")
2062// QueueNextAttemptSet sets a new time for next delivery attempt of matching
2063// messages from the queue.
2064func (Admin) QueueNextAttemptSet(ctx context.Context, filter queue.Filter, minutes int) (affected int) {
2065 n, err := queue.NextAttemptSet(ctx, filter, time.Now().Add(time.Duration(minutes)*time.Minute))
2066 xcheckf(ctx, err, "setting new next delivery attempt time for matching messages in queue")
2070// QueueNextAttemptAdd adds a duration to the time of next delivery attempt of
2071// matching messages from the queue.
2072func (Admin) QueueNextAttemptAdd(ctx context.Context, filter queue.Filter, minutes int) (affected int) {
2073 n, err := queue.NextAttemptAdd(ctx, filter, time.Duration(minutes)*time.Minute)
2074 xcheckf(ctx, err, "adding duration to next delivery attempt for matching messages in queue")
2078// QueueHoldSet sets the Hold field of matching messages in the queue.
2079func (Admin) QueueHoldSet(ctx context.Context, filter queue.Filter, onHold bool) (affected int) {
2080 n, err := queue.HoldSet(ctx, filter, onHold)
2081 xcheckf(ctx, err, "changing onhold for matching messages in queue")
2085// QueueFail fails delivery for matching messages, causing DSNs to be sent.
2086func (Admin) QueueFail(ctx context.Context, filter queue.Filter) (affected int) {
2087 log := pkglog.WithContext(ctx)
2088 n, err := queue.Fail(ctx, log, filter)
2089 xcheckf(ctx, err, "drop messages from queue")
2093// QueueDrop removes matching messages from the queue.
2094func (Admin) QueueDrop(ctx context.Context, filter queue.Filter) (affected int) {
2095 log := pkglog.WithContext(ctx)
2096 n, err := queue.Drop(ctx, log, filter)
2097 xcheckf(ctx, err, "drop messages from queue")
2101// QueueRequireTLSSet updates the requiretls field for matching messages in the
2102// queue, to be used for the next delivery.
2103func (Admin) QueueRequireTLSSet(ctx context.Context, filter queue.Filter, requireTLS *bool) (affected int) {
2104 n, err := queue.RequireTLSSet(ctx, filter, requireTLS)
2105 xcheckf(ctx, err, "update requiretls for messages in queue")
2109// QueueTransportSet initiates delivery of a message from the queue and sets the transport
2110// to use for delivery.
2111func (Admin) QueueTransportSet(ctx context.Context, filter queue.Filter, transport string) (affected int) {
2112 n, err := queue.TransportSet(ctx, filter, transport)
2113 xcheckf(ctx, err, "changing transport for messages in queue")
2117// RetiredList returns messages retired from the queue (delivery could
2118// have succeeded or failed).
2119func (Admin) RetiredList(ctx context.Context, filter queue.RetiredFilter, sort queue.RetiredSort) []queue.MsgRetired {
2120 l, err := queue.RetiredList(ctx, filter, sort)
2121 xcheckf(ctx, err, "listing retired messages")
2125// HookQueueSize returns the number of webhooks still to be delivered.
2126func (Admin) HookQueueSize(ctx context.Context) int {
2127 n, err := queue.HookQueueSize(ctx)
2128 xcheckf(ctx, err, "get hook queue size")
2132// HookList lists webhooks still to be delivered.
2133func (Admin) HookList(ctx context.Context, filter queue.HookFilter, sort queue.HookSort) []queue.Hook {
2134 l, err := queue.HookList(ctx, filter, sort)
2135 xcheckf(ctx, err, "listing hook queue")
2139// HookNextAttemptSet sets a new time for next delivery attempt of matching
2140// hooks from the queue.
2141func (Admin) HookNextAttemptSet(ctx context.Context, filter queue.HookFilter, minutes int) (affected int) {
2142 n, err := queue.HookNextAttemptSet(ctx, filter, time.Now().Add(time.Duration(minutes)*time.Minute))
2143 xcheckf(ctx, err, "setting new next delivery attempt time for matching webhooks in queue")
2147// HookNextAttemptAdd adds a duration to the time of next delivery attempt of
2148// matching hooks from the queue.
2149func (Admin) HookNextAttemptAdd(ctx context.Context, filter queue.HookFilter, minutes int) (affected int) {
2150 n, err := queue.HookNextAttemptAdd(ctx, filter, time.Duration(minutes)*time.Minute)
2151 xcheckf(ctx, err, "adding duration to next delivery attempt for matching webhooks in queue")
2155// HookRetiredList lists retired webhooks.
2156func (Admin) HookRetiredList(ctx context.Context, filter queue.HookRetiredFilter, sort queue.HookRetiredSort) []queue.HookRetired {
2157 l, err := queue.HookRetiredList(ctx, filter, sort)
2158 xcheckf(ctx, err, "listing retired hooks")
2162// HookCancel prevents further delivery attempts of matching webhooks.
2163func (Admin) HookCancel(ctx context.Context, filter queue.HookFilter) (affected int) {
2164 log := pkglog.WithContext(ctx)
2165 n, err := queue.HookCancel(ctx, log, filter)
2166 xcheckf(ctx, err, "cancel hooks in queue")
2170// LogLevels returns the current log levels.
2171func (Admin) LogLevels(ctx context.Context) map[string]string {
2172 m := map[string]string{}
2173 for pkg, level := range mox.Conf.LogLevels() {
2174 s, ok := mlog.LevelStrings[level]
2183// LogLevelSet sets a log level for a package.
2184func (Admin) LogLevelSet(ctx context.Context, pkg string, levelStr string) {
2185 level, ok := mlog.Levels[levelStr]
2187 xcheckuserf(ctx, errors.New("unknown"), "lookup level")
2189 mox.Conf.LogLevelSet(pkglog.WithContext(ctx), pkg, level)
2192// LogLevelRemove removes a log level for a package, which cannot be the empty string.
2193func (Admin) LogLevelRemove(ctx context.Context, pkg string) {
2194 mox.Conf.LogLevelRemove(pkglog.WithContext(ctx), pkg)
2197// CheckUpdatesEnabled returns whether checking for updates is enabled.
2198func (Admin) CheckUpdatesEnabled(ctx context.Context) bool {
2199 return mox.Conf.Static.CheckUpdates
2202// WebserverConfig is the combination of WebDomainRedirects and WebHandlers
2203// from the domains.conf configuration file.
2204type WebserverConfig struct {
2205 WebDNSDomainRedirects [][2]dns.Domain // From server to frontend.
2206 WebDomainRedirects [][2]string // From frontend to server, it's not convenient to create dns.Domain in the frontend.
2207 WebHandlers []config.WebHandler
2210// WebserverConfig returns the current webserver config
2211func (Admin) WebserverConfig(ctx context.Context) (conf WebserverConfig) {
2212 conf = webserverConfig()
2213 conf.WebDomainRedirects = nil
2217func webserverConfig() WebserverConfig {
2218 conf := mox.Conf.DynamicConfig()
2219 r := conf.WebDNSDomainRedirects
2220 l := conf.WebHandlers
2222 x := make([][2]dns.Domain, 0, len(r))
2223 xs := make([][2]string, 0, len(r))
2224 for k, v := range r {
2225 x = append(x, [2]dns.Domain{k, v})
2226 xs = append(xs, [2]string{k.Name(), v.Name()})
2228 sort.Slice(x, func(i, j int) bool {
2229 return x[i][0].ASCII < x[j][0].ASCII
2231 sort.Slice(xs, func(i, j int) bool {
2232 return xs[i][0] < xs[j][0]
2234 return WebserverConfig{x, xs, l}
2237// WebserverConfigSave saves a new webserver config. If oldConf is not equal to
2238// the current config, an error is returned.
2239func (Admin) WebserverConfigSave(ctx context.Context, oldConf, newConf WebserverConfig) (savedConf WebserverConfig) {
2240 current := webserverConfig()
2241 webhandlersEqual := func() bool {
2242 if len(current.WebHandlers) != len(oldConf.WebHandlers) {
2245 for i, wh := range current.WebHandlers {
2246 if !wh.Equal(oldConf.WebHandlers[i]) {
2252 if !reflect.DeepEqual(oldConf.WebDNSDomainRedirects, current.WebDNSDomainRedirects) || !webhandlersEqual() {
2253 xcheckuserf(ctx, errors.New("config has changed"), "comparing old/current config")
2256 // Convert to map, check that there are no duplicates here. The canonicalized
2257 // dns.Domain are checked again for uniqueness when parsing the config before
2259 domainRedirects := map[string]string{}
2260 for _, x := range newConf.WebDomainRedirects {
2261 if _, ok := domainRedirects[x[0]]; ok {
2262 xcheckuserf(ctx, errors.New("already present"), "checking redirect %s", x[0])
2264 domainRedirects[x[0]] = x[1]
2267 err := mox.ConfigSave(ctx, func(conf *config.Dynamic) {
2268 conf.WebDomainRedirects = domainRedirects
2269 conf.WebHandlers = newConf.WebHandlers
2271 xcheckf(ctx, err, "saving webserver config")
2273 savedConf = webserverConfig()
2274 savedConf.WebDomainRedirects = nil
2278// Transports returns the configured transports, for sending email.
2279func (Admin) Transports(ctx context.Context) map[string]config.Transport {
2280 return mox.Conf.Static.Transports
2283// DMARCEvaluationStats returns a map of all domains with evaluations to a count of
2284// the evaluations and whether those evaluations will cause a report to be sent.
2285func (Admin) DMARCEvaluationStats(ctx context.Context) map[string]dmarcdb.EvaluationStat {
2286 stats, err := dmarcdb.EvaluationStats(ctx)
2287 xcheckf(ctx, err, "get evaluation stats")
2291// DMARCEvaluationsDomain returns all evaluations for aggregate reports for the
2292// domain, sorted from oldest to most recent.
2293func (Admin) DMARCEvaluationsDomain(ctx context.Context, domain string) (dns.Domain, []dmarcdb.Evaluation) {
2294 dom, err := dns.ParseDomain(domain)
2295 xcheckf(ctx, err, "parsing domain")
2297 evals, err := dmarcdb.EvaluationsDomain(ctx, dom)
2298 xcheckf(ctx, err, "get evaluations for domain")
2302// DMARCRemoveEvaluations removes evaluations for a domain.
2303func (Admin) DMARCRemoveEvaluations(ctx context.Context, domain string) {
2304 dom, err := dns.ParseDomain(domain)
2305 xcheckf(ctx, err, "parsing domain")
2307 err = dmarcdb.RemoveEvaluationsDomain(ctx, dom)
2308 xcheckf(ctx, err, "removing evaluations for domain")
2311// DMARCSuppressAdd adds a reporting address to the suppress list. Outgoing
2312// reports will be suppressed for a period.
2313func (Admin) DMARCSuppressAdd(ctx context.Context, reportingAddress string, until time.Time, comment string) {
2314 addr, err := smtp.ParseAddress(reportingAddress)
2315 xcheckuserf(ctx, err, "parsing reporting address")
2317 ba := dmarcdb.SuppressAddress{ReportingAddress: addr.String(), Until: until, Comment: comment}
2318 err = dmarcdb.SuppressAdd(ctx, &ba)
2319 xcheckf(ctx, err, "adding address to suppresslist")
2322// DMARCSuppressList returns all reporting addresses on the suppress list.
2323func (Admin) DMARCSuppressList(ctx context.Context) []dmarcdb.SuppressAddress {
2324 l, err := dmarcdb.SuppressList(ctx)
2325 xcheckf(ctx, err, "listing reporting addresses in suppresslist")
2329// DMARCSuppressRemove removes a reporting address record from the suppress list.
2330func (Admin) DMARCSuppressRemove(ctx context.Context, id int64) {
2331 err := dmarcdb.SuppressRemove(ctx, id)
2332 xcheckf(ctx, err, "removing reporting address from suppresslist")
2335// DMARCSuppressExtend updates the until field of a suppressed reporting address record.
2336func (Admin) DMARCSuppressExtend(ctx context.Context, id int64, until time.Time) {
2337 err := dmarcdb.SuppressUpdate(ctx, id, until)
2338 xcheckf(ctx, err, "updating reporting address in suppresslist")
2341// TLSRPTResults returns all TLSRPT results in the database.
2342func (Admin) TLSRPTResults(ctx context.Context) []tlsrptdb.TLSResult {
2343 results, err := tlsrptdb.Results(ctx)
2344 xcheckf(ctx, err, "get results")
2348// TLSRPTResultsPolicyDomain returns the TLS results for a domain.
2349func (Admin) TLSRPTResultsDomain(ctx context.Context, isRcptDom bool, policyDomain string) (dns.Domain, []tlsrptdb.TLSResult) {
2350 dom, err := dns.ParseDomain(policyDomain)
2351 xcheckf(ctx, err, "parsing domain")
2354 results, err := tlsrptdb.ResultsRecipientDomain(ctx, dom)
2355 xcheckf(ctx, err, "get result for recipient domain")
2358 results, err := tlsrptdb.ResultsPolicyDomain(ctx, dom)
2359 xcheckf(ctx, err, "get result for policy domain")
2363// LookupTLSRPTRecord looks up a TLSRPT record and returns the parsed form, original txt
2364// form from DNS, and error with the TLSRPT record as a string.
2365func (Admin) LookupTLSRPTRecord(ctx context.Context, domain string) (record *TLSRPTRecord, txt string, errstr string) {
2366 log := pkglog.WithContext(ctx)
2367 dom, err := dns.ParseDomain(domain)
2368 xcheckf(ctx, err, "parsing domain")
2370 resolver := dns.StrictResolver{Pkg: "webadmin", Log: log.Logger}
2371 r, txt, err := tlsrpt.Lookup(ctx, log.Logger, resolver, dom)
2372 if err != nil && (errors.Is(err, tlsrpt.ErrNoRecord) || errors.Is(err, tlsrpt.ErrMultipleRecords) || errors.Is(err, tlsrpt.ErrRecordSyntax) || errors.Is(err, tlsrpt.ErrDNS)) {
2373 errstr = err.Error()
2376 xcheckf(ctx, err, "fetching tlsrpt record")
2379 record = &TLSRPTRecord{Record: *r}
2382 return record, txt, errstr
2385// TLSRPTRemoveResults removes the TLS results for a domain for the given day. If
2386// day is empty, all results are removed.
2387func (Admin) TLSRPTRemoveResults(ctx context.Context, isRcptDom bool, domain string, day string) {
2388 dom, err := dns.ParseDomain(domain)
2389 xcheckf(ctx, err, "parsing domain")
2392 err = tlsrptdb.RemoveResultsRecipientDomain(ctx, dom, day)
2393 xcheckf(ctx, err, "removing tls results")
2395 err = tlsrptdb.RemoveResultsPolicyDomain(ctx, dom, day)
2396 xcheckf(ctx, err, "removing tls results")
2400// TLSRPTSuppressAdd adds a reporting address to the suppress list. Outgoing
2401// reports will be suppressed for a period.
2402func (Admin) TLSRPTSuppressAdd(ctx context.Context, reportingAddress string, until time.Time, comment string) {
2403 addr, err := smtp.ParseAddress(reportingAddress)
2404 xcheckuserf(ctx, err, "parsing reporting address")
2406 ba := tlsrptdb.SuppressAddress{ReportingAddress: addr.String(), Until: until, Comment: comment}
2407 err = tlsrptdb.SuppressAdd(ctx, &ba)
2408 xcheckf(ctx, err, "adding address to suppresslist")
2411// TLSRPTSuppressList returns all reporting addresses on the suppress list.
2412func (Admin) TLSRPTSuppressList(ctx context.Context) []tlsrptdb.SuppressAddress {
2413 l, err := tlsrptdb.SuppressList(ctx)
2414 xcheckf(ctx, err, "listing reporting addresses in suppresslist")
2418// TLSRPTSuppressRemove removes a reporting address record from the suppress list.
2419func (Admin) TLSRPTSuppressRemove(ctx context.Context, id int64) {
2420 err := tlsrptdb.SuppressRemove(ctx, id)
2421 xcheckf(ctx, err, "removing reporting address from suppresslist")
2424// TLSRPTSuppressExtend updates the until field of a suppressed reporting address record.
2425func (Admin) TLSRPTSuppressExtend(ctx context.Context, id int64, until time.Time) {
2426 err := tlsrptdb.SuppressUpdate(ctx, id, until)
2427 xcheckf(ctx, err, "updating reporting address in suppresslist")
2430// LookupCid turns an ID from a Received header into a cid as used in logging.
2431func (Admin) LookupCid(ctx context.Context, recvID string) (cid string) {
2432 v, err := mox.ReceivedToCid(recvID)
2433 xcheckf(ctx, err, "received id to cid")
2434 return fmt.Sprintf("%x", v)
2437// Config returns the dynamic config.
2438func (Admin) Config(ctx context.Context) config.Dynamic {
2439 return mox.Conf.DynamicConfig()
2442// AccountRoutesSave saves routes for an account.
2443func (Admin) AccountRoutesSave(ctx context.Context, accountName string, routes []config.Route) {
2444 err := mox.AccountSave(ctx, accountName, func(acc *config.Account) {
2447 xcheckf(ctx, err, "saving account routes")
2450// DomainRoutesSave saves routes for a domain.
2451func (Admin) DomainRoutesSave(ctx context.Context, domainName string, routes []config.Route) {
2452 err := mox.DomainSave(ctx, domainName, func(domain *config.Domain) error {
2453 domain.Routes = routes
2456 xcheckf(ctx, err, "saving domain routes")
2459// RoutesSave saves global routes.
2460func (Admin) RoutesSave(ctx context.Context, routes []config.Route) {
2461 err := mox.ConfigSave(ctx, func(config *config.Dynamic) {
2462 config.Routes = routes
2464 xcheckf(ctx, err, "saving global routes")
2467// DomainDescriptionSave saves the description for a domain.
2468func (Admin) DomainDescriptionSave(ctx context.Context, domainName, descr string) {
2469 err := mox.DomainSave(ctx, domainName, func(domain *config.Domain) error {
2470 domain.Description = descr
2473 xcheckf(ctx, err, "saving domain description")
2476// DomainClientSettingsDomainSave saves the client settings domain for a domain.
2477func (Admin) DomainClientSettingsDomainSave(ctx context.Context, domainName, clientSettingsDomain string) {
2478 err := mox.DomainSave(ctx, domainName, func(domain *config.Domain) error {
2479 domain.ClientSettingsDomain = clientSettingsDomain
2482 xcheckf(ctx, err, "saving client settings domain")
2485// DomainLocalpartConfigSave saves the localpart catchall and case-sensitive
2486// settings for a domain.
2487func (Admin) DomainLocalpartConfigSave(ctx context.Context, domainName, localpartCatchallSeparator string, localpartCaseSensitive bool) {
2488 err := mox.DomainSave(ctx, domainName, func(domain *config.Domain) error {
2489 domain.LocalpartCatchallSeparator = localpartCatchallSeparator
2490 domain.LocalpartCaseSensitive = localpartCaseSensitive
2493 xcheckf(ctx, err, "saving localpart settings for domain")
2496// DomainDMARCAddressSave saves the DMARC reporting address/processing
2497// configuration for a domain. If localpart is empty, processing reports is
2499func (Admin) DomainDMARCAddressSave(ctx context.Context, domainName, localpart, domain, account, mailbox string) {
2500 err := mox.DomainSave(ctx, domainName, func(d *config.Domain) error {
2501 if localpart == "" {
2504 d.DMARC = &config.DMARC{
2505 Localpart: localpart,
2513 xcheckf(ctx, err, "saving dmarc reporting address/settings for domain")
2516// DomainTLSRPTAddressSave saves the TLS reporting address/processing
2517// configuration for a domain. If localpart is empty, processing reports is
2519func (Admin) DomainTLSRPTAddressSave(ctx context.Context, domainName, localpart, domain, account, mailbox string) {
2520 err := mox.DomainSave(ctx, domainName, func(d *config.Domain) error {
2521 if localpart == "" {
2524 d.TLSRPT = &config.TLSRPT{
2525 Localpart: localpart,
2533 xcheckf(ctx, err, "saving tls reporting address/settings for domain")
2536// DomainMTASTSSave saves the MTASTS policy for a domain. If policyID is empty,
2537// no MTASTS policy is served.
2538func (Admin) DomainMTASTSSave(ctx context.Context, domainName, policyID string, mode mtasts.Mode, maxAge time.Duration, mx []string) {
2539 err := mox.DomainSave(ctx, domainName, func(d *config.Domain) error {
2543 d.MTASTS = &config.MTASTS{
2552 xcheckf(ctx, err, "saving mtasts policy for domain")
2555// DomainDKIMAdd adds a DKIM selector for a domain, generating a new private
2556// key. The selector is not enabled for signing.
2557func (Admin) DomainDKIMAdd(ctx context.Context, domainName, selector, algorithm, hash string, headerRelaxed, bodyRelaxed, seal bool, headers []string, lifetime time.Duration) {
2558 d, err := dns.ParseDomain(domainName)
2559 xcheckuserf(ctx, err, "parsing domain")
2560 s, err := dns.ParseDomain(selector)
2561 xcheckuserf(ctx, err, "parsing selector")
2562 err = mox.DKIMAdd(ctx, d, s, algorithm, hash, headerRelaxed, bodyRelaxed, seal, headers, lifetime)
2563 xcheckf(ctx, err, "adding dkim key")
2566// DomainDKIMRemove removes a DKIM selector for a domain.
2567func (Admin) DomainDKIMRemove(ctx context.Context, domainName, selector string) {
2568 d, err := dns.ParseDomain(domainName)
2569 xcheckuserf(ctx, err, "parsing domain")
2570 s, err := dns.ParseDomain(selector)
2571 xcheckuserf(ctx, err, "parsing selector")
2572 err = mox.DKIMRemove(ctx, d, s)
2573 xcheckf(ctx, err, "removing dkim key")
2576// DomainDKIMSave saves the settings of selectors, and which to enable for
2577// signing, for a domain. All currently configured selectors must be present,
2578// selectors cannot be added/removed with this function.
2579func (Admin) DomainDKIMSave(ctx context.Context, domainName string, selectors map[string]config.Selector, sign []string) {
2580 for _, s := range sign {
2581 if _, ok := selectors[s]; !ok {
2582 xcheckuserf(ctx, fmt.Errorf("cannot sign unknown selector %q", s), "checking selectors")
2586 err := mox.DomainSave(ctx, domainName, func(d *config.Domain) error {
2587 if len(selectors) != len(d.DKIM.Selectors) {
2588 xcheckuserf(ctx, fmt.Errorf("cannot add/remove dkim selectors with this function"), "checking selectors")
2590 for s := range selectors {
2591 if _, ok := d.DKIM.Selectors[s]; !ok {
2592 xcheckuserf(ctx, fmt.Errorf("unknown selector %q", s), "checking selectors")
2595 // At least the selectors are the same.
2597 // Build up new selectors.
2598 sels := map[string]config.Selector{}
2599 for name, nsel := range selectors {
2600 osel := d.DKIM.Selectors[name]
2601 xsel := config.Selector{
2603 Canonicalization: nsel.Canonicalization,
2604 DontSealHeaders: nsel.DontSealHeaders,
2605 Expiration: nsel.Expiration,
2607 PrivateKeyFile: osel.PrivateKeyFile,
2609 if !slices.Equal(osel.HeadersEffective, nsel.Headers) {
2610 xsel.Headers = nsel.Headers
2615 // Enable the new selector settings.
2616 d.DKIM = config.DKIM{
2622 xcheckf(ctx, err, "saving dkim selector for domain")
2625func xparseAddress(ctx context.Context, lp, domain string) smtp.Address {
2626 xlp, err := smtp.ParseLocalpart(lp)
2627 xcheckuserf(ctx, err, "parsing localpart")
2628 d, err := dns.ParseDomain(domain)
2629 xcheckuserf(ctx, err, "parsing domain")
2630 return smtp.NewAddress(xlp, d)
2633func (Admin) AliasAdd(ctx context.Context, aliaslp string, domainName string, alias config.Alias) {
2634 addr := xparseAddress(ctx, aliaslp, domainName)
2635 err := mox.AliasAdd(ctx, addr, alias)
2636 xcheckf(ctx, err, "adding alias")
2639func (Admin) AliasUpdate(ctx context.Context, aliaslp string, domainName string, postPublic, listMembers, allowMsgFrom bool) {
2640 addr := xparseAddress(ctx, aliaslp, domainName)
2641 alias := config.Alias{
2642 PostPublic: postPublic,
2643 ListMembers: listMembers,
2644 AllowMsgFrom: allowMsgFrom,
2646 err := mox.AliasUpdate(ctx, addr, alias)
2647 xcheckf(ctx, err, "saving alias")
2650func (Admin) AliasRemove(ctx context.Context, aliaslp string, domainName string) {
2651 addr := xparseAddress(ctx, aliaslp, domainName)
2652 err := mox.AliasRemove(ctx, addr)
2653 xcheckf(ctx, err, "removing alias")
2656func (Admin) AliasAddressesAdd(ctx context.Context, aliaslp string, domainName string, addresses []string) {
2657 addr := xparseAddress(ctx, aliaslp, domainName)
2658 err := mox.AliasAddressesAdd(ctx, addr, addresses)
2659 xcheckf(ctx, err, "adding address to alias")
2662func (Admin) AliasAddressesRemove(ctx context.Context, aliaslp string, domainName string, addresses []string) {
2663 addr := xparseAddress(ctx, aliaslp, domainName)
2664 err := mox.AliasAddressesRemove(ctx, addr, addresses)
2665 xcheckf(ctx, err, "removing address from alias")