本文基于 Spring Boot 2.x + JDK 1.8 + Jakarta Mail 1.6.2,提供生产环境可用的完整邮件发送解决方案,涵盖附件发送、内嵌图片、中文乱码处理等核心场景。
一、技术选型与依赖配置
1.1 Maven 依赖配置(JDK 1.8 兼容)
xml
<?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>com.xxx</groupId>
<artifactId>springboot-email-demo</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<name>springboot-email-demo</name>
<description>Spring Boot 邮件发送完整解决方案</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.18</version>
<relativePath/>
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!-- Spring Boot Mail Starter(Jakarta Mail 1.6.2,JDK 1.8 兼容) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
<!-- Spring Boot Web(可选,用于提供接口) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Lombok(简化代码,可选) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- Spring Boot Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
重要说明:
- Spring Boot 2.7.18 内置 Jakarta Mail 1.6.2,完美兼容 JDK 1.8
- 如果使用 Spring Boot 3.x,需要 JDK 17+,且邮件API包名从
javax.mail变为jakarta.mail
1.2 application.yml 配置
yaml
spring:
mail:
# SMTP服务器配置
host: smtp.qq.com
port: 587
username: your_email@qq.com
password: your_smtp_authorization_code # 注意:是授权码,不是邮箱密码
# 编码配置(根治中文乱码)
default-encoding: UTF-8
# 协议配置
protocol: smtp
# TLS加密配置
properties:
mail:
smtp:
auth: true
starttls:
enable: true
required: true
ssl:
protocols: TLSv1.2
connectiontimeout: 10000
timeout: 10000
writetimeout: 10000
# 服务器端口
server:
port: 8080
常用SMTP服务器配置参考:
| 邮箱服务商 | SMTP地址 | 端口 | 认证方式 |
|---|---|---|---|
| QQ邮箱 | smtp.qq.com | 587 (TLS) / 465 (SSL) | 授权码 |
| 163邮箱 | smtp.163.com | 465 (SSL) / 25 | 授权码 |
| Gmail | smtp.gmail.com | 587 (TLS) | 应用专用密码 |
| Outlook | smtp.office365.com | 587 (TLS) | 邮箱密码或应用密码 |
二、核心工具类实现
2.1 邮件发送工具类
java
package com.xxx.email.util;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Component;
import javax.activation.DataSource;
import javax.activation.FileDataSource;
import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimeUtility;
import java.io.File;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.List;
/**
* 邮件发送工具类
* <p>
* 支持功能:
* 1. 纯文本邮件发送
* 2. HTML邮件发送
* 3. 带附件邮件发送(支持中文附件名)
* 4. 内嵌图片邮件发送(图片显示在正文中)
* 5. 混合场景:同时包含内嵌图片和附件
* </p>
*
* @author auth
* @since 2026-02-13
*/
@Slf4j
@Component
public class EmailUtil {
@Autowired
private JavaMailSender mailSender;
/**
* 发送纯文本邮件
*
* @param from 发件人邮箱
* @param to 收件人邮箱(支持多个,用逗号分隔)
* @param subject 邮件主题
* @param content 邮件内容
*/
public void sendSimpleEmail(String from, String to, String subject, String content) {
try {
SimpleMailMessage message = new SimpleMailMessage();
message.setFrom(from);
message.setTo(to.split(","));
message.setSubject(subject);
message.setText(content);
mailSender.send(message);
log.info("纯文本邮件发送成功!收件人:{}, 主题:{}", to, subject);
} catch (Exception e) {
log.error("纯文本邮件发送失败!收件人:{}, 主题:{}, 错误信息:{}", to, subject, e.getMessage(), e);
throw new RuntimeException("邮件发送失败:" + e.getMessage(), e);
}
}
/**
* 发送纯文本带附件邮件
* @param from 发件人邮箱
* @param to 收件人邮箱(支持多个,用逗号分隔)
* @param subject 邮件主题
* @param content 邮件主题
* @param attachments 附件文件列表
*/
public void sendSimpleEmailAttachments(String from, String to, String subject, String content, List<File> attachments) {
try {
MimeMessage message = mailSender.createMimeMessage();
// 第二个参数true表示创建multipart消息
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
helper.setFrom(from);
helper.setTo(to.split(","));
helper.setSubject(subject);
helper.setText(content);
// 添加附件(自动处理中文文件名)
if (attachments != null && !attachments.isEmpty()) {
for (File file : attachments) {
if (file != null && file.exists()) {
helper.addAttachment(encodeFileName(file.getName()), file);
log.debug("添加附件:{}", file.getName());
} else {
log.warn("附件文件不存在,跳过:{}", file != null ? file.getName() : "null");
}
}
}
mailSender.send(message);
log.info("纯文本带附件邮件发送成功!收件人:{}, 主题:{}, 附件数量:{}", to, subject,
attachments != null ? attachments.size() : 0);
} catch (Exception e) {
log.error("纯文本带附件邮件发送失败!收件人:{}, 主题:{}, 错误信息:{}", to, subject, e.getMessage(), e);
throw new RuntimeException("邮件发送失败:" + e.getMessage(), e);
}
}
/**
* 发送HTML邮件
*
* @param from 发件人邮箱
* @param to 收件人邮箱
* @param subject 邮件主题
* @param htmlContent HTML格式的邮件内容
*/
public void sendHtmlEmail(String from, String to, String subject, String htmlContent) {
try {
MimeMessage message = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
helper.setFrom(from);
helper.setTo(to.split(","));
helper.setSubject(encodeText(subject));
helper.setText(htmlContent, true);
mailSender.send(message);
log.info("HTML邮件发送成功!收件人:{}, 主题:{}", to, subject);
} catch (Exception e) {
log.error("HTML邮件发送失败!收件人:{}, 主题:{}, 错误信息:{}", to, subject, e.getMessage(), e);
throw new RuntimeException("邮件发送失败:" + e.getMessage(), e);
}
}
/**
* 发送带附件的邮件(支持中文附件名)
*
* @param from 发件人邮箱
* @param to 收件人邮箱
* @param subject 邮件主题
* @param htmlContent HTML格式的邮件内容
* @param attachments 附件文件列表
*/
public void sendEmailWithAttachments(String from, String to, String subject,
String htmlContent, List<File> attachments) {
try {
MimeMessage message = mailSender.createMimeMessage();
// 第二个参数true表示创建multipart消息
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
helper.setFrom(from);
helper.setTo(to.split(","));
helper.setSubject(encodeText(subject));
helper.setText(htmlContent, true);
// 添加附件(自动处理中文文件名)
if (attachments != null && !attachments.isEmpty()) {
for (File file : attachments) {
if (file != null && file.exists()) {
helper.addAttachment(encodeFileName(file.getName()), file);
log.debug("添加附件:{}", file.getName());
} else {
log.warn("附件文件不存在,跳过:{}", file != null ? file.getName() : "null");
}
}
}
mailSender.send(message);
log.info("带附件邮件发送成功!收件人:{}, 主题:{}, 附件数量:{}", to, subject,
attachments != null ? attachments.size() : 0);
} catch (Exception e) {
log.error("带附件邮件发送失败!收件人:{}, 主题:{}, 错误信息:{}", to, subject, e.getMessage(), e);
throw new RuntimeException("邮件发送失败:" + e.getMessage(), e);
}
}
/**
* 发送带内嵌图片的邮件(图片显示在正文中)
*
* @param from 发件人邮箱
* @param to 收件人邮箱
* @param subject 邮件主题
* @param htmlContent HTML格式的邮件内容(通过cid:xxx引用图片)
* @param inlineImages 内嵌图片列表
*/
public void sendEmailWithInlineImages(String from, String to, String subject,
String htmlContent, List<InlineImage> inlineImages) {
try {
MimeMessage message = mailSender.createMimeMessage();
// 必须使用true创建multipart消息
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
helper.setFrom(from);
helper.setTo(to.split(","));
helper.setSubject(encodeText(subject));
helper.setText(htmlContent, true);
// 添加内嵌图片
if (inlineImages != null && !inlineImages.isEmpty()) {
for (InlineImage inlineImage : inlineImages) {
if (inlineImage != null && inlineImage.getFile() != null && inlineImage.getFile().exists()) {
DataSource dataSource = new FileDataSource(inlineImage.getFile());
helper.addInline(inlineImage.getContentId(), dataSource);
log.debug("添加内嵌图片:CID={}, 文件名:{}",
inlineImage.getContentId(), inlineImage.getFile().getName());
} else {
log.warn("内嵌图片文件不存在,跳过:CID={}",
inlineImage != null ? inlineImage.getContentId() : "null");
}
}
}
mailSender.send(message);
log.info("内嵌图片邮件发送成功!收件人:{}, 主题:{}, 图片数量:{}", to, subject,
inlineImages != null ? inlineImages.size() : 0);
} catch (Exception e) {
log.error("内嵌图片邮件发送失败!收件人:{}, 主题:{}, 错误信息:{}", to, subject, e.getMessage(), e);
throw new RuntimeException("邮件发送失败:" + e.getMessage(), e);
}
}
/**
* 发送复杂邮件(同时包含内嵌图片和附件)
*
* @param from 发件人邮箱
* @param to 收件人邮箱
* @param subject 邮件主题
* @param htmlContent HTML格式的邮件内容
* @param inlineImages 内嵌图片列表
* @param attachments 附件文件列表
*/
public void sendComplexEmail(String from, String to, String subject,
String htmlContent, List<InlineImage> inlineImages,
List<File> attachments) {
try {
MimeMessage message = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
helper.setFrom(from);
helper.setTo(to.split(","));
helper.setSubject(encodeText(subject));
helper.setText(htmlContent, true);
// 添加内嵌图片
if (inlineImages != null && !inlineImages.isEmpty()) {
for (InlineImage inlineImage : inlineImages) {
if (inlineImage != null && inlineImage.getFile() != null && inlineImage.getFile().exists()) {
DataSource dataSource = new FileDataSource(inlineImage.getFile());
helper.addInline(inlineImage.getContentId(), dataSource);
log.debug("添加内嵌图片:CID={}, 文件名:{}",
inlineImage.getContentId(), inlineImage.getFile().getName());
}
}
}
// 添加附件
if (attachments != null && !attachments.isEmpty()) {
for (File file : attachments) {
if (file != null && file.exists()) {
helper.addAttachment(encodeFileName(file.getName()), file);
log.debug("添加附件:{}", file.getName());
}
}
}
mailSender.send(message);
log.info("复杂邮件发送成功!收件人:{}, 主题:{}, 图片数量:{}, 附件数量:{}",
to, subject, inlineImages != null ? inlineImages.size() : 0,
attachments != null ? attachments.size() : 0);
} catch (Exception e) {
log.error("复杂邮件发送失败!收件人:{}, 主题:{}, 错误信息:{}", to, subject, e.getMessage(), e);
throw new RuntimeException("邮件发送失败:" + e.getMessage(), e);
}
}
/**
* 编码文本(处理中文乱码)
* 使用RFC 2047标准编码
*
* @param text 原始文本
* @return 编码后的文本
*/
private String encodeText(String text) {
if (text == null || text.isEmpty()) {
return text;
}
try {
// 使用Base64编码(B编码),UTF-8字符集
return MimeUtility.encodeText(text, "UTF-8", "B");
} catch (UnsupportedEncodingException e) {
log.warn("文本编码失败,返回原始文本:{}", text, e);
return text;
}
}
/**
* 编码文件名(处理中文附件名乱码)
*
* @param fileName 原始文件名
* @return 编码后的文件名
*/
private String encodeFileName(String fileName) {
if (fileName == null || fileName.isEmpty()) {
return fileName;
}
try {
// 使用Base64编码(B编码),UTF-8字符集
return MimeUtility.encodeText(fileName, "UTF-8", "B");
} catch (UnsupportedEncodingException e) {
log.warn("文件名编码失败,返回原始文件名:{}", fileName, e);
return fileName;
}
}
/**
* 内嵌图片包装类
*/
public static class InlineImage {
/**
* 图片文件
*/
private File file;
/**
* Content-ID(用于HTML中通过cid:引用)
*/
private String contentId;
public InlineImage(File file, String contentId) {
this.file = file;
this.contentId = contentId;
}
public File getFile() {
return file;
}
public void setFile(File file) {
this.file = file;
}
public String getContentId() {
return contentId;
}
public void setContentId(String contentId) {
this.contentId = contentId;
}
}
}
工具类设计亮点:
- 完整的日志记录:成功/失败均有详细日志,便于排查问题
- 异常处理:所有异常统一捕获并记录,重新抛出为运行时异常
- 中文乱码根治 :通过
MimeUtility.encodeText()统一处理主题和附件名 - 参数校验:对文件存在性、null值进行校验
- 支持多种场景:纯文本、HTML、附件、内嵌图片、混合场景
2.2 Spring Boot 启动类
java
package com.xxx.email;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class EmailApplication {
public static void main(String[] args) {
SpringApplication.run(EmailApplication.class, args);
System.out.println("========================================");
System.out.println("邮件服务启动成功!");
System.out.println("========================================");
}
}
三、使用示例
3.1 测试控制器
java
package com.xxx.email.controller;
import com.example.email.util.EmailUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
/**
* 邮件发送测试控制器
*/
@RestController
@RequestMapping("/email")
public class EmailController {
@Autowired
private EmailUtil emailUtil;
@Value("${spring.mail.username}")
private String fromEmail;
/**
* 测试1:发送纯文本邮件
*/
@GetMapping("/sendSimple")
public String sendSimpleEmail() {
try {
String to = "recipient@example.com";
String subject = "测试纯文本邮件";
String content = "你好!\n\n这是一封测试邮件。\n\n祝好!";
emailUtil.sendSimpleEmail(fromEmail, to, subject, content);
return "纯文本邮件发送成功!";
} catch (Exception e) {
return "发送失败:" + e.getMessage();
}
}
/**
* 测试2:发送HTML邮件
*/
@GetMapping("/sendHtml")
public String sendHtmlEmail() {
try {
String to = "recipient@example.com";
String subject = "测试HTML邮件";
String htmlContent =
"<html>" +
"<head>" +
" <style>" +
" body { font-family: Arial, sans-serif; padding: 20px; }" +
" .header { background: #4CAF50; color: white; padding: 20px; text-align: center; }" +
" .content { margin-top: 20px; line-height: 1.6; }" +
" </style>" +
"</head>" +
"<body>" +
" <div class='header'>" +
" <h1>欢迎使用我们的服务</h1>" +
" </div>" +
" <div class='content'>" +
" <p>亲爱的用户:</p>" +
" <p>这是一封<b>HTML格式</b>的测试邮件。</p>" +
" <p>感谢您的使用!</p>" +
" </div>" +
"</body>" +
"</html>";
emailUtil.sendHtmlEmail(fromEmail, to, subject, htmlContent);
return "HTML邮件发送成功!";
} catch (Exception e) {
return "发送失败:" + e.getMessage();
}
}
/**
* 测试3:发送带附件的邮件(支持中文附件名)
*/
@GetMapping("/sendWithAttachments")
public String sendEmailWithAttachments() {
try {
String to = "recipient@example.com";
String subject = "测试带附件的邮件";
String htmlContent =
"<html>" +
"<body>" +
" <h2>附件测试邮件</h2>" +
" <p>你好!</p>" +
" <p>这是一封带附件的测试邮件,附件包含中文名称。</p>" +
" <p>请查收附件。</p>" +
"</body>" +
"</html>";
// 准备附件(包含中文文件名)
List<File> attachments = new ArrayList<>();
attachments.add(new File("D:/test/中文文档.docx"));
attachments.add(new File("D:/test/数据报表.xlsx"));
emailUtil.sendEmailWithAttachments(fromEmail, to, subject, htmlContent, attachments);
return "带附件邮件发送成功!";
} catch (Exception e) {
return "发送失败:" + e.getMessage();
}
}
/**
* 测试4:发送带内嵌图片的邮件(图片显示在正文中)
*/
@GetMapping("/sendWithInlineImages")
public String sendEmailWithInlineImages() {
try {
String to = "recipient@example.com";
String subject = "测试内嵌图片邮件";
// HTML正文:通过cid:引用图片
String htmlContent =
"<html>" +
"<head>" +
" <style>" +
" body { font-family: Arial, sans-serif; padding: 20px; }" +
" .header { background: #4CAF50; color: white; padding: 20px; text-align: center; }" +
" .content { margin-top: 20px; line-height: 1.6; }" +
" img { max-width: 100%; height: auto; display: block; margin: 20px auto; }" +
" </style>" +
"</head>" +
"<body>" +
" <div class='header'>" +
" <h1>内嵌图片测试</h1>" +
" </div>" +
" <div class='content'>" +
" <p>亲爱的用户:</p>" +
" <p>这是一封带内嵌图片的测试邮件。</p>" +
" <p>以下是公司Logo:</p>" +
" <!-- 通过cid:logo引用内嵌图片 -->" +
" <img src='cid:logo' alt='Logo'>" +
" <p>以下是产品示意图:</p>" +
" <!-- 通过cid:product引用内嵌图片 -->" +
" <img src='cid:product' alt='产品示意图'>" +
" <p>图片直接显示在邮件正文中,无需下载。</p>" +
" </div>" +
"</body>" +
"</html>";
// 准备内嵌图片(File + Content-ID)
List<EmailUtil.InlineImage> inlineImages = new ArrayList<>();
inlineImages.add(new EmailUtil.InlineImage(new File("D:/test/logo.png"), "logo"));
inlineImages.add(new EmailUtil.InlineImage(new File("D:/test/product.png"), "product"));
emailUtil.sendEmailWithInlineImages(fromEmail, to, subject, htmlContent, inlineImages);
return "内嵌图片邮件发送成功!";
} catch (Exception e) {
return "发送失败:" + e.getMessage();
}
}
/**
* 测试5:发送复杂邮件(同时包含内嵌图片和附件)
*/
@GetMapping("/sendComplex")
public String sendComplexEmail() {
try {
String to = "recipient@example.com";
String subject = "测试复杂邮件(内嵌图片 + 附件)";
// HTML正文
String htmlContent =
"<html>" +
"<head>" +
" <style>" +
" body { font-family: Arial, sans-serif; padding: 20px; }" +
" .header { background: #4CAF50; color: white; padding: 20px; text-align: center; }" +
" .content { margin-top: 20px; line-height: 1.6; }" +
" img { max-width: 100%; height: auto; display: block; margin: 20px auto; }" +
" </style>" +
"</head>" +
"<body>" +
" <div class='header'>" +
" <h1>复杂邮件测试</h1>" +
" </div>" +
" <div class='content'>" +
" <p>亲爱的用户:</p>" +
" <p>这封邮件同时包含:</p>" +
" <ul>" +
" <li>内嵌图片(直接显示在正文中)</li>" +
" <li>附件(需要下载查看)</li>" +
" </ul>" +
" <p>以下是内嵌图片:</p>" +
" <img src='cid:logo' alt='Logo'>" +
" <p>请查收附件文件。</p>" +
" </div>" +
"</body>" +
"</html>";
// 内嵌图片
List<EmailUtil.InlineImage> inlineImages = new ArrayList<>();
inlineImages.add(new EmailUtil.InlineImage(new File("D:/test/logo.png"), "logo"));
// 附件
List<File> attachments = new ArrayList<>();
attachments.add(new File("D:/test/中文文档.docx"));
attachments.add(new File("D:/test/数据报表.xlsx"));
emailUtil.sendComplexEmail(fromEmail, to, subject, htmlContent, inlineImages, attachments);
return "复杂邮件发送成功!";
} catch (Exception e) {
return "发送失败:" + e.getMessage();
}
}
}
3.2 单元测试
java
package com.xxx.email;
import com.example.email.util.EmailUtil;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
@SpringBootTest
class EmailApplicationTests {
@Autowired
private EmailUtil emailUtil;
@Value("${spring.mail.username}")
private String fromEmail;
/**
* 测试纯文本邮件
*/
@Test
void testSendSimpleEmail() {
String to = "recipient@example.com";
String subject = "【测试】纯文本邮件";
String content = "你好!\n\n这是一封测试邮件。\n\n祝好!";
emailUtil.sendSimpleEmail(fromEmail, to, subject, content);
}
/**
* 测试HTML邮件
*/
@Test
void testSendHtmlEmail() {
String to = "recipient@example.com";
String subject = "【测试】HTML邮件";
String htmlContent =
"<html>" +
"<body>" +
" <h2>HTML邮件测试</h2>" +
" <p>这是一封<b>HTML格式</b>的测试邮件。</p>" +
"</body>" +
"</html>";
emailUtil.sendHtmlEmail(fromEmail, to, subject, htmlContent);
}
/**
* 测试带附件的邮件
*/
@Test
void testSendEmailWithAttachments() {
String to = "recipient@example.com";
String subject = "【测试】带附件的邮件";
String htmlContent =
"<html>" +
"<body>" +
" <h2>附件测试</h2>" +
" <p>请查收附件。</p>" +
"</body>" +
"</html>";
List<File> attachments = new ArrayList<>();
attachments.add(new File("D:/test/测试文档.txt"));
emailUtil.sendEmailWithAttachments(fromEmail, to, subject, htmlContent, attachments);
}
/**
* 测试内嵌图片邮件
*/
@Test
void testSendEmailWithInlineImages() {
String to = "recipient@example.com";
String subject = "【测试】内嵌图片邮件";
String htmlContent =
"<html>" +
"<body>" +
" <h2>内嵌图片测试</h2>" +
" <img src='cid:test' alt='测试图片'>" +
"</body>" +
"</html>";
List<EmailUtil.InlineImage> inlineImages = new ArrayList<>();
inlineImages.add(new EmailUtil.InlineImage(new File("D:/test/test.png"), "test"));
emailUtil.sendEmailWithInlineImages(fromEmail, to, subject, htmlContent, inlineImages);
}
}
四、关键技术与原理
4.1 中文乱码的根源与解决方案
乱码产生的原因
邮件协议(SMTP)基于文本传输,只支持7位ASCII字符。当传输中文等非ASCII字符时,必须进行编码。
解决方案
| 乱码类型 | 根本原因 | 解决方案 |
|---|---|---|
| 附件文件名乱码 | 文件名未按MIME标准编码(RFC 2047) | 使用 MimeUtility.encodeText(filename, "UTF-8", "B") |
| 邮件正文乱码 | 正文未指定charset或使用了非UTF-8编码 | 在 MimeMessageHelper 构造函数中指定 UTF-8 |
| 主题乱码 | 主题未进行MIME编码 | 使用 MimeUtility.encodeText(subject, "UTF-8", "B") |
编码方式说明
java
MimeUtility.encodeText(text, charset, encoding)
- charset : 必须使用
"UTF-8",确保能处理所有Unicode字符 - encoding :
"B"- Base64编码(推荐,兼容性最好)"Q"- Quoted-Printable编码(适用于ASCII字符较多的场景)
生产环境建议始终使用 "B"(Base64)编码,因为各邮件客户端对Base64的支持最稳定。
4.2 内嵌图片的技术原理
Multipart 模式选择
java
// 错误:使用默认的mixed模式(附件模式)
MimeMultipart multipart = new MimeMultipart();
// 正确:使用related模式(内嵌资源模式)
MimeMultipart multipart = new MimeMultipart("related");
| Multipart类型 | 用途 | 说明 |
|---|---|---|
mixed |
混合模式 | 用于独立的附件(如Word、PDF) |
related |
相关模式 | 用于内嵌资源(图片、CSS、字体) |
alternative |
替代模式 | 用于同一内容的多版本(纯文本 + HTML) |
Content-ID(CID)设置规则
java
// 1. 在Java代码中设置Content-ID
helper.addInline("logo", dataSource);
// 2. 在HTML中引用(注意:必须加上cid:前缀)
<img src="cid:logo" alt="Logo">
重要规则:
- Content-ID 建议使用字母、数字、下划线,避免使用中文或特殊字符
- HTML引用时使用
cid:前缀 - 同一邮件中的CID必须唯一,否则会显示错误或无法加载
4.3 Spring Boot Mail 自动配置原理
Spring Boot 通过 org.springframework.boot.autoconfigure.mail.MailAutoConfiguration 自动配置 JavaMailSender:
- 自动检测配置 :从
application.yml中读取spring.mail.*配置 - 创建 Session :根据配置创建
JavaMailSender - 注入 Bean :将
JavaMailSender注入到Spring容器中
开发者只需:
- 配置
spring.mail.*属性 - 注入
JavaMailSender或JavaMailSenderImpl - 使用
MimeMessageHelper简化邮件构建
五、常见问题与解决方案
5.1 认证失败
问题:
AuthenticationFailedException: 535 Error: authentication failed
原因:
- 使用了邮箱密码而非授权码
- 授权码已过期或被重置
- SMTP服务未开启
解决方案:
- 登录邮箱设置,开启SMTP服务
- 生成新的授权码
- 在
application.yml中使用授权码
获取授权码示例(QQ邮箱):
- 登录QQ邮箱 → 设置 → 账户
- 找到"POP3/IMAP/SMTP/Exchange/CardDAV/CalDAV服务"
- 开启"IMAP/SMTP服务"
- 按提示发送短信获取授权码
5.2 连接超时
问题:
MailConnectException: Couldn't connect to host, port: smtp.qq.com
原因:
- 端口配置错误
- 网络问题
- 防火墙拦截
解决方案:
-
检查端口配置:
- TLS加密:端口 587
- SSL加密:端口 465
-
检查网络连接
-
配置超时时间:
yamlspring: mail: properties: mail: smtp: connectiontimeout: 10000 timeout: 10000 writetimeout: 10000
5.3 附件文件名乱码
问题:附件文件名显示为乱码
解决方案 :
使用 MimeUtility.encodeText() 编码文件名:
java
private String encodeFileName(String fileName) {
try {
return MimeUtility.encodeText(fileName, "UTF-8", "B");
} catch (UnsupportedEncodingException e) {
return fileName;
}
}
5.4 内嵌图片显示为红叉
问题:图片显示为红叉或无法加载
可能原因及解决方案:
| 可能原因 | 解决方案 |
|---|---|
| CID不匹配 | 检查HTML中的 cid: 与Java中的 addInline() 的参数是否一致 |
| 图片文件不存在 | 使用 file.exists() 验证文件是否存在 |
| 图片格式不支持 | 转换为PNG、JPG或GIF |
| 图片过大 | 压缩图片(建议单张不超过500KB) |
5.5 Outlook 中内嵌图片显示为附件
问题:内嵌图片在Outlook中显示为附件而非正文
解决方案:
-
确保设置了正确的Multipart模式
-
设置文件名:
javahelper.addInline("logo", dataSource);
5.6 邮件发送慢
问题:邮件发送耗时较长
解决方案:
- 使用异步发送(推荐):
java
@Service
public class AsyncEmailService {
@Autowired
private EmailUtil emailUtil;
@Async("emailTaskExecutor")
public void sendEmailAsync(String from, String to, String subject, String content) {
emailUtil.sendHtmlEmail(from, to, subject, content);
}
}
- 配置线程池:
java
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean("emailTaskExecutor")
public TaskExecutor emailTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("Email-Async-");
executor.initialize();
return executor;
}
}
六、最佳实践与优化建议
6.1 邮件发送建议
- 控制附件大小:单个附件不超过10MB,总大小不超过25MB(大多数邮箱的限制)
- 图片优化 :
- 使用PNG或JPG格式
- 控制单张图片大小在500KB以内
- 使用合适的分辨率(72-150 DPI)
- HTML规范 :
- 使用内联CSS样式
- 避免使用JavaScript
- 使用table布局提升兼容性
- 多客户端测试:在Gmail、Outlook、QQ邮箱等主流客户端中测试显示效果
6.2 异常处理建议
- 统一异常处理:
java
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public ResponseEntity<String> handleException(Exception e) {
log.error("系统异常", e);
return ResponseEntity.status(500).body("系统异常:" + e.getMessage());
}
}
- 重试机制:对于网络异常,可以添加重试逻辑
6.3 安全建议
- 敏感信息加密 :使用
application-{profile}.yml区分环境,敏感信息加密存储 - 限制发送频率:防止被滥用
- 添加DKIM签名:提升邮件信誉度(可选)
6.4 监控建议
- 记录发送日志:记录发送状态、耗时、失败原因
- 监控指标 :
- 发送成功率
- 平均发送耗时
- 失败原因分布
- 告警机制:发送失败率超过阈值时触发告警
6.5 生产环境配置示例
yaml
spring:
profiles:
active: prod
mail:
host: ${SMTP_HOST:smtp.qq.com}
port: ${SMTP_PORT:587}
username: ${SMTP_USERNAME}
password: ${SMTP_PASSWORD}
default-encoding: UTF-8
properties:
mail:
smtp:
auth: true
starttls:
enable: true
connectiontimeout: 15000
timeout: 15000
writetimeout: 15000
# 日志配置
logging:
level:
org.springframework.mail: DEBUG
file:
name: logs/email-service.log
七、总结
本文提供了一个完整的、生产级可用的 Spring Boot 邮件发送解决方案,核心要点:
- 技术选型:Spring Boot 2.7.18 + Jakarta Mail 1.6.2,兼容 JDK 1.8
- 核心工具类 :
EmailUtil提供了完整的邮件发送功能,包括:- 纯文本邮件
- HTML邮件
- 带附件邮件(支持中文附件名)
- 内嵌图片邮件
- 复杂邮件(内嵌图片 + 附件)
- 中文乱码根治 :使用
MimeUtility.encodeText()统一处理 - 内嵌图片原理:通过 Content-ID 建立 HTML 与图片的映射关系
- 异常处理:完整的日志记录和异常处理机制
- 最佳实践:异步发送、监控告警、安全配置等