mirror of
https://github.com/mjl-/mox.git
synced 2024-12-26 08:23:48 +03:00
add debug logging about bstore db schema upgrades
bstore was updated to v0.0.6 to add this logging. this simplifies some of the db-handling code in mtastsdb,tlsrptdb,dmarcdb. we now call the package-level Init() and Close() in all tests properly.
This commit is contained in:
parent
3e4cce826e
commit
bf8cfd9724
31 changed files with 298 additions and 428 deletions
|
@ -313,7 +313,8 @@ func backupctl(ctx context.Context, ctl *ctl) {
|
||||||
}
|
}
|
||||||
|
|
||||||
dstdbpath := filepath.Join(dstDataDir, path)
|
dstdbpath := filepath.Join(dstDataDir, path)
|
||||||
db, err := bstore.Open(ctx, dstdbpath, &bstore.Options{MustExist: true}, queue.DBTypes...)
|
opts := bstore.Options{MustExist: true, RegisterLogger: ctl.log.Logger}
|
||||||
|
db, err := bstore.Open(ctx, dstdbpath, &opts, queue.DBTypes...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
xerrx("open copied queue database", err, slog.String("dstpath", dstdbpath), slog.Duration("duration", time.Since(tmQueue)))
|
xerrx("open copied queue database", err, slog.String("dstpath", dstdbpath), slog.Duration("duration", time.Since(tmQueue)))
|
||||||
return
|
return
|
||||||
|
@ -419,7 +420,8 @@ func backupctl(ctx context.Context, ctl *ctl) {
|
||||||
}
|
}
|
||||||
|
|
||||||
dstdbpath := filepath.Join(dstDataDir, dbpath)
|
dstdbpath := filepath.Join(dstDataDir, dbpath)
|
||||||
db, err := bstore.Open(ctx, dstdbpath, &bstore.Options{MustExist: true}, store.DBTypes...)
|
opts := bstore.Options{MustExist: true, RegisterLogger: ctl.log.Logger}
|
||||||
|
db, err := bstore.Open(ctx, dstdbpath, &opts, store.DBTypes...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
xerrx("open copied account database", err, slog.String("dstpath", dstdbpath), slog.Duration("duration", time.Since(tmAccount)))
|
xerrx("open copied account database", err, slog.String("dstpath", dstdbpath), slog.Duration("duration", time.Since(tmAccount)))
|
||||||
return
|
return
|
||||||
|
|
|
@ -11,6 +11,15 @@
|
||||||
package dmarcdb
|
package dmarcdb
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/mjl-/bstore"
|
||||||
|
|
||||||
|
"github.com/mjl-/mox/mlog"
|
||||||
"github.com/mjl-/mox/mox-"
|
"github.com/mjl-/mox/mox-"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -19,11 +28,49 @@ import (
|
||||||
// The incoming reports and evaluations for outgoing reports are in separate
|
// The incoming reports and evaluations for outgoing reports are in separate
|
||||||
// databases for simpler file-based handling of the databases.
|
// databases for simpler file-based handling of the databases.
|
||||||
func Init() error {
|
func Init() error {
|
||||||
if _, err := reportsDB(mox.Shutdown); err != nil {
|
if ReportsDB != nil || EvalDB != nil {
|
||||||
return err
|
return fmt.Errorf("already initialized")
|
||||||
}
|
}
|
||||||
if _, err := evalDB(mox.Shutdown); err != nil {
|
|
||||||
return err
|
log := mlog.New("dmarcdb", nil)
|
||||||
|
var err error
|
||||||
|
|
||||||
|
ReportsDB, err = openReportsDB(mox.Shutdown, log)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("open reports db: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
EvalDB, err = openEvalDB(mox.Shutdown, log)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("open eval db: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Close() error {
|
||||||
|
if err := ReportsDB.Close(); err != nil {
|
||||||
|
return fmt.Errorf("closing reports db: %w", err)
|
||||||
|
}
|
||||||
|
ReportsDB = nil
|
||||||
|
|
||||||
|
if err := EvalDB.Close(); err != nil {
|
||||||
|
return fmt.Errorf("closing eval db: %w", err)
|
||||||
|
}
|
||||||
|
EvalDB = nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func openReportsDB(ctx context.Context, log mlog.Log) (*bstore.DB, error) {
|
||||||
|
p := mox.DataDirPath("dmarcrpt.db")
|
||||||
|
os.MkdirAll(filepath.Dir(p), 0770)
|
||||||
|
opts := bstore.Options{Timeout: 5 * time.Second, Perm: 0660, RegisterLogger: log.Logger}
|
||||||
|
return bstore.Open(ctx, p, &opts, ReportsDBTypes...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func openEvalDB(ctx context.Context, log mlog.Log) (*bstore.DB, error) {
|
||||||
|
p := mox.DataDirPath("dmarceval.db")
|
||||||
|
os.MkdirAll(filepath.Dir(p), 0770)
|
||||||
|
opts := bstore.Options{Timeout: 5 * time.Second, Perm: 0660, RegisterLogger: log.Logger}
|
||||||
|
return bstore.Open(ctx, p, &opts, EvalDBTypes...)
|
||||||
|
}
|
||||||
|
|
|
@ -15,7 +15,6 @@ import (
|
||||||
"net/textproto"
|
"net/textproto"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
"slices"
|
"slices"
|
||||||
"sort"
|
"sort"
|
||||||
|
@ -67,7 +66,6 @@ var (
|
||||||
// to the database. Every hour, a goroutine wakes up that gathers evaluations from
|
// to the database. Every hour, a goroutine wakes up that gathers evaluations from
|
||||||
// the last hour(s), sends a report, and removes the evaluations from the database.
|
// the last hour(s), sends a report, and removes the evaluations from the database.
|
||||||
EvalDB *bstore.DB
|
EvalDB *bstore.DB
|
||||||
evalMutex sync.Mutex
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Evaluation is the result of an evaluation of a DMARC policy, to be included
|
// Evaluation is the result of an evaluation of a DMARC policy, to be included
|
||||||
|
@ -162,21 +160,6 @@ func (e Evaluation) ReportRecord(count int) dmarcrpt.ReportRecord {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func evalDB(ctx context.Context) (rdb *bstore.DB, rerr error) {
|
|
||||||
evalMutex.Lock()
|
|
||||||
defer evalMutex.Unlock()
|
|
||||||
if EvalDB == nil {
|
|
||||||
p := mox.DataDirPath("dmarceval.db")
|
|
||||||
os.MkdirAll(filepath.Dir(p), 0770)
|
|
||||||
db, err := bstore.Open(ctx, p, &bstore.Options{Timeout: 5 * time.Second, Perm: 0660}, EvalDBTypes...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
EvalDB = db
|
|
||||||
}
|
|
||||||
return EvalDB, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var intervalOpts = []int{24, 12, 8, 6, 4, 3, 2}
|
var intervalOpts = []int{24, 12, 8, 6, 4, 3, 2}
|
||||||
|
|
||||||
func intervalHours(seconds int) int {
|
func intervalHours(seconds int) int {
|
||||||
|
@ -197,23 +180,13 @@ func intervalHours(seconds int) int {
|
||||||
func AddEvaluation(ctx context.Context, aggregateReportingIntervalSeconds int, e *Evaluation) error {
|
func AddEvaluation(ctx context.Context, aggregateReportingIntervalSeconds int, e *Evaluation) error {
|
||||||
e.IntervalHours = intervalHours(aggregateReportingIntervalSeconds)
|
e.IntervalHours = intervalHours(aggregateReportingIntervalSeconds)
|
||||||
|
|
||||||
db, err := evalDB(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
e.ID = 0
|
e.ID = 0
|
||||||
return db.Insert(ctx, e)
|
return EvalDB.Insert(ctx, e)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Evaluations returns all evaluations in the database.
|
// Evaluations returns all evaluations in the database.
|
||||||
func Evaluations(ctx context.Context) ([]Evaluation, error) {
|
func Evaluations(ctx context.Context) ([]Evaluation, error) {
|
||||||
db, err := evalDB(ctx)
|
q := bstore.QueryDB[Evaluation](ctx, EvalDB)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
q := bstore.QueryDB[Evaluation](ctx, db)
|
|
||||||
q.SortAsc("Evaluated")
|
q.SortAsc("Evaluated")
|
||||||
return q.List()
|
return q.List()
|
||||||
}
|
}
|
||||||
|
@ -229,14 +202,9 @@ type EvaluationStat struct {
|
||||||
|
|
||||||
// EvaluationStats returns evaluation counts and report-sending status per domain.
|
// EvaluationStats returns evaluation counts and report-sending status per domain.
|
||||||
func EvaluationStats(ctx context.Context) (map[string]EvaluationStat, error) {
|
func EvaluationStats(ctx context.Context) (map[string]EvaluationStat, error) {
|
||||||
db, err := evalDB(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
r := map[string]EvaluationStat{}
|
r := map[string]EvaluationStat{}
|
||||||
|
|
||||||
err = bstore.QueryDB[Evaluation](ctx, db).ForEach(func(e Evaluation) error {
|
err := bstore.QueryDB[Evaluation](ctx, EvalDB).ForEach(func(e Evaluation) error {
|
||||||
if stat, ok := r[e.PolicyDomain]; ok {
|
if stat, ok := r[e.PolicyDomain]; ok {
|
||||||
if !slices.Contains(stat.Dispositions, string(e.Disposition)) {
|
if !slices.Contains(stat.Dispositions, string(e.Disposition)) {
|
||||||
stat.Dispositions = append(stat.Dispositions, string(e.Disposition))
|
stat.Dispositions = append(stat.Dispositions, string(e.Disposition))
|
||||||
|
@ -263,12 +231,7 @@ func EvaluationStats(ctx context.Context) (map[string]EvaluationStat, error) {
|
||||||
|
|
||||||
// EvaluationsDomain returns all evaluations for a domain.
|
// EvaluationsDomain returns all evaluations for a domain.
|
||||||
func EvaluationsDomain(ctx context.Context, domain dns.Domain) ([]Evaluation, error) {
|
func EvaluationsDomain(ctx context.Context, domain dns.Domain) ([]Evaluation, error) {
|
||||||
db, err := evalDB(ctx)
|
q := bstore.QueryDB[Evaluation](ctx, EvalDB)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
q := bstore.QueryDB[Evaluation](ctx, db)
|
|
||||||
q.FilterNonzero(Evaluation{PolicyDomain: domain.Name()})
|
q.FilterNonzero(Evaluation{PolicyDomain: domain.Name()})
|
||||||
q.SortAsc("Evaluated")
|
q.SortAsc("Evaluated")
|
||||||
return q.List()
|
return q.List()
|
||||||
|
@ -277,14 +240,9 @@ func EvaluationsDomain(ctx context.Context, domain dns.Domain) ([]Evaluation, er
|
||||||
// RemoveEvaluationsDomain removes evaluations for domain so they won't be sent in
|
// RemoveEvaluationsDomain removes evaluations for domain so they won't be sent in
|
||||||
// an aggregate report.
|
// an aggregate report.
|
||||||
func RemoveEvaluationsDomain(ctx context.Context, domain dns.Domain) error {
|
func RemoveEvaluationsDomain(ctx context.Context, domain dns.Domain) error {
|
||||||
db, err := evalDB(ctx)
|
q := bstore.QueryDB[Evaluation](ctx, EvalDB)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
q := bstore.QueryDB[Evaluation](ctx, db)
|
|
||||||
q.FilterNonzero(Evaluation{PolicyDomain: domain.Name()})
|
q.FilterNonzero(Evaluation{PolicyDomain: domain.Name()})
|
||||||
_, err = q.Delete()
|
_, err := q.Delete()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -318,12 +276,6 @@ func Start(resolver dns.Resolver) {
|
||||||
|
|
||||||
ctx := mox.Shutdown
|
ctx := mox.Shutdown
|
||||||
|
|
||||||
db, err := evalDB(ctx)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorx("opening dmarc evaluations database for sending dmarc aggregate reports, not sending reports", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for {
|
for {
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
nextEnd := nextWholeHour(now)
|
nextEnd := nextWholeHour(now)
|
||||||
|
@ -355,12 +307,12 @@ func Start(resolver dns.Resolver) {
|
||||||
// 24 hour interval). They should have been processed by now. We may have kept them
|
// 24 hour interval). They should have been processed by now. We may have kept them
|
||||||
// during temporary errors, but persistent temporary errors shouldn't fill up our
|
// during temporary errors, but persistent temporary errors shouldn't fill up our
|
||||||
// database. This also cleans up evaluations that were all optional for a domain.
|
// database. This also cleans up evaluations that were all optional for a domain.
|
||||||
_, err := bstore.QueryDB[Evaluation](ctx, db).FilterLess("Evaluated", nextEnd.Add(-48*time.Hour)).Delete()
|
_, err := bstore.QueryDB[Evaluation](ctx, EvalDB).FilterLess("Evaluated", nextEnd.Add(-48*time.Hour)).Delete()
|
||||||
log.Check(err, "removing stale dmarc evaluations from database")
|
log.Check(err, "removing stale dmarc evaluations from database")
|
||||||
|
|
||||||
clog := log.WithCid(mox.Cid())
|
clog := log.WithCid(mox.Cid())
|
||||||
clog.Info("sending dmarc aggregate reports", slog.Time("end", nextEnd.UTC()), slog.Any("intervals", intervals))
|
clog.Info("sending dmarc aggregate reports", slog.Time("end", nextEnd.UTC()), slog.Any("intervals", intervals))
|
||||||
if err := sendReports(ctx, clog, resolver, db, nextEnd, intervals); err != nil {
|
if err := sendReports(ctx, clog, resolver, EvalDB, nextEnd, intervals); err != nil {
|
||||||
clog.Errorx("sending dmarc aggregate reports", err)
|
clog.Errorx("sending dmarc aggregate reports", err)
|
||||||
metricReportError.Inc()
|
metricReportError.Inc()
|
||||||
} else {
|
} else {
|
||||||
|
@ -1091,46 +1043,26 @@ func dkimSign(ctx context.Context, log mlog.Log, fromAddr smtp.Address, smtputf8
|
||||||
|
|
||||||
// SuppressAdd adds an address to the suppress list.
|
// SuppressAdd adds an address to the suppress list.
|
||||||
func SuppressAdd(ctx context.Context, ba *SuppressAddress) error {
|
func SuppressAdd(ctx context.Context, ba *SuppressAddress) error {
|
||||||
db, err := evalDB(ctx)
|
return EvalDB.Insert(ctx, ba)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return db.Insert(ctx, ba)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SuppressList returns all reporting addresses on the suppress list.
|
// SuppressList returns all reporting addresses on the suppress list.
|
||||||
func SuppressList(ctx context.Context) ([]SuppressAddress, error) {
|
func SuppressList(ctx context.Context) ([]SuppressAddress, error) {
|
||||||
db, err := evalDB(ctx)
|
return bstore.QueryDB[SuppressAddress](ctx, EvalDB).SortDesc("ID").List()
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return bstore.QueryDB[SuppressAddress](ctx, db).SortDesc("ID").List()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SuppressRemove removes a reporting address record from the suppress list.
|
// SuppressRemove removes a reporting address record from the suppress list.
|
||||||
func SuppressRemove(ctx context.Context, id int64) error {
|
func SuppressRemove(ctx context.Context, id int64) error {
|
||||||
db, err := evalDB(ctx)
|
return EvalDB.Delete(ctx, &SuppressAddress{ID: id})
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return db.Delete(ctx, &SuppressAddress{ID: id})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SuppressUpdate updates the until field of a reporting address record.
|
// SuppressUpdate updates the until field of a reporting address record.
|
||||||
func SuppressUpdate(ctx context.Context, id int64, until time.Time) error {
|
func SuppressUpdate(ctx context.Context, id int64, until time.Time) error {
|
||||||
db, err := evalDB(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
ba := SuppressAddress{ID: id}
|
ba := SuppressAddress{ID: id}
|
||||||
err = db.Get(ctx, &ba)
|
err := EvalDB.Get(ctx, &ba)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
ba.Until = until
|
ba.Until = until
|
||||||
return db.Update(ctx, &ba)
|
return EvalDB.Update(ctx, &ba)
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,13 +41,13 @@ func TestEvaluations(t *testing.T) {
|
||||||
mox.Context = ctxbg
|
mox.Context = ctxbg
|
||||||
mox.ConfigStaticPath = filepath.FromSlash("../testdata/dmarcdb/mox.conf")
|
mox.ConfigStaticPath = filepath.FromSlash("../testdata/dmarcdb/mox.conf")
|
||||||
mox.MustLoadConfig(true, false)
|
mox.MustLoadConfig(true, false)
|
||||||
EvalDB = nil
|
|
||||||
|
|
||||||
_, err := evalDB(ctxbg)
|
os.Remove(mox.DataDirPath("dmarceval.db"))
|
||||||
tcheckf(t, err, "database")
|
err := Init()
|
||||||
|
tcheckf(t, err, "init")
|
||||||
defer func() {
|
defer func() {
|
||||||
EvalDB.Close()
|
err := Close()
|
||||||
EvalDB = nil
|
tcheckf(t, err, "close")
|
||||||
}()
|
}()
|
||||||
|
|
||||||
parseJSON := func(s string) (e Evaluation) {
|
parseJSON := func(s string) (e Evaluation) {
|
||||||
|
@ -163,13 +163,13 @@ func TestSendReports(t *testing.T) {
|
||||||
mox.Context = ctxbg
|
mox.Context = ctxbg
|
||||||
mox.ConfigStaticPath = filepath.FromSlash("../testdata/dmarcdb/mox.conf")
|
mox.ConfigStaticPath = filepath.FromSlash("../testdata/dmarcdb/mox.conf")
|
||||||
mox.MustLoadConfig(true, false)
|
mox.MustLoadConfig(true, false)
|
||||||
EvalDB = nil
|
|
||||||
|
|
||||||
db, err := evalDB(ctxbg)
|
os.Remove(mox.DataDirPath("dmarceval.db"))
|
||||||
tcheckf(t, err, "database")
|
err := Init()
|
||||||
|
tcheckf(t, err, "init")
|
||||||
defer func() {
|
defer func() {
|
||||||
EvalDB.Close()
|
err := Close()
|
||||||
EvalDB = nil
|
tcheckf(t, err, "close")
|
||||||
}()
|
}()
|
||||||
|
|
||||||
resolver := dns.MockResolver{
|
resolver := dns.MockResolver{
|
||||||
|
@ -288,7 +288,7 @@ func TestSendReports(t *testing.T) {
|
||||||
mox.Shutdown, mox.ShutdownCancel = context.WithCancel(ctxbg)
|
mox.Shutdown, mox.ShutdownCancel = context.WithCancel(ctxbg)
|
||||||
|
|
||||||
for _, e := range evals {
|
for _, e := range evals {
|
||||||
err := db.Insert(ctxbg, &e)
|
err := EvalDB.Insert(ctxbg, &e)
|
||||||
tcheckf(t, err, "inserting evaluation")
|
tcheckf(t, err, "inserting evaluation")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -359,13 +359,13 @@ func TestSendReports(t *testing.T) {
|
||||||
|
|
||||||
// Address is suppressed.
|
// Address is suppressed.
|
||||||
sa := SuppressAddress{ReportingAddress: "dmarcrpt@sender.example", Until: time.Now().Add(time.Minute)}
|
sa := SuppressAddress{ReportingAddress: "dmarcrpt@sender.example", Until: time.Now().Add(time.Minute)}
|
||||||
err = db.Insert(ctxbg, &sa)
|
err = EvalDB.Insert(ctxbg, &sa)
|
||||||
tcheckf(t, err, "insert suppress address")
|
tcheckf(t, err, "insert suppress address")
|
||||||
test([]Evaluation{eval}, map[string]struct{}{}, map[string]struct{}{}, nil)
|
test([]Evaluation{eval}, map[string]struct{}{}, map[string]struct{}{}, nil)
|
||||||
|
|
||||||
// Suppression has expired.
|
// Suppression has expired.
|
||||||
sa.Until = time.Now().Add(-time.Minute)
|
sa.Until = time.Now().Add(-time.Minute)
|
||||||
err = db.Update(ctxbg, &sa)
|
err = EvalDB.Update(ctxbg, &sa)
|
||||||
tcheckf(t, err, "update suppress address")
|
tcheckf(t, err, "update suppress address")
|
||||||
test([]Evaluation{eval}, map[string]struct{}{"dmarcrpt@sender.example": {}}, map[string]struct{}{}, expFeedback)
|
test([]Evaluation{eval}, map[string]struct{}{"dmarcrpt@sender.example": {}}, map[string]struct{}{}, expFeedback)
|
||||||
|
|
||||||
|
|
|
@ -3,9 +3,6 @@ package dmarcdb
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"sync"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
@ -15,13 +12,11 @@ import (
|
||||||
|
|
||||||
"github.com/mjl-/mox/dmarcrpt"
|
"github.com/mjl-/mox/dmarcrpt"
|
||||||
"github.com/mjl-/mox/dns"
|
"github.com/mjl-/mox/dns"
|
||||||
"github.com/mjl-/mox/mox-"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ReportsDBTypes = []any{DomainFeedback{}} // Types stored in DB.
|
ReportsDBTypes = []any{DomainFeedback{}} // Types stored in DB.
|
||||||
ReportsDB *bstore.DB // Exported for backups.
|
ReportsDB *bstore.DB // Exported for backups.
|
||||||
reportsMutex sync.Mutex
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -59,38 +54,18 @@ type DomainFeedback struct {
|
||||||
dmarcrpt.Feedback
|
dmarcrpt.Feedback
|
||||||
}
|
}
|
||||||
|
|
||||||
func reportsDB(ctx context.Context) (rdb *bstore.DB, rerr error) {
|
|
||||||
reportsMutex.Lock()
|
|
||||||
defer reportsMutex.Unlock()
|
|
||||||
if ReportsDB == nil {
|
|
||||||
p := mox.DataDirPath("dmarcrpt.db")
|
|
||||||
os.MkdirAll(filepath.Dir(p), 0770)
|
|
||||||
db, err := bstore.Open(ctx, p, &bstore.Options{Timeout: 5 * time.Second, Perm: 0660}, ReportsDBTypes...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
ReportsDB = db
|
|
||||||
}
|
|
||||||
return ReportsDB, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddReport adds a DMARC aggregate feedback report from an email to the database,
|
// AddReport adds a DMARC aggregate feedback report from an email to the database,
|
||||||
// and updates prometheus metrics.
|
// and updates prometheus metrics.
|
||||||
//
|
//
|
||||||
// fromDomain is the domain in the report message From header.
|
// fromDomain is the domain in the report message From header.
|
||||||
func AddReport(ctx context.Context, f *dmarcrpt.Feedback, fromDomain dns.Domain) error {
|
func AddReport(ctx context.Context, f *dmarcrpt.Feedback, fromDomain dns.Domain) error {
|
||||||
db, err := reportsDB(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
d, err := dns.ParseDomain(f.PolicyPublished.Domain)
|
d, err := dns.ParseDomain(f.PolicyPublished.Domain)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("parsing domain in report: %v", err)
|
return fmt.Errorf("parsing domain in report: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
df := DomainFeedback{0, d.Name(), fromDomain.Name(), *f}
|
df := DomainFeedback{0, d.Name(), fromDomain.Name(), *f}
|
||||||
if err := db.Insert(ctx, &df); err != nil {
|
if err := ReportsDB.Insert(ctx, &df); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -129,38 +104,23 @@ func AddReport(ctx context.Context, f *dmarcrpt.Feedback, fromDomain dns.Domain)
|
||||||
|
|
||||||
// Records returns all reports in the database.
|
// Records returns all reports in the database.
|
||||||
func Records(ctx context.Context) ([]DomainFeedback, error) {
|
func Records(ctx context.Context) ([]DomainFeedback, error) {
|
||||||
db, err := reportsDB(ctx)
|
return bstore.QueryDB[DomainFeedback](ctx, ReportsDB).List()
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return bstore.QueryDB[DomainFeedback](ctx, db).List()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// RecordID returns the report for the ID.
|
// RecordID returns the report for the ID.
|
||||||
func RecordID(ctx context.Context, id int64) (DomainFeedback, error) {
|
func RecordID(ctx context.Context, id int64) (DomainFeedback, error) {
|
||||||
db, err := reportsDB(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return DomainFeedback{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
e := DomainFeedback{ID: id}
|
e := DomainFeedback{ID: id}
|
||||||
err = db.Get(ctx, &e)
|
err := ReportsDB.Get(ctx, &e)
|
||||||
return e, err
|
return e, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// RecordsPeriodDomain returns the reports overlapping start and end, for the given
|
// RecordsPeriodDomain returns the reports overlapping start and end, for the given
|
||||||
// domain. If domain is empty, all records match for domain.
|
// domain. If domain is empty, all records match for domain.
|
||||||
func RecordsPeriodDomain(ctx context.Context, start, end time.Time, domain string) ([]DomainFeedback, error) {
|
func RecordsPeriodDomain(ctx context.Context, start, end time.Time, domain string) ([]DomainFeedback, error) {
|
||||||
db, err := reportsDB(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
s := start.Unix()
|
s := start.Unix()
|
||||||
e := end.Unix()
|
e := end.Unix()
|
||||||
|
|
||||||
q := bstore.QueryDB[DomainFeedback](ctx, db)
|
q := bstore.QueryDB[DomainFeedback](ctx, ReportsDB)
|
||||||
if domain != "" {
|
if domain != "" {
|
||||||
q.FilterNonzero(DomainFeedback{Domain: domain})
|
q.FilterNonzero(DomainFeedback{Domain: domain})
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,16 +20,12 @@ func TestDMARCDB(t *testing.T) {
|
||||||
mox.ConfigStaticPath = filepath.FromSlash("../testdata/dmarcdb/mox.conf")
|
mox.ConfigStaticPath = filepath.FromSlash("../testdata/dmarcdb/mox.conf")
|
||||||
mox.MustLoadConfig(true, false)
|
mox.MustLoadConfig(true, false)
|
||||||
|
|
||||||
dbpath := mox.DataDirPath("dmarcrpt.db")
|
os.Remove(mox.DataDirPath("dmarcrpt.db"))
|
||||||
os.MkdirAll(filepath.Dir(dbpath), 0770)
|
err := Init()
|
||||||
|
tcheckf(t, err, "init")
|
||||||
if err := Init(); err != nil {
|
|
||||||
t.Fatalf("init database: %s", err)
|
|
||||||
}
|
|
||||||
defer os.Remove(dbpath)
|
|
||||||
defer func() {
|
defer func() {
|
||||||
ReportsDB.Close()
|
err := Close()
|
||||||
ReportsDB = nil
|
tcheckf(t, err, "close")
|
||||||
}()
|
}()
|
||||||
|
|
||||||
feedback := &dmarcrpt.Feedback{
|
feedback := &dmarcrpt.Feedback{
|
||||||
|
|
|
@ -62,7 +62,8 @@ func xcmdExport(mbox, single bool, args []string, c *cmd) {
|
||||||
}
|
}
|
||||||
|
|
||||||
dbpath := filepath.Join(accountDir, "index.db")
|
dbpath := filepath.Join(accountDir, "index.db")
|
||||||
db, err := bstore.Open(context.Background(), dbpath, &bstore.Options{Timeout: 5 * time.Second, Perm: 0660}, store.DBTypes...)
|
opts := bstore.Options{Timeout: 5 * time.Second, Perm: 0660, RegisterLogger: c.log.Logger}
|
||||||
|
db, err := bstore.Open(context.Background(), dbpath, &opts, store.DBTypes...)
|
||||||
xcheckf(err, "open database %q", dbpath)
|
xcheckf(err, "open database %q", dbpath)
|
||||||
defer func() {
|
defer func() {
|
||||||
if err := db.Close(); err != nil {
|
if err := db.Close(); err != nil {
|
||||||
|
|
2
go.mod
2
go.mod
|
@ -5,7 +5,7 @@ go 1.21
|
||||||
require (
|
require (
|
||||||
github.com/mjl-/adns v0.0.0-20240509092456-2dc8715bf4af
|
github.com/mjl-/adns v0.0.0-20240509092456-2dc8715bf4af
|
||||||
github.com/mjl-/autocert v0.0.0-20231214125928-31b7400acb05
|
github.com/mjl-/autocert v0.0.0-20231214125928-31b7400acb05
|
||||||
github.com/mjl-/bstore v0.0.5
|
github.com/mjl-/bstore v0.0.6
|
||||||
github.com/mjl-/sconf v0.0.6
|
github.com/mjl-/sconf v0.0.6
|
||||||
github.com/mjl-/sherpa v0.6.7
|
github.com/mjl-/sherpa v0.6.7
|
||||||
github.com/mjl-/sherpadoc v0.0.16
|
github.com/mjl-/sherpadoc v0.0.16
|
||||||
|
|
4
go.sum
4
go.sum
|
@ -28,8 +28,8 @@ github.com/mjl-/adns v0.0.0-20240509092456-2dc8715bf4af h1:sEDWZPIi5K1qKk7JQoAZy
|
||||||
github.com/mjl-/adns v0.0.0-20240509092456-2dc8715bf4af/go.mod h1:v47qUMJnipnmDTRGaHwpCwzE6oypa5K33mUvBfzZBn8=
|
github.com/mjl-/adns v0.0.0-20240509092456-2dc8715bf4af/go.mod h1:v47qUMJnipnmDTRGaHwpCwzE6oypa5K33mUvBfzZBn8=
|
||||||
github.com/mjl-/autocert v0.0.0-20231214125928-31b7400acb05 h1:s6ay4bh4tmpPLdxjyeWG45mcwHfEluBMuGPkqxHWUJ4=
|
github.com/mjl-/autocert v0.0.0-20231214125928-31b7400acb05 h1:s6ay4bh4tmpPLdxjyeWG45mcwHfEluBMuGPkqxHWUJ4=
|
||||||
github.com/mjl-/autocert v0.0.0-20231214125928-31b7400acb05/go.mod h1:taMFU86abMxKLPV4Bynhv8enbYmS67b8LG80qZv2Qus=
|
github.com/mjl-/autocert v0.0.0-20231214125928-31b7400acb05/go.mod h1:taMFU86abMxKLPV4Bynhv8enbYmS67b8LG80qZv2Qus=
|
||||||
github.com/mjl-/bstore v0.0.5 h1:Cx+LWEBnFBsqSxZNMxeVujkfc0kG10lUJaAU4vWSRHo=
|
github.com/mjl-/bstore v0.0.6 h1:ntlu9MkfCkpm2XfBY4+Ws4KK9YzXzewr3+lCueFB+9c=
|
||||||
github.com/mjl-/bstore v0.0.5/go.mod h1:/cD25FNBaDfvL/plFRxI3Ba3E+wcB0XVOS8nJDqndg0=
|
github.com/mjl-/bstore v0.0.6/go.mod h1:/cD25FNBaDfvL/plFRxI3Ba3E+wcB0XVOS8nJDqndg0=
|
||||||
github.com/mjl-/sconf v0.0.6 h1:5Dt58488ZOoVx680zgK2K3vUrokLsp5mXDUACrJlrUc=
|
github.com/mjl-/sconf v0.0.6 h1:5Dt58488ZOoVx680zgK2K3vUrokLsp5mXDUACrJlrUc=
|
||||||
github.com/mjl-/sconf v0.0.6/go.mod h1:uF8OdWtLT8La3i4ln176i1pB0ps9pXGCaABEU55ZkE0=
|
github.com/mjl-/sconf v0.0.6/go.mod h1:uF8OdWtLT8La3i4ln176i1pB0ps9pXGCaABEU55ZkE0=
|
||||||
github.com/mjl-/sherpa v0.6.7 h1:C5F8XQdV5nCuS4fvB+ye/ziUQrajEhOoj/t2w5T14BY=
|
github.com/mjl-/sherpa v0.6.7 h1:C5F8XQdV5nCuS4fvB+ye/ziUQrajEhOoj/t2w5T14BY=
|
||||||
|
|
|
@ -125,7 +125,7 @@ func OpenFilter(ctx context.Context, log mlog.Log, params Params, dbPath, bloomP
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
db, err := openDB(ctx, dbPath)
|
db, err := openDB(ctx, log, dbPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("open database: %s", err)
|
return nil, fmt.Errorf("open database: %s", err)
|
||||||
}
|
}
|
||||||
|
@ -230,18 +230,20 @@ func newDB(ctx context.Context, log mlog.Log, path string) (db *bstore.DB, rerr
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
db, err := bstore.Open(ctx, path, &bstore.Options{Timeout: 5 * time.Second, Perm: 0660}, DBTypes...)
|
opts := bstore.Options{Timeout: 5 * time.Second, Perm: 0660, RegisterLogger: log.Logger}
|
||||||
|
db, err := bstore.Open(ctx, path, &opts, DBTypes...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("open new database: %w", err)
|
return nil, fmt.Errorf("open new database: %w", err)
|
||||||
}
|
}
|
||||||
return db, nil
|
return db, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func openDB(ctx context.Context, path string) (*bstore.DB, error) {
|
func openDB(ctx context.Context, log mlog.Log, path string) (*bstore.DB, error) {
|
||||||
if _, err := os.Stat(path); err != nil {
|
if _, err := os.Stat(path); err != nil {
|
||||||
return nil, fmt.Errorf("stat db file: %w", err)
|
return nil, fmt.Errorf("stat db file: %w", err)
|
||||||
}
|
}
|
||||||
return bstore.Open(ctx, path, &bstore.Options{Timeout: 5 * time.Second, Perm: 0660}, DBTypes...)
|
opts := bstore.Options{Timeout: 5 * time.Second, Perm: 0660, RegisterLogger: log.Logger}
|
||||||
|
return bstore.Open(ctx, path, &opts, DBTypes...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save stores modifications, e.g. from training, to the database and bloom
|
// Save stores modifications, e.g. from training, to the database and bloom
|
||||||
|
|
|
@ -14,7 +14,6 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
@ -68,27 +67,17 @@ var (
|
||||||
|
|
||||||
var DBTypes = []any{PolicyRecord{}} // Types stored in DB.
|
var DBTypes = []any{PolicyRecord{}} // Types stored in DB.
|
||||||
var DB *bstore.DB // Exported for backups.
|
var DB *bstore.DB // Exported for backups.
|
||||||
var mutex sync.Mutex
|
|
||||||
|
|
||||||
func database(ctx context.Context) (rdb *bstore.DB, rerr error) {
|
|
||||||
mutex.Lock()
|
|
||||||
defer mutex.Unlock()
|
|
||||||
if DB == nil {
|
|
||||||
p := mox.DataDirPath("mtasts.db")
|
|
||||||
os.MkdirAll(filepath.Dir(p), 0770)
|
|
||||||
db, err := bstore.Open(ctx, p, &bstore.Options{Timeout: 5 * time.Second, Perm: 0660}, DBTypes...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
DB = db
|
|
||||||
}
|
|
||||||
return DB, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Init opens the database and starts a goroutine that refreshes policies in
|
// Init opens the database and starts a goroutine that refreshes policies in
|
||||||
// the database, and keeps doing so periodically.
|
// the database, and keeps doing so periodically.
|
||||||
func Init(refresher bool) error {
|
func Init(refresher bool) error {
|
||||||
_, err := database(mox.Shutdown)
|
log := mlog.New("mtastsdb", nil)
|
||||||
|
|
||||||
|
p := mox.DataDirPath("mtasts.db")
|
||||||
|
os.MkdirAll(filepath.Dir(p), 0770)
|
||||||
|
opts := bstore.Options{Timeout: 5 * time.Second, Perm: 0660, RegisterLogger: log.Logger}
|
||||||
|
var err error
|
||||||
|
DB, err = bstore.Open(mox.Shutdown, p, &opts, DBTypes...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -102,14 +91,12 @@ func Init(refresher bool) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close closes the database.
|
// Close closes the database.
|
||||||
func Close() {
|
func Close() error {
|
||||||
mutex.Lock()
|
if err := DB.Close(); err != nil {
|
||||||
defer mutex.Unlock()
|
return fmt.Errorf("close db: %w", err)
|
||||||
if DB != nil {
|
|
||||||
err := DB.Close()
|
|
||||||
mlog.New("mtastsdb", nil).Check(err, "closing database")
|
|
||||||
DB = nil
|
|
||||||
}
|
}
|
||||||
|
DB = nil
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// lookup looks up a policy for the domain in the database.
|
// lookup looks up a policy for the domain in the database.
|
||||||
|
@ -119,16 +106,11 @@ func Close() {
|
||||||
// Returns ErrNotFound if record is not present.
|
// Returns ErrNotFound if record is not present.
|
||||||
// Returns ErrBackoff if a recent attempt to fetch a record failed.
|
// Returns ErrBackoff if a recent attempt to fetch a record failed.
|
||||||
func lookup(ctx context.Context, log mlog.Log, domain dns.Domain) (*PolicyRecord, error) {
|
func lookup(ctx context.Context, log mlog.Log, domain dns.Domain) (*PolicyRecord, error) {
|
||||||
db, err := database(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if domain.IsZero() {
|
if domain.IsZero() {
|
||||||
return nil, fmt.Errorf("empty domain")
|
return nil, fmt.Errorf("empty domain")
|
||||||
}
|
}
|
||||||
now := timeNow()
|
now := timeNow()
|
||||||
q := bstore.QueryDB[PolicyRecord](ctx, db)
|
q := bstore.QueryDB[PolicyRecord](ctx, DB)
|
||||||
q.FilterNonzero(PolicyRecord{Domain: domain.Name()})
|
q.FilterNonzero(PolicyRecord{Domain: domain.Name()})
|
||||||
q.FilterGreater("ValidEnd", now)
|
q.FilterGreater("ValidEnd", now)
|
||||||
pr, err := q.Get()
|
pr, err := q.Get()
|
||||||
|
@ -139,7 +121,7 @@ func lookup(ctx context.Context, log mlog.Log, domain dns.Domain) (*PolicyRecord
|
||||||
}
|
}
|
||||||
|
|
||||||
pr.LastUse = now
|
pr.LastUse = now
|
||||||
if err := db.Update(ctx, &pr); err != nil {
|
if err := DB.Update(ctx, &pr); err != nil {
|
||||||
log.Errorx("marking cached mta-sts policy as used in database", err)
|
log.Errorx("marking cached mta-sts policy as used in database", err)
|
||||||
}
|
}
|
||||||
if pr.Backoff {
|
if pr.Backoff {
|
||||||
|
@ -151,12 +133,7 @@ func lookup(ctx context.Context, log mlog.Log, domain dns.Domain) (*PolicyRecord
|
||||||
// Upsert adds the policy to the database, overwriting an existing policy for the domain.
|
// Upsert adds the policy to the database, overwriting an existing policy for the domain.
|
||||||
// Policy can be nil, indicating a failure to fetch the policy.
|
// Policy can be nil, indicating a failure to fetch the policy.
|
||||||
func Upsert(ctx context.Context, domain dns.Domain, recordID string, policy *mtasts.Policy, policyText string) error {
|
func Upsert(ctx context.Context, domain dns.Domain, recordID string, policy *mtasts.Policy, policyText string) error {
|
||||||
db, err := database(ctx)
|
return DB.Write(ctx, func(tx *bstore.Tx) error {
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return db.Write(ctx, func(tx *bstore.Tx) error {
|
|
||||||
pr := PolicyRecord{Domain: domain.Name()}
|
pr := PolicyRecord{Domain: domain.Name()}
|
||||||
err := tx.Get(&pr)
|
err := tx.Get(&pr)
|
||||||
if err != nil && err != bstore.ErrAbsent {
|
if err != nil && err != bstore.ErrAbsent {
|
||||||
|
@ -195,11 +172,7 @@ func Upsert(ctx context.Context, domain dns.Domain, recordID string, policy *mta
|
||||||
// PolicyRecords returns all policies in the database, sorted descending by last
|
// PolicyRecords returns all policies in the database, sorted descending by last
|
||||||
// use, domain.
|
// use, domain.
|
||||||
func PolicyRecords(ctx context.Context) ([]PolicyRecord, error) {
|
func PolicyRecords(ctx context.Context) ([]PolicyRecord, error) {
|
||||||
db, err := database(ctx)
|
return bstore.QueryDB[PolicyRecord](ctx, DB).SortDesc("LastUse", "Domain").List()
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return bstore.QueryDB[PolicyRecord](ctx, db).SortDesc("LastUse", "Domain").List()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get retrieves an MTA-STS policy for domain and whether it is fresh.
|
// Get retrieves an MTA-STS policy for domain and whether it is fresh.
|
||||||
|
|
|
@ -51,19 +51,14 @@ func refresh() int {
|
||||||
// jitter to the timing. Each refresh is done in a new goroutine, so a single slow
|
// jitter to the timing. Each refresh is done in a new goroutine, so a single slow
|
||||||
// refresh doesn't mess up the timing.
|
// refresh doesn't mess up the timing.
|
||||||
func refresh1(ctx context.Context, log mlog.Log, resolver dns.Resolver, sleep func(d time.Duration)) (int, error) {
|
func refresh1(ctx context.Context, log mlog.Log, resolver dns.Resolver, sleep func(d time.Duration)) (int, error) {
|
||||||
db, err := database(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
now := timeNow()
|
now := timeNow()
|
||||||
qdel := bstore.QueryDB[PolicyRecord](ctx, db)
|
qdel := bstore.QueryDB[PolicyRecord](ctx, DB)
|
||||||
qdel.FilterLess("LastUse", now.Add(-180*24*time.Hour))
|
qdel.FilterLess("LastUse", now.Add(-180*24*time.Hour))
|
||||||
if _, err := qdel.Delete(); err != nil {
|
if _, err := qdel.Delete(); err != nil {
|
||||||
return 0, fmt.Errorf("deleting old unused policies: %s", err)
|
return 0, fmt.Errorf("deleting old unused policies: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
qup := bstore.QueryDB[PolicyRecord](ctx, db)
|
qup := bstore.QueryDB[PolicyRecord](ctx, DB)
|
||||||
qup.FilterLess("LastUpdate", now.Add(-12*time.Hour))
|
qup.FilterLess("LastUpdate", now.Add(-12*time.Hour))
|
||||||
prs, err := qup.List()
|
prs, err := qup.List()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -89,7 +84,7 @@ func refresh1(ctx context.Context, log mlog.Log, resolver dns.Resolver, sleep fu
|
||||||
log.Debug("will refresh mta-sts policies over next 3 hours", slog.Int("count", len(prs)))
|
log.Debug("will refresh mta-sts policies over next 3 hours", slog.Int("count", len(prs)))
|
||||||
start := timeNow()
|
start := timeNow()
|
||||||
for i, pr := range prs {
|
for i, pr := range prs {
|
||||||
go refreshDomain(ctx, log, db, resolver, pr)
|
go refreshDomain(ctx, log, DB, resolver, pr)
|
||||||
if i < len(prs)-1 {
|
if i < len(prs)-1 {
|
||||||
interval := 3 * int64(time.Hour) / int64(len(prs)-1)
|
interval := 3 * int64(time.Hour) / int64(len(prs)-1)
|
||||||
extra := time.Duration(rand.Int63n(interval) - interval/2)
|
extra := time.Duration(rand.Int63n(interval) - interval/2)
|
||||||
|
|
|
@ -9,7 +9,7 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
golog "log"
|
||||||
"math/big"
|
"math/big"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
@ -39,15 +39,14 @@ func TestRefresh(t *testing.T) {
|
||||||
os.Remove(dbpath)
|
os.Remove(dbpath)
|
||||||
defer os.Remove(dbpath)
|
defer os.Remove(dbpath)
|
||||||
|
|
||||||
if err := Init(false); err != nil {
|
log := mlog.New("mtastsdb", nil)
|
||||||
t.Fatalf("init database: %s", err)
|
|
||||||
}
|
|
||||||
defer Close()
|
|
||||||
|
|
||||||
db, err := database(ctxbg)
|
err := Init(false)
|
||||||
if err != nil {
|
tcheckf(t, err, "init database")
|
||||||
t.Fatalf("database: %s", err)
|
defer func() {
|
||||||
}
|
err := Close()
|
||||||
|
tcheckf(t, err, "close database")
|
||||||
|
}()
|
||||||
|
|
||||||
cert := fakeCert(t, false)
|
cert := fakeCert(t, false)
|
||||||
defer func() {
|
defer func() {
|
||||||
|
@ -70,7 +69,7 @@ func TestRefresh(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
pr := PolicyRecord{domain, time.Time{}, validEnd, lastUpdate, lastUse, backoff, recordID, policy, policy.String()}
|
pr := PolicyRecord{domain, time.Time{}, validEnd, lastUpdate, lastUse, backoff, recordID, policy, policy.String()}
|
||||||
if err := db.Insert(ctxbg, &pr); err != nil {
|
if err := DB.Insert(ctxbg, &pr); err != nil {
|
||||||
t.Fatalf("insert policy: %s", err)
|
t.Fatalf("insert policy: %s", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -114,7 +113,7 @@ func TestRefresh(t *testing.T) {
|
||||||
TLSConfig: &tls.Config{
|
TLSConfig: &tls.Config{
|
||||||
Certificates: []tls.Certificate{cert},
|
Certificates: []tls.Certificate{cert},
|
||||||
},
|
},
|
||||||
ErrorLog: log.New(io.Discard, "", 0),
|
ErrorLog: golog.New(io.Discard, "", 0),
|
||||||
}
|
}
|
||||||
s.ServeTLS(l, "", "")
|
s.ServeTLS(l, "", "")
|
||||||
}()
|
}()
|
||||||
|
@ -136,7 +135,6 @@ func TestRefresh(t *testing.T) {
|
||||||
t.Fatalf("bad sleep duration %v", d)
|
t.Fatalf("bad sleep duration %v", d)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
log := mlog.New("mtastsdb", nil)
|
|
||||||
if n, err := refresh1(ctxbg, log, resolver, sleep); err != nil || n != 3 {
|
if n, err := refresh1(ctxbg, log, resolver, sleep); err != nil || n != 3 {
|
||||||
t.Fatalf("refresh1: err %s, n %d, expected no error, 3", err, n)
|
t.Fatalf("refresh1: err %s, n %d, expected no error, 3", err, n)
|
||||||
}
|
}
|
||||||
|
@ -146,7 +144,7 @@ func TestRefresh(t *testing.T) {
|
||||||
time.Sleep(time.Second / 10) // Give goroutine time to write result, before we cleanup the database.
|
time.Sleep(time.Second / 10) // Give goroutine time to write result, before we cleanup the database.
|
||||||
|
|
||||||
// Should not do any more refreshes and return immediately.
|
// Should not do any more refreshes and return immediately.
|
||||||
q := bstore.QueryDB[PolicyRecord](ctxbg, db)
|
q := bstore.QueryDB[PolicyRecord](ctxbg, DB)
|
||||||
q.FilterNonzero(PolicyRecord{Domain: "policybad.mox.example"})
|
q.FilterNonzero(PolicyRecord{Domain: "policybad.mox.example"})
|
||||||
if _, err := q.Delete(); err != nil {
|
if _, err := q.Delete(); err != nil {
|
||||||
t.Fatalf("delete record that would be refreshed: %v", err)
|
t.Fatalf("delete record that would be refreshed: %v", err)
|
||||||
|
|
|
@ -24,8 +24,6 @@ import (
|
||||||
func TestHookIncoming(t *testing.T) {
|
func TestHookIncoming(t *testing.T) {
|
||||||
acc, cleanup := setup(t)
|
acc, cleanup := setup(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
err := Init()
|
|
||||||
tcheck(t, err, "queue init")
|
|
||||||
|
|
||||||
accret, err := store.OpenAccount(pkglog, "retired")
|
accret, err := store.OpenAccount(pkglog, "retired")
|
||||||
tcheck(t, err, "open account for retired")
|
tcheck(t, err, "open account for retired")
|
||||||
|
@ -119,8 +117,6 @@ func TestHookIncoming(t *testing.T) {
|
||||||
func TestFromIDIncomingDelivery(t *testing.T) {
|
func TestFromIDIncomingDelivery(t *testing.T) {
|
||||||
acc, cleanup := setup(t)
|
acc, cleanup := setup(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
err := Init()
|
|
||||||
tcheck(t, err, "queue init")
|
|
||||||
|
|
||||||
accret, err := store.OpenAccount(pkglog, "retired")
|
accret, err := store.OpenAccount(pkglog, "retired")
|
||||||
tcheck(t, err, "open account for retired")
|
tcheck(t, err, "open account for retired")
|
||||||
|
@ -525,8 +521,6 @@ func TestFromIDIncomingDelivery(t *testing.T) {
|
||||||
func TestHookListFilterSort(t *testing.T) {
|
func TestHookListFilterSort(t *testing.T) {
|
||||||
_, cleanup := setup(t)
|
_, cleanup := setup(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
err := Init()
|
|
||||||
tcheck(t, err, "queue init")
|
|
||||||
|
|
||||||
now := time.Now().Round(0)
|
now := time.Now().Round(0)
|
||||||
h := Hook{0, 0, "fromid", "messageid", "subj", nil, "mjl", "http://localhost", "", false, "delivered", "", now, 0, now, []HookResult{}}
|
h := Hook{0, 0, "fromid", "messageid", "subj", nil, "mjl", "http://localhost", "", false, "delivered", "", now, 0, now, []HookResult{}}
|
||||||
|
@ -534,7 +528,7 @@ func TestHookListFilterSort(t *testing.T) {
|
||||||
h1.Submitted = now.Add(-time.Second)
|
h1.Submitted = now.Add(-time.Second)
|
||||||
h1.NextAttempt = now.Add(time.Minute)
|
h1.NextAttempt = now.Add(time.Minute)
|
||||||
hl := []Hook{h, h, h, h, h, h1}
|
hl := []Hook{h, h, h, h, h, h1}
|
||||||
err = DB.Write(ctxbg, func(tx *bstore.Tx) error {
|
err := DB.Write(ctxbg, func(tx *bstore.Tx) error {
|
||||||
for i := range hl {
|
for i := range hl {
|
||||||
err := hookInsert(tx, &hl[i], now, time.Minute)
|
err := hookInsert(tx, &hl[i], now, time.Minute)
|
||||||
tcheck(t, err, "insert hook")
|
tcheck(t, err, "insert hook")
|
||||||
|
|
|
@ -350,7 +350,9 @@ func Init() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
DB, err = bstore.Open(mox.Shutdown, qpath, &bstore.Options{Timeout: 5 * time.Second, Perm: 0660}, DBTypes...)
|
log := mlog.New("queue", nil)
|
||||||
|
opts := bstore.Options{Timeout: 5 * time.Second, Perm: 0660, RegisterLogger: log.Logger}
|
||||||
|
DB, err = bstore.Open(mox.Shutdown, qpath, &opts, DBTypes...)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
err = DB.Read(mox.Shutdown, func(tx *bstore.Tx) error {
|
err = DB.Read(mox.Shutdown, func(tx *bstore.Tx) error {
|
||||||
return metricHoldUpdate(tx)
|
return metricHoldUpdate(tx)
|
||||||
|
|
|
@ -28,6 +28,7 @@ import (
|
||||||
"github.com/mjl-/mox/dns"
|
"github.com/mjl-/mox/dns"
|
||||||
"github.com/mjl-/mox/mlog"
|
"github.com/mjl-/mox/mlog"
|
||||||
"github.com/mjl-/mox/mox-"
|
"github.com/mjl-/mox/mox-"
|
||||||
|
"github.com/mjl-/mox/mtastsdb"
|
||||||
"github.com/mjl-/mox/smtp"
|
"github.com/mjl-/mox/smtp"
|
||||||
"github.com/mjl-/mox/smtpclient"
|
"github.com/mjl-/mox/smtpclient"
|
||||||
"github.com/mjl-/mox/store"
|
"github.com/mjl-/mox/store"
|
||||||
|
@ -60,6 +61,12 @@ func setup(t *testing.T) (*store.Account, func()) {
|
||||||
mox.Context = ctxbg
|
mox.Context = ctxbg
|
||||||
mox.ConfigStaticPath = filepath.FromSlash("../testdata/queue/mox.conf")
|
mox.ConfigStaticPath = filepath.FromSlash("../testdata/queue/mox.conf")
|
||||||
mox.MustLoadConfig(true, false)
|
mox.MustLoadConfig(true, false)
|
||||||
|
err := Init()
|
||||||
|
tcheck(t, err, "queue init")
|
||||||
|
err = mtastsdb.Init(false)
|
||||||
|
tcheck(t, err, "mtastsdb init")
|
||||||
|
err = tlsrptdb.Init()
|
||||||
|
tcheck(t, err, "tlsrptdb init")
|
||||||
acc, err := store.OpenAccount(log, "mjl")
|
acc, err := store.OpenAccount(log, "mjl")
|
||||||
tcheck(t, err, "open account")
|
tcheck(t, err, "open account")
|
||||||
err = acc.SetPassword(log, "testtest")
|
err = acc.SetPassword(log, "testtest")
|
||||||
|
@ -72,6 +79,10 @@ func setup(t *testing.T) (*store.Account, func()) {
|
||||||
mox.ShutdownCancel()
|
mox.ShutdownCancel()
|
||||||
mox.Shutdown, mox.ShutdownCancel = context.WithCancel(ctxbg)
|
mox.Shutdown, mox.ShutdownCancel = context.WithCancel(ctxbg)
|
||||||
Shutdown()
|
Shutdown()
|
||||||
|
err := mtastsdb.Close()
|
||||||
|
tcheck(t, err, "mtastsdb close")
|
||||||
|
err = tlsrptdb.Close()
|
||||||
|
tcheck(t, err, "tlsrptdb close")
|
||||||
switchStop()
|
switchStop()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -95,8 +106,6 @@ func prepareFile(t *testing.T) *os.File {
|
||||||
func TestQueue(t *testing.T) {
|
func TestQueue(t *testing.T) {
|
||||||
acc, cleanup := setup(t)
|
acc, cleanup := setup(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
err := Init()
|
|
||||||
tcheck(t, err, "queue init")
|
|
||||||
|
|
||||||
idfilter := func(msgID int64) Filter {
|
idfilter := func(msgID int64) Filter {
|
||||||
return Filter{IDs: []int64{msgID}}
|
return Filter{IDs: []int64{msgID}}
|
||||||
|
@ -951,8 +960,6 @@ func checkTLSResults(t *testing.T, policyDomain, expRecipientDomain string, expI
|
||||||
func TestRetiredHooks(t *testing.T) {
|
func TestRetiredHooks(t *testing.T) {
|
||||||
_, cleanup := setup(t)
|
_, cleanup := setup(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
err := Init()
|
|
||||||
tcheck(t, err, "queue init")
|
|
||||||
|
|
||||||
addr, err := smtp.ParseAddress("mjl@mox.example")
|
addr, err := smtp.ParseAddress("mjl@mox.example")
|
||||||
tcheck(t, err, "parse address")
|
tcheck(t, err, "parse address")
|
||||||
|
@ -1193,6 +1200,7 @@ func TestQueueStart(t *testing.T) {
|
||||||
<-done
|
<-done
|
||||||
mox.Shutdown, mox.ShutdownCancel = context.WithCancel(ctxbg)
|
mox.Shutdown, mox.ShutdownCancel = context.WithCancel(ctxbg)
|
||||||
}()
|
}()
|
||||||
|
Shutdown() // DB was opened already. Start will open it again. Just close it before.
|
||||||
err := Start(resolver, done)
|
err := Start(resolver, done)
|
||||||
tcheck(t, err, "queue start")
|
tcheck(t, err, "queue start")
|
||||||
|
|
||||||
|
@ -1284,8 +1292,6 @@ func TestQueueStart(t *testing.T) {
|
||||||
func TestListFilterSort(t *testing.T) {
|
func TestListFilterSort(t *testing.T) {
|
||||||
_, cleanup := setup(t)
|
_, cleanup := setup(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
err := Init()
|
|
||||||
tcheck(t, err, "queue init")
|
|
||||||
|
|
||||||
// insert Msgs. insert RetiredMsgs based on that. call list with filters and sort. filter to select a single. filter to paginate one by one, and in reverse.
|
// insert Msgs. insert RetiredMsgs based on that. call list with filters and sort. filter to select a single. filter to paginate one by one, and in reverse.
|
||||||
|
|
||||||
|
@ -1301,7 +1307,7 @@ func TestListFilterSort(t *testing.T) {
|
||||||
qm1.Queued = now.Add(-time.Second)
|
qm1.Queued = now.Add(-time.Second)
|
||||||
qm1.NextAttempt = now.Add(time.Minute)
|
qm1.NextAttempt = now.Add(time.Minute)
|
||||||
qml := []Msg{qm, qm, qm, qm, qm, qm1}
|
qml := []Msg{qm, qm, qm, qm, qm, qm1}
|
||||||
err = Add(ctxbg, pkglog, "mjl", mf, qml...)
|
err := Add(ctxbg, pkglog, "mjl", mf, qml...)
|
||||||
tcheck(t, err, "add messages to queue")
|
tcheck(t, err, "add messages to queue")
|
||||||
qm1 = qml[len(qml)-1]
|
qm1 = qml[len(qml)-1]
|
||||||
|
|
||||||
|
|
|
@ -10,8 +10,6 @@ import (
|
||||||
func TestSuppression(t *testing.T) {
|
func TestSuppression(t *testing.T) {
|
||||||
_, cleanup := setup(t)
|
_, cleanup := setup(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
err := Init()
|
|
||||||
tcheck(t, err, "queue init")
|
|
||||||
|
|
||||||
l, err := SuppressionList(ctxbg, "bogus")
|
l, err := SuppressionList(ctxbg, "bogus")
|
||||||
tcheck(t, err, "listing suppressions for unknown account")
|
tcheck(t, err, "listing suppressions for unknown account")
|
||||||
|
|
12
serve.go
12
serve.go
|
@ -71,11 +71,15 @@ func start(mtastsdbRefresher, sendDMARCReports, sendTLSReports, skipForkExec boo
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := mtastsdb.Init(mtastsdbRefresher); err != nil {
|
if err := mtastsdb.Init(mtastsdbRefresher); err != nil {
|
||||||
return fmt.Errorf("mtasts init: %s", err)
|
return fmt.Errorf("mtastsdb init: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := tlsrptdb.Init(); err != nil {
|
if err := tlsrptdb.Init(); err != nil {
|
||||||
return fmt.Errorf("tlsrpt init: %s", err)
|
return fmt.Errorf("tlsrptdb init: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := dmarcdb.Init(); err != nil {
|
||||||
|
return fmt.Errorf("dmarcdb init: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
done := make(chan struct{}) // Goroutines for messages and webhooks, and cleaners.
|
done := make(chan struct{}) // Goroutines for messages and webhooks, and cleaners.
|
||||||
|
@ -83,10 +87,6 @@ func start(mtastsdbRefresher, sendDMARCReports, sendTLSReports, skipForkExec boo
|
||||||
return fmt.Errorf("queue start: %s", err)
|
return fmt.Errorf("queue start: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// dmarcdb starts after queue because it may start sending reports through the queue.
|
|
||||||
if err := dmarcdb.Init(); err != nil {
|
|
||||||
return fmt.Errorf("dmarc init: %s", err)
|
|
||||||
}
|
|
||||||
if sendDMARCReports {
|
if sendDMARCReports {
|
||||||
dmarcdb.Start(dns.StrictResolver{Pkg: "dmarcdb"})
|
dmarcdb.Start(dns.StrictResolver{Pkg: "dmarcdb"})
|
||||||
}
|
}
|
||||||
|
|
|
@ -108,7 +108,8 @@ func TestReputation(t *testing.T) {
|
||||||
p := filepath.FromSlash("../testdata/smtpserver-reputation.db")
|
p := filepath.FromSlash("../testdata/smtpserver-reputation.db")
|
||||||
defer os.Remove(p)
|
defer os.Remove(p)
|
||||||
|
|
||||||
db, err := bstore.Open(ctxbg, p, &bstore.Options{Timeout: 5 * time.Second}, store.DBTypes...)
|
opts := bstore.Options{Timeout: 5 * time.Second, RegisterLogger: log.Logger}
|
||||||
|
db, err := bstore.Open(ctxbg, p, &opts, store.DBTypes...)
|
||||||
tcheck(t, err, "open db")
|
tcheck(t, err, "open db")
|
||||||
defer db.Close()
|
defer db.Close()
|
||||||
|
|
||||||
|
|
|
@ -105,11 +105,6 @@ func newTestServer(t *testing.T, configPath string, resolver dns.Resolver) *test
|
||||||
|
|
||||||
ts := testserver{t: t, cid: 1, resolver: resolver, tlsmode: smtpclient.TLSOpportunistic}
|
ts := testserver{t: t, cid: 1, resolver: resolver, tlsmode: smtpclient.TLSOpportunistic}
|
||||||
|
|
||||||
if dmarcdb.EvalDB != nil {
|
|
||||||
dmarcdb.EvalDB.Close()
|
|
||||||
dmarcdb.EvalDB = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
log := mlog.New("smtpserver", nil)
|
log := mlog.New("smtpserver", nil)
|
||||||
mox.Context = ctxbg
|
mox.Context = ctxbg
|
||||||
mox.ConfigStaticPath = configPath
|
mox.ConfigStaticPath = configPath
|
||||||
|
@ -117,7 +112,11 @@ func newTestServer(t *testing.T, configPath string, resolver dns.Resolver) *test
|
||||||
dataDir := mox.ConfigDirPath(mox.Conf.Static.DataDir)
|
dataDir := mox.ConfigDirPath(mox.Conf.Static.DataDir)
|
||||||
os.RemoveAll(dataDir)
|
os.RemoveAll(dataDir)
|
||||||
|
|
||||||
var err error
|
err := dmarcdb.Init()
|
||||||
|
tcheck(t, err, "dmarcdb init")
|
||||||
|
err = tlsrptdb.Init()
|
||||||
|
tcheck(t, err, "tlsrptdb init")
|
||||||
|
|
||||||
ts.acc, err = store.OpenAccount(log, "mjl")
|
ts.acc, err = store.OpenAccount(log, "mjl")
|
||||||
tcheck(t, err, "open account")
|
tcheck(t, err, "open account")
|
||||||
err = ts.acc.SetPassword(log, password0)
|
err = ts.acc.SetPassword(log, password0)
|
||||||
|
@ -136,10 +135,14 @@ func (ts *testserver) close() {
|
||||||
if ts.acc == nil {
|
if ts.acc == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
err := dmarcdb.Close()
|
||||||
|
tcheck(ts.t, err, "dmarcdb close")
|
||||||
|
err = tlsrptdb.Close()
|
||||||
|
tcheck(ts.t, err, "tlsrptdb close")
|
||||||
ts.comm.Unregister()
|
ts.comm.Unregister()
|
||||||
queue.Shutdown()
|
queue.Shutdown()
|
||||||
ts.switchStop()
|
ts.switchStop()
|
||||||
err := ts.acc.Close()
|
err = ts.acc.Close()
|
||||||
tcheck(ts.t, err, "closing account")
|
tcheck(ts.t, err, "closing account")
|
||||||
ts.acc.CheckClosed()
|
ts.acc.CheckClosed()
|
||||||
ts.acc = nil
|
ts.acc = nil
|
||||||
|
|
|
@ -907,7 +907,8 @@ func OpenAccountDB(log mlog.Log, accountDir, accountName string) (a *Account, re
|
||||||
os.MkdirAll(accountDir, 0770)
|
os.MkdirAll(accountDir, 0770)
|
||||||
}
|
}
|
||||||
|
|
||||||
db, err := bstore.Open(context.TODO(), dbpath, &bstore.Options{Timeout: 5 * time.Second, Perm: 0660}, DBTypes...)
|
opts := bstore.Options{Timeout: 5 * time.Second, Perm: 0660, RegisterLogger: log.Logger}
|
||||||
|
db, err := bstore.Open(context.TODO(), dbpath, &opts, DBTypes...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -121,7 +121,8 @@ func TestThreadingUpgrade(t *testing.T) {
|
||||||
|
|
||||||
// Now clear the threading upgrade, and the threading fields and close the account.
|
// Now clear the threading upgrade, and the threading fields and close the account.
|
||||||
// We open the database file directly, so we don't trigger the consistency checker.
|
// We open the database file directly, so we don't trigger the consistency checker.
|
||||||
db, err := bstore.Open(ctxbg, dbpath, &bstore.Options{Timeout: 5 * time.Second, Perm: 0660}, DBTypes...)
|
opts := bstore.Options{Timeout: 5 * time.Second, Perm: 0660, RegisterLogger: log.Logger}
|
||||||
|
db, err := bstore.Open(ctxbg, dbpath, &opts, DBTypes...)
|
||||||
err = db.Write(ctxbg, func(tx *bstore.Tx) error {
|
err = db.Write(ctxbg, func(tx *bstore.Tx) error {
|
||||||
up := Upgrade{ID: 1}
|
up := Upgrade{ID: 1}
|
||||||
err := tx.Delete(&up)
|
err := tx.Delete(&up)
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
package tlsrptdb
|
package tlsrptdb
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"sync"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/mjl-/bstore"
|
"github.com/mjl-/bstore"
|
||||||
|
|
||||||
|
@ -12,7 +16,6 @@ import (
|
||||||
var (
|
var (
|
||||||
ReportDBTypes = []any{Record{}}
|
ReportDBTypes = []any{Record{}}
|
||||||
ReportDB *bstore.DB
|
ReportDB *bstore.DB
|
||||||
mutex sync.Mutex
|
|
||||||
|
|
||||||
// Accessed directly by tlsrptsend.
|
// Accessed directly by tlsrptsend.
|
||||||
ResultDBTypes = []any{TLSResult{}, SuppressAddress{}}
|
ResultDBTypes = []any{TLSResult{}, SuppressAddress{}}
|
||||||
|
@ -21,29 +24,48 @@ var (
|
||||||
|
|
||||||
// Init opens and possibly initializes the databases.
|
// Init opens and possibly initializes the databases.
|
||||||
func Init() error {
|
func Init() error {
|
||||||
if _, err := reportDB(mox.Shutdown); err != nil {
|
if ReportDB != nil || ResultDB != nil {
|
||||||
return err
|
return fmt.Errorf("already initialized")
|
||||||
}
|
}
|
||||||
if _, err := resultDB(mox.Shutdown); err != nil {
|
|
||||||
return err
|
log := mlog.New("tlsrptdb", nil)
|
||||||
|
var err error
|
||||||
|
|
||||||
|
ReportDB, err = openReportDB(mox.Shutdown, log)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("opening report db: %v", err)
|
||||||
|
}
|
||||||
|
ResultDB, err = openResultDB(mox.Shutdown, log)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("opening result db: %v", err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close closes the database connections.
|
func openReportDB(ctx context.Context, log mlog.Log) (*bstore.DB, error) {
|
||||||
func Close() {
|
p := mox.DataDirPath("tlsrpt.db")
|
||||||
log := mlog.New("tlsrptdb", nil)
|
os.MkdirAll(filepath.Dir(p), 0770)
|
||||||
if ResultDB != nil {
|
opts := bstore.Options{Timeout: 5 * time.Second, Perm: 0660, RegisterLogger: log.Logger}
|
||||||
err := ResultDB.Close()
|
return bstore.Open(ctx, p, &opts, ReportDBTypes...)
|
||||||
log.Check(err, "closing result database")
|
}
|
||||||
ResultDB = nil
|
|
||||||
}
|
func openResultDB(ctx context.Context, log mlog.Log) (*bstore.DB, error) {
|
||||||
|
p := mox.DataDirPath("tlsrptresult.db")
|
||||||
mutex.Lock()
|
os.MkdirAll(filepath.Dir(p), 0770)
|
||||||
defer mutex.Unlock()
|
opts := bstore.Options{Timeout: 5 * time.Second, Perm: 0660, RegisterLogger: log.Logger}
|
||||||
if ReportDB != nil {
|
return bstore.Open(ctx, p, &opts, ResultDBTypes...)
|
||||||
err := ReportDB.Close()
|
}
|
||||||
log.Check(err, "closing report database")
|
|
||||||
ReportDB = nil
|
// Close closes the database connections.
|
||||||
}
|
func Close() error {
|
||||||
|
if err := ResultDB.Close(); err != nil {
|
||||||
|
return fmt.Errorf("closing result db: %w", err)
|
||||||
|
}
|
||||||
|
ResultDB = nil
|
||||||
|
|
||||||
|
if err := ReportDB.Close(); err != nil {
|
||||||
|
return fmt.Errorf("closing report db: %w", err)
|
||||||
|
}
|
||||||
|
ReportDB = nil
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,8 +5,6 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
@ -55,21 +53,6 @@ type Record struct {
|
||||||
Report tlsrpt.Report
|
Report tlsrpt.Report
|
||||||
}
|
}
|
||||||
|
|
||||||
func reportDB(ctx context.Context) (rdb *bstore.DB, rerr error) {
|
|
||||||
mutex.Lock()
|
|
||||||
defer mutex.Unlock()
|
|
||||||
if ReportDB == nil {
|
|
||||||
p := mox.DataDirPath("tlsrpt.db")
|
|
||||||
os.MkdirAll(filepath.Dir(p), 0770)
|
|
||||||
db, err := bstore.Open(ctx, p, &bstore.Options{Timeout: 5 * time.Second, Perm: 0660}, ReportDBTypes...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
ReportDB = db
|
|
||||||
}
|
|
||||||
return ReportDB, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddReport adds a TLS report to the database.
|
// AddReport adds a TLS report to the database.
|
||||||
//
|
//
|
||||||
// The report should have come in over SMTP, with a DKIM-validated
|
// The report should have come in over SMTP, with a DKIM-validated
|
||||||
|
@ -82,17 +65,12 @@ func reportDB(ctx context.Context) (rdb *bstore.DB, rerr error) {
|
||||||
//
|
//
|
||||||
// Prometheus metrics are updated only for configured domains.
|
// Prometheus metrics are updated only for configured domains.
|
||||||
func AddReport(ctx context.Context, log mlog.Log, verifiedFromDomain dns.Domain, mailFrom string, hostReport bool, r *tlsrpt.Report) error {
|
func AddReport(ctx context.Context, log mlog.Log, verifiedFromDomain dns.Domain, mailFrom string, hostReport bool, r *tlsrpt.Report) error {
|
||||||
db, err := reportDB(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(r.Policies) == 0 {
|
if len(r.Policies) == 0 {
|
||||||
return fmt.Errorf("no policies in report")
|
return fmt.Errorf("no policies in report")
|
||||||
}
|
}
|
||||||
|
|
||||||
var inserted int
|
var inserted int
|
||||||
return db.Write(ctx, func(tx *bstore.Tx) error {
|
return ReportDB.Write(ctx, func(tx *bstore.Tx) error {
|
||||||
for _, p := range r.Policies {
|
for _, p := range r.Policies {
|
||||||
pp := p.Policy
|
pp := p.Policy
|
||||||
|
|
||||||
|
@ -132,22 +110,13 @@ func AddReport(ctx context.Context, log mlog.Log, verifiedFromDomain dns.Domain,
|
||||||
|
|
||||||
// Records returns all TLS reports in the database.
|
// Records returns all TLS reports in the database.
|
||||||
func Records(ctx context.Context) ([]Record, error) {
|
func Records(ctx context.Context) ([]Record, error) {
|
||||||
db, err := reportDB(ctx)
|
return bstore.QueryDB[Record](ctx, ReportDB).List()
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return bstore.QueryDB[Record](ctx, db).List()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// RecordID returns the report for the ID.
|
// RecordID returns the report for the ID.
|
||||||
func RecordID(ctx context.Context, id int64) (Record, error) {
|
func RecordID(ctx context.Context, id int64) (Record, error) {
|
||||||
db, err := reportDB(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return Record{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
e := Record{ID: id}
|
e := Record{ID: id}
|
||||||
err = db.Get(ctx, &e)
|
err := ReportDB.Get(ctx, &e)
|
||||||
return e, err
|
return e, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -155,12 +124,7 @@ func RecordID(ctx context.Context, id int64) (Record, error) {
|
||||||
// given policy domain. If policy domain is empty, records for all domains are
|
// given policy domain. If policy domain is empty, records for all domains are
|
||||||
// returned.
|
// returned.
|
||||||
func RecordsPeriodDomain(ctx context.Context, start, end time.Time, policyDomain dns.Domain) ([]Record, error) {
|
func RecordsPeriodDomain(ctx context.Context, start, end time.Time, policyDomain dns.Domain) ([]Record, error) {
|
||||||
db, err := reportDB(ctx)
|
q := bstore.QueryDB[Record](ctx, ReportDB)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
q := bstore.QueryDB[Record](ctx, db)
|
|
||||||
var zerodom dns.Domain
|
var zerodom dns.Domain
|
||||||
if policyDomain != zerodom {
|
if policyDomain != zerodom {
|
||||||
q.FilterNonzero(Record{Domain: policyDomain.Name()})
|
q.FilterNonzero(Record{Domain: policyDomain.Name()})
|
||||||
|
|
|
@ -3,14 +3,11 @@ package tlsrptdb
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/mjl-/bstore"
|
"github.com/mjl-/bstore"
|
||||||
|
|
||||||
"github.com/mjl-/mox/dns"
|
"github.com/mjl-/mox/dns"
|
||||||
"github.com/mjl-/mox/mox-"
|
|
||||||
"github.com/mjl-/mox/tlsrpt"
|
"github.com/mjl-/mox/tlsrpt"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -70,33 +67,13 @@ type SuppressAddress struct {
|
||||||
Comment string
|
Comment string
|
||||||
}
|
}
|
||||||
|
|
||||||
func resultDB(ctx context.Context) (rdb *bstore.DB, rerr error) {
|
|
||||||
mutex.Lock()
|
|
||||||
defer mutex.Unlock()
|
|
||||||
if ResultDB == nil {
|
|
||||||
p := mox.DataDirPath("tlsrptresult.db")
|
|
||||||
os.MkdirAll(filepath.Dir(p), 0770)
|
|
||||||
db, err := bstore.Open(ctx, p, &bstore.Options{Timeout: 5 * time.Second, Perm: 0660}, ResultDBTypes...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
ResultDB = db
|
|
||||||
}
|
|
||||||
return ResultDB, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddTLSResults adds or merges all tls results for delivering to a policy domain,
|
// AddTLSResults adds or merges all tls results for delivering to a policy domain,
|
||||||
// on its UTC day to a recipient domain to the database. Results may cause multiple
|
// on its UTC day to a recipient domain to the database. Results may cause multiple
|
||||||
// separate reports to be sent.
|
// separate reports to be sent.
|
||||||
func AddTLSResults(ctx context.Context, results []TLSResult) error {
|
func AddTLSResults(ctx context.Context, results []TLSResult) error {
|
||||||
db, err := resultDB(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
|
|
||||||
err = db.Write(ctx, func(tx *bstore.Tx) error {
|
err := ResultDB.Write(ctx, func(tx *bstore.Tx) error {
|
||||||
for _, result := range results {
|
for _, result := range results {
|
||||||
// Ensure all slices are non-nil. We do this now so all readers will marshal to
|
// Ensure all slices are non-nil. We do this now so all readers will marshal to
|
||||||
// compliant with the JSON schema. And also for consistent equality checks when
|
// compliant with the JSON schema. And also for consistent equality checks when
|
||||||
|
@ -148,102 +125,57 @@ func AddTLSResults(ctx context.Context, results []TLSResult) error {
|
||||||
// Results returns all TLS results in the database, for all policy domains each
|
// Results returns all TLS results in the database, for all policy domains each
|
||||||
// with potentially multiple days. Sorted by RecipientDomain and day.
|
// with potentially multiple days. Sorted by RecipientDomain and day.
|
||||||
func Results(ctx context.Context) ([]TLSResult, error) {
|
func Results(ctx context.Context) ([]TLSResult, error) {
|
||||||
db, err := resultDB(ctx)
|
return bstore.QueryDB[TLSResult](ctx, ResultDB).SortAsc("PolicyDomain", "DayUTC", "RecipientDomain").List()
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return bstore.QueryDB[TLSResult](ctx, db).SortAsc("PolicyDomain", "DayUTC", "RecipientDomain").List()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResultsDomain returns all TLSResults for a policy domain, potentially for
|
// ResultsDomain returns all TLSResults for a policy domain, potentially for
|
||||||
// multiple days.
|
// multiple days.
|
||||||
func ResultsPolicyDomain(ctx context.Context, policyDomain dns.Domain) ([]TLSResult, error) {
|
func ResultsPolicyDomain(ctx context.Context, policyDomain dns.Domain) ([]TLSResult, error) {
|
||||||
db, err := resultDB(ctx)
|
return bstore.QueryDB[TLSResult](ctx, ResultDB).FilterNonzero(TLSResult{PolicyDomain: policyDomain.Name()}).SortAsc("DayUTC", "RecipientDomain").List()
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return bstore.QueryDB[TLSResult](ctx, db).FilterNonzero(TLSResult{PolicyDomain: policyDomain.Name()}).SortAsc("DayUTC", "RecipientDomain").List()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResultsRecipientDomain returns all TLSResults for a recipient domain,
|
// ResultsRecipientDomain returns all TLSResults for a recipient domain,
|
||||||
// potentially for multiple days.
|
// potentially for multiple days.
|
||||||
func ResultsRecipientDomain(ctx context.Context, recipientDomain dns.Domain) ([]TLSResult, error) {
|
func ResultsRecipientDomain(ctx context.Context, recipientDomain dns.Domain) ([]TLSResult, error) {
|
||||||
db, err := resultDB(ctx)
|
return bstore.QueryDB[TLSResult](ctx, ResultDB).FilterNonzero(TLSResult{RecipientDomain: recipientDomain.Name()}).SortAsc("DayUTC", "PolicyDomain").List()
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return bstore.QueryDB[TLSResult](ctx, db).FilterNonzero(TLSResult{RecipientDomain: recipientDomain.Name()}).SortAsc("DayUTC", "PolicyDomain").List()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// RemoveResultsPolicyDomain removes all TLSResults for the policy domain on the
|
// RemoveResultsPolicyDomain removes all TLSResults for the policy domain on the
|
||||||
// day from the database.
|
// day from the database.
|
||||||
func RemoveResultsPolicyDomain(ctx context.Context, policyDomain dns.Domain, dayUTC string) error {
|
func RemoveResultsPolicyDomain(ctx context.Context, policyDomain dns.Domain, dayUTC string) error {
|
||||||
db, err := resultDB(ctx)
|
_, err := bstore.QueryDB[TLSResult](ctx, ResultDB).FilterNonzero(TLSResult{PolicyDomain: policyDomain.Name(), DayUTC: dayUTC}).Delete()
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = bstore.QueryDB[TLSResult](ctx, db).FilterNonzero(TLSResult{PolicyDomain: policyDomain.Name(), DayUTC: dayUTC}).Delete()
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// RemoveResultsRecipientDomain removes all TLSResults for the recipient domain on
|
// RemoveResultsRecipientDomain removes all TLSResults for the recipient domain on
|
||||||
// the day from the database.
|
// the day from the database.
|
||||||
func RemoveResultsRecipientDomain(ctx context.Context, recipientDomain dns.Domain, dayUTC string) error {
|
func RemoveResultsRecipientDomain(ctx context.Context, recipientDomain dns.Domain, dayUTC string) error {
|
||||||
db, err := resultDB(ctx)
|
_, err := bstore.QueryDB[TLSResult](ctx, ResultDB).FilterNonzero(TLSResult{RecipientDomain: recipientDomain.Name(), DayUTC: dayUTC}).Delete()
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = bstore.QueryDB[TLSResult](ctx, db).FilterNonzero(TLSResult{RecipientDomain: recipientDomain.Name(), DayUTC: dayUTC}).Delete()
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// SuppressAdd adds an address to the suppress list.
|
// SuppressAdd adds an address to the suppress list.
|
||||||
func SuppressAdd(ctx context.Context, ba *SuppressAddress) error {
|
func SuppressAdd(ctx context.Context, ba *SuppressAddress) error {
|
||||||
db, err := resultDB(ctx)
|
return ResultDB.Insert(ctx, ba)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return db.Insert(ctx, ba)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SuppressList returns all reporting addresses on the suppress list.
|
// SuppressList returns all reporting addresses on the suppress list.
|
||||||
func SuppressList(ctx context.Context) ([]SuppressAddress, error) {
|
func SuppressList(ctx context.Context) ([]SuppressAddress, error) {
|
||||||
db, err := resultDB(ctx)
|
return bstore.QueryDB[SuppressAddress](ctx, ResultDB).SortDesc("ID").List()
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return bstore.QueryDB[SuppressAddress](ctx, db).SortDesc("ID").List()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SuppressRemove removes a reporting address record from the suppress list.
|
// SuppressRemove removes a reporting address record from the suppress list.
|
||||||
func SuppressRemove(ctx context.Context, id int64) error {
|
func SuppressRemove(ctx context.Context, id int64) error {
|
||||||
db, err := resultDB(ctx)
|
return ResultDB.Delete(ctx, &SuppressAddress{ID: id})
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return db.Delete(ctx, &SuppressAddress{ID: id})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SuppressUpdate updates the until field of a reporting address record.
|
// SuppressUpdate updates the until field of a reporting address record.
|
||||||
func SuppressUpdate(ctx context.Context, id int64, until time.Time) error {
|
func SuppressUpdate(ctx context.Context, id int64, until time.Time) error {
|
||||||
db, err := resultDB(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
ba := SuppressAddress{ID: id}
|
ba := SuppressAddress{ID: id}
|
||||||
err = db.Get(ctx, &ba)
|
err := ResultDB.Get(ctx, &ba)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
ba.Until = until
|
ba.Until = until
|
||||||
return db.Update(ctx, &ba)
|
return ResultDB.Update(ctx, &ba)
|
||||||
}
|
}
|
||||||
|
|
18
vendor/github.com/mjl-/bstore/doc.go
generated
vendored
18
vendor/github.com/mjl-/bstore/doc.go
generated
vendored
|
@ -216,24 +216,24 @@ stays trivial (though not plan9, unfortunately). Although bstore is much more
|
||||||
limited in so many aspects than sqlite, bstore also offers some advantages as
|
limited in so many aspects than sqlite, bstore also offers some advantages as
|
||||||
well. Some points of comparison:
|
well. Some points of comparison:
|
||||||
|
|
||||||
- Cross-compilation and reproducibility: Trivial with bstore due to pure Go,
|
- Cross-compilation and reproducibility: Trivial with bstore due to pure Go,
|
||||||
much harder with sqlite because of cgo.
|
much harder with sqlite because of cgo.
|
||||||
- Code complexity: low with bstore (7k lines including comments/docs), high
|
- Code complexity: low with bstore (7k lines including comments/docs), high
|
||||||
with sqlite.
|
with sqlite.
|
||||||
- Query language: mostly-type-checked function calls in bstore, free-form query
|
- Query language: mostly-type-checked function calls in bstore, free-form query
|
||||||
strings only checked at runtime with sqlite.
|
strings only checked at runtime with sqlite.
|
||||||
- Functionality: very limited with bstore, much more full-featured with sqlite.
|
- Functionality: very limited with bstore, much more full-featured with sqlite.
|
||||||
- Schema management: mostly automatic based on Go type definitions in bstore,
|
- Schema management: mostly automatic based on Go type definitions in bstore,
|
||||||
manual with ALTER statements in sqlite.
|
manual with ALTER statements in sqlite.
|
||||||
- Types and packing/parsing: automatic/transparent in bstore based on Go types
|
- Types and packing/parsing: automatic/transparent in bstore based on Go types
|
||||||
(including maps, slices, structs and custom MarshalBinary encoding), versus
|
(including maps, slices, structs and custom MarshalBinary encoding), versus
|
||||||
manual scanning and parameter passing with sqlite with limited set of SQL
|
manual scanning and parameter passing with sqlite with limited set of SQL
|
||||||
types.
|
types.
|
||||||
- Performance: low to good performance with bstore, high performance with
|
- Performance: low to good performance with bstore, high performance with
|
||||||
sqlite.
|
sqlite.
|
||||||
- Database files: single file with bstore, several files with sqlite (due to
|
- Database files: single file with bstore, several files with sqlite (due to
|
||||||
WAL or journal files).
|
WAL or journal files).
|
||||||
- Test coverage: decent coverage but limited real-world for bstore, versus
|
- Test coverage: decent coverage but limited real-world for bstore, versus
|
||||||
extremely thoroughly tested and with enormous real-world use.
|
extremely thoroughly tested and with enormous real-world use.
|
||||||
*/
|
*/
|
||||||
package bstore
|
package bstore
|
||||||
|
|
21
vendor/github.com/mjl-/bstore/register.go
generated
vendored
21
vendor/github.com/mjl-/bstore/register.go
generated
vendored
|
@ -7,6 +7,7 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
"sort"
|
"sort"
|
||||||
|
@ -50,6 +51,17 @@ var errSchemaCheck = errors.New("schema check")
|
||||||
// to "changed", an error is returned if there is no schema change. If it is set to
|
// to "changed", an error is returned if there is no schema change. If it is set to
|
||||||
// "unchanged", an error is returned if there was a schema change.
|
// "unchanged", an error is returned if there was a schema change.
|
||||||
func (db *DB) Register(ctx context.Context, typeValues ...any) error {
|
func (db *DB) Register(ctx context.Context, typeValues ...any) error {
|
||||||
|
return db.register(ctx, slog.New(discardHandler{}), typeValues...)
|
||||||
|
}
|
||||||
|
|
||||||
|
type discardHandler struct{}
|
||||||
|
|
||||||
|
func (l discardHandler) Enabled(context.Context, slog.Level) bool { return false }
|
||||||
|
func (l discardHandler) Handle(context.Context, slog.Record) error { return nil }
|
||||||
|
func (l discardHandler) WithAttrs(attrs []slog.Attr) slog.Handler { return l }
|
||||||
|
func (l discardHandler) WithGroup(name string) slog.Handler { return l }
|
||||||
|
|
||||||
|
func (db *DB) register(ctx context.Context, log *slog.Logger, typeValues ...any) error {
|
||||||
// We will drop/create new indices as needed. For changed indices, we drop
|
// We will drop/create new indices as needed. For changed indices, we drop
|
||||||
// and recreate. E.g. if an index becomes a unique index, or if a field in
|
// and recreate. E.g. if an index becomes a unique index, or if a field in
|
||||||
// an index changes. These values map type and index name to their index.
|
// an index changes. These values map type and index name to their index.
|
||||||
|
@ -148,6 +160,9 @@ func (db *DB) Register(ctx context.Context, typeValues ...any) error {
|
||||||
tv.Version = 1
|
tv.Version = 1
|
||||||
if st.Current != nil {
|
if st.Current != nil {
|
||||||
tv.Version = st.Current.Version + 1
|
tv.Version = st.Current.Version + 1
|
||||||
|
log.Debug("updating schema for type", slog.String("type", tv.name), slog.Uint64("version", uint64(tv.Version)))
|
||||||
|
} else {
|
||||||
|
log.Debug("adding schema for new type", slog.String("type", tv.name), slog.Uint64("version", uint64(tv.Version)))
|
||||||
}
|
}
|
||||||
k, v, err := packSchema(tv)
|
k, v, err := packSchema(tv)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -299,6 +314,7 @@ func (db *DB) Register(ctx context.Context, typeValues ...any) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
foundField = true
|
foundField = true
|
||||||
|
log.Debug("verifying foreign key constraint for new reference", slog.String("fromtype", ntname), slog.String("fromfield", f.Name), slog.String("totype", name))
|
||||||
|
|
||||||
// For newly added references, check they are valid.
|
// For newly added references, check they are valid.
|
||||||
b, err := tx.recordsBucket(ntname, ntv.fillPercent)
|
b, err := tx.recordsBucket(ntname, ntv.fillPercent)
|
||||||
|
@ -396,6 +412,7 @@ func (db *DB) Register(ctx context.Context, typeValues ...any) error {
|
||||||
db.types[st.Type] = *st
|
db.types[st.Type] = *st
|
||||||
db.typeNames[st.Name] = *st
|
db.typeNames[st.Name] = *st
|
||||||
ntvp = &ntv
|
ntvp = &ntv
|
||||||
|
log.Debug("updating schema for type due to new incoming reference", slog.String("type", name), slog.Uint64("version", uint64(ntv.Version)))
|
||||||
}
|
}
|
||||||
|
|
||||||
k, v, err := packSchema(ntvp)
|
k, v, err := packSchema(ntvp)
|
||||||
|
@ -453,6 +470,7 @@ func (db *DB) Register(ctx context.Context, typeValues ...any) error {
|
||||||
if !drop {
|
if !drop {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
log.Debug("dropping old/modified index", slog.String("type", name), slog.String("indexname", iname))
|
||||||
b, err := tx.typeBucket(name)
|
b, err := tx.typeBucket(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -482,6 +500,7 @@ func (db *DB) Register(ctx context.Context, typeValues ...any) error {
|
||||||
if !create {
|
if !create {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
log.Debug("preparing for new/modified index for type", slog.String("type", name), slog.String("indexname", iname))
|
||||||
b, err := tx.typeBucket(name)
|
b, err := tx.typeBucket(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -578,6 +597,8 @@ func (db *DB) Register(ctx context.Context, typeValues ...any) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Debug("creating new/modified indexes for type", slog.String("type", name))
|
||||||
|
|
||||||
// Now do all sorts + inserts.
|
// Now do all sorts + inserts.
|
||||||
for i, ib := range ibs {
|
for i, ib := range ibs {
|
||||||
idx := idxs[i]
|
idx := idxs[i]
|
||||||
|
|
13
vendor/github.com/mjl-/bstore/store.go
generated
vendored
13
vendor/github.com/mjl-/bstore/store.go
generated
vendored
|
@ -8,6 +8,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
"sync"
|
"sync"
|
||||||
|
@ -285,6 +286,7 @@ type Options struct {
|
||||||
Timeout time.Duration // Abort if opening DB takes longer than Timeout. If not set, the deadline from the context is used.
|
Timeout time.Duration // Abort if opening DB takes longer than Timeout. If not set, the deadline from the context is used.
|
||||||
Perm fs.FileMode // Permissions for new file if created. If zero, 0600 is used.
|
Perm fs.FileMode // Permissions for new file if created. If zero, 0600 is used.
|
||||||
MustExist bool // Before opening, check that file exists. If not, io/fs.ErrNotExist is returned.
|
MustExist bool // Before opening, check that file exists. If not, io/fs.ErrNotExist is returned.
|
||||||
|
RegisterLogger *slog.Logger // For debug logging about schema upgrades.
|
||||||
}
|
}
|
||||||
|
|
||||||
// Open opens a bstore database and registers types by calling Register.
|
// Open opens a bstore database and registers types by calling Register.
|
||||||
|
@ -326,7 +328,16 @@ func Open(ctx context.Context, path string, opts *Options, typeValues ...any) (*
|
||||||
typeNames := map[string]storeType{}
|
typeNames := map[string]storeType{}
|
||||||
types := map[reflect.Type]storeType{}
|
types := map[reflect.Type]storeType{}
|
||||||
db := &DB{bdb: bdb, typeNames: typeNames, types: types}
|
db := &DB{bdb: bdb, typeNames: typeNames, types: types}
|
||||||
if err := db.Register(ctx, typeValues...); err != nil {
|
var log *slog.Logger
|
||||||
|
if opts != nil {
|
||||||
|
log = opts.RegisterLogger
|
||||||
|
}
|
||||||
|
if log == nil {
|
||||||
|
log = slog.New(discardHandler{})
|
||||||
|
} else {
|
||||||
|
log = log.With("dbpath", path)
|
||||||
|
}
|
||||||
|
if err := db.register(ctx, log, typeValues...); err != nil {
|
||||||
bdb.Close()
|
bdb.Close()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
2
vendor/modules.txt
vendored
2
vendor/modules.txt
vendored
|
@ -16,7 +16,7 @@ github.com/mjl-/adns/internal/singleflight
|
||||||
# github.com/mjl-/autocert v0.0.0-20231214125928-31b7400acb05
|
# github.com/mjl-/autocert v0.0.0-20231214125928-31b7400acb05
|
||||||
## explicit; go 1.20
|
## explicit; go 1.20
|
||||||
github.com/mjl-/autocert
|
github.com/mjl-/autocert
|
||||||
# github.com/mjl-/bstore v0.0.5
|
# github.com/mjl-/bstore v0.0.6
|
||||||
## explicit; go 1.19
|
## explicit; go 1.19
|
||||||
github.com/mjl-/bstore
|
github.com/mjl-/bstore
|
||||||
# github.com/mjl-/sconf v0.0.6
|
# github.com/mjl-/sconf v0.0.6
|
||||||
|
|
|
@ -120,7 +120,8 @@ possibly making them potentially no longer readable by the previous version.
|
||||||
checkf(err, path, "reading bolt database")
|
checkf(err, path, "reading bolt database")
|
||||||
bdb.Close()
|
bdb.Close()
|
||||||
|
|
||||||
db, err := bstore.Open(ctxbg, path, nil, types...)
|
opts := bstore.Options{RegisterLogger: c.log.Logger}
|
||||||
|
db, err := bstore.Open(ctxbg, path, &opts, types...)
|
||||||
checkf(err, path, "open database with bstore")
|
checkf(err, path, "open database with bstore")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
|
@ -162,7 +163,8 @@ possibly making them potentially no longer readable by the previous version.
|
||||||
|
|
||||||
// Check that all messages present in the database also exist on disk.
|
// Check that all messages present in the database also exist on disk.
|
||||||
seen := map[string]struct{}{}
|
seen := map[string]struct{}{}
|
||||||
db, err := bstore.Open(ctxbg, dbpath, &bstore.Options{MustExist: true}, queue.DBTypes...)
|
opts := bstore.Options{MustExist: true, RegisterLogger: c.log.Logger}
|
||||||
|
db, err := bstore.Open(ctxbg, dbpath, &opts, queue.DBTypes...)
|
||||||
checkf(err, dbpath, "opening queue database to check messages")
|
checkf(err, dbpath, "opening queue database to check messages")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
err := bstore.QueryDB[queue.Msg](ctxbg, db).ForEach(func(m queue.Msg) error {
|
err := bstore.QueryDB[queue.Msg](ctxbg, db).ForEach(func(m queue.Msg) error {
|
||||||
|
@ -237,7 +239,8 @@ possibly making them potentially no longer readable by the previous version.
|
||||||
// And check consistency of UIDs with the mailbox UIDNext, and check UIDValidity.
|
// And check consistency of UIDs with the mailbox UIDNext, and check UIDValidity.
|
||||||
seen := map[string]struct{}{}
|
seen := map[string]struct{}{}
|
||||||
dbpath := filepath.Join(accdir, "index.db")
|
dbpath := filepath.Join(accdir, "index.db")
|
||||||
db, err := bstore.Open(ctxbg, dbpath, &bstore.Options{MustExist: true}, store.DBTypes...)
|
opts := bstore.Options{MustExist: true, RegisterLogger: c.log.Logger}
|
||||||
|
db, err := bstore.Open(ctxbg, dbpath, &opts, store.DBTypes...)
|
||||||
checkf(err, dbpath, "opening account database to check messages")
|
checkf(err, dbpath, "opening account database to check messages")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
uidvalidity := store.NextUIDValidity{ID: 1}
|
uidvalidity := store.NextUIDValidity{ID: 1}
|
||||||
|
|
|
@ -17,6 +17,7 @@ import (
|
||||||
"github.com/mjl-/mox/dns"
|
"github.com/mjl-/mox/dns"
|
||||||
"github.com/mjl-/mox/mlog"
|
"github.com/mjl-/mox/mlog"
|
||||||
"github.com/mjl-/mox/mox-"
|
"github.com/mjl-/mox/mox-"
|
||||||
|
"github.com/mjl-/mox/mtastsdb"
|
||||||
"github.com/mjl-/mox/queue"
|
"github.com/mjl-/mox/queue"
|
||||||
"github.com/mjl-/mox/store"
|
"github.com/mjl-/mox/store"
|
||||||
)
|
)
|
||||||
|
@ -58,6 +59,8 @@ func TestAPI(t *testing.T) {
|
||||||
defer store.Switchboard()()
|
defer store.Switchboard()()
|
||||||
|
|
||||||
log := mlog.New("webmail", nil)
|
log := mlog.New("webmail", nil)
|
||||||
|
err := mtastsdb.Init(false)
|
||||||
|
tcheck(t, err, "mtastsdb init")
|
||||||
acc, err := store.OpenAccount(log, "mjl")
|
acc, err := store.OpenAccount(log, "mjl")
|
||||||
tcheck(t, err, "open account")
|
tcheck(t, err, "open account")
|
||||||
const pw0 = "te\u0301st \u00a0\u2002\u200a" // NFD and various unicode spaces.
|
const pw0 = "te\u0301st \u00a0\u2002\u200a" // NFD and various unicode spaces.
|
||||||
|
@ -65,7 +68,9 @@ func TestAPI(t *testing.T) {
|
||||||
err = acc.SetPassword(log, pw0)
|
err = acc.SetPassword(log, pw0)
|
||||||
tcheck(t, err, "set password")
|
tcheck(t, err, "set password")
|
||||||
defer func() {
|
defer func() {
|
||||||
err := acc.Close()
|
err := mtastsdb.Close()
|
||||||
|
tcheck(t, err, "mtastsdb close")
|
||||||
|
err = acc.Close()
|
||||||
pkglog.Check(err, "closing account")
|
pkglog.Check(err, "closing account")
|
||||||
acc.CheckClosed()
|
acc.CheckClosed()
|
||||||
}()
|
}()
|
||||||
|
|
Loading…
Reference in a new issue