1#! /usr/bin/env python3 2"""An RFC 5321 smtp proxy with optional RFC 1870 and RFC 6531 extensions. 3 4Usage: %(program)s [options] [localhost:localport [remotehost:remoteport]] 5 6Options: 7 8 --nosetuid 9 -n 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 12 which case, use this flag). 13 14 --version 15 -V 16 Print the version number and exit. 17 18 --class classname 19 -c classname 20 Use `classname' as the concrete SMTP proxy class. Uses `PureProxy' by 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 28 --smtputf8 29 -u 30 Enable the SMTPUTF8 extension and behave as an RFC 6531 smtp proxy. 31 32 --debug 33 -d 34 Turn on debugging prints. 35 36 --help 37 -h 38 Print this message and exit. 39 40Version: %(__version__)s 41 42If localhost is not given then `localhost' is used, and if localport is not 43given then 8025 is used. If remotehost is not given then `localhost' is used, 44and if remoteport is not given, then 25 is used. 45""" 46 47# Overview: 48# 49# This file implements the minimal SMTP protocol as defined in RFC 5321. It 50# has a hierarchy of classes which implement the backend functionality for the 51# smtpd. A number of classes are provided: 52# 53# SMTPServer - the base class for the backend. Raises NotImplementedError 54# if you try to use it. 55# 56# DebuggingServer - simply prints each message it receives on stdout. 57# 58# PureProxy - Proxies all messages to a real smtpd which does final 59# delivery. One known problem with this class is that it doesn't handle 60# SMTP errors from the backend server at all. This should be fixed 61# (contributions are welcome!). 62# 63# 64# Author: Barry Warsaw <barry@python.org> 65# 66# TODO: 67# 68# - support mailbox delivery 69# - alias files 70# - Handle more ESMTP extensions 71# - handle error codes from the backend smtpd 72 73import sys 74import os 75import errno 76import getopt 77import time 78import socket 79import collections 80from test.support import asyncore, asynchat 81from warnings import warn 82from email._header_value_parser import get_addr_spec, get_angle_addr 83 84__all__ = [ 85 "SMTPChannel", "SMTPServer", "DebuggingServer", "PureProxy", 86] 87 88program = sys.argv[0] 89__version__ = 'Python SMTP proxy version 0.3' 90 91 92class Devnull: 93 def write(self, msg): pass 94 def flush(self): pass 95 96 97DEBUGSTREAM = Devnull() 98NEWLINE = '\n' 99COMMASPACE = ', ' 100DATA_SIZE_DEFAULT = 33554432 101 102 103def usage(code, msg=''): 104 print(__doc__ % globals(), file=sys.stderr) 105 if msg: 106 print(msg, file=sys.stderr) 107 sys.exit(code) 108 109 110class SMTPChannel(asynchat.async_chat): 111 COMMAND = 0 112 DATA = 1 113 114 command_size_limit = 512 115 command_size_limits = collections.defaultdict(lambda x=command_size_limit: x) 116 117 @property 118 def max_command_size_limit(self): 119 try: 120 return max(self.command_size_limits.values()) 121 except ValueError: 122 return self.command_size_limit 123 124 def __init__(self, server, conn, addr, data_size_limit=DATA_SIZE_DEFAULT, 125 map=None, enable_SMTPUTF8=False, decode_data=False): 126 asynchat.async_chat.__init__(self, conn, map=map) 127 self.smtp_server = server 128 self.conn = conn 129 self.addr = addr 130 self.data_size_limit = data_size_limit 131 self.enable_SMTPUTF8 = enable_SMTPUTF8 132 self._decode_data = decode_data 133 if enable_SMTPUTF8 and decode_data: 134 raise ValueError("decode_data and enable_SMTPUTF8 cannot" 135 " be set to True at the same time") 136 if decode_data: 137 self._emptystring = '' 138 self._linesep = '\r\n' 139 self._dotsep = '.' 140 self._newline = NEWLINE 141 else: 142 self._emptystring = b'' 143 self._linesep = b'\r\n' 144 self._dotsep = ord(b'.') 145 self._newline = b'\n' 146 self._set_rset_state() 147 self.seen_greeting = '' 148 self.extended_smtp = False 149 self.command_size_limits.clear() 150 self.fqdn = socket.getfqdn() 151 try: 152 self.peer = conn.getpeername() 153 except OSError as err: 154 # a race condition may occur if the other end is closing 155 # before we can get the peername 156 self.close() 157 if err.errno != errno.ENOTCONN: 158 raise 159 return 160 print('Peer:', repr(self.peer), file=DEBUGSTREAM) 161 self.push('220 %s %s' % (self.fqdn, __version__)) 162 163 def _set_post_data_state(self): 164 """Reset state variables to their post-DATA state.""" 165 self.smtp_state = self.COMMAND 166 self.mailfrom = None 167 self.rcpttos = [] 168 self.require_SMTPUTF8 = False 169 self.num_bytes = 0 170 self.set_terminator(b'\r\n') 171 172 def _set_rset_state(self): 173 """Reset all state variables except the greeting.""" 174 self._set_post_data_state() 175 self.received_data = '' 176 self.received_lines = [] 177 178 179 # properties for backwards-compatibility 180 @property 181 def __server(self): 182 warn("Access to __server attribute on SMTPChannel is deprecated, " 183 "use 'smtp_server' instead", DeprecationWarning, 2) 184 return self.smtp_server 185 @__server.setter 186 def __server(self, value): 187 warn("Setting __server attribute on SMTPChannel is deprecated, " 188 "set 'smtp_server' instead", DeprecationWarning, 2) 189 self.smtp_server = value 190 191 @property 192 def __line(self): 193 warn("Access to __line attribute on SMTPChannel is deprecated, " 194 "use 'received_lines' instead", DeprecationWarning, 2) 195 return self.received_lines 196 @__line.setter 197 def __line(self, value): 198 warn("Setting __line attribute on SMTPChannel is deprecated, " 199 "set 'received_lines' instead", DeprecationWarning, 2) 200 self.received_lines = value 201 202 @property 203 def __state(self): 204 warn("Access to __state attribute on SMTPChannel is deprecated, " 205 "use 'smtp_state' instead", DeprecationWarning, 2) 206 return self.smtp_state 207 @__state.setter 208 def __state(self, value): 209 warn("Setting __state attribute on SMTPChannel is deprecated, " 210 "set 'smtp_state' instead", DeprecationWarning, 2) 211 self.smtp_state = value 212 213 @property 214 def __greeting(self): 215 warn("Access to __greeting attribute on SMTPChannel is deprecated, " 216 "use 'seen_greeting' instead", DeprecationWarning, 2) 217 return self.seen_greeting 218 @__greeting.setter 219 def __greeting(self, value): 220 warn("Setting __greeting attribute on SMTPChannel is deprecated, " 221 "set 'seen_greeting' instead", DeprecationWarning, 2) 222 self.seen_greeting = value 223 224 @property 225 def __mailfrom(self): 226 warn("Access to __mailfrom attribute on SMTPChannel is deprecated, " 227 "use 'mailfrom' instead", DeprecationWarning, 2) 228 return self.mailfrom 229 @__mailfrom.setter 230 def __mailfrom(self, value): 231 warn("Setting __mailfrom attribute on SMTPChannel is deprecated, " 232 "set 'mailfrom' instead", DeprecationWarning, 2) 233 self.mailfrom = value 234 235 @property 236 def __rcpttos(self): 237 warn("Access to __rcpttos attribute on SMTPChannel is deprecated, " 238 "use 'rcpttos' instead", DeprecationWarning, 2) 239 return self.rcpttos 240 @__rcpttos.setter 241 def __rcpttos(self, value): 242 warn("Setting __rcpttos attribute on SMTPChannel is deprecated, " 243 "set 'rcpttos' instead", DeprecationWarning, 2) 244 self.rcpttos = value 245 246 @property 247 def __data(self): 248 warn("Access to __data attribute on SMTPChannel is deprecated, " 249 "use 'received_data' instead", DeprecationWarning, 2) 250 return self.received_data 251 @__data.setter 252 def __data(self, value): 253 warn("Setting __data attribute on SMTPChannel is deprecated, " 254 "set 'received_data' instead", DeprecationWarning, 2) 255 self.received_data = value 256 257 @property 258 def __fqdn(self): 259 warn("Access to __fqdn attribute on SMTPChannel is deprecated, " 260 "use 'fqdn' instead", DeprecationWarning, 2) 261 return self.fqdn 262 @__fqdn.setter 263 def __fqdn(self, value): 264 warn("Setting __fqdn attribute on SMTPChannel is deprecated, " 265 "set 'fqdn' instead", DeprecationWarning, 2) 266 self.fqdn = value 267 268 @property 269 def __peer(self): 270 warn("Access to __peer attribute on SMTPChannel is deprecated, " 271 "use 'peer' instead", DeprecationWarning, 2) 272 return self.peer 273 @__peer.setter 274 def __peer(self, value): 275 warn("Setting __peer attribute on SMTPChannel is deprecated, " 276 "set 'peer' instead", DeprecationWarning, 2) 277 self.peer = value 278 279 @property 280 def __conn(self): 281 warn("Access to __conn attribute on SMTPChannel is deprecated, " 282 "use 'conn' instead", DeprecationWarning, 2) 283 return self.conn 284 @__conn.setter 285 def __conn(self, value): 286 warn("Setting __conn attribute on SMTPChannel is deprecated, " 287 "set 'conn' instead", DeprecationWarning, 2) 288 self.conn = value 289 290 @property 291 def __addr(self): 292 warn("Access to __addr attribute on SMTPChannel is deprecated, " 293 "use 'addr' instead", DeprecationWarning, 2) 294 return self.addr 295 @__addr.setter 296 def __addr(self, value): 297 warn("Setting __addr attribute on SMTPChannel is deprecated, " 298 "set 'addr' instead", DeprecationWarning, 2) 299 self.addr = value 300 301 # Overrides base class for convenience. 302 def push(self, msg): 303 asynchat.async_chat.push(self, bytes( 304 msg + '\r\n', 'utf-8' if self.require_SMTPUTF8 else 'ascii')) 305 306 # Implementation of base class abstract method 307 def collect_incoming_data(self, data): 308 limit = None 309 if self.smtp_state == self.COMMAND: 310 limit = self.max_command_size_limit 311 elif self.smtp_state == self.DATA: 312 limit = self.data_size_limit 313 if limit and self.num_bytes > limit: 314 return 315 elif limit: 316 self.num_bytes += len(data) 317 if self._decode_data: 318 self.received_lines.append(str(data, 'utf-8')) 319 else: 320 self.received_lines.append(data) 321 322 # Implementation of base class abstract method 323 def found_terminator(self): 324 line = self._emptystring.join(self.received_lines) 325 print('Data:', repr(line), file=DEBUGSTREAM) 326 self.received_lines = [] 327 if self.smtp_state == self.COMMAND: 328 sz, self.num_bytes = self.num_bytes, 0 329 if not line: 330 self.push('500 Error: bad syntax') 331 return 332 if not self._decode_data: 333 line = str(line, 'utf-8') 334 i = line.find(' ') 335 if i < 0: 336 command = line.upper() 337 arg = None 338 else: 339 command = line[:i].upper() 340 arg = line[i+1:].strip() 341 max_sz = (self.command_size_limits[command] 342 if self.extended_smtp else self.command_size_limit) 343 if sz > max_sz: 344 self.push('500 Error: line too long') 345 return 346 method = getattr(self, 'smtp_' + command, None) 347 if not method: 348 self.push('500 Error: command "%s" not recognized' % command) 349 return 350 method(arg) 351 return 352 else: 353 if self.smtp_state != self.DATA: 354 self.push('451 Internal confusion') 355 self.num_bytes = 0 356 return 357 if self.data_size_limit and self.num_bytes > self.data_size_limit: 358 self.push('552 Error: Too much mail data') 359 self.num_bytes = 0 360 return 361 # Remove extraneous carriage returns and de-transparency according 362 # to RFC 5321, Section 4.5.2. 363 data = [] 364 for text in line.split(self._linesep): 365 if text and text[0] == self._dotsep: 366 data.append(text[1:]) 367 else: 368 data.append(text) 369 self.received_data = self._newline.join(data) 370 args = (self.peer, self.mailfrom, self.rcpttos, self.received_data) 371 kwargs = {} 372 if not self._decode_data: 373 kwargs = { 374 'mail_options': self.mail_options, 375 'rcpt_options': self.rcpt_options, 376 } 377 status = self.smtp_server.process_message(*args, **kwargs) 378 self._set_post_data_state() 379 if not status: 380 self.push('250 OK') 381 else: 382 self.push(status) 383 384 # SMTP and ESMTP commands 385 def smtp_HELO(self, arg): 386 if not arg: 387 self.push('501 Syntax: HELO hostname') 388 return 389 # See issue #21783 for a discussion of this behavior. 390 if self.seen_greeting: 391 self.push('503 Duplicate HELO/EHLO') 392 return 393 self._set_rset_state() 394 self.seen_greeting = arg 395 self.push('250 %s' % self.fqdn) 396 397 def smtp_EHLO(self, arg): 398 if not arg: 399 self.push('501 Syntax: EHLO hostname') 400 return 401 # See issue #21783 for a discussion of this behavior. 402 if self.seen_greeting: 403 self.push('503 Duplicate HELO/EHLO') 404 return 405 self._set_rset_state() 406 self.seen_greeting = arg 407 self.extended_smtp = True 408 self.push('250-%s' % self.fqdn) 409 if self.data_size_limit: 410 self.push('250-SIZE %s' % self.data_size_limit) 411 self.command_size_limits['MAIL'] += 26 412 if not self._decode_data: 413 self.push('250-8BITMIME') 414 if self.enable_SMTPUTF8: 415 self.push('250-SMTPUTF8') 416 self.command_size_limits['MAIL'] += 10 417 self.push('250 HELP') 418 419 def smtp_NOOP(self, arg): 420 if arg: 421 self.push('501 Syntax: NOOP') 422 else: 423 self.push('250 OK') 424 425 def smtp_QUIT(self, arg): 426 # args is ignored 427 self.push('221 Bye') 428 self.close_when_done() 429 430 def _strip_command_keyword(self, keyword, arg): 431 keylen = len(keyword) 432 if arg[:keylen].upper() == keyword: 433 return arg[keylen:].strip() 434 return '' 435 436 def _getaddr(self, arg): 437 if not arg: 438 return '', '' 439 if arg.lstrip().startswith('<'): 440 address, rest = get_angle_addr(arg) 441 else: 442 address, rest = get_addr_spec(arg) 443 if not address: 444 return address, rest 445 return address.addr_spec, rest 446 447 def _getparams(self, params): 448 # Return params as dictionary. Return None if not all parameters 449 # appear to be syntactically valid according to RFC 1869. 450 result = {} 451 for param in params: 452 param, eq, value = param.partition('=') 453 if not param.isalnum() or eq and not value: 454 return None 455 result[param] = value if eq else True 456 return result 457 458 def smtp_HELP(self, arg): 459 if arg: 460 extended = ' [SP <mail-parameters>]' 461 lc_arg = arg.upper() 462 if lc_arg == 'EHLO': 463 self.push('250 Syntax: EHLO hostname') 464 elif lc_arg == 'HELO': 465 self.push('250 Syntax: HELO hostname') 466 elif lc_arg == 'MAIL': 467 msg = '250 Syntax: MAIL FROM: <address>' 468 if self.extended_smtp: 469 msg += extended 470 self.push(msg) 471 elif lc_arg == 'RCPT': 472 msg = '250 Syntax: RCPT TO: <address>' 473 if self.extended_smtp: 474 msg += extended 475 self.push(msg) 476 elif lc_arg == 'DATA': 477 self.push('250 Syntax: DATA') 478 elif lc_arg == 'RSET': 479 self.push('250 Syntax: RSET') 480 elif lc_arg == 'NOOP': 481 self.push('250 Syntax: NOOP') 482 elif lc_arg == 'QUIT': 483 self.push('250 Syntax: QUIT') 484 elif lc_arg == 'VRFY': 485 self.push('250 Syntax: VRFY <address>') 486 else: 487 self.push('501 Supported commands: EHLO HELO MAIL RCPT ' 488 'DATA RSET NOOP QUIT VRFY') 489 else: 490 self.push('250 Supported commands: EHLO HELO MAIL RCPT DATA ' 491 'RSET NOOP QUIT VRFY') 492 493 def smtp_VRFY(self, arg): 494 if arg: 495 address, params = self._getaddr(arg) 496 if address: 497 self.push('252 Cannot VRFY user, but will accept message ' 498 'and attempt delivery') 499 else: 500 self.push('502 Could not VRFY %s' % arg) 501 else: 502 self.push('501 Syntax: VRFY <address>') 503 504 def smtp_MAIL(self, arg): 505 if not self.seen_greeting: 506 self.push('503 Error: send HELO first') 507 return 508 print('===> MAIL', arg, file=DEBUGSTREAM) 509 syntaxerr = '501 Syntax: MAIL FROM: <address>' 510 if self.extended_smtp: 511 syntaxerr += ' [SP <mail-parameters>]' 512 if arg is None: 513 self.push(syntaxerr) 514 return 515 arg = self._strip_command_keyword('FROM:', arg) 516 address, params = self._getaddr(arg) 517 if not address: 518 self.push(syntaxerr) 519 return 520 if not self.extended_smtp and params: 521 self.push(syntaxerr) 522 return 523 if self.mailfrom: 524 self.push('503 Error: nested MAIL command') 525 return 526 self.mail_options = params.upper().split() 527 params = self._getparams(self.mail_options) 528 if params is None: 529 self.push(syntaxerr) 530 return 531 if not self._decode_data: 532 body = params.pop('BODY', '7BIT') 533 if body not in ['7BIT', '8BITMIME']: 534 self.push('501 Error: BODY can only be one of 7BIT, 8BITMIME') 535 return 536 if self.enable_SMTPUTF8: 537 smtputf8 = params.pop('SMTPUTF8', False) 538 if smtputf8 is True: 539 self.require_SMTPUTF8 = True 540 elif smtputf8 is not False: 541 self.push('501 Error: SMTPUTF8 takes no arguments') 542 return 543 size = params.pop('SIZE', None) 544 if size: 545 if not size.isdigit(): 546 self.push(syntaxerr) 547 return 548 elif self.data_size_limit and int(size) > self.data_size_limit: 549 self.push('552 Error: message size exceeds fixed maximum message size') 550 return 551 if len(params.keys()) > 0: 552 self.push('555 MAIL FROM parameters not recognized or not implemented') 553 return 554 self.mailfrom = address 555 print('sender:', self.mailfrom, file=DEBUGSTREAM) 556 self.push('250 OK') 557 558 def smtp_RCPT(self, arg): 559 if not self.seen_greeting: 560 self.push('503 Error: send HELO first'); 561 return 562 print('===> RCPT', arg, file=DEBUGSTREAM) 563 if not self.mailfrom: 564 self.push('503 Error: need MAIL command') 565 return 566 syntaxerr = '501 Syntax: RCPT TO: <address>' 567 if self.extended_smtp: 568 syntaxerr += ' [SP <mail-parameters>]' 569 if arg is None: 570 self.push(syntaxerr) 571 return 572 arg = self._strip_command_keyword('TO:', arg) 573 address, params = self._getaddr(arg) 574 if not address: 575 self.push(syntaxerr) 576 return 577 if not self.extended_smtp and params: 578 self.push(syntaxerr) 579 return 580 self.rcpt_options = params.upper().split() 581 params = self._getparams(self.rcpt_options) 582 if params is None: 583 self.push(syntaxerr) 584 return 585 # XXX currently there are no options we recognize. 586 if len(params.keys()) > 0: 587 self.push('555 RCPT TO parameters not recognized or not implemented') 588 return 589 self.rcpttos.append(address) 590 print('recips:', self.rcpttos, file=DEBUGSTREAM) 591 self.push('250 OK') 592 593 def smtp_RSET(self, arg): 594 if arg: 595 self.push('501 Syntax: RSET') 596 return 597 self._set_rset_state() 598 self.push('250 OK') 599 600 def smtp_DATA(self, arg): 601 if not self.seen_greeting: 602 self.push('503 Error: send HELO first'); 603 return 604 if not self.rcpttos: 605 self.push('503 Error: need RCPT command') 606 return 607 if arg: 608 self.push('501 Syntax: DATA') 609 return 610 self.smtp_state = self.DATA 611 self.set_terminator(b'\r\n.\r\n') 612 self.push('354 End data with <CR><LF>.<CR><LF>') 613 614 # Commands that have not been implemented 615 def smtp_EXPN(self, arg): 616 self.push('502 EXPN not implemented') 617 618 619class SMTPServer(asyncore.dispatcher): 620 # SMTPChannel class to use for managing client connections 621 channel_class = SMTPChannel 622 623 def __init__(self, localaddr, remoteaddr, 624 data_size_limit=DATA_SIZE_DEFAULT, map=None, 625 enable_SMTPUTF8=False, decode_data=False): 626 self._localaddr = localaddr 627 self._remoteaddr = remoteaddr 628 self.data_size_limit = data_size_limit 629 self.enable_SMTPUTF8 = enable_SMTPUTF8 630 self._decode_data = decode_data 631 if enable_SMTPUTF8 and decode_data: 632 raise ValueError("decode_data and enable_SMTPUTF8 cannot" 633 " be set to True at the same time") 634 asyncore.dispatcher.__init__(self, map=map) 635 try: 636 gai_results = socket.getaddrinfo(*localaddr, 637 type=socket.SOCK_STREAM) 638 self.create_socket(gai_results[0][0], gai_results[0][1]) 639 # try to re-use a server port if possible 640 self.set_reuse_addr() 641 self.bind(localaddr) 642 self.listen(5) 643 except: 644 self.close() 645 raise 646 else: 647 print('%s started at %s\n\tLocal addr: %s\n\tRemote addr:%s' % ( 648 self.__class__.__name__, time.ctime(time.time()), 649 localaddr, remoteaddr), file=DEBUGSTREAM) 650 651 def handle_accepted(self, conn, addr): 652 print('Incoming connection from %s' % repr(addr), file=DEBUGSTREAM) 653 channel = self.channel_class(self, 654 conn, 655 addr, 656 self.data_size_limit, 657 self._map, 658 self.enable_SMTPUTF8, 659 self._decode_data) 660 661 # API for "doing something useful with the message" 662 def process_message(self, peer, mailfrom, rcpttos, data, **kwargs): 663 """Override this abstract method to handle messages from the client. 664 665 peer is a tuple containing (ipaddr, port) of the client that made the 666 socket connection to our smtp port. 667 668 mailfrom is the raw address the client claims the message is coming 669 from. 670 671 rcpttos is a list of raw addresses the client wishes to deliver the 672 message to. 673 674 data is a string containing the entire full text of the message, 675 headers (if supplied) and all. It has been `de-transparencied' 676 according to RFC 821, Section 4.5.2. In other words, a line 677 containing a `.' followed by other text has had the leading dot 678 removed. 679 680 kwargs is a dictionary containing additional information. It is 681 empty if decode_data=True was given as init parameter, otherwise 682 it will contain the following keys: 683 'mail_options': list of parameters to the mail command. All 684 elements are uppercase strings. Example: 685 ['BODY=8BITMIME', 'SMTPUTF8']. 686 'rcpt_options': same, for the rcpt command. 687 688 This function should return None for a normal `250 Ok' response; 689 otherwise, it should return the desired response string in RFC 821 690 format. 691 692 """ 693 raise NotImplementedError 694 695 696class DebuggingServer(SMTPServer): 697 698 def _print_message_content(self, peer, data): 699 inheaders = 1 700 lines = data.splitlines() 701 for line in lines: 702 # headers first 703 if inheaders and not line: 704 peerheader = 'X-Peer: ' + peer[0] 705 if not isinstance(data, str): 706 # decoded_data=false; make header match other binary output 707 peerheader = repr(peerheader.encode('utf-8')) 708 print(peerheader) 709 inheaders = 0 710 if not isinstance(data, str): 711 # Avoid spurious 'str on bytes instance' warning. 712 line = repr(line) 713 print(line) 714 715 def process_message(self, peer, mailfrom, rcpttos, data, **kwargs): 716 print('---------- MESSAGE FOLLOWS ----------') 717 if kwargs: 718 if kwargs.get('mail_options'): 719 print('mail options: %s' % kwargs['mail_options']) 720 if kwargs.get('rcpt_options'): 721 print('rcpt options: %s\n' % kwargs['rcpt_options']) 722 self._print_message_content(peer, data) 723 print('------------ END MESSAGE ------------') 724 725 726class PureProxy(SMTPServer): 727 def __init__(self, *args, **kwargs): 728 if 'enable_SMTPUTF8' in kwargs and kwargs['enable_SMTPUTF8']: 729 raise ValueError("PureProxy does not support SMTPUTF8.") 730 super(PureProxy, self).__init__(*args, **kwargs) 731 732 def process_message(self, peer, mailfrom, rcpttos, data): 733 lines = data.split('\n') 734 # Look for the last header 735 i = 0 736 for line in lines: 737 if not line: 738 break 739 i += 1 740 lines.insert(i, 'X-Peer: %s' % peer[0]) 741 data = NEWLINE.join(lines) 742 refused = self._deliver(mailfrom, rcpttos, data) 743 # TBD: what to do with refused addresses? 744 print('we got some refusals:', refused, file=DEBUGSTREAM) 745 746 def _deliver(self, mailfrom, rcpttos, data): 747 import smtplib 748 refused = {} 749 try: 750 s = smtplib.SMTP() 751 s.connect(self._remoteaddr[0], self._remoteaddr[1]) 752 try: 753 refused = s.sendmail(mailfrom, rcpttos, data) 754 finally: 755 s.quit() 756 except smtplib.SMTPRecipientsRefused as e: 757 print('got SMTPRecipientsRefused', file=DEBUGSTREAM) 758 refused = e.recipients 759 except (OSError, smtplib.SMTPException) as e: 760 print('got', e.__class__, file=DEBUGSTREAM) 761 # All recipients were refused. If the exception had an associated 762 # error code, use it. Otherwise,fake it with a non-triggering 763 # exception code. 764 errcode = getattr(e, 'smtp_code', -1) 765 errmsg = getattr(e, 'smtp_error', 'ignore') 766 for r in rcpttos: 767 refused[r] = (errcode, errmsg) 768 return refused 769 770 771class Options: 772 setuid = True 773 classname = 'PureProxy' 774 size_limit = None 775 enable_SMTPUTF8 = False 776 777 778def parseargs(): 779 global DEBUGSTREAM 780 try: 781 opts, args = getopt.getopt( 782 sys.argv[1:], 'nVhc:s:du', 783 ['class=', 'nosetuid', 'version', 'help', 'size=', 'debug', 784 'smtputf8']) 785 except getopt.error as e: 786 usage(1, e) 787 788 options = Options() 789 for opt, arg in opts: 790 if opt in ('-h', '--help'): 791 usage(0) 792 elif opt in ('-V', '--version'): 793 print(__version__) 794 sys.exit(0) 795 elif opt in ('-n', '--nosetuid'): 796 options.setuid = False 797 elif opt in ('-c', '--class'): 798 options.classname = arg 799 elif opt in ('-d', '--debug'): 800 DEBUGSTREAM = sys.stderr 801 elif opt in ('-u', '--smtputf8'): 802 options.enable_SMTPUTF8 = True 803 elif opt in ('-s', '--size'): 804 try: 805 int_size = int(arg) 806 options.size_limit = int_size 807 except: 808 print('Invalid size: ' + arg, file=sys.stderr) 809 sys.exit(1) 810 811 # parse the rest of the arguments 812 if len(args) < 1: 813 localspec = 'localhost:8025' 814 remotespec = 'localhost:25' 815 elif len(args) < 2: 816 localspec = args[0] 817 remotespec = 'localhost:25' 818 elif len(args) < 3: 819 localspec = args[0] 820 remotespec = args[1] 821 else: 822 usage(1, 'Invalid arguments: %s' % COMMASPACE.join(args)) 823 824 # split into host/port pairs 825 i = localspec.find(':') 826 if i < 0: 827 usage(1, 'Bad local spec: %s' % localspec) 828 options.localhost = localspec[:i] 829 try: 830 options.localport = int(localspec[i+1:]) 831 except ValueError: 832 usage(1, 'Bad local port: %s' % localspec) 833 i = remotespec.find(':') 834 if i < 0: 835 usage(1, 'Bad remote spec: %s' % remotespec) 836 options.remotehost = remotespec[:i] 837 try: 838 options.remoteport = int(remotespec[i+1:]) 839 except ValueError: 840 usage(1, 'Bad remote port: %s' % remotespec) 841 return options 842 843 844if __name__ == '__main__': 845 options = parseargs() 846 # Become nobody 847 classname = options.classname 848 if "." in classname: 849 lastdot = classname.rfind(".") 850 mod = __import__(classname[:lastdot], globals(), locals(), [""]) 851 classname = classname[lastdot+1:] 852 else: 853 import __main__ as mod 854 class_ = getattr(mod, classname) 855 proxy = class_((options.localhost, options.localport), 856 (options.remotehost, options.remoteport), 857 options.size_limit, enable_SMTPUTF8=options.enable_SMTPUTF8) 858 if options.setuid: 859 try: 860 import pwd 861 except ImportError: 862 print('Cannot import module "pwd"; try running with -n option.', file=sys.stderr) 863 sys.exit(1) 864 nobody = pwd.getpwnam('nobody')[2] 865 try: 866 os.setuid(nobody) 867 except PermissionError: 868 print('Cannot setuid "nobody"; try running with -n option.', file=sys.stderr) 869 sys.exit(1) 870 try: 871 asyncore.loop() 872 except KeyboardInterrupt: 873 pass 874