~semi-hallikas/sspamm/3.0-devel

« back to all changes in this revision

Viewing changes to sspamm.py

  • Committer: Sami-Pekka Hallikas
  • Date: 2010-08-19 11:11:42 UTC
  • Revision ID: semi@hallikas.com-20100819111142-3xs43ugudcptrnv4
Basic functions, debug and timing... This is just core.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#!/usr/bin/env python2
 
2
# -*- coding: UTF-8 -*-
 
3
 
 
4
"""Semi's SPAM Milter - Who would like to get trash on email? Not me.
 
5
"""
 
6
 
 
7
__author__ = "Sami-Pekka Hallikas <semi@hallikas.com>"
 
8
__email__ = "semi@hallikas.com"
 
9
__date__ = "18 Aug 2010"
 
10
__version__ = "3.0-devel"
 
11
 
 
12
import sys
 
13
import os
 
14
import time
 
15
import locale
 
16
import signal
 
17
import re
 
18
 
 
19
# Debug
 
20
import syslog
 
21
from syslog import \
 
22
        LOG_ERR, LOG_WARNING, LOG_NOTICE, LOG_INFO, \
 
23
        LOG_DEBUG, LOG_MAIL, LOG_PID, LOG_CONS
 
24
 
 
25
# http://sourceforge.net/projects/pymilter
 
26
import Milter
 
27
 
 
28
from milter import \
 
29
        ACCEPT, CONTINUE, REJECT, DISCARD, TEMPFAIL, \
 
30
        ADDHDRS, CHGBODY, ADDRCPT, DELRCPT, CHGHDRS
 
31
 
 
32
try: from milter import QUARANTINE
 
33
except: pass
 
34
 
 
35
 
 
36
 
 
37
##
 
38
## Global variables
 
39
##
 
40
conffile = "sspamm.conf"
 
41
 
 
42
conf = {}
 
43
conf["runtime"] = {
 
44
        "starttime": 0,
 
45
        "endtime": 0,
 
46
#       "conftime": 0,
 
47
#       "logtime": 0,
 
48
        "offline": False,
 
49
#       "syslog": False,
 
50
}
 
51
 
 
52
conf["main"] = {
 
53
        "timeme": True,
 
54
}
 
55
 
 
56
def debug(args, level=LOG_DEBUG, id=None, trace=None):
 
57
        datetime = ""
 
58
        msg = ""
 
59
        datetime = time.strftime('%Y%m%d %H:%M:%S')+" "
 
60
        if id:
 
61
                msg += "(%08d) " % (int(id))
 
62
        else:
 
63
                msg += "%8s " % ("")
 
64
        msg = str(msg) + str(args)
 
65
        msg = datetime + msg
 
66
        print msg
 
67
        if trace:
 
68
                traceback.print_exc(limit=None, file=sys.stderr)
 
69
        sys.stdout.flush()
 
70
        return
 
71
 
 
72
def show_vars(var, lvl=0):
 
73
        if lvl==0: lvl=1
 
74
        st=""
 
75
        tab = "\t"
 
76
 
 
77
        if type(var) is dict:
 
78
                st += "{\n"
 
79
                for k in var.keys():
 
80
                        st += tab*lvl
 
81
                        if(0): # Reserve space for keys
 
82
                                if type(k) is int:
 
83
                                        st += "%-16s" % ("%d: " % k)
 
84
                                else:
 
85
                                        st += "%-16s" % ("\"%s\": " % k)
 
86
                        else:
 
87
                                if type(k) is int:
 
88
                                        st += "%d: " % k
 
89
                                else:
 
90
                                        st += "\"%s\": " % k
 
91
                        st += show_vars(var[k], lvl+1)
 
92
                        st += ",\n"
 
93
                st += tab*(lvl-1)
 
94
                st += "}"
 
95
        elif type(var) is tuple:
 
96
                if len(var) < 1:
 
97
                        st += str(var)
 
98
                else:
 
99
                        st += "(\n"
 
100
                        for k in var:
 
101
                                st += tab*lvl
 
102
                                st += show_vars(k, lvl+1)
 
103
                                st += ",\n"
 
104
                        st += tab*(lvl-1)
 
105
                        st += ")"
 
106
        elif type(var) is list:
 
107
                if len(var) < 1:
 
108
                        st += str(var)
 
109
                elif len(var) == 1 and type(var[0]) in [str, int]:
 
110
                        st += str(var)
 
111
                else:
 
112
                        st += "[\n"
 
113
                        for k in var:
 
114
                                st += tab*lvl
 
115
                                st += show_vars(k, lvl+1)
 
116
                                st += ",\n"
 
117
                        st += tab*(lvl-1)
 
118
                        st += "]"
 
119
        elif type(var) is str:
 
120
                st += "\'%s\'" % re.sub("\'", "\\\'", var)
 
121
        elif type(var) is int:
 
122
                st += "%d" % var
 
123
        elif var == None:
 
124
                st += "None"
 
125
        else:
 
126
                st += str(var)
 
127
        return st
 
128
 
 
129
def timeme(timer=0, noshow=None, id=None, title="Timer"):
 
130
        if timer == 0: return time.time()
 
131
        timer = float(time.time())-float(timer)
 
132
        if timer < 0: timer = 0
 
133
        if not noshow: debug("\t%s: %.4f" % (title, timer), LOG_NOTICE, id=id)
 
134
        return float(timer)
 
135
 
 
136
##
 
137
## SpamMilter Class 
 
138
##
 
139
class SpamMilter(Milter.Milter):
 
140
        def maildef(self):
 
141
                return {
 
142
                        "smtpcmds": [ ],
 
143
                        "timer": {},
 
144
                        "my": {
 
145
                                "ip": "",
 
146
                                "dns": "",
 
147
                        },
 
148
                        "received": {
 
149
                                1: {
 
150
                                        "ip": None,
 
151
                                        "dns": None,
 
152
                                        "helo": None,
 
153
                                }
 
154
                        },
 
155
                        "from": [],
 
156
                        "to": [],
 
157
                        "todomain": "",
 
158
                        "size": 0,
 
159
                        "subject": "",
 
160
#                       "charset": "",
 
161
#                       "header": { },
 
162
#                       "tests": [],
 
163
#                       "type": "",
 
164
                }
 
165
 
 
166
        def __init__(self):
 
167
                self.id = Milter.uniqueID()
 
168
                self.log("SpamMilter.__init__()")
 
169
                self.mail = self.maildef()
 
170
                self.mail["id"] = self.id
 
171
 
 
172
        def _cleanup(self):
 
173
                self.log("SpamMilter._cleanup()")
 
174
                if conf["main"]["timeme"] is True and self.mail["timer"].has_key("timepass"): self.mail["timer"]["timepass"] = str("%.4f") % timeme(self.mail["timer"]["timepass"], id=self.id, title="TTimer")
 
175
                print show_vars(self.mail)
 
176
                sys.stdout.flush()
 
177
                if not conf["runtime"]["offline"]: del self.mail
 
178
                return
 
179
 
 
180
        def log(self,*msg):
 
181
                debug(msg, LOG_INFO, id=self.id)
 
182
 
 
183
        def connect(self,hostname,family,hostaddr):
 
184
                if conf["main"]["timeme"] is True: timer = timeme()
 
185
                self.log("SpamMilter.connect(%s, %s)" % (hostname,hostaddr))
 
186
                if self.mail: self.mail["smtpcmds"].append("connect")
 
187
                self.mail["received"][1]["ip"] = hostaddr[0]
 
188
                self.mail["received"][1]["dns"] = hostname
 
189
                if conf["main"]["timeme"] is True: self.mail["timer"]["smtp_connect"] = str("%.4f") % timeme(timer, id=self.mail["id"], noshow=True)
 
190
                return CONTINUE
 
191
 
 
192
        def hello(self,hostname):
 
193
                if conf["main"]["timeme"] is True: timer = timeme()
 
194
                self.log("SpamMilter.hello(%s)" % (hostname))
 
195
                if self.mail: self.mail["smtpcmds"].append("hello")
 
196
                self.mail["received"][1]["helo"] = hostname
 
197
                self.reuse = self.mail["received"][1]
 
198
                if conf["main"]["timeme"] is True: self.mail["timer"]["smtp_hello"] = str("%.4f") % timeme(timer, noshow=True)
 
199
                return CONTINUE
 
200
 
 
201
        def envfrom(self,mailfrom,*vars):
 
202
                if conf["main"]["timeme"] is True: timer = timeme()
 
203
                self.log("SpamMilter.envfrom(\"%s\", %s)" % (mailfrom,vars))
 
204
                if not self.mail or "eom" in self.mail["smtpcmds"]:
 
205
                        self._cleanup()
 
206
                        debug("Connection reused" % (self.id), LOG_INFO, id=self.id)
 
207
                        self.__init__()
 
208
                        self.mail["received"][1] = self.reuse
 
209
                        self.mail["smtpcmds"].append("reused")
 
210
                if self.mail: self.mail["smtpcmds"].append("envfrom")
 
211
                if not conf["runtime"]["offline"]:
 
212
                        self.mail["my"]["ip"] = self.getsymval('{if_addr}')
 
213
                        self.mail["my"]["dns"] = self.getsymval('{if_name}')
 
214
                if conf["main"]["timeme"] is True: self.mail["timer"]["smtp_envfrom"] = str("%.4f") % timeme(timer, noshow=True)
 
215
                return CONTINUE
 
216
 
 
217
        def envrcpt(self,rcpt,*vars):
 
218
                if conf["main"]["timeme"] is True: timer = timeme()
 
219
                self.log("SpamMilter.envrcpt(\"%s\")" % (rcpt))
 
220
                if self.mail: self.mail["smtpcmds"].append("envrcpt")
 
221
                if conf["main"]["timeme"] is True:
 
222
                        if self.mail["timer"].has_key("smtp_envrcpt"):
 
223
                                self.mail["timer"]["smtp_envrcpt"] = str("%.4f") % (float(self.mail["timer"]["smtp_envrcpt"]) + timeme(timer, noshow=True))
 
224
                        else:
 
225
                                self.mail["timer"]["smtp_envrcpt"] = str("%.4f") % timeme(timer, noshow=True)
 
226
                return CONTINUE
 
227
 
 
228
        def header(self,field,value):
 
229
                if conf["main"]["timeme"] is True: timer = timeme()
 
230
                self.log("SpamMilter.header(%s, %s)" % (field,value))
 
231
                if self.mail and "header" not in self.mail["smtpcmds"]: self.mail["smtpcmds"].append("header")
 
232
                if conf["main"]["timeme"] is True:
 
233
                        if self.mail["timer"].has_key("smtp_header"):
 
234
                                self.mail["timer"]["smtp_header"] = str("%.4f") % (float(self.mail["timer"]["smtp_header"]) + timeme(timer, noshow=True))
 
235
                        else:
 
236
                                self.mail["timer"]["smtp_header"] = str("%.4f") % timeme(timer, noshow=True)
 
237
                return CONTINUE
 
238
 
 
239
        def eoh(self):
 
240
                if conf["main"]["timeme"] is True: timer = timeme()
 
241
                self.log("SpamMilter.eoh()")
 
242
                if self.mail: self.mail["smtpcmds"].append("eoh")
 
243
                if conf["main"]["timeme"] is True: self.mail["timer"]["smtp_eoh"] = str("%.4f") % timeme(timer, id=self.mail["id"], noshow=True)
 
244
                return CONTINUE
 
245
 
 
246
        def body(self,chunk):
 
247
                if conf["main"]["timeme"] is True: timer = timeme()
 
248
                self.log("SpamMilter.body() (chunk size: %d)" % len(chunk))
 
249
                if self.mail: self.mail["smtpcmds"].append("body")
 
250
                if conf["main"]["timeme"] is True: self.mail["timer"]["smtp_body"] = str("%.4f") % timeme(timer, noshow=True)
 
251
                return CONTINUE
 
252
 
 
253
        def eom(self):
 
254
                if conf["main"]["timeme"] is True: timer = timeme()
 
255
                self.log("SpamMilter.eom()")
 
256
                if self.mail: self.mail["smtpcmds"].append("eom")
 
257
                if conf["main"]["timeme"] is True: self.mail["timer"]["smtp_eom"] = str("%.4f") % timeme(timer, noshow=True)
 
258
 
 
259
                return CONTINUE
 
260
 
 
261
        def abort(self):
 
262
                self.log("SpamMilter.abort()")
 
263
                self.mail = None
 
264
                self._cleanup()
 
265
                return CONTINUE
 
266
 
 
267
        def close(self):
 
268
                self.log("SpamMilter.close()")
 
269
                if self.mail: self.mail["smtpcmds"].append("close")
 
270
                self._cleanup()
 
271
                return CONTINUE
 
272
 
 
273
## List of Milter commands:
 
274
#       def setreply(self,rcode,xcode=None,msg=None,*ml):
 
275
#       def addheader(self,field,value,idx=-1):
 
276
#       def chgheader(self,field,idx,value):
 
277
#       def addrcpt(self,rcpt,params=None):
 
278
#       def delrcpt(self,rcpt):
 
279
#       def replacebody(self,body):
 
280
#       def chgfrom(self,sender,params=None):
 
281
#       def quarantine(self,reason):
 
282
#       def progress(self):
 
283
 
 
284
 
 
285
        
 
286
 
 
287
##
 
288
## Main Function
 
289
##
 
290
def cleanquit(signal=None, frame=None):
 
291
        global conf
 
292
 
 
293
        debug("cleanquit()")
 
294
        conf["runtime"]["endtime"] = time.time()
 
295
        debug("Spam Filter runtime was %.3f" % (conf["runtime"]["endtime"]-conf["runtime"]["starttime"]), LOG_ERR)
 
296
        sys.stdout.flush()
 
297
        sys.exit(0)
 
298
 
 
299
def main():
 
300
        global conf
 
301
 
 
302
        # Signals does not work with MilterModule, so they are here only as sample
 
303
        signal.signal(signal.SIGHUP , signal.SIG_IGN) # 1
 
304
        signal.signal(signal.SIGQUIT, signal.SIG_IGN) # 3
 
305
        signal.signal(signal.SIGTRAP, signal.SIG_IGN) # 5
 
306
        signal.signal(signal.SIGABRT, signal.SIG_IGN) # 6
 
307
        signal.signal(signal.SIGBUS , cleanquit     ) # 7
 
308
        signal.signal(signal.SIGUSR1, signal.SIG_IGN) # 10
 
309
        signal.signal(signal.SIGUSR2, signal.SIG_IGN) # 12
 
310
        signal.signal(signal.SIGTERM, cleanquit     ) # 15
 
311
        signal.signal(signal.SIGINT , cleanquit     ) # ^C
 
312
        #signal.signal(signal.SIGALRM, cleanquit    )
 
313
        #signal.alarm(90)
 
314
 
 
315
        Milter.factory = SpamMilter
 
316
        Milter.set_flags(ADDRCPT + DELRCPT + ADDHDRS + CHGHDRS + CHGBODY)
 
317
 
 
318
        try:
 
319
                Milter.runmilter("sspamm","inet:7999",300)
 
320
        except SystemExit:
 
321
                pass
 
322
        except:
 
323
                debug("%s: %s" % (sys.exc_type, sys.exc_value), LOG_ERR)
 
324
        cleanquit()
 
325
 
 
326
##
 
327
## Start
 
328
##
 
329
if __name__ == "__main__":
 
330
        tmp = None
 
331
        conf["runtime"]["starttime"] = time.time()
 
332
        startdir = os.getcwd()
 
333
 
 
334
        os.nice(5)
 
335
        locale.setlocale(locale.LC_CTYPE, 'fi_FI.UTF-8')
 
336
        if not sys.argv[1:]:
 
337
                main()
 
338
        else:
 
339
                sys.exit(1)
 
340
sys.exit(0)