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