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