It is 2am. You restored a user's mailbox from last night's backup, listed the directory, counted the files — everything is there, all 5,123 messages. You close the ticket. By 6am the same user has reopened it: Thunderbird spent twenty minutes re-syncing, their phone burned through a chunk of mobile data, and every message in the account now shows as unread. Nothing was lost. And yet the restore failed.
The one-line diagnosis: UIDVALIDITY changed. Files being present is not the same thing as the mailbox being preserved. A Maildir is not a bag of message files — it is three independent layers of state, and a naive rsync -a quietly destroys the one layer that IMAP clients depend on to trust their local cache.
The three layers of Maildir state
When you back up a Maildir, three separate things have to survive, and they live in completely different places.
- Layer 1 — Flags. Whether a message is Seen, Replied, Flagged, Draft, Trashed or Passed is not stored in any database. It is appended to the message *filename* after the
:2,info marker. A file named1719878400.M451P2231.mx1,S=8241:2,FRSis Flagged + Replied + Seen. Rename the file and you have silently changed the message's state. The,S=8241is a size hint baked into the same name. - Layer 2 — Identity.
dovecot-uidlistis the authoritative UID→filename map. Its header line carries the UIDVALIDITY, the next UID to hand out, and the mailbox GUID. Alongside it, a zero-bytedovecot-uidvalidity.<hex>file encodes the UIDVALIDITY value in its own name. This layer is what makes a message *the same message* to every client that has ever touched the account. - Layer 3 — Performance.
dovecot.index,dovecot.index.loganddovecot.index.cacheare derived, regenerable acceleration structures. They speed up FETCH and SEARCH, they are disposable, and across a restore you should *not* trust them — rebuild them from layers 1 and 2 instead.
Layers 1 and 2 are irreplaceable. Layer 3 is garbage you can regenerate on demand. Almost every broken restore comes from a tool that preserves layer 1 (the filenames come along for free), ignores layer 3 (fine), and mangles layer 2 (fatal).
Why UIDVALIDITY is the one number that matters
RFC 3501 defines a hard contract: the pair (UIDVALIDITY, UID) is a permanent, unique identity for a message within a mailbox. A client caches messages keyed on that pair. As long as UIDVALIDITY is stable, the client trusts that UID 4711 is the same message it saw yesterday — read state, local flags, offline copy and all.
The moment UIDVALIDITY changes, the spec *requires* the client to throw its entire local cache for that mailbox on the floor and re-fetch from scratch. That is not a bug in Thunderbird or iOS Mail; it is the client obeying the protocol. And the cost is real:
- Full re-download of every message body the client had cached — brutal on mobile data, and on a server already under IMAP load.
- Loss of any client-side flag changes that had not yet synced upstream.
- Broken offline drafts, and a notification storm as thousands of "new" messages arrive.
The value lives in two places that must agree: the hex-encoded dovecot-uidvalidity.5a3f... filename, and the V field in the dovecot-uidlist header. A good restore keeps that number byte-identical. If your restore cannot promise that, it is not a backup of the mailbox — it is a backup of the message bodies with the mailbox thrown away.
Here is the trap. If dovecot-uidlist is missing or corrupt when Dovecot opens the mailbox, it does the only thing it can: re-scan the directory, re-mint UIDs in filesystem order, and bump UIDVALIDITY. Flags survive — they were in the filenames — but every client resyncs. That is exactly the 2am ticket.
Filesystem backups done right — and where they bite
File-level copies are fine for cold data: a stopped service, a point-in-time snapshot, or a one-shot migration where nothing is delivering. On a live box they are a gamble, for two reasons — the live-copy race, and flag/ownership fidelity.
Start with fidelity. rsync -a is not enough. -a does not carry POSIX ACLs or extended attributes, and it maps ownership by name — but on this stack mail files are literally uid/gid 5000, which may not resolve to the same name on the backup host. You need:
# WRONG for a mail store — silently drops ACLs/xattrs, may remap ownership
rsync -a /var/mail/vhosts/ /backup/vhosts/
# RIGHT — hardlinks, ACLs, xattrs, numeric ownership; never rename files
rsync -aHAX --numeric-ids --delete \
--exclude 'tmp/' \
/var/mail/vhosts/ /backup/vhosts/-H keeps hardlinks (some delivery setups hardlink between mailboxes), -A ACLs, -X xattrs, --numeric-ids pins the raw 5000:5000 so ownership does not silently break. And you exclude `tmp/` — it holds in-flight deliveries; anything there older than 36 hours is stale by convention and should never be restored.
Then the race. Copying a running Maildir mid-delivery can capture a half-written dovecot-uidlist: the message file lands but the uidlist entry does not, or vice versa, and you restore an internally inconsistent mailbox. The fix on a hot box is a point-in-time snapshot — freeze the filesystem, copy from the frozen image, discard it.
# Point-in-time consistent image of a live mail volume
lvcreate -s -n mail_snap -L 10G /dev/vg0/mail
mount -o ro /dev/vg0/mail_snap /mnt/snap
rsync -aHAX --numeric-ids --delete --exclude 'tmp/' \
/mnt/snap/vhosts/ /backup/vhosts/
umount /mnt/snap && lvremove -y /dev/vg0/mail_snapDone this way, a file backup is *correct* — but notice what you had to buy to get there: exact flags, exact ownership, an excluded tmp/, and a filesystem freeze. Get any one wrong and you are back to a re-minted uidlist.
The right way: doveadm backup (dsync)
For anything live and anything you actually care about, stop copying files. Use dsync via doveadm backup. It does not walk the filesystem — it operates through Dovecot's mail-storage layer, so it transports UIDs, UIDVALIDITY, flags, keywords, GUIDs and modseq as first-class data. It is order-independent and storage-format-independent: you can dsync a Maildir source into an mdbox target and every client-visible identity still holds.
Two directions matter. doveadm sync is two-way and *will modify the source*. doveadm backup is strictly one-way and never touches the source — which is precisely what a backup must guarantee. Always use backup for backups.
# One-way backup to a local Maildir target
doveadm backup -u [email protected] maildir:/backup/[email protected]
# One-way backup to a remote server over SSH
doveadm backup -u [email protected] \
ssh [email protected] doveadm dsync-server -u [email protected]It is incremental after the first run and noticeably heavier than rsync — it is talking to the storage engine, not read()-ing bytes. That cost buys the one thing rsync cannot promise: a restore where UIDVALIDITY comes back unchanged, because dsync carried it as data instead of hoping the filesystem preserved it.
Restoring — and proving it worked
Restoring a file-level backup. Copy the tree back, fix ownership, then rebuild *only* the disposable index layer. force-resync regenerates layer 3 from the filenames and uidlist; it does not renumber UIDs:
rsync -aHAX --numeric-ids --exclude 'tmp/' \
/backup/vhosts/evilmail.pro/user/ \
/var/mail/vhosts/evilmail.pro/user/
chown -R 5000:5000 /var/mail/vhosts/evilmail.pro/user
doveadm force-resync -u [email protected] '*'Restoring a dsync backup. Run doveadm backup in the reverse direction, from the backup store into the live user. Same tool, same guarantees.
Then comes the step everyone skips and everyone regrets: verify UIDVALIDITY held. Do not eyeball the file count and close the ticket.
doveadm mailbox status -u [email protected] uidvalidity messages unseen '*'
# INBOX uidvalidity=1687000123 messages=5123 unseen=4
# Sent uidvalidity=1687000124 messages=812 unseen=0Compare uidvalidity against what you recorded before the incident. A match means clients do a quiet incremental sync and move on. A changed number means the uidlist did not survive — Dovecot re-minted UIDs — and you have found your problem *before* the user does. For a per-message spot check:
doveadm fetch -u [email protected] 'uid flags' mailbox INBOXPre-flight and restore checklist
- Quiesce or LVM-snapshot before any file-level copy — never rsync a mailbox mid-delivery.
- Confirm
dovecot-uidlistis present in the backup for every mailbox; it is the identity layer, not an accessory. - Preserve ownership as raw 5000:5000 with
--numeric-ids; a name-mapped chown silently breaks access. - Always
--exclude 'tmp/', and purge tmp/ files older than 36h. - On restore, run
doveadm force-resync, never a full reindex that could renumber — rebuild layer 3, keep layers 1 and 2. - Record UIDVALIDITY before, assert it is unchanged after, with
doveadm mailbox status. This is the acceptance test. - Prefer
doveadm backup(one-way, never mutates source) overdoveadm syncfor anything labeled "backup."


