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},