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:

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

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

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

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

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

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