Exim Implementation
Here we cover the integration of techniques and tools described
in this document into the Exim .
Prerequisites
For these examples, you need the , preferrably with Tom Kistner's
patch applied. Prebuilt
packages exist for the most
popular Linux distributions as well as FreeBSD; see the Exiscan-ACL
home page for details
In particular, Exim is perhaps most popular among users of
Debian GNU/Linux,
as it is the default MTA in that distribution. If you use
Debian (Sarge or later), you can obtain
Exim+Exiscan-ACL by installing the
package:
# apt-get install exim4-daemon-heavy.
The final implementation example at the end incorporates these
additional tools:
SpamAssassin
- a popular spam filtering tool that analyzes mail content
against a large and highly sophisticated set of
heuristics.
greylistd
- a simple greylisting solution written by yours truly,
specifically with Exim in mind.
Other optional software is used in examples throughout.
The Exim Configuration File
The Exim configuration file contains global definitions at the
top (we will call this the main section),
followed by several other sectionsDebian users: The
package gives you a choice
between splitting the Exim configuration into several small
chunks distributed within subdirectories below
, or to keep the entire
configuration in a single file.
If you chose the former option (I recommend this!), you can
keep your customization well separated from the stock
configuration provided with the
package by creating new files within these subdirectories,
rather than modifying the existing ones. For instance, you
may create a file named
to declare your own ACL for the RCPT TO:
command (see below).
The Exim init script
() will automatically
consolidate all these files into a single large run-time
configuration file next time you (re)start.
. Each of these other sections starts with:
begin section
We will spend most of our time in the
section (i.e. after ); but we will
also add and/or modify a few items in the
and
sections, as well as in the main section at the top of the file.
Access Control Lists
As of version 4.xx, Exim incorporates perhaps the most
sophisticated and flexible mechanism for SMTP-time filtering
available anywhere, by way of so-called Access
Control Lists (ACLs).
An ACL can be used to evaluate whether to accept or reject an
aspect of an incoming message transaction, such as the initial
connection from a remote host, or the
HELO/EHLO, MAIL FROM:,
or RCPT TO: SMTP commands. So, for
instance, you may have an ACL named
to validate each RCPT
TO: command received from the peer.
An ACL consists of a series of statements
(or rules). Each statement starts with
an action verb, such as ,
, ,
, or , followed by
a list of conditions, options, and other settings pertaining
to that statement. Every statement is
evaluated in order, until a definitive action (besides
) is taken. There is an implicit
at the end of the ACL.
A sample statement in the ACL
above may look like this:
deny
message = relay not permitted
!hosts = +relay_from_hosts
!domains = +local_domains : +relay_to_domains
delay = 1m
This statement will reject the RCPT TO:
command if it was not delivered by a host in the
+relay_from_hosts host list, and the recipient
domain is not in the +local_domains or
+relay_to_domains domain lists. However, before
issuing the 550 SMTP response to this command,
the server will wait for one minute.
To evaluate a particular ACL at a given stage of the message
transaction, you need to point one of Exim's policy
controls to that ACL. For instance, to use the
ACL mentioned above to evaluate the
RCPT TO:, the main section of your Exim
configuration file (before any keywords)
should include:
acl_smtp_rcpt = acl_rcpt_to
For a full list of such policy controls,
refer to section 14.11 in the Exim specifications.
Expansions
A large number of expansion items are
available, including run-time variables, lookup functions,
string/regex manipulations, host/domain lists, etc. etc. An
exhaustive reference for the last x.x0 release (i.e. 4.20,
4.30..) can be found in the file spec.txt; ACLs
are described in section 38.
In particular, Exim provides twenty general purpose expansion
variables to which we can assign values in an ACL statement:
$acl_c0 - $acl_c9 can
hold values that will persist through the lifetime of an
SMTP connection.
$acl_m0 - $acl_m9 can
hold values while a message is being received, but are
then reset. They are also reset by the
HELO, EHLO,
MAIL, and RSET
commands.
Options and Settings
The main section of the Exim configuration file (before the
first keyword) contains various macros,
policy controls, and other general settings. Let us start by
defining a couple of macros we will use later:
# Define the message size limit; we will use this in the DATA ACL.
MESSAGE_SIZE_LIMIT = 10M
# Maximum message size for which we will run Spam or Virus scanning.
# This is to reduce the load imposed on the server by very large messages.
MESSAGE_SIZE_SPAM_MAX = 1M
# Macro defining a secret that we will use to generate various hashes.
# PLEASE CHANGE THIS!.
SECRET = some-secret
Let us tweak some general Exim settings:
# Treat DNS failures (SERVFAIL) as lookup failures.
# This is so that we can later reject sender addresses
# within non-existing domains, or domains for which no
# nameserver exists.
dns_again_means_nonexist = !+local_domains : !+relay_to_domains
# Enable HELO verification in ACLs for all hosts
helo_try_verify_hosts = *
# Remove any limitation on the maximum number of incoming
# connections we can serve at one time. This is so that while
# we later impose SMTP transaction delays for spammers, we
# will not refuse to serve new connections.
smtp_accept_max = 0
# ..unless the system load is above 10
smtp_load_reserve = 10
# Do not advertise ESMTP "PIPELINING" to any hosts.
# This is to trip up ratware, which often tries to pipeline
# commands anyway.
pipelining_advertise_hosts = :
Finally, we will point some Exim policy controls to five ACLs
that we will create to evaluate the various stages of an
incoming SMTP transaction:
acl_smtp_connect = acl_connect
acl_smtp_helo = acl_helo
acl_smtp_mail = acl_mail_from
acl_smtp_rcpt = acl_rcpt_to
acl_smtp_data = acl_data
Building the ACLs - First Pass
In the acl section (following ), we
need to define these ACLs. In doing so, we will incorporate
some of the basic
described earlier in this document, namely
and
.
In this pass, we will do most of the checks in , and leave the other ACLs largely
empty. That is because most of the commonly used ratware does
not understand rejections early in the SMTP transaction - it
keeps trying. On the other hand, most ratware clients give up
if the RCPT TO: fails.
We create all these ACLs, however, because we will use them
later.
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:
# In this pass, we do not perform any checks here.
accept
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:
# In this pass, we do not perform any checks here.
accept
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:
# Accept the command.
accept
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 = *
######################################################################
# DNS checks
######################################################################
#
# The results of these checks are cached, so multiple recipients
# does not translate into multiple DNS lookups.
#
# If the connecting host is in one of a select few DNSbls, then
# reject the message. Be careful when selecting these lists; many
# would cause a large number of false postives, and/or have no
# clear removal policy.
#
deny
dnslists = dnsbl.sorbs.net : \
dnsbl.njabl.org : \
cbl.abuseat.org : \
bl.spamcop.net
message = $sender_host_address is listed in $dnslist_domain\
${if def:dnslist_text { ($dnslist_text)}}
# 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 reject the message.
#
deny
message = Reverse DNS lookup failed for host $sender_host_address.
!verify = reverse_host_lookup
######################################################################
# Hello checks
######################################################################
# If the remote host greets with an IP address, then reject the mail.
#
deny
message = Message was delivered by ratware
log_message = remote host used IP address in HELO/EHLO greeting
condition = ${if isip {$sender_helo_name}{true}{false}}
# Likewise if the peer greets with one of our own names
#
deny
message = Message was delivered by ratware
log_message = remote host used our name in HELO/EHLO greeting.
condition = ${if match_domain{$sender_helo_name}\
{$primary_hostname:+local_domains:+relay_to_domains}\
{true}{false}}
deny
message = Message was delivered by ratware
log_message = remote host did not present HELO/EHLO greeting.
condition = ${if def:sender_helo_name {false}{true}}
# If HELO verification fails, we add a X-HELO-Warning: header in
# the message.
#
warn
message = 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.
!verify = helo
######################################################################
# Sender Address Checks
######################################################################
# If we cannot verify the sender address, deny the message.
#
# You may choose to remove the "callout" option. In particular,
# if you are sending outgoing mail through a smarthost, it will not
# give any useful information.
#
# Details regarding the failed callout verification attempt are
# included in the 550 response; to omit these, change
# "sender/callout" to "sender/callout,no_details".
#
deny
message = <$sender_address> does not appear to be a \
valid sender address.
!verify = sender/callout
######################################################################
# Recipent Address Checks
######################################################################
# 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 = ^.*[@%!/|] : ^\\.
# 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
# Reject the recipient address if it is not in a domain for
# which we are handling mail.
#
deny
message = relay not permitted
!domains = +local_domains : +relay_to_domains
# Reject the recipient if it is not a valid mailbox.
# If the mailbox is not on our system (e.g. if we are a
# backup MX for the recipient domain), then perform a
# callout verification; but if the destination server is
# not responding, accept the recipient anyway.
#
deny
message = unknown user
!verify = recipient/callout=20s,defer_ok
# Otherwise, the recipient address is OK.
#
accept
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:
# 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 = *
# 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}{true}{false}}
# Deny unless the address list header is syntactically correct.
#
deny
message = Your message does not conform to RFC2822 standard
log_message = message header fail syntax check
!verify = header_syntax
# Deny non-local messages with no Message-ID, or no Date
#
# 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 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
# Accept the message.
#
accept
Adding SMTP transaction delaysThe simple way
The simplest way to add SMTP transaction delays is to append a
control to the final
statement in each of the ACLs we have
declared, as follows:
accept
delay = 20s
In addition, you may want to add progressive delays in the
statement pertaining to invalid
recipients (unknown user) within . This is to slow down dictionary
attacks. For instance:
deny
message = unknown user
!verify = recipient/callout=20s,defer_ok,use_sender
delay = ${eval:$rcpt_fail_count*10 + 20}s
It should be noted that there is no point in imposing a delay
in , after the message data has
been received. Ratware commonly disconnect at this point,
before even receiving a response from your server. In any
case, whether or not the client disconnects at this point has
no bearing on whether Exim will proceed with the delivery of
the message.
Selective Delays
If you are like me, you want to be a little bit more selective
about which hosts you subject to SMTP transaction delays. For
instance, as described earlier in this document, you may
decide that a match from a DNS blacklist or a non-verifiable
EHLO/HELO greeting are not conditions that by themselves
warrant a rejection - but they may well be sufficient triggers
for transaction delays.
In order perform selective delays, we want move some of the
checks that we previously did in to earlier points in the SMTP
transaction. This is so that we can start imposing the delays
as soon as we see any sign of trouble, and thereby increase
the chance of causing synchronization errors and other trouble
for ratware.
Specifically, we want to:
Move the DNS checks to
.
Move the Hello checks to .
One exception: We cannot yet check for a missing Hello
greeting at this point, because this ACL is processed
in response to an EHLO or HELO
command. We will do this check in the ACL.
Move the Sender Address Checks checks to .
However, for reasons described above, we do not want to
actually reject the mail until after the RCPT
TO: command. Instead, in the earlier ACLs, we
will convert the various statements
into statements, and use Exim's
general purpose ACL variables to store any error messages or
warnings until after the RCPT TO:
command. We do that as follows:
If we decide to reject the delivery, we store an error
message to be used in the forthcoming
550 response in
$acl_c0 or $acl_m0:
If we identify the condition before a mail delivery
has started (i.e. in
or
), we use the
connection-persistent variable
$acl_c0
Once a mail transaction has started (i.e. after the
MAIL FROM: command), we copy any
contents from $acl_c0 into the
message-specific variable $acl_m0,
and use the latter from this point forward. This
way, any conditions identified in this particular
message will not affect any subsequent messages
received in the same connection.
Also, we store a corresponding log
message in $acl_c1 or
$acl_m1, in a similar manner.
If we come across a condition that does not warrant an
outright rejection, we only store a warning message in
$acl_c1 or $acl_m1.
Once a mail transaction has started (i.e. in ), we add any content in
this variable to the message header as well.
If we decide to accept a message
without regard to the results of any subsequent checks
(such as a SpamAssassin scan), we set a flag in
$acl_c0 or $acl_m0, but
$acl_c1 and $acl_m1
empty.
At the beginning of every ACL to and including , we record the current
timestamp in $acl_m2. At the end of the
ACL, we use the presence of $acl_c1 or
$acl_m1 to trigger a SMTP transaction
delay until a total of 20 seconds has elapsed.
The following table summarizes our use of these variables:
Use of ACL connection/message variablesVariables:$acl_[cm]0 unset$acl_[cm]0 set$acl_[cm]1 unset(No decision yet)Accept the mail$acl_[cm]1 setAdd warning in headerReject the mail
As an example of this approach, let us consider two checks
that we do in response to the Hello greeting; one that will
reject mails if the peer greets with an IP address, and one
that will warn about an unverifiable name in the greeting.
Previously, we did both of these checks in - now we move them to the ACL.
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
# 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.
#
# ... additional checks omitted for this example ...
#
# 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
Then, in we transfer the
messages from to
. We also add the contents of
$acl_c1 to the message header.
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.
#
accept
hosts = : +relay_from_hosts
# 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
#
# ... additional checks omitted for this example ...
#
# 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
All the pertinent changes are incorporated in the , to follow.
Adding Greylisting Support
There are several alternate greylisting implementations
available for Exim. Here we will cover a couple of these.
greylistd
This is a Python implementation developed by yours
truly. (So naturally, this is the implementation
I will include in the to
follow). It operates as a stand-alone daemon, and thus does
not depend on any external database. Greylist data is
stored as simple 32-bit hashes for efficiency.
You can find it at .
Debian users can get it via APT:
# apt-get install greylistd
To consult , we insert two
statements in ACL that we
previously declared, right before the final
statement:
# Consult "greylistd" to obtain greylisting status for this particular
# peer/sender/recipient triplet.
#
# We do not greylist messages with a 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}}
Unless you incorporate envelope
sender signatures to block bogus s, you may want to add a similar statement in
your to also greylist messages
with a NULL sender.
The data we use for greylisting purposes here will be a little
different than above. In addition to
being emtpy, neither
nor is
defined at this point. Instead, the variable
contains a comma-separated list
of all recipient addresses. For a legitimate DSN, there
should be only one address.
# 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.
#
defer
message = $sender_host_address is not yet authorized to send \
delivery status reports to <$recipients>. \
Please try later.
log_message = greylisted.
senders = : postmaster@*
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}}
MySQL implementation
The following inline implementation was contributed by
Johannes Berg johannes (at) sipsolutions.net,
based in part on:
work by Rick Stewart rick.stewart (at)
theinternetco.net, published at ,
in turn based on
a Postgres implementation created by Tollef Fog Heen
tfheen (at) raw.no, available at
It requires no external programs - the entire implementation
is based on these configuration snippets along with a MySQL
database.
An archive containing up-to-date configuration snippets as
well as a file is available at:
.
MySQL needs to be installed on your system. At a MySQL
prompt, create an database with two
tables named and
, as follows:
CREATE DATABASE exim4;
use exim4;
CREATE TABLE exim_greylist (
id bigint(20) NOT NULL auto_increment,
relay_ip varchar(80) default NULL,
sender varchar(255) default NULL,
recipient varchar(255) default NULL,
block_expires datetime NOT NULL default '0000-00-00 00:00:00',
record_expires datetime NOT NULL default '9999-12-31 23:59:59',
create_time datetime NOT NULL default '0000-00-00 00:00:00',
type enum('AUTO','MANUAL') NOT NULL default 'MANUAL',
passcount bigint(20) NOT NULL default '0',
blockcount bigint(20) NOT NULL default '0',
PRIMARY KEY (id)
);
CREATE TABLE exim_greylist_log (
id bigint(20) NOT NULL auto_increment,
listid bigint(20) NOT NULL,
timestamp datetime NOT NULL default '0000-00-00 00:00:00',
kind enum('deferred', 'accepted') NOT NULL,
PRIMARY KEY (id)
);
In the main section of your Exim
configuration file, declare the following macros:
# if you don't have another database defined, then define it here
hide mysql_servers = localhost/exim4/user/password
# options
# these need to be valid as xxx in mysql's DATE_ADD(..,INTERVAL xxx)
# not valid, for example, are plurals: "2 HOUR" instead of "2 HOURS"
GREYLIST_INITIAL_DELAY = 1 HOUR
GREYLIST_INITIAL_LIFETIME = 4 HOUR
GREYLIST_WHITE_LIFETIME = 36 DAY
GREYLIST_BOUNCE_LIFETIME = 0 HOUR
# you can change the table names
GREYLIST_TABLE=exim_greylist
GREYLIST_LOG_TABLE=exim_greylist_log
# comment out to the following line to disable greylisting (temporarily)
GREYLIST_ENABLED=
# uncomment the following to enable logging
#GREYLIST_LOG_ENABLED=
# below here, nothing should normally be edited
.ifdef GREYLIST_ENABLED
# database macros
GREYLIST_TEST = SELECT CASE \
WHEN now() > block_expires THEN "accepted" \
ELSE "deferred" \
END AS result, id \
FROM GREYLIST_TABLE \
WHERE (now() < record_expires) \
AND (sender = '${quote_mysql:$sender_address}' \
OR (type='MANUAL' \
AND ( sender IS NULL \
OR sender = '${quote_mysql:@$sender_address_domain}' \
) \
) \
) \
AND (recipient = '${quote_mysql:$local_part@$domain}' \
OR (type = 'MANUAL' \
AND ( recipient IS NULL \
OR recipient = '${quote_mysql:$local_part@}' \
OR recipient = '${quote_mysql:@$domain}' \
) \
) \
) \
AND (relay_ip = '${quote_mysql:$sender_host_address}' \
OR (type='MANUAL' \
AND ( relay_ip IS NULL \
OR relay_ip = substring('${quote_mysql:$sender_host_address}',1,length(relay_ip)) \
) \
) \
) \
ORDER BY result DESC LIMIT 1
GREYLIST_ADD = INSERT INTO GREYLIST_TABLE \
(relay_ip, sender, recipient, block_expires, \
record_expires, create_time, type) \
VALUES ( '${quote_mysql:$sender_host_address}', \
'${quote_mysql:$sender_address}', \
'${quote_mysql:$local_part@$domain}', \
DATE_ADD(now(), INTERVAL GREYLIST_INITIAL_DELAY), \
DATE_ADD(now(), INTERVAL GREYLIST_INITIAL_LIFETIME), \
now(), \
'AUTO' \
)
GREYLIST_DEFER_HIT = UPDATE GREYLIST_TABLE \
SET blockcount=blockcount+1 \
WHERE id = $acl_m9
GREYLIST_OK_COUNT = UPDATE GREYLIST_TABLE \
SET passcount=passcount+1 \
WHERE id = $acl_m9
GREYLIST_OK_NEWTIME = UPDATE GREYLIST_TABLE \
SET record_expires = DATE_ADD(now(), INTERVAL GREYLIST_WHITE_LIFETIME) \
WHERE id = $acl_m9 AND type='AUTO'
GREYLIST_OK_BOUNCE = UPDATE GREYLIST_TABLE \
SET record_expires = DATE_ADD(now(), INTERVAL GREYLIST_BOUNCE_LIFETIME) \
WHERE id = $acl_m9 AND type='AUTO'
GREYLIST_LOG = INSERT INTO GREYLIST_LOG_TABLE \
(listid, timestamp, kind) \
VALUES ($acl_m9, now(), '$acl_m8')
.endif
Now, in the ACL section (after ),
declare a new ACL named greylist_acl:
.ifdef GREYLIST_ENABLED
# this acl returns either deny or accept
# since we use it inside a defer with acl = greylist_acl,
# accepting here makes the condition TRUE thus deferring,
# denying here makes the condition FALSE thus not deferring
greylist_acl:
# For regular deliveries, check greylist.
# check greylist tuple, returning "accepted", "deferred" or "unknown"
# in acl_m8, and the record id in acl_m9
warn set acl_m8 = ${lookup mysql{GREYLIST_TEST}{$value}{result=unknown}}
# here acl_m8 = "result=x id=y"
set acl_m9 = ${extract{id}{$acl_m8}{$value}{-1}}
# now acl_m9 contains the record id (or -1)
set acl_m8 = ${extract{result}{$acl_m8}{$value}{unknown}}
# now acl_m8 contains unknown/deferred/accepted
# check if we know a certain triple, add and defer message if not
accept
# if above check returned unknown (no record yet)
condition = ${if eq{$acl_m8}{unknown}{1}}
# then also add a record
condition = ${lookup mysql{GREYLIST_ADD}{yes}{no}}
# now log, no matter what the result was
# if the triple was unknown, we don't need a log entry
# (and don't get one) because that is implicit through
# the creation time above.
.ifdef GREYLIST_LOG_ENABLED
warn condition = ${lookup mysql{GREYLIST_LOG}}
.endif
# check if the triple is still blocked
accept
# if above check returned deferred then defer
condition = ${if eq{$acl_m8}{deferred}{1}}
# and note it down
condition = ${lookup mysql{GREYLIST_DEFER_HIT}{yes}{yes}}
# use a warn verb to count records that were hit
warn condition = ${lookup mysql{GREYLIST_OK_COUNT}}
# use a warn verb to set a new expire time on automatic records,
# but only if the mail was not a bounce, otherwise set to now().
warn !senders = : postmaster@*
condition = ${lookup mysql{GREYLIST_OK_NEWTIME}}
warn senders = : postmaster@*
condition = ${lookup mysql{GREYLIST_OK_BOUNCE}}
deny
.endif
Incorporate this ACL into your
to greylist triplets where the sender address is non-empty.
This is to allow for sender callout verifications:
.ifdef GREYLIST_ENABLED
defer !senders = : postmaster@*
acl = greylist_acl
message = greylisted - try again later
.endif
Also incorporate it into your
block, but this time only if the sender address is empty.
This is to prevent spammers from getting around greylisting by
setting the sender address to NULL.
.ifdef GREYLIST_ENABLED
defer senders = : postmaster@*
acl = greylist_acl
message = greylisted - try again later
.endif
Adding SPF Checks
Here we cover two different ways to check
records using Exim. In addition to these explicit mechanisms,
the SpamAssassin suite will in the near future (around version
2.70) incorporate more sophisticated SPF checks, by assigning
weighted scores to the various SPF results.
Although we could perform this check as
early as in the ACL, there
is an issue that will affect this decision: SPF is incompatible
with traditional e-mail forwarding. Unless the forwarding host
implements SRS, you may end up
rejecting forwarded mail because you receive it from a host that
is not authorized to do so per the SPF policy of the domain in
the address.
To avoid doing this, we need to consult a user-specific list of
hosts from which forwarded mails should be accepted (as
described in , to follow).
This is only possible after the RCPT TO:,
when we know the username of the recipient.
As such, we will add this check prior to any greylisting
checks and/or the final statement in
.
SPF checks via Exiscan-ACL
Recent versions of Tom Kistner's
patch (see ) have native support
for SPF.
Debian users: As of July 14th, 2004, the version of
Exiscan-ACL that is included in the
package does not yet
have support for SPF. In the mean time, you may choose
the other SPF implementation; install
.
Usage is very simple. An ACL condition
is added, and can be compared against any of the keywords
, ,
, ,
, or
.
Prior to any greylisting checks and/or the final
statement in , insert the following snippet:
# 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: header to the message
warn
message = $spf_received
This statement will reject the mail if the owner of the domain
in the sender address has disallowed deliveries from the
calling host. Some people find that this gives the domain
owner a little bit too much control, even to the point of
shooting themselves in the foot. A suggested alternative is
to combine the SPF check with other checks, such as Sender
Callout Verification (but note that as before, there is no
point in doing this if you are sending your outgoing mail
through a smarthost):
# Reject the mail if we cannot verify the sender address via callouts,
# and if SPF information for the sending domain does not grant explicit
# authority to the sending host.
#
deny
message = The sender address does not seem to be valid, and SPF \
information does not grant $sender_host_address explicit \
authority to send mail from $sender_address_domain
log_message = SPF check failed.
!verify = sender/callout,random,postmaster
!spf = pass
# Add a SPF-Received: header to the message
warn
message = $spf_received
SPF checks via Mail::SPF::Query
is a the official SPF test
suite, available from . Debian users,
install .
The package comes with a
daemon (spfd) that listens for requests on
a UNIX domain socket. Unfortunately, it does not come with an
init script to start this daemon automatically.
Therefore, in the following example, we will use the
standalone spfquery utility to make our SPF
requests.
As above, insert the following prior to any greylisting checks
and/or the final statement in :
# Use "spfquery" to obtain SPF status for this particular sender/host.
# If the return code of that command is 1, this is an unauthorized sender.
#
deny
message = [SPF] $sender_host_address is not allowed to send mail \
from $sender_address_domain.
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}}
Adding MIME and Filetype Checks
These checks depend on features found in Tom Kistner's
patch - see for details.
Exiscan-ACL includes support for MIME decoding, and file name
suffix checks (or to use a misnomer from the Windows world,
file extension checks). This check alone will
block most Windows virii - but not those that are transmitted in
archives or those that exploit
Outlook/MSIE HTML rendering vulnerabilities - see the discussion
on .
These checks should go into ,
before the final statement:
# 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
You will note that the condition is
invoked twice in the example above. However, the results are
cached, so the message is not actually processed twice.
Adding Anti-Virus Software
Exiscan-ACL plugs into a number of different virus scanners
directly, or any other scanner that can be run from the
command line via its backend.
To use this feature, the main
section of your Exim configuration file must specify
which virus scanner to use, along with any options you wish to
pass to that scanner. The basic syntax is:
av_scanner = scanner-type:option1:option:...
For instance:
av_scanner = sophie:/var/run/sophie
av_scanner = kavdaemon:/opt/AVP/AvpCtl
av_scanner = clamd:127.0.0.1 1234
av_scanner = clamd:/opt/clamd/socket
av_scanner = cmdline:/path/to/sweep -all -rec -archive %s:found:'(.+)'
...
In the DATA ACL, you then want to use the
condition to perform the actual
scanning:
deny
message = This message contains a virus ($malware_name)
demime = *
malware = */defer_ok
The included file
contains full usage information.
Adding SpamAssassin
Invoking SpamAssassin at SMTP-time is commonly done in either
of two ways in Exim:
Via the condition offered by
. This is the mechanism we
will cover here.
Via , another utility written by
Marc Merlins (marc (at) merlins.org),
specifically for running SpamAssassin at SMTP time in Exim.
This program operates through Exim's
interface, either patched
directly into the Exim source code, or via Marc's own
plugin (which, by the way, is
included in Debian's
and packages).
offers some other features as
well, namely greylisting and
teergrubing. However, because the
scan happens after the message data has been received,
neither of these two features may be as useful as they
would be earlier in the SMTP transaction.
can be found at:
.
Invoke SpamAssassin via Exiscan
's
condition passes the
message through either SpamAssassin or Brightmail, and
triggers if these indicate that the message is junk. By
default, it connects to a SpamAssassin daemon
() running on
. The host address and port can be
changed by adding a setting in
the main section of the Exim
configuration file. For more information, see the
file included with the
patch.
In our implementation, we are going to reject messages
classified as spam. However, we would like to keep a copy of
such messages in a separate mail folder, at least for the time
being. This is so that the user can periodically scan for
s.
Exim offers controls that can be applied
to a message that is accepted, such as
. The Exiscan-ACL patch adds one more
of these controls, namely .
This causes the following SMTP response:
550-FAKEREJECT id=message-id
550-Your message has been rejected but is being kept for evaluation.
550 If it was a legit message, it may still be delivered to the target recipient(s).
We can incorporate this feature into our implementation, by
inserting the following snippet in , prior to the final
statement:
# 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, pretend to reject it.
#
warn
set acl_m9 = ham
spam = mail
set acl_m9 = spam
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)
In this example, $acl_m9 is initially set to
ham. Then SpamAssassin is invoked as the user
. If the message is classified as spam,
then $acl_m9 is set to spam,
and the response above is issued.
Finally, an header is added to
the message. The idea is that the or
the recipient's can use this header to
filter junk mail into a separate folder.
Configure SpamAssassin
By default, SpamAssassin presents its report in a verbose,
table-like format, mainly suitable for inclusion in or
attachment to the message body. In our case, we want a terse
report, suitable for the
header in the example above. To do this, we add the following
snippet in its site specific configuration file
(,
, or similar):
### Report template
clear_report_template
report "_TESTSSCORES(, )_"
Also, a Bayesian scoring
feature is built in, and is turned on by default. We normally
want to turn this off, because it requires training that will
be specific to each user, and thus is not suitable for
system-wide SMTP time filtering:
### Disable Bayesian scoring
use_bayes 0
For these changes to take effect, you have to restart the
SpamAssassin daemon (spamd).
User Settings and Data
Say you have a number of users that want to specify their
individual SpamAssassin preferences, such as the spam
threshold, acceptable languages and character sets,
white/blacklisted senders, and so on. Or perhaps they really
want to be able to make use of SpamAssassin's native Bayesian
scoring (though I don't see why
Although it is true that Bayesian training is specific to
each user, it should be noted that SpamAssassin's
Bayesian classifier is, IMHO, not that stellar in any case.
Especially I find this to be the case since spammers have
learned to defeat such systems by seeding random dictionary
words or stories in their mail (e.g. in the metadata of
HTML messages).
).
As discussed in the section
earlier in the document, there is a way for this to happen.
We need to limit the number of recipients we accept per
incoming mail delivery to one. We accept the first
RCPT TO: command issued by the caller, then
defer subsequent ones using a 451 SMTP
response. As with greylisting, if the caller
is a well-behaved MTA it will know how to interpret this
response, and retry later.
Tell Exim to accept only one recipient per delivery
In the , we insert the
following statement after validating the recipient address,
but before any statements pertaining
to unauthenticated deliveries from remote hosts to local
users (i.e. before any greylist checks, envelope signature
checks, etc):
# 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.
#
defer
message = We only accept one recipient at a time - please try later.
condition = $recipients_count
Pass the recipient username to SpamAssassin
In , we modify the
condition given in the previous
section, so that it passes on to SpamAssassin the username
specified in the local part of the recipient address.
# Invoke SpamAssassin to obtain $spam_score and $spam_report.
# Depending on the classification, $acl_m9 is set to "ham" or "spam".
#
# 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.
#
# If the message is classified as spam, pretend to reject it.
#
warn
set acl_m9 = ham
spam = ${lc:${extract{1}{=@}{$recipients}{$value}{mail}}}
set acl_m9 = spam
control = fakereject
logwrite = :reject: Rejected spam (score $spam_score): $spam_report
Note that instead of using Exim's
function to get the
username, we manually extracted the portion before any
@ or = character. This is
because we will use the latter character in our envelope signature scheme, to
follow.
Enable per-user settings in SpamAssassin
Let us now again look at SpamAssassin. First of all, you
may choose to remove the
setting that we previously added in its site-wide
configuration file. In any case, each user will now have
the ability to decide whether to override this setting for
themselves.
If mailboxes on your system map directly to local UNIX
accounts with home directories, you are done. By default,
the SpamAssassin daemon (spamd) performs
a to the username we pass to it,
and stores user data and settings in that user's home
directory.
If this is not the case (for instance, if your mail accounts
are managed by Cyrus SASL or by another server), you need to
tell SpamAssassin where to find each user's preferences and
data files. Also, spamd needs to keep
running as a specific local user instead of attempting to
to a non-existing user.
We do these things by specifying the options passed to
spamd at startup:
On a Debian system, edit the
setting in .
On a RedHat system, edit the
setting in
.
Others, figure it out.
The options you need are:
username -
specify the user under which spamd
will run (e.g. )
- disable configuration files in
user's home directory.
- specify where per-user settings and data are stored.
%u is replaced with the calling username.
spamd must be able to create or
modify this directory:
# mkdir /var/lib/spamassassin
# chown -R mail:mail /var/lib/spamassassin
Needless to say, after making these changes, you need to
restart spamd.
Adding Envelope Sender Signatures
Here we implement in our outgoing
mail, and check for these signatures before accepting incoming
bounces (i.e. mail with no envelope sender).
The envelope sender address of outgoing mails from your host
will be modified as follows:
sender=recipient=recipient.domain=hash@sender.domain
However, because this scheme may produce unintended consequences
(e.g. in the case of mailing list servers), we make it optional
for your users. We sign the envelope sender address of outgoing
mail only if we find a file named
.return-path-sign in the sender's home directory,
and only if the domain we are sending to is matched in that
file. If the file exists, but is empty, all domains match.
Similarly, we only require the recipient address to be signed in
incoming bounce messages (i.e. messages with no
envelope sender) if the same file exists in recipient's home
directory. Users can exempt specific hosts from this check via
their user specific whitelist, as described in
.
Also, because this scheme involves tweaking with routers and
transports in addition to ACLs, we do not include it in the
to follow. If you are able to
follow the instructions pertaining to those sections, you should
also be able to add the ACL section as described here.
Create a Transport to Sign the Sender Address
First we create an Exim transport that
will be used to sign the envelope sender for remote
deliveries:
remote_smtp_signed:
debug_print = "T: remote_smtp_signed for $local_part@$domain"
driver = smtp
max_rcpt = 1
return_path = $sender_address_local_part=$local_part=$domain=\
${hash_8:${hmac{md5}{SECRET}{${lc:\
$sender_address_local_part=$local_part=$domain}}}}\
@$sender_address_domain
The local part of the sender address now
consists of the following components, separated by equal
signs (=):
the sender's username, i.e. the original local part,
the local part of the recipient address,
the domain part of the recipient address,
a string unique to this sender/recipient
combination, generated by:
encrypting the three prior components of the rewritten
sender address, using Exim's
function along with
the we declared in the
section,
If you think this is an overkill, would I tend to
agree on the surface. In previous versions of
this document, I simply used
to generate
the last component of the signature. However,
with this it would be technically possible, with a
bit of insight into Exim's
function and some
samples of your outgoing mail sent to different
recipients, to forge the signature. Matthew
Byng-Maddic mbm (at) colondot.net
notes:
What you're writing is a document that you
expect many people to just copy. Given that,
kerchoff's principle starts applying, and all of
your secrecy should be in the key. If the key
can be reversed out, as seems likely with a few
return paths, then the spammer kan once again
start emitting valid return-paths from that
domain, and you're back to where you
started. [...] Better, IMO, to have it being
strong from the start.
hashing the result into 8 lowercase letters, using
Exim's function.
If you need authentication for deliveries to
smarthosts, add an appropriate
line here as well.
(Take it from your existing smarthost transport).
Create a New Router for Remote Deliveries
Add a new router prior to the existing router(s) that
currently handles your outgoing mail. This router will use
the transport above for remote deliveries, but only if the
file .return-path-sign exists in the sender's
home directory, and if the recipient's domain is matched in
that file. For instance, if you send mail directly over the
internet to the final destination:
# Sign the envelope sender address (return path) for deliveries to
# remote domains if the sender's home directory contains the file
# ".return-path-sign", and if the remote domain is matched in that
# file. If the file exists, but is empty, the envelope sender
# address is always signed.
#
dnslookup_signed:
debug_print = "R: dnslookup_signed for $local_part@$domain"
driver = dnslookup
transport = remote_smtp_signed
senders = ! : *
domains = ! +local_domains : !+relay_to_domains : \
${if exists {/home/$sender_address_local_part/.return-path-sign}\
{/home/$sender_address_local_part/.return-path-sign}\
{!*}}
no_more
Or if you use a smarthost:
# Sign the envelope sender address (return path) for deliveries to
# remote domains if the sender's home directory contains the file
# ".return-path-sign", and if the remote domain is matched in that
# file. If the file exists, but is empty, the envelope sender
# address is always signed.
#
smarthost_signed:
debug_print = "R: smarthost_signed for $local_part@$domain"
driver = manualroute
transport = remote_smtp_signed
senders = ! : *
route_list = * smarthost.address
host_find_failed = defer
domains = ! +local_domains : !+relay_to_domains : \
${if exists {/home/$sender_address_local_part/.return-path-sign}\
{/home/$sender_address_local_part/.return-path-sign}\
{!*}}
no_more
Add other options as you see fit
(e.g. ),
perhaps modelled after your existing routers.
Note that we do not use this router for mails with no envelope
sender address - we wouldn't want to tamper with those!
In the examples above, the
condition is actually redundant, since the file
is not likely to
exist. However, we make the condition explicit for
clarity.
Create New Redirect Router for Local Deliveries
Next, you need to tell Exim that incoming recipient
addresses that match the format above should be delivered to
the mailbox identified by the portion before the first equal
(=) sign. For this purpose, you want to
insert a router early in the
section of your configuration file
- before any other routers pertaining to local deliveries
(such as a system alias router):
hashed_local:
debug_print = "R: hashed_local for $local_part@$domain"
driver = redirect
domains = +local_domains
local_part_suffix = =*
data = $local_part@$domain
Recipient addresses that contain a equal sign are rewritten
such that the portion of the local part that follows the
equal sign are stripped off. Then all routers are
processed again.
ACL Signature Check
The final part of this scheme is to tell Exim that mails
delivered to valid recipient addresses with this signature
should always be accepted, and that
other messages with a NULL envelope sender should be
rejected if the recipient has opted in to this scheme.
No greylisting should be done in either case.
The following snippet should be placed in , prior to any SPF checks,
greylisting, and/or the final
statement:
# 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}}
You will have an issue when sending mail to hosts that
perform callout verification on addresses in the message
header, such as the one provided in the
field of your outgoing mail. The
statement here will effectively give a
negative response to such a verification attempt.
For that reason, you may want to convert the last
statement into a
statement, store the rejection message in
$acl_m0, and perform the actual rejection
after the DATA command, in a fashion
similar to previously described:
# 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, store a reject
# message in $acl_m0, and a log message in $acl_m1. We will later
# use these to reject the mail. In the mean time, their presence
# indicate that we should keep stalling the sender.
#
warn
senders = : postmaster@*
domains = +local_domains
set acl_m9 = /home/${extract{1}{=}{${lc:$local_part}}}/.return-path-sign
condition = ${if exists {$acl_m9}{true}}
set acl_m0 = The recipient address <$local_part@$domain> does not \
match a valid, signed return path from here.\n\
You are responding to a forged sender address.
set acl_m1 = bogus bounce for <$local_part@$domain>.
Also, even if the recipient has chosen to use envelope sender
signatures in their outgoing mail, they may want to exempt
specific hosts from having to provide this signature in
incoming mail, even if the mail has no envelope sender
address. This may be required for specific mailing list
servers, see the discussion on
for details.
Accept Bounces Only for Real Users
As discussed in , there is now a
loophole that prevents us from catching bogus sent to system users and aliases, such as
. Here we cover two alternate ways
to ensure that bounces are only accepted for users that actually
send outgoing mail.
Check for Recipient Mailbox
The first method is performed in the ACL. Here, we check that the
recipient address corresponds to a local mailbox:
# Deny mail for 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.
#
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
!mailbox check
Unfortunately, how we perform the mailbox
check will depend on how you deliver your mail (as
before, we extract the portion before the first =
sign of the recipient address, to accomodate for Envelope Sender Signatures):
If mailboxes map to local user accounts on your server, we
can check that the recipient name maps to a user ID that
corresponds to regular users on your
system, e.g. in the range 500 - 60000:
set acl_m9 = ${extract{1}{=}{${lc:$local_part}}}
set acl_m9 = ${extract{2}{:}{${lookup passwd {$acl_m9}{$value}}}{0}}
condition = ${if and {{>={$acl_m9}{500}} {<${acl_m9}{60000}}} {true}}
If you deliver mail to the Cyrus IMAP
suite, you can use the provided mbpath
command-line utility to check that the mailbox exists.
You will want to make sure that the Exim user has
permission to check for mailboxes (for instance, you may
add it to the group:
# adduser exim4 cyrus).
set acl_m9 = ${extract{1}{=}{${lc:$local_part}}}
condition = ${run {/usr/sbin/mbpath -q -s user.$acl_m9} {true}}
If you forward all mail to a remote machine for delivery,
you may need to perform a
and let that machine decide whether to accept the mail.
You need to keep the original envelope sender intact in
the callout:
verify = recipient/callout=use_sender
Since in the case of locally delivered mail, this mailbox
check duplicates some of the logic that is performed in the
routers, and since it is specific to the mail delivery
mechanism on our site, it is perhaps a bit kludgy for the
perfectionists among us. So we will now provide an alternate
way.
Check for Empty Sender in Aliases Router
You probably have a router named
or similar, to redirect mail
for users such as and
. Typically, these aliases are
not used in the sender address of outgoing mail. As such, you
can ensure that incoming s are not routed
through it by adding the following condition to the router:
!senders = : postmaster@*
A sample aliases router may now look like this:
system_aliases:
driver = redirect
domains = +local_domains
!senders = : postmaster@*
allow_fail
allow_defer
data = ${lookup{$local_part}lsearch{/etc/aliases}}
user = mail
group = mail
file_transport = address_file
pipe_transport = address_pipe
Although we now block bounces to some
system aliases, other aliases were merely shadowing existing
system users (such as root,
daemon, etc). If you deliver local mail
through the driver, and use
to validate the recipient
address, you may now find yourself routing mail directly to
these system accounts.
To fix this problem, we now want to add an additional
condition in the router that handles your local mail
(e.g. local_user) to ensure that the
recipient not only exists, but is a regular
user. For instance, as above, we can check that the user ID
is in the range 500 - 60000:
condition = ${if and {{>={$local_user_uid}{500}}\
{<{$local_user_uid}{60000}}}\
{true}}
A sample router for local delivery may now look like this:
local_user:
driver = accept
domains = +local_domains
check_local_user
condition = ${if and {{>={$local_user_uid}{500}}\
{<{$local_user_uid}{60000}}}\
{true}}
transport = transport
Beware that if you implement this method, the reject response
from your server in response to bogus bounce mail for system
users will be the same as for unknown recipients (550
Unknown User in our case).
Exempting Forwarded Mail
After adding all these checks in the SMTP transaction, we may
find ourselves indirectly creating collateral spam as a result
of rejecting mails forwarded from trusted sources, such as
mailing lists and mail accounts on other sites (see the
discussion on for details). We
now need to whitelist these hosts in order to exempt them from
SMTP rejections -- at least those rejections that are caused by
our spam and/or virus filtering.
In this example, we will consult two files in response to each
RCPT TO: command:
A global whitelist in
, containing
backup MX hosts and other whitelisted senders
, and
A user-specific list in
,
specifying hosts from which that particuar user will receive
forwarded mail (e.g. mailing list servers, outgoing mail
servers for accounts elsewhere...)
If your mail users do not have local user accounts and home
directories, you may want to modify the file paths and/or lookup
mechanisms to something more suitable for your system
(e.g. database lookups or LDAP queries).
If the sender host is found in one of these whitelists, we save
the word accept in $acl_m0, and
clear the contents of $acl_m1, as described in
the previous section on . This will indicate that
we should not reject the mail in subsequent statements.
In the , we insert the
following statement after validating the recipient address, but
before any statements pertaining to
unauthenticated deliveries from remote hosts to local users
(i.e. before any greylist checks, envelope signature checks,
etc):
# 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 =
# 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 =
In various statements in the
ACL, we check the contents of $acl_m0 to avoid
rejecting the mail if this is set as per above. For instance,
to avoid rejecting mail from whitelisted hosts due to a missing
RFC2822 header:
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}}
The appropriate checks are embedded in the , next.
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 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.
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
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
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
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
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