bpo-38615: Add timeout parameter for IMAP4 and IMAP4_SSL constructor (GH-17203)

imaplib.IMAP4 and imaplib.IMAP4_SSL now have an 
optional *timeout* parameter for their constructors.
Also, the imaplib.IMAP4.open() method now has an optional *timeout* parameter
with this change. The overridden methods of imaplib.IMAP4_SSL and
imaplib.IMAP4_stream were applied to this change.
This commit is contained in:
Dong-hee Na 2020-01-08 02:28:10 +09:00 committed by Victor Stinner
parent 950c6795aa
commit 13a7ee8d62
5 changed files with 90 additions and 25 deletions

View File

@ -30,12 +30,14 @@ Three classes are provided by the :mod:`imaplib` module, :class:`IMAP4` is the
base class: base class:
.. class:: IMAP4(host='', port=IMAP4_PORT) .. class:: IMAP4(host='', port=IMAP4_PORT, timeout=None)
This class implements the actual IMAP4 protocol. The connection is created and This class implements the actual IMAP4 protocol. The connection is created and
protocol version (IMAP4 or IMAP4rev1) is determined when the instance is protocol version (IMAP4 or IMAP4rev1) is determined when the instance is
initialized. If *host* is not specified, ``''`` (the local host) is used. If initialized. If *host* is not specified, ``''`` (the local host) is used. If
*port* is omitted, the standard IMAP4 port (143) is used. *port* is omitted, the standard IMAP4 port (143) is used. The optional *timeout*
parameter specifies a timeout in seconds for the connection attempt.
If timeout is not given or is None, the global default socket timeout is used.
The :class:`IMAP4` class supports the :keyword:`with` statement. When used The :class:`IMAP4` class supports the :keyword:`with` statement. When used
like this, the IMAP4 ``LOGOUT`` command is issued automatically when the like this, the IMAP4 ``LOGOUT`` command is issued automatically when the
@ -50,6 +52,9 @@ base class:
.. versionchanged:: 3.5 .. versionchanged:: 3.5
Support for the :keyword:`with` statement was added. Support for the :keyword:`with` statement was added.
.. versionchanged:: 3.9
The optional *timeout* parameter was added.
Three exceptions are defined as attributes of the :class:`IMAP4` class: Three exceptions are defined as attributes of the :class:`IMAP4` class:
@ -78,7 +83,7 @@ There's also a subclass for secure connections:
.. class:: IMAP4_SSL(host='', port=IMAP4_SSL_PORT, keyfile=None, \ .. class:: IMAP4_SSL(host='', port=IMAP4_SSL_PORT, keyfile=None, \
certfile=None, ssl_context=None) certfile=None, ssl_context=None, timeout=None)
This is a subclass derived from :class:`IMAP4` that connects over an SSL This is a subclass derived from :class:`IMAP4` that connects over an SSL
encrypted socket (to use this class you need a socket module that was compiled encrypted socket (to use this class you need a socket module that was compiled
@ -95,8 +100,12 @@ There's also a subclass for secure connections:
mutually exclusive with *ssl_context*, a :class:`ValueError` is raised mutually exclusive with *ssl_context*, a :class:`ValueError` is raised
if *keyfile*/*certfile* is provided along with *ssl_context*. if *keyfile*/*certfile* is provided along with *ssl_context*.
The optional *timeout* parameter specifies a timeout in seconds for the
connection attempt. If timeout is not given or is None, the global default
socket timeout is used.
.. versionchanged:: 3.3 .. versionchanged:: 3.3
*ssl_context* parameter added. *ssl_context* parameter was added.
.. versionchanged:: 3.4 .. versionchanged:: 3.4
The class now supports hostname check with The class now supports hostname check with
@ -110,6 +119,8 @@ There's also a subclass for secure connections:
:func:`ssl.create_default_context` select the system's trusted CA :func:`ssl.create_default_context` select the system's trusted CA
certificates for you. certificates for you.
.. versionchanged:: 3.9
The optional *timeout* parameter was added.
The second subclass allows for connections created by a child process: The second subclass allows for connections created by a child process:
@ -353,16 +364,22 @@ An :class:`IMAP4` instance has the following methods:
Send ``NOOP`` to server. Send ``NOOP`` to server.
.. method:: IMAP4.open(host, port) .. method:: IMAP4.open(host, port, timeout=None)
Opens socket to *port* at *host*. This method is implicitly called by Opens socket to *port* at *host*. The optional *timeout* parameter
the :class:`IMAP4` constructor. The connection objects established by this specifies a timeout in seconds for the connection attempt.
method will be used in the :meth:`IMAP4.read`, :meth:`IMAP4.readline`, If timeout is not given or is None, the global default socket timeout
:meth:`IMAP4.send`, and :meth:`IMAP4.shutdown` methods. You may override is used. Also note that if the *timeout* parameter is set to be zero,
this method. it will raise a :class:`ValueError` to reject creating a non-blocking socket.
This method is implicitly called by the :class:`IMAP4` constructor.
The connection objects established by this method will be used in
the :meth:`IMAP4.read`, :meth:`IMAP4.readline`, :meth:`IMAP4.send`,
and :meth:`IMAP4.shutdown` methods. You may override this method.
.. audit-event:: imaplib.open self,host,port imaplib.IMAP4.open .. audit-event:: imaplib.open self,host,port imaplib.IMAP4.open
.. versionchanged:: 3.9
The *timeout* parameter was added.
.. method:: IMAP4.partial(message_num, message_part, start, length) .. method:: IMAP4.partial(message_num, message_part, start, length)

View File

@ -167,6 +167,16 @@ When the garbage collector makes a collection in which some objects resurrect
been executed), do not block the collection of all objects that are still been executed), do not block the collection of all objects that are still
unreachable. (Contributed by Pablo Galindo and Tim Peters in :issue:`38379`.) unreachable. (Contributed by Pablo Galindo and Tim Peters in :issue:`38379`.)
imaplib
-------
:class:`~imaplib.IMAP4` and :class:`~imaplib.IMAP4_SSL` now have
an optional *timeout* parameter for their constructors.
Also, the :meth:`~imaplib.IMAP4.open` method now has an optional *timeout* parameter
with this change. The overridden methods of :class:`~imaplib.IMAP4_SSL` and
:class:`~imaplib.IMAP4_stream` were applied to this change.
(Contributed by Dong-hee Na in :issue:`38615`.)
os os
-- --

View File

@ -135,10 +135,13 @@ class IMAP4:
r"""IMAP4 client class. r"""IMAP4 client class.
Instantiate with: IMAP4([host[, port]]) Instantiate with: IMAP4([host[, port[, timeout=None]]])
host - host's name (default: localhost); host - host's name (default: localhost);
port - port number (default: standard IMAP4 port). port - port number (default: standard IMAP4 port).
timeout - socket timeout (default: None)
If timeout is not given or is None,
the global default socket timeout is used
All IMAP4rev1 commands are supported by methods of the same All IMAP4rev1 commands are supported by methods of the same
name (in lower-case). name (in lower-case).
@ -181,7 +184,7 @@ class IMAP4:
class abort(error): pass # Service errors - close and retry class abort(error): pass # Service errors - close and retry
class readonly(abort): pass # Mailbox status changed to READ-ONLY class readonly(abort): pass # Mailbox status changed to READ-ONLY
def __init__(self, host='', port=IMAP4_PORT): def __init__(self, host='', port=IMAP4_PORT, timeout=None):
self.debug = Debug self.debug = Debug
self.state = 'LOGOUT' self.state = 'LOGOUT'
self.literal = None # A literal argument to a command self.literal = None # A literal argument to a command
@ -195,7 +198,7 @@ class IMAP4:
# Open socket to server. # Open socket to server.
self.open(host, port) self.open(host, port, timeout)
try: try:
self._connect() self._connect()
@ -284,15 +287,20 @@ class IMAP4:
# Overridable methods # Overridable methods
def _create_socket(self): def _create_socket(self, timeout):
# Default value of IMAP4.host is '', but socket.getaddrinfo() # Default value of IMAP4.host is '', but socket.getaddrinfo()
# (which is used by socket.create_connection()) expects None # (which is used by socket.create_connection()) expects None
# as a default value for host. # as a default value for host.
if timeout is not None and not timeout:
raise ValueError('Non-blocking socket (timeout=0) is not supported')
host = None if not self.host else self.host host = None if not self.host else self.host
sys.audit("imaplib.open", self, self.host, self.port) sys.audit("imaplib.open", self, self.host, self.port)
return socket.create_connection((host, self.port)) address = (host, self.port)
if timeout is not None:
return socket.create_connection(address, timeout)
return socket.create_connection(address)
def open(self, host = '', port = IMAP4_PORT): def open(self, host='', port=IMAP4_PORT, timeout=None):
"""Setup connection to remote server on "host:port" """Setup connection to remote server on "host:port"
(default: localhost:standard IMAP4 port). (default: localhost:standard IMAP4 port).
This connection will be used by the routines: This connection will be used by the routines:
@ -300,7 +308,7 @@ class IMAP4:
""" """
self.host = host self.host = host
self.port = port self.port = port
self.sock = self._create_socket() self.sock = self._create_socket(timeout)
self.file = self.sock.makefile('rb') self.file = self.sock.makefile('rb')
@ -1261,7 +1269,7 @@ if HAVE_SSL:
"""IMAP4 client class over SSL connection """IMAP4 client class over SSL connection
Instantiate with: IMAP4_SSL([host[, port[, keyfile[, certfile[, ssl_context]]]]]) Instantiate with: IMAP4_SSL([host[, port[, keyfile[, certfile[, ssl_context[, timeout=None]]]]]])
host - host's name (default: localhost); host - host's name (default: localhost);
port - port number (default: standard IMAP4 SSL port); port - port number (default: standard IMAP4 SSL port);
@ -1271,13 +1279,15 @@ if HAVE_SSL:
and private key (default: None) and private key (default: None)
Note: if ssl_context is provided, then parameters keyfile or Note: if ssl_context is provided, then parameters keyfile or
certfile should not be set otherwise ValueError is raised. certfile should not be set otherwise ValueError is raised.
timeout - socket timeout (default: None) If timeout is not given or is None,
the global default socket timeout is used
for more documentation see the docstring of the parent class IMAP4. for more documentation see the docstring of the parent class IMAP4.
""" """
def __init__(self, host='', port=IMAP4_SSL_PORT, keyfile=None, def __init__(self, host='', port=IMAP4_SSL_PORT, keyfile=None,
certfile=None, ssl_context=None): certfile=None, ssl_context=None, timeout=None):
if ssl_context is not None and keyfile is not None: if ssl_context is not None and keyfile is not None:
raise ValueError("ssl_context and keyfile arguments are mutually " raise ValueError("ssl_context and keyfile arguments are mutually "
"exclusive") "exclusive")
@ -1294,20 +1304,20 @@ if HAVE_SSL:
ssl_context = ssl._create_stdlib_context(certfile=certfile, ssl_context = ssl._create_stdlib_context(certfile=certfile,
keyfile=keyfile) keyfile=keyfile)
self.ssl_context = ssl_context self.ssl_context = ssl_context
IMAP4.__init__(self, host, port) IMAP4.__init__(self, host, port, timeout)
def _create_socket(self): def _create_socket(self, timeout):
sock = IMAP4._create_socket(self) sock = IMAP4._create_socket(self, timeout)
return self.ssl_context.wrap_socket(sock, return self.ssl_context.wrap_socket(sock,
server_hostname=self.host) server_hostname=self.host)
def open(self, host='', port=IMAP4_SSL_PORT): def open(self, host='', port=IMAP4_SSL_PORT, timeout=None):
"""Setup connection to remote server on "host:port". """Setup connection to remote server on "host:port".
(default: localhost:standard IMAP4 SSL port). (default: localhost:standard IMAP4 SSL port).
This connection will be used by the routines: This connection will be used by the routines:
read, readline, send, shutdown. read, readline, send, shutdown.
""" """
IMAP4.open(self, host, port) IMAP4.open(self, host, port, timeout)
__all__.append("IMAP4_SSL") __all__.append("IMAP4_SSL")
@ -1329,7 +1339,7 @@ class IMAP4_stream(IMAP4):
IMAP4.__init__(self) IMAP4.__init__(self)
def open(self, host = None, port = None): def open(self, host=None, port=None, timeout=None):
"""Setup a stream connection. """Setup a stream connection.
This connection will be used by the routines: This connection will be used by the routines:
read, readline, send, shutdown. read, readline, send, shutdown.

View File

@ -440,6 +440,29 @@ class NewIMAPTestsMixin():
with self.imap_class(*server.server_address): with self.imap_class(*server.server_address):
pass pass
def test_imaplib_timeout_test(self):
_, server = self._setup(SimpleIMAPHandler)
addr = server.server_address[1]
client = self.imap_class("localhost", addr, timeout=None)
self.assertEqual(client.sock.timeout, None)
client.shutdown()
client = self.imap_class("localhost", addr, timeout=support.LOOPBACK_TIMEOUT)
self.assertEqual(client.sock.timeout, support.LOOPBACK_TIMEOUT)
client.shutdown()
with self.assertRaises(ValueError):
client = self.imap_class("localhost", addr, timeout=0)
def test_imaplib_timeout_functionality_test(self):
class TimeoutHandler(SimpleIMAPHandler):
def handle(self):
time.sleep(1)
SimpleIMAPHandler.handle(self)
_, server = self._setup(TimeoutHandler)
addr = server.server_address[1]
with self.assertRaises(socket.timeout):
client = self.imap_class("localhost", addr, timeout=0.001)
def test_with_statement(self): def test_with_statement(self):
_, server = self._setup(SimpleIMAPHandler, connect=False) _, server = self._setup(SimpleIMAPHandler, connect=False)
with self.imap_class(*server.server_address) as imap: with self.imap_class(*server.server_address) as imap:

View File

@ -0,0 +1,5 @@
:class:`~imaplib.IMAP4` and :class:`~imaplib.IMAP4_SSL` now have an
optional *timeout* parameter for their constructors.
Also, the :meth:`~imaplib.IMAP4.open` method now has an optional *timeout* parameter
with this change. The overridden methods of :class:`~imaplib.IMAP4_SSL` and
:class:`~imaplib.IMAP4_stream` were applied to this change. Patch by Dong-hee Na.