comparison spamc.py @ 0:7f4443127958 default tip

Import
author Daniele Nicolodi <daniele@grinta.net>
date Wed, 02 Nov 2011 12:03:09 +0100
parents
children
comparison
equal deleted inserted replaced
-1:000000000000 0:7f4443127958
1 # implements http://svn.apache.org/repos/asf/spamassassin/trunk/spamd/PROTOCOL
2
3 import re
4 import socket
5 import email.parser
6
7
8 # default spamd port
9 PORT = 783
10
11 # maximal line length when calling readline()
12 MAXLINE = 65536
13
14 # maximal amount of data to read at one time in
15 MAXAMOUNT = 1048576
16
17 # marker
18 UNKNOWN = 'UNKNOWN'
19
20
21 class ProtocolError(Exception):
22 pass
23
24
25 class Response(object):
26 def __init__(self, sock, method):
27 self.fd = sock.makefile('rb', 0)
28 self.method = method
29
30 self.version = UNKNOWN
31 self.status = UNKNOWN
32 self.reason = UNKNOWN
33 self.result = UNKNOWN
34 self.score = UNKNOWN
35 self.required = UNKNOWN
36
37 def _read_status_line(self):
38 line = self.fd.readline()
39 if not line:
40 raise ProtocolError('empty status line')
41
42 try:
43 version, status, reason = line.split()
44 except ValueError:
45 raise ProtocolError('bad status line: %s' % line)
46
47 if not version.startswith('SPAMD/'):
48 raise ProtocolError('bad status line: %s' % line)
49
50 try:
51 version = float(version[6:])
52 status = int(status)
53 reason = reason.strip()
54 except ValueError:
55 raise ProtocolError('bad status line: %s' % line)
56
57 if not self.version > 1.0:
58 raise ProtocolError('unknown protocol: %s' % self.version)
59
60 return version, status, reason
61
62 def _read_headers(self):
63 headers = []
64 while True:
65 line = self.fd.readline(MAXLINE + 1)
66 if len(line) > MAXLINE:
67 raise ProtocolError('header line too long')
68 headers.append(line)
69 if line in ('\r\n', '\n', ''):
70 break
71 hstring = ''.join(headers)
72 return email.parser.Parser().parsestr(hstring)
73
74 def begin(self):
75 # parse response status line
76 self.version, self.status, self.reason = self._read_status_line()
77
78 # parse headers
79 self.headers = self._read_headers()
80
81 # response length
82 try:
83 self.length = int(self.headers.get('content-length', ''))
84 except ValueError:
85 self.length = None
86
87 # if the used methods returns a Spam header
88 if self.method in ('CHECK', 'SYMBOLS', 'REPORT',
89 'REPORT_IFSPAM', 'PROCESS', 'HEADERS'):
90 # parse Spam header
91 value = self.headers.get("spam")
92 try:
93 m = re.match(r'^(\w+)\s;\s(-?\d+.?\d?)\s/\s(\d+.?\d?)$', value)
94 result, score, required = m.groups()
95 self.result = result == 'True'
96 self.score = float(score)
97 self.required = float(required)
98 except (AttributeError, ValueError):
99 raise ProtocolError('bad results header: %s' % value)
100
101 def _read_data(self, size):
102 s = []
103 while size > 0:
104 chunk = self.fd.read(min(size, MAXAMOUNT))
105 if not chunk:
106 raise ProtocolError('incomplete read')
107 s.append(chunk)
108 size = size - len(chunk)
109 return ''.join(s)
110
111 def read(self, size=None):
112 if self.fd is None:
113 return ''
114
115 if self.length is not None:
116 if size is None or size > self.length:
117 # clip read to response size
118 size = self.length
119 s = self._read_data(size)
120 self.length = self.length - size
121 if not self.length:
122 # we read everything
123 self.close()
124 return s
125
126 s = self.fd.read(size)
127 return s
128
129 def close(self):
130 if self.fd:
131 self.fd.close()
132 self.fd = None
133
134 def __str__(self):
135 return '%s %s' % (self.status, self.reason)
136
137
138 class Client(object):
139 def __init__(self, host, port=PORT, timeout=socket._GLOBAL_DEFAULT_TIMEOUT, source=None):
140 self.host = host
141 self.port = port
142 self.timeout = timeout
143 self.source = source
144 self.sock = None
145 self.method = None
146 self.response = None
147
148 def connect(self):
149 self.sock = socket.create_connection((self.host,self.port),
150 self.timeout, self.source)
151
152 def close(self):
153 if self.sock:
154 self.sock.close()
155 self.sock = None
156 if self.response:
157 self.response.close()
158 self.response = None
159
160 def request(self, method, body=None, user=None, headers={}):
161 # save the used method
162 self.method = method
163
164 # buffer output
165 output = []
166
167 # construct request
168 request = '%s SPAMC/1.5' % method
169 output.append(request)
170 if body is not None:
171 output.append('Content-length: %d' % len(body))
172 if user is not None:
173 output.append('User: %s' % user)
174 # remove user from provided headers
175 headers.pop('User', None)
176 for header, value in headers.iteritems():
177 output.append('%s: %s' % (header, value))
178 output.append('')
179 if body is not None:
180 output.append(body)
181 output.append('')
182
183 # send
184 data = '\r\n'.join(output)
185 del output[:]
186 self.connect()
187 self.sock.sendall(data)
188
189 # parse response
190 self.response = Response(self.sock, self.method)
191 self.response.begin()
192 return self.response
193
194
195
196 MESSAGE = """\
197 Message-ID: <4E9D52C3.7060305@example.org>
198 Date: Tue, 18 Oct 2011 12:19:47 +0200
199 From: Example <example@example.org>
200 MIME-Version: 1.0
201 To: example@example.org
202 Subject: Test
203 Content-Type: text/plain; charset=ISO-8859-1
204 Content-Transfer-Encoding: 7bit
205
206 Hello
207
208 """
209
210
211 def main():
212
213 conn = Client(HOST, PORT)
214 resp = conn.request('PING')
215 print resp
216 print resp.read()
217
218 for method in ('CHECK', 'SYMBOLS', 'REPORT', 'PROCESS', 'HEADERS', ):
219 print method
220 conn = Client(HOST, PORT)
221 resp = conn.request(method, MESSAGE, user='daniele')
222 print resp
223 print resp.read(1024)
224
225 method = 'HEADERS'
226 conn = Client(HOST, PORT)
227 resp = conn.request(method, MESSAGE, user='daniele')
228
229 headers = email.parser.Parser().parsestr(resp.read())
230 for name in ('X-Spam-Checker-Version', 'X-Spam-Level', 'X-Spam-Status', ):
231 print name, headers.get(name, '')
232
233
234 if __name__ == '__main__':
235 main()