Troubleshoot Invisible Mail (Stale uidlist Lock)

Symptom

A user reports receiving no new mail, but the server looks healthy:

  • Postfix/LMTP logs show successful deliveries: stored mail into mailbox 'INBOX'

  • The user’s IMAP client logs in without errors

  • Other users receive mail normally

Diagnosis

Compare what the logs claim with what the index knows:

# LMTP says mail was stored today...
kubectl logs -n mailu deploy/<dovecot-deployment> --since=24h | grep "lmtp(<user>)"

# ...but the index finds nothing recent (empty = broken)
kubectl exec -n mailu deploy/<dovecot-deployment> -- \
  doveadm search -u <user> mailbox INBOX SAVEDSINCE <yesterday>

Then check the maildir on disk:

kubectl exec -n mailu deploy/<dovecot-deployment> -- sh -c \
  'ls -lt /mail/<user>/new/ | head; ls -lt /mail/<user>/cur/ | head -3'

Broken state: recent files pile up in new/, the newest file in cur/ is days old, and dovecot-uidlist has a stale modification time.

Confirm the root cause — a leftover dotlock:

kubectl exec -n mailu deploy/<dovecot-deployment> -- \
  ls -la /mail/<user>/dovecot-uidlist.lock

The lock file contains <pid>:<hostname>. Because a container restart keeps the pod hostname while PIDs restart from 1, Dovecot can mistake a dead writer’s lock for a live one (PID reuse) and never breaks it. New mail then never gets UIDs assigned — delivered to disk, invisible to IMAP. IMAP session log lines show long in locks times for the affected user.

Fix

# 1. Remove the stale lock (safe once the owning PID is gone)
kubectl exec -n mailu deploy/<dovecot-deployment> -- \
  rm /mail/<user>/dovecot-uidlist.lock

# 2. Rebuild the index from the maildir (non-destructive)
kubectl exec -n mailu deploy/<dovecot-deployment> -- \
  doveadm force-resync -u <user> INBOX

# 3. Verify the message count increased and recent mail is indexed
kubectl exec -n mailu deploy/<dovecot-deployment> -- \
  doveadm mailbox status -u <user> "messages unseen" INBOX
kubectl exec -n mailu deploy/<dovecot-deployment> -- \
  doveadm search -u <user> mailbox INBOX SAVEDSINCE <incident-date> | wc -l

The user’s client shows the backlog on its next sync.

Check the Whole Fleet

After any dovecot crash or forced restart, scan for further stale locks:

kubectl exec -n mailu deploy/<dovecot-deployment> -- \
  find /mail -maxdepth 2 -name "dovecot-uidlist.lock" -mmin +60 -exec ls -la {} \;

Mail sitting in new/ alone is not proof of breakage — mailboxes that no client ever opens keep mail in new/ legitimately. The stale .lock file plus a frozen dovecot-uidlist mtime is the signal.

Incident Reference

2026-08-24: a dovecot container restart left dovecot-uidlist.lock behind for one user; 53 delivered mails were invisible for two days while LMTP kept logging successful stores. Removing the lock and running force-resync restored visibility within seconds.