Skip to content

email.message get_payload throws UnicodeEncodeError with some surrogate Unicode characters #94606

Closed

Description

@sidney

email.message get_payload gets a UnicodeEncodeError if the message body contains a line that has either:
a Unicode surrogate code point that is valid for surrogateescape encoding (U-DC80 through U-DCFF) and a non ASCII UTF-8 character
OR
a Unicode surrogate character that is not valid for surrogateescape encoding

Here is a minimal code example with one of the cases commented out

from email import message_from_string
from email.message import EmailMessage

m = message_from_string("surrogate char \udcc3 and 8-bit utf-8 ÃŦ on same line")
# m = message_from_string("surrogate char \udfff does it by itself")
payload = m.get_payload(decode=True)

On my python 3.10.5 on macOS this produces:

Traceback (most recent call last):
  File "/Users/sidney/tmp/./test5.py", line 8, in <module>
    payload = m.get_payload(decode=True)
  File "/usr/local/Cellar/python@3.10/3.10.5/Frameworks/Python.framework/Versions/3.10/lib/python3.10/email/message.py", line 264, in get_payload
    bpayload = payload.encode('ascii', 'surrogateescape')
UnicodeEncodeError: 'ascii' codec can't encode character '\xeb' in position 33: ordinal not in range(128)

This was tested on python 3.10.5 on macOS, however I tracked it down based on a report in the wild that was running python 3.8 on Ubuntu 20.04 processing actual emails

Linked PRs

Activity

  1. added
    type-bugAn unexpected behavior, bug, or error
    on Jul 6, 2022
  2. sidney commented on Jul 6, 2022

    @sidney
    ContributorAuthor

    I tracked this down to code in email.message.py that has the following

       if utils._has_surrogates(payload):
           bpayload = payload.encode('ascii', 'surrogateescape')
    

    This looks like it is supposed to check if payload is a string that was created by decode with surrogateescape, and if it was, then convert it back to bytes the same way. PEP 383 is clear that surrogateescape is only for round trip decoding then encoding. encode with surrogateescaape should never be called on strings not created with decode surrogateescape.

    The problem is that utils._has_surrogates(payload) is only a fast heuristic when it is used to guess if payload contains a string that was produced by decoding with surrogateescape. The function actually flags strings that contain any Unicode surrogate characters. However, strings that were created by decode with surrogateescape will only have Unicode surrogate characters in the U-DC80 through U-DCFF range and remaining characters will only be 7 bit ASCII. If a string either has an non-ASCII UTF8 character in addition to a Unicode surrogate character, or has a Unicode surrogate character out of that range, utils._has_surrogates() will return true but the string encode will raise the exception.

    This should be able to be fixed by catching the exception raised by the encode and proceeding to get the same result the code would have if utils._has_surrogates(payload) had returned false.

    I'll submit a PR for that after testing.

  3. added 2 commits that reference this issue on Jul 7, 2022
  4. added a commit that references this issue on Dec 11, 2023
  5. added 2 commits that reference this issue on Dec 11, 2023
  6. added 2 commits that reference this issue on Dec 11, 2023
  7. added a commit that references this issue on Feb 11, 2024
  8. added a commit that references this issue on Sep 2, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions