b707c4f
#
b707c4f
# Exim ACL for greylisting. David Woodhouse <dwmw2@infradead.org>
b707c4f
#
b707c4f
# For full background on the logic behind greylisting and how this
b707c4f
# ACL works, see https://github.com/Exim/exim/wiki/SimpleGreylisting
b707c4f
#
b707c4f
b707c4f
# UPDATING TO EXIM 4.94+
b707c4f
# ======================
b707c4f
#
b707c4f
# Previous versions of this ACL specified the sqlite database filename 
b707c4f
# in the sqlite lookup strings directly, but since Exim 4.94 is it no 
b707c4f
# longer permitted to mix "tainted" text which comes from the message 
b707c4f
# itself, with the filename. Thus, you now have to set
b707c4f
#
b707c4f
# sqlite_dbfile = /var/spool/exim/db/greylist.db
b707c4f
#
b707c4f
# ... in the main configuration because it can't be specified within
b707c4f
# the ACL in this file any more.
dcfda48
b707c4f
# USING THIS ACL
b707c4f
# ==============
b707c4f
#
b707c4f
# First set sqlite_dbfile in the main configuration file to point to
b707c4f
# the greylist sqlite database, as described above.
b707c4f
#
b707c4f
# In your main ACLs, gather reason(s) for greylisting into a variable 
b707c4f
# named $acl_m_greylistreasons before invoking this ACL with
b707c4f
# 'require acl = greylist_mail'. The reasons should be separate lines
b707c4f
# of text, and will be reported in the SMTP rejection message as well
b707c4f
# as the log message. Anything "suspicious" about the email can be
b707c4f
# used as criteria here — being HTML, having even a few SpamAssassin
b707c4f
# points, even lacking SPF authorisation (which is OK for greylisting
b707c4f
# although you should never reject outright for an SPF "failure"
b707c4f
# because of the flaws in SPF).
b707c4f
#
b707c4f
# Obviously you need to .include this file too in order to be able
b707c4f
# to invoke this greylist_mail ACL.
dcfda48
b707c4f
# HOW IT WORKS
b707c4f
# ============
dcfda48
#
dcfda48
# When a suspicious mail is seen, we temporarily reject it and wait to see
dcfda48
# if the sender tries again. Most spam robots won't bother. Real mail hosts
dcfda48
# _will_ retry, and we'll accept it the second time. For hosts which are
dcfda48
# observed to retry, we don't bother greylisting again in the future --
dcfda48
# it's obviously pointless. We remember such hosts, or 'known resenders',
dcfda48
# by a tuple of their IP address and the name they used in HELO.
dcfda48
#
dcfda48
# We also include the time of listing for 'known resenders', just in case
dcfda48
# someone wants to expire them after a certain amount of time. So the 
dcfda48
# database table for these 'known resenders' looks like this:
dcfda48
#
dcfda48
# CREATE TABLE resenders (
ba91edb
#        host            TEXT,
dcfda48
#        helo            TEXT,
ba91edb
#        time            INTEGER,
ba91edb
#    PRIMARY KEY (host, helo) );
dcfda48
#
dcfda48
# To remember mail we've rejected, we create an 'identity' from its sender
dcfda48
# and recipient addresses and its Message-ID: header. We don't include the
dcfda48
# sending IP address in the identity, because sometimes the second and 
dcfda48
# subsequent attempts may come from a different IP address to the original.
dcfda48
#
dcfda48
# We do record the original IP address and HELO name though, because if
dcfda48
# the message _is_ retried from another machine, it's the _first_ one we
dcfda48
# want to record as a 'known resender'; not just its backup path.
dcfda48
#
dcfda48
# Obviously we record the time too, so the main table of greylisted mail
dcfda48
# looks like this:
dcfda48
#
dcfda48
# CREATE TABLE greylist (
ba91edb
#        id              TEXT,
dcfda48
#        expire          INTEGER,
dcfda48
#        host            TEXT,
ba91edb
#        helo            TEXT);
dcfda48
#
dcfda48
dcfda48
greylist_mail:
b707c4f
  # Firstly,  accept if it was generated locally or by authenticated clients.
dcfda48
  accept hosts = :
dcfda48
  accept authenticated = *
dcfda48
dcfda48
  # Secondly, there's _absolutely_ no point in greylisting mail from
dcfda48
  # hosts which are known to resend their mail. Just accept it.
b707c4f
  accept condition = ${lookup sqlite {SELECT host from resenders \
dcfda48
			       WHERE helo='${quote_sqlite:$sender_helo_name}' \
e65f1fc
			       AND host='$sender_host_address';} {1}}
dcfda48
04f7f89
  # Generate a hashed 'identity' for the mail, as described above.
dcfda48
  warn set acl_m_greyident = ${hash{20}{62}{$sender_address$recipients$h_message-id:}}
dcfda48
dcfda48
  # Attempt to look up this mail in the greylist database. If it's there,
dcfda48
  # remember the expiry time for it; we need to make sure they've waited
dcfda48
  # long enough.
b707c4f
  warn set acl_m_greyexpiry = ${lookup sqlite {SELECT expire FROM greylist \
dcfda48
				WHERE id='${quote_sqlite:$acl_m_greyident}';}{$value}}
dcfda48
b707c4f
b707c4f
  # If there's absolutely nothing suspicious about the email, accept it. BUT...
b707c4f
  accept condition = ${if eq {$acl_m_greylistreasons}{} {1}}
b707c4f
         condition = ${if eq {$acl_m_greyexpiry}{} {1}}
b707c4f
b707c4f
  # ..if this same mail was greylisted before (perhaps because it came from a
b707c4f
  # host which *was* suspicious), then we still want to mark that original host
b707c4f
  # as a "known resender". If we don't, then hosts which attempt to deliver from
b707c4f
  # a dodgy Legacy IP address but then fall back to using IPv6 after greylisting
b707c4f
  # will *never* see their Legacy IP address added to the 'known resenders' list.
b707c4f
  accept condition = ${if eq {$acl_m_greylistreasons}{} {1}}
b707c4f
         acl = write_known_resenders
b707c4f
04f7f89
  # If the mail isn't already the database -- i.e. if the $acl_m_greyexpiry
04f7f89
  # variable we just looked up is empty -- then try to add it now. This is 
04f7f89
  # where the 5 minute timeout is set ($tod_epoch + 300), should you wish
04f7f89
  # to change it.
04f7f89
  warn  condition = ${if eq {$acl_m_greyexpiry}{} {1}}
b707c4f
	set acl_m_dontcare = ${lookup sqlite {INSERT INTO greylist \
dcfda48
					VALUES ( '$acl_m_greyident', \
dfa8e8d
						 '${eval10:$tod_epoch+300}', \
dcfda48
						 '$sender_host_address', \
96ea9a4
						 '${quote_sqlite:$sender_helo_name}' );}}
04f7f89
04f7f89
  # Be paranoid, and check if the insertion succeeded (by doing another lookup).
04f7f89
  # Otherwise, if there's a database error we might end up deferring for ever.
04f7f89
  defer condition = ${if eq {$acl_m_greyexpiry}{} {1}}
b707c4f
        condition = ${lookup sqlite {SELECT expire FROM greylist \
04f7f89
				WHERE id='${quote_sqlite:$acl_m_greyident}';} {1}}
dcfda48
        message = Your mail was considered suspicious for the following reason(s):\n$acl_m_greylistreasons \
dcfda48
		  The mail has been greylisted for 5 minutes, after which it should be accepted. \
dcfda48
		  We apologise for the inconvenience. Your mail system should keep the mail on \
dcfda48
		  its queue and retry. When that happens, your system will be added to the list \
dcfda48
		  genuine mail systems, and mail from it should not be greylisted any more. \
dcfda48
		  In the event of problems, please contact postmaster@$qualify_domain
04f7f89
	log_message = Greylisted <$h_message-id:> from <$sender_address> for offences: ${sg {$acl_m_greylistreasons}{\n}{,}}
04f7f89
04f7f89
  # Handle the error case (which should never happen, but would be bad if it did).
04f7f89
  # First by whining about it in the logs, so the admin can deal with it...
04f7f89
  warn   condition = ${if eq {$acl_m_greyexpiry}{} {1}}
04f7f89
         log_message = Greylist insertion failed. Bypassing greylist.
04f7f89
  # ... and then by just accepting the message.
04f7f89
  accept condition = ${if eq {$acl_m_greyexpiry}{} {1}}
04f7f89
04f7f89
  # OK, we've dealt with the "new" messages. Now we deal with messages which
04f7f89
  # _were_ already in the database...
dcfda48
dcfda48
  # If the message was already listed but its time hasn't yet expired, keep rejecting it
dcfda48
  defer condition = ${if > {$acl_m_greyexpiry}{$tod_epoch}}
dcfda48
	message = Your mail was previously greylisted and the time has not yet expired.\n\
dcfda48
		  You should wait another ${eval10:$acl_m_greyexpiry-$tod_epoch} seconds.\n\
dcfda48
		  Reason(s) for greylisting: \n$acl_m_greylistreasons
dcfda48
b707c4f
  accept acl = write_known_resenders
b707c4f
b707c4f
write_known_resenders:
dcfda48
  # The message was listed but it's been more than five minutes. Accept it now and whitelist
04f7f89
  # the _original_ sending host by its { IP, HELO } so that we don't delay its mail again.
b707c4f
  warn set acl_m_orighost = ${lookup sqlite {SELECT host FROM greylist \
04f7f89
				WHERE id='${quote_sqlite:$acl_m_greyident}';}{$value}}
b707c4f
       set acl_m_orighelo = ${lookup sqlite {SELECT helo FROM greylist \
04f7f89
				WHERE id='${quote_sqlite:$acl_m_greyident}';}{$value}}
b707c4f
       set acl_m_dontcare = ${lookup sqlite {INSERT INTO resenders \
04f7f89
				VALUES ( '$acl_m_orighost', \
04f7f89
					 '${quote_sqlite:$acl_m_orighelo}', \
04f7f89
					 '$tod_epoch' ); }}
04f7f89
       logwrite = Added host $acl_m_orighost with HELO '$acl_m_orighelo' to known resenders
dcfda48
04f7f89
  accept