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.
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 |
|
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 onLIST "*".
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 withcur/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_homefollows userdb. A userdb-returnedhomefield overrides the globalmail_homesetting, 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 -athe 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 touchdovecot.index*. - Don't trust a local-injection test to exercise Sieve
redirect: Postfixcleanupheader_checks may intercept it first, anddovecot-ldamay not apply the same before-script as LMTP. Verify the relocation at the authoritative level (doveadm userhome), since the db location is home-relative by design.
Related
- 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.