diff --git a/Doc/library/smtpd.rst b/Doc/library/smtpd.rst index 2ca71ffc3cc..2c2df8ada11 100644 --- a/Doc/library/smtpd.rst +++ b/Doc/library/smtpd.rst @@ -27,7 +27,8 @@ SMTPServer Objects ------------------ -.. class:: SMTPServer(localaddr, remoteaddr, data_size_limit=33554432) +.. class:: SMTPServer(localaddr, remoteaddr, data_size_limit=33554432, + map=None) Create a new :class:`SMTPServer` object, which binds to local address *localaddr*. It will treat *remoteaddr* as an upstream SMTP relayer. It @@ -38,6 +39,8 @@ SMTPServer Objects accepted in a ``DATA`` command. A value of ``None`` or ``0`` means no limit. + A dictionary can be specified in *map* to avoid using a global socket map. + .. method:: process_message(peer, mailfrom, rcpttos, data) Raise :exc:`NotImplementedError` exception. Override this in subclasses to @@ -53,6 +56,9 @@ SMTPServer Objects Override this in subclasses to use a custom :class:`SMTPChannel` for managing SMTP clients. + .. versionchanged:: 3.4 + The *map* argument was added. + DebuggingServer Objects ----------------------- @@ -90,11 +96,20 @@ MailmanProxy Objects SMTPChannel Objects ------------------- -.. class:: SMTPChannel(server, conn, addr) +.. class:: SMTPChannel(server, conn, addr, data_size_limit=33554432, + map=None)) Create a new :class:`SMTPChannel` object which manages the communication between the server and a single SMTP client. + *conn* and *addr* are as per the instance variables described below. + + *data_size_limit* specifies the maximum number of bytes that will be + accepted in a ``DATA`` command. A value of ``None`` or ``0`` means no + limit. + + A dictionary can be specified in *map* to avoid using a global socket map. + To use a custom SMTPChannel implementation you need to override the :attr:`SMTPServer.channel_class` of your :class:`SMTPServer`. diff --git a/Lib/smtpd.py b/Lib/smtpd.py index 3cab264df8b..f28036a5b4f 100755 --- a/Lib/smtpd.py +++ b/Lib/smtpd.py @@ -121,8 +121,9 @@ class SMTPChannel(asynchat.async_chat): }) max_command_size_limit = max(command_size_limits.values()) - def __init__(self, server, conn, addr, data_size_limit=DATA_SIZE_DEFAULT): - asynchat.async_chat.__init__(self, conn) + def __init__(self, server, conn, addr, data_size_limit=DATA_SIZE_DEFAULT, + map=None): + asynchat.async_chat.__init__(self, conn, map=map) self.smtp_server = server self.conn = conn self.addr = addr @@ -576,11 +577,11 @@ class SMTPServer(asyncore.dispatcher): channel_class = SMTPChannel def __init__(self, localaddr, remoteaddr, - data_size_limit=DATA_SIZE_DEFAULT): + data_size_limit=DATA_SIZE_DEFAULT, map=None): self._localaddr = localaddr self._remoteaddr = remoteaddr self.data_size_limit = data_size_limit - asyncore.dispatcher.__init__(self) + asyncore.dispatcher.__init__(self, map=map) try: self.create_socket(socket.AF_INET, socket.SOCK_STREAM) # try to re-use a server port if possible @@ -597,7 +598,8 @@ def __init__(self, localaddr, remoteaddr, 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) + channel = self.channel_class(self, conn, addr, self.data_size_limit, + self._map) # API for "doing something useful with the message" def process_message(self, peer, mailfrom, rcpttos, data): diff --git a/Lib/test/test_logging.py b/Lib/test/test_logging.py index aa878daf264..63d49fe5c77 100644 --- a/Lib/test/test_logging.py +++ b/Lib/test/test_logging.py @@ -659,41 +659,6 @@ def test_error_handling(self): # -- if it proves to be of wider utility than just test_logging if threading: - class TestSMTPChannel(smtpd.SMTPChannel): - """ - This derived class has had to be created because smtpd does not - support use of custom channel maps, although they are allowed by - asyncore's design. Issue #11959 has been raised to address this, - and if resolved satisfactorily, some of this code can be removed. - """ - def __init__(self, server, conn, addr, sockmap): - asynchat.async_chat.__init__(self, conn, sockmap) - self.smtp_server = server - self.conn = conn - self.addr = addr - self.data_size_limit = None - self.received_lines = [] - self.smtp_state = self.COMMAND - self.seen_greeting = '' - self.mailfrom = None - self.rcpttos = [] - self.received_data = '' - self.fqdn = socket.getfqdn() - self.num_bytes = 0 - 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.args[0] != errno.ENOTCONN: - raise - return - self.push('220 %s %s' % (self.fqdn, smtpd.__version__)) - self.set_terminator(b'\r\n') - self.extended_smtp = False - - class TestSMTPServer(smtpd.SMTPServer): """ This class implements a test SMTP server. @@ -714,37 +679,14 @@ class TestSMTPServer(smtpd.SMTPServer): :func:`asyncore.loop`. This avoids changing the :mod:`asyncore` module's global state. """ - channel_class = TestSMTPChannel def __init__(self, addr, handler, poll_interval, sockmap): - self._localaddr = addr - self._remoteaddr = None - self.data_size_limit = None - self.sockmap = sockmap - asyncore.dispatcher.__init__(self, map=sockmap) - try: - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - sock.setblocking(0) - self.set_socket(sock, map=sockmap) - # try to re-use a server port if possible - self.set_reuse_addr() - self.bind(addr) - self.port = sock.getsockname()[1] - self.listen(5) - except: - self.close() - raise + smtpd.SMTPServer.__init__(self, addr, None, map=sockmap) + self.port = self.socket.getsockname()[1] self._handler = handler self._thread = None self.poll_interval = poll_interval - def handle_accepted(self, conn, addr): - """ - Redefined only because the base class does not pass in a - map, forcing use of a global in :mod:`asyncore`. - """ - channel = self.channel_class(self, conn, addr, self.sockmap) - def process_message(self, peer, mailfrom, rcpttos, data): """ Delegates to the handler passed in to the server's constructor. @@ -775,7 +717,7 @@ def serve_forever(self, poll_interval): :func:`asyncore.loop`. """ try: - asyncore.loop(poll_interval, map=self.sockmap) + asyncore.loop(poll_interval, map=self._map) except OSError: # On FreeBSD 8, closing the server repeatably # raises this error. We swallow it if the diff --git a/Misc/NEWS b/Misc/NEWS index 8e4dbcb7722..84e913742db 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -112,6 +112,9 @@ Core and Builtins Library ------- +- Issue #11959: SMTPServer and SMTPChannel now take an optional map, use of + which avoids affecting global state. + - Issue #18109: os.uname() now decodes fields from the locale encoding, and socket.gethostname() now decodes the hostname from the locale encoding, instead of using the UTF-8 encoding in strict mode.