[Из песочницы] Электронная почта и работа с ней в Java-приложениях
Disclaimer
Статья написана для новичков и тех, кому хочется шаг за шагом понять как устроена работа с электронной почтой из Java-приложений. Желающие быстро понять как отправлять электронные письма из Spring-приложений могут сразу переходить к 3 части.
Эту статью я решил написать, потому что не нашел русскоязычных источников про работу c электронной почтой из Java, описывающих имеющиеся библиотеки достаточно полно. На хабре существует статья, посвященная очень узкой задаче по чтению писем (и выводу их содержимого в консоль) и статья с how-to по отправлению
письма с вложениями при помощи Spring Email. Также существует несколько статей (например) на тематических ресурсах, которые приводят порядок действий при работе
с электронной почтой. В этих источниках мне не хватало объяснения основ, на которых стоит
электронная почта и взгляда с высоты на существующие библиотеки для работы с почтой в Java.
Для таких же лапкообразных, как я, написана эта статья. Она дает общие представления о работе электронной почты, разъясняет основные сущности библиотеки Jakarta Mail и дает советы о том, как работать с электронной почтой в Spring-приложениях.
Содержание:
- Электронная почта
- Работа с электронной почтой с помощью Jakarta Mail
- Работа с электронной почтой в среде Spring
1. Электронная почта
Сегодня электронная почта кажется устаревшей технологией, которая нужна только для того чтобы зарегистрироваться на сайтах, куда забыли прикрутить авторизацию с помощью Google, Vk или Facebook -аккаунта. Возможно, так и есть, но наверняка вы до сих пор получаете электронные письма о падении сборки от вашего CI-инструмента или уведомления о пул-реквестах от вашего репозитория на свой почтовый ящик.
Электронная почта была создана на заре интернета и позаимствовала многие свои идеи из реального мира. Подобно тому как в оффлайне для отправки и получения писем нам требуются почтовые отделения, в электронной почте нам нужны почтовые сервера, т.е. электронная почта основана на модели клиент-сервер. В крупных компаниях сисадмины разворачивают свои собственные почтовые сервера, в маленьких компаниях используют почтовые сервера, предоставляемые специальными провайдерами, например: Яндекс.Почта, Gmail и др.
Популярные почтовые серверы: Postfix, Sendmail, Apache James, Zimbra.
Для работы с письмами на стороне клиента используются десктопные приложения вроде Outlook или The Bat!, либо веб-приложения вроде Gmail или Яндекс.Почта.
Предположим, что человек А решил отправить письмо человеку Б. Он пишет его в своем почтовом клиенте и нажимает кнопку «отправить». Письмо отправляется по протоколу SMTP на почтовый сервер клиента А. Этот почтовый сервер пересылает письмо почтовому серверу клиенту Б также при помощи протокола SMTP. Когда клиент Б решит проверить почту, он сделает запрос к своему почтовому серверу с помощью протокола POP3 или IMAP. В случае использования протокола POP3 письма будут выкачены на устройство клиента Б и удалены с почтового сервера. При использовании протокола IMAP клиенту Б будут переданы копии писем,
а оригиналы останутся храниться на почтовом сервере.
Электронное письмо
Электронное письмо состоит из следующих частей:
- Заголовки — тема письма, адрес отправителя, адреса получателей и служебная информация.
- Тело письма — текст, html, картинки, вложения.
Раньше тело письма могло состоять только из символов ASCII, но с развитием почтовых систем, появилась возможность добавлять в тело html-разметку, изображения, а также прикладывать к письму различные файлы. Для того чтобы правильно выделять различные части из письма, были разработаны MIME-типы — многоцелевые расширения интернет-почты.
Для отправки писем используется протокол SMTP. А для их приема используется либо протокол POP3, либо IMAP.
Существуют и другие почтовые протоколы, но три перечисленных выше — наиболее популярны.
Почтовые протоколы
Все почтовые протоколы относятся к самому высокому уровню модели OSI и основаны на TCP.
У каждого из нижеперечисленных протоколов существует защищенная версия, основанная на SSL.
SMTP
Simple Mail Transfer Protocol — простой протокол передачи писем. Используется для рассылки писем.
SMTP использует порт 25 или 587. А его защищенная версия SMTPS слушает порт 465.
POP3
Post Office Protocol v3 — почтовый протокол. Используется для чтения писем. POP3 при выкачивании письма на клиентский компьютер удаляет письмо с почтового сервера. Недостатком этого протокола является то, что при просмотре письма на одном клиентском компьютере это письмо уже нельзя будет посмотреть с другого устройства.
POP3 слушает порт 110. А его защищенная версия POP3S слушает порт 995.
IMAP
Internet Message Access Protocol — протокол доступа к электронной почте. Альтернатива протоколу POP3. IMAP подгружает на клиент только мета-информацию письма, а остальные
данные предоставляет по требованию.
IMAP слушает порт 143. А его защищенная версия IMAPS слушает порт 993.
MIME-типы
Multipurpose Internet Mail Extensions — многоцелевые расширения интернет-почты. MIME используется, для того чтобы обозначить тип передаваемого контента с помощью протоколов, которые первоначально были предназначены только для передачи текстовой информации, например SMTP и HTTP.
MIME первоначально предназначался для электронной почты, но впоследствии начал использоваться не только в ней, но и в HTTP.
Тип определяется двумя словами, записанными через прямой слеш. Первое слово — это общий тип, а второе — уточнение.
image/jpeg
MIME позволяет устанавливать иерархию вложенности одних блоков контента в другие.
При этом блоки контента могут быть смешанного содержимого. Для таких блоков используется общий тип multipart
, нам еще встретится этот термин.
2. Работа с электронной почтой с помощью Jakarta Mail
Jakarta Mail — это библиотека / фреймворк, использующийся для создания, отправки и чтения электронной почты из Java-кода. Библиотека входит в состав Jakarta EE. Ранее библиотека называлась JavaMail, но с 2017 года она переехала под крыло Jakarta.
Как принято в Java EE, сама библиотека предоставляет только базовые сущности, не зависящие от конкретных реализаций. Эти основные классы находятся в пакете javax.mail
. Помимо этого к библиотеке прикладывается Reference Implementation для основных протоколов — SMTP, POP3, IMAP. Все классы, относящиеся к этим протоколам, находятся в пакете com.sun.mail
. Для использования других протоколов, не входящих в Reference Implementation, необходимо подключать сторонние библиотеки.
Для обработки различных форматов данных, которые могут быть вложены в электронное письмо, Jakarta Mail использует библиотеку JavaBeans Activation Framework — при использовании автоматического сборщика эта зависимость будет подтянута в ваш проект транзитивно.
На текущий момент актуальной версией Jakarta Mail является 1.6.5, но близится выпуск версии 2.0.0, главным «нововведением» которого станет переименование основного пакета сjavax.mail
на jakarta.mail
.
В Jakarta Mail не входит почтовый сервер. Для отправки писем по SMTP или получения по IMAP или POP3 требуется развернуть собственный почтовый сервер или использовать существующий.
Настройка почтовых серверов
Как следует из первой части, для отправки и получения электронных писем необходимо поддерживать соединение с почтовыми серверами. В Jakarta Mail почтовые сервера представляют объекты классов Transport
и Store
для отправления и чтения писем соответственно.
Настройка соединения с почтовыми серверами происходит с помощью пар ключ-значение, передаваемых в виде объекта класса Properties
. Например для настройки SMTP-соединения с почтовым сервером Яндекс.Почты достаточно следующих параметров:
mail.transport.protocol = smtps
mail.smtp.host = smtp.yandex.ru
mail.smtp.port = 465
mail.smtp.user = artem.boiar
mail.smtp.ssl.enable = true
mail.smtp.auth = true
mail.debug = true
Полный список различных настроек и их описание можно найти в документации на пакеты основной библиотеки и ее реализаций для различных протоколов.
Сессия
На основе объекта Properties
создается сессия:
final Session session = Session.getInstance(mailProperties, authenticator);
Обычно сессия создается одна для всего приложения. Она представляет собой сеанс работы с почтовыми серверами. Почти каждый объект библиотеки должен принадлежать какой-либо сессии.
Аутентификация
Вторым аргументом при создании сессии необходимо передать объект абстрактного класса Authenticator
, у которого требуется переопределить метод getPasswordAuthentication()
(по умолчанию возвращает null, почему он не был сделан абстрактным — неизвестно).
Этот метод используется сессией для связи с почтовым сервером — метод будет вызываться при каждом обращении к серверу. При его вызове возвращается объект класса PasswordAuthentication
, который представляет собой обычное DTO для хранения логина и пароля.
final Authenticator authenticator = new Authenticator() {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(
this.getDefaultUserName(),
PropUtils.getPassword()
);
}
}
Разработчиками библиотеки подразумевается, что внутри этого метода можно запросить пароль у пользователя или достать его из хранилища. При получении пароля можно пользоваться значениями полей класса Authenticator
: IP почтового сервера, тип протокола, используемый
порт и даже имя дефолтного пользователя.
Почти все сущности, представляющие какие-либо объекты предметной области, представлены абстрактными классами, реализация которых зависит от конкретного протокола:
Message
— электронное письмо;BodyPart
— фрагмент электронного письма: основная часть или вложение;Address
— адрес отправителя или получателя;Folder
— папка, в которой хранятся электронные письма;SearchTerm
— условие поиска писем в папке;MailEvent
— событие, произошедшее с письмом.
Обработка событий основывается на паттерне Наблюдатель.
Примечание — в данной статье обработка событий не рассмотрена.
Письма
Электронные письма представляются с помощью абстрактного класса Message
, у которого есть один наследник — MimeMessage
(реализации различных протоколов могут дополнительно расширять этот класс).
Электронное письмо состоит из метаинформации: отправитель, получатели, тема, дата отправки и др.,- и основной части: текст и вложения. Основная часть письма представлена в виде фрагментов BodyPart
, которые хранятся в контейнере Multipart
. Подробнее о фрагментах — ниже.
Каждое письмо привязывается к конкретной сессии. При создании нового письма в конструктор передается объект сессии:
final Message message = new MimeMessage(session);
Для установки значений полей метаинформации используются обычные сеттеры. При установке получателей необходимо указывать еще тип адресантов, для этого у класса Message
есть внутренний класс RecipientType
, который представляет собой различные виды адресантов:
TO
— прямой получатель;CC
— получатель копии;BCC
— получатель скрытой копии.
Поля с такими названиями можно найти в интерфейсе любого почтового клиента при написании письма.
message.setFrom("artem.boiar@yandex.ru");
message.setRecipients(Message.RecipientType.TO, "joshua.bloch@google.com");
message.setRecipients(Message.RecipientType.CC, "tagir.valeev@jetbrains.com");
message.setRecipients(Message.RecipientType.BCC, "sergey.egorov@pivotal.com");
message.setSubject("Java 20 new hot features");
Флаги
При чтении писем можно изменять их состояние, устанавливая им различные флаги. Стандартные флаги представлены в классе Flags
:
ANSWERED
— отвеченоDELETED
— удаленоDRAFT
— черновикFLAGGED
— выделеноRECENT
— недавнееSEEN
— просмотреноUSER
— пользовательский флаг
Флаги устанавливаются в сообщении в нужное положение с помощью метода setFlag(Flag, boolean set)
.
Адреса
Адреса используются для идентификации получателя письма.
Класс Address
является абстрактным и по сути имеет всего одну реализацию — InternetAddress
(есть еще NewsAddress
для мамонтов, пользующихся Usenet’ом).
InternetAddress
представляет привычные нам адреса электронной почты, записанные в формате:
trisha.gee@jetbrains.com
Помимо адреса объекты могут хранить в себе имя адресанта в любой кодировке:
internetAddress.setPersonal("Сергей Куксенко");
Для того чтобы создать объект InternetAddress
можно воспользоваться одним из конструкторов, либо распарсить строку с несколькими адресами в массив:
InternetAddress[] recipients = InternetAddress.parse(
"kuksenko@oracle.com, baruh@jfrog.com, golodnyj@yandex.ru");
Примечание — для валидации адресов электронной почты, приходящих от пользователя в виде строк, можно пользоваться аннотацией @Email
, объявленной в библиотеке Bean Validation.
Фрагменты письма
Фрагмент письма — это неделимая часть письма, несущая полезную нагрузку адресанту. Фрагмент письма может либо относиться к основной части письма, либо быть вложением.
В Jakarta Mail фрагмент письма представлен абстрактным классом BodyPart
и его реализацией MimeBodyPart
.
Тип фрагмента определяется параметром disposition
:
INLINE
для основной части письма иATTACHMENT
для вложения.
Тип фрагмента можно не объявлять явно, чаще всего он выводится автоматически, в зависимости от того, что записано во фрагмент.
Наиболее часто используемые MIME-типы для частей:
text/plain
для основной части письма иapplication/octet-stream
для вложений.
Для создания фрагмента письма можно пользоваться обычным конструктором:
final MimeBodyPart mailBody = new MimeBodyPart();
final MimeBodyPart attachment = new MimeBodyPart();
На UML-диаграмме наследования, приведенной выше, объявлен еще интерфейс Part
, который реализуется почти всеми классами, относящимися к письму. В нем объявлены методы доступа к метаинформации письма. Это довольно странное решение — как-будто у различных фрагментов одного письма могут быть различные адресанты или темы письма.
Запись контента
После создания фрагмента необходимо записать в него полезную нагрузку. Для основной части используется метод setText()
, а для вложения — attachFile()
или setContent()
.
mailBody.setText("Java 20 new features.\nLook at the attachments.");
attachment.attachFile(file);
MIME-тип вложения в большинстве случаев определяется автоматически по расширению файла.
Чтение контента
Для чтения контента из пришедшего письма можно воспользоваться методомvoid saveFile(File)
для сохранения контента на диск илиvoid writeTo(OutputStream)
для перенаправления контента в указанный поток.
Мультифрагменты
Фрагменты BodyPart
для помещения в письмо объединяются в структуру Multipart
. MimeBodyPart
соответственно объединяются в структуру MimeMultipart
.
final Multipart multipart = new MimeMultipart();
for (BodyPart bodyPart: bodyParts) {
//cannot use streams because of checked exception
multipart.addBodyPart(bodyPart);
}
После этого мультипарт вкладывается в письмо:
message.setContent(multipart);
Папка с письмами
Для хранения писем на почтовом сервере используются папки. Обычно папка по умолчанию называется INBOX
(в POP3 это единственная папка).
Папки организуют древовидную структуру. В некоторых реализациях папки могут хранить в себе что-то одно: папки или письма, — в других (например, IMAP) сразу и то, и другое.
Папка может находиться в открытом и закрытом состоянии. При извлечении папки она находится в закрытом состоянии. Чтобы прочитать письма, находящиеся в ней, необходимо сначала открыть ее open(int mode)
в одной из двух режимов: только для чтения READ_ONLY
или на чтение и запись — READ_WRITE
.
Folder folder = store.getDefaultFolder();
folder.open(Folder.READ_WRITE);
Операции с содержимым папки могут выполняться только в открытом состоянии, а операции над самой папкой только в закрытом состоянии.
Для извлечения писем из папки можно использовать метод getMessages()
, возвращающий все письма из папки:
Message[] messages = folder.getMessages();
Письма располагаются в папке аналогично списку и имеют номера, начиная с 1
. Для получения писем по их порядковым номерам используются разновидности приведенного выше метода.
При использовании протокола IMAP из папки достаются легковесные объекты
вместо полноценных сообщений. Необходимые части сообщения подтягиваются с почтового сервера по требованию, либо можно сразу загрузить полную версию письма, вызвав метод fetch()
класса Folder, передав в него желаемые письма.
Папки позволяют удалять письма, но сделать это можно только с теми письмами, которые помечены флагом DELETED
.
Поиск писем
Jakarta Mail позволяет проводить поиск по письмам в папках. Для составления поискового запроса предназначен абстрактный класс SearchTerm
, имеющий множество реализаций для поиска по различным критериям: теме письма, дате отправления, отправителю, флагам и пр. Различные критерии поиска могут объединяться в один поисковый запрос.
После составление поискового запроса он запускается по указанной папке:
final FromTerm fromTerm = new FromTerm(new InternetAddress("artem.boiar@yandex.ru"));
final SubjectTerm subjectTerm = new SubjectTerm("Java");
final AndTerm termsSummary = new AndTerm(fromTerm, subjectTerm);
final Message[] foundMessages = folder.search(termsSummary);
3. Работа с электронной почтой в среде Spring
Для удобства использования библиотеки Jakarta Mail в Spring-приложениях была разработана библиотека Spring Email, которая по сути является фасадом к API Jakarta Mail.
Все классы библиотеки находятся в пакете org.springframework.mail
, который можно подтянуть с помощью зависимости spring-boot-starter-mail
.
В большинстве Java-приложений не требуется читать письма с почтового сервера, чаще всего необходимо только рассылать электронную почту (например для подтверждения регистрации в вашем веб-приложении или для рассылки уведомлений). В связи с этим Spring Email предоставляет API только для отправки писем.
Интересный факт — в разработке Spring Mail принимал участие наш земляк — Дмитрий Копыленко.
Библиотека Spring Email предоставляет ряд классов и интерфейсов, которые хорошо вписываются в общую концепцию фреймворка. Все что нужно для отправки электронного сообщения из вашего кода, это заинжектить бин отправителя и передать ему письмо с помощью метода send()
.
В библиотеке выделяются два вида писем: простые и MIME-письма. Простые не могут содержать в своем теле ничего кроме текста, MIME-письма могут содержать в теле html-верстку, изображения и иметь вложения.
Отправка простых писем
Простые письма представлены классом SimpleMailMessage
. Этот класс полностью независим от сущностей библиотеки Jakarta Mail — все его методы принимают примитивы и объекты стандартной библиотеки Java.
final SimpleMailMessage simpleMail = new SimpleMailMessage();
simpleMail.setFrom("artem.boiar@yandex.ru");
simpleMail.setTo("yegor.bugaenko@huawei.com");
simpleMail.setSubject("Java 20 new hot features");
simpleMail.setText("Java 20 new hot features. No attachments :(");
Отправить созданное письмо можно с помощью бина интерфейса MailSender
.
this.mailSender.send(simpleMail);
Отправка MIME-писем
Для работы с MIME-письмами используется класс MimeMessage
из Jakarta Mail. Для тех случаев, когда требуется одинаковый интерфейс для работы с простыми и MIME-письмами существует класс адаптер MimeMailMessage
, который как и SimpleMailMessage
реализует интерфейс MailMessage
.
Для создания объектов MimeMessage
в библиотеке есть класс-помощник — MimeMessageHelper
. Этот класс предоставляет удобный фасад для работы с телом письма, его
мультимедийными вставками и вложениями. Пользоваться им очень просто: достаточно обернуть в него объект MIME-письма и задать все необходимые параметры:
final MimeMessage mimeMessage = this.mailSender.createMimeMessage();
final MimeMessageHelper messageHelper = new MimeMessageHelper(mimeMessage, true);
messageHelper.setFrom("artem.boiar@yandex.ru");
messageHelper.setTo("artyom.boyarshinov@cosysoft.ru");
messageHelper.setSubject("Java 20 new hot features");
messageHelper.setText("Java 20 new hot features. Look at the attachment.\nAlso look at my great cat!");
messageHelper.addInline("", FileUtils.getImage());
messageHelper.addAttachment("java-new-features.txt", FileUtils.getFile());
Таким образом MimeMessageHelper
избавляет разработчика от необходимости создания BodyPart
, и их сборки в Multipart
, предоставляя удобное линейное API.
Для отправки MIME-писем используется бин интерфейса JavaMailSender
:
this.javaMailSender.send(mimeMessage);
Помимо всего прочего в Spring Email существует callback-интерфейс для создания MIME-писем — MimeMessagePreparator
. Этот интерфейс является функциональным и может быть заменен лямбдой Consumer
, принимающей MimeMessage
.
final MimeMessagePreparator preparator = mimeMessage -> {
final MimeMessageHelper messageHelper = new MimeMessageHelper(mimeMessage, true);
messageHelper.setFrom("artem.boiar@yandex.ru");
messageHelper.setTo("vlad.mihalcea@hibernate.com");
messageHelper.setSubject("Java 20 new hot features");
messageHelper.setText("Java 20 new hot features. Look at the attachment");
messageHelper.addAttachment("java-new-features.txt", FileUtils.getFile());
};
Обработка исключений
В отличие от Jakarta Mail, все методы Spring Email бросают непроверяемые исключения.
Эти исключения являются runtime обертками над проверяемыми исключениями Jakarta Mail.
Особый интерес представляет исключение MailSendException
. Оно позволяет извлечь конкретные письма, при отправке которых были инициированы исключения, с помощью метода getFailedMessages()
:
catch (MailSendException exc) {
Map
Настройка Spring Mail
Spring Email предоставляет удобный способ настройки соединения с SMTP сервером посредством обычного application.properties
/ application.yml
-файла. Это упрощает настройку отправителя электронных писем, так как бин JavaMailSenderImpl
конфигурируется самостоятельно при поднятии контекста.
spring:
mail:
protocol: smtps
host: smtp.yandex.ru
port: 465
username: artem.boiar
password: passw0rd