Most of the online documentation that we can currently find about setting up this insane pile of moving parts are either outdated or frustrating by their lack of clear explanations. I wanted to learn how to build a reasonably secure mail system and to understand how things interact with each other on such a setup so that I can later debug problems that arise with it.

Most of the steps can be found in different other howtos, but they're generally split apart the Internet. I'll be stitching, cutting, upgrading and completing the steps needed to have a full setup with the postfixadmin web interface. You can find the bibliography at the end of the article.

I will probably skip over some details if there's already a good howto that explains things very well. I'll try and point out those reference materials as I go along, so make sure to consult them if there's something you don't understand.

Note: this howto can very well be used with Debian Wheezy. Simply skip the steps about adding "apt" sources and pinning for backports and wheezy, and start with package installation directly.


We want to install a full mail stack with encrypted IMAP and SMTP access for client connections (no plaintext on the network whenever possible), and also classic unencrypted SMTP for reception to the domains our server will be hosting, because it's unfortunately necessary. This server will give users control over e-mail redirections and "vacation" auto-replies. The whole thing needs to filter out unwanted crap like spam and virii.


In order to give users control over mailboxes and mail aliases, and also over auto-reply messages, we'll install postfixadmin. We'll need to integrate the underlying tools to postfixadmin's database.

Since I already have MySQL, Apache2 and PHP5 in place on the target machine, I won't cover installation of these components. We'll be concentrating on the mail-related software in this howto, and the database and webserver setup will be left out as an exercise. You can easily find documentation about setting up a LAMP server on the Internetz.

Visualising the message flow

With e-mail, the way I like to conceptualize things is by imagining the path the email will follow along the course of its life. From the sender, to the SMTP server, via a bunch of checks, then to storage, and to the receiver via IMAP.

The setup can be complexified for scaling to a lot more users with larger infrastructures in order to include multiple underlying machines. Simply make sure to always see the whole system as a pipeline through which an email transits. For this howto, we'll be keeping things relatively simple and install everything on the same machine.

We will be using Milters (mail filters). Milter comes from Sendmail, and is a protocol used for filtering mail during or after the SMTP session (reception). They usually use a unix socket file to communicate with the MTA, which is faster than initializing TCP connections to localhost. They let your server decide whether it wants to accept the email or not while it is still being fed to it -- in other words, before the mail is even queued -- so it can potentially free your system of some costly (slow) disk operations.

Postfix is fully capable of delivering mail to users' Maildir storage, but for performance reasons, we'll let Dovecot handle final delivery. This is because Dovecot automatically generates an index of messages while storing it to disk, which should make accessing them via IMAP much faster.

Let's visualize things a bit. They say a picture is worth a thousand words, so let's use words to draw a picture:

                     ,--> spamass-milter <--> SpamAssassin
 other MTAs         |,--> clamav-milter <--> ClamAV
    _____           |,--> milter-greylist
  d[ o_0 ]b         |
   ,-----.  ----> SMTP (Postfix  ------------+-> Dovecot-LDA
  || ::: || <-.   unauthenticated)          /        |
  {{     }}    \                           /         |
                `--to domains          local         v
                     not hosted    recipient   Local Maildir
                      here                 /      storage
   ,,,,   encrypted    \                  /          |
  (^__^)    ,------> SMTP (Postfix  -----'           |
 v__--__v  /         authenticated)                  /
    []                                              /
   /  \    <-------- IMAP (Dovecot) <--------------'
  /    \  encrypted

We'll build the whole thing by somewhat following the arrows, building small steps at a time and testing that everything works properly after every step. We'll start with unauthenticated SMTP and see if mail gets to the the local mailboxes, then we'll add Dovecot and see if we can retrieve the stored email, and finally we'll setup the authenticated SMTP and see if we can use it to send email abroad. The milters will be kept out till the end: this'll keep the other steps easier to test because we won't risk having our tests rejected or delayed.


If you're using wheezy, skip adding squeeze-backports to sources.list.d and the pin in preferences.d. Also, you won't need to specify any release from which to install packages (everything can just come from wheezy directly).

We'll be installing dovecot 2.x, which is in squeeze-backports:

echo deb squeeze-backports main > /etc/apt/sources.list.d/squeeze_backports.list

The postfixadmin package is only available in wheezy. However, since it's quite simply a web application that doesn't require specific versions of its dependencies, we can install the package directly in squeeze. So first, we'll want to ensure we can access wheezy packages, but that they don't upgrade automatically:

echo deb wheezy main > /etc/apt/sources.list.d/wheezy.list
cat > /etc/apt/preferences.d/no_auto_wheezy <<EOF
Package: *
Pin: release n=squeeze
Pin-Priority: 900

Package: *
Pin: release n=wheezy
Pin-Priority: -10

Move on to installing packages. We'll want to install postfixadmin's dependencies from squeeze, but the package itself from wheezy:

apt-get update
apt-get install postfix postfix-mysql openssl ssl-cert
apt-get install php5-imap php5-mysql wwwconfig-common dbconfig-common
apt-get -t squeeze-backports install dovecot-imapd dovecot-mysql
apt-get -t wheezy install postfixadmin

Let postfixadmin create its own database during installation. This'll make our work easier.


Priming the database

Since we'd like to be able to test each step of the configuration, we'll need to start by building the database structure and by priming it with some data. This might seem a bit backwards, since our starting point is the web app that will control accounts when everything is set up, but actually the web app only depends on the presence of the database and the web server to let us play with it (e.g. you can control accounts as long as you have a database. Postfix and Dovecot will be using the data from the database when we'll tell them to).

We'll start by modifying the config file to our needs. Let's use the /etc/postfixadmin/config.local.php file so that our changes survive package upgrades. Here's an example of what you might want to override (declare) in this file:

$CONF['default_language'] = 'en';
$CONF['admin_email'] = '';
$CONF['default_aliases'] =array (
    'abuse' => '',
    'hostmaster' => '',
    'postmaster' => '',
    'webmaster' => ''
$CONF['domain_path'] = 'YES';
$CONF['domain_in_mailbox'] = 'NO';
$CONF['aliases'] = 0;  // optional : I'm setting a default of unlimited aliases for new domains
$CONF['mailboxes'] = 0;  // idem
$CONF['backup'] = 'NO';  // In my case, the users won't need this (I'll automate the backup)
$CONF['sendmail'] = 'NO';  // It's better to have a real webmail
$CONF['fetchmail'] = 'NO';  // This could be useful, but for now I prefer simplifying the interface
$CONF['user_footer_link'] = '';
$CONF['footer_text'] = 'Return to';
$CONF['footer_link'] = '';
$CONF['welcome_text'] = <<<EOM

Welcome to the team, sonny! Here at "someprovider" inc. we pride ourselves in our world domination efforts.
blah blah blah blah
$CONF['create_mailbox_subdirs_prefix'] = '';  // recommended value that should be used with Dovecot.
$CONF['theme_logo'] = 'images/logo-default.png';  // optional.. it's the right-most part of a URL so it needs to be accessible via your web server
$CONF['theme_css'] = 'css/default.css';  // idem

Go to the postfixadmin's setup page on your web server: This will create the tables in which postfixadmin keeps accounts and other info.

Next we need to create a setup password. This password will ensure that setup.php is not used by just anybody to create unwanted "super admin" accounts. Enter a password for the setup page and send the form. Then, copy the line that's printed out with your password's hash (should look like a line with $CONF in the example configuration overrides above) and paste it at the end of /etc/postfixadmin/config.local.php.

Visit setup.php again (reload the page). This time, type in the setup password, then an email address, and enter a password for the new super admin account and send the form. This email address needs to be from a domain that actually exists, not on the server you're setting up, else you'll get the message "Admin is not a valid email address!". If you really need to use an address from a domain that does not resolve, you can add the line "$CONF['emailcheck_resolve_domain']='NO';" to config.local.php). That email will then have super admin privileges on the data: this means that it can administrate all of the domains understood by postfix and manage administrator accounts (other accounts that can administrate a subset of the domains).

Now go to the main postfix admin page and login with the super admin you've just created. Once inside, create a new domain: in the menus, hover over 'Domain List' and click on 'New Domain'. For this howto, I'll create the domain

Create a new mailbox by hovering over 'Virtual List' and clicking on 'Add Mailbox'. I'll create the mailbox named someone, thus creating the e-mail

Now we should have enough info in the database for testing the next steps.

Basic SMTP with virtual domains and mailboxes with Postfix

Create the directory that will hold the mailboxes for the virtual accounts and give it to the mail user so that Dovecot, our final LDA, can create directories and files in there:

mkdir /var/mail/vmail
chown mail:mail /var/mail/vmail 

Create a read-only user on the postfixadmin database:

mysql -p -e "GRANT SELECT ON postfixadmin.* TO postfix@localhost IDENTIFIED BY 'something';"

We'll put all config files for accessing Postfix's virtual resources (in the database) via MySQL into a directory that we need to create:

mkdir /etc/postfix/virtual

Now, under the directory we've just created, we'll create a bunch of config files:

user = postfix
password = something
hosts = localhost
dbname = postfixadmin
query = SELECT domain FROM domain WHERE domain='%s' AND backupmx = true

user = postfix
password = something
hosts = localhost
dbname = postfixadmin
query = SELECT goto FROM alias a INNER JOIN domain d ON a.domain=d.domain WHERE a.address='%s' AND = true AND = true

user = postfix
password = something
hosts = localhost
dbname = postfixadmin
query = SELECT domain FROM domain WHERE domain='%s' AND backupmx = false AND active = true

user = postfix
password = something
hosts = localhost
dbname = postfixadmin
query = SELECT maildir FROM mailbox m INNER JOIN domain d ON m.domain=d.domain WHERE m.username='%s' AND = true AND = true

user = postfix
password = something
hosts = localhost
dbname = postfixadmin
query = SELECT quota FROM mailbox m INNER JOIN domain d ON m.domain=d.domain WHERE m.username='%s' AND = true AND = true

If you've read the tutorials that I link to in the bibliography, or searched for tutorials about virtual domains and mailboxes at all, you might have observed that contrary to all the other tutorials out there, I don't use simple SELECT statements in, and This is because what they teach you is buggy! When you disable a domain in postfixadmin you expect that domain to cease working altogether. With simple SELECT statements you can still login to individual mailboxes and send out e-mail even though the domain has been disabled! So to fix that I use an INNER JOIN on the domain table to check whether the appropriate domain is active or not. With this, when you disable a domain in the web interface, it stops working for real; expect support calls if users were still working with their accounts at that moment ;)

The newly created files contain a database password so you might like to tighten permissions a little bit so that only the postfix daemon is authorized to read them:

chown -R root:postfix /etc/postfix/virtual
chmod 0750 /etc/postfix/virtual
chmod 0640 /etc/postfix/virtual/*.cf

With the above permissions you should be fine, but if the postfix daemon logs a bunch of messages like the following in /var/log/mail.err, it means the daemon can't access the files in the /etc/postfix/virtual directory so you should fix their permissions:

Sep 21 23:59:39 debian-squeeze postfix/proxymap[2510]: fatal: open /etc/postfix/virtual/ Permission denied

Now that we have everything, we need to indicate to postfix that it needs to use those configuration files:

postconf -e 'relay_domains = proxy:mysql:/etc/postfix/virtual/'
postconf -e 'virtual_alias_maps = proxy:mysql:/etc/postfix/virtual/'
postconf -e 'virtual_mailbox_domains = proxy:mysql:/etc/postfix/virtual/'
postconf -e 'virtual_mailbox_maps = proxy:mysql:/etc/postfix/virtual/'
postconf -e 'virtual_create_maildirsize = yes'
postconf -e 'virtual_mailbox_extended = yes'
postconf -e 'virtual_mailbox_limit_maps = proxy:mysql:/etc/postfix/virtual/'
postconf -e 'virtual_mailbox_limit_override = yes'
postconf -e "virtual_maildir_limit_message = Sorry, the user's maildir has overdrawn his diskspace quota, please try again later."
postconf -e 'virtual_overquota_bounce = yes'
postconf -e 'virtual_mailbox_base = /var/mail/vmail'
postconf -e "virtual_minimum_uid = $(id -u mail)"
postconf -e 'virtual_transport = dovecot'
postconf -e 'dovecot_destination_recipient_limit = 1'
postconf -e "virtual_uid_maps = static:$(id -u mail)"
postconf -e "virtual_gid_maps = static:$(id -g mail)"
postconf -e 'transport_maps = hash:/etc/postfix/transport'
touch /etc/postfix/transport
postmap /etc/postfix/transport

Now add the following line to the end of /etc/postfix/

dovecot   unix  -       n       n       -       -       pipe
  flags=DRhu user=mail:mail argv=/usr/lib/dovecot/deliver -f ${sender} -d ${user}@${nexthop} -a ${recipient}

And reload config:

postfix reload

To save up on storage space used, we'll enable reading and writing gzip files in Dovecot. Delivery and retrieval will use a little more CPU with this setting, but e-mail files should take at least twice as less disk space. Add the following line to /etc/dovecot/conf.d/10-mail.conf:

mail_plugins = zlib

And in /etc/dovecot/conf.d/90-plugin.conf, add configuration for the plugin inside the plugin block, like this:

plugin {
  zlib_save_level = 6
  zlib_save = gz

XXX: here there's a bug: dovecot doesn't know about users at this point because we haven't configured its userdb dictionary method. You can either comment out "virtual_transport" to have postfix do the final delivery, or configure userdb and all glue from next section (auth with dovecot) before testing delivery. Since this HOWTO is getting really old, I won't reorganize everything, but a notice about this problem seemed required.

Now restart Dovecot to enable the plugin:

service dovecot restart

Testing delivery

There we are, we have a functional virtual mailboxes-based SMTP reception system. It's not yet fit for relaying mail for users of the domains your server is hosting, but that'll come later. If you'd like to test out your setup, you can use the following:

apt-get install swaks
swaks --server=localhost
ls -l /var/mail/vmail/   # You should see the delivered mail there.

Mail retrieval with Dovecot

Next stop, mail retrieval with dovecot. In dovecot 2.x, most of the configuration has been broken down into files in /etc/dovecot/conf.d, but some of it stayed in files laying in /etc/dovecot. Let's begin by configuring the SQL connection. We need to tell dovecot where to find user passwords. Remove contents of /etc/dovecot/dovecot-sql.conf.ext and replace it with the following:

driver = mysql
connect = host= dbname=postfixadmin user=postfix password=something
default_pass_scheme = MD5-CRYPT
password_query = SELECT username as user, password, CONCAT('/var/mail/vmail/', maildir) as userdb_home, 8 as userdb_uid, 8 as userdb_gid FROM mailbox INNER JOIN domain ON mailbox.domain=domain.domain WHERE username='%u' AND = true AND = true;
user_query = SELECT CONCAT('/var/mail/vmail/', maildir) as home, 8 as uid, 8 as gid FROM mailbox INNER JOIN domain ON mailbox.domain=domain.domain WHERE username='%u' AND = true AND = true;

In a similar fashion to what we did in the postfix SQL config files, the above queries will prevent users to login to dovecot and postfix (via SASL for sending out email) when their domain has been disabled.

Now, replace the contents of /etc/dovecot/conf.d/10-mail.conf with the following:

mail_location = maildir:%h
mail_uid = mail
mail_gid = mail
first_valid_uid = 8
first_valid_gid = 8
namespace inbox {
  inbox = yes

Replace the contents of 10-auth.conf by:

auth_mechanisms = plain login
passdb {
  driver = sql
  args = /etc/dovecot/dovecot-sql.conf.ext
userdb {
  driver = sql
  args = /etc/dovecot/dovecot-sql.conf.ext

Open up the file 10-master.conf in an editor and somewhere near the end of the file, uncomment and change the line #user = root so that it looks something like the following:

service auth-worker {
  user = nobody

Replace the contents of 10-ssl.conf by the following. For the howto, we'll be using the self-signed certificate that's created by the dovecot package upon installation. If you're using your own certificate change the path to point to the right files for your case:

ssl = required
ssl_cert = </etc/ssl/certs/dovecot.pem
ssl_key = </etc/ssl/private/dovecot.pem

Now, restart the service:

service dovecot restart

Testing mail retrieval with Dovecot

And now we should be able to test a connection. There's no quick and handy tool for testing this out, so brace yourselves: we'll have to make an IMAP session manually (it's not very complicated, really). From your computer, or another point which should have acces to the IMAP server:

openssl s_client -connect youhost.domain:143 -starttls imap
[... you'll see a bunch of info about the SSL certificate bein printed out]
. login something
[... list of capabilities]
. select inbox
[... if you did at least one test in the last step, you should see 1 EXISTS in the output]
. fetch 1 rfc822.text
[... message body]
. logout

Let's try logging in without encryption to see if we get through (we shouldn't !). If the server responds that login was successful there's a problem somewhere.

telnet localhost 143
. login something
* BAD [ALERT] Plaintext authentication not allowed without SSL/TLS, but your client did it anyway. If anyone was listening, the password was exposed.
. logout

Mail delivery for authenticated users

We're close to going round the loop in the graph. We'll configure SMTP to let people relay mail to external domains (e.g. send mail to domains that we are not hosting), but only authenticated users should be able to do that, else you have an open relay and can be the source of much spam in the world! Authentication and reception of e-mails that are to be relayed elsewhere needs to be encrypted.

We'll need to configure Postfix so that it knows how to behave with SSL connections, and then we'll tell Postfix to use Dovecot's SASL library for authentication (Postfix will use Dovecot's authentication mechanism we configured earlier).

Dovecot SASL

First things first: let's tell Dovecot how to "expose" SASL, its login facilities. In /etc/dovecot/conf.d/10-master.conf, modify the block "service auth" so that it looks like this:

service auth {
  unix_listener /var/spool/postfix/private/auth {
    mode = 0660
    user = postfix
    group = postfix

When you're done, restart dovecot:

service dovecot restart

Postfix STARTTLS + SASL on port 25

Let's now work on Postfix. First, configure TLS. For this example, I'll use the same cert as was used with Dovecot:

postconf -e 'smtpd_tls_cert_file = /etc/ssl/certs/dovecot.pem'
postconf -e 'smtpd_tls_key_file = /etc/ssl/private/dovecot.pem'
postconf -e 'smtp_tls_session_cache_database = btree:$data_directory/smtp_tls_session_cache'
postconf -e 'smtpd_tls_session_cache_database = btree:$data_directory/smtpd_tls_session_cache'
postconf -e 'smtp_tls_security_level = may'
postconf -e 'smtpd_tls_security_level = may'
postconf -e 'smtpd_tls_ask_ccert = no'
postconf -e 'smtpd_tls_loglevel = 0'
postconf -e 'tls_random_source = dev:/dev/urandom'

Then, we want to tell it to use dovecot's SASL style authentication:

postconf -e 'smtpd_sasl_auth_enable = yes'
postconf -e 'smtpd_tls_auth_only = yes'
postconf -e 'smtpd_sasl_type = dovecot'
postconf -e 'smtpd_sasl_path = private/auth'
postconf -e 'smtpd_sasl_exceptions_networks = $mynetworks'
postconf -e 'smtpd_sasl_security_options = noanonymous'
postconf -e 'smtpd_sasl_tls_security_options = noanonymous'
postconf -e 'broken_sasl_auth_clients = yes'
postconf -e 'smtpd_relay_restrictions = permit_mynetworks,permit_sasl_authenticated,reject_unauth_destination'
postconf -e 'smtpd_recipient_restrictions = permit_mynetworks,reject_non_fqdn_recipient,permit_sasl_authenticated,reject_unauth_destination,reject_rbl_client,reject_rhsbl_helo,reject_rhsbl_sender'
postconf -e 'smtpd_sender_restrictions = permit_mynetworks,reject_unknown_sender_domain'
postconf -e 'smtpd_helo_restrictions = reject_invalid_helo_hostname'
postconf -e 'smtpd_data_restrictions = reject_unauth_pipelining,reject_multi_recipient_bounce,permit'
# Those two configs should help a little bit with older spammers
postconf -e 'smtpd_helo_required = yes'
postconf -e 'disable_vrfy_command = yes'
postfix reload

Postfix STARTTLS + SASL on port 587 (mail submission)

Some ISPs have taken very drastic measures to block some worms and spam and they decided to entirely block port 25 out of home users. For those users, mail delivery will not work. So we need to configure Postfix to listen to another port: the Mail Submission port, 587.

In /etc/postfix/, right above the line that starts with "smtp inet", add the following:

submission inet n       -       -       -       -       smtpd
  -o smtpd_tls_security_level=encrypt
  -o smtpd_sasl_auth_enable=yes
  -o smtpd_sasl_type=dovecot
  -o smtpd_sasl_path=private/auth
  -o smtpd_sasl_security_options=noanonymous
  -o smtpd_client_restrictions=permit_sasl_authenticated,reject
  -o smtpd_sender_login_maps=proxy:mysql:/etc/postfix/virtual/
  -o smtpd_sender_restrictions=reject_sender_login_mismatch
  -o smtpd_recipient_restrictions=reject_non_fqdn_recipient,permit_sasl_authenticated,reject

Now reload postfix config:

postfix reload

Testing mail relay

To test mail relay for authenticated users, you should go to another computer which should have access to the mail server.

First, let's see if things work. Watch out not to put any sensible password here. you can remove the --auth-password argument to have it ask the password upon startup, which will be echoed to the screen (d'oh!!):

swaks --tls --from --to external@email_address --server server.domain:25 --auth LOGIN --auth-user --auth-password something
swaks --tls --from --to external@email_address --server server.domain:587 --auth LOGIN --auth-user --auth-password something

Now let's verify that relaying without being authenticated fails:

# This should give "Relay access denied"
swaks --tls --from --to external@email_address --server server.domain:25

# This should give "Client host rejected: Access denied"
swaks --tls --from --to external@email_address --server server.domain:587

Filtering out undesired crap

Now that we're able to receive and send email and let users read them, we should make our users' lives better by filtering crap out. We'll use four different tools to reject undesired messages: ClamAV, greylisting, SpamAssassin and Sender Policy Framework checking.

Like described at the beginning of the howto, we'll be using Milters so that we can reject crap before queueing it on the disk, so legitimate senders will know that their message didn't get through with a bounce that states a reason why it wasn't accepted.

In all cases, we'll want to set postfix to accept mail by default when a milter fails to function properly, else we'd loose e-mails because of the errors:

postconf -e 'milter_default_action = accept'


Scanning mail with ClamAV is all about preventing virii and also a bunch of phishing and scamming messages from getting to users. This helps you to remove infections from spreading on your corporate / community computers (because -- yes -- some people will open those infected files if they reach them). Running ClamAV uses a good amount of RAM, so if you're setting up a mail system for yourself and you think you'll be wise enough to avoid infected files, you can skip this step. However, scanning for virii is almost mandatory for a system that will deliver mail for a good number of users, especially when a bunch of users are not computer geeks. But even for computer geeks, the sheer volume of crap that can get to their email is going to be annoying them badly.

So let's start. First, install the milter and some additional signature files for filtering more email-related badness (we need to install the backports version of the package to get up-to-date signatures correctly):

apt-get install clamav-milter
apt-get -t squeeze-backports install clamav-unofficial-sigs

Next, we need to tell it to let postfix have write access to it's socket. Edit /etc/default/clamav-milter and uncomment the last line:


Note that if you are running wheezy with the wheezy-updates source (and you should!) with the clamav-milter version 0.98.5, the file in /etc/default doesn't exist anymore and the bug with regards to the socket permission has been fixed. So you should skip editing the file as mentioned just above and instead ensure you have the following in your /etc/clamav/clamav-milter.conf:

MilterSocketGroup postfix
MilterSocketMode 660

Postfix runs in a chroot by default, so we'll have to reconfigure the milter to place its socket inside postfix's chroot directory. But first, we need to create a directory for the socket:

mkdir /var/spool/postfix/clamav
chown clamav /var/spool/postfix/clamav

We also want to change the action taken when an infected e-mail is detected to reject it immediately. To ensure our config stays through upgrades, we'll do it the debian way: run dpkg-reconfigure clamav-milter and answer the following:

Handle configuration automatically --> yes
User for daemon --> clamav
Additional groups --> none (empty field)
path to socket --> /var/spool/postfix/clamav/clamav-milter.ctl
group owner for the socket --> clamav
permissions (mode) for socket --> 660
remove stale socket --> yes
wait timeout for clamd --> 120
foreground --> no
chroot --> none (empty field)
pid file --> /var/run/clamav/
temporary path --> /tmp
clamd socket --> unix:/var/run/clamav/clamd.ctl
hosts excluded for scanning --> none (empty field)
mail whitelist --> none (empty field)
action for "infected" mail --> reject
action on error --> defer
reason for rejection --> Rejecting harmful email: %v found.
headers -> replace
log file --> /var/log/clamav/clamav-milter.log
disable log file locking --> no
maximum log file size --> 0
log time --> yes
use syslog --> no
log facility (type of syslog message) --> LOG_LOCAL6
verbose logging --> no
log level when infected --> off
log level when no threat --> off
size limit for scanned messages  --> 25

The above reconfiguration should have automatically restarted clamav-milter. Verify that the socket is in place. Normally, the clam daemon should be enabled in the boot procedure, but it's possible that you need to start it manually right after install: service clamav-daemon start

Next, we'll configure Postfix to use the milter to inspect incoming e-mail. Remember the smtp process is running inside a chroot, so the path needs to have its root at the top of the chroot dir:

postconf -e 'smtpd_milters = unix:/clamav/clamav-milter.ctl'
postfix reload

Testing the filter by sending a virus

To test this out, we'll have to send a malicious file to the SMTP server in the hope that it will block it. For this, you can install the package clamav-testfiles on your computer; it will install sample virus files under /usr/share/clamav-testfiles. Let's send one as an e-mail attachment:

swaks --server=localhost --attach - --suppress-data < /usr/share/clamav-testfiles/clam.exe

You should see the rejection message you configured towards the end:

<** 550 5.7.1 Rejecting harmful email: ClamAV-Test-File found.


Next thing we want to add in the filtering pipe is spam filtering. SpamAssassin is a powerful solution used by many for this purpose. We'll integrate it with postfix via a milter so that we can reject very awful mail right up front.

Let's start by installing the necessary packages:

apt-get install spamass-milter

Now, in /etc/default/spamass-milter we want to add "-m" so that it doesn't change the subject header (required for running as a milter with Postfix), "-r -1" so that it rejects what SpamAssassin flags as spam and "-I" to avoid scanning mail sent by logged-in users:

OPTIONS="-u spamass-milter -i -m -r -1 -I"

Restart the milter:

service spamass-milter restart

The spamass-milter connects, by default, to a spamd instance running on the same server (localhost). We need to configure SpamAssassin and start it. However, since it runs under root by default, we'll start by creating a dedicated user for the daemon:

adduser --shell /bin/false --home /var/lib/spamassassin --disabled-password --disabled-login --gecos "" spamd

Open up /etc/default/spamassassin and change three lines in it to enable the daemon, the automatic update for rules and also to add options so that we use the user we created above instead of "root" and so that it uses the helper directory we created above. The lines should look like the following:

OPTIONS="--create-prefs --max-children 5 --helper-home-dir=/var/lib/spamassassin -u spamd -g spamd -x"

Now let's update the rules for the first time and restart the daemon:

service spamassassin restart

We have all the needed pieces, so let's tell postfix to use the milter:

postconf -e 'smtpd_milters = unix:/clamav/clamav-milter.ctl, unix:/spamass/spamass.sock'
postfix reload

Testing the spam filter

For testing this, we'll use the special string from GTUBE. This string triggers a special rule that sets the score to a very high value so that SpamAssassin flags the mail as spam without a doubt.

Since we installed the package "clamav-unofficial-sigs" earlier, ClamAV will be catching a bunch of spams, phishing and scams by signature. When I first tried using the GTUBE signature to verify spam filtering, it was filtered by ClamAV instead of SpamAssassin, which is not what we want to verify here. So I've had to disable the clamav milter in /etc/postfix/ so that the emails goes straight to spamass-milter.

On your computer, download a text file with the GTUBE signature line and use it as the body of a test email:

wget -O /tmp/gtube.txt
swaks --from some_existing@email.address --server=your.domain --body=/tmp/gtube.txt

The email should be blocked with the message:

<** 550 5.7.1 Blocked by SpamAssassin

Don't forget to re-enable the clamav milter after your tests are done.


Greylisting is a simple method that drives off a good portion of dumb spammer bots. It bases on the assumption that spammers won't come back after their deed is done. So when it first receives mail from a sender, it refuses it with a "Temporarily unavailable" error message that has the purpose to make the client try again later. The sender is then accepted from the second trial onward. Since most spammers don't come back, they'll get that defferal message and it'll prevent them from sending you their crap.

The reason we're installing this milter last is that it can be a bit annoying when testing the other milters to have to wait around 30mins before being able to send actual tests. However, since greylisting is lightweight and cuts out a bunch of old spammers, we'll place it as the first one in the list so that we avoid running ClamAV and SpamAssassin on most useless spam.

Let's install the milter:

apt-get install milter-greylist

Edit the config file /etc/milter-greylist/greylist.conf place the socket file under Postfix's chroot directory. We'll also want to edit the ACLs properly so that the milter works like we intend it to (you probably want to edit the list "my network" so that if reflects your server's real situation). Here's a condensed version of the resulting config file. Add more ACLs as you see fit:

pidfile "/var/run/"
dumpfile "/var/lib/milter-greylist/greylist.db" 600
dumpfreq 10m
socket "/var/spool/postfix/milter-greylist/milter-greylist.sock" 660
user "greylist"
list "my network" addr { }
list "broken mta" addr {   \
  [... cut for brevity. You might consider keeping this list here from default config]
racl whitelist list "my network"
racl whitelist list "broken mta"
racl greylist default delay 30m autowhite 30d

Now, edit /etc/default/milter-greylist and set the following (make sure that the socket path is good, since the value that is commented out by default points to a different filename than the one set by default in the milter config file -- the filename that we're using):


Let's create the directory where the socket will be held. Because of a longstanding bug in the debian package we need to hack our way to having the right permissions for the socket. We can then restart the milter:

mkdir /var/spool/postfix/milter-greylist
chmod 2755 /var/spool/postfix/milter-greylist
chown greylist:postfix /var/spool/postfix/milter-greylist
service milter-greylist restart

The only step left is to let Postfix know how to use that milter. We'll want to place it before the two milters we set above since they are both very slow and CPU intensive. This way, greylisting will remove dumb spammers while also avoiding them to overload your CPU (which could easily lead to a denial of service):

postconf -e 'milter_connect_macros = i b j _ {daemon_name} {if_name} {client_addr}'
postconf -e 'smtpd_milters = unix:/milter-greylist/milter-greylist.sock, unix:/clamav/clamav-milter.ctl, unix:/spamass/spamass.sock'
postfix reload

Testing Greylist

If you want to have a better understanding of what's going on, you can comment out the line "quiet" in /etc/milter-greylist/greylist.conf. This will make the rejection message specify how much time is left for the greylisting.

Send an e-mail to a user on the server in the same manner as you did above when testing delivery, although for this test you'll need to send the e-mail from another computer (since we whitelisted


You should get rejected with a message looking like this:

<** 451 4.7.1 Greylisting in action, please come back in 00:23:33

Wait until that period is elapsed and re-try sending your mail. You should now be accepted.

Make sure you go back to the config file and comment out the "quiet" line again so that you don't tell spammers how much time they need to wait.

Additional functionality

Vacation autoreplies

Postfixadmin comes with a script for handling vacation/away messages (e.g. autoreplies). By default, though, that script is not functional. And the reason for this is that some of its dependencies are in "non-free". So in order to activate this feature, we'll first have to ensure that we are using the non-free section. Since I don't yet, here's how I've added it:

sed -i 's/\(main\)$/\1 contrib non-free/' /etc/apt/sources.list
apt-get update

Now, all the remaining steps come almost exaclty as they were written on this tutorial (and also documented in /usr/share/doc/postfixadmin/examples/VIRTUAL_VACATION/INSTALL.TXT.gz, which comes with the postfixadmin package).

First, install the script's dependencies:

apt-get install libmail-sender-perl libdbd-mysql-perl libemail-valid-perl libmime-perl liblog-log4perl-perl liblog-dispatch-perl libgetopt-argvfile-perl libmime-charset-perl libmime-encwords-perl

Next, we'll create a user solely for running the script. It will run as a transport for postfix, so we need to isolate it from accessing anything useful. We'll also copy the script into the user's home directory. Lastly, we'll setup a log directory in which the user is able to write so that we can have something to debug problems:

groupadd -r -g 65501 vacation
useradd -r -u 65501 -g vacation -d /var/spool/vacation -s /sbin/nologin vacation
mkdir /var/spool/vacation
cp /usr/share/doc/postfixadmin/examples/VIRTUAL_VACATION/ /var/spool/vacation/
gunzip /var/spool/vacation/
chown -R vacation:vacation /var/spool/vacation
chmod -R 0700 /var/spool/vacation

The script parses, if it exists, an alternate configuration file in which we can override some values. But first, since the script needs to be able to update the vacation_notification table, let's grant insertion privileges to the "postfix" user so that we can use the lesser-priviledged account with it:

mysql -e 'GRANT INSERT,UPDATE ON postfixadmin.vacation_notification TO "postfix"@"localhost"';

Create the file /etc/postfixadmin/vacation.conf and set its content to the following:

# db_type - uncomment one of these
our $db_type = 'mysql';

# leave empty for connection via UNIX socket
our $db_host = '';

# connection details
our $db_username = 'postfix';
our $db_password = 'something';
our $db_name     = 'postfixadmin';
our $vacation_domain = '';

# Set to 1 to enable logging to syslog.
our $syslog = 1;
# 2 = debug + info, 1 = info only, 0 = error only
our $log_level = 1;

# notification interval, in seconds
# set to 0 to notify only once
# e.g. 1 day ...
#my $interval = 60*60*24;
# disabled by default
our $interval = 0;

# perl will crash if the imported script doesn't end with a positive value .... wth

Now, we need to enable the feature in the postfixadmin configuration. Edit /etc/postfixadmin/config.local.php and add the following lines:

$CONF['vacation'] = 'YES';
$CONF['vacation_domain'] = '';

Last but not least, we need to teach postfix how to handle mail directed to that subdomain we configured above. First, we'll setup a new transport which sends mail to the perl script we just installed, then we'll map the subdomain to that transport.

Edit /etc/postfix/ and add the following to the end of the file:

vacation  unix  -       n       n       -       -       pipe
  flags=Rq user=vacation argv=/var/spool/vacation/ -f ${sender} ${recipient}

Since we've already created an empty transport map file earlier, we now only need to add an entry to it and to refresh it so that mail to the special subdomain is sent to the transport we just created. Edit /etc/postfix/transport and add the following line: vacation

Now refresh the compiled form of the file, add a final configuration item that's needed so that things go well with multiple recipients, and reload postfix so that it considers the new transport:

postmap /etc/postfix/transport
postconf -e 'vacation_destination_recipient_limit = 1'
postfix reload

Testing vacation messages

If you need more output from the vacation script to better debug what's happening, edit /etc/postfixadmin/vacation.conf and change the value for "$log_level" to 2.

To test this feature, we will first need to set the vacation message for an address. Login to postfixadmin as the super admin and go to the "Virtual list" and then click on the link named "Set vacation" beside "". Now type some text and hit the "Change/save vacation message" button. You should now see the link you used is marked "VACATION IS ON".

Now send an email to that address and see what happens:

swaks --from some_existing@email.address --server=hostname.yourdomain

You should see something similar to the following in syslog:

Dec  4 02:54:42 debian-squeeze /var/spool/vacation/ DEBUG - Script argument SMTP recipient is : '' and smtp_sender : 'some_existing@email.address'
Dec  4 02:54:42 debian-squeeze /var/spool/vacation/ DEBUG - Converted autoreply mailbox back to normal style - from to
Dec  4 02:54:42 debian-squeeze /var/spool/vacation/ DEBUG - Email headers have to: '' and From: 'some_existing@email.address'
Dec  4 02:54:43 debian-squeeze /var/spool/vacation/ DEBUG - Found '' has vacation active
Dec  4 02:54:43 debian-squeeze /var/spool/vacation/ DEBUG - Attempting to send vacation response for: unknown to: some_existing@email.address,, (test_mode = 0)
Dec  4 02:54:43 debian-squeeze /var/spool/vacation/ DEBUG - Asked to send vacation reply to thanks to unknown
Dec  4 02:54:43 debian-squeeze /var/spool/vacation/ DEBUG - Will send vacation response for unknown: FROM: (orig_to:, TO: some_existing@email.address; VACATION SUBJECT: Out of Office ; VACATION BODY: I will be away from january until mars.#015#012For urgent matters you can contact this person.
Dec  4 02:54:43 debian-squeeze postfix/smtpd[4267]: connect from localhost[]
Dec  4 02:54:43 debian-squeeze milter-greylist: smfi_getsymval failed for {i}
Dec  4 02:54:43 debian-squeeze milter-greylist: (unknown id): Sender IP and address <> are SPF-compliant, bypassing greylist
Dec  4 02:54:43 debian-squeeze postfix/smtpd[4267]: 0864440DBB: client=localhost[]
Dec  4 02:54:43 debian-squeeze postfix/cleanup[4272]: 0864440DBB: message-id=<>
Dec  4 02:54:43 debian-squeeze milter-greylist: smfi_getsymval failed for {if_addr}
Dec  4 02:54:43 debian-squeeze postfix/qmgr[4091]: 0864440DBB: from=<>, size=585, nrcpt=1 (queue active)
Dec  4 02:54:43 debian-squeeze postfix/smtpd[4267]: disconnect from localhost[]
Dec  4 02:54:43 debian-squeeze /var/spool/vacation/ DEBUG - Vacation response sent to some_existing@email.address, from
Dec  4 02:54:43 debian-squeeze postfix/pipe[4277]: 8C70B40851: to=<>, orig_to=<>, relay=vacation, delay=16, delays=16/0.03/0/0.41, dsn=2.0.0, status=sent (delivered via vacation service)
Dec  4 02:54:43 debian-squeeze postfix/qmgr[4091]: 8C70B40851: removed
Dec  4 02:54:43 debian-squeeze postfix/smtp[4279]: 0864440DBB: to=<some_existing@email.address>, relay=your.hostname[]:25, delay=0.7, delays=0.07/0.03/0.49/0.1, dsn=2.0.0, status=sent (250 2.0.0 Ok: queued as 98826FC1EFA)
Dec  4 02:54:43 debian-squeeze postfix/qmgr[4091]: 0864440DBB: removed

If all went well you should now have received an email with the appropriate subject and body. If you're having difficulty understanding the above log output, here's a short summary of what it says:

  • The script searches whether the addressed email has its vacation message set and sends a message with the subject and body that were set.
  • The script then connects to localhost to send an e-mail from the receiver to the sender and the message is queued.
  • The script exits after succeeding in its duty
  • The queued message is processed by postfix and is sent to the original sender.

If you changed the "$log_level" to 2 earlier, don't forget to set it back to 1 else you'll have lots of useless lines in your logs.

With the above settings, if a notification gets sent to the original sender but you don't receive it and want to test it again, you'll have to make a manipulation: either truncate the "vacation_notification" table or change "vacation.conf" to set "$interval" to something very low, like 1 (see above). Another trick is to remove the auto-reply in postfixadmin, which will delete all notifications for that address, and then to set it back.


In order to get quota to work, you need to tell dovecot how to get quota information, and then to activate it in the postfixadmin interface.

First, let's load the plugin by adding it to the list of plugins in /etc/dovecot/conf.d/10-mail.conf:

mail_plugins = $mail_plugins zlib quota

and also in /etc/dovecot/conf.d/20-imap.conf:

# Space separated list of plugins to load (default is global mail_plugins).
mail_plugins = $mail_plugins imap_quota

Next, we need to tell dovecot to use per-user quota values. We'll add a second rule in there so that the Trash directory has a little bit more space so that people can do some cleanup when they are overquota. Edit /etc/dovecot/conf.d/90-quota.conf:

plugin {
  # [...]
  quota = maildir:User quota
  quota_rule2 = Trash:storage=+100M

Telling dovecot where to find quota info is pretty simple, we need to modify "user_query" in dovecot's SQL configuration so that it returns a quota_rule column with dovecot's quota_rule format. Since we're using prefetch for userdb, we'll have to add the column to the "password_query" too. Edit /etc/dovecot/dovecot-sql.conf.ext and add the column. The two queries should now look like this:

password_query = SELECT username as user, password, CONCAT('/var/mail/vmail/', maildir) as userdb_home, 8 as userdb_uid, 8 as userdb_gid, concat('*:bytes=', quota) as userdb_quota_rule FROM mailbox INNER JOIN domain ON mailbox.domain=domain.domain WHERE username='%u' AND = true AND = true;
user_query = SELECT CONCAT('/var/mail/vmail/', maildir) as home, 8 as uid, 8 as gid, concat('*:bytes=', quota) as quota_rule FROM mailbox INNER JOIN domain ON mailbox.domain=domain.domain WHERE username='%u' AND = true AND = true;

Now to enable quotas in the postfixadmin interface, edit /etc/postfixadmin/config.local.php and add the following to it:

$CONF['quota'] = 'YES';
$CONF['maxquota'] = 0;  // I decided to not set a quota by default, but change this value to a number of Mb that should be the default value for your case.

Finally, restart dovecot:

service dovecot restart

Testing user quotas

In order to test this feature, we'll have to set a low quota on a user. Login to postfixadmin and click on the "Edit" link beside a user, then specify a low quota and click on the "Save" button. For this example we'll set a quota of 1Mb to the user "".

Find a file slightly below the quota you set (note that the contents of the mail itself is also accounted for in the quota, so the attachment shouldn't be exactly as big as the limit), and send it as an attachment to that user. We'll generate one with random content:

dd if=/dev/urandom of=900k_file bs=1024 count=900
swaks --from some_existing@email.address --server=hostname.yourdomain --attach - --suppress-data < 900k_file

This first attachment should be able to get to destination (supposing you haven't already sent more than 100kb of test emails). If you launch that swaks command again, you'll get an answer that looks odd at first: the server tells you "OK message queued as <some_message_ID>". However, if you look at the syslog, you'll see a message about an email being sent to some_existing@email.address:

Dec  4 16:48:09 debian-squeeze postfix/pipe[5316]: 3B73A40DA7: to=<>, relay=dovecot, delay=1.1, delays=0.97/0.02/0/0.1, dsn=2.0.0, status=sent (delivered via dovecot service)
Dec  4 16:48:09 debian-squeeze postfix/qmgr[4601]: 3B73A40DA7: removed
Dec  4 16:48:09 debian-squeeze postfix/cleanup[5313]: AA1D940DB0: message-id=<dovecot-1354657689-665110-0@hostname.yourdomain>
Dec  4 16:48:09 debian-squeeze postfix/qmgr[4601]: AA1D940DB0: from=<>, size=1930, nrcpt=1 (queue active)
Dec  4 16:48:10 debian-squeeze postfix/smtp[5323]: AA1D940DB0: to=<some_existing@email.address>, relay=your_mx[]:25, delay=0.47, delays=0.01/0.01/0.35/0.1, dsn=2.0.0, status=sent (250 2.0.0 Ok: queued as 13017FC3158)
Dec  4 16:48:10 debian-squeeze postfix/qmgr[4601]: AA1D940DB0: removed

Now in your inbox, you should see a message got to you that says your e-mail was rejected and in the body, you can see the cause which is because the user's inbox is over quota.

Configuring Mailman

We'd like to be able to control mailing lists under Make sure to create the appropriate DNS entry and to have it pointed to the mail server's IP address. Since this tutorial does a pretty good job of explaining the process and showing how to do things, we'll mostly stick to the steps there and keep explanations only to what differs that tutorial or needs further clarification.

If you remember we chose at the beginning of this howto to assume you already had a web server. We'll be working with Apache, so if you need to adapt to other software, you'll need to do it on your own. But it shouldn't be fairly difficult.

apt-get install mailman

ln -s /etc/mailman/apache.conf /etc/apache2/sites-available/

Edit /etc/mailman/apache.conf, uncomment the vhost block at the end and change it to suit your needs. (see the tutorial mentioned above for details)

mkdir /var/www/lists
apache2ctl graceful

Edit /etc/mailman/ and ensure the following configuration is present:

DEFAULT_URL_PATTERN = 'http://%s/'
GLOBAL_PIPELINE.insert(1, 'SpamAssassin')

postconf -e 'relay_domains = proxy:mysql:/etc/postfix/virtual/'
postconf -e 'mailman_destination_recipient_limit = 1'

Edit /etc/postfix/transport and append the following line: mailman

postmap /etc/postfix/transport
postfix reload
newlist mailman

Edit /etc/aliases and append to it the following lines:

mailman:              "|/var/lib/mailman/mail/mailman post mailman"
mailman-admin:        "|/var/lib/mailman/mail/mailman admin mailman"
mailman-bounces:      "|/var/lib/mailman/mail/mailman bounces mailman"
mailman-confirm:      "|/var/lib/mailman/mail/mailman confirm mailman"
mailman-join:         "|/var/lib/mailman/mail/mailman join mailman"
mailman-leave:        "|/var/lib/mailman/mail/mailman leave mailman"
mailman-owner:        "|/var/lib/mailman/mail/mailman owner mailman"
mailman-request:      "|/var/lib/mailman/mail/mailman request mailman"
mailman-subscribe:    "|/var/lib/mailman/mail/mailman subscribe mailman"
mailman-unsubscribe:  "|/var/lib/mailman/mail/mailman unsubscribe mailman"

service mailman start

Now visit with a browser and your should see the mailman interface.

We'll skip testing the setup for this piece of software for brevity.

However, it would require us to create a new list and subscribe more than one address to it, and then to send a message to the list and see if it gets sent to all members.

If you want to be able to manage lists on more than one subdomain, check out the last link in the bibliography section: it has a section about configuring Mailman for this purpose. It needs you to perform more operations before Mailman is functional, but you won't need to manually add aliases to /etc/aliases every time there is a new list.



Some of the software we've installed need to perform lots of DNS lookups, so in order to speed up subsequent lookups to host names we've already seen, you might consider setting up a DNS caching service: it can make something between a good and a huge difference on performance. Here's a quick example of such a setup with dnsmasq:

apt-get install dnsmasq

Edit /etc/dnsmasq.conf and uncomment and modify the lines #interface= and #bind-interfaces so that they look like:


Now edit /etc/resolv.conf and add a nameserver line for in the first position (e.g. above all others):


Finally, restart dnsmasq and then all services that process e-mails so that they consider the new contents of the /etc/resolv.conf file.

Sieve / ManageSieve

Server-side filters have the advantage that once they are set up, users can use whatever client and messages will get delivered to the right directory without the email client application having to do anything. Also, users won't loose their filters if they loose their laptop/desktop or if their desktop disk explodes.

It can also manage vacation messages for you, so it could be an interesting replacement to the feature from postfixadmin we're using above which is pretty clunky to setup.

However, sieve filters are not very user-friendly for most non-geek users. Users basically need to learn a (simple -- but still...) programming language to be able to write their filters. Roundcube and Squirrelmail (last updated in 2009!) have plugins to interact with ManageSieve to manage your server-side filters. That's something I'd need to explore. I just hope that some plugins for some clients do a good job of making it easy to build filters. Thunderbird's "Sieve" add-on is not very user-friendly for non-IT people. If you only want to use the vacation message feature from sieve, then Thunderbird's "Sieve Out of Office" add-on might be more interesting and easy to use.

Sieve is pretty easy to setup. Just install the following packages:

apt-get install dovecot-managesieved dovecot-sieve

then edit /etc/dovecot/15-lda.conf and uncomment the line that sets plugins and add "sieve" to the list:

protocol lda {
  # Space separated list of plugins to load (default is global mail_plugins).
  mail_plugins = $mail_plugins sieve

Then restart dovecot:

service dovecot restart

Watch out! ManageSieve might accept unencrypted connections. This could be an easy way to leak your users' passwords so you need to verify that this is not happening. After following the above guide, dovecot's ManageSieve server should offer no options for the "SASL" capability as long as you haven't initiated a TLS transaction. If the "SASL" capability is empty and the "STARTTLS" capability is visible, then your server is rejecting non-encrypted logins (which is what we want):

$ telnet your.server 4190
Connected to you.server.
Escape character is '^]'.
"IMPLEMENTATION" "Dovecot Pigeonhole"
"SIEVE" "fileinto reject envelope encoded-character vacation subaddress comparator-i;ascii-numeric relational regex imap4flags copy include variables body enotify environment mailbox date ihave"
"NOTIFY" "mailto"
"SASL" ""
"VERSION" "1.0"
OK "Dovecot ready."

If you're seeing "STARTTLS" in the server's welcome message but the "SASL" capability is not an empty list, then you need to correct this. This can be enforced by setting the following in /etc/dovecot/conf.d/10-auth.conf:

disable_plaintext_auth = yes

For more info on troubleshooting ManageSieve, check out the last link in the bibliography.