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// todo: TLSRPTSuppressAddress should be named just SuppressAddress, but would clash with dmarcdb.SuppressAddress in sherpa api.
64
65// TLSRPTSuppressAddress is a reporting address for which outgoing TLS reports
66// will be suppressed for a period.
67type TLSRPTSuppressAddress struct {
68 ID int64
69 Inserted time.Time `bstore:"default now"`
70 ReportingAddress string `bstore:"unique"`
71 Until time.Time `bstore:"nonzero"`
72 Comment string
73}
74
75func resultDB(ctx context.Context) (rdb *bstore.DB, rerr error) {
76 mutex.Lock()
77 defer mutex.Unlock()
78 if ResultDB == nil {
79 p := mox.DataDirPath("tlsrptresult.db")
80 os.MkdirAll(filepath.Dir(p), 0770)
81 db, err := bstore.Open(ctx, p, &bstore.Options{Timeout: 5 * time.Second, Perm: 0660}, ResultDBTypes...)
82 if err != nil {
83 return nil, err
84 }
85 ResultDB = db
86 }
87 return ResultDB, nil
88}
89
90// AddTLSResults adds or merges all tls results for delivering to a policy domain,
91// on its UTC day to a recipient domain to the database. Results may cause multiple
92// separate reports to be sent.
93func AddTLSResults(ctx context.Context, results []TLSResult) error {
94 db, err := resultDB(ctx)
95 if err != nil {
96 return err
97 }
98
99 now := time.Now()
100
101 err = db.Write(ctx, func(tx *bstore.Tx) error {
102 for _, result := range results {
103 // Ensure all slices are non-nil. We do this now so all readers will marshal to
104 // compliant with the JSON schema. And also for consistent equality checks when
105 // merging policies created in different places.
106 for i, r := range result.Results {
107 if r.Policy.String == nil {
108 r.Policy.String = []string{}
109 }
110 if r.Policy.MXHost == nil {
111 r.Policy.MXHost = []string{}
112 }
113 if r.FailureDetails == nil {
114 r.FailureDetails = []tlsrpt.FailureDetails{}
115 }
116 result.Results[i] = r
117 }
118
119 q := bstore.QueryTx[TLSResult](tx)
120 q.FilterNonzero(TLSResult{PolicyDomain: result.PolicyDomain, DayUTC: result.DayUTC, RecipientDomain: result.RecipientDomain})
121 r, err := q.Get()
122 if err == bstore.ErrAbsent {
123 result.ID = 0
124 if err := tx.Insert(&result); err != nil {
125 return fmt.Errorf("insert: %w", err)
126 }
127 continue
128 } else if err != nil {
129 return err
130 }
131
132 report := tlsrpt.Report{Policies: r.Results}
133 report.Merge(result.Results...)
134 r.Results = report.Policies
135
136 r.IsHost = result.IsHost
137 if result.SendReport {
138 r.SendReport = true
139 }
140 r.Updated = now
141 if err := tx.Update(&r); err != nil {
142 return fmt.Errorf("update: %w", err)
143 }
144 }
145 return nil
146 })
147 return err
148}
149
150// Results returns all TLS results in the database, for all policy domains each
151// with potentially multiple days. Sorted by RecipientDomain and day.
152func Results(ctx context.Context) ([]TLSResult, error) {
153 db, err := resultDB(ctx)
154 if err != nil {
155 return nil, err
156 }
157
158 return bstore.QueryDB[TLSResult](ctx, db).SortAsc("PolicyDomain", "DayUTC", "RecipientDomain").List()
159}
160
161// ResultsDomain returns all TLSResults for a policy domain, potentially for
162// multiple days.
163func ResultsPolicyDomain(ctx context.Context, policyDomain dns.Domain) ([]TLSResult, error) {
164 db, err := resultDB(ctx)
165 if err != nil {
166 return nil, err
167 }
168
169 return bstore.QueryDB[TLSResult](ctx, db).FilterNonzero(TLSResult{PolicyDomain: policyDomain.Name()}).SortAsc("DayUTC", "RecipientDomain").List()
170}
171
172// ResultsRecipientDomain returns all TLSResults for a recipient domain,
173// potentially for multiple days.
174func ResultsRecipientDomain(ctx context.Context, recipientDomain dns.Domain) ([]TLSResult, error) {
175 db, err := resultDB(ctx)
176 if err != nil {
177 return nil, err
178 }
179
180 return bstore.QueryDB[TLSResult](ctx, db).FilterNonzero(TLSResult{RecipientDomain: recipientDomain.Name()}).SortAsc("DayUTC", "PolicyDomain").List()
181}
182
183// RemoveResultsPolicyDomain removes all TLSResults for the policy domain on the
184// day from the database.
185func RemoveResultsPolicyDomain(ctx context.Context, policyDomain dns.Domain, dayUTC string) error {
186 db, err := resultDB(ctx)
187 if err != nil {
188 return err
189 }
190
191 _, err = bstore.QueryDB[TLSResult](ctx, db).FilterNonzero(TLSResult{PolicyDomain: policyDomain.Name(), DayUTC: dayUTC}).Delete()
192 return err
193}
194
195// RemoveResultsRecipientDomain removes all TLSResults for the recipient domain on
196// the day from the database.
197func RemoveResultsRecipientDomain(ctx context.Context, recipientDomain dns.Domain, dayUTC string) error {
198 db, err := resultDB(ctx)
199 if err != nil {
200 return err
201 }
202
203 _, err = bstore.QueryDB[TLSResult](ctx, db).FilterNonzero(TLSResult{RecipientDomain: recipientDomain.Name(), DayUTC: dayUTC}).Delete()
204 return err
205}
206
207// SuppressAdd adds an address to the suppress list.
208func SuppressAdd(ctx context.Context, ba *TLSRPTSuppressAddress) error {
209 db, err := resultDB(ctx)
210 if err != nil {
211 return err
212 }
213
214 return db.Insert(ctx, ba)
215}
216
217// SuppressList returns all reporting addresses on the suppress list.
218func SuppressList(ctx context.Context) ([]TLSRPTSuppressAddress, error) {
219 db, err := resultDB(ctx)
220 if err != nil {
221 return nil, err
222 }
223
224 return bstore.QueryDB[TLSRPTSuppressAddress](ctx, db).SortDesc("ID").List()
225}
226
227// SuppressRemove removes a reporting address record from the suppress list.
228func SuppressRemove(ctx context.Context, id int64) error {
229 db, err := resultDB(ctx)
230 if err != nil {
231 return err
232 }
233
234 return db.Delete(ctx, &TLSRPTSuppressAddress{ID: id})
235}
236
237// SuppressUpdate updates the until field of a reporting address record.
238func SuppressUpdate(ctx context.Context, id int64, until time.Time) error {
239 db, err := resultDB(ctx)
240 if err != nil {
241 return err
242 }
243
244 ba := TLSRPTSuppressAddress{ID: id}
245 err = db.Get(ctx, &ba)
246 if err != nil {
247 return err
248 }
249 ba.Until = until
250 return db.Update(ctx, &ba)
251}
252