0
|
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()
|