1package tlsrptsend
2
3import (
4 "context"
5 "fmt"
6 "io"
7 "os"
8 "path/filepath"
9 "reflect"
10 "sync"
11 "testing"
12 "time"
13
14 "github.com/mjl-/bstore"
15
16 "github.com/mjl-/mox/dns"
17 "github.com/mjl-/mox/mlog"
18 "github.com/mjl-/mox/mox-"
19 "github.com/mjl-/mox/moxio"
20 "github.com/mjl-/mox/queue"
21 "github.com/mjl-/mox/tlsrpt"
22 "github.com/mjl-/mox/tlsrptdb"
23)
24
25var ctxbg = context.Background()
26
27func tcheckf(t *testing.T, err error, format string, args ...any) {
28 t.Helper()
29 if err != nil {
30 t.Fatalf("%s: %s", fmt.Sprintf(format, args...), err)
31 }
32}
33
34func tcompare(t *testing.T, got, expect any) {
35 t.Helper()
36 if !reflect.DeepEqual(got, expect) {
37 t.Fatalf("got:\n%v\nexpected:\n%v", got, expect)
38 }
39}
40
41func TestSendReports(t *testing.T) {
42 os.RemoveAll("../testdata/tlsrptsend/data")
43 mox.Context = ctxbg
44 mox.ConfigStaticPath = filepath.FromSlash("../testdata/tlsrptsend/mox.conf")
45 mox.MustLoadConfig(true, false)
46
47 err := tlsrptdb.Init()
48 tcheckf(t, err, "init database")
49 defer tlsrptdb.Close()
50
51 db := tlsrptdb.ResultDB
52
53 resolver := dns.MockResolver{
54 TXT: map[string][]string{
55 "_smtp._tls.xn--74h.example.": {
56 "v=TLSRPTv1; rua=mailto:tls-reports@xn--74h.example,https://ignored.example/",
57 },
58 "_smtp._tls.mailhost.xn--74h.example.": {
59 "v=TLSRPTv1; rua=mailto:tls-reports1@mailhost.xn--74h.example,mailto:tls-reports2@mailhost.xn--74h.example; rua=mailto:tls-reports3@mailhost.xn--74h.example",
60 },
61 "_smtp._tls.noreport.example.": {
62 "v=TLSRPTv1; rua=mailto:tls-reports@noreport.example",
63 },
64 "_smtp._tls.mailhost.norua.example.": {
65 "v=TLSRPTv1;",
66 },
67 },
68 }
69
70 endUTC := midnightUTC(time.Now())
71 dayUTC := endUTC.Add(-12 * time.Hour).Format("20060102")
72
73 tlsResults := []tlsrptdb.TLSResult{
74 // For report1 below.
75 {
76 PolicyDomain: "☺.example",
77 DayUTC: dayUTC,
78 RecipientDomain: "☺.example",
79 IsHost: false,
80 SendReport: true,
81 Results: []tlsrpt.Result{
82 {
83 Policy: tlsrpt.ResultPolicy{
84 Type: tlsrpt.STS,
85 Domain: "xn--74h.example",
86 String: []string{"... mtasts policy ..."},
87 MXHost: []string{"*.xn--74h.example"},
88 },
89 Summary: tlsrpt.Summary{
90 TotalSuccessfulSessionCount: 10,
91 TotalFailureSessionCount: 3,
92 },
93 FailureDetails: []tlsrpt.FailureDetails{
94 {
95 ResultType: tlsrpt.ResultCertificateExpired,
96 SendingMTAIP: "1.2.3.4",
97 ReceivingMXHostname: "mailhost.xn--74h.example",
98 ReceivingMXHelo: "mailhost.xn--74h.example",
99 ReceivingIP: "4.3.2.1",
100 FailedSessionCount: 3,
101 },
102 },
103 },
104 },
105 },
106
107 // For report2 below.
108 {
109 PolicyDomain: "mailhost.☺.example",
110 DayUTC: dayUTC,
111 RecipientDomain: "☺.example",
112 IsHost: true,
113 SendReport: false, // Would be ignored if on its own, but we have another result for this policy domain.
114 Results: []tlsrpt.Result{
115 {
116 Policy: tlsrpt.ResultPolicy{
117 Type: tlsrpt.TLSA,
118 Domain: "mailhost.xn--74h.example",
119 String: []string{"... tlsa record ..."},
120 },
121 Summary: tlsrpt.Summary{
122 TotalSuccessfulSessionCount: 10,
123 TotalFailureSessionCount: 1,
124 },
125 FailureDetails: []tlsrpt.FailureDetails{
126 {
127 ResultType: tlsrpt.ResultValidationFailure,
128 SendingMTAIP: "1.2.3.4",
129 ReceivingMXHostname: "mailhost.xn--74h.example",
130 ReceivingMXHelo: "mailhost.xn--74h.example",
131 ReceivingIP: "4.3.2.1",
132 FailedSessionCount: 1,
133 FailureReasonCode: "dns-extended-error-7-signature-expired",
134 },
135 },
136 },
137 },
138 },
139 {
140 PolicyDomain: "mailhost.☺.example",
141 DayUTC: dayUTC,
142 RecipientDomain: "sharedsender.example",
143 IsHost: true,
144 SendReport: true, // Causes previous result to be included in this report.
145 Results: []tlsrpt.Result{
146 {
147 Policy: tlsrpt.ResultPolicy{
148 Type: tlsrpt.TLSA,
149 Domain: "mailhost.xn--74h.example",
150 String: []string{"... tlsa record ..."},
151 },
152 Summary: tlsrpt.Summary{
153 TotalSuccessfulSessionCount: 10,
154 TotalFailureSessionCount: 1,
155 },
156 FailureDetails: []tlsrpt.FailureDetails{
157 {
158 ResultType: tlsrpt.ResultValidationFailure,
159 SendingMTAIP: "1.2.3.4",
160 ReceivingMXHostname: "mailhost.xn--74h.example",
161 ReceivingMXHelo: "mailhost.xn--74h.example",
162 ReceivingIP: "4.3.2.1",
163 FailedSessionCount: 1,
164 FailureReasonCode: "dns-extended-error-7-signature-expired",
165 },
166 },
167 },
168 },
169 },
170
171 // No report due to SendReport false.
172 {
173 PolicyDomain: "mailhost.noreport.example",
174 DayUTC: dayUTC,
175 RecipientDomain: "noreport.example",
176 IsHost: true,
177 SendReport: false, // No report.
178 Results: []tlsrpt.Result{
179 {
180 Policy: tlsrpt.ResultPolicy{
181 Type: tlsrpt.NoPolicyFound,
182 Domain: "mailhost.noreport.example",
183 },
184 Summary: tlsrpt.Summary{
185 TotalSuccessfulSessionCount: 2,
186 TotalFailureSessionCount: 1,
187 },
188 },
189 },
190 },
191
192 // No report due to no mailto rua.
193 {
194 PolicyDomain: "mailhost.norua.example",
195 DayUTC: dayUTC,
196 RecipientDomain: "norua.example",
197 IsHost: true,
198 SendReport: false, // No report.
199 Results: []tlsrpt.Result{
200 {
201 Policy: tlsrpt.ResultPolicy{
202 Type: tlsrpt.NoPolicyFound,
203 Domain: "mailhost.norua.example",
204 },
205 Summary: tlsrpt.Summary{
206 TotalSuccessfulSessionCount: 2,
207 TotalFailureSessionCount: 1,
208 },
209 },
210 },
211 },
212
213 // No report due to no TLSRPT record.
214 {
215 PolicyDomain: "mailhost.notlsrpt.example",
216 DayUTC: dayUTC,
217 RecipientDomain: "notlsrpt.example",
218 IsHost: true,
219 SendReport: true,
220 Results: []tlsrpt.Result{
221 {
222 Policy: tlsrpt.ResultPolicy{
223 Type: tlsrpt.NoPolicyFound,
224 Domain: "mailhost.notlsrpt.example",
225 },
226 Summary: tlsrpt.Summary{
227 TotalSuccessfulSessionCount: 2,
228 TotalFailureSessionCount: 1,
229 },
230 },
231 },
232 },
233 }
234
235 report1 := tlsrpt.Report{
236 OrganizationName: "mox.example",
237 DateRange: tlsrpt.TLSRPTDateRange{
238 Start: endUTC.Add(-24 * time.Hour),
239 End: endUTC.Add(-time.Second),
240 },
241 ContactInfo: "postmaster@mox.example",
242 ReportID: endUTC.Add(-12*time.Hour).Format("20060102") + ".xn--74h.example@mox.example",
243 Policies: []tlsrpt.Result{
244 {
245 Policy: tlsrpt.ResultPolicy{
246 Type: tlsrpt.STS,
247 Domain: "xn--74h.example",
248 String: []string{"... mtasts policy ..."},
249 MXHost: []string{"*.xn--74h.example"},
250 },
251 Summary: tlsrpt.Summary{
252 TotalSuccessfulSessionCount: 10,
253 TotalFailureSessionCount: 3,
254 },
255 FailureDetails: []tlsrpt.FailureDetails{
256 {
257 ResultType: tlsrpt.ResultCertificateExpired,
258 SendingMTAIP: "1.2.3.4",
259 ReceivingMXHostname: "mailhost.xn--74h.example",
260 ReceivingMXHelo: "mailhost.xn--74h.example",
261 ReceivingIP: "4.3.2.1",
262 FailedSessionCount: 3,
263 },
264 },
265 },
266 // Includes reports about MX target, for DANE policies.
267 {
268 Policy: tlsrpt.ResultPolicy{
269 Type: tlsrpt.TLSA,
270 Domain: "mailhost.xn--74h.example",
271 String: []string{"... tlsa record ..."},
272 },
273 Summary: tlsrpt.Summary{
274 TotalSuccessfulSessionCount: 10,
275 TotalFailureSessionCount: 1,
276 },
277 FailureDetails: []tlsrpt.FailureDetails{
278 {
279 ResultType: tlsrpt.ResultValidationFailure,
280 SendingMTAIP: "1.2.3.4",
281 ReceivingMXHostname: "mailhost.xn--74h.example",
282 ReceivingMXHelo: "mailhost.xn--74h.example",
283 ReceivingIP: "4.3.2.1",
284 FailedSessionCount: 1,
285 FailureReasonCode: "dns-extended-error-7-signature-expired",
286 },
287 },
288 },
289 },
290 }
291 report2 := tlsrpt.Report{
292 OrganizationName: "mox.example",
293 DateRange: tlsrpt.TLSRPTDateRange{
294 Start: endUTC.Add(-24 * time.Hour),
295 End: endUTC.Add(-time.Second),
296 },
297 ContactInfo: "postmaster@mox.example",
298 ReportID: endUTC.Add(-12*time.Hour).Format("20060102") + ".mailhost.xn--74h.example@mox.example",
299 Policies: []tlsrpt.Result{
300 // The MX target policies are per-recipient domain, so the MX operator can see the
301 // affected recipient domains.
302 {
303 Policy: tlsrpt.ResultPolicy{
304 Type: tlsrpt.TLSA,
305 Domain: "sharedsender.example", // Recipient domain.
306 String: []string{"... tlsa record ..."},
307 MXHost: []string{"mailhost.xn--74h.example"}, // Original policy domain.
308 },
309 Summary: tlsrpt.Summary{
310 TotalSuccessfulSessionCount: 10,
311 TotalFailureSessionCount: 1,
312 },
313 FailureDetails: []tlsrpt.FailureDetails{
314 {
315 ResultType: tlsrpt.ResultValidationFailure,
316 SendingMTAIP: "1.2.3.4",
317 ReceivingMXHostname: "mailhost.xn--74h.example",
318 ReceivingMXHelo: "mailhost.xn--74h.example",
319 ReceivingIP: "4.3.2.1",
320 FailedSessionCount: 1,
321 FailureReasonCode: "dns-extended-error-7-signature-expired",
322 },
323 },
324 },
325 {
326 Policy: tlsrpt.ResultPolicy{
327 Type: tlsrpt.TLSA,
328 Domain: "xn--74h.example", // Recipient domain.
329 String: []string{"... tlsa record ..."},
330 MXHost: []string{"mailhost.xn--74h.example"}, // Original policy domain.
331 },
332 Summary: tlsrpt.Summary{
333 TotalSuccessfulSessionCount: 10,
334 TotalFailureSessionCount: 1,
335 },
336 FailureDetails: []tlsrpt.FailureDetails{
337 {
338 ResultType: tlsrpt.ResultValidationFailure,
339 SendingMTAIP: "1.2.3.4",
340 ReceivingMXHostname: "mailhost.xn--74h.example",
341 ReceivingMXHelo: "mailhost.xn--74h.example",
342 ReceivingIP: "4.3.2.1",
343 FailedSessionCount: 1,
344 FailureReasonCode: "dns-extended-error-7-signature-expired",
345 },
346 },
347 },
348 },
349 }
350 report3 := tlsrpt.Report{
351 OrganizationName: "mox.example",
352 DateRange: tlsrpt.TLSRPTDateRange{
353 Start: endUTC.Add(-24 * time.Hour),
354 End: endUTC.Add(-time.Second),
355 },
356 ContactInfo: "postmaster@mox.example",
357 ReportID: endUTC.Add(-12*time.Hour).Format("20060102") + ".mailhost.xn--74h.example@mox.example",
358 Policies: []tlsrpt.Result{
359 // The MX target policies are per-recipient domain, so the MX operator can see the
360 // affected recipient domains.
361 {
362 Policy: tlsrpt.ResultPolicy{
363 Type: tlsrpt.TLSA,
364 Domain: "sharedsender.example", // Recipient domain.
365 String: []string{"... tlsa record ..."},
366 MXHost: []string{"mailhost.xn--74h.example"}, // Original policy domain.
367 },
368 Summary: tlsrpt.Summary{
369 TotalSuccessfulSessionCount: 10,
370 TotalFailureSessionCount: 1,
371 },
372 FailureDetails: []tlsrpt.FailureDetails{
373 {
374 ResultType: tlsrpt.ResultValidationFailure,
375 SendingMTAIP: "1.2.3.4",
376 ReceivingMXHostname: "mailhost.xn--74h.example",
377 ReceivingMXHelo: "mailhost.xn--74h.example",
378 ReceivingIP: "4.3.2.1",
379 FailedSessionCount: 1,
380 FailureReasonCode: "dns-extended-error-7-signature-expired",
381 },
382 },
383 },
384 },
385 }
386
387 // Set a timeUntil that we steplock and that causes the actual sleep to return
388 // immediately when we want to.
389 wait := make(chan struct{})
390 step := make(chan time.Duration)
391 jitteredTimeUntil = func(_ time.Time) time.Duration {
392 wait <- struct{}{}
393 return <-step
394 }
395
396 sleepBetween = func(ctx context.Context, d time.Duration) (ok bool) { return true }
397
398 test := func(results []tlsrptdb.TLSResult, expReports map[string][]tlsrpt.Report) {
399 t.Helper()
400
401 mox.Shutdown, mox.ShutdownCancel = context.WithCancel(ctxbg)
402
403 for _, r := range results {
404 err := db.Insert(ctxbg, &r)
405 tcheckf(t, err, "inserting tlsresult")
406 }
407
408 haveReports := map[string][]tlsrpt.Report{}
409
410 var mutex sync.Mutex
411
412 var index int
413 queueAdd = func(ctx context.Context, log mlog.Log, senderAccount string, msgFile *os.File, qml ...queue.Msg) error {
414 if len(qml) != 1 {
415 return fmt.Errorf("queued %d messages, expect 1", len(qml))
416 }
417
418 mutex.Lock()
419 defer mutex.Unlock()
420
421 // Read message file. Also write copy to disk for inspection.
422 buf, err := io.ReadAll(&moxio.AtReader{R: msgFile})
423 tcheckf(t, err, "read report message")
424 p := fmt.Sprintf("../testdata/tlsrptsend/data/report%d.eml", index)
425 index++
426 err = os.WriteFile(p, append(append([]byte{}, qml[0].MsgPrefix...), buf...), 0600)
427 tcheckf(t, err, "write report message")
428
429 reportJSON, err := tlsrpt.ParseMessage(log.Logger, msgFile)
430 tcheckf(t, err, "parsing generated report message")
431
432 addr := qml[0].Recipient().String()
433 haveReports[addr] = append(haveReports[addr], reportJSON.Convert())
434
435 return nil
436 }
437
438 Start(resolver)
439 // Run first loop.
440 <-wait
441 step <- 0
442 <-wait
443
444 tcompare(t, haveReports, expReports)
445
446 // Second loop. Evaluations cleaned, should not result in report messages.
447 haveReports = map[string][]tlsrpt.Report{}
448 step <- 0
449 <-wait
450 tcompare(t, haveReports, map[string][]tlsrpt.Report{})
451
452 // Caus Start to stop.
453 mox.ShutdownCancel()
454 step <- time.Minute
455
456 leftover, err := bstore.QueryDB[tlsrptdb.TLSResult](ctxbg, db).List()
457 tcheckf(t, err, "querying database")
458 if len(leftover) != 0 {
459 t.Fatalf("leftover results in database after sending reports: %v", leftover)
460 }
461 _, err = bstore.QueryDB[tlsrptdb.TLSResult](ctxbg, db).Delete()
462 tcheckf(t, err, "cleaning from database")
463 }
464
465 // Multiple results, some are combined into a single report, another result
466 // generates a separate report to multiple rua's, and the last don't send a report.
467 test(tlsResults, map[string][]tlsrpt.Report{
468 "tls-reports@xn--74h.example": {report1},
469 "tls-reports1@mailhost.xn--74h.example": {report2},
470 "tls-reports2@mailhost.xn--74h.example": {report2},
471 "tls-reports3@mailhost.xn--74h.example": {report2},
472 })
473
474 // If MX target has same reporting addresses as recipient domain, only recipient
475 // domain should get a report.
476 resolver.TXT["_smtp._tls.mailhost.xn--74h.example."] = []string{"v=TLSRPTv1; rua=mailto:tls-reports@xn--74h.example"}
477 test(tlsResults[:2], map[string][]tlsrpt.Report{
478 "tls-reports@xn--74h.example": {report1},
479 })
480
481 resolver.TXT["_smtp._tls.sharedsender.example."] = []string{"v=TLSRPTv1; rua=mailto:tls-reports@xn--74h.example"}
482 test(tlsResults, map[string][]tlsrpt.Report{
483 "tls-reports@xn--74h.example": {report1, report3},
484 })
485
486 // Suppressed addresses don't get a report.
487 resolver.TXT["_smtp._tls.mailhost.xn--74h.example."] = []string{"v=TLSRPTv1; rua=mailto:tls-reports1@mailhost.xn--74h.example,mailto:tls-reports2@mailhost.xn--74h.example; rua=mailto:tls-reports3@mailhost.xn--74h.example"}
488 db.Insert(ctxbg,
489 &tlsrptdb.SuppressAddress{ReportingAddress: "tls-reports@xn--74h.example", Until: time.Now().Add(-time.Minute)}, // Expired, so ignored.
490 &tlsrptdb.SuppressAddress{ReportingAddress: "tls-reports1@mailhost.xn--74h.example", Until: time.Now().Add(time.Minute)}, // Still valid.
491 &tlsrptdb.SuppressAddress{ReportingAddress: "tls-reports3@mailhost.xn--74h.example", Until: time.Now().Add(31 * 24 * time.Hour)}, // Still valid.
492 )
493 test(tlsResults, map[string][]tlsrpt.Report{
494 "tls-reports@xn--74h.example": {report1},
495 "tls-reports2@mailhost.xn--74h.example": {report2},
496 })
497
498 // Make reports success-only, ensuring we don't get a report anymore.
499 for i := range tlsResults {
500 for j := range tlsResults[i].Results {
501 tlsResults[i].Results[j].Summary.TotalFailureSessionCount = 0
502 tlsResults[i].Results[j].FailureDetails = nil
503 }
504 }
505 test(tlsResults, map[string][]tlsrpt.Report{})
506
507 // But when we want to send report for all-successful connections, we get reports again.
508 mox.Conf.Static.OutgoingTLSReportsForAllSuccess = true
509 for _, report := range []*tlsrpt.Report{&report1, &report2} {
510 for i := range report.Policies {
511 report.Policies[i].Summary.TotalFailureSessionCount = 0
512 report.Policies[i].FailureDetails = nil
513 }
514 }
515 test(tlsResults, map[string][]tlsrpt.Report{
516 "tls-reports@xn--74h.example": {report1},
517 "tls-reports2@mailhost.xn--74h.example": {report2},
518 })
519}
520