10 "github.com/mjl-/bstore"
12 "github.com/mjl-/mox/dns"
13 "github.com/mjl-/mox/mox-"
14 "github.com/mjl-/mox/tlsrpt"
17// TLSResult is stored in the database to track TLS results per policy domain, day
18// and recipient domain. These records will be included in TLS reports.
19type TLSResult struct {
22 // Domain potentially with TLSRPT DNS record, with addresses that will receive
23 // reports. Either a recipient domain (for MTA-STS policies) or an (MX) host (for
24 // DANE policies). Unicode.
25 PolicyDomain string `bstore:"unique PolicyDomain+DayUTC+RecipientDomain,nonzero"`
27 // DayUTC is of the form yyyymmdd.
28 DayUTC string `bstore:"nonzero"`
31 // Reports are sent per recipient domain and per MX host. For reports to a
32 // recipient domain, we type send a result for MTA-STS and one or more MX host
33 // (DANE) results. Unicode.
34 RecipientDomain string `bstore:"index,nonzero"`
36 Created time.Time `bstore:"default now"`
37 Updated time.Time `bstore:"default now"`
39 IsHost bool // Result is for MX host (DANE), not recipient domain (MTA-STS).
41 // Whether to send a report. TLS results for delivering messages with TLS reports
42 // will be recorded, but will not cause a report to be sent.
44 //
../rfc/8460:318 says we should not include TLS results for sending a TLS report,
45 // but presumably that's to prevent mail servers sending a report every day once
48 // Set after sending to recipient domain, before sending results to policy domain
49 // (after which the record is removed).
50 SentToRecipientDomain bool
51 // Reporting addresses from the recipient domain TLSRPT record, not necessarily
52 // those we sent to (e.g. due to failure). Used to leave results to MX target
53 // (DANE) policy domains out that were already sent in the report to the recipient
54 // domain, so we don't report twice.
55 RecipientDomainReportingAddresses []string
56 // Set after sending report to policy domain.
57 SentToPolicyDomain bool
59 // Results is updated for each TLS attempt.
60 Results []tlsrpt.Result
63// todo: TLSRPTSuppressAddress should be named just SuppressAddress, but would clash with dmarcdb.SuppressAddress in sherpa api.
65// TLSRPTSuppressAddress is a reporting address for which outgoing TLS reports
66// will be suppressed for a period.
67type TLSRPTSuppressAddress struct {
69 Inserted time.Time `bstore:"default now"`
70 ReportingAddress string `bstore:"unique"`
71 Until time.Time `bstore:"nonzero"`
75func resultDB(ctx context.Context) (rdb *bstore.DB, rerr error) {
79 p := mox.DataDirPath("tlsrptresult.db")
80 os.MkdirAll(filepath.Dir(p), 0770)
81 db, err := bstore.Open(ctx, p, &bstore.Options{Timeout: 5 * time.Second, Perm: 0660}, ResultDBTypes...)
90// AddTLSResults adds or merges all tls results for delivering to a policy domain,
91// on its UTC day to a recipient domain to the database. Results may cause multiple
92// separate reports to be sent.
93func AddTLSResults(ctx context.Context, results []TLSResult) error {
94 db, err := resultDB(ctx)
101 err = db.Write(ctx, func(tx *bstore.Tx) error {
102 for _, result := range results {
103 // Ensure all slices are non-nil. We do this now so all readers will marshal to
104 // compliant with the JSON schema. And also for consistent equality checks when
105 // merging policies created in different places.
106 for i, r := range result.Results {
107 if r.Policy.String == nil {
108 r.Policy.String = []string{}
110 if r.Policy.MXHost == nil {
111 r.Policy.MXHost = []string{}
113 if r.FailureDetails == nil {
114 r.FailureDetails = []tlsrpt.FailureDetails{}
116 result.Results[i] = r
119 q := bstore.QueryTx[TLSResult](tx)
120 q.FilterNonzero(TLSResult{PolicyDomain: result.PolicyDomain, DayUTC: result.DayUTC, RecipientDomain: result.RecipientDomain})
122 if err == bstore.ErrAbsent {
124 if err := tx.Insert(&result); err != nil {
125 return fmt.Errorf("insert: %w", err)
128 } else if err != nil {
132 report := tlsrpt.Report{Policies: r.Results}
133 report.Merge(result.Results...)
134 r.Results = report.Policies
136 r.IsHost = result.IsHost
137 if result.SendReport {
141 if err := tx.Update(&r); err != nil {
142 return fmt.Errorf("update: %w", err)
150// Results returns all TLS results in the database, for all policy domains each
151// with potentially multiple days. Sorted by RecipientDomain and day.
152func Results(ctx context.Context) ([]TLSResult, error) {
153 db, err := resultDB(ctx)
158 return bstore.QueryDB[TLSResult](ctx, db).SortAsc("PolicyDomain", "DayUTC", "RecipientDomain").List()
161// ResultsDomain returns all TLSResults for a policy domain, potentially for
163func ResultsPolicyDomain(ctx context.Context, policyDomain dns.Domain) ([]TLSResult, error) {
164 db, err := resultDB(ctx)
169 return bstore.QueryDB[TLSResult](ctx, db).FilterNonzero(TLSResult{PolicyDomain: policyDomain.Name()}).SortAsc("DayUTC", "RecipientDomain").List()
172// ResultsRecipientDomain returns all TLSResults for a recipient domain,
173// potentially for multiple days.
174func ResultsRecipientDomain(ctx context.Context, recipientDomain dns.Domain) ([]TLSResult, error) {
175 db, err := resultDB(ctx)
180 return bstore.QueryDB[TLSResult](ctx, db).FilterNonzero(TLSResult{RecipientDomain: recipientDomain.Name()}).SortAsc("DayUTC", "PolicyDomain").List()
183// RemoveResultsPolicyDomain removes all TLSResults for the policy domain on the
184// day from the database.
185func RemoveResultsPolicyDomain(ctx context.Context, policyDomain dns.Domain, dayUTC string) error {
186 db, err := resultDB(ctx)
191 _, err = bstore.QueryDB[TLSResult](ctx, db).FilterNonzero(TLSResult{PolicyDomain: policyDomain.Name(), DayUTC: dayUTC}).Delete()
195// RemoveResultsRecipientDomain removes all TLSResults for the recipient domain on
196// the day from the database.
197func RemoveResultsRecipientDomain(ctx context.Context, recipientDomain dns.Domain, dayUTC string) error {
198 db, err := resultDB(ctx)
203 _, err = bstore.QueryDB[TLSResult](ctx, db).FilterNonzero(TLSResult{RecipientDomain: recipientDomain.Name(), DayUTC: dayUTC}).Delete()
207// SuppressAdd adds an address to the suppress list.
208func SuppressAdd(ctx context.Context, ba *TLSRPTSuppressAddress) error {
209 db, err := resultDB(ctx)
214 return db.Insert(ctx, ba)
217// SuppressList returns all reporting addresses on the suppress list.
218func SuppressList(ctx context.Context) ([]TLSRPTSuppressAddress, error) {
219 db, err := resultDB(ctx)
224 return bstore.QueryDB[TLSRPTSuppressAddress](ctx, db).SortDesc("ID").List()
227// SuppressRemove removes a reporting address record from the suppress list.
228func SuppressRemove(ctx context.Context, id int64) error {
229 db, err := resultDB(ctx)
234 return db.Delete(ctx, &TLSRPTSuppressAddress{ID: id})
237// SuppressUpdate updates the until field of a reporting address record.
238func SuppressUpdate(ctx context.Context, id int64, until time.Time) error {
239 db, err := resultDB(ctx)
244 ba := TLSRPTSuppressAddress{ID: id}
245 err = db.Get(ctx, &ba)
250 return db.Update(ctx, &ba)