Стартер spring-boot-starter-mail собирает JavaMailSender из нескольких свойств, поэтому своего кода клиента писать не нужно. Ниже — подключение к fmailer, HTML-письма на Thymeleaf, вложения и асинхронная отправка.
| Параметр | Значение |
|---|---|
| SMTP-хост | smtp.fmailer.ru |
| Порт | 587 |
| Шифрование | STARTTLS (соединение переводится в TLS до авторизации) |
| Логин | Логин SMTP-токена вашего домена |
| Пароль | Пароль этого токена — показывается один раз, при создании |
Без неё бин JavaMailSender просто не создаётся:
<!-- Maven -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>spring.mail.host=smtp.fmailer.ru
spring.mail.port=587
spring.mail.username=${SMTP_USER}
spring.mail.password=${SMTP_PASS}
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.starttls.enable=true
spring.mail.properties.mail.smtp.starttls.required=true
spring.mail.properties.mail.smtp.connectiontimeout=10000
spring.mail.properties.mail.smtp.timeout=10000
spring.mail.properties.mail.smtp.writetimeout=10000
app.mail.from=Магазин <noreply@mail.example.ru>Для шифрования с первого байта на порту 465:
# Порт 465 — шифрование сразу, без STARTTLS
spring.mail.port=465
spring.mail.properties.mail.smtp.ssl.enable=true
spring.mail.properties.mail.smtp.starttls.enable=falseimport org.springframework.beans.factory.annotation.Value;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.stereotype.Service;
@Service
public class EmailService {
private final JavaMailSender mailSender;
@Value("${app.mail.from}")
private String from;
public EmailService(JavaMailSender mailSender) {
this.mailSender = mailSender;
}
public void sendPlainText(String to, String subject, String text) {
SimpleMailMessage message = new SimpleMailMessage();
message.setFrom(from);
message.setTo(to);
message.setSubject(subject);
message.setText(text);
mailSender.send(message);
}
}SimpleMailMessage умеет только текст. Для HTML соберите MimeMessage через MimeMessageHelper: метод setText с двумя аргументами принимает текстовую и HTML-часть и собирает multipart/alternative:
import jakarta.mail.internet.MimeMessage;
import org.springframework.mail.javamail.MimeMessageHelper;
public void sendHtml(String to, String subject, String html, String text) throws Exception {
MimeMessage message = mailSender.createMimeMessage();
// true = multipart, чтобы в одно письмо поместились обе части
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
helper.setFrom(from);
helper.setTo(to);
helper.setSubject(subject);
helper.setText(text, html); // (текст, HTML) — отправляйте обе части
mailSender.send(message);
}import org.thymeleaf.context.Context;
import org.thymeleaf.spring6.SpringTemplateEngine;
private final SpringTemplateEngine templateEngine;
public void sendOrderConfirmation(Order order) throws Exception {
Context ctx = new Context();
ctx.setVariable("order", order);
String html = templateEngine.process("email/order-confirmed", ctx);
sendHtml(order.getCustomerEmail(),
"Подтверждение заказа №" + order.getId(),
html,
"Заказ принят. Доставка — завтра до 18:00.");
}public void sendInvoice(String to, File pdf) throws Exception {
MimeMessage message = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
helper.setFrom(from);
helper.setTo(to);
helper.setSubject("Счёт на оплату");
helper.setText("Счёт за март во вложении.");
helper.addAttachment("invoice.pdf", pdf);
helper.addInline("logo", new ClassPathResource("static/logo.png")); // <img src="cid:logo">
mailSender.send(message);
}mailSender.send() блокирует поток на всё время обмена с SMTP. В веб-приложении это сотни миллисекунд удерживаемого потока запроса — вынесите отправку в отдельный executor:
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean(name = "mailExecutor")
public Executor mailExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(2);
executor.setMaxPoolSize(5);
executor.setQueueCapacity(500);
executor.setThreadNamePrefix("mail-");
executor.initialize();
return executor;
}
}
// В сервисе — вызывающий поток не ждёт SMTP.
@Async("mailExecutor")
public void sendOrderConfirmationAsync(Order order) throws Exception {
sendOrderConfirmation(order);
}| Ошибка | Причина и решение |
|---|---|
NoSuchBeanDefinitionException: JavaMailSender | Нет зависимости spring-boot-starter-mail или не задан spring.mail.host. |
AuthenticationFailedException (535) | Неверные доступы, либо mail.smtp.auth не равен true. |
| Отправка зависает | Нет таймаутов, порт заблокирован. Задайте таймауты и используйте 8587. |
MailSendException: Could not convert socket to TLS | STARTTLS требуется на порту, где его нет. Проверьте пару порт — режим. |
| Кириллица приходит «кракозябрами» | Передайте "UTF-8" в MimeMessageHelper. |
SMTPSendFailedException (550) | Домен отправителя не подтверждён. |
Автоконфигурация срабатывает только когда в classpath есть spring-boot-starter-mail и задан spring.mail.host. Без любого из двух бин JavaMailSender не создаётся, и приложение падает с NoSuchBeanDefinitionException.
На порту 587 — обязательно. Spring Boot не включает STARTTLS сам, поэтому без этого свойства сессия остаётся открытой и сервер отказывает в авторизации. Ставьте ещё и starttls.required=true, чтобы неудачный переход в TLS был ошибкой, а не молчаливым откатом.
У JavaMail нет таймаутов по умолчанию — при заблокированном порте поток ждёт бесконечно. Всегда задавайте mail.smtp.connectiontimeout, mail.smtp.timeout и mail.smtp.writetimeout. Если после этого начинается таймаут, значит хостинг блокирует исходящий 587: используйте 8587.
SimpleMailMessage отправляет только простой текст. Всё, где есть HTML, вложения, встроенные картинки или своя кодировка, требует MimeMessage — обычно через MimeMessageHelper с флагом multipart, равным true.
Пометьте метод отправки аннотацией @Async и заведите под него отдельный бин Executor, чтобы обмен с SMTP шёл в почтовом потоке, а не в потоке запроса. Помните, что @Async работает только при вызове из другого бина — вызов внутри того же класса минует прокси.
Проверьте, задано ли spring.mail.properties.mail.smtp.auth=true — без него JavaMail вообще не отправляет учётные данные. И убедитесь, что в username указан полный логин SMTP-токена домена, а не только его первая часть.