diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b1b2af7..77294e96 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [0.9.9] - 2019-09-25 +### Changed +- Replace the leading '.' in an quoted-printable encoded mime part to avoid + obscure SMTP bug + ## [0.9.0] - 2018-05-16 ### Changed - Support for Python 3 was added with preserving the Python 2 behavior in mind. diff --git a/flanker/mime/message/part.py b/flanker/mime/message/part.py index 008d51a3..d35c934c 100644 --- a/flanker/mime/message/part.py +++ b/flanker/mime/message/part.py @@ -631,7 +631,8 @@ def _encode_charset(preferred_charset, text): def _encode_transfer_encoding(encoding, body): if six.PY3: if encoding == 'quoted-printable': - body = fix_leading_dot(quopri.encodestring(body, quotetabs=False)) + body = quopri.encodestring(body, quotetabs=False) + body = fix_leading_dot(body) return body.decode('utf-8') if encoding == 'base64': @@ -647,7 +648,8 @@ def _encode_transfer_encoding(encoding, body): return body if encoding == 'quoted-printable': - return fix_leading_dot(quopri.encodestring(body, quotetabs=False)) + body = quopri.encodestring(body, quotetabs=False) + return fix_leading_dot(body) elif encoding == 'base64': return _email.encode_base64(body) else: @@ -668,22 +670,27 @@ def fix_leading_dot(s): We have observed some remote SMTP servers have an intermittent obscure bug where the leading '.' is removed according to the above spec. Even when the '.' - is obviously within the bounds of a mime part. To combat this we convert any - leading '.' to a '=2E' + is obviously within the bounds of a mime part, and with our sending SMTP + clients dot stuffing the line. To combat this we convert any leading '.' + to a '=2E'. """ - infp = StringIO(s) - outfp = StringIO() + infp = six.BytesIO(s) + outfp = six.BytesIO() # TODO(thrawn01): We could scan the entire string looking for leading '.' # If none found return the original string. This would save memory at the # expense of some additional processing + dot = b"." + if six.PY3: + dot = ord('.') + while 1: line = infp.readline() if not line: break - if line[0] == '.': + if line[0] == dot: line = _quote_and_cut(line) outfp.write(line) @@ -697,8 +704,7 @@ def _quote_and_cut(ln): cut the line in half without dividing any quoted characters and conforming to the quoted-printable RFC in regards to ending characters. """ - q = quopri.quote(ln[0]) - ln = q + ln[1:] + ln = quopri.quote(ln[0:1]) + ln[1:] # If the line is under the 76 + '\n' character limit if len(ln) <= 77: @@ -719,10 +725,13 @@ def _quote_and_cut(ln): if pos > len(ln)/2: break + if six.PY3: + c = bytes((c,)) + # Should be a quoted character - if c == '=': + if c == b'=': # Peak ahead, do the next 2 chars appear to be a hex values? - if quopri.ishex(ln[pos+1]) and quopri.ishex(ln[pos+2]): + if quopri.ishex(ln[pos+1:pos+3]): in_quote = 1 continue @@ -730,14 +739,18 @@ def _quote_and_cut(ln): next_line = ln[pos:] # If new line ends with a :space or :tab - if new_line[-1:] in ' \t': + if new_line[-1:] in b' \t': new_line = new_line[:-1] + quopri.quote(new_line[-1:]) + dot = b'.' + if six.PY3: + dot = ord('.') + # If the next line starts with a '.' - if next_line[0] == '.': - next_line = quopri.quote(next_line[0]) + next_line[1:] + if next_line[0] == dot: + next_line = quopri.quote(next_line[0:1]) + next_line[1:] - return new_line + "=\n" + next_line + return new_line + b"=\n" + next_line def _choose_text_encoding(charset, preferred_encoding, body): diff --git a/tests/mime/message/headers/part_test.py b/tests/mime/message/headers/part_test.py index 7d82a8b3..8ffc9fbd 100644 --- a/tests/mime/message/headers/part_test.py +++ b/tests/mime/message/headers/part_test.py @@ -5,44 +5,44 @@ STRINGS = ( # Some normal strings - ('', ''), - ('hello', 'hello'), - ('''hello + (b'', ''), + (b'hello', 'hello'), + (b'''hello there world''', '''hello there world'''), - ('''hello + (b'''hello there world ''', '''hello there world '''), - ('\201\202\203', '=81=82=83'), + (b'\201\202\203', '=81=82=83'), # Add some trailing MUST QUOTE strings - ('hello ', 'hello=20'), - ('hello\t', 'hello=09'), + (b'hello ', 'hello=20'), + (b'hello\t', 'hello=09'), # Some long lines. First, a single line of 108 characters - ('xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\xd8\xd9\xda\xdb\xdc\xdd\xde\xdfxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', + (b'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\xd8\xd9\xda\xdb\xdc\xdd\xde\xdfxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', '''xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx=D8=D9=DA=DB=DC=DD=DE=DFx= xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'''), # A line of exactly 76 characters, no soft line break should be needed - ('yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy', + (b'yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy', 'yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy'), # A line of 77 characters, forcing a soft line break at position 75, # and a second line of exactly 2 characters (because the soft line # break `=' sign counts against the line length limit). - ('zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz', + (b'zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz', '''zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz= zz'''), # A line of 151 characters, forcing a soft line break at position 75, # with a second line of exactly 76 characters and no trailing = - ('zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz', + (b'zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz', '''zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz= zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz'''), @@ -50,88 +50,88 @@ # 151 characters and the second line is exactly 76 characters. This # should leave us with three lines, the first which has a soft line # break, and which the second and third do not. - ('''yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy + (b'''yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz''', '''yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy= yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz'''), # Lines that end with space or tab should be quoted - ('yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy ', + (b'yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy ', '''yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy= =20'''), # Lines that end with a partial quoted character - ('yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy=y', + (b'yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy=y', '''yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy= =3Dy'''), # Lines that lead with a dot '.' should have the dot quoted - ('zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz.z', + (b'zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz.z', 'zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz=\n' + '=2Ez'), # Lines that end with a dot '.' are not quoted - ('zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz.zz', + (b'zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz.zz', 'zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz.=\n' + 'zz'), # Lines that lead with a dot '.' should have the dot quoted and cut # if the quoted line is longer than 76 characters. - ('zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz.zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz', + (b'zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz.zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz', 'zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz=\n' + '=2Ezzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz=\nzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz=\n' + 'zz'), # Respect quoted characters when considering leading '.' - ('zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz' + - '.\x7f\x7f\x7f\x7f\x7f\x7f\x7f\x7f\x7f\x7f\x7f\x7f\x7f\x7f\x7f\x7f\x7f\x7f\x7f\x7f\x7f\x7f\x7f\x7f\x7f\x7f\x7f', + (b'zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz' + + b'.\x7f\x7f\x7f\x7f\x7f\x7f\x7f\x7f\x7f\x7f\x7f\x7f\x7f\x7f\x7f\x7f\x7f\x7f\x7f\x7f\x7f\x7f\x7f\x7f\x7f\x7f\x7f', 'zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz=\n' + '=2E=7F=7F=7F=7F=7F=7F=7F=7F=7F=7F=7F=7F=7F=7F=7F=7F=7F=7F=7F=7F=7F=7F=7F=7F=\n' + '=7F=7F=7F'), # Should cut somewhere near the middle of the line - ('zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz' + - '.quick brown fox, quick brown cat, quick hot dog, quick read dog, quick white bird', + (b'zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz' + + b'.quick brown fox, quick brown cat, quick hot dog, quick read dog, quick white bird', 'zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz=\n' '=2Equick brown fox, quick brown cat, qui=\n' + 'ck hot dog, quick read dog, quick whi=\n' + 'te bird'), # Respect quoted character when considering where to cut - ('zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz' + - '.quick brown fox, quick brown cat\x7f\x7f\x7f\x7f\x7f, quick read dog, quick white bird', + (b'zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz' + + b'.quick brown fox, quick brown cat\x7f\x7f\x7f\x7f\x7f, quick read dog, quick white bird', 'zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz=\n' + '=2Equick brown fox, quick brown cat=7F=7F=\n' + '=7F=7F=7F, quick read dog, quick whi=\n' + 'te bird'), # Avoid considering non quoted characters when cutting - ('zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz' + - '.quick brown fox, quick brown cat=20=================, quick read dog, quick white bird', + (b'zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz' + + b'.quick brown fox, quick brown cat=20=================, quick read dog, quick white bird', 'zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz=\n' + '=2Equick brown fox, quick brown cat=3D20=\n' + '=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=\n' + '=3D=3D=3D=3D=3D, quick read dog, quick white bird'), # Should quote leading '.' if the cut results in a '.' on the next line - ('zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz' + - '.quick brown fox, quick brown cat..................... quick read dog, quick white bird', + (b'zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz' + + b'.quick brown fox, quick brown cat..................... quick read dog, quick white bird', 'zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz=\n' + '=2Equick brown fox, quick brown cat.....=\n' + '=2E............... quick read dog, quic=\n' + 'k white bird'), # Should quote :space if the cut results in a :space at the end of the next line - ('zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz' + - '.quick brown fox, quick brown cat quick read dog, quick white bird', + (b'zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz' + + b'.quick brown fox, quick brown cat quick read dog, quick white bird', 'zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz=\n' + '=2Equick brown fox, quick brown cat =20=\n' + ' quick read dog, quic=\n' + 'k white bird'), # Should quote :tab if the cut results in a :tab at the end of the next line - ('zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz' + - '.quick brown fox, quick brown cat \t quick read dog, quick white bird', + (b'zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz' + + b'.quick brown fox, quick brown cat \t quick read dog, quick white bird', 'zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz=\n' + '=2Equick brown fox, quick brown cat =09=\n' + ' quick read dog, quic=\n' +