There is a lot of documentation about how to set up a web server on Debian, but not so much about an email server. Let’s say you want all your registered users to get an email @yourdomain.tld. Let’s see how to do it.
Postfix
First, let’s install postfix, which is the most popular mail server.
apt-get install postfix postfix-mysql
Linux natively provides a simple mail system. Just enter mail root, type your message and end with Ctrl+D. The email will be stored in root’s home directory. The root user can then read its messages by typing mail.
Postfix is an SMTP server and will allow your computer to send or receive emails from the Internet. For example, after setting up postfix, you can write from anywhere an email to john@yourserverip, and it will be stored in john’s home directory on your server.
However we don’t want to create a UNIX user for each email account. We want to have a list of email accounts in a database so we can easily manage them. This is done by using postfix’s virtual hosting system.
Here is how it works. When postfix receives an email, it will first check whether the @domain of the destination address is in its configuration file, under "mydestination". If so, it will deliver the email to the UNIX user, if the user exists. Then it will look whether the @domain is in its configuration file, under "virtual_mailbox_domains". If so, it will search for the user in our database and store the email at the path specified in the database. With this second approach, we don’t have to create a UNIX user for each email account.
Now we’ll configure postfix. Open /etc/postfix/main.cf with vim or whatever text editor you like.
Let’s start by setting “myorigin” to “yourdomain.tld”. If the “john” user sends an email, the source address will be “john@yourdomain.tld”. For “mydestination” just write “localhost”, and for “virtual_mailbox_domains” write “yourdomain.tld”. You can put multiple domains here (separated by comas) or even look for a domains list from your database (same method as we’ll see below). Do not put “yourdomain.tld” in “mydestination”, or it will conflict.
Virtual mailboxes
Whenever the server receives an email targeted at @yourdomain.tld (or one of your virtual domains), it will first look for an alias in virtual_alias_maps. An alias is an alternative name, for example “webmaster@yourdomain” can be set as an alternative name for “john@yourdomain”. This mechanics allows you to easily create redirections, including redirections to multiple addresses (the email will be duplicated) and to external domains.
Since we want to look for these redirections from our database, lets write virtual_alias_maps = mysql:/etc/postfix/virtual_forward.cf. This tells postfix that we want to use the MySQL driver, and the configuration provided in /etc/postfix/virtual_forward.cf. This configuration file has a syntax like this:
hosts = localhost
user =
password =
dbname =
query =
I recommend creating a MySQL user named “email” which will only have access to the tables accessible by your email system. Just write the appropriate username, password, database name, and SQL query in the configuration file. The SQL query simply needs to return rows with one field containing the email address to redirect the email to. In the query, ‘%s’ is replaced by the source email address, ‘%u’ by the ‘user’ part of ‘user@domain’ and ‘%d’ by the ‘domain’ part of ‘user@domain’. If you have a simple table with a “source” column and a “destination” column, your query would be “SELECT destination FROM table WHERE source = ‘%s’”.
If it doesn’t find any alias, postfix will look for a proper mailbox where to store the destination email. This is done again by looking into our database, this time by reading “virtual_mailbox_maps“. Like above, this parameter should be like “mysql:/etc/postfix/virtual_accounts.cf”, and virtual_accounts.cf a file containing the MySQL host, username, password and query. The query should return one row of one column, whose value is the path where to store the email. Note that the value of “virtual_mailbox_base” is prepended to this result. If the value is a file (ie. doesn’t end with ‘/’) then the email is stored in this file, in a standard way. If the result is a directory (ie. ends with ‘/’) then a maildir is created and the email added to it. The latter solution is preferred, since it allows emails to be received and read at the same time.
You should add a “virtual_mailbox_base” directive, with a value like “/var/mail/vhosts”. This is the base directory where all virtual emails will be stored. Personally, I use the email address as a directory name by writing a query like "SELECT CONCAT(email, '/') FROM email_accounts WHERE email = '%s'". Therefore, emails for ‘john@yourdomain.tld’ will be stored into ‘/var/mail/vhosts/john@yourdomain.tld/’.
Finally, you should add virtual_uid_maps = static:5000 and virtual_gid_maps = static:5000. This defines the UNIX user used to write the emails to the directory.
Note that if an alias exists, it will overwrite any proper mailbox. For example if I have a mailbox named “john@mydomain” and a redirection from “john@mydomain” to “somethingelse@otherdomain”, then the mailbox won’t receive anything. You need to add a redirection from “john@mydomain” to “john@mydomain” in your aliases list. The easiest way to do so is to add a union in your aliases’ SQL query.
Conclusion
You need to reload postfix by calling /etc/init.d/postfix reload after modifying one the .cf files, but you don’t need to reload after modifying the database. The database is read each time an email is received, so updates are instant.
To test whether it works, simply call ‘mail someuser@yourdomain.com‘ where ‘someuser@yourdomain.com’ is in the database. The ‘/var/mail/vhosts/someuser@yourdomain.com/new/’ directory should contain the email you just sent. If it doesn’t work, you can find informations in /var/log/mail.err and /var/log/mail.log. The next step to be sure it works is to send an email from an external computer and see if it arrives in the directory.
You may see access errors in your logs. In this case, edit /etc/postfix/master.cf and write ‘n’ at the fifth column for any program that generates an error (for me it was smtp, qmgr, cleanup, rewrite, local and virtual). This will disable the chroot mode. Remember to reload postfix afterwards.
Don’t forget to set the owner of your virtual_accounts.cf and virtual_forward.cf files to postfix, and to forbid reading for anybody else (chmod 700 virtual_*). After all, these files contain your database’s password.
Courier
Now you can send emails to any user whose name is in your database, and they will be stored on the disk. But your users can’t read their emails since they are virtual users. To do so, we’ll set up courier, a POP3/IMAP server.
apt-get install courier-base courier-authlib-mysql courier-imap courier-pop
Let’s start by modifying /etc/courier/authdaemonrc. Change “authmodulelist” to “authmysql”. In /etc/courier/authmysql, put your MySQL server’s host, username and password (remember that you should have a MySQL account dedicated to emails). Don’t bother modifying MYSQL_USER_TABLE or anything below. Instead, just uncomment MYSQL_SELECT_CLAUSE. The query specified here should return some fields (see the text right above in the file). For example, my query is something like “SELECT email, NULL, password, 5000, 5000, '', CONCAT('/var/mail/vhosts/', email), NULL, '' FROM email_accounts WHERE login = CONCAT('$(local_part)', '@', '$(domain)')“.
Whenever a user connects to your POP3/IMAP server, this query will be called to determine the possible accounts. The maildir where the email are stored is returned in the SQL query.
Important: if the maildir doesn’t exist, courier will send an error to your user (contrary to postfix which will simply create the directory). A simple fix is to send a welcome email to the user whenever you create a new account. A more complicated fix is to add a crontab task which will read the database every 5 minutes or so and create the maildirs that don’t exist. Here is an example shell script:
#!/bin/bash
accounts="`mysql -s -N -e "SELECT email FROM table.email_accounts"`"
for element in $accounts
do
if [ ! -d "/var/mail/vhosts/$element" ]
then
echo "Creating $element"
maildirmake /var/mail/vhosts/$element
fi
done
Simply add it to crontab by calling crontab -e -u root and adding:
*/15 * * * * /path/to/script/create_maildirs.sh >>/var/log/create_maildirs_log
Note that a more elaborated script should also destroy any directory whose name is not in the database.
Autoreply
A very common and useful feature is an autoreply system. When one of your users is on vacations and an email is sent to him, an automatic response is sent to inform the sender. The trick (described here) is to create a redirection from the email address to a fake email address. For example, let’s say john is on vacation. We’ll redirect john@yourdomain.tld to john@autoreply.yourdomain.tld. When postfix receives an email for john@yourdomain.tld, it will do the redirection. But we’ll also tell postfix that emails for autoreply.yourdomain.tld should be processed by another program named YAA. And this program will send the automatic response.
First, download YAA and put it somewhere. For the rest of this article, I’ll put it in /usr/local/yaa-0.3.1. You will also need perl to execute this program.
Now create a MySQL table containing our automatic replies. For example, one column for the email, one column for the message, one column for the start date and one column for the ending date. Then edit /etc/postfix/virtual_forward.cf. We’ll add to the SQL request a union: SELECT destination FROM email_forward WHERE source = '%s' UNION SELECT REPLACE(email, '@', '@autoreply.') FROM email_autoreply WHERE email = '%s'. When postfix receives an email for john@yourdomain and john@yourdomain is in the email_autoreply table, then a redirection to john@autoreply.yourdomain will be added.
Don’t forget that aliases overwrite mailboxes. So when setting up an autoreply, the destinator won’t receive anything. To solve this, we’ll add another union to the query: SELECT destination FROM email_forward WHERE source = '%s' UNION SELECT REPLACE(email, '@', '@autoreply.') FROM email_autoreply WHERE email = '%s' UNION SELECT email FROM email_accounts WHERE email = '%s'
Now we’ll tell postfix that emails to autoreply.yourdomain should be handled by YAA. Edit /etc/postfix/main.cf and add “transport_maps = hash:/etc/postfix/transport”. Edit /etc/postfix/transport and add “autoreply.yourdomain yaa”. Note that you can also put a MySQL query here instead of the hash, the same way as we did above. The ‘yaa’ alias must now be added to master.cf. Edit master.cf and add “yaa unix - n n - - pipe flags= user=mail argv=/usr/local/yaa-0.3.1/bin/yaa.pl“. Don’t forget to reload postfix after this.
Rename /usr/local/yaa-0.3.1/conf/yaa.conf.sample to yaa.conf and edit this file. The main section of this configuration file is $lookup_map_query_order. Each element in the table tells YAA where to find the information.
$lookup_map_query_order = {
active => [ 'my_sql_map:active' ],
subject => [ 'my_sql_map:subject' ],
message => [ 'my_sql_map:message' ],
charset => [ 'utf-8' ],
forward => [ '' ]
}
The ‘my_sql_map’ is defined above, in a commented section that you will need to uncomment.
'my_sql_map' => {
'driver' => 'SQL',
'sql_dsn' => 'dbi:mysql:database=xxx;host=localhost',
'sql_username' => "",
'sql_password' => "",
'sql_select' => "SELECT 1 AS active, `text` AS message, 'Automatic response' AS subject FROM email_autoreply WHERE email = %m AND (start IS NULL OR start <= NOW()) AND (end IS NULL OR end >= NOW())",
},
Everything should work fine. If not, you can find some informations in /var/log/mail.log. Beware that in some cases if your SQL query is incorrect, no error message is generated (this happened to me) so check your query if it doesn’t work for no reason.