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")
123 db, err := bstore.Open(ctxbg, path, nil, types...)
124 checkf(err, path, "open database with bstore")
130 err = db.Read(ctxbg, func(tx *bstore.Tx) error {
131 // Check bstore consistency, if it can export all records for all types. This is a
132 // quick way to get bstore to parse all records.
133 types, err := tx.Types()
134 checkf(err, path, "getting bstore types from database")
138 for _, t := range types {
140 err := tx.Records(t, &fields, func(m map[string]any) error {
143 checkf(err, path, "parsing record for type %q", t)
147 checkf(err, path, "checking database file")
150 checkFile := func(dbpath, path string, prefixSize int, size int64) {
151 st, err := os.Stat(path)
152 checkf(err, path, "checking if file exists")
153 if !skipSizeCheck && err == nil && int64(prefixSize)+st.Size() != size {
154 filesize := st.Size()
155 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")
159 checkQueue := func() {
160 dbpath := filepath.Join(dataDir, "queue/index.db")
161 checkDB(true, dbpath, queue.DBTypes)
163 // Check that all messages present in the database also exist on disk.
164 seen := map[string]struct{}{}
165 db, err := bstore.Open(ctxbg, dbpath, &bstore.Options{MustExist: true}, queue.DBTypes...)
166 checkf(err, dbpath, "opening queue database to check messages")
168 err := bstore.QueryDB[queue.Msg](ctxbg, db).ForEach(func(m queue.Msg) error {
169 mp := store.MessagePath(m.ID)
170 seen[mp] = struct{}{}
171 p := filepath.Join(dataDir, "queue", mp)
172 checkFile(dbpath, p, len(m.MsgPrefix), m.Size)
175 checkf(err, dbpath, "reading messages in queue database to check files")
178 // Check that there are no files that could be treated as a message.
179 qdir := filepath.Join(dataDir, "queue")
180 err = filepath.WalkDir(qdir, func(qpath string, d fs.DirEntry, err error) error {
181 checkf(err, qpath, "walk")
188 p := qpath[len(qdir)+1:]
192 if _, ok := seen[p]; ok {
195 l := strings.Split(p, string(filepath.Separator))
197 log.Printf("warning: %s: unrecognized file in queue directory, ignoring", qpath)
200 // If it doesn't look like a message number, there is no risk of it being the name
201 // of a message enqueued in the future.
203 if _, err := strconv.ParseInt(l[1], 10, 64); err != nil {
204 log.Printf("warning: %s: unrecognized file in queue directory, ignoring", qpath)
209 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)")
212 npath := filepath.Join(dataDir, "moved", "queue", p)
214 err = os.Rename(qpath, npath)
215 checkf(err, qpath, "moving queue message file away")
217 log.Printf("warning: moved %s to %s", qpath, npath)
221 checkf(err, qdir, "walking queue directory")
224 // Check an account, with its database file and messages.
225 checkAccount := func(name string) {
226 accdir := filepath.Join(dataDir, "accounts", name)
227 checkDB(true, filepath.Join(accdir, "index.db"), store.DBTypes)
229 jfdbpath := filepath.Join(accdir, "junkfilter.db")
230 jfbloompath := filepath.Join(accdir, "junkfilter.bloom")
231 if exists(jfdbpath) || exists(jfbloompath) {
232 checkDB(true, jfdbpath, junk.DBTypes)
234 // todo: add some kind of check for the bloom filter?
236 // Check that all messages in the database have a message file on disk.
237 // And check consistency of UIDs with the mailbox UIDNext, and check UIDValidity.
238 seen := map[string]struct{}{}
239 dbpath := filepath.Join(accdir, "index.db")
240 db, err := bstore.Open(ctxbg, dbpath, &bstore.Options{MustExist: true}, store.DBTypes...)
241 checkf(err, dbpath, "opening account database to check messages")
243 uidvalidity := store.NextUIDValidity{ID: 1}
244 if err := db.Get(ctxbg, &uidvalidity); err != nil {
245 checkf(err, dbpath, "missing nextuidvalidity")
248 up := store.Upgrade{ID: 1}
249 if err := db.Get(ctxbg, &up); err != nil {
250 log.Printf("warning: %s: getting upgrade record (continuing, but not checking message threading): %v", dbpath, err)
251 } else if up.Threads != 2 {
252 log.Printf("warning: %s: no message threading in database, skipping checks for threading consistency", dbpath)
255 mailboxes := map[int64]store.Mailbox{}
256 err := bstore.QueryDB[store.Mailbox](ctxbg, db).ForEach(func(mb store.Mailbox) error {
257 mailboxes[mb.ID] = mb
259 if mb.UIDValidity >= uidvalidity.Next {
260 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)
264 checkf(err, dbpath, "reading mailboxes to check uidnext consistency")
266 mbCounts := map[int64]store.MailboxCounts{}
268 err = bstore.QueryDB[store.Message](ctxbg, db).ForEach(func(m store.Message) error {
269 mb := mailboxes[m.MailboxID]
270 if m.UID >= mb.UIDNext {
271 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)
274 if m.ModSeq < m.CreateSeq {
275 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)
278 mc := mbCounts[mb.ID]
279 mc.Add(m.MailboxCounts())
287 mp := store.MessagePath(m.ID)
288 seen[mp] = struct{}{}
289 p := filepath.Join(accdir, "msg", mp)
290 checkFile(dbpath, p, len(m.MsgPrefix), m.Size)
297 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)
299 if len(m.ThreadParentIDs) == 0 {
302 if slices.Contains(m.ThreadParentIDs, m.ID) {
303 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)
305 for i, pid := range m.ThreadParentIDs {
306 am := store.Message{ID: pid}
307 if err := db.Get(ctxbg, &am); err == bstore.ErrAbsent {
309 } else if err != nil {
310 return fmt.Errorf("get ancestor message: %v", err)
311 } else if !slices.Equal(m.ThreadParentIDs[i+1:], am.ThreadParentIDs) {
312 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)
319 checkf(err, dbpath, "reading messages in account database to check files")
322 for _, mb := range mailboxes {
323 // We only check if database doesn't have zero values, i.e. not yet set.
327 if mb.HaveCounts && mb.MailboxCounts != mbCounts[mb.ID] {
328 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])
333 du := store.DiskUsage{ID: 1}
334 err := db.Get(ctxbg, &du)
336 if du.MessageSize != totalSize {
337 checkf(errors.New(`wrong total message size, see mox recalculatemailboxcounts"`), dbpath, "account has wrong total message size %d, should be %d", du.MessageSize, totalSize)
339 } else if err != nil && !errors.Is(err, bstore.ErrAbsent) {
340 checkf(err, dbpath, "get disk usage")
345 // Walk through all files in the msg directory. Warn about files that weren't in
346 // the database as message file. Possibly move away files that could cause trouble.
347 msgdir := filepath.Join(accdir, "msg")
349 // New accounts with messages don't have a msg directory.
352 err = filepath.WalkDir(msgdir, func(msgpath string, d fs.DirEntry, err error) error {
353 checkf(err, msgpath, "walk")
360 p := msgpath[len(msgdir)+1:]
361 if _, ok := seen[p]; ok {
364 l := strings.Split(p, string(filepath.Separator))
366 log.Printf("warning: %s: unrecognized file in message directory, ignoring", msgpath)
370 checkf(errors.New("may interfere with future account messages"), msgpath, "unrecognized file in account message directory (use the -fix flag to move it away)")
373 npath := filepath.Join(dataDir, "moved", "accounts", name, "msg", p)
375 err = os.Rename(msgpath, npath)
376 checkf(err, msgpath, "moving account message file away")
378 log.Printf("warning: moved %s to %s", msgpath, npath)
382 checkf(err, msgdir, "walking account message directory")
385 // Check everything in the "accounts" directory.
386 checkAccounts := func() {
387 accountsDir := filepath.Join(dataDir, "accounts")
388 entries, err := os.ReadDir(accountsDir)
389 checkf(err, accountsDir, "reading accounts directory")
390 for _, e := range entries {
391 // We treat all directories as accounts. When we were backing up, we only verified
392 // accounts from the config and made regular file copies of all other files
393 // (perhaps an old account, but at least not with an open database file). It may
394 // turn out that that account was/is not valid, generating warnings. Better safe
395 // than sorry. It should hopefully get the admin to move away such an old account.
397 checkAccount(e.Name())
399 log.Printf("warning: %s: unrecognized file in accounts directory, ignoring", filepath.Join("accounts", e.Name()))
404 // Check all files, skipping the known files, queue and accounts directories. Warn
405 // about unknown files. Skip a "tmp" directory. And a "moved" directory, we
406 // probably created it ourselves.
407 backupmoxversion := "(unknown)"
408 checkOther := func() {
409 err := filepath.WalkDir(dataDir, func(dpath string, d fs.DirEntry, err error) error {
410 checkf(err, dpath, "walk")
414 if dpath == dataDir {
419 p = p[len(dataDir)+1:]
422 case "dmarcrpt.db", "dmarceval.db", "mtasts.db", "tlsrpt.db", "tlsrptresult.db", "receivedid.key", "lastknownversion":
424 case "acme", "queue", "accounts", "tmp", "moved":
427 buf, err := os.ReadFile(dpath)
428 checkf(err, dpath, "reading moxversion")
430 backupmoxversion = string(buf)
434 log.Printf("warning: %s: unrecognized other file, ignoring", dpath)
437 checkf(err, dataDir, "walking data directory")
440 checkDB(true, filepath.Join(dataDir, "dmarcrpt.db"), dmarcdb.ReportsDBTypes)
441 checkDB(false, filepath.Join(dataDir, "dmarceval.db"), dmarcdb.EvalDBTypes) // After v0.0.7.
442 checkDB(true, filepath.Join(dataDir, "mtasts.db"), mtastsdb.DBTypes)
443 checkDB(true, filepath.Join(dataDir, "tlsrpt.db"), tlsrptdb.ReportDBTypes)
444 checkDB(false, filepath.Join(dataDir, "tlsrptresult.db"), tlsrptdb.ResultDBTypes) // After v0.0.7.
449 if backupmoxversion != moxvar.Version {
450 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)
454 log.Fatalf("errors were found")
456 fmt.Printf("%s: OK\n", dataDir)