if translate_line_endings:
message = MapCRLF.sub(CRLF, message)
self.literal = message
- return self._simple_command(name, self._astring(mailbox), flags, date_time)
+ return self._simple_command(name, self._mailbox(mailbox), flags, date_time)
def authenticate(self, mechanism, authobject):
(typ, [data]) = <instance>.copy(message_set, new_mailbox)
"""
return self._simple_command('COPY', self._sequence_set(message_set),
- self._astring(new_mailbox))
+ self._mailbox(new_mailbox))
def create(self, mailbox):
(typ, [data]) = <instance>.create(mailbox)
"""
- return self._simple_command('CREATE', self._astring(mailbox))
+ return self._simple_command('CREATE', self._mailbox(mailbox))
def delete(self, mailbox):
(typ, [data]) = <instance>.delete(mailbox)
"""
- return self._simple_command('DELETE', self._astring(mailbox))
+ return self._simple_command('DELETE', self._mailbox(mailbox))
def deleteacl(self, mailbox, who):
"""Delete the ACLs (remove any rights) set for who on mailbox.
(typ, [data]) = <instance>.deleteacl(mailbox, who)
"""
- return self._simple_command('DELETEACL', self._astring(mailbox),
+ return self._simple_command('DELETEACL', self._mailbox(mailbox),
self._astring(who))
def enable(self, capability):
(typ, [data]) = <instance>.getacl(mailbox)
"""
- typ, dat = self._simple_command('GETACL', self._astring(mailbox))
+ typ, dat = self._simple_command('GETACL', self._mailbox(mailbox))
return self._untagged_response(typ, dat, 'ACL')
"""(typ, [data]) = <instance>.getannotation(mailbox, entry, attribute)
Retrieve ANNOTATIONs."""
- typ, dat = self._simple_command('GETANNOTATION', self._astring(mailbox),
+ typ, dat = self._simple_command('GETANNOTATION', self._mailbox(mailbox),
entry, attribute)
return self._untagged_response(typ, dat, 'ANNOTATION')
(typ, [data]) = <instance>.getquota(root)
"""
- typ, dat = self._simple_command('GETQUOTA', self._astring(root))
+ typ, dat = self._simple_command('GETQUOTA', self._mailbox(root))
return self._untagged_response(typ, dat, 'QUOTA')
(typ, [[QUOTAROOT responses...], [QUOTA responses]]) = <instance>.getquotaroot(mailbox)
"""
- typ, dat = self._simple_command('GETQUOTAROOT', self._astring(mailbox))
+ typ, dat = self._simple_command('GETQUOTAROOT', self._mailbox(mailbox))
typ, quota = self._untagged_response(typ, dat, 'QUOTA')
typ, quotaroot = self._untagged_response(typ, dat, 'QUOTAROOT')
return typ, [quotaroot, quota]
'data' is list of LIST responses.
"""
name = 'LIST'
- typ, dat = self._simple_command(name, self._astring(directory),
+ typ, dat = self._simple_command(name, self._mailbox(directory),
self._list_mailbox(pattern))
return self._untagged_response(typ, dat, name)
'data' are tuples of message part envelope and data.
"""
name = 'LSUB'
- typ, dat = self._simple_command(name, self._astring(directory),
+ typ, dat = self._simple_command(name, self._mailbox(directory),
self._list_mailbox(pattern))
return self._untagged_response(typ, dat, name)
(typ, [data]) = <instance>.move(message_set, new_mailbox)
"""
return self._simple_command('MOVE', self._sequence_set(message_set),
- self._astring(new_mailbox))
+ self._mailbox(new_mailbox))
def myrights(self, mailbox):
"""Show my ACLs for a mailbox (i.e. the rights that I have on mailbox).
(typ, [data]) = <instance>.myrights(mailbox)
"""
- typ,dat = self._simple_command('MYRIGHTS', self._astring(mailbox))
+ typ,dat = self._simple_command('MYRIGHTS', self._mailbox(mailbox))
return self._untagged_response(typ, dat, 'MYRIGHTS')
def namespace(self):
(typ, [data]) = <instance>.rename(oldmailbox, newmailbox)
"""
- return self._simple_command('RENAME', self._astring(oldmailbox),
- self._astring(newmailbox))
+ return self._simple_command('RENAME', self._mailbox(oldmailbox),
+ self._mailbox(newmailbox))
def search(self, charset, *criteria):
name = 'EXAMINE'
else:
name = 'SELECT'
- typ, dat = self._simple_command(name, self._astring(mailbox))
+ typ, dat = self._simple_command(name, self._mailbox(mailbox))
if typ != 'OK':
self.state = 'AUTH' # Might have been 'SELECTED'
return typ, dat
(typ, [data]) = <instance>.setacl(mailbox, who, what)
"""
- return self._simple_command('SETACL', self._astring(mailbox),
+ return self._simple_command('SETACL', self._mailbox(mailbox),
self._astring(who), self._astring(what))
"""(typ, [data]) = <instance>.setannotation(mailbox[, entry, attribute]+)
Set ANNOTATIONs."""
- typ, dat = self._simple_command('SETANNOTATION', self._astring(mailbox), *args)
+ typ, dat = self._simple_command('SETANNOTATION', self._mailbox(mailbox), *args)
return self._untagged_response(typ, dat, 'ANNOTATION')
(typ, [data]) = <instance>.setquota(root, limits)
"""
- typ, dat = self._simple_command('SETQUOTA', self._astring(root),
+ typ, dat = self._simple_command('SETQUOTA', self._mailbox(root),
self._set_quote(limits))
return self._untagged_response(typ, dat, 'QUOTA')
name = 'STATUS'
#if self.PROTOCOL_VERSION == 'IMAP4': # Let the server decide!
# raise self.error('%s unimplemented in IMAP4 (obtain IMAP4rev1 server, or re-code)' % name)
- typ, dat = self._simple_command(name, self._astring(mailbox),
+ typ, dat = self._simple_command(name, self._mailbox(mailbox),
self._set_quote(names))
return self._untagged_response(typ, dat, name)
(typ, [data]) = <instance>.subscribe(mailbox)
"""
- return self._simple_command('SUBSCRIBE', self._astring(mailbox))
+ return self._simple_command('SUBSCRIBE', self._mailbox(mailbox))
def thread(self, threading_algorithm, charset, *search_criteria):
if command in ('COPY', 'MOVE'):
message_set, new_mailbox = args
args = (self._sequence_set(message_set),
- self._astring(new_mailbox))
+ self._mailbox(new_mailbox))
elif command == 'FETCH':
message_set, message_parts = args
args = (self._sequence_set(message_set),
(typ, [data]) = <instance>.unsubscribe(mailbox)
"""
- return self._simple_command('UNSUBSCRIBE', self._astring(mailbox))
+ return self._simple_command('UNSUBSCRIBE', self._mailbox(mailbox))
def unselect(self):
return arg
return self._quote(arg)
+ def _encode_mailbox(self, arg, safe):
+ # Encode a mailbox name (or LIST pattern) for the wire. In the default
+ # (ASCII) mode a non-ASCII name uses modified UTF-7 (RFC 3501, 5.1.3).
+ # For backward compatibility a str that is already a valid modified
+ # UTF-7 token -- one that needs no quoting, or an explicitly quoted
+ # string -- is passed through unchanged, so that a name obtained as raw
+ # bytes from LIST (and decoded as ASCII) round-trips without being
+ # encoded again. Pass bytes to bypass encoding entirely.
+ if self._encoding != 'ascii':
+ return bytes(arg, self._encoding)
+ try:
+ raw = arg.encode('ascii')
+ except UnicodeEncodeError:
+ return arg.encode('utf-7-imap')
+ if _quoted.fullmatch(raw) or (raw and safe.search(raw) is None):
+ try:
+ raw.decode('utf-7-imap')
+ return raw
+ except UnicodeDecodeError:
+ pass
+ return arg.encode('utf-7-imap')
+
+ def _mailbox(self, arg):
+ if isinstance(arg, str):
+ arg = self._encode_mailbox(arg, _non_astring_char)
+ return self._astring(arg)
+
def _list_mailbox(self, arg):
if isinstance(arg, str):
- arg = bytes(arg, self._encoding)
+ arg = self._encode_mailbox(arg, _non_list_char)
if _quoted.fullmatch(arg):
return arg
if arg and _non_list_char.search(arg) is None:
# But spaces still require quoting.
self.assertEqual(m._list_mailbox('New folder'), b'"New folder"')
self.assertEqual(m._list_mailbox('"New folder"'), b'"New folder"')
- # As do non-ASCII names; wildcards keep their meaning inside a
- # quoted string.
+ # A non-ASCII pattern is encoded as modified UTF-7; the wildcards keep
+ # their meaning (they are printable ASCII, passed through directly).
+ self.assertEqual(m._list_mailbox('Entwürfe/%'), b'Entw&APw-rfe/%')
+ # Under UTF8=ACCEPT the name is sent as UTF-8 in a quoted string.
m._encoding = 'utf-8'
self.assertEqual(m._list_mailbox('Entwürfe/%'),
'"Entwürfe/%"'.encode())
+ def test_mailbox(self):
+ m = imaplib.IMAP4.__new__(imaplib.IMAP4)
+ m._encoding = 'ascii'
+ # A plain atom is left unquoted; bytes are sent verbatim.
+ self.assertEqual(m._mailbox('INBOX'), b'INBOX')
+ self.assertEqual(m._mailbox(b'Entw&APw-rfe'), b'Entw&APw-rfe')
+ # A non-ASCII name is encoded as modified UTF-7 (RFC 3501 5.1.3).
+ self.assertEqual(m._mailbox('Entwürfe'), b'Entw&APw-rfe')
+ # A str that is already valid modified UTF-7 is passed through, so a
+ # name obtained from LIST (raw bytes, decoded as ASCII) round-trips.
+ self.assertEqual(m._mailbox('Entw&APw-rfe'), b'Entw&APw-rfe')
+ self.assertEqual(m._mailbox('"Entw&APw-rfe"'), b'"Entw&APw-rfe"')
+ # A literal '&' (not a valid shift on its own) is encoded as '&-'.
+ self.assertEqual(m._mailbox('A&B'), b'A&-B')
+ # Names needing quoting are still quoted.
+ self.assertEqual(m._mailbox('New folder'), b'"New folder"')
+ self.assertEqual(m._mailbox(''), b'""')
+ # Control characters are Base64-encoded, never sent as raw bytes.
+ self.assertEqual(m._mailbox('a\x01b'), b'a&AAE-b')
+ # Under UTF8=ACCEPT a non-ASCII name is sent as UTF-8, quoted.
+ m._encoding = 'utf-8'
+ self.assertEqual(m._mailbox('Entwürfe'), '"Entwürfe"'.encode())
+ self.assertEqual(m._mailbox('Entw&APw-rfe'), b'Entw&APw-rfe')
+
if ssl:
class SecureTCPServer(socketserver.TCPServer):
def test_control_characters(self):
client, server = self._setup(SimpleIMAPHandler)
client.login('user', 'pass')
- for c in '\0\r\n':
- with self.assertRaises(ValueError):
- client.select(f'a{c}b')
- # Other control characters are valid in a quoted string and can
- # occur in mailbox names returned by the server, so the client
- # must be able to send them back.
+ # In the default (ASCII) mode a mailbox name is sent as modified UTF-7,
+ # which Base64-encodes every non-printable character (NUL, CR and LF
+ # included). So a control character is never sent as a raw byte -- it
+ # cannot corrupt the command -- and the name round-trips.
for c in support.control_characters_c0():
- if c in '\0\r\n':
- continue
- typ, _ = client.select(f'a{c}b')
- self.assertEqual(typ, 'OK')
- self.assertEqual(server.is_selected, [f'"a{c}b"'])
+ with self.subTest(c=c):
+ typ, _ = client.select(f'a{c}b')
+ self.assertEqual(typ, 'OK')
+ [name] = server.is_selected
+ self.assertNotIn(c, name)
+ self.assertEqual(name.encode('ascii').decode('utf-7-imap'),
+ f'a{c}b')
+
+ def test_mutf7_mailbox_name(self):
+ # Without UTF8=ACCEPT a non-ASCII mailbox name is sent as modified
+ # UTF-7 (RFC 3501, 5.1.3). A name that is already modified UTF-7 (e.g.
+ # obtained from a raw LIST response) is sent unchanged, whether as str
+ # or bytes, so it round-trips without being encoded again.
+ client, server = self._setup(SimpleIMAPHandler)
+ client.login('user', 'pass')
+ for name in ['Entwürfe', 'Entw&APw-rfe', b'Entw&APw-rfe']:
+ with self.subTest(name=name):
+ server.is_selected = None
+ typ, _ = client.select(name)
+ self.assertEqual(typ, 'OK')
+ self.assertEqual(server.is_selected, ['Entw&APw-rfe'])
# property tests