Skip to content

Commit a47bcb4

Browse files
author
Forest
committed
imaplib: preserve partial reads on exception
This ensures that short IDLE durations / burst() intervals won't risk corrupting response lines that span multiple packets.
1 parent fcaf355 commit a47bcb4

2 files changed

Lines changed: 62 additions & 18 deletions

File tree

Lib/imaplib.py

Lines changed: 33 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ def __init__(self, host='', port=IMAP4_PORT, timeout=None):
201201
self.tagnum = 0
202202
self._tls_established = False
203203
self._mode_ascii()
204-
self._readbuf = b''
204+
self._readbuf = []
205205

206206
# Open socket to server.
207207

@@ -346,16 +346,24 @@ def read(self, size):
346346
# https://github.com/python/cpython/issues/51571
347347

348348
parts = []
349-
while True:
350-
if len(self._readbuf) >= size:
351-
parts.append(self._readbuf[:size])
352-
self._readbuf = self._readbuf[size:]
353-
break
354-
parts.append(self._readbuf)
355-
size -= len(self._readbuf)
356-
self._readbuf = self.sock.recv(DEFAULT_BUFFER_SIZE)
357-
if not self._readbuf:
349+
350+
while size > 0:
351+
352+
if len(parts) < len(self._readbuf):
353+
buf = self._readbuf[len(parts)]
354+
else:
355+
buf = self.sock.recv(DEFAULT_BUFFER_SIZE)
356+
if not buf:
357+
break
358+
self._readbuf.append(buf)
359+
360+
if len(buf) >= size:
361+
parts.append(buf[:size])
362+
self._readbuf = [buf[size:]]
358363
break
364+
parts.append(buf)
365+
size -= len(buf)
366+
359367
return b''.join(parts)
360368

361369

@@ -366,18 +374,25 @@ def readline(self):
366374
LF = b'\n'
367375
parts = []
368376
length = 0
377+
369378
while length < _MAXLINE:
370-
pos = self._readbuf.find(LF)
379+
380+
if len(parts) < len(self._readbuf):
381+
buf = self._readbuf[len(parts)]
382+
else:
383+
buf = self.sock.recv(DEFAULT_BUFFER_SIZE)
384+
if not buf:
385+
break
386+
self._readbuf.append(buf)
387+
388+
pos = buf.find(LF)
371389
if pos != -1:
372390
pos += 1
373-
parts.append(self._readbuf[:pos])
374-
self._readbuf = self._readbuf[pos:]
375-
break
376-
parts.append(self._readbuf)
377-
length += len(parts[-1])
378-
self._readbuf = self.sock.recv(DEFAULT_BUFFER_SIZE)
379-
if not self._readbuf:
391+
parts.append(buf[:pos])
392+
self._readbuf = [buf[pos:]]
380393
break
394+
parts.append(buf)
395+
length += len(buf)
381396

382397
line = b''.join(parts)
383398
if len(line) > _MAXLINE:

Lib/test/test_imaplib.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,23 @@ def cmd_IDLE(self, tag, args):
239239
self._send_tagged(tag, 'BAD', 'Expected DONE')
240240

241241

242+
class IdleCmdDelayedPacketHandler(SimpleIMAPHandler):
243+
capabilities = 'IDLE'
244+
def cmd_IDLE(self, tag, args):
245+
self._send_textline('+ idling')
246+
# response line spanning multiple packets, the last one delayed
247+
self._send(b'* 1 EX')
248+
time.sleep(0.2)
249+
self._send(b'IS')
250+
time.sleep(1)
251+
self._send(b'TS\r\n')
252+
r = yield
253+
if r == b'DONE\r\n':
254+
self._send_tagged(tag, 'OK', 'Idle completed')
255+
else:
256+
self._send_tagged(tag, 'BAD', 'Expected DONE')
257+
258+
242259
class NewIMAPTestsMixin():
243260
client = None
244261

@@ -583,6 +600,18 @@ def test_idle_burst(self):
583600
_, data = client.response('RECENT')
584601
self.assertEqual(data, [b'1', b'9'])
585602

603+
def test_idle_delayed_packet(self):
604+
client, _ = self._setup(IdleCmdDelayedPacketHandler)
605+
client.login('user', 'pass')
606+
# If our readline() implementation fails to preserve line fragments
607+
# when idle timeouts trigger, a response spanning delayed packets
608+
# can be corrupted, leaving the protocol stream in a bad state.
609+
try:
610+
with client.idle(0.5) as idler:
611+
self.assertRaises(StopIteration, next, idler)
612+
except client.abort as err:
613+
self.fail('multi-packet response was corrupted by idle timeout')
614+
586615
def test_login(self):
587616
client, _ = self._setup(SimpleIMAPHandler)
588617
typ, data = client.login('user', 'pass')

0 commit comments

Comments
 (0)