Greylisting
This commit is contained in:
@@ -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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user