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,
4// etc.
5package webadmin
6
7import (
8 "bufio"
9 "bytes"
10 "context"
11 "crypto"
12 "crypto/ed25519"
13 cryptorand "crypto/rand"
14 "crypto/rsa"
15 "crypto/sha256"
16 "crypto/tls"
17 "crypto/x509"
18 "encoding/base64"
19 "encoding/json"
20 "errors"
21 "fmt"
22 "log/slog"
23 "maps"
24 "net"
25 "net/http"
26 "net/url"
27 "os"
28 "path/filepath"
29 "reflect"
30 "runtime"
31 "runtime/debug"
32 "slices"
33 "sort"
34 "strings"
35 "sync"
36 "time"
37
38 _ "embed"
39
40 "golang.org/x/text/unicode/norm"
41
42 "github.com/mjl-/adns"
43
44 "github.com/mjl-/bstore"
45 "github.com/mjl-/sherpa"
46 "github.com/mjl-/sherpadoc"
47 "github.com/mjl-/sherpaprom"
48
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"
71)
72
73var pkglog = mlog.New("webadmin", nil)
74
75//go:embed api.json
76var adminapiJSON []byte
77
78//go:embed admin.html
79var adminHTML []byte
80
81//go:embed admin.js
82var adminJS []byte
83
84var webadminFile = &mox.WebappFile{
85 HTML: adminHTML,
86 JS: adminJS,
87 HTMLPath: filepath.FromSlash("webadmin/admin.html"),
88 JSPath: filepath.FromSlash("webadmin/admin.js"),
89 CustomStem: "webadmin",
90}
91
92var adminDoc = mustParseAPI("admin", adminapiJSON)
93
94func mustParseAPI(api string, buf []byte) (doc sherpadoc.Section) {
95 err := json.Unmarshal(buf, &doc)
96 if err != nil {
97 pkglog.Fatalx("parsing webadmin api docs", err, slog.String("api", api))
98 }
99 return doc
100}
101
102var sherpaHandlerOpts *sherpa.HandlerOpts
103
104func makeSherpaHandler(cookiePath string, isForwarded bool) (http.Handler, error) {
105 return sherpa.NewHandler("/api/", moxvar.Version, Admin{cookiePath, isForwarded}, &adminDoc, sherpaHandlerOpts)
106}
107
108func init() {
109 collector, err := sherpaprom.NewCollector("moxadmin", nil)
110 if err != nil {
111 pkglog.Fatalx("creating sherpa prometheus collector", err)
112 }
113
114 sherpaHandlerOpts = &sherpa.HandlerOpts{Collector: collector, AdjustFunctionNames: "none", NoCORS: true}
115 // Just to validate.
116 _, err = makeSherpaHandler("", false)
117 if err != nil {
118 pkglog.Fatalx("sherpa handler", err)
119 }
120
121 mox.NewWebadminHandler = func(basePath string, isForwarded bool) http.Handler {
122 return http.HandlerFunc(Handler(basePath, isForwarded))
123 }
124}
125
126// Handler returns a handler for the webadmin endpoints, customized for the
127// cookiePath.
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) {
131 if err != nil {
132 http.Error(w, "500 - internal server error - cannot handle requests", http.StatusInternalServerError)
133 return
134 }
135 handle(sh, isForwarded, w, r)
136 }
137}
138
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.
142type Admin struct {
143 cookiePath string // From listener, for setting authentication cookies.
144 isForwarded bool // From listener, whether we look at X-Forwarded-* headers.
145}
146
147type ctxKey string
148
149var requestInfoCtxKey ctxKey = "requestInfo"
150
151type requestInfo struct {
152 SessionToken store.SessionToken
153 Response http.ResponseWriter
154 Request *http.Request // For Proto and TLS connection state during message submit.
155}
156
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", ""))
160
161 // HTML/JS can be retrieved without authentication.
162 if r.URL.Path == "/" {
163 switch r.Method {
164 case "GET", "HEAD":
165 webadminFile.Serve(ctx, log, w, r)
166 default:
167 http.Error(w, "405 - method not allowed - use get", http.StatusMethodNotAllowed)
168 }
169 return
170 } else if r.URL.Path == "/licenses.txt" {
171 switch r.Method {
172 case "GET", "HEAD":
173 w.Header().Set("Content-Type", "text/plain; charset=utf-8")
174 mox.LicensesWrite(w)
175 default:
176 http.Error(w, "405 - method not allowed - use get", http.StatusMethodNotAllowed)
177 }
178 return
179 }
180
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)
185 return
186 }
187
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" {
191 var ok bool
192 _, sessionToken, _, ok = webauth.Check(ctx, log, webauth.Admin, "webadmin", isForwarded, w, r, isAPI, isAPI, false)
193 if !ok {
194 // Response has been written already.
195 return
196 }
197 }
198
199 if isAPI {
200 reqInfo := requestInfo{sessionToken, w, r}
201 ctx = context.WithValue(ctx, requestInfoCtxKey, reqInfo)
202 apiHandler.ServeHTTP(w, r.WithContext(ctx))
203 return
204 }
205
206 http.NotFound(w, r)
207}
208
209func xcheckf(ctx context.Context, err error, format string, args ...any) {
210 if err == nil {
211 return
212 }
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...)
216 }
217
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) {
223 code = "user:error"
224 }
225 panic(&sherpa.Error{Code: code, Message: errmsg})
226}
227
228func xcheckuserf(ctx context.Context, err error, format string, args ...any) {
229 if err == nil {
230 return
231 }
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})
236}
237
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})
242}
243
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)
249
250 var data [8]byte
251 cryptorand.Read(data[:])
252 loginToken := base64.RawURLEncoding.EncodeToString(data[:])
253
254 webauth.LoginPrep(ctx, log, "webadmin", w.cookiePath, w.isForwarded, reqInfo.Response, reqInfo.Request, loginToken)
255
256 return loginToken
257}
258
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)
264
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 {
267 panic(err)
268 }
269 xcheckf(ctx, err, "login")
270 return csrfToken
271}
272
273// Logout invalidates the session token.
274func (w Admin) Logout(ctx context.Context) {
275 log := pkglog.WithContext(ctx)
276 reqInfo := ctx.Value(requestInfoCtxKey).(requestInfo)
277
278 err := webauth.Logout(ctx, log, webauth.Admin, "webadmin", w.cookiePath, w.isForwarded, reqInfo.Response, reqInfo.Request, "", reqInfo.SessionToken)
279 xcheckf(ctx, err, "logout")
280}
281
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
285}
286
287type Result struct {
288 Errors []string
289 Warnings []string
290 Instructions []string
291}
292
293type DNSSECResult struct {
294 Result
295}
296
297type IPRevCheckResult struct {
298 Hostname dns.Domain // This hostname, IPs must resolve back to this.
299 IPNames map[string][]string // IP to names.
300 Result
301}
302
303type MX struct {
304 Host string
305 Pref int
306 IPs []string
307}
308
309type MXCheckResult struct {
310 Records []MX
311 Result
312}
313
314type TLSCheckResult struct {
315 Result
316}
317
318type DANECheckResult struct {
319 Result
320}
321
322type SPFRecord struct {
323 spf.Record
324}
325
326type SPFCheckResult struct {
327 DomainTXT string
328 DomainRecord *SPFRecord
329 HostTXT string
330 HostRecord *SPFRecord
331 Result
332}
333
334type DKIMCheckResult struct {
335 Records []DKIMRecord
336 Result
337}
338
339type DKIMRecord struct {
340 Selector string
341 TXT string
342 Record *dkim.Record
343}
344
345type DMARCRecord struct {
346 dmarc.Record
347}
348
349type DMARCCheckResult struct {
350 Domain string
351 TXT string
352 Record *DMARCRecord
353 Result
354}
355
356type TLSRPTRecord struct {
357 tlsrpt.Record
358}
359
360type TLSRPTCheckResult struct {
361 TXT string
362 Record *TLSRPTRecord
363 Result
364}
365
366type MTASTSRecord struct {
367 mtasts.Record
368}
369type MTASTSCheckResult struct {
370 TXT string
371 Record *MTASTSRecord
372 PolicyText string
373 Policy *mtasts.Policy
374 Result
375}
376
377type SRVConfCheckResult struct {
378 SRVs map[string][]net.SRV // Service (e.g. "_imaps") to records.
379 Result
380}
381
382type AutoconfCheckResult struct {
383 ClientSettingsDomainIPs []string
384 IPs []string
385 Result
386}
387
388type AutodiscoverSRV struct {
389 net.SRV
390 IPs []string
391}
392
393type AutodiscoverCheckResult struct {
394 Records []AutodiscoverSRV
395 Result
396}
397
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 {
402 Domain string
403 DNSSEC DNSSECResult
404 IPRev IPRevCheckResult
405 MX MXCheckResult
406 TLS TLSCheckResult
407 DANE DANECheckResult
408 SPF SPFCheckResult
409 DKIM DKIMCheckResult
410 DMARC DMARCCheckResult
411 HostTLSRPT TLSRPTCheckResult
412 DomainTLSRPT TLSRPTCheckResult
413 MTASTS MTASTSCheckResult
414 SRVConf SRVConfCheckResult
415 Autoconf AutoconfCheckResult
416 Autodiscover AutodiscoverCheckResult
417}
418
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) {
421 x := recover()
422 if x == nil {
423 return
424 }
425 pkglog.WithContext(ctx).Error("recover from panic", slog.Any("panic", x))
426 debug.PrintStack()
427 metrics.PanicInc(metrics.Webadmin)
428}
429
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")
434 return ips
435}
436
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")
441 return ips
442}
443
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.
448
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)
452 defer cancel()
453 return checkDomain(nctx, resolver, dialer, domainName)
454}
455
456func unptr[T any](l []*T) []T {
457 if l == nil {
458 return nil
459 }
460 r := make([]T, len(l))
461 for i, e := range l {
462 r[i] = *e
463 }
464 return r
465}
466
467func checkDomain(ctx context.Context, resolver dns.Resolver, dialer *net.Dialer, domainName string) (r CheckResult) {
468 log := pkglog.WithContext(ctx)
469
470 domain, xerr := dns.ParseDomain(domainName)
471 xcheckuserf(ctx, xerr, "parsing domain")
472
473 domConf, ok := mox.Conf.Domain(domain)
474 if !ok {
475 panic(&sherpa.Error{Code: "user:notFound", Message: "domain not found"})
476 }
477
478 listenIPs := xlistenIPs(ctx, true)
479 isListenIP := func(ip net.IP) bool {
480 return slices.ContainsFunc(listenIPs, ip.Equal)
481 }
482
483 addf := func(l *[]string, format string, args ...any) {
484 *l = append(*l, fmt.Sprintf(format, args...))
485 }
486
487 // Host must be an absolute dns name, ending with a dot.
488 lookupIPs := func(errors *[]string, host string) (ips []string, ourIPs, notOurIPs []net.IP, rerr error) {
489 addrs, _, err := resolver.LookupHost(ctx, host)
490 if err != nil {
491 addf(errors, "Looking up %q: %s", host, err)
492 return nil, nil, nil, err
493 }
494 for _, addr := range addrs {
495 ip := net.ParseIP(addr)
496 if ip == nil {
497 addf(errors, "Bad IP %q", addr)
498 continue
499 }
500 ips = append(ips, ip.String())
501 if isListenIP(ip) {
502 ourIPs = append(ourIPs, ip)
503 } else {
504 notOurIPs = append(notOurIPs, ip)
505 }
506 }
507 return ips, ourIPs, notOurIPs, nil
508 }
509
510 checkTLS := func(errors *[]string, host string, ips []string, port string) {
511 d := tls.Dialer{
512 NetDialer: dialer,
513 Config: &tls.Config{
514 ServerName: host,
515 MinVersion: tls.VersionTLS12, // ../rfc/8996:31 ../rfc/8997:66
516 RootCAs: mox.Conf.Static.TLS.CertPool,
517 },
518 }
519 for _, ip := range ips {
520 conn, err := d.DialContext(ctx, "tcp", net.JoinHostPort(ip, port))
521 if err != nil {
522 addf(errors, "TLS connection to hostname %q, IP %q: %s", host, ip, err)
523 } else {
524 err := conn.Close()
525 log.Check(err, "closing tcp connection")
526 }
527 }
528 }
529
530 // If at least one listener with SMTP enabled has unspecified NATed IPs, we'll skip
531 // some checks related to these IPs.
532 var isNAT, isUnspecifiedNAT bool
533 for _, l := range mox.Conf.Static.Listeners {
534 if !l.SMTP.Enabled {
535 continue
536 }
537 if l.IPsNATed {
538 isUnspecifiedNAT = true
539 isNAT = true
540 }
541 if len(l.NATIPs) > 0 {
542 isNAT = true
543 }
544 }
545
546 var wg sync.WaitGroup
547
548 // DNSSEC
549 wg.Add(1)
550 go func() {
551 defer logPanic(ctx)
552 defer wg.Done()
553
554 // Some DNSSEC-verifying resolvers return unauthentic data for ".", so we check "com".
555 _, result, err := resolver.LookupNS(ctx, "com.")
556 if err != nil {
557 addf(&r.DNSSEC.Errors, "Looking up NS for DNS root (.) to check support in resolver for DNSSEC-verification: %s", err)
558 } else if !result.Authentic {
559 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.`)
560 } else {
561 _, result, _ := resolver.LookupMX(ctx, domain.ASCII+".")
562 if !result.Authentic {
563 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 }
565 }
566
567 addf(&r.DNSSEC.Instructions, `Enable DNSSEC-signing of the DNS records of your domain (zone) at your DNS hosting provider.`)
568
569 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".
570
571cat <<EOF >/etc/unbound/unbound.conf.d/ede.conf
572server:
573 ede: yes
574 val-log-level: 2
575EOF
576`)
577 }()
578
579 // IPRev
580 wg.Add(1)
581 go func() {
582 defer logPanic(ctx)
583 defer wg.Done()
584
585 // For each mox.Conf.SpecifiedSMTPListenIPs and all NATIPs, and each IP for
586 // mox.Conf.HostnameDomain, check if they resolve back to the host name.
587 hostIPs := map[dns.Domain][]net.IP{}
588 ips, _, err := resolver.LookupIP(ctx, "ip", mox.Conf.Static.HostnameDomain.ASCII+".")
589 if err != nil {
590 addf(&r.IPRev.Errors, "Looking up IPs for hostname: %s", err)
591 }
592
593 gatherMoreIPs := func(publicIPs []net.IP) {
594 nextip:
595 for _, ip := range publicIPs {
596 for _, xip := range ips {
597 if ip.Equal(xip) {
598 continue nextip
599 }
600 }
601 ips = append(ips, ip)
602 }
603 }
604 if !isNAT {
605 gatherMoreIPs(mox.Conf.Static.SpecifiedSMTPListenIPs)
606 }
607 for _, l := range mox.Conf.Static.Listeners {
608 if !l.SMTP.Enabled {
609 continue
610 }
611 var natips []net.IP
612 for _, ip := range l.NATIPs {
613 natips = append(natips, net.ParseIP(ip))
614 }
615 gatherMoreIPs(natips)
616 }
617 hostIPs[mox.Conf.Static.HostnameDomain] = ips
618
619 iplist := func(ips []net.IP) string {
620 var ipstrs []string
621 for _, ip := range ips {
622 ipstrs = append(ipstrs, ip.String())
623 }
624 return strings.Join(ipstrs, ", ")
625 }
626
627 r.IPRev.Hostname = mox.Conf.Static.HostnameDomain
628 r.IPRev.Instructions = []string{
629 fmt.Sprintf("Ensure IPs %s have reverse address %s.", iplist(ips), mox.Conf.Static.HostnameDomain.ASCII),
630 }
631
632 // If we have a socks transport, also check its host and IP.
633 for tname, t := range mox.Conf.Static.Transports {
634 if t.Socks != nil {
635 hostIPs[t.Socks.Hostname] = append(hostIPs[t.Socks.Hostname], t.Socks.IPs...)
636 instr := fmt.Sprintf("For SOCKS transport %s, ensure IPs %s have reverse address %s.", tname, iplist(t.Socks.IPs), t.Socks.Hostname)
637 r.IPRev.Instructions = append(r.IPRev.Instructions, instr)
638 }
639 }
640
641 type result struct {
642 Host dns.Domain
643 IP string
644 Addrs []string
645 Err error
646 }
647 results := make(chan result)
648 n := 0
649 for host, ips := range hostIPs {
650 for _, ip := range ips {
651 n++
652 s := ip.String()
653 host := host
654 go func() {
655 addrs, _, err := resolver.LookupAddr(ctx, s)
656 results <- result{host, s, addrs, err}
657 }()
658 }
659 }
660 r.IPRev.IPNames = map[string][]string{}
661 for range n {
662 lr := <-results
663 host, addrs, ip, err := lr.Host, lr.Addrs, lr.IP, lr.Err
664 if err != nil {
665 addf(&r.IPRev.Errors, "Looking up reverse name for %s of %s: %v", ip, host, err)
666 continue
667 }
668 var match bool
669 for i, a := range addrs {
670 a = strings.TrimRight(a, ".")
671 addrs[i] = a
672 ad, err := dns.ParseDomain(a)
673 if err != nil {
674 addf(&r.IPRev.Errors, "Parsing reverse name %q for %s: %v", a, ip, err)
675 }
676 if ad == host {
677 match = true
678 }
679 }
680 if !match && !isNAT && host == mox.Conf.Static.HostnameDomain {
681 addf(&r.IPRev.Warnings, "IP %s with name(s) %s is forward confirmed, but does not match hostname %s.", ip, strings.Join(addrs, ","), host)
682 }
683 r.IPRev.IPNames[ip] = addrs
684 }
685
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 {
691 if ip.IsLoopback() {
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())
693 }
694 }
695 }()
696
697 // MX
698 wg.Add(1)
699 go func() {
700 defer logPanic(ctx)
701 defer wg.Done()
702
703 mxs, _, err := resolver.LookupMX(ctx, domain.ASCII+".")
704 if err != nil {
705 addf(&r.MX.Errors, "Looking up MX records for %s: %s", domain, err)
706 }
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}
710 }
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.`)
713 return
714 }
715 for i, mx := range mxs {
716 ips, ourIPs, notOurIPs, err := lookupIPs(&r.MX.Errors, mx.Host)
717 if err != nil {
718 addf(&r.MX.Errors, "Looking up IPs for mx host %q: %s", mx.Host, err)
719 }
720 r.MX.Records[i].IPs = ips
721 if isUnspecifiedNAT {
722 continue
723 }
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)
728 }
729
730 }
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+"."),
733 }
734 }()
735
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.
738 wg.Add(1)
739 go func() {
740 defer logPanic(ctx)
741 defer wg.Done()
742
743 // MTA-STS, autoconfig, autodiscover are checked in their sections.
744
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"))
748 if err != nil {
749 return err
750 }
751 defer func() {
752 if conn != nil {
753 err := conn.Close()
754 log.Check(err, "closing tcp connection")
755 }
756 }()
757
758 end := time.Now().Add(10 * time.Second)
759 cctx, cancel := context.WithTimeout(ctx, 10*time.Second)
760 defer cancel()
761 err = conn.SetDeadline(end)
762 log.WithContext(ctx).Check(err, "setting deadline")
763
764 br := bufio.NewReader(conn)
765 _, err = br.ReadString('\n')
766 if err != nil {
767 return fmt.Errorf("reading SMTP banner from remote: %s", err)
768 }
769 if _, err := fmt.Fprintf(conn, "EHLO moxtest\r\n"); err != nil {
770 return fmt.Errorf("writing SMTP EHLO to remote: %s", err)
771 }
772 for {
773 line, err := br.ReadString('\n')
774 if err != nil {
775 return fmt.Errorf("reading SMTP EHLO response from remote: %s", err)
776 }
777 if strings.HasPrefix(line, "250-") {
778 continue
779 }
780 if strings.HasPrefix(line, "250 ") {
781 break
782 }
783 return fmt.Errorf("unexpected response to SMTP EHLO from remote: %q", strings.TrimSuffix(line, "\r\n"))
784 }
785 if _, err := fmt.Fprintf(conn, "STARTTLS\r\n"); err != nil {
786 return fmt.Errorf("writing SMTP STARTTLS to remote: %s", err)
787 }
788 line, err := br.ReadString('\n')
789 if err != nil {
790 return fmt.Errorf("reading response to SMTP STARTTLS from remote: %s", err)
791 }
792 if !strings.HasPrefix(line, "220 ") {
793 return fmt.Errorf("SMTP STARTTLS response from remote not 220 OK: %q", strings.TrimSuffix(line, "\r\n"))
794 }
795 config := &tls.Config{
796 ServerName: host,
797 RootCAs: mox.Conf.Static.TLS.CertPool,
798 }
799 tlsconn := tls.Client(conn, config)
800 if err := tlsconn.HandshakeContext(cctx); err != nil {
801 return fmt.Errorf("TLS handshake after SMTP STARTTLS: %s", err)
802 }
803 cancel()
804 err = conn.Close()
805 log.Check(err, "closing smtp connection")
806 conn = nil
807 return nil
808 }
809
810 checkSMTPSTARTTLS := func() {
811 // Initial errors are ignored, will already have been warned about by MX checks.
812 mxs, _, err := resolver.LookupMX(ctx, domain.ASCII+".")
813 if err != nil {
814 return
815 }
816 if len(mxs) == 1 && mxs[0].Host == "." {
817 return
818 }
819 for _, mx := range mxs {
820 ips, _, _, err := lookupIPs(&r.MX.Errors, mx.Host)
821 if err != nil {
822 continue
823 }
824
825 for _, ip := range ips {
826 if err := dialSMTPSTARTTLS(mx.Host, ip); err != nil {
827 addf(&r.TLS.Errors, "SMTP connection with STARTTLS to MX hostname %q IP %s: %s", mx.Host, ip, err)
828 }
829 }
830 }
831 }
832
833 checkSMTPSTARTTLS()
834
835 }()
836
837 // DANE
838 wg.Add(1)
839 go func() {
840 defer logPanic(ctx)
841 defer wg.Done()
842
843 daneRecords := func(l config.Listener) map[string]struct{} {
844 if l.TLS == nil {
845 return nil
846 }
847 records := map[string]struct{}{}
848 addRecord := func(privKey crypto.Signer) {
849 spkiBuf, err := x509.MarshalPKIXPublicKey(privKey.Public())
850 if err != nil {
851 addf(&r.DANE.Errors, "marshal SubjectPublicKeyInfo for DANE record: %v", err)
852 return
853 }
854 sum := sha256.Sum256(spkiBuf)
855 r := adns.TLSA{
856 Usage: adns.TLSAUsageDANEEE,
857 Selector: adns.TLSASelectorSPKI,
858 MatchType: adns.TLSAMatchTypeSHA256,
859 CertAssoc: sum[:],
860 }
861 records[r.Record()] = struct{}{}
862 }
863 for _, privKey := range l.TLS.HostPrivateRSA2048Keys {
864 addRecord(privKey)
865 }
866 for _, privKey := range l.TLS.HostPrivateECDSAP256Keys {
867 addRecord(privKey)
868 }
869 return records
870 }
871
872 expectedDANERecords := func(host string) map[string]struct{} {
873 for _, l := range mox.Conf.Static.Listeners {
874 if l.HostnameDomain.ASCII == host {
875 return daneRecords(l)
876 }
877 }
878 public := mox.Conf.Static.Listeners["public"]
879 if mox.Conf.Static.HostnameDomain.ASCII == host && public.HostnameDomain.ASCII == "" {
880 return daneRecords(public)
881 }
882 return nil
883 }
884
885 mxl, result, err := resolver.LookupMX(ctx, domain.ASCII+".")
886 if err != nil {
887 addf(&r.DANE.Errors, "Looking up MX hosts to check for DANE records: %s", err)
888 } else {
889 if !result.Authentic {
890 addf(&r.DANE.Warnings, "DANE is inactive because MX records are not DNSSEC-signed.")
891 }
892 for _, mx := range mxl {
893 expect := expectedDANERecords(mx.Host)
894
895 tlsal, tlsaResult, err := resolver.LookupTLSA(ctx, 25, "tcp", mx.Host+".")
896 if dns.IsNotFound(err) {
897 if len(expect) > 0 {
898 addf(&r.DANE.Errors, "No DANE records for MX host %s, expected: %s.", mx.Host, strings.Join(slices.Collect(maps.Keys(expect)), "; "))
899 }
900 continue
901 } else if err != nil {
902 addf(&r.DANE.Errors, "Looking up DANE records for MX host %s: %v", mx.Host, err)
903 continue
904 } else if !tlsaResult.Authentic && len(tlsal) > 0 {
905 addf(&r.DANE.Errors, "DANE records exist for MX host %s, but are not DNSSEC-signed.", mx.Host)
906 }
907
908 extra := map[string]struct{}{}
909 for _, e := range tlsal {
910 s := e.Record()
911 if _, ok := expect[s]; ok {
912 delete(expect, s)
913 } else {
914 extra[s] = struct{}{}
915 }
916 }
917 if len(expect) > 0 {
918 l := slices.Sorted(maps.Keys(expect))
919 addf(&r.DANE.Errors, "Missing DANE records of type TLSA for MX host _25._tcp.%s: %s", mx.Host, strings.Join(l, "; "))
920 }
921 if len(extra) > 0 {
922 l := slices.Sorted(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, "; "))
924 }
925 }
926 }
927
928 public := mox.Conf.Static.Listeners["public"]
929 pubDom := public.HostnameDomain
930 if pubDom.ASCII == "" {
931 pubDom = mox.Conf.Static.HostnameDomain
932 }
933 records := slices.Sorted(maps.Keys(daneRecords(public)))
934 if len(records) > 0 {
935 var instr strings.Builder
936 instr.WriteString("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.WriteString(fmt.Sprintf("\t_25._tcp.%s. TLSA %s\n", pubDom.ASCII, r))
939 }
940 addf(&r.DANE.Instructions, "%s", instr.String())
941 } else {
942 addf(&r.DANE.Warnings, "DANE not configured: no static TLS host keys.")
943
944 const 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)
946 }
947 }()
948
949 // SPF
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.
951 wg.Add(1)
952 go func() {
953 defer logPanic(ctx)
954 defer wg.Done()
955
956 ips := mox.DomainSPFIPs()
957
958 // Verify a domain with the configured IPs that do SMTP.
959 verifySPF := func(isHost bool, domain dns.Domain) (string, *SPFRecord, spf.Record) {
960 kind := "domain"
961 if isHost {
962 kind = "host"
963 }
964
965 _, txt, record, _, err := spf.Lookup(ctx, log.Logger, resolver, domain)
966 if err != nil {
967 addf(&r.SPF.Errors, "Looking up %s SPF record: %s", kind, err)
968 }
969 var xrecord *SPFRecord
970 if record != nil {
971 xrecord = &SPFRecord{*record}
972 }
973
974 spfr := spf.Record{
975 Version: "spf1",
976 }
977
978 checkSPFIP := func(ip net.IP) {
979 mechanism := "ip4"
980 if ip.To4() == nil {
981 mechanism = "ip6"
982 }
983 spfr.Directives = append(spfr.Directives, spf.Directive{Mechanism: mechanism, IP: ip})
984
985 if record == nil {
986 return
987 }
988
989 args := spf.Args{
990 RemoteIP: ip,
991 MailFromLocalpart: "postmaster",
992 MailFromDomain: domain,
993 HelloDomain: dns.IPDomain{Domain: domain},
994 LocalIP: net.ParseIP("127.0.0.1"),
995 LocalHostname: dns.Domain{ASCII: "localhost"},
996 }
997 status, mechanism, expl, _, err := spf.Evaluate(ctx, log.Logger, record, resolver, args)
998 if err != nil {
999 addf(&r.SPF.Errors, "Evaluating IP %q against %s SPF record: %s", ip, kind, err)
1000 } else if status != spf.StatusPass {
1001 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)
1002 }
1003 }
1004
1005 for _, ip := range ips {
1006 checkSPFIP(ip)
1007 }
1008 if !isHost {
1009 spfr.Directives = append(spfr.Directives, spf.Directive{Mechanism: "mx"})
1010 }
1011
1012 qual := "~"
1013 if isHost {
1014 qual = "-"
1015 }
1016 spfr.Directives = append(spfr.Directives, spf.Directive{Qualifier: qual, Mechanism: "all"})
1017 return txt, xrecord, spfr
1018 }
1019
1020 // Check SPF record for domain.
1021 var dspfr spf.Record
1022 r.SPF.DomainTXT, r.SPF.DomainRecord, dspfr = verifySPF(false, domain)
1023 // todo: possibly check all hosts for MX records? assuming they are also sending mail servers.
1024 r.SPF.HostTXT, r.SPF.HostRecord, _ = verifySPF(true, mox.Conf.Static.HostnameDomain)
1025
1026 if len(ips) == 0 {
1027 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.`)
1028 }
1029
1030 dtxt, err := dspfr.Record()
1031 if err != nil {
1032 addf(&r.SPF.Errors, "Making SPF record for instructions: %s", err)
1033 }
1034 domainspf := fmt.Sprintf("%s TXT %s", domain.ASCII+".", mox.TXTStrings(dtxt))
1035
1036 // Check SPF record for sending host. ../rfc/7208:2263 ../rfc/7208:2287
1037 hostspf := fmt.Sprintf(`%s TXT "v=spf1 a -all"`, mox.Conf.Static.HostnameDomain.ASCII+".")
1038
1039 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)
1040 }()
1041
1042 // DKIM
1043 // 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 wg.Add(1)
1045 go func() {
1046 defer logPanic(ctx)
1047 defer wg.Done()
1048
1049 var missing []string
1050 for sel, selc := range domConf.DKIM.Selectors {
1051 _, record, txt, _, err := dkim.Lookup(ctx, log.Logger, resolver, selc.Domain, domain)
1052 if err != nil {
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)
1058 } else {
1059 addf(&r.DKIM.Errors, "Fetching DKIM record for selector %q: %s", sel, err)
1060 }
1061 }
1062 if txt != "" {
1063 r.DKIM.Records = append(r.DKIM.Records, DKIMRecord{sel, txt, record})
1064 pubKey := selc.Key.Public()
1065 var pk []byte
1066 switch k := pubKey.(type) {
1067 case *rsa.PublicKey:
1068 var err error
1069 pk, err = x509.MarshalPKIXPublicKey(k)
1070 if err != nil {
1071 addf(&r.DKIM.Errors, "Marshal public key for %q to compare against DNS: %s", sel, err)
1072 continue
1073 }
1074 case ed25519.PublicKey:
1075 pk = []byte(k)
1076 default:
1077 addf(&r.DKIM.Errors, "Internal error: unknown public key type %T.", pubKey)
1078 continue
1079 }
1080
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)
1084 }
1085 }
1086 }
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 }
1090 instr := ""
1091 for _, sel := range missing {
1092 dkimr := dkim.Record{
1093 Version: "DKIM1",
1094 Hashes: []string{"sha256"},
1095 PublicKey: domConf.DKIM.Selectors[sel].Key.Public(),
1096 }
1097 switch dkimr.PublicKey.(type) {
1098 case *rsa.PublicKey:
1099 case ed25519.PublicKey:
1100 dkimr.Key = "ed25519"
1101 default:
1102 addf(&r.DKIM.Errors, "Internal error: unknown public key type %T.", dkimr.PublicKey)
1103 }
1104 txt, err := dkimr.Record()
1105 if err != nil {
1106 addf(&r.DKIM.Errors, "Making DKIM record for instructions: %s", err)
1107 continue
1108 }
1109 instr += fmt.Sprintf("\n\t%s._domainkey.%s TXT %s\n", sel, domain.ASCII+".", mox.TXTStrings(txt))
1110 }
1111 if instr != "" {
1112 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
1113 addf(&r.DKIM.Instructions, "%s", instr)
1114 }
1115 }()
1116
1117 // DMARC
1118 wg.Add(1)
1119 go func() {
1120 defer logPanic(ctx)
1121 defer wg.Done()
1122
1123 _, dmarcDomain, record, txt, _, err := dmarc.Lookup(ctx, log.Logger, resolver, domain)
1124 if err != nil {
1125 addf(&r.DMARC.Errors, "Looking up DMARC record: %s", err)
1126 } else if record == nil {
1127 addf(&r.DMARC.Errors, "No DMARC record")
1128 }
1129 r.DMARC.Domain = dmarcDomain.Name()
1130 r.DMARC.TXT = txt
1131 if record != nil {
1132 r.DMARC.Record = &DMARCRecord{*record}
1133 }
1134 if record != nil && record.Policy == "none" {
1135 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.")
1136 }
1137 if record != nil && record.SubdomainPolicy == "none" {
1138 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.")
1139 }
1140 if record != nil && len(record.AggregateReportAddresses) == 0 {
1141 addf(&r.DMARC.Warnings, "It is recommended you specify you would like aggregate reports about delivery success in the DMARC record, see instructions.")
1142 }
1143
1144 dmarcr := dmarc.DefaultRecord
1145 dmarcr.Policy = "reject"
1146
1147 var extInstr string
1148 if domConf.DMARC != nil {
1149 // If the domain is in a different Organizational Domain, the receiving domain
1150 // needs a special DNS record to opt-in to receiving reports. We check for that
1151 // record.
1152 // ../rfc/7489:1541
1153 orgDom := publicsuffix.Lookup(ctx, log.Logger, domain)
1154 destOrgDom := publicsuffix.Lookup(ctx, log.Logger, domConf.DMARC.DNSDomain)
1155 if orgDom != destOrgDom {
1156 accepts, status, _, _, _, err := dmarc.LookupExternalReportsAccepted(ctx, log.Logger, resolver, domain, domConf.DMARC.DNSDomain)
1157 if status != dmarc.StatusNone {
1158 addf(&r.DMARC.Errors, "Checking if external destination accepts reports: %s", err)
1159 } else if !accepts {
1160 addf(&r.DMARC.Errors, "External destination does not accept reports (%s)", err)
1161 }
1162 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)
1163 }
1164
1165 uri := url.URL{
1166 Scheme: "mailto",
1167 Opaque: smtp.NewAddress(domConf.DMARC.ParsedLocalpart, domConf.DMARC.DNSDomain).Pack(false),
1168 }
1169 uristr := uri.String()
1170 dmarcr.AggregateReportAddresses = []dmarc.URI{
1171 {Address: uristr, MaxSize: 10, Unit: "m"},
1172 }
1173
1174 if record != nil {
1175 found := false
1176 for _, addr := range record.AggregateReportAddresses {
1177 if addr.Address == uristr {
1178 found = true
1179 break
1180 }
1181 }
1182 if !found {
1183 addf(&r.DMARC.Errors, "Configured DMARC reporting address is not present in record.")
1184 }
1185 }
1186 } else {
1187 addf(&r.DMARC.Instructions, `Configure a DMARC destination in domain in config file.`)
1188 }
1189 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()))
1190 addf(&r.DMARC.Instructions, "%s", instr)
1191 if extInstr != "" {
1192 addf(&r.DMARC.Instructions, "%s", extInstr)
1193 }
1194 }()
1195
1196 checkTLSRPT := func(result *TLSRPTCheckResult, dom dns.Domain, address smtp.Address, isHost bool) {
1197 defer logPanic(ctx)
1198 defer wg.Done()
1199
1200 record, txt, err := tlsrpt.Lookup(ctx, log.Logger, resolver, dom)
1201 if err != nil {
1202 addf(&result.Errors, "Looking up TLSRPT record for domain %s: %s", dom, err)
1203 }
1204 result.TXT = txt
1205 if record != nil {
1206 result.Record = &TLSRPTRecord{*record}
1207 }
1208
1209 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.`
1210 var zeroaddr smtp.Address
1211 if address != zeroaddr {
1212 // TLSRPT does not require validation of reporting addresses outside the domain.
1213 // ../rfc/8460:1463
1214 uri := url.URL{
1215 Scheme: "mailto",
1216 Opaque: address.Pack(false),
1217 }
1218 rua := tlsrpt.RUA(uri.String())
1219 tlsrptr := &tlsrpt.Record{
1220 Version: "TLSRPTv1",
1221 RUAs: [][]tlsrpt.RUA{{rua}},
1222 }
1223 instr += fmt.Sprintf(`
1224
1225Ensure a DNS TXT record like the following exists:
1226
1227 _smtp._tls.%s TXT %s
1228
1229`, dom.ASCII+".", mox.TXTStrings(tlsrptr.String()))
1230
1231 if err == nil {
1232 found := false
1233 RUA:
1234 for _, l := range record.RUAs {
1235 for _, e := range l {
1236 if e == rua {
1237 found = true
1238 break RUA
1239 }
1240 }
1241 }
1242 if !found {
1243 addf(&result.Errors, `Configured reporting address is not present in TLSRPT record.`)
1244 }
1245 }
1246
1247 } else if isHost {
1248 instr += fmt.Sprintf(`
1249
1250Ensure the following snippet is present in mox.conf (ensure tabs are used for indenting, not spaces):
1251
1252HostTLSRPT:
1253 Account: %s
1254 Mailbox: TLSRPT
1255 Localpart: tlsrpt
1256
1257`, mox.Conf.Static.Postmaster.Account)
1258 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.`)
1259 } else {
1260 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.`)
1261 }
1262 addf(&result.Instructions, "%s", instr)
1263 }
1264
1265 // Host TLSRPT
1266 wg.Add(1)
1267 var hostTLSRPTAddr smtp.Address
1268 if mox.Conf.Static.HostTLSRPT.Localpart != "" {
1269 hostTLSRPTAddr = smtp.NewAddress(mox.Conf.Static.HostTLSRPT.ParsedLocalpart, mox.Conf.Static.HostnameDomain)
1270 }
1271 go checkTLSRPT(&r.HostTLSRPT, mox.Conf.Static.HostnameDomain, hostTLSRPTAddr, true)
1272
1273 // Domain TLSRPT
1274 wg.Add(1)
1275 var domainTLSRPTAddr smtp.Address
1276 if domConf.TLSRPT != nil {
1277 domainTLSRPTAddr = smtp.NewAddress(domConf.TLSRPT.ParsedLocalpart, domain)
1278 }
1279 go checkTLSRPT(&r.DomainTLSRPT, domain, domainTLSRPTAddr, false)
1280
1281 // MTA-STS
1282 wg.Add(1)
1283 go func() {
1284 defer logPanic(ctx)
1285 defer wg.Done()
1286
1287 // The admin has explicitly disabled mta-sts, keep warning about it.
1288 if domConf.MTASTS == nil {
1289 addf(&r.MTASTS.Warnings, "MTA-STS is not configured for this domain.")
1290 }
1291
1292 record, txt, err := mtasts.LookupRecord(ctx, log.Logger, resolver, domain)
1293 if err != nil && !(domConf.MTASTS == nil && errors.Is(err, mtasts.ErrNoRecord)) {
1294 addf(&r.MTASTS.Errors, "Looking up MTA-STS record: %s", err)
1295 }
1296 r.MTASTS.TXT = txt
1297 if record != nil {
1298 r.MTASTS.Record = &MTASTSRecord{*record}
1299 }
1300
1301 policy, text, err := mtasts.FetchPolicy(ctx, log.Logger, domain)
1302 if err != nil {
1303 if !(domConf.MTASTS == nil && errors.Is(err, mtasts.ErrNoPolicy)) {
1304 addf(&r.MTASTS.Errors, "Fetching MTA-STS policy: %s", err)
1305 }
1306 } else if policy.Mode == mtasts.ModeNone {
1307 addf(&r.MTASTS.Warnings, "MTA-STS policy is present, but does not require TLS.")
1308 } else if policy.Mode == mtasts.ModeTesting {
1309 addf(&r.MTASTS.Warnings, "MTA-STS policy is in testing mode, do not forget to change to mode enforce after testing period.")
1310 }
1311 r.MTASTS.PolicyText = text
1312 r.MTASTS.Policy = policy
1313 if policy != nil && policy.Mode != mtasts.ModeNone {
1314 if !policy.Matches(mox.Conf.Static.HostnameDomain) {
1315 addf(&r.MTASTS.Warnings, "Configured hostname is missing from policy MX list.")
1316 }
1317 if policy.MaxAgeSeconds <= 24*3600 {
1318 addf(&r.MTASTS.Warnings, "Policy has a MaxAge of less than 1 day. For stable configurations, the recommended period is in weeks.")
1319 }
1320
1321 mxl, _, _ := resolver.LookupMX(ctx, domain.ASCII+".")
1322 // We do not check for errors, the MX check will complain about mx errors, we assume we will get the same error here.
1323 mxs := map[dns.Domain]struct{}{}
1324 for _, mx := range mxl {
1325 d, err := dns.ParseDomain(strings.TrimSuffix(mx.Host, "."))
1326 if err != nil {
1327 addf(&r.MTASTS.Warnings, "MX record %q is invalid: %s", mx.Host, err)
1328 continue
1329 }
1330 mxs[d] = struct{}{}
1331 }
1332 for mx := range mxs {
1333 if !policy.Matches(mx) {
1334 addf(&r.MTASTS.Warnings, "MX record %q does not match MTA-STS policy MX list.", mx)
1335 }
1336 }
1337 for _, mx := range policy.MX {
1338 if mx.Wildcard {
1339 continue
1340 }
1341 if _, ok := mxs[mx.Domain]; !ok {
1342 addf(&r.MTASTS.Warnings, "MX %q in MTA-STS policy is not in MX record.", mx.LogString())
1343 }
1344 }
1345 }
1346
1347 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.
1348
1349After 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.
1350
1351You 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.
1352
1353You 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.
1354
1355The _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.
1356
1357When enabling MTA-STS, or updating a policy, always update the policy first (through a configuration change and reload/restart), and the DNS record second.
1358`
1359 addf(&r.MTASTS.Instructions, "%s", intro)
1360
1361 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.`)
1362
1363 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+".")
1364 addf(&r.MTASTS.Instructions, "%s", host)
1365
1366 mtastsr := mtasts.Record{
1367 Version: "STSv1",
1368 ID: time.Now().Format("20060102T150405"),
1369 }
1370 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())
1371 addf(&r.MTASTS.Instructions, "%s", dns)
1372 }()
1373
1374 // SRVConf
1375 wg.Add(1)
1376 go func() {
1377 defer logPanic(ctx)
1378 defer wg.Done()
1379
1380 type srvReq struct {
1381 name string
1382 port uint16
1383 // First entry is host we suggest and prefer, but we won't complain if the current
1384 // value is one of the later values, to account for historic values we suggested
1385 // that aren't wrong and we don't want to bother admins with.
1386 host []string
1387 srvs []*net.SRV
1388 err error
1389 }
1390
1391 // We'll assume if any submissions is configured, it is public. Same for imap. And
1392 // if not, that there is a plain option.
1393 var submissions, imaps bool
1394 for _, l := range mox.Conf.Static.Listeners {
1395 if l.TLS != nil && l.Submissions.Enabled {
1396 submissions = true
1397 }
1398 if l.TLS != nil && l.IMAPS.Enabled {
1399 imaps = true
1400 }
1401 }
1402 srvhost := func(ok bool) []string {
1403 if !ok {
1404 return []string{"."}
1405 }
1406 if domConf.ClientSettingsDomain != "" {
1407 return []string{
1408 domConf.ClientSettingsDNSDomain.ASCII + ".",
1409 mox.Conf.Static.HostnameDomain.ASCII + ".",
1410 }
1411 }
1412 return []string{mox.Conf.Static.HostnameDomain.ASCII + "."}
1413 }
1414 var reqs = []srvReq{
1415 {name: "_submissions", port: 465, host: srvhost(submissions)},
1416 {name: "_submission", port: 587, host: srvhost(!submissions)},
1417 {name: "_imaps", port: 993, host: srvhost(imaps)},
1418 {name: "_imap", port: 143, host: srvhost(!imaps)},
1419 {name: "_pop3", port: 110, host: []string{"."}},
1420 {name: "_pop3s", port: 995, host: []string{"."}},
1421 }
1422 // Host "." indicates the service is not available. We suggested in the DNS records
1423 // that the port be set to 0, so check for that. ../rfc/6186:242
1424 for i := range reqs {
1425 if reqs[i].host[0] == "." {
1426 reqs[i].port = 0
1427 }
1428 }
1429 var srvwg sync.WaitGroup
1430 srvwg.Add(len(reqs))
1431 for i := range reqs {
1432 go func(i int) {
1433 defer srvwg.Done()
1434 _, reqs[i].srvs, _, reqs[i].err = resolver.LookupSRV(ctx, reqs[i].name[1:], "tcp", domain.ASCII+".")
1435 }(i)
1436 }
1437 srvwg.Wait()
1438
1439 var instr strings.Builder
1440 instr.WriteString("Ensure DNS records like the following exist:\n\n")
1441 r.SRVConf.SRVs = map[string][]net.SRV{}
1442 for _, req := range reqs {
1443 name := req.name + "._tcp." + domain.ASCII
1444 weight := 1
1445 if req.host[0] == "." {
1446 weight = 0
1447 }
1448 instr.WriteString(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]))
1449 r.SRVConf.SRVs[req.name] = unptr(req.srvs)
1450 if req.err != nil {
1451 addf(&r.SRVConf.Errors, "Looking up SRV record %q: %s", name, req.err)
1452 } else if len(req.srvs) == 0 {
1453 if req.host[0] == "." {
1454 addf(&r.SRVConf.Warnings, "Missing optional SRV record %q", name)
1455 } else {
1456 addf(&r.SRVConf.Errors, "Missing SRV record %q", name)
1457 }
1458 } else if len(req.srvs) != 1 || !slices.Contains(req.host, req.srvs[0].Target) || req.srvs[0].Port != req.port {
1459 var srvs []string
1460 for _, srv := range req.srvs {
1461 srvs = append(srvs, fmt.Sprintf("%d %d %d %s", srv.Priority, srv.Weight, srv.Port, srv.Target))
1462 }
1463 msg := fmt.Sprintf("Unexpected SRV record(s) for %q: %s", name, strings.Join(srvs, ", "))
1464 // If only port is different (non-zero), don't raise an error, just a warning is
1465 // fine.
1466 if req.host[0] == "." && len(req.srvs) == 1 && req.srvs[0].Target == "." {
1467 addf(&r.SRVConf.Warnings, "%s", msg)
1468 } else {
1469 addf(&r.SRVConf.Errors, "%s", msg)
1470 }
1471 }
1472 }
1473 addf(&r.SRVConf.Instructions, "%s", instr.String())
1474 }()
1475
1476 // Autoconf
1477 wg.Add(1)
1478 go func() {
1479 defer logPanic(ctx)
1480 defer wg.Done()
1481
1482 if domConf.ClientSettingsDomain != "" {
1483 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+".")
1484
1485 ips, ourIPs, notOurIPs, err := lookupIPs(&r.Autoconf.Errors, domConf.ClientSettingsDNSDomain.ASCII+".")
1486 if err != nil {
1487 addf(&r.Autoconf.Errors, "Looking up client settings DNS CNAME: %s", err)
1488 }
1489 r.Autoconf.ClientSettingsDomainIPs = ips
1490 if !isUnspecifiedNAT {
1491 if len(ourIPs) == 0 {
1492 addf(&r.Autoconf.Errors, "Client settings domain does not point to one of our IPs.")
1493 } else if len(notOurIPs) > 0 {
1494 addf(&r.Autoconf.Errors, "Client settings domain points to some IPs that are not ours: %v", notOurIPs)
1495 }
1496 }
1497 }
1498
1499 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+".")
1500
1501 host := "autoconfig." + domain.ASCII + "."
1502 ips, ourIPs, notOurIPs, err := lookupIPs(&r.Autoconf.Errors, host)
1503 if err != nil {
1504 addf(&r.Autoconf.Errors, "Looking up autoconfig host: %s", err)
1505 return
1506 }
1507
1508 r.Autoconf.IPs = ips
1509 if !isUnspecifiedNAT {
1510 if len(ourIPs) == 0 {
1511 addf(&r.Autoconf.Errors, "Autoconfig does not point to one of our IPs.")
1512 } else if len(notOurIPs) > 0 {
1513 addf(&r.Autoconf.Errors, "Autoconfig points to some IPs that are not ours: %v", notOurIPs)
1514 }
1515 }
1516
1517 checkTLS(&r.Autoconf.Errors, "autoconfig."+domain.ASCII, ips, "443")
1518 }()
1519
1520 // Autodiscover
1521 wg.Add(1)
1522 go func() {
1523 defer logPanic(ctx)
1524 defer wg.Done()
1525
1526 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+".")
1527
1528 _, srvs, _, err := resolver.LookupSRV(ctx, "autodiscover", "tcp", domain.ASCII+".")
1529 if err != nil {
1530 addf(&r.Autodiscover.Errors, "Looking up SRV record %q: %s", "autodiscover", err)
1531 return
1532 }
1533 match := false
1534 for _, srv := range srvs {
1535 ips, ourIPs, notOurIPs, err := lookupIPs(&r.Autodiscover.Errors, srv.Target)
1536 if err != nil {
1537 addf(&r.Autodiscover.Errors, "Looking up target %q from SRV record: %s", srv.Target, err)
1538 continue
1539 }
1540 if srv.Port != 443 {
1541 continue
1542 }
1543 match = true
1544 r.Autodiscover.Records = append(r.Autodiscover.Records, AutodiscoverSRV{*srv, ips})
1545 if !isUnspecifiedNAT {
1546 if len(ourIPs) == 0 {
1547 addf(&r.Autodiscover.Errors, "SRV target %q does not point to our IPs.", srv.Target)
1548 } else if len(notOurIPs) > 0 {
1549 addf(&r.Autodiscover.Errors, "SRV target %q points to some IPs that are not ours: %v", srv.Target, notOurIPs)
1550 }
1551 }
1552
1553 checkTLS(&r.Autodiscover.Errors, strings.TrimSuffix(srv.Target, "."), ips, "443")
1554 }
1555 if !match {
1556 addf(&r.Autodiscover.Errors, "No SRV record for port 443 for https.")
1557 }
1558 }()
1559
1560 wg.Wait()
1561 return
1562}
1563
1564// Domains returns all configured domain names.
1565func (Admin) Domains(ctx context.Context) []config.Domain {
1566 return mox.Conf.DomainConfigs()
1567}
1568
1569// Domain returns the dns domain for a (potentially unicode as IDNA) domain name.
1570func (Admin) Domain(ctx context.Context, domain string) dns.Domain {
1571 d, err := dns.ParseDomain(domain)
1572 xcheckuserf(ctx, err, "parse domain")
1573 _, ok := mox.Conf.Domain(d)
1574 if !ok {
1575 xcheckuserf(ctx, errors.New("no such domain"), "looking up domain")
1576 }
1577 return d
1578}
1579
1580// ParseDomain parses a domain, possibly an IDNA domain.
1581func (Admin) ParseDomain(ctx context.Context, domain string) dns.Domain {
1582 d, err := dns.ParseDomain(domain)
1583 xcheckuserf(ctx, err, "parse domain")
1584 return d
1585}
1586
1587// DomainConfig returns the configuration for a domain.
1588func (Admin) DomainConfig(ctx context.Context, domain string) config.Domain {
1589 d, err := dns.ParseDomain(domain)
1590 xcheckuserf(ctx, err, "parse domain")
1591 conf, ok := mox.Conf.Domain(d)
1592 if !ok {
1593 xcheckuserf(ctx, errors.New("no such domain"), "looking up domain")
1594 }
1595 return conf
1596}
1597
1598// DomainLocalparts returns the encoded localparts and accounts configured in domain.
1599func (Admin) DomainLocalparts(ctx context.Context, domain string) (localpartAccounts map[string]string, localpartAliases map[string]config.Alias) {
1600 d, err := dns.ParseDomain(domain)
1601 xcheckuserf(ctx, err, "parsing domain")
1602 _, ok := mox.Conf.Domain(d)
1603 if !ok {
1604 xcheckuserf(ctx, errors.New("no such domain"), "looking up domain")
1605 }
1606 return mox.Conf.DomainLocalparts(d)
1607}
1608
1609// Accounts returns the names of all configured and all disabled accounts.
1610func (Admin) Accounts(ctx context.Context) (all, disabled []string) {
1611 all, disabled = mox.Conf.AccountsDisabled()
1612 slices.Sort(all)
1613 return
1614}
1615
1616// Account returns the parsed configuration of an account.
1617func (Admin) Account(ctx context.Context, account string) (accountConfig config.Account, diskUsage int64) {
1618 log := pkglog.WithContext(ctx)
1619
1620 acc, err := store.OpenAccount(log, account, false)
1621 if err != nil && errors.Is(err, store.ErrAccountUnknown) {
1622 xcheckuserf(ctx, err, "looking up account")
1623 }
1624 xcheckf(ctx, err, "open account")
1625 defer func() {
1626 err := acc.Close()
1627 log.Check(err, "closing account")
1628 }()
1629
1630 var ac config.Account
1631 acc.WithRLock(func() {
1632 ac, _ = mox.Conf.Account(acc.Name)
1633
1634 err := acc.DB.Read(ctx, func(tx *bstore.Tx) error {
1635 du := store.DiskUsage{ID: 1}
1636 err := tx.Get(&du)
1637 diskUsage = du.MessageSize
1638 return err
1639 })
1640 xcheckf(ctx, err, "get disk usage")
1641 })
1642
1643 return ac, diskUsage
1644}
1645
1646// ConfigFiles returns the paths and contents of the static and dynamic configuration files.
1647func (Admin) ConfigFiles(ctx context.Context) (staticPath, dynamicPath, static, dynamic string) {
1648 buf0, err := os.ReadFile(mox.ConfigStaticPath)
1649 xcheckf(ctx, err, "read static config file")
1650 buf1, err := os.ReadFile(mox.ConfigDynamicPath)
1651 xcheckf(ctx, err, "read dynamic config file")
1652 return mox.ConfigStaticPath, mox.ConfigDynamicPath, string(buf0), string(buf1)
1653}
1654
1655// MTASTSPolicies returns all mtasts policies from the cache.
1656func (Admin) MTASTSPolicies(ctx context.Context) (records []mtastsdb.PolicyRecord) {
1657 records, err := mtastsdb.PolicyRecords(ctx)
1658 xcheckf(ctx, err, "fetching mtasts policies from database")
1659 return records
1660}
1661
1662// TLSReports returns TLS reports overlapping with period start/end, for the given
1663// policy domain (or all domains if empty). The reports are sorted first by period
1664// end (most recent first), then by policy domain.
1665func (Admin) TLSReports(ctx context.Context, start, end time.Time, policyDomain string) (reports []tlsrptdb.Record) {
1666 var polDom dns.Domain
1667 if policyDomain != "" {
1668 var err error
1669 polDom, err = dns.ParseDomain(policyDomain)
1670 xcheckuserf(ctx, err, "parsing domain %q", policyDomain)
1671 }
1672
1673 records, err := tlsrptdb.RecordsPeriodDomain(ctx, start, end, polDom)
1674 xcheckf(ctx, err, "fetching tlsrpt report records from database")
1675 sort.Slice(records, func(i, j int) bool {
1676 iend := records[i].Report.DateRange.End
1677 jend := records[j].Report.DateRange.End
1678 if iend == jend {
1679 return records[i].Domain < records[j].Domain
1680 }
1681 return iend.After(jend)
1682 })
1683 return records
1684}
1685
1686// TLSReportID returns a single TLS report.
1687func (Admin) TLSReportID(ctx context.Context, domain string, reportID int64) tlsrptdb.Record {
1688 record, err := tlsrptdb.RecordID(ctx, reportID)
1689 if err == nil && record.Domain != domain {
1690 err = bstore.ErrAbsent
1691 }
1692 if err == bstore.ErrAbsent {
1693 xcheckuserf(ctx, err, "fetching tls report from database")
1694 }
1695 xcheckf(ctx, err, "fetching tls report from database")
1696 return record
1697}
1698
1699// TLSRPTSummary presents TLS reporting statistics for a single domain
1700// over a period.
1701type TLSRPTSummary struct {
1702 PolicyDomain dns.Domain
1703 Success int64
1704 Failure int64
1705 ResultTypeCounts map[tlsrpt.ResultType]int64
1706}
1707
1708// TLSRPTSummaries returns a summary of received TLS reports overlapping with
1709// period start/end for one or all domains (when domain is empty).
1710// The returned summaries are ordered by domain name.
1711func (Admin) TLSRPTSummaries(ctx context.Context, start, end time.Time, policyDomain string) (domainSummaries []TLSRPTSummary) {
1712 var polDom dns.Domain
1713 if policyDomain != "" {
1714 var err error
1715 polDom, err = dns.ParseDomain(policyDomain)
1716 xcheckuserf(ctx, err, "parsing policy domain")
1717 }
1718 reports, err := tlsrptdb.RecordsPeriodDomain(ctx, start, end, polDom)
1719 xcheckf(ctx, err, "fetching tlsrpt reports from database")
1720
1721 summaries := map[dns.Domain]TLSRPTSummary{}
1722 for _, r := range reports {
1723 dom, err := dns.ParseDomain(r.Domain)
1724 xcheckf(ctx, err, "parsing domain %q", r.Domain)
1725
1726 sum := summaries[dom]
1727 sum.PolicyDomain = dom
1728 for _, result := range r.Report.Policies {
1729 sum.Success += result.Summary.TotalSuccessfulSessionCount
1730 sum.Failure += result.Summary.TotalFailureSessionCount
1731 for _, details := range result.FailureDetails {
1732 if sum.ResultTypeCounts == nil {
1733 sum.ResultTypeCounts = map[tlsrpt.ResultType]int64{}
1734 }
1735 sum.ResultTypeCounts[details.ResultType] += details.FailedSessionCount
1736 }
1737 }
1738 summaries[dom] = sum
1739 }
1740 sums := make([]TLSRPTSummary, 0, len(summaries))
1741 for _, sum := range summaries {
1742 sums = append(sums, sum)
1743 }
1744 sort.Slice(sums, func(i, j int) bool {
1745 return sums[i].PolicyDomain.Name() < sums[j].PolicyDomain.Name()
1746 })
1747 return sums
1748}
1749
1750// DMARCReports returns DMARC reports overlapping with period start/end, for the
1751// given domain (or all domains if empty). The reports are sorted first by period
1752// end (most recent first), then by domain.
1753func (Admin) DMARCReports(ctx context.Context, start, end time.Time, domain string) (reports []dmarcdb.DomainFeedback) {
1754 reports, err := dmarcdb.RecordsPeriodDomain(ctx, start, end, domain)
1755 xcheckf(ctx, err, "fetching dmarc aggregate reports from database")
1756 sort.Slice(reports, func(i, j int) bool {
1757 iend := reports[i].ReportMetadata.DateRange.End
1758 jend := reports[j].ReportMetadata.DateRange.End
1759 if iend == jend {
1760 return reports[i].Domain < reports[j].Domain
1761 }
1762 return iend > jend
1763 })
1764 return reports
1765}
1766
1767// DMARCReportID returns a single DMARC report.
1768func (Admin) DMARCReportID(ctx context.Context, domain string, reportID int64) (report dmarcdb.DomainFeedback) {
1769 report, err := dmarcdb.RecordID(ctx, reportID)
1770 if err == nil && report.Domain != domain {
1771 err = bstore.ErrAbsent
1772 }
1773 if err == bstore.ErrAbsent {
1774 xcheckuserf(ctx, err, "fetching dmarc aggregate report from database")
1775 }
1776 xcheckf(ctx, err, "fetching dmarc aggregate report from database")
1777 return report
1778}
1779
1780// DMARCSummary presents DMARC aggregate reporting statistics for a single domain
1781// over a period.
1782type DMARCSummary struct {
1783 Domain string
1784 Total int
1785 DispositionNone int
1786 DispositionQuarantine int
1787 DispositionReject int
1788 DKIMFail int
1789 SPFFail int
1790 PolicyOverrides map[dmarcrpt.PolicyOverride]int
1791}
1792
1793// DMARCSummaries returns a summary of received DMARC reports overlapping with
1794// period start/end for one or all domains (when domain is empty).
1795// The returned summaries are ordered by domain name.
1796func (Admin) DMARCSummaries(ctx context.Context, start, end time.Time, domain string) (domainSummaries []DMARCSummary) {
1797 reports, err := dmarcdb.RecordsPeriodDomain(ctx, start, end, domain)
1798 xcheckf(ctx, err, "fetching dmarc aggregate reports from database")
1799 summaries := map[string]DMARCSummary{}
1800 for _, r := range reports {
1801 sum := summaries[r.Domain]
1802 sum.Domain = r.Domain
1803 for _, record := range r.Records {
1804 n := record.Row.Count
1805
1806 sum.Total += n
1807
1808 switch record.Row.PolicyEvaluated.Disposition {
1809 case dmarcrpt.DispositionNone:
1810 sum.DispositionNone += n
1811 case dmarcrpt.DispositionQuarantine:
1812 sum.DispositionQuarantine += n
1813 case dmarcrpt.DispositionReject:
1814 sum.DispositionReject += n
1815 }
1816
1817 if record.Row.PolicyEvaluated.DKIM == dmarcrpt.DMARCFail {
1818 sum.DKIMFail += n
1819 }
1820 if record.Row.PolicyEvaluated.SPF == dmarcrpt.DMARCFail {
1821 sum.SPFFail += n
1822 }
1823
1824 for _, reason := range record.Row.PolicyEvaluated.Reasons {
1825 if sum.PolicyOverrides == nil {
1826 sum.PolicyOverrides = map[dmarcrpt.PolicyOverride]int{}
1827 }
1828 sum.PolicyOverrides[reason.Type] += n
1829 }
1830 }
1831 summaries[r.Domain] = sum
1832 }
1833 sums := make([]DMARCSummary, 0, len(summaries))
1834 for _, sum := range summaries {
1835 sums = append(sums, sum)
1836 }
1837 sort.Slice(sums, func(i, j int) bool {
1838 return sums[i].Domain < sums[j].Domain
1839 })
1840 return sums
1841}
1842
1843// Reverse is the result of a reverse lookup.
1844type Reverse struct {
1845 Hostnames []string
1846
1847 // In the future, we can add a iprev-validated host name, and possibly the IPs of the host names.
1848}
1849
1850// LookupIP does a reverse lookup of ip.
1851func (Admin) LookupIP(ctx context.Context, ip string) Reverse {
1852 resolver := dns.StrictResolver{Pkg: "webadmin", Log: pkglog.WithContext(ctx).Logger}
1853 names, _, err := resolver.LookupAddr(ctx, ip)
1854 xcheckuserf(ctx, err, "looking up ip")
1855 return Reverse{names}
1856}
1857
1858// DNSBLStatus returns the IPs from which outgoing connections may be made and
1859// their current status in DNSBLs that are configured. The IPs are typically the
1860// configured listen IPs, or otherwise IPs on the machines network interfaces, with
1861// internal/private IPs removed.
1862//
1863// The returned value maps IPs to per DNSBL statuses, where "pass" means not listed and
1864// anything else is an error string, e.g. "fail: ..." or "temperror: ...".
1865func (Admin) DNSBLStatus(ctx context.Context) (results map[string]map[string]string, using, monitoring []dns.Domain) {
1866 log := mlog.New("webadmin", nil).WithContext(ctx)
1867 resolver := dns.StrictResolver{Pkg: "check", Log: log.Logger}
1868 return dnsblsStatus(ctx, log, resolver)
1869}
1870
1871func dnsblsStatus(ctx context.Context, log mlog.Log, resolver dns.Resolver) (results map[string]map[string]string, using, monitoring []dns.Domain) {
1872 // todo: check health before using dnsbl?
1873 using = mox.Conf.Static.Listeners["public"].SMTP.DNSBLZones
1874 zones := slices.Clone(using)
1875 conf := mox.Conf.DynamicConfig()
1876 for _, zone := range conf.MonitorDNSBLZones {
1877 if !slices.Contains(zones, zone) {
1878 zones = append(zones, zone)
1879 monitoring = append(monitoring, zone)
1880 }
1881 }
1882
1883 r := map[string]map[string]string{}
1884 for _, ip := range xsendingIPs(ctx) {
1885 if ip.IsLoopback() || ip.IsPrivate() {
1886 continue
1887 }
1888 ipstr := ip.String()
1889 r[ipstr] = map[string]string{}
1890 for _, zone := range zones {
1891 status, expl, err := dnsbl.Lookup(ctx, log.Logger, resolver, zone, ip)
1892 result := string(status)
1893 if err != nil {
1894 result += ": " + err.Error()
1895 }
1896 if expl != "" {
1897 result += ": " + expl
1898 }
1899 r[ipstr][zone.LogString()] = result
1900 }
1901 }
1902 return r, using, monitoring
1903}
1904
1905func (Admin) MonitorDNSBLsSave(ctx context.Context, text string) {
1906 var zones []dns.Domain
1907 publicZones := mox.Conf.Static.Listeners["public"].SMTP.DNSBLZones
1908 for line := range strings.SplitSeq(text, "\n") {
1909 line = strings.TrimSpace(line)
1910 if line == "" {
1911 continue
1912 }
1913 d, err := dns.ParseDomain(line)
1914 xcheckuserf(ctx, err, "parsing dnsbl zone %s", line)
1915 if slices.Contains(zones, d) {
1916 xusererrorf(ctx, "duplicate dnsbl zone %s", line)
1917 }
1918 if slices.Contains(publicZones, d) {
1919 xusererrorf(ctx, "dnsbl zone %s already present in public listener", line)
1920 }
1921 zones = append(zones, d)
1922 }
1923
1924 err := admin.ConfigSave(ctx, func(conf *config.Dynamic) {
1925 conf.MonitorDNSBLs = make([]string, len(zones))
1926 conf.MonitorDNSBLZones = nil
1927 for i, z := range zones {
1928 conf.MonitorDNSBLs[i] = z.Name()
1929 }
1930 })
1931 xcheckf(ctx, err, "saving monitoring dnsbl zones")
1932}
1933
1934// DomainRecords returns lines describing DNS records that should exist for the
1935// configured domain.
1936func (Admin) DomainRecords(ctx context.Context, domain string) []string {
1937 log := pkglog.WithContext(ctx)
1938 return DomainRecords(ctx, log, domain)
1939}
1940
1941// DomainRecords is the implementation of API function Admin.DomainRecords, taking
1942// a logger.
1943func DomainRecords(ctx context.Context, log mlog.Log, domain string) []string {
1944 d, err := dns.ParseDomain(domain)
1945 xcheckuserf(ctx, err, "parsing domain")
1946 dc, ok := mox.Conf.Domain(d)
1947 if !ok {
1948 xcheckuserf(ctx, errors.New("unknown domain"), "lookup domain")
1949 }
1950 resolver := dns.StrictResolver{Pkg: "webadmin", Log: pkglog.WithContext(ctx).Logger}
1951 _, result, err := resolver.LookupTXT(ctx, domain+".")
1952 if !dns.IsNotFound(err) {
1953 xcheckf(ctx, err, "looking up record to determine if dnssec is implemented")
1954 }
1955
1956 var certIssuerDomainName, acmeAccountURI string
1957 public := mox.Conf.Static.Listeners["public"]
1958 if public.TLS != nil && public.TLS.ACME != "" {
1959 acme, ok := mox.Conf.Static.ACME[public.TLS.ACME]
1960 if ok && acme.Manager.Manager.Client != nil {
1961 certIssuerDomainName = acme.IssuerDomainName
1962 acc, err := acme.Manager.Manager.Client.GetReg(ctx, "")
1963 log.Check(err, "get public acme account")
1964 if err == nil {
1965 acmeAccountURI = acc.URI
1966 }
1967 }
1968 }
1969
1970 records, err := admin.DomainRecords(dc, d, result.Authentic, certIssuerDomainName, acmeAccountURI)
1971 xcheckf(ctx, err, "dns records")
1972 return records
1973}
1974
1975// DomainAdd adds a new domain and reloads the configuration.
1976func (Admin) DomainAdd(ctx context.Context, disabled bool, domain, accountName, localpart string) {
1977 d, err := dns.ParseDomain(domain)
1978 xcheckuserf(ctx, err, "parsing domain")
1979
1980 err = admin.DomainAdd(ctx, disabled, d, accountName, smtp.Localpart(norm.NFC.String(localpart)))
1981 xcheckf(ctx, err, "adding domain")
1982}
1983
1984// DomainRemove removes an existing domain and reloads the configuration.
1985func (Admin) DomainRemove(ctx context.Context, domain string) {
1986 d, err := dns.ParseDomain(domain)
1987 xcheckuserf(ctx, err, "parsing domain")
1988
1989 err = admin.DomainRemove(ctx, d)
1990 xcheckf(ctx, err, "removing domain")
1991}
1992
1993// AccountAdd adds existing a new account, with an initial email address, and
1994// reloads the configuration.
1995func (Admin) AccountAdd(ctx context.Context, accountName, address string) {
1996 err := admin.AccountAdd(ctx, accountName, address)
1997 xcheckf(ctx, err, "adding account")
1998}
1999
2000// AccountRemove removes an existing account and reloads the configuration.
2001func (Admin) AccountRemove(ctx context.Context, accountName string) {
2002 err := admin.AccountRemove(ctx, accountName)
2003 xcheckf(ctx, err, "removing account")
2004}
2005
2006// AddressAdd adds a new address to the account, which must already exist.
2007func (Admin) AddressAdd(ctx context.Context, address, accountName string) {
2008 err := admin.AddressAdd(ctx, address, accountName)
2009 xcheckf(ctx, err, "adding address")
2010}
2011
2012// AddressRemove removes an existing address.
2013func (Admin) AddressRemove(ctx context.Context, address string) {
2014 err := admin.AddressRemove(ctx, address)
2015 xcheckf(ctx, err, "removing address")
2016}
2017
2018// SetPassword saves a new password for an account, invalidating the previous password.
2019// Sessions are not interrupted, and will keep working. New login attempts must use the new password.
2020// Password must be at least 8 characters.
2021func (Admin) SetPassword(ctx context.Context, accountName, password string) {
2022 log := pkglog.WithContext(ctx)
2023 if len(password) < 8 {
2024 xusererrorf(ctx, "message must be at least 8 characters")
2025 }
2026 acc, err := store.OpenAccount(log, accountName, false)
2027 xcheckf(ctx, err, "open account")
2028 defer func() {
2029 err := acc.Close()
2030 log.WithContext(ctx).Check(err, "closing account")
2031 }()
2032 err = acc.SetPassword(log, password)
2033 xcheckf(ctx, err, "setting password")
2034}
2035
2036// AccountSettingsSave set new settings for an account that only an admin can set.
2037func (Admin) AccountSettingsSave(ctx context.Context, accountName string, maxOutgoingMessagesPerDay, maxFirstTimeRecipientsPerDay int, maxMsgSize int64, firstTimeSenderDelay, noCustomPassword bool) {
2038 err := admin.AccountSave(ctx, accountName, func(acc *config.Account) {
2039 acc.MaxOutgoingMessagesPerDay = maxOutgoingMessagesPerDay
2040 acc.MaxFirstTimeRecipientsPerDay = maxFirstTimeRecipientsPerDay
2041 acc.QuotaMessageSize = maxMsgSize
2042 acc.NoFirstTimeSenderDelay = !firstTimeSenderDelay
2043 acc.NoCustomPassword = noCustomPassword
2044 })
2045 xcheckf(ctx, err, "saving account settings")
2046}
2047
2048// AccountLoginDisabledSave saves the LoginDisabled field of an account.
2049func (Admin) AccountLoginDisabledSave(ctx context.Context, accountName string, loginDisabled string) {
2050 log := pkglog.WithContext(ctx)
2051
2052 acc, err := store.OpenAccount(log, accountName, false)
2053 xcheckf(ctx, err, "open account")
2054 defer func() {
2055 err := acc.Close()
2056 log.Check(err, "closing account")
2057 }()
2058
2059 err = admin.AccountSave(ctx, accountName, func(acc *config.Account) {
2060 acc.LoginDisabled = loginDisabled
2061 })
2062 xcheckf(ctx, err, "saving login disabled account")
2063
2064 err = acc.SessionsClear(ctx, log)
2065 xcheckf(ctx, err, "removing current sessions")
2066}
2067
2068// ClientConfigsDomain returns configurations for email clients, IMAP and
2069// Submission (SMTP) for the domain.
2070func (Admin) ClientConfigsDomain(ctx context.Context, domain string) admin.ClientConfigs {
2071 d, err := dns.ParseDomain(domain)
2072 xcheckuserf(ctx, err, "parsing domain")
2073
2074 cc, err := admin.ClientConfigsDomain(d)
2075 xcheckf(ctx, err, "client config for domain")
2076 return cc
2077}
2078
2079// QueueSize returns the number of messages currently in the outgoing queue.
2080func (Admin) QueueSize(ctx context.Context) int {
2081 n, err := queue.Count(ctx)
2082 xcheckf(ctx, err, "listing messages in queue")
2083 return n
2084}
2085
2086// QueueHoldRuleList lists the hold rules.
2087func (Admin) QueueHoldRuleList(ctx context.Context) []queue.HoldRule {
2088 l, err := queue.HoldRuleList(ctx)
2089 xcheckf(ctx, err, "listing queue hold rules")
2090 return l
2091}
2092
2093// QueueHoldRuleAdd adds a hold rule. Newly submitted and existing messages
2094// matching the hold rule will be marked "on hold".
2095func (Admin) QueueHoldRuleAdd(ctx context.Context, hr queue.HoldRule) queue.HoldRule {
2096 var err error
2097 hr.SenderDomain, err = dns.ParseDomain(hr.SenderDomainStr)
2098 xcheckuserf(ctx, err, "parsing sender domain %q", hr.SenderDomainStr)
2099 hr.RecipientDomain, err = dns.ParseDomain(hr.RecipientDomainStr)
2100 xcheckuserf(ctx, err, "parsing recipient domain %q", hr.RecipientDomainStr)
2101
2102 log := pkglog.WithContext(ctx)
2103 hr, err = queue.HoldRuleAdd(ctx, log, hr)
2104 xcheckf(ctx, err, "adding queue hold rule")
2105 return hr
2106}
2107
2108// QueueHoldRuleRemove removes a hold rule. The Hold field of messages in
2109// the queue are not changed.
2110func (Admin) QueueHoldRuleRemove(ctx context.Context, holdRuleID int64) {
2111 log := pkglog.WithContext(ctx)
2112 err := queue.HoldRuleRemove(ctx, log, holdRuleID)
2113 xcheckf(ctx, err, "removing queue hold rule")
2114}
2115
2116// QueueList returns the messages currently in the outgoing queue.
2117func (Admin) QueueList(ctx context.Context, filter queue.Filter, sort queue.Sort) []queue.Msg {
2118 l, err := queue.List(ctx, filter, sort)
2119 xcheckf(ctx, err, "listing messages in queue")
2120 return l
2121}
2122
2123// QueueNextAttemptSet sets a new time for next delivery attempt of matching
2124// messages from the queue.
2125func (Admin) QueueNextAttemptSet(ctx context.Context, filter queue.Filter, minutes int) (affected int) {
2126 n, err := queue.NextAttemptSet(ctx, filter, time.Now().Add(time.Duration(minutes)*time.Minute))
2127 xcheckf(ctx, err, "setting new next delivery attempt time for matching messages in queue")
2128 return n
2129}
2130
2131// QueueNextAttemptAdd adds a duration to the time of next delivery attempt of
2132// matching messages from the queue.
2133func (Admin) QueueNextAttemptAdd(ctx context.Context, filter queue.Filter, minutes int) (affected int) {
2134 n, err := queue.NextAttemptAdd(ctx, filter, time.Duration(minutes)*time.Minute)
2135 xcheckf(ctx, err, "adding duration to next delivery attempt for matching messages in queue")
2136 return n
2137}
2138
2139// QueueHoldSet sets the Hold field of matching messages in the queue.
2140func (Admin) QueueHoldSet(ctx context.Context, filter queue.Filter, onHold bool) (affected int) {
2141 n, err := queue.HoldSet(ctx, filter, onHold)
2142 xcheckf(ctx, err, "changing onhold for matching messages in queue")
2143 return n
2144}
2145
2146// QueueFail fails delivery for matching messages, causing DSNs to be sent.
2147func (Admin) QueueFail(ctx context.Context, filter queue.Filter) (affected int) {
2148 log := pkglog.WithContext(ctx)
2149 n, err := queue.Fail(ctx, log, filter)
2150 xcheckf(ctx, err, "drop messages from queue")
2151 return n
2152}
2153
2154// QueueDrop removes matching messages from the queue.
2155func (Admin) QueueDrop(ctx context.Context, filter queue.Filter) (affected int) {
2156 log := pkglog.WithContext(ctx)
2157 n, err := queue.Drop(ctx, log, filter)
2158 xcheckf(ctx, err, "drop messages from queue")
2159 return n
2160}
2161
2162// QueueRequireTLSSet updates the requiretls field for matching messages in the
2163// queue, to be used for the next delivery.
2164func (Admin) QueueRequireTLSSet(ctx context.Context, filter queue.Filter, requireTLS *bool) (affected int) {
2165 n, err := queue.RequireTLSSet(ctx, filter, requireTLS)
2166 xcheckf(ctx, err, "update requiretls for messages in queue")
2167 return n
2168}
2169
2170// QueueTransportSet initiates delivery of a message from the queue and sets the transport
2171// to use for delivery.
2172func (Admin) QueueTransportSet(ctx context.Context, filter queue.Filter, transport string) (affected int) {
2173 n, err := queue.TransportSet(ctx, filter, transport)
2174 xcheckf(ctx, err, "changing transport for messages in queue")
2175 return n
2176}
2177
2178// RetiredList returns messages retired from the queue (delivery could
2179// have succeeded or failed).
2180func (Admin) RetiredList(ctx context.Context, filter queue.RetiredFilter, sort queue.RetiredSort) []queue.MsgRetired {
2181 l, err := queue.RetiredList(ctx, filter, sort)
2182 xcheckf(ctx, err, "listing retired messages")
2183 return l
2184}
2185
2186// HookQueueSize returns the number of webhooks still to be delivered.
2187func (Admin) HookQueueSize(ctx context.Context) int {
2188 n, err := queue.HookQueueSize(ctx)
2189 xcheckf(ctx, err, "get hook queue size")
2190 return n
2191}
2192
2193// HookList lists webhooks still to be delivered.
2194func (Admin) HookList(ctx context.Context, filter queue.HookFilter, sort queue.HookSort) []queue.Hook {
2195 l, err := queue.HookList(ctx, filter, sort)
2196 xcheckf(ctx, err, "listing hook queue")
2197 return l
2198}
2199
2200// HookNextAttemptSet sets a new time for next delivery attempt of matching
2201// hooks from the queue.
2202func (Admin) HookNextAttemptSet(ctx context.Context, filter queue.HookFilter, minutes int) (affected int) {
2203 n, err := queue.HookNextAttemptSet(ctx, filter, time.Now().Add(time.Duration(minutes)*time.Minute))
2204 xcheckf(ctx, err, "setting new next delivery attempt time for matching webhooks in queue")
2205 return n
2206}
2207
2208// HookNextAttemptAdd adds a duration to the time of next delivery attempt of
2209// matching hooks from the queue.
2210func (Admin) HookNextAttemptAdd(ctx context.Context, filter queue.HookFilter, minutes int) (affected int) {
2211 n, err := queue.HookNextAttemptAdd(ctx, filter, time.Duration(minutes)*time.Minute)
2212 xcheckf(ctx, err, "adding duration to next delivery attempt for matching webhooks in queue")
2213 return n
2214}
2215
2216// HookRetiredList lists retired webhooks.
2217func (Admin) HookRetiredList(ctx context.Context, filter queue.HookRetiredFilter, sort queue.HookRetiredSort) []queue.HookRetired {
2218 l, err := queue.HookRetiredList(ctx, filter, sort)
2219 xcheckf(ctx, err, "listing retired hooks")
2220 return l
2221}
2222
2223// HookCancel prevents further delivery attempts of matching webhooks.
2224func (Admin) HookCancel(ctx context.Context, filter queue.HookFilter) (affected int) {
2225 log := pkglog.WithContext(ctx)
2226 n, err := queue.HookCancel(ctx, log, filter)
2227 xcheckf(ctx, err, "cancel hooks in queue")
2228 return n
2229}
2230
2231// LogLevels returns the current log levels.
2232func (Admin) LogLevels(ctx context.Context) map[string]string {
2233 m := map[string]string{}
2234 for pkg, level := range mox.Conf.LogLevels() {
2235 s, ok := mlog.LevelStrings[level]
2236 if !ok {
2237 s = level.String()
2238 }
2239 m[pkg] = s
2240 }
2241 return m
2242}
2243
2244// LogLevelSet sets a log level for a package.
2245func (Admin) LogLevelSet(ctx context.Context, pkg string, levelStr string) {
2246 level, ok := mlog.Levels[levelStr]
2247 if !ok {
2248 xcheckuserf(ctx, errors.New("unknown"), "lookup level")
2249 }
2250 mox.Conf.LogLevelSet(pkglog.WithContext(ctx), pkg, level)
2251}
2252
2253// LogLevelRemove removes a log level for a package, which cannot be the empty string.
2254func (Admin) LogLevelRemove(ctx context.Context, pkg string) {
2255 mox.Conf.LogLevelRemove(pkglog.WithContext(ctx), pkg)
2256}
2257
2258// CheckUpdatesEnabled returns whether checking for updates is enabled.
2259func (Admin) CheckUpdatesEnabled(ctx context.Context) bool {
2260 return mox.Conf.Static.CheckUpdates
2261}
2262
2263// WebserverConfig is the combination of WebDomainRedirects and WebHandlers
2264// from the domains.conf configuration file.
2265type WebserverConfig struct {
2266 WebDNSDomainRedirects [][2]dns.Domain // From server to frontend.
2267 WebDomainRedirects [][2]string // From frontend to server, it's not convenient to create dns.Domain in the frontend.
2268 WebHandlers []config.WebHandler
2269}
2270
2271// WebserverConfig returns the current webserver config
2272func (Admin) WebserverConfig(ctx context.Context) (conf WebserverConfig) {
2273 conf = webserverConfig()
2274 conf.WebDomainRedirects = nil
2275 return conf
2276}
2277
2278func webserverConfig() WebserverConfig {
2279 conf := mox.Conf.DynamicConfig()
2280 r := conf.WebDNSDomainRedirects
2281 l := conf.WebHandlers
2282
2283 x := make([][2]dns.Domain, 0, len(r))
2284 xs := make([][2]string, 0, len(r))
2285 for k, v := range r {
2286 x = append(x, [2]dns.Domain{k, v})
2287 xs = append(xs, [2]string{k.Name(), v.Name()})
2288 }
2289 sort.Slice(x, func(i, j int) bool {
2290 return x[i][0].ASCII < x[j][0].ASCII
2291 })
2292 sort.Slice(xs, func(i, j int) bool {
2293 return xs[i][0] < xs[j][0]
2294 })
2295 return WebserverConfig{x, xs, l}
2296}
2297
2298// WebserverConfigSave saves a new webserver config. If oldConf is not equal to
2299// the current config, an error is returned.
2300func (Admin) WebserverConfigSave(ctx context.Context, oldConf, newConf WebserverConfig) (savedConf WebserverConfig) {
2301 current := webserverConfig()
2302 webhandlersEqual := func() bool {
2303 if len(current.WebHandlers) != len(oldConf.WebHandlers) {
2304 return false
2305 }
2306 for i, wh := range current.WebHandlers {
2307 if !wh.Equal(oldConf.WebHandlers[i]) {
2308 return false
2309 }
2310 }
2311 return true
2312 }
2313 if !reflect.DeepEqual(oldConf.WebDNSDomainRedirects, current.WebDNSDomainRedirects) || !webhandlersEqual() {
2314 xcheckuserf(ctx, errors.New("config has changed"), "comparing old/current config")
2315 }
2316
2317 // Convert to map, check that there are no duplicates here. The canonicalized
2318 // dns.Domain are checked again for uniqueness when parsing the config before
2319 // storing.
2320 domainRedirects := map[string]string{}
2321 for _, x := range newConf.WebDomainRedirects {
2322 if _, ok := domainRedirects[x[0]]; ok {
2323 xcheckuserf(ctx, errors.New("already present"), "checking redirect %s", x[0])
2324 }
2325 domainRedirects[x[0]] = x[1]
2326 }
2327
2328 err := admin.ConfigSave(ctx, func(conf *config.Dynamic) {
2329 conf.WebDomainRedirects = domainRedirects
2330 conf.WebHandlers = newConf.WebHandlers
2331 })
2332 xcheckf(ctx, err, "saving webserver config")
2333
2334 savedConf = webserverConfig()
2335 savedConf.WebDomainRedirects = nil
2336 return savedConf
2337}
2338
2339// Transports returns the configured transports, for sending email.
2340func (Admin) Transports(ctx context.Context) map[string]config.Transport {
2341 return mox.Conf.Static.Transports
2342}
2343
2344// DMARCEvaluationStats returns a map of all domains with evaluations to a count of
2345// the evaluations and whether those evaluations will cause a report to be sent.
2346func (Admin) DMARCEvaluationStats(ctx context.Context) map[string]dmarcdb.EvaluationStat {
2347 stats, err := dmarcdb.EvaluationStats(ctx)
2348 xcheckf(ctx, err, "get evaluation stats")
2349 return stats
2350}
2351
2352// DMARCEvaluationsDomain returns all evaluations for aggregate reports for the
2353// domain, sorted from oldest to most recent.
2354func (Admin) DMARCEvaluationsDomain(ctx context.Context, domain string) (dns.Domain, []dmarcdb.Evaluation) {
2355 dom, err := dns.ParseDomain(domain)
2356 xcheckf(ctx, err, "parsing domain")
2357
2358 evals, err := dmarcdb.EvaluationsDomain(ctx, dom)
2359 xcheckf(ctx, err, "get evaluations for domain")
2360 return dom, evals
2361}
2362
2363// DMARCRemoveEvaluations removes evaluations for a domain.
2364func (Admin) DMARCRemoveEvaluations(ctx context.Context, domain string) {
2365 dom, err := dns.ParseDomain(domain)
2366 xcheckf(ctx, err, "parsing domain")
2367
2368 err = dmarcdb.RemoveEvaluationsDomain(ctx, dom)
2369 xcheckf(ctx, err, "removing evaluations for domain")
2370}
2371
2372// DMARCSuppressAdd adds a reporting address to the suppress list. Outgoing
2373// reports will be suppressed for a period.
2374func (Admin) DMARCSuppressAdd(ctx context.Context, reportingAddress string, until time.Time, comment string) {
2375 addr, err := smtp.ParseAddress(reportingAddress)
2376 xcheckuserf(ctx, err, "parsing reporting address")
2377
2378 ba := dmarcdb.SuppressAddress{ReportingAddress: addr.String(), Until: until, Comment: comment}
2379 err = dmarcdb.SuppressAdd(ctx, &ba)
2380 xcheckf(ctx, err, "adding address to suppresslist")
2381}
2382
2383// DMARCSuppressList returns all reporting addresses on the suppress list.
2384func (Admin) DMARCSuppressList(ctx context.Context) []dmarcdb.SuppressAddress {
2385 l, err := dmarcdb.SuppressList(ctx)
2386 xcheckf(ctx, err, "listing reporting addresses in suppresslist")
2387 return l
2388}
2389
2390// DMARCSuppressRemove removes a reporting address record from the suppress list.
2391func (Admin) DMARCSuppressRemove(ctx context.Context, id int64) {
2392 err := dmarcdb.SuppressRemove(ctx, id)
2393 xcheckf(ctx, err, "removing reporting address from suppresslist")
2394}
2395
2396// DMARCSuppressExtend updates the until field of a suppressed reporting address record.
2397func (Admin) DMARCSuppressExtend(ctx context.Context, id int64, until time.Time) {
2398 err := dmarcdb.SuppressUpdate(ctx, id, until)
2399 xcheckf(ctx, err, "updating reporting address in suppresslist")
2400}
2401
2402// TLSRPTResults returns all TLSRPT results in the database.
2403func (Admin) TLSRPTResults(ctx context.Context) []tlsrptdb.TLSResult {
2404 results, err := tlsrptdb.Results(ctx)
2405 xcheckf(ctx, err, "get results")
2406 return results
2407}
2408
2409// TLSRPTResultsPolicyDomain returns the TLS results for a domain.
2410func (Admin) TLSRPTResultsDomain(ctx context.Context, isRcptDom bool, policyDomain string) (dns.Domain, []tlsrptdb.TLSResult) {
2411 dom, err := dns.ParseDomain(policyDomain)
2412 xcheckf(ctx, err, "parsing domain")
2413
2414 if isRcptDom {
2415 results, err := tlsrptdb.ResultsRecipientDomain(ctx, dom)
2416 xcheckf(ctx, err, "get result for recipient domain")
2417 return dom, results
2418 }
2419 results, err := tlsrptdb.ResultsPolicyDomain(ctx, dom)
2420 xcheckf(ctx, err, "get result for policy domain")
2421 return dom, results
2422}
2423
2424// LookupTLSRPTRecord looks up a TLSRPT record and returns the parsed form, original txt
2425// form from DNS, and error with the TLSRPT record as a string.
2426func (Admin) LookupTLSRPTRecord(ctx context.Context, domain string) (record *TLSRPTRecord, txt string, errstr string) {
2427 log := pkglog.WithContext(ctx)
2428 dom, err := dns.ParseDomain(domain)
2429 xcheckf(ctx, err, "parsing domain")
2430
2431 resolver := dns.StrictResolver{Pkg: "webadmin", Log: log.Logger}
2432 r, txt, err := tlsrpt.Lookup(ctx, log.Logger, resolver, dom)
2433 if err != nil && (errors.Is(err, tlsrpt.ErrNoRecord) || errors.Is(err, tlsrpt.ErrMultipleRecords) || errors.Is(err, tlsrpt.ErrRecordSyntax) || errors.Is(err, tlsrpt.ErrDNS)) {
2434 errstr = err.Error()
2435 err = nil
2436 }
2437 xcheckf(ctx, err, "fetching tlsrpt record")
2438
2439 if r != nil {
2440 record = &TLSRPTRecord{Record: *r}
2441 }
2442
2443 return record, txt, errstr
2444}
2445
2446// TLSRPTRemoveResults removes the TLS results for a domain for the given day. If
2447// day is empty, all results are removed.
2448func (Admin) TLSRPTRemoveResults(ctx context.Context, isRcptDom bool, domain string, day string) {
2449 dom, err := dns.ParseDomain(domain)
2450 xcheckf(ctx, err, "parsing domain")
2451
2452 if isRcptDom {
2453 err = tlsrptdb.RemoveResultsRecipientDomain(ctx, dom, day)
2454 xcheckf(ctx, err, "removing tls results")
2455 } else {
2456 err = tlsrptdb.RemoveResultsPolicyDomain(ctx, dom, day)
2457 xcheckf(ctx, err, "removing tls results")
2458 }
2459}
2460
2461// TLSRPTSuppressAdd adds a reporting address to the suppress list. Outgoing
2462// reports will be suppressed for a period.
2463func (Admin) TLSRPTSuppressAdd(ctx context.Context, reportingAddress string, until time.Time, comment string) {
2464 addr, err := smtp.ParseAddress(reportingAddress)
2465 xcheckuserf(ctx, err, "parsing reporting address")
2466
2467 ba := tlsrptdb.SuppressAddress{ReportingAddress: addr.String(), Until: until, Comment: comment}
2468 err = tlsrptdb.SuppressAdd(ctx, &ba)
2469 xcheckf(ctx, err, "adding address to suppresslist")
2470}
2471
2472// TLSRPTSuppressList returns all reporting addresses on the suppress list.
2473func (Admin) TLSRPTSuppressList(ctx context.Context) []tlsrptdb.SuppressAddress {
2474 l, err := tlsrptdb.SuppressList(ctx)
2475 xcheckf(ctx, err, "listing reporting addresses in suppresslist")
2476 return l
2477}
2478
2479// TLSRPTSuppressRemove removes a reporting address record from the suppress list.
2480func (Admin) TLSRPTSuppressRemove(ctx context.Context, id int64) {
2481 err := tlsrptdb.SuppressRemove(ctx, id)
2482 xcheckf(ctx, err, "removing reporting address from suppresslist")
2483}
2484
2485// TLSRPTSuppressExtend updates the until field of a suppressed reporting address record.
2486func (Admin) TLSRPTSuppressExtend(ctx context.Context, id int64, until time.Time) {
2487 err := tlsrptdb.SuppressUpdate(ctx, id, until)
2488 xcheckf(ctx, err, "updating reporting address in suppresslist")
2489}
2490
2491// LookupCid turns an ID from a Received header into a cid as used in logging.
2492func (Admin) LookupCid(ctx context.Context, recvID string) (cid string) {
2493 v, err := mox.ReceivedToCid(recvID)
2494 xcheckf(ctx, err, "received id to cid")
2495 return fmt.Sprintf("%x", v)
2496}
2497
2498// Config returns the dynamic config.
2499func (Admin) Config(ctx context.Context) config.Dynamic {
2500 return mox.Conf.DynamicConfig()
2501}
2502
2503// AccountRoutesSave saves routes for an account.
2504func (Admin) AccountRoutesSave(ctx context.Context, accountName string, routes []config.Route) {
2505 err := admin.AccountSave(ctx, accountName, func(acc *config.Account) {
2506 acc.Routes = routes
2507 })
2508 xcheckf(ctx, err, "saving account routes")
2509}
2510
2511// DomainRoutesSave saves routes for a domain.
2512func (Admin) DomainRoutesSave(ctx context.Context, domainName string, routes []config.Route) {
2513 err := admin.DomainSave(ctx, domainName, func(domain *config.Domain) error {
2514 domain.Routes = routes
2515 return nil
2516 })
2517 xcheckf(ctx, err, "saving domain routes")
2518}
2519
2520// RoutesSave saves global routes.
2521func (Admin) RoutesSave(ctx context.Context, routes []config.Route) {
2522 err := admin.ConfigSave(ctx, func(config *config.Dynamic) {
2523 config.Routes = routes
2524 })
2525 xcheckf(ctx, err, "saving global routes")
2526}
2527
2528// DomainDescriptionSave saves the description for a domain.
2529func (Admin) DomainDescriptionSave(ctx context.Context, domainName, descr string) {
2530 err := admin.DomainSave(ctx, domainName, func(domain *config.Domain) error {
2531 domain.Description = descr
2532 return nil
2533 })
2534 xcheckf(ctx, err, "saving domain description")
2535}
2536
2537// DomainClientSettingsDomainSave saves the client settings domain for a domain.
2538func (Admin) DomainClientSettingsDomainSave(ctx context.Context, domainName, clientSettingsDomain string) {
2539 err := admin.DomainSave(ctx, domainName, func(domain *config.Domain) error {
2540 domain.ClientSettingsDomain = clientSettingsDomain
2541 return nil
2542 })
2543 xcheckf(ctx, err, "saving client settings domain")
2544}
2545
2546// DomainLocalpartConfigSave saves the localpart catchall and case-sensitive
2547// settings for a domain.
2548func (Admin) DomainLocalpartConfigSave(ctx context.Context, domainName string, localpartCatchallSeparators []string, localpartCaseSensitive bool) {
2549 err := admin.DomainSave(ctx, domainName, func(domain *config.Domain) error {
2550 // We don't allow introducing new catchall separators that are used in DMARC/TLS
2551 // reporting. Can occur in existing configs for backwards compatibility.
2552 containsSep := func(seps []string) bool {
2553 for _, sep := range seps {
2554 if domain.DMARC != nil && strings.Contains(domain.DMARC.Localpart, sep) {
2555 return true
2556 }
2557 if domain.TLSRPT != nil && strings.Contains(domain.TLSRPT.Localpart, sep) {
2558 return true
2559 }
2560 }
2561 return false
2562 }
2563 if !containsSep(domain.LocalpartCatchallSeparatorsEffective) && containsSep(localpartCatchallSeparators) {
2564 xusererrorf(ctx, "cannot add localpart catchall separators that are used in dmarc and/or tls reporting addresses, change reporting addresses first")
2565 }
2566
2567 domain.LocalpartCatchallSeparatorsEffective = localpartCatchallSeparators
2568 // If there is a single separator, we prefer the non-list form, it's easier to
2569 // read/edit and should suffice for most setups.
2570 domain.LocalpartCatchallSeparator = ""
2571 domain.LocalpartCatchallSeparators = nil
2572 if len(localpartCatchallSeparators) == 1 {
2573 domain.LocalpartCatchallSeparator = localpartCatchallSeparators[0]
2574 } else {
2575 domain.LocalpartCatchallSeparators = localpartCatchallSeparators
2576 }
2577
2578 domain.LocalpartCaseSensitive = localpartCaseSensitive
2579 return nil
2580 })
2581 xcheckf(ctx, err, "saving localpart settings for domain")
2582}
2583
2584// DomainDMARCAddressSave saves the DMARC reporting address/processing
2585// configuration for a domain. If localpart is empty, processing reports is
2586// disabled.
2587func (Admin) DomainDMARCAddressSave(ctx context.Context, domainName, localpart, domain, account, mailbox string) {
2588 err := admin.DomainSave(ctx, domainName, func(d *config.Domain) error {
2589 // DMARC reporting addresses can contain the localpart catchall separator(s) for
2590 // backwards compability (hence not enforced when parsing the config files), but we
2591 // don't allow creating them.
2592 if d.DMARC == nil || d.DMARC.Localpart != localpart {
2593 for _, sep := range d.LocalpartCatchallSeparatorsEffective {
2594 if strings.Contains(localpart, sep) {
2595 xusererrorf(ctx, "dmarc reporting address cannot contain catchall separator %q in localpart (%q)", sep, localpart)
2596 }
2597 }
2598 }
2599
2600 if localpart == "" {
2601 d.DMARC = nil
2602 } else {
2603 d.DMARC = &config.DMARC{
2604 Localpart: localpart,
2605 Domain: domain,
2606 Account: account,
2607 Mailbox: mailbox,
2608 }
2609 }
2610 return nil
2611 })
2612 xcheckf(ctx, err, "saving dmarc reporting address/settings for domain")
2613}
2614
2615// DomainTLSRPTAddressSave saves the TLS reporting address/processing
2616// configuration for a domain. If localpart is empty, processing reports is
2617// disabled.
2618func (Admin) DomainTLSRPTAddressSave(ctx context.Context, domainName, localpart, domain, account, mailbox string) {
2619 err := admin.DomainSave(ctx, domainName, func(d *config.Domain) error {
2620 // TLS reporting addresses can contain the localpart catchall separator(s) for
2621 // backwards compability (hence not enforced when parsing the config files), but we
2622 // don't allow creating them.
2623 if d.TLSRPT == nil || d.TLSRPT.Localpart != localpart {
2624 for _, sep := range d.LocalpartCatchallSeparatorsEffective {
2625 if strings.Contains(localpart, sep) {
2626 xusererrorf(ctx, "tls reporting address cannot contain catchall separator %q in localpart (%q)", sep, localpart)
2627 }
2628 }
2629 }
2630
2631 if localpart == "" {
2632 d.TLSRPT = nil
2633 } else {
2634 d.TLSRPT = &config.TLSRPT{
2635 Localpart: localpart,
2636 Domain: domain,
2637 Account: account,
2638 Mailbox: mailbox,
2639 }
2640 }
2641 return nil
2642 })
2643 xcheckf(ctx, err, "saving tls reporting address/settings for domain")
2644}
2645
2646// DomainMTASTSSave saves the MTASTS policy for a domain. If policyID is empty,
2647// no MTASTS policy is served.
2648func (Admin) DomainMTASTSSave(ctx context.Context, domainName, policyID string, mode mtasts.Mode, maxAge time.Duration, mx []string) {
2649 err := admin.DomainSave(ctx, domainName, func(d *config.Domain) error {
2650 if policyID == "" {
2651 d.MTASTS = nil
2652 } else {
2653 d.MTASTS = &config.MTASTS{
2654 PolicyID: policyID,
2655 Mode: mode,
2656 MaxAge: maxAge,
2657 MX: mx,
2658 }
2659 }
2660 return nil
2661 })
2662 xcheckf(ctx, err, "saving mtasts policy for domain")
2663}
2664
2665// DomainDKIMAdd adds a DKIM selector for a domain, generating a new private
2666// key. The selector is not enabled for signing.
2667func (Admin) DomainDKIMAdd(ctx context.Context, domainName, selector, algorithm, hash string, headerRelaxed, bodyRelaxed, seal bool, headers []string, lifetime time.Duration) {
2668 d, err := dns.ParseDomain(domainName)
2669 xcheckuserf(ctx, err, "parsing domain")
2670 s, err := dns.ParseDomain(selector)
2671 xcheckuserf(ctx, err, "parsing selector")
2672 err = admin.DKIMAdd(ctx, d, s, algorithm, hash, headerRelaxed, bodyRelaxed, seal, headers, lifetime)
2673 xcheckf(ctx, err, "adding dkim key")
2674}
2675
2676// DomainDKIMRemove removes a DKIM selector for a domain.
2677func (Admin) DomainDKIMRemove(ctx context.Context, domainName, selector string) {
2678 d, err := dns.ParseDomain(domainName)
2679 xcheckuserf(ctx, err, "parsing domain")
2680 s, err := dns.ParseDomain(selector)
2681 xcheckuserf(ctx, err, "parsing selector")
2682 err = admin.DKIMRemove(ctx, d, s)
2683 xcheckf(ctx, err, "removing dkim key")
2684}
2685
2686// DomainDKIMSave saves the settings of selectors, and which to enable for
2687// signing, for a domain. All currently configured selectors must be present,
2688// selectors cannot be added/removed with this function.
2689func (Admin) DomainDKIMSave(ctx context.Context, domainName string, selectors map[string]config.Selector, sign []string) {
2690 for _, s := range sign {
2691 if _, ok := selectors[s]; !ok {
2692 xcheckuserf(ctx, fmt.Errorf("cannot sign unknown selector %q", s), "checking selectors")
2693 }
2694 }
2695
2696 err := admin.DomainSave(ctx, domainName, func(d *config.Domain) error {
2697 if len(selectors) != len(d.DKIM.Selectors) {
2698 xcheckuserf(ctx, fmt.Errorf("cannot add/remove dkim selectors with this function"), "checking selectors")
2699 }
2700 for s := range selectors {
2701 if _, ok := d.DKIM.Selectors[s]; !ok {
2702 xcheckuserf(ctx, fmt.Errorf("unknown selector %q", s), "checking selectors")
2703 }
2704 }
2705 // At least the selectors are the same.
2706
2707 // Build up new selectors.
2708 sels := map[string]config.Selector{}
2709 for name, nsel := range selectors {
2710 osel := d.DKIM.Selectors[name]
2711 xsel := config.Selector{
2712 Hash: nsel.Hash,
2713 Canonicalization: nsel.Canonicalization,
2714 DontSealHeaders: nsel.DontSealHeaders,
2715 Expiration: nsel.Expiration,
2716
2717 PrivateKeyFile: osel.PrivateKeyFile,
2718 }
2719 if !slices.Equal(osel.HeadersEffective, nsel.Headers) {
2720 xsel.Headers = nsel.Headers
2721 }
2722 sels[name] = xsel
2723 }
2724
2725 // Enable the new selector settings.
2726 d.DKIM = config.DKIM{
2727 Selectors: sels,
2728 Sign: sign,
2729 }
2730 return nil
2731 })
2732 xcheckf(ctx, err, "saving dkim selector for domain")
2733}
2734
2735// DomainDisabledSave saves the Disabled field of a domain. A disabled domain
2736// rejects incoming/outgoing messages involving the domain and does not request new
2737// TLS certificats with ACME.
2738func (Admin) DomainDisabledSave(ctx context.Context, domainName string, disabled bool) {
2739 err := admin.DomainSave(ctx, domainName, func(d *config.Domain) error {
2740 d.Disabled = disabled
2741 return nil
2742 })
2743 xcheckf(ctx, err, "saving disabled setting for domain")
2744}
2745
2746func xparseAddress(ctx context.Context, lp, domain string) smtp.Address {
2747 xlp, err := smtp.ParseLocalpart(lp)
2748 xcheckuserf(ctx, err, "parsing localpart")
2749 d, err := dns.ParseDomain(domain)
2750 xcheckuserf(ctx, err, "parsing domain")
2751 return smtp.NewAddress(xlp, d)
2752}
2753
2754func (Admin) AliasAdd(ctx context.Context, aliaslp string, domainName string, alias config.Alias) {
2755 addr := xparseAddress(ctx, aliaslp, domainName)
2756 err := admin.AliasAdd(ctx, addr, alias)
2757 xcheckf(ctx, err, "adding alias")
2758}
2759
2760func (Admin) AliasUpdate(ctx context.Context, aliaslp string, domainName string, postPublic, listMembers, allowMsgFrom bool) {
2761 addr := xparseAddress(ctx, aliaslp, domainName)
2762 alias := config.Alias{
2763 PostPublic: postPublic,
2764 ListMembers: listMembers,
2765 AllowMsgFrom: allowMsgFrom,
2766 }
2767 err := admin.AliasUpdate(ctx, addr, alias)
2768 xcheckf(ctx, err, "saving alias")
2769}
2770
2771func (Admin) AliasRemove(ctx context.Context, aliaslp string, domainName string) {
2772 addr := xparseAddress(ctx, aliaslp, domainName)
2773 err := admin.AliasRemove(ctx, addr)
2774 xcheckf(ctx, err, "removing alias")
2775}
2776
2777func (Admin) AliasAddressesAdd(ctx context.Context, aliaslp string, domainName string, addresses []string) {
2778 addr := xparseAddress(ctx, aliaslp, domainName)
2779 err := admin.AliasAddressesAdd(ctx, addr, addresses)
2780 xcheckf(ctx, err, "adding address to alias")
2781}
2782
2783func (Admin) AliasAddressesRemove(ctx context.Context, aliaslp string, domainName string, addresses []string) {
2784 addr := xparseAddress(ctx, aliaslp, domainName)
2785 err := admin.AliasAddressesRemove(ctx, addr, addresses)
2786 xcheckf(ctx, err, "removing address from alias")
2787}
2788
2789func (Admin) TLSPublicKeys(ctx context.Context, accountOpt string) ([]store.TLSPublicKey, error) {
2790 return store.TLSPublicKeyList(ctx, accountOpt)
2791}
2792
2793func (Admin) LoginAttempts(ctx context.Context, accountName string, limit int) []store.LoginAttempt {
2794 l, err := store.LoginAttemptList(ctx, accountName, limit)
2795 xcheckf(ctx, err, "listing login attempts")
2796 return l
2797}
2798