| OLD | NEW |
| 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 Loading... |
| 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 Loading... |
| 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 Loading... |
| 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 Loading... |
| 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 |
| OLD | NEW |