Introduction

This article is an explained example of exim 4 ACL configuration. For a larger point of view see "Spam Filtering for Mail Exchangers".

With this configuration we got approximately (for one person) 1 spam per day and we reject 20 per day (see the exim rejectlog file).

The context is a host server with a public IP (82.224.147.80, www.maretmmanu.org), wich is also used as local mail server:

######################################################################
#                    MAIN CONFIGURATION SETTINGS                     #
######################################################################

# If exim is used localy in batch mode (exim4 -bs) then "$host" is empty, the ": :" adds the empty string.
hostlist   own_hosts = 127.0.0.1 : : 192.168.113.114 : 192.168.113.113 : 82.224.147.80
domainlist public_domains = maretmmanu.org
    

Remote host IP checking

We allow connections from our own hosts and a white-list (Some hosts from big internet providers) with no more check. We refuse connections with some hosts (marketing company etc).


acl_check_host:
        accept  
                hosts = +own_hosts : /etc/exim4/filters/host_white.list

        deny
                log_message = match host_reject.list
                hosts = /etc/exim4/filters/host_reject.list

        accept

Remote host IP checking by DNS black-list

Hosts listed by the dns list "sbl-xbl.spamhaus.org" are spammers or relays for spams. Often if you refuse the connection for one of these hosts then a new try is done by another relay some seconds later (see my old reject log). A better solution is to do the rejection when the RCPT is received. Then the spammer does not try again (see the new reject log).

acl_check_rcpt:
 . . . 
        drop
                log_message = match sbl-xbl.spamhaus.org
                dnslists = sbl-xbl.spamhaus.org

HELO checking

Often spammers send for the HELO argument the name or the IP of your host. Here my own domain is "maretmmanu.org" and my own IP is 82.224.147.80.

acl_check_helo:
        accept  
                hosts = +own_hosts

	# If the HELO pretend to be this host
	deny	condition = ${if or { \
					{eq {${lc:$sender_helo_name}}{maretmmanu.org}} \
					{eq {${lc:$sender_helo_name}}{82.224.147.80}} \
				    } {true}{false} }

        # by default we accept
        accept

Sender checking

We refuse some senders, from some marketing companies.

acl_check_sender:
        deny    senders = /etc/exim4/filters/sender_reject.list
        accept

Recipient: emails addresses to catch spams

You can publish a sacrified email address in a web page to trap spammers (some spammers crawl other web pages to get emails). When this email address matches then an error is returned and all the message reception is droped. There are changes that the spammer software will not retry with this recipient removed.

When you write to a suspicious company wich could send you spam or when you write in a newsgroup, you can use a special email, with date (like echant-td-n040531@maretmmanu.org) or with an included identifier (like echant-tr-lemonde@maretmmanu.org). Then if you receive spam for this email you can put it in the drop list (in this example: /etc/exim4/filters/recipients_drop.list).

acl_check_rcpt:
 . . . 
        drop
                log_message   = match recipients_drop.list.
                recipients = /etc/exim4/filters/recipients_drop.list

I use this script in cron.daily/ to update my emails with a date incorporated. The letter before the date is used to trace the origine (web, news, email).

#!/bin/bash

# Update my email wich include the today date
set -e
T=$(tempfile)
D=$(date '+%y%m%d')

function mod_file {
    EMAIL="$1"
    LETTRE="$2"
    CONF="$3"
    if [ -f "$CONF" -a -r "$CONF" ]; then
        lockfile-create "$CONF"
        sed "s/${EMAIL}-td-${LETTRE}[0-9]\{6\}@maretmmanu.org/${EMAIL}-td-${LETTRE}${D}@maretmmanu.org/g" <"$CONF" >"$T"
        cp "$T" "$CONF"
        lockfile-remove "$CONF"
    fi
}

# The first line will replace echant-td-n040625@maretmmanu.org 
# with echant-td-n040626@maretmmanu.org
mod_file echant n /home/manu/.kde/share/config/knoderc
mod_file echant e /home/manu/.sylpheed/accountrc
mod_file echant e /home/manu/.initvar
# For apache we should reload but it is done by
# logrotate from time to time.
mod_file echant w /etc/apache-extern/httpd.conf

rm $T

Recipient: no hack

(From /usr/share/doc/exim4-doc-html/html/C043.txt.gz):

Deny if the local part contains @ or % or / or | or !. These are rarely found in genuine local parts, but are often tried by people looking to circumvent relaying restrictions.

Also deny if the local part starts with a dot. Empty components aren't strictly legal in RFC 2822, but Exim allows them because this is common. However, actually starting with a dot may cause trouble if the local part is used as a file name (e.g. for a mailing list).

acl_check_rcpt:
 . . . 
        # refuse if the recipient string is a hack, 
        # see exim file example C043.txt.gz
        deny    
                local_parts = ^.*[@%!/|] : ^\\.

Recipient: no relay

I refuse to relay spams:

acl_check_rcpt:
 . . . 
        # For the rest, the domain of the recipient address
        # must be my public domain. (no relay) 
        require
                log_message = no relay.
                domains     = +public_domains

Recipient: manual redirect by the sender

The idea is to send an automatic reply, using "mail" command in a filter, to inform that an email is blocked and that the user must use an other email address. This can be used to change a user email which receive to much spam or to protect a public email address.

In a filter:

### reply for echant@maretmmanu.org
if $original_local_part is "echant" then
    seen mail from drop@maretmmanu.org subject "Re: $h_subject" file .echant_reponse.txt
    finish
endif

greylist

Greylisting use the fact that most of the time spammers softwares do not take account tempory errors to retry later. It's very effective. You can use a daemon like greylistd.

When exim send a "tempory error":

The two lines with "set acl_m9" are used to send the request to the daemon and get the result. The "/24" is here because some big MTA can be spreaded on multiple hosts.

######################################################################
#                    MAIN CONFIGURATION SETTINGS                     #
######################################################################
# Mandatory to use  "verify = helo"
helo_try_verify_hosts = !+own_hosts
 . . . 
######################################################################
#                          ACL CONFIGURATION                         #
######################################################################
# ACL "subroutine" used by acl_check_rcpt below. Used to detect 
# hosts wich have not their own registered domain-name (probably spammer).
# Return ok if the HELO argument correspond to the connected HOST and 
# if the argument does not contain an IP in decimal or hexa.
# I have created this ACL subroutine because we can't do a list of "or" 
# in ACL (it's a list of "and"), so I use a negation of "and": 
# no (no A and no B) = A or B.
acl_clean_helo:
        accept
                verify     = helo
                condition  = ${if match{$sender_helo_name}{\N(\d{1,3}[.-]\d{1,3}[.-]\d{1,3}[.-]\d{1,3})|([0-9a-f]{8})|([0-9A-F]{8})\N}{false}{true}}


acl_check_rcpt:
 . . . 

        # Greylisting, if the HELO argument seems bad or 
        # a dialin name (with IP included in the name). Some hosts from big
        # providers are in a white list to avoid testing. When there is no
        # sender then it is a bounce message, so no greylist.
        defer
                message = Please try later.
                !hosts      = /etc/exim4/filters/host_white.list
                !senders    = :
                !acl        = acl_clean_helo
                log_message = greylisted.
                set acl_m9  = ${mask:$sender_host_address/24} $sender_address $local_part@$domain
                set acl_m9  = ${readsocket{/var/run/greylistd/socket}{$acl_m9}{5s}{}{}}
                condition   = ${if eq {$acl_m9}{grey}{true}{false}}


anti-virus: windows executables in attachment

It's a very basic anti-virus: every emails with a windows executable as attachment is rejected.

acl_check_data:
 . . . 
        deny    message = This message contains an attachment of a type which we do not accept (.$found_extension) 
                demime = bat:btm:cmd:com:cpl:dll:exe:lnk:msi:pif:prf:reg:scr:vbs:url
  

anti-virus: clamav

I keep this chapter but I have removed clamav from my computer since there are some vulnerabilities announced times to times and because, with other spam filters, it seems not very usefull.

We used an anti-virus not to avoid virus (we have just linux hosts) but to remove unwanted emails.

######################################################################
#                    MAIN CONFIGURATION SETTINGS                     #
######################################################################
av_scanner = clamd:/var/run/clamd.ctl
 . . . 
######################################################################
#                          ACL CONFIGURATION                         #
######################################################################
acl_check_data:
 . . . 
        deny    message = This message contains a virus or other harmful content ($malware_name)
                demime = * 
                malware = *

anti-spam external detector: spamassassin

We add a "X-SA-Score:" in the header of all emails, a "X-SA-Report:" for all email with spam score >0, we consider it a spam if score >5 (adding "X-SA-Status: Yes" and we don't accept the email if score >7.

Because of the "accept" we must put this acl block at the end of the acl_check_data.

######################################################################
#                    MAIN CONFIGURATION SETTINGS                     #
######################################################################
spamd_address = 127.0.0.1 783
 . . . 
######################################################################
#                          ACL CONFIGURATION                         #
######################################################################
acl_check_data:
 . . . 
        ## spamassassin, spams are never big and spamassassin can die on big emails, so we
        ## limit its use under 500ko.
        accept  condition = ${if >={$message_size}{500k}{yes}{no}}
        warn    message = X-SA-Score: $spam_score 
                spam = nobody:true 
        warn    message = X-SA-Report: $spam_report 
                spam = nobody:true
                condition = ${if >{$spam_score_int}{0}{true}{false}}
        warn    message = X-SA-Status: Yes 
                spam = nobody:true
                condition = ${if >{$spam_score_int}{50}{true}{false}}
        deny    message = This message scored $spam_score spam points. 
                spam = nobody:true 
                condition = ${if >{$spam_score_int}{70}{true}{false}}

  

In your "~/.forward" you can redirect spams (5< score ≤7) in a special inbox:

#   Exim filter   <<== do not edit or remove this line!
if $h_X-SA-Status: matches "^Yes" then
     save $home/.Mailboxes/incoming/spam
     finish
endif
  

Checking source of email associated with your domain in whois

If you have an email published in a whois database (spammers scan these databases) but want emails just from your registrar, you can add this in your "~/.forward" filter:

#   Exim filter   <<== do not edit or remove this line!
if $original_local_part is "echant-tr-myregistrar"
  then
  if $sender_address_domain is "myregistrar.net" then
    deliver marcel
  else
    save $home/.Mailboxes/incoming/spam
    finish
  endif
endif
  

All in one

######################################################################
#                    MAIN CONFIGURATION SETTINGS                     #
######################################################################

hostlist   own_hosts = 127.0.0.1 : 192.168.109.24 : 192.168.109.23 : 82.224.147.80
domainlist public_domains = maretmmanu.org
 . . . 
# Mandatory to use  "verify = helo"
helo_try_verify_hosts = !+own_hosts

av_scanner = clamd:/var/run/clamd.ctl
spamd_address = 127.0.0.1 783

acl_smtp_rcpt = acl_check_rcpt
acl_smtp_mail = acl_check_sender
acl_smtp_connect = acl_check_host
acl_smtp_data = acl_check_data
acl_smtp_helo = acl_check_helo


 . . . 
######################################################################
#                          ACL CONFIGURATION                         #
######################################################################
acl_check_host:
        accept
                hosts = +own_hosts : /etc/exim4/filters/host_white.list

        deny    
                log_message = match host_reject.list
                hosts = /etc/exim4/filters/host_reject.list

        accept

acl_check_helo:
        accept  hosts = +own_hosts

	# If the HELO pretend to be this host
	deny	condition = ${if or { \
					{eq {${lc:$sender_helo_name}}{maretmmanu.org}} \
					{eq {${lc:$sender_helo_name}}{82.224.147.80}} \
				    } {true}{false} }

        # by default we accept
        accept

acl_check_sender:
        deny    senders = /etc/exim4/filters/sender_reject.list
        accept

# ACL "subroutine" used by acl_check_rcpt below.
# Return ok if the HELO argument correspond to the connected HOST and 
# if the HELO argument does not contain an IP in decimal or hexa.
# I have created this ACL subroutine because we can't do a list of "or" 
# in ACL (it's a list of "and"), so I use a negation of "and": 
# no (no A and no B) = A or B.
acl_clean_helo:
        accept
                verify     = helo
                condition  = ${if match{$sender_helo_name}{\N(\d{1,3}[.-]\d{1,3}[.-]\d{1,3}[.-]\d{1,3})|([0-9a-f]{8})|([0-9A-F]{8})\N}{false}{true}}


acl_check_rcpt:
        # refuse if the recipient string is a hack, 
        # see exim file example C043.txt.gz
        deny    
                local_parts = ^.*[@%!/|] : ^\\.

        # Relaying with no more check for my own hosts.
        accept  
                hosts = +own_hosts

        # For the rest, the domain of the recipient address
        # must be my public domain. (no relay) 
        require
                log_message = no relay.
                domains     = +public_domains

        # Reffuse all the message if the recipient is only used by spammers.
        drop
                log_message   = match recipients_drop.list.
                recipients = /etc/exim4/filters/recipients_drop.list

        drop
                log_message = match sbl-xbl.spamhaus.org
                dnslists = sbl-xbl.spamhaus.org

        # Greylisting, if the HELO argument seems bad or 
        # a dialin name (with IP included in the name). Some hosts from big
        # providers are in a white list to avoid testing. When there is no
        # sender then it is a bounce message, so no greylist.
        defer
                message = Please try later.
                !hosts      = /etc/exim4/filters/host_white.list
                !senders    = :
                !acl        = acl_clean_helo
                log_message = greylisted.
                set acl_m9  = ${mask:$sender_host_address/24} $sender_address $local_part@$domain
                set acl_m9  = ${readsocket{/var/run/greylistd/socket}{$acl_m9}{5s}{}{}}
                condition   = ${if eq {$acl_m9}{grey}{true}{false}}

        # Default rule: accept except if recipient address is unrouteable.
        accept  
                message = unrouteable address
                verify = recipient


acl_check_data:

        accept  hosts = +own_hosts

        # if there is a windows executable as attachment then we reject
        deny    message = This message contains an attachment of a type which we do not accept (.$found_extension) 
                demime = bat:btm:cmd:com:cpl:dll:exe:lnk:msi:pif:prf:reg:scr:vbs:url

        # clamav
        deny    message = This message contains a virus or other harmful content ($malware_name)
                demime = * 
                malware = *
        
        ## spamassassin, spams are never big and spamassassin can die on big emails, so we
        ## limit its use under 500ko.
        accept  condition = ${if >={$message_size}{500k}{yes}{no}}
        warn    message = X-SA-Score: $spam_score 
                spam = nobody:true 
        warn    message = X-SA-Report: $spam_report 
                spam = nobody:true
                condition = ${if >{$spam_score_int}{0}{true}{false}}
        warn    message = X-SA-Status: Yes 
                spam = nobody:true
                condition = ${if >{$spam_score_int}{50}{true}{false}}
        deny    message = This message scored $spam_score spam points. 
                spam = nobody:true 
                condition = ${if >{$spam_score_int}{70}{true}{false}}

        # accept by default
        accept