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