I recently upgraded Dovecot to 2.4+ and it completely broke my mail server config. A refactor of the configuration language meaning tbings such as %d and %n are gone, replaced by a new object-like variable structure.
After wasting a bit of time fixing it, here is how I got Exim and Dovecot 2.4 working together using local virtual domain files and LMTP for my setup of virtual mail users.
The Global Exim Macros (/etc/exim4/conf.d/main/00_vmail_config)
To keep things neat, I define all of my virtual mail macros in one place. This points Exim to the right domain directories, aliases, password files, and handoff transports:
VMAIL_DELIVERY=dovecot_vmail
VMAIL_DOMAINS=dsearch;/etc/vmail
VMAIL_ALIASES=/etc/vmail/$domain_data/aliases
VMAIL_PASSWD=/etc/vmail/$domain_data/passwd
The Dovecot Layout (/etc/dovecot/local.conf)
Instead of digging through 20 different files in conf.d, I keep everything consolidated in a single file.
Because Exim is configured to handle stripping suffixes before talking to Dovecot, we don’t need any complex regex string-manipulation filters inside our mail_path and protocol lmtp blocks.
# Dovecot config and storage versions required for 2.4+
dovecot_config_version = 2.4.0
dovecot_storage_version = 2.4.0
protocols = imap lmtp
ssl = yes
ssl = required
ssl_server_cert_file = /etc/letsencrypt/live/fullchain.pem
ssl_server_key_file = /etc/letsencrypt/live/privkey.pem
# Let Exim drop affixes so Dovecot keeps paths simple
mail_driver = maildir
mail_path = /srv/vmail/%{user | domain}/%{user | username}
mail_inbox_path = .
# Kept globally so Sieve can still read sub-address envelopes
recipient_delimiter = + -
passdb passwd-file {
auth_username_format = %{user | username}
passwd_file_path = /etc/vmail/%{user | domain}/passwd
}
service auth {
unix_listener auth-client {
mode = 0660
user = Debian-exim
group = Debian-exim
}
unix_listener auth-userdb {
mode = 0660
user = vmail
group = vmail
}
}
userdb passwd-file {
auth_username_format = %{user | username}
passwd_file_path = /etc/vmail/%{user | domain}/passwd
fields {
uid = vmail
gid = vmail
home = /srv/vmail/%{user | domain}/%{user | username}
}
}
# Personal sieve script location
sieve_script personal {
driver = file
path = /srv/vmail/%{user | domain}/%{user | username}/sieve
active_path = /srv/vmail/%{user | domain}/%{user | username}/dovecot.sieve
}
# Default sieve script location
sieve_script default {
type = default
name = default
driver = file
path = /srv/vmail/default.sieve
}
service stats {
unix_listener stats-reader {
user = vmail
group = vmail
mode = 0660
}
unix_listener stats-writer {
user = vmail
group = vmail
mode = 0660
}
}
service lmtp {
inet_listener lmtp {
address = 127.0.0.1
port = 24
}
}
protocol lmtp {
auth_username_format = %{user | username | lower}@%{user | domain | lower}
mail_plugins {
sieve = yes
}
}
The Exim Integration Setup
To pipe arriving messages from Exim straight into this structure, we use a localhost TCP connection for LMTP.
1. The Transport (/etc/exim4/conf.d/transport/20_dovecot_vmail)
We define a transport that talks directly to Dovecot’s loopback port. By explicitly setting rcpt_include_affixes = false, Exim drops trailing suffixes (like +spam) from the local part before transmitting the command string to Dovecot.
dovecot_vmail:
driver = lmtp
hosts = 127.0.0.1
port = 24
batch_max = 200
rcpt_include_affixes = false
delivery_date_add = true
envelope_to_add = false
return_path_add = true
user = vmail
2. Alias Routing (/etc/exim4/conf.d/router/170_vmail_aliases)
Before verifying actual accounts, we run messages through our domain alias list. It detects the + or - suffix variations cleanly and searches your alias database macro:
vmail_aliases:
driver = redirect
domains = VMAIL_DOMAINS
allow_fail
allow_defer
local_part_suffix = +* : -*
local_part_suffix_optional
data = ${lookup{$local_part}lsearch{VMAIL_ALIASES}}
qualify_domain = $domain
3. User Routing & Error Handling (/etc/exim4/conf.d/router/180_vmail_user)
If it is a straight user delivery, Exim validates that the recipient explicitly exists in the domain’s virtual password file (VMAIL_PASSWD). If found, it routes it to our dovecot_vmail LMTP transport. If the user doesn’t exist, we forcefully reject it immediately so our server isn’t processing noise.
vmail_user:
local_part_suffix = +* : -*
local_part_suffix_optional
driver = accept
domains = VMAIL_DOMAINS
local_parts = lsearch;VMAIL_PASSWD
transport = VMAIL_DELIVERY
vmail_no_such_user:
driver = redirect
domains = VMAIL_DOMAINS
allow_fail = true
data = :fail: Unknown user
more = false
The Sieve Caveat
You might notice we kept recipient_delimiter = + - inside the global Dovecot configuration block. Even though Exim strips these headers for filesystem routing, keeping this flag allows Dovecot’s Sieve mail filter plugin to understand sub-address rules. If a user writes a custom script targeting an explicit sub-address rule (e.g., matching the envelope To line containing +spam), Sieve can parse it out successfully.