| Trees | Indices | Help |
|
|---|
|
|
1 # -*- coding: utf-8 -*-
2 #
3 # Copyright (C) 2005-2009 Edgewall Software
4 # Copyright (C) 2005-2006 Emmanuel Blot <[email protected]>
5 # All rights reserved.
6 #
7 # This software is licensed as described in the file COPYING, which
8 # you should have received as part of this distribution. The terms
9 # are also available at http://trac.edgewall.org/wiki/TracLicense.
10 #
11 # This software consists of voluntary contributions made by many
12 # individuals. For the exact contribution history, see the revision
13 # history and logs, available at http://trac.edgewall.org/log/.
14 #
15 # Include a basic SMTP server, based on L. Smithson
16 # ([email protected]) extensible Python SMTP Server
17 #
18 # This file does not contain unit tests, but provides a set of
19 # classes to run SMTP notification tests
20 #
21
22 import socket
23 import string
24 import threading
25 import re
26 import base64
27 import quopri
28
29
30 LF = '\n'
31 CR = '\r'
32 email_re = re.compile(r"([\w\d_\.\-])+\@(([\w\d\-])+\.)+([\w\d]{2,4})+")
33 header_re = re.compile(r'^=\?(?P<charset>[\w\d\-]+)\?(?P<code>[qb])\?(?P<value>.*)\?=$')
34
35
37 """
38 A base class for the imlementation of an application specific SMTP
39 Server. Applications should subclass this and overide these
40 methods, which by default do nothing.
41
42 A method is defined for each RFC821 command. For each of these
43 methods, 'args' is the complete command received from the
44 client. The 'data' method is called after all of the client DATA
45 is received.
46
47 If a method returns 'None', then a '250 OK'message is
48 automatically sent to the client. If a subclass returns a non-null
49 string then it is returned instead.
50 """
51
54
57
60
63
66
69
70 #
71 # Some helper functions for manipulating from & to addresses etc.
72 #
73
75 """
76 Strip the leading & trailing <> from an address. Handy for
77 getting FROM: addresses.
78 """
79 start = string.index(address, '<') + 1
80 end = string.index(address, '>')
81 return address[start:end]
82
84 """
85 Return 'address' as undressed (host, fulladdress) tuple.
86 Handy for use with TO: addresses.
87 """
88 start = string.index(address, '<') + 1
89 sep = string.index(address, '@') + 1
90 end = string.index(address, '>')
91 return (address[sep:end], address[start:end],)
92
93
94 #
95 # This drives the state for a single RFC821 message.
96 #
98 """
99 Server engine that calls methods on the SMTPServerInterface object
100 passed at construction time. It is constructed with a bound socket
101 connection to a client. The 'chug' method drives the state,
102 returning when the client RFC821 transaction is complete.
103 """
104
105 ST_INIT = 0
106 ST_HELO = 1
107 ST_MAIL = 2
108 ST_RCPT = 3
109 ST_DATA = 4
110 ST_QUIT = 5
111
116
118 """
119 Chug the engine, till QUIT is received from the client. As
120 each RFC821 message is received, calls are made on the
121 SMTPServerInterface methods on the object passed at
122 construction time.
123 """
124 self.socket.send("220 Welcome to Trac notification test server\r\n")
125 while 1:
126 data = ''
127 completeLine = 0
128 # Make sure an entire line is received before handing off
129 # to the state engine. Thanks to John Hall for pointing
130 # this out.
131 while not completeLine:
132 try:
133 lump = self.socket.recv(1024)
134 if len(lump):
135 data += lump
136 if (len(data) >= 2) and data[-2:] == '\r\n':
137 completeLine = 1
138 if self.state != SMTPServerEngine.ST_DATA:
139 rsp, keep = self.do_command(data)
140 else:
141 rsp = self.do_data(data)
142 if rsp == None:
143 continue
144 self.socket.send(rsp + "\r\n")
145 if keep == 0:
146 self.socket.close()
147 return
148 else:
149 # EOF
150 return
151 except socket.error:
152 return
153 return
154
156 """Process a single SMTP Command"""
157 cmd = data[0:4]
158 cmd = string.upper(cmd)
159 keep = 1
160 rv = None
161 if cmd == "HELO":
162 self.state = SMTPServerEngine.ST_HELO
163 rv = self.impl.helo(data[5:])
164 elif cmd == "RSET":
165 rv = self.impl.reset(data[5:])
166 self.data_accum = ""
167 self.state = SMTPServerEngine.ST_INIT
168 elif cmd == "NOOP":
169 pass
170 elif cmd == "QUIT":
171 rv = self.impl.quit(data[5:])
172 keep = 0
173 elif cmd == "MAIL":
174 if self.state != SMTPServerEngine.ST_HELO:
175 return ("503 Bad command sequence", 1)
176 self.state = SMTPServerEngine.ST_MAIL
177 rv = self.impl.mail_from(data[5:])
178 elif cmd == "RCPT":
179 if (self.state != SMTPServerEngine.ST_MAIL) and \
180 (self.state != SMTPServerEngine.ST_RCPT):
181 return ("503 Bad command sequence", 1)
182 self.state = SMTPServerEngine.ST_RCPT
183 rv = self.impl.rcpt_to(data[5:])
184 elif cmd == "DATA":
185 if self.state != SMTPServerEngine.ST_RCPT:
186 return ("503 Bad command sequence", 1)
187 self.state = SMTPServerEngine.ST_DATA
188 self.data_accum = ""
189 return ("354 OK, Enter data, terminated with a \\r\\n.\\r\\n", 1)
190 else:
191 return ("505 Eh? WTF was that?", 1)
192
193 if rv:
194 return (rv, keep)
195 else:
196 return("250 OK", keep)
197
199 """
200 Process SMTP Data. Accumulates client DATA until the
201 terminator is found.
202 """
203 self.data_accum = self.data_accum + data
204 if len(self.data_accum) > 4 and self.data_accum[-5:] == '\r\n.\r\n':
205 self.data_accum = self.data_accum[:-5]
206 rv = self.impl.data(self.data_accum)
207 self.state = SMTPServerEngine.ST_HELO
208 if rv:
209 return rv
210 else:
211 return "250 OK - Data and terminator. found"
212 else:
213 return None
214
215
217 """
218 A single threaded SMTP Server connection manager. Listens for
219 incoming SMTP connections on a given port. For each connection,
220 the SMTPServerEngine is chugged, passing the given instance of
221 SMTPServerInterface.
222 """
223
225 self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
226 self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
227 self._socket.bind(("127.0.0.1", port))
228 self._socket_service = None
229
231 while ( self._resume ):
232 try:
233 nsd = self._socket.accept()
234 except socket.error:
235 return
236 self._socket_service = nsd[0]
237 engine = SMTPServerEngine(self._socket_service, impl)
238 engine.chug()
239 self._socket_service = None
240
244
247
249 if self._socket_service:
250 # force the blocking socket to stop waiting for data
251 try:
252 #self._socket_service.shutdown(2)
253 self._socket_service.close()
254 except AttributeError:
255 # the SMTP server may also discard the socket
256 pass
257 self._socket_service = None
258 if self._socket:
259 #self._socket.shutdown(2)
260 self._socket.close()
261 self._socket = None
262
264 """
265 Simple store for SMTP data
266 """
267
269 self.reset(None)
270
272 self.reset(None)
273
275 if args.lower().startswith('from:'):
276 self.sender = strip_address(args[5:].replace('\r\n','').strip())
277
279 if args.lower().startswith('to:'):
280 rcpt = args[3:].replace('\r\n','').strip()
281 self.recipients.append(strip_address(rcpt))
282
284 self.message = args
285
288
293
294
296 """
297 Run a SMTP server for a single connection, within a dedicated thread
298 """
299
301 self.port = port
302 self.server = SMTPServer(port)
303 self.store = SMTPServerStore()
304 threading.Thread.__init__(self)
305
309
314
316 # run from the main thread
317 self.server.stop()
318 # send a message to make the SMTP server quit gracefully
319 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
320 try:
321 s.connect(('127.0.0.1', self.port))
322 r = s.send("QUIT\r\n")
323 except socket.error:
324 pass
325 s.close()
326 # wait for the SMTP server to complete (for up to 2 secs)
327 self.join(2.0)
328 # clean up the SMTP server (and force quit if needed)
329 self.server.terminate()
330
332 return self.store.sender
333
335 return self.store.recipients
336
339
342
344 mo = email_re.search(fulladdr)
345 if mo:
346 return mo.group(0)
347 if start >= 0:
348 return fulladdr[start+1:-1]
349 return fulladdr
350
352 """ Decode a MIME-encoded header value """
353 mo = header_re.match(header)
354 # header does not seem to be MIME-encoded
355 if not mo:
356 return header
357 # attempts to decode the hedear,
358 # following the specified MIME endoding and charset
359 try:
360 encoding = mo.group('code').lower()
361 if encoding == 'q':
362 val = quopri.decodestring(mo.group('value'), header=True)
363 elif encoding == 'b':
364 val = base64.decodestring(mo.group('value'))
365 else:
366 raise AssertionError, "unsupported encoding: %s" % encoding
367 header = unicode(val, mo.group('charset'))
368 except Exception, e:
369 raise AssertionError, e
370 return header
371
373 """ Split a SMTP message into its headers and body.
374 Returns a (headers, body) tuple
375 We do not use the email/MIME Python facilities here
376 as they may accept invalid RFC822 data, or data we do not
377 want to support nor generate """
378 headers = {}
379 lh = None
380 body = None
381 # last line does not contain the final line ending
382 msg += '\r\n'
383 for line in msg.splitlines(True):
384 if body != None:
385 # append current line to the body
386 if line[-2] == CR:
387 body += line[0:-2]
388 body += '\n'
389 else:
390 raise AssertionError, "body misses CRLF: %s (0x%x)" \
391 % (line, ord(line[-1]))
392 else:
393 if line[-2] != CR:
394 # RFC822 requires CRLF at end of field line
395 raise AssertionError, "header field misses CRLF: %s (0x%x)" \
396 % (line, ord(line[-1]))
397 # discards CR
398 line = line[0:-2]
399 if line.strip() == '':
400 # end of headers, body starts
401 body = ''
402 else:
403 val = None
404 if line[0] in ' \t':
405 # continution of the previous line
406 if not lh:
407 # unexpected multiline
408 raise AssertionError, \
409 "unexpected folded line: %s" % line
410 val = decode_header(line.strip(' \t'))
411 # appends the current line to the previous one
412 if not isinstance(headers[lh], tuple):
413 headers[lh] += val
414 else:
415 headers[lh][-1] = headers[lh][-1] + val
416 else:
417 # splits header name from value
418 (h, v) = line.split(':', 1)
419 val = decode_header(v.strip())
420 if headers.has_key(h):
421 if isinstance(headers[h], tuple):
422 headers[h] += val
423 else:
424 headers[h] = (headers[h], val)
425 else:
426 headers[h] = val
427 # stores the last header (for multilines headers)
428 lh = h
429 # returns the headers and the message body
430 return (headers, body)
431
| Trees | Indices | Help |
|
|---|
| Generated by Epydoc 3.0.1 on Mon Feb 28 09:35:38 2011 | http://epydoc.sourceforge.net |