IMAP4_TLS stores the connect timeout it is given and then never reads it,
so IMAPClient(..., ssl=True, timeout=SocketTimeout(connect=N, ...)) leaves
the socket blocking. The handshake runs to the kernel's SYN retry ceiling
instead of to N — about 130 s on Linux with the default
tcp_syn_retries=6. The non-TLS path is unaffected.
This bites whenever SYNs are dropped rather than refused: a silent packet
filter, a blackholed ECMP path member, a host that has gone away. The
application asked for a bounded connect and does not get one.
Version: imapclient 3.1.0, Python 3.13, Linux
Reproducing
repro.py connects to 192.0.2.1 (RFC 5737 TEST-NET-1, routed nowhere, drops
SYNs) with a 5 second connect timeout, once with ssl=False and once with
ssl=True:
imapclient 3.1.0, connect timeout 5.0s
ssl=False gave up after 5.0s (TimeoutError) OK
ssl=True gave up after 134.0s (TimeoutError) BUG: timeout ignored
Both should give up after ~5 s.
Cause
imapclient/tls.py:
def __init__(self, host, port=993, ssl_context=None, timeout=None):
self.ssl_context = ssl_context
self._timeout = timeout # line 39: stored
super().__init__(host, port) # line 40: not passed on
def _create_socket(self, timeout): # line 42
sock = socket.create_connection((self.host, self.port), timeout=timeout)
imaplib.IMAP4.__init__ has signature (self, host='', port=IMAP4_PORT, timeout=None), so the call on line 40 leaves timeout at None and passes
that straight to self.open(host, port, timeout), which hands it to
_create_socket. _create_socket reads only its argument, so it connects
with timeout=None.
self._timeout is written on line 39 and never read anywhere — it is the
only occurrence of the name in the file:
$ grep -rn _timeout imapclient/tls.py
39: self._timeout = timeout
The non-TLS sibling imapclient/imap4.py already does the right thing, which
is why only ssl=True is affected:
def _create_socket(self, timeout=None):
return socket.create_connection(
(self.host, self.port), timeout if timeout is not None else self._timeout
)
Suggested fix
Apply the same fallback in IMAP4_TLS._create_socket. PATCH.diff:
--- a/imapclient/tls.py
+++ b/imapclient/tls.py
@@ -39,7 +39,9 @@
self._timeout = timeout
super().__init__(host, port)
- def _create_socket(self, timeout: Optional[float]) -> socket.socket:
+ def _create_socket(self, timeout: Optional[float] = None) -> socket.socket:
+ if timeout is None:
+ timeout = self._timeout
sock = socket.create_connection((self.host, self.port), timeout=timeout)
return wrap_socket(sock, self.ssl_context, self.host)
With it applied, the reproduction gives:
ssl=False gave up after 5.0s (TimeoutError) OK
ssl=True gave up after 5.0s (TimeoutError) OK
The default on the parameter keeps _create_socket callable with no argument,
matching imap4.IMAP4WithTimeout and imaplib's own callers.
repro.py
IMAP4_TLSstores the connect timeout it is given and then never reads it,so
IMAPClient(..., ssl=True, timeout=SocketTimeout(connect=N, ...))leavesthe socket blocking. The handshake runs to the kernel's SYN retry ceiling
instead of to
N— about 130 s on Linux with the defaulttcp_syn_retries=6. The non-TLS path is unaffected.This bites whenever SYNs are dropped rather than refused: a silent packet
filter, a blackholed ECMP path member, a host that has gone away. The
application asked for a bounded connect and does not get one.
Version: imapclient 3.1.0, Python 3.13, Linux
Reproducing
repro.pyconnects to192.0.2.1(RFC 5737 TEST-NET-1, routed nowhere, dropsSYNs) with a 5 second connect timeout, once with
ssl=Falseand once withssl=True:Both should give up after ~5 s.
Cause
imapclient/tls.py:imaplib.IMAP4.__init__has signature(self, host='', port=IMAP4_PORT, timeout=None), so the call on line 40 leavestimeoutatNoneand passesthat straight to
self.open(host, port, timeout), which hands it to_create_socket._create_socketreads only its argument, so it connectswith
timeout=None.self._timeoutis written on line 39 and never read anywhere — it is theonly occurrence of the name in the file:
The non-TLS sibling
imapclient/imap4.pyalready does the right thing, whichis why only
ssl=Trueis affected:Suggested fix
Apply the same fallback in
IMAP4_TLS._create_socket.PATCH.diff:With it applied, the reproduction gives:
The default on the parameter keeps
_create_socketcallable with no argument,matching
imap4.IMAP4WithTimeoutandimaplib's own callers.repro.py