Penguin

How to make your Debian or Ubuntu machine an amazing Exim 4 mail filter

This is how I've set up a new exim4 installation to do all the filtering I used to do with MailScanner or amavis. It's much less CPU intensive to use the daemon mode of SpamAssassin and have your MTA do all the work instead of a big perl script.

There are a number of changes that aren't immediately apparent between using exim3 and exim4 on Debian: the configuration system is completely different. You either have a large configuration template file or a number of small files, but either way, the live config isn't updated until you run update-exim4.conf. Running an /etc/init.d/exim4 restart will run this command for you.

Get exim4

Sarge/Dapper

apt-get install exim4-daemon-heavy clamav-daemon clamav-freshclam spamassassin (add more or less as required.)

Woody

You can only go as far as Exim 4.34, so you should really consider moving to Sarge.

Add these lines to your apt sources.list:

deb http://www.linux.org.au/backports.org/debian woody exim4
deb http://www.linux.org.au/backports.org/debian woody gnutls11

apt-get install exim4-daemon-heavy. You might like to purge exim3 at this point too else your ex<tab> completion will pick exim instead of exim4. At this point I assume you're running clamav-daemon, spamassassin 3.03 and have recent versions of libnet-perl-dns etc, but I'll deal to those later.

Configure exim4

Configure exim4 to use the small config files.

Note: I use 'itp' to signify my changes. You will want to use your own tag.

Most of the snippets below go into the 'acl_smtp_data' ACL, which has the potential to accept or deny a message at SMTP DATA time. When putting them in, realise that the order of 'warn' entries is irrelevant, but if you hit a 'deny' the message is denied and further processing is stopped. Therefore the 'drop messages that are obviously spam' sits nicely before the 'redirect messages that -might- be spam' rule. Don't accidentally lose the 'accept' at the bottom of the file either.

Get ClamAV working

Change into /etc/exim4/conf.d/main. Copy 02_exim4-config_options to 02_exim4-config_options.rul and add these lines:

# itp: set ClamAV path
#
av_scanner = clamd:/var/run/clamav/clamd.ctl

Now change into /etc/exim4/conf.d/acl. Copy 40_exim4-config_check_data to 40_exim4-config_check_data.rul and add these lines:

   # itp: Reject messages containing malware.
   deny message = This message contains malware ($malware_name)
       demime = *
       malware = *

just above "# accept otherwise".

Also add the clamav user to group `Debian-exim': usermod -G Debian-exim clamav and make sure that /etc/clamav/clamd.conf contains `User clamav' and `AllowSupplementaryGroups'. This is so clamav can access the /var/spool/exim4 dir.

To restart exim4, use invoke-rc.d exim4 restart which builds the config file from the templates.

Restart clamav daemon, user invoke-rc.d clamav-daemon restart which makes the new security work.

Test it:

telnet localhost 25
220 firewall.test ESMTP Exim 4.34 Tue, 14 Dec 2004 14:20:28 +1300
HELO test.co.nz
250 firewall.test Hello localhost [127.0.0.1]
MAIL FROM: sdg@adfgsdg.co.nz
250 OK
RCPT TO: foo@foo.co.nz
250 Accepted
DATA
354 Enter message, ending with "." on a line by itself
X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*
.
550 This message contains malware (Eicar-Test-Signature)

Get spamassasin working

Get spamassassin 3.0 from backports.org. Edit /etc/default/spamassassin to enable spamd, but make sure you're happy with the risks.

We need to teach Exim how to talk to spamd. To your main/02_exim4-config_options.rul, add:

# itp: set SpamAssassin path
#
spamd_address = 127.0.0.1 783

If you are running SpamAssassin on the local machine and don't like the idea of opening any more TCP sockets than you have to, add the following to the /etc/default/spamassassin OPTIONS line:

--socketpath=/var/run/spamd.ctl

and set exim's configuration to read:

# itp: set SpamAssassin path
#
spamd_address = /var/run/spamd.ctl

Really spammy stuff

Now, we'll add an ACL to automatically drop anything that scores over a certain threshold that is obviously spam. To your acl/40_exim4-config_check_data.rul, add:

   # itp: reject spam at high scores (> 15)
   deny message = Message scored $spam_score spam points.
        condition = ${if <{$message_size}{100k}{1}{0}}
        spam = nobody:true
        condition = ${if >{$spam_score_int}{150}{1}{0}}

Restart and test like so:

MAIL FROM: me@them.co.nz
250 OK
RCPT TO: foo@bar.com
250 Accepted
DATA
354 Enter message, ending with "." on a line by itself
XJS*C4JDBQADN1.NSBN3*2IDNEN*GTUBE-STANDARD-ANTI-UBE-TEST-EMAIL*C.34X
.
550 This message scored 998.8 spam points.
QUIT

Less spammy stuff

In a corporate mail filter I don't want to send users any spam - there is a body that exists to filter what little spam is left after the above rule, but we need to get it to another mailbox. We do this by adding an X- header to any messages that are over the spam level as defined in spamassassin's local.cf (if you don't set it there, it defaults to 5), and use a router to rewrite them to that address.

Drop a file called 050_exim4-config_spam_redirect in /etc/exim4/conf.d/router, containing something very much like this:

# itp: Spam redirection router
# Modified from http://duncanthrax.net/exiscan-acl/exiscan-acl-examples.txt,
# and with changes made by RafalJankowski on the WLUG Wiki, this router takes
# any message tagged as spam and redirects it to the redirect user.

spam_redirect:
  debug_print = "R: scan_redirect for $domain"
  driver = redirect
  condition = ${if def:acl_m1 {1}{0}}
  headers_add = X-Original-Recipient: $local_part@$domain
  data = $acl_m1
  redirect_router = hubbed_hosts

This sits just before the hubbed_hosts router, which was previously the first router in the queue. Set the redirect router to whichever router you want to process your message next.

Now, to have the redirect headers written on your messages, in our acl/40_exim4-config_check_data.rul:

   # itp: put a spam warning on all messages
   # and redirect messages over the SA threshold to quarantine
   warn message = X-Spam-Score: $spam_score {$spam_bar}
        condition = ${if <{$message_size}{100k}{1}{0}}
        spam = nobody:true

   warn message = X-Spam-Report: $spam_report
        condition = ${if <{$message_size}{100k}{1}{0}}
        spam = nobody:true

   accept
        condition = ${if <{$message_size}{100k}{1}{0}}
        spam = nobody
        set acl_m1 = "postmaster@yoursite.co.nz"
        #delay = 60s
        control = fakereject
        logwrite = :main,reject: This message scored $spam_score spam points. Please contact postmaster

"nobody:true" matches everyone (the nobody is the user to call SpamAssassin as; as we're always using the same one the result is cached per message). Make sure you always check the message size before calling "spam" else you will end up passing huge messages to SA.

Change the acl_m1 to refer to your postmaster. Strangely, not everyone picks up on this.

In order to get a small sensible spam report instead of the huge default SpamAssassin one, put this in your /etc/spamassassin/local.cf:

clear_report_template
report "YESNO, hits=HITS required=REQD tests=TESTS autolearn=AUTOLEARN
 version=VERSION"

MIME errors & file attachments

Pre-Exim 4.50

Noone wants to receive executable file attachments: in acl/40_exim4-config_check_data.rul

   # itp: Unpack MIME containers and reject file extensions used by worms.
   # This calls the demime condition again, but it will return cached results.
   deny message = We do not accept ".$found_extension" attachments here. If you \
                  legitimately need to send these files please zip them first.
        demime  = bat:btm:cmd:com:cpl:dll:exe:lnk:msi:pif:prf:reg:scr:vbs:url

And for MIME errors:

   # itp: Reject messages that have serious MIME errors.
   deny message = Serious MIME defect detected ($demime_reason)
        demime = *
        condition = ${if >{$demime_errorlevel}{2}{1}{0}}

Exim 4.50 and higher

Recent exiscans (including the one included with Exim 4.50) have deprecated demime, instead adding a acl_smtp_mime ACL. This is more powerful than the precvious demime, but as always, is more complex to get the above features.

This example was originally built from an acl_smtp_mime thread on exim-users, but many typos have been corrected.

Add something like the following:

In main/02_exim4-config_options.rul:

# itp: define MIME ACL
#
.ifndef MAIN_ACL_CHECK_MIME
MAIN_ACL_CHECK_MIME = acl_check_mime
.endif
acl_smtp_mime = MAIN_ACL_CHECK_MIME

Create acl/50_exim4-config_check_mime:

### acl/50_exim4-config_check_mime
##################################

acl_check_mime:

  # Decode MIME parts to disk. This will support virus scanners later.
  deny
        decode      = default
        condition   = ${if > {$mime_anomaly_level}{2} \
                            {true}{false}}
        message     = This message contains a MIME error ($mime_anomaly_text)
        log_message = DENY: MIME Error ($mime_anomaly_text)

  # Too many MIME parts
  #
  deny
        condition   = ${if >{$mime_part_count}{1024}{yes}{no}}
        message     = MIME error: Too many parts (max 1024)
        log_message = DENY: MIME Error (Too many MIME parts: $mime_part_count)

  # Excessive line length
  #
  # BEWARE: Exim 4.50 has a bug that means regex's don't work in the MIME ACL.
  # Don't use in that case!  It works fine in Exim 4.60.
  deny
        regex       = ^.{8000}
        message     = MIME error: Line length in message or single header exceeds 8000.
        log_message = DENY: MIME Error (Maximum line length exceeded)

  # Partial message
  #
  deny
        condition   = ${if eq {$mime_content_type}{message/partial}{yes}{no}}
        message     = MIME error: MIME type message/partial not allowed here
        log_message = DENY: MIME Error (MIME type message/partial found)

  # Filename length too long (> 255 characters)
  #
  deny
        condition   = ${if >{${strlen:$mime_filename}}{255}{yes}{no}}
        message     = MIME error: Proposed filename exceeds 255 characters
        log_message = DENY: MIME Error (Proposed filename too long)

  # MIME boundary length too long (> 1024)
  #
  deny
        condition   = ${if >{${strlen:$mime_boundary}}{1024}{yes}{no}}
        message     = MIME error: MIME boundary length exceed 1024 characters
        log_message = DENY: MIME Error (Boundary length too long)

  # File extension filtering.
  deny
        condition = ${if match \
                        {${lc:$mime_filename}} \
                        {\N(\.bat|\.btm|\.cmd|\.com|\.cpl|\.dll|\.exe|\.lnk|\.msi|\.pif|\.prf|\.reg|\.scr|\.vbs|\.url)$\N} \
                        {1}{0}}
        message = Blacklisted file extension detected in "$mime_filename". If you legitimately need to send these files please zip them first.
        log_message = DENY: Blacklisted extension ("$mime_filename")

   # accept otherwise
   accept

Unfortunately, because of a bug in Exim 4.50, you may see "cannot test regex condition in MIME ACL". This stops you doing the Line Length check. Enable that section only for Exim 4.6.

You can tweak the values for Proposed Filename, MIME boundary length and Line Length to work for your users Some mailers conform more strictly to the MIME spec than others.