diff -r a82d7e028458 Lib/imaplib.py --- a/Lib/imaplib.py Mon Jun 16 19:26:56 2014 -0400 +++ b/Lib/imaplib.py Tue Jul 01 02:49:48 2014 +0200 @@ -59,6 +59,7 @@ 'APPEND': ('AUTH', 'SELECTED'), 'AUTHENTICATE': ('NONAUTH',), 'CAPABILITY': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'), + 'ENABLE': ('AUTH', ), 'CHECK': ('SELECTED',), 'CLOSE': ('SELECTED',), 'COPY': ('SELECTED',), @@ -106,13 +107,13 @@ br' (?P[0-9][0-9]):(?P[0-9][0-9]):(?P[0-9][0-9])' br' (?P[-+])(?P[0-9][0-9])(?P[0-9][0-9])' br'"') -Literal = re.compile(br'.*{(?P\d+)}$', re.ASCII) MapCRLF = re.compile(br'\r\n|\r|\n') Response_code = re.compile(br'\[(?P[A-Z-]+)( (?P[^\]]*))?\]') Untagged_response = re.compile(br'\* (?P[A-Z-]+)( (?P.*))?') +# XXX: The following two regexes get never visited by test. +Literal = re.compile(br'.*{(?P\d+)}$') Untagged_status = re.compile( - br'\* (?P\d+) (?P[A-Z-]+)( (?P.*))?', re.ASCII) - + br'\* (?P\d+) (?P[A-Z-]+)( (?P.*))?') class IMAP4: @@ -165,7 +166,7 @@ class abort(error): pass # Service errors - close and retry class readonly(abort): pass # Mailbox status changed to READ-ONLY - def __init__(self, host = '', port = IMAP4_PORT): + def __init__(self, host='', port=IMAP4_PORT, enable_UTF8=False): self.debug = Debug self.state = 'LOGOUT' self.literal = None # A literal argument to a command @@ -175,6 +176,7 @@ self.is_readonly = False # READ-ONLY desired state self.tagnum = 0 self._tls_established = False + self._encoding = 'ascii' # Open socket to server. @@ -189,6 +191,10 @@ pass raise + if enable_UTF8 and not self._utf8_in_capabilities: + raise KeyError('Server does not allow enable_UTF8 to be True') + self._enable_UTF8_after_authentication = enable_UTF8 + def _connect(self): # Create unique tag for this session, @@ -243,6 +249,12 @@ # Overridable methods + def _get_message_literal(self, message): + literal = MapCRLF.sub(CRLF, message) + if self._encoding == 'utf-8': + literal = b'UTF8 (' + literal + b')' + return literal + def _create_socket(self): return socket.create_connection((self.host, self.port)) @@ -351,7 +363,7 @@ date_time = Time2Internaldate(date_time) else: date_time = None - self.literal = MapCRLF.sub(CRLF, message) + self.literal = self._get_message_literal(message) return self._simple_command(name, mailbox, flags, date_time) @@ -381,6 +393,8 @@ if typ != 'OK': raise self.error(dat[-1]) self.state = 'AUTH' + if self._enable_UTF8_after_authentication: + self.enable_UTF8_accept() return typ, dat @@ -552,7 +566,7 @@ def _CRAM_MD5_AUTH(self, challenge): """ Authobject to use with CRAM-MD5 authentication. """ import hmac - pwd = (self.password.encode('ASCII') if isinstance(self.password, str) + pwd = (self.password.encode('utf-8') if isinstance(self.password, str) else self.password) return self.user + " " + hmac.HMAC(pwd, challenge, 'md5').hexdigest() @@ -732,6 +746,26 @@ return self._untagged_response(typ, dat, name) + @property + def _utf8_in_capabilities(self): + for name in ['UTF8=ACCEPT', 'UTF8=ONLY']: + if name in self.capabilities: + return True + return False + + + def enable_UTF8_accept(self): + """Enable UTF-8 support by sending an 'ENABLE UTF8=ACCEPT' message to + the server and enabeling the 'UTF8' extencion for append commands. + Specified in RFC 6855. + """ + if not self._utf8_in_capabilities: + raise self.abort('UTF8 not supported by server') + typ = self._simple_command('ENABLE', 'UTF8=ACCEPT') + if typ != 'OK': + self._encoding = 'utf-8' + + def starttls(self, ssl_context=None): name = 'STARTTLS' if not HAVE_SSL: @@ -869,7 +903,7 @@ def _check_bye(self): bye = self.untagged_responses.get('BYE') if bye: - raise self.abort(bye[-1].decode('ascii', 'replace')) + raise self.abort(bye[-1].decode(self._encoding, 'replace')) def _command(self, name, *args): @@ -890,12 +924,12 @@ raise self.readonly('mailbox status changed to READ-ONLY') tag = self._new_tag() - name = bytes(name, 'ASCII') + name = bytes(name, self._encoding) data = tag + b' ' + name for arg in args: if arg is None: continue if isinstance(arg, str): - arg = bytes(arg, "ASCII") + arg = bytes(arg, self._encoding) data = data + b' ' + arg literal = self.literal @@ -905,7 +939,7 @@ literator = literal else: literator = None - data = data + bytes(' {%s}' % len(literal), 'ASCII') + data = data + bytes(' {%s}' % len(literal), self._encoding) if __debug__: if self.debug >= 4: @@ -970,7 +1004,7 @@ typ, dat = self.capability() if dat == [None]: raise self.error('no CAPABILITY response from server') - dat = str(dat[-1], "ASCII") + dat = str(dat[-1], self._encoding) dat = dat.upper() self.capabilities = tuple(dat.split()) @@ -992,7 +1026,7 @@ raise self.abort('unexpected tagged response: %s' % resp) typ = self.mo.group('type') - typ = str(typ, 'ASCII') + typ = str(typ, self._encoding) dat = self.mo.group('data') self.tagged_commands[tag] = (typ, [dat]) else: @@ -1014,7 +1048,7 @@ raise self.abort("unexpected response: '%s'" % resp) typ = self.mo.group('type') - typ = str(typ, 'ascii') + typ = str(typ, self._encoding) dat = self.mo.group('data') if dat is None: dat = b'' # Null untagged response if dat2: dat = dat + b' ' + dat2 @@ -1045,7 +1079,7 @@ if typ in ('OK', 'NO', 'BAD') and self._match(Response_code, dat): typ = self.mo.group('type') - typ = str(typ, "ASCII") + typ = str(typ, self._encoding) self._append_untagged(typ, self.mo.group('data')) if __debug__: @@ -1115,7 +1149,7 @@ def _new_tag(self): - tag = self.tagpre + bytes(str(self.tagnum), 'ASCII') + tag = self.tagpre + bytes(str(self.tagnum), self._encoding) self.tagnum = self.tagnum + 1 self.tagged_commands[tag] = None return tag @@ -1213,6 +1247,7 @@ raise ValueError("ssl_context and certfile arguments are mutually " "exclusive") + self._encoding = 'ascii' self.keyfile = keyfile self.certfile = certfile if ssl_context is None: @@ -1321,7 +1356,7 @@ # oup = b'' if isinstance(inp, str): - inp = inp.encode('ASCII') + inp = inp.encode('utf-8') while inp: if len(inp) > 48: t = inp[:48] diff -r a82d7e028458 Lib/test/test_imaplib.py --- a/Lib/test/test_imaplib.py Mon Jun 16 19:26:56 2014 -0400 +++ b/Lib/test/test_imaplib.py Tue Jul 01 02:49:48 2014 +0200 @@ -252,6 +252,57 @@ self.assertRaises(imaplib.IMAP4.abort, self.imap_class, *server.server_address) + + @reap_threads + def test_enable_UTF8_accept_raises_error_if_not_supported(self): + class LegecyServer(SimpleIMAPHandler): + pass + + with self.reaped_pair(LegecyServer) as (server, client): + self.assertRaises(client.abort, client.enable_UTF8_accept) + + @reap_threads + def test_enable_UTF8_accept_works_in_AUTH_state_only(self): + class UTF8Server(SimpleIMAPHandler): + capabilities = 'AUTH UTF8=ACCEPT' + + def cmd_ENABLE(self, tag, args): + self._send_tagged(tag, 'OK', 'ENABLE successful') + + def cmd_AUTHENTICATE(self, tag, args): + self._send_textline('+') + self.server.response = yield + self._send_tagged(tag, 'OK', 'FAKEAUTH successful') + + def cmd_APPEND(self, tag, args): + self._send_textline('+') + self.server.response = yield + self._send_tagged(tag, 'OK', 'okay') + + # enable_UTF8_accept() should raise an error when issued before AUTH + with self.reaped_pair(UTF8Server) as (server, client): + self.assertEqual(client._encoding, 'ascii') + self.assertRaises(client.error, client.enable_UTF8_accept) + self.assertEqual(client._encoding, 'ascii') + + # enable_UTF8_accept() should work in AUTH state + with self.reaped_pair(UTF8Server) as (server, client): + self.assertEqual(client._encoding, 'ascii') + code, data = client.authenticate('MYAUTH', lambda x: b'fake') + self.assertEqual(code, 'OK') + self.assertEqual(server.response, + b'ZmFrZQ==\r\n') # b64 encoded 'fake' + client.enable_UTF8_accept() + self.assertEqual(client._encoding, 'utf-8') + msg_string = 'Subject: üñí©öðé' + typ, data = client.append( + None, None, None, msg_string.encode('utf-8')) + self.assertEqual(typ, 'OK') + self.assertEqual( + server.response, + ('UTF8 (%s)\r\n' % msg_string).encode('utf-8') + ) + @reap_threads def test_bad_auth_name(self): @@ -301,6 +352,7 @@ self.assertEqual(server.response, b'ZmFrZQ==\r\n') #b64 encoded 'fake' + @reap_threads def test_login_cram_md5(self):