Howto: ISP-style Email Server with Debian-Etch and Postfix 2.3

Title:Howto: ISP-style Email Server with Debian-Etch and Postfix
Authors: Dipl.-Inform. Christoph Haas
License:(C) 2007 Christoph Haas. This document is published under the terms of the GNU General Public License (
Source:The most current version of this document can be found at If you feel you need to mirror this document then please keep your copy up to date. Otherwise you are causing the author a lot of headaches because readers stumble upon problems that have likely been dealt with in a newer version of this document.
Our sponsor:

German translation


You surely know the big web hosters that allow you to rent a domain and use it to receive emails. If you have a Debian computer connected permanently to the internet you can offer the same service. You do not even need to have a fixed IP address thanks to dynamic DNS services like All you need is this document, a pot of delicious tea and a little time. When you are done with the setup your server will be able to...

  • receive and store emails for your users
  • let your users retrieve the email through IMAP and POP3 - even with SSL to encrypt to connection
  • receive and forward (relay) email for your users if they are authenticated
  • offer a webmail interface to read emails in a web browser
  • detect most spam emails and filter them out or tag them

This document is not a simple copy-and-paste tutorial. The intention is rather to make you understand the different components that you are using. There is deliberately no script that will do all the setup automatically. But in the end you will be skilled enough to debug problems yourself. If you feel you need help with your setup then read the hints on Troubleshooting. The setup in this tutorial has been tested very thoroughly by several readers. Unlike many other Postfix tutorials on the internet this is already the third edition. Writing this tutorial took a month of work so these are not just quick draft notes thrown together but a consistent document guiding you.

The configuration described here is not very complicated but still needs to be done carefully. You are expected to have at least basic knowledge of:

  • MySQL (creating a database, granting access to users, basic SQL queries)
  • SMTP (what it is and what a basic SMTP dialog looks like)
  • POP3, IMAP (what they do and what the differences are)
  • basic Postfix configuration (understand the default settings in your, have read through the basic configuration document and know that your mail log file is at /var/log/mail.log)
  • Debian/Linux (general system administration, using a text editor, reading log files)

Migrating from the Sarge Tutorial

The "ispmail" tutorial is maintained since the ancient times of Debian "Woody". You may have followed former tutorials and now want to know how to upgrade to Etch properly. It is hard to provide exact instructions on what steps to go through. And you will definitely have a downtime unless you have multiple mail servers so you can migrate them one-by-one. Some basic information:

  • The mailbox format and path stays the same. (Phew.)
  • Courier POP/IMAP is replaced by Dovecot.
  • SASL is no longer done through auxprop. It uses the Dovecot authentication setup thus saving you time.
  • Passwords are now encrypted in the database.
  • The virtual delivery agent is replaced by Dovecot's LDA.
  • The database schema has changed completely. But there is a conversion script available to help you migrate your database.

Also you should definitely read Etch's release notes before attempting to upgrade your system from Sarge to Etch.

Please note that when switching from Courier to Dove your users will have to subscribe to their IMAP folders again. The subscription information is not migrated.

The Components

The configuration described here uses these components:

  • Postfix for receiving incoming emails from the internet and storing them to the users' mailboxes on the harddisk. And for receiving emails from your users that are sent out to the internet (relaying)
  • Dovecot to allow your users to get their emails into their email client through POP3 and IMAP
  • Squirrelmail as a webmail interface (although any IMAP capable webmail interface will do)
  • MySQL as the database system that stores information about your domains, the user accounts and email forwardings
  • AMaViS for scanning incoming emails for viruses, spam and unwanted attachments

Virtual Domains in a Database

Have you ever wondered how internet service providers (ISPs) maintain their thousands of domains they receive mail for? There is surely nobody entering all these domains and aliases into a text configuration file manually. Postfix offers a better way to handle such domains and accounts by means of virtual domains and virtual users.

In addition to local users (those being listed in your /etc/passwd) Postfix can handle any number of virtual users on virtual domains. Virtual users cannot log into your computer and they have neither a user ID nor a home directory. They just exist in the database. But if you connect Postfix to the database those users can suddenly receive emails. Postfix can work on a list of those virtual users and deliver emails to any directory you like. This would look like:

Virtual user Virtual mailbox location /var/mail/ /var/mail/ /var/mail/

You also need to tell Postfix which virtual domains you want to use. If a domain is not used on your system then Postfix will reject emails. This would be a list of domains:

Virtual domain Just some dummy string banana daiquiri rose garden

So far the information could have been written into a text file. But you can imagine that this will become confusing quickly. Fortunately Postfix can also get this information from other sources like LDAP or SQL databases. So I am using SQL database tables here.

You have now seen that a mapping assigns one value to another. If you query a database you need to tell Postfix which two columns you mean. This is done through 'cf' files as documented at or through "man 5 mysql_table".

Example file:

# Information on how to connect to your MySQL server
user = someone
password = some_password
hosts =

# The database name on the servers.
dbname = mailserver

# The SQL query template.
query = SELECT destination FROM virtual_aliases WHERE source='%s'

This file defines the way that Postfix can access data from your database. It would be suitable for a virtual_alias_maps mapping. Imagine you saved the above lines into a file /etc/postfix/ Then the following line in your would make Postfix query the database:

virtual_alias_maps = mysql:/etc/postfix/

How does this work? Imagine that Postfix is about to send an email to and wants to check the virtual alias map. Postfix then opens up a connection to the MySQL server at the IP address and authenticates to the MySQL server with the username someone and the password some_password. It selects the database mailserver and finally runs a query:

SELECT destination FROM virtual_aliases WHERE source=''

Let us assume this query returns several results:

That would be equal if you used a text file with aliases like this:,,

So much as a quick introduction on how mappings are used with databases.

Virtual domains versus local domains

It is important to understand the three different kinds of domains that Postfix knows. Most of the "it does not work" emails result from people mixing virtual and local domains. A domain is either a...

local domain

All domains listed as mydestination in your are treated as local domains. Your default domain (/etc/defaultdomain) is usually configured as a local domain. Emails for local domains are delivered to system users (those you list in /etc/passwd). The mails will be delivered to /var/mail by default. You should consider using at least "localhost" as a local domain so that you can always receive mails for root@localhost. Imagine your database server has problems and sends that to your root account but your root account is on a virtual domain. Think of it as a safety net.

virtual alias domain

Domains listed as virtual_alias_domains can be used for forwarding ("aliasing") email from an email address to another email address (or multiple addresses). Virtual alias domains do not receive email for any users. They only forward mail somewhere else.

The virtual_alias_maps mapping contains forwardings (source, destination) of users or domains to other email addresses or whole domains. Incidentally virtual_alias_maps also works for local email addresses, too. So you do not really need virtual alias domains as you can declare all domains as virtual mailbox domains and use virtual alias maps for aliases.

virtual mailbox domain

The most interesting domain type in this tutorial is the virtual mailbox domain. Such domains are listed in virtual_mailbox_domains and they will receive email for virtual users and store the email to mailboxes on your hard disk.

The virtual_mailbox_maps parameter tells Postfix where the mailbox directory is located on the hard disk for a certain user. The path is relative to the virtual_mailbox_base directory which is unset by default.


A domain must only be listed in one of these three categories. Getting this wrong will lead to warnings and unpredictable behavior.

If you want to declare all domains as virtual mailbox domains you may wonder what you still need local domains for. You may at least want to set:

mydestination = localhost

so that you can send email to root@localhost for example.

We recommend you also betimes read the upstream documentation on virtual domains also known as the VIRTUAL_README.

Step 1: Installing the needed Debian packages

Now that you hopefully understood the basic concepts of virtual domains, mappings and database accesses it is time to get your hands dirty. Become root on your server and make sure that your /etc/hostname contains the host name without the domain part. The file /etc/mailname is supposed to contain the fully-qualified host name with the domain part.

Your /etc/hosts likely also needs to be fixed. Run hostname --fqdn and see if you get the fully-qualified hostname. If you just get the hostname without the domain please check that your /etc/hosts file has the fully-qualified hostname first in the list.

Wrong:   mailserver42

Right: mailserver42

Then begin by installing Postfix with MySQL map support:

$> aptitude install postfix-mysql

This will also install the postfix package as a dependency. When asked for the general type of configuration choose "Internet Site". Answer the question for the "Mail name" by entering the fully qualified hostname (that means including the domain part) of your system.

If you intend to run the MySQL server on the same server:

$> aptitude install mysql-server-5.0

As you probably want to offer POP3 and IMAP services to your users you need to install:

$> aptitude install dovecot-pop3d
$> aptitude install dovecot-imapd

Your users may like it if you filter out spam and (windows) viruses for them. Amavis is doing a good job here:

$> aptitude install amavisd-new libclass-dbi-mysql-perl

The Perl DBI module is needed so that AMaViS can read domain information from the database later. AMaViS is nearly useless unless you install its suggested packages to scan attachments for viruses and detect spam. You want to install these packages:

$> aptitude install spamassassin clamav-daemon cpio arj zoo nomarch lzop cabextract pax

Some of the suggested packages are not free enough to be included in Debian's main section. If you want to install them you first need to add the contrib and non-free sections to your Debian mirror in the /etc/apt/sources.list file. You usually just need to add the words contrib and non-free to your existing mirror line:

deb etch main contrib non-free

Then you are ready to install the non-free packages:

$> aptitude update
$> aptitude install lha unrar

Next you will want to install the OpenSSL package so that you can later create a proper SSL certificate letting your users use your mail server securely:

$> aptitude install openssl

If you intend to offer a webmail service I can recommend the Squirrelmail package. It will automatically install an Apache server if you do not yet have one installed. Type:

$> aptitude install squirrelmail

As your control information for Postfix will be stored in a MySQL database you may want to install the PhpMyAdmin software that allows you to administer the database and its data in your web browser:

$> aptitude install phpmyadmin libapache2-mod-php5 php5-mysql

You may want to test the mail server with the simple "telnet" client later:

$> aptitude install telnet

The console-based mutt email client lets you read mail from mailboxes directly from the hard disk. It will be helpful for testing the configuration. And it's even a very powerful IMAP email client that many people user as their main mail program. Maybe you start to like it, too. You should install it:

$> aptitude install mutt

Step 2: Create the database and user

Now it is time to create the MySQL database and its tables. If you are experienced in using MySQL you can enter SQL statements on the 'mysql' command line. Alternatively you may use phpmyadmin by pointing your browser at http://yourmailserver/phpmyadmin

If you just installed your MySQL server you will be able to login as user 'root' with an empty password. Set a new password for that account now. In the shell you need to run:

$> mysqladmin password root2007


Replace 'root2007' by a more secure password. Install and use 'pwgen' if you do not feel creative. I will use 'root2007' throughout the tutorial though. Replace it by your own choice where appropriate.

Then create the database. Call it 'mailserver' (you will be asked for the above password):

$> mysqladmin -p create mailserver

For security reasons you will want to create another less privileged MySQL user account that your mail server will use. Connect to your database:

$> mysql -p

When you see the mysql> prompt enter the following SQL statement to grant the appropriate privileges:

GRANT SELECT ON mailserver.*
TO mailuser@localhost
IDENTIFIED BY 'mailuser2007';

This will create a user called 'mailuser' that has only the privilege to select/read data from the database but not to alter it. If you want to add or alter data in the database either use the 'root' account or create another account for that purpose. The password 'mailuser2007' is just an example. Please replace it by a more decent password.

Step 3: Create the database tables

Inside the newly created database you will have to create tables that store information about domains, forwardings and the users' mailboxes. Connect to the MySQL again and choose the 'mailserver' database:

$> mysql -p mailserver

You will see the mysql> prompt again. First create a table for the list of virtual domains that you want to host:

CREATE TABLE `virtual_domains` (
) ENGINE = InnoDB;

The next table contains information on the actual user accounts. Every user has a username and password. It is used for accessing the mailbox by POP3 or IMAP, logging into the webmail service or to send mail if they are not in your local network. As users tend to easily forget things the user's email address is also used as the login username. Create the users table:

CREATE TABLE `virtual_users` (
domain_id INT(11) NOT NULL,
password VARCHAR(32) NOT NULL,
FOREIGN KEY (domain_id) REFERENCES virtual_domains(id) ON DELETE CASCADE
) ENGINE = InnoDB;

And finally a table is needed for aliases (email forwardings) from one account to another:

CREATE TABLE `virtual_aliases` (
domain_id INT(11) NOT NULL,
source VARCHAR(40) NOT NULL,
destination VARCHAR(80) NOT NULL,
FOREIGN KEY (domain_id) REFERENCES virtual_domains(id) ON DELETE CASCADE
) ENGINE = InnoDB;

You wonder about the foreign keys? They express that entries in the virtual_aliases and virtual_users tables are connected to entries in the virtual_domains table. This will keep the data in your database consistent because you cannot create virtual aliases or virtual users that are not connected to a virtual domain. And you avoid redundancy because you just store the domain name once - in the virtual_domains table - and nowhere else. The suffix 'ON DELETE CASCADE' means that if you delete a row from the referenced table that the deletion will also be done on the current table automatically. So you do not leave orphaned entries accidentally. Imagine that you do not host a certain domain any longer. You can remove the domain entry from the virtual_domains table and all dependent/referenced entries in the other tables will also be removed. This approach is also called a normalized database.

An example of the data in the tables:

id name
id domain_id user password
1 1 john summersun
2 1 steve veryloud
3 2 kerstin dogfood

The email addresses of these three users in the database would be:

Let us add a simple alias:

id domain_id source destination
1 1 steve
2 2 kerstin
3 2 kerstin

This will make the mail for be forwarded to And the mail for is forwarded to both and

Do not be scared if this way looks incredibly complicated. Such a database is not supposed to be maintained by manual SQL commands. But it is still important that you understand the schema of the database tables. There are already web-based administration tools that are supposed to make managing accounts easier.

Step 4: Create the database mapping files


As described earlier a mapping in Postfix is just a table that contains a left-hand side (LHS) and a right-hand side (RHS). To make Postfix use MySQL to define a mapping we need a 'cf' file (configuration file). Start by creating a file called /etc/postfix/ for the virtual_mailbox_domains mapping:

user = mailuser
password = mailuser2007
hosts =
dbname = mailserver
query = SELECT 1 FROM virtual_domains WHERE name='%s'

Imagine Postfix wants to find out if is a virtual mailbox domain. It will run the above SQL query and replace '%s' by ''. If it finds such an entry in the virtual_domains table it will return a '1'. Actually it does not matter what exactly is returned as long as there is a result.

And you need to make Postfix use this database mapping:

$> postconf -e virtual_mailbox_domains=mysql:/etc/postfix/

(The postconf -e command conveniently adds configuration lines to your /etc/postfix/ file. It also activates the new setting instantly so you do not have to reload the Postfix process.)

Postfix will now search your virtual_domains table to find out if a certain domain is a virtual mailbox domain. Let us try if this works. Create a new row in the virtual_domains table with one domain. Connect to your database:

$> mysql -p mailserver

and run this query:

INSERT INTO virtual_domains (id, name) VALUES (1, '');

Back on your shell you can now check if the '' domain is known as a virtual mailbox domain:

$> postmap -q mysql:/etc/postfix/

You should get '1' as a result.


If you get an error message telling you postmap: warning: connect to mysql server Access denied... then you have a problem with the user account "mailuser" that you use to connect to the database. Check the MySQL privileges again.

If you get an error message reading postmap: warning: connect to mysql server Can't connect to MySQL server on '' then your MySQL server is either not running or at least not listening on Check your MySQL server configuration (/etc/mysql/my.cnf).


Your first mapping is working. Great. Get straight to the second one. You will now define the virtual_mailbox_maps which is usually the mapping of email addresses (left-hand side) to the location of the user's mailbox on your harddisk (right-hand side). If you saved incoming email to the hard disk using Postfix' built-in virtual delivery agent then it would be queried to find out the mailbox path. But in this case the actual delivery is done by Dovecot's LDA (local delivery agent) so Postfix does not really care about the path. Postfix just needs to see if a certain email address belongs to a virtual user. Just as above you need an SQL query that searches for an email address and returns "1".

But first you need to deal with file system permissions. For security reasons it is suggested you create a new system user that will own all virtual mailboxes. The following shell commands will create a system group "vmail" with GID (group ID) 5000 and a system "user" with UID (user ID) 5000. (Make sure that UID and GID is not yet used or choose another - the number can be anything between 1000 and 65000 that is not yet used):

$> groupadd -g 5000 vmail
$> useradd -g vmail -u 5000 vmail -d /home/vmail -m

You need to set the virtual_uid_maps and virtual_gid_maps to these IDs:

$> postconf -e virtual_uid_maps=static:5000
$> postconf -e virtual_gid_maps=static:5000

Go create an entry in the virtual_users table for a test user

INSERT INTO virtual_users (id, domain_id, user, password)
VALUES (1, 1, 'john', MD5('summersun'));

Next you will need to create a cf file to tell postfix about the SQL query for this table. But apparently you cannot get all the information from just the virtual_users table. The domain name has to be fetched from the virtual_domains table. "" is our virtual user but the user part "john" is stored in the virtual_users table while the domain "" is stored in the virtual_domains table. Thanks to the JOIN SQL statement you can join these two tables together. A query that would tell us the email addresses of all virtual users would look like this:

SELECT CONCAT(virtual_users.user, '@', AS email
FROM virtual_users
LEFT JOIN virtual_domains ON;

MySQL should print:

| email            |
| |

In addition to the email address it is also important to get the user's password later on. Since the path to the user's mailbox is fixed it is not important to get that information from the database. The directory structure will be /home/vmail/$DOMAIN/$USER. So in John's example it would be /home/vmail/

The query just has to ask for the password field, too:

SELECT CONCAT(virtual_users.user, '@', AS email,
FROM virtual_users
LEFT JOIN virtual_domains ON;

The result of that query would be:

| email            | password                         |
| | 14cbfb845af1f030e372b1cb9275e6dd |

As you see the password is not stored as plain text but the MD5 hash is saved. The query may look a bit complicated if you do not have to deal with SQL queries every day. At least it is not very handy. So instead of writing this query into the cf file there is a feature called views that has finally been introduced with MySQL 5.0. A view allows to store queries under a table name. Go create that view:

CREATE VIEW view_users AS
SELECT CONCAT(virtual_users.user, '@', AS email,
FROM virtual_users
LEFT JOIN virtual_domains ON;

Now we can get the information about virtual users with a simple SELECT query on this view_users view:

SELECT * FROM view_users;

You should get the same result as above:

| email            | password                         |
| | 14cbfb845af1f030e372b1cb9275e6dd |

Now things are a bit simpler and you can finally create a cf file at /etc/postfix/ that is as simple as:

user = mailuser
password = mailuser2007
hosts =
dbname = mailserver
query = SELECT 1 FROM view_users WHERE email='%s'

Tell Postfix that this mapping file is supposed to be used for the virtual_mailbox_maps mapping:

$> postconf -e virtual_mailbox_maps=mysql:/etc/postfix/

Try if Postfix is happy with this mapping by asking it where the mailbox directory of our user would be:

$> postmap -q mysql:/etc/postfix/

You should get 1 back which means that is an existing virtual mailbox user on your server. Later in the Dovecot configuration part you will also use the email and password fields but Postfix does not need them here.


The virtual_alias_maps mapping is used for forwarding emails from one email address to another. Examples of how entries in this mapping may look:

source (LHS) destination (RHS) Meaning Forward John's mail to gmail, Deliver one copy to the original account but also send a copy to Deliver all email for the domain to unless there is a specific user account. If does not exist then her mail would be sent to john. If is a valid user then she would get the mail. This is called a catchall account.

See man 5 virtual for a more formal definition.

As you see it is possible to name multiple destinations seperated by commas. In the database the same effect can be achieved by using different rows instead. The second entry above should be split into two rows:

source (LHS) destination (RHS)

Let us add this example to the database:

INSERT INTO virtual_aliases (id, domain_id, source, destination)
VALUES (1, 1, 'john', ''),
       (2, 1, 'john', '');

Let us also create a view for the virtual aliases:

CREATE VIEW view_aliases AS
SELECT CONCAT(virtual_aliases.source, '@', AS email,
FROM virtual_aliases
LEFT JOIN virtual_domains ON;

This query is a bit more complicated than the previous one. It gets all rows from virtual_aliases and JOINs the virtual_domains table (the domain_id field of the virtual_aliases table matches the id field of the virtual_domains table). Too complicated? That will not bother you any longer because you can now use the view_aliases view:

SELECT * FROM view_aliases;

and get the appropriate result:

| email            | destination       |
| |  |
| | |

The SQL view works perfectly so we can use it for Postfix. Create another cf file at /etc/postfix/

user = mailuser
password = mailuser2007
hosts =
dbname = mailserver
query = SELECT destination FROM view_aliases WHERE email='%s'

Test if the mapping file works as expected:

$> postmap -q mysql:/etc/postfix/

You should see the two expected destinations:,

Before you define the virtual_alias_maps setting there is a small quirk you need to take care of. There is a special kind of forwarding: the "catchall" alias. Catchalls catch all emails for a domain if there is no specific user account. A catchall alias looks like "" and forwards email for the whole domain to one account. We have created the '' user and would like to forward all other email on the domain to ''. So we would add a catchall alias like:

email destination

Now imagine what happens when Postfix receives an email for ''. Postfix will first check if there are any aliases in the virtual_alias_maps table. It finds the catchall entry as above and since there is no more specific alias the catchall account matches and the email is redirected to ''. This is probably not what you wanted. So you would need to make the table rather look like this:

email destination

More specific aliases have precedence over general catchall aliases. Postfix will find an entry for '' first and find that email should be "forwarded" to '' - the same email address. This trickery may sound weird but it is needed if you plan to use catchall accounts. So the virtual_alias_maps mapping must obey both the "view_aliases" view and this "john-to-himself" mapping. Create a cf file /etc/postfix/ for the latter mapping:

user = mailuser
password = mailuser2007
hosts =
dbname = mailserver
query = SELECT email FROM view_users WHERE email='%s'

Check that you get John's email address back when you ask Postfix if there are any aliases for him:

$> postmap -q mysql:/etc/postfix/

The result should be the same address:

Now you need to tell Postfix that these two mappings should be searched by adding this line to your

$> postconf -e virtual_alias_maps=mysql:/etc/postfix/,mysql:/etc/postfix/

Please note that the order of the two mappings are important here. Postfix will look for all matching entries in the first virtual-aliases mapping and use them as aliases. Only if there are no results found then Postfix will consult the second email2email mapping.

You did it! All mappings are set up and the database is generally ready to be filled with domains and users. Make sure that only 'root' and the 'postfix' user can read the cf files - after all your database password is stored there:

chgrp postfix /etc/postfix/mysql-*.cf
chmod u=rw,g=r,o= /etc/postfix/mysql-*.cf

Step 5: Deliver emails through the Dovecot LDA

Postfix comes with a mail delivery agent (MDA) called "virtual" that will usually deliver emails to virtual mailboxes. But Dovecot comes with an own (local) delivery agent also called "Dovecot LDA" that offers more functionality. To make Postfix use that agent you will have to add a service to your /etc/postfix/

dovecot   unix  -       n       n       -       -       pipe
    flags=DRhu user=vmail:vmail argv=/usr/lib/dovecot/deliver -d ${recipient}

Also make Postfix use that service for virtual delivery lines to your /etc/postfix/

$> postconf -e virtual_transport=dovecot
$> postconf -e dovecot_destination_recipient_limit=1

So far this will make Postfix pass on incoming emails to virtual users to the /usr/lib/dovecot/deliver program. Now it is time to configure Dovecot.

Laatst gewijzigd op: 2008-02-05 19:58:32