Mercurial > hg > pyspamc
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() |