Обходное решение: ошибка создания текста сообщения при наличии двоичных вложений

obhodnoe reshenie oshibka sozdaniya teksta soobshheniya pri nalichii dvoichnyh vlozhenij Сценарии на стороне клиента

Последние две проблемы с поддержкой Юникода в пакете email являются самыми настоящими ошибками, из-за которых сейчас приходится искать обходные решения, но которые наверняка будут исправлены в одной из будущих версий Python. Первая из них препятствует созданию текста практически всех, кроме самых тривиальных, сообщений — нынешняя версия пакета email больше не поддерживает возможность создания полного текста сообщений, содержащих любые двоичные части, такие как изображения или аудиофайлы. Без обходного решения с помощью пакета email в Python 3.1 можно создавать только самые простые почтовые сообщения, содержащие исключительно текстовые части, — любое двоичное содержимое в формате MIME вызывает ошибку при создании полного текста сообщения.

Это сложно понять без детального изучения исходных текстов пакета email (что, к счастью, нам доступно в мире открытых исходных текстов). Но чтобы продемонстрировать проблему, сначала посмотрите, как отображается простое текстовое содержимое в полный текст сообщения при печати:

C:\…\PP4E\Internet\Email> python

>  >> from email.message import Message # обобщенный объект сообщения

>  >> m = Message()

>  >> m[‘From’] = ‘bob@bob.com’

>  >> m.set_payload(open(‘text.txt’).read()) # содержимое текст str

>  >> print(m) # print использует as_string()

From: bob@bob.com

spam

Spam

SPAM!

Мы также уже видели, что для удобства пакет email предоставляет специализированные подклассы класса Message, предназначенные для добавления заголовков с дополнительной описательной информацией, которая используется почтовыми клиентами, чтобы определить, как обрабатывать данные:

>  >> from email.mime.text import MIMEText # подкласс Message с заголовками

>  >> text = open(‘text.txt’).read()

>  >> m = MIMEText(text) # содержимое — текст str

>  >> m[‘From’] = ‘bob@bob.com’

>  >> print(m)

Content-Type: text/plain; charset="us-ascii"

MIME-Version: 1.0

Content-Transfer-Encoding: 7bit

From: bob@bob.com spam Spam SPAM!

С текстом никаких проблем не возникает, но посмотрите, что произойдет, если попытаться отобразить часть сообщения с действительно двоичными данными, такими как изображение, которое не может быть декодировано в текст Юникода:

>  >> from email.message import Message # обобщенный объект сообщения

>  >> m = Message()

>  >> m[‘From’] = ‘bob@bob.com’

>  >> bytes = open(‘monkeys.jpg’, ‘rb’).read() # прочитать байты (не Юникод)

>  >> m.set_payload(bytes) # установить, что содержимое двоичное

>  >> print(m)

Traceback (most recent call last):

…часть строк опущена…

File "C:\Python31\lib\email\generator.py", line 155, in _handle_text raise TypeError(‘string payload expected: %s’ % type(payload)) TypeError: string payload expected: <class ‘bytes’> (TypeError: ожидается строковое содержимое: <class ‘bytes’>)

>  >> m.get_payload()[:20]

b’\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x01\x01\x00x\x00x\x00\x00′

Проблема состоит в том, что механизм создания полного текста сообщения в пакете email предполагает получить данные для содержимого в виде строки str в MIME-кодировке Base64 (или подобной ей), но не bytes. В действительности, в данном случае, вероятно, мы сами виноваты в появлении ошибки, потому что мы устанавливаем двоичное содержимое вручную. Мы должны использовать подкласс MIMEImage с поддержкой кодирования в формат MIME, предназначенный для изображений, — в этом случае пакет email автоматически выполнит MIME-кодирование Base64 для данных в процессе создания объекта сообщения. К сожалению, само содержимое останется при этом строкой bytes, а не str, несмотря на то, что главная цель кодирования Base64 как раз и состоит в том, чтобы преобразовать двоичные данные в текст (хотя точная разновидность Юникода, в которую должен трансформироваться этот текст, может оставаться неясной). Это ведет к появлению других ошибок в Python 3.1:

>  >> from email.mime.image import MIMEImage # подкласс Message # с заголовками+base64

>  >> bytes = open(‘monkeys.jpg’, ‘rb’).read() # снова прочитать

# двоичные данные

>  >> m = MIMEImage(bytes) # MIME-класс выполнит кодирование

>  >> print(m) # данных в формат Base64

Traceback (most recent call last):

…часть строк опущена…

File "C:\Python31\lib\email\generator.py", line 155, in _handle_text raise TypeError(‘string payload expected: %s’ % type(payload))

TypeError: string payload expected: <class ‘bytes’>

(TypeError: ожидается строковое содержимое: <class ‘bytes’>)

>  >> m.get_payload()[:40] # это уже текст в формате Base64

b’/9j/4AAQSkZJRgABAQEAeAB4AAD/2wBDAAIBAQIB’

>  >> m.get_payload()[:40].decode(‘ascii’) # но в действительности ‘/9j/4AAQSkZJRgABAQEAeAB4AAD/2wBDAAIBAQIB’ # это строка bytes!

Иными словами, разделение типов str/bytes в Python 3.X не только не поддерживается полностью пакетом email в Python 3.1, но и фактически нарушает его работоспособность. К счастью, данная проблема поддается решению.

Чтобы решить эту конкретную проблему, я создал собственную функцию MIME-кодирования двоичных вложений и передал ее всем подклассам с поддержкой кодирования в формат MIME, предназначенным для создания двоичных вложений. Эта функция реализована в пакете mailtools, который будет представлен далее в этой главе (пример 13.23). Поскольку пакет email использует ее для преобразования данных типа bytes в текст на этапе инициализации, появляется возможность декодировать текст ASCII в Юникод после преобразования двоичных данных в формат Base64 и установить заголовки с информацией о кодировке содержимого. То обстоятельство, что пакет email не выполняет это дополнительное декодирование в Юникод, можно считать ошибкой в пакете (хотя она и вызвана изменениями где-то в другом месте в стандартной библиотеке Python), но обходное решение справляется с заданием:

# в модуле mailtools.mailSender, далее в этой главе… def fix_encode_base64(msgobj):

from email.encoders import encode_base64

encode_base64(msgobj) # пакет email оставляет данные в виде bytes

bytes = msgobj.get_payload() # операция создания текста терпит неудачу # при наличии двоичных данных

text = bytes.decode(‘ascii‘) # декодировать в str, чтобы обеспечить # создание текста

…логика разбиения строк опущена…

msgobj.set_payload(‘\n‘.join(lines))

>>> from email.mime.image import MIMEImage

>>> from mailtools.mailSender import fix_encode_base64 # использовать

# решение

>>> bytes = open(‘monkeys.jpg’, ‘rb’).read()

>  >> m = MIMEImage(bytes, _encoder=fix_encode_base64) # преобразовать # в ascii str

>  >> print(m.as_string()[:500])

Content-Type: image/jpeg MIME-Version: 1.0

Content-Transfer-Encoding: base64

/9j/4AAQSkZJRgABAQEAeAB4AAD/2wBDAAIBAQIBAQICAgICAgICAwUDAwMDAwYEBAMFBwYHBwcG BwcICQsJCAgKCAcHCg0KCgsMDAwMBwkODw0MDgsMDAz/2wBDAQICAgMDAwYDAwYMCAcIDAwMDAwM DAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAz/wAARCAHoAvQDASIA AhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQA AAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3 ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc

>  >> print(m) # вывести все сообщение: очень длинное

Другое возможное обходное решение вовлекает определение собственного класса MIMEImage, похожего на оригинальный, но не выполняющего кодирование Base64 на этапе создания. При таком подходе мы должны сами выполнять кодирование и преобразование данных в тип str перед созданием объекта сообщения, но по-прежнему можем использовать логику создания заголовков из оригинального класса. Однако если вы пойдете таким путем, то обнаружите, что он требует повторения (то есть простого копирования) слишком большого объема использованной в оригинале логики, чтобы считаться достаточно разумным, — в этом повторяющемся программном коде придется в будущем отражать все изменения в пакете email:

>  >> from email.mime.nonmultipart import MIMENonMultipart

>  >> class MyImage(MIMENonMultipart):

def __init__(self, imagedata, subtype):

MIMENonMultipart.__init__(self, ‘image’, subtype)

self.set_payload(_imagedata)

…повторить всю логику кодирования base64 с дополнительным декодированием в Юникод…

>  >> m = MyImage(text_from_bytes)

Интересно отметить, что эта регрессия в пакете email фактически отражает изменения в модуле base64, выполненные в 2007 году и никак не связанные с поддержкой Юникода, которые были вполне разумными, пока не было сделано строгое разделение типов bytes/str в Python 3.X. До этого механизм кодирования электронной почты работал в Python 2.X по той простой причине, что тип bytes в действительности был типом str. Однако в версии 3.X, так как base64 возвращает строки bytes, механизм кодирования электронной почты в пакете email также оставляет
содержимое в виде строк bytes, даже при том, что в результате кодирования получается текст в формате Base64. Это в свою очередь нарушает работу механизма создания полного текста сообщения в пакете email, потому что в данном случае он ожидает получить содержимое в виде текста и требует, чтобы оно было строкой str. Как и в любых крупномасштабных программных системах, эффект воздействия некоторых изменений в 3.X, возможно, было трудно предугадать или полностью согласовать.

Анализ двоичных вложений (в противоположность созданию текста из них), напротив, прекрасно работает в Python 3.X, потому что полученное содержимое сохраняется в объектах сообщений в виде строк str в формате Base64, а не в виде строк bytes, и преобразуется в тип bytes только при извлечении. Эта ошибка, скорее всего, также будет исправлена в будущих версиях Python и пакета email (вероятно, даже в виде простой «заплаты» в Python 3.2), но она является более серьезной по сравнению с другими проблемами декодирования в Юникод, описанными здесь, потому что препятствует составлению почтовых сообщений, кроме самых простых.

Гибкость, предоставляемая пакетом и языком Python, позволяет разрабатывать подобные обходные решения, внешние по отношению к пакету, вместо того, чтобы вторгаться непосредственно в программный код пакета. С открытым программным обеспечением и прикладным интерфейсом, терпимым к ошибкам, вы редко будете попадать в безвыходные ситуации.

Самые последние новости: Ошибка, описанная в этом разделе, намечена к исправлению в версии Python 3.21, представленные здесь обходные решения могут оказаться ненужными в этой и в следующих версиях Python. Это выяснилось в ходе диалога с членами специальной заинтересованной группы по проблемам пакета email (в списке рассылки «email-sig»).

К сожалению, это исправление не появилось до момента окончания работы над этой главой и над примерами в ней. Я с удовольствием удалил бы обходное решение и его описание полностью, но эта книга основана на версии Python 3.1, которая существовала до и останется существовать после внесения этих исправлений.

 Действительно, описываемая проблема была исправлена в версии Python 3.2. И, как пишет автор на сайте поддержки книги: «Исправления в библиотеке версии 3.2 включают важные улучшения в пакете email по сравнению с версией 3.1: решение проблемы декодирования в тип str, а также другие обходные решения для пакета email, которые приводятся в главе 13, стали излишними в Python 3.2. В то же время, эти обходные решения, необходимые при работе с пакетом email в версии 3.1, можно безопасно использовать в версии 3.2 и их следует рассматривать как примеры решения проблем, с которыми приходится сталкиваться в практической деятельности, что является основной темой книги». — Прим. перев.

Однако, чтобы это обходное решение работало также под управлением альфа-версии Python 3.2, его реализация была скорректирована непосредственно перед публикацией книги, куда была добавлена проверка на принадлежность типу bytes перед декодированием. Кроме того, в обходном решении по-прежнему необходимо вручную разбивать строки в данных Base64, потому что это еще не реализовано в версии 3.2.

Использованная литература:

Марк Лутц — Программирование на Python, 4-е издание, II том, 2011

Каталог сайтов Всего.ру
Оцените статью
Секреты программирования
Добавить комментарий