diff -r 03a55e207720 Lib/email/charset.py --- a/Lib/email/charset.py Fri Nov 22 20:17:24 2013 -0500 +++ b/Lib/email/charset.py Sat Nov 23 22:50:45 2013 +0800 @@ -386,7 +386,8 @@ string using the ascii codec produces the correct string version of the content. """ - # 7bit/8bit encodings return the string unchanged (module conversions) + if not string: + return string if self.body_encoding is BASE64: if isinstance(string, str): string = string.encode(self.output_charset) @@ -398,13 +399,9 @@ # character set, then, we must turn it into pseudo bytes via the # latin1 charset, which will encode any byte as a single code point # between 0 and 255, which is what body_encode is expecting. - # - # Note that this clause doesn't handle the case of a _payload that - # is already bytes. It never did, and the semantics of _payload - # being bytes has never been nailed down, so fixing that is a - # longer term TODO. if isinstance(string, str): - string = string.encode(self.output_charset).decode('latin1') + string = string.encode(self.output_charset) + string = string.decode('latin1') return email.quoprimime.body_encode(string) else: if isinstance(string, str): diff -r 03a55e207720 Lib/email/generator.py --- a/Lib/email/generator.py Fri Nov 22 20:17:24 2013 -0500 +++ b/Lib/email/generator.py Sat Nov 23 22:50:45 2013 +0800 @@ -223,7 +223,12 @@ if not isinstance(payload, str): raise TypeError('string payload expected: %s' % type(payload)) if _has_surrogates(msg._payload): - charset = msg.get_param('charset') + cs_object = msg.get_charset() + # Use original charset is we disable base64 encoding + if cs_object is not None and cs_object.body_encoding is None: + charset = cs_object + else: + charset = msg.get_param('charset') if charset is not None: del msg['content-transfer-encoding'] msg.set_payload(payload, charset) diff -r 03a55e207720 Lib/email/message.py --- a/Lib/email/message.py Fri Nov 22 20:17:24 2013 -0500 +++ b/Lib/email/message.py Sat Nov 23 22:50:45 2013 +0800 @@ -275,9 +275,16 @@ Optional charset sets the message's default character set. See set_charset() for details. """ - if isinstance(payload, bytes): - payload = payload.decode('ascii', 'surrogateescape') - self._payload = payload + if hasattr(payload, 'encode'): + if charset is None: + # We should check for ASCII-only here, but we can't do that + # for backward compatibility reasons. Fixed in 3.4. + self._payload = payload + return + if not isinstance(charset, Charset): + charset = Charset(charset) + payload = payload.encode(charset.output_charset) + self._payload = payload.decode('ascii', 'surrogateescape') if charset is not None: self.set_charset(charset) @@ -316,7 +323,15 @@ try: cte(self) except TypeError: - self._payload = charset.body_encode(self._payload) + # This if is for backward compatibility and will be removed + # in 3.4 when the ascii check is added to set_payload. + payload = self._payload + if payload: + try: + payload = payload.encode('ascii', 'surrogateescape') + except UnicodeError: + payload = payload.encode(charset.output_charset) + self._payload = charset.body_encode(payload) self.add_header('Content-Transfer-Encoding', cte) def get_charset(self): diff -r 03a55e207720 Lib/test/test_email/test_email.py --- a/Lib/test/test_email/test_email.py Fri Nov 22 20:17:24 2013 -0500 +++ b/Lib/test/test_email/test_email.py Sat Nov 23 22:50:45 2013 +0800 @@ -92,6 +92,49 @@ msg.set_payload('This is a string payload', charset) self.assertEqual(msg.get_charset().input_charset, 'iso-8859-1') + def test_set_payload_with_8bit_data_and_charset(self): + data = b'\xd0\x90\xd0\x91\xd0\x92' + charset = Charset('utf-8') + msg = Message() + msg.set_payload(data, charset) + self.assertEqual(msg['content-transfer-encoding'], 'base64') + self.assertEqual(msg.get_payload(decode=True), data) + self.assertEqual(msg.get_payload(), '0JDQkdCS\n') + + def test_set_payload_with_non_ascii_and_charset_body_encoding_none(self): + data = b'\xd0\x90\xd0\x91\xd0\x92' + charset = Charset('utf-8') + charset.body_encoding = None # Disable base64 encoding + msg = Message() + msg.set_payload(data.decode('utf-8'), charset) + self.assertEqual(msg['content-transfer-encoding'], '8bit') + self.assertEqual(msg.get_payload(decode=True), data) + + def test_set_payload_with_8bit_data_and_charset_body_encoding_none(self): + data = b'\xd0\x90\xd0\x91\xd0\x92' + charset = Charset('utf-8') + charset.body_encoding = None # Disable base64 encoding + msg = Message() + msg.set_payload(data, charset) + self.assertEqual(msg['content-transfer-encoding'], '8bit') + self.assertEqual(msg.get_payload(decode=True), data) + + def test_bytes_generator_after_calling_as_string(self): + data = b'\xd0\x90\xd0\x91\xd0\x92'.decode('utf-8') + cs = Charset('utf-8') + cs.body_encoding = None # Disable base64 encoding + msg = Message() + msg.set_payload(data, cs) + msg.as_string() # It will call set_payload implicitly! + fp = BytesIO() + g = BytesGenerator(fp) + g.flatten(msg) + self.assertEqual(fp.getvalue(), + (b'MIME-Version: 1.0\n' + b'Content-Type: text/plain; charset="utf-8"\n' + b'Content-Transfer-Encoding: 8bit\n\n' + b'\xd0\x90\xd0\x91\xd0\x92')) + def test_get_charsets(self): eq = self.assertEqual