Package trac :: Package tests :: Module notification

Source Code for Module trac.tests.notification

  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   
36 -class SMTPServerInterface:
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
52 - def helo(self, args):
53 return None
54
55 - def mail_from(self, args):
56 return None
57
58 - def rcpt_to(self, args):
59 return None
60
61 - def data(self, args):
62 return None
63
64 - def quit(self, args):
65 return None
66
67 - def reset(self, args):
68 return None
69 70 # 71 # Some helper functions for manipulating from & to addresses etc. 72 # 73
74 -def strip_address(address):
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
83 -def split_to(address):
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 #
97 -class SMTPServerEngine:
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
112 - def __init__(self, socket, impl):
113 self.impl = impl 114 self.socket = socket 115 self.state = SMTPServerEngine.ST_INIT
116
117 - def chug(self):
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
155 - def do_command(self, data):
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
198 - def do_data(self, data):
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
216 -class SMTPServer:
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
224 - def __init__(self, port):
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
230 - def serve(self, impl):
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
241 - def start(self):
242 self._socket.listen(1) 243 self._resume = True
244
245 - def stop(self):
246 self._resume = False
247
248 - def terminate(self):
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
263 -class SMTPServerStore(SMTPServerInterface):
264 """ 265 Simple store for SMTP data 266 """ 267
268 - def __init__(self):
269 self.reset(None)
270
271 - def helo(self, args):
272 self.reset(None)
273
274 - def mail_from(self, args):
275 if args.lower().startswith('from:'): 276 self.sender = strip_address(args[5:].replace('\r\n','').strip())
277
278 - def rcpt_to(self, args):
279 if args.lower().startswith('to:'): 280 rcpt = args[3:].replace('\r\n','').strip() 281 self.recipients.append(strip_address(rcpt))
282
283 - def data(self, args):
284 self.message = args
285
286 - def quit(self, args):
287 pass
288
289 - def reset(self, args):
290 self.sender = None 291 self.recipients = [] 292 self.message = None
293 294
295 -class SMTPThreadedServer(threading.Thread):
296 """ 297 Run a SMTP server for a single connection, within a dedicated thread 298 """ 299
300 - def __init__(self, port):
301 self.port = port 302 self.server = SMTPServer(port) 303 self.store = SMTPServerStore() 304 threading.Thread.__init__(self)
305
306 - def run(self):
307 # run from within the SMTP server thread 308 self.server.serve(impl = self.store)
309
310 - def start(self):
311 # run from the main thread 312 self.server.start() 313 threading.Thread.start(self)
314
315 - def stop(self):
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
331 - def get_sender(self):
332 return self.store.sender
333
334 - def get_recipients(self):
335 return self.store.recipients
336
337 - def get_message(self):
338 return self.store.message
339
340 - def cleanup(self):
341 self.store.reset(None)
342
343 -def smtp_address(fulladdr):
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
351 -def decode_header(header):
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
372 -def parse_smtp_message(msg):
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