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