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