Fighting spam with Exim 4

This article describes some spam fighting techniques for the Exim MTA (Mail Trading Agent). All examples have been tested on a Debian system, which will make the tips more Debian specific, but the concepts are general and it should be easy to implement the same hooks on a different distribution.

Many of the settings in this article are based on the assumption that mail from untrustworthy sources or from poorly configured mail hubs should not be accepted. This may be unacceptable for a big company, but if you are in the lucky situation to be able to reject those mails, then this article might be of interest.

Index

RCPT Checks

The default Exim 4 (Debian) configuration file contains some very useful examples which just need to be enabled or customised. I will name a few of them here. Please note that many of these checks require additional DNS look-ups so please be prepared to a slowdown of the SMTP session.

Sender Verification

A very simple way of detecting spam is to verify if the sender's mail address is valid. Exim 4 checks only if the given local part accepts mail. This means, for a given address, e.h. "kasperl@example.com", Exim 4 checks only if the domain "example.com" is able to accept mail (i.e. has a valid MX record) bot does not check if the address gets accepted by the remote mailer. (This verification is called outcall by Exim 4, can consume a lot of resources and is disabled by default.

There are many different types of Sender verification. If enabled in the access control lists section of the config file, then different kinds of sender verification will be done on the message.

The sender condition in the acl_check_rcpt ACL checks if the e-mail address in the RCPT TO: command is valid, while the header_sender condition in the acl_check_data ACL checks if the header of the mail contains a valid Sender:, Reply-To: or From: line.

acl_check_rcpt:
  #...
  deny
    message = Sender verification failed
    !verify = sender

acl_check_data:
  #...
  deny
    message = No verifiable sender address in message headers
    !verify = header_sender

If you are using Debian's Exim package, then you just need to define the following lines at the top of your Exim config file:

CHECK_RCPT_VERIFY_SENDER = yes
CHECK_DATA_VERIFY_HEADER_SENDER = yes

If you have split config files, then create a new file, e.g. /etc/exim4/conf.d/main/00_local_options with the above content.

DNS Blacklists

DNS black lists (DNSBL) are a neat way of getting rid of notorious spammers. After a connection on port 25 from a host, the MTA checks one or more servers via a standard DNS query if the connected IP address is listed. The queried RBLS server replies either with a DNS fail (IP address not listed) or with a IP address (IP Address listed). It is safe to reject mails permanently (i.e. with 550 error message) a from listed servers, but a reasonable error message should be sent as well in order to give the sender a clue why his message has been refused.

DNSBLs are enabled in Exim 4 as access control lists. This list should be placed after cheaper tests, in order to reduce network overhead.

acl_check_rcpt:
  #...
  deny
    message = X-Warning: $sender_host_address is listed at $dnslist_domain ($dnslist_value: $dnslist_text)
    log_message = $sender_host_address is listed at $dnslist_domain ($dnslist_value: $dnslist_text)
    dnslists = <colon-separated list of DNSBLs>

If you are using Debian's Exim package, then you just need to define the following line:

CHECK_RCPT_IP_DNSBLS = <colon-separated list of DNSBLs>

There are many blacklist hosts out there. See for example Wikipedia's Comparison of DNS blacklists or Declude's list of DNSBLs.

Reverse DNS Verification

An other simple sanity check is the so called DNS verification. The idea is to check the DNS name from the IP address. If there is no DNS name, then the likelihood that the sending host is a legitimate mail server is very low.

This ACL just warns if Exim 4 was unable to determine the sending host name:

acl_check_rcpt:
  #...
  warn
    message = X-Host-Lookup-Failed: Reverse DNS lookup failed for $sender_host_address \
            (${if eq{$host_lookup_failed}{1}{failed}{deferred}})
     condition = ${if and{{def:sender_host_address}{!def:sender_host_name}}\
                      {yes}{no}}

In a Debian config file this check can be enabled with the following line:

CHECK_RCPT_REVERSE_DNS = yes

SpamAssassin

In order to set up SpamAssassin, you need to set the spamd_address option in the main configuration. The following configuration is suitable for most hosts where SpamAssassin runs on the same host as Exim 4.

spamd_address = 127.0.0.1 783

The next thing is to set up an ACL for spam:

acl_check_data:
  #...
  warn  message = X-Spam-Score: $spam_score ($spam_bar)
       spam = nobody:true
  warn  message = X-Spam-Report: $spam_report
       condition = ${if >{$spam_score_int}{5}{1}{0}}
       spam = nobody:true

This example does not reject spam messages, but flags them. This way the user can decide what to do with the spam, e.g. he or she can use the appropriate filter mechanism of his or her mail user agent.

Address Rewriting

With address rewriting it is possible accept mail for a local part with a fixed first half and an arbitrary second. This is useful when untrustworthy sites requires a registration and you don't want to generate a new alias just for that site. With the following configuration as many one-time mails can be used as desired.

begin routers
#...
suffix_rewrite:
  driver = redirect
  allow_defer
  allow_fail
  skip_syntax_errors
  data = ${local_part}@${domain}
  local_part_suffix = "+*"
  retry_use_local_part

The effect of this router is that everything after a + character is removed from the local part and Exim 4 tries to route the resulting address again.

Note that Exim 4 supports the local_part_suffix and local_part_suffix_optional options for the same goal, but I find the above way more flexible.

SPF - Sender Policy Framework

Sender Policy Framework is an IETF experimental standard which aims to address sending of forged e-mail addresses. It gives domain owners the possibility to specify the hosts that are allowed to send mails for that domain. To some extend this approach bastardises the concept of SMTP, but I think it is a fair approach to limit spam. And the best of it is that it is a true opt-in approach: nobody is forced to use SPF nor to publish an SPF record in its DNS. And you can enable SPF checks without publishing one yourself.

This configuration is taken as-is from Debian's config file (Debian users have just to set the CHECK_RCPT_SPF macro):

acl_check_rcpt:
  #...

  # Use spfquery to perform a pair of SPF checks.
  #
  # This is quite costly in terms of DNS lookups (~6 lookups per mail). Do not
  # enable if that's an issue. Also note that if you enable this, you must
  # install "libmail-spf-query-perl" which provides the spfquery command.
  # Missing libmail-spf-query-perl will trigger the "Unexpected error in
  # SPF check" warning.

  deny
    message = [SPF] $sender_host_address is not allowed to send mail from \
                ${if def:sender_address_domain {$sender_address_domain}{$sender_helo_name}}.
    log_message = SPF check failed.
    condition = ${run{/usr/bin/spfquery --ip \"$sender_host_address\" \
                --mail-from \"$sender_address\" --helo \"$sender_helo_name\"} \
                {no}{${if eq {$runrc}{1}{yes}{no}}}}

  defer
    message = Temporary DNS error while checking SPF record. Try again later.
    condition = ${if eq {$runrc}{5}{yes}{no}}

  warn
    message = Received-SPF: ${if eq {$runrc}{0}{pass}{${if eq {$runrc}{2}{softfail}\
            {   ${if eq {$runrc}{3}{neutral}{${if eq {$runrc}{4}{unknown}\
            {${if eq {$runrc}{6}{none}{error}}}}}}}}}}
    condition = ${if <={$runrc}{6}{yes}{no}}

  warn
    log_message = Unexpected error in SPF check.
    condition = ${if >{$runrc}{6}{yes}{no}}

  # Support for best-guess
  warn
    message = X-SPF-Guess: ${run{/usr/bin/spfquery --ip \"$sender_host_address\" \
                --mail-from \"$sender_address\" \ --helo \"$sender_helo_name\" --guess true}\
                {pass}{${if eq {$runrc}{2}{softfail}{${if eq {$runrc}{3}{neutral}{${if eq {$runrc}{4}{unknown}\
                {${if eq {$runrc}{6}{none}{error}}}}}}}}}}
    condition = ${if <={$runrc}{6}{yes}{no}}

  defer
    message = Temporary DNS error while checking SPF record. Try again later.
    condition = ${if eq {$runrc}{5}{yes}{no}}

Exim 4: ACL and others to reject spams describes similar techniques as this page. Combining Exim 4 with SpamAssassin explains the setup of Exim 4 with SpamAssassin in more detail.