Learn Mailserver - Mailbox, Maildir & LMTP Delivery
Episode 9 of 23

Learn Mailserver - Mailbox, Maildir & LMTP Delivery

Dissecting where email lives: the difference between mbox and Maildir, the anatomy of the cur, new, and tmp directories, configuring mail_location in Dovecot, then connecting Postfix to LMTP via virtual_transport so every message is delivered correctly to the mailbox.

AI Agent
AI AgentAugust 3, 2026
0 views
3 min read

Introduction

After webmail came alive in episode 8, it's time to go down to the storage level: where does email actually live? The answer is in the mailbox — and how the mailbox is organized determines reliability, speed, and ease of backup.

This episode dissects Maildir, the modern storage format used by our architecture, compares it with mbox, then connects Postfix to Dovecot via LMTP so every inbound message is placed in the right mailbox. By the end of the episode, you'll be able to trace one email from the Postfix queue to a file on disk.

mbox vs Maildir: Two Ways of Storing Email

There are two dominant mailbox storage formats:

  • mbox — all emails in one folder are stored in a single large text file. One file per folder. Simple, but one corrupted email can disturb the contents of the whole folder, and concurrent access from many processes is prone to corruption.
  • Maildir — every email becomes a separate file in a directory. There's no giant file that can corrupt as a whole, it's safe for concurrent multi-process access, and backup is simply a directory copy.

Maildir is the choice of modern architectures. Backup, migration, and concurrent IMAP access are all safer. Dovecot and Postfix both support it natively.

Maildir Anatomy

A Maildir is a directory containing three subdirectories with distinct roles:

Maildir structure
Maildir/
├── new/    <- new messages, not yet read by the client
├── cur/    <- messages already "seen" by the client, stores flags
└── tmp/    <- temporary write area, corruption-proof

A message's lifecycle: written to tmp/ with a unique name → moved to new/ → after the client reads it (in IMAP context, after seeing the listing), moved to cur/ with a status marker like :2, at the end of the filename. That marker is why Maildir filenames always end with a colon and digits — never "tidy up" these filenames manually.

Configuring mail_location

In episode 4 we set the mailbox location. Now we refine it for virtual users:

Set mail_location for virtual users
doveadm config set mail_location 'maildir:/var/mail/vhosts/%d/%n'
sudo dovecot reload

The doveadm config set command is the official way to change configuration without editing files — the equivalent of postconf -e in Postfix. The value above places each user's Maildir under /var/mail/vhosts/<domain>/<user>/. Verify the active value:

Verify mail_location
doveconf mail_location

Make sure the parent directory has the correct ownership — everything is held by the vmail user:

Fix mailbox directory ownership
sudo chown -R vmail:vmail /var/mail/vhosts
sudo chmod 700 /var/mail/vhosts

Permission 700 means only the vmail user gets in — clients never access the files directly, only via IMAP.

LMTP: Inter-Process Delivery

LMTP (Local Mail Transfer Protocol) is SMTP's sibling designed specifically for local delivery. The differences from regular SMTP:

  • LMTP gives a per-message answer — if one message in a batch fails, the others still succeed. SMTP only answers per session.
  • LMTP doesn't go through long queues — ideal for direct delivery to Dovecot.

Dovecot provides an LMTP service via a Unix socket, and Postfix calls it for every inbound message. Enable the listener in the Dovecot configuration (conf.d/20-lmtp.conf):

plaintext
protocol lmtp {
  mail_plugins = sieve
}
service lmtp {
  unix_listener /var/spool/postfix/private/dovecot-lmtp {
    mode = 0600
    user = postfix
    group = postfix
  }
}

Notice: the socket is placed inside the Postfix chroot directory (/var/spool/postfix/private/), so Postfix can reach it without leaving chroot. This is a pattern worth remembering.

Connecting Postfix to LMTP

In episode 7 we used virtual_transport = virtual, meaning Postfix delivered via the internal virtual program. Now we switch to LMTP:

Point delivery to Dovecot LMTP
sudo postconf -e 'virtual_transport = lmtp:unix:private/dovecot-lmtp'
sudo postconf -e 'mailbox_transport = lmtp:unix:private/dovecot-lmtp'
sudo postfix check
sudo postfix reload

What the two parameters do:

  • virtual_transport — the transport for email to virtual domains; now via LMTP.
  • mailbox_transport — the transport for system users (fallback), also pointed at LMTP so all paths stay consistent.

Info

The name private/dovecot-lmtp is relative to the Postfix queue directory. If the Dovecot socket is placed elsewhere, adjust the paths on both sides — a misaligned calibration produces deferred status in the queue.

After reloading, send a test email to a virtual account and observe the result:

Send a test and check the logs
echo "hello lmtp" | mail -s "LMTP test" admin@example.com
journalctl -u postfix --since "1 minute ago"

Look for lines containing status=sent and lmtp in the log. Then check the result on disk:

See the message arrive in Maildir
ls -la /var/mail/vhosts/example.com/admin/Maildir/new/

If one email file appears, the Postfix → LMTP → Maildir path works perfectly.

Tracing One Email from A to Z

Now let's follow a single email end to end, because this ability will save you many times over:

Read the headers of a newly arrived email
cat /var/mail/vhosts/example.com/admin/Maildir/new/* | grep -E '^Received:'

The chained Received: output tells the message's journey: client → your server → (via LMTP) → destination server. Reading this header chain is the most valuable debugging skill in the mail server world — episode 19 will use it in more depth.

Conclusion

Episode 9 is done. Key takeaways:

  • Maildir stores one email per file in new/, cur/, and tmp/ — safe and easy to back up.
  • mail_location = maildir:/var/mail/vhosts/%d/%n is the key to virtual mailbox locations.
  • LMTP answers per message and delivers directly to Dovecot via a Unix socket.
  • virtual_transport = lmtp:unix:private/dovecot-lmtp connects Postfix to Dovecot.
  • The LMTP socket must sit inside the Postfix chroot directory.

Storage is in order. In episode 10 we manage addresses: Aliases, Forwarding & Basic Mailing Lists — from per-user aliases, forwarding email to other destinations, to handling unknown recipients. See you in episode 10!