From f3bc71a95995fb06442eeb041835f6b24acfb952 Mon Sep 17 00:00:00 2001 From: Maxim Vladimirskiy Date: Mon, 9 Apr 2018 10:46:11 +0300 Subject: [PATCH 1/3] Distinguish binary and text test fixtures --- tests/__init__.py | 150 ++++++++++++++++++++++------------------------ 1 file changed, 72 insertions(+), 78 deletions(-) diff --git a/tests/__init__.py b/tests/__init__.py index 9f3940d3..bf8d9cb0 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,8 +1,6 @@ # coding:utf-8 -from os.path import join, abspath, dirname, exists -from nose.tools import * -import codecs +from os.path import join, abspath, dirname def fixtures_path(): @@ -20,92 +18,88 @@ def skip_if_asked(): raise SkipTest() -def read_fixture_bytes(path): +def read_fixture(path, binary=False): absolute_path = join(abspath(dirname(__file__)), 'fixtures', path) - with open(absolute_path, 'rb') as f: + mode = 'rb' if binary else 'r' + with open(absolute_path, mode) as f: return f.read() # mime fixture files -BOUNCE = read_fixture_bytes('messages/bounce/zed.eml') -BOUNCE_OFFICE365 = read_fixture_bytes('messages/bounce/office365.eml') -MAILBOX_FULL = read_fixture_bytes('messages/bounce/mailbox-full.eml') -NDN = read_fixture_bytes('messages/bounce/delayed.eml') -NDN_BROKEN = read_fixture_bytes('messages/bounce/delayed-broken.eml') - -SIGNED = read_fixture_bytes('messages/signed.eml') -LONG_LINKS = read_fixture_bytes('messages/long-links.eml') -MULTI_RECEIVED_HEADERS = read_fixture_bytes( +BOUNCE = read_fixture('messages/bounce/zed.eml') +BOUNCE_OFFICE365 = read_fixture('messages/bounce/office365.eml') +MAILBOX_FULL = read_fixture('messages/bounce/mailbox-full.eml') +NDN = read_fixture('messages/bounce/delayed.eml') +NDN_BROKEN = read_fixture('messages/bounce/delayed-broken.eml') + +SIGNED = read_fixture('messages/signed.eml') +LONG_LINKS = read_fixture('messages/long-links.eml') +MULTI_RECEIVED_HEADERS = read_fixture( 'messages/multi-received-headers.eml') -MAILGUN_PNG = read_fixture_bytes('messages/attachments/mailgun.png') -MAILGUN_WAV = read_fixture_bytes('messages/attachments/mailgun-rocks.wav') - -TORTURE = read_fixture_bytes('messages/torture.eml') -TORTURE_PART = read_fixture_bytes('messages/torture-part.eml') -BILINGUAL = read_fixture_bytes('messages/bilingual-simple.eml') -RELATIVE = read_fixture_bytes('messages/relative.eml') -IPHONE = read_fixture_bytes('messages/iphone.eml') - -MULTIPART = read_fixture_bytes('messages/multipart.eml') -FROM_ENCODING = read_fixture_bytes('messages/from-encoding.eml') -NO_CTYPE = read_fixture_bytes('messages/no-ctype.eml') -APACHE_MIME_MESSAGE_NEWS = read_fixture_bytes( +MAILGUN_PNG = read_fixture('messages/attachments/mailgun.png', binary=True) +MAILGUN_WAV = read_fixture('messages/attachments/mailgun-rocks.wav', + binary=True) + +TORTURE = read_fixture('messages/torture.eml') +TORTURE_PART = read_fixture('messages/torture-part.eml') +BILINGUAL = read_fixture('messages/bilingual-simple.eml') +RELATIVE = read_fixture('messages/relative.eml') +IPHONE = read_fixture('messages/iphone.eml') + +MULTIPART = read_fixture('messages/multipart.eml') +FROM_ENCODING = read_fixture('messages/from-encoding.eml', binary=True) +NO_CTYPE = read_fixture('messages/no-ctype.eml') +APACHE_MIME_MESSAGE_NEWS = read_fixture( 'messages/apache-message-news-mime.eml') -ENCLOSED = read_fixture_bytes('messages/enclosed.eml') -ENCLOSED_BROKEN_BOUNDARY = read_fixture_bytes('messages/enclosed-broken.eml') -ENCLOSED_ENDLESS = read_fixture_bytes('messages/enclosed-endless.eml') -ENCLOSED_BROKEN_BODY = read_fixture_bytes('messages/enclosed-broken-body.eml') -ENCLOSED_BROKEN_ENCODING = read_fixture_bytes( - 'messages/enclosed-bad-encoding.eml') -FALSE_MULTIPART = read_fixture_bytes('messages/false-multipart.eml') -ENCODED_HEADER = read_fixture_bytes('messages/encoded-header.eml') -MESSAGE_EXTERNAL_BODY= read_fixture_bytes( +ENCLOSED = read_fixture('messages/enclosed.eml') +ENCLOSED_BROKEN_BOUNDARY = read_fixture('messages/enclosed-broken.eml') +ENCLOSED_ENDLESS = read_fixture('messages/enclosed-endless.eml') +ENCLOSED_BROKEN_BODY = read_fixture('messages/enclosed-broken-body.eml') +ENCLOSED_BROKEN_ENCODING = read_fixture( + 'messages/enclosed-bad-encoding.eml', binary=True) +FALSE_MULTIPART = read_fixture('messages/false-multipart.eml') +ENCODED_HEADER = read_fixture('messages/encoded-header.eml') +MESSAGE_EXTERNAL_BODY= read_fixture( 'messages/message-external-body.eml') -EIGHT_BIT = read_fixture_bytes('messages/8bitmime.eml') -BIG = read_fixture_bytes('messages/big.eml') -RUSSIAN_ATTACH_YAHOO = read_fixture_bytes( - 'messages/russian-attachment-yahoo.eml') -QUOTED_PRINTABLE = read_fixture_bytes('messages/quoted-printable.eml') -TEXT_ONLY = read_fixture_bytes('messages/text-only.eml') -MAILGUN_PIC = read_fixture_bytes('messages/mailgun-pic.eml') -BZ2_ATTACHMENT = read_fixture_bytes('messages/bz2-attachment.eml') -OUTLOOK_EXPRESS = read_fixture_bytes('messages/outlook-express.eml') - -AOL_FBL = read_fixture_bytes('messages/complaints/aol.eml') -YAHOO_FBL = read_fixture_bytes('messages/complaints/yahoo.eml') -NOTIFICATION = read_fixture_bytes('messages/bounce/no-mx.eml') -DASHED_BOUNDARIES = read_fixture_bytes('messages/dashed-boundaries.eml') -WEIRD_BOUNCE = read_fixture_bytes('messages/bounce/gmail-no-dns.eml') -WEIRD_BOUNCE_2 = read_fixture_bytes( +EIGHT_BIT = read_fixture('messages/8bitmime.eml') +BIG = read_fixture('messages/big.eml') +RUSSIAN_ATTACH_YAHOO = read_fixture( + 'messages/russian-attachment-yahoo.eml', binary=True) +QUOTED_PRINTABLE = read_fixture('messages/quoted-printable.eml') +TEXT_ONLY = read_fixture('messages/text-only.eml') +MAILGUN_PIC = read_fixture('messages/mailgun-pic.eml') +BZ2_ATTACHMENT = read_fixture('messages/bz2-attachment.eml') +OUTLOOK_EXPRESS = read_fixture('messages/outlook-express.eml') + +AOL_FBL = read_fixture('messages/complaints/aol.eml') +YAHOO_FBL = read_fixture('messages/complaints/yahoo.eml') +NOTIFICATION = read_fixture('messages/bounce/no-mx.eml') +DASHED_BOUNDARIES = read_fixture('messages/dashed-boundaries.eml') +WEIRD_BOUNCE = read_fixture('messages/bounce/gmail-no-dns.eml') +WEIRD_BOUNCE_2 = read_fixture( 'messages/bounce/gmail-invalid-address.eml') -WEIRD_BOUNCE_3 = read_fixture_bytes('messages/bounce/broken-mime.eml') -MISSING_BOUNDARIES = read_fixture_bytes('messages/missing-boundaries.eml') -MISSING_FINAL_BOUNDARY = read_fixture_bytes( +WEIRD_BOUNCE_3 = read_fixture('messages/bounce/broken-mime.eml') +MISSING_BOUNDARIES = read_fixture('messages/missing-boundaries.eml') +MISSING_FINAL_BOUNDARY = read_fixture( 'messages/missing-final-boundary.eml') -DISPOSITION_NOTIFICATION = read_fixture_bytes( +DISPOSITION_NOTIFICATION = read_fixture( 'messages/disposition-notification.eml') -MAILFORMED_HEADERS = read_fixture_bytes('messages/mailformed-headers.eml') - -SPAM_BROKEN_HEADERS = read_fixture_bytes('messages/spam/broken-headers.eml') -SPAM_BROKEN_CTYPE = read_fixture_bytes('messages/spam/broken-ctype.eml') -LONG_HEADER = read_fixture_bytes('messages/long-header.eml') -ATTACHED_PDF = read_fixture_bytes('messages/attached-pdf.eml') +MAILFORMED_HEADERS = read_fixture( + 'messages/mailformed-headers.eml', binary=True) +SPAM_BROKEN_HEADERS = read_fixture( + 'messages/spam/broken-headers.eml', binary=True) +SPAM_BROKEN_CTYPE = read_fixture('messages/spam/broken-ctype.eml') +LONG_HEADER = read_fixture('messages/long-header.eml') +ATTACHED_PDF = read_fixture('messages/attached-pdf.eml') # addresslib fixture files -MAILBOX_VALID_TESTS = read_fixture_bytes( - 'mailbox_valid.txt').decode('utf-8') -MAILBOX_INVALID_TESTS = read_fixture_bytes( - 'mailbox_invalid.txt').decode('utf-8') -ABRIDGED_LOCALPART_VALID_TESTS = read_fixture_bytes( - 'abridged_localpart_valid.txt').decode('utf-8') -ABRIDGED_LOCALPART_INVALID_TESTS = read_fixture_bytes( - 'abridged_localpart_invalid.txt').decode('utf-8') -URL_VALID_TESTS = read_fixture_bytes( - 'url_valid.txt').decode('utf-8') -URL_INVALID_TESTS = read_fixture_bytes( - 'url_invalid.txt').decode('utf-8') -DOMAIN_TYPO_VALID_TESTS = read_fixture_bytes( - 'domain_typos_valid.txt').decode('utf-8') -DOMAIN_TYPO_INVALID_TESTS = read_fixture_bytes( - 'domain_typos_invalid.txt').decode('utf-8') +MAILBOX_VALID_TESTS = read_fixture('mailbox_valid.txt') +MAILBOX_INVALID_TESTS = read_fixture('mailbox_invalid.txt') +ABRIDGED_LOCALPART_VALID_TESTS = read_fixture('abridged_localpart_valid.txt') +ABRIDGED_LOCALPART_INVALID_TESTS = read_fixture( + 'abridged_localpart_invalid.txt') +URL_VALID_TESTS = read_fixture('url_valid.txt') +URL_INVALID_TESTS = read_fixture('url_invalid.txt') +DOMAIN_TYPO_VALID_TESTS = read_fixture('domain_typos_valid.txt') +DOMAIN_TYPO_INVALID_TESTS = read_fixture('domain_typos_invalid.txt') From 508442dedfd678834f7ff0c62f189105c926f7a5 Mon Sep 17 00:00:00 2001 From: Maxim Vladimirskiy Date: Mon, 9 Apr 2018 10:46:47 +0300 Subject: [PATCH 2/3] Make flanker.message.headers package Python 3 compatible --- build.sh | 1 + flanker/mime/message/headers/encoding.py | 41 ++++++++++++--------- tests/mime/message/headers/encoding_test.py | 4 +- tests/mime/message/headers/headers_test.py | 26 ++++++------- 4 files changed, 39 insertions(+), 33 deletions(-) diff --git a/build.sh b/build.sh index 05a2cf39..bf839dc5 100755 --- a/build.sh +++ b/build.sh @@ -10,6 +10,7 @@ else nosetests --with-coverage --cover-package=flanker \ tests/addresslib \ tests/mime/bounce_tests.py \ + tests/mime/message/headers \ tests/mime/message/threading_test.py \ tests/mime/message/tokenizer_test.py \ tests/mime/message/headers/encodedword_test.py \ diff --git a/flanker/mime/message/headers/encoding.py b/flanker/mime/message/headers/encoding.py index 6f0dbcab..4b8613b4 100644 --- a/flanker/mime/message/headers/encoding.py +++ b/flanker/mime/message/headers/encoding.py @@ -11,15 +11,20 @@ _log = logging.getLogger(__name__) +# The value of email.header.MAXLINELEN constant changed from 76 to 78 in +# Python 3. To make sure that the library behaviour is consistent across all +# Python versions we introduced our own constant. +_MAX_LINE_LEN = 76 + _ADDRESS_HEADERS = ('From', 'To', 'Delivered-To', 'Cc', 'Bcc', 'Reply-To') def to_mime(key, value): if not value: - return "" + return '' if type(value) == list: - return "; ".join(encode(key, v) for v in value) + return '; '.join(encode(key, v) for v in value) return encode(key, value) @@ -32,38 +37,40 @@ def encode(name, value): return _encode_unstructured(name, value) except Exception: - _log.exception("Failed to encode %s %s" % (name, value)) + _log.exception('Failed to encode %s %s' % (name, value)) raise def _encode_unstructured(name, value): try: - return Header( - value.encode("ascii"), "ascii", - header_name=name).encode(splitchars=' ;,') + header = Header(value.encode('ascii'), 'ascii', header_name=name) + return header.encode(splitchars=' ;,') except (UnicodeEncodeError, UnicodeDecodeError): if _is_address_header(name, value): return _encode_address_header(name, value) - return Header( - to_utf8(value), "utf-8", - header_name=name).encode(splitchars=' ;,') + header = Header(to_utf8(value), 'utf-8', header_name=name) + return header.encode(splitchars=' ;,') def _encode_address_header(name, value): out = deque() for addr in flanker.addresslib.address.parse_list(value): if addr.requires_non_ascii(): - out.append(addr.to_unicode().encode('utf-8')) + encoded_addr = addr.to_unicode() + if six.PY2: + encoded_addr = encoded_addr.encode('utf-8') else: - out.append(addr.full_spec().encode('utf-8')) + encoded_addr = addr.full_spec() + + out.append(encoded_addr) return '; '.join(out) def _encode_parametrized(key, value, params): if params: params = [_encode_param(key, n, v) for n, v in six.iteritems(params)] - return value + "; " + ("; ".join(params)) + return value + '; ' + ('; '.join(params)) return value @@ -75,16 +82,16 @@ def _encode_param(key, name, value): return email.message._formatparam(name, value) except Exception: - value = Header(value.encode("utf-8"), "utf-8", header_name=key).encode(splitchars=' ;,') + header = Header(value.encode('utf-8'), 'utf-8', header_name=key) + value = header.encode(splitchars=' ;,') return email.message._formatparam(name, value) -def encode_string(name, value, maxlinelen=None): +def encode_string(name, value, maxlinelen=_MAX_LINE_LEN): try: - header = Header(value.encode("ascii"), "ascii", maxlinelen, - header_name=name) + header = Header(value.encode('ascii'), 'ascii', maxlinelen, name) except UnicodeEncodeError: - header = Header(value.encode("utf-8"), "utf-8", header_name=name) + header = Header(value.encode('utf-8'), 'utf-8', maxlinelen, name) return header.encode(splitchars=' ;,') diff --git a/tests/mime/message/headers/encoding_test.py b/tests/mime/message/headers/encoding_test.py index 3d648ffa..f9909494 100644 --- a/tests/mime/message/headers/encoding_test.py +++ b/tests/mime/message/headers/encoding_test.py @@ -59,12 +59,12 @@ def max_header_length_test(): message.to_string() ascii_subject = "This is simple ascii subject" - eq_(Header(ascii_subject.encode("ascii"), "ascii", header_name="Subject"), + eq_(Header(ascii_subject.encode("ascii"), "ascii", header_name="Subject").encode(), _encode_unstructured("Subject", ascii_subject)) unicode_subject = (u"Это сообщение с длинным сабжектом " u"специально чтобы проверить кодировки") - eq_(Header(unicode_subject.encode("utf-8"), "utf-8", header_name="Subject"), + eq_(Header(unicode_subject.encode("utf-8"), "utf-8", header_name="Subject").encode(), _encode_unstructured("Subject", unicode_subject)) diff --git a/tests/mime/message/headers/headers_test.py b/tests/mime/message/headers/headers_test.py index 9dbc13ce..fdb11bda 100644 --- a/tests/mime/message/headers/headers_test.py +++ b/tests/mime/message/headers/headers_test.py @@ -1,12 +1,10 @@ # coding:utf-8 import zlib -import six -from nose.tools import eq_, ok_, assert_false, assert_raises +from nose.tools import eq_, ok_, assert_false +from six.moves import StringIO -from flanker.mime.message.errors import DecodingError -from flanker.mime.message.headers import MimeHeaders -from flanker.mime.message.headers import encoding +from flanker.mime.message.headers import MimeHeaders, encoding from tests import BILINGUAL @@ -129,30 +127,30 @@ def headers_transform_encodedword_test(): eq_(u'Hello ☃', h.get('Subject')) def headers_parsing_empty_test(): - h = MimeHeaders.from_stream(six.StringIO("")) + h = MimeHeaders.from_stream(StringIO("")) eq_(0, len(h)) def headers_parsing_ridiculously_long_line_test(): val = "abcdefg"*100000 header = "Hello: {0}\r\n".format(val) - MimeHeaders.from_stream(six.StringIO(header)) + MimeHeaders.from_stream(StringIO(header)) def headers_parsing_binary_stuff_survives_test(): value = zlib.compress(b"abcdefg") header = "Hello: {0}\r\n".format(value) - ok_(MimeHeaders.from_stream(six.StringIO(header))) + ok_(MimeHeaders.from_stream(StringIO(header))) def broken_sequences_test(): - headers = six.StringIO(" hello this is a bad header\nGood: this one is ok") + headers = StringIO(" hello this is a bad header\nGood: this one is ok") headers = MimeHeaders.from_stream(headers) eq_(1, len(headers)) eq_("this one is ok", headers["Good"]) def bilingual_message_test(): - headers = MimeHeaders.from_stream(six.StringIO(BILINGUAL.decode('utf-8'))) + headers = MimeHeaders.from_stream(StringIO(BILINGUAL)) eq_(21, len(headers)) eq_(u"Simple text. How are you? Как ты поживаешь?", headers['Subject']) received_headers = headers.getall('Received') @@ -161,11 +159,11 @@ def bilingual_message_test(): def headers_roundtrip_test(): - headers = MimeHeaders.from_stream(six.StringIO(BILINGUAL.decode('utf-8'))) - out = six.StringIO() + headers = MimeHeaders.from_stream(StringIO(BILINGUAL)) + out = StringIO() headers.to_stream(out) - headers2 = MimeHeaders.from_stream(six.StringIO(out.getvalue())) + headers2 = MimeHeaders.from_stream(StringIO(out.getvalue())) eq_(21, len(headers2)) eq_(u"Simple text. How are you? Как ты поживаешь?", headers['Subject']) received_headers = headers.getall('Received') @@ -179,7 +177,7 @@ def headers_roundtrip_test(): def test_folding_combinations(): message = """From mrc@example.com Mon Feb 8 02:53:47 PST 1993\nTo: sasha\r\n continued\n line\nFrom: single line \r\nSubject: hello, how are you\r\n today?""" - headers = MimeHeaders.from_stream(six.StringIO(message)) + headers = MimeHeaders.from_stream(StringIO(message)) eq_('sasha continued line', headers['To']) eq_('single line ', headers['From']) eq_("hello, how are you today?", headers['Subject']) From ab7f56a3be126a751f2295c17b02ed9f2b9a7e5d Mon Sep 17 00:00:00 2001 From: Maxim Vladimirskiy Date: Mon, 9 Apr 2018 11:22:18 +0300 Subject: [PATCH 3/3] Make flanker.message.fallback package Python 3 compatible --- build.sh | 1 + flanker/mime/message/fallback/create.py | 6 ++ flanker/mime/message/fallback/part.py | 27 +++++---- flanker/mime/message/headers/headers.py | 5 +- tests/mime/message/fallback/fallback_test.py | 58 +++++++++++++------- 5 files changed, 64 insertions(+), 33 deletions(-) diff --git a/build.sh b/build.sh index bf839dc5..fee3e487 100755 --- a/build.sh +++ b/build.sh @@ -10,6 +10,7 @@ else nosetests --with-coverage --cover-package=flanker \ tests/addresslib \ tests/mime/bounce_tests.py \ + tests/mime/message/fallback \ tests/mime/message/headers \ tests/mime/message/threading_test.py \ tests/mime/message/tokenizer_test.py \ diff --git a/flanker/mime/message/fallback/create.py b/flanker/mime/message/fallback/create.py index ecc4f70c..e34154d7 100644 --- a/flanker/mime/message/fallback/create.py +++ b/flanker/mime/message/fallback/create.py @@ -1,8 +1,14 @@ import email + +import six + from flanker.mime.message.fallback.part import FallbackMimePart def from_string(string): + if six.PY3 and isinstance(string, six.binary_type): + string = string.decode('utf-8') + return FallbackMimePart(email.message_from_string(string)) diff --git a/flanker/mime/message/fallback/part.py b/flanker/mime/message/fallback/part.py index 2550deb0..1ec3a419 100644 --- a/flanker/mime/message/fallback/part.py +++ b/flanker/mime/message/fallback/part.py @@ -141,16 +141,16 @@ def add(self, key, value): MimeHeaders.add(self, key, value) self._m[key] = headers.to_mime(normalize(key), remove_newlines(value)) - def transform(self, func): + def transform(self, fn, decode=False): changed = [False] - def wrapped_func(key, value): - new_key, new_value = func(key, value) - if new_value != value or new_key != key: + def wrapper(key, val): + new_key, new_value = fn(key, val) + if new_value != val or new_key != key: changed[0] = True return new_key, new_value - transformed_headers = [wrapped_func(k, v) for k, v in self._m.items()] + transformed_headers = [wrapper(k, v) for k, v in self._m.items()] if changed[0]: self._m._headers = transformed_headers self._v = MultiDict([(normalize(k), remove_newlines(v)) @@ -161,15 +161,22 @@ def wrapped_func(key, value): def _try_decode(key, value): if isinstance(value, (tuple, list)): return value - elif isinstance(value, six.binary_type): + + if six.PY3: + assert (isinstance(key, six.text_type) and + isinstance(value, six.text_type)) + try: + return headers.parse_header_value(key, value) + except Exception: + return value + + if isinstance(value, six.binary_type): try: return headers.parse_header_value(key, value) except Exception: return value.decode('utf-8', 'ignore') - elif isinstance(value, six.text_type): + if isinstance(value, six.text_type): return value - else: - return "" - + raise TypeError('%s is not allowed type of header %s' % (type(value), key)) diff --git a/flanker/mime/message/headers/headers.py b/flanker/mime/message/headers/headers.py index 98938c84..e6b289e9 100644 --- a/flanker/mime/message/headers/headers.py +++ b/flanker/mime/message/headers/headers.py @@ -71,16 +71,15 @@ def transform(self, fn, decode=False): a new pair of key, val and applies the function to all header, value pairs in the message. """ - changed = [False] - def tracking_fn(key, val): + def wrapper(key, val): new_key, new_val = fn(key, val) if new_val != val or new_key != key: changed[0] = True return new_key, new_val - v = MultiDict(tracking_fn(key, val) for key, val in self.iteritems(raw=not decode)) + v = MultiDict(wrapper(k, v) for k, v in self.iteritems(raw=not decode)) if changed[0]: self._v = v self.changed = True diff --git a/tests/mime/message/fallback/fallback_test.py b/tests/mime/message/fallback/fallback_test.py index eb322eda..5dd45111 100644 --- a/tests/mime/message/fallback/fallback_test.py +++ b/tests/mime/message/fallback/fallback_test.py @@ -1,13 +1,14 @@ # coding:utf-8 import email -from cStringIO import StringIO from contextlib import closing from email import message_from_string -from nose.tools import ok_, eq_, assert_false -from flanker.mime.message import ContentType +import six +from nose.tools import ok_, eq_, assert_false, assert_equals +from six.moves import StringIO +from flanker.mime.message import ContentType from flanker.mime.message.fallback import create from flanker.mime.message.scanner import scan from tests import (IPHONE, ENCLOSED, TORTURE, TEXT_ONLY, MAILFORMED_HEADERS, @@ -95,7 +96,9 @@ def message_content_dispositions_test(): parts = list(message.walk(with_self=True)) # content disposition value is anything (including unicode chars) up to the first space, tab or semicolon # but non-ascii value will raise DecodeError - eq_(('Нельзя', {}), parts[0].content_disposition) + # FIXME: In python 2 the returned value is binary, should it be unicode? + expected_cd = u'нельзя' if six.PY3 else 'Нельзя' + eq_((expected_cd, {}), parts[0].content_disposition) def message_from_python_test(): @@ -169,13 +172,20 @@ def bounce_test(): # Then ok_(message.is_bounce()) eq_('5.1.1', message.bounce.status) - eq_('smtp; 550-5.1.1 The email account that you tried to reach does ' - 'not exist. Please try 550-5.1.1 double-checking the recipient\'s email ' - 'address for typos or 550-5.1.1 unnecessary spaces. Learn more at ' - '550 5.1.1 http://mail.google.com/support/bin/answer.py?answer=6596 ' - '17si20661415yxe.22', - message.bounce.diagnostic_code) + expected_code = ( + 'smtp; 550-5.1.1 The email account that you tried to reach does ' + 'not exist. Please try 550-5.1.1 double-checking the recipient\'s email ' + 'address for typos or 550-5.1.1 unnecessary spaces. Learn more at ' + '550 5.1.1 http://mail.google.com/support/bin/answer.py?answer=6596 ' + '17si20661415yxe.22') + + # In Python 2 email.message.Message used to truncate leading spaces, but + # in Python 3 leading spaces are preserved. + if six.PY2: + expected_code = expected_code.replace(' ', ' ').replace(' ', ' ') + + eq_(expected_code, message.bounce.diagnostic_code) def torture_test(): message = create.from_string(TORTURE) @@ -296,18 +306,26 @@ def bilingual_test(): def broken_headers_test(): - message = create.from_string(MAILFORMED_HEADERS) + if six.PY2: + message = create.from_string(MAILFORMED_HEADERS) + else: + message = create.from_string(MAILFORMED_HEADERS.decode('utf-8', 'replace')) + ok_(message.headers['Subject']) - eq_(unicode, type(message.headers['Subject'])) + eq_(six.text_type, type(message.headers['Subject'])) def broken_headers_test_2(): - message = create.from_string(SPAM_BROKEN_HEADERS) + if six.PY2: + message = create.from_string(SPAM_BROKEN_HEADERS) + else: + message = create.from_string(SPAM_BROKEN_HEADERS.decode('utf-8', 'replace')) + ok_(message.headers['Subject']) - eq_(unicode, type(message.headers['Subject'])) + eq_(six.text_type, type(message.headers['Subject'])) eq_(('text/plain', {'charset': 'iso-8859-1'}), message.headers['Content-Type']) - eq_(unicode, type(message.body)) + eq_(six.text_type, type(message.body)) def test_walk(): @@ -339,9 +357,9 @@ def test_binary_attachment(): # Then def part_spec(p): - return str(p.content_type), str(type(p.body)) + return str(p.content_type), type(p.body) - eq_(('multipart/mixed', ""), part_spec(parts[0])) - eq_(('multipart/alternative', ""), part_spec(parts[1])) - eq_(('text/plain', ""), part_spec(parts[2])) - eq_(('application/pdf', ""), part_spec(parts[3])) + eq_(('multipart/mixed', type(None)), part_spec(parts[0])) + eq_(('multipart/alternative', type(None)), part_spec(parts[1])) + eq_(('text/plain', six.text_type), part_spec(parts[2])) + eq_(('application/pdf', six.binary_type), part_spec(parts[3]))