#!/usr/bin/env python
import anydbm, logging, time
from stat import *
from SmtpException import *
LogLineFormat = '%(asctime)s %(levelname)s: %(message)s'
logging.addLevelName (25, 'NOTICE')
Log = logging.getLogger ('greyd')
Log.setLevel (logging.DEBUG)
Log.notice = lambda m: Log.log(25, m)
# TODO Desperately need a database cleanup method
class Greylist:
"""Encapsulation of routines to access the greylist database.
Database is constructed as key/value pairs and involves three
databases: grey, white and black.
The key is specified as any of the following forms:
H=hostip;F=email;T=email
H=hostip # white or black lists
T=email;F=email # outbound tracking (to/from swapped)
F=email # white or black lists
email values can consist of full email or just a domain
prefixed with an @.
The value contains the following information
+ Entry creation time
+ Expiration time for blocking
+ Entry lifetime
+ Count of emails blocked
+ Count of emails passed
Formatted as "create=121;expire=145;lifetime=235;blocked=4;passed=6"
When a triplet is now found in the database, the value is set to
'create=[curtime];expire=[curtime+delay]'. Any further messages
received prior to expire passing, will receive temp. unavailable
SMTP message. Once the expire time passes, the next message
"""
def __init__(self, config):
self._db_filename = config['database']
self._db = anydbm.open (self._db_filename, 'c')
Log.notice('greylist database opened')
self.delaySecs = self._configTime(config['delay'])
self.stillbornSecs = self._configTime(config['stillborn'])
self.lifetimeSecs = self._configTime(config['lifetime'])
# initialize the internal connection table
self._table = {}
def destroy(self):
self._db.close()
Log.notice('greylist database closed')
def remoteIP(self, uuid, ip):
Log.debug ('Greylist.remoteIP (%s, %s)' % (uuid, ip))
# save IP in connection table
self._table[uuid] = {'ip': ip, 'born': int(time.time())}
def mailFrom(self, uuid, email):
Log.debug ('Greylist.mailFrom (%s, %s)' % (uuid, email))
# save the from email address in the connection table
self._table[uuid]['from'] = email
def mailTo(self, uuid, email):
"""mailTo() is the workhorse of the Greylist class. Raises
SmtpAccept, SmtpDeny or SmtpDelay exception to indicate how to
handle the email. """
Log.debug ('Greylist.mailTo (%s, %s)' % (uuid, email))
host = self._table[uuid]['ip']
frm = self._table[uuid]['from']
to = email
Log.debug('Greylist testing (H=%s,F=%s,T=%s)' % (host, frm, to))
# Step 3: consult greylist database
delayFlag = False
now = int(time.time())
for rcpt in to:
triplet = "H=%s;F=%s;T=%s" % (host, frm, rcpt)
if not self._db.has_key(triplet):
# not entry in the database
data = {'create': now,
'expire': now + self.delaySecs,
'stillborn': now + self.stillbornSecs,
'lifetime': now + self.lifetimeSecs,
'blocked': 1,
'passed': 0}
self._db[triplet] = self._dict2value(data)
raise SmtpDelay ('greylist DB entry created')
else:
# entry found. Need to look closer at the entry
data = self._value2dict(self._db[triplet])
if now <= data['expire']:
# we are still within the greylist delay period
data['blocked'] += 1
self._db[triplet] = self._dict2value(data)
raise SmtpDelay ('still delaying')
elif now <= data['lifetime']:
# greylist delay has expired, but we can accept the message
data['passed'] += 1
data['lifetime'] = now + self.lifetimeSecs
self._db[triplet] = self._dict2value(data)
else:
# lifetime has expired and we need to start again
data = {'create': now,
'expire': now + self.delaySecs,
'stillborn': now + self.stillbornSecs,
'lifetime': now + self.lifetimeSecs,
'blocked': 1,
'passed': 0}
self._db[triplet] = self._dict2value(data)
delayFlag = True
# if we make it this far, we must be good.
def _value2dict(self, val):
"""Convert a value retrieved from the greylist database to
a dictionary."""
data = {}
vals = val.split(';')
for entry in vals:
k,v = entry.split('=')
data[k] = int(v)
return data
def _dict2value(self, dict):
"""Convert a dictionary to a value stored in the greylist
database."""
values = []
for k in dict.keys():
values.append('%s=%s' % (k, dict[k]))
return ';'.join(values)
def _configTime(self, timespec):
"""Convert from a human readable time format (such as 54m) to
the number of seconds that can be used in calculations. The
timespec consists of a decimal number ending in a single letter
designating the multiplier. Supported multipliers are m (minutes),
h (hours), d (days), and w (weeks). If the multiplier is not
specified, then the time specification is interpreted as seconds.
"""
if timespec[-1] == 'm':
return (int(timespec[0:-1]) * 60)
elif timespec[-1] == 'h':
return (int(timespec[0:-1]) * 3600)
elif timespec[-1] == 'd':
return (int(timespec[0:-1]) * 86400)
elif timespec[-1] == 'w':
return (int(timespec[0:-1]) * 604800)
else:
return (int(timespec))