Rietveld Code Review Tool
Help | Bug tracker | Discussion group | Source code | Sign in
(277)

Side by Side Diff: Lib/smtpd.py

Issue 14843: support define_macros / undef_macros in setup.cfg
Patch Set: Created 11 months, 1 week ago
Left:
Right:
Use n/p to move between diff chunks; N/P to move between comments. Please Sign in to add in-line comments.
Jump to:
View unified diff | Download patch
« no previous file with comments | « Lib/site.py ('k') | Lib/subprocess.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 #! /usr/bin/env python3 1 #! /usr/bin/env python3
2 """An RFC 5321 smtp proxy. 2 """An RFC 2821 smtp proxy.
3 3
4 Usage: %(program)s [options] [localhost:localport [remotehost:remoteport]] 4 Usage: %(program)s [options] [localhost:localport [remotehost:remoteport]]
5 5
6 Options: 6 Options:
7 7
8 --nosetuid 8 --nosetuid
9 -n 9 -n
10 This program generally tries to setuid `nobody', unless this flag is 10 This program generally tries to setuid `nobody', unless this flag is
11 set. The setuid call will fail if this program is not run as root (in 11 set. The setuid call will fail if this program is not run as root (in
12 which case, use this flag). 12 which case, use this flag).
13 13
14 --version 14 --version
15 -V 15 -V
16 Print the version number and exit. 16 Print the version number and exit.
17 17
18 --class classname 18 --class classname
19 -c classname 19 -c classname
20 Use `classname' as the concrete SMTP proxy class. Uses `PureProxy' by 20 Use `classname' as the concrete SMTP proxy class. Uses `PureProxy' by
21 default. 21 default.
22
23 --size limit
24 -s limit
25 Restrict the total size of the incoming message to "limit" number of
26 bytes via the RFC 1870 SIZE extension. Defaults to 33554432 bytes.
27 22
28 --debug 23 --debug
29 -d 24 -d
30 Turn on debugging prints. 25 Turn on debugging prints.
31 26
32 --help 27 --help
33 -h 28 -h
34 Print this message and exit. 29 Print this message and exit.
35 30
36 Version: %(__version__)s 31 Version: %(__version__)s
37 32
38 If localhost is not given then `localhost' is used, and if localport is not 33 If localhost is not given then `localhost' is used, and if localport is not
39 given then 8025 is used. If remotehost is not given then `localhost' is used, 34 given then 8025 is used. If remotehost is not given then `localhost' is used,
40 and if remoteport is not given, then 25 is used. 35 and if remoteport is not given, then 25 is used.
41 """ 36 """
42 37
38
43 # Overview: 39 # Overview:
44 # 40 #
45 # This file implements the minimal SMTP protocol as defined in RFC 5321. It 41 # This file implements the minimal SMTP protocol as defined in RFC 821. It
46 # has a hierarchy of classes which implement the backend functionality for the 42 # has a hierarchy of classes which implement the backend functionality for the
47 # smtpd. A number of classes are provided: 43 # smtpd. A number of classes are provided:
48 # 44 #
49 # SMTPServer - the base class for the backend. Raises NotImplementedError 45 # SMTPServer - the base class for the backend. Raises NotImplementedError
50 # if you try to use it. 46 # if you try to use it.
51 # 47 #
52 # DebuggingServer - simply prints each message it receives on stdout. 48 # DebuggingServer - simply prints each message it receives on stdout.
53 # 49 #
54 # PureProxy - Proxies all messages to a real smtpd which does final 50 # PureProxy - Proxies all messages to a real smtpd which does final
55 # delivery. One known problem with this class is that it doesn't handle 51 # delivery. One known problem with this class is that it doesn't handle
56 # SMTP errors from the backend server at all. This should be fixed 52 # SMTP errors from the backend server at all. This should be fixed
57 # (contributions are welcome!). 53 # (contributions are welcome!).
58 # 54 #
59 # MailmanProxy - An experimental hack to work with GNU Mailman 55 # MailmanProxy - An experimental hack to work with GNU Mailman
60 # <www.list.org>. Using this server as your real incoming smtpd, your 56 # <www.list.org>. Using this server as your real incoming smtpd, your
61 # mailhost will automatically recognize and accept mail destined to Mailman 57 # mailhost will automatically recognize and accept mail destined to Mailman
62 # lists when those lists are created. Every message not destined for a list 58 # lists when those lists are created. Every message not destined for a list
63 # gets forwarded to a real backend smtpd, as with PureProxy. Again, errors 59 # gets forwarded to a real backend smtpd, as with PureProxy. Again, errors
64 # are not handled correctly yet. 60 # are not handled correctly yet.
65 # 61 #
66 # 62 #
67 # Author: Barry Warsaw <barry@python.org> 63 # Author: Barry Warsaw <barry@python.org>
68 # 64 #
69 # TODO: 65 # TODO:
70 # 66 #
71 # - support mailbox delivery 67 # - support mailbox delivery
72 # - alias files 68 # - alias files
73 # - Handle more ESMTP extensions 69 # - ESMTP
74 # - handle error codes from the backend smtpd 70 # - handle error codes from the backend smtpd
75 71
76 import sys 72 import sys
77 import os 73 import os
78 import errno 74 import errno
79 import getopt 75 import getopt
80 import time 76 import time
81 import socket 77 import socket
82 import asyncore 78 import asyncore
83 import asynchat 79 import asynchat
84 import collections
85 from warnings import warn 80 from warnings import warn
86 from email._header_value_parser import get_addr_spec, get_angle_addr
87 81
88 __all__ = ["SMTPServer","DebuggingServer","PureProxy","MailmanProxy"] 82 __all__ = ["SMTPServer","DebuggingServer","PureProxy","MailmanProxy"]
89 83
90 program = sys.argv[0] 84 program = sys.argv[0]
91 __version__ = 'Python SMTP proxy version 0.3' 85 __version__ = 'Python SMTP proxy version 0.2'
92 86
93 87
94 class Devnull: 88 class Devnull:
95 def write(self, msg): pass 89 def write(self, msg): pass
96 def flush(self): pass 90 def flush(self): pass
97 91
98 92
99 DEBUGSTREAM = Devnull() 93 DEBUGSTREAM = Devnull()
100 NEWLINE = '\n' 94 NEWLINE = '\n'
101 EMPTYSTRING = '' 95 EMPTYSTRING = ''
102 COMMASPACE = ', ' 96 COMMASPACE = ', '
103 DATA_SIZE_DEFAULT = 33554432
104 97
105 98
99
106 def usage(code, msg=''): 100 def usage(code, msg=''):
107 print(__doc__ % globals(), file=sys.stderr) 101 print(__doc__ % globals(), file=sys.stderr)
108 if msg: 102 if msg:
109 print(msg, file=sys.stderr) 103 print(msg, file=sys.stderr)
110 sys.exit(code) 104 sys.exit(code)
111 105
112 106
107
113 class SMTPChannel(asynchat.async_chat): 108 class SMTPChannel(asynchat.async_chat):
114 COMMAND = 0 109 COMMAND = 0
115 DATA = 1 110 DATA = 1
116 111
112 data_size_limit = 33554432
117 command_size_limit = 512 113 command_size_limit = 512
118 command_size_limits = collections.defaultdict(lambda x=command_size_limit: x )
119 command_size_limits.update({
120 'MAIL': command_size_limit + 26,
121 })
122 max_command_size_limit = max(command_size_limits.values())
123 114
124 def __init__(self, server, conn, addr, data_size_limit=DATA_SIZE_DEFAULT): 115 def __init__(self, server, conn, addr):
125 asynchat.async_chat.__init__(self, conn) 116 asynchat.async_chat.__init__(self, conn)
126 self.smtp_server = server 117 self.smtp_server = server
127 self.conn = conn 118 self.conn = conn
128 self.addr = addr 119 self.addr = addr
129 self.data_size_limit = data_size_limit
130 self.received_lines = [] 120 self.received_lines = []
131 self.smtp_state = self.COMMAND 121 self.smtp_state = self.COMMAND
132 self.seen_greeting = '' 122 self.seen_greeting = ''
133 self.mailfrom = None 123 self.mailfrom = None
134 self.rcpttos = [] 124 self.rcpttos = []
135 self.received_data = '' 125 self.received_data = ''
136 self.fqdn = socket.getfqdn() 126 self.fqdn = socket.getfqdn()
137 self.num_bytes = 0 127 self.num_bytes = 0
138 try: 128 try:
139 self.peer = conn.getpeername() 129 self.peer = conn.getpeername()
140 except socket.error as err: 130 except socket.error as err:
141 # a race condition may occur if the other end is closing 131 # a race condition may occur if the other end is closing
142 # before we can get the peername 132 # before we can get the peername
143 self.close() 133 self.close()
144 if err.args[0] != errno.ENOTCONN: 134 if err.args[0] != errno.ENOTCONN:
145 raise 135 raise
146 return 136 return
147 print('Peer:', repr(self.peer), file=DEBUGSTREAM) 137 print('Peer:', repr(self.peer), file=DEBUGSTREAM)
148 self.push('220 %s %s' % (self.fqdn, __version__)) 138 self.push('220 %s %s' % (self.fqdn, __version__))
149 self.set_terminator(b'\r\n') 139 self.set_terminator(b'\r\n')
150 self.extended_smtp = False
151 140
152 # properties for backwards-compatibility 141 # properties for backwards-compatibility
153 @property 142 @property
154 def __server(self): 143 def __server(self):
155 warn("Access to __server attribute on SMTPChannel is deprecated, " 144 warn("Access to __server attribute on SMTPChannel is deprecated, "
156 "use 'smtp_server' instead", DeprecationWarning, 2) 145 "use 'smtp_server' instead", DeprecationWarning, 2)
157 return self.smtp_server 146 return self.smtp_server
158 @__server.setter 147 @__server.setter
159 def __server(self, value): 148 def __server(self, value):
160 warn("Setting __server attribute on SMTPChannel is deprecated, " 149 warn("Setting __server attribute on SMTPChannel is deprecated, "
(...skipping 111 matching lines...) Expand 10 before | Expand all | Expand 10 after
272 self.addr = value 261 self.addr = value
273 262
274 # Overrides base class for convenience 263 # Overrides base class for convenience
275 def push(self, msg): 264 def push(self, msg):
276 asynchat.async_chat.push(self, bytes(msg + '\r\n', 'ascii')) 265 asynchat.async_chat.push(self, bytes(msg + '\r\n', 'ascii'))
277 266
278 # Implementation of base class abstract method 267 # Implementation of base class abstract method
279 def collect_incoming_data(self, data): 268 def collect_incoming_data(self, data):
280 limit = None 269 limit = None
281 if self.smtp_state == self.COMMAND: 270 if self.smtp_state == self.COMMAND:
282 limit = self.max_command_size_limit 271 limit = self.command_size_limit
283 elif self.smtp_state == self.DATA: 272 elif self.smtp_state == self.DATA:
284 limit = self.data_size_limit 273 limit = self.data_size_limit
285 if limit and self.num_bytes > limit: 274 if limit and self.num_bytes > limit:
286 return 275 return
287 elif limit: 276 elif limit:
288 self.num_bytes += len(data) 277 self.num_bytes += len(data)
289 self.received_lines.append(str(data, "utf-8")) 278 self.received_lines.append(str(data, "utf-8"))
290 279
291 # Implementation of base class abstract method 280 # Implementation of base class abstract method
292 def found_terminator(self): 281 def found_terminator(self):
293 line = EMPTYSTRING.join(self.received_lines) 282 line = EMPTYSTRING.join(self.received_lines)
294 print('Data:', repr(line), file=DEBUGSTREAM) 283 print('Data:', repr(line), file=DEBUGSTREAM)
295 self.received_lines = [] 284 self.received_lines = []
296 if self.smtp_state == self.COMMAND: 285 if self.smtp_state == self.COMMAND:
297 sz, self.num_bytes = self.num_bytes, 0 286 if self.num_bytes > self.command_size_limit:
287 self.push('500 Error: line too long')
288 self.num_bytes = 0
289 return
290 self.num_bytes = 0
298 if not line: 291 if not line:
299 self.push('500 Error: bad syntax') 292 self.push('500 Error: bad syntax')
300 return 293 return
301 method = None 294 method = None
302 i = line.find(' ') 295 i = line.find(' ')
303 if i < 0: 296 if i < 0:
304 command = line.upper() 297 command = line.upper()
305 arg = None 298 arg = None
306 else: 299 else:
307 command = line[:i].upper() 300 command = line[:i].upper()
308 arg = line[i+1:].strip() 301 arg = line[i+1:].strip()
309 max_sz = (self.command_size_limits[command]
310 if self.extended_smtp else self.command_size_limit)
311 if sz > max_sz:
312 self.push('500 Error: line too long')
313 return
314 method = getattr(self, 'smtp_' + command, None) 302 method = getattr(self, 'smtp_' + command, None)
315 if not method: 303 if not method:
316 self.push('500 Error: command "%s" not recognized' % command) 304 self.push('502 Error: command "%s" not implemented' % command)
317 return 305 return
318 method(arg) 306 method(arg)
319 return 307 return
320 else: 308 else:
321 if self.smtp_state != self.DATA: 309 if self.smtp_state != self.DATA:
322 self.push('451 Internal confusion') 310 self.push('451 Internal confusion')
323 self.num_bytes = 0 311 self.num_bytes = 0
324 return 312 return
325 if self.data_size_limit and self.num_bytes > self.data_size_limit: 313 if self.num_bytes > self.data_size_limit:
326 self.push('552 Error: Too much mail data') 314 self.push('552 Error: Too much mail data')
327 self.num_bytes = 0 315 self.num_bytes = 0
328 return 316 return
329 # Remove extraneous carriage returns and de-transparency according 317 # Remove extraneous carriage returns and de-transparency according
330 # to RFC 5321, Section 4.5.2. 318 # to RFC 821, Section 4.5.2.
331 data = [] 319 data = []
332 for text in line.split('\r\n'): 320 for text in line.split('\r\n'):
333 if text and text[0] == '.': 321 if text and text[0] == '.':
334 data.append(text[1:]) 322 data.append(text[1:])
335 else: 323 else:
336 data.append(text) 324 data.append(text)
337 self.received_data = NEWLINE.join(data) 325 self.received_data = NEWLINE.join(data)
338 status = self.smtp_server.process_message(self.peer, 326 status = self.smtp_server.process_message(self.peer,
339 self.mailfrom, 327 self.mailfrom,
340 self.rcpttos, 328 self.rcpttos,
341 self.received_data) 329 self.received_data)
342 self.rcpttos = [] 330 self.rcpttos = []
343 self.mailfrom = None 331 self.mailfrom = None
344 self.smtp_state = self.COMMAND 332 self.smtp_state = self.COMMAND
345 self.num_bytes = 0 333 self.num_bytes = 0
346 self.set_terminator(b'\r\n') 334 self.set_terminator(b'\r\n')
347 if not status: 335 if not status:
348 self.push('250 OK') 336 self.push('250 Ok')
349 else: 337 else:
350 self.push(status) 338 self.push(status)
351 339
352 # SMTP and ESMTP commands 340 # SMTP and ESMTP commands
353 def smtp_HELO(self, arg): 341 def smtp_HELO(self, arg):
354 if not arg: 342 if not arg:
355 self.push('501 Syntax: HELO hostname') 343 self.push('501 Syntax: HELO hostname')
356 return 344 return
357 if self.seen_greeting: 345 if self.seen_greeting:
358 self.push('503 Duplicate HELO/EHLO') 346 self.push('503 Duplicate HELO/EHLO')
359 else: 347 else:
360 self.seen_greeting = arg 348 self.seen_greeting = arg
361 self.extended_smtp = False
362 self.push('250 %s' % self.fqdn) 349 self.push('250 %s' % self.fqdn)
363
364 def smtp_EHLO(self, arg):
365 if not arg:
366 self.push('501 Syntax: EHLO hostname')
367 return
368 if self.seen_greeting:
369 self.push('503 Duplicate HELO/EHLO')
370 else:
371 self.seen_greeting = arg
372 self.extended_smtp = True
373 self.push('250-%s' % self.fqdn)
374 if self.data_size_limit:
375 self.push('250-SIZE %s' % self.data_size_limit)
376 self.push('250 HELP')
377 350
378 def smtp_NOOP(self, arg): 351 def smtp_NOOP(self, arg):
379 if arg: 352 if arg:
380 self.push('501 Syntax: NOOP') 353 self.push('501 Syntax: NOOP')
381 else: 354 else:
382 self.push('250 OK') 355 self.push('250 Ok')
383 356
384 def smtp_QUIT(self, arg): 357 def smtp_QUIT(self, arg):
385 # args is ignored 358 # args is ignored
386 self.push('221 Bye') 359 self.push('221 Bye')
387 self.close_when_done() 360 self.close_when_done()
388 361
389 def _strip_command_keyword(self, keyword, arg): 362 # factored
363 def __getaddr(self, keyword, arg):
364 address = None
390 keylen = len(keyword) 365 keylen = len(keyword)
391 if arg[:keylen].upper() == keyword: 366 if arg[:keylen].upper() == keyword:
392 return arg[keylen:].strip() 367 address = arg[keylen:].strip()
393 return '' 368 if not address:
394 369 pass
395 def _getaddr(self, arg): 370 elif address[0] == '<' and address[-1] == '>' and address != '<>':
396 if not arg: 371 # Addresses can be in the form <person@dom.com> but watch out
397 return '', '' 372 # for null address, e.g. <>
398 if arg.lstrip().startswith('<'): 373 address = address[1:-1]
399 address, rest = get_angle_addr(arg) 374 return address
400 else:
401 address, rest = get_addr_spec(arg)
402 if not address:
403 return address, rest
404 return address.addr_spec, rest
405
406 def _getparams(self, params):
407 # Return any parameters that appear to be syntactically valid according
408 # to RFC 1869, ignore all others. (Postel rule: accept what we can.)
409 params = [param.split('=', 1) for param in params.split()
410 if '=' in param]
411 return {k: v for k, v in params if k.isalnum()}
412
413 def smtp_HELP(self, arg):
414 if arg:
415 extended = ' [SP <mail parameters]'
416 lc_arg = arg.upper()
417 if lc_arg == 'EHLO':
418 self.push('250 Syntax: EHLO hostname')
419 elif lc_arg == 'HELO':
420 self.push('250 Syntax: HELO hostname')
421 elif lc_arg == 'MAIL':
422 msg = '250 Syntax: MAIL FROM: <address>'
423 if self.extended_smtp:
424 msg += extended
425 self.push(msg)
426 elif lc_arg == 'RCPT':
427 msg = '250 Syntax: RCPT TO: <address>'
428 if self.extended_smtp:
429 msg += extended
430 self.push(msg)
431 elif lc_arg == 'DATA':
432 self.push('250 Syntax: DATA')
433 elif lc_arg == 'RSET':
434 self.push('250 Syntax: RSET')
435 elif lc_arg == 'NOOP':
436 self.push('250 Syntax: NOOP')
437 elif lc_arg == 'QUIT':
438 self.push('250 Syntax: QUIT')
439 elif lc_arg == 'VRFY':
440 self.push('250 Syntax: VRFY <address>')
441 else:
442 self.push('501 Supported commands: EHLO HELO MAIL RCPT '
443 'DATA RSET NOOP QUIT VRFY')
444 else:
445 self.push('250 Supported commands: EHLO HELO MAIL RCPT DATA '
446 'RSET NOOP QUIT VRFY')
447
448 def smtp_VRFY(self, arg):
449 if arg:
450 address, params = self._getaddr(arg)
451 if address:
452 self.push('252 Cannot VRFY user, but will accept message '
453 'and attempt delivery')
454 else:
455 self.push('502 Could not VRFY %s' % arg)
456 else:
457 self.push('501 Syntax: VRFY <address>')
458 375
459 def smtp_MAIL(self, arg): 376 def smtp_MAIL(self, arg):
460 if not self.seen_greeting: 377 if not self.seen_greeting:
461 self.push('503 Error: send HELO first'); 378 self.push('503 Error: send HELO first');
462 return 379 return
380
463 print('===> MAIL', arg, file=DEBUGSTREAM) 381 print('===> MAIL', arg, file=DEBUGSTREAM)
464 syntaxerr = '501 Syntax: MAIL FROM: <address>' 382 address = self.__getaddr('FROM:', arg) if arg else None
465 if self.extended_smtp:
466 syntaxerr += ' [SP <mail-parameters>]'
467 if arg is None:
468 self.push(syntaxerr)
469 return
470 arg = self._strip_command_keyword('FROM:', arg)
471 address, params = self._getaddr(arg)
472 if not address: 383 if not address:
473 self.push(syntaxerr) 384 self.push('501 Syntax: MAIL FROM:<address>')
474 return
475 if not self.extended_smtp and params:
476 self.push(syntaxerr)
477 return
478 if not address:
479 self.push(syntaxerr)
480 return 385 return
481 if self.mailfrom: 386 if self.mailfrom:
482 self.push('503 Error: nested MAIL command') 387 self.push('503 Error: nested MAIL command')
483 return 388 return
484 params = self._getparams(params.upper())
485 if params is None:
486 self.push(syntaxerr)
487 return
488 size = params.pop('SIZE', None)
489 if size:
490 if not size.isdigit():
491 self.push(syntaxerr)
492 return
493 elif self.data_size_limit and int(size) > self.data_size_limit:
494 self.push('552 Error: message size exceeds fixed maximum message size')
495 return
496 if len(params.keys()) > 0:
497 self.push('555 MAIL FROM parameters not recognized or not implemente d')
498 return
499 self.mailfrom = address 389 self.mailfrom = address
500 print('sender:', self.mailfrom, file=DEBUGSTREAM) 390 print('sender:', self.mailfrom, file=DEBUGSTREAM)
501 self.push('250 OK') 391 self.push('250 Ok')
502 392
503 def smtp_RCPT(self, arg): 393 def smtp_RCPT(self, arg):
504 if not self.seen_greeting: 394 if not self.seen_greeting:
505 self.push('503 Error: send HELO first'); 395 self.push('503 Error: send HELO first');
506 return 396 return
397
507 print('===> RCPT', arg, file=DEBUGSTREAM) 398 print('===> RCPT', arg, file=DEBUGSTREAM)
508 if not self.mailfrom: 399 if not self.mailfrom:
509 self.push('503 Error: need MAIL command') 400 self.push('503 Error: need MAIL command')
510 return 401 return
511 syntaxerr = '501 Syntax: RCPT TO: <address>' 402 address = self.__getaddr('TO:', arg) if arg else None
512 if self.extended_smtp:
513 syntaxerr += ' [SP <mail-parameters>]'
514 if arg is None:
515 self.push(syntaxerr)
516 return
517 arg = self._strip_command_keyword('TO:', arg)
518 address, params = self._getaddr(arg)
519 if not address:
520 self.push(syntaxerr)
521 return
522 if params:
523 if self.extended_smtp:
524 params = self._getparams(params.upper())
525 if params is None:
526 self.push(syntaxerr)
527 return
528 else:
529 self.push(syntaxerr)
530 return
531 if not address:
532 self.push(syntaxerr)
533 return
534 if params and len(params.keys()) > 0:
535 self.push('555 RCPT TO parameters not recognized or not implemented' )
536 return
537 if not address: 403 if not address:
538 self.push('501 Syntax: RCPT TO: <address>') 404 self.push('501 Syntax: RCPT TO: <address>')
539 return 405 return
540 self.rcpttos.append(address) 406 self.rcpttos.append(address)
541 print('recips:', self.rcpttos, file=DEBUGSTREAM) 407 print('recips:', self.rcpttos, file=DEBUGSTREAM)
542 self.push('250 OK') 408 self.push('250 Ok')
543 409
544 def smtp_RSET(self, arg): 410 def smtp_RSET(self, arg):
545 if arg: 411 if arg:
546 self.push('501 Syntax: RSET') 412 self.push('501 Syntax: RSET')
547 return 413 return
548 # Resets the sender, recipients, and data, but not the greeting 414 # Resets the sender, recipients, and data, but not the greeting
549 self.mailfrom = None 415 self.mailfrom = None
550 self.rcpttos = [] 416 self.rcpttos = []
551 self.received_data = '' 417 self.received_data = ''
552 self.smtp_state = self.COMMAND 418 self.smtp_state = self.COMMAND
553 self.push('250 OK') 419 self.push('250 Ok')
554 420
555 def smtp_DATA(self, arg): 421 def smtp_DATA(self, arg):
556 if not self.seen_greeting: 422 if not self.seen_greeting:
557 self.push('503 Error: send HELO first'); 423 self.push('503 Error: send HELO first');
558 return 424 return
425
559 if not self.rcpttos: 426 if not self.rcpttos:
560 self.push('503 Error: need RCPT command') 427 self.push('503 Error: need RCPT command')
561 return 428 return
562 if arg: 429 if arg:
563 self.push('501 Syntax: DATA') 430 self.push('501 Syntax: DATA')
564 return 431 return
565 self.smtp_state = self.DATA 432 self.smtp_state = self.DATA
566 self.set_terminator(b'\r\n.\r\n') 433 self.set_terminator(b'\r\n.\r\n')
567 self.push('354 End data with <CR><LF>.<CR><LF>') 434 self.push('354 End data with <CR><LF>.<CR><LF>')
568 435
569 # Commands that have not been implemented
570 def smtp_EXPN(self, arg):
571 self.push('502 EXPN not implemented')
572 436
573 437
574 class SMTPServer(asyncore.dispatcher): 438 class SMTPServer(asyncore.dispatcher):
575 # SMTPChannel class to use for managing client connections 439 # SMTPChannel class to use for managing client connections
576 channel_class = SMTPChannel 440 channel_class = SMTPChannel
577 441
578 def __init__(self, localaddr, remoteaddr, 442 def __init__(self, localaddr, remoteaddr):
579 data_size_limit=DATA_SIZE_DEFAULT):
580 self._localaddr = localaddr 443 self._localaddr = localaddr
581 self._remoteaddr = remoteaddr 444 self._remoteaddr = remoteaddr
582 self.data_size_limit = data_size_limit
583 asyncore.dispatcher.__init__(self) 445 asyncore.dispatcher.__init__(self)
584 try: 446 try:
585 self.create_socket(socket.AF_INET, socket.SOCK_STREAM) 447 self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
586 # try to re-use a server port if possible 448 # try to re-use a server port if possible
587 self.set_reuse_addr() 449 self.set_reuse_addr()
588 self.bind(localaddr) 450 self.bind(localaddr)
589 self.listen(5) 451 self.listen(5)
590 except: 452 except:
591 self.close() 453 self.close()
592 raise 454 raise
593 else: 455 else:
594 print('%s started at %s\n\tLocal addr: %s\n\tRemote addr:%s' % ( 456 print('%s started at %s\n\tLocal addr: %s\n\tRemote addr:%s' % (
595 self.__class__.__name__, time.ctime(time.time()), 457 self.__class__.__name__, time.ctime(time.time()),
596 localaddr, remoteaddr), file=DEBUGSTREAM) 458 localaddr, remoteaddr), file=DEBUGSTREAM)
597 459
598 def handle_accepted(self, conn, addr): 460 def handle_accepted(self, conn, addr):
599 print('Incoming connection from %s' % repr(addr), file=DEBUGSTREAM) 461 print('Incoming connection from %s' % repr(addr), file=DEBUGSTREAM)
600 channel = self.channel_class(self, conn, addr, self.data_size_limit) 462 channel = self.channel_class(self, conn, addr)
601 463
602 # API for "doing something useful with the message" 464 # API for "doing something useful with the message"
603 def process_message(self, peer, mailfrom, rcpttos, data): 465 def process_message(self, peer, mailfrom, rcpttos, data):
604 """Override this abstract method to handle messages from the client. 466 """Override this abstract method to handle messages from the client.
605 467
606 peer is a tuple containing (ipaddr, port) of the client that made the 468 peer is a tuple containing (ipaddr, port) of the client that made the
607 socket connection to our smtp port. 469 socket connection to our smtp port.
608 470
609 mailfrom is the raw address the client claims the message is coming 471 mailfrom is the raw address the client claims the message is coming
610 from. 472 from.
611 473
612 rcpttos is a list of raw addresses the client wishes to deliver the 474 rcpttos is a list of raw addresses the client wishes to deliver the
613 message to. 475 message to.
614 476
615 data is a string containing the entire full text of the message, 477 data is a string containing the entire full text of the message,
616 headers (if supplied) and all. It has been `de-transparencied' 478 headers (if supplied) and all. It has been `de-transparencied'
617 according to RFC 821, Section 4.5.2. In other words, a line 479 according to RFC 821, Section 4.5.2. In other words, a line
618 containing a `.' followed by other text has had the leading dot 480 containing a `.' followed by other text has had the leading dot
619 removed. 481 removed.
620 482
621 This function should return None, for a normal `250 Ok' response; 483 This function should return None, for a normal `250 Ok' response;
622 otherwise it returns the desired response string in RFC 821 format. 484 otherwise it returns the desired response string in RFC 821 format.
623 485
624 """ 486 """
625 raise NotImplementedError 487 raise NotImplementedError
626 488
627 489
490
628 class DebuggingServer(SMTPServer): 491 class DebuggingServer(SMTPServer):
629 # Do something with the gathered message 492 # Do something with the gathered message
630 def process_message(self, peer, mailfrom, rcpttos, data): 493 def process_message(self, peer, mailfrom, rcpttos, data):
631 inheaders = 1 494 inheaders = 1
632 lines = data.split('\n') 495 lines = data.split('\n')
633 print('---------- MESSAGE FOLLOWS ----------') 496 print('---------- MESSAGE FOLLOWS ----------')
634 for line in lines: 497 for line in lines:
635 # headers first 498 # headers first
636 if inheaders and not line: 499 if inheaders and not line:
637 print('X-Peer:', peer[0]) 500 print('X-Peer:', peer[0])
638 inheaders = 0 501 inheaders = 0
639 print(line) 502 print(line)
640 print('------------ END MESSAGE ------------') 503 print('------------ END MESSAGE ------------')
641 504
642 505
506
643 class PureProxy(SMTPServer): 507 class PureProxy(SMTPServer):
644 def process_message(self, peer, mailfrom, rcpttos, data): 508 def process_message(self, peer, mailfrom, rcpttos, data):
645 lines = data.split('\n') 509 lines = data.split('\n')
646 # Look for the last header 510 # Look for the last header
647 i = 0 511 i = 0
648 for line in lines: 512 for line in lines:
649 if not line: 513 if not line:
650 break 514 break
651 i += 1 515 i += 1
652 lines.insert(i, 'X-Peer: %s' % peer[0]) 516 lines.insert(i, 'X-Peer: %s' % peer[0])
(...skipping 20 matching lines...) Expand all
673 # All recipients were refused. If the exception had an associated 537 # All recipients were refused. If the exception had an associated
674 # error code, use it. Otherwise,fake it with a non-triggering 538 # error code, use it. Otherwise,fake it with a non-triggering
675 # exception code. 539 # exception code.
676 errcode = getattr(e, 'smtp_code', -1) 540 errcode = getattr(e, 'smtp_code', -1)
677 errmsg = getattr(e, 'smtp_error', 'ignore') 541 errmsg = getattr(e, 'smtp_error', 'ignore')
678 for r in rcpttos: 542 for r in rcpttos:
679 refused[r] = (errcode, errmsg) 543 refused[r] = (errcode, errmsg)
680 return refused 544 return refused
681 545
682 546
547
683 class MailmanProxy(PureProxy): 548 class MailmanProxy(PureProxy):
684 def process_message(self, peer, mailfrom, rcpttos, data): 549 def process_message(self, peer, mailfrom, rcpttos, data):
685 from io import StringIO 550 from io import StringIO
686 from Mailman import Utils 551 from Mailman import Utils
687 from Mailman import Message 552 from Mailman import Message
688 from Mailman import MailList 553 from Mailman import MailList
689 # If the message is to a Mailman mailing list, then we'll invoke the 554 # If the message is to a Mailman mailing list, then we'll invoke the
690 # Mailman script directly, without going through the real smtpd. 555 # Mailman script directly, without going through the real smtpd.
691 # Otherwise we'll forward it to the local proxy for disposition. 556 # Otherwise we'll forward it to the local proxy for disposition.
692 listnames = [] 557 listnames = []
(...skipping 58 matching lines...) Expand 10 before | Expand all | Expand 10 after
751 msg.Enqueue(mlist, torequest=1) 616 msg.Enqueue(mlist, torequest=1)
752 elif command in ('join', 'leave'): 617 elif command in ('join', 'leave'):
753 # TBD: this is a hack! 618 # TBD: this is a hack!
754 if command == 'join': 619 if command == 'join':
755 msg['Subject'] = 'subscribe' 620 msg['Subject'] = 'subscribe'
756 else: 621 else:
757 msg['Subject'] = 'unsubscribe' 622 msg['Subject'] = 'unsubscribe'
758 msg.Enqueue(mlist, torequest=1) 623 msg.Enqueue(mlist, torequest=1)
759 624
760 625
626
761 class Options: 627 class Options:
762 setuid = 1 628 setuid = 1
763 classname = 'PureProxy' 629 classname = 'PureProxy'
764 size_limit = None
765 630
766 631
632
767 def parseargs(): 633 def parseargs():
768 global DEBUGSTREAM 634 global DEBUGSTREAM
769 try: 635 try:
770 opts, args = getopt.getopt( 636 opts, args = getopt.getopt(
771 sys.argv[1:], 'nVhc:s:d', 637 sys.argv[1:], 'nVhc:d',
772 ['class=', 'nosetuid', 'version', 'help', 'size=', 'debug']) 638 ['class=', 'nosetuid', 'version', 'help', 'debug'])
773 except getopt.error as e: 639 except getopt.error as e:
774 usage(1, e) 640 usage(1, e)
775 641
776 options = Options() 642 options = Options()
777 for opt, arg in opts: 643 for opt, arg in opts:
778 if opt in ('-h', '--help'): 644 if opt in ('-h', '--help'):
779 usage(0) 645 usage(0)
780 elif opt in ('-V', '--version'): 646 elif opt in ('-V', '--version'):
781 print(__version__, file=sys.stderr) 647 print(__version__, file=sys.stderr)
782 sys.exit(0) 648 sys.exit(0)
783 elif opt in ('-n', '--nosetuid'): 649 elif opt in ('-n', '--nosetuid'):
784 options.setuid = 0 650 options.setuid = 0
785 elif opt in ('-c', '--class'): 651 elif opt in ('-c', '--class'):
786 options.classname = arg 652 options.classname = arg
787 elif opt in ('-d', '--debug'): 653 elif opt in ('-d', '--debug'):
788 DEBUGSTREAM = sys.stderr 654 DEBUGSTREAM = sys.stderr
789 elif opt in ('-s', '--size'):
790 try:
791 int_size = int(arg)
792 options.size_limit = int_size
793 except:
794 print('Invalid size: ' + arg, file=sys.stderr)
795 sys.exit(1)
796 655
797 # parse the rest of the arguments 656 # parse the rest of the arguments
798 if len(args) < 1: 657 if len(args) < 1:
799 localspec = 'localhost:8025' 658 localspec = 'localhost:8025'
800 remotespec = 'localhost:25' 659 remotespec = 'localhost:25'
801 elif len(args) < 2: 660 elif len(args) < 2:
802 localspec = args[0] 661 localspec = args[0]
803 remotespec = 'localhost:25' 662 remotespec = 'localhost:25'
804 elif len(args) < 3: 663 elif len(args) < 3:
805 localspec = args[0] 664 localspec = args[0]
(...skipping 14 matching lines...) Expand all
820 if i < 0: 679 if i < 0:
821 usage(1, 'Bad remote spec: %s' % remotespec) 680 usage(1, 'Bad remote spec: %s' % remotespec)
822 options.remotehost = remotespec[:i] 681 options.remotehost = remotespec[:i]
823 try: 682 try:
824 options.remoteport = int(remotespec[i+1:]) 683 options.remoteport = int(remotespec[i+1:])
825 except ValueError: 684 except ValueError:
826 usage(1, 'Bad remote port: %s' % remotespec) 685 usage(1, 'Bad remote port: %s' % remotespec)
827 return options 686 return options
828 687
829 688
689
830 if __name__ == '__main__': 690 if __name__ == '__main__':
831 options = parseargs() 691 options = parseargs()
832 # Become nobody 692 # Become nobody
833 classname = options.classname 693 classname = options.classname
834 if "." in classname: 694 if "." in classname:
835 lastdot = classname.rfind(".") 695 lastdot = classname.rfind(".")
836 mod = __import__(classname[:lastdot], globals(), locals(), [""]) 696 mod = __import__(classname[:lastdot], globals(), locals(), [""])
837 classname = classname[lastdot+1:] 697 classname = classname[lastdot+1:]
838 else: 698 else:
839 import __main__ as mod 699 import __main__ as mod
840 class_ = getattr(mod, classname) 700 class_ = getattr(mod, classname)
841 proxy = class_((options.localhost, options.localport), 701 proxy = class_((options.localhost, options.localport),
842 (options.remotehost, options.remoteport), 702 (options.remotehost, options.remoteport))
843 options.size_limit)
844 if options.setuid: 703 if options.setuid:
845 try: 704 try:
846 import pwd 705 import pwd
847 except ImportError: 706 except ImportError:
848 print('Cannot import module "pwd"; try running with -n option.', fil e=sys.stderr) 707 print('Cannot import module "pwd"; try running with -n option.', fil e=sys.stderr)
849 sys.exit(1) 708 sys.exit(1)
850 nobody = pwd.getpwnam('nobody')[2] 709 nobody = pwd.getpwnam('nobody')[2]
851 try: 710 try:
852 os.setuid(nobody) 711 os.setuid(nobody)
853 except OSError as e: 712 except OSError as e:
854 if e.errno != errno.EPERM: raise 713 if e.errno != errno.EPERM: raise
855 print('Cannot setuid "nobody"; try running with -n option.', file=sy s.stderr) 714 print('Cannot setuid "nobody"; try running with -n option.', file=sy s.stderr)
856 sys.exit(1) 715 sys.exit(1)
857 try: 716 try:
858 asyncore.loop() 717 asyncore.loop()
859 except KeyboardInterrupt: 718 except KeyboardInterrupt:
860 pass 719 pass
OLDNEW
« no previous file with comments | « Lib/site.py ('k') | Lib/subprocess.py » ('j') | no next file with comments »

RSS Feeds Recent Issues | This issue
This is Rietveld cbc36f91f3f7