15 "github.com/mjl-/bstore"
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"
26var ctxbg = context.Background()
28func tcheckf(t *testing.T, err error, format string, args ...any) {
31 t.Fatalf("%s: %s", fmt.Sprintf(format, args...), err)
35func tcompare(t *testing.T, got, expect any) {
37 if !reflect.DeepEqual(got, expect) {
38 t.Fatalf("got:\n%v\nexpected:\n%v", got, expect)
42func TestSendReports(t *testing.T) {
43 mlog.SetConfig(map[string]slog.Level{"": mlog.LevelDebug})
45 os.RemoveAll("../testdata/tlsrptsend/data")
47 mox.ConfigStaticPath = filepath.FromSlash("../testdata/tlsrptsend/mox.conf")
48 mox.MustLoadConfig(true, false)
50 err := tlsrptdb.Init()
51 tcheckf(t, err, "init database")
53 db := tlsrptdb.ResultDB
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/",
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",
63 "_smtp._tls.noreport.example.": {
64 "v=TLSRPTv1; rua=mailto:tls-reports@noreport.example",
66 "_smtp._tls.mailhost.norua.example.": {
72 endUTC := midnightUTC(time.Now())
73 dayUTC := endUTC.Add(-12 * time.Hour).Format("20060102")
75 tlsResults := []tlsrptdb.TLSResult{
78 PolicyDomain: "☺.example",
80 RecipientDomain: "☺.example",
83 Results: []tlsrpt.Result{
85 Policy: tlsrpt.ResultPolicy{
87 Domain: "xn--74h.example",
88 String: []string{"... mtasts policy ..."},
89 MXHost: []string{"*.xn--74h.example"},
91 Summary: tlsrpt.Summary{
92 TotalSuccessfulSessionCount: 10,
93 TotalFailureSessionCount: 3,
95 FailureDetails: []tlsrpt.FailureDetails{
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,
109 // For report2 below.
111 PolicyDomain: "mailhost.☺.example",
113 RecipientDomain: "☺.example",
115 SendReport: false, // Would be ignored if on its own, but we have another result for this policy domain.
116 Results: []tlsrpt.Result{
118 Policy: tlsrpt.ResultPolicy{
120 Domain: "mailhost.xn--74h.example",
121 String: []string{"... tlsa record ..."},
123 Summary: tlsrpt.Summary{
124 TotalSuccessfulSessionCount: 10,
125 TotalFailureSessionCount: 1,
127 FailureDetails: []tlsrpt.FailureDetails{
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",
142 PolicyDomain: "mailhost.☺.example",
144 RecipientDomain: "sharedsender.example",
146 SendReport: true, // Causes previous result to be included in this report.
147 Results: []tlsrpt.Result{
149 Policy: tlsrpt.ResultPolicy{
151 Domain: "mailhost.xn--74h.example",
152 String: []string{"... tlsa record ..."},
154 Summary: tlsrpt.Summary{
155 TotalSuccessfulSessionCount: 10,
156 TotalFailureSessionCount: 1,
158 FailureDetails: []tlsrpt.FailureDetails{
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",
173 // No report due to SendReport false.
175 PolicyDomain: "mailhost.noreport.example",
177 RecipientDomain: "noreport.example",
179 SendReport: false, // No report.
180 Results: []tlsrpt.Result{
182 Policy: tlsrpt.ResultPolicy{
183 Type: tlsrpt.NoPolicyFound,
184 Domain: "mailhost.noreport.example",
186 Summary: tlsrpt.Summary{
187 TotalSuccessfulSessionCount: 2,
188 TotalFailureSessionCount: 1,
194 // No report due to no mailto rua.
196 PolicyDomain: "mailhost.norua.example",
198 RecipientDomain: "norua.example",
200 SendReport: false, // No report.
201 Results: []tlsrpt.Result{
203 Policy: tlsrpt.ResultPolicy{
204 Type: tlsrpt.NoPolicyFound,
205 Domain: "mailhost.norua.example",
207 Summary: tlsrpt.Summary{
208 TotalSuccessfulSessionCount: 2,
209 TotalFailureSessionCount: 1,
215 // No report due to no TLSRPT record.
217 PolicyDomain: "mailhost.notlsrpt.example",
219 RecipientDomain: "notlsrpt.example",
222 Results: []tlsrpt.Result{
224 Policy: tlsrpt.ResultPolicy{
225 Type: tlsrpt.NoPolicyFound,
226 Domain: "mailhost.notlsrpt.example",
228 Summary: tlsrpt.Summary{
229 TotalSuccessfulSessionCount: 2,
230 TotalFailureSessionCount: 1,
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),
243 ContactInfo: "postmaster@mox.example",
244 ReportID: endUTC.Add(-12*time.Hour).Format("20060102") + ".xn--74h.example@mox.example",
245 Policies: []tlsrpt.Result{
247 Policy: tlsrpt.ResultPolicy{
249 Domain: "xn--74h.example",
250 String: []string{"... mtasts policy ..."},
251 MXHost: []string{"*.xn--74h.example"},
253 Summary: tlsrpt.Summary{
254 TotalSuccessfulSessionCount: 10,
255 TotalFailureSessionCount: 3,
257 FailureDetails: []tlsrpt.FailureDetails{
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,
268 // Includes reports about MX target, for DANE policies.
270 Policy: tlsrpt.ResultPolicy{
272 Domain: "mailhost.xn--74h.example",
273 String: []string{"... tlsa record ..."},
275 Summary: tlsrpt.Summary{
276 TotalSuccessfulSessionCount: 10,
277 TotalFailureSessionCount: 1,
279 FailureDetails: []tlsrpt.FailureDetails{
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",
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),
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.
305 Policy: tlsrpt.ResultPolicy{
307 Domain: "sharedsender.example", // Recipient domain.
308 String: []string{"... tlsa record ..."},
309 MXHost: []string{"mailhost.xn--74h.example"}, // Original policy domain.
311 Summary: tlsrpt.Summary{
312 TotalSuccessfulSessionCount: 10,
313 TotalFailureSessionCount: 1,
315 FailureDetails: []tlsrpt.FailureDetails{
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",
328 Policy: tlsrpt.ResultPolicy{
330 Domain: "xn--74h.example", // Recipient domain.
331 String: []string{"... tlsa record ..."},
332 MXHost: []string{"mailhost.xn--74h.example"}, // Original policy domain.
334 Summary: tlsrpt.Summary{
335 TotalSuccessfulSessionCount: 10,
336 TotalFailureSessionCount: 1,
338 FailureDetails: []tlsrpt.FailureDetails{
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",
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),
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.
364 Policy: tlsrpt.ResultPolicy{
366 Domain: "sharedsender.example", // Recipient domain.
367 String: []string{"... tlsa record ..."},
368 MXHost: []string{"mailhost.xn--74h.example"}, // Original policy domain.
370 Summary: tlsrpt.Summary{
371 TotalSuccessfulSessionCount: 10,
372 TotalFailureSessionCount: 1,
374 FailureDetails: []tlsrpt.FailureDetails{
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",
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 {
398 sleepBetween = func(ctx context.Context, d time.Duration) (ok bool) { return true }
400 test := func(results []tlsrptdb.TLSResult, expReports map[string][]tlsrpt.Report) {
403 mox.Shutdown, mox.ShutdownCancel = context.WithCancel(ctxbg)
405 for _, r := range results {
406 err := db.Insert(ctxbg, &r)
407 tcheckf(t, err, "inserting tlsresult")
410 haveReports := map[string][]tlsrpt.Report{}
415 queueAdd = func(ctx context.Context, log mlog.Log, senderAccount string, msgFile *os.File, qml ...queue.Msg) error {
417 return fmt.Errorf("queued %d messages, expect 1", len(qml))
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)
428 err = os.WriteFile(p, append(append([]byte{}, qml[0].MsgPrefix...), buf...), 0600)
429 tcheckf(t, err, "write report message")
431 reportJSON, err := tlsrpt.ParseMessage(log.Logger, msgFile)
432 tcheckf(t, err, "parsing generated report message")
434 addr := qml[0].Recipient().String()
435 haveReports[addr] = append(haveReports[addr], reportJSON.Convert())
446 tcompare(t, haveReports, expReports)
448 // Second loop. Evaluations cleaned, should not result in report messages.
449 haveReports = map[string][]tlsrpt.Report{}
452 tcompare(t, haveReports, map[string][]tlsrpt.Report{})
454 // Caus Start to stop.
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)
463 _, err = bstore.QueryDB[tlsrptdb.TLSResult](ctxbg, db).Delete()
464 tcheckf(t, err, "cleaning from database")
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},
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},
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},
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"}
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.
495 test(tlsResults, map[string][]tlsrpt.Report{
496 "tls-reports@xn--74h.example": {report1},
497 "tls-reports2@mailhost.xn--74h.example": {report2},
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
507 test(tlsResults, map[string][]tlsrpt.Report{})
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
517 test(tlsResults, map[string][]tlsrpt.Report{
518 "tls-reports@xn--74h.example": {report1},
519 "tls-reports2@mailhost.xn--74h.example": {report2},