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