majorwiki/05-troubleshooting/networking/dovecot-mail-home-maildir-root-phantom-mailboxes.md
MajorLinux 01ae62e621 troubleshooting: Dovecot phantom mailboxes from .dovecot.lda-dupes (mail_home overlapping maildir root)
Document the majormail 2026-06-07 incident: when userdb home == maildir
root, the LDA/Sieve duplicate database (.dovecot.lda-dupes + .locks) lands
inside the mail store and the maildir lister exposes it as phantom
mailboxes ("dovecot.lda-dupes"), logging stat(.../tmp) "Not a directory".
Fix: point home at a non-dotted subdir. Wired into the troubleshooting
index and SUMMARY.
2026-06-07 05:06:43 -04:00

5.3 KiB

title domain category tags status created updated
Dovecot Phantom Mailboxes from .dovecot.lda-dupes (mail_home Overlapping the Maildir Root) troubleshooting networking
dovecot
maildir
mail_home
sieve
lda-dupes
duplicate-database
pigeonhole
phantom-mailbox
published 2026-06-07 2026-06-07

Dovecot Phantom Mailboxes from .dovecot.lda-dupes (mail_home Overlapping the Maildir Root)

Dovecot starts logging errors like this on mailbox LIST, and doveadm mailbox list grows phantom mailboxes named after Dovecot's own control files:

imap(user@example.com): Error: maildir: stat(/var/vmail/example.com/user/.dovecot.lda-dupes/tmp) failed: Not a directory
$ doveadm mailbox list -u user@example.com
INBOX
…
dovecot
dovecot.lda-dupes
dovecot.lda-dupes.locks

Hit on majormail (2026-06-07), the day after switching the global spam Sieve to redirect. Mail delivery was unaffected — purely log noise plus phantom folders a client could see on LIST "*".

Why

The LDA/Sieve duplicate database (.dovecot.lda-dupes, plus a .dovecot.lda-dupes.locks lock dir) is created in the user's home directory. Per the Dovecot maintainer, its location strictly follows the user's home — it is not separately configurable.

If mail_home (the userdb home field) is set equal to the maildir root (mail_path), those control files get written inside the mail store:

mail_path = /var/vmail/%{user|domain}/%{user|username}     # maildir root
userdb static { fields { home = /var/vmail/%{user|domain}/%{user|username} } }   # SAME path — the bug

The maildir++ layout treats every .-prefixed entry in the root as a mailbox folder. So:

  • .dovecot.lda-dupes (a file) → lister stats .dovecot.lda-dupes/tmp"Not a directory" (cosmetic, logged every LIST).
  • .dovecot.lda-dupes.locks (a directory) → opened as a maildir, auto-populated with cur/new/tmp/dovecot-uidlist/dovecot.index.log, and exposed as a real phantom mailbox.

The trigger is anything that exercises duplicate tracking — Sieve redirect (loop-guard), vacation, or the duplicate test. A pure fileinto setup never creates the db, which is why the error can appear suddenly after a Sieve change.

How to confirm

# Phantom mailboxes named after the control files:
doveadm mailbox list -u user@example.com | grep -E '^dovecot'

# Is home the SAME as the maildir root? (the root cause)
doveadm user user@example.com | grep -E 'home|mail_path'
#   home       /var/vmail/example.com/user        <- equals mail_path == bug
#   mail_path  /var/vmail/example.com/user

# The offending control files living inside the maildir root:
ls -la /var/vmail/example.com/user/.dovecot.lda-dupes*
#   -rw------- … .dovecot.lda-dupes          (regular file — the dedup db)
#   drwx------ … .dovecot.lda-dupes.locks    (dir — the lock dir, mis-listed)

Fix

Point home at a path separate from the maildir root. The cleanest low-risk option is a non-dotted subdir of the user dir, so mail_path stays put and no mail migration is needed (a dotted name would just become another phantom folder):

 userdb static {
   fields {
     uid = vmail
     gid = vmail
-    home = /var/vmail/%{user|domain}/%{user|username}
+    home = /var/vmail/%{user|domain}/%{user|username}/home
   }
 }

Then deploy and clean up the stale artifacts:

# 1. Deploy the config change, restart/reload Dovecot.

# 2. Confirm home moved:
doveadm user user@example.com | grep home   # -> /var/vmail/example.com/user/home

# 3. Remove the stale dupe-db + the cached list index from the maildir root
#    (all regenerable):
cd /var/vmail/example.com/user/
rm -rf .dovecot.lda-dupes .dovecot.lda-dupes.locks dovecot.list.index dovecot.list.index.log

# 4. Pre-create the new home (so the first dupe-db write can't fail):
install -d -o vmail -g vmail -m 700 /var/vmail/example.com/user/home

# 5. Verify:
doveadm mailbox list -u user@example.com | grep -E '^dovecot' || echo CLEAN

The duplicate db now regenerates under …/user/home/, where the maildir lister never looks.

Gotchas

  • mail_home follows userdb. A userdb-returned home field overrides the global mail_home setting, so fix it where userdb defines it (here, userdb static { fields { home = … } }).
  • What else keys off ~: personal Sieve (~/.dovecot.sieve, ~/sieve), mail_attribute_dict, and some quota backends. Before moving home, confirm none of those hold live data in the old location (ls -a the maildir root). A global spam Sieve at a fixed path (/etc/dovecot/sieve/global/…) is unaffected.
  • Indexes default to mail_path, not home, so moving home doesn't touch dovecot.index*.
  • Don't trust a local-injection test to exercise Sieve redirect: Postfix cleanup header_checks may intercept it first, and dovecot-lda may not apply the same before-script as LMTP. Verify the relocation at the authoritative level (doveadm user home), since the db location is home-relative by design.
  • postfix-header-checks-vs-milter-headers — the spam-routing migration that introduced the Sieve redirect (and thus the dupe db) on majormail.
  • Upstream: Dovecot mailing-list thread "Change location where .dovecot.lda-dupes* file/dir are created" — maintainer confirms the db follows the user's home.