Home | History | Annotate | Download | only in Lib
      1 #! /usr/bin/env python3
      2 """An RFC 5321 smtp proxy with optional RFC 1870 and RFC 6531 extensions.
      3 
      4 Usage: %(program)s [options] [localhost:localport [remotehost:remoteport]]
      5 
      6 Options:
      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 
     40 Version: %(__version__)s
     41 
     42 If localhost is not given then `localhost' is used, and if localport is not
     43 given then 8025 is used.  If remotehost is not given then `localhost' is used,
     44 and 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 (at] 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 
     80 import sys
     81 import os
     82 import errno
     83 import getopt
     84 import time
     85 import socket
     86 import asyncore
     87 import asynchat
     88 import collections
     89 from warnings import warn
     90 from email._header_value_parser import get_addr_spec, get_angle_addr
     91 
     92 __all__ = [
     93     "SMTPChannel", "SMTPServer", "DebuggingServer", "PureProxy",
     94     "MailmanProxy",
     95 ]
     96 
     97 program = sys.argv[0]
     98 __version__ = 'Python SMTP proxy version 0.3'
     99 
    100 
    101 class Devnull:
    102     def write(self, msg): pass
    103     def flush(self): pass
    104 
    105 
    106 DEBUGSTREAM = Devnull()
    107 NEWLINE = '\n'
    108 COMMASPACE = ', '
    109 DATA_SIZE_DEFAULT = 33554432
    110 
    111 
    112 def 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 
    119 class 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 
    628 class 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 
    705 class 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 
    735 class 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 
    780 class MailmanProxy(PureProxy):
    781     def __init__(self, *args, **kwargs):
    782         if 'enable_SMTPUTF8' in kwargs and kwargs['enable_SMTPUTF8']:
    783             raise ValueError("MailmanProxy does not support SMTPUTF8.")
    784         super(PureProxy, self).__init__(*args, **kwargs)
    785 
    786     def process_message(self, peer, mailfrom, rcpttos, data):
    787         from io import StringIO
    788         from Mailman import Utils
    789         from Mailman import Message
    790         from Mailman import MailList
    791         # If the message is to a Mailman mailing list, then we'll invoke the
    792         # Mailman script directly, without going through the real smtpd.
    793         # Otherwise we'll forward it to the local proxy for disposition.
    794         listnames = []
    795         for rcpt in rcpttos:
    796             local = rcpt.lower().split('@')[0]
    797             # We allow the following variations on the theme
    798             #   listname
    799             #   listname-admin
    800             #   listname-owner
    801             #   listname-request
    802             #   listname-join
    803             #   listname-leave
    804             parts = local.split('-')
    805             if len(parts) > 2:
    806                 continue
    807             listname = parts[0]
    808             if len(parts) == 2:
    809                 command = parts[1]
    810             else:
    811                 command = ''
    812             if not Utils.list_exists(listname) or command not in (
    813                     '', 'admin', 'owner', 'request', 'join', 'leave'):
    814                 continue
    815             listnames.append((rcpt, listname, command))
    816         # Remove all list recipients from rcpttos and forward what we're not
    817         # going to take care of ourselves.  Linear removal should be fine
    818         # since we don't expect a large number of recipients.
    819         for rcpt, listname, command in listnames:
    820             rcpttos.remove(rcpt)
    821         # If there's any non-list destined recipients left,
    822         print('forwarding recips:', ' '.join(rcpttos), file=DEBUGSTREAM)
    823         if rcpttos:
    824             refused = self._deliver(mailfrom, rcpttos, data)
    825             # TBD: what to do with refused addresses?
    826             print('we got refusals:', refused, file=DEBUGSTREAM)
    827         # Now deliver directly to the list commands
    828         mlists = {}
    829         s = StringIO(data)
    830         msg = Message.Message(s)
    831         # These headers are required for the proper execution of Mailman.  All
    832         # MTAs in existence seem to add these if the original message doesn't
    833         # have them.
    834         if not msg.get('from'):
    835             msg['From'] = mailfrom
    836         if not msg.get('date'):
    837             msg['Date'] = time.ctime(time.time())
    838         for rcpt, listname, command in listnames:
    839             print('sending message to', rcpt, file=DEBUGSTREAM)
    840             mlist = mlists.get(listname)
    841             if not mlist:
    842                 mlist = MailList.MailList(listname, lock=0)
    843                 mlists[listname] = mlist
    844             # dispatch on the type of command
    845             if command == '':
    846                 # post
    847                 msg.Enqueue(mlist, tolist=1)
    848             elif command == 'admin':
    849                 msg.Enqueue(mlist, toadmin=1)
    850             elif command == 'owner':
    851                 msg.Enqueue(mlist, toowner=1)
    852             elif command == 'request':
    853                 msg.Enqueue(mlist, torequest=1)
    854             elif command in ('join', 'leave'):
    855                 # TBD: this is a hack!
    856                 if command == 'join':
    857                     msg['Subject'] = 'subscribe'
    858                 else:
    859                     msg['Subject'] = 'unsubscribe'
    860                 msg.Enqueue(mlist, torequest=1)
    861 
    862 
    863 class Options:
    864     setuid = True
    865     classname = 'PureProxy'
    866     size_limit = None
    867     enable_SMTPUTF8 = False
    868 
    869 
    870 def parseargs():
    871     global DEBUGSTREAM
    872     try:
    873         opts, args = getopt.getopt(
    874             sys.argv[1:], 'nVhc:s:du',
    875             ['class=', 'nosetuid', 'version', 'help', 'size=', 'debug',
    876              'smtputf8'])
    877     except getopt.error as e:
    878         usage(1, e)
    879 
    880     options = Options()
    881     for opt, arg in opts:
    882         if opt in ('-h', '--help'):
    883             usage(0)
    884         elif opt in ('-V', '--version'):
    885             print(__version__)
    886             sys.exit(0)
    887         elif opt in ('-n', '--nosetuid'):
    888             options.setuid = False
    889         elif opt in ('-c', '--class'):
    890             options.classname = arg
    891         elif opt in ('-d', '--debug'):
    892             DEBUGSTREAM = sys.stderr
    893         elif opt in ('-u', '--smtputf8'):
    894             options.enable_SMTPUTF8 = True
    895         elif opt in ('-s', '--size'):
    896             try:
    897                 int_size = int(arg)
    898                 options.size_limit = int_size
    899             except:
    900                 print('Invalid size: ' + arg, file=sys.stderr)
    901                 sys.exit(1)
    902 
    903     # parse the rest of the arguments
    904     if len(args) < 1:
    905         localspec = 'localhost:8025'
    906         remotespec = 'localhost:25'
    907     elif len(args) < 2:
    908         localspec = args[0]
    909         remotespec = 'localhost:25'
    910     elif len(args) < 3:
    911         localspec = args[0]
    912         remotespec = args[1]
    913     else:
    914         usage(1, 'Invalid arguments: %s' % COMMASPACE.join(args))
    915 
    916     # split into host/port pairs
    917     i = localspec.find(':')
    918     if i < 0:
    919         usage(1, 'Bad local spec: %s' % localspec)
    920     options.localhost = localspec[:i]
    921     try:
    922         options.localport = int(localspec[i+1:])
    923     except ValueError:
    924         usage(1, 'Bad local port: %s' % localspec)
    925     i = remotespec.find(':')
    926     if i < 0:
    927         usage(1, 'Bad remote spec: %s' % remotespec)
    928     options.remotehost = remotespec[:i]
    929     try:
    930         options.remoteport = int(remotespec[i+1:])
    931     except ValueError:
    932         usage(1, 'Bad remote port: %s' % remotespec)
    933     return options
    934 
    935 
    936 if __name__ == '__main__':
    937     options = parseargs()
    938     # Become nobody
    939     classname = options.classname
    940     if "." in classname:
    941         lastdot = classname.rfind(".")
    942         mod = __import__(classname[:lastdot], globals(), locals(), [""])
    943         classname = classname[lastdot+1:]
    944     else:
    945         import __main__ as mod
    946     class_ = getattr(mod, classname)
    947     proxy = class_((options.localhost, options.localport),
    948                    (options.remotehost, options.remoteport),
    949                    options.size_limit, enable_SMTPUTF8=options.enable_SMTPUTF8)
    950     if options.setuid:
    951         try:
    952             import pwd
    953         except ImportError:
    954             print('Cannot import module "pwd"; try running with -n option.', file=sys.stderr)
    955             sys.exit(1)
    956         nobody = pwd.getpwnam('nobody')[2]
    957         try:
    958             os.setuid(nobody)
    959         except PermissionError:
    960             print('Cannot setuid "nobody"; try running with -n option.', file=sys.stderr)
    961             sys.exit(1)
    962     try:
    963         asyncore.loop()
    964     except KeyboardInterrupt:
    965         pass
    966