11	"github.com/mjl-/bstore"
 
13	"github.com/mjl-/mox/dns"
 
14	"github.com/mjl-/mox/metrics"
 
15	"github.com/mjl-/mox/mlog"
 
16	"github.com/mjl-/mox/mox-"
 
17	"github.com/mjl-/mox/mtasts"
 
21	interval := 24 * time.Hour
 
22	ticker := time.NewTicker(interval)
 
29		ticker.Reset(interval)
 
31		ctx := context.WithValue(mox.Context, mlog.CidKey, mox.Cid())
 
32		n, err := refresh1(ctx, dns.StrictResolver{Pkg: "mtastsdb"}, time.Sleep)
 
34			xlog.WithContext(ctx).Errorx("periodic refresh of cached mtasts policies", err)
 
41		case <-mox.Shutdown.Done():
 
48// refresh policies that have not been updated in the past 12 hours and remove
 
49// policies not used for 180 days. We start with the first domain immediately, so
 
50// an admin can see any (configuration) issues that are logged. We spread the
 
51// refreshes evenly over the next 3 hours, randomizing the domains, and we add some
 
52// jitter to the timing. Each refresh is done in a new goroutine, so a single slow
 
53// refresh doesn't mess up the timing.
 
54func refresh1(ctx context.Context, resolver dns.Resolver, sleep func(d time.Duration)) (int, error) {
 
55	db, err := database(ctx)
 
61	qdel := bstore.QueryDB[PolicyRecord](ctx, db)
 
62	qdel.FilterLess("LastUse", now.Add(-180*24*time.Hour))
 
63	if _, err := qdel.Delete(); err != nil {
 
64		return 0, fmt.Errorf("deleting old unused policies: %s", err)
 
67	qup := bstore.QueryDB[PolicyRecord](ctx, db)
 
68	qup.FilterLess("LastUpdate", now.Add(-12*time.Hour))
 
69	prs, err := qup.List()
 
71		return 0, fmt.Errorf("querying policies to refresh: %s", err)
 
80	rand := mathrand.New(mathrand.NewSource(time.Now().UnixNano()))
 
86		prs[i], prs[j] = prs[j], prs[i]
 
89	// Launch goroutine with the refresh.
 
90	xlog.WithContext(ctx).Debug("will refresh mta-sts policies over next 3 hours", mlog.Field("count", len(prs)))
 
92	for i, pr := range prs {
 
93		go refreshDomain(ctx, db, resolver, pr)
 
95			interval := 3 * int64(time.Hour) / int64(len(prs)-1)
 
96			extra := time.Duration(rand.Int63n(interval) - interval/2)
 
97			next := start.Add(time.Duration(int64(i+1)*interval) + extra)
 
98			d := next.Sub(timeNow())
 
107func refreshDomain(ctx context.Context, db *bstore.DB, resolver dns.Resolver, pr PolicyRecord) {
 
108	log := xlog.WithContext(ctx)
 
112			// Should not happen, but make sure errors don't take down the application.
 
113			log.Error("refresh1", mlog.Field("panic", x))
 
115			metrics.PanicInc(metrics.Mtastsdb)
 
119	ctx, cancel := context.WithTimeout(ctx, time.Minute)
 
122	d, err := dns.ParseDomain(pr.Domain)
 
124		log.Errorx("refreshing mta-sts policy: parsing policy domain", err, mlog.Field("domain", d))
 
127	log.Debug("refreshing mta-sts policy for domain", mlog.Field("domain", d))
 
128	record, _, err := mtasts.LookupRecord(ctx, resolver, d)
 
129	if err == nil && record.ID == pr.RecordID {
 
130		qup := bstore.QueryDB[PolicyRecord](ctx, db)
 
131		qup.FilterNonzero(PolicyRecord{Domain: pr.Domain, LastUpdate: pr.LastUpdate})
 
133		update := PolicyRecord{
 
135			ValidEnd:   now.Add(time.Duration(pr.MaxAgeSeconds) * time.Second),
 
137		if n, err := qup.UpdateNonzero(update); err != nil {
 
138			log.Errorx("updating refreshed, unmodified policy in database", err)
 
140			log.Info("expected to update 1 policy after refresh", mlog.Field("count", n))
 
144	if err != nil && pr.Mode == mtasts.ModeNone {
 
145		if errors.Is(err, mtasts.ErrNoRecord) {
 
146			// Policy was in mode "none". Now it doesn't have a policy anymore. Remove from our
 
147			// database so we don't keep refreshing it.
 
148			err := db.Delete(ctx, &pr)
 
149			log.Check(err, "removing mta-sts policy with mode none, dns record is gone")
 
151		// Else, don't bother operator with temporary error about policy none.
 
154	} else if err != nil {
 
155		log.Errorx("looking up mta-sts record for domain", err, mlog.Field("domain", d))
 
156		// Try to fetch new policy. It could be just DNS that is down. We don't want to let our policy expire.
 
159	p, _, err := mtasts.FetchPolicy(ctx, d)
 
161		if !errors.Is(err, mtasts.ErrNoPolicy) || pr.Mode != mtasts.ModeNone {
 
162			log.Errorx("refreshing mtasts policy for domain", err, mlog.Field("domain", d))
 
167	update := map[string]any{
 
169		"ValidEnd":   now.Add(time.Duration(p.MaxAgeSeconds) * time.Second),
 
174		update["RecordID"] = record.ID
 
176	qup := bstore.QueryDB[PolicyRecord](ctx, db)
 
177	qup.FilterNonzero(PolicyRecord{Domain: pr.Domain, LastUpdate: pr.LastUpdate})
 
178	if n, err := qup.UpdateFields(update); err != nil {
 
179		log.Errorx("updating refreshed, modified policy in database", err)
 
181		log.Info("updating refreshed, did not update 1 policy", mlog.Field("count", n))