Exim and Dovecot with virtual users and LMTP in order to enable SIEVE filtering

Long overdue, I want to use SIEVE in order to have some filtering options for mail. This requires that mail is deliverd by Exim to Dovecot via LMTP.

Decades ago I configured my Exim / Dovecot setup so that I can handle several domains with mailusers that don’t need and don’t have a system login (virtual users).

My legacy setup

I created a folder /var/mail/virtual/ for this purpose and populated it with further subfolders with the names of all $domains that I receive mail for. I then needed to decide for a folder holding all the virutal user mailfolders with its $local_part and went for */var/mail/virtusers/. Within each $domain-folder I now created a symlink to each virtual user mailfolder that should be able to receive mail with this domain. If there is a domain that has the same users as an already existing domain I simply created a simlink to that $domain-folder.

So for a virtual user with $local_part@$domain (e.g. james.t@kirk.org) I have the folder /var/mail/virtusers/james.t/ and /var/mail/virtual/kirk.org/james.t/ where the james.t folder is a symlink to the folder with the same name underneath virtusers.

For delivering mail to these folders I had the following router:

virtual_users_maildir:
      debug_print = "R: virtual_users_maildir for $local_part@$domain"
      driver = redirect
      allow_defer
      allow_fail
      domains = dsearch;/var/mail/virtual
      require_files = /var/mail/virtual/$domain/$local_part/
      data = /var/mail/virtual/$domain_data/$local_part/Maildir/
      directory_transport = address_directory
      pipe_transport   = address_pipe
      file_transport   = address_file
      user = vmail
      group = vmail

The above redirect router expands a list of all valid domains by dsearching all entries beneath /var/mail/virutal/. This folder contains all the domain folders as mentioned above (domains = …). If the current domain is contained in the list, the router continues and checks if there is a file (in this case a directory) with the $local_part name (require_files = …) underneath the $domain folder. Next, the full path including the Maildir is given as redirection data (data = …). As redirection data is a directory, the directory transport will kick in and hand over the job to the address_directory transport.

Maybe James wants to receive mail as captain@kirk.org. This address has a valid $domain but $local_part does not match any of the subfolders under the $domain folder , so next came another redirect router handling aliases:

vdom_aliases:
      debug_print = "R: vdom_aliases for $domain"
      driver = redirect
      allow_defer
      allow_fail
      domains = dsearch;/etc/mail/vdom_aliases
      data = ${expand:${lookup{$local_part}lsearch*@{/etc/mail/vdom_aliases/$domain_data}}}
      retry_use_local_part
      pipe_transport   = address_pipe
      file_transport   = address_file

I got the idea for this setup decades ago from a discussion on the good old debian-administration.org site (now only available through the Internet Archive’s Wayback Machine). This setup is quite handy as all useres and all domains can be realized simply by having the correct folders and symlinks in the filesystem below /var/mail/ thus avoiding the use of any database within the mail setup. Dovecot is instructed to take /var/mail/virtusers/%{user | username}/Maildir as the user’s mail_path and takes care of authentication.

Sieve enteres the stage

Long overdue, I want to use Sieve filters in order to give my mail users some filtering options that they can easily use. Using sieve with Dovecot requires that mail is deliverd to Dovecot via either LDA or LMTP. So I have to give up the direct delivery into the user’s Maildir folder by Exim and let Exim hand over the mail to Dovecot via LMTP.

In order to do that I have to rewrite the virtual_users_maildir router mentioned above and setup a LMTP transport.

The LMTP transport is quite easy:

dovecot_lmtp_udp:
        debug_print = "T: dovecot_lmtp_udp for $local_part@$domain"
        driver = lmtp
        socket = /var/run/dovecot/lmtp
        #maximum number of deliveries per batch, default 1
        batch_max = 200
        #allow suffixes/prefixes (default unset)
        #rcpt_include_affixes
        delivery_date_add
        envelope_to_add
        return_path_add

The virtual_users_maildir router has now to be changed into a virtual_useres_lmtp router while keeping the domain and local part validation via the filesystem structure. Actually, this is also quite straightforward.

virtual_users_lmtp:
      debug_print = "R: virtual_users_lmtp for $local_part@$domain"
      driver = accept
      domains = dsearch;/var/mail/virtual
      require_files = /var/mail/virtual/$domain/$local_part/
      transport = dovecot_lmtp_udp

The vdom_aliases router is still in place unchanged. The new virtual_users_lmtp router still checks if there is a folder with the domain’s name below /var/mail/virtual/ and requires a further folder with the virtual user’s local part below the $domain folder. So I can keep my original legacy setup for validating domains and local_parts. If these two checks are passed, mail is handed over to Dovecot via the dovecot_lmtp_udp transport.

Sieve needs to be activated in Dovecot. I have this in 20-lmtp.conf

protocol lmtp {
  mail_plugins {
    sieve = yes
  }

and this in 10-mail.conf

mail_home = /var/mail/virtusers/%{user | username}
mail_path = /var/mail/virtusers/%{user | username}/Maildir

in order for the ManageSieve server to find the home directories of the mail users.

If you see “Permission denied.” lines in your logfiles for dovecot you might need to check the user that runs the lmtp service.