找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

1972

积分

0

好友

282

主题
发表于 2025-12-25 05:36:39 | 查看: 29| 回复: 0

一、Spring Email 核心优势

Java 生态中,Spring Framework 内置了高效的邮件发送抽象层,其核心优势主要体现在以下几个方面:

  • 简化配置:仅需简单配置即可快速连接各类邮件服务器。
  • 模板支持:无缝集成 Thymeleaf、Freemarker 等主流模板引擎,便于构建动态邮件内容。
  • 多格式支持:全面支持文本、HTML、附件及内联资源等多种邮件格式。
  • 异步发送:通过异步机制提升应用响应速度,避免阻塞主线程。
  • 测试友好:提供 Mock 实现,方便进行单元测试和集成测试。

二、快速开始:5分钟完成基础配置

1. 添加依赖

<!-- 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 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. 基础配置

# 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. 主流邮件服务商配置示例

# 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 接口

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:用户注册验证邮件

@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:带附件的报表邮件

@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:密码重置邮件

@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:批量发送营销邮件

@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:带跟踪的邮件发送

@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:邮件队列与重试机制

@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. 多邮件服务器配置

@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. 邮件发送频率限制

@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. 邮件模板引擎集成

@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. 单元测试

@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)

<!-- 添加GreenMail依赖 -->
<dependency>
    <groupId>com.icegreen</groupId>
    <artifactId>greenmail</artifactId>
    <version>1.6.9</version>
    <scope>test</scope>
</dependency>
@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[0].getSubject());
    }
}

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

@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:发送超时或连接失败

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

问题2:中文乱码

@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:被邮件服务器拒绝

@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 加密传输。
    • 定期更换应用专用密码,避免使用账户登录密码。
  2. 可靠性

    • 实现邮件队列和重试机制,确保送达率。
    • 详细记录邮件发送日志,便于排查问题。
    • 监控邮件发送成功率,设置失败告警。
  3. 性能优化

    • 采用异步发送避免阻塞主线程,提升响应速度。
    • 合理设置连接池大小,避免资源浪费。
    • 批量发送时添加适当延迟,防止被邮件服务器限流。
  4. 可维护性

    • 使用模板引擎(如 Thymeleaf)分离邮件内容和样式,便于维护。
    • 统一异常处理逻辑,提供清晰的错误信息。
    • 在开发环境集成邮件预览功能,方便调试。
  5. 监控与告警

    • 监控邮件发送成功率、失败率及延迟指标。
    • 设置自动化告警机制,及时响应异常。
    • 定期清理过期邮件记录,保持数据整洁。

通过以上配置、案例与最佳实践,您可以构建高可靠、高性能的邮件发送系统,有效支撑用户注册、通知、营销等多种业务场景。




上一篇:AMD HX 370迷你主机评测:80 TOPS AI算力、OCulink与双2.5G网口的全能体验
下一篇:Temu全球化闪电战解析:3年5.3亿月活的增长策略与未来挑战
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-1-11 11:55 , Processed in 0.402120 second(s), 40 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

快速回复 返回顶部 返回列表