mirror of
				https://github.com/python/cpython.git
				synced 2025-11-04 11:49:12 +00:00 
			
		
		
		
	As reported in #117847 and #115366, an unpaired backtick in a docstring tends to confuse e.g. Sphinx running on subclasses of standard library objects, and the typographic style of using a backtick as an opening quote is no longer in favor. Convert almost all uses of the form The variable `foo' should do xyz to The variable 'foo' should do xyz and also fix up miscellaneous other unpaired backticks (extraneous / missing characters). No functional change is intended here other than in human-readable docstrings.
		
			
				
	
	
		
			873 lines
		
	
	
	
		
			30 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable file
		
	
	
	
	
			
		
		
	
	
			873 lines
		
	
	
	
		
			30 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable file
		
	
	
	
	
#! /usr/bin/env python3
 | 
						|
"""An RFC 5321 smtp proxy with optional RFC 1870 and RFC 6531 extensions.
 | 
						|
 | 
						|
Usage: %(program)s [options] [localhost:localport [remotehost:remoteport]]
 | 
						|
 | 
						|
Options:
 | 
						|
 | 
						|
    --nosetuid
 | 
						|
    -n
 | 
						|
        This program generally tries to setuid 'nobody', unless this flag is
 | 
						|
        set.  The setuid call will fail if this program is not run as root (in
 | 
						|
        which case, use this flag).
 | 
						|
 | 
						|
    --version
 | 
						|
    -V
 | 
						|
        Print the version number and exit.
 | 
						|
 | 
						|
    --class classname
 | 
						|
    -c classname
 | 
						|
        Use 'classname' as the concrete SMTP proxy class.  Uses 'PureProxy' by
 | 
						|
        default.
 | 
						|
 | 
						|
    --size limit
 | 
						|
    -s limit
 | 
						|
        Restrict the total size of the incoming message to "limit" number of
 | 
						|
        bytes via the RFC 1870 SIZE extension.  Defaults to 33554432 bytes.
 | 
						|
 | 
						|
    --smtputf8
 | 
						|
    -u
 | 
						|
        Enable the SMTPUTF8 extension and behave as an RFC 6531 smtp proxy.
 | 
						|
 | 
						|
    --debug
 | 
						|
    -d
 | 
						|
        Turn on debugging prints.
 | 
						|
 | 
						|
    --help
 | 
						|
    -h
 | 
						|
        Print this message and exit.
 | 
						|
 | 
						|
Version: %(__version__)s
 | 
						|
 | 
						|
If localhost is not given then 'localhost' is used, and if localport is not
 | 
						|
given then 8025 is used.  If remotehost is not given then 'localhost' is used,
 | 
						|
and if remoteport is not given, then 25 is used.
 | 
						|
"""
 | 
						|
 | 
						|
# Overview:
 | 
						|
#
 | 
						|
# This file implements the minimal SMTP protocol as defined in RFC 5321.  It
 | 
						|
# has a hierarchy of classes which implement the backend functionality for the
 | 
						|
# smtpd.  A number of classes are provided:
 | 
						|
#
 | 
						|
#   SMTPServer - the base class for the backend.  Raises NotImplementedError
 | 
						|
#   if you try to use it.
 | 
						|
#
 | 
						|
#   DebuggingServer - simply prints each message it receives on stdout.
 | 
						|
#
 | 
						|
#   PureProxy - Proxies all messages to a real smtpd which does final
 | 
						|
#   delivery.  One known problem with this class is that it doesn't handle
 | 
						|
#   SMTP errors from the backend server at all.  This should be fixed
 | 
						|
#   (contributions are welcome!).
 | 
						|
#
 | 
						|
#
 | 
						|
# Author: Barry Warsaw <barry@python.org>
 | 
						|
#
 | 
						|
# TODO:
 | 
						|
#
 | 
						|
# - support mailbox delivery
 | 
						|
# - alias files
 | 
						|
# - Handle more ESMTP extensions
 | 
						|
# - handle error codes from the backend smtpd
 | 
						|
 | 
						|
import sys
 | 
						|
import os
 | 
						|
import errno
 | 
						|
import getopt
 | 
						|
import time
 | 
						|
import socket
 | 
						|
import collections
 | 
						|
from test.support import asyncore, asynchat
 | 
						|
from warnings import warn
 | 
						|
from email._header_value_parser import get_addr_spec, get_angle_addr
 | 
						|
 | 
						|
__all__ = [
 | 
						|
    "SMTPChannel", "SMTPServer", "DebuggingServer", "PureProxy",
 | 
						|
]
 | 
						|
 | 
						|
program = sys.argv[0]
 | 
						|
__version__ = 'Python SMTP proxy version 0.3'
 | 
						|
 | 
						|
 | 
						|
class Devnull:
 | 
						|
    def write(self, msg): pass
 | 
						|
    def flush(self): pass
 | 
						|
 | 
						|
 | 
						|
DEBUGSTREAM = Devnull()
 | 
						|
NEWLINE = '\n'
 | 
						|
COMMASPACE = ', '
 | 
						|
DATA_SIZE_DEFAULT = 33554432
 | 
						|
 | 
						|
 | 
						|
def usage(code, msg=''):
 | 
						|
    print(__doc__ % globals(), file=sys.stderr)
 | 
						|
    if msg:
 | 
						|
        print(msg, file=sys.stderr)
 | 
						|
    sys.exit(code)
 | 
						|
 | 
						|
 | 
						|
class SMTPChannel(asynchat.async_chat):
 | 
						|
    COMMAND = 0
 | 
						|
    DATA = 1
 | 
						|
 | 
						|
    command_size_limit = 512
 | 
						|
    command_size_limits = collections.defaultdict(lambda x=command_size_limit: x)
 | 
						|
 | 
						|
    @property
 | 
						|
    def max_command_size_limit(self):
 | 
						|
        try:
 | 
						|
            return max(self.command_size_limits.values())
 | 
						|
        except ValueError:
 | 
						|
            return self.command_size_limit
 | 
						|
 | 
						|
    def __init__(self, server, conn, addr, data_size_limit=DATA_SIZE_DEFAULT,
 | 
						|
                 map=None, enable_SMTPUTF8=False, decode_data=False):
 | 
						|
        asynchat.async_chat.__init__(self, conn, map=map)
 | 
						|
        self.smtp_server = server
 | 
						|
        self.conn = conn
 | 
						|
        self.addr = addr
 | 
						|
        self.data_size_limit = data_size_limit
 | 
						|
        self.enable_SMTPUTF8 = enable_SMTPUTF8
 | 
						|
        self._decode_data = decode_data
 | 
						|
        if enable_SMTPUTF8 and decode_data:
 | 
						|
            raise ValueError("decode_data and enable_SMTPUTF8 cannot"
 | 
						|
                             " be set to True at the same time")
 | 
						|
        if decode_data:
 | 
						|
            self._emptystring = ''
 | 
						|
            self._linesep = '\r\n'
 | 
						|
            self._dotsep = '.'
 | 
						|
            self._newline = NEWLINE
 | 
						|
        else:
 | 
						|
            self._emptystring = b''
 | 
						|
            self._linesep = b'\r\n'
 | 
						|
            self._dotsep = ord(b'.')
 | 
						|
            self._newline = b'\n'
 | 
						|
        self._set_rset_state()
 | 
						|
        self.seen_greeting = ''
 | 
						|
        self.extended_smtp = False
 | 
						|
        self.command_size_limits.clear()
 | 
						|
        self.fqdn = socket.getfqdn()
 | 
						|
        try:
 | 
						|
            self.peer = conn.getpeername()
 | 
						|
        except OSError as err:
 | 
						|
            # a race condition  may occur if the other end is closing
 | 
						|
            # before we can get the peername
 | 
						|
            self.close()
 | 
						|
            if err.errno != errno.ENOTCONN:
 | 
						|
                raise
 | 
						|
            return
 | 
						|
        print('Peer:', repr(self.peer), file=DEBUGSTREAM)
 | 
						|
        self.push('220 %s %s' % (self.fqdn, __version__))
 | 
						|
 | 
						|
    def _set_post_data_state(self):
 | 
						|
        """Reset state variables to their post-DATA state."""
 | 
						|
        self.smtp_state = self.COMMAND
 | 
						|
        self.mailfrom = None
 | 
						|
        self.rcpttos = []
 | 
						|
        self.require_SMTPUTF8 = False
 | 
						|
        self.num_bytes = 0
 | 
						|
        self.set_terminator(b'\r\n')
 | 
						|
 | 
						|
    def _set_rset_state(self):
 | 
						|
        """Reset all state variables except the greeting."""
 | 
						|
        self._set_post_data_state()
 | 
						|
        self.received_data = ''
 | 
						|
        self.received_lines = []
 | 
						|
 | 
						|
 | 
						|
    # properties for backwards-compatibility
 | 
						|
    @property
 | 
						|
    def __server(self):
 | 
						|
        warn("Access to __server attribute on SMTPChannel is deprecated, "
 | 
						|
            "use 'smtp_server' instead", DeprecationWarning, 2)
 | 
						|
        return self.smtp_server
 | 
						|
    @__server.setter
 | 
						|
    def __server(self, value):
 | 
						|
        warn("Setting __server attribute on SMTPChannel is deprecated, "
 | 
						|
            "set 'smtp_server' instead", DeprecationWarning, 2)
 | 
						|
        self.smtp_server = value
 | 
						|
 | 
						|
    @property
 | 
						|
    def __line(self):
 | 
						|
        warn("Access to __line attribute on SMTPChannel is deprecated, "
 | 
						|
            "use 'received_lines' instead", DeprecationWarning, 2)
 | 
						|
        return self.received_lines
 | 
						|
    @__line.setter
 | 
						|
    def __line(self, value):
 | 
						|
        warn("Setting __line attribute on SMTPChannel is deprecated, "
 | 
						|
            "set 'received_lines' instead", DeprecationWarning, 2)
 | 
						|
        self.received_lines = value
 | 
						|
 | 
						|
    @property
 | 
						|
    def __state(self):
 | 
						|
        warn("Access to __state attribute on SMTPChannel is deprecated, "
 | 
						|
            "use 'smtp_state' instead", DeprecationWarning, 2)
 | 
						|
        return self.smtp_state
 | 
						|
    @__state.setter
 | 
						|
    def __state(self, value):
 | 
						|
        warn("Setting __state attribute on SMTPChannel is deprecated, "
 | 
						|
            "set 'smtp_state' instead", DeprecationWarning, 2)
 | 
						|
        self.smtp_state = value
 | 
						|
 | 
						|
    @property
 | 
						|
    def __greeting(self):
 | 
						|
        warn("Access to __greeting attribute on SMTPChannel is deprecated, "
 | 
						|
            "use 'seen_greeting' instead", DeprecationWarning, 2)
 | 
						|
        return self.seen_greeting
 | 
						|
    @__greeting.setter
 | 
						|
    def __greeting(self, value):
 | 
						|
        warn("Setting __greeting attribute on SMTPChannel is deprecated, "
 | 
						|
            "set 'seen_greeting' instead", DeprecationWarning, 2)
 | 
						|
        self.seen_greeting = value
 | 
						|
 | 
						|
    @property
 | 
						|
    def __mailfrom(self):
 | 
						|
        warn("Access to __mailfrom attribute on SMTPChannel is deprecated, "
 | 
						|
            "use 'mailfrom' instead", DeprecationWarning, 2)
 | 
						|
        return self.mailfrom
 | 
						|
    @__mailfrom.setter
 | 
						|
    def __mailfrom(self, value):
 | 
						|
        warn("Setting __mailfrom attribute on SMTPChannel is deprecated, "
 | 
						|
            "set 'mailfrom' instead", DeprecationWarning, 2)
 | 
						|
        self.mailfrom = value
 | 
						|
 | 
						|
    @property
 | 
						|
    def __rcpttos(self):
 | 
						|
        warn("Access to __rcpttos attribute on SMTPChannel is deprecated, "
 | 
						|
            "use 'rcpttos' instead", DeprecationWarning, 2)
 | 
						|
        return self.rcpttos
 | 
						|
    @__rcpttos.setter
 | 
						|
    def __rcpttos(self, value):
 | 
						|
        warn("Setting __rcpttos attribute on SMTPChannel is deprecated, "
 | 
						|
            "set 'rcpttos' instead", DeprecationWarning, 2)
 | 
						|
        self.rcpttos = value
 | 
						|
 | 
						|
    @property
 | 
						|
    def __data(self):
 | 
						|
        warn("Access to __data attribute on SMTPChannel is deprecated, "
 | 
						|
            "use 'received_data' instead", DeprecationWarning, 2)
 | 
						|
        return self.received_data
 | 
						|
    @__data.setter
 | 
						|
    def __data(self, value):
 | 
						|
        warn("Setting __data attribute on SMTPChannel is deprecated, "
 | 
						|
            "set 'received_data' instead", DeprecationWarning, 2)
 | 
						|
        self.received_data = value
 | 
						|
 | 
						|
    @property
 | 
						|
    def __fqdn(self):
 | 
						|
        warn("Access to __fqdn attribute on SMTPChannel is deprecated, "
 | 
						|
            "use 'fqdn' instead", DeprecationWarning, 2)
 | 
						|
        return self.fqdn
 | 
						|
    @__fqdn.setter
 | 
						|
    def __fqdn(self, value):
 | 
						|
        warn("Setting __fqdn attribute on SMTPChannel is deprecated, "
 | 
						|
            "set 'fqdn' instead", DeprecationWarning, 2)
 | 
						|
        self.fqdn = value
 | 
						|
 | 
						|
    @property
 | 
						|
    def __peer(self):
 | 
						|
        warn("Access to __peer attribute on SMTPChannel is deprecated, "
 | 
						|
            "use 'peer' instead", DeprecationWarning, 2)
 | 
						|
        return self.peer
 | 
						|
    @__peer.setter
 | 
						|
    def __peer(self, value):
 | 
						|
        warn("Setting __peer attribute on SMTPChannel is deprecated, "
 | 
						|
            "set 'peer' instead", DeprecationWarning, 2)
 | 
						|
        self.peer = value
 | 
						|
 | 
						|
    @property
 | 
						|
    def __conn(self):
 | 
						|
        warn("Access to __conn attribute on SMTPChannel is deprecated, "
 | 
						|
            "use 'conn' instead", DeprecationWarning, 2)
 | 
						|
        return self.conn
 | 
						|
    @__conn.setter
 | 
						|
    def __conn(self, value):
 | 
						|
        warn("Setting __conn attribute on SMTPChannel is deprecated, "
 | 
						|
            "set 'conn' instead", DeprecationWarning, 2)
 | 
						|
        self.conn = value
 | 
						|
 | 
						|
    @property
 | 
						|
    def __addr(self):
 | 
						|
        warn("Access to __addr attribute on SMTPChannel is deprecated, "
 | 
						|
            "use 'addr' instead", DeprecationWarning, 2)
 | 
						|
        return self.addr
 | 
						|
    @__addr.setter
 | 
						|
    def __addr(self, value):
 | 
						|
        warn("Setting __addr attribute on SMTPChannel is deprecated, "
 | 
						|
            "set 'addr' instead", DeprecationWarning, 2)
 | 
						|
        self.addr = value
 | 
						|
 | 
						|
    # Overrides base class for convenience.
 | 
						|
    def push(self, msg):
 | 
						|
        asynchat.async_chat.push(self, bytes(
 | 
						|
            msg + '\r\n', 'utf-8' if self.require_SMTPUTF8 else 'ascii'))
 | 
						|
 | 
						|
    # Implementation of base class abstract method
 | 
						|
    def collect_incoming_data(self, data):
 | 
						|
        limit = None
 | 
						|
        if self.smtp_state == self.COMMAND:
 | 
						|
            limit = self.max_command_size_limit
 | 
						|
        elif self.smtp_state == self.DATA:
 | 
						|
            limit = self.data_size_limit
 | 
						|
        if limit and self.num_bytes > limit:
 | 
						|
            return
 | 
						|
        elif limit:
 | 
						|
            self.num_bytes += len(data)
 | 
						|
        if self._decode_data:
 | 
						|
            self.received_lines.append(str(data, 'utf-8'))
 | 
						|
        else:
 | 
						|
            self.received_lines.append(data)
 | 
						|
 | 
						|
    # Implementation of base class abstract method
 | 
						|
    def found_terminator(self):
 | 
						|
        line = self._emptystring.join(self.received_lines)
 | 
						|
        print('Data:', repr(line), file=DEBUGSTREAM)
 | 
						|
        self.received_lines = []
 | 
						|
        if self.smtp_state == self.COMMAND:
 | 
						|
            sz, self.num_bytes = self.num_bytes, 0
 | 
						|
            if not line:
 | 
						|
                self.push('500 Error: bad syntax')
 | 
						|
                return
 | 
						|
            if not self._decode_data:
 | 
						|
                line = str(line, 'utf-8')
 | 
						|
            i = line.find(' ')
 | 
						|
            if i < 0:
 | 
						|
                command = line.upper()
 | 
						|
                arg = None
 | 
						|
            else:
 | 
						|
                command = line[:i].upper()
 | 
						|
                arg = line[i+1:].strip()
 | 
						|
            max_sz = (self.command_size_limits[command]
 | 
						|
                        if self.extended_smtp else self.command_size_limit)
 | 
						|
            if sz > max_sz:
 | 
						|
                self.push('500 Error: line too long')
 | 
						|
                return
 | 
						|
            method = getattr(self, 'smtp_' + command, None)
 | 
						|
            if not method:
 | 
						|
                self.push('500 Error: command "%s" not recognized' % command)
 | 
						|
                return
 | 
						|
            method(arg)
 | 
						|
            return
 | 
						|
        else:
 | 
						|
            if self.smtp_state != self.DATA:
 | 
						|
                self.push('451 Internal confusion')
 | 
						|
                self.num_bytes = 0
 | 
						|
                return
 | 
						|
            if self.data_size_limit and self.num_bytes > self.data_size_limit:
 | 
						|
                self.push('552 Error: Too much mail data')
 | 
						|
                self.num_bytes = 0
 | 
						|
                return
 | 
						|
            # Remove extraneous carriage returns and de-transparency according
 | 
						|
            # to RFC 5321, Section 4.5.2.
 | 
						|
            data = []
 | 
						|
            for text in line.split(self._linesep):
 | 
						|
                if text and text[0] == self._dotsep:
 | 
						|
                    data.append(text[1:])
 | 
						|
                else:
 | 
						|
                    data.append(text)
 | 
						|
            self.received_data = self._newline.join(data)
 | 
						|
            args = (self.peer, self.mailfrom, self.rcpttos, self.received_data)
 | 
						|
            kwargs = {}
 | 
						|
            if not self._decode_data:
 | 
						|
                kwargs = {
 | 
						|
                    'mail_options': self.mail_options,
 | 
						|
                    'rcpt_options': self.rcpt_options,
 | 
						|
                }
 | 
						|
            status = self.smtp_server.process_message(*args, **kwargs)
 | 
						|
            self._set_post_data_state()
 | 
						|
            if not status:
 | 
						|
                self.push('250 OK')
 | 
						|
            else:
 | 
						|
                self.push(status)
 | 
						|
 | 
						|
    # SMTP and ESMTP commands
 | 
						|
    def smtp_HELO(self, arg):
 | 
						|
        if not arg:
 | 
						|
            self.push('501 Syntax: HELO hostname')
 | 
						|
            return
 | 
						|
        # See issue #21783 for a discussion of this behavior.
 | 
						|
        if self.seen_greeting:
 | 
						|
            self.push('503 Duplicate HELO/EHLO')
 | 
						|
            return
 | 
						|
        self._set_rset_state()
 | 
						|
        self.seen_greeting = arg
 | 
						|
        self.push('250 %s' % self.fqdn)
 | 
						|
 | 
						|
    def smtp_EHLO(self, arg):
 | 
						|
        if not arg:
 | 
						|
            self.push('501 Syntax: EHLO hostname')
 | 
						|
            return
 | 
						|
        # See issue #21783 for a discussion of this behavior.
 | 
						|
        if self.seen_greeting:
 | 
						|
            self.push('503 Duplicate HELO/EHLO')
 | 
						|
            return
 | 
						|
        self._set_rset_state()
 | 
						|
        self.seen_greeting = arg
 | 
						|
        self.extended_smtp = True
 | 
						|
        self.push('250-%s' % self.fqdn)
 | 
						|
        if self.data_size_limit:
 | 
						|
            self.push('250-SIZE %s' % self.data_size_limit)
 | 
						|
            self.command_size_limits['MAIL'] += 26
 | 
						|
        if not self._decode_data:
 | 
						|
            self.push('250-8BITMIME')
 | 
						|
        if self.enable_SMTPUTF8:
 | 
						|
            self.push('250-SMTPUTF8')
 | 
						|
            self.command_size_limits['MAIL'] += 10
 | 
						|
        self.push('250 HELP')
 | 
						|
 | 
						|
    def smtp_NOOP(self, arg):
 | 
						|
        if arg:
 | 
						|
            self.push('501 Syntax: NOOP')
 | 
						|
        else:
 | 
						|
            self.push('250 OK')
 | 
						|
 | 
						|
    def smtp_QUIT(self, arg):
 | 
						|
        # args is ignored
 | 
						|
        self.push('221 Bye')
 | 
						|
        self.close_when_done()
 | 
						|
 | 
						|
    def _strip_command_keyword(self, keyword, arg):
 | 
						|
        keylen = len(keyword)
 | 
						|
        if arg[:keylen].upper() == keyword:
 | 
						|
            return arg[keylen:].strip()
 | 
						|
        return ''
 | 
						|
 | 
						|
    def _getaddr(self, arg):
 | 
						|
        if not arg:
 | 
						|
            return '', ''
 | 
						|
        if arg.lstrip().startswith('<'):
 | 
						|
            address, rest = get_angle_addr(arg)
 | 
						|
        else:
 | 
						|
            address, rest = get_addr_spec(arg)
 | 
						|
        if not address:
 | 
						|
            return address, rest
 | 
						|
        return address.addr_spec, rest
 | 
						|
 | 
						|
    def _getparams(self, params):
 | 
						|
        # Return params as dictionary. Return None if not all parameters
 | 
						|
        # appear to be syntactically valid according to RFC 1869.
 | 
						|
        result = {}
 | 
						|
        for param in params:
 | 
						|
            param, eq, value = param.partition('=')
 | 
						|
            if not param.isalnum() or eq and not value:
 | 
						|
                return None
 | 
						|
            result[param] = value if eq else True
 | 
						|
        return result
 | 
						|
 | 
						|
    def smtp_HELP(self, arg):
 | 
						|
        if arg:
 | 
						|
            extended = ' [SP <mail-parameters>]'
 | 
						|
            lc_arg = arg.upper()
 | 
						|
            if lc_arg == 'EHLO':
 | 
						|
                self.push('250 Syntax: EHLO hostname')
 | 
						|
            elif lc_arg == 'HELO':
 | 
						|
                self.push('250 Syntax: HELO hostname')
 | 
						|
            elif lc_arg == 'MAIL':
 | 
						|
                msg = '250 Syntax: MAIL FROM: <address>'
 | 
						|
                if self.extended_smtp:
 | 
						|
                    msg += extended
 | 
						|
                self.push(msg)
 | 
						|
            elif lc_arg == 'RCPT':
 | 
						|
                msg = '250 Syntax: RCPT TO: <address>'
 | 
						|
                if self.extended_smtp:
 | 
						|
                    msg += extended
 | 
						|
                self.push(msg)
 | 
						|
            elif lc_arg == 'DATA':
 | 
						|
                self.push('250 Syntax: DATA')
 | 
						|
            elif lc_arg == 'RSET':
 | 
						|
                self.push('250 Syntax: RSET')
 | 
						|
            elif lc_arg == 'NOOP':
 | 
						|
                self.push('250 Syntax: NOOP')
 | 
						|
            elif lc_arg == 'QUIT':
 | 
						|
                self.push('250 Syntax: QUIT')
 | 
						|
            elif lc_arg == 'VRFY':
 | 
						|
                self.push('250 Syntax: VRFY <address>')
 | 
						|
            else:
 | 
						|
                self.push('501 Supported commands: EHLO HELO MAIL RCPT '
 | 
						|
                          'DATA RSET NOOP QUIT VRFY')
 | 
						|
        else:
 | 
						|
            self.push('250 Supported commands: EHLO HELO MAIL RCPT DATA '
 | 
						|
                      'RSET NOOP QUIT VRFY')
 | 
						|
 | 
						|
    def smtp_VRFY(self, arg):
 | 
						|
        if arg:
 | 
						|
            address, params = self._getaddr(arg)
 | 
						|
            if address:
 | 
						|
                self.push('252 Cannot VRFY user, but will accept message '
 | 
						|
                          'and attempt delivery')
 | 
						|
            else:
 | 
						|
                self.push('502 Could not VRFY %s' % arg)
 | 
						|
        else:
 | 
						|
            self.push('501 Syntax: VRFY <address>')
 | 
						|
 | 
						|
    def smtp_MAIL(self, arg):
 | 
						|
        if not self.seen_greeting:
 | 
						|
            self.push('503 Error: send HELO first')
 | 
						|
            return
 | 
						|
        print('===> MAIL', arg, file=DEBUGSTREAM)
 | 
						|
        syntaxerr = '501 Syntax: MAIL FROM: <address>'
 | 
						|
        if self.extended_smtp:
 | 
						|
            syntaxerr += ' [SP <mail-parameters>]'
 | 
						|
        if arg is None:
 | 
						|
            self.push(syntaxerr)
 | 
						|
            return
 | 
						|
        arg = self._strip_command_keyword('FROM:', arg)
 | 
						|
        address, params = self._getaddr(arg)
 | 
						|
        if not address:
 | 
						|
            self.push(syntaxerr)
 | 
						|
            return
 | 
						|
        if not self.extended_smtp and params:
 | 
						|
            self.push(syntaxerr)
 | 
						|
            return
 | 
						|
        if self.mailfrom:
 | 
						|
            self.push('503 Error: nested MAIL command')
 | 
						|
            return
 | 
						|
        self.mail_options = params.upper().split()
 | 
						|
        params = self._getparams(self.mail_options)
 | 
						|
        if params is None:
 | 
						|
            self.push(syntaxerr)
 | 
						|
            return
 | 
						|
        if not self._decode_data:
 | 
						|
            body = params.pop('BODY', '7BIT')
 | 
						|
            if body not in ['7BIT', '8BITMIME']:
 | 
						|
                self.push('501 Error: BODY can only be one of 7BIT, 8BITMIME')
 | 
						|
                return
 | 
						|
        if self.enable_SMTPUTF8:
 | 
						|
            smtputf8 = params.pop('SMTPUTF8', False)
 | 
						|
            if smtputf8 is True:
 | 
						|
                self.require_SMTPUTF8 = True
 | 
						|
            elif smtputf8 is not False:
 | 
						|
                self.push('501 Error: SMTPUTF8 takes no arguments')
 | 
						|
                return
 | 
						|
        size = params.pop('SIZE', None)
 | 
						|
        if size:
 | 
						|
            if not size.isdigit():
 | 
						|
                self.push(syntaxerr)
 | 
						|
                return
 | 
						|
            elif self.data_size_limit and int(size) > self.data_size_limit:
 | 
						|
                self.push('552 Error: message size exceeds fixed maximum message size')
 | 
						|
                return
 | 
						|
        if len(params.keys()) > 0:
 | 
						|
            self.push('555 MAIL FROM parameters not recognized or not implemented')
 | 
						|
            return
 | 
						|
        self.mailfrom = address
 | 
						|
        print('sender:', self.mailfrom, file=DEBUGSTREAM)
 | 
						|
        self.push('250 OK')
 | 
						|
 | 
						|
    def smtp_RCPT(self, arg):
 | 
						|
        if not self.seen_greeting:
 | 
						|
            self.push('503 Error: send HELO first');
 | 
						|
            return
 | 
						|
        print('===> RCPT', arg, file=DEBUGSTREAM)
 | 
						|
        if not self.mailfrom:
 | 
						|
            self.push('503 Error: need MAIL command')
 | 
						|
            return
 | 
						|
        syntaxerr = '501 Syntax: RCPT TO: <address>'
 | 
						|
        if self.extended_smtp:
 | 
						|
            syntaxerr += ' [SP <mail-parameters>]'
 | 
						|
        if arg is None:
 | 
						|
            self.push(syntaxerr)
 | 
						|
            return
 | 
						|
        arg = self._strip_command_keyword('TO:', arg)
 | 
						|
        address, params = self._getaddr(arg)
 | 
						|
        if not address:
 | 
						|
            self.push(syntaxerr)
 | 
						|
            return
 | 
						|
        if not self.extended_smtp and params:
 | 
						|
            self.push(syntaxerr)
 | 
						|
            return
 | 
						|
        self.rcpt_options = params.upper().split()
 | 
						|
        params = self._getparams(self.rcpt_options)
 | 
						|
        if params is None:
 | 
						|
            self.push(syntaxerr)
 | 
						|
            return
 | 
						|
        # XXX currently there are no options we recognize.
 | 
						|
        if len(params.keys()) > 0:
 | 
						|
            self.push('555 RCPT TO parameters not recognized or not implemented')
 | 
						|
            return
 | 
						|
        self.rcpttos.append(address)
 | 
						|
        print('recips:', self.rcpttos, file=DEBUGSTREAM)
 | 
						|
        self.push('250 OK')
 | 
						|
 | 
						|
    def smtp_RSET(self, arg):
 | 
						|
        if arg:
 | 
						|
            self.push('501 Syntax: RSET')
 | 
						|
            return
 | 
						|
        self._set_rset_state()
 | 
						|
        self.push('250 OK')
 | 
						|
 | 
						|
    def smtp_DATA(self, arg):
 | 
						|
        if not self.seen_greeting:
 | 
						|
            self.push('503 Error: send HELO first');
 | 
						|
            return
 | 
						|
        if not self.rcpttos:
 | 
						|
            self.push('503 Error: need RCPT command')
 | 
						|
            return
 | 
						|
        if arg:
 | 
						|
            self.push('501 Syntax: DATA')
 | 
						|
            return
 | 
						|
        self.smtp_state = self.DATA
 | 
						|
        self.set_terminator(b'\r\n.\r\n')
 | 
						|
        self.push('354 End data with <CR><LF>.<CR><LF>')
 | 
						|
 | 
						|
    # Commands that have not been implemented
 | 
						|
    def smtp_EXPN(self, arg):
 | 
						|
        self.push('502 EXPN not implemented')
 | 
						|
 | 
						|
 | 
						|
class SMTPServer(asyncore.dispatcher):
 | 
						|
    # SMTPChannel class to use for managing client connections
 | 
						|
    channel_class = SMTPChannel
 | 
						|
 | 
						|
    def __init__(self, localaddr, remoteaddr,
 | 
						|
                 data_size_limit=DATA_SIZE_DEFAULT, map=None,
 | 
						|
                 enable_SMTPUTF8=False, decode_data=False):
 | 
						|
        self._localaddr = localaddr
 | 
						|
        self._remoteaddr = remoteaddr
 | 
						|
        self.data_size_limit = data_size_limit
 | 
						|
        self.enable_SMTPUTF8 = enable_SMTPUTF8
 | 
						|
        self._decode_data = decode_data
 | 
						|
        if enable_SMTPUTF8 and decode_data:
 | 
						|
            raise ValueError("decode_data and enable_SMTPUTF8 cannot"
 | 
						|
                             " be set to True at the same time")
 | 
						|
        asyncore.dispatcher.__init__(self, map=map)
 | 
						|
        try:
 | 
						|
            gai_results = socket.getaddrinfo(*localaddr,
 | 
						|
                                             type=socket.SOCK_STREAM)
 | 
						|
            self.create_socket(gai_results[0][0], gai_results[0][1])
 | 
						|
            # try to re-use a server port if possible
 | 
						|
            self.set_reuse_addr()
 | 
						|
            self.bind(localaddr)
 | 
						|
            self.listen(5)
 | 
						|
        except:
 | 
						|
            self.close()
 | 
						|
            raise
 | 
						|
        else:
 | 
						|
            print('%s started at %s\n\tLocal addr: %s\n\tRemote addr:%s' % (
 | 
						|
                self.__class__.__name__, time.ctime(time.time()),
 | 
						|
                localaddr, remoteaddr), file=DEBUGSTREAM)
 | 
						|
 | 
						|
    def handle_accepted(self, conn, addr):
 | 
						|
        print('Incoming connection from %s' % repr(addr), file=DEBUGSTREAM)
 | 
						|
        channel = self.channel_class(self,
 | 
						|
                                     conn,
 | 
						|
                                     addr,
 | 
						|
                                     self.data_size_limit,
 | 
						|
                                     self._map,
 | 
						|
                                     self.enable_SMTPUTF8,
 | 
						|
                                     self._decode_data)
 | 
						|
 | 
						|
    # API for "doing something useful with the message"
 | 
						|
    def process_message(self, peer, mailfrom, rcpttos, data, **kwargs):
 | 
						|
        """Override this abstract method to handle messages from the client.
 | 
						|
 | 
						|
        peer is a tuple containing (ipaddr, port) of the client that made the
 | 
						|
        socket connection to our smtp port.
 | 
						|
 | 
						|
        mailfrom is the raw address the client claims the message is coming
 | 
						|
        from.
 | 
						|
 | 
						|
        rcpttos is a list of raw addresses the client wishes to deliver the
 | 
						|
        message to.
 | 
						|
 | 
						|
        data is a string containing the entire full text of the message,
 | 
						|
        headers (if supplied) and all.  It has been 'de-transparencied'
 | 
						|
        according to RFC 821, Section 4.5.2.  In other words, a line
 | 
						|
        containing a '.' followed by other text has had the leading dot
 | 
						|
        removed.
 | 
						|
 | 
						|
        kwargs is a dictionary containing additional information.  It is
 | 
						|
        empty if decode_data=True was given as init parameter, otherwise
 | 
						|
        it will contain the following keys:
 | 
						|
            'mail_options': list of parameters to the mail command.  All
 | 
						|
                            elements are uppercase strings.  Example:
 | 
						|
                            ['BODY=8BITMIME', 'SMTPUTF8'].
 | 
						|
            'rcpt_options': same, for the rcpt command.
 | 
						|
 | 
						|
        This function should return None for a normal '250 Ok' response;
 | 
						|
        otherwise, it should return the desired response string in RFC 821
 | 
						|
        format.
 | 
						|
 | 
						|
        """
 | 
						|
        raise NotImplementedError
 | 
						|
 | 
						|
 | 
						|
class DebuggingServer(SMTPServer):
 | 
						|
 | 
						|
    def _print_message_content(self, peer, data):
 | 
						|
        inheaders = 1
 | 
						|
        lines = data.splitlines()
 | 
						|
        for line in lines:
 | 
						|
            # headers first
 | 
						|
            if inheaders and not line:
 | 
						|
                peerheader = 'X-Peer: ' + peer[0]
 | 
						|
                if not isinstance(data, str):
 | 
						|
                    # decoded_data=false; make header match other binary output
 | 
						|
                    peerheader = repr(peerheader.encode('utf-8'))
 | 
						|
                print(peerheader)
 | 
						|
                inheaders = 0
 | 
						|
            if not isinstance(data, str):
 | 
						|
                # Avoid spurious 'str on bytes instance' warning.
 | 
						|
                line = repr(line)
 | 
						|
            print(line)
 | 
						|
 | 
						|
    def process_message(self, peer, mailfrom, rcpttos, data, **kwargs):
 | 
						|
        print('---------- MESSAGE FOLLOWS ----------')
 | 
						|
        if kwargs:
 | 
						|
            if kwargs.get('mail_options'):
 | 
						|
                print('mail options: %s' % kwargs['mail_options'])
 | 
						|
            if kwargs.get('rcpt_options'):
 | 
						|
                print('rcpt options: %s\n' % kwargs['rcpt_options'])
 | 
						|
        self._print_message_content(peer, data)
 | 
						|
        print('------------ END MESSAGE ------------')
 | 
						|
 | 
						|
 | 
						|
class PureProxy(SMTPServer):
 | 
						|
    def __init__(self, *args, **kwargs):
 | 
						|
        if 'enable_SMTPUTF8' in kwargs and kwargs['enable_SMTPUTF8']:
 | 
						|
            raise ValueError("PureProxy does not support SMTPUTF8.")
 | 
						|
        super(PureProxy, self).__init__(*args, **kwargs)
 | 
						|
 | 
						|
    def process_message(self, peer, mailfrom, rcpttos, data):
 | 
						|
        lines = data.split('\n')
 | 
						|
        # Look for the last header
 | 
						|
        i = 0
 | 
						|
        for line in lines:
 | 
						|
            if not line:
 | 
						|
                break
 | 
						|
            i += 1
 | 
						|
        lines.insert(i, 'X-Peer: %s' % peer[0])
 | 
						|
        data = NEWLINE.join(lines)
 | 
						|
        refused = self._deliver(mailfrom, rcpttos, data)
 | 
						|
        # TBD: what to do with refused addresses?
 | 
						|
        print('we got some refusals:', refused, file=DEBUGSTREAM)
 | 
						|
 | 
						|
    def _deliver(self, mailfrom, rcpttos, data):
 | 
						|
        import smtplib
 | 
						|
        refused = {}
 | 
						|
        try:
 | 
						|
            s = smtplib.SMTP()
 | 
						|
            s.connect(self._remoteaddr[0], self._remoteaddr[1])
 | 
						|
            try:
 | 
						|
                refused = s.sendmail(mailfrom, rcpttos, data)
 | 
						|
            finally:
 | 
						|
                s.quit()
 | 
						|
        except smtplib.SMTPRecipientsRefused as e:
 | 
						|
            print('got SMTPRecipientsRefused', file=DEBUGSTREAM)
 | 
						|
            refused = e.recipients
 | 
						|
        except (OSError, smtplib.SMTPException) as e:
 | 
						|
            print('got', e.__class__, file=DEBUGSTREAM)
 | 
						|
            # All recipients were refused.  If the exception had an associated
 | 
						|
            # error code, use it.  Otherwise,fake it with a non-triggering
 | 
						|
            # exception code.
 | 
						|
            errcode = getattr(e, 'smtp_code', -1)
 | 
						|
            errmsg = getattr(e, 'smtp_error', 'ignore')
 | 
						|
            for r in rcpttos:
 | 
						|
                refused[r] = (errcode, errmsg)
 | 
						|
        return refused
 | 
						|
 | 
						|
 | 
						|
class Options:
 | 
						|
    setuid = True
 | 
						|
    classname = 'PureProxy'
 | 
						|
    size_limit = None
 | 
						|
    enable_SMTPUTF8 = False
 | 
						|
 | 
						|
 | 
						|
def parseargs():
 | 
						|
    global DEBUGSTREAM
 | 
						|
    try:
 | 
						|
        opts, args = getopt.getopt(
 | 
						|
            sys.argv[1:], 'nVhc:s:du',
 | 
						|
            ['class=', 'nosetuid', 'version', 'help', 'size=', 'debug',
 | 
						|
             'smtputf8'])
 | 
						|
    except getopt.error as e:
 | 
						|
        usage(1, e)
 | 
						|
 | 
						|
    options = Options()
 | 
						|
    for opt, arg in opts:
 | 
						|
        if opt in ('-h', '--help'):
 | 
						|
            usage(0)
 | 
						|
        elif opt in ('-V', '--version'):
 | 
						|
            print(__version__)
 | 
						|
            sys.exit(0)
 | 
						|
        elif opt in ('-n', '--nosetuid'):
 | 
						|
            options.setuid = False
 | 
						|
        elif opt in ('-c', '--class'):
 | 
						|
            options.classname = arg
 | 
						|
        elif opt in ('-d', '--debug'):
 | 
						|
            DEBUGSTREAM = sys.stderr
 | 
						|
        elif opt in ('-u', '--smtputf8'):
 | 
						|
            options.enable_SMTPUTF8 = True
 | 
						|
        elif opt in ('-s', '--size'):
 | 
						|
            try:
 | 
						|
                int_size = int(arg)
 | 
						|
                options.size_limit = int_size
 | 
						|
            except:
 | 
						|
                print('Invalid size: ' + arg, file=sys.stderr)
 | 
						|
                sys.exit(1)
 | 
						|
 | 
						|
    # parse the rest of the arguments
 | 
						|
    if len(args) < 1:
 | 
						|
        localspec = 'localhost:8025'
 | 
						|
        remotespec = 'localhost:25'
 | 
						|
    elif len(args) < 2:
 | 
						|
        localspec = args[0]
 | 
						|
        remotespec = 'localhost:25'
 | 
						|
    elif len(args) < 3:
 | 
						|
        localspec = args[0]
 | 
						|
        remotespec = args[1]
 | 
						|
    else:
 | 
						|
        usage(1, 'Invalid arguments: %s' % COMMASPACE.join(args))
 | 
						|
 | 
						|
    # split into host/port pairs
 | 
						|
    i = localspec.find(':')
 | 
						|
    if i < 0:
 | 
						|
        usage(1, 'Bad local spec: %s' % localspec)
 | 
						|
    options.localhost = localspec[:i]
 | 
						|
    try:
 | 
						|
        options.localport = int(localspec[i+1:])
 | 
						|
    except ValueError:
 | 
						|
        usage(1, 'Bad local port: %s' % localspec)
 | 
						|
    i = remotespec.find(':')
 | 
						|
    if i < 0:
 | 
						|
        usage(1, 'Bad remote spec: %s' % remotespec)
 | 
						|
    options.remotehost = remotespec[:i]
 | 
						|
    try:
 | 
						|
        options.remoteport = int(remotespec[i+1:])
 | 
						|
    except ValueError:
 | 
						|
        usage(1, 'Bad remote port: %s' % remotespec)
 | 
						|
    return options
 | 
						|
 | 
						|
 | 
						|
if __name__ == '__main__':
 | 
						|
    options = parseargs()
 | 
						|
    # Become nobody
 | 
						|
    classname = options.classname
 | 
						|
    if "." in classname:
 | 
						|
        lastdot = classname.rfind(".")
 | 
						|
        mod = __import__(classname[:lastdot], globals(), locals(), [""])
 | 
						|
        classname = classname[lastdot+1:]
 | 
						|
    else:
 | 
						|
        import __main__ as mod
 | 
						|
    class_ = getattr(mod, classname)
 | 
						|
    proxy = class_((options.localhost, options.localport),
 | 
						|
                   (options.remotehost, options.remoteport),
 | 
						|
                   options.size_limit, enable_SMTPUTF8=options.enable_SMTPUTF8)
 | 
						|
    if options.setuid:
 | 
						|
        try:
 | 
						|
            import pwd
 | 
						|
        except ImportError:
 | 
						|
            print('Cannot import module "pwd"; try running with -n option.', file=sys.stderr)
 | 
						|
            sys.exit(1)
 | 
						|
        nobody = pwd.getpwnam('nobody')[2]
 | 
						|
        try:
 | 
						|
            os.setuid(nobody)
 | 
						|
        except PermissionError:
 | 
						|
            print('Cannot setuid "nobody"; try running with -n option.', file=sys.stderr)
 | 
						|
            sys.exit(1)
 | 
						|
    try:
 | 
						|
        asyncore.loop()
 | 
						|
    except KeyboardInterrupt:
 | 
						|
        pass
 |