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 sections Debian 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 delays
The 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 variables Variables: $acl_[cm]0 unset $acl_[cm]0 set $acl_[cm]1 unset (No decision yet) Accept the mail $acl_[cm]1 set Add warning in header Reject 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