15 bolt "go.etcd.io/bbolt"
17 "github.com/mjl-/bstore"
19 "github.com/mjl-/mox/dmarcdb"
20 "github.com/mjl-/mox/junk"
21 "github.com/mjl-/mox/moxvar"
22 "github.com/mjl-/mox/mtastsdb"
23 "github.com/mjl-/mox/queue"
24 "github.com/mjl-/mox/store"
25 "github.com/mjl-/mox/tlsrptdb"
28func cmdVerifydata(c *cmd) {
30 c.help = `Verify the contents of a data directory, typically of a backup.
32Verifydata checks all database files to see if they are valid BoltDB/bstore
33databases. It checks that all messages in the database have a corresponding
34on-disk message file and there are no unrecognized files. If option -fix is
35specified, unrecognized message files are moved away. This may be needed after
36a restore, because messages enqueued or delivered in the future may get those
37message sequence numbers assigned and writing the message file would fail.
38Consistency of message/mailbox UID, UIDNEXT and UIDVALIDITY is verified as
41Because verifydata opens the database files, schema upgrades may automatically
42be applied. This can happen if you use a new mox release. It is useful to run
43"mox verifydata" with a new binary before attempting an upgrade, but only on a
44copy of the database files, as made with "mox backup". Before upgrading, make a
45new backup again since "mox verifydata" may have upgraded the database files,
46possibly making them potentially no longer readable by the previous version.
49 c.flag.BoolVar(&fix, "fix", false, "fix fixable problems, such as moving away message files not referenced by their database")
51 // To prevent aborting the upgrade test with v0.0.[45] that had a message with
53 var skipSizeCheck bool
54 c.flag.BoolVar(&skipSizeCheck, "skip-size-check", false, "skip the check for message size")
61 dataDir := filepath.Clean(args[0])
63 ctxbg := context.Background()
65 // Check whether file exists, or rather, that it doesn't not exist. Other errors
66 // will return true as well, so the triggered check can give the details.
67 exists := func(path string) bool {
68 _, err := os.Stat(path)
69 return err == nil || !os.IsNotExist(err)
72 // Check for error. If so, write a log line, including the path, and set fail so we
73 // can warn at the end.
75 checkf := func(err error, path, format string, args ...any) {
80 log.Printf("error: %s: %s: %v", path, fmt.Sprintf(format, args...), err)
83 // When we fix problems, we may have to move files/dirs. We need to ensure the
84 // directory of the destination path exists before we move. We keep track of
85 // created dirs so we don't try to create the same directory all the time.
86 createdDirs := map[string]struct{}{}
87 ensureDir := func(path string) {
88 dir := filepath.Dir(path)
89 if _, ok := createdDirs[dir]; ok {
92 err := os.MkdirAll(dir, 0770)
93 checkf(err, dir, "creating directory")
94 createdDirs[dir] = struct{}{}
97 // Check a database file by opening it with BoltDB and bstore and lightly checking
99 checkDB := func(required bool, path string, types []any) {
100 _, err := os.Stat(path)
101 if !required && err != nil && errors.Is(err, fs.ErrNotExist) {
104 checkf(err, path, "checking if database file exists")
108 bdb, err := bolt.Open(path, 0600, nil)
109 checkf(err, path, "open database with bolt")
113 // Check BoltDB consistency.
114 err = bdb.View(func(tx *bolt.Tx) error {
115 for err := range tx.Check() {
116 checkf(err, path, "bolt database problem")
120 checkf(err, path, "reading bolt database")
121 if err := bdb.Close(); err != nil {
122 log.Printf("closing database file: %v", err)
125 opts := bstore.Options{RegisterLogger: c.log.Logger}
126 db, err := bstore.Open(ctxbg, path, &opts, types...)
127 checkf(err, path, "open database with bstore")
132 if err := db.Close(); err != nil {
133 log.Printf("closing database file: %v", err)
137 err = db.Read(ctxbg, func(tx *bstore.Tx) error {
138 // Check bstore consistency, if it can export all records for all types. This is a
139 // quick way to get bstore to parse all records.
140 types, err := tx.Types()
141 checkf(err, path, "getting bstore types from database")
145 for _, t := range types {
147 err := tx.Records(t, &fields, func(m map[string]any) error {
150 checkf(err, path, "parsing record for type %q", t)
154 checkf(err, path, "checking database file")
157 checkFile := func(dbpath, path string, prefixSize int, size int64) {
158 st, err := os.Stat(path)
159 checkf(err, path, "checking if file exists")
160 if !skipSizeCheck && err == nil && int64(prefixSize)+st.Size() != size {
161 filesize := st.Size()
162 checkf(fmt.Errorf("%s: message size is %d, should be %d (length of MsgPrefix %d + file size %d), see \"mox fixmsgsize\"", path, size, int64(prefixSize)+st.Size(), prefixSize, filesize), dbpath, "checking message size")
166 checkQueue := func() {
167 dbpath := filepath.Join(dataDir, "queue/index.db")
168 checkDB(true, dbpath, queue.DBTypes)
170 // Check that all messages present in the database also exist on disk.
171 seen := map[string]struct{}{}
172 opts := bstore.Options{MustExist: true, RegisterLogger: c.log.Logger}
173 db, err := bstore.Open(ctxbg, dbpath, &opts, queue.DBTypes...)
174 checkf(err, dbpath, "opening queue database to check messages")
176 err := bstore.QueryDB[queue.Msg](ctxbg, db).ForEach(func(m queue.Msg) error {
177 mp := store.MessagePath(m.ID)
178 seen[mp] = struct{}{}
179 p := filepath.Join(dataDir, "queue", mp)
180 checkFile(dbpath, p, len(m.MsgPrefix), m.Size)
183 checkf(err, dbpath, "reading messages in queue database to check files")
186 // Check that there are no files that could be treated as a message.
187 qdir := filepath.Join(dataDir, "queue")
188 err = filepath.WalkDir(qdir, func(qpath string, d fs.DirEntry, err error) error {
189 checkf(err, qpath, "walk")
196 p := qpath[len(qdir)+1:]
200 if _, ok := seen[p]; ok {
203 l := strings.Split(p, string(filepath.Separator))
205 log.Printf("warning: %s: unrecognized file in queue directory, ignoring", qpath)
208 // If it doesn't look like a message number, there is no risk of it being the name
209 // of a message enqueued in the future.
211 if _, err := strconv.ParseInt(l[1], 10, 64); err != nil {
212 log.Printf("warning: %s: unrecognized file in queue directory, ignoring", qpath)
217 checkf(errors.New("may interfere with messages enqueued in the future"), qpath, "unrecognized file in queue directory (use the -fix flag to move it away)")
220 npath := filepath.Join(dataDir, "moved", "queue", p)
222 err = os.Rename(qpath, npath)
223 checkf(err, qpath, "moving queue message file away")
225 log.Printf("warning: moved %s to %s", qpath, npath)
229 checkf(err, qdir, "walking queue directory")
232 // Check an account, with its database file and messages.
233 checkAccount := func(name string) {
234 accdir := filepath.Join(dataDir, "accounts", name)
235 checkDB(true, filepath.Join(accdir, "index.db"), store.DBTypes)
237 jfdbpath := filepath.Join(accdir, "junkfilter.db")
238 jfbloompath := filepath.Join(accdir, "junkfilter.bloom")
239 if exists(jfdbpath) || exists(jfbloompath) {
240 checkDB(true, jfdbpath, junk.DBTypes)
242 // todo: add some kind of check for the bloom filter?
244 // Check that all messages in the database have a message file on disk.
245 // And check consistency of UIDs with the mailbox UIDNext, and check UIDValidity.
246 seen := map[string]struct{}{}
247 dbpath := filepath.Join(accdir, "index.db")
248 opts := bstore.Options{MustExist: true, RegisterLogger: c.log.Logger}
249 db, err := bstore.Open(ctxbg, dbpath, &opts, store.DBTypes...)
250 checkf(err, dbpath, "opening account database to check messages")
252 uidvalidity := store.NextUIDValidity{ID: 1}
253 if err := db.Get(ctxbg, &uidvalidity); err != nil {
254 checkf(err, dbpath, "missing nextuidvalidity")
257 up := store.Upgrade{ID: 1}
258 if err := db.Get(ctxbg, &up); err != nil {
259 log.Printf("warning: %s: getting upgrade record (continuing, but not checking message threading): %v", dbpath, err)
260 } else if up.Threads != 2 {
261 log.Printf("warning: %s: no message threading in database, skipping checks for threading consistency", dbpath)
264 mailboxes := map[int64]store.Mailbox{}
265 err := bstore.QueryDB[store.Mailbox](ctxbg, db).ForEach(func(mb store.Mailbox) error {
266 mailboxes[mb.ID] = mb
268 if mb.UIDValidity >= uidvalidity.Next {
269 checkf(errors.New(`inconsistent uidvalidity for mailbox/account, see "mox fixuidmeta"`), dbpath, "mailbox %q (id %d) has uidvalidity %d >= account nextuidvalidity %d", mb.Name, mb.ID, mb.UIDValidity, uidvalidity.Next)
273 checkf(err, dbpath, "reading mailboxes to check uidnext consistency")
275 mbCounts := map[int64]store.MailboxCounts{}
277 err = bstore.QueryDB[store.Message](ctxbg, db).ForEach(func(m store.Message) error {
278 mb := mailboxes[m.MailboxID]
279 if m.UID >= mb.UIDNext {
280 checkf(errors.New(`inconsistent uidnext for message/mailbox, see "mox fixuidmeta"`), dbpath, "message id %d in mailbox %q (id %d) has uid %d >= mailbox uidnext %d", m.ID, mb.Name, mb.ID, m.UID, mb.UIDNext)
283 if m.ModSeq < m.CreateSeq {
284 checkf(errors.New(`inconsistent modseq/createseq for message`), dbpath, "message id %d in mailbox %q (id %d) has modseq %d < createseq %d", m.ID, mb.Name, mb.ID, m.ModSeq, m.CreateSeq)
287 mc := mbCounts[mb.ID]
288 mc.Add(m.MailboxCounts())
295 checkf(errors.New("mailbox is expunged but message is not"), dbpath, "message id %d is in expunged mailbox %q (id %d)", m.ID, mb.Name, mb.ID)
299 mp := store.MessagePath(m.ID)
300 seen[mp] = struct{}{}
301 p := filepath.Join(accdir, "msg", mp)
302 checkFile(dbpath, p, len(m.MsgPrefix), m.Size)
309 checkf(errors.New(`see "mox reassignthreads"`), dbpath, "message id %d, thread %d in mailbox %q (id %d) has bad threadid", m.ID, m.ThreadID, mb.Name, mb.ID)
311 if len(m.ThreadParentIDs) == 0 {
314 if slices.Contains(m.ThreadParentIDs, m.ID) {
315 checkf(errors.New(`see "mox reassignthreads"`), dbpath, "message id %d, thread %d in mailbox %q (id %d) has itself as thread parent", m.ID, m.ThreadID, mb.Name, mb.ID)
317 for i, pid := range m.ThreadParentIDs {
318 am := store.Message{ID: pid}
319 if err := db.Get(ctxbg, &am); err == bstore.ErrAbsent || err == nil && am.Expunged {
321 } else if err != nil {
322 return fmt.Errorf("get ancestor message: %v", err)
323 } else if !slices.Equal(m.ThreadParentIDs[i+1:], am.ThreadParentIDs) {
324 checkf(errors.New(`see "mox reassignthreads"`), dbpath, "message %d, thread %d has ancestor ids %v, and ancestor at index %d with id %d should have the same tail but has %v", m.ID, m.ThreadID, m.ThreadParentIDs, i, am.ID, am.ThreadParentIDs)
331 checkf(err, dbpath, "reading messages in account database to check files")
334 for _, mb := range mailboxes {
335 // We only check if database doesn't have zero values, i.e. not yet set.
339 if mb.HaveCounts && mb.MailboxCounts != mbCounts[mb.ID] {
340 checkf(errors.New(`wrong mailbox counts, see "mox recalculatemailboxcounts"`), dbpath, "mailbox %q (id %d) has wrong counts %s, should be %s", mb.Name, mb.ID, mb.MailboxCounts, mbCounts[mb.ID])
345 du := store.DiskUsage{ID: 1}
346 err := db.Get(ctxbg, &du)
348 if du.MessageSize != totalSize {
349 checkf(errors.New(`wrong total message size, see mox recalculatemailboxcounts"`), dbpath, "account has wrong total message size %d, should be %d", du.MessageSize, totalSize)
351 } else if err != nil && !errors.Is(err, bstore.ErrAbsent) {
352 checkf(err, dbpath, "get disk usage")
357 // Walk through all files in the msg directory. Warn about files that weren't in
358 // the database as message file. Possibly move away files that could cause trouble.
359 msgdir := filepath.Join(accdir, "msg")
361 // New accounts with messages don't have a msg directory.
364 err = filepath.WalkDir(msgdir, func(msgpath string, d fs.DirEntry, err error) error {
365 checkf(err, msgpath, "walk")
372 p := msgpath[len(msgdir)+1:]
373 if _, ok := seen[p]; ok {
376 l := strings.Split(p, string(filepath.Separator))
378 log.Printf("warning: %s: unrecognized file in message directory, ignoring", msgpath)
382 checkf(errors.New("may interfere with future account messages"), msgpath, "unrecognized file in account message directory (use the -fix flag to move it away)")
385 npath := filepath.Join(dataDir, "moved", "accounts", name, "msg", p)
387 err = os.Rename(msgpath, npath)
388 checkf(err, msgpath, "moving account message file away")
390 log.Printf("warning: moved %s to %s", msgpath, npath)
394 checkf(err, msgdir, "walking account message directory")
397 // Check everything in the "accounts" directory.
398 checkAccounts := func() {
399 accountsDir := filepath.Join(dataDir, "accounts")
400 entries, err := os.ReadDir(accountsDir)
401 checkf(err, accountsDir, "reading accounts directory")
402 for _, e := range entries {
403 // We treat all directories as accounts. When we were backing up, we only verified
404 // accounts from the config and made regular file copies of all other files
405 // (perhaps an old account, but at least not with an open database file). It may
406 // turn out that that account was/is not valid, generating warnings. Better safe
407 // than sorry. It should hopefully get the admin to move away such an old account.
409 checkAccount(e.Name())
411 log.Printf("warning: %s: unrecognized file in accounts directory, ignoring", filepath.Join("accounts", e.Name()))
416 // Check all files, skipping the known files, queue and accounts directories. Warn
417 // about unknown files. Skip a "tmp" directory. And a "moved" directory, we
418 // probably created it ourselves.
419 backupmoxversion := "(unknown)"
420 checkOther := func() {
421 err := filepath.WalkDir(dataDir, func(dpath string, d fs.DirEntry, err error) error {
422 checkf(err, dpath, "walk")
426 if dpath == dataDir {
431 p = p[len(dataDir)+1:]
434 case "auth.db", "dmarcrpt.db", "dmarceval.db", "mtasts.db", "tlsrpt.db", "tlsrptresult.db", "receivedid.key", "lastknownversion":
436 case "acme", "queue", "accounts", "tmp", "moved":
439 buf, err := os.ReadFile(dpath)
440 checkf(err, dpath, "reading moxversion")
442 backupmoxversion = string(buf)
446 log.Printf("warning: %s: unrecognized other file, ignoring", dpath)
449 checkf(err, dataDir, "walking data directory")
452 checkDB(false, filepath.Join(dataDir, "auth.db"), store.AuthDBTypes) // Since v0.0.14.
453 checkDB(true, filepath.Join(dataDir, "dmarcrpt.db"), dmarcdb.ReportsDBTypes)
454 checkDB(false, filepath.Join(dataDir, "dmarceval.db"), dmarcdb.EvalDBTypes) // After v0.0.7.
455 checkDB(true, filepath.Join(dataDir, "mtasts.db"), mtastsdb.DBTypes)
456 checkDB(true, filepath.Join(dataDir, "tlsrpt.db"), tlsrptdb.ReportDBTypes)
457 checkDB(false, filepath.Join(dataDir, "tlsrptresult.db"), tlsrptdb.ResultDBTypes) // After v0.0.7.
462 if backupmoxversion != moxvar.Version {
463 log.Printf("NOTE: The backup was made with mox version %q, while verifydata was run with mox version %q. Database files have probably been modified by running mox verifydata. Make a fresh backup before upgrading.", backupmoxversion, moxvar.Version)
467 log.Fatalf("errors were found")
469 fmt.Printf("%s: OK\n", dataDir)