Spring Boot Email 邮件发送完全指南

Spring Boot Email 邮件发送完全指南

一、Spring Email 核心优势

Spring Framework 提供了强大的邮件发送抽象层,主要优势包括:

  • 简化配置:只需几行配置即可连接邮件服务器
  • 模板支持:集成 Thymeleaf、Freemarker 等模板引擎
  • 多格式支持:文本、HTML、附件、内联资源
  • 异步发送:支持异步发送提升应用响应速度
  • 测试友好:提供 Mock 实现用于单元测试

二、快速开始:5分钟配置

1. 添加依赖

xml 复制代码
<!-- Maven pom.xml -->
<dependencies>
<!-- Spring Boot Mail Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>

<!-- 可选:HTML模板支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

<!-- 可选:异步支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-async</artifactId>
</dependency>
</dependencies>
gradle 复制代码
// Gradle build.gradle
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-mail'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-async'
}

2. 基础配置

yaml 复制代码
# application.yml
spring:
mail:
# SMTP服务器配置
host: smtp.gmail.com
port: 587
username: your-email@gmail.com
password: your-app-password# 注意:不是登录密码,是应用专用密码

# 通用配置
protocol: smtp
default-encoding: UTF-8

# JavaMail配置
properties:
mail:
smtp:
auth: true
starttls:
enable: true# TLS加密
connectiontimeout: 5000# 连接超时(ms)
timeout: 5000# 读写超时(ms)
writetimeout: 5000# 写超时(ms)

# 调试模式(开发环境开启)
debug: true

# 解决中文乱码
mime:
charset: UTF-8

3. 不同邮件服务商配置示例

yaml 复制代码
# Gmail配置
spring:
mail:
host: smtp.gmail.com
port: 587
username: your-email@gmail.com
password: your-app-password# 需要开启两步验证后生成应用密码
properties:
mail:
smtp:
auth: true
starttls:
enable: true

# 腾讯企业邮箱
spring:
mail:
host: smtp.exmail.qq.com
port: 465
username: your-email@your-domain.com
password: your-password
properties:
mail:
smtp:
auth: true
ssl:
enable: true# 使用SSL

# 阿里云企业邮箱
spring:
mail:
host: smtp.mxhichina.com
port: 465
username: your-email@your-domain.com
password: your-password
properties:
mail:
smtp:
auth: true
ssl:
enable: true

# 网易163邮箱
spring:
mail:
host: smtp.163.com
port: 465
username: your-email@163.com
password: your-password
properties:
mail:
smtp:
auth: true
ssl:
enable: true

三、核心API详解

1. JavaMailSender 接口

java 复制代码
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;

@Service
public class EmailService {

@Autowired
private JavaMailSender mailSender;

/**
* 发送简单文本邮件
*/
public void sendSimpleEmail() {
SimpleMailMessage message = new SimpleMailMessage();
message.setFrom("sender@example.com");
message.setTo("recipient@example.com");
message.setCc("cc@example.com");// 抄送
message.setBcc("bcc@example.com"); // 密送
message.setSubject("邮件主题");
message.setText("邮件正文内容");

mailSender.send(message);
}

/**
* 发送HTML邮件
*/
public void sendHtmlEmail() throws MessagingException {
MimeMessage message = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");

helper.setFrom("sender@example.com");
helper.setTo("recipient@example.com");
helper.setSubject("HTML邮件");

// HTML内容
String htmlContent = """
<html>
<body>
<h1 style="color: #4CAF50;">欢迎注册!</h1>
<p>感谢您注册我们的服务。</p>
<p>请点击以下链接激活账户:</p>
<a href="https://example.com/activate?token=abc123">
激活账户
</a>
</body>
</html>
""";

helper.setText(htmlContent, true);// true表示发送HTML

mailSender.send(message);
}
}

四、实战案例:6种常见邮件场景

案例1:用户注册验证邮件

java 复制代码
@Service
@Slf4j
public class RegistrationEmailService {

@Autowired
private JavaMailSender mailSender;

@Value("${app.base-url}")
private String baseUrl;

@Async// 异步发送,不阻塞主线程
public void sendVerificationEmail(String toEmail, String username, String verificationToken) {
try {
MimeMessage message = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");

helper.setFrom("noreply@example.com", "系统管理员");
helper.setTo(toEmail);
helper.setSubject("账户激活邮件 - " + username);

// 构建激活链接
String activationLink = baseUrl + "/auth/activate?token=" + verificationToken;

// HTML内容
String htmlContent = buildVerificationEmailHtml(username, activationLink);

helper.setText(htmlContent, true);

mailSender.send(message);
log.info("验证邮件已发送至: {}", toEmail);

} catch (Exception e) {
log.error("发送验证邮件失败: {}", toEmail, e);
// 可以加入重试逻辑或记录到数据库
}
}

private String buildVerificationEmailHtml(String username, String activationLink) {
return """
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>账户激活</title>
<style>
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
.header { background-color: #4CAF50; color: white; padding: 20px; text-align: center; }
.content { padding: 30px; background-color: #f9f9f9; }
.button {
display: inline-block;
padding: 12px 24px;
background-color: #4CAF50;
color: white;
text-decoration: none;
border-radius: 5px;
margin: 20px 0;
}
.footer { margin-top: 30px; padding-top: 20px; border-top: 1px solid #eee; color: #666; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>欢迎注册!</h1>
</div>
<div class="content">
<h2>亲爱的 """ + username + """,</h2>
<p>感谢您注册我们的服务。请点击下面的按钮激活您的账户:</p>

<div style="text-align: center;">
<a href=\"""" + activationLink + """\" class="button">
激活账户
</a>
</div>

<p>如果按钮无法点击,请复制以下链接到浏览器:</p>
<p style="word-break: break-all; color: #4CAF50;">
""" + activationLink + """
</p>

<p><strong>注意:</strong>此链接将在24小时后失效。</p>
</div>
<div class="footer">
<p>此为系统自动发送邮件,请勿回复。</p>
<p>如有问题,请联系客服:support@example.com</p>
<p>© 2024 公司名称. 保留所有权利。</p>
</div>
</div>
</body>
</html>
""";
}
}

案例2:带附件的报表邮件

java 复制代码
@Service
public class ReportEmailService {

@Autowired
private JavaMailSender mailSender;

@Autowired
private ReportGenerator reportGenerator;

public void sendMonthlyReport(String toEmail, String month) {
try {
MimeMessage message = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");

helper.setFrom("reports@example.com", "报表系统");
helper.setTo(toEmail);
helper.setSubject(month + "月份销售报表");

// 正文内容
String text = "尊敬的经理:\n\n" +
"附件中是" + month + "月份的销售报表,请查收。\n\n" +
"数据摘要:\n" +
"- 总销售额:¥1,234,567\n" +
"- 同比增长:15.6%\n" +
"- 新客户数:234\n\n" +
"祝好!\n报表系统";

helper.setText(text);

// 添加附件1:Excel报表
byte[] excelReport = reportGenerator.generateExcelReport(month);
helper.addAttachment(month + "销售报表.xlsx",
new ByteArrayResource(excelReport));

// 添加附件2:PDF总结
byte[] pdfSummary = reportGenerator.generatePdfSummary(month);
helper.addAttachment(month + "销售总结.pdf",
new ByteArrayResource(pdfSummary));

// 添加图片作为内联资源
ClassPathResource image = new ClassPathResource("static/images/logo.png");
helper.addInline("companyLogo", image);

mailSender.send(message);

} catch (Exception e) {
throw new EmailException("发送报表邮件失败", e);
}
}
}

// 报表生成器示例
@Component
public class ReportGenerator {

public byte[] generateExcelReport(String month) {
try (Workbook workbook = new XSSFWorkbook()) {
Sheet sheet = workbook.createSheet("销售数据");
// 填充Excel数据...
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
workbook.write(outputStream);
return outputStream.toByteArray();
} catch (Exception e) {
throw new RuntimeException("生成Excel报表失败", e);
}
}
}

案例3:密码重置邮件

java 复制代码
@Service
public class PasswordResetService {

@Autowired
private JavaMailSender mailSender;

@Autowired
private PasswordEncoder passwordEncoder;

@Value("${app.reset-password-expire-hours}")
private int expireHours;

public void sendPasswordResetEmail(String email, String resetToken) {
try {
MimeMessage message = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");

helper.setFrom("security@example.com", "安全中心");
helper.setTo(email);
helper.setSubject("密码重置请求");

// 构建重置链接(带过期时间)
String resetLink = buildResetLink(resetToken);

// 使用Thymeleaf模板
Context context = new Context();
context.setVariable("resetLink", resetLink);
context.setVariable("expireHours", expireHours);
context.setVariable("userEmail", email);

String htmlContent = templateEngine.process("email/password-reset", context);
helper.setText(htmlContent, true);

mailSender.send(message);

} catch (Exception e) {
log.error("发送密码重置邮件失败: {}", email, e);
throw new BusinessException("邮件发送失败");
}
}

private String buildResetLink(String token) {
return String.format("%s/auth/reset-password?token=%s",
baseUrl, URLEncoder.encode(token, StandardCharsets.UTF_8));
}
}

// Thymeleaf模板 resources/templates/email/password-reset.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>密码重置</title>
<style>
/* 样式同上,略 */
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>密码重置</h1>
</div>
<div class="content">
<p>您收到了这封邮件是因为您请求重置密码。</p>
<p>请点击下面的链接重置您的密码:</p>

<div style="text-align: center;">
<a th:href="${resetLink}" class="button">
重置密码
</a>
</div>

<p><strong>安全提示:</strong></p>
<ul>
<li>此链接将在 <span th:text="${expireHours}"></span> 小时后失效</li>
<li>如果您没有请求重置密码,请忽略此邮件</li>
<li>为保障账户安全,请勿将链接分享给他人</li>
</ul>
</div>
</div>
</body>
</html>

案例4:批量发送营销邮件

java 复制代码
@Service
@Slf4j
public class MarketingEmailService {

@Autowired
private JavaMailSender mailSender;

@Autowired
private UserRepository userRepository;

@Value("${spring.mail.batch-size:100}")
private int batchSize;

@Value("${spring.mail.delay-between-batches:1000}")
private long delayBetweenBatches;

/**
* 批量发送营销邮件(带限流和错误处理)
*/
@Async
public CompletableFuture<Void> sendBulkMarketingEmails(String campaignId,
String subject,
String content) {
return CompletableFuture.runAsync(() -> {
List<User> subscribers = userRepository.findBySubscribedTrue();
List<List<User>> batches = partitionList(subscribers, batchSize);

int successCount = 0;
int failCount = 0;

for (List<User> batch : batches) {
try {
sendBatchEmails(batch, subject, content);
successCount += batch.size();

// 添加延迟,避免被邮件服务器限制
Thread.sleep(delayBetweenBatches);

} catch (Exception e) {
failCount += batch.size();
log.error("批量发送邮件失败,批次大小: {}", batch.size(), e);
// 记录失败的用户,后续重试
recordFailedUsers(batch, campaignId);
}
}

log.info("营销邮件发送完成: 成功={}, 失败={}, 活动ID={}",
successCount, failCount, campaignId);
});
}

private void sendBatchEmails(List<User> users, String subject, String content) {
for (User user : users) {
try {
sendIndividualEmail(user.getEmail(), user.getName(), subject, content);
} catch (Exception e) {
log.warn("发送给 {} 的邮件失败: {}", user.getEmail(), e.getMessage());
// 继续发送下一个,不中断批量发送
}
}
}

private void sendIndividualEmail(String email, String name, String subject, String content) {
MimeMessage message = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message, "UTF-8");

helper.setFrom("marketing@example.com", "市场部");
helper.setTo(email);
helper.setSubject(subject);

// 个性化内容
String personalizedContent = content.replace("{{name}}", name);
helper.setText(personalizedContent, true);

mailSender.send(message);
}

private <T> List<List<T>> partitionList(List<T> list, int size) {
return IntStream.range(0, (list.size() + size - 1) / size)
.mapToObj(i -> list.subList(i * size, Math.min((i + 1) * size, list.size())))
.collect(Collectors.toList());
}
}

案例5:带跟踪的邮件发送

java 复制代码
@Component
public class TrackableEmailService {

@Autowired
private JavaMailSender mailSender;

@Autowired
private EmailTrackingRepository trackingRepository;

/**
* 发送可追踪的邮件(用于统计打开率、点击率)
*/
public String sendTrackableEmail(String toEmail, String subject, String htmlContent) {
String trackingId = UUID.randomUUID().toString();

try {
// 在HTML中插入追踪像素和链接追踪
String trackedHtml = injectTracking(htmlContent, trackingId);

MimeMessage message = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");

helper.setFrom("newsletter@example.com", "新闻订阅");
helper.setTo(toEmail);
helper.setSubject(subject);
helper.setText(trackedHtml, true);

mailSender.send(message);

// 保存追踪记录
EmailTracking tracking = new EmailTracking();
tracking.setTrackingId(trackingId);
tracking.setRecipient(toEmail);
tracking.setSubject(subject);
tracking.setSentAt(LocalDateTime.now());
trackingRepository.save(tracking);

return trackingId;

} catch (Exception e) {
log.error("发送可追踪邮件失败", e);
throw new EmailException("发送邮件失败");
}
}

private String injectTracking(String originalHtml, String trackingId) {
// 1. 添加打开追踪像素
String trackingPixel = String.format(
"<img src=\"%s/api/email/track/open/%s\" width=\"1\" height=\"1\"/>",
baseUrl, trackingId
);

// 2. 转换所有链接为可追踪链接
String trackedHtml = originalHtml.replaceAll(
"href=\"(https?://[^\"]+)\"",
String.format("href=\"%s/api/email/track/click/%s?url=$1\"", baseUrl, trackingId)
);

// 3. 在邮件末尾添加追踪像素
return trackedHtml + trackingPixel;
}

/**
* 追踪邮件打开
*/
@GetMapping("/track/open/{trackingId}")
public ResponseEntity<byte[]> trackOpen(@PathVariable String trackingId) {
EmailTracking tracking = trackingRepository.findByTrackingId(trackingId);
if (tracking != null && tracking.getOpenedAt() == null) {
tracking.setOpenedAt(LocalDateTime.now());
tracking.setOpenCount(tracking.getOpenCount() + 1);
trackingRepository.save(tracking);
}

// 返回1x1透明GIF
byte[] gif = Base64.getDecoder().decode("R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7");
return ResponseEntity.ok()
.contentType(MediaType.IMAGE_GIF)
.body(gif);
}

/**
* 追踪链接点击
*/
@GetMapping("/track/click/{trackingId}")
public RedirectView trackClick(@PathVariable String trackingId,
@RequestParam String url) {
EmailTracking tracking = trackingRepository.findByTrackingId(trackingId);
if (tracking != null) {
tracking.setClickedAt(LocalDateTime.now());
tracking.setClickCount(tracking.getClickCount() + 1);
trackingRepository.save(tracking);
}

return new RedirectView(url);
}
}

@Entity
@Table(name = "email_tracking")
@Data
public class EmailTracking {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private String trackingId;
private String recipient;
private String subject;

private LocalDateTime sentAt;
private LocalDateTime openedAt;
private LocalDateTime clickedAt;

private Integer openCount = 0;
private Integer clickCount = 0;
}

案例6:邮件队列与重试机制

java 复制代码
@Service
@Slf4j
public class EmailQueueService {

@Autowired
private JavaMailSender mailSender;

@Autowired
private EmailQueueRepository queueRepository;

@Autowired
private RetryTemplate retryTemplate;

/**
* 将邮件加入队列
*/
public void enqueueEmail(EmailRequest request) {
EmailQueueItem item = new EmailQueueItem();
item.setToEmail(request.getToEmail());
item.setSubject(request.getSubject());
item.setContent(request.getContent());
item.setStatus(EmailStatus.PENDING);
item.setRetryCount(0);
item.setCreatedAt(LocalDateTime.now());

queueRepository.save(item);
}

/**
* 处理队列中的邮件(定时任务调用)
*/
@Scheduled(fixedDelay = 60000) // 每分钟执行一次
public void processEmailQueue() {
List<EmailQueueItem> pendingEmails = queueRepository.findByStatusAndRetryCountLessThan(
EmailStatus.PENDING, 3);

for (EmailQueueItem email : pendingEmails) {
try {
sendWithRetry(email);
email.setStatus(EmailStatus.SENT);
email.setSentAt(LocalDateTime.now());
} catch (Exception e) {
email.setRetryCount(email.getRetryCount() + 1);
email.setLastError(e.getMessage());

if (email.getRetryCount() >= 3) {
email.setStatus(EmailStatus.FAILED);
}
}

queueRepository.save(email);
}
}

private void sendWithRetry(EmailQueueItem email) {
retryTemplate.execute(context -> {
try {
sendEmail(email);
return null;
} catch (MailException e) {
log.warn("发送邮件失败,重试次数: {}", context.getRetryCount(), e);
throw e;
}
});
}

private void sendEmail(EmailQueueItem email) throws MessagingException {
MimeMessage message = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");

helper.setFrom("system@example.com");
helper.setTo(email.getToEmail());
helper.setSubject(email.getSubject());
helper.setText(email.getContent(), true);

mailSender.send(message);
}

@Bean
public RetryTemplate retryTemplate() {
RetryTemplate retryTemplate = new RetryTemplate();

// 指数退避策略:初始1000ms,最大10000ms,乘数2
ExponentialBackOffPolicy backOffPolicy = new ExponentialBackOffPolicy();
backOffPolicy.setInitialInterval(1000);
backOffPolicy.setMaxInterval(10000);
backOffPolicy.setMultiplier(2);

// 简单重试策略:最多重试3次
SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy();
retryPolicy.setMaxAttempts(3);

retryTemplate.setBackOffPolicy(backOffPolicy);
retryTemplate.setRetryPolicy(retryPolicy);

return retryTemplate;
}
}

@Entity
@Table(name = "email_queue")
@Data
public class EmailQueueItem {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private String toEmail;
private String subject;

@Column(columnDefinition = "TEXT")
private String content;

@Enumerated(EnumType.STRING)
private EmailStatus status;

private Integer retryCount = 0;
private String lastError;

private LocalDateTime createdAt;
private LocalDateTime sentAt;
}

public enum EmailStatus {
PENDING,// 等待发送
SENT,// 发送成功
FAILED// 发送失败
}

五、高级配置与优化

1. 多邮件服务器配置

java 复制代码
@Configuration
public class MultipleMailConfig {

@Bean
@Primary
@ConfigurationProperties(prefix = "spring.mail.primary")
public JavaMailSender primaryMailSender() {
return new JavaMailSenderImpl();
}

@Bean
@ConfigurationProperties(prefix = "spring.mail.secondary")
public JavaMailSender secondaryMailSender() {
return new JavaMailSenderImpl();
}

@Bean
public EmailRouter emailRouter() {
return new EmailRouter(primaryMailSender(), secondaryMailSender());
}
}

@Component
public class EmailRouter {
private final JavaMailSender primarySender;
private final JavaMailSender secondarySender;
private final AtomicInteger counter = new AtomicInteger(0);

public EmailRouter(JavaMailSender primarySender, JavaMailSender secondarySender) {
this.primarySender = primarySender;
this.secondarySender = secondarySender;
}

public JavaMailSender getSender() {
// 简单轮询负载均衡
int index = counter.getAndIncrement() % 2;
return index == 0 ? primarySender : secondarySender;
}
}

// application.yml配置
spring:
mail:
primary:
host: smtp.gmail.com
port: 587
username: primary@gmail.com
password: password1
properties:
mail:
smtp:
auth: true
starttls:
enable: true

secondary:
host: smtp.office365.com
port: 587
username: secondary@outlook.com
password: password2
properties:
mail:
smtp:
auth: true
starttls:
enable: true

2. 邮件发送频率限制

java 复制代码
@Component
public class RateLimitedEmailService {

private final RateLimiter rateLimiter;
private final JavaMailSender mailSender;

public RateLimitedEmailService(JavaMailSender mailSender) {
this.mailSender = mailSender;
// 限制每秒最多发送10封邮件
this.rateLimiter = RateLimiter.create(10.0);
}

public void sendEmailWithRateLimit(String to, String subject, String content) {
// 获取令牌,如果没有可用令牌则等待
rateLimiter.acquire();

try {
sendEmail(to, subject, content);
} catch (Exception e) {
log.error("发送邮件失败", e);
// 可以考虑重试逻辑
}
}

private void sendEmail(String to, String subject, String content) {
SimpleMailMessage message = new SimpleMailMessage();
message.setTo(to);
message.setSubject(subject);
message.setText(content);
mailSender.send(message);
}
}

3. 邮件模板引擎集成

java 复制代码
@Configuration
public class EmailTemplateConfig {

@Bean
public SpringTemplateEngine templateEngine() {
SpringTemplateEngine templateEngine = new SpringTemplateEngine();
templateEngine.setTemplateResolver(templateResolver());
templateEngine.addDialect(new Java8TimeDialect());
return templateEngine;
}

private ITemplateResolver templateResolver() {
ClassLoaderTemplateResolver templateResolver = new ClassLoaderTemplateResolver();
templateResolver.setPrefix("templates/email/");
templateResolver.setSuffix(".html");
templateResolver.setTemplateMode(TemplateMode.HTML);
templateResolver.setCharacterEncoding("UTF-8");
templateResolver.setCacheable(false); // 开发时关闭缓存
return templateResolver;
}
}

@Service
public class TemplateEmailService {

@Autowired
private SpringTemplateEngine templateEngine;

@Autowired
private JavaMailSender mailSender;

public void sendTemplatedEmail(String toEmail, String templateName,
Map<String, Object> variables) {
try {
// 渲染模板
Context context = new Context();
context.setVariables(variables);
String htmlContent = templateEngine.process(templateName, context);

// 发送邮件
MimeMessage message = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");

helper.setFrom("noreply@example.com");
helper.setTo(toEmail);
helper.setSubject(variables.getOrDefault("subject", "通知").toString());
helper.setText(htmlContent, true);

mailSender.send(message);

} catch (Exception e) {
throw new EmailException("发送模板邮件失败", e);
}
}

// 使用示例
public void sendWelcomeEmail(User user) {
Map<String, Object> variables = new HashMap<>();
variables.put("user", user);
variables.put("subject", "欢迎加入我们!");
variables.put("welcomeMessage", "感谢您注册我们的服务。");
variables.put("activationLink", generateActivationLink(user));

sendTemplatedEmail(user.getEmail(), "welcome-email", variables);
}
}

六、测试与调试

1. 单元测试

java 复制代码
@SpringBootTest
@ExtendWith(MockitoExtension.class)
class EmailServiceTest {

@MockBean
private JavaMailSender mailSender;

@Autowired
private EmailService emailService;

@Test
void testSendSimpleEmail() {
// 准备
String to = "test@example.com";
String subject = "测试邮件";
String content = "测试内容";

// 执行
emailService.sendSimpleEmail(to, subject, content);

// 验证
ArgumentCaptor<SimpleMailMessage> messageCaptor =
ArgumentCaptor.forClass(SimpleMailMessage.class);

verify(mailSender).send(messageCaptor.capture());

SimpleMailMessage sentMessage = messageCaptor.getValue();
assertEquals(to, sentMessage.getTo()[0]);
assertEquals(subject, sentMessage.getSubject());
assertEquals(content, sentMessage.getText());
}

@Test
void testSendEmailWithAttachment() throws Exception {
// 模拟MimeMessage
MimeMessage mimeMessage = mock(MimeMessage.class);
when(mailSender.createMimeMessage()).thenReturn(mimeMessage);

// 执行
emailService.sendEmailWithAttachment(
"test@example.com",
"带附件的邮件",
"正文内容",
new File("test.pdf")
);

// 验证
verify(mailSender).send(mimeMessage);
}
}

2. 集成测试(使用GreenMail)

xml 复制代码
<!-- 添加GreenMail依赖 -->
<dependency>
<groupId>com.icegreen</groupId>
<artifactId>greenmail</artifactId>
<version>1.6.9</version>
<scope>test</scope>
</dependency>
java 复制代码
@SpringBootTest
@Testcontainers
class EmailIntegrationTest {

@Container
private static final GreenMailContainer greenMail =
new GreenMailContainer(ServerSetupTest.SMTP)
.withConfiguration(
GreenMailConfiguration.aConfig()
.withUser("test", "password")
);

@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.mail.host", greenMail::getSmtp);
registry.add("spring.mail.port", greenMail::getSmtpPort);
registry.add("spring.mail.username", () -> "test");
registry.add("spring.mail.password", () -> "password");
}

@Test
void testSendEmail() throws Exception {
// 发送邮件
emailService.sendSimpleEmail("recipient@example.com", "主题", "内容");

// 等待邮件到达
boolean received = greenMail.waitForIncomingEmail(5000, 1);
assertTrue(received);

// 验证邮件内容
MimeMessage[] messages = greenMail.getReceivedMessages();
assertEquals(1, messages.length);
assertEquals("主题", messages.getSubject());
}
}

3. 邮件预览(开发环境)

java 复制代码
@Profile("dev")
@Configuration
public class DevEmailConfig {

@Bean
public JavaMailSender mockMailSender() {
return new JavaMailSender() {
@Override
public MimeMessage createMimeMessage() {
return new MimeMessage((Session) null);
}

@Override
public void send(MimeMessage mimeMessage) throws MailException {
// 不真正发送,只记录日志
try {
log.info("【开发环境】模拟发送邮件:");
log.info("收件人: {}", Arrays.toString(mimeMessage.getAllRecipients()));
log.info("主题: {}", mimeMessage.getSubject());
log.info("内容预览: {}", getTextFromMessage(mimeMessage));
} catch (Exception e) {
log.warn("解析邮件失败", e);
}
}

private String getTextFromMessage(MimeMessage message) throws Exception {
if (message.isMimeType("text/plain")) {
return message.getContent().toString();
} else if (message.isMimeType("multipart/*")) {
MimeMultipart mimeMultipart = (MimeMultipart) message.getContent();
return getTextFromMimeMultipart(mimeMultipart);
}
return "[无法解析的内容]";
}
};
}
}

七、常见问题与解决方案

问题1:发送超时或连接失败

yaml 复制代码
# 调整超时配置
spring:
mail:
properties:
mail:
smtp:
connectiontimeout: 10000# 连接超时10秒
timeout: 10000# 读取超时10秒
writetimeout: 10000# 写入超时10秒
connectionpool:
enabled: true# 启用连接池
maxsize: 10# 连接池大小
timeout: 5000# 从连接池获取连接的超时时间

问题2:中文乱码

java 复制代码
@Configuration
public class MailConfig {

@Bean
public JavaMailSender javaMailSender() {
JavaMailSenderImpl mailSender = new JavaMailSenderImpl();
mailSender.setHost("smtp.gmail.com");
mailSender.setPort(587);
mailSender.setUsername("your-email@gmail.com");
mailSender.setPassword("your-password");

Properties props = mailSender.getJavaMailProperties();
props.put("mail.transport.protocol", "smtp");
props.put("mail.smtp.auth", "true");
props.put("mail.smtp.starttls.enable", "true");
props.put("mail.debug", "true");

// 解决中文乱码
props.put("mail.mime.charset", "UTF-8");
mailSender.setDefaultEncoding("UTF-8");

return mailSender;
}
}

问题3:被邮件服务器拒绝

java 复制代码
@Service
public class SmartEmailSender {

@Autowired
private JavaMailSender mailSender;

/**
* 智能发送邮件,处理常见拒绝原因
*/
public void sendSmartEmail(String to, String subject, String content) {
try {
// 1. 验证收件人格式
validateEmail(to);

// 2. 检查主题和内容(避免被识别为垃圾邮件)
validateContent(subject, content);

// 3. 设置合适的发件人
MimeMessage message = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");

helper.setFrom(new InternetAddress("no-reply@yourdomain.com", "系统通知", "UTF-8"));
helper.setTo(to);
helper.setSubject(subject);
helper.setText(content, true);

// 4. 添加必要的邮件头(减少被标记为垃圾邮件的概率)
message.addHeader("Precedence", "bulk");
message.addHeader("X-Priority", "3");
message.addHeader("X-Mailer", "YourApp Mailer");
message.addHeader("X-Auto-Response-Suppress", "All");

mailSender.send(message);

} catch (MailAuthenticationException e) {
log.error("邮件认证失败,请检查用户名密码配置", e);
throw new EmailException("邮件服务配置错误");
} catch (MailSendException e) {
log.error("邮件发送失败,可能被服务器拒绝", e);
handleSendFailure(to, subject, e);
} catch (Exception e) {
log.error("发送邮件时发生未知错误", e);
throw new EmailException("邮件发送失败");
}
}

private void validateEmail(String email) {
if (!email.matches("^[A-Za-z0-9+_.-]+@(.+)$")) {
throw new IllegalArgumentException("邮箱格式不正确: " + email);
}
}

private void validateContent(String subject, String content) {
// 避免垃圾邮件关键词
String[] spamKeywords = {"免费", "赢取", "大奖", "立即购买", "限时优惠"};
for (String keyword : spamKeywords) {
if (subject.contains(keyword) || content.contains(keyword)) {
log.warn("邮件内容可能被识别为垃圾邮件,包含关键词: {}", keyword);
}
}
}
}

八、最佳实践总结

  1. 安全性
  • 使用环境变量存储敏感信息(密码、密钥)
  • 启用 TLS/SSL 加密
  • 定期更换应用专用密码
  1. 可靠性
  • 实现邮件队列和重试机制
  • 记录邮件发送日志
  • 监控邮件发送成功率
  1. 性能
  • 使用异步发送避免阻塞主线程
  • 合理设置连接池大小
  • 批量发送时添加延迟
  1. 可维护性
  • 使用模板引擎分离邮件内容和样式
  • 统一异常处理
  • 提供邮件预览功能(开发环境)
  1. 监控
  • 监控邮件发送成功率、失败率
  • 设置告警机制
  • 定期清理过期邮件记录

通过以上配置和案例,您应该能够处理大多数邮件发送需求。记住,邮件服务的关键在于可靠性用户体验,确保邮件能够及时、准确地送达用户邮箱。

相关推荐
sheji34162 小时前
【开题答辩全过程】以 基于Springboot的体检中心信息管理系统设计与实现为例,包含答辩的问题和答案
java·spring boot·后端
天河归来2 小时前
本地windows环境升级dify到1.11.1版本
java·spring boot·docker
超级种码2 小时前
Java:JavaAgent技术(java.instrument和java.attach)
java·开发语言·python
天天向上10243 小时前
go 配置热更新
开发语言·后端·golang
甜鲸鱼3 小时前
【Spring AOP】操作日志的完整实现与原理剖析
java·spring boot·spring
狗头大军之江苏分军3 小时前
年底科技大考:2025 中国前端工程师的 AI 辅助工具实战盘点
java·前端·后端
一 乐3 小时前
酒店客房预订|基于springboot + vue酒店客房预订系统(源码+数据库+文档)
java·数据库·vue.js·spring boot·后端
计算机毕设指导63 小时前
基于Spring Boot的防诈骗管理系统【源码文末联系】
java·spring boot·后端·spring·tomcat·maven·intellij-idea
开心就好20253 小时前
IOScer 开发环境证书包括哪些,证书、描述文件与 App ID 的协同管理实践
后端