Загрузить файл
  1. base64hub
  2. Статьи о Base64
  3. Base64 в почте
Руководство

Base64 в почте: MIME, вложения и заголовки

Любое вложение в письме передаётся как текст в Base64, перенесённый по 76 символов. Если вы умеете читать окружающую его структуру MIME, файл из сырого письма достаётся за минуту.

Зачем почте Base64

Интернет-почта создавалась для 7-битного текста ASCII в строках ограниченной длины. RFC 5322 до сих пор ограничивает строку 998 символами, а SMTP гарантирует безопасную доставку только 7-битных данных, если принимающий сервер не объявил расширение 8BITMIME. Даже при 8-битной передаче в письме не должно быть байтов NUL и одиночных символов возврата каретки и перевода строки, а в любом двоичном файле они рано или поздно встречаются. Для отправки сырых байтов нужно расширение BINARYMIME, и отправитель не может рассчитывать на него на всём пути от сервера к серверу.

MIME (RFC 2045) решает это заголовком Content-Transfer-Encoding. Он сообщает получателю, как тело части превратили в безопасный текст. Значений пять:

ЗначениеСмысл
7bitОбычные строки ASCII, без кодирования
8bit8-битный текст в коротких строках, нужен 8BITMIME
binaryСырые байты, нужен BINARYMIME
quoted-printableВ основном читаемый текст, байты не из ASCII экранированы как =XX
base64Base64 в строках не длиннее 76 символов

Для вложений почтовые программы почти всегда выбирают base64. Сам алгоритм разобран в статье как работает Base64.

Base64 или quoted-printable

Quoted-printable оставляет символы ASCII как есть, а каждый остальной байт записывает как = и две hex-цифры. é в UTF-8 (байты C3 A9) превращается в =C3=A9. Строки длиннее 76 символов разбиваются мягким переносом, то есть знаком = в конце строки.

Поэтому он хорош для английского текста с редкими диакритиками и расточителен для всего остального: каждый байт не из ASCII стоит 3 символа. Base64 стоит ровно 4/3 независимо от содержимого. Мы закодировали по 20 повторов предложения на трёх языках функциями quopri.encodestring и base64.encodebytes из Python (обе с окончаниями строк LF):

Пример (UTF-8)БайтыQuoted-printableBase64
Английская панграмма9009261216
Французское предложение с диакритиками106018301435
Русская панграмма160046022165

Обычно этот выбор делает за вас почтовый клиент: quoted-printable или 7bit для текста почти целиком из ASCII, Base64 для вложений и для текста не латиницей. Если нужно прочитать или сформировать quoted-printable вручную, инструмент quoted-printable конвертирует в обе стороны.

Вложения в сыром письме

Тело письма с вложением имеет тип multipart/mixed. У каждой части свои заголовки, пустая строка, а затем содержимое. Вот полное минимальное письмо, сгенерированное пакетом email из Python (граница сокращена для читаемости):

From: Ana <ana@example.com>
To: bo@example.org
Subject: =?utf-8?b?Q2Fmw6kgbWVudSDigJMgR3LDtsOfZQ==?=
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="b1"

--b1
Content-Type: text/plain; charset="utf-8"
Content-Transfer-Encoding: quoted-printable

Hi Bo,

The caf=C3=A9 menu is attached.

--b1
Content-Type: application/pdf
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="menu.pdf"

JVBERi0xLjcKJeLjz9MKMSAwIG9iago8PCAvVHlwZSAvQ2F0YWxvZyA+PgplbmRvYmoKdHJhaWxl
cgo8PCAvUm9vdCAxIDAgUiA+PgolJUVPRgo=

--b1--

На что смотреть:

  • Content-Type указывает тип файла, Content-Disposition задаёт имя файла.
  • Content-Transfer-Encoding: base64 подтверждает, что тело в Base64.
  • Base64 начинается после первой пустой строки части и заканчивается перед следующей строкой --boundary.
  • Все строки, кроме последней, ровно по 76 символов. Каждая полная строка содержит 57 байт файла.
  • JVBERi0 в начале кодирует %PDF-, значит, перед нами PDF. Другие сигнатуры перечислены в статье частые префиксы Base64.

Картинки, встроенные в HTML-письма, используют тот же механизм в части multipart/related. Каждая картинка получает заголовок Content-ID, а HTML ссылается на неё как <img src="cid:...">. При передаче эти строки заканчиваются на CRLF, так что каждые 76 символов Base64 обходятся в 78 байт; полный расчёт есть в статье рост размера в Base64. Чтобы самому получить вывод с переносами, используйте кодировщик MIME Base64.

Закодированные заголовки: =?UTF-8?B?…?=

Заголовки вроде Subject, отображаемые имена в From и некоторые имена файлов должны быть в ASCII. Любой другой текст RFC 2047 кодирует как encoded-words:

=?charset?encoding?encoded-text?=

На месте encoding стоит B для Base64 или Q для варианта quoted-printable, в котором _ обозначает пробел. Регистр обеих букв не важен, поэтому выше Python написал =?utf-8?b?. Несколько реальных примеров, все проверены функцией email.header.decode_header из Python:

В заголовкеПосле декодирования
=?UTF-8?B?0J/RgNC40LLQtdGC?=Привет
=?utf-8?q?Caf=C3=A9_menu?=Café menu
=?ISO-8859-1?Q?Caf=E9_menu?=Café menu
=?utf-8?b?Q2Fmw6kgbWVudSDigJMgR3LDtsOfZQ==?=Café menu – Größe

Encoded-word может быть не длиннее 75 символов, поэтому длинные темы разбиваются на несколько слов, по одному на каждую свёрнутую строку заголовка. По RFC 2047 пробелы между соседними encoded-words игнорируются, а каждое слово должно содержать целые символы, так что многобайтовый символ UTF-8 никогда не разрывается между двумя словами. Русская тема из 44 символов вышла из Python такой:

Subject: =?utf-8?b?0KHRh9GR0YIg0LfQsCDQvtC60YLRj9Cx0YDRjCDQuCDQsNC60YIg0YE=?=
 =?utf-8?b?0LLQtdGA0LrQuCDQtNC70Y8g0LHRg9GF0LPQsNC70YLQtdGA0LjQuA==?=

Каждое слово образует законченную строку Base64 со своим дополнением. По отдельности первое декодируется в Счёт за октябрь и акт с, второе в верки для бухгалтерии; декодер склеивает их в исходную тему.

Для имён файлов вложений есть второй стандарт, RFC 2231, который выглядит так: filename*=UTF-8''%D0%BE%D1%82%D1%87%D1%91%D1%82.pdf. Многие почтовые программы к тому же вставляют encoded-words по RFC 2047 внутрь filename="...", так что парсер должен понимать оба варианта.

Как извлечь вложение из файла .eml

В браузере. Откройте .eml в декодере EML, чтобы прочитать тела письма и сохранить вложения. Он работает локально на странице, а это важно, когда в письме счета или персональные данные.

На Python. Стандартная библиотека разбирает MIME и декодирует и тела в Base64, и заголовки по RFC 2047:

import sys
from email import policy
from email.parser import BytesParser

with open(sys.argv[1], "rb") as f:
    msg = BytesParser(policy=policy.default).parse(f)

print("Subject:", msg["Subject"])        # encoded-words decoded
for part in msg.iter_attachments():
    name = part.get_filename()
    data = part.get_content()             # Base64 already decoded to bytes
    with open(name, "wb") as out:
        out.write(data)
    print("saved", name, len(data), "bytes")

На примере письма выше скрипт вывел Subject: Café menu – Größe и saved menu.pdf 83 bytes. В реальном коде очищайте name перед записью: его задаёт отправитель.

Вручную. Скопируйте строки Base64 нужной части в файл и декодируйте. Сырые файлы .eml используют окончания строк CRLF, и GNU base64 -d останавливается с invalid input на первом же возврате каретки. Сначала удалите их или передайте -i, чтобы пропускать символы вне алфавита:

tr -d '\r' < block.txt | base64 -d > menu.pdf

openssl base64 -d -in block.txt -out menu.pdf в наших тестах принял вариант с CRLF без изменений. Если у вас есть только текст в Base64, вставьте его в Base64 в файл (инструмент определит тип по первым байтам), а PDF можно сразу открыть в Base64 в PDF.

Открыть файл .eml

Заголовки, тела и вложения сырого письма, декодированные локально в вашем браузере.

Декодер писем

Вопросы и ответы

Как открыть вложение в Base64 из исходника письма?

Скопируйте строки Base64 между пустой строкой части и следующей границей (boundary) и декодируйте их как файл. Декодер EML сам извлечёт вложения из файла .eml.

Что значит =?UTF-8?B? в теме письма?

Так начинается encoded-word по RFC 2047: кодировка UTF-8, а B означает, что текст между следующими ? и ?= записан в Base64. Q вместо B означает вариант quoted-printable.

Почему строки Base64 в письмах длиной 76 символов?

RFC 2045 ограничивает строки Base64 в MIME 76 символами. 76 символов вмещают ровно 57 байт, а сам лимит держит строки намного короче максимума в 998 символов, который допускает почта.

Что лучше для текста письма: quoted-printable или Base64?

Quoted-printable подходит для текста почти целиком из ASCII: он остаётся читаемым и почти не растёт. Base64 лучше для двоичных данных и текста не латиницей, где quoted-printable может увеличить размер почти втрое.

Читайте также