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