Menu

[r10]: / trunk / Greylist.py  Maximize  Restore  History

Download this file

178 lines (135 with data), 6.5 kB

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
#!/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))