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