Aleksey Nemiro's curriculum vitae

Работаем с MIME

Не так давно мне пришлось немного поработать MIME (Multipurpose Internet Mail Extension) - это стандарт почтовых сообщений, в РуНете (Russian Internet) я не нашел нормального описания спецификации MIME, и посему решил написать небольшую статейку на эту тему.

В данной статье вы узнаете об основных особенностях и стандартах MIME, а также научитесь «читать» MIME с использованием синтаксиса языка Visual Basic .NET 2005.

Что такое MIME и зачем нам с ним работать

MIME (Multipurpose Internet Mail Extension) – стандарт почтовых сообщений.

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

Если у вас установлен почтовый клиент The Bat!, то вы легко можете посмотреть на внутренности любого почтового сообщения, для этого выберите любое письмо и нажмите меню Специальное => Исходный текст письма, либо клавишу F9 (см. рис. 1).

Рис. 1. Просмотр MIME в The Bat!

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

Спецификация MIME

Стандарт MIME подробно описан в RFC-1341 и является расширяемым стандартом, ниже я описал основные поля MIME.

Версия MIME (MIME-Version)

Поле MIME-Version содержит данные о версии MIME, обычно это версия 1.0.

Синтаксис:

MIME-Version: 1.0

Тип контента почтового сообщения (Content-Type)

Тип содержимого почтового сообщения описывается в поле Content-Type.

Существуют 7 основных типов контента:

Каждый тип может иметь подтип, количество подтипов неограниченно, но тем не менее, каждый подтип должен быть зарегистрирован в организации IANA (Internet Assigned Numbers Authority).

Синтаксис:

Content-Type: тип/подтип[; параметры]  

Тип text указывает на то, что сообщение содержит только текстовые данные.

Тип text имеет три основных подтипа:

Наиболее часто встречается подтип plain и html.

Подтип plain указывает, что сообщение содержит только текст; подтип richtext указывает, что текст содержит элементы форматирования в соответствии со стандартом SGML – это нечто вроде HTML, но немного проще; подтип html – указывает на то, что сообщение является гипертекстом (HTML).

Пример:

Content-Type: text/plain

Тип application указывает на то, что сообщение содержит любые данные не являющиеся текстовыми, как правило - это бинарные (двоичные) данные.

Тип application может иметь неограниченное количество подтипов определяющих тип данных.

Пример:

Content-Type: application/x-zip-compressed

Тип image указывает на то, что сообщение содержит графические данные. Как правило, подтипами данного типа могут быть gif, jpeg, png и т.п.

Пример:

Content-Type: image/gif

Типы video и audio указывают на то, что сообщение содержит видео, либо аудио данные.

Подтипами audio могут быть: midi, mpeg, x-wav и т.п., а подтипами video: mpeg, quicktime и т.п.

Пример:

Content-Type: audio/x-wav

Тип multipart указывает на то, что сообщение содержит смешанный тип данных, т.е. одно сообщение может содержать один или несколько из выше описанных типов данных.

Тип multipart имеет 4 основных подтипа:

Подтип mixed определяет сообщение, состоящее из нескольких (multi) частей (part), разделенных друг от друга границей (boundary).

Граница определяется в параметре boundary в поле Content-Type. Граница представляет собой набор ASCII-символов.

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

Части сообщения разделены друг от друга именем границы, причем имя границы в теле сообщения всегда начинается с символов --, а последняя граница также дополнительно заканчивается символами --.

Чтобы вам было более понятно, о чем идет речь, посмотрите на следующий фрагмент MIME:

Content-Type: multipart/mixed; boundary="moia granica"
--moia granica
Content-Type: text-plain
Hello! Is sample my boundary!
--moia granica
Content-Type: text/plain
А это следующая часть сообщения!
--moia granica--

Как видите, это сообщение имеет тип содержимого multipart с подтипом mixed, здесь указано имя границы – moia granica.

Сообщение состоит из двух частей, каждая часть имеет тип text с подтипом plain.

Первая часть сообщения содержит текст: «Hello! Is sample my boundary!», а вторая часть сообщения содержит текст: «А это следующая часть сообщения!».

Кончено, текстовое сообщение никто делить на части не будет, обычно это делается, если в сообщении присутствуют какие-либо вложения (аттачи).

Обратите также внимание, перед первой границей и после последней может отображаться любой текст, который не будет отображаться в почтовых клиентах (web-клиентах, Outlook, The Bat! и т.п.), эту особенность можно использовать, например, для комментариев.

Подтип alternative идентичен подтипу mixed, однако каждая часть сообщения представляет собой сообщение оптимизированное под возможности почтового клиента.

Например, сообщение может состоять из нескольких частей, одна часть будет содержать текстовой контент text/plain, другая - гипертекст text/html, в данном случае, если у клиента почтовая программа не будет поддерживать html, то отобразиться первая часть сообщения (text/plain), в противном случае – вторая (text/html).

Подтип digest идентичен подтипу mixed, однако, каждая часть сообщения имеет более детальные заголовки и может содержать такие поля как: From и Subject, что, в свою очередь, позволяет направлять одно сообщение нескольким адресатам.

Подтип parallel идентичен подтипу mixed и предназначен главным образом для отображения одновременно всех частей сообщения.

Это основные подтипы multipart, их количество, как я уже ранее говорил, может увеличиваться.

Тип message главным образом используется в случаях, когда сообщение не может быть передано полностью. Основными подтипами данного типа являются: partial – указывает на то, что сообщение разделено на части, при этом, в параметрах поля Content-Type указывается количество частей (total), номер части (number) и идентификатор (id); external-body – позволяет ссылаться на внешние источники.

Следует также отметить, что поле Content-Type может содержать параметр charset, который содержит информацию об используемой кодировке, это могут быть windows-1251, kio8-r и т.п.

В случае если сообщение содержит вложения, то Content-Type также может иметь параметр name, в котором содержится имя файла вложения, например:

Content-Type: application/x-zip-compressed; name="MyFile.zip"

Как вы уже, наверное, заметили, все параметры разделены друг от друга точкой с запятой (;), при этом, каждый параметр может быть написан на отдельной строке, а также, значения параметров может быть заключено в кавычки, хотя это вовсе не обязательно. Это может вызвать некоторые проблемы при написание MIME-ридера (MIME Reader), но тем не менее эту особенность нужно учесть.

Тип кодирования сообщения (Content-Type-Encoding)

Поле Content-Type-Encoding содержит информацию об использованном типе кодирования сообщения.

Существует 6 основных типов кодирования:

Типы кодирования 7Bit, 8Bit и Binary не требуют никакого преобразования, поскольку данные передаются по байтам.

Тип кодирования Base64 – позиционная система счисления с основанием 64, где 64 – наибольшая степень двойки, которая представляется с использованием ASCII-символов. Кодировка Base64 использует символы A-Z, a-z и 0-9, в MIME также используются символы +, / и =.

Тип кодирования Quoted-Printable представляет собой порядок символов в шестнадцатеричном виде, при этом кодируются только символы ASCII-код которых превышает 122, а остальные символы остаются как есть. Перед закодированными символами ставит знак =.

Тип кодирования X-Token позволяет пользователю самому задавать правила кодирования.

Пример:

Content-Type-Encoding: quoted-printable

Прочие поля

Поле Subject содержит тему сообщения.

Синтаксис:

Subject: Тема сообщения

Поля From и To содержат адрес отправителя и получателя, а также могут содержать имя отправителя и наименование компании.

Помимо этого, в сообщении может присутствовать поле CC, которое содержит список адресатов, которым будет направлена копия сообщения.

Синтаксис:

From: [Имя отправителя <]email@отправителя.ru[> (Компания)]  

Для названия компании также может использоваться отдельное поле Organization.

Пример:

To: mailer@kbyte.ru
From: Немиро Алексей <admin@kbyte.ru>
CC: User <user@kbyte.ru>
Organization: www.Kbyte.Ru  

Следует также упомянуть о поле Received, которое содержит информацию о пути сообщения. Полей Received может быть несколько, их количество зависит от количества серверов, через которое проходит сообщение до конечного получателя, каждый сервер оставляет информацию о себе в этом поле.

Еще одно интересное поле - X-Mailer, которое содержит название почтового клиента, через который было отправлено сообщение.

Дату отправки сообщения можно узнать в поле Date.

Также, сообщение может содержать поле Reply-To, в котором указывается адрес, на который будет направлен ответ.

Поле X-Priority содержит отметку о приоритете письма, как правило, это числовое значение, либо комбинация числового и буквенного значения. Письмо может иметь следующие приоритеты:

При наличии вложений (аттачей) в сообщении также может присутствовать поле Content-Disposition, которое содержит описание вложения (аттача), в частности имя файла, например:

Content-Disposition: attachment; filename="MyFile.rar"

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

Обратите внимание, все значения полей могут быть также зашифрованы, обычно для шифрования используются типы кодирования Base64 и Quoted-Printable, а также, может быть указана кодировка текста, например: windows-1251, kio8-r, utf-8 и т.п.

Если значение поля зашифровано, то оно записывается в следующем формате:

=?кодовая страница?тип кодирования?значение поля?=

Кодовая страница – это, собственно, и есть windows-1251, kio8-r, utf-8 и т.п.

Тип кодирования – представляет первый символ названия типа кодирования, это может быть либо BBase64, либо Q - Quoted-Printable.

Значение поля – это закодированное указанным типом кодирования значение поля.

Пример:

Subject: =?windows-1251?Q?=EF=F0=E8=EC=E5=F0_=ED=E0_VB?=

Здесь следует отметить, что в Quoted-Printable преобразуются только русские символы, т.е. символы с кодом более 122, остальные символы записываются как есть, при этом перед каждым закодированным символом ставится знак =.

В Base64 кодируется весь текст.

Читаем MIME

Переходим к самому интересному – чтению MIME программно. Здесь будут рассмотрены только основы чтения MIME с использованием Visual Basic .NET 2005.

Итак, для начала попробуем написать функции декодирования текста из Base64 и Quoted-Printable.

Начнем с простого и напишем функцию чтения Quoted-Printable. Как я уже говорил, Quoted-Printable преобразует некоторые символы в шестнадцатеричный код и перед каждым преобразованным символом ставит знак =, на этом и будем основываться:

Private Function QPDecode(ByVal sText As String) As String
  If sText.Length <= 0 Then Return ""

  Dim sResult As String = ""
  Dim chrCurrentChar As Char
  Dim intTextLength As Integer = sText.Length
  Dim i As Integer = 1

  Do While i <= intTextLength
    'берем символ
    chrCurrentChar = Convert.ToChar(Mid(sText, i, 1))
    'если начинается с =, то это шестнадцатеричный код
    If chrCurrentChar = "=" Then
      Try
        'берем два следующих символа
        'и пытаемся преобразовать в десятичное число,
        'а затем в символ
        sResult += Chr(CInt("&H0" & Mid(sText, i + 1, 1) & _
                   Mid(sText, i + 2, 1)))
        i += 3
      Catch ex As Exception
        'это обычный символ, оставляем как есть
        sResult += chrCurrentChar
        i += 1
      End Try
    Else
      'это обычный символ, оставляем как есть
      sResult += chrCurrentChar
      i += 1
    End If
  Loop

  'возвращаем декодированный текст
  Return sResult
End Function

Как видите, ничего сложно в этом нет, данная функция запросто преобразует текст Quoted-Printable в обычный.

Перейдем к Base64. Текст, кодированный в Base64, состоит из последовательности больших и маленьких символов английского алфавита, цифр, а также символов +, / и =.

Для декодирования Base64 можно использовать следующую функцию:

Private Function Base64Decode(ByVal sText As String) As String
  Dim sResult As String = ""
  Dim i As Integer

  For i = 1 To sText.Length
    If Not InStr(1, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=", Mid(sText, i, 1)) = 0 Then
      sResult += Mid(sText, i, 1)
    End If
  Next i

  If (sResult.Length Mod 4) <> 0 Then
    sResult += StrDup(4 - (sResult.Length Mod 4), "=")
  End If

  Dim encoding As System.Text.Encoding = System.Text.Encoding.GetEncoding(1251)

  Try
    'преобразуем в 8-разрядный массив символов
    sResult = encoding.GetString(Convert.FromBase64String(sResult))
  Catch ex As Exception
    'ошибка
  End Try

  Return sResult
End Function

Данная функция предназначена главным образом для декодирования текстовых данных, чтобы получить бинарные (двоичные) данные достаточно пропустить полученный результат через функцию System.Text.Encoding.GetBytes, либо возвратить Convert.FromBase64String(sResult).

Для получения значений полей MIME я использую регулярные выражения:

Private Function GetHeaderBySource(ByVal sSource As String, ByVal sHeader As String) As String
  Dim myRegex As New Regex("((?<key>[a-zA-Z0-9\-]*): (?<value>.*))|((?<key>[a-zA-Z0-9\-]*):\s\n(?<value>.*))", RegexOptions.Multiline)
  Dim myMatchCollection As MatchCollection = myRegex.Matches(sSource)
  Dim sResult As String = ""
  Dim iStrt As Integer, iLngth As Integer

  For i As Integer = 0 To (myMatchCollection.Count - 1)
    If myMatchCollection(i).Groups("key").Value.Trim.ToLower = sHeader.Trim.ToLower Then
      sResult += (myMatchCollection(i).Groups("value").Value.Trim) & vbCrLf

      'смотрим, есть ли еще что-нибудь после этой группы
      If i < myMatchCollection.Count - 1 Then
        iStrt = myMatchCollection(i).Groups("value").Index + myMatchCollection(i).Groups("value").Length + 1
        iLngth = myMatchCollection(i + 1).Groups("key").Index - iStrt - 1

        If iStrt < myMatchCollection(i + 1).Groups("key").Index Then
          sResult += Replace(sSource.Substring(iStrt, iLngth).Trim, vbCrLf & Chr(9), vbCrLf)
        End If
      Else
        'получаем все до конца заголовка
        iStrt = myMatchCollection(i).Groups("value").Index + myMatchCollection(i).Groups("value").Length + 1
        iLngth = sSource.Length - iStrt - 1

        If iLngth > 0 Then
          sResult += Replace(sSource.Substring(iStrt, iLngth).Trim, vbCrLf & Chr(9), vbCrLf)
        End If
      End If
    End If
  Next

  If sResult.EndsWith(vbCrLf) Then sResult = Mid(sResult, 1, sResult.Length - vbCrLf.Length)
  If sResult.StartsWith(vbCrLf) Then sResult = Mid(sResult, vbCrLf.Length, sResult.Length - (vbCrLf.Length + 1))

  Return sResult
End Function

Для получения данных из поля, достаточно указать текст MIME и имя поля, значение которого требуется получить, например:

Dim sMIME As String = "Date: Sat, 18 Nov 2006 15:28:22 +1000" & vbCrLf & _
  "From: admin@kbyte.ru" & vbCrLf & _
  "X-Mailer: The Bat! (v3.60.07) Professional" & vbCrLf & _
  "Reply-To: admin@kbyte.ru" & vbCrLf & _
  "Organization: Kbyte.Ru" & vbCrLf & _
  "X-Priority: 3 (Normal)"

MsgBox(GetHeaderBySource(sMIME, "from"))

Данный пример возвратит значение поля From.

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

Как я уже говорил, если данные кодированы, то они будут иметь следующий формат:

=?кодовая страница?тип кодирования?значение поля?=

Это стандартный формат и изменяться он не может, поэтому для получения необходимой информации запросто можно использовать регулярные выражения:

Regex("(^\=\?(?<encod>[a-zA-Z0-9\-]*)\?(?<type>[a-zA-Z]{1})\?(?<text>.*)\?\=$)|(^\=\?(?<encod>[a-zA-Z0-9\-]*)\?(?<type>[a-zA-Z]{1})\?(?<text>.*)\?\=(?<othertext>\s.*)$)")

Используя этот синтаксис можно получить название кодировки (windows-1251, kio8-r и т.п.) – группа encode, тип кодирования (Q или B) – группа type, а также значение параметра – группа text.

Помимо этого, после кодированных данных, также может содержаться любой другой текст – группа othertext.

Теперь, получив эти данные можно запросто пропустить их через одну из ранее написанных функций декодирования и радоваться жизни ;-)

Хотя нет, рано радоваться, после декодирования, нужно преобразовать текст в правильную кодовую страницу. Для этого можно воспользоваться функцией System.Text.Encoding.GetEncoding.

Сам текст сообщения, либо аттач, идет сразу после заголовков. При этом следует учесть, что границы заголовков определяются наличием в конце последовательности двух символов CRLF. Другими словами, заголовки кончаются сразу после пары символов CRLF:

InStr(sMIME, vbCrLf & vbCrLf)

Все остальное – это текст сообщения, либо тело вложения (аттача).

Вот собственно и все, что я хотел рассказать.

Конечно, готового MIME-ридера в данной статье вы не нашли, только основы для его создания, собственно, этого более чем достаточно.

В идеале все должно быть оформлено в виде класса.

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

Удачи!


Алексей Немиро
2006-11-29
http://kbyte.ru