1package tlsrptdb
2
3import (
4 "context"
5 "fmt"
6 "time"
7
8 "github.com/mjl-/bstore"
9
10 "github.com/mjl-/mox/dns"
11 "github.com/mjl-/mox/tlsrpt"
12)
13
14// TLSResult is stored in the database to track TLS results per policy domain, day
15// and recipient domain. These records will be included in TLS reports.
16type TLSResult struct {
17 ID int64
18
19 // Domain potentially with TLSRPT DNS record, with addresses that will receive
20 // reports. Either a recipient domain (for MTA-STS policies) or an (MX) host (for
21 // DANE policies). Unicode.
22 PolicyDomain string `bstore:"unique PolicyDomain+DayUTC+RecipientDomain,nonzero"`
23
24 // DayUTC is of the form yyyymmdd.
25 DayUTC string `bstore:"nonzero"`
26 // We send per 24h UTC-aligned days. ../rfc/8460:474
27
28 // Reports are sent per recipient domain and per MX host. For reports to a
29 // recipient domain, we type send a result for MTA-STS and one or more MX host
30 // (DANE) results. Unicode.
31 RecipientDomain string `bstore:"index,nonzero"`
32
33 Created time.Time `bstore:"default now"`
34 Updated time.Time `bstore:"default now"`
35
36 IsHost bool // Result is for MX host (DANE), not recipient domain (MTA-STS).
37
38 // Whether to send a report. TLS results for delivering messages with TLS reports
39 // will be recorded, but will not cause a report to be sent.
40 SendReport bool
41 // ../rfc/8460:318 says we should not include TLS results for sending a TLS report,
42 // but presumably that's to prevent mail servers sending a report every day once
43 // they start.
44
45 // Set after sending to recipient domain, before sending results to policy domain
46 // (after which the record is removed).
47 SentToRecipientDomain bool
48 // Reporting addresses from the recipient domain TLSRPT record, not necessarily
49 // those we sent to (e.g. due to failure). Used to leave results to MX target
50 // (DANE) policy domains out that were already sent in the report to the recipient
51 // domain, so we don't report twice.
52 RecipientDomainReportingAddresses []string
53 // Set after sending report to policy domain.
54 SentToPolicyDomain bool
55
56 // Results is updated for each TLS attempt.
57 Results []tlsrpt.Result
58}
59
60// SuppressAddress is a reporting address for which outgoing TLS reports
61// will be suppressed for a period.
62type SuppressAddress struct {
63 ID int64 `bstore:"typename TLSRPTSuppressAddress"`
64 Inserted time.Time `bstore:"default now"`
65 ReportingAddress string `bstore:"unique"`
66 Until time.Time `bstore:"nonzero"`
67 Comment string
68}
69
70// AddTLSResults adds or merges all tls results for delivering to a policy domain,
71// on its UTC day to a recipient domain to the database. Results may cause multiple
72// separate reports to be sent.
73func AddTLSResults(ctx context.Context, results []TLSResult) error {
74 now := time.Now()
75
76 err := ResultDB.Write(ctx, func(tx *bstore.Tx) error {
77 for _, result := range results {
78 // Ensure all slices are non-nil. We do this now so all readers will marshal to
79 // compliant with the JSON schema. And also for consistent equality checks when
80 // merging policies created in different places.
81 for i, r := range result.Results {
82 if r.Policy.String == nil {
83 r.Policy.String = []string{}
84 }
85 if r.Policy.MXHost == nil {
86 r.Policy.MXHost = []string{}
87 }
88 if r.FailureDetails == nil {
89 r.FailureDetails = []tlsrpt.FailureDetails{}
90 }
91 result.Results[i] = r
92 }
93
94 q := bstore.QueryTx[TLSResult](tx)
95 q.FilterNonzero(TLSResult{PolicyDomain: result.PolicyDomain, DayUTC: result.DayUTC, RecipientDomain: result.RecipientDomain})
96 r, err := q.Get()
97 if err == bstore.ErrAbsent {
98 result.ID = 0
99 if err := tx.Insert(&result); err != nil {
100 return fmt.Errorf("insert: %w", err)
101 }
102 continue
103 } else if err != nil {
104 return err
105 }
106
107 report := tlsrpt.Report{Policies: r.Results}
108 report.Merge(result.Results...)
109 r.Results = report.Policies
110
111 r.IsHost = result.IsHost
112 if result.SendReport {
113 r.SendReport = true
114 }
115 r.Updated = now
116 if err := tx.Update(&r); err != nil {
117 return fmt.Errorf("update: %w", err)
118 }
119 }
120 return nil
121 })
122 return err
123}
124
125// Results returns all TLS results in the database, for all policy domains each
126// with potentially multiple days. Sorted by RecipientDomain and day.
127func Results(ctx context.Context) ([]TLSResult, error) {
128 return bstore.QueryDB[TLSResult](ctx, ResultDB).SortAsc("PolicyDomain", "DayUTC", "RecipientDomain").List()
129}
130
131// ResultsDomain returns all TLSResults for a policy domain, potentially for
132// multiple days.
133func ResultsPolicyDomain(ctx context.Context, policyDomain dns.Domain) ([]TLSResult, error) {
134 return bstore.QueryDB[TLSResult](ctx, ResultDB).FilterNonzero(TLSResult{PolicyDomain: policyDomain.Name()}).SortAsc("DayUTC", "RecipientDomain").List()
135}
136
137// ResultsRecipientDomain returns all TLSResults for a recipient domain,
138// potentially for multiple days.
139func ResultsRecipientDomain(ctx context.Context, recipientDomain dns.Domain) ([]TLSResult, error) {
140 return bstore.QueryDB[TLSResult](ctx, ResultDB).FilterNonzero(TLSResult{RecipientDomain: recipientDomain.Name()}).SortAsc("DayUTC", "PolicyDomain").List()
141}
142
143// RemoveResultsPolicyDomain removes all TLSResults for the policy domain on the
144// day from the database.
145func RemoveResultsPolicyDomain(ctx context.Context, policyDomain dns.Domain, dayUTC string) error {
146 _, err := bstore.QueryDB[TLSResult](ctx, ResultDB).FilterNonzero(TLSResult{PolicyDomain: policyDomain.Name(), DayUTC: dayUTC}).Delete()
147 return err
148}
149
150// RemoveResultsRecipientDomain removes all TLSResults for the recipient domain on
151// the day from the database.
152func RemoveResultsRecipientDomain(ctx context.Context, recipientDomain dns.Domain, dayUTC string) error {
153 _, err := bstore.QueryDB[TLSResult](ctx, ResultDB).FilterNonzero(TLSResult{RecipientDomain: recipientDomain.Name(), DayUTC: dayUTC}).Delete()
154 return err
155}
156
157// SuppressAdd adds an address to the suppress list.
158func SuppressAdd(ctx context.Context, ba *SuppressAddress) error {
159 return ResultDB.Insert(ctx, ba)
160}
161
162// SuppressList returns all reporting addresses on the suppress list.
163func SuppressList(ctx context.Context) ([]SuppressAddress, error) {
164 return bstore.QueryDB[SuppressAddress](ctx, ResultDB).SortDesc("ID").List()
165}
166
167// SuppressRemove removes a reporting address record from the suppress list.
168func SuppressRemove(ctx context.Context, id int64) error {
169 return ResultDB.Delete(ctx, &SuppressAddress{ID: id})
170}
171
172// SuppressUpdate updates the until field of a reporting address record.
173func SuppressUpdate(ctx context.Context, id int64, until time.Time) error {
174 ba := SuppressAddress{ID: id}
175 err := ResultDB.Get(ctx, &ba)
176 if err != nil {
177 return err
178 }
179 ba.Until = until
180 return ResultDB.Update(ctx, &ba)
181}
182