Greylisting

This commit is contained in:
Stuart Gathman
2008-10-08 04:57:28 +00:00
parent d64aad95c1
commit d4cafcd435
4 changed files with 121 additions and 2 deletions
+73
View File
@@ -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
+6
View File
@@ -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. When policy is OK, do not use cbv_cache for blacklist.
Add postmaster option or general rcpt list to dsn. Can send dsn to Add postmaster option or general rcpt list to dsn. Can send dsn to
+36 -2
View File
@@ -1,6 +1,10 @@
#!/usr/bin/env python #!/usr/bin/env python
# A simple milter that has grown quite a bit. # A simple milter that has grown quite a bit.
# $Log$ # $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 # Revision 1.129 2008/09/09 23:24:56 customdesigned
# Never ban a trusted relay. # Never ban a trusted relay.
# #
@@ -213,6 +217,7 @@ from Milter.dynip import is_dynip as dynip
from Milter.utils import \ from Milter.utils import \
iniplist,parse_addr,parse_header,ip4re,addr2bin,parseaddr iniplist,parse_addr,parse_header,ip4re,addr2bin,parseaddr
from Milter.config import MilterConfigParser from Milter.config import MilterConfigParser
from Milter.greylist import Greylist
from fnmatch import fnmatchcase from fnmatch import fnmatchcase
from email.Utils import getaddresses from email.Utils import getaddresses
@@ -326,6 +331,7 @@ supply_sender = False
access_file = None access_file = None
timeout = 600 timeout = 600
banned_ips = set() banned_ips = set()
greylist = None
logging.basicConfig( logging.basicConfig(
stream=sys.stdout, stream=sys.stdout,
@@ -499,6 +505,21 @@ def read_config(list):
else: else:
gossip_ttl = 1 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): def findsrs(fp):
lastln = None lastln = None
for ln in fp: for ln in fp:
@@ -765,6 +786,7 @@ class bmsMilter(Milter.Milter):
self.dspam = True self.dspam = True
self.whitelist = False self.whitelist = False
self.blacklist = False self.blacklist = False
self.greylist = False
self.reject_spam = True self.reject_spam = True
self.data_allowed = True self.data_allowed = True
self.delayed_failure = None self.delayed_failure = None
@@ -873,6 +895,7 @@ class bmsMilter(Milter.Milter):
if rc != Milter.CONTINUE: if rc != Milter.CONTINUE:
if rc != Milter.TEMPFAIL: self.offense() if rc != Milter.TEMPFAIL: self.offense()
return rc return rc
self.greylist = True
else: else:
rc = Milter.CONTINUE rc = Milter.CONTINUE
# FIXME: parse Received-SPF from trusted_relay for SPF result # 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 hres = self.spf and self.spf_helo
# Check whitelist and blacklist # Check whitelist and blacklist
if auto_whitelist.has_key(self.canon_from): if auto_whitelist.has_key(self.canon_from):
self.greylist = False
if res == 'pass' or self.trusted_relay: if res == 'pass' or self.trusted_relay:
self.whitelist = True self.whitelist = True
self.log("WHITELIST",self.canon_from) self.log("WHITELIST",self.canon_from)
@@ -1197,8 +1221,18 @@ class bmsMilter(Milter.Milter):
except: except:
self.log("rcpt to",to,str) self.log("rcpt to",to,str)
raise 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) self.smart_alias(to)
# get recipient after virtusertable aliasing # get recipient after virtusertable aliasing
#rcpt = self.getsymval("{rcpt_addr}") #rcpt = self.getsymval("{rcpt_addr}")
@@ -1876,7 +1910,7 @@ class bmsMilter(Milter.Milter):
self.log('CBV:',sender,'Using:',fname) self.log('CBV:',sender,'Using:',fname)
except IOError: pass except IOError: pass
if not m: if not m:
self.log('CBV:',sender,'PLAIN') self.log('CBV:',sender,'PLAIN (%s)'%q.result)
else: else:
if srs: if srs:
# Add SRS coded sender to various headers. When (incorrectly) # Add SRS coded sender to various headers. When (incorrectly)
+6
View File
@@ -227,3 +227,9 @@ blind = 1
# domains. Peer reputation is also tracked as to how often they # domains. Peer reputation is also tracked as to how often they
# agree with us, and weighted accordingly. # agree with us, and weighted accordingly.
;peers=host1:port,host2 ;peers=host1:port,host2
[greylist]
dbfile=greylist.db
grey_time=10 # mins
grey_expire=4 # hours
grey_retain=36 # days