пятница, 22 февраля 2019 г.

Работа с почтой на Java

Несмотря на засилье мессенджеров, смс и прочих скайпов, почта - наше все!
Без преувеличения.
Поэтому уметь рассылать почту на Java - архинужно и архиважно!
И в принципе, это легко сделать, но есть нюансы. как обычно=)
Первые шаги
Чтобы работать с почтовыми сообщениями на Java нужно использовать замечательную библиотеку Java Mail
Если ваш проект использует Maven, необходимо лишь добавить Java Mail в зависимости. Должно получиться что-то такое:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>ru.toolkas</groupId>
    <artifactId>email</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>
        <dependency>
            <groupId>javax.mail</groupId>
            <artifactId>mail</artifactId>
            <version>1.4.7</version>
        </dependency>
    </dependencies>
</project>

Теперь напишем самый простой код, присылающий сообщение на Yandex-почту:
package ru.toolkas.email;

import javax.mail.*;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import java.util.Properties;

public class HelloEmail {
    public static void main(String[] args) throws MessagingException {
        //Объект properties хранит параметры соединения.
        //Для каждого почтового сервера они разные.
        //Если не знаете нужные - обратитесь к администратору почтового сервера.
        //Ну или гуглите;=)
        //Конкретно для Yandex параметры соединения можно подсмотреть тут: 
        //https://yandex.ru/support/mail/mail-clients.html (раздел "Исходящая почта")
        Properties properties = new Properties();
        //Хост или IP-адрес почтового сервера
        properties.put("mail.smtp.host", "smtp.yandex.ru");
        //Требуется ли аутентификация для отправки сообщения
        properties.put("mail.smtp.auth", "true");
        //Порт для установки соединения
        properties.put("mail.smtp.socketFactory.port", "465");
        //Фабрика сокетов, так как при отправке сообщения Yandex требует SSL-соединения
        properties.put("mail.smtp.socketFactory.class", "javax.net.ssl.SSLSocketFactory");

        //Создаем соединение для отправки почтового сообщения
        Session session = Session.getDefaultInstance(properties,
                //Аутентификатор - объект, который передает логин и пароль
                new Authenticator() {
                    @Override
                    protected PasswordAuthentication getPasswordAuthentication() {
                        return new PasswordAuthentication("rogovdn@yandex.ru", "ПАРОЛЬ");
                    }
                });

        //Создаем новое почтовое сообщение
        Message message = new MimeMessage(session);
        //От кого
        message.setFrom(new InternetAddress("rogovdn@yandex.ru"));
        //Кому
        message.setRecipient(Message.RecipientType.TO, new InternetAddress("rogovdn@yandex.ru"));
        //Тема письма
        message.setSubject("Очень важное письмо!!!");
        //Текст письма
        message.setText("Hello, Email!");
        //Поехали!!!
        Transport.send(message);
    }
}


Если выполнить этот код, то на почтовый адрес rogovdn@yandex.ru придет вот такое сообщение:

Несложно, правда? Единственное, что тут совсем непонятно, откуда берутся параметры соединения, такие как "mail.smtp.host". Но на самом деле все довольно просто. Java Mail использует для работы с почтой 3 основных протокола: SMTP - для отправки сообщений, IMAP и POP3 - для чтения. Для настройки каждого из них Java Mail имеет множество параметров, полный список которых можно посмотреть тут, тут и тут.
Пусть вас не пугает их количество, в большинстве случаев требуется лишь хост и порт сервера и параметры, связанные с аутентификацией. Поэтому запоминать их все ни к чему.
А теперь - сбрось мне файлик
Что за письма без вложений? Несерьезно даже как-то...
Но есть одна сложность: содержимое письма с вложением придется буквально собирать по частям. 
У нас всего две части: текстовое сообщение внутри письма и файл-вложение. 
Для того, чтобы отправить такое сообщение программно, придется поменять строчку кода
message.setText("Hello, Email!");
на код
//Файл вложения
File file = new File("D:/1.txt");
//Собираем содержимое письма из кусочков
MimeMultipart multipart = new MimeMultipart();
//Первый кусочек - текст письма
MimeBodyPart part1 = new MimeBodyPart();
part1.addHeader("Content-Type", "text/plain; charset=UTF-8");
part1.setDataHandler(new DataHandler("Письмо с файлом!!", "text/plain; charset=\"utf-8\""));
multipart.addBodyPart(part1);

//Второй кусочек - файл
MimeBodyPart part2 = new MimeBodyPart();
part2.setFileName(MimeUtility.encodeWord(file.getName()));
part2.setDataHandler(new DataHandler(new FileDataSource(file)));
multipart.addBodyPart(part2);
//Добавляем оба кусочка в сообщение
message.setContent(multipart);
Теперь, открыв почту, мы увидим файл во вложении.
Код, конечно, стал немного сложнее, но понять его можно.
Наводим красоту
Теперь, когда мы умеем слать и просто сообщения и сообщения с вложениями, душа требует чего-то прекрасного, яркого и незабываемого!
Что ж, нет ничего проще, если подойти к делу с огоньком. Мы можем слать не только простой и скучный текст, но и HTML-разметку. 
А это значит, что если написать, например так:
//Собираем содержимое письма из кусочков
MimeMultipart multipart = new MimeMultipart();
//Первый кусочек - текст письма
MimeBodyPart part1 = new MimeBodyPart();
part1.addHeader("Content-Type", "text/html; charset=UTF-8");
part1.setDataHandler(
        new DataHandler(
                "<html><body style=\"background-color: #FFFF00;color: #FF0033;\"><h1>Привет!</h1></body></html>",
                "text/html; charset=\"utf-8\""
 )
);
multipart.addBodyPart(part1);
message.setContent(multipart);
Запустив этот код, мы получим на почту вот такую красоту:
Если вы хорошо знаете HTML, то можете поразить адресата изысканным дизайном своего послания=)
Странные проблемы
При использовании Java Mail в сложной среде загрузчиков иногда можно встретить следующую ошибку:
javax.activation.UnsupportedDataTypeException: no object DCH for MIME type multipart/mixed;
        boundary="----=_Part_0_347165349.1510819320461"
        at javax.activation.ObjectDataContentHandler.writeTo(DataHandler.java:896)
        at javax.activation.DataHandler.writeTo(DataHandler.java:317)
        at javax.mail.internet.MimeBodyPart.writeTo(MimeBodyPart.java:1652)
        at javax.mail.internet.MimeMessage.writeTo(MimeMessage.java:1850)
        at javax.mail.internet.MimeMessage.writeTo(MimeMessage.java:1824)
        at org.simplejavamail.converter.EmailConverter.mimeMessageToEML(EmailConverter.java:186)
        ... 12 common frames omitted
DCH object, который не смогла найти библиотека - это Data Content Handler, один из классов, который определяет как добавляется в сообщение содержимое того или иного формата.
Эти классы загружаются довольно специфическим образом, поэтому, чтобы помочь библиотеке найти их, можно использовать следующий трюк:
package ru.toolkas.email;

import javax.mail.*;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import java.util.Properties;

public class HelloEmail {
    public static void main(String[] args) throws MessagingException {
        //Запомним контекстный загрузчик классов
        ClassLoader loader = Thread.currentThread().getContextClassLoader();
        try {
            Сделаем контекстным загрузчик, которым загружены классы библиотеки Java Mail
            Thread.currentThread().setContextClassLoader(javax.mail.Session.class.getClassLoader());
         
            Properties properties = new Properties();
            properties.put("mail.smtp.host", "smtp.yandex.ru");
            properties.put("mail.smtp.auth", "true");
            properties.put("mail.smtp.socketFactory.port", "465");
            properties.put("mail.smtp.socketFactory.class", "javax.net.ssl.SSLSocketFactory");

            Session session = Session.getDefaultInstance(properties,
                    new Authenticator() {
                        @Override
                        protected PasswordAuthentication getPasswordAuthentication() {
                            return new PasswordAuthentication("rogovdn@yandex.ru", "ПАРОЛЬ");
                        }
                    });

            Message message = new MimeMessage(session);
            message.setFrom(new InternetAddress("rogovdn@yandex.ru"));
            message.setRecipient(Message.RecipientType.TO, new InternetAddress("rogovdn@yandex.ru"));
            message.setSubject("Очень важное письмо!!!");
            message.setText("Hello, Email!");
            Transport.send(message);
        } finally {
            //Вернем исходный контекстный загрузчик классов
            Thread.currentThread().setContextClassLoader(loader);
        }
    }
}
Надеюсь, этот финт сэкономит вам время и нервы и позволит дальше программировать без ошибок=)
Чтение почты
Иногда нужно не рассылать почту, а читать ее. И в этом случае Java Mail так же придет нам на помощь. Код для чтения почтовых писем не сильно сложнее, чем для отправки:
package ru.toolkas.email;

import javax.mail.*;
import javax.mail.internet.InternetAddress;
import java.util.Properties;

public class ReadEmail {
    public static void main(String[] args) throws MessagingException {
        //Объект properties содержит параметры соединения
        Properties properties = new Properties();
        //Так как для чтения Yandex требует SSL-соединения - нужно использовать фабрику SSL-сокетов
        properties.setProperty("mail.imap.socketFactory.class", "javax.net.ssl.SSLSocketFactory");

        //Создаем соединение для чтения почтовых сообщений
        Session session = Session.getDefaultInstance(properties);
        //Это хранилище почтовых сообщений. По сути - это и есть почтовый ящик=)
        Store store = null;
        try {
            //Для чтения почтовых сообщений используем протокол IMAP.
            //Почему? Так Yandex сказал: https://yandex.ru/support/mail/mail-clients.html 
            //см. раздел "Входящая почта"
            store = session.getStore("imap");
            //Подключаемся к почтовому ящику
            store.connect("imap.yandex.ru", 993, "rogovdn@yandex.ru", "ПАРОЛЬ");
            //Это папка, которую будем читать
            Folder inbox = null;
            try {
                //Читаем папку "Входящие сообщения"
                inbox = store.getFolder("INBOX");
                //Будем только читать сообщение, не меняя их
                inbox.open(Folder.READ_ONLY);

                //Получаем количество сообщения в папке
                int count = inbox.getMessageCount();
                //Вытаскиваем все сообщения с первого по последний
                Message[] messages = inbox.getMessages(1, count);
                //Циклом пробегаемся по всем сообщениям
                for (Message message : messages) {
                    //От кого
                    String from = ((InternetAddress) message.getFrom()[0]).getAddress();
                    System.out.println("FROM: " + from);
                    //Тема письма
                    System.out.println("SUBJECT: " + message.getSubject());
                }
            } finally {
                if (inbox != null) {
                    //Не забываем закрыть собой папку сообщений.
                    inbox.close(false);
                }
            }

        } finally {
            if (store != null) {
                //И сам почтовый ящик тоже закрываем
                store.close();
            }
        }
    }
}
Вот таким простым способом можно прочитать содержимое почтового ящика=)
Конец
Итак, друзья, на этом заканчивается мое короткое введение в библиотеку Java Mail.
Хорошего вам программирования, отправляйте и получайте письма, экспериментируйте - во имя добра!

10 комментариев:

  1. Здравствуйте! Спасибо большое за код! Антивирус конечно уничтожал мои клетки, но я смог совладать с собой и снес его!Может ли Imap сохранять вложения входящих писем, или нужно изменить Properties изменить? Я заимствовал код по скачке и у меня сохранялся заголовок файла типа "ЛР.odt", но поток был пуст, как и файл.Или я что-то не доделал?

    ОтветитьУдалить
    Ответы
    1. Добрый день!
      Вложения можно сохранит приблизительно так:

      package ru.documentum.business.services;

      import org.apache.commons.lang.StringUtils;

      import javax.mail.Message;
      import javax.mail.MessagingException;
      import javax.mail.Multipart;
      import javax.mail.Session;
      import javax.mail.internet.MimeBodyPart;
      import javax.mail.internet.MimeMessage;
      import javax.mail.internet.MimeUtility;
      import java.io.ByteArrayInputStream;
      import java.io.ByteArrayOutputStream;
      import java.io.File;
      import java.io.IOException;

      public class SaveAttachments {
      private static void saveAttachments(Session session, Message message, File dir) throws MessagingException, IOException {
      ByteArrayOutputStream baos = new ByteArrayOutputStream();
      message.writeTo(baos);

      ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
      MimeMessage mimeMessage = new MimeMessage(session, bais);

      Multipart multipart = (Multipart) mimeMessage.getContent();
      for (int index = 0; index < multipart.getCount(); index++) {
      MimeBodyPart part = (MimeBodyPart) multipart.getBodyPart(index);

      if (StringUtils.isNotBlank(part.getFileName())) {
      String fileName = MimeUtility.decodeText(part.getFileName());

      File f = new File(dir, fileName);
      part.saveFile(f);
      }
      }
      }
      }

      Удалить
  2. пригодится?
    https://stackoverflow.com/questions/2996514/inline-images-in-email-using-javamail

    ""

    MimeBodyPart imagePart = new MimeBodyPart();
    imagePart.attachFile("resources/teapot.jpg");
    imagePart.setContentID("<" + cid + ">");
    imagePart.setDisposition(MimeBodyPart.INLINE);
    content.addBodyPart(imagePart);

    ОтветитьУдалить
  3. Привет! Этот трюк с ClassLoader не помогает. Что ещё можно сделать? К слову, в IDE вложение отправляется, как только я собираю jar и делаю из него .exe, запускаю его на сервере, в лог вылетает это исключение "javax.activation.UnsupportedDataTypeException: no object DCH for MIME type multipart/mixed;". Не понимаю в чем магия, в IDE работает в jar нет. Это зависит от того, что библиотека не может найти файлы для прикрепления или не может их прочитать?

    ОтветитьУдалить
    Ответы
    1. Добрый день, Никита!
      Код полностью как в статье или с вариациями?

      опишите окружение, ось, Java, Антивирус?

      Удалить
    2. Добрый день! В моем приложении код был естественно с вариациями. Но ради эксперимента, я с копипастил. Результата за приделами IDE не было, т.е. в IDE(Idea) все работает, вне нет. Нашел на стеке (https://stackoverflow.com/questions/21856211/javax-activation-unsupporteddatatypeexception-no-object-dch-for-mime-type-multi) вот такую формацию:

      MailcapCommandMap mc = (MailcapCommandMap) CommandMap.getDefaultCommandMap();
      mc.addMailcap("text/html;; x-java-content-handler=com.sun.mail.handlers.text_html");
      mc.addMailcap("text/xml;; x-java-content-handler=com.sun.mail.handlers.text_xml");
      mc.addMailcap("text/plain;; x-java-content-handler=com.sun.mail.handlers.text_plain");
      mc.addMailcap("multipart/*;; x-java-content-handler=com.sun.mail.handlers.multipart_mixed");
      mc.addMailcap("message/rfc822;; x-java-content- handler=com.sun.mail.handlers.message_rfc822");

      Вставил в конструктор класса и всё работает. До конца не понимаю, что такое MailCamp, не сталкивались с этим? Если да, можете пожалуйста вкратце объяснить?

      Удалить
  4. Как мне Message[], отправить на фронт в формате Json ? Resolved [org.springframework.http.converter.HttpMessageNotWritableException: Could not write JSON: Not connected; nested exception is com.fasterxml.jackson.databind.JsonMappingException: Not connected (through reference chain: java.util.Arrays$ArrayList[0]->com.sun.mail.imap.IMAPMessage["folder"]->com.sun.mail.imap.IMAPFolder["store"]->com.sun.mail.imap.IMAPStore["sharedNamespaces"])]

    ОтветитьУдалить