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"
25var ctxbg = context.Background()
27func tcheckf(t *testing.T, err error, format string, args ...any) {
30 t.Fatalf("%s: %s", fmt.Sprintf(format, args...), err)
34func tcompare(t *testing.T, got, expect any) {
36 if !reflect.DeepEqual(got, expect) {
37 t.Fatalf("got:\n%v\nexpected:\n%v", got, expect)
41func TestSendReports(t *testing.T) {
42 os.RemoveAll("../testdata/tlsrptsend/data")
44 mox.ConfigStaticPath = filepath.FromSlash("../testdata/tlsrptsend/mox.conf")
45 mox.MustLoadConfig(true, false)
47 err := tlsrptdb.Init()
48 tcheckf(t, err, "init database")
49 defer tlsrptdb.Close()
51 db := tlsrptdb.ResultDB
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/",
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",
61 "_smtp._tls.noreport.example.": {
62 "v=TLSRPTv1; rua=mailto:tls-reports@noreport.example",
64 "_smtp._tls.mailhost.norua.example.": {
70 endUTC := midnightUTC(time.Now())
71 dayUTC := endUTC.Add(-12 * time.Hour).Format("20060102")
73 tlsResults := []tlsrptdb.TLSResult{
76 PolicyDomain: "☺.example",
78 RecipientDomain: "☺.example",
81 Results: []tlsrpt.Result{
83 Policy: tlsrpt.ResultPolicy{
85 Domain: "xn--74h.example",
86 String: []string{"... mtasts policy ..."},
87 MXHost: []string{"*.xn--74h.example"},
89 Summary: tlsrpt.Summary{
90 TotalSuccessfulSessionCount: 10,
91 TotalFailureSessionCount: 3,
93 FailureDetails: []tlsrpt.FailureDetails{
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,
107 // For report2 below.
109 PolicyDomain: "mailhost.☺.example",
111 RecipientDomain: "☺.example",
113 SendReport: false, // Would be ignored if on its own, but we have another result for this policy domain.
114 Results: []tlsrpt.Result{
116 Policy: tlsrpt.ResultPolicy{
118 Domain: "mailhost.xn--74h.example",
119 String: []string{"... tlsa record ..."},
121 Summary: tlsrpt.Summary{
122 TotalSuccessfulSessionCount: 10,
123 TotalFailureSessionCount: 1,
125 FailureDetails: []tlsrpt.FailureDetails{
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",
140 PolicyDomain: "mailhost.☺.example",
142 RecipientDomain: "sharedsender.example",
144 SendReport: true, // Causes previous result to be included in this report.
145 Results: []tlsrpt.Result{
147 Policy: tlsrpt.ResultPolicy{
149 Domain: "mailhost.xn--74h.example",
150 String: []string{"... tlsa record ..."},
152 Summary: tlsrpt.Summary{
153 TotalSuccessfulSessionCount: 10,
154 TotalFailureSessionCount: 1,
156 FailureDetails: []tlsrpt.FailureDetails{
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",
171 // No report due to SendReport false.
173 PolicyDomain: "mailhost.noreport.example",
175 RecipientDomain: "noreport.example",
177 SendReport: false, // No report.
178 Results: []tlsrpt.Result{
180 Policy: tlsrpt.ResultPolicy{
181 Type: tlsrpt.NoPolicyFound,
182 Domain: "mailhost.noreport.example",
184 Summary: tlsrpt.Summary{
185 TotalSuccessfulSessionCount: 2,
186 TotalFailureSessionCount: 1,
192 // No report due to no mailto rua.
194 PolicyDomain: "mailhost.norua.example",
196 RecipientDomain: "norua.example",
198 SendReport: false, // No report.
199 Results: []tlsrpt.Result{
201 Policy: tlsrpt.ResultPolicy{
202 Type: tlsrpt.NoPolicyFound,
203 Domain: "mailhost.norua.example",
205 Summary: tlsrpt.Summary{
206 TotalSuccessfulSessionCount: 2,
207 TotalFailureSessionCount: 1,
213 // No report due to no TLSRPT record.
215 PolicyDomain: "mailhost.notlsrpt.example",
217 RecipientDomain: "notlsrpt.example",
220 Results: []tlsrpt.Result{
222 Policy: tlsrpt.ResultPolicy{
223 Type: tlsrpt.NoPolicyFound,
224 Domain: "mailhost.notlsrpt.example",
226 Summary: tlsrpt.Summary{
227 TotalSuccessfulSessionCount: 2,
228 TotalFailureSessionCount: 1,
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),
241 ContactInfo: "postmaster@mox.example",
242 ReportID: endUTC.Add(-12*time.Hour).Format("20060102") + ".xn--74h.example@mox.example",
243 Policies: []tlsrpt.Result{
245 Policy: tlsrpt.ResultPolicy{
247 Domain: "xn--74h.example",
248 String: []string{"... mtasts policy ..."},
249 MXHost: []string{"*.xn--74h.example"},
251 Summary: tlsrpt.Summary{
252 TotalSuccessfulSessionCount: 10,
253 TotalFailureSessionCount: 3,
255 FailureDetails: []tlsrpt.FailureDetails{
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,
266 // Includes reports about MX target, for DANE policies.
268 Policy: tlsrpt.ResultPolicy{
270 Domain: "mailhost.xn--74h.example",
271 String: []string{"... tlsa record ..."},
273 Summary: tlsrpt.Summary{
274 TotalSuccessfulSessionCount: 10,
275 TotalFailureSessionCount: 1,
277 FailureDetails: []tlsrpt.FailureDetails{
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",
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),
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.
303 Policy: tlsrpt.ResultPolicy{
305 Domain: "sharedsender.example", // Recipient domain.
306 String: []string{"... tlsa record ..."},
307 MXHost: []string{"mailhost.xn--74h.example"}, // Original policy domain.
309 Summary: tlsrpt.Summary{
310 TotalSuccessfulSessionCount: 10,
311 TotalFailureSessionCount: 1,
313 FailureDetails: []tlsrpt.FailureDetails{
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",
326 Policy: tlsrpt.ResultPolicy{
328 Domain: "xn--74h.example", // Recipient domain.
329 String: []string{"... tlsa record ..."},
330 MXHost: []string{"mailhost.xn--74h.example"}, // Original policy domain.
332 Summary: tlsrpt.Summary{
333 TotalSuccessfulSessionCount: 10,
334 TotalFailureSessionCount: 1,
336 FailureDetails: []tlsrpt.FailureDetails{
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",
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),
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.
362 Policy: tlsrpt.ResultPolicy{
364 Domain: "sharedsender.example", // Recipient domain.
365 String: []string{"... tlsa record ..."},
366 MXHost: []string{"mailhost.xn--74h.example"}, // Original policy domain.
368 Summary: tlsrpt.Summary{
369 TotalSuccessfulSessionCount: 10,
370 TotalFailureSessionCount: 1,
372 FailureDetails: []tlsrpt.FailureDetails{
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",
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 {
396 sleepBetween = func(ctx context.Context, d time.Duration) (ok bool) { return true }
398 test := func(results []tlsrptdb.TLSResult, expReports map[string][]tlsrpt.Report) {
401 mox.Shutdown, mox.ShutdownCancel = context.WithCancel(ctxbg)
403 for _, r := range results {
404 err := db.Insert(ctxbg, &r)
405 tcheckf(t, err, "inserting tlsresult")
408 haveReports := map[string][]tlsrpt.Report{}
413 queueAdd = func(ctx context.Context, log mlog.Log, senderAccount string, msgFile *os.File, qml ...queue.Msg) error {
415 return fmt.Errorf("queued %d messages, expect 1", len(qml))
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)
426 err = os.WriteFile(p, append(append([]byte{}, qml[0].MsgPrefix...), buf...), 0600)
427 tcheckf(t, err, "write report message")
429 reportJSON, err := tlsrpt.ParseMessage(log.Logger, msgFile)
430 tcheckf(t, err, "parsing generated report message")
432 addr := qml[0].Recipient().String()
433 haveReports[addr] = append(haveReports[addr], reportJSON.Convert())
444 tcompare(t, haveReports, expReports)
446 // Second loop. Evaluations cleaned, should not result in report messages.
447 haveReports = map[string][]tlsrpt.Report{}
450 tcompare(t, haveReports, map[string][]tlsrpt.Report{})
452 // Caus Start to stop.
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)
461 _, err = bstore.QueryDB[tlsrptdb.TLSResult](ctxbg, db).Delete()
462 tcheckf(t, err, "cleaning from database")
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},
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},
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},
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"}
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.
493 test(tlsResults, map[string][]tlsrpt.Report{
494 "tls-reports@xn--74h.example": {report1},
495 "tls-reports2@mailhost.xn--74h.example": {report2},
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
505 test(tlsResults, map[string][]tlsrpt.Report{})
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
515 test(tlsResults, map[string][]tlsrpt.Report{
516 "tls-reports@xn--74h.example": {report1},
517 "tls-reports2@mailhost.xn--74h.example": {report2},