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