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