Spam Filtering for Mail Exchangers:
Prev | Appendix A. Exim Implementation | Next

# A.14. Final ACLs

OK, time to wake up! This has been very long reading - but congratulations on making it this far!

The following ACLs incorporate all of the checks we have described so in this implementation. However, some have been commented out, for the following reasons:

- Greylisting. This either requires additional software to be installed, or fairly complex inline configuration by way of additional ACLs and definitions in the Exim configuration file. I highly recommend it, though.

- Virus scanning. There is no ubiquitous scanner that nearly everyone uses, similar to SpamAssassin for spam identification. On the other hand, the documentation that comes with `Exiscan-ACL` should be easy to follow.

- Per-user settings for SpamAssassin. This is a trade-off that for many is unacceptable, as it involves deferring mail to all but the first recipient of a message.

- Envelope Sender Signatures. There are consequences, e.g. for roaming users. Also, it involves configuring routers and transports as well as ACLs. See that section for details.

- Accepting Bounces Only for Real Users. There are several ways of doing this, and determining which users are real is specific to how mail is delivered.

Without further ado, here comes the final result we have all been waiting for.

## A.14.1. acl_connect

    # This access control list is used at the start of an incoming
    # connection.  The tests are run in order until the connection is
    # either accepted or denied.

    acl_connect:
      # Record the current timestamp, in order to calculate elapsed time
      # for subsequent delays
      warn
        set acl_m2  = $tod_epoch

      # Accept mail received over local SMTP (i.e. not over TCP/IP).  We do
      # this by testing for an empty sending host field.
      # Also accept mails received over a local interface, and from hosts
      # for which we relay mail.
      accept
        hosts       = : +relay_from_hosts

      # If the connecting host is in one of several DNSbl's, then prepare
      # a warning message in $acl_c1.  We will later add this message to
      # the mail header.  In the mean time, its presence indicates that
      # we should keep stalling the sender.
      #

      warn
        !hosts      = ${if exists {/etc/mail/whitelist-hosts} \
                                  {/etc/mail/whitelist-hosts}}
        dnslists    = list.dsbl.org : \
                      dnsbl.sorbs.net : \
                      dnsbl.njabl.org : \
                      bl.spamcop.net : \
                      dsn.rfc-ignorant.org : \
                      sbl-xbl.spamhaus.org : \
                      l1.spews.dnsbl.sorbs.net
        set acl_c1  = X-DNSbl-Warning: \
                      $sender_host_address is listed in $dnslist_domain\
                      ${if def:dnslist_text { ($dnslist_text)}}

      # Likewise, if reverse DNS lookup of the sender's host fails (i.e.
      # there is no rDNS entry, or a forward lookup of the resulting name
      # does not match the original IP address), then generate a warning
      # message in $acl_c1.  We will later add this message to the mail
      # header.
      warn
        condition   = ${if !def:acl_c1 {true}{false}}
        !verify     = reverse_host_lookup
        set acl_m9  = Reverse DNS lookup failed for host $sender_host_address
        set acl_c1  = X-DNS-Warning: $acl_m9

      # Accept the connection, but if we previously generated a message in
      # $acl_c1, stall the sender until 20 seconds has elapsed.
      accept
        set acl_m2  = ${if def:acl_c1 {${eval:20 + $acl_m2 - $tod_epoch}}{0}}
        delay       = ${if >{$acl_m2}{0}{$acl_m2}{0}}s

## A.14.2. acl_helo

    # This access control list is used for the HELO or EHLO command in
    # an incoming SMTP transaction. The tests are run in order until the
    # greeting is either accepted or denied.

    acl_helo:
      # Record the current timestamp, in order to calculate elapsed time
      # for subsequent delays
      warn
        set acl_m2  = $tod_epoch

      # Accept mail received over local SMTP (i.e. not over TCP/IP).
      # We do this by testing for an empty sending host field.
      # Also accept mails received from hosts for which we relay mail.
      #
      accept
        hosts       = : +relay_from_hosts

      # If the remote host greets with an IP address, then prepare a reject
      # message in $acl_c0, and a log message in $acl_c1.  We will later use
      # these in a "deny" statement.  In the mean time, their presence indicate
      # that we should keep stalling the sender.
      #
      warn
        condition   = ${if isip {$sender_helo_name}{true}{false}}
        set acl_c0  = Message was delivered by ratware
        set acl_c1  = remote host used IP address in HELO/EHLO greeting

      # Likewise if the peer greets with one of our own names
      #
      warn
        condition   = ${if match_domain{$sender_helo_name}\
                           {$primary_hostname:+local_domains:+relay_to_domains}\
                           {true}{false}}
        set acl_c0  = Message was delivered by ratware
        set acl_c1  = remote host used our name in HELO/EHLO greeting.

      # If HELO verification fails, we prepare a warning message in acl_c1.
      # We will later add this message to the mail header.  In the mean time,
      # its presence indicates that we should keep stalling the sender.
      #
      warn
        condition   = ${if !def:acl_c1 {true}{false}}
        !verify     = helo
        set acl_c1  = X-HELO-Warning: Remote host $sender_host_address \
                      ${if def:sender_host_name {($sender_host_name) }}\
                      incorrectly presented itself as $sender_helo_name
        log_message = remote host presented unverifiable HELO/EHLO greeting.

      # Accept the greeting, but if we previously generated a message in
      # $acl_c1, stall the sender until 20 seconds has elapsed.
      accept
        set acl_m2  = ${if def:acl_c1 {${eval:20 + $acl_m2 - $tod_epoch}}{0}}
        delay       = ${if >{$acl_m2}{0}{$acl_m2}{0}}s

## A.14.3. acl_mail_from

    # This access control list is used for the MAIL FROM: command in an
    # incoming SMTP transaction.  The tests are run in order until the
    # sender address is either accepted or denied.
    #

    acl_mail_from:
      # Record the current timestamp, in order to calculate elapsed time
      # for subsequent delays
      warn
        set acl_m2  = $tod_epoch

      # Accept mail received over local SMTP (i.e. not over TCP/IP).
      # We do this by testing for an empty sending host field.
      # Also accept mails received from hosts for which we relay mail.
      #
      # Sender verification is omitted here, because in many cases
      # the clients are dumb MUAs that don't cope well with SMTP
      # error responses.
      #
      accept
        hosts     = : +relay_from_hosts

      # Accept if the message arrived over an authenticated connection,
      # from any host. Again, these messages are usually from MUAs.
      #
      accept
        authenticated = *

      # If present, the ACL variables $acl_c0 and $acl_c1 contain rejection
      # and/or warning messages to be applied to every delivery attempt in
      # in this SMTP transaction.  Assign these to the corresponding
      # $acl_m{0,1} message-specific variables, and add any warning message
      # from $acl_m1 to the message header.  (In the case of a rejection,
      # $acl_m1 actually contains a log message instead, but this does not
      # matter, as we will discard the header along with the message).
      #
      warn
        set acl_m0  = $acl_c0
        set acl_m1  = $acl_c1
        message     = $acl_c1

      # If sender did not provide a HELO/EHLO greeting, then prepare a reject
      # message in $acl_m0, and a log message in $acl_m1.  We will later use
      # these in a "deny" statement.  In the mean time, their presence indicate
      # that we should keep stalling the sender.
      #
      warn
        condition   = ${if def:sender_helo_name {0}{1}}
        set acl_m0  = Message was delivered by ratware
        set acl_m1  = remote host did not present HELO/EHLO greeting.

      # If we could not verify the sender address, create a warning message
      # in $acl_m1 and add it to the mail header.  The presence of this
      # message indicates that we should keep stalling the sender.
      #
      # You may choose to omit the "callout" option.  In particular, if
      # you are sending outgoing mail through a smarthost, it will not
      # give any useful information.
      #
      warn
        condition   = ${if !def:acl_m1 {true}{false}}
        !verify     = sender/callout
        set acl_m1  = Invalid sender <$sender_address>
        message     = X-Sender-Verify-Failed: $acl_m1
        log_message = $acl_m1

      # Accept the sender, but if we previously generated a message in
      # $acl_c1, stall the sender until 20 seconds has elapsed.
      accept
        set acl_m2  = ${if def:acl_c1 {${eval:20 + $acl_m2 - $tod_epoch}}{0}}
        delay       = ${if >{$acl_m2}{0}{$acl_m2}{0}}s

## A.14.4. acl_rcpt_to

    # This access control list is used for every RCPT command in an
    # incoming SMTP message.  The tests are run in order until the
    # recipient address is either accepted or denied.

    acl_rcpt_to:

      # Accept mail received over local SMTP (i.e. not over TCP/IP).
      # We do this by testing for an empty sending host field.
      # Also accept mails received from hosts for which we relay mail.
      #
      # Recipient verification is omitted here, because in many
      # cases the clients are dumb MUAs that don't cope well with
      # SMTP error responses.
      #
      accept
        hosts       = : +relay_from_hosts

      # Accept if the message arrived over an authenticated connection,
      # from any host. Again, these messages are usually from MUAs, so
      # recipient verification is omitted.
      #
      accept
        authenticated = *

      # 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).
      #
      deny
        local_parts = ^.*[@%!/|] : ^\\.

      # Deny if we have previously given a reason for doing so in $acl_m0.
      # Also stall the sender for another 20s first.
      #
      deny
        message     = $acl_m0
        log_message = $acl_m1
        condition   = ${if and {{def:acl_m0}{def:acl_m1}} {true}}
        delay       = 20s

      # If the recipient address is not in a domain for which we are handling
      # mail, stall the sender and reject.
      #
      deny
        message     = relay not permitted
        !domains    = +local_domains : +relay_to_domains
        delay       = 20s

      # If the address is in a local domain or in a domain for which are
      # relaying, but is invalid, stall and reject.
      #
      deny
        message     = unknown user
        !verify     = recipient/callout=20s,defer_ok,use_sender
        delay       = ${if def:sender_address {1m}{0s}}

      # Drop the connection if the envelope sender is empty, but there is
      # more than one recipient address.  Legitimate DSNs are never sent
      # to more than one address.
      #
      drop
        message      = Legitimate bounces are never sent to more than one \
                       recipient.
        senders      = : postmaster@*
        condition    = $recipients_count
        delay        = 5m

      # --------------------------------------------------------------------
      # Limit the number of recipients in each incoming message to one
      # to support per-user settings and data (e.g. for SpamAssassin).
      #
      # NOTE: Every mail sent to several users at your site will be
      #       delayed for 30 minutes or more per recipient.  This
      #       significantly slow down the pace of discussion threads
      #       involving several internal and external parties.
      #       Thus, it is commented out by default.
      #
      #defer
      #  message      = We only accept one recipient at a time - please try later.
      #  condition    = $recipients_count
      # --------------------------------------------------------------------

      # Accept the mail if the sending host is matched in the ".forwarders"
      # file in the recipient's home directory.  Temporarily set $acl_m9 to
      # point to this file.  If the host is found, set a flag in $acl_m0 and
      # clear $acl_m1 to indicate that we should not reject this mail later.
      #
      accept
        domains     = +local_domains
        set acl_m9  = /home/${extract{1}{=}{${lc:$local_part}}}/.forwarders
        hosts       = ${if exists {$acl_m9}{$acl_m9}}
        set acl_m0  = accept
        set acl_m1  =

      # Accept the mail if the sending host is matched in the global
      # whitelist file.  Temporarily set $acl_m9 to point to this  file.
      # If the host is found, set a flag in $acl_m0 and clear $acl_m1 to
      # indicate that we should not reject this mail later.
      #
      accept
        set acl_m9  = /etc/mail/whitelist-hosts
        hosts       = ${if exists {$acl_m9}{$acl_m9}}
        set acl_m0  = accept
        set acl_m1  =

      # --------------------------------------------------------------------
      # Envelope Sender Signature Check.
      # This is commented out by default, because it requires additional
      # configuration in the 'transports' and 'routers' sections.
      #
      # Accept the recipient addresss if it contains our own signature.
      # This means this is a response (DSN, sender callout verification...)
      # to a message that was previously sent from here.
      #
      #accept
      #  domains     = +local_domains
      #  condition   = ${if and {{match{${lc:$local_part}}{^(.*)=(.*)}}\
      #                          {eq{${hash_8:${hmac{md5}{SECRET}{$1}}}}{$2}}}\
      #                         {true}{false}}
      #
      # Otherwise, if this message claims to be a bounce (i.e. if there
      # is no envelope sender), but if the receiver has elected to use
      # and check against envelope sender signatures, reject it.
      #
      #deny
      #  message     = This address does not match a valid, signed \
      #                return path from here.\n\
      #                You are responding to a forged sender address.
      #  log_message = bogus bounce.
      #  senders     = : postmaster@*
      #  domains     = +local_domains
      #  set acl_m9  = /home/${extract{1}{=}{${lc:$local_part}}}/.return-path-sign
      #  condition   = ${if exists {$acl_m9}{true}}
      # --------------------------------------------------------------------

      # --------------------------------------------------------------------
      # Deny mail for local users that do not have a mailbox (i.e. postmaster,
      # webmaster...) if no sender address is provided.  These users do
      # not send outgoing mail, so they should not receive returned mail.
      #
      # NOTE: This is commented out by default, because the condition is
      #       specific to how local mail is delivered.  If you want to
      #       enable this check, uncomment one and only one of the
      #       conditions below.
      #
      #deny
      #  message     = This address never sends outgoing mail. \
      #                You are responding to a forged sender address.
      #  log_message = bogus bounce for system user <$local_part@$domain>
      #  senders     = : postmaster@*
      #  domains     = +local_domains
      #  set acl_m9  = ${extract{1}{=}{${lc:$local_part}}}
      #
      # --- Uncomment the following 2 lines if recipients have local accounts:
      #  set acl_m9  = ${extract{2}{:}{${lookup passwd {$acl_m9}{$value}}}{0}}
      #  !condition  = ${if and {{>={$acl_m9}{500}} {<${acl_m9}{60000}}} {true}}
      #
      # --- Uncomment the following line if you deliver mail to Cyrus:
      #  condition  = ${run {/usr/sbin/mbpath -q -s user.$acl_m9} {true}}
      # --------------------------------------------------------------------

      # Query the SPF information for the sender address domain, if any,
      # to see if the sending host is authorized to deliver its mail.
      # If not, reject the mail.
      #
      deny
        message     = [SPF] $sender_host_address is not allowed to send mail \
                      from $sender_address_domain
        log_message = SPF check failed.
        spf         = fail

      # Add a SPF-Received: line to the message header
      warn
        message     = $spf_received

      # --------------------------------------------------------------------
      # Check greylisting status for this particular peer/sender/recipient.
      # Before uncommenting this statement, you need to install "greylistd".
      # See:  http://packages.debian.org/unstable/main/greylistd
      #
      # Note that we do not greylist messages with NULL sender, because
      # sender callout verification would break (and we might not be able
      # to send mail to a host that performs callouts).
      #
      #defer
      #  message     = $sender_host_address is not yet authorized to deliver mail \
      #                from <$sender_address> to <$local_part@$domain>. \
      #                Please try later.
      #  log_message = greylisted.
      #  domains     = +local_domains : +relay_to_domains
      #  !senders    = : postmaster@*
      #  set acl_m9  = $sender_host_address $sender_address $local_part@$domain
      #  set acl_m9  = ${readsocket{/var/run/greylistd/socket}{$acl_m9}{5s}{}{}}
      #  condition   = ${if eq {$acl_m9}{grey}{true}{false}}
      #  delay       = 20s
      # --------------------------------------------------------------------

      # Accept the recipient.
      accept

## A.14.5. acl_data

    # This access control list is used for message data received via
    # SMTP.  The tests are run in order until the recipient address
    # is either accepted or denied.

    acl_data:
      # Log some header lines
      warn
        logwrite    = Subject: $h_Subject:

      # Add Message-ID if missing in messages received from our own hosts.
      warn
        condition   = ${if !def:h_Message-ID: {1}}
        hosts       = +relay_from_hosts
        message     = Message-ID: <E$message_id@$primary_hostname>

      # Accept mail received over local SMTP (i.e. not over TCP/IP).
      # We do this by testing for an empty sending host field.
      # Also accept mails received from hosts for which we relay mail.
      #
      accept
        hosts       = : +relay_from_hosts

      # Accept if the message arrived over an authenticated connection, from
      # any host.
      #
      accept
        authenticated = *

      # Deny if we have previously given a reason for doing so in $acl_m0.
      # Also stall the sender for another 20s first.
      #
      deny
        message     = $acl_m0
        log_message = $acl_m1
        condition   = ${if and {{def:acl_m0}{def:acl_m1}} {true}{false}}
        delay       = 20s

      # enforce a message-size limit
      #
      deny
        message     = Message size $message_size is larger than limit of \
                      MESSAGE_SIZE_LIMIT
        condition   = ${if >{$message_size}{MESSAGE_SIZE_LIMIT}{yes}{no}}

      # Deny unless the addresses in the header is syntactically correct.
      #
      deny
        message     = Your message does not conform to RFC2822 standard
        log_message = message header fail syntax check
        !verify     = header_syntax

      # Uncomment the following to deny non-local messages without
      # a Message-ID:, Date:, or Subject: header.
      #
      # Note that some specialized MTAs, such as certain mailing list
      # servers, do not automatically generate a Message-ID for bounces.
      # Thus, we add the check for a non-empty sender.
      #
      #deny
      #  message     = Your message does not conform to RFC2822 standard
      #  log_message = missing header lines
      #  !hosts      = +relay_from_hosts
      #  !senders    = : postmaster@*
      #  condition   = ${if !eq {$acl_m0}{accept}{true}}
      #  condition   = ${if or {{!def:h_Message-ID:}\
      #                         {!def:h_Date:}\
      #                         {!def:h_Subject:}} {true}{false}}

      # Warn unless there is a verifiable sender address in at least
      # one of the "Sender:", "Reply-To:", or "From:" header lines.
      #
      warn
        message     = X-Sender-Verify-Failed: No valid sender in message header
        log_message = No valid sender in message header
        !verify     = header_sender

      # --------------------------------------------------------------------
      # Perform greylisting on messages with no envelope sender here.
      # We did not subject these to greylisting after RCPT TO: because
      # that would interfere with remote hosts doing sender callouts.
      # Note that the sender address is empty, so we don't bother using it.
      #
      # Before uncommenting this statement, you need to install "greylistd".
      # See:  http://packages.debian.org/unstable/main/greylistd
      #
      #defer
      #  message     = $sender_host_address is not yet authorized to send \
      #                delivery status reports to <$recipients>. \
      #                Please try later.
      #  log_message = greylisted.
      #  senders     = : postmaster@*
      #  condition   = ${if !eq {$acl_m0}{accept}{true}}
      #  set acl_m9  = $sender_host_address $recipients
      #  set acl_m9  = ${readsocket{/var/run/greylistd/socket}{$acl_m9}{5s}{}{}}
      #  condition   = ${if eq {$acl_m9}{grey}{true}{false}}
      #  delay       = 20s
      # --------------------------------------------------------------------

      # --- BEGIN EXISCAN configuration ---

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

      # Unpack MIME containers and reject file extensions used by worms.
      # This calls the demime condition again, but it will return cached results.
      # Note that the extension list may be incomplete.
      #
      deny
        message     = We do not accept ".$found_extension" attachments here.
        demime      = bat:btm:cmd:com:cpl:dll:exe:lnk:msi:pif:prf:reg:scr:vbs:url

      # Messages larger than MESSAGE_SIZE_SPAM_MAX are accepted without
      # spam or virus scanning
      accept
        condition   = ${if >{$message_size}{MESSAGE_SIZE_SPAM_MAX} {true}}
        logwrite    = :main: Not classified \
                      (message size larger than MESSAGE_SIZE_SPAM_MAX)

      # --------------------------------------------------------------------
      # Anti-Virus scanning
      # This requires an 'av_scanner' setting in the main section.
      #
      #deny
      #  message  = This message contains a virus ($malware_name)
      #  demime   = *
      #  malware  = */defer_ok
      # --------------------------------------------------------------------

      # Invoke SpamAssassin to obtain $spam_score and $spam_report.
      # Depending on the classification, $acl_m9 is set to "ham" or "spam".
      #
      # If the message is classified as spam, and we have not previously
      # set $acl_m0 to indicate that we want to accept it anyway, pretend
      # reject it.
      #
      warn
        set acl_m9  = ham
        # ------------------------------------------------------------------
        # If you want to allow per-user settings for SpamAssassin,
        # uncomment the following line, and comment out "spam = mail".
        # We pass on the username specified in the recipient address,
        # i.e. the portion before any '=' or '@' character, converted
        # to lowercase.  Multiple recipients should not occur, since
        # we previously limited delivery to one recipient at a time.
        #
        # spam        = ${lc:${extract{1}{=@}{$recipients}{$value}{mail}}}
        # ------------------------------------------------------------------
        spam        = mail
        set acl_m9  = spam
        condition   = ${if !eq {$acl_m0}{accept}{true}}
        control     = fakereject
        logwrite    = :reject: Rejected spam (score $spam_score): $spam_report

      # Add an appropriate X-Spam-Status: header to the message.
      #
      warn
        message     = X-Spam-Status: \
                      ${if eq {$acl_m9}{spam}{Yes}{No}} (score $spam_score)\
                      ${if def:spam_report {: $spam_report}}
        logwrite    = :main: Classified as $acl_m9 (score $spam_score)

      # --- END EXISCAN configuration ---

      # Accept the message.
      #
      accept

Prev | Home | Next
Exempting Forwarded Mail | Up | Glossary