diff --git a/Lib/imaplib.py b/Lib/imaplib.py index 977d6bdb1c3d86b..0d9929b48468b11 100644 --- a/Lib/imaplib.py +++ b/Lib/imaplib.py @@ -1986,13 +1986,20 @@ def _pop(self, timeout, default=('', None)): if __debug__ and imap.debug >= 4: imap._mesg(f'idle _pop({timeout}) reading') + deadline = None if timeout is not None: if timeout <= 0: return default timeout = float(timeout) # Required by socket.settimeout() + deadline = time.monotonic() + timeout try: - imap._get_response(timeout) # Reads line, calls _append_untagged() + while not imap._idle_responses: + if deadline is not None: + timeout = deadline - time.monotonic() + if timeout <= 0: + return default + imap._get_response(timeout) # Reads line, calls _append_untagged() except IMAP4._responsetimeout: if __debug__ and imap.debug >= 4: imap._mesg(f'idle _pop({timeout}) done') diff --git a/Lib/test/test_imaplib.py b/Lib/test/test_imaplib.py index da63da54e8ef733..541179116b2c818 100644 --- a/Lib/test/test_imaplib.py +++ b/Lib/test/test_imaplib.py @@ -938,6 +938,52 @@ def test_idle_denied(self): with client.idle() as idler: pass + def test_idle_tagged_response(self): + # An earlier command's tagged response may arrive during IDLE. + class IdleTaggedHandler(SimpleIMAPHandler): + capabilities = 'IDLE' + + def cmd_NOOP(self, tag, args): + self.noop_tag = tag + + def cmd_IDLE(self, tag, args): + self._send_textline('+ idling') + self._send_tagged(self.noop_tag, 'OK', 'NOOP completed') + self._send_line(b'* 1 EXISTS') + r = yield + if r == b'DONE\r\n': + self._send_tagged(tag, 'OK', 'Idle completed') + else: + self._send_tagged(tag, 'BAD', 'Expected DONE') + client, _ = self._setup(IdleTaggedHandler) + client.login('user', 'pass') + noop_tag = client._command('NOOP') + with client.idle() as idler: + self.assertEqual(next(idler), ('EXISTS', [b'1'])) + self.assertEqual(client._get_tagged_response(noop_tag), + ('OK', [b'NOOP completed'])) + + def test_idle_pop_timeout_after_tagged_response(self): + class FakeIMAP: + def __init__(self): + self.state = 'IDLING' + self.debug = 0 + self._idle_responses = [] + self.timeouts = [] + + def _get_response(self, timeout): + self.timeouts.append(timeout) + + imap = FakeIMAP() + idler = imaplib.Idler.__new__(imaplib.Idler) + idler._imap = imap + with mock.patch.object(imaplib.time, 'monotonic', + side_effect=(0, 0.06, 0.1)): + response = idler._pop(0.1) + self.assertEqual(response, ('', None)) + self.assertEqual(len(imap.timeouts), 1) + self.assertAlmostEqual(imap.timeouts[0], 0.04) + def test_idle_iter(self): client, _ = self._setup(IdleCmdHandler) client.login('user', 'pass') diff --git a/Misc/NEWS.d/next/Library/2026-10-17-03-24-36.gh-issue-159583.AyW3gc.rst b/Misc/NEWS.d/next/Library/2026-10-17-03-24-36.gh-issue-159583.AyW3gc.rst new file mode 100644 index 000000000000000..f7b4979ae4d6cde --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-10-17-03-24-36.gh-issue-159583.AyW3gc.rst @@ -0,0 +1,2 @@ +Fix :exc:`IndexError` in :mod:`imaplib` IDLE iteration when a tagged +response for an earlier command arrives before an untagged response.