1package tlsrpt
2
3import (
4 "context"
5 "errors"
6 "fmt"
7 "time"
8
9 "golang.org/x/exp/slog"
10
11 "github.com/mjl-/mox/dns"
12 "github.com/mjl-/mox/mlog"
13 "github.com/mjl-/mox/stub"
14)
15
16var (
17 MetricLookup stub.HistogramVec = stub.HistogramVecIgnore{}
18)
19
20var (
21 ErrNoRecord = errors.New("tlsrpt: no tlsrpt dns txt record")
22 ErrMultipleRecords = errors.New("tlsrpt: multiple tlsrpt records") // Must be treated as if domain does not implement TLSRPT.
23 ErrDNS = errors.New("tlsrpt: temporary error")
24 ErrRecordSyntax = errors.New("tlsrpt: record syntax error")
25)
26
27// Lookup looks up a TLSRPT DNS TXT record for domain at "_smtp._tls.<domain>" and
28// parses it.
29func Lookup(ctx context.Context, elog *slog.Logger, resolver dns.Resolver, domain dns.Domain) (rrecord *Record, rtxt string, rerr error) {
30 log := mlog.New("tlsrpt", elog)
31 start := time.Now()
32 defer func() {
33 result := "ok"
34 if rerr != nil {
35 if errors.Is(rerr, ErrNoRecord) {
36 result = "notfound"
37 } else if errors.Is(rerr, ErrMultipleRecords) {
38 result = "multiple"
39 } else if errors.Is(rerr, ErrDNS) {
40 result = "temperror"
41 } else if errors.Is(rerr, ErrRecordSyntax) {
42 result = "malformed"
43 } else {
44 result = "error"
45 }
46 }
47 MetricLookup.ObserveLabels(float64(time.Since(start))/float64(time.Second), result)
48 log.Debugx("tlsrpt lookup result", rerr,
49 slog.Any("domain", domain),
50 slog.Any("record", rrecord),
51 slog.Duration("duration", time.Since(start)))
52 }()
53
54 name := "_smtp._tls." + domain.ASCII + "."
55 txts, _, err := dns.WithPackage(resolver, "tlsrpt").LookupTXT(ctx, name)
56 if dns.IsNotFound(err) {
57 return nil, "", ErrNoRecord
58 } else if err != nil {
59 return nil, "", fmt.Errorf("%w: %s", ErrDNS, err)
60 }
61
62 var text string
63 var record *Record
64 for _, txt := range txts {
65 r, istlsrpt, err := ParseRecord(txt)
66 if !istlsrpt {
67 // This is a loose but probably reasonable interpretation of ../rfc/8460:375 which
68 // wants us to discard otherwise valid records that start with e.g. "v=TLSRPTv1 ;"
69 // (note the space before the ";") when multiple TXT records were returned.
70 continue
71 }
72 if err != nil {
73 return nil, "", fmt.Errorf("parsing record: %w", err)
74 }
75 if record != nil {
76 return nil, "", ErrMultipleRecords
77 }
78 record = r
79 text = txt
80 }
81 if record == nil {
82 return nil, "", ErrNoRecord
83 }
84 return record, text, nil
85}
86