_quoted = re.compile(br'"(?:[^"\\]|\\.)*+"')
+def _paren_depth(data, depth=0):
+ # Net parenthesis nesting of data, ignoring parentheses in quoted strings.
+ data = _quoted.sub(b'', data)
+ return depth + data.count(b'(') - data.count(b')')
+
+
class IMAP4:
r"""IMAP4 client class.
else:
resp = self._get_line()
+ # Skip spurious blank lines between responses (some servers send one
+ # after a literal that ends a response).
+ while resp == b'':
+ resp = self._get_line()
+
# Command completion response?
if self._match(self.tagre, resp):
# Is there a literal to come?
+ depth = 0 # open parenthesis nesting so far
while self._match(self.Literal, dat):
# Read literal direct from connection.
# Store response with literal as tuple
self._append_untagged(typ, (dat, data))
+ depth = _paren_depth(dat, depth)
# Read trailer - possibly containing another literal
dat = self._get_line()
- # Skip a blank line that some servers send after a literal.
- if dat == b'':
+ # Skip spurious blank lines after a literal, but only inside an
+ # unclosed parenthesis (at top level they end the response).
+ while dat == b'' and depth > 0:
dat = self._get_line()
self._append_untagged(typ, dat)
self.assertEqual(data, [(b'1 (BODY[HEADER] {13}', b'Subject: test'),
b')'])
+ def test_literal_terminating_response(self):
+ # A literal ending a response (a LIST mailbox name sent as a literal)
+ # has an empty trailer that must not be swallowed. Conforming case:
+ # no spurious blank lines.
+ names = [b'My (box)"', b'Another', b'Third']
+ class Handler(SimpleIMAPHandler):
+ def cmd_LIST(self, tag, args):
+ for name in names:
+ self._send(b'* LIST (\\HasNoChildren) "/" {%d}\r\n'
+ % len(name))
+ self._send(name)
+ self._send(b'\r\n') # ends the response, no blank
+ self._send_tagged(tag, 'OK', 'LIST completed')
+ client, _ = self._setup(Handler)
+ client.login('user', 'pass')
+ typ, data = client.list()
+ self.assertEqual(typ, 'OK')
+ self.assertEqual(data, [
+ (b'(\\HasNoChildren) "/" {9}', b'My (box)"'), b'',
+ (b'(\\HasNoChildren) "/" {7}', b'Another'), b'',
+ (b'(\\HasNoChildren) "/" {5}', b'Third'), b'',
+ ])
+
+ def test_spurious_blank_lines_between_responses(self):
+ # A spurious blank line after each terminating literal falls between the
+ # untagged responses and must be skipped, even several in a row.
+ names = [b'My (box)"', b'Another', b'Third']
+ class Handler(SimpleIMAPHandler):
+ def cmd_LIST(self, tag, args):
+ for name in names:
+ self._send(b'* LIST (\\HasNoChildren) "/" {%d}\r\n'
+ % len(name))
+ self._send(name)
+ self._send(b'\r\n\r\n') # ends the response, then a blank
+ self._send_tagged(tag, 'OK', 'LIST completed')
+ client, _ = self._setup(Handler)
+ client.login('user', 'pass')
+ typ, data = client.list()
+ self.assertEqual(typ, 'OK')
+ self.assertEqual(data, [
+ (b'(\\HasNoChildren) "/" {9}', b'My (box)"'), b'',
+ (b'(\\HasNoChildren) "/" {7}', b'Another'), b'',
+ (b'(\\HasNoChildren) "/" {5}', b'Third'), b'',
+ ])
+
def test_unselect(self):
client, server = self._setup(SimpleIMAPHandler)
client.login('user', 'pass')