package main import ( "errors" "io/fs" "os" "path/filepath" "time" ) // MaildirStats are message counts and storage for one mailbox tree. type MaildirStats struct { Messages int64 // files in cur+new of the INBOX maildir Total int64 // files in cur+new across all mailboxes (incl. subfolders) Storage int64 // total bytes across all mailboxes Newest time.Time // mtime of the newest message file } // statMaildir walks one user's Maildir (e.g. /var/mail/produktor.io/info) // and counts messages the same way doveadm reports them: every regular file // inside a mailbox's cur/ or new/ directory is one message; tmp/ holds // transient uploads and is ignored. doveadm.index.* files live next to the // maildir, never in cur/ or new/, so they do not pollute the count. func statMaildir(root string) (MaildirStats, error) { var st MaildirStats if _, err := os.Stat(root); err != nil { if errors.Is(err, fs.ErrNotExist) { return st, nil // mailbox not on disk yet: report zero, not error } return st, err } err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error { if err != nil { return err } name := d.Name() if d.IsDir() && (name == "cur" || name == "new") { n, size, newest, err := countMaildirFiles(path) if err != nil { return err } st.Total += n st.Storage += size if newest.After(st.Newest) { st.Newest = newest } if filepath.Dir(path) == root { st.Messages += n // INBOX maildir sits directly under the user root } return filepath.SkipDir // cur/new contain only message files } return nil }) if err != nil { return st, err } if st.Newest.IsZero() { st.Newest = time.Unix(0, 0) } return st, nil } // countMaildirFiles counts message files in one cur/ or new/ directory and // sums their sizes. Only regular files count (dovecot never places anything // else in cur/new); the newest mtime is returned for sorting by recency. func countMaildirFiles(dir string) (n, size int64, newest time.Time, err error) { entries, err := os.ReadDir(dir) if err != nil { return 0, 0, time.Time{}, err } for _, e := range entries { if e.IsDir() { continue } info, err := e.Info() if err != nil { return 0, 0, time.Time{}, err } if !info.Mode().IsRegular() { continue } n++ size += info.Size() if info.ModTime().After(newest) { newest = info.ModTime() } } return n, size, newest, nil }