- PVSM.RU - https://www.pvsm.ru -

Разбираем email в Java

К моему последнему проекту, написанному на 80% на Java, надо было дописать модуль — парсер всех писем, проходящих через сервер. Религиозные мотивы модуля очень странные, но некоторыми деталями хотелось бы поделиться.

В наличии имеются:

Почтовый сервер Postfix со службой доставки Dovecot на CentOS. Ну и JVM.

Структура сообщений

Что такое электронное письмо, его составные части, их примерная структура, заголовки и MIME типы по-человечески описано на википедии [1].
Более интересной является структура имени файла письма на сервере. Пример имени новоиспеченного (не прочитанного/не запрошенного клиентом) письма:

1348142977.M852516P31269.mail.example.com,S=3309,W=3371


Имя состоит из флагов. Флаги разделяются запятыми, при создании нового письма указывается «куда», «когда» пришло письмо и его размеры.

  • Указываются два размера письма. Обычный Size, обозначенный «S» и Vsize, обозначенный символом «W», что есть rfc822.SIZE. (Тут [2] отвечают на вопрос «Что такое RFC822.SIZE?» ).
  • Время указывается в формате Unix, в секундах.
  • В одном флаге со временем, через точку, могут идти «P» — ID процесса и «M» — счетчик в микросекундах, добавляемый для уникальности имени (могут быть и другие атрибуты, дополнительно в примечаниях)
  • Сервер указывается конечный, т.е. тот, на котором хранится письмо, а не relay-сервер в случае, если письмо было переслано.

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

Дополнительные/клиентские флаги

Клиентский почтовый интерфейс (далее клиент) может добавлять в имя письма свои флаги. Начало клиентских флагов обозначается символом ":"

Как только клиент доберется запросит новые письма с сервера — отправляется запрос транспорту на перемещение каждого из запрошенных писем в директорию «прочитанные» и добавление к имени информационного флага (одного из двух), отделенного от последующих флагов запятой:

  • «1» — как говорит документация «Флаг, несущий экспериментальный смысл».
  • «2» — то, что у меня на практике было в 100% случаях. Означает то, что каждый последующий символ после запятой, является отдельным флагом.

Не смотря на то, что письмо на сервере уже лежит в папке «прочитанное», у пользователя оно будет отображаться как новое, т.к. клиенты считывают флаги, а не местонахождение письма.
То есть, только тогда, когда пользователь сам откроет письмо (либо другое действие с ним) и к его имени добавится флаг «S» (Seen), оно станет визуально «прочитанным». Различные действия над письмом, как и следовало бы ожидать, добавляют свои флаги, см. примечания.

Пример:
На сервер для нашего ящика пришло новое сообщение, его имя будет иметь приблизительно следующий вид:

1348142977.M852516P31269.mail.example.com,S=3309,W=3371

У нас на фоне запущен не дай Бог Outlook, который запрашивает список новых писем и говорит переместить их на сервере в директорию «прочитанные», добавляя при этом флаг:

1348142977.M852516P31269.mail.example.com,S=3309,W=3371:2,

Далее мы удаляем открываем Outlook и щелкаем на новое письмо, при этом добавляется флаг S:

1348142977.M852516P31269.mail.example.com,S=3309,W=3371:2,S

А потом еще отвечаем на него и удаляем:

1348142977.M852516P31269.mail.example.com,S=3309,W=3371:2,SRT

Как мы видим, флаги перечисляются без разделителей.

Примечания: некоторые клиенты имеют возможность настройки (не)перемещения письма в папку «прочитанное». Так же клиенты иногда добавляют не указанные в документации флаги «для своих нужд», на которые я особо не обращал внимания.
Больше полезной информации о флагах: cr.yp.to/proto/maildir.html [3]

И немного Джавы

Для работы с письмами я использовал javax.mail [4]. Нам любезно предоставлен абстрактный класс javax.mail.Message [5], хотя в данном случае я ограничился javax.mail.MimeMessage [6].
Модуль крутится на сервере, поэтому к сообщениям обращаемся локально (проверки и обработки исключений в коде опущены):

// в примере properties оставляю дефолтными
Session session = Session.getDefaultInstance(System.getProperties());   
FileInputStream fis = new FileInputStream(pathToMessage);
MimeMessage mimeMessage = new MimeMessage(session, fis);       

Теперь мы можем считать заголовки письма, которые ожидаются в ASCII. Если заголовок не найден, то нам вернется null. Например:

String messageSubject = mimeMessage.getSubject();
String messageId = mimeMessage.getMessageID();

Для определения списка получателей нам предоставлен метод getRecipients, принимающий в качестве аргумента Message.RecipientType. Метод возвращает массив объектов типа Address [7]. Например, выведем список получателей письма:

for(Address recipient : mimeMessage.getRecipients(Message.RecipientType.TO)){
    System.out.println(recipient.toString());
}

Что-бы узнать отправителя(ей) письма, у нас есть метод getFrom. Так же возвращает массив объектов типа Address. Метод считывает заголовок «From», если тот отсутствует — читает заголовок «Sender», если отсутствует и «Sender» — тогда null.

for(Address sender : mimeMessage.getFrom()){
    System.out.println(sender.toString());
}

Далее разберем тело сообщения (в большинстве случаев нам нужен текст и вложения). Оно может быть составным (Mime multipart message), либо содержать только один блок формата text/plain. Если тело письма состоит только из вложения (без текста), оно все равно помечается как multipart message. По RFC822 формат указывается для тела письма (и его частей) в заголовке Content-Type.

 // Если контент письма состоит из нескольких частей
if(mimeMessage.isMimeType("multipart/mixed")){ 
         // getContent() возвращает содержимое тела письма, либо его части. 
         // Возвращаемый тип - Object, делаем каст в Multipart
  Multipart multipart = (Multipart) mimeMessage.getContent(); 
        // Перебираем все части составного тела письма
  for(int i = 0; i < multipart.getCount(); i ++){
         BodyPart part = multipart.getBodyPart(i); 
    //формат "text/plain" указывается даже для html содержимого (кроме передаваемой html страницы, но это уже вложение) 
        if(part.isMimeType("text/plain")){ 
           System.out.println(part.getContent().toString());
        }
         // Проверяем является ли part вложением
        else if(Part.ATTACHMENT.equalsIgnoreCase(part.getDisposition()){
        // Опускаю проверку на совпадение имен. Имя может быть закодировано, используем decode
                 String fileName = MimeUtility.decodeText(part.getFileName());
                 // Получаем InputStream
                 InputStream is = part.getInputStream(); 
                 // Далее можем записать файл, или что-угодно от нас требуется
                 ....
        }
  }
}
// Сообщение состоит только из одного блока с текстом сообщения
else if(mimeMessage.isMimeType("text/plain")){ 
       System.out.println(mimeMessage.getContent().toString());
}

Вот, собственно, и все. Надеюсь, что материал может быть полезным.
Так же на oracle.com есть полезный FAQ [8] по javax.mail.

Автор: Encircled


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/java/16472

Ссылки в тексте:

[1] википедии: http://ru.wikipedia.org/wiki/%D0%AD%D0%BB%D0%B5%D0%BA%D1%82%D1%80%D0%BE%D0%BD%D0%BD%D0%B0%D1%8F_%D0%BF%D0%BE%D1%87%D1%82%D0%B0

[2] Тут: http://marc.info/?l=imap&m=109097194218047

[3] cr.yp.to/proto/maildir.html: http://cr.yp.to/proto/maildir.html

[4] javax.mail: http://www.oracle.com/technetwork/java/javamail/index.html

[5] javax.mail.Message: http://docs.oracle.com/javaee/6/api/javax/mail/Message.html

[6] javax.mail.MimeMessage: http://javamail.kenai.com/nonav/javadocs/javax/mail/internet/MimeMessage.html

[7] Address: http://docs.oracle.com/javaee/5/api/javax/mail/Address.html

[8] FAQ: http://www.oracle.com/technetwork/java/faq-135477.html