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