Email Domain Aliases with Exim 4

https://www.kodiakskorner.com/log/280

As I’ve noted before, I recently moved several of my sites to a server running VestaCP. One of the convenient features of Vesta is the ability to specify any number of domain aliases when setting up a new site. I’ve now learned, however, that Vesta only aliases these domains for web access, not for email.

One of the sites I host has the .com, .net, and .org variants of its domain. Being a not-for-profit organization, we use the .org as our primary domain and do 301 redirects to it on the web, but for historic reasons much of our email comes through several .net addresses, so it was important to me to keep email flowing through this variant.

We currently have around 100 email addresses hosted with this site, most of which are aliases. Since Vesta configures Exim, its standard SMTP server, to check per-domain text files for aliases, it would have been trivial for me to set up additional files for the additional domains. However, since the aliases change on a somewhat-regular basis, I wanted to avoid having to duplicate files. And since the domain name is part of the alias in these files, simple symlinks aren’t an option.

The Internet wasn’t much help here, either. I found several references on how to set up individual redirects on each of the domains, and references on how to create wildcard redirects that send all mail on a domain to a single address on another domain, neither of which works for what I want to do. I simply want to redirect any mail that comes in on one of the secondary domains to the same “local part” on the primary.

Not being an Exim expert, it took some work to get what I wanted. After some trial and error, I finally found a working solution. I’m not sure that this is the best way to handle this, but since the Internet seems to be devoid of this specific solution, I figured I’d share it here. I should also point out that I’m running this on CentOS 6; YMMV with other platforms and configurations.

To start, I created a file, /etc/exim/domain_aliases that simply maps secondary domains to the primary domain to which they should forward:

sudo vi /etc/exim/alias_domains

The contents of this file are as follows:

ourdomain.com: ourdomain.org
ourdomain.net: ourdomain.org

Next, I added a new router to /etc/exim/exim.conf. This can go anywhere after the “begin routers” section of the config as long as it falls before the “begin transports” section.

sudo vi /etc/exim/exim.conf

The configuration to add looks like this:

domain_aliases:
     driver = redirect
     data = ${extract{1}{:}{${lookup{$domain}lsearch{/etc/exim/domain_aliases}{"$local_part@$value"}}}}
     require_files = /etc/exim/domain_aliases

The most important line here is the one that starts with “data.” Since it’s rather complex, I’ll break it down.

The first part of this action is the “extract.” This takes a string, parses it at a delimiter, and returns a result based on whether or not a search string (in this case it’s the incoming domain name) is found. The general syntax of this directive is as follows:

${extract{search_parameter}{delimiter}{search_string}{return_string_success}{return_string_fail}}

* search_parameter is an integer that specifies which parameter position will be checked for the match. In this example, we want to check the first (leftmost) parameter.
* delimiter is the character used as the delimiter between columns. I’m using a colon.
* search_string is the full string being searched. This can be a simple string or, as in this case, a more complex expression. I’ll explain what I’m doing in more detail below.
* return_string_success is returned when a successful match is made. If this parameter is not specified, the result of the match (i.e. the string searched) is returned instead.
* return_string_fail is returned when no match is made. It defaults to an empty string when not specified.

In this implementation a second directive, a lookup, is inserted in place of the search_string. Lookups match a value to a key and take this form:

${lookup{key}search_type{path}{return_string_success}{return_string_fail}}

* key is a string value containing the key we want to find. In this case we pass in $domainwhich holds the domain name to which the incoming message was sent.
* search_type is the type of search we want to do. I’m using lsearch which allows searching in the individual lines of a file.
* path is the absolute path to the text file to be used for the lookup. This is hedomain_aliases file created above.
* return_string_success and return_string_fail work the same way they do in the extract. In this case, if a match on the original domain is found we return a new email address, built from the $local_part (everything to the left of the “@“) of the original email address and the $value returned by the lookup, a variable containing the string result of the match. Again, if no match is made, an empty string is returned.

Together, these two directives provide exam with a new destination address for messages coming in via the secondary domains. Once the match is made, Exim restarts the lookup process, looking now for a handler for the message using the newly returned address.

There is still one more step, however, because Exim needs to know that it is able to accept mail for the secondary domains. To do this, the secondary domains need to be added to Exim’s local_domains and relay_to_domains lists. While there’s a number of ways to do this, I found it easiest to add another reference to my domain_aliases file.

First, find the lines in /etc/exim/exim.conf that start “domainlist local_domains” and “domainlist relay_to_domains.” Under Vesta, they’ll look something like this:

domainlist local_domains = dsearch;/etc/exim/domains/
domainlist relay_to_domains = dsearch;/etc/exim/domains/

Vesta stores each mail domain’s configuration in a directory named with the domain name, so it does a directory search (dsearch) and, if it finds a matching directory, it knows it can handle mail for that domain. We’ll add second option: another lsearch of the domain_aliasesfile, like so:

domainlist local_domains = dsearch;/etc/exim/domains/:lsearch;/etc/exim/domain_aliases
domainlist relay_to_domains = dsearch;/etc/exim/domains/:lsearch;/etc/exim/domain_aliases

Finally reload Exim’s configuration and your secondary domains should start handling mail:

service exim reload

Again, as I mentioned above, I am far from being an expert at configuring Exim, so I could be missing something completely obvious. Still, this seems like it would be a pretty normal thing to do and unless my Google-fu has failed me, it seems not many others are doing it. Until I learn of a better way, this is how I’m configuring my domains. If you know of a better way to handle multiple domains, or you find this helpful, please comment. Exim, like most popular daemons, is a Swiss Army Knife of possibilities. Getting the most from it takes time, patience, and the generosity of those willing to share their struggles.