Skip to content

Commit 13a7ee8

Browse files
corona10vstinner
authored andcommitted
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.
1 parent 950c679 commit 13a7ee8

File tree

5 files changed

+90
-25
lines changed

5 files changed

+90
-25
lines changed

Doc/library/imaplib.rst

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,14 @@ Three classes are provided by the :mod:`imaplib` module, :class:`IMAP4` is the
3030
base class:
3131

3232

33-
.. class:: IMAP4(host='', port=IMAP4_PORT)
33+
.. class:: IMAP4(host='', port=IMAP4_PORT, timeout=None)
3434

3535
This class implements the actual IMAP4 protocol. The connection is created and
3636
protocol version (IMAP4 or IMAP4rev1) is determined when the instance is
3737
initialized. If *host* is not specified, ``''`` (the local host) is used. If
38-
*port* is omitted, the standard IMAP4 port (143) is used.
38+
*port* is omitted, the standard IMAP4 port (143) is used. The optional *timeout*
39+
parameter specifies a timeout in seconds for the connection attempt.
40+
If timeout is not given or is None, the global default socket timeout is used.
3941

4042
The :class:`IMAP4` class supports the :keyword:`with` statement. When used
4143
like this, the IMAP4 ``LOGOUT`` command is issued automatically when the
@@ -50,6 +52,9 @@ base class:
5052
.. versionchanged:: 3.5
5153
Support for the :keyword:`with` statement was added.
5254

55+
.. versionchanged:: 3.9
56+
The optional *timeout* parameter was added.
57+
5358
Three exceptions are defined as attributes of the :class:`IMAP4` class:
5459

5560

@@ -78,7 +83,7 @@ There's also a subclass for secure connections:
7883

7984

8085
.. class:: IMAP4_SSL(host='', port=IMAP4_SSL_PORT, keyfile=None, \
81-
certfile=None, ssl_context=None)
86+
certfile=None, ssl_context=None, timeout=None)
8287

8388
This is a subclass derived from :class:`IMAP4` that connects over an SSL
8489
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:
95100
mutually exclusive with *ssl_context*, a :class:`ValueError` is raised
96101
if *keyfile*/*certfile* is provided along with *ssl_context*.
97102

103+
The optional *timeout* parameter specifies a timeout in seconds for the
104+
connection attempt. If timeout is not given or is None, the global default
105+
socket timeout is used.
106+
98107
.. versionchanged:: 3.3
99-
*ssl_context* parameter added.
108+
*ssl_context* parameter was added.
100109

101110
.. versionchanged:: 3.4
102111
The class now supports hostname check with
@@ -110,6 +119,8 @@ There's also a subclass for secure connections:
110119
:func:`ssl.create_default_context` select the system's trusted CA
111120
certificates for you.
112121

122+
.. versionchanged:: 3.9
123+
The optional *timeout* parameter was added.
113124

114125
The second subclass allows for connections created by a child process:
115126

@@ -353,16 +364,22 @@ An :class:`IMAP4` instance has the following methods:
353364
Send ``NOOP`` to server.
354365

355366

356-
.. method:: IMAP4.open(host, port)
367+
.. method:: IMAP4.open(host, port, timeout=None)
357368

358-
Opens socket to *port* at *host*. This method is implicitly called by
359-
the :class:`IMAP4` constructor. The connection objects established by this
360-
method will be used in the :meth:`IMAP4.read`, :meth:`IMAP4.readline`,
361-
:meth:`IMAP4.send`, and :meth:`IMAP4.shutdown` methods. You may override
362-
this method.
369+
Opens socket to *port* at *host*. The optional *timeout* parameter
370+
specifies a timeout in seconds for the connection attempt.
371+
If timeout is not given or is None, the global default socket timeout
372+
is used. Also note that if the *timeout* parameter is set to be zero,
373+
it will raise a :class:`ValueError` to reject creating a non-blocking socket.
374+
This method is implicitly called by the :class:`IMAP4` constructor.
375+
The connection objects established by this method will be used in
376+
the :meth:`IMAP4.read`, :meth:`IMAP4.readline`, :meth:`IMAP4.send`,
377+
and :meth:`IMAP4.shutdown` methods. You may override this method.
363378

364379
.. audit-event:: imaplib.open self,host,port imaplib.IMAP4.open
365380

381+
.. versionchanged:: 3.9
382+
The *timeout* parameter was added.
366383

367384
.. method:: IMAP4.partial(message_num, message_part, start, length)
368385

Doc/whatsnew/3.9.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,16 @@ When the garbage collector makes a collection in which some objects resurrect
167167
been executed), do not block the collection of all objects that are still
168168
unreachable. (Contributed by Pablo Galindo and Tim Peters in :issue:`38379`.)
169169

170+
imaplib
171+
-------
172+
173+
:class:`~imaplib.IMAP4` and :class:`~imaplib.IMAP4_SSL` now have
174+
an optional *timeout* parameter for their constructors.
175+
Also, the :meth:`~imaplib.IMAP4.open` method now has an optional *timeout* parameter
176+
with this change. The overridden methods of :class:`~imaplib.IMAP4_SSL` and
177+
:class:`~imaplib.IMAP4_stream` were applied to this change.
178+
(Contributed by Dong-hee Na in :issue:`38615`.)
179+
170180
os
171181
--
172182

Lib/imaplib.py

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -135,10 +135,13 @@ class IMAP4:
135135

136136
r"""IMAP4 client class.
137137
138-
Instantiate with: IMAP4([host[, port]])
138+
Instantiate with: IMAP4([host[, port[, timeout=None]]])
139139
140140
host - host's name (default: localhost);
141141
port - port number (default: standard IMAP4 port).
142+
timeout - socket timeout (default: None)
143+
If timeout is not given or is None,
144+
the global default socket timeout is used
142145
143146
All IMAP4rev1 commands are supported by methods of the same
144147
name (in lower-case).
@@ -181,7 +184,7 @@ class error(Exception): pass # Logical errors - debug required
181184
class abort(error): pass # Service errors - close and retry
182185
class readonly(abort): pass # Mailbox status changed to READ-ONLY
183186

184-
def __init__(self, host='', port=IMAP4_PORT):
187+
def __init__(self, host='', port=IMAP4_PORT, timeout=None):
185188
self.debug = Debug
186189
self.state = 'LOGOUT'
187190
self.literal = None # A literal argument to a command
@@ -195,7 +198,7 @@ def __init__(self, host='', port=IMAP4_PORT):
195198

196199
# Open socket to server.
197200

198-
self.open(host, port)
201+
self.open(host, port, timeout)
199202

200203
try:
201204
self._connect()
@@ -284,23 +287,28 @@ def __exit__(self, *args):
284287
# Overridable methods
285288

286289

287-
def _create_socket(self):
290+
def _create_socket(self, timeout):
288291
# Default value of IMAP4.host is '', but socket.getaddrinfo()
289292
# (which is used by socket.create_connection()) expects None
290293
# as a default value for host.
294+
if timeout is not None and not timeout:
295+
raise ValueError('Non-blocking socket (timeout=0) is not supported')
291296
host = None if not self.host else self.host
292297
sys.audit("imaplib.open", self, self.host, self.port)
293-
return socket.create_connection((host, self.port))
298+
address = (host, self.port)
299+
if timeout is not None:
300+
return socket.create_connection(address, timeout)
301+
return socket.create_connection(address)
294302

295-
def open(self, host = '', port = IMAP4_PORT):
303+
def open(self, host='', port=IMAP4_PORT, timeout=None):
296304
"""Setup connection to remote server on "host:port"
297305
(default: localhost:standard IMAP4 port).
298306
This connection will be used by the routines:
299307
read, readline, send, shutdown.
300308
"""
301309
self.host = host
302310
self.port = port
303-
self.sock = self._create_socket()
311+
self.sock = self._create_socket(timeout)
304312
self.file = self.sock.makefile('rb')
305313

306314

@@ -1261,7 +1269,7 @@ class IMAP4_SSL(IMAP4):
12611269

12621270
"""IMAP4 client class over SSL connection
12631271
1264-
Instantiate with: IMAP4_SSL([host[, port[, keyfile[, certfile[, ssl_context]]]]])
1272+
Instantiate with: IMAP4_SSL([host[, port[, keyfile[, certfile[, ssl_context[, timeout=None]]]]]])
12651273
12661274
host - host's name (default: localhost);
12671275
port - port number (default: standard IMAP4 SSL port);
@@ -1271,13 +1279,15 @@ class IMAP4_SSL(IMAP4):
12711279
and private key (default: None)
12721280
Note: if ssl_context is provided, then parameters keyfile or
12731281
certfile should not be set otherwise ValueError is raised.
1282+
timeout - socket timeout (default: None) If timeout is not given or is None,
1283+
the global default socket timeout is used
12741284
12751285
for more documentation see the docstring of the parent class IMAP4.
12761286
"""
12771287

12781288

12791289
def __init__(self, host='', port=IMAP4_SSL_PORT, keyfile=None,
1280-
certfile=None, ssl_context=None):
1290+
certfile=None, ssl_context=None, timeout=None):
12811291
if ssl_context is not None and keyfile is not None:
12821292
raise ValueError("ssl_context and keyfile arguments are mutually "
12831293
"exclusive")
@@ -1294,20 +1304,20 @@ def __init__(self, host='', port=IMAP4_SSL_PORT, keyfile=None,
12941304
ssl_context = ssl._create_stdlib_context(certfile=certfile,
12951305
keyfile=keyfile)
12961306
self.ssl_context = ssl_context
1297-
IMAP4.__init__(self, host, port)
1307+
IMAP4.__init__(self, host, port, timeout)
12981308

1299-
def _create_socket(self):
1300-
sock = IMAP4._create_socket(self)
1309+
def _create_socket(self, timeout):
1310+
sock = IMAP4._create_socket(self, timeout)
13011311
return self.ssl_context.wrap_socket(sock,
13021312
server_hostname=self.host)
13031313

1304-
def open(self, host='', port=IMAP4_SSL_PORT):
1314+
def open(self, host='', port=IMAP4_SSL_PORT, timeout=None):
13051315
"""Setup connection to remote server on "host:port".
13061316
(default: localhost:standard IMAP4 SSL port).
13071317
This connection will be used by the routines:
13081318
read, readline, send, shutdown.
13091319
"""
1310-
IMAP4.open(self, host, port)
1320+
IMAP4.open(self, host, port, timeout)
13111321

13121322
__all__.append("IMAP4_SSL")
13131323

@@ -1329,7 +1339,7 @@ def __init__(self, command):
13291339
IMAP4.__init__(self)
13301340

13311341

1332-
def open(self, host = None, port = None):
1342+
def open(self, host=None, port=None, timeout=None):
13331343
"""Setup a stream connection.
13341344
This connection will be used by the routines:
13351345
read, readline, send, shutdown.

Lib/test/test_imaplib.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -440,6 +440,29 @@ def test_simple_with_statement(self):
440440
with self.imap_class(*server.server_address):
441441
pass
442442

443+
def test_imaplib_timeout_test(self):
444+
_, server = self._setup(SimpleIMAPHandler)
445+
addr = server.server_address[1]
446+
client = self.imap_class("localhost", addr, timeout=None)
447+
self.assertEqual(client.sock.timeout, None)
448+
client.shutdown()
449+
client = self.imap_class("localhost", addr, timeout=support.LOOPBACK_TIMEOUT)
450+
self.assertEqual(client.sock.timeout, support.LOOPBACK_TIMEOUT)
451+
client.shutdown()
452+
with self.assertRaises(ValueError):
453+
client = self.imap_class("localhost", addr, timeout=0)
454+
455+
def test_imaplib_timeout_functionality_test(self):
456+
class TimeoutHandler(SimpleIMAPHandler):
457+
def handle(self):
458+
time.sleep(1)
459+
SimpleIMAPHandler.handle(self)
460+
461+
_, server = self._setup(TimeoutHandler)
462+
addr = server.server_address[1]
463+
with self.assertRaises(socket.timeout):
464+
client = self.imap_class("localhost", addr, timeout=0.001)
465+
443466
def test_with_statement(self):
444467
_, server = self._setup(SimpleIMAPHandler, connect=False)
445468
with self.imap_class(*server.server_address) as imap:
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
:class:`~imaplib.IMAP4` and :class:`~imaplib.IMAP4_SSL` now have an
2+
optional *timeout* parameter for their constructors.
3+
Also, the :meth:`~imaplib.IMAP4.open` method now has an optional *timeout* parameter
4+
with this change. The overridden methods of :class:`~imaplib.IMAP4_SSL` and
5+
:class:`~imaplib.IMAP4_stream` were applied to this change. Patch by Dong-hee Na.

0 commit comments

Comments
 (0)