Spam filtering with Exim

This page contains tips and tricks on how to filter spam using the Exim MTA.

For this purpose, Exim 3 just won't do. You need Exim 4 to do the job. Filtering solutions involving Exim 3 will often bounce when they really should reject. And bouncing is evil!. Some ISPs will even terminate your connection if you bounce when you really should reject!
And just using Exim 4 won't do either. You have to apply the exiscan patch, or install an Exim 4 package, which has this patch already applied, such as Debian's 'exim4-daemon-heavy'.
And don't install SA-Exim either; It's perfectly pointless. The exiscan patch does all you need.

The stuff below is based on Debian 3.0 / Woody. For Debian 3.1 / Sarge you can use the old Woody config tree, but you will have to make a few changes (a fully Sarge based page will be published later).

Installation on Debian

Fortunately, there are Debian Woody backports for all the stuff you need. Just edit your /etc/apt/sources.list (replace 'nl' with your favourite mirror);
# Exim 4
deb http://www.nl.backports.org/debian woody exim4
deb http://www.backports.org/debian woody exim4
If you want to be able to encrypt your mail using 'TLS' also install;
deb http://www.nl.backports.org/debian woody gnutls11
deb http://www.backports.org/debian woody gnutls11
For SPF also add;
# SPF
deb http://www.nl.backports.org/debian woody libmail-spf-query-perl
deb http://www.backports.org/debian woody libmail-spf-query-perl
The SpamAssassin spam filter;
# SpamAssassin
deb http://www.nl.backports.org/debian woody spamassassin
deb http://www.backports.org/debian woody spamassassin
And the SpamAssassin plugin Razor;
# Razor
deb http://www.nl.backports.org/debian woody razor
deb http://www.backports.org/debian woody razor
There is no SRS backport

Select the following packages and anything that they depend on;

Plus any documentation you might want to install and then do a lot of RTFM.

After this configure the lot;

Running Exim from inetd

If you want to run Exim from inetd add / edit the following line in /etc/inetd.conf;
smtp	stream	tcp	nowait.64	Debian-exim	/usr/sbin/tcpd	/usr/sbin/exim4 -bs
And restart inetd.
'.64' is the maximum number of SMTP connections per minute.

You also need to run the queue from cron. Add / edit the following line in /etc/cron.d/exim;

08,23,38,53 * * * *	Debian-exim	if [ -x /usr/sbin/exim4 -a -f /var/lib/exim4/config.autogenerated ]; then /usr/sbin/exim4 -q ; fi
And restart cron.

Exim configuration

The next thing to do, is to try to update your config files in /etc/exim4/. There is a config utility which will convert your /etc/exim/exim.conf into /etc/exim4/exim4.conf. Check this file to see if it is correct.

The Debian Exim 4 way of doing things however, is to use a large tree of config files in /etc/exim4/conf.d/ and generate one single file /var/lib/exim4/config.autogenerated using update-exim4.conf. This file will only be used if /etc/exim4/exim4.conf doesn't exist.

During install, debconf will edit /etc/exim4/update-exim4.conf.conf. update-exim4.conf uses this file, together with the tree under /etc/exim4/conf.d/ to generate /var/lib/exim4/config.autogenerated. It may be necessary to edit /etc/exim4/update-exim4.conf.conf.

Below are various additions and enhancements to the default Exim 4 config.

main

main/01_exim4-config_listmacrosdefs

Exim will use the hostname of your primary interface for its HELO. This is not necessarily what your want.
If you have several interfaces each with their own IP address and hostname, create one 'fake' hostname and have this point to all IP addresses. Use this name as your HELO / EHLO;
primary_hostname = Your_Helo_Name
Some spam filters will complain if your helo doesn't match the reverse, even if the forward matches (forged helo). Fortunately newer versions of Exim support 'smtp_active_hostname'. The statement below will use the host name of the interface used for the SMTP connection. If the lookup fails or the mail isn't received via TCP but via stdin, the primary_hostname will be used instead;
smtp_active_hostname = ${lookup dnsdb{ptr=$interface_address}{$value}fail}
The statement below allows you to use a special name 'Helo_For_1.2.3.4' when connected via interface '1.2.3.4'. In other cases 'Other_Helo' will be used;
smtp_active_hostname = ${if eq{$interface_address}{1.2.3.4}\
   {Helo_For_1.2.3.4}{Other_Helo}}
This works for incoming mail and callouts. For outgoing mail see transport/30_exim4-config_remote_smtp

And the qualify_domain isn't necessarily what you want either;

# Qualify_domain
#qualify_domain = DEBCONFvisiblenameDEBCONF
qualify_domain = Your_Domain
Set the maximum message size.
'MESSAGE_SIZE_LIMIT' is the value actually used to block large messages. 'message_size_limit' on the other hand, the the value reported in the EHLO. By making 'message_size_limit' larger then 'MESSAGE_SIZE_LIMIT', one gets an error after RCPT, rather then immediately after MAIL.
MESSAGE_SIZE_LIMIT = 128K
A whitelist for RBL checking;
hostlist rbl_white_hosts = : \
    127.0.0.0/8 : \
    192.168.1.0/24 : \
    Your_Own_Ip_Address
The qualify_domain isn't necessarily what you want either;
# Qualify_domain
#qualify_domain = DEBCONFvisiblenameDEBCONF
qualify_domain = example.com

main/02_exim4-config_options

Enable VRFY;
acl_smtp_vrfy = check_vrfy
A message size limit. This one is quite large. That way one gets an error after RCPT, not immediately after MAIL.
message_size_limit = 2M
Avoid defers on failing DNS lookups
dns_again_means_nonexist = *
Add additional info with your bounces;
bounce_message_text = "See Url_To_Your_Spam_Policy"
Check for underscores but complain after RCPT
helo_allow_chars = _
Do a DNS lookup, but complain after RCPT
helo_try_verify_hosts = *
Disable pipelining
This will cripple a lot of ratware.
# No pipelining
pipelining_advertise_hosts = :
Limit return size
return_size_limit = 16K
Optional virus scanner
Most virus scanners aren't really scanners but wrappers around virus scanners. AFAIK the the only real OSS virus scanner is Clam AntiVirus;
deb http://www.nl.backports.org/debian woody clamav
deb http://www.backports.org/debian woody clamav
Replace 'nl' with your favourite mirror. Don't edit routers and transports to use a virus scanner All you need is an entry in main/02_exim4-config_options and the data or mime ACL;
av_scanner = Virus_Scanner_Command
Exim temporarily stores the mail and attachments in /var/spool/exim4/scan/$message_id. This path is represented by '%s'. You can tell the scanner to scan the files in this directory;
av_scanner = cmdline:/path/to/scanner %s
A F-Prot Example;
av_scanner = cmdline:/usr/local/bin/f-prot -archive -packed %s ; \
echo -e "\nfprot_retval $?":fprot_retval 3:Infection:: (.*)
Some scanners use an unix socket
av_scanner = clamd:/path/to/socket
Or an IP port
av_scanner = clamd:127.0.0.1 3310
See Adding Anti-Virus Software for more information.

Enable SpamAssassin

spamd_address = 127.0.0.1 783
Allow 8 bits
accept_8bitmime
print_topbitchars
Root should never send or receive mail!
Maybe this a default now. But I put it in anyway;
never_users = root

acl

acl/30_exim4-config_check_rcpt

The stuff below is based on IP address 213.84.159.78 and domain sput.nl. Replace with your own address and hostnames / domains.

The original config files whitelist mail to postmaster. Experience shows that this is not a good idea.

  # Accept mail to postmaster in any local domain, regardless of the source,
  # and without verifying the sender.
  #
  #accept local_parts = postmaster
  #       domains = +local_domains
However, if you administer a lot of boxes which all forward their postmaster mail to you, it's probably a good idea to accept mail that was accepted by those systems;
  accept hosts       = Host_List_Of_My_Boxes
         local_parts = postmaster
I insist on a matching reverse lookup;
  deny message = Broken Reverse DNS  no host name found for IP address $sender_host_address  See http://www.sput.nl/spam/
       !verify = reverse_host_lookup
This doesn't work on on boxes newer then Woody. Instead of '!verify' use 'condition';
  deny message = Broken Reverse DNS  no host name found for IP address $sender_host_address  See http://www.sput.nl/spam/
     condition = ${if and\
      {{def:sender_host_address}\
      {!def:sender_host_name}}\
     {yes}{no}}
You may need to create a whitelist for badly configured nameservers. One way of doing this, is adding entries to /etc/hosts. You need order hosts,bind in your /etc/resolv.conf or /etc/host.conf to make this work;
Ip_Address	Host_Name
A lot of zombie ISPs don't have a MX. And therefore no abuse address either. Below an experimental test to see if they have one and if it is a hostname. If the host is 'a.b.c.d.e' it will first try 'd.e', then 'c.d.e' and finally 'b.c.d.e'. If none if these result in a hostname, the mail is rejected. This stuff only works with newer versions of Exim.
  # Check host's MX domain
  # Looks for mx for host a.b.c.d.e
  # First check d.e, then c.d.e and then b.c.d.e
  deny message = No MX for host $sender_host_name domain
       hosts   = ! : !+relay_from_hosts
       # Put lookup result in variable
       set acl_m8 = ${lookup dnsdb {mxh=\
        ${extract{-2}{.}{$sender_host_name}}.${extract{-1}{.}{$sender_host_name}} : \
        ${extract{-3}{.}{$sender_host_name}}.${extract{-2}{.}{$sender_host_name}}.\
        ${extract{-1}{.}{$sender_host_name}} : \
        ${extract{-4}{.}{$sender_host_name}}.${extract{-3}{.}{$sender_host_name}}.\
        ${extract{-2}{.}{$sender_host_name}}.${extract{-1}{.}{$sender_host_name}}\
       }{$value}fail}
       # Test content of variable; The MX has to be a hostname.
       condition = ${if !match {$acl_m8}{\N.*[A-Za-z].*\..*[A-Za-z].*\N}{yes}{no}}
If all went well '$acl_m8' will now contain a number of hosts separated by newlines. The next thing to do is to check if these resolve to IP addresses. To this end 'tr' replaces the newlines with colons (':'). None of the addresses should be in use on your lan. Edit to suit your needs (don't forget to replace '192.168.1.' and '213.84.159.78' with your own IP address(es)).
  deny message = Bogus MX for host $sender_host_name domain
       hosts   = ! : !+relay_from_hosts
       set acl_m8 = ${lookup dnsdb {a=${tr{$acl_m8}{\n}{:}}}{$value}fail}
       condition = ${if \
        or{\
         {!match {$acl_m8}{\N[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}\N}}\
         {match {$acl_m8}{\N127\.0\.0\.1\N}}\
         {match {$acl_m8}{\N192\.168\.1\.[0-9]{1,3}\N}}\
         {match {$acl_m8}{\N213\.84\.159\.78\N}}\
        }\
       {yes}{no}}
In some cases however, the remote host DOES accept SMTP connections. You may add these to a whitelist.

Lots of HELO checks. Just choose and pick

  # Don't allow underscores
  deny message = Underscores are not allowed in hostnames
       condition = ${if match\
       {$sender_helo_name}\
       {\N.*_.*\N}\
       {yes}{no}}

  # A remote host using my helo is wrong
  deny hosts   = !+relay_from_hosts
       message = Using my HELO is identity theft
       condition = ${if match\
       {$sender_helo_name}\
       {\N^(213\.84\.159\.78|(.*\.)?sput\.nl)$\N}\
       {yes}{no}}

  # A remote host can't be localhost or localdomain
  deny hosts   = !+relay_from_hosts
       message = $sender_helo_name is a silly HELO
       condition = ${if match\
       {$sender_helo_name}\
       {\N^(127\.0\.0\.1|localhost(\.localdomain)?)$\N}\
       {yes}{no}}

  # Helo should not be RFC 1918 address
  deny hosts   = !+relay_from_hosts
       message = RFC 1918 IP address in HELO
       condition = ${if match\
       {$sender_helo_name}\
       {\N^(\[)?(10\.[0-9]{1,3}|172\.(1[6-9]|2[0-9]|31)|192\.168)\.[0-9]{1,3}\.[0-9]{1,3}(\])?$\N}\
       {yes}{no}}

  # Helo should be hostname
  deny hosts   = !+relay_from_hosts
       message = HELO should be hostname. See http://www.sput.nl/spam/
       condition = ${if !match\
       {$sender_helo_name}\
       {\N.*[A-Za-z].*\N}\
       {yes}{no}}

  # Helo should be FQDN
  deny hosts   = !+relay_from_hosts
       message = HELO should be Fully Qualified Domain Name  Host.Domain.Tld  See RFC821
       condition = ${if !match\
       {$sender_helo_name}\
       {\N.*[A-Za-z].*\..*[A-Za-z].*\N}\
       {yes}{no}}
Don't accept mail from Some_User@localhost.localdomain
  # A domain can't be localhost or localdomain
  deny message = $sender_address_domain is a silly domain.
       condition = ${if match\
       {$sender_address_domain}\
       {\N^(localhost|localhost\.localdomain|localdomain)$\N}\
       {yes}{no}}
I also added an SPF record to my NS. Addresses like 'root@localhost' really shouldn't be used;
	IN	NS	localhost.
	IN	TXT	"v=spf1 a -all"
	IN	A	127.0.0.1
An other identity theft check.
Keep in mind, that if you send an email to a forward which sends it back to you, this forward mechanism should change the envelope from, otherwise this will block your mail.
So you may need to add a whitelist to this check.
  # A remote host using my Domain is wrong
  deny hosts   = !+relay_from_hosts
       message = Using my domain is identity theft
       condition = ${if match\
       {$sender_address_domain}\
       {\N^(.*\.)?sput\.nl$\N}\
       {yes}{no}}
Check if the HELO has got anything to do with the hostname. If not, reject the mail.
The '{$value}fail' stuff forces a reject instead of a defer;
  # Check the helo after recipient.
  # If that doesn't work try to match Host's Domain with Helo's Domain
  # Next see if they are in the same /24
  # After that try the MX

  deny hosts   = !+relay_from_hosts
       message = Lookup of $sender_helo_name failed or did not match. See http://www.sput.nl/spam/
      !verify  = helo
       condition = \
       ${if \
        and {\
         {!eq \
          {${extract{-2}{.}{${lc:$sender_host_name}}}}\
          {${extract{-2}{.}{${lc:$sender_helo_name}}}}\
         }\
         {!match \
          {${lookup dnsdb{a=$sender_helo_name}{$value}fail}}\
          {\
           ${extract{1}{.}{$sender_host_address}}\.\
           ${extract{2}{.}{$sender_host_address}}\.\
           ${extract{3}{.}{$sender_host_address}}\.\
          }\
         }\
         {!match \
          {${lc:${lookup dnsdb{mx=$sender_helo_name}{$value}fail}}}\
          {${lc:$sender_host_name}}\
         }\
        }\
       {yes}{no}}
You may need to create a whitelist for badly configured hosts. One way of doing this, is adding entries to /etc/hosts. You need order hosts,bind in your /etc/resolv.conf or /etc/host.conf to make this work;
Ip_Address	Host_Name Helo_Name
Forged Yahoo email addresses are a major spam source. Unfortunately, Yahoo doesn't publish SPF records.
  deny message = This is a fake Yahoo mail. See http://www.sput.nl/spam/
     condition = \
     ${if \
      and {\
       {eq \
        {$sender_address_domain}\
        {yahoo.com}\
       }\
       {!match \
        {$sender_host_name}\
        {\N^.+\.yahoo\.com$\N}\
       }\
      }\
     {yes}{no}}
A local (rather then DNS based) blacklist;
  deny message = sender envelope address $sender_address is locally blacklisted here. See http://www.sput.nl/spam/
       !acl = acl_whitelist_local_deny
       senders = ${if exists{CONFDIR/local_sender_blacklist}\
                             {CONFDIR/local_sender_blacklist}\
                             {}}
Various DNS based blacklists
  # Check IP address in DNS based blacklists
  deny hosts    = !+rbl_white_hosts
       message  = Host is listed in $dnslist_domain.
       dnslists = \
       virbl.dnsbl.bit.nl : \
       list.dsbl.org : \
       relays.ordb.org : \
       dnsbl.sorbs.net : \
       bl.spamcop.net : \
       sbl.spamhaus.org : \
       xbl.spamhaus.org
       #relays.visi.com
  
  # Check hostname in domain DNS based blacklists
  deny message = Host name is listed in $dnslist_domain.
       hosts    = !+rbl_white_hosts
       dnslists = \
       bogusmx.rfc-ignorant.org/$sender_host_name : \
       dsn.rfc-ignorant.org/$sender_host_name : \
       postmaster.rfc-ignorant.org/$sender_host_name : \
       abuse.rfc-ignorant.org/$sender_host_name
Helo
  # Check helo in domain DNS based blacklists
  deny message  = Helo is listed in $dnslist_domain.
       hosts    = !+rbl_white_hosts
       dnslists = \
       abuse.rfc-ignorant.org/$sender_helo_name : \
       bogusmx.rfc-ignorant.org/$sender_helo_name : \
       dsn.rfc-ignorant.org/$sender_helo_name : \
       postmaster.rfc-ignorant.org/$sender_helo_name: \
       abuse.rfc-ignorant.org/$sender_helo_name
And address
  # Check email address domain in DNS based blacklists
  deny hosts    = !+rbl_white_hosts
       senders  = ! :
       message  = Domain is listed in $dnslist_domain.
       dnslists = \
       bogusmx.rfc-ignorant.org/$sender_address_domain : \
       dsn.rfc-ignorant.org/$sender_address_domain : \
       postmaster.rfc-ignorant.org/$sender_address_domain : \
       abuse.rfc-ignorant.org/$sender_address_domain
If you want mail from really cheap ISPs, don't use abuse- and postmaster.rfc-ignorant.org

The 'mx_domains' option is a bit too strict for my taste. The stuff below does something similar, but only for incoming mail and not for DSN's.

  # There has to be an MX, except in case of DSN
  deny message = No MX for envelope sender domain $sender_address_domain. See http://www.sput.nl/spam/
       hosts   = ! : !+relay_from_hosts
       senders = ! :
       condition = ${if eq\
        {${lookup dnsdb{mx=$sender_address_domain}{$value}fail}}\
        {fail}\
       {yes}{no}}
And the MX should be a hostname, not an IP address.
  # The MX has to be a hostname.
  deny message = MX for transport sender domain $sender_address_domain should be FQDN. See http://www.sput.nl/spam/
       hosts   = ! : !+relay_from_hosts
       senders = ! :
       condition = ${if !match\
        {${lookup dnsdb{mx=$sender_address_domain}{$value}fail}}\
        {\N.*[A-Za-z].*\..*[A-Za-z].*\N}\
       {yes}{no}}
Keep in mind that the above MX tests may result in a lot of false positives. They are also in violation of RFCs.

A callout verification. This also checks the existence of postmaster.

  # This test is done after blacklists
  deny !verify  = sender/callout=postmaster,100s,random
        message = No verifiable envelope sender address. See http://www.sput.nl/spam/
A combination of a callout and a postmaster check doesn't work with Hotmail. This is caused by a reset between the two on which Hotmail terminates the connection. If you want to do callouts on Hotmail, use a separate callout.

The stuff below checks if the remote MX accepts mail for random addresses. Non whitelisted hosts get rejected.

  # A warn if random test succeeds
  warn hosts   = ! : !+relay_from_hosts
       senders = ! :
       message = Envelope MX accepts mail for random addresses. See http://www.sput.nl/spam/
       #set acl_m7 = $sender_address_domain
       #set acl_m7 = ${run{/usr/local/sbin/chckcal.pl $acl_m7}}
       set acl_m7 = ${run{/usr/local/sbin/chckcal.pl $sender_address_domain}}
       condition  = ${if eq {$runrc}{1}{true}{false}}

  # A deny for non whitelisted hosts
  deny hosts   = ! : !+relay_from_hosts : !+rand_white_hosts
       senders = ! : !*@*.nl
       message = Envelope MX accepts mail for random addresses. See http://www.sput.nl/spam/
       set acl_m7 = ${run{/usr/local/sbin/chckcal.pl $sender_address_domain}}
       condition  = ${if eq {$runrc}{1}{true}{false}}
I tried to have Exim read its own own callout cache, but didn't succeed. So I wrote a little Perl script instead;
#!/usr/bin/perl

# Looks for 'random' domain in callout cache

# The default return value
$ret = 0;

open (FILE, "/usr/sbin/exim_dumpdb /var/spool/exim4 callout |") or die "can't open $!";
while (<FILE>) {
	if ($_ =~ / ${ARGV[0]} .* random=accept /) {
		#print $_;
		$ret = 1;
		break;
	}
}
close(FILE);

#print "$ret\n";
exit $ret
SPF Check.
If you use a HELO verify there is no need to check 'MAIL FROM: <>'. And if you don't want to SPF check your LAN add '!+relay_from_hosts'.
  # Just after callout, do a SPF check
  # Use 'spfquery' to obtain SPF status for this particular sender/host.
  # If the return code of that command is 1, this is an unauthorised sender.
  #
  deny
    message     = [SPF] $sender_host_address is not allowed to send mail \
                  from $sender_address_domain.
        senders = ! :
        hosts   = ! : !+relay_from_hosts
    #log_message = SPF check failed.
    set acl_m9  = -ipv4=$sender_host_address \
                  -sender=$sender_address \
                  -helo=$sender_helo_name
    set acl_m9  = ${run{/usr/bin/spfquery $acl_m9}}
    condition   = ${if eq {$runrc}{1}{true}{false}}

acl/40_exim4-config_check_data

Enable syntax check;
   deny message = Message headers fail syntax check
        # !acl = acl_whitelist_local_deny
        !verify = header_syntax
And a home made syntax check.
Headers shouldn't contain non ASCII chars. Any non ASCII stuff should be escaped;
From: "=?UTF-8?Q?Andr=C3=A9?=" <andre@example.com>
After all, without the 'UTF-8' bit, your MUA wouldn' know how to interpret the non ASCII chars.
Below a filter to block non escaped stuff. The 'r' in 'rh' tells Exim not to de-escape before filtering;
   # More syntax: Non ASCII
   deny message   = Message headers contain non ASCII chars.
        condition = \
        ${if \
         or {\
          {match{$rh_bcc:}{\N[\x80-\xff]\N}}\
          {match{$rh_cc:}{\N[\x80-\xff]\N}}\
          {match{$rh_from:}{\N[\x80-\xff]\N}}\
          {match{$rh_reply-to:}{\N[\x80-\xff]\N}}\
          {match{$rh_sender:}{\N[\x80-\xff]\N}}\
          {match{$rh_subject:}{\N[\x80-\xff]\N}}\
          {match{$rh_to:}{\N[\x80-\xff]\N}}\
         }\
        {yes}{no}}
If you administer a lot of boxes which all forward their postmaster mail to you, it's probably a good idea to accept mail that was accepted by those systems;
  accept hosts     = Host_List_Of_My_Boxes
         condition = \
         ${if !match{$h_to:}\
         {\N<?postmaster@.+>?\N}\
         {yes}{no}}
Don't accept mail from Some_User@localhost.localdomain
  # A domain can't be localhost or localdomain
  deny message = ${domain:$h_from:} is a silly domain.
       condition = ${if match\
       {${domain:$h_from:}}\
       {\N^(localhost|localhost\.localdomain|localdomain)$\N}\
       {yes}{no}}
Below a way to use the envelope blacklist for content. The blacklist needs to exist;
   # Blacklist unreachable sender before h_from: verification
   deny message = Sender content address $h_from: is locally blacklisted here. See http://www.sput.nl/spam/
        condition = \
        ${lookup{${address:$h_from:}}\
        nwildlsearch {/etc/exim4/local_sender_blacklist}\
        {yes}{no}} 
Apply DNS based blacklists to the content from address.
If you want mail from really cheap ISPs, don't use abuse- and postmaster.rfc-ignorant.org
  # Blacklist based on domain
  deny hosts    = !+rbl_white_hosts
       message  = Sender content domain is listed in $dnslist_domain
       dnslists = \
       bogusmx.rfc-ignorant.org/${domain:$h_from:} : \
       dsn.rfc-ignorant.org/${domain:$h_from:} : \
       postmaster.rfc-ignorant.org/${domain:$h_from:} : \
       abuse.rfc-ignorant.org/${domain:$h_from:}
Spammers will often use a content domain for which there is no MX. Usually there is a host with the domain name, but it doesn't accept SMTP connections. This will lead to a defer for each delivery attempt. By checking the MX, this type of mail will be rejected immediately.
  # There has to be an MX, except in case of DSN
  deny message = No MX for content sender domain ${domain:$h_from:}. See http://www.sput.nl/spam/
       hosts   = ! : !+relay_from_hosts
       senders = ! :
       condition = ${if eq\
        {${lookup dnsdb{mx=${domain:$h_from:}}{$value}fail}}\
        {fail}\
       {yes}{no}}
And the MX should be a hostname, not an IP address.
  # And the MX has to be a hostname.
  deny message = MX for content sender domain ${domain:$h_from:} should be FQDN. See http://www.sput.nl/spam/
       hosts   = ! : !+relay_from_hosts
       senders = ! :
       condition = ${if !match\
        {${lookup dnsdb{mx=${domain:$h_from:}}{$value}fail}}\
        {\N.*[A-Za-z].*\..*[A-Za-z].*\N}\
       {yes}{no}}
Keep in mind that the above MX tests may result in a lot of false positives. They are also in violation of RFCs.

And a callout verification

   # require that there is a verifiable sender address in at least
   # one of the 'Sender:', 'Reply-To:', or 'From:' header lines.
   deny message = No verifiable sender address in message headers. See http://www.sput.nl/spam/
        # !acl = acl_whitelist_local_deny
        !verify = header_sender/callout=postmaster,100s,random
If you want Hotmail use this instead;
   # This one doesn't test postmaster
   deny message = No verifiable sender address in message headers. See http://www.sput.nl/spam/
        !acl    = acl_whitelist_local_deny
        !verify = header_sender/callout
The stuff below checks if the remote MX accepts mail for random addresses. Non whitelisted hosts get rejected.
  # A warn if random test succeeds
  warn hosts   = ! : !+relay_from_hosts
       senders = ! :
       message = Content MX accepts mail for random addresses. See http://www.sput.nl/spam/
       #set acl_m7 = $sender_address_domain
       #set acl_m7 = ${run{/usr/local/sbin/chckcal.pl $acl_m7}}
       set acl_m7 = ${run{/usr/local/sbin/chckcal.pl ${domain:$h_from:}}}
       condition  = ${if eq {$runrc}{1}{true}{false}}

  # A deny for non whitelisted hosts
  deny hosts   = ! : !+relay_from_hosts : !+rand_white_hosts
       senders = ! : !*@*.nl
       message = Content MX accepts mail for random addresses. See http://www.sput.nl/spam/
       set acl_m7 = ${run{/usr/local/sbin/chckcal.pl ${domain:$h_from:}}}
       condition  = ${if eq {$runrc}{1}{true}{false}}
Require a message id.
   # deny no Message-Id
   deny message   = RFC compliant Message-Id required. See http://www.sput.nl/spam/
        condition = \
        ${if !match{$h_message-id:}\
        {\N<.+@.+>\N}\
        {yes}{no}}
An other identity theft test
   # Deny fake Message-Id
   deny hosts     = ! : !+relay_from_hosts
        message   = RFC compliant Message-Id required. See http://www.sput.nl/spam/
        condition = \
        ${if match{$h_message-id:}\
        {\N^<.+@(.+\.)?sput\.nl>$\N}\
        {yes}{no}}
Some spammers base64 their entire message to avoid scanning.
   # Deny 'attachment' only
   deny message   = Plain text required. See http://www.sput.nl/spam/
        condition = ${if eq\
         {$h_content-transfer-encoding:}\
         {base64}\ 
        {yes}{no}}
In my experience, only spammers use CP-1252. But maybe your experience is different.
   # Block proprietary charsets
   deny message   = Unsupported character set. See http://www.sput.nl/spam/
        condition = \
        ${if \
         and {\
          {match\
           {${lc:$h_content-type:}}\
           {\Ncharset\N}\
          }\
          {!match\
           {${lc:$h_content-type:}}\
           {\N(us-ascii|iso-?8859-15?|utf-8)\N}\
          }\
         }\
        {yes}{no}}
More identity theft checks
   # Block fake bounces on ip address
   deny hosts     = !+relay_from_hosts
        senders   = :
        message   = This is a fake (joe job) or sub standard (lacking original headers) DSN. See http://www.sput.nl/spam/
        condition = \
        ${if !match{${lc:$message_body}}\
        {\N( |^)received: from .+\.sput\.nl.+213\.84\.159\.78\N}\
        {yes}{no}}

   # Block fake bounces on mesg-id
   deny hosts     = !+relay_from_hosts
        senders   = :
        message   = This is a fake (joe job) or sub standard (lacking original headers) DSN. See http://www.sput.nl/spam/
        condition = \
        ${if !match{${lc:$message_body}}\
        {\N( |^)message-id: <.+@(.+\.)?sput\.nl>( |$)\N}\
        {yes}{no}}
Is the biz TLD used for anything other then spam? I don't think so.

   # Block .biz links
   deny message   = Use of the biz TLD is very bad taste. See http://www.sput.nl/spam/
        condition = \
        ${if match{${lc:$message_body}}\
        {\N<a\ href=(\&|\0x27)?http://.*\.biz\N}\
        {yes}{no}}
HTML mail sux.
   # Block HTML, except DSN's
   # Content-type based
   deny hosts     = ! :
        senders   = ! :
        message   = HTML mail not wanted here. See http://www.sput.nl/spam/
        condition = \
        ${if \
         and {\
          {match{${lc:$h_content-type:}}\
           {\Nmultipart\N}}\
          {match{${lc:$message_body}}\
           {\N --.* content-type: text/html\N}}\
          {!match{${lc:$message_body}}\
           {\N name=\N}}\
          {!match{${lc:$message_body}}\
           {\N filename=\N}}\
         }\
        {yes}{no}}
   # Block HTML, except DSN's
   # Content based
   deny hosts     = ! :
        senders   = ! :
        message   = HTML mail not wanted here. See http://www.sput.nl/spam/
        condition = \
        ${if \
         or {\
          {match{${lc:$message_body}}\
           {\N<html\N}}\
          {match{${lc:$message_body}}\
           {\N<head\N}}\
          {match{${lc:$message_body}}\
           {\N<meta\N}}\
          {match{${lc:$message_body}}\
           {\N<title\N}}\
          {match{${lc:$message_body}}\
           {\N<body\N}}\
         }\
        {yes}{no}}
        condition = \
        ${if \
         or {\
          {match{${lc:$message_body}}\
           {\N<font\N}}\
          {match{${lc:$message_body}}\
           {\N<a\ href=\N}}\
          {match{${lc:$message_body}}\
           {\N<img\ src=\N}}\
          {match{${lc:$message_body}}\
           {\N<br>\N}}\
         }\
        {yes}{no}}
Check the max message size.
   # enforce a message-size limit
   deny message = Message size $message_size is larger than limit of MESSAGE_SIZE_LIMIT  See http://www.sput.nl/spam/
        condition = ${if >{$message_size}{MESSAGE_SIZE_LIMIT}{yes}{no}}
Refuse broken attachments
   # Scan stuff
   deny message = This message contains malformed MIME $demime_reason
        demime  = *
        condition = ${if >{$demime_errorlevel}{2}{1}{0}}
More attachments I don't want. Edit this to suit your needs.
   deny message = Attachment has unsupported file format .$found_extension  try text or PDF instead. See http://www.sput.nl/spam/
        demime  = bat:btm:cmd:com:cpl:dll:doc:exe:lnk:msi:pif:ppt:prf:rar:reg:scr:vb:vbs:url:xls

   deny message = Try gzip, bzip or tar instead of zip
        demime  = zip
Virus scanner
If the scanner doesn't demime, uncomment 'demime = *'.
   deny message = Message contains a virus or other harmful content ($malware_name)
   #      demime = *
        malware = *
SpamAssassin stuff
In this case at 4.0 points a report will be added to the headers and the headers will be logged. At 5.0 the message will be rejected.
   # Don't spam check local generated mail
   # (Comment out for spam check test purposes)
   accept hosts =  : +relay_from_hosts

   # Don't spam filter abuse
   accept condition = \
          ${if \
           and {\
            {match{$recipients}{\N<?(abuse|postmaster)@(.+\.)?sput\.nl>?\N}}\
            {match{$h_to:}{\N>?(abuse|postmaster)@(.+\.)?sput\.nl<?\N}}\
           }\
          {yes}{no}}

   warn message   = X-Spam-Score: $spam_score ($spam_bar)
        condition = ${if <{$message_size}{80k}{1}{0}}
        spam      = spamd:true
        condition = ${if >{$spam_score_int}{40}{1}{0}}
   warn message   = X-Spam-Report: $spam_report
        condition = ${if <{$message_size}{80k}{1}{0}}
        spam      = spamd:true
        condition = ${if >{$spam_score_int}{40}{1}{0}}
   deny message   = This message scored $spam_score spam points. See http://www.sput.nl/spam/
        condition = ${if <{$message_size}{80k}{1}{0}}
        spam      = spamd:true
        condition = ${if >{$spam_score_int}{50}{1}{0}}

acl/50_exim4-config_check_vrfy

This simply enables VRFY;
# ACL that is used after the VRFY command
check_vrfy:
  accept

router

router/200_exim4-config_primary

If you want to mail across your LAN or tunnel using private address space, enable this;
  # ignore private rfc1918 and APIPA addresses
  #ignore_target_hosts = 0.0.0.0 : 127.0.0.0/8 : 192.168.0.0/16 :\
  #                      172.16.0.0/12 : 10.0.0.0/8 : 169.254.0.0/16
  ignore_target_hosts = 0.0.0.0 : 127.0.0.0/8 : 255.255.255.255 : \
                        172.16.0.0/12 : 10.0.0.0/8 : 169.254.0.0/16
I added '255.255.255.255' because it seemed like a good idea.

Alternatively use;

  # ignore private rfc1918 and APIPA addresses
  #ignore_target_hosts = 0.0.0.0 : 127.0.0.0/8 : 192.168.0.0/16 :\
  #                      172.16.0.0/12 : 10.0.0.0/8 : 169.254.0.0/16
  ignore_target_hosts = !192.168.1.0/24 : 0.0.0.0 : 127.0.0.0/8 : 255.255.255.255 : \
                        192.168.0.0/16 : 172.16.0.0/12 : 10.0.0.0/8 : 169.254.0.0/16
Adapt '!192.168.1.0/24' to suit your needs.

transport

transport/30_exim4-config_remote_smtp

The following will add a warning in some MUAs;
  headers_add = \
  X-message-flag: In case of problems see Url_To_Your_spam_policy
The following will change the outgoing helo;
helo_data = ns.sput.nl
Adapt 'ns.sput.nl' to suit your needs.

SPF configuration

spfd doesn't come with a start-stop script. So I created my own. The spfd doesn't write a pid, so I used a rather blunt kill;
#! /bin/sh

# Spf init script

test -f /usr/sbin/spfd || exit 0

set -e

case "$1" in
  start)
	echo -n "Starting SPF Mail Filter Daemon: "
	( /usr/sbin/spfd -port=5970 -setuser=spfd -setgroup=spfd ) &
	echo "spfd."
	;;

  stop)
	echo -n "Stopping SPF Mail Filter Daemon: "
	killall spfd
	sleep 1
	killall -9 spfd
	echo "spfd."
	;;

  *)
	echo "Usage: spf {start|stop}" >&2
	exit 1
	;;
esac

exit 0

To run as a non privileged user, create system user and group spfd

You also need to create various /etc/rc* symlinks;

rc0.d: K19spf -> ../init.d/spf
rc1.d: K19spf -> ../init.d/spf
Rc2.d: S19spf -> ../init.d/spf
rc3.d: S19spf -> ../init.d/spf
rc4.d: S19spf -> ../init.d/spf
rc5.d: S19spf -> ../init.d/spf
rc6.d: K19spf -> ../init.d/spf
Adding spfd to /etc/services looks nice in netstat;
# Local services
spamd		783/tcp				# SpamAssassin
clamd		3310/tcp			# Clam Anti Virus
spfd		5970/tcp			# SPF

SpamAssassin configuration

You need to edit /etc/default/spamassassin. Esp if you want to run spamd as a non privileged user.
ENABLED=1

OPTIONS="--create-prefs --max-children 5 --helper-home-dir --username spamd"

PIDFILE="/var/run/spamd/spamd.pid"
Adding spamd to /etc/services looks nice in netstat;
# Local services
spamd		783/tcp				# SpamAssassin
clamd		3310/tcp			# Clam Anti Virus
spfd		5970/tcp			# SPF  
Create system user and group spamd. /var/run/spamd/ should be owned spamd:spamd. You also need a spamd homedir, such as /home/spamd/ or /var/lib/spamd/. Any local configuration stuff you put in /etc/spamassassin/local.cf;

Block HTML only mail, switch on auto whitelist, tell razor where its config is and supply sensible contact info;

# HTML only sux
score MIME_HTML_ONLY    10.0

use_auto_whitelist	1

# Location of razor.conf
razor_config            /home/spamd/.razor/razor-agent.conf

# Sensible contact
report_contact          postmaster@example.com

Razor configuration

Tell razor where its stuff is. In /etc/razor/razor-agent.conf;
# Use ~spamd/
razorhome = /home/spamd/.razor

Misc

RCPT Whitelisting

The default whitelists are based on host or sender. But what if you want to whitelist based on RCPT?
The problem here is that 'recipients' isn't available in the DATA ACL and '$recipients' isn't available in the RCPT ACL. This means the you need a different approach for these ACLs.

In the RCPT ACL just put an accept statement;

accept: senders = jdoe@example.com
For the DATA ACL, do the following; Create an '/etc/exim4/conf.d/acl/25_exim4-config_whitelist_rcpt'. In this file put something like;
acl_whitelist_rcpt:

accept condition = \
       ${if match\
        {$recipients}\
        {\N<?jdoe@example\.com>?\N}\
       {yes}{no}}
Replace 'jdoe@example.com' with the desired RCPT email address.
Put a '!acl = acl_whitelist_rcpt' in all your DATA ACL reject statements EXCEPT the broken mime and virus ACLs.
Keep in mind that, unlike RCPT ACLs, an accept in the DATA ACL applies to ALL recipients of that same message!

Fetching files

If you don't have ANY Windows boxes on your LAN, you might want to install http://www.timj.co.uk/linux/bogus-virus-warnings.cf. I used to use Wget to fetch this file. However, since Wget doesn't support an IMS GET, I use a shell script to fetch this file.
If your locale is not C or Posix, you need to set the LC_TIME before the first date statement, or change the way the date is processed.
Newer systems use a different 'ls --full-time' format. This script uses the new format.
If you use cron to update this file it will mail you whenever it updates the file;
/etc/cron.daily/spamassassin:
Last-Modified: Fri, 06 May 2005 23:10:22 GMT
ETag: "2fe3375e44faf197a8a9c2cf8c380735"
Content-Length: 109543
Restarting SpamAssassin Mail Filter Daemon: spamd.
Squid log entries should look somewhat like;
1115440296.709    625 localhost TCP_REFRESH_MISS/200 109914 GET http://www.timj.co.uk/linux/bogus-virus-warnings.cf - DIRECT/212.69.37.57 text/plain
When the file was changed.
And;
1115353868.292    202 localhost TCP_REFRESH_HIT/304 239 GET http://www.timj.co.uk/linux/bogus-virus-warnings.cf - DIRECT/212.69.37.57 -
When it didn't change.

Links