Appearance
Spring Email 邮件发送完全指南 📧
概述
在现代企业应用中,邮件发送是一个不可或缺的功能。无论是用户注册确认、订单通知、还是系统告警,邮件都扮演着重要的沟通桥梁角色。Spring Framework 为我们提供了强大而灵活的邮件发送支持,让开发者能够轻松地集成邮件功能到应用中。
NOTE
Spring 的邮件支持基于 Jakarta Mail(原 JavaMail)库,提供了从简单文本邮件到复杂 HTML 邮件的完整解决方案。
为什么需要 Spring Email? 🤔
解决的核心痛点
在没有 Spring Email 支持之前,开发者直接使用 JavaMail API 会遇到以下问题:
kotlin
// 繁琐的配置和异常处理
val props = Properties()
props["mail.smtp.host"] = "smtp.gmail.com"
props["mail.smtp.port"] = "587"
props["mail.smtp.auth"] = "true"
props["mail.smtp.starttls.enable"] = "true"
val session = Session.getInstance(props, object : Authenticator() {
override fun getPasswordAuthentication(): PasswordAuthentication {
return PasswordAuthentication("username", "password")
}
})
try {
val message = MimeMessage(session)
message.setFrom(InternetAddress("[email protected]"))
message.setRecipients(Message.RecipientType.TO, InternetAddress.parse("[email protected]"))
message.subject = "Test Subject"
message.setText("Hello World!")
Transport.send(message)
println("邮件发送成功")
} catch (e: MessagingException) {
e.printStackTrace()
}
kotlin
@Service
class EmailService(
private val mailSender: JavaMailSender
) {
fun sendSimpleEmail(to: String, subject: String, text: String) {
val message = SimpleMailMessage().apply {
setTo(to)
setSubject(subject)
setText(text)
setFrom("[email protected]")
}
try {
mailSender.send(message)
} catch (ex: MailException) {
logger.error("邮件发送失败", ex)
}
}
}
Spring Email 的优势
- 简化配置:通过 Spring Boot 自动配置,大大减少了样板代码
- 异常抽象:提供了统一的
MailException
异常体系 - 模板支持:内置模板消息支持,避免重复代码
- 资源管理:自动处理底层资源的创建和释放
- 测试友好:提供了便于单元测试的抽象接口
核心组件架构 🏗️
核心接口说明
接口/类 | 作用 | 使用场景 |
---|---|---|
MailSender | 基础邮件发送接口 | 简单文本邮件 |
JavaMailSender | 扩展邮件发送接口 | 复杂邮件(HTML、附件等) |
SimpleMailMessage | 简单邮件消息封装 | 纯文本邮件 |
MimeMessageHelper | MIME消息辅助类 | HTML邮件、附件处理 |
依赖配置 📦
首先,需要添加必要的依赖:
kotlin
dependencies {
implementation("org.springframework.boot:spring-boot-starter-mail")
implementation("com.sun.mail:jakarta.mail:2.0.1")
}
xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
<dependency>
<groupId>com.sun.mail</groupId>
<artifactId>jakarta.mail</artifactId>
<version>2.0.1</version>
</dependency>
</dependencies>
IMPORTANT
确保使用 Jakarta Mail 2.x 版本(使用 jakarta.mail
包名),而不是旧版本的 JavaMail 1.6.x(使用 javax.mail
包名)。
基础邮件发送 📝
1. 简单文本邮件
让我们从一个实际的业务场景开始:用户下单后发送确认邮件。
kotlin
// 业务接口定义
interface OrderManager {
fun placeOrder(order: Order)
}
// 订单实体
data class Order(
val orderNumber: String,
val customer: Customer
)
data class Customer(
val firstName: String,
val lastName: String,
val emailAddress: String
)
kotlin
@Service
class SimpleOrderManager(
private val mailSender: MailSender,
private val templateMessage: SimpleMailMessage
) : OrderManager {
override fun placeOrder(order: Order) {
// 执行业务逻辑...
processOrder(order)
// 发送确认邮件
sendOrderConfirmationEmail(order)
}
private fun sendOrderConfirmationEmail(order: Order) {
// 创建线程安全的模板消息副本
val message = SimpleMailMessage(templateMessage).apply {
setTo(order.customer.emailAddress)
text = """
亲爱的 ${order.customer.firstName} ${order.customer.lastName},
感谢您的订购!您的订单号是:${order.orderNumber}
我们会尽快为您处理订单。
此致
客服团队
""".trimIndent()
}
try {
mailSender.send(message)
logger.info("订单确认邮件已发送至: ${order.customer.emailAddress}")
} catch (ex: MailException) {
logger.error("邮件发送失败,订单号: ${order.orderNumber}", ex)
// 可以考虑重试机制或者记录到失败队列
}
}
private fun processOrder(order: Order) {
// 订单处理逻辑...
logger.info("处理订单: ${order.orderNumber}")
}
companion object {
private val logger = LoggerFactory.getLogger(SimpleOrderManager::class.java)
}
}
2. 配置邮件发送器
kotlin
@Configuration
class MailConfiguration {
@Bean
fun mailSender(): JavaMailSender {
return JavaMailSenderImpl().apply {
host = "smtp.gmail.com"
port = 587
username = "[email protected]"
password = "your-app-password"
javaMailProperties = Properties().apply {
put("mail.transport.protocol", "smtp")
put("mail.smtp.auth", "true")
put("mail.smtp.starttls.enable", "true")
put("mail.debug", "false")
}
}
}
@Bean
fun templateMessage(): SimpleMailMessage {
return SimpleMailMessage().apply {
from = "[email protected]"
subject = "订单确认通知"
}
}
}
TIP
使用模板消息可以预设一些通用属性(如发件人、主题前缀等),避免在每次发送时重复设置。
3. 使用 application.yml 配置
yaml
spring:
mail:
host: smtp.gmail.com
port: 587
username: [email protected]
password: your-app-password
properties:
mail:
smtp:
auth: true
starttls:
enable: true
timeout: 8000
connectiontimeout: 8000
writetimeout: 8000
高级邮件功能 🚀
1. 使用 MimeMessagePreparator
对于更复杂的邮件内容,可以使用 MimeMessagePreparator
:
kotlin
@Service
class AdvancedOrderManager(
private val mailSender: JavaMailSender
) : OrderManager {
override fun placeOrder(order: Order) {
processOrder(order)
sendAdvancedOrderEmail(order)
}
private fun sendAdvancedOrderEmail(order: Order) {
val preparator = MimeMessagePreparator { mimeMessage ->
mimeMessage.setRecipient(
Message.RecipientType.TO,
InternetAddress(order.customer.emailAddress)
)
mimeMessage.setFrom(InternetAddress("[email protected]"))
mimeMessage.subject = "订单 ${order.orderNumber} 确认"
// 设置邮件内容
mimeMessage.setText(
buildOrderEmailContent(order),
"UTF-8",
"html"
)
}
try {
mailSender.send(preparator)
} catch (ex: MailException) {
logger.error("高级邮件发送失败", ex)
}
}
private fun buildOrderEmailContent(order: Order): String {
return """
<html>
<body>
<h2>订单确认</h2>
<p>亲爱的 <strong>${order.customer.firstName} ${order.customer.lastName}</strong>,</p>
<p>您的订单已成功提交!</p>
<div style="background-color: #f0f0f0; padding: 10px; margin: 10px 0;">
<h3>订单详情</h3>
<p><strong>订单号:</strong>${order.orderNumber}</p>
<p><strong>下单时间:</strong>${LocalDateTime.now()}</p>
</div>
<p>感谢您的信任!</p>
</body>
</html>
""".trimIndent()
}
private fun processOrder(order: Order) {
logger.info("处理订单: ${order.orderNumber}")
}
companion object {
private val logger = LoggerFactory.getLogger(AdvancedOrderManager::class.java)
}
}
2. 使用 MimeMessageHelper 发送 HTML 邮件
MimeMessageHelper
提供了更便捷的 API 来处理复杂邮件:
kotlin
@Service
class EmailService(
private val mailSender: JavaMailSender
) {
fun sendHtmlEmail(
to: String,
subject: String,
htmlContent: String
) {
val message = mailSender.createMimeMessage()
val helper = MimeMessageHelper(message, true, "UTF-8")
helper.setTo(to)
helper.setFrom("[email protected]")
helper.setSubject(subject)
helper.setText(htmlContent, true) // [!code highlight] // true 表示 HTML 格式
mailSender.send(message)
}
fun sendWelcomeEmail(user: User) {
val htmlContent = """
<html>
<head>
<style>
.welcome-container {
font-family: Arial, sans-serif;
max-width: 600px;
margin: 0 auto;
padding: 20px;
background-color: #f9f9f9;
}
.header {
background-color: #4CAF50;
color: white;
padding: 20px;
text-align: center;
border-radius: 5px 5px 0 0;
}
.content {
background-color: white;
padding: 20px;
border-radius: 0 0 5px 5px;
}
.button {
display: inline-block;
padding: 10px 20px;
background-color: #4CAF50;
color: white;
text-decoration: none;
border-radius: 5px;
margin: 10px 0;
}
</style>
</head>
<body>
<div class="welcome-container">
<div class="header">
<h1>欢迎加入我们!</h1>
</div>
<div class="content">
<h2>你好,${user.name}!</h2>
<p>感谢您注册我们的服务。您的账户已经创建成功。</p>
<p>请点击下面的按钮来激活您的账户:</p>
<a href="https://yourapp.com/activate?token=${user.activationToken}" class="button">
激活账户
</a>
<p>如果您有任何问题,请随时联系我们的客服团队。</p>
<p>祝好!<br>团队</p>
</div>
</div>
</body>
</html>
""".trimIndent()
sendHtmlEmail(user.email, "欢迎加入我们!", htmlContent)
}
}
3. 发送带附件的邮件
kotlin
@Service
class AttachmentEmailService(
private val mailSender: JavaMailSender
) {
fun sendEmailWithAttachment(
to: String,
subject: String,
text: String,
attachmentPath: String,
attachmentName: String
) {
val message = mailSender.createMimeMessage()
val helper = MimeMessageHelper(message, true) // [!code highlight] // true 表示支持多部分消息
helper.setTo(to)
helper.setFrom("[email protected]")
helper.setSubject(subject)
helper.setText(text)
// 添加附件
val file = FileSystemResource(File(attachmentPath))
helper.addAttachment(attachmentName, file)
mailSender.send(message)
}
fun sendInvoiceEmail(order: Order, invoicePdfPath: String) {
val emailText = """
亲爱的 ${order.customer.firstName} ${order.customer.lastName},
您的订单 ${order.orderNumber} 的发票已生成,请查看附件。
如有任何问题,请联系我们。
谢谢!
""".trimIndent()
sendEmailWithAttachment(
to = order.customer.emailAddress,
subject = "订单发票 - ${order.orderNumber}",
text = emailText,
attachmentPath = invoicePdfPath,
attachmentName = "invoice-${order.orderNumber}.pdf"
)
}
}
4. 发送内联资源邮件
内联资源通常用于在 HTML 邮件中嵌入图片:
kotlin
@Service
class InlineResourceEmailService(
private val mailSender: JavaMailSender
) {
fun sendEmailWithInlineImage(
to: String,
subject: String,
imagePath: String
) {
val message = mailSender.createMimeMessage()
val helper = MimeMessageHelper(message, true)
helper.setTo(to)
helper.setFrom("[email protected]")
helper.setSubject(subject)
// HTML 内容,注意 cid: 引用
val htmlText = """
<html>
<body>
<h2>查看这张图片!</h2>
<p>这是一张内联图片:</p>
<img src='cid:companyLogo' alt='公司Logo' style='width: 200px;'> <!-- [!code highlight] -->
<p>感谢您的关注!</p>
</body>
</html>
""".trimIndent()
helper.setText(htmlText, true)
// 添加内联资源
val image = FileSystemResource(File(imagePath))
helper.addInline("companyLogo", image) // [!code highlight] // ID 必须与 HTML 中的 cid 匹配
mailSender.send(message)
}
}
WARNING
添加内联资源时,必须先设置文本内容,然后再添加资源。顺序很重要!
邮件模板系统 📄
使用 Thymeleaf 模板引擎
对于复杂的邮件内容,建议使用模板引擎:
kotlin
@Service
class TemplateEmailService(
private val mailSender: JavaMailSender,
private val templateEngine: TemplateEngine
) {
fun sendOrderConfirmationEmail(order: Order) {
val context = Context().apply {
setVariable("customerName", "${order.customer.firstName} ${order.customer.lastName}")
setVariable("orderNumber", order.orderNumber)
setVariable("orderDate", LocalDateTime.now())
setVariable("items", order.items)
setVariable("totalAmount", order.totalAmount)
}
val htmlContent = templateEngine.process("email/order-confirmation", context)
val message = mailSender.createMimeMessage()
val helper = MimeMessageHelper(message, true, "UTF-8")
helper.setTo(order.customer.emailAddress)
helper.setFrom("[email protected]")
helper.setSubject("订单确认 - ${order.orderNumber}")
helper.setText(htmlContent, true)
mailSender.send(message)
}
}
Thymeleaf 模板示例 (order-confirmation.html)
html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>订单确认</title>
<style>
body { font-family: Arial, sans-serif; }
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
.header { background-color: #4CAF50; color: white; padding: 20px; text-align: center; }
.content { background-color: #f9f9f9; padding: 20px; }
.order-item { border-bottom: 1px solid #ddd; padding: 10px 0; }
.total { font-weight: bold; font-size: 1.2em; color: #4CAF50; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>订单确认</h1>
</div>
<div class="content">
<h2 th:text="'你好,' + ${customerName} + '!'">你好,客户!</h2>
<p>感谢您的订购!以下是您的订单详情:</p>
<div class="order-details">
<p><strong>订单号:</strong><span th:text="${orderNumber}">ORDER123</span></p>
<p><strong>下单时间:</strong><span th:text="${#temporals.format(orderDate, 'yyyy-MM-dd HH:mm:ss')}">2023-01-01 12:00:00</span></p>
</div>
<h3>订单商品</h3>
<div th:each="item : ${items}" class="order-item">
<span th:text="${item.name}">商品名称</span> -
<span th:text="'数量:' + ${item.quantity}">数量:1</span> -
<span th:text="'单价:¥' + ${item.price}">单价:¥100</span>
</div>
<div class="total">
<p th:text="'总金额:¥' + ${totalAmount}">总金额:¥100</p>
</div>
<p>我们会尽快为您处理订单,请耐心等待。</p>
<p>如有任何问题,请联系我们的客服团队。</p>
</div>
</div>
</body>
</html>
异步邮件发送 ⚡
在高并发场景下,同步发送邮件可能会影响用户体验。建议使用异步方式:
kotlin
@Service
class AsyncEmailService(
private val mailSender: JavaMailSender
) {
@Async("emailTaskExecutor")
fun sendEmailAsync(
to: String,
subject: String,
content: String,
isHtml: Boolean = false
): CompletableFuture<Boolean> {
return try {
val message = mailSender.createMimeMessage()
val helper = MimeMessageHelper(message, true, "UTF-8")
helper.setTo(to)
helper.setFrom("[email protected]")
helper.setSubject(subject)
helper.setText(content, isHtml)
mailSender.send(message)
logger.info("邮件发送成功: $to")
CompletableFuture.completedFuture(true)
} catch (ex: Exception) {
logger.error("邮件发送失败: $to", ex)
CompletableFuture.completedFuture(false)
}
}
companion object {
private val logger = LoggerFactory.getLogger(AsyncEmailService::class.java)
}
}
@Configuration
@EnableAsync
class AsyncConfig {
@Bean("emailTaskExecutor")
fun emailTaskExecutor(): TaskExecutor {
return ThreadPoolTaskExecutor().apply {
corePoolSize = 5
maxPoolSize = 10
queueCapacity = 100
setThreadNamePrefix("email-")
initialize()
}
}
}
邮件发送监控与重试 📊
邮件发送状态跟踪
kotlin
enum class EmailStatus {
PENDING, // 待发送
SENT, // 已发送
FAILED, // 发送失败
RETRY // 重试中
}
@Entity
@Table(name = "email_logs")
data class EmailLog(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long = 0,
val recipient: String,
val subject: String,
val content: String,
val status: EmailStatus = EmailStatus.PENDING,
val attempts: Int = 0,
val lastAttemptTime: LocalDateTime? = null,
val errorMessage: String? = null,
val createdTime: LocalDateTime = LocalDateTime.now()
)
@Repository
interface EmailLogRepository : JpaRepository<EmailLog, Long> {
fun findByStatusAndAttemptsLessThan(status: EmailStatus, maxAttempts: Int): List<EmailLog>
}
带重试机制的邮件服务
kotlin
@Service
class ReliableEmailService(
private val mailSender: JavaMailSender,
private val emailLogRepository: EmailLogRepository
) {
fun sendEmailWithRetry(
to: String,
subject: String,
content: String,
isHtml: Boolean = false
) {
val emailLog = EmailLog(
recipient = to,
subject = subject,
content = content
)
val savedLog = emailLogRepository.save(emailLog)
attemptToSendEmail(savedLog, isHtml)
}
private fun attemptToSendEmail(emailLog: EmailLog, isHtml: Boolean) {
try {
val message = mailSender.createMimeMessage()
val helper = MimeMessageHelper(message, true, "UTF-8")
helper.setTo(emailLog.recipient)
helper.setFrom("[email protected]")
helper.setSubject(emailLog.subject)
helper.setText(emailLog.content, isHtml)
mailSender.send(message)
// 更新发送成功状态
emailLogRepository.save(emailLog.copy(
status = EmailStatus.SENT,
attempts = emailLog.attempts + 1,
lastAttemptTime = LocalDateTime.now()
))
logger.info("邮件发送成功: ${emailLog.recipient}")
} catch (ex: Exception) {
val updatedLog = emailLog.copy(
status = if (emailLog.attempts + 1 >= MAX_RETRY_ATTEMPTS) EmailStatus.FAILED else EmailStatus.RETRY,
attempts = emailLog.attempts + 1,
lastAttemptTime = LocalDateTime.now(),
errorMessage = ex.message
)
emailLogRepository.save(updatedLog)
logger.error("邮件发送失败 (尝试 ${updatedLog.attempts}/${MAX_RETRY_ATTEMPTS}): ${emailLog.recipient}", ex)
}
}
@Scheduled(fixedDelay = 300000) // 每5分钟执行一次
fun retryFailedEmails() {
val failedEmails = emailLogRepository.findByStatusAndAttemptsLessThan(
EmailStatus.RETRY,
MAX_RETRY_ATTEMPTS
)
logger.info("开始重试失败邮件,数量: ${failedEmails.size}")
failedEmails.forEach { emailLog ->
attemptToSendEmail(emailLog, true) // 假设都是HTML邮件
}
}
companion object {
private const val MAX_RETRY_ATTEMPTS = 3
private val logger = LoggerFactory.getLogger(ReliableEmailService::class.java)
}
}
邮件测试 🧪
单元测试
kotlin
@ExtendWith(MockitoExtension::class)
class EmailServiceTest {
@Mock
private lateinit var mailSender: JavaMailSender
@Mock
private lateinit var mimeMessage: MimeMessage
@InjectMocks
private lateinit var emailService: EmailService
@Test
fun `should send simple email successfully`() {
// Given
given(mailSender.createMimeMessage()).willReturn(mimeMessage)
// When
emailService.sendHtmlEmail(
to = "[email protected]",
subject = "Test Subject",
htmlContent = "<h1>Test</h1>"
)
// Then
verify(mailSender).createMimeMessage()
verify(mailSender).send(mimeMessage)
}
@Test
fun `should handle mail exception gracefully`() {
// Given
given(mailSender.createMimeMessage()).willReturn(mimeMessage)
given(mailSender.send(any<MimeMessage>())).willThrow(MailSendException("SMTP Error"))
// When & Then
assertThrows<MailSendException> {
emailService.sendHtmlEmail(
to = "[email protected]",
subject = "Test Subject",
htmlContent = "<h1>Test</h1>"
)
}
}
}
集成测试
kotlin
@SpringBootTest
@TestPropertySource(properties = [
"spring.mail.host=localhost",
"spring.mail.port=2525" // 使用测试邮件服务器
])
class EmailIntegrationTest {
@Autowired
private lateinit var emailService: EmailService
@Test
fun `should send email in integration environment`() {
// 可以使用 GreenMail 或 WireMock 来模拟 SMTP 服务器
emailService.sendHtmlEmail(
to = "[email protected]",
subject = "Integration Test",
htmlContent = "<h1>Integration Test Email</h1>"
)
// 验证邮件是否被正确发送到测试服务器
}
}