Add Debian directory
[mspang/vmailman.git] / debian / contrib / spamd.py
1 # Copyright (C) 2002-2003 by James Henstridge <james@daa.com.au>
2 #
3 # This program is free software; you can redistribute it and/or
4 # modify it under the terms of the GNU General Public License
5 # as published by the Free Software Foundation; either version 2
6 # of the License, or (at your option) any later version.
7
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 # GNU General Public License for more details.
12
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, write to the Free Software 
15 # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, US
16
17 '''Module used to communicate with a SpamAssassin spamd process to check
18 or tag messages.
19
20 Usage is as follows:
21   >>> conn = spamd.SpamdConnection()
22   >>> conn.addheader('User', 'username')
23   >>> conn.check(spamd.SYMBOLS, 'From: user@example.com\n...')
24   >>> print conn.getspamstatus()
25   (True, 4.0)
26   >>> print conn.response_message
27   ...
28 '''
29
30 import socket
31 import mimetools, StringIO
32
33 import __builtin__
34 if not hasattr(__builtin__, 'True'):
35     __builtin__.True = (1 == 1)
36     __builtin__.False = (1 != 1)
37 del __builtin__
38
39 class error(Exception): pass
40
41 SPAMD_PORT = 783
42
43 # available methods
44 SKIP          = 'SKIP'
45 PROCESS       = 'PROCESS'
46 CHECK         = 'CHECK'
47 SYMBOLS       = 'SYMBOLS'
48 REPORT        = 'REPORT'
49 REPORT_IFSPAM = 'REPORT_IFSPAM'
50
51 # error codes
52 EX_OK          = 0
53 EX_USAGE       = 64
54 EX_DATAERR     = 65
55 EX_NOINPUT     = 66
56 EX_NOUSER      = 67
57 EX_NOHOST      = 68
58 EX_UNAVAILABLE = 69
59 EX_SOFTWARE    = 70
60 EX_OSERR       = 71
61 EX_OSFILE      = 72
62 EX_CANTCREAT   = 73
63 EX_IOERR       = 74
64 EX_TEMPFAIL    = 75
65 EX_PROTOCOL    = 76
66 EX_NOPERM      = 77
67 EX_CONFIG      = 78
68
69 class SpamdConnection:
70     '''Class to handle talking to SpamAssassin spamd servers.'''
71     # default spamd 
72     host = 'localhost'
73     port = SPAMD_PORT
74
75     PROTOCOL_VERSION = 'SPAMC/1.3'
76
77     def __init__(self, host='', port=0):
78         if not port and ':' in host:
79             host, port = host.split(':', 1)
80             port = int(port)
81         if host: self.host = host
82         if port: self.port = port
83
84         # message structure to hold request headers
85         self.request_headers = mimetools.Message(StringIO.StringIO(), seekable=False)
86         self.request_headers.fp = None
87
88         # stuff that will be filled in after check()
89         self.server_version = None
90         self.result_code = None
91         self.response_message = None
92         self.response_headers = mimetools.Message(StringIO.StringIO(), seekable=False)
93
94     def addheader(self, header, value):
95         '''Adds a header to the request.'''
96         self.request_headers[header] = value
97
98     def check(self, method='PROCESS', message=''):
99         '''Sends a request to the spamd process.'''
100         try:
101             sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
102             sock.connect((self.host, self.port))
103         except socket.error:
104             raise error('could not connect to spamd on %s' % self.host)
105
106         # set content length request header
107         del self.request_headers['Content-length']
108         self.request_headers['Content-length'] = str(len(message))
109
110         request = '%s %s\r\n%s\r\n' % \
111                   (method, self.PROTOCOL_VERSION,
112                    str(self.request_headers).replace('\n', '\r\n'))
113
114         try:
115             sock.send(request)
116             sock.send(message)
117             sock.shutdown(1) # shut down the send half of the socket
118         except (socket.error, IOError):
119             raise error('could not send request to spamd')
120
121         fp = sock.makefile('rb')
122         response = fp.readline()
123         words = response.split(None, 2)
124         if len(words) != 3:
125             raise error('not enough words in response header')
126         if words[0][:6] != 'SPAMD/':
127             raise error('bad protocol name in response string')
128         self.server_version = float(words[0][6:])
129         if self.server_version < 1.0 or self.server_version >= 2.0:
130             raise error('incompatible server version')
131         self.result_code = int(words[1])
132         if self.result_code != 0:
133             raise error('spamd server returned error %s' % words[2])
134
135         try:
136             # parse header
137             self.response_headers = mimetools.Message(fp, seekable=False)
138             self.response_headers.fp = None
139         except IOError:
140             raise error('could not read in response headers')
141
142         try:
143             # read in response message
144             self.response_message = fp.read()
145         except IOError:
146             raise error('could not read in response message')
147             
148         fp.close()
149         sock.close()
150
151     def getspamstatus(self):
152         '''Decode the "Spam" response header.'''
153         if not self.response_headers.has_key('Spam'):
154             raise error('Spam header not found in response')
155
156         isspam, score = self.response_headers['Spam'].split(';', 1)
157         isspam = (isspam.strip() != 'False')
158         score = float(score.split('/',1)[0])
159         return isspam, score