From d4cafcd435dd868e8e3a898db3e7167143d9e100 Mon Sep 17 00:00:00 2001 From: Stuart Gathman Date: Wed, 8 Oct 2008 04:57:28 +0000 Subject: [PATCH] Greylisting --- Milter/greylist.py | 73 ++++++++++++++++++++++++++++++++++++++++++++++ TODO | 6 ++++ bms.py | 38 ++++++++++++++++++++++-- milter.cfg | 6 ++++ 4 files changed, 121 insertions(+), 2 deletions(-) create mode 100644 Milter/greylist.py diff --git a/Milter/greylist.py b/Milter/greylist.py new file mode 100644 index 0000000..6ff1d84 --- /dev/null +++ b/Milter/greylist.py @@ -0,0 +1,73 @@ +import time +import shelve +import thread +import logging +import urllib + +log = logging.getLogger('milter.greylist') + +def quoteAddress(s): + '''Quote an address so that it's safe to store in the file-system. + Address can either be a domain name, or local part. + Returns the quoted address.''' + + s = urllib.quote(s, '@_-+~!.%') + if s.startswith('.'): s = '%2e' + s[1:] + return s + +class Record(object): + __slots__ = ( 'firstseen', 'lastseen', 'umis', 'cnt' ) + + def __init__(self): + now = time.time() + self.firstseen = now + self.lastseen = now + self.cnt = 0 + self.umis = None + +class Greylist(object): + + def __init__(self,dbname,grey_time=10,grey_expire=4,grey_retain=36): + self.ignoreLastByte = False + self.greylist_time = grey_time * 60 # minutes + self.greylist_expire = grey_expire * 3600 # hours + self.greylist_retain = grey_retain * 24 * 3600 # days + self.dbp = shelve.open(dbname,'c',protocol=2) + self.lock = thread.allocate_lock() + + def check(self,ip,sender,recipient): + "Return number of allowed messages for greylist triple." + sender = quoteAddress(sender) + recipient = quoteAddress(recipient) + key = ip + ':' + sender + ':' + recipient + self.lock.acquire() + try: + dbp = self.dbp + try: + r = dbp[key] + now = time.time() + if now > r.lastseen + self.greylist_retain: + # expired + log.debug('Expired greylist: %s',key) + r = Record() + elif now < r.firstseen + self.greylist_time: + # still greylisted + log.debug('Reset greylist: %s',key) + r = Record() + elif r.cnt or now < r.firstseen + self.greylist_expire: + # in greylist window or active + r.lastseen = now + r.cnt += 1 + log.debug('Active greylist(%d): %s',r.cnt,key) + else: + # passed greylist window + log.debug('Late greylist: %s',key) + r = Record() + dbp[key] = r + except: + r = Record() + dbp[key] = r + dbp.sync() + finally: + self.lock.release() + return r.cnt diff --git a/TODO b/TODO index 44a1bec..ccfbf23 100644 --- a/TODO +++ b/TODO @@ -1,3 +1,9 @@ +The recent feature to let a REJECT policy for SPF None be overridden +by whitelisting is working for CSI and CMS. However, there could be +a sender that we want to REJECT even when whitelisted - because they +normally get a guessed PASS. Need another policy name - or else just +add them to local SPF so they won't ever get 'None'. + When policy is OK, do not use cbv_cache for blacklist. Add postmaster option or general rcpt list to dsn. Can send dsn to diff --git a/bms.py b/bms.py index 74c6e7a..e38330e 100644 --- a/bms.py +++ b/bms.py @@ -1,6 +1,10 @@ #!/usr/bin/env python # A simple milter that has grown quite a bit. # $Log$ +# Revision 1.130 2008/10/02 03:19:00 customdesigned +# Delay strike3 REJECT and don't reject if whitelisted. +# Recognize vacation messages as autoreplies. +# # Revision 1.129 2008/09/09 23:24:56 customdesigned # Never ban a trusted relay. # @@ -213,6 +217,7 @@ from Milter.dynip import is_dynip as dynip from Milter.utils import \ iniplist,parse_addr,parse_header,ip4re,addr2bin,parseaddr from Milter.config import MilterConfigParser +from Milter.greylist import Greylist from fnmatch import fnmatchcase from email.Utils import getaddresses @@ -326,6 +331,7 @@ supply_sender = False access_file = None timeout = 600 banned_ips = set() +greylist = None logging.basicConfig( stream=sys.stdout, @@ -499,6 +505,21 @@ def read_config(list): else: gossip_ttl = 1 + # greylist section + if cp.has_option('greylist','dbfile'): + global greylist + grey_db = cp.getdefault('greylist','dbfile') + grey_days = 36 + if cp.has_option('greylist','retain'): + grey_days = cp.getint('greylist','retain') + grey_expire = 4 + if cp.has_option('greylist','expire'): + grey_expire = cp.getint('greylist','expire') + grey_time = 10 + if cp.has_option('greylist','time'): + grey_time = cp.getint('greylist','expire') + greylist = Greylist(grey_db,grey_time,grey_expire,grey_days) + def findsrs(fp): lastln = None for ln in fp: @@ -765,6 +786,7 @@ class bmsMilter(Milter.Milter): self.dspam = True self.whitelist = False self.blacklist = False + self.greylist = False self.reject_spam = True self.data_allowed = True self.delayed_failure = None @@ -873,6 +895,7 @@ class bmsMilter(Milter.Milter): if rc != Milter.CONTINUE: if rc != Milter.TEMPFAIL: self.offense() return rc + self.greylist = True else: rc = Milter.CONTINUE # FIXME: parse Received-SPF from trusted_relay for SPF result @@ -880,6 +903,7 @@ class bmsMilter(Milter.Milter): hres = self.spf and self.spf_helo # Check whitelist and blacklist if auto_whitelist.has_key(self.canon_from): + self.greylist = False if res == 'pass' or self.trusted_relay: self.whitelist = True self.log("WHITELIST",self.canon_from) @@ -1197,8 +1221,18 @@ class bmsMilter(Milter.Milter): except: self.log("rcpt to",to,str) raise - self.log("rcpt to",to,str) + if self.greylist and greylist: + # no policy for trusted or internal + rc = greylist.check(self.connectip,self.canon_from,canon_to) + if rc == 0: + self.log("GREYLIST:",self.connectip,self.canon_from,canon_to) + self.setreply('451','4.7.1', + 'Greylisted: http://projects.puremagic.com/greylisting/', + 'Please retry in %.1f minutes'%(greylist.greylist_time/60.0)) + return Milter.TEMPFAIL + self.log("GREYLISTED: %d"%rc) + self.log("rcpt to",to,str) self.smart_alias(to) # get recipient after virtusertable aliasing #rcpt = self.getsymval("{rcpt_addr}") @@ -1876,7 +1910,7 @@ class bmsMilter(Milter.Milter): self.log('CBV:',sender,'Using:',fname) except IOError: pass if not m: - self.log('CBV:',sender,'PLAIN') + self.log('CBV:',sender,'PLAIN (%s)'%q.result) else: if srs: # Add SRS coded sender to various headers. When (incorrectly) diff --git a/milter.cfg b/milter.cfg index 0c8ead2..f6c9213 100644 --- a/milter.cfg +++ b/milter.cfg @@ -227,3 +227,9 @@ blind = 1 # domains. Peer reputation is also tracked as to how often they # agree with us, and weighted accordingly. ;peers=host1:port,host2 + +[greylist] +dbfile=greylist.db +grey_time=10 # mins +grey_expire=4 # hours +grey_retain=36 # days