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 opts := bstore.Options{RegisterLogger: c.log.Logger}
124 db, err := bstore.Open(ctxbg, path, &opts, types...)
125 checkf(err, path, "open database with bstore")
131 err = db.Read(ctxbg, func(tx *bstore.Tx) error {
132 // Check bstore consistency, if it can export all records for all types. This is a
133 // quick way to get bstore to parse all records.
134 types, err := tx.Types()
135 checkf(err, path, "getting bstore types from database")
139 for _, t := range types {
141 err := tx.Records(t, &fields, func(m map[string]any) error {
144 checkf(err, path, "parsing record for type %q", t)
148 checkf(err, path, "checking database file")
151 checkFile := func(dbpath, path string, prefixSize int, size int64) {
152 st, err := os.Stat(path)
153 checkf(err, path, "checking if file exists")
154 if !skipSizeCheck && err == nil && int64(prefixSize)+st.Size() != size {
155 filesize := st.Size()
156 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")
160 checkQueue := func() {
161 dbpath := filepath.Join(dataDir, "queue/index.db")
162 checkDB(true, dbpath, queue.DBTypes)
164 // Check that all messages present in the database also exist on disk.
165 seen := map[string]struct{}{}
166 opts := bstore.Options{MustExist: true, RegisterLogger: c.log.Logger}
167 db, err := bstore.Open(ctxbg, dbpath, &opts, queue.DBTypes...)
168 checkf(err, dbpath, "opening queue database to check messages")
170 err := bstore.QueryDB[queue.Msg](ctxbg, db).ForEach(func(m queue.Msg) error {
171 mp := store.MessagePath(m.ID)
172 seen[mp] = struct{}{}
173 p := filepath.Join(dataDir, "queue", mp)
174 checkFile(dbpath, p, len(m.MsgPrefix), m.Size)
177 checkf(err, dbpath, "reading messages in queue database to check files")
180 // Check that there are no files that could be treated as a message.
181 qdir := filepath.Join(dataDir, "queue")
182 err = filepath.WalkDir(qdir, func(qpath string, d fs.DirEntry, err error) error {
183 checkf(err, qpath, "walk")
190 p := qpath[len(qdir)+1:]
194 if _, ok := seen[p]; ok {
197 l := strings.Split(p, string(filepath.Separator))
199 log.Printf("warning: %s: unrecognized file in queue directory, ignoring", qpath)
202 // If it doesn't look like a message number, there is no risk of it being the name
203 // of a message enqueued in the future.
205 if _, err := strconv.ParseInt(l[1], 10, 64); err != nil {
206 log.Printf("warning: %s: unrecognized file in queue directory, ignoring", qpath)
211 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)")
214 npath := filepath.Join(dataDir, "moved", "queue", p)
216 err = os.Rename(qpath, npath)
217 checkf(err, qpath, "moving queue message file away")
219 log.Printf("warning: moved %s to %s", qpath, npath)
223 checkf(err, qdir, "walking queue directory")
226 // Check an account, with its database file and messages.
227 checkAccount := func(name string) {
228 accdir := filepath.Join(dataDir, "accounts", name)
229 checkDB(true, filepath.Join(accdir, "index.db"), store.DBTypes)
231 jfdbpath := filepath.Join(accdir, "junkfilter.db")
232 jfbloompath := filepath.Join(accdir, "junkfilter.bloom")
233 if exists(jfdbpath) || exists(jfbloompath) {
234 checkDB(true, jfdbpath, junk.DBTypes)
236 // todo: add some kind of check for the bloom filter?
238 // Check that all messages in the database have a message file on disk.
239 // And check consistency of UIDs with the mailbox UIDNext, and check UIDValidity.
240 seen := map[string]struct{}{}
241 dbpath := filepath.Join(accdir, "index.db")
242 opts := bstore.Options{MustExist: true, RegisterLogger: c.log.Logger}
243 db, err := bstore.Open(ctxbg, dbpath, &opts, store.DBTypes...)
244 checkf(err, dbpath, "opening account database to check messages")
246 uidvalidity := store.NextUIDValidity{ID: 1}
247 if err := db.Get(ctxbg, &uidvalidity); err != nil {
248 checkf(err, dbpath, "missing nextuidvalidity")
251 up := store.Upgrade{ID: 1}
252 if err := db.Get(ctxbg, &up); err != nil {
253 log.Printf("warning: %s: getting upgrade record (continuing, but not checking message threading): %v", dbpath, err)
254 } else if up.Threads != 2 {
255 log.Printf("warning: %s: no message threading in database, skipping checks for threading consistency", dbpath)
258 mailboxes := map[int64]store.Mailbox{}
259 err := bstore.QueryDB[store.Mailbox](ctxbg, db).ForEach(func(mb store.Mailbox) error {
260 mailboxes[mb.ID] = mb
262 if mb.UIDValidity >= uidvalidity.Next {
263 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)
267 checkf(err, dbpath, "reading mailboxes to check uidnext consistency")
269 mbCounts := map[int64]store.MailboxCounts{}
271 err = bstore.QueryDB[store.Message](ctxbg, db).ForEach(func(m store.Message) error {
272 mb := mailboxes[m.MailboxID]
273 if m.UID >= mb.UIDNext {
274 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)
277 if m.ModSeq < m.CreateSeq {
278 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)
281 mc := mbCounts[mb.ID]
282 mc.Add(m.MailboxCounts())
290 mp := store.MessagePath(m.ID)
291 seen[mp] = struct{}{}
292 p := filepath.Join(accdir, "msg", mp)
293 checkFile(dbpath, p, len(m.MsgPrefix), m.Size)
300 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)
302 if len(m.ThreadParentIDs) == 0 {
305 if slices.Contains(m.ThreadParentIDs, m.ID) {
306 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)
308 for i, pid := range m.ThreadParentIDs {
309 am := store.Message{ID: pid}
310 if err := db.Get(ctxbg, &am); err == bstore.ErrAbsent {
312 } else if err != nil {
313 return fmt.Errorf("get ancestor message: %v", err)
314 } else if !slices.Equal(m.ThreadParentIDs[i+1:], am.ThreadParentIDs) {
315 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)
322 checkf(err, dbpath, "reading messages in account database to check files")
325 for _, mb := range mailboxes {
326 // We only check if database doesn't have zero values, i.e. not yet set.
330 if mb.HaveCounts && mb.MailboxCounts != mbCounts[mb.ID] {
331 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])
336 du := store.DiskUsage{ID: 1}
337 err := db.Get(ctxbg, &du)
339 if du.MessageSize != totalSize {
340 checkf(errors.New(`wrong total message size, see mox recalculatemailboxcounts"`), dbpath, "account has wrong total message size %d, should be %d", du.MessageSize, totalSize)
342 } else if err != nil && !errors.Is(err, bstore.ErrAbsent) {
343 checkf(err, dbpath, "get disk usage")
348 // Walk through all files in the msg directory. Warn about files that weren't in
349 // the database as message file. Possibly move away files that could cause trouble.
350 msgdir := filepath.Join(accdir, "msg")
352 // New accounts with messages don't have a msg directory.
355 err = filepath.WalkDir(msgdir, func(msgpath string, d fs.DirEntry, err error) error {
356 checkf(err, msgpath, "walk")
363 p := msgpath[len(msgdir)+1:]
364 if _, ok := seen[p]; ok {
367 l := strings.Split(p, string(filepath.Separator))
369 log.Printf("warning: %s: unrecognized file in message directory, ignoring", msgpath)
373 checkf(errors.New("may interfere with future account messages"), msgpath, "unrecognized file in account message directory (use the -fix flag to move it away)")
376 npath := filepath.Join(dataDir, "moved", "accounts", name, "msg", p)
378 err = os.Rename(msgpath, npath)
379 checkf(err, msgpath, "moving account message file away")
381 log.Printf("warning: moved %s to %s", msgpath, npath)
385 checkf(err, msgdir, "walking account message directory")
388 // Check everything in the "accounts" directory.
389 checkAccounts := func() {
390 accountsDir := filepath.Join(dataDir, "accounts")
391 entries, err := os.ReadDir(accountsDir)
392 checkf(err, accountsDir, "reading accounts directory")
393 for _, e := range entries {
394 // We treat all directories as accounts. When we were backing up, we only verified
395 // accounts from the config and made regular file copies of all other files
396 // (perhaps an old account, but at least not with an open database file). It may
397 // turn out that that account was/is not valid, generating warnings. Better safe
398 // than sorry. It should hopefully get the admin to move away such an old account.
400 checkAccount(e.Name())
402 log.Printf("warning: %s: unrecognized file in accounts directory, ignoring", filepath.Join("accounts", e.Name()))
407 // Check all files, skipping the known files, queue and accounts directories. Warn
408 // about unknown files. Skip a "tmp" directory. And a "moved" directory, we
409 // probably created it ourselves.
410 backupmoxversion := "(unknown)"
411 checkOther := func() {
412 err := filepath.WalkDir(dataDir, func(dpath string, d fs.DirEntry, err error) error {
413 checkf(err, dpath, "walk")
417 if dpath == dataDir {
422 p = p[len(dataDir)+1:]
425 case "dmarcrpt.db", "dmarceval.db", "mtasts.db", "tlsrpt.db", "tlsrptresult.db", "receivedid.key", "lastknownversion":
427 case "acme", "queue", "accounts", "tmp", "moved":
430 buf, err := os.ReadFile(dpath)
431 checkf(err, dpath, "reading moxversion")
433 backupmoxversion = string(buf)
437 log.Printf("warning: %s: unrecognized other file, ignoring", dpath)
440 checkf(err, dataDir, "walking data directory")
443 checkDB(true, filepath.Join(dataDir, "dmarcrpt.db"), dmarcdb.ReportsDBTypes)
444 checkDB(false, filepath.Join(dataDir, "dmarceval.db"), dmarcdb.EvalDBTypes) // After v0.0.7.
445 checkDB(true, filepath.Join(dataDir, "mtasts.db"), mtastsdb.DBTypes)
446 checkDB(true, filepath.Join(dataDir, "tlsrpt.db"), tlsrptdb.ReportDBTypes)
447 checkDB(false, filepath.Join(dataDir, "tlsrptresult.db"), tlsrptdb.ResultDBTypes) // After v0.0.7.
452 if backupmoxversion != moxvar.Version {
453 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)
457 log.Fatalf("errors were found")
459 fmt.Printf("%s: OK\n", dataDir)