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