Skip to content

Commit 333bc2f

Browse files
committed
gh-159583: Fix IndexError in imaplib IDLE iteration
1 parent 75ce53d commit 333bc2f

3 files changed

Lines changed: 56 additions & 1 deletion

File tree

‎Lib/imaplib.py‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1986,13 +1986,20 @@ def _pop(self, timeout, default=('', None)):
19861986
if __debug__ and imap.debug >= 4:
19871987
imap._mesg(f'idle _pop({timeout}) reading')
19881988

1989+
deadline = None
19891990
if timeout is not None:
19901991
if timeout <= 0:
19911992
return default
19921993
timeout = float(timeout) # Required by socket.settimeout()
1994+
deadline = time.monotonic() + timeout
19931995

19941996
try:
1995-
imap._get_response(timeout) # Reads line, calls _append_untagged()
1997+
while not imap._idle_responses:
1998+
if deadline is not None:
1999+
timeout = deadline - time.monotonic()
2000+
if timeout <= 0:
2001+
return default
2002+
imap._get_response(timeout) # Reads line, calls _append_untagged()
19962003
except IMAP4._responsetimeout:
19972004
if __debug__ and imap.debug >= 4:
19982005
imap._mesg(f'idle _pop({timeout}) done')

‎Lib/test/test_imaplib.py‎

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -938,6 +938,52 @@ def test_idle_denied(self):
938938
with client.idle() as idler:
939939
pass
940940

941+
def test_idle_tagged_response(self):
942+
# An earlier command's tagged response may arrive during IDLE.
943+
class IdleTaggedHandler(SimpleIMAPHandler):
944+
capabilities = 'IDLE'
945+
946+
def cmd_NOOP(self, tag, args):
947+
self.noop_tag = tag
948+
949+
def cmd_IDLE(self, tag, args):
950+
self._send_textline('+ idling')
951+
self._send_tagged(self.noop_tag, 'OK', 'NOOP completed')
952+
self._send_line(b'* 1 EXISTS')
953+
r = yield
954+
if r == b'DONE\r\n':
955+
self._send_tagged(tag, 'OK', 'Idle completed')
956+
else:
957+
self._send_tagged(tag, 'BAD', 'Expected DONE')
958+
client, _ = self._setup(IdleTaggedHandler)
959+
client.login('user', 'pass')
960+
noop_tag = client._command('NOOP')
961+
with client.idle() as idler:
962+
self.assertEqual(next(idler), ('EXISTS', [b'1']))
963+
self.assertEqual(client._get_tagged_response(noop_tag),
964+
('OK', [b'NOOP completed']))
965+
966+
def test_idle_pop_timeout_after_tagged_response(self):
967+
class FakeIMAP:
968+
def __init__(self):
969+
self.state = 'IDLING'
970+
self.debug = 0
971+
self._idle_responses = []
972+
self.timeouts = []
973+
974+
def _get_response(self, timeout):
975+
self.timeouts.append(timeout)
976+
977+
imap = FakeIMAP()
978+
idler = imaplib.Idler.__new__(imaplib.Idler)
979+
idler._imap = imap
980+
with mock.patch.object(imaplib.time, 'monotonic',
981+
side_effect=(0, 0.06, 0.1)):
982+
response = idler._pop(0.1)
983+
self.assertEqual(response, ('', None))
984+
self.assertEqual(len(imap.timeouts), 1)
985+
self.assertAlmostEqual(imap.timeouts[0], 0.04)
986+
941987
def test_idle_iter(self):
942988
client, _ = self._setup(IdleCmdHandler)
943989
client.login('user', 'pass')
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix :exc:`IndexError` in :mod:`imaplib` IDLE iteration when a tagged
2+
response for an earlier command arrives before an untagged response.

0 commit comments

Comments
 (0)