Repository navigation
imaplib.Idler raises IndexError when another command's tagged response arrives during IDLE #159583
Description
Bug report
imaplib.IMAP4.idle() iteration raises an unhandled IndexError: pop from empty list when a tagged response arrives while the client is iterating over an IDLE session.
In Idler._pop() (Lib/imaplib.py, line 2001), after calling _get_response(timeout) the method unconditionally executes imap._idle_responses.pop(0). However, _get_response() handles a tagged response by storing it in self.tagged_commands only — it appends nothing to the _idle_responses queue. The queue is therefore still empty and pop(0) raises IndexError, which escapes into user code iterating the Idler.
This happens when a tagged reply for an earlier pipelined command arrives after the IDLE continuation (+) has been consumed and before the next untagged response. RFC 3501 section 5.5 permits command pipelining for IMAP4rev1. The tagged response below belongs to NOOP; the IDLE command's tagged completion still follows DONE as required by RFC 2177 section 3.
Reproducer (run with a CPython build from this checkout, e.g. PCbuild/amd64/python_d.exe repro.py):
import socketserver
import threading
import traceback
import imaplib
class IdleTaggedHandler(socketserver.StreamRequestHandler):
def _send_line(self, line):
if isinstance(line, str):
line = line.encode('ascii')
self.wfile.write(line + b'\r\n')
def _send_tagged(self, tag, code, text):
self._send_line(tag + b' ' + code.encode() + b' ' + text.encode())
def handle(self):
self._send_line('* OK [CAPABILITY IMAP4rev1 IDLE] ready')
pending_noop = None
while True:
line = self.rfile.readline()
if not line:
return
tag, _, rest = line.partition(b' ')
cmd = rest.strip().split(b' ')[0].upper()
if cmd == b'LOGIN':
self._send_line(b'* CAPABILITY IMAP4rev1 IDLE')
self._send_tagged(tag, 'OK', 'LOGIN completed')
elif cmd == b'NOOP':
pending_noop = tag
elif cmd == b'CAPABILITY':
self._send_line(b'* CAPABILITY IMAP4rev1 IDLE')
self._send_tagged(tag, 'OK', 'CAPABILITY completed')
elif cmd == b'IDLE':
self._send_line(b'+ idling')
self._send_tagged(pending_noop, 'OK', 'NOOP completed')
self._send_line(b'* 1 EXISTS')
line = self.rfile.readline() # wait for DONE
if line == b'DONE\r\n':
self._send_tagged(tag, 'OK', 'Idle completed')
else:
self._send_tagged(tag, 'BAD', 'Expected DONE')
else:
self._send_tagged(tag, 'OK', 'done')
server = socketserver.TCPServer(('127.0.0.1', 0), IdleTaggedHandler)
threading.Thread(target=server.serve_forever, daemon=True).start()
client = imaplib.IMAP4(*server.server_address)
client.login('user', 'pass')
# Leave NOOP's tagged completion pending while starting IDLE.
noop_tag = client._command('NOOP')
try:
with client.idle() as idler:
print(next(idler)) # <-- IndexError raised here
print(client._get_tagged_response(noop_tag))
except Exception:
traceback.print_exc()Output on current main (d3ada45):
Traceback (most recent call last):
File "repro.py", line 56, in <module>
print(next(idler))
File "Lib/imaplib.py", line 2014, in __next__
typ, data = self._pop(timeout)
File "Lib/imaplib.py", line 2001, in _pop
resp = imap._idle_responses.pop(0)
IndexError: pop from empty list
Expected behavior: _get_response() should retain the tagged reply in tagged_commands, then IDLE iteration should continue until it can return the next untagged response. Any timeout must continue to use the original deadline.
Reproduced on CPython 3.16.0a1+dev, revision d3ada4509ea, on Windows x64. The reproduction uses the private _command() method to leave a tagged command response pending; the regular public command methods wait for their tagged response before returning.
CPython versions tested on:
3.16 (CPython 3.16.0a1+dev, revision d3ada4509ea)
Operating systems tested on:
Windows (x64)
Activity
picnixz commentedon Oct 16, 2026
cc @foresto I think you implemented the idler