14 "github.com/mjl-/bstore"
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"
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 os.RemoveAll("../testdata/tlsrptsend/data")
45 mox.ConfigStaticPath = filepath.FromSlash("../testdata/tlsrptsend/mox.conf")
46 mox.MustLoadConfig(true, false)
48 err := tlsrptdb.Init()
49 tcheckf(t, err, "init database")
50 defer tlsrptdb.Close()
52 db := tlsrptdb.ResultDB
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/",
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",
62 "_smtp._tls.noreport.example.": {
63 "v=TLSRPTv1; rua=mailto:tls-reports@noreport.example",
65 "_smtp._tls.mailhost.norua.example.": {
71 endUTC := midnightUTC(time.Now())
72 dayUTC := endUTC.Add(-12 * time.Hour).Format("20060102")
74 tlsResults := []tlsrptdb.TLSResult{
77 PolicyDomain: "☺.example",
79 RecipientDomain: "☺.example",
82 Results: []tlsrpt.Result{
84 Policy: tlsrpt.ResultPolicy{
86 Domain: "xn--74h.example",
87 String: []string{"... mtasts policy ..."},
88 MXHost: []string{"*.xn--74h.example"},
90 Summary: tlsrpt.Summary{
91 TotalSuccessfulSessionCount: 10,
92 TotalFailureSessionCount: 3,
94 FailureDetails: []tlsrpt.FailureDetails{
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,
108 // For report2 below.
110 PolicyDomain: "mailhost.☺.example",
112 RecipientDomain: "☺.example",
114 SendReport: false, // Would be ignored if on its own, but we have another result for this policy domain.
115 Results: []tlsrpt.Result{
117 Policy: tlsrpt.ResultPolicy{
119 Domain: "mailhost.xn--74h.example",
120 String: []string{"... tlsa record ..."},
122 Summary: tlsrpt.Summary{
123 TotalSuccessfulSessionCount: 10,
124 TotalFailureSessionCount: 1,
126 FailureDetails: []tlsrpt.FailureDetails{
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",
141 PolicyDomain: "mailhost.☺.example",
143 RecipientDomain: "sharedsender.example",
145 SendReport: true, // Causes previous result to be included in this report.
146 Results: []tlsrpt.Result{
148 Policy: tlsrpt.ResultPolicy{
150 Domain: "mailhost.xn--74h.example",
151 String: []string{"... tlsa record ..."},
153 Summary: tlsrpt.Summary{
154 TotalSuccessfulSessionCount: 10,
155 TotalFailureSessionCount: 1,
157 FailureDetails: []tlsrpt.FailureDetails{
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",
172 // No report due to SendReport false.
174 PolicyDomain: "mailhost.noreport.example",
176 RecipientDomain: "noreport.example",
178 SendReport: false, // No report.
179 Results: []tlsrpt.Result{
181 Policy: tlsrpt.ResultPolicy{
182 Type: tlsrpt.NoPolicyFound,
183 Domain: "mailhost.noreport.example",
185 Summary: tlsrpt.Summary{
186 TotalSuccessfulSessionCount: 2,
187 TotalFailureSessionCount: 1,
193 // No report due to no mailto rua.
195 PolicyDomain: "mailhost.norua.example",
197 RecipientDomain: "norua.example",
199 SendReport: false, // No report.
200 Results: []tlsrpt.Result{
202 Policy: tlsrpt.ResultPolicy{
203 Type: tlsrpt.NoPolicyFound,
204 Domain: "mailhost.norua.example",
206 Summary: tlsrpt.Summary{
207 TotalSuccessfulSessionCount: 2,
208 TotalFailureSessionCount: 1,
214 // No report due to no TLSRPT record.
216 PolicyDomain: "mailhost.notlsrpt.example",
218 RecipientDomain: "notlsrpt.example",
221 Results: []tlsrpt.Result{
223 Policy: tlsrpt.ResultPolicy{
224 Type: tlsrpt.NoPolicyFound,
225 Domain: "mailhost.notlsrpt.example",
227 Summary: tlsrpt.Summary{
228 TotalSuccessfulSessionCount: 2,
229 TotalFailureSessionCount: 1,
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),
242 ContactInfo: "postmaster@mox.example",
243 ReportID: endUTC.Add(-12*time.Hour).Format("20060102") + ".xn--74h.example@mox.example",
244 Policies: []tlsrpt.Result{
246 Policy: tlsrpt.ResultPolicy{
248 Domain: "xn--74h.example",
249 String: []string{"... mtasts policy ..."},
250 MXHost: []string{"*.xn--74h.example"},
252 Summary: tlsrpt.Summary{
253 TotalSuccessfulSessionCount: 10,
254 TotalFailureSessionCount: 3,
256 FailureDetails: []tlsrpt.FailureDetails{
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,
267 // Includes reports about MX target, for DANE policies.
269 Policy: tlsrpt.ResultPolicy{
271 Domain: "mailhost.xn--74h.example",
272 String: []string{"... tlsa record ..."},
274 Summary: tlsrpt.Summary{
275 TotalSuccessfulSessionCount: 10,
276 TotalFailureSessionCount: 1,
278 FailureDetails: []tlsrpt.FailureDetails{
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",
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),
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.
304 Policy: tlsrpt.ResultPolicy{
306 Domain: "sharedsender.example", // Recipient domain.
307 String: []string{"... tlsa record ..."},
308 MXHost: []string{"mailhost.xn--74h.example"}, // Original policy domain.
310 Summary: tlsrpt.Summary{
311 TotalSuccessfulSessionCount: 10,
312 TotalFailureSessionCount: 1,
314 FailureDetails: []tlsrpt.FailureDetails{
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",
327 Policy: tlsrpt.ResultPolicy{
329 Domain: "xn--74h.example", // Recipient domain.
330 String: []string{"... tlsa record ..."},
331 MXHost: []string{"mailhost.xn--74h.example"}, // Original policy domain.
333 Summary: tlsrpt.Summary{
334 TotalSuccessfulSessionCount: 10,
335 TotalFailureSessionCount: 1,
337 FailureDetails: []tlsrpt.FailureDetails{
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",
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),
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.
363 Policy: tlsrpt.ResultPolicy{
365 Domain: "sharedsender.example", // Recipient domain.
366 String: []string{"... tlsa record ..."},
367 MXHost: []string{"mailhost.xn--74h.example"}, // Original policy domain.
369 Summary: tlsrpt.Summary{
370 TotalSuccessfulSessionCount: 10,
371 TotalFailureSessionCount: 1,
373 FailureDetails: []tlsrpt.FailureDetails{
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",
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 {
397 sleepBetween = func(ctx context.Context, d time.Duration) (ok bool) { return true }
399 test := func(results []tlsrptdb.TLSResult, expReports map[string][]tlsrpt.Report) {
402 mox.Shutdown, mox.ShutdownCancel = context.WithCancel(ctxbg)
404 for _, r := range results {
405 err := db.Insert(ctxbg, &r)
406 tcheckf(t, err, "inserting tlsresult")
409 haveReports := map[string][]tlsrpt.Report{}
414 queueAdd = func(ctx context.Context, log mlog.Log, senderAccount string, msgFile *os.File, qml ...queue.Msg) error {
416 return fmt.Errorf("queued %d messages, expect 1", len(qml))
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)
427 err = os.WriteFile(p, slices.Concat(qml[0].MsgPrefix, buf), 0600)
428 tcheckf(t, err, "write report message")
430 reportJSON, err := tlsrpt.ParseMessage(log.Logger, msgFile)
431 tcheckf(t, err, "parsing generated report message")
433 addr := qml[0].Recipient().String()
434 haveReports[addr] = append(haveReports[addr], reportJSON.Convert())
445 tcompare(t, haveReports, expReports)
447 // Second loop. Evaluations cleaned, should not result in report messages.
448 haveReports = map[string][]tlsrpt.Report{}
451 tcompare(t, haveReports, map[string][]tlsrpt.Report{})
453 // Caus Start to stop.
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)
462 _, err = bstore.QueryDB[tlsrptdb.TLSResult](ctxbg, db).Delete()
463 tcheckf(t, err, "cleaning from database")
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},
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},
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},
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"}
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.
494 test(tlsResults, map[string][]tlsrpt.Report{
495 "tls-reports@xn--74h.example": {report1},
496 "tls-reports2@mailhost.xn--74h.example": {report2},
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
506 test(tlsResults, map[string][]tlsrpt.Report{})
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
516 test(tlsResults, map[string][]tlsrpt.Report{
517 "tls-reports@xn--74h.example": {report1},
518 "tls-reports2@mailhost.xn--74h.example": {report2},