This HOWTO presents a way for configuring a Virtual Mail System using Exim, MySQL, SpamAssassin, ClamAV and Dovecot.
If you find this guide useful or have any ideas / improvements don't hesitate to drop me a mail.

Exim will be the main component in this HOWTO, as it's the mail server. Why exim? Because I like it! Have a look at a MTA Comparison.

A quick overwiew of this projects goals and features. Some assumptions I made about the setup and howto use it. You'll get the whole plot when I'm describing the Idea. All the techniques used will be explained there.
The implementation consists of installing and configuring the tools / software components that are connected. Either follow these instructions, or just grab all configfiles and adapt them to your needs.
Some additional material is provided at the end.

Best viewed with Clean Style.


The last 5 changes:
2010-11-02	+ SMAD -> VMSA (rename + SourceForge project)
2009-10-30	+ only shortened changelog showed by default
		+ /etc/exim/ better support for non-GNU systems by using awk instead of gawk 
		  and POSIX-compliant function definitions (thanks to Jesco Freund)
2009-10-03	+ exim.conf parsing error (in exec double ext archive) fixed
2009-09-27	+ missing routers fixed (introduced during update)
2009-09-15	+ domain-list double entry fixed (introduced during update)
For a complete list please see the CHANGELOG

Successful Feedback

I've received some feedback about successful installations using this guide under a number of Distributions. Perhaps you'll have to tweak a litte bit here and there but it already worked on:

Remember: try to contribute and give feedback.

Goals / Features

already implemented

  1. Virtual Users (they only exist in the database, so there's no need to give each user a real account including ssh-access)
  2. All User-data is stored centralized in a Database
  3. All mail (less than 32M) is virus-scanned. Virus-mails are rejected during SMTP-session (before accepting it), so the sender gets to know and we aren't responsible for bounces
  4. It's possible to integrate multiple virus scannig engines, which will scan the mail sequentially
  5. All mail (less than 1M) is spam-scored. Mail is tagged in headers only. User may define a personal spam-score-threshold and how to rewrite the subject (if at all). Very-highscore-mails are rejected during SMTP-session (before accepting it), so the sender gets to know and we aren't responsible for bounces
  6. Sending mail to foreign domains is only permitted after successful SMTP-authentification. The Connection has to be encrypted using SSL/TLS
  7. Receiving mail is only possible using SSL/TLS via IMAP/POP3
  8. Aliases (including plussed addresses), Catchall, allusers and conditional email-addresses (see Idea for complete overview!)
  9. Mails are stored in a MailDir /var/mail/DOMAIN/USER for user USER@DOMAIN
  10. Selective GreyListing support for mails that are likely to be spam, but not enough to be rejected
  11. Passwords are stored as base64-encoded SHA1-hashes
  12. Every daemon is running under an own user to seperate privilleges
  13. Users may change their preferences (spam-score-threshold) and passwords with SMAD
  14. Web-based Admin Interface
  15. reject archive attachments for files containing executeable double extensions (to reject unknown worms that embedds itself as *.pdf.exe in a zip-file)


  1. integrate Dovecot Antispam Plugin with sa-learn to correct SPAM/HAM learning
  2. keep track of Message-IDs of mails sent by users to reduce spam-score, if they appread in 'In-Reply-To:'-header (There's a SpamAssassin Plugin from GenieGate, which is rather old)
  3. limit users in sending rate or message size (this probably won't be implemented as I can't see any need in domineering a user)
  4. auto-responder (people keep asking for such stuff...)
  5. disable account with hint for new address (and silently forward mails)
  6. quota (just buy another disk...)
  7. group exim config by features to be enabled through config (.ifdef)
  8. admin (smad) may write mails to all users on system (exim needs access to smad tables :()
  9. sieve support for server-side filtering. which mail-client supports setting this? May only be usefull, if implemented in SMAD (sieve-php), or Squirrelmail (avelsieve), or Thunderbird (add-on)
  10. use more 'no_more' in routers
  11. ...


The following assumptions are made:

I will not go into details installing the needed software, as it's dependent on your distro. I'll just assume it's all right there.
As configuration may vary accross different distros and I'm using Gentoo, you'll have to figure out the difference to your one. However it should not be that hard, perhaps some different config file locations or user names running the services. Keep in mind to adapt them.
So far I've got success stories from Debian and FreeBSD users. Please provide feedback!

You are familiar with the used programs or at least with their functionality. This guide does not make sense, if you don't know what e.g. MySQL or an IMAP-Server may be useful for.
The reason is that you should be able to adjust the config to your needs. This way you may exchange a component through another piece of software. If you don't want to use ClamAV, but another or additional virus-scanner it's helpful to understand the Exim-config. Or perhaps you don't like the idea of GreyListing and want to get rid of this functionality.
So if you understand the way of configuring it you may use this guide as the glue for connecting the components. This way you're able to adapt it better to your needs as they may be different to mine.

As various used Programs need to communicate with each other, I configure them to use UNIX Domain Sockets, whenever possible. Otherwise they are bound to the loopback-interface. This way no unnecessary ports are opened to the public, thus avoiding additional attack-vectors.

Of course this does not affect the SMTP-server or IMAP/POP3-server, as we do want to provide these services to the public. ;-)

Apart from that it is possible to deploy each tool onto another hardware, for better scalability. They all are able to communicate via IP.

I'm not using dovecots new SASL-Authenticator to implement SMTP-Auth in exim, instead both of them authenticate against MySQL on their own.

I'm not using dovecots delivery feature (nor any other maildropper) to store incomming mails in MailDir, instead exim stores them directly there. If you experience problems with dovecot getting slow on building an index on access, you may take the benefit of using dovecots deliver, which updates the index on every new incomming mail. That depends on your hardware, but this average server will take just about a second in folders with 16k+ mails for a full index-recreation, so in general you shouldn't even notice...

Tools / Software used

The following software is used in this HOWTO, so I suggest installing it via the packet-management of your favorite distro. The exact versions I used are noted, but you should be fine with the features the major-version-number gives you.


The idea of the whole virtual mail system is to reduce administrative overhead.
All settings are stored in a MySQL database and all services such as SMTP and IMAP should use them.


The domains to accept mail for are not configured explicit. They are automatically taken from User-, Alias- and Catchall-Definitions.


A user is identified by his email-address (served by the system) and his password.
You can consider it as an account.

Users are able to send mail using SMTP-AUTH and read mail using IMAP or POP3. These privileges may be revoked.

There are several settings a user may set (e.g. through SMAD):

In all further examples let's say is a user.


Aliases are email-addresses that are redirected to another address (local or remote, even aliases again).
E.g. may redirect all mails to

An alias may redirect a mail to multiple recipients.
E.g. may redirect an incomming mail to and

By default every user gets some aliases defined automatically according to the "plussed addressing scheme".
Mail to gets redirected to the user's mailbox, where string can be anything valid in an email-local-part.
string gets introduced by the +-sign. Multiple +-signs are valid.

This can be used to publish different mail-addresses to different people in order to track who sold your address to whom.
E.g. you give the address to company A and one day you receive a mail to that alias-address from company B, you'll notice where they got the address from.

By the way I noticed that most harvesters used to gather email-addresses from web-pages seem to choke on these plussed addresses.
I've setup a page with fake addresses that is only visible to crawlers/harvesters and was disappointed that this spamtrap didn't received a mail for months (half a year as of 2007-04-22).
On examining the mail-server logs I discovered a lot of rejected mails due to non-existent local email-addresses. Taking a closer look on these failed addresses I found out that most of them where cropped after the +-sign. E.g. from this page harvesters may find, but are adding to their database.
Obviously these broken harvesters can be tricked by using such a plussed address where the wrongly cropped address is invalid and rejected by the mail-server.

Note: some websites are coded with too restrictive input filters that won't allow some characters in an email-address. However - using them is not your fault. If you are in doubt, see this comprehensive list of allowed characters.


Taking the last mentioned method to the next level, you can publish alias-addresses that are valid only if a condition that is embeded in the address matches.
Conditionals are introduced by the #-sign and take a parameter that's seperated from the condition by another #-sign.
The general syntax for a conditional email-address is for user.
Remember: they do not need to be setup by the user. Just use them!

For now there a two conditions implemented:


An email-address like is valid and the mail server accepts it, if the current date is not higher than YYYYMMDD, which is a day encoded like 20070308 and the user exists.

This way you can publish a temporary valid address e.g. for an one-time online-game.
Or you may embed such an address on your webpage that's dynamically generated to be valid one week, like, which is generated with the following PHP-code: <?="user#before#" . date("Ymd", ( time() + (7 * 24 * 60 * 60) )) . ""?>
People may contact you, but crawlers harvesting email-addresses won't be able to spam you after the defined time.


An email-address like is valid and the mail server accepts it, if the domain of the sender's address matches domain and the user exists.

E.g. you can publish for the newsletter of company A which is expected to send news from domain
If they trade this address to some other company, mail from them will get rejected.


This one is quite simple.
If a mail could not be accepted because there's no user like and you've defined a CatchAll for the domain, incomming mail gets redirected like an alias to another address.

Somebody requested this feature. In my opinion it just increases spam in your inbox, but on the other hand if you've plenty of unused domains...


For all hosted domains there's an email-address named alle@domain, that redirect incomming mail to all users of domain.
The only limitation is that it's only for internal use, which means to send a mail to this address you have to be authenticated using SMTP-AUTH.

Maintaining the System

There's a fully buzzword-compliant web-interface available, which should allow you to to everything you need.
You may also maintain it using phpMyAdmin, as all tables and columns are documented as comments in their database-structure. Additionally the database is really flat structured and thus more like a (network-attached) config file, for easy maintainance (this was actually a feature in the old times without SMAD).

adding a user directly in MySQL

Just create a line in the user-table.
The password-hash can be generated with

dovecotpw -s SHA1

Make sure, you chop the prefix {SHA1} from the output hash.
The database-structure itself should be self-documenting. If you're unsure, check out the table-comments.
The following SQL should create a new user, using default values for optional fields:

INSERT INTO `user` (`username`, `domain`, `password`, `Full Name`) VALUES ('test', '', 'W6ph5Mm5Pz8GgiULbPgzG37mj9g=', 'test user entry for demo')

There are more ways to create the password hash. Just have a look at the Addon Section.


There are several techniques implemented to catch spam & virii.
All of them have their advantages and disadvantages, so our goal is to mix them up at a good ratio.

Tagging & Rejecting at SMTP-time vs. User-Thresholds

It's important to handle such incomming mail in a transparent way.
No mail should get lost without a trace, so we cannot accept it in the first place and silently drop it later. In this situation the sender doesn't know that his mail never reaches the recipient. As spam & virus mails often use faked sender-addresses, we end up in clogged mail-queues or flooding the wrong person, if we send out notifications.

So the whole decision has to be made while receiving the mail.
On the one hand this is quite ressource-intensive, but on the other hand we can reject the mail and the sending mail-server recognizes that and is responsible for notifying the sender.
So our mail-queue keeps clean and we don't bother the wrong person.

But now there's another inconvenience.
As we want different users to have different spam-thresholds we cannot reject spam because an incomming mail may have multiple users with different thresholds.
Aliases need to be resolved and the final recipient may even reside on a foreign mail-server.

Thus we just tag the spammy mail with the score in a header or reject it, if its score is far above the maximum threshold in all user preferences. There are ways to limit the number of recipients per mail, but I dont't like the idea because of broken mail-servers. Later at delivery-time, we know the user the mail is locally deliverd to. So we can evaluate the spam-score with his preferences, but it's to late to reject the mail.

So finally it's the users choice to automatically filter out mails with a high spam-score in his client.

For the same reason we only can use a system-wide bayes-database.

I know these are a lot of disadvantages, but for me it's important to have a reliable mail system and I definitely like the advantages of getting rid of most unwanted mail at SMTP-time.
If you know a way to solve this problem, please let me know!

Selective GreyListing

I don't like GreyListing in general, because it may delay legitimate mail.
But as the advantage of rejecting spam- & worm-spreading bot-nets is inviting, I've implemented a simple selective version within Exim.

All incomming mails that scores more than GREYLIST_SPAM_THRESHOLD (in /etc/exim/exim.conf) will be temporary rejected if seen for the first time and on consecutive attempts for at least GREYLIST_TIMEOUT seconds.

This way we catch low scored mail that's likely spam, while not delaying most legitimate mail. Through the delay period we increase the chance that updated Colaborative Checksum Lists, URIBL or DNSBL will score the mail higher in the meantime.

If you have backup MX servers for you domain, or some foreign accounts forwarded, you'll probably won't benefit that much from this method.
However - it won't harm you, so give it a try...

Rejecting dangerous extensions

Some Windows executeable file extensions won't be allowed as attachments. This avoids installing a trojan horse accidentally.

Since the business gets more progressive, executeable viruses may be hidden inside archives (e.g. zip). As it now needs more than one click to get infected (open the archive, click the trojan) we won't do anything against executeables in archives.

However - there's a technique to put an executeable in an archive and camouflage it by prepending another (harmless) extension, like invoice.pdf.exe. Since Windows may supress the extension, it will actually trick the user by showing only invoice.pdf.

We will reject these potentially dangerous executeable double extensions, because there's no valid use case to send those files.

Aliases & Conditional addresses

See Aliases and Conditionals to learn how to avoid spam by protecting your mail-address.

Exim hints to SpamAssassin

Exim adds several headers to an incomming mail that will give SpamAssassin some hints, later (via /etc/spamassassin/

Sender Address Verification

Exim performs a callout verification on the sender address. This tests wheather this address accepts mail, initiating a SMTP-connection to the senders mail-server.
Valid and existing addressed will get some negative spam-points, while non-existant ones will score real hard.

Some spammers use the recipents address as the senders address, or an address of the recipients domain, thinking they may be whitelisted.
As we only allow our users to send mail using SMTP-AUTH and spammers can't do this, we can reject those mails.

Authenticated Sender

Authenticated senders will receive some negative spam-points, as we trust them.
However - since they may have a worm / bot installed that uses their authentication credencials, we still scan 'em!

Non-RFC-conformant (E)HELO

Most spam uses non-RFC-conformant (E)HELO greetings at the beginning of their SMTP-connection. These failures will score some spam-points later. As there are some MTAs out there, which seem to be broken, it's better not to reject them.

Some spammers try to impersonate our mail-server, thinking it's more trusted or whitelisted.
These are rejected, as they can't have our IP ;-).

Multiple virus-scanning engines

It's possible to configure exim for more than one virus scanning engine. Each incomming mail has to pass a chain of all engines to be accepted. After the first engine detects a virus, the message is rejected.

The list of supported engines in long: Kaspersky Version 4/5, ClamAV, DrWeb, F-Secure, Sophie and any command-line-scanner like F-Prot. (from Exim Documentation - Content Scanning at ACL time - 41.1 Scanning for viruses)



Install ClamAV, SpamAssassin, Dovecot and Exim. It's obvious that you'll need MySQL, too - but that may be installed as a dependency. Just make sure you're using at least MySQL version 5, since older versions lacked handling of trailing blanks in strings. (That's annoying if a user configures the system to prepend 'SPAM ' to the subject of a spammy mail and MySQL forgets about that blank.)

Be sure to install/compile your packages with all needed features. In Gentoo it'll be usefull to add support for mysql, ssl, maildir as global USE-flags for all packages.

After installing all services, keep in mind to add them to your startup-scripts as you want to run them after booting ;-)

SSL-Certificate / Key

In order to use SSL encrypted connections you need a certificate and key. There are several possibilities to create them. If you need to manage some more you may want to use a tool like TinyCA.

For simplicity I use the same cert/key for both programs: Exim and Dovecot. They should be placed under /etc/exim/exim.crt and /etc/exim/exim.key.

Don't forget to set the file-permissions correctly. (owner=mail / exim.key should be readable only for owner)


Include support for exiscan-acl. Optional lmtp, spf, srs. Exclude gnutls, as ssl will provide the same features.

Daemon should be run as user mail.


Optional include POP3 support: pop3d

Daemon should be run as user dovecot.


Daemon should be run as user clamav. Make sure it's compiled with gmp-support to verify signature updates.


This service isn't installed the same way as the others in Gentoo. Somehow they prefer to run it on a per-system-user basis, but actually we only want users to be virtual. That's why this setup is described more detailed.

We are running SpamAssassin in a system-wide config under user spamassassin, so create him and his home-directory /home/spamassassin with appropriate permissions. (bayes database will be stored there):

adduser --home-dir /home/spamassassin --create-home --comment "added for spamd" --user-group spamassassin

Create a directory /var/run/spamassassin owned by the user spamassassin. I prefer all runtime-files for a service like sockets or pid-files in a own directory.

Make sure to start spamd with correct options. This is done in /etc/conf.d/spamd in Gentoo. (Debian e.g. uses /etc/default/spamassassin and a slightly different syntax):

SPAMD_OPTS="--max-children 15 --max-conn-per-child=25 -H -u spamassassin --socketpath=/var/run/spamassassin/spamd.sock --socketowner=spamassassin --socketgroup=spamassassin"

As all mail is content-scanned during the SMTP-session it's nessessary to react fast. Some seconds scan-time per mail is ok, but when SpamAssassin is expiring bayes tokens during a session that may take long and timeout the connection.
Therefore we create a daily cronjob /etc/cron.daily/spamassassin-expire-bayes that expires bayes tokens.
Make sure, it's executeable!

Be sure to create another daily cronjob /etc/cron.daily/spamassassin-update that checks for rule-updates.
Make sure, it's executeable!


All packages installed? Everything works so far? Then let's get started...


Install the database scheme from system-scheme.sql:

mysql -p <system-scheme.sql

Create MySQL users for exim and dovecot with read-only permissions on the new database. In addition exim needs INSERT-permission for the greylist-table. Substitute EXIMPASSWORD and DOVECOTPASSWORD with some random secret (e.g. created with 'pwgen --secure 32 2').

GRANT SELECT ON `system`.* TO 'exim'@'localhost';
GRANT INSERT ON `system`.`greylist` TO 'exim'@'localhost';

GRANT SELECT ON `system`.* TO 'dovecot'@'localhost';


Create /etc/dovecot/dovecot-sql.conf with following content (and substitute your DOVECOTPASSWORD):

driver = mysql
connect = host=/var/run/mysqld/mysqld.sock dbname=system user=dovecot password=DOVECOTPASSWORD
default_pass_scheme = SHA1
password_query = SELECT password, '/var/mail/%d/%n' AS userdb_home, 'mail' AS userdb_uid, 'mail' AS userdb_gid FROM user WHERE username = '%n' AND domain = '%d' AND IMAP_allowed = 'YES'

Edit /etc/dovecot/dovecot.conf to suit your needs and be sure it contains:

protocols = imap imaps
ssl_cert_file = /etc/exim/exim.crt
ssl_key_file = /etc/exim/exim.key
disable_plaintext_auth = yes
login_user = dovecot
verbose_proctitle = yes
default_mail_env = maildir:/var/mail/%d/%n
protocol pop3 {
	# fix broken pop3 logins
	pop3_uidl_format = %08Xu%08Xv 
auth default {
	mechanisms = plain
	passdb sql {
		args = /etc/dovecot/dovecot-sql.conf
	userdb prefetch {
	user = nobody


Because ClamAV needs to access the temporary files created by Exim for scanning, make sure the user running clamd (clamav) is in mail-group (the GID that Exim runs = mail) by adding to /etc/group a line like:


Since version 0.90 the configuration syntax has changed. Pick your version:

current versions (since 0.90)

Edit /etc/clamd.conf to suit your needs and make it contain:

PidFile /var/run/clamav/
TemporaryDirectory /tmp
DatabaseDirectory /var/lib/clamav
LocalSocket /var/run/clamav/clamd.sock
FixStaleSocket yes
StreamMaxLength 32M
FollowDirectorySymlinks yes
FollowFileSymlinks yes
User clamav
AllowSupplementaryGroups yes

Edit /etc/freshclam.conf to suit your needs and contain:

UpdateLogFile /var/log/clamav/freshclam.log
PidFile /var/run/clamav/
DatabaseOwner clamav
ScriptedUpdates yes
Checks 96
NotifyClamd /etc/clamd.conf
older versions (before 0.90)

Edit /etc/clamd.conf to suit your needs and make it contain:


Edit /etc/freshclam.conf to suit your needs and contain:



Configure SpamAssassin to your needs, but ensure to have all needed plugins installed (edit *.pre)

Add /etc/spamassassin/ to take advantage of spam-hints configured in Exim.

Add /etc/spamassassin/ and /etc/spamassassin/ to catch spam that is send through a low priority MX, while your server is up.

Add /etc/spamassassin/ and /etc/spamassassin/ to catch spam with fuzzy checksums by iX magazine (german article, project page).

Add /etc/spamassassin/ to use some additional DNSRBLs.

Make sure your /etc/spamassassin/ contains the following directives:

# we run spamassassin through exiscan-acl from exim.
# thus, in exim we cannot take modified spamd output as mail.
# no message modifications will be visible to exim!
# the only way to get debugging output into the mail is a report.
report_safe 0
# somehow bayes did not work, so try to specify database path directly
bayes_path		/home/spamassassin/.spamassassin/bayes
# Enable the Bayes system
use_bayes               	1
use_bayes_rules			1
bayes_auto_learn		1
bayes_use_hapaxes		1
bayes_expiry_max_db_size	400000
# do not automatically expire database, as it causes long scantimes (and timeouts for exim)
# make sure the cronjob doing this in an external process is setup!
bayes_auto_expire		0
# Enable or disable network checks
skip_rbl_checks         0
use_dcc                 1
use_pyzor               1
dcc_path		/usr/bin/dccproc
pyzor_path		/usr/bin/pyzor
# as the server is unreliable in these times, increase the timeout
pyzor_timeout 10
# new template. Try to keep it under 78 columns (inside the the dots below).
#      ........................................................................
report SpamAssassin _VERSION_ on host _HOSTNAME_ 
report scan-date              = _DATE_ 
report score                  = _SCORE_ 
report bayes-score            = _BAYES_  
report bayes-token-summary    = _TOKENSUMMARY_ 
report bayes-token-spam-count = _BAYESTCSPAMMY_
report bayes-token-ham-count  = _BAYESTCHAMMY_
report bayes-token-spam       = _SPAMMYTOKENS(16,short)_
report bayes-token-ham        = _HAMMYTOKENS(16,short)_
report bayes-auto-learned     = _AUTOLEARN_ _AUTOLEARNSCORE_
report ASN                    = _ASN_ _ASNCIDR_
report possible-languages     = _LANGUAGES_
report relayed-countries      = _RELAYCOUNTRY_
report pyzor                  = _PYZOR_
report RBL                    = _RBL_
report DCC-brand              = _DCCB_
report DCC-result             = _DCCR_
report ==== ====================== ==================================================
report " pts  rule name              description"
report ---- ---------------------- --------------------------------------------------
report _SUMMARY_
#      ........................................................................


Create the helper script /etc/exim/

Use exim.conf.dist as initial /etc/exim/exim.conf and edit it to suit your needs by inserting/modifing the following lines:

Insert MySQL-connection-config with substituted EXIMPASSWORD:

# mysql auth
hide mysql_servers = localhost/system/exim/EXIMPASSWORD

Define and adjust some variables:

# seconds after a greylisted message is accepted (10 minutes)
GREYLIST_TIMEOUT = ${eval:10*60}
# integer spam score threshold to activate selective greylisting (1.0 points)
# messages bigger than this aren't spam-scanned
# messages bigger than this aren't virus-scanned

Set primary_hostname to your fully qualified server-name.

Make Exim listen on SMTP-SSL (465) and submission port (587), too:

# ports to listen on (smtps is forced to use TLS/SSL via tls_on_connect_ports)
daemon_smtp_ports = smtp : smtps : submission

localdomains are fetched from database. if mail is comming for USER@DOMAIN, it will return DOMAIN, so to make Exim feel responsible for a domain just add a user in the database.

domainlist local_domains = ${lookup mysql {\
    				SELECT domain FROM user WHERE domain='${quote_mysql:$domain}' \
      			      UNION \
    				SELECT domain FROM alias WHERE domain='${quote_mysql:$domain}' \
      			      UNION \
    				SELECT domain FROM catchall WHERE domain='${quote_mysql:$domain}'\

Some SSL/TLS configuration. use your certificates and enforces SSL on SMTP-SSL

# SSL/TLS config
tls_advertise_hosts = *
# additionally listen on ssl/smtp
tls_on_connect_ports = 465		
tls_certificate = /etc/exim/exim.crt
tls_privatekey = /etc/exim/exim.key
# log some details
log_selector = +tls_cipher +tls_peerdn

Define our ACLs to be called on RCPT and DATA SMTP-command:

acl_smtp_rcpt = acl_check_rcpt
acl_smtp_data = acl_check_data

Define Control for virus-scanner as an exim variable, so we can use multiple engines later

av_scanner = $acl_m0

Define SpamAssassin-connection via socket:

spamd_address = /var/run/spamassassin/spamd.sock

Add these lines after 'accept hosts = :' in acl_check_rcpt:

# temporary reject message, if already greylisted and entry hasn't expired yet                                                                                                                                                       
# authenticated users skip this                                                                                                                                                                                                      
defer message         = Your Message is currently still greylisted! Please try again later.                                                                                                                                          
        log_message     = message from ${sender_address} over [${sender_host_address}] is still GreyListed   
        !authenticated  = *
        # true, if triple is in db and not yet GREYLIST_TIMEOUT seconds since first seen
        # false, else (older or not in db)
        condition       = ${if >={GREYLIST_TIMEOUT}{${lookup mysql{\
                            SELECT (UNIX_TIMESTAMP()-MAX(first_seen)) AS QueueTime \ 
                            FROM greylist \
                            WHERE SenderIP = '${quote_mysql:$sender_host_address}' \ 
                            AND SenderAddress = '${quote_mysql:$sender_address}' \

  # deny, if foreign, unauthenticated connection claims to come from a local domain
  # 2009-08-01   this has some strange behaviour (blocking) on two systems (A & B are different servers) with this config under the following condition
  #                                  A: user@Adomain writes to user user@Bdomain (which is an alias for someotheruser@Adomain)
  #                                  if your users have such circular aliases on different servers using this config, simply comment it out!
  deny  message         = Sender claims to have a local address, but is neither authenticated nor relayed (try using SMTP-AUTH!)
        log_message     = Forged Sender address (claims to be local user [${sender_address}], but isn't authenticated)
        !hosts          = +relay_from_hosts
        !authenticated  = *
        condition       = ${if match_domain{$sender_address_domain}{+local_domains}}
  warn  message         = You cannot be localhost.localdomain in the internet
        log_message     = HELO is faked as localhost.localdomain
        condition       = ${if match{$sender_helo_name}{\Nlocalhost\.localdomain\N}}
  # we're doing HELO checks here, because we can't add headers in acl_smtp_helo
  warn  message         = X-Invalid-HELO: HELO is IP only (See RFC2821 4.1.3)
        log_message     = HELO ($sender_helo_name) is IP only (See RFC2821 4.1.3)
        condition       = ${if isip{$sender_helo_name}}
  warn  message         = X-Invalid-HELO: HELO is no FQDN (contains no dot) (See RFC2821
        log_message     = HELO ($sender_helo_name) is no FQDN (contains no dot) (See RFC2821
        # Required because "[IPv6:<address>]" will have no .s
        condition       = ${if match{$sender_helo_name}{\N^\[\N}{no}{yes}}
        condition       = ${if match{$sender_helo_name}{\N\.\N}{no}{yes}}
  warn  message         = X-Invalid-HELO: HELO is no FQDN (ends in dot) (See RFC2821
        log_message     = HELO ($sender_helo_name) is no FQDN (ends in dot) (See RFC2821
        condition       = ${if match{$sender_helo_name}{\N\.$\N}}
  warn  message         = X-Invalid-HELO: HELO is no FQDN (contains double dot) (See RFC2821
        log_message     = HELO ($sender_helo_name) is no FQDN (contains double dot) (See RFC2821
        condition       = ${if match{$sender_helo_name}{\N\.\.\N}}
  warn  message         = X-Invalid-HELO: Host impersonating [$primary_hostname]
        log_message     = HELO ($sender_helo_name) impersonating [$primary_hostname]
        condition       = ${if match{$sender_helo_name}{$primary_hostname}{yes}{no}}
        # TODO: nicht auf loopback generieren
  warn  message         = X-Invalid-HELO: $interface_address is _my_ address
        log_message     = HELO ($sender_helo_name) uses _my_ address ($interface_address)
        # [own IP] or even without brackets as HELO
        condition       = ${if or{{\
  warn  message         = X-Invalid-HELO: no HELO 
        log_message     = no HELO ($sender_helo_name)
        condition       = ${if !def:sender_helo_name}

Add these lines after 'require verify = sender' in acl_check_rcpt:

# embed a header flag, if sender callout verification fails. this may lead to rejection in future, or give a hint to bayes filter
# the next both directives have complement verify conditions, so only one matches
warn	message		= X-Sender-Verify: FAILED ($sender_verify_failure)
	log_message	= Sender ($sender_address) could not be verified using callout: $acl_verify_message ($sender_verify_failure)
	!verify		= sender/callout=10s,random
warn	message		= X-Sender-Verify: SUCCEEDED (sender exists & accepts mail)
	verify		= sender/callout=10s,random

Edit the line after 'accept authenticated = *' in acl_check_rcpt:

	control       = submission/sender_retain/domain=

Exchange all lines of DATA-ACL acl_check_data with (write new):

# Unpack MIME containers and reject file extensions
# used by worms. Note that the extension list may be
# incomplete.
deny	message 	= $found_extension files are not accepted here
	demime 		= com:exe:vbs:bat:pif:reg:scr

# Reject messages that have serious MIME errors.
# This calls the demime condition again, but will return cached results.
deny	message 	= Serious MIME defect detected ($demime_reason).
	demime 		= *
	condition 	= ${if >{$demime_errorlevel}{2}{1}{0}}

  # Deny if the message contains a virus. Before enabling this check, you
  # must install a virus scanner and set the av_scanner option above.
  deny  message         = This message contains a virus ($malware_name) and is rejected.
        log_message     = rejected VIRUS ($malware_name) from $sender_address to $recipients (ClamAV)
        set acl_m0      = clamd:/var/run/clamav/clamd.sock
        condition       = ${if < {$message_size}{VIRUS_FILESIZE_LIMIT}}
        demime          = *
        malware         = *

# 2009-08-01   disable f-prot for now, since its usage has changed
#                                  this is the place to configure additional virus scanning engines.
#                                  just copy and modify this block (read exim doc for available scanners)
#  deny message         = This message contains a virus ($malware_name) and is rejected.
#       log_message     = rejected VIRUS ($malware_name) from $sender_address to $recipients (F-Prot)
#       set acl_m0      = cmdline:/usr/bin/f-prot -ai -archive -collect -dumb -packed %s:Infection. :Infection. (.+)\$
#       condition       = ${if < {$message_size}{VIRUS_FILESIZE_LIMIT}}
#       demime          = *
#       malware         = *

# reject executeable double extensions in archives
  deny  demime         = zip:rar:arj:tar:tgz:gz:bz2
        condition      = ${run{/etc/exim/ $message_exim_id ${lc:$found_extension}}{no}{yes}}
        message        = This message contains an unwanted binary Attachment in ${uc:$found_extension} file using a double extension
        log_message    = ${uc:$found_extension} archive contains potential dangerous double extension.
        delay          = 15s

 # Add headers to all messages (:true). Before enabling this,
  # you must install SpamAssassin. You may also need to set the spamd_address
  # option above.
  warn  message         = X-Spam-Score: $spam_score\n\
                          X-Spam-Score-Int: $spam_score_int\n\
                          X-Spam-Bar: $spam_bar\n\
                          X-Spam-Report: $spam_report
        !authenticated  = *
        condition       = ${if < {$message_size}{SPAM_FILESIZE_LIMIT}}  
        spam            = spamassassin:true

  # temp. reject messages that seem to have timeouts during spam-scan
  defer message         = Temporary error while spam-scanning. Please try again later.
        log_message     = message temporarily rejected, because of spam-scan error (maybe timeout)
        !authenticated  = *
        condition       = ${if < {$message_size}{SPAM_FILESIZE_LIMIT}}
        condition       = ${if !def:spam_score}

  # Reject spam messages with score over 10+2*max_score_from_db (fallback=15 if mysql fails), using an extra condition.
  deny  message         = This message is classified as UBE (SPAM) and therefore rejected. You scored $spam_score points. Congratulations!
        #spam           = spamassassin:true
        !authenticated  = *
        condition       = ${if >={$spam_score_int}{${lookup mysql{\
                                SELECT ((max(spam_threshold)*2+10)*10) AS spam_reject_threshold \
                                FROM user \
                                WHERE SMTP_allowed='YES' \
  # temporary reject message for greylisting, if integer spamscore is above GREYLIST_SPAM_THRESHOLD and the message (sender address + IP) is seen for the first time
  # authenticated users skip this
  defer message         = Your Message will be greylisted! Please try again in GREYLIST_TIMEOUT seconds.
  log_message           = message from ${sender_address} over [${sender_host_address}] will be GreyListed as it scores $spam_score spam points
  !authenticated        = *
  condition             = ${if >={$spam_score_int}{GREYLIST_SPAM_THRESHOLD}{true}{false}}
  # false, if triple is in db (at this point if it's in the timeout has expired)
  # true, if not
  condition             = ${lookup mysql{ \
                                SELECT MAX(first_seen) \
                                FROM greylist \
                                WHERE SenderIP = '${quote_mysql:$sender_host_address}' \
                                AND SenderAddress = '${quote_mysql:$sender_address}' \
  # insert triple into database (which should succeed)
  condition             = ${lookup mysql{ \
                                INSERT INTO greylist ( SenderIP, SenderAddress, first_seen ) \
                                VALUES ( '${quote_mysql:$sender_host_address}', '${quote_mysql:$sender_address}', UNIX_TIMESTAMP() ) \

  # log, if mail successfully passed greylisting
  warn  message         = X-GreyList: Message successfully passed GreyListing after $acl_m0 seconds.
        log_message     = message from ${sender_address} over [${sender_host_address}] with HELO ($sender_helo_name) successfully passed GreyListing after $acl_m0 seconds and scores $spam_score spam points
        !authenticated  = *
        # true, if triple is in db (at this point if it's in the timeout has expired)
        # false, if not
        condition       = ${lookup mysql{ \
                                SELECT MAX(first_seen) \
                                FROM greylist \
                                WHERE SenderIP = '${quote_mysql:$sender_host_address}' \
                                AND SenderAddress = '${quote_mysql:$sender_address}' \
        set     acl_m0  = ${eval:$tod_epoch-${lookup mysql{ \
                                SELECT MAX(first_seen) \
                                FROM greylist \
                                WHERE SenderIP = '${quote_mysql:$sender_host_address}' \
                                AND SenderAddress = '${quote_mysql:$sender_address}' \

 # save exim version and current date in header
  warn  message         = X-Exim-Version: $version_number (build at $compile_date)\n\
                          X-Date: $tod_log\n\
                          X-Connected-IP: $sender_host_address:$sender_host_port
  # save additional information in header
  warn message          = X-Message-Linecount: $message_linecount\n\  
                          X-Body-Linecount: $body_linecount\n\
                          X-Message-Size: $message_size\n\
                          X-Body-Size: $message_body_size
                          #X-Received-Count: $received_count\n\
                          #X-Recipient-Count: $recipients_count\n\
                          #X-Local-Recipient-Count: $rcpt_count\n\
                          #X-Local-Recipient-Defer-Count: $rcpt_defer_count\n\          
                          #X-Local-Recipient-Fail-Count: $rcpt_fail_count
  warn log_message = DEBUG  load_avgx1000: $load_average  spam_score: $spam_score  message_size: $message_size
  # finally accept the message in DATA ACL.

Define the following routers directly after the dnslookup-router in the router-section (Order is important!):

# alle@ is an auto-generated alias for all users of , which is only available for authenticated senders
# NOTE: we need to respect SMTP_allowed for every user!
  driver        = redirect
  # restriction to local domains only may be a double check, as data takes care of it already ;-)
  domains       = +local_domains
  local_parts   = alle
  data          = ${lookup mysql{ \
                        SELECT CONCAT(username,'@',domain) AS sendto \
                        FROM user \
                        WHERE domain='${quote_mysql:$domain}' \
                        AND SMTP_allowed='YES' \
  # treat localhost as authenticated
  condition     = ${if or {{\
                        eq {$sender_host_address}{}\
  file_transport = address_file
  pipe_transport = address_pipe
# an alias can be specified by giving one or more db-entries that match username and domain,
# or return a comma-seperated list of recipients.
# when no domain is specified in db-entry, recipients are taken from all domains with a matching username
# setting internal='YES' only allows sending mail to this alias, if authenticated (for internal usage)
  driver                = redirect
  # restriction to local domains only may be a double check, as data takes care of it already ;-)
  domains               = +local_domains
  file_transport        = address_file
  pipe_transport        = address_pipe
  data                  = ${if or {{\
                                eq {$sender_host_address}{}\
                                ${lookup mysql{ \
                                        SELECT sendto \
                                        FROM alias \
                                        WHERE ( username='${quote_mysql:$local_part}' \
                                        AND (domain='${quote_mysql:$domain}' OR domain='') )}}\
                          } {\
                                ${lookup mysql{ \
                                        SELECT sendto \
                                        FROM alias \
                                        WHERE ( ( username='${quote_mysql:$local_part}' AND (domain='${quote_mysql:$domain}' OR domain='') ) \
                                        AND internal='NO' )}}\
  local_part_suffix     = +*

# 2006-10-03 
# virtual user in mysql-db? and suffixed with a condition?
# currently supported:
#       #before#@                  e.g.: will accept mail for existing user, if current date is before 20061003
#       #fromdomain#@          e.g.: will accept mail for existing user, if current domain of sender is
  driver                = accept
  # restriction to local domains only may be a double check, as the condition takes care of it already ;-)
  domains               = +local_domains
  # 2006-10-08 
  # as we embed base64 encoded strings in local_part_suffix, and these are case sensitive, we must take care of them.
  # NOTE: this results in the missing feature, that conditional-mails in this router are case-sensitive! ( !=
  caseful_local_part    = true
  condition             = ${if and {{\
                                        # existing user
                                        eq {${lookup mysql{ \
                                                SELECT CONCAT(username,'@',domain) AS email \
                                                FROM user \
                                                WHERE username='${quote_mysql:$local_part}' \
                                                AND domain='${quote_mysql:$domain}' \
                                                AND SMTP_allowed='YES' \
                                        # different conditions
                                        or {{\
                                                # suffix contains #before# and date (yyyymmdd) is not yet #before#yyyymmdd
                                                and {{\
                                                        eq {${sg{$local_part_suffix}{^#([^#]+)#[0-9]\{8\}\$}{\$1}}}{before}\
                                                        lt {$tod_logfile}{${sg{$local_part_suffix}{^#[^#]+#([0-9]\{8\})\$}{\$1}}}\
                                                # suffix contains #fromdomain# and the domain-name of sender
                                                and {{\
                                                        eq {${sg{$local_part_suffix}{^#([^#]+)#.*\$}{\$1}}}{fromdomain}\
                                                        eq {$sender_address_domain}{${sg{$local_part_suffix}{^#[^#]+#(.*)\$}{\$1}}}\
                                                # suffix contains #b64from# and the base64 encoded address of sender    DOES NOT WORK YET!
                                                and {{\
                                                        eq {${sg{$local_part_suffix}{^#([^#]+)#.*\$}{\$1}}}{b64from}\
                                                        eq {${str2b64:$sender_address}}{${sg{$local_part_suffix}{^#[^#]+#(.*)\$}{\$1}}}\
  local_part_suffix     = #*
  transport             = local_mysql_delivery

# 2006-09-07        virtual user in mysql-db? (note: it's not nessessary to return real data)
  driver                = accept
  # restriction to local domains only may be a double check, as the condition takes care of it already ;-)
  domains               = +local_domains
  condition             = ${lookup mysql{ \
                                SELECT CONCAT(username,'@',domain) AS email \
                                FROM user \
                                WHERE username='${quote_mysql:$local_part}' \
                                AND domain='${quote_mysql:$domain}' \
                                AND SMTP_allowed='YES' \
  local_part_suffix     = +*
  transport             = local_mysql_delivery

# 2007-01-16        catchall domains
# a catchall domain can be specified by giving one or more db-entries that match the domain,
# or return a comma-seperated list of recipients.
# this router acts as a fallback, so it has to be placed below all routers that react on 'users'.
# any mail to a not otherwise (in another router above) defined local_prefix in these domains are forwarded.
# so keep in mind that this mostly may forward unsolicited mail and should not be used at all ;-)
  driver                = redirect
  # restriction to local domains only may be a double check, as data takes care of it already ;-)
  domains               = +local_domains
  file_transport        = address_file
  pipe_transport        = address_pipe
  data                  = ${lookup mysql{ \
                                SELECT sendto \
                                FROM catchall \
                                WHERE domain='${quote_mysql:$domain}' \

Add the following line to the remote_smtp-transport in the transport-section to remove some headers for outbound mail.

  headers_remove = X-Spam-Report:X-Spam-Bar

Define a new transport local_mysql_delivery in the transport-section. This one will be called for virtual users to deliver the mail into their maildir:

  driver        = appendfile
#  file         = /var/mail/$local_part
#  directory    = /home/$local_part/.maildir
  directory     = /var/mail/${domain}/${local_part}/
  user          = mail
  group         = mail
  mode          = 0660
  # at this time, we know a local user to get his individual preferences to tag the mail
  # the '${eval:$header_X-Spam-Score-Int:}' is is a hack to cope with negative ints that seem to be parsed as strings, thus failing the comparsion
  # if there's no X-Spam-Score-Int header set by data-acl above, don't panic ;-)
  # another hack is that we remove important headers, we add later to be sure there are no multiple versions from earlier relays, or forged ones (this is BUGGY right now as it merges all equal headers!)
  #     therefore i implemented the ${sg{$header_X-Spam-Score-Int:}{^.*\n}{}} regex hack, that strips all
  headers_remove = Subject : X-Spam-Flag : X-Spam-Score-Int : X-Spam-Score : X-Spam-Bar : X-Spam-Report
  headers_add   = "X-Spam-Threshold: ${lookup mysql{ \
                                        SELECT spam_threshold \
                                        FROM user \
                                        WHERE username='${quote_mysql:$local_part}' \
                                        AND domain='${quote_mysql:$domain}' \
                                        AND SMTP_allowed='YES' \
                  X-Spam-Score: $header_X-Spam-Score:\n\
                  X-Spam-Score-Int: $header_X-Spam-Score-Int:\n\
                  X-Spam-Bar: $header_X-Spam-Bar:\n\
                  X-Spam-Report: $header_X-Spam-Report:\n\
                  X-Spam-Flag: ${if def:header_X-Spam-Score-Int:{\
                                        ${if >={${eval:${sg{$header_X-Spam-Score-Int:}{^.*\n}{}}}}\
                                                {${lookup mysql{ \
                                                        SELECT spam_threshold*10 \
                                                        FROM user \
                                                        WHERE username='${quote_mysql:$local_part}' \
                                                        AND domain='${quote_mysql:$domain}' \
                                                        AND SMTP_allowed='YES' \
                  Subject: ${if def:header_X-Spam-Score-Int:{\
                                        ${if >={${eval:${sg{$header_X-Spam-Score-Int:}{^.*\n}{}}}}\
                                                {${lookup mysql{ \
                                                        SELECT spam_threshold*10 \
                                                        FROM user \
                                                        WHERE username='${quote_mysql:$local_part}' \
                                                        AND domain='${quote_mysql:$domain}' \
                                                        AND SMTP_allowed='YES' \
                                                }{$value}{ERROR}}}{${lookup mysql{ \
                                                                        SELECT spam_tag \
                                                                        FROM user \
                                                                        WHERE username='${quote_mysql:$local_part}' \
                                                                        AND domain='${quote_mysql:$domain}' \
                                                                        AND SMTP_allowed='YES' \
                  X-Delivered-To: $original_local_part@$original_domain ($local_part@$domain)\n\
                  X-Message-Age: $message_age"


Define the following two authenticators in authenticators-section. They will verify the password for SMTP-AUTH from the database, but advertise this functionality only if the connection is encrypted to avoid disclosure:

driver                          = plaintext
public_name                     = PLAIN
server_advertise_condition      = ${if eq{$tls_cipher}{}{no}{yes}}
server_condition                = ${if crypteq {$3}{\{sha1\}${lookup mysql{ \
                                                                SELECT password \
                                                                FROM user \
                                                                WHERE CONCAT(username,'@',domain)='${quote_mysql:$2}' \
                                                                AND SMTPAUTH_allowed='YES' \
server_set_id                   = $2

driver                          = "plaintext"
public_name                     = "LOGIN"
server_prompts                  = Username:: : Password::
server_advertise_condition      = ${if eq{$tls_cipher}{}{no}{yes}}
server_condition                = ${if crypteq {$2}{\{sha1\}${lookup mysql{ \
                                                                SELECT password \
                                                                FROM user \
                                                                WHERE CONCAT(username,'@',domain)='${quote_mysql:$1}' \
                                                                AND SMTPAUTH_allowed='YES' \
server_set_id                   = $1

Create a daily cronjob /etc/cron.daily/tidy_exim_dbs that cleans up old stuff from Exim's internal DBs.
Make sure, it's executeable!

List of Config-Files

Have a look for complete (Gentoo-based) config-files.

Please do not use without reading/understanding them first. There may be references to my domain-name in them and I don't want to read your mail that gets routed to me!







Web-based User/Admin Interface

NOTE: the current version of SMAD (20090516) refers to the old version of this HOWTO. It should work with this version, but since automatic domain-recognition has changed its behaviour, this needs to be redesigned in SMAD, too. The main difference is that in SMAD you still need to create a (dummy) user to create a new domain. This will be changed in the next release of SMAD, in order to create alias- or catchall-only domains.

Simple Mail ADmin is available for beta-testing now. feel free to test it and file bugreports, if nessessary.

it should be feature-complete and allows you to:

SMAD will be renamed to VMSA (Virtual Mail System Admin) and hosted on SourceForge. you can try the development version in Mercurial SCM until the first new version is released:

hg clone


for intructions how to install smad, please refer to the INSTALL file. we also provide a CHANGELOG.

On Gentoo you should be fine with the following additional dependencies:


you may try out the development version of the interface on our live-demo.
There are three roles to log in:

The database is reset once a day! If you experience problems or strange error messages - remember: it's the development version!

Virtual Identity Thunderbird Add-on

There's a great third-party add-on for Thunderbird called Virtual Identity, which allows you to manage your virtual identities (e.g. automatic plussed aliases, ...) with ease.

It has all the special voodoo to answer with the correct address if you reply and you don't have to setup these addresses as real accounts. Instead you can use them on the fly. You may even create a new one, just by editing the "From:"-line of your mail.

change password plugin for squirrelmail

For SquirrelMail (an IMAP-WebMail) there's a plugin called change_sqlpass which does not seem to be maintained for some years now.
I've written a patch against the last version 3.3 to file change_sqlpass/functions.php to support the base64 encoded sha1 password hashes.

Configure it by creating a MySQL-user for squirrelmail and containing the following lines in change_sqlpass/config.php:

$csp_dsn = 'mysql://squirrelmail:SQUIRRELMAILPASSWORD@unix(/var/run/mysqld/mysqld.sock)/system';
$lookup_password_query = 'SELECT count(*) FROM user WHERE username = "%2" AND domain = "%3" AND password = %4';
$password_update_queries = array( 'UPDATE user SET password = %4 WHERE username = "%2" AND domain = "%3"' );
$force_change_password_check_query = '';
$password_encryption = 'B64SHA1';
$csp_salt_static = '';
$csp_salt_query = '';

Apache mod_auth_mysql encryption scheme patch

For Apache's mod_auth_mysql I've written a patch against the last version 3.0.0 to file mod_auth_mysql.c to support the base64 encoded sha1 password hashes in our database. (see it on SourceForge's project patch page)

Configure it by creating a MySQL-user called apache with read-only access to the relevant username/domain/password fields using the password APACHEPASSWORD:

GRANT SELECT ( `username` , `domain` , `password` ) ON `system`.`user` TO 'apache'@'localhost';

After compiling the patched module, you can create a password authenticated directory, all users of the mail-system can login to, by placing the following content in a .htaccess file (or the apache config):

AuthName 		"MySQL authenticated zone"
AuthType 		Basic

AuthMySQLUser 		apache
AuthMySQLDB		system
AuthMySQLUserTable 	user
AuthMySQLNameField 	CONCAT(username,'@',domain)
AuthMySQLPasswordField	password
AuthMySQLPwEncryption	sha1base64

require 		valid-user