Random Musings...

Dovecot 2.4 Migration: New Config Syntax with Exim LMTP Integration

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.