Skip to content

Commit dc16812

Browse files
Quote non-ASCII names and do not parenthesize FETCH macros
Non-ASCII mailbox names are only allowed in a quoted string or a literal, never in an atom (RFC 6855). "ALL", "FULL" and "FAST" are macros, not data item names; they cannot be enclosed in parentheses. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent d74a402 commit dc16812

2 files changed

Lines changed: 54 additions & 5 deletions

File tree

Lib/imaplib.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -130,8 +130,8 @@
130130
_Literal = br'.*{(?P<size>\d+)}$'
131131
_Untagged_status = br'\* (?P<data>\d+) (?P<type>[A-Z-]+)( (?P<data2>.*))?'
132132
_control_chars = re.compile(b'[\x00-\x1F\x7F]')
133-
_non_astring_char = re.compile(br'[(){ \x00-\x1f\x7f%*\\"]')
134-
_non_list_char = re.compile(br'[(){ \x00-\x1f\x7f\\"]')
133+
_non_astring_char = re.compile(br'[(){ \x00-\x1f\x7f-\xff%*\\"]')
134+
_non_list_char = re.compile(br'[(){ \x00-\x1f\x7f-\xff\\"]')
135135
_quoted = re.compile(br'"(?:[^"\\]|\\.)*+"')
136136

137137

@@ -651,7 +651,7 @@ def fetch(self, message_set, message_parts):
651651
"""
652652
name = 'FETCH'
653653
typ, dat = self._simple_command(name, self._sequence_set(message_set),
654-
self._set_quote(message_parts))
654+
self._fetch_parts(message_parts))
655655
return self._untagged_response(typ, dat, name)
656656

657657

@@ -1036,7 +1036,7 @@ def uid(self, command, *args):
10361036
elif command == 'FETCH':
10371037
message_set, message_parts = args
10381038
args = (self._sequence_set(message_set),
1039-
self._set_quote(message_parts))
1039+
self._fetch_parts(message_parts))
10401040
elif command == 'STORE':
10411041
message_set, op, flags = args
10421042
args = (self._sequence_set(message_set), op,
@@ -1442,6 +1442,13 @@ def _set_quote(self, arg):
14421442
return arg
14431443
return '(' + arg + ')'
14441444

1445+
def _fetch_parts(self, arg):
1446+
# "ALL", "FULL" and "FAST" are macros, not data item names;
1447+
# they cannot be enclosed in parentheses.
1448+
if arg.upper() in ('ALL', 'FULL', 'FAST'):
1449+
return arg
1450+
return self._set_quote(arg)
1451+
14451452
def _quote(self, arg):
14461453
if isinstance(arg, str):
14471454
arg = bytes(arg, self._encoding)

Lib/test/test_imaplib.py

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,11 @@ def test_astring(self):
190190
self.assertEqual(m._astring('"a" SELECT evil "'),
191191
b'"\\"a\\" SELECT evil \\""')
192192
self.assertEqual(m._astring('"'), b'"\\""')
193+
# Non-ASCII names are only allowed in a quoted string or a
194+
# literal, never in an atom (RFC 6855).
195+
m._encoding = 'utf-8'
196+
self.assertEqual(m._astring('Entwürfe'), '"Entwürfe"'.encode())
197+
self.assertEqual(m._astring(b'Entw\xc3\xbcrfe'), b'"Entw\xc3\xbcrfe"')
193198

194199
def test_astring_idempotent(self):
195200
# Quoting an already quoted argument should not change it, so that
@@ -198,7 +203,7 @@ def test_astring_idempotent(self):
198203
m._encoding = 'ascii'
199204
for arg in ['INBOX', 'New folder', 'a"b', 'a\\b', '', '*', '%',
200205
'"New folder"', '""', '"a\\b"', '"a" SELECT evil "',
201-
'"', 'a\tb', 'a\rb', '\x7f', '(a)']:
206+
'"', 'a\tb', 'a\rb', '\x7f', '(a)', b'Entw\xc3\xbcrfe']:
202207
with self.subTest(arg=arg):
203208
once = m._astring(arg)
204209
self.assertEqual(m._astring(once), once)
@@ -215,6 +220,11 @@ def test_list_mailbox(self):
215220
# But spaces still require quoting.
216221
self.assertEqual(m._list_mailbox('New folder'), b'"New folder"')
217222
self.assertEqual(m._list_mailbox('"New folder"'), b'"New folder"')
223+
# As do non-ASCII names; wildcards keep their meaning inside a
224+
# quoted string.
225+
m._encoding = 'utf-8'
226+
self.assertEqual(m._list_mailbox('Entwürfe/%'),
227+
'"Entwürfe/%"'.encode())
218228

219229

220230
if ssl:
@@ -578,6 +588,26 @@ def cmd_AUTHENTICATE(self, tag, args):
578588
with self.assertRaisesRegex(imaplib.IMAP4.error, 'charset.*UTF8'):
579589
client.search('foo', 'bar')
580590

591+
def test_utf8_mailbox_name(self):
592+
class UTF8Server(SimpleIMAPHandler):
593+
capabilities = 'AUTH ENABLE UTF8=ACCEPT'
594+
def cmd_ENABLE(self, tag, args):
595+
self._send_tagged(tag, 'OK', 'ENABLE successful')
596+
def cmd_AUTHENTICATE(self, tag, args):
597+
self._send_textline('+')
598+
self.server.response = yield
599+
self._send_tagged(tag, 'OK', 'FAKEAUTH successful')
600+
client, server = self._setup(UTF8Server)
601+
typ, _ = client.authenticate('MYAUTH', lambda x: b'fake')
602+
self.assertEqual(typ, 'OK')
603+
typ, _ = client.enable('UTF8=ACCEPT')
604+
self.assertEqual(typ, 'OK')
605+
# A non-ASCII mailbox name is only allowed in a quoted string
606+
# or a literal, never in an atom (RFC 6855).
607+
typ, _ = client.select('Entwürfe')
608+
self.assertEqual(typ, 'OK')
609+
self.assertEqual(server.is_selected, ['"Entwürfe"'])
610+
581611
def test_bad_auth_name(self):
582612
class MyServer(SimpleIMAPHandler):
583613
def cmd_AUTHENTICATE(self, tag, args):
@@ -1208,6 +1238,14 @@ def cmd_FETCH(self, tag, args):
12081238
self.assertEqual(typ, 'OK')
12091239
self.assertEqual(server.args, ['2:4', '(FLAGS)'])
12101240

1241+
# But the macros are not, as they are not data item names.
1242+
typ, data = client.fetch('2:4', 'ALL')
1243+
self.assertEqual(typ, 'OK')
1244+
self.assertEqual(server.args, ['2:4', 'ALL'])
1245+
typ, data = client.fetch('2:4', 'fast')
1246+
self.assertEqual(typ, 'OK')
1247+
self.assertEqual(server.args, ['2:4', 'fast'])
1248+
12111249
def test_uid_fetch(self):
12121250
client, server = self._setup(make_simple_handler('UID', [
12131251
r'* 23 FETCH (FLAGS (\Seen) UID 4827313)',
@@ -1229,6 +1267,10 @@ def test_uid_fetch(self):
12291267
self.assertEqual(typ, 'OK')
12301268
self.assertEqual(server.args, ['FETCH', '4827313:4828442', '(FLAGS)'])
12311269

1270+
typ, data = client.uid('fetch', '4827313:4828442', 'ALL')
1271+
self.assertEqual(typ, 'OK')
1272+
self.assertEqual(server.args, ['FETCH', '4827313:4828442', 'ALL'])
1273+
12321274
def test_partial(self):
12331275
client, server = self._setup(make_simple_handler('PARTIAL',
12341276
['* 1 FETCH (RFC822.TEXT<0.10> "0123456789")']))

0 commit comments

Comments
 (0)