This document no longer describes a live configuration; I'll hopefully write up my current Postfix setup at some point.

Assumptions

This is a slightly anonymized version of pseudorandom.co.uk's mail configuration, in which I'll pretend I'm configuring a host whose real name is colo.example.com, which hosts domains example.com, example.org and subdomain.example.com. You can find out what those names are pseudonyms for if you spend 5 minutes doing DNS lookups ;-)

In my configuration, only real system users in /etc/passwd receive mail; for each user, e.g. "fred", there is a "real" address fred@colo.example.com. There are also hosted virtual domains like example.com, example.org and subdomain.example.com - each address at one of these domains either gets delivered to one or more system users, or rejected as unknown. You can do catch-all delivery too, but I don't, for spam reasons.

I'm not using databases for configuration, for simplicity - I don't host many domains.

Simple Exim 4 virtual hosting

apt-get install exim4-daemon-heavy exim4

I used the -heavy rather than -light variant so I could have content scanning using clamav, which is configured later.

When exim4-config prompts you, select:

  • Internet site
  • Use split configuration
  • Local hostnames are e.g. colo.example.com, colo and spam.example.com (there's no need to include your virtual hosts, we'll set that up in a moment). spam.example.com is for dspam support, which I'll configure later on. Depending which version of Debian you're using, you may need to separate the hostnames with colons : or with semicolons ; - pay attention to the debconf prompt.

Create an empty directory /etc/exim4/virtual.

If you want users' mail delivered into a Maildir (for use with Courier or Dovecot IMAP) by default, edit /etc/exim4/update-exim4.conf.conf to include

dc_localdelivery='maildir_home'

To add all your virtual hosts to the list of local hostnames, edit /etc/exim4/conf.d/main/01_exim4-config_listmacrosdefs and add dsearch;/etc/exim4/virtual to the line that defines local_domains, so it looks something like::

local_domains = MAIN_LOCAL_DOMAINS : dsearch;/etc/exim4/virtual

(For Debian etch you can create a file /etc/exim4/conf.d/main/005_local_hostnames containing MAIN_LOCAL_DOMAINS = DEBCONFlocal_domainsDEBCONF : dsearch;/etc/exim4/virtual but it seems that in lenny/sid the configuration scheme has had incompatible changes again.)

(If you're not using the split configuration like I recommended, you can put this in /etc/exim4/exim4.conf.localmacros instead.)

In /etc/exim4/conf.d/router, add a file 250_local_vdom_aliases::

vdom_aliases:
  debug_print = "R: vdom_aliases for $local_part@$domain"
  driver = redirect
  allow_defer
  allow_fail
  domains = dsearch;/etc/exim4/virtual
  data = ${expand:${lookup{$local_part}lsearch*@{/etc/exim4/virtual/$domain}}}
  retry_use_local_part
  pipe_transport = address_pipe
  file_transport = address_file
  # not no_more - we try again without the suffix

vdom_aliases_suffix:
  debug_print = "R: vdom_aliases_suffix for $local_part@$domain"
  driver = redirect
  local_part_suffix = -*
  local_part_suffix_optional
  allow_defer
  allow_fail
  domains = dsearch;/etc/exim4/virtual
  data = ${expand:${lookup{$local_part}lsearch*@{/etc/exim4/virtual/$domain}}}
  retry_use_local_part
  pipe_transport = address_pipe
  file_transport = address_file
  no_more

(If you're not using the "split files" configuration style, add this to /etc/exim4/exim4.conf.template just before the real_local router instead.)

Now you can just drop a file in /etc/exim4/virtual named after each virtual domain, e.g. /etc/exim4/virtual/example.com should look something like this::

abuse : fred@localhost
postmaster : fred@localhost
fred : fred@localhost
fred.smith : fred@localhost
joe : joe@localhost
joseph.bloggs : joe@localhost
...

where fred and joe are local system users. Now mail to joseph.bloggs@example.com goes to joe, and so on.

Also, suffixes are set up, so mail to joe-anything@example.com also goes to joe; but these can be overridden in the virtual domain file. For instance, if Joe runs some mailing lists in example.com under the local uid joeslists you can do::

example : joe@localhost
example-users : joeslists@localhost
example-users-request : joe@localhost
example-devel : joeslists@localhost
example-devel-request : joe@localhost

and it'll work.

Greylisting and DNSBLs

You can get simple greylisting as quickly as::

apt-get install greylistd
greylistd-setup-exim4

I reduce the delay from greylisting, and the collateral damage from DNSBLs, by only greylisting mail if the sender is listed in one of a variety of DNSBLs. To achieve this, add something like the following to the greylistd stanzas in 30_exim4-config_check_rcpt and 40_exim4-config_check_data::

dnslists = sbl-xbl.spamhaus.org \
  : list.dsbl.org \
  : dnsbl.sorbs.net

(Amended 2007-09-22: I used to use relays.ordb.org too, but that no longer works.)

Add any troublesome hosts (hi Chiark) to /etc/exim4/local_host_whitelist and they'll be allowed through without any checking or greylisting.

Rejecting viruses

Viruses are bounced at SMTP time - this will usually not actually cause a bounce message (the virus's internal SMTP engine will ignore the SMTP failure), but false positives get a proper bounce message rather than having mail dropped silently, which is advantageous.

(Configuration taken from http://www.debian-administration.org/articles/141)

Add this to the ACL in 40_exim4-config_check_data:

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

#
# Reject file extensions used by worms.
#
deny message = This domain has a policy of not accepting certain types \
               of attachments in mail as they may contain a virus.  \
               \
               This mail has a file with a .$found_extension attachment and \
               is not accepted. \
               \
               If you have a legitimate need to send this attachment, send it
               in a compressed archive, and it will then be forwarded to the 
               recipient.
 !acl = acl_whitelist_local_deny
#
deny message = This domain has a policy of not accepting certain types \
               of attachments in mail as they may contain a virus.  \
               \
               This mail has a file with a .$found_extension attachment and \
               is not accepted. \
               \
               If you have a legitimate need to send this attachment, send it \
               in a compressed archive, and it will then be forwarded to the  \
               recipient.
 !acl = acl_whitelist_local_deny
demime = vbs:bat:pif:scr
.ifdef TEERGRUBE
   delay = TEERGRUBE
.endif

# Reject messages containing malware.
deny message = This message contains a virus ($malware_name) and has been rejected
 !acl = acl_whitelist_local_deny
malware = *

Filtering spam

apt-get install dspam libdspam7-drv-pgsql

dspam doesn't seem to like using passwordless (uid-based) authentication to PostgreSQL, so when prompted by dbconfig-common, tell it to use "ident" authentication for the administrative user, but "password" authentication for the database user. Leave the password blank and dbconfig-common will generate a random password, then generate a configuration file in /etc/dspam/dspam.d with that password in.

In /etc/dspam/dspam.conf, set, among others:

StorageDriver /usr/lib/dspam/libpgsql_drv.so
TrustedDeliveryAgent "/usr/sbin/exim4 -oi -oMr despammed"

Put this in /etc/exim4/conf.d/router/550_local_dspam:

dspam_router:
  no_verify
  check_local_user
  condition = "${if and { \
    {!def:h_X-My-Dspam:} \
    {!eq {$received_protocol}{local}} \
    {!eq {$received_protocol}{despammed}} \
    { <= {$message_size}{3M}} \
    }\
    {1}{0}}"
  headers_add = "X-My-Dspam: scanned by $primary_hostname, $tod_full"
  driver = accept
  transport = dspam_transport

dspam_error_spam_router:
  driver = accept
  domains = spam.example.com
  local_part_suffix = -spam
  transport = dspam_error_spam_transport

dspam_error_ham_router:
  driver = accept
  domains = spam.example.com
  local_part_suffix = -fp
  transport = dspam_error_ham_transport

and this in /etc/exim4/conf.d/transport/40_local_dspam:

dspam_transport:
  driver = pipe
  command = "/usr/bin/dspam --deliver=innocent,spam --user ${lc:$local_part} -f '$sender_address' -oi -oMr despammed -- %u"
  user = dspam
  group = dspam
  log_output = true
  return_fail_output = true
  return_path_add = false
  message_prefix =
  message_suffix =

dspam_error_spam_transport:
  driver = pipe
  command = "/usr/bin/dspam --source=error --class=spam --user ${lc:$local_part} -f '$sender_address' -oi -oMr despammed -- %u"
  user = dspam
  group = dspam
  log_output = true
  return_fail_output = true
  return_path_add = false
  message_prefix =
  message_suffix =

dspam_error_ham_transport:
  driver = pipe
  command = "/usr/bin/dspam --source=error --class=innocent --user ${lc:$local_part} -f '$sender_address' -oi -oMr despammed -- %u"
  user = dspam
  group = dspam
  log_output = true
  return_fail_output = true
  return_path_add = false
  message_prefix =
  message_suffix =

Now if user fred gets a spam message which wasn't caught, he can bounce it to fred-spam@spam.example.com, and if one of his legitimate messages gets classified as spam (a false positive) he can bounce it to fred-fp@spam.example.com.

Users can now filter mail using the X-DSPAM-Result header, for instance in Procmail:

DEFAULT=/home/fred/Maildir/
MAILDIR=/home/fred/Maildir/

:0:
* ^X-DSPAM-Result: spam
.spam/