Note 24. SpringBoot3集成SpringMail - 发送您的第一个邮件信息


第一章. 邮件服务的核心认知

本章摘要:在动手写代码之前,我们需要先理解"邮件是怎么发出去的"。本章将用最直白的方式讲清楚 SMTP 协议、端口选择、以及前后端分离架构下的邮件设计原则——这些认知将帮助你在后续遇到问题时快速定位原因。

本章学习路径

阶段目标解锁能力
1.1理解邮件发送的业务价值能判断何时该用邮件、何时该用短信
1.2掌握 SMTP 协议与端口选择能解释为什么云服务器连不上 25 端口
1.3理解前后端分离的邮件架构能说清楚为什么前端不能生成 HTML

1.1. 为什么你的应用需要发邮件

几乎所有的 Web 应用都绑定了邮件功能。打开你的收件箱看看——注册验证码、密码重置链接、订单发货通知、每周数据报告——这些都是程序自动发出的。

“那为什么不用短信呢?微信通知不是更快吗?”

问得好。我们来做个对比:

通知方式单条成本触达率适用场景
邮件几乎免费中等(可能进垃圾箱)验证码、通知、营销、报表
短信0.03-0.05 元极高强实时性场景(登录验证)
微信/App 推送免费依赖用户安装已有私域流量的产品

结论很清晰:邮件是性价比最高的通知渠道,尤其适合验证码、系统通知这类"必须送达但不紧急"的场景。


1.2. 邮件发送的技术原理

1.2.1. SMTP 协议是什么

SMTP(Simple Mail Transfer Protocol,简单邮件传输协议)是互联网发送邮件的标准协议。

可以把它想象成"邮局的收发室":

  1. 你的 Spring Boot 应用写好一封信(邮件内容)
  2. 把信交给收发室(SMTP 服务器,如 smtp.qq.com
  3. 收发室负责把信投递到收件人的邮箱

我们写代码时,本质上就是在和这个"收发室"对话。

1.2.2. 端口之争:25 vs 465 vs 587

SMTP 服务器监听三个端口,它们的区别是加密方式不同:

端口加密方式现状
25无加密(明文)几乎所有云服务器都封禁,别想了
465SSL(全程加密)QQ 邮箱、阿里云企业邮箱的首选
587STARTTLS(先明文再升级加密)Gmail、国际服务的首选

“为什么云服务器要封 25 端口?”

因为 25 端口是垃圾邮件的重灾区。早年黑客租一台云服务器就能疯狂发垃圾邮件,所以阿里云、AWS、腾讯云等厂商一刀切封禁了 25 端口。

记住这个结论:在云服务器上部署时,只能用 465 或 587 端口。

1.2.3. 本节小结

概念一句话解释
SMTP发邮件的标准协议,相当于"邮局收发室"
端口 465SSL 加密通道,国内邮箱首选
端口 587STARTTLS 加密通道,国际邮箱首选
端口 25已被云厂商封禁,不要使用

1.3. 前后端分离架构下的邮件设计哲学

在 Vue + Spring Boot 的分离架构下,一个常见的错误想法是:“既然前端负责页面,那邮件的 HTML 模板也让前端生成,传给后端发送不就行了?”

这是一个严重的安全反模式。

1.3.1. 为什么前端不能生成邮件 HTML

假设我们允许前端传 HTML 给后端发送,恶意用户可以这样攻击:

1
2
3
4
5
// 恶意用户篡改请求
axios.post('/api/email/send', {
to: 'victim@example.com',
html: '<a href="http://钓鱼网站.com">点击领取奖品</a>'
})

后端如果直接把这段 HTML 发出去,就成了钓鱼邮件的帮凶。

正确的职责划分

1
2
3
4
┌─────────────┐      JSON 指令       ┌─────────────────┐      API 调用      ┌─────────────┐
│ Vue 前端 │ ──────────────────▶ │ Spring Boot │ ────────────────▶ │ 邮件服务商 │
│ 只发指令 │ {type, email} │ 生成 HTML │ 或本地 SMTP │ 投递邮件 │
└─────────────┘ └─────────────────┘ └─────────────┘

前端只告诉后端"给这个邮箱发一封注册验证码邮件",具体的 HTML 内容由后端控制。

1.3.2. 两种"去模板化"方案

在前后端分离架构下,后端生成邮件 HTML 有两种主流方式:

方案 A:Java 21 Text Blocks(本地轻量方案)

直接在 Java 代码中用多行字符串拼接 HTML,适合验证码、密码重置等简单邮件。

方案 B:云端 SaaS 模板(企业级方案)

把 HTML 模板托管在 SendGrid、阿里云等服务商后台,后端只传变量 JSON,服务商负责渲染和发送。运营人员改邮件样式无需后端发版。

本教程会依次讲解这两种方案。

1.3.3. 本节小结

原则说明
前端只发指令{type: "REGISTER", email: "xx@xx.com"}
后端控制内容HTML 由后端生成或由云端模板渲染
禁止前端传 HTML防止 XSS 和钓鱼攻击

1.4. 本章总结与核心概念速查

本章建立了邮件服务的基础认知:SMTP 是发邮件的协议,465/587 是可用的加密端口,前后端分离架构下邮件内容必须由后端控制。

速查表

场景推荐方案
本地开发测试QQ 邮箱 SMTP(465 端口)
国际用户产品Gmail SMTP 或 SendGrid
国内生产环境阿里云 DirectMail
简单邮件(验证码)Java 21 Text Blocks
复杂邮件(营销模板)云端 SaaS 模板

第二章. 本地开发环境搭建

本章摘要:动手时间到!本章将从零开始,用 QQ 邮箱的 SMTP 服务发出第一封邮件。这是所有后续内容的基础——在对接云服务之前,先在本地把流程跑通。

环境版本锁定

组件版本说明
JDK21必须使用 21,我们会用到 Text Blocks 和 Record
Spring Boot3.3.x当前最新稳定版
Maven3.9+构建工具
IDEIntelliJ IDEA 2024.1+社区版即可

本章学习路径

阶段目标解锁能力
2.1创建项目并引入依赖拥有一个可运行的 Spring Boot 骨架
2.2配置 QQ 邮箱 SMTP能解释 application.yml 中每个参数的作用
2.3编写邮件发送服务能发送纯文本和 HTML 格式的邮件
2.4暴露测试接口通过 HTTP 请求触发邮件发送

2.1. 项目初始化与依赖引入

2.1.1. 创建 Spring Boot 项目

步骤 1:打开 Spring Initializr

在浏览器中访问 https://start.spring.io/,按以下配置填写:

  • Project:Maven
  • Language:Java
  • Spring Boot:3.3.x(选择最新的 3.3 稳定版)
  • Groupcom.example
  • Artifactmail-demo
  • Packaging:Jar
  • Java:21

步骤 2:添加依赖

在右侧 “Dependencies” 区域,点击 “ADD DEPENDENCIES”,搜索并添加:

  • Spring Web:提供 REST 接口能力
  • Java Mail Sender:邮件发送核心包

步骤 3:生成并导入项目

点击 “GENERATE” 下载压缩包,解压后用 IDEA 打开。等待 Maven 依赖下载完成(观察右下角进度条)。

成功标志pom.xml 中无红色报错,且包含以下依赖:

1
2
3
4
5
6
7
8
9
10
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
</dependencies>

2.1.2. 本节小结

完成项状态
创建 Spring Boot 3.3 项目
引入 spring-boot-starter-mail
Maven 依赖下载成功

2.2. QQ 邮箱 SMTP 配置实战

在写代码之前,我们需要先获取 QQ 邮箱的"授权码"——这是 SMTP 服务的专用密码,和你的 QQ 登录密码不同。

2.2.1. 获取 QQ 邮箱授权码

步骤 1:登录 QQ 邮箱

在浏览器中打开 https://mail.qq.com/,登录你的 QQ 邮箱。

步骤 2:进入设置页面

点击页面顶部的 “设置” → “账户”。

步骤 3:开启 SMTP 服务

向下滚动找到 “POP3/IMAP/SMTP/Exchange/CardDAV/CalDAV 服务” 区域,点击 “IMAP/SMTP服务” 右侧的 “开启”。

步骤 4:验证并获取授权码

按提示用密保手机发送短信验证。验证成功后,页面会显示一个 16 位授权码(类似 abcdefghijklmnop)。

重要:这个授权码只显示一次!请立即复制保存。如果忘记了,需要重新生成。

成功标志:你手上有一个 16 位的授权码字符串。

2.2.2. 编写 application.yml 配置

src/main/resources/ 目录下,将 application.properties 重命名为 application.yml(YAML 格式更易读),然后填入以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
spring:
mail:
# SMTP 服务器地址
host: smtp.qq.com
# 端口号:QQ 邮箱强制使用 465(SSL 加密)
port: 465
# 你的 QQ 邮箱地址
username: 你的QQ号@qq.com
# 授权码(不是 QQ 密码!)
password: 你的16位授权码
# 默认编码
default-encoding: UTF-8
properties:
mail:
smtp:
# 开启身份认证
auth: true
# 开启 SSL 加密(465 端口必须开启)
ssl:
enable: true
# 显式指定 SSL Socket 工厂,防止某些 JDK 版本的兼容问题
socketFactory:
class: javax.net.ssl.SSLSocketFactory

配置项解读

配置项作用不配会怎样
host指定 SMTP 服务器地址连接失败,不知道往哪发
port指定端口号默认 25,但会被云服务器封禁
usernameSMTP 认证账号535 认证失败
passwordSMTP 认证密码(授权码)535 认证失败
ssl.enable开启 SSL 加密465 端口连接超时

2.2.3. 本节小结

完成项状态
获取 QQ 邮箱授权码
配置 application.yml
理解每个配置项的作用

2.3. 编写邮件发送服务

配置完成后,我们来编写核心的邮件发送逻辑。

2.3.1. 创建项目目录结构

src/main/java/com/example/maildemo/ 下创建以下包结构:

1
2
3
4
5
6
src/main/java/com/example/maildemo/
├── MailDemoApplication.java # 启动类(已存在)
├── service/
│ └── EmailService.java # [新建] 邮件发送服务
└── controller/
└── EmailController.java # [新建] 测试接口

2.3.2. 编写 EmailService

我们先实现两个方法:发送纯文本邮件和发送 HTML 邮件。

📄 文件src/main/java/com/example/maildemo/service/EmailService.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
package com.example.maildemo.service;

import jakarta.mail.internet.MimeMessage;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Service;

/**
* 邮件发送服务
* 提供纯文本邮件和 HTML 邮件两种发送方式
*/
@Service
public class EmailService {

// Spring Boot 自动注入的邮件发送器
private final JavaMailSender mailSender;

// 发件人地址(必须和 application.yml 中的 username 一致)
private static final String FROM_EMAIL = "你的QQ号@qq.com";

// 构造器注入(推荐方式,比 @Autowired 字段注入更利于测试)
public EmailService(JavaMailSender mailSender) {
this.mailSender = mailSender;
}

/**
* 发送纯文本邮件
* 适用场景:简单通知、测试
*
* @param to 收件人邮箱
* @param subject 邮件主题
* @param content 邮件正文(纯文本)
*/
public void sendTextEmail(String to, String subject, String content) {
// SimpleMailMessage 用于发送纯文本邮件
SimpleMailMessage message = new SimpleMailMessage();
message.setFrom(FROM_EMAIL);
message.setTo(to);
message.setSubject(subject);
message.setText(content);

mailSender.send(message);
}

/**
* 发送 HTML 格式邮件
* 适用场景:验证码、营销邮件、带样式的通知
*
* @param to 收件人邮箱
* @param subject 邮件主题
* @param html 邮件正文(HTML 格式)
*/
public void sendHtmlEmail(String to, String subject, String html) {
try {
// MimeMessage 支持 HTML 和附件
MimeMessage message = mailSender.createMimeMessage();
// MimeMessageHelper 简化了 MimeMessage 的操作
// 第二个参数 true 表示支持多部分内容(HTML + 附件)
MimeMessageHelper helper = new MimeMessageHelper(message, true);

helper.setFrom(FROM_EMAIL);
helper.setTo(to);
helper.setSubject(subject);
// 第二个参数 true 表示内容是 HTML 格式
helper.setText(html, true);

mailSender.send(message);
} catch (Exception e) {
// 生产环境应使用日志框架记录,这里简化处理
throw new RuntimeException("邮件发送失败: " + e.getMessage(), e);
}
}

/**
* 发送验证码邮件(HTML 格式)
* 使用 Java 21 Text Blocks 构建邮件模板
*
* @param to 收件人邮箱
* @param username 用户名
* @param code 验证码
*/
public void sendVerificationCode(String to, String username, String code) {
// Java 21 Text Blocks:用三引号包裹多行字符串
// 比传统的字符串拼接更清晰、更易维护
String html = """
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px;">
<h2 style="color: #333;">欢迎注册</h2>
<p>亲爱的 <strong>%s</strong>,</p>
<p>您的验证码是:</p>
<div style="background: #f5f5f5; padding: 15px; text-align: center; font-size: 24px; letter-spacing: 5px; font-weight: bold; border-radius: 5px; margin: 20px 0;">
%s
</div>
<p style="color: #888; font-size: 12px;">验证码 10 分钟内有效,请勿泄露给他人。</p>
<hr style="border: none; border-top: 1px solid #eee; margin: 20px 0;">
<p style="color: #888; font-size: 12px;">此邮件由系统自动发送,请勿回复。</p>
</div>
""".formatted(username, code);

sendHtmlEmail(to, "【验证码】您的注册验证码", html);
}
}

代码要点解读

  1. SimpleMailMessage vs MimeMessage:前者只能发纯文本,后者支持 HTML 和附件
  2. FROM_EMAIL 必须一致:发件人地址必须和 application.yml 中的 username 完全相同,否则会报 553 错误
  3. Text Blocks:Java 21 的多行字符串语法,用 """ 包裹,.formatted() 填充变量

2.3.3. 本节小结

完成项状态
创建 EmailService
实现纯文本邮件发送
实现 HTML 邮件发送
使用 Text Blocks 构建验证码模板

2.4. 编写测试接口验证成果

最后一步:暴露一个 HTTP 接口,让我们能通过浏览器或 Postman 触发邮件发送。

2.4.1. 创建请求 DTO

📄 文件src/main/java/com/example/maildemo/controller/EmailRequest.java

1
2
3
4
5
6
7
8
9
10
11
package com.example.maildemo.controller;

/**
* 邮件发送请求参数
* 使用 Java 17+ Record 语法,自动生成 getter、equals、hashCode、toString
*/
public record EmailRequest(
String to, // 收件人邮箱
String username, // 用户名(用于验证码邮件)
String code // 验证码
) {}

2.4.2. 创建 EmailController

📄 文件src/main/java/com/example/maildemo/controller/EmailController.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package com.example.maildemo.controller;

import com.example.maildemo.service.EmailService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Map;

@RestController
@RequestMapping("/api/email")
public class EmailController {

private final EmailService emailService;

public EmailController(EmailService emailService) {
this.emailService = emailService;
}

/**
* 发送验证码邮件
* POST /api/email/send-code
*/
@PostMapping("/send-code")
public ResponseEntity<Map<String, String>> sendVerificationCode(@RequestBody EmailRequest request) {
emailService.sendVerificationCode(request.to(), request.username(), request.code());
return ResponseEntity.ok(Map.of("message", "验证码已发送"));
}
}

2.4.3. 启动项目并测试

步骤 1:启动应用

在 IDEA 中运行 MailDemoApplicationmain 方法。

成功标志:控制台输出 Started MailDemoApplication in x.xxx seconds,无报错。

步骤 2:发送测试请求

使用 IDEA 自带的 HTTP Client(或 Postman),发送以下请求:

1
2
3
4
5
6
7
8
POST http://localhost:8080/api/email/send-code
Content-Type: application/json

{
"to": "你的另一个邮箱@example.com",
"username": "测试用户",
"code": "886655"
}

成功标志

  1. 接口返回 {"message": "验证码已发送"}
  2. 收件箱收到一封带有验证码的 HTML 邮件

2.4.4. 本节小结

完成项状态
创建 EmailController
暴露 /api/email/send-code 接口
成功发送并收到测试邮件

2.5. 本章总结与本地方案速查

本章完成了从零到一的本地邮件发送功能。我们使用 QQ 邮箱的 SMTP 服务,通过 JavaMailSender 发送了纯文本和 HTML 格式的邮件。

遇到以下场景时,直接 Copy 下方代码

场景 1:发送纯文本通知

需求:发送一封简单的文字通知邮件

1
2
3
4
5
6
7
8
9
10
11
@Autowired
private JavaMailSender mailSender;

public void sendNotice(String to, String content) {
SimpleMailMessage msg = new SimpleMailMessage();
msg.setFrom("你的邮箱@qq.com");
msg.setTo(to);
msg.setSubject("系统通知");
msg.setText(content);
mailSender.send(msg);
}

场景 2:发送 HTML 验证码邮件

需求:发送带样式的验证码邮件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public void sendCode(String to, String code) {
String html = """
<div style="text-align:center; padding:20px;">
<h2>您的验证码</h2>
<p style="font-size:24px; font-weight:bold;">%s</p>
</div>
""".formatted(code);

MimeMessage msg = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(msg, true);
helper.setFrom("你的邮箱@qq.com");
helper.setTo(to);
helper.setSubject("验证码");
helper.setText(html, true);
mailSender.send(msg);
}