Repository navigation
BytesGenerator breaks UTF8 string #92081
Description
Hi!
I found an issue when sending emails with Cyrillic letters in Subject header. Some spaces at Subject header are trimmed when sent.
Example:
When sending email with below subject:
ÐĢÐēÐĩÐīÐūОÐŧÐĩÐ―ÐļÐĩ Ðū ÐŋŅÐļÐ―ŅŅÐļÐļ Ðē ŅÐ°ÐąÐūŅŅ ÐūÐąŅаŅÐĩÐ―ÐļŅ
at SMTP server logs I see subject that differs from original:
ÐĢÐēÐĩÐīÐūОÐŧÐĩÐ―ÐļÐĩ Ðū ÐŋŅÐļÐ―ŅŅÐļÐļÐē ŅÐ°ÐąÐūŅŅ ÐūÐąŅаŅÐĩÐ―ÐļŅ
- As you can see, space between words "ÐŋŅÐļÐ―ŅŅÐļÐļ Ðē" was stripped.
During research I've found that problem relates to small piece of code which encodes EmailMessage instance to byte string.
Python versions tested and problem confirmed: 3.8, 3.9, 3.10
Here is minimal reproducible example. Code can be used "as is", without any third party packages.
Minimal reproducible example
import io
import email.generator
from email.message import EmailMessage
from email.header import decode_header
def encode_decode(subject: str):
# preparing EmailMessage
msg = EmailMessage()
msg['Subject'] = subject
# below code sample was taken from "send_message" function (lib/python3.8/smtplib.py)
# this is the place where problem actually appears
with io.BytesIO() as bytesmsg:
g = email.generator.BytesGenerator(bytesmsg)
g.flatten(msg, linesep='\r\n')
flatmsg = bytesmsg.getvalue()
# assembling string and cutting off beginning part ('Subject: ')
result = ''
for string, encoding in decode_header(flatmsg.decode()):
result += string.decode(encoding=encoding or 'utf8')
return result[9:]
if __name__ == '__main__':
test_cases = [
'ŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅ', # ok
'ŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅ ', # ok
'ŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅ Ņ', # ok
'ŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅ ŅŅ', # ok
'ŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅ Ņ Ņ', # broken
'ŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅ ŅŅ Ņ', # ok
'ŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅ Ņ Ņ Ņ', # ok
'ŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅ Ņ Ņ Ņ Ņ', # broken
'ŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅ Ņ ŅŅ Ņ Ņ', # broken
'ŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅ Ņ ŅŅŅ Ņ Ņ', # broken
'ŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅ Ņ ŅŅŅŅ Ņ Ņ', # broken
'ŅŅŅŅŅŅŅŅŅŅŅŅŅŅ Ņ ŅŅŅŅŅ Ņ Ņ', # broken
'ŅŅŅŅŅŅŅŅŅŅŅŅŅ Ņ ŅŅŅŅŅŅ Ņ Ņ', # broken
'ŅŅŅŅŅŅŅŅŅŅŅŅ Ņ ŅŅŅŅŅŅŅ Ņ Ņ', # broken
'ŅŅŅŅŅŅŅŅŅŅŅ Ņ ŅŅŅŅŅŅŅŅ Ņ Ņ', # broken
'ŅŅŅŅŅŅŅŅŅŅ Ņ ŅŅŅŅŅŅŅŅŅ Ņ Ņ', # broken
'ŅŅŅŅŅŅŅŅŅ Ņ ŅŅŅŅŅŅŅŅŅŅ Ņ Ņ', # broken
'ŅŅŅŅŅŅŅŅ Ņ ŅŅŅŅŅŅŅŅŅŅŅ Ņ Ņ', # broken
'ŅŅŅŅŅŅŅ Ņ ŅŅŅŅŅŅŅŅŅŅŅŅ Ņ Ņ', # broken
'ŅŅŅŅŅŅ Ņ ŅŅŅŅŅŅŅŅŅŅŅŅŅ Ņ Ņ', # broken
'ŅŅŅŅŅ Ņ ŅŅŅŅŅŅŅŅŅŅŅŅŅŅ Ņ Ņ', # broken
'ŅŅŅŅ Ņ ŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅ Ņ Ņ', # broken
'ŅŅŅ Ņ ŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅ Ņ Ņ', # broken
'ŅŅ Ņ ŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅ Ņ Ņ', # broken
'Ņ Ņ ŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅ Ņ Ņ', # broken
' Ņ ŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅ Ņ Ņ', # broken
'Ņ ŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅ Ņ Ņ', # ok
' ŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅ Ņ Ņ', # broken
'ŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅ Ņ Ņ', # ok
]
for in_ in test_cases:
out_ = encode_decode(in_)
res = 'ok' if out_ == in_ else 'broken'
print(f'In | {in_}', f'Out | {out_}', f'Res | {res}\n', sep='\n')
Above code demonstrates inequality of input and output strings after encoding message with BytesGenerator. Please note that not all strings with Cyrillic letters are broken. Only those strings that have word with single Cyrillic char only are affected under some conditions.
Small additional list of string with explanations you can find below:
Additional strings with explanations
# Example 1 - below string will be broken
'ÐĢÐēÐĩÐīÐūОÐŧÐĩÐ―ÐļÐĩ Ðū ÐŋŅÐļÐ―ŅŅÐļÐļ Ðē ŅÐ°ÐąÐūŅŅ ÐūÐąŅаŅÐĩÐ―ÐļŅ ÐīÐŧŅ ÐŋÐūÐīКÐŧŅŅÐĩÐ―ÐļŅ ŅŅÐŧŅÐģÐļ',
# fixed version (removed one cyrillic UTF8 "Ðļ" from word "ÐŋŅÐļÐ―ŅŅÐļÐļ")
'ÐĢÐēÐĩÐīÐūОÐŧÐĩÐ―ÐļÐĩ Ðū ÐŋŅÐļÐ―ŅŅÐļ Ðē ŅÐ°ÐąÐūŅŅ ÐūÐąŅаŅÐĩÐ―ÐļŅ ÐīÐŧŅ ÐŋÐūÐīКÐŧŅŅÐĩÐ―ÐļŅ ŅŅÐŧŅÐģÐļ',
# fixed version (changed cyrillic UTF8 letter "Ðū" to ASCII letter "o")
'ÐĢÐēÐĩÐīÐūОÐŧÐĩÐ―ÐļÐĩ o ÐŋŅÐļÐ―ŅŅÐļÐļ Ðē ŅÐ°ÐąÐūŅŅ ÐūÐąŅаŅÐĩÐ―ÐļŅ ÐīÐŧŅ ÐŋÐūÐīКÐŧŅŅÐĩÐ―ÐļŅ ŅŅÐŧŅÐģÐļ',
#
# Example 2 - below string will be broken
'ÐĢÐēÐĩÐīÐūОÐŧÐĩÐ―ÐļÐĩ ÐŋŅÐļÐ―ŅŅÐļÐļ Ðē ŅÐ°ÐąÐūŅŅ ÐūÐąŅаŅÐĩÐ―ÐļŅ ÐīÐŧŅ ÐŋÐūÐīКÐŧŅŅÐĩÐ―ÐļŅ ŅŅÐŧŅÐģÐļ',
# fixed version (removed preposition "Ðē" that consist from single cyrillic UTF8 letter)
'ÐĢÐēÐĩÐīÐūОÐŧÐĩÐ―ÐļÐĩ ÐŋŅÐļÐ―ŅŅÐļÐļ ŅÐ°ÐąÐūŅŅ ÐūÐąŅаŅÐĩÐ―ÐļŅ ÐīÐŧŅ ÐŋÐūÐīКÐŧŅŅÐĩÐ―ÐļŅ ŅŅÐŧŅÐģÐļ',
# fixed version (changed preposition "Ðē" with cyrillic UTF8 letter to ASCII letter "B")
'ÐĢÐēÐĩÐīÐūОÐŧÐĩÐ―ÐļÐĩ ÐŋŅÐļÐ―ŅŅÐļÐļ B ŅÐ°ÐąÐūŅŅ ÐūÐąŅаŅÐĩÐ―ÐļŅ ÐīÐŧŅ ÐŋÐūÐīКÐŧŅŅÐĩÐ―ÐļŅ ŅŅÐŧŅÐģÐļ',
Linked PRs
- gh-92081: Fix for email.generator.Generator with whitespace between encoded words. #92281
- [3.13] gh-92081: Fix for email.generator.Generator with whitespace between encoded words. (GH-92281)Â #119245
- [3.12] gh-92081: Fix for email.generator.Generator with whitespace between encoded words. (GH-92281)Â #119246
Activity
mrabarnett commentedon Apr 30, 2022
The problem has something to do with the maximum header length when the message is encoded to ASCII.
Increasing the maximum header length (the default is 78) makes the problem disappear for the sample tests:
Yuribtr commentedon May 1, 2022
Thanks for hint. As temporary workaround I set maxheaderlen to zero:
g = email.generator.BytesGenerator(bytesmsg, maxheaderlen=0)UPD.
For sending emails you should set max_line_length greater than 3. This is necessary in order to avoid ValueError at /Lib/email/quoprimime.py::body_encode() in some cases. I prefer to set max_line_length to maximum.
abadger commentedon May 3, 2022
Note: This problem will occur with
Generatoras well asBytesGenerator.What I believe is happening in the problem cases is that the Generator is creating two or more encoded-words. A space character present in the input is placed between the encoded words rather than becoming a part of the encoded words. When the decoder is run, the decoder discards the linear whitespace that occurs between the encoded words. This leads to the omission of the space in the round tripped data.
Here's the output from the Generator for one of the problem cases:
You can see that there are two spaces (in addition to the
\r\n) in between the two encoded blocks.After reading the examples in https://www.rfc-editor.org/rfc/rfc2047 I believe that the generator is at fault here. The space characters should be added to either the leading or trailing encoded-word rather than being emitted as a literal between the words.
abadger commentedon May 3, 2022
Still exploring precisely what is going on but I have been able to modify the code to fix all of the cases reported by forcing whitespace to be encoded more frequently in
_header_value_parser.py::_refold_parse_tree()and preventing leading and trailing whitespace from being detected in_fold_as_ew(). I think my changes are too broad but I'll post them here in case something happens to me before I have a chance to finish diagnosing this:EDIT: Only leading whitespace needs to be undetected in
_fold_as_ew()abadger commentedon May 3, 2022
Closer..... This change in
_fold_as_ewfixes all but two of the newly reported problems while all the unittests continue to pass:The two which are broken are:
ŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅ Ņ ŅandŅ ŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅŅ Ņ ŅI believe something like the above should make it into the final fix as the comment for this block says that it should only be invoked if we're joining this encoded-word to a non-encoded word but there's nothing in this condition which checks that the previous word was non-encoded.
And
elif to_encode[0]:might also do the right thing here.Fix for email.generator.Generator with whitespace between encoded words.
abadger commentedon May 4, 2022
I've opened a PR that fixes the cases where we need spaces between encoded words.
I've figured out that the remaining two problems start with a space. I'm searching the RFCs but so far haven't found anything that says whether the generator should handle this by including the initial space in the initial encoded word or if the decoder should handle it by displaying the space (or as a third option, that initial spaces in a Subject should be ignored).
Fix for email.generator.Generator with whitespace between encoded words.
Fix for email.generator.Generator with whitespace between encoded words.
abadger commentedon Apr 27, 2023
I believe #92281 now fixes all the problems reported here. If anyone would like to test that the problems are resolved by it, that would be appreciated!
Fix for email.generator.Generator with whitespace between encoded words.
gh-92081: Fix for email.generator.Generator with whitespace between eâĶ
pythongh-92081: Fix for email.generator.Generator with whitespace betâĶ
pythongh-92081: Fix for email.generator.Generator with whitespace betâĶ
[3.13] gh-92081: Fix for email.generator.Generator with whitespace beâĶ
[3.12] gh-92081: Fix for email.generator.Generator with whitespace beâĶ
warsaw commentedon May 20, 2024
Thanks for the fix @abadger
pythongh-92081: Fix for email.generator.Generator with whitespace betâĶ
gh-144156: Fix email header folding concatenating encoded words (#144692
pythongh-144156: Fix email header folding concatenating encoded words (âĶ
[3.14] gh-144156: Fix email header folding concatenating encoded words (
[3.13] gh-144156: Fix email header folding concatenating encoded words (
pythongh-144156: Fix email header folding concatenating encoded words (âĶ
pythongh-144156: Fix email header folding concatenating encoded words (âĶ