bley-0.1.5/0000755000175000017500000000000011572425057011774 5ustar evgenievgenibley-0.1.5/bley.py0000644000175000017500000003233111572424723013302 0ustar evgenievgeni# Copyright (c) 2009 Evgeni Golov # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions # are met: # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # 3. Neither the name of the University nor the names of its contributors # may be used to endorse or promote products derived from this software # without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF # SUCH DAMAGE. from twisted.internet.protocol import Factory from twisted.names import client from twisted.internet import defer from twisted.internet import reactor import datetime from bleyhelpers import * from postfix import PostfixPolicy from time import sleep class BleyPolicy(PostfixPolicy): '''Implementation of intelligent greylisting based on `PostfixPolicy`''' db = None dbc = None @defer.inlineCallbacks def check_policy (self): '''Check the incoming mail based on our policy and tell Postfix about our decision. The policy works as follows: 1. Accept if recipient=(postmaster|abuse) 2. Check local DB for an existing entry 3. When not found, check 1. DNSWLs (accept if found) 2. DNSBLs (reject if found) 3. HELO/dyn_host/sender_eq_recipient (reject if over threshold) 4. SPF (reject if over threshold) 5. Accept if not yet rejected 4. When found 1. Whitelisted: accept 2. Greylisted and waited: accept 3. Greylisted and not waited: reject @type postfix_params: dict @param postfix_params: parameters we got from Postfix ''' if not self.db: self.db = self.factory.settings.db try: self.dbc = self.db.cursor() except: self.safe_reconnect() check_results = {'DNSWL': 0, 'DNSBL': 0, 'HELO': 0, 'DYN': 0, 'DB': -1, 'SPF': 0, 'S_EQ_R': 0 } action = 'DUNNO' postfix_params = self.params # Strip everything after a + in the localpart, usefull for mailinglists etc if postfix_params['sender'].find('+') != -1: postfix_params['sender'] = postfix_params['sender'][:postfix_params['sender'].find('+')]+postfix_params['sender'][postfix_params['sender'].find('@'):] if postfix_params['recipient'].find('+') != -1: postfix_params['recipient'] = postfix_params['recipient'][:postfix_params['recipient'].find('+')]+postfix_params['recipient'][postfix_params['recipient'].find('@'):] if postfix_params['client_address'] in self.factory.bad_cache.keys(): delta = datetime.datetime.now()-self.factory.bad_cache[postfix_params['client_address']] if delta < datetime.timedelta(0,60,0): action = 'DEFER_IF_PERMIT %s (cached result)' % self.factory.settings.reject_msg if self.factory.settings.verbose: self.factory.settings.logger('decided CACHED action=%s, checks: %s, postfix: %s\n' % (action, check_results, postfix_params)) else: self.factory.settings.logger('decided CACHED action=%s, from=%s, to=%s\n' % (action, postfix_params['sender'], postfix_params['recipient'])) self.send_action(action) return else: del self.factory.bad_cache[postfix_params['client_address']] if postfix_params['client_address'] in self.factory.good_cache.keys(): delta = datetime.datetime.now()-self.factory.good_cache[postfix_params['client_address']] if delta < datetime.timedelta(0,60,0): action = 'DUNNO' if self.factory.settings.verbose: self.factory.settings.logger('decided CACHED action=%s, checks: %s, postfix: %s\n' % (action, check_results, postfix_params)) else: self.factory.settings.logger('decided CACHED action=%s, from=%s, to=%s\n' % (action, postfix_params['sender'], postfix_params['recipient'])) self.send_action(action) return else: del self.factory.good_cache[postfix_params['client_address']] status = self.check_local_db(postfix_params) # -1 : not found # 0 : regular host, not in black, not in white, let it go # 1 : regular host, but in white, let it go, dont check EHLO # 2 : regular host, but in black, lets grey for now if postfix_params['recipient'].lower().startswith('postmaster'): action = 'DUNNO' elif status == -1: # not found in local db... check_results['DNSWL'] = yield self.check_dnswls(postfix_params['client_address'], self.factory.settings.dnswl_threshold) if check_results['DNSWL'] >= self.factory.settings.dnswl_threshold: new_status = 1 else: check_results['DNSBL'] = yield self.check_dnsbls(postfix_params['client_address'], self.factory.settings.dnsbl_threshold) check_results['HELO'] = check_helo(postfix_params) check_results['DYN'] = check_dyn_host(postfix_params['client_name']) # check_sender_eq_recipient: if postfix_params['sender']==postfix_params['recipient']: check_results['S_EQ_R'] = 1 if check_results['DNSBL'] < self.factory.settings.dnsbl_threshold and check_results['HELO']+check_results['DYN']+check_results['S_EQ_R'] < self.factory.settings.rfc_threshold: check_results['SPF'] = check_spf(postfix_params) else: check_results['SPF'] = 0 if check_results['DNSBL'] >= self.factory.settings.dnsbl_threshold or check_results['HELO']+check_results['DYN']+check_results['SPF']+check_results['S_EQ_R'] >= self.factory.settings.rfc_threshold: new_status = 2 action = 'DEFER_IF_PERMIT %s' % self.factory.settings.reject_msg self.factory.bad_cache[postfix_params['client_address']] = datetime.datetime.now() else: new_status = 0 self.factory.good_cache[postfix_params['client_address']] = datetime.datetime.now() query = "INSERT INTO bley_status (ip, status, last_action, sender, recipient) VALUES(%(client_address)s, %(new_status)s, NOW(), %(sender)s, %(recipient)s)" postfix_params['new_status'] = new_status try: self.safe_execute(query, postfix_params) except: # the other thread already commited while we checked, ignore pass elif status[0] >= 2: # found to be greyed check_results['DB'] = status[0] delta = datetime.datetime.now()-status[1] if delta > self.factory.settings.greylist_period+status[2]*self.factory.settings.greylist_penalty or delta > self.factory.settings.greylist_max: action = 'DUNNO' query = "UPDATE bley_status SET status=0, last_action=NOW() WHERE ip=%(client_address)s AND sender=%(sender)s AND recipient=%(recipient)s" self.factory.good_cache[postfix_params['client_address']] = datetime.datetime.now() else: action = 'DEFER_IF_PERMIT %s' % self.factory.settings.reject_msg query = "UPDATE bley_status SET fail_count=fail_count+1 WHERE ip=%(client_address)s AND sender=%(sender)s AND recipient=%(recipient)s" self.factory.bad_cache[postfix_params['client_address']] = datetime.datetime.now() self.safe_execute(query, postfix_params) else: # found to be clean check_results['DB'] = status[0] action = 'DUNNO' query = "UPDATE bley_status SET last_action=NOW() WHERE ip=%(client_address)s AND sender=%(sender)s AND recipient=%(recipient)s" self.safe_execute(query, postfix_params) self.factory.good_cache[postfix_params['client_address']] = datetime.datetime.now() if self.factory.settings.verbose: self.factory.settings.logger('decided action=%s, checks: %s, postfix: %s\n' % (action, check_results, postfix_params)) else: self.factory.settings.logger('decided action=%s, from=%s, to=%s\n' % (action, postfix_params['sender'], postfix_params['recipient'])) self.send_action(action) def check_local_db(self, postfix_params): '''Check the sender for being in the local database. Queries the local SQL database for the (ip,sender,recipient) tuple. @type postfix_params: dict @param postfix_params: parameters we got from Postfix @rtype: list @return: the result from SQL if any ''' query = """SELECT status,last_action,fail_count,sender,recipient FROM bley_status WHERE ip=%(client_address)s AND sender=%(sender)s AND recipient=%(recipient)s ORDER BY status ASC LIMIT 1""" try: self.safe_execute(query, postfix_params) result = self.dbc.fetchone() except: result = None self.factory.settings.logger('check_local_db failed. sending unknown.\n') if not result: return -1 else: return result @defer.inlineCallbacks def check_dnswls(self, ip, max): '''Check the IP address in DNSWLs. @type ip: string @param ip: the IP to check @type max: int @param max: stop after max hits @rtype: int @return: in how many DNSWLs did we find ip? ''' result = 0 for l in self.factory.settings.dnswls: try: d = yield self.check_dnsl(l, ip) result += 1 except Exception: pass if result >= max: break defer.returnValue(result) @defer.inlineCallbacks def check_dnsbls(self, ip, max): '''Check the IP address in DNSBLs. @type ip: string @param ip: the IP to check @type max: int @param max: stop after max hits @rtype: int @return: in how many DNSBLs did we find ip? ''' result = 0 for l in self.factory.settings.dnsbls: try: d = yield self.check_dnsl(l, ip) result += 1 except Exception: pass if result >= max: break defer.returnValue(result) def check_dnsl(self, lst, ip): '''Check the IP address in a DNS list. @type ip: string @param ip: the IP to check @type lst: sting @param lst: the DNS list to check in @rtype: C{Deferred} @return: twisted.names.client resolver ''' rip = reverse_ip(ip) lookup = '%s.%s' % (rip, lst) d = client.lookupAddress(lookup) return d def safe_execute(self, query, params=None): try: self.dbc.execute(query, params) self.db.commit() except self.factory.settings.database.OperationalError: self.safe_reconnect() if self.db: self.dbc.execute(query, params) self.db.commit() else: self.factory.settings.logger('Could not reconnect to the database, exiting.\n') reactor.stop() def safe_reconnect(self): self.factory.settings.logger('Reconnecting to the database after an error.\n') try: self.db.close() except: pass self.db = None retries = 0 while not self.db and retries < 30: try: self.factory.settings.db = self.db = self.factory.settings.database.connect(**self.factory.settings.dbsettings) self.dbc = self.db.cursor() except self.factory.settings.database.OperationalError: self.db = None retries += 1 sleep(1) class BleyPolicyFactory(Factory): protocol = BleyPolicy def __init__(self, settings): self.settings = settings self.good_cache = {} self.bad_cache = {} bley-0.1.5/MANIFEST.in0000644000175000017500000000003611572424760013531 0ustar evgenievgeniinclude *.conf include bley.1 bley-0.1.5/bley0000755000175000017500000002000711572424723012653 0ustar evgenievgeni#!/usr/bin/env python # # Copyright (c) 2009 Evgeni Golov # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions # are met: # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # 3. Neither the name of the University nor the names of its contributors # may be used to endorse or promote products derived from this software # without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF # SUCH DAMAGE. import os import sys import datetime from optparse import OptionParser from ConfigParser import SafeConfigParser # Find the best reactor reactorchoices = ["epollreactor", "pollreactor", "selectreactor", "posixbase"] for choice in reactorchoices: try: exec("from twisted.internet import %s as bestreactor" % choice) break except: pass try: bestreactor.install() except: print "Unable to find a reactor. Exiting..." sys.exit(1) from twisted.internet import reactor try: from twisted.scripts import _twistd_unix as twistd except: from twisted.scripts import twistd from bley import BleyPolicyFactory __CREATE_DB_QUERY = ''' CREATE TABLE IF NOT EXISTS bley_status ( ip VARCHAR(39) NOT NULL, status SMALLINT NOT NULL DEFAULT 1, last_action TIMESTAMP NOT NULL default CURRENT_TIMESTAMP, sender VARCHAR(254), recipient VARCHAR(254), fail_count INT DEFAULT 0 ); ''' __UPDATE_DB_QUERY = ''' ALTER TABLE bley_status CHANGE last_action last_action TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP; ''' __CREATE_DB_QUERY_PG = ''' CREATE TABLE bley_status ( ip VARCHAR(39) NOT NULL, status SMALLINT NOT NULL DEFAULT 1, last_action TIMESTAMP NOT NULL, sender VARCHAR(254), recipient VARCHAR(254), fail_count INT DEFAULT 0 ); ''' __CHECK_DB_QUERY_PG = ''' SELECT tablename FROM pg_catalog.pg_tables WHERE tablename = 'bley_status' ''' __CLEAN_DB_QUERY = ''' DELETE FROM bley_status WHERE last_action<%(old)s OR (last_action<%(old_bad)s AND status>=2) ''' def bley_start(): parser = OptionParser(version='0.1.5') parser.add_option("-p", "--pidfile", dest="pid_file", help="use PID_FILE for storing the PID") parser.add_option("-c", "--config", dest="conffile", help="load configuration from CONFFILE") parser.add_option("-v", "--verbose", action="store_true", dest="verbose", help="use verbose output") parser.add_option("-d", "--debug", action="store_true", dest="debug", help="don't daemonize the process and log to stdout") (settings, args) = parser.parse_args() if not settings.conffile: settings.conffile = '/etc/bley/bley.conf' defaults = { 'listen_addr': '127.0.0.1', 'log_file': None, 'pid_file': None, } config = SafeConfigParser(defaults) config.read(settings.conffile) settings.listen_addr = config.get('bley', 'listen_addr') settings.listen_port = config.getint('bley', 'listen_port') settings.pid_file = settings.pid_file or config.get('bley', 'pid_file') settings.log_file = config.get('bley', 'log_file') settings.dbtype = config.get('bley', 'dbtype') if settings.dbtype == 'pgsql': database = 'psycopg2' settings.dbsettings = {'host': config.get('bley', 'dbhost'), 'database': config.get('bley', 'dbname'), 'user': config.get('bley', 'dbuser'), 'password': config.get('bley', 'dbpass')} elif settings.dbtype == 'mysql': database = 'MySQLdb' settings.dbsettings = {'host': config.get('bley', 'dbhost'), 'db': config.get('bley', 'dbname'), 'user': config.get('bley', 'dbuser'), 'passwd': config.get('bley', 'dbpass')} else: print "No supported database configured." sys.exit(1) exec("import %s as database" % database) settings.database = database settings.reject_msg = config.get('bley', 'reject_msg') settings.dnswls = [d.strip() for d in config.get('bley', 'dnswls').split(',')] settings.dnsbls = [d.strip() for d in config.get('bley', 'dnsbls').split(',')] settings.dnswl_threshold = config.getint('bley', 'dnswl_threshold') settings.dnsbl_threshold = config.getint('bley', 'dnsbl_threshold') settings.rfc_threshold = config.getint('bley', 'rfc_threshold') settings.greylist_period = datetime.timedelta(0, config.getint('bley', 'greylist_period')*60, 0) settings.greylist_max = datetime.timedelta(0, config.getint('bley', 'greylist_max')*60, 0) settings.greylist_penalty = datetime.timedelta(0, config.getint('bley', 'greylist_penalty')*60, 0) settings.purge_days = config.getint('bley', 'purge_days') settings.purge_bad_days = config.getint('bley', 'purge_bad_days') if settings.debug: settings.log_file = None if settings.log_file == 'syslog': import syslog syslog.openlog('bley', syslog.LOG_PID, syslog.LOG_MAIL) settings.logger = syslog.syslog elif settings.log_file in ['-', '', None]: settings.logger = sys.stdout.write else: settings.logger = open(settings.log_file, 'a').write db = settings.database.connect(**settings.dbsettings) dbc = db.cursor() if settings.dbtype == 'pgsql': dbc.execute(__CHECK_DB_QUERY_PG) if not dbc.fetchall(): dbc.execute(__CREATE_DB_QUERY_PG) else: dbc.execute(__CREATE_DB_QUERY) dbc.execute(__UPDATE_DB_QUERY) db.commit() dbc.close() db.close() settings.db = settings.database.connect(**settings.dbsettings) reactor.listenTCP(settings.listen_port, BleyPolicyFactory(settings), interface=settings.listen_addr) reactor.addSystemEventTrigger('before', 'shutdown', bley_stop, settings) reactor.callWhenRunning(clean_db, settings) if not settings.debug: twistd.checkPID(settings.pid_file) twistd.daemonize() if settings.pid_file: writePID(settings.pid_file) reactor.run() def bley_stop(settings): if settings.pid_file and not settings.debug: delPID(settings.pid_file) if settings.log_file == 'syslog': syslog.closelog() def writePID(pidfile): # Create a PID file pid = str(os.getpid()) pf = open(pidfile, "w") pf.write("%s\n" % pid) pf.close() def delPID(pidfile): if os.path.exists(pidfile): os.remove(pidfile) def clean_db(settings): if settings.verbose: settings.logger("cleaning database\n") db = settings.database.connect(**settings.dbsettings) dbc = db.cursor() now = datetime.datetime.now() old = now - datetime.timedelta(settings.purge_days, 0, 0) old_bad = now - datetime.timedelta(settings.purge_bad_days, 0, 0) p = {'old': str(old), 'old_bad': str(old_bad)} dbc.execute(__CLEAN_DB_QUERY, p) db.commit() dbc.close() db.close() reactor.callLater(30*60, clean_db, settings) bley_start() bley-0.1.5/epydoc.conf0000644000175000017500000000102511361113442014110 0ustar evgenievgeni[epydoc] # Epydoc section marker (required by ConfigParser) # Information about the project. name: bley url: http://bley.mx # The list of modules to document. Modules can be named using # dotted names, module filenames, or package directory names. # This option may be repeated. modules: bleyhelpers.py, bley.py, postfix.py # Write html output to the directory "apidocs" output: html target: docs/ # Include all automatically generated graphs. These graphs are # generated using Graphviz dot. graph: all dotpath: /usr/bin/dot bley-0.1.5/setup.cfg0000644000175000017500000000007311572425057013615 0ustar evgenievgeni[egg_info] tag_build = tag_date = 0 tag_svn_revision = 0 bley-0.1.5/postfix.py0000644000175000017500000000573311572424723014051 0ustar evgenievgeni# Copyright (c) 2009 Evgeni Golov # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions # are met: # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # 3. Neither the name of the University nor the names of its contributors # may be used to endorse or promote products derived from this software # without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF # SUCH DAMAGE. from twisted.protocols.basic import LineOnlyReceiver from twisted.internet.protocol import Factory class PostfixPolicy(LineOnlyReceiver): '''Basic implementation of a Postfix policy service.''' def __init__(self): self.params = {} self.delimiter = '\n' def lineReceived(self, line): '''Parse stuff from Postfix and call check_policy() afterwards.''' line = line.strip().lower() if line == '': if len(self.params) > 0: self.check_policy() self.params = {} else: try: (pkey, pval) = line.split('=', 1) try: pval = pval.decode('utf-8', 'ignore').encode('us-ascii', 'ignore') except: pass self.params[pkey] = pval except: print 'Could not parse "%s"' % line def check_policy (self): '''Check the incoming mail based on our policy and tell Postfix about our decision. You probably want to override this one with a function that does something useful. ''' self.send_action('DUNNO') def send_action (self, action='DUNNO'): '''Send action back to Postfix. @type action: string @param action: the action to be sent to Postfix (default: 'DUNNO') ''' self.sendLine('action=%s\n' % action) class PostfixPolicyFactory(Factory): protocol = PostfixPolicy bley-0.1.5/setup.py0000644000175000017500000000077211572424723013513 0ustar evgenievgenifrom setuptools import setup setup( name = "bley", version = "0.1.5", description = "intelligent greylisting daemon for postfix", author = "Evgeni Golov", author_email = "sargentd@die-welt.net", url = "http://bley.mx", license = "BSD", py_modules = ['bley', 'bleyhelpers', 'postfix'], scripts = ['bley'], zip_safe = False, install_requires=['Twisted-Core>=8.1.0', 'pyspf'], data_files = [('/etc/bley', ['bley.conf']), ('/usr/share/man/man1', ['bley.1'])] ) bley-0.1.5/bley.egg-info/0000755000175000017500000000000011572425057014421 5ustar evgenievgenibley-0.1.5/bley.egg-info/SOURCES.txt0000644000175000017500000000040511572425057016304 0ustar evgenievgeniMANIFEST.in README bley bley.1 bley.conf bley.py bleyhelpers.py epydoc.conf postfix.py setup.py bley.egg-info/PKG-INFO bley.egg-info/SOURCES.txt bley.egg-info/dependency_links.txt bley.egg-info/not-zip-safe bley.egg-info/requires.txt bley.egg-info/top_level.txtbley-0.1.5/bley.egg-info/dependency_links.txt0000644000175000017500000000000111572425057020467 0ustar evgenievgeni bley-0.1.5/bley.egg-info/requires.txt0000644000175000017500000000003111572425057017013 0ustar evgenievgeniTwisted-Core>=8.1.0 pyspfbley-0.1.5/bley.egg-info/not-zip-safe0000644000175000017500000000000111363261723016643 0ustar evgenievgeni bley-0.1.5/bley.egg-info/top_level.txt0000644000175000017500000000003111572425057017145 0ustar evgenievgenibleyhelpers postfix bley bley-0.1.5/bley.egg-info/PKG-INFO0000644000175000017500000000035311572425057015517 0ustar evgenievgeniMetadata-Version: 1.0 Name: bley Version: 0.1.5 Summary: intelligent greylisting daemon for postfix Home-page: http://bley.mx Author: Evgeni Golov Author-email: sargentd@die-welt.net License: BSD Description: UNKNOWN Platform: UNKNOWN bley-0.1.5/bleyhelpers.py0000644000175000017500000001046111572424723014665 0ustar evgenievgeni# Copyright (c) 2009 Evgeni Golov # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions # are met: # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # 3. Neither the name of the University nor the names of its contributors # may be used to endorse or promote products derived from this software # without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF # SUCH DAMAGE. import spf import re __dyn_host = re.compile('(\.bb\.|broadband|cable|dial|dip|dsl|dyn|gprs|pool|ppp|umts|wimax|wwan|[0-9]{1,3}[.-][0-9]{1,3}[.-][0-9]{1,3}[.-][0-9]{1,3})', re.I) __static_host = re.compile('(colo|dedi|hosting|mail|mx[^$]|smtp|static)', re.I) def reverse_ip(ip): '''Returns the IP address in reversed notation (A.B.C.D -> D.C.B.A). @type ip: string @param ip: the IP address to reverse @rtype: string @return: the reversed IP address ''' return spf.reverse_dots(ip) def domain_from_host(host): '''Return the domain part of a host. @type host: string @param host: the host to extract the domain from @rtype: string @return: the extracted domain ''' d = host.split('.') if len(d) > 1: domain = '%s.%s' % (d[-2], d[-1]) else: domain = host return domain def check_dyn_host(host): '''Check the host for being a dynamic/dialup one. @type host: string @param host: the host to check @rtype: int @return: 0 if host is not dynamic, 1 if it is ''' if __static_host.search(host): return 0 if __dyn_host.search(host): return 1 return 0 def check_helo(params): '''Check the HELO for being RFC 5321 complaint. Returns 0 when the HELO match the reverse DNS. Returns 1 when the domain in the HELO match the domain of the reverse DNS or when the HELO is the IP address. Returns 2 else. @type params: dict @param params: the params from Postfix @rtype: int @return: the score of the HELO ''' if params['client_name'] != 'unknown' and params['client_name'] == params['helo_name']: score = 0 elif domain_from_host(params['helo_name']) == domain_from_host(params['client_name']) or params['helo_name'] == '[%s]' % params['client_address']: score = 1 else: score = 2 return score def check_spf(params): '''Check the SPF record of the sending address. Try Best Guess when the domain has no SPF record. Returns 1 when the SPF result is in ['fail', 'softfail'], returns 0 else. @type params: dict @param params: the params from Postfix @rtype: int @return: 1 if bad SPF, 0 else ''' score = 0 try: s = spf.query(params['client_address'], params['sender'], params['helo_name']) r = s.check() if r[0] in ['fail', 'softfail']: score = 1 elif r[0] in ['pass']: score = 0 else: r = s.best_guess() if r[0] in ['fail', 'softfail']: score = 1 elif r[0] in ['pass']: score = 0 except: # DNS Errors, yay... print 'something went wrong in check_spf()' return score bley-0.1.5/PKG-INFO0000644000175000017500000000035311572425057013072 0ustar evgenievgeniMetadata-Version: 1.0 Name: bley Version: 0.1.5 Summary: intelligent greylisting daemon for postfix Home-page: http://bley.mx Author: Evgeni Golov Author-email: sargentd@die-welt.net License: BSD Description: UNKNOWN Platform: UNKNOWN bley-0.1.5/bley.conf0000644000175000017500000000222211572424723013573 0ustar evgenievgeni[bley] # On which IP and port should the daemon listen? listen_addr = 127.0.0.1 listen_port = 1337 # Where to save the PID file? pid_file = bley.pid # Where to save the log file? log_file = bley.log reject_msg = greylisted, try again later # Which database to use? [pgsql|mysql] dbtype = pgsql dbhost = localhost dbuser = bley dbpass = bley dbname = bley # Which DNSBLs and DNSWLs to use? dnsbls = ix.dnsbl.manitu.net, dnsbl.njabl.org, dnsbl.ahbl.org, dnsbl.sorbs.net dnswls = list.dnswl.org # Whitelist after dnswl_threshold hits. dnswl_threshold = 1 # Greylist after dnsbl_threshold hits. dnsbl_threshold = 1 # Greylist after rfc_threshold hits in the RFC checks. rfc_threshold = 2 # Wait greylist_period minutes before accepting greyed sender. greylist_period = 29 # Max wait greylist_max minutes before accepting greyed sender. # (Accept all senders after 12h.) greylist_max = 720 # Add greylist_penalty minutesfor every connection before greylist_period. greylist_penalty= 10 # Purge good entries from the database after purge_days inactivities. purge_days = 40 # Purge bad entries from the database after purge_bad_days inactivities. purge_bad_days = 10 bley-0.1.5/bley.10000644000175000017500000000112411555105554013005 0ustar evgenievgeni.TH BLEY "1" "April 2010" "bley 0.1" "bley" .SH NAME bley \- intelligent greylisting daemon for Postfix .SH SYNOPSIS .B bley [\fIoptions\fR] .SH OPTIONS .TP \fB\-\-version\fR show program's version number and exit .TP \fB\-h\fR, \fB\-\-help\fR show this help message and exit .TP \fB\-p\fR \fIPID_FILE\fR, \fB\-\-pidfile\fR=\fIPID_FILE\fR use PID_FILE for storing the PID .TP \fB\-c\fR \fICONFFILE\fR, \fB\-\-config\fR=\fICONFFILE\fR load configuration from CONFFILE .TP \fB\-v\fR, \fB\-\-verbose\fR use verbose output .TP \fB\-d\fR, \fB\-\-debug\fR don't daemonize the process and log to stdout bley-0.1.5/README0000644000175000017500000000301011572424723012645 0ustar evgenievgeniABOUT: bley is an intelligent greylisting daemon for Postfix. It uses various test (incl. RBL and SPF) to decide whether a sender should be grylisted or not, thus mostly eliminating the usual greylisting delay while still filtering most of the spam. DEPENDENCIES: twisted-core from http://twistedmatrix.com/ pyspf from http://pypi.python.org/pypi/pyspf psycopg2 from http://initd.org/psycopg/ INSTALLATION: Quick and dirty: Unpack the tarball, adjust bley.conf and run bley (NOT bley.py!). Still quick, but not dirty: Unpack the tarball, python setup.py build, python setup.py install, adjust /etc/bley/bley.conf (see CONFIGURATION below), run /usr/bin/bley CONFIGURATION: Basically you just have to set 'dbtype' and the various 'db*' settings so bley can talk to the database. 'database' is either 'pgsql' for PostgreSQL, or 'mysql' for MySQL. The other db settings are: 'dbhost' - the host where the database runs on (usually localhost) 'dbname' - the name of the database (usually bley) 'dbuser' - the name of the database user 'dbpass' - the password of the database user After that you can point your Postfix to bley as a policy server by adding 'check_policy_service inet:127.0.0.1:1337' to your 'smtpd_recipient_restrictions' in main.cf. bley will be working now, but you probably would like to tune it more for your needs (esp. the used DNWLs and DNSBLs, the greylisting times and the hit thresholds) [1] http://www.python.org/dev/peps/pep-0249/ [2] http://www.postfix.org/SMTPD_POLICY_README.html