email.message: Представление сообщения электронной почты

Источник: Lib/email/message.py


Added in version 3.6: [1]

Центральным классом в пакете email является класс EmailMessage, импортированный из модуля email.message. Он является базовым классом для объектной модели email. EmailMessage предоставляет основную функциональность для установки и запроса полей заголовка, доступа к телу сообщения, а также для создания или изменения структурированных сообщений.

Электронное сообщение состоит из заголовков и груза (который также называют содержанием). Заголовки представляют собой имена и значения полей в стиле RFC 5322 или RFC 6532, где имя поля и значение разделены двоеточием. Двоеточие не является частью ни имени поля, ни его значения. Полезная нагрузка может быть простым текстовым сообщением, или двоичным объектом, или структурированной последовательностью подсообщений, каждое из которых имеет свой собственный набор заголовков и свою собственную полезную нагрузку. На последний тип полезной нагрузки указывает сообщение, имеющее тип MIME, например multipart/* или message/rfc822.

Концептуальная модель, предоставляемая объектом EmailMessage, представляет собой упорядоченный словарь заголовков, соединенный с платежной нагрузкой, которая представляет собой RFC 5322 тело сообщения, которое может быть списком под-EmailMessage объектов. В дополнение к обычным методам словаря для доступа к именам и значениям заголовков, существуют методы для доступа к специализированной информации из заголовков (например, тип содержимого MIME), для работы с полезной нагрузкой, для генерации сериализованной версии сообщения и для рекурсивного перехода по дереву объектов.

Словарный интерфейс EmailMessage индексируется по именам заголовков, которые должны быть ASCII-значениями. Значения словаря - это строки с некоторыми дополнительными методами. Заголовки хранятся и возвращаются в форме, сохраняющей регистр, но имена полей сопоставляются без учета регистра. Ключи упорядочены, но, в отличие от настоящего словаря, могут быть дубликаты. Для работы с заголовками, имеющими дубликаты ключей, предусмотрены дополнительные методы.

В случае простых объектов сообщений payload представляет собой либо строку, либо объект байтов, либо список объектов EmailMessage для документов-контейнеров MIME, таких как объекты сообщений multipart/* и message/rfc822.

class email.message.EmailMessage(policy=default)

Если задана policy, используйте указанные в ней правила для обновления и сериализации представления сообщения. Если policy не задана, используется политика default, которая следует правилам почтовых RFC, за исключением окончаний строк (вместо предписанных RFC \r\n используются стандартные для Python окончания строк \n). Для получения дополнительной информации см. документацию policy.

as_string(unixfrom=False, maxheaderlen=None, policy=None)

Возвращает все сообщение, сглаженное в виде строки. Если необязательное значение unixfrom равно true, заголовок конверта будет включен в возвращаемую строку. Значение unixfrom по умолчанию равно False. Для обратной совместимости с базовым классом Message принимается значение maxheaderlen, но по умолчанию оно равно None, что означает, что по умолчанию длина строки контролируется max_line_length политики. Аргумент policy может быть использован для переопределения политики по умолчанию, полученной от экземпляра сообщения. Это может быть использовано для управления некоторым форматированием, производимым методом, поскольку указанная политика будет передана в Generator.

Сплющивание сообщения может вызвать изменения в EmailMessage, если необходимо заполнить значения по умолчанию для завершения преобразования в строку (например, могут быть созданы или изменены границы MIME).

Обратите внимание, что этот метод предоставляется в качестве удобства и может быть не самым удобным способом сериализации сообщений в вашем приложении, особенно если вы имеете дело с несколькими сообщениями. Смотрите email.generator.Generator для более гибкого API для сериализации сообщений. Обратите внимание также на то, что этот метод предназначен только для создания сообщений, сериализованных как «7-битные чистые», когда utf8 равен False, что является значением по умолчанию.

Изменено в версии 3.6: поведение по умолчанию, когда maxheaderlen не указан, было изменено с значения по умолчанию 0 на значение по умолчанию max_line_length из политики.

__str__()

Эквивалентно as_string(policy=self.policy.clone(utf8=True)). Позволяет str(msg) получить строку, содержащую сериализованное сообщение в удобочитаемом формате.

Изменено в версии 3.4: метод был изменен на использование utf8=True, что позволило получить представление сообщения, похожее на RFC 6531, вместо того, чтобы быть прямым псевдонимом для as_string().

as_bytes(unixfrom=False, policy=None)

Возвращает все сообщение, сплющенное в виде объекта bytes. Если необязательное значение unixfrom равно true, заголовок конверта будет включен в возвращаемую строку. По умолчанию unixfrom принимает значение False. Аргумент policy может быть использован для переопределения политики по умолчанию, полученной из экземпляра сообщения. Это может быть использовано для управления некоторым форматированием, производимым методом, поскольку указанная политика будет передана в BytesGenerator.

Сплющивание сообщения может вызвать изменения в EmailMessage, если необходимо заполнить значения по умолчанию для завершения преобразования в строку (например, могут быть созданы или изменены границы MIME).

Обратите внимание, что этот метод предоставляется в качестве удобства и может быть не самым удобным способом сериализации сообщений в вашем приложении, особенно если вы имеете дело с несколькими сообщениями. Смотрите email.generator.BytesGenerator для более гибкого API для сериализации сообщений.

__bytes__()

Эквивалентно as_bytes(). Позволяет bytes(msg) создавать объект bytes, содержащий сериализованное сообщение.

is_multipart()

Возвращает True, если полезная нагрузка сообщения представляет собой список объектов sub-EmailMessage, в противном случае возвращает False. Если is_multipart() возвращает False, полезная нагрузка должна быть строковым объектом (который может быть двоичным объектом, закодированным в CTE). Обратите внимание, что is_multipart(), возвращающий True, не обязательно означает, что «msg.get_content_maintype() == „multipart“» вернет True. Например, is_multipart вернет True, если EmailMessage имеет тип message/rfc822.

set_unixfrom(unixfrom)

Устанавливает в заголовке конверта сообщения значение unixfrom, которое должно быть строкой. (Краткое описание этого заголовка см. в mboxMessage).

get_unixfrom()

Возвращает заголовок конверта сообщения. По умолчанию принимает значение None, если заголовок конверта никогда не устанавливался.

Следующие методы реализуют маппинг-интерфейс для доступа к заголовкам сообщения. Обратите внимание, что между этими методами и обычным интерфейсом отображения (т. е. словарем) есть некоторые семантические различия. Например, в словаре нет дублирующихся ключей, а здесь могут быть дублирующиеся заголовки сообщений. Кроме того, в словарях нет гарантированного порядка ключей, возвращаемых keys(), но в объекте EmailMessage заголовки всегда возвращаются в том порядке, в каком они были в исходном сообщении или в каком они были добавлены к сообщению позже. Любой заголовок, удаленный, а затем вновь добавленный, всегда добавляется в конец списка заголовков.

Эти семантические различия являются преднамеренными и направлены на удобство в наиболее распространенных случаях использования.

Обратите внимание, что во всех случаях любой заголовок конверта, присутствующий в сообщении, не включается в интерфейс отображения.

__len__()

Возвращает общее количество заголовков, включая дубликаты.

__contains__(name)

Возвращает True, если объект сообщения имеет поле с именем name. Поиск осуществляется без учета регистра, и name не включает двоеточие в конце. Используется для оператора in. Например:

if 'message-id' in myMessage:
   print('Message-ID:', myMessage['message-id'])
__getitem__(name)

Возвращает значение именованного поля заголовка. Имя не включает разделитель полей двоеточие. Если заголовок отсутствует, возвращается значение None; значение KeyError не возвращается.

Обратите внимание, что если именованное поле встречается в заголовках сообщения более одного раза, то какое именно из значений этого поля будет возвращено, не определено. Используйте метод get_all(), чтобы получить значения всех имеющихся заголовков с именем name.

При использовании стандартных (не``compat32``) политик возвращаемое значение является экземпляром подкласса email.headerregistry.BaseHeader.

__setitem__(name, val)

Добавляет в сообщение заголовок с именем поля name и значением val. Поле добавляется в конец существующих заголовков сообщения.

Обратите внимание, что при этом не перезаписываются или удаляются существующие заголовки с тем же именем. Если вы хотите, чтобы новый заголовок был единственным, присутствующим в сообщении с именем поля name, сначала удалите это поле, например:

del msg['subject']
msg['subject'] = 'Python roolz!'

Если в policy определено, что некоторые заголовки должны быть уникальными (как это делают стандартные политики), этот метод может выдать ошибку ValueError при попытке присвоить значение такому заголовку, когда он уже существует. Такое поведение намеренно для обеспечения согласованности, но не стоит полагаться на него, поскольку в будущем мы можем решить сделать так, чтобы такие присваивания приводили к автоматическому удалению существующего заголовка.

__delitem__(name)

Удаляет все вхождения поля с именем name из заголовков сообщения. Если именованное поле не присутствует в заголовках, исключение не возникает.

keys()

Возвращает список имен всех полей заголовков сообщения.

values()

Возвращает список всех значений полей сообщения.

items()

Возвращает список из 2 кортежей, содержащих все заголовки и значения полей сообщения.

get(name, failobj=None)

Возвращает значение именованного поля заголовка. Это идентично __getitem__(), за исключением того, что необязательное значение failobj возвращается, если именованный заголовок отсутствует (failobj по умолчанию равно None).

Вот несколько дополнительных полезных методов, связанных с заголовками:

get_all(name, failobj=None)

Возвращает список всех значений для поля с именем name. Если в сообщении нет таких именованных заголовков, возвращается failobj (по умолчанию None).

add_header(_name, _value, **_params)

Расширенная настройка заголовка. Этот метод аналогичен __setitem__(), за исключением того, что дополнительные параметры заголовка могут быть предоставлены в качестве аргументов ключевых слов. _name - это поле заголовка, которое нужно добавить, а _value - это основное значение для заголовка.

Для каждого элемента словаря аргументов _params в качестве имени параметра берется ключ, а подчеркивание преобразуется в тире (поскольку тире в идентификаторах Python недопустимо). Обычно параметр добавляется в виде key="value", если только значение не равно None, в этом случае добавляется только ключ.

Если значение содержит не-ASCII символы, то кодовая сетка и язык могут быть явно заданы путем указания значения в виде кортежа из трех символов в формате (CHARSET, LANGUAGE, VALUE), где CHARSET - строка, называющая кодовую сетку, которая будет использоваться для кодирования значения, LANGUAGE обычно может быть установлена в None или пустую строку (см. RFC 2231 для других возможностей), а VALUE - строковое значение, содержащее не-ASCII кодовые точки. Если не передается кортеж из трех, а значение содержит не-ASCII символы, оно автоматически кодируется в формате RFC 2231 с использованием CHARSET из utf-8 и LANGUAGE из None.

Вот пример:

msg.add_header('Content-Disposition', 'attachment', filename='bud.gif')

Это добавит заголовок, который будет выглядеть так:

Content-Disposition: attachment; filename="bud.gif"

Пример расширенного интерфейса с символами, отличными от ASCII:

msg.add_header('Content-Disposition', 'attachment',
               filename=('iso-8859-1', '', 'Fußballer.ppt'))
replace_header(_name, _value)

Заменить заголовок. Заменяет первый найденный в сообщении заголовок, который соответствует _имя, сохраняя порядок заголовков и регистр имен полей исходного заголовка. Если подходящего заголовка не найдено, выдает сообщение KeyError.

get_content_type()

Возвращает тип содержимого сообщения, принудительно приведенный к нижнему регистру в виде maintype/subtype. Если в сообщении нет заголовка Content-Type, возвращается значение, возвращаемое командой get_default_type(). Если заголовок Content-Type недействителен, возвращается text/plain.

(Согласно RFC 2045, сообщения всегда имеют тип по умолчанию, get_content_type() всегда будет возвращать значение. RFC 2045 определяет тип сообщения по умолчанию как text/plain, если только оно не находится внутри контейнера multipart/digest, в этом случае оно будет message/rfc822. Если в заголовке Content-Type указаны неверные типы, RFC 2045 предписывает использовать тип по умолчанию text/plain).

get_content_maintype()

Возвращает основной тип содержимого сообщения. Это часть maintype строки, возвращаемой get_content_type().

get_content_subtype()

Возвращает тип субконтента сообщения. Это часть subtype строки, возвращаемой get_content_type().

get_default_type()

Возвращает тип содержимого по умолчанию. Большинство сообщений имеют тип содержимого по умолчанию text/plain, за исключением сообщений, которые являются вложенными частями контейнеров multipart/digest. Такие вложенные части имеют тип содержимого по умолчанию message/rfc822.

set_default_type(ctype)

Установите тип содержимого по умолчанию. ctype должен быть либо text/plain, либо message/rfc822, хотя это не является обязательным. Тип содержимого по умолчанию не хранится в заголовке Content-Type, поэтому он влияет на возвращаемое значение методов get_content_type только в том случае, если в сообщении нет заголовка Content-Type.

set_param(param, value, header='Content-Type', requote=True, charset=None, language='', replace=False)

Устанавливает параметр в заголовке Content-Type. Если параметр уже существует в заголовке, замените его значение на value. Если header имеет значение Content-Type (по умолчанию) и заголовок еще не существует в сообщении, добавьте его, установите его значение в text/plain и добавьте новое значение параметра. Необязательный параметр header задает альтернативный заголовок Content-Type.

Если значение содержит символы, отличные от ASCII, кодовая сетка и язык могут быть явно указаны с помощью необязательных параметров charset и language. Необязательный параметр language задает язык RFC 2231, по умолчанию - пустую строку. И charset, и language должны быть строками. По умолчанию используется utf8 charset и None для language.

Если replace равно False (по умолчанию), заголовок будет перемещен в конец списка заголовков. Если replace равно True, заголовок будет обновлен на месте.

Использование параметра requote с объектами EmailMessage устарело.

Обратите внимание, что доступ к существующим значениям параметров заголовков можно получить через атрибут params значения заголовка (например, msg['Content-Type'].params['charset']).

Изменено в версии 3.4: Было добавлено ключевое слово replace.

del_param(param, header='content-type', requote=True)

Полностью удаляет заданный параметр из заголовка Content-Type. Заголовок будет переписан на место без параметра или его значения. Необязательный параметр header задает альтернативу Content-Type.

Использование параметра requote с объектами EmailMessage устарело.

get_filename(failobj=None)

Возвращает значение параметра filename заголовка Content-Disposition сообщения. Если в заголовке нет параметра filename, метод переходит к поиску параметра name в заголовке Content-Type. Если ни один из параметров не найден или заголовок отсутствует, то возвращается failobj. Возвращаемая строка всегда будет без кавычек, согласно email.utils.unquote().

get_boundary(failobj=None)

Возвращает значение параметра boundary заголовка Content-Type сообщения, или failobj, если заголовок отсутствует или не имеет параметра boundary. Возвращаемая строка всегда будет без кавычек в соответствии с email.utils.unquote().

set_boundary(boundary)

Установите параметр boundary заголовка Content-Type в значение boundary. set_boundary() при необходимости всегда будет приводить boundary. Если объект сообщения не имеет заголовка Content-Type, возникает ошибка HeaderParseError.

Обратите внимание, что использование этого метода мало чем отличается от удаления старого заголовка Content-Type и добавления нового с новой границей через add_header(), поскольку set_boundary() сохраняет порядок заголовка Content-Type в списке заголовков.

get_content_charset(failobj=None)

Возвращает параметр charset заголовка Content-Type, приведенный к нижнему регистру. Если заголовок Content-Type отсутствует или у него нет параметра charset, возвращается failobj.

get_charsets(failobj=None)

Возвращает список, содержащий имена наборов символов в сообщении. Если сообщение является multipart, то список будет содержать по одному элементу для каждого подраздела в полезной нагрузке, в противном случае это будет список длины 1.

Каждый элемент в списке будет строкой, которая является значением параметра charset в заголовке Content-Type для представленного подраздела. Если подраздел не имеет заголовка Content-Type, параметра charset или не относится к основному MIME-типу text, то этот элемент в возвращаемом списке будет failobj.

is_attachment()

Возвращает True, если есть заголовок Content-Disposition и его значение (без учета регистра) равно attachment, False в противном случае.

Изменено в версии 3.4.2: is_attachment теперь является методом, а не свойством, для согласованности с is_multipart().

get_content_disposition()

Возвращает значение (без параметров) заголовка Content-Disposition сообщения, если он есть, или None. Возможные значения для этого метода: inline, attachment или None, если сообщение следует за RFC 2183.

Added in version 3.5.

Следующие методы относятся к опросу и манипулированию содержимым (полезной нагрузкой) сообщения.

walk()

Метод walk() - это универсальный генератор, который можно использовать для итерации по всем частям и подчастям дерева объектов сообщений в порядке обхода в глубину. Обычно вы используете walk() в качестве итератора в цикле for; каждая итерация возвращает следующую подчасть.

Вот пример, который выводит MIME-тип каждой части многокомпонентной структуры сообщения:

>>> for part in msg.walk():
...     print(part.get_content_type())
multipart/report
text/plain
message/delivery-status
text/plain
text/plain
message/rfc822
text/plain

walk перебирает подчасти любой части, где is_multipart() возвращает True, даже если msg.get_content_maintype() == 'multipart' может вернуть False. Мы можем убедиться в этом на нашем примере, используя вспомогательную функцию _structure для отладки:

>>> from email.iterators import _structure
>>> for part in msg.walk():
...     print(part.get_content_maintype() == 'multipart',
...           part.is_multipart())
True True
False False
False True
False False
False False
False True
False False
>>> _structure(msg)
multipart/report
    text/plain
    message/delivery-status
        text/plain
        text/plain
    message/rfc822
        text/plain

Здесь части message не являются multiparts, но содержат слагаемые. is_multipart() возвращает True, а walk спускается в слагаемые.

get_body(preferencelist=('related', 'html', 'plain'))

Возвращает часть MIME, которая является наилучшим кандидатом на роль «тела» сообщения.

preferencelist должен быть последовательностью строк из множества related, html и plain, и указывает порядок предпочтений для типа содержимого возвращаемой части.

Начните поиск кандидатов на совпадение с объектом, на котором вызван метод get_body.

Если related не включен в preferencelist, рассматривайте корневую часть (или подчасть корневой части) любой встреченной связи как кандидата, если эта (под-)часть соответствует предпочтению.

При встрече с multipart/related проверьте параметр start и, если найдена часть с совпадающим Content-ID, учитывайте только ее при поиске кандидатов на совпадение. В противном случае рассматривается только первая (по умолчанию корневая) часть multipart/related.

Если часть имеет заголовок Content-Disposition, считайте часть кандидатом на совпадение, только если значение заголовка равно inline.

Если ни один из кандидатов не соответствует ни одному из предпочтений в preferencelist, верните None.

Примечания: (1) Для большинства приложений единственными комбинациями preferencelist, которые действительно имеют смысл, являются ('plain',), ('html', 'plain') и ('related', 'html', 'plain') по умолчанию. (2) Поскольку поиск начинается с объекта, для которого вызывается get_body, вызов get_body на multipart/related вернет сам объект, если preferencelist не имеет значения по умолчанию. (3) Сообщения (или части сообщений), в которых не указан Content-Type или заголовок Content-Type которых недействителен, будут рассматриваться как имеющие тип text/plain, что может иногда приводить к неожиданным результатам get_body.

iter_attachments()

Возвращает итератор по всем непосредственным вложенным частям сообщения, которые не являются частями «тела» кандидата. То есть, пропустите первое вхождение каждого из text/plain, text/html, multipart/related или multipart/alternative (если они явно не помечены как вложения через Content-Disposition: attachment), и верните все оставшиеся части. При прямом применении к multipart/related возвращает итератор по всем связанным частям, кроме корневой (т. е. части, на которую указывает параметр start, или первой части, если параметр start отсутствует или параметр start не совпадает с Content-ID ни одной из частей). При непосредственном применении к multipart/alternative или не``multipart`` возвращает пустой итератор.

iter_parts()

Возвращает итератор по всем непосредственным вложенным частям сообщения, который будет пуст для сообщений, не являющихся``multipart`` (см. также walk()).

get_content(*args, content_manager=None, **kw)

Вызовите метод get_content() менеджера content_manager, передавая self в качестве объекта сообщения и передавая любые другие аргументы или ключевые слова в качестве дополнительных аргументов. Если content_manager не указан, используйте content_manager, указанный текущим policy.

set_content(*args, content_manager=None, **kw)

Вызовите метод set_content() менеджера content_manager, передавая self в качестве объекта сообщения и передавая любые другие аргументы или ключевые слова в качестве дополнительных аргументов. Если content_manager не указан, используйте content_manager, указанный текущим policy.

Преобразуйте сообщение, не являющееся multipart, в сообщение multipart/related, перемещая все существующие заголовки Content- и полезную нагрузку в (новую) первую часть multipart. Если указано boundary, используйте его в качестве строки границы в многочастном сообщении, в противном случае оставьте границу для автоматического создания, когда она потребуется (например, при сериализации сообщения).

make_alternative(boundary=None)

Преобразуйте не``multipart`` или multipart/related в multipart/alternative, перемещая все существующие заголовки Content- и полезную нагрузку в (новую) первую часть multipart. Если указано boundary, используйте его в качестве строки границы в многочастичном сообщении, в противном случае оставьте границу для автоматического создания, когда она потребуется (например, при сериализации сообщения).

make_mixed(boundary=None)

Преобразуйте сообщение, не являющееся multipart, multipart/related или multipart-alternative, в multipart/mixed, перемещая все существующие заголовки Content- и полезную нагрузку в (новую) первую часть multipart. Если указано boundary, используйте его в качестве строки границы в многочастном сообщении, в противном случае оставьте границу для автоматического создания, когда она понадобится (например, при сериализации сообщения).

Если сообщение является multipart/related, создайте новый объект message, передайте все аргументы в его метод set_content() и attach() его в multipart. Если сообщение не является multipart, вызовите make_related(), а затем действуйте, как описано выше. Если сообщение является любым другим типом multipart, вызовите TypeError. Если content_manager не указан, используйте content_manager, указанный текущим policy. Если добавляемая часть не имеет заголовка Content-Disposition, добавьте заголовок со значением inline.

add_alternative(*args, content_manager=None, **kw)

Если сообщение - это multipart/alternative, создайте новый объект message, передайте все аргументы в его метод set_content() и attach() его в multipart. Если сообщение не является multipart или multipart/related, вызовите make_alternative(), а затем действуйте, как описано выше. Если сообщение является любым другим типом multipart, вызовите TypeError. Если content_manager не указан, используйте content_manager, указанный текущим policy.

add_attachment(*args, content_manager=None, **kw)

Если сообщение - это multipart/mixed, создайте новый объект message, передайте все аргументы в его метод set_content() и attach() его в multipart. Если сообщение не является multipart, multipart/related или multipart/alternative, вызовите make_mixed(), а затем действуйте, как описано выше. Если content_manager не указан, используйте content_manager, указанный текущим policy. Если добавляемая часть не имеет заголовка Content-Disposition, добавьте его со значением attachment. Этот метод можно использовать как для явных вложений (Content-Disposition: attachment), так и для inline вложений (Content-Disposition: inline), передавая соответствующие параметры в content_manager.

clear()

Удалите полезную нагрузку и все заголовки.

clear_content()

Удалите полезную нагрузку и все заголовки !Content-, оставив все остальные заголовки нетронутыми и в их первоначальном порядке.

Объекты EmailMessage имеют следующие атрибуты экземпляра:

preamble

Формат MIME-документа допускает наличие некоторого текста между пустой строкой, следующей за заголовками, и первой многочастной граничной строкой. Обычно этот текст никогда не виден в программе чтения почты с поддержкой MIME, поскольку он выходит за рамки стандартных MIME-броней. Однако при просмотре необработанного текста сообщения или при просмотре сообщения в программе чтения, не поддерживающей MIME, этот текст может стать видимым.

Атрибут preamble содержит этот ведущий дополнительный текст для документов MIME. Когда Parser обнаруживает некоторый текст после заголовков, но до первой граничной строки, он присваивает этот текст атрибуту preamble сообщения. Когда Generator записывает обычное текстовое представление MIME-сообщения и обнаруживает, что в сообщении есть атрибут preamble, он записывает этот текст в область между заголовками и первой граничной строкой. Подробности см. в email.parser и email.generator.

Обратите внимание, что если объект сообщения не имеет преамбулы, атрибут preamble будет равен None.

epilogue

Атрибут epilogue действует так же, как и атрибут preamble, за исключением того, что он содержит текст, который появляется между последней границей и концом сообщения. Как и в случае с preamble, если текст эпилога отсутствует, этот атрибут будет равен None.

defects

Атрибут defects содержит список всех проблем, обнаруженных при разборе этого сообщения. Подробное описание возможных дефектов разбора см. в email.errors.

class email.message.MIMEPart(policy=default)

Этот класс представляет подчасть сообщения MIME. Он идентичен EmailMessage, за исключением того, что при вызове set_content() не добавляются заголовки MIME-Version, поскольку подразделам не нужны собственные заголовки MIME-Version.

Сноски