邮件发送

Spring Boot 通过 spring-boot-starter-mail 封装 JavaMail,支持文本邮件、HTML 邮件、附件和模板邮件,几行配置即可发送。


依赖与配置

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-mail</artifactId>
</dependency>
spring:
  mail:
    host: smtp.example.com
    port: 587
    username: noreply@example.com
    password: ${MAIL_PASSWORD}       # 从环境变量读取,不要硬编码
    default-encoding: UTF-8
    properties:
      mail:
        smtp:
          auth: true
          starttls:
            enable: true            # 587 端口用 STARTTLS
            required: true
          connectiontimeout: 5000
          timeout: 5000
          writetimeout: 5000

常用邮件服务商配置:

服务商hostport说明
Gmailsmtp.gmail.com587需开启”应用专用密码”
QQ 邮箱smtp.qq.com587需开启 SMTP 并获取授权码
163 邮箱smtp.163.com465SSL,port 改 465,starttls 关闭
企业微信smtp.exmail.qq.com465SSL
Outlooksmtp-mail.outlook.com587STARTTLS

发送文本邮件

@Service
@RequiredArgsConstructor
@Slf4j
public class MailService {
 
    private final JavaMailSender mailSender;
 
    @Value("${spring.mail.username}")
    private String from;
 
    // 简单文本邮件
    public void sendText(String to, String subject, String content) {
        SimpleMailMessage message = new SimpleMailMessage();
        message.setFrom(from);
        message.setTo(to);
        message.setSubject(subject);
        message.setText(content);
 
        mailSender.send(message);
        log.info("文本邮件已发送: to={}, subject={}", to, subject);
    }
 
    // 群发
    public void sendToMultiple(String[] recipients, String subject, String content) {
        SimpleMailMessage message = new SimpleMailMessage();
        message.setFrom(from);
        message.setTo(recipients);
        message.setCc("admin@example.com");       // 抄送
        message.setBcc("archive@example.com");    // 密送
        message.setSubject(subject);
        message.setText(content);
 
        mailSender.send(message);
    }
}

发送 HTML 邮件

public void sendHtml(String to, String subject, String htmlContent) throws MessagingException {
    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(htmlContent, true);   // true = HTML 内容
 
    mailSender.send(message);
}

发送带附件的邮件

public void sendWithAttachment(String to, String subject,
                               String content, File attachment) throws MessagingException {
    MimeMessage message = mailSender.createMimeMessage();
    MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
 
    helper.setFrom(from);
    helper.setTo(to);
    helper.setSubject(subject);
    helper.setText(content, true);
 
    // 文件附件
    helper.addAttachment(attachment.getName(), attachment);
 
    // 从 classpath 加载附件
    ClassPathResource resource = new ClassPathResource("templates/contract.pdf");
    helper.addAttachment("合同.pdf", resource);
 
    // 从字节数组添加附件(如动态生成的报表)
    byte[] reportBytes = reportService.generate();
    helper.addAttachment("报表.xlsx",
        new ByteArrayResource(reportBytes),
        "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
 
    mailSender.send(message);
}

内嵌图片(邮件正文图片)

public void sendWithInlineImage(String to, String subject) throws MessagingException {
    MimeMessage message = mailSender.createMimeMessage();
    MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
 
    helper.setFrom(from);
    helper.setTo(to);
    helper.setSubject(subject);
 
    // HTML 中用 cid:logoImage 引用内嵌图片
    String html = """
        <html>
          <body>
            <h1>欢迎注册</h1>
            <img src='cid:logoImage' width='200'/>
            <p>感谢您的注册!</p>
          </body>
        </html>
        """;
    helper.setText(html, true);
 
    // 内嵌图片(cid 需与 HTML 中的 cid: 一致)
    ClassPathResource logo = new ClassPathResource("static/images/logo.png");
    helper.addInline("logoImage", logo);
 
    mailSender.send(message);
}

Thymeleaf 模板邮件

将 HTML 内容从代码中分离到模板文件,便于维护:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

src/main/resources/templates/mail/order-confirm.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
  <meta charset="UTF-8">
  <title>订单确认</title>
</head>
<body>
  <h2 th:text="'亲爱的 ' + ${userName} + ',您好!'"></h2>
  <p>您的订单 <strong th:text="${orderId}"></strong> 已确认。</p>
  <table>
    <tr th:each="item : ${items}">
      <td th:text="${item.productName}"></td>
      <td th:text="${item.quantity}"></td>
      <td th:text="${#numbers.formatDecimal(item.price, 1, 2)}"></td>
    </tr>
  </table>
  <p>总计:<strong th:text="${#numbers.formatDecimal(total, 1, 2)}"></strong> 元</p>
  <a th:href="${trackingUrl}">查看物流</a>
</body>
</html>
@Service
@RequiredArgsConstructor
@Slf4j
public class TemplateMailService {
 
    private final JavaMailSender mailSender;
    private final TemplateEngine templateEngine;   // Thymeleaf TemplateEngine
 
    @Value("${spring.mail.username}")
    private String from;
 
    public void sendOrderConfirm(String to, Order order) throws MessagingException {
        // 1. 填充模板变量
        Context context = new Context(Locale.CHINESE);
        context.setVariable("userName", order.getUserName());
        context.setVariable("orderId", order.getId());
        context.setVariable("items", order.getItems());
        context.setVariable("total", order.getTotal());
        context.setVariable("trackingUrl", "https://example.com/track/" + order.getId());
 
        // 2. 渲染 HTML
        String html = templateEngine.process("mail/order-confirm", context);
 
        // 3. 发送
        MimeMessage message = mailSender.createMimeMessage();
        MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
        helper.setFrom(from);
        helper.setTo(to);
        helper.setSubject("订单确认 - " + order.getId());
        helper.setText(html, true);
 
        mailSender.send(message);
        log.info("订单确认邮件已发送: orderId={}, to={}", order.getId(), to);
    }
}

Thymeleaf 模板语法详见 Thymeleaf


异步发送

邮件发送通常耗时数百毫秒到数秒,应放入异步线程池,不阻塞业务线程:

@Service
@RequiredArgsConstructor
@Slf4j
public class AsyncMailService {
 
    private final TemplateMailService templateMailService;
 
    @Async("mailExecutor")   // 指定邮件专用线程池
    public void sendOrderConfirmAsync(String to, Order order) {
        try {
            templateMailService.sendOrderConfirm(to, order);
        } catch (MessagingException e) {
            log.error("邮件发送失败: to={}, orderId={}", to, order.getId(), e);
            // 可结合重试机制或写入失败队列
        }
    }
}
 
// 邮件专用线程池(与主业务线程池隔离)
@Bean("mailExecutor")
public Executor mailExecutor() {
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    executor.setCorePoolSize(2);
    executor.setMaxPoolSize(5);
    executor.setQueueCapacity(200);
    executor.setThreadNamePrefix("mail-");
    executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
    executor.initialize();
    return executor;
}

异步线程池详见 异步与线程池


重试机制

@Service
@RequiredArgsConstructor
@Slf4j
public class ReliableMailService {
 
    private final JavaMailSender mailSender;
 
    // Spring Retry:最多重试 3 次,每次间隔 2 秒
    @Retryable(
        retryFor = {MailException.class, MessagingException.class},
        maxAttempts = 3,
        backoff = @Backoff(delay = 2000, multiplier = 2)
    )
    public void sendWithRetry(String to, String subject, String content)
            throws MessagingException {
        MimeMessage message = mailSender.createMimeMessage();
        MimeMessageHelper helper = new MimeMessageHelper(message, "UTF-8");
        helper.setTo(to);
        helper.setSubject(subject);
        helper.setText(content, true);
        mailSender.send(message);
    }
 
    // 全部重试失败后的兜底处理
    @Recover
    public void recover(MailException ex, String to, String subject, String content) {
        log.error("邮件发送最终失败,写入失败记录: to={}, subject={}", to, subject, ex);
        failedMailRepository.save(FailedMail.of(to, subject, content));
    }
}

需引入 Spring Retry 依赖:

<dependency>
    <groupId>org.springframework.retry</groupId>
    <artifactId>spring-retry</artifactId>
</dependency>

邮件发送记录

@Entity
@Table(name = "mail_log")
@Data
@Builder
public class MailLog {
    @Id @GeneratedValue
    private Long id;
 
    private String recipient;
    private String subject;
 
    @Enumerated(EnumType.STRING)
    private MailStatus status;   // PENDING / SENT / FAILED
 
    private String errorMessage;
    private Integer retryCount;
 
    private LocalDateTime sentAt;
    private LocalDateTime createdAt;
}
 
@Service
@RequiredArgsConstructor
public class LoggedMailService {
 
    private final JavaMailSender mailSender;
    private final MailLogRepository logRepository;
 
    @Transactional
    public void send(String to, String subject, String html) throws MessagingException {
        MailLog log = logRepository.save(MailLog.builder()
            .recipient(to)
            .subject(subject)
            .status(MailStatus.PENDING)
            .createdAt(LocalDateTime.now())
            .build());
        try {
            MimeMessage message = mailSender.createMimeMessage();
            MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
            helper.setTo(to);
            helper.setSubject(subject);
            helper.setText(html, true);
            mailSender.send(message);
 
            logRepository.updateStatus(log.getId(), MailStatus.SENT, LocalDateTime.now(), null);
        } catch (Exception e) {
            logRepository.updateStatus(log.getId(), MailStatus.FAILED,
                null, e.getMessage());
            throw e;
        }
    }
}

测试邮件发送

本地开发使用 MailpitMailHog 捕获邮件,不实际发出:

# 开发环境:所有邮件发到本地 SMTP 捕获工具
spring:
  mail:
    host: localhost
    port: 1025     # Mailpit 默认 SMTP 端口
    username: ""
    password: ""
    properties:
      mail.smtp.auth: false
      mail.smtp.starttls.enable: false
# Docker 启动 Mailpit(Web 界面在 8025 端口)
docker run -d -p 1025:1025 -p 8025:8025 axllent/mailpit

单元测试(使用 GreenMail):

<dependency>
    <groupId>com.icegreen</groupId>
    <artifactId>greenmail-spring</artifactId>
    <version>2.1.0</version>
    <scope>test</scope>
</dependency>
@SpringBootTest
@ExtendWith(GreenMailExtension.class)
class MailServiceTest {
 
    @RegisterExtension
    static GreenMailExtension greenMail = new GreenMailExtension(ServerSetupTest.SMTP)
        .withConfiguration(GreenMailConfiguration.aConfig()
            .withUser("test@example.com", "password"))
        .withPerMethodLifecycle(false);
 
    @Autowired
    private MailService mailService;
 
    @Test
    void shouldSendWelcomeEmail() throws Exception {
        mailService.sendText("user@example.com", "欢迎注册", "感谢注册!");
 
        MimeMessage[] messages = greenMail.getReceivedMessages();
        assertThat(messages).hasSize(1);
        assertThat(messages[0].getSubject()).isEqualTo("欢迎注册");
        assertThat(messages[0].getAllRecipients()[0].toString())
            .isEqualTo("user@example.com");
    }
}

常见问题

问题可能原因解决方案
535 认证失败密码错误或使用了登录密码而非授权码QQ / 163 需获取授权码,Gmail 用应用专用密码
连接超时防火墙阻断 SMTP 端口确认 587 / 465 出站端口已开放
邮件进垃圾箱SPF / DKIM / DMARC 未配置域名 DNS 配置 SPF 和 DKIM 记录
中文乱码编码未设置MimeMessageHelper 构造时指定 UTF-8
大附件发送失败超出 SMTP 服务器大小限制改用对象存储分享链接,不直接发附件

相关链接