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.
|
||||
|
||||
Add postmaster option or general rcpt list to dsn. Can send dsn to
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user