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// SuppressAddress is a reporting address for which outgoing TLS reports
64// will be suppressed for a period.
65type SuppressAddress struct {
66 ID int64 `bstore:"typename TLSRPTSuppressAddress"`
67 Inserted time.Time `bstore:"default now"`
68 ReportingAddress string `bstore:"unique"`
69 Until time.Time `bstore:"nonzero"`
73func resultDB(ctx context.Context) (rdb *bstore.DB, rerr error) {
77 p := mox.DataDirPath("tlsrptresult.db")
78 os.MkdirAll(filepath.Dir(p), 0770)
79 db, err := bstore.Open(ctx, p, &bstore.Options{Timeout: 5 * time.Second, Perm: 0660}, ResultDBTypes...)
88// AddTLSResults adds or merges all tls results for delivering to a policy domain,
89// on its UTC day to a recipient domain to the database. Results may cause multiple
90// separate reports to be sent.
91func AddTLSResults(ctx context.Context, results []TLSResult) error {
92 db, err := resultDB(ctx)
99 err = db.Write(ctx, func(tx *bstore.Tx) error {
100 for _, result := range results {
101 // Ensure all slices are non-nil. We do this now so all readers will marshal to
102 // compliant with the JSON schema. And also for consistent equality checks when
103 // merging policies created in different places.
104 for i, r := range result.Results {
105 if r.Policy.String == nil {
106 r.Policy.String = []string{}
108 if r.Policy.MXHost == nil {
109 r.Policy.MXHost = []string{}
111 if r.FailureDetails == nil {
112 r.FailureDetails = []tlsrpt.FailureDetails{}
114 result.Results[i] = r
117 q := bstore.QueryTx[TLSResult](tx)
118 q.FilterNonzero(TLSResult{PolicyDomain: result.PolicyDomain, DayUTC: result.DayUTC, RecipientDomain: result.RecipientDomain})
120 if err == bstore.ErrAbsent {
122 if err := tx.Insert(&result); err != nil {
123 return fmt.Errorf("insert: %w", err)
126 } else if err != nil {
130 report := tlsrpt.Report{Policies: r.Results}
131 report.Merge(result.Results...)
132 r.Results = report.Policies
134 r.IsHost = result.IsHost
135 if result.SendReport {
139 if err := tx.Update(&r); err != nil {
140 return fmt.Errorf("update: %w", err)
148// Results returns all TLS results in the database, for all policy domains each
149// with potentially multiple days. Sorted by RecipientDomain and day.
150func Results(ctx context.Context) ([]TLSResult, error) {
151 db, err := resultDB(ctx)
156 return bstore.QueryDB[TLSResult](ctx, db).SortAsc("PolicyDomain", "DayUTC", "RecipientDomain").List()
159// ResultsDomain returns all TLSResults for a policy domain, potentially for
161func ResultsPolicyDomain(ctx context.Context, policyDomain dns.Domain) ([]TLSResult, error) {
162 db, err := resultDB(ctx)
167 return bstore.QueryDB[TLSResult](ctx, db).FilterNonzero(TLSResult{PolicyDomain: policyDomain.Name()}).SortAsc("DayUTC", "RecipientDomain").List()
170// ResultsRecipientDomain returns all TLSResults for a recipient domain,
171// potentially for multiple days.
172func ResultsRecipientDomain(ctx context.Context, recipientDomain dns.Domain) ([]TLSResult, error) {
173 db, err := resultDB(ctx)
178 return bstore.QueryDB[TLSResult](ctx, db).FilterNonzero(TLSResult{RecipientDomain: recipientDomain.Name()}).SortAsc("DayUTC", "PolicyDomain").List()
181// RemoveResultsPolicyDomain removes all TLSResults for the policy domain on the
182// day from the database.
183func RemoveResultsPolicyDomain(ctx context.Context, policyDomain dns.Domain, dayUTC string) error {
184 db, err := resultDB(ctx)
189 _, err = bstore.QueryDB[TLSResult](ctx, db).FilterNonzero(TLSResult{PolicyDomain: policyDomain.Name(), DayUTC: dayUTC}).Delete()
193// RemoveResultsRecipientDomain removes all TLSResults for the recipient domain on
194// the day from the database.
195func RemoveResultsRecipientDomain(ctx context.Context, recipientDomain dns.Domain, dayUTC string) error {
196 db, err := resultDB(ctx)
201 _, err = bstore.QueryDB[TLSResult](ctx, db).FilterNonzero(TLSResult{RecipientDomain: recipientDomain.Name(), DayUTC: dayUTC}).Delete()
205// SuppressAdd adds an address to the suppress list.
206func SuppressAdd(ctx context.Context, ba *SuppressAddress) error {
207 db, err := resultDB(ctx)
212 return db.Insert(ctx, ba)
215// SuppressList returns all reporting addresses on the suppress list.
216func SuppressList(ctx context.Context) ([]SuppressAddress, error) {
217 db, err := resultDB(ctx)
222 return bstore.QueryDB[SuppressAddress](ctx, db).SortDesc("ID").List()
225// SuppressRemove removes a reporting address record from the suppress list.
226func SuppressRemove(ctx context.Context, id int64) error {
227 db, err := resultDB(ctx)
232 return db.Delete(ctx, &SuppressAddress{ID: id})
235// SuppressUpdate updates the until field of a reporting address record.
236func SuppressUpdate(ctx context.Context, id int64, until time.Time) error {
237 db, err := resultDB(ctx)
242 ba := SuppressAddress{ID: id}
243 err = db.Get(ctx, &ba)
248 return db.Update(ctx, &ba)