Автор много лет использует самописный скрипт на Python для отображения MIME-писем в терминале. Недавно он перевёл его с Python 2 на Python 3, и в процессе накопил несколько неочевидных наблюдений о модуле email.
Документация утверждает, что современный интерфейс — это email.message.EmailMessage, а старый email.message.Message оставлен только для совместимости с Python 3.2. На практике это не совсем так. Большинство методов EmailMessage для чтения — просто обёртки над Message.get_payload(), и эта функция остаётся незаменимой для кривых писем. Реальный пример: почтовая система присылает bounce, в котором оригинальное письмо с типом multipart/alternative вложено в message/rfc822, но всё тело заменено на '(Body suppressed)'. Никакой EmailMessage API не даст просто текст такого вложения, только старый get_payload().
При этом EmailMessage.get_content() — удобная штука, он сам декодирует текст в Unicode. Автор использует его везде, где можно, но с осторожностью: get_content() доверяет указанной в MIME кодировке, а некоторые программы просто пихают UTF-8 даже без указания типа (теоретически там ASCII).
Отдельная боль — парсинг message/delivery-status. Документация email.parser в этом месте врёт. Реальность такая: этот тип MIME разбирается как multipart, каждый блок заголовков (разделённых пустой строкой) становится отдельным EmailMessage с типом text/plain, но без тела — содержимое сидит в заголовках дочернего объекта. Чтобы вытащить текст, нужно вызывать .as_string() на каждом ребёнке. get_content() на родительском multipart'е не работает, возможно, это баг.
И про типизацию. Автор добавил типы в свой скрипт и называет это «ценным опытом». Основная проблема: mypy часто выводит тип email.message.Message вместо EmailMessage. Помогает assert isinstance(m, EmailMessage) — это сужает тип. Хуже с get_payload(): mypy видит list[Message], а автору нужен list[EmailMessage]. Приходится использовать typing.cast(list[EmailMessage], m).