Skip to content

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简单邮件消息封装纯文本邮件
MimeMessageHelperMIME消息辅助类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>"
        )
        
        // 验证邮件是否被正确发送到测试服务器
    }
}

最佳实践与注意事项 ⚠️

1.