首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

Java 郵件發送實戰:JavaMail API 完整封裝與最佳實踐

     編輯:Java 相關知識

前言

JavaMail 是 Java 平台郵件收發標準 API。本文提供生產級封裝:簡單文本/HTML/附件/內嵌圖片郵件發送、異步模板發送、郵件服務器配置、異常處理、安全最佳實踐。

一、郵件協議速覽

協議用途默認端口SSL/TLS 端口
SMTP發送25465 (SSL), 587 (STARTTLS)
POP3接收(下載)110995
IMAP接收(同步)143993

常見郵箱 SMTP 設置


# 163/126/yeah.net
host: smtp.163.com, port: 465/994 (SSL) 或 25/587 (STARTTLS)
# QQ 郵箱
host: smtp.qq.com, port: 465/587 (需授權碼)
# Gmail
host: smtp.gmail.com, port: 465/587 (需應用專用密碼)
# Outlook
host: smtp.office365.com, port: 587 (STARTTLS)
# 企業郵箱
咨詢 IT 部門獲取專用 SMTP 服務器

二、Maven 依賴


<dependency>
    <groupId>com.sun.mail</groupId>
    <artifactId>jakarta.mail</artifactId>
    <version>2.0.1</version>
</dependency>
<!-- Spring Boot 整合 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-mail</artifactId>
</dependency>

三、核心封裝:MailService

1. 配置屬性類


@Data
@ConfigurationProperties(prefix = "spring.mail")
public class MailProperties {
    private String host;
    private int port = 587;
    private String username;
    private String password;
    private String protocol = "smtp";
    private String defaultEncoding = "UTF-8";
    private Properties properties = new Properties();
    
    @PostConstruct
    public void init() {
        properties.put("mail.smtp.host", host);
        properties.put("mail.smtp.port", port);
        properties.put("mail.smtp.auth", "true");
        properties.put("mail.transport.protocol", protocol);
        properties.put("mail.smtp.starttls.enable", "true"); // STARTTLS
        properties.put("mail.smtp.ssl.enable", "false");     // SSL 另行配置
        properties.put("mail.debug", "false");
        properties.put("mail.mime.charset", defaultEncoding);
    }
    
    // SSL 配置方法
    public void enableSsl() {
        properties.put("mail.smtp.ssl.enable", "true");
        properties.put("mail.smtp.socketFactory.class", "javax.net.ssl.SSLSocketFactory");
        properties.put("mail.smtp.socketFactory.port", port);
    }
}

2. 郵件數據模型


@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class EmailMessage {
    private String[] to;           // 收件人
    private String[] cc;           // 抄送
    private String[] bcc;          // 密送
    private String subject;        // 主題
    private String text;           // 純文本內容
    private String html;           // HTML 內容
    private List attachments; // 附件
    private List inlineResources; // 內嵌資源
    private String from;           // 發件人(可選,默認配置)
    private String replyTo;        // 回復地址
    private Map headers; // 自定義頭部
    
    @Data
    @Builder
    public static class Attachment {
        private String filename;
        private byte[] content;
        private String contentType;
        private File file; // 或直接文件
    }
    
    @Data
    @Builder
    public static class InlineResource {
        private String contentId;  // CID,HTML 中引用 cid:xxx
        private byte[] content;
        private String contentType;
        private File file;
    }
}

3. 發送服務實現


@Service
@Slf4j
public class MailService {
    
    private final Session session;
    private final String defaultFrom;
    
    public MailService(MailProperties props) {
        this.session = Session.getInstance(props.getProperties(), 
            new Authenticator() {
                @Override
                protected PasswordAuthentication getPasswordAuthentication() {
                    return new PasswordAuthentication(props.getUsername(), props.getPassword());
                }
            });
        this.defaultFrom = props.getUsername();
    }
    
    // 同步發送入口
    public void send(EmailMessage email) {
        sendMail(buildMimeMessage(email));
    }
    
    // 異步發送
    @Async("mailExecutor")
    public CompletableFuture sendAsync(EmailMessage email) {
        sendMail(buildMimeMessage(email));
        return CompletableFuture.completedFuture(null);
    }
    
    // 模板發送
    public void sendTemplate(String templateName, Map model, String... to) {
        String html = templateEngine.process(templateName, model);
        EmailMessage email = EmailMessage.builder()
            .to(to)
            .subject(model.get("subject").toString())
            .html(html)
            .build();
        send(email);
    }
    
    private MimeMessage buildMimeMessage(EmailMessage email) {
        try {
            MimeMessage message = new MimeMessage(session);
            
            // 發件人
            message.setFrom(new InternetAddress(
                StringUtils.hasText(email.getFrom()) ? email.getFrom() : defaultFrom));
            
            // 收件人
            setRecipients(message, Message.RecipientType.TO, email.getTo());
            setRecipients(message, Message.RecipientType.CC, email.getCc());
            setRecipients(message, Message.RecipientType.BCC, email.getBcc());
            
            // 回復地址
            if (StringUtils.hasText(email.getReplyTo())) {
                message.setReplyTo(InternetAddress.parse(email.getReplyTo()));
            }
            
            // 主題
            message.setSubject(email.getSubject(), "UTF-8");
            
            // 自定義頭部
            if (email.getHeaders() != null) {
                email.getHeaders().forEach((k, v) -> {
                    try { message.addHeader(k, v); } catch (MessagingException ignored) {}
                });
            }
            
            // 構建內容
            Multipart multipart = buildContent(email);
            message.setContent(multipart);
            message.saveChanges(); // 重要:生成 Message-ID、Date 等頭部
            
            return message;
        } catch (MessagingException | UnsupportedEncodingException e) {
            throw new MailBuildException("郵件構建失敗", e);
        }
    }
    
    private void setRecipients(MimeMessage message, Message.RecipientType type, String[] addresses) 
            throws MessagingException {
        if (addresses != null && addresses.length > 0) {
            InternetAddress[] internetAddresses = Arrays.stream(addresses)
                .filter(StringUtils::hasText)
                .map(addr -> {
                    try { return new InternetAddress(addr); }
                    catch (Exception e) { throw new IllegalArgumentException("無效郵箱: " + addr, e); }
                })
                .toArray(InternetAddress[]::new);
            message.setRecipients(type, internetAddresses);
        }
    }
    
    private Multipart buildContent(EmailMessage email) throws MessagingException {
        // 判斷內容類型選擇 multipart 子類型
        boolean hasAttachments = email.getAttachments() != null && !email.getAttachments().isEmpty();
        boolean hasInline = email.getInlineResources() != null && !email.getInlineResources().isEmpty();
        boolean hasHtml = StringUtils.hasText(email.getHtml());
        boolean hasText = StringUtils.hasText(email.getText());
        
        // 策略:
        // 1. 只有文本 → 直接 setText
        // 2. 只有 HTML → mixed 或 related
        // 3. 文本+HTML → alternative
        // 4. 有內嵌資源 → related 包裝 alternative
        // 5. 有附件 → mixed 包裝以上所有
        
        if (!hasAttachments && !hasInline) {
            // 簡單情況
            if (hasHtml && hasText) {
                MimeMultipart mp = new MimeMultipart("alternative");
                addTextPart(mp, email.getText());
                addHtmlPart(mp, email.getHtml());
                return mp;
            } else if (hasHtml) {
                MimeMultipart mp = new MimeMultipart("related");
                addHtmlPart(mp, email.getHtml());
                return mp;
            } else {
                MimeMessage msg = new MimeMessage(session);
                msg.setText(email.getText(), "UTF-8");
                return new MimeMultipart(); // 不會走這裡
            }
        }
        
        // 複雜情況:mixed 最外層
        MimeMultipart mixed = new MimeMultipart("mixed");
        
        // 內容主體:alternative (文本+HTML) 或 related (HTML+內嵌)
        Multipart body;
        if (hasHtml && hasText) {
            body = new MimeMultipart("alternative");
            addTextPart(body, email.getText());
            addHtmlPart(body, email.getHtml());
        } else if (hasHtml) {
            body = new MimeMultipart("related");
            addHtmlPart(body, email.getHtml());
        } else {
            body = new MimeMultipart("mixed");
            addTextPart(body, email.getText());
        }
        
        // 內嵌資源加入 body (若 body 是 related)
        if (hasInline && body instanceof MimeMultipart) {
            MimeMultipart related = (MimeMultipart) body;
            for (EmailMessage.InlineResource res : email.getInlineResources()) {
                addInlinePart(related, res);
            }
        }
        
        // 將 body 加入 mixed
        MimeBodyPart bodyPart = new MimeBodyPart();
        try {
            bodyPart.setContent(body);
            mixed.addBodyPart(bodyPart);
        } catch (MessagingException e) {
            throw new MailBuildException("內容組裝失敗", e);
        }
        
        // 附件加入 mixed
        if (hasAttachments) {
            for (EmailMessage.Attachment att : email.getAttachments()) {
                addAttachmentPart(mixed, att);
            }
        }
        
        return mixed;
    }
    
    private void addTextPart(Multipart mp, String text) throws MessagingException {
        MimeBodyPart part = new MimeBodyPart();
        part.setContent(text, "text/plain;charset=UTF-8");
        mp.addBodyPart(part);
    }
    
    private void addHtmlPart(Multipart mp, String html) throws MessagingException {
        MimeBodyPart part = new MimeBodyPart();
        part.setContent(html, "text/html;charset=UTF-8");
        mp.addBodyPart(part);
    }
    
    private void addInlinePart(Multipart mp, EmailMessage.InlineResource res) throws MessagingException {
        MimeBodyPart part = new MimeBodyPart();
        if (res.getFile() != null) {
            part.attachFile(res.getFile());
        } else {
            part.setContent(res.getContent(), res.getContentType());
        }
        part.setContentID("<" + res.getContentId() + ">"); // 標準 CID 格式
        part.setDisposition(MimeBodyPart.INLINE);
        mp.addBodyPart(part);
    }
    
    private void addAttachmentPart(Multipart mp, EmailMessage.Attachment att) throws MessagingException {
        MimeBodyPart part = new MimeBodyPart();
        if (att.getFile() != null) {
            part.attachFile(att.getFile());
        } else {
            part.setContent(att.getContent(), att.getContentType());
        }
        // 解決中文附件名亂碼
        String encodedName = MimeUtility.encodeText(att.getFilename(), "UTF-8", "B");
        part.setFileName(encodedName);
        part.setDisposition(MimeBodyPart.ATTACHMENT);
        mp.addBodyPart(part);
    }
    
    private void sendMail(MimeMessage message) {
        try {
            Transport.send(message);
            log.info("郵件發送成功: to={}, subject={}", 
                Arrays.toString(message.getRecipients(Message.RecipientType.TO)), 
                message.getSubject());
        } catch (MessagingException e) {
            log.error("郵件發送失敗: {}", message.getSubject(), e);
            throw new MailSendException("郵件發送失敗", e);
        }
    }
}

四、Spring Boot 整合配置

application.yml


spring:
  mail:
    host: smtp.163.com
    port: 465
    username: your_email@163.com
    password: your_auth_code  # 授權碼,非登錄密碼
    protocol: smtp
    default-encoding: UTF-8
    properties:
      mail:
        smtp:
          ssl:
            enable: true
          auth: true
          starttls:
            enable: false
          socketFactory:
            class: javax.net.ssl.SSLSocketFactory
            port: 465
      debug: false

# 異步線程池
  task:
    execution:
      pool:
        mail:
          core-size: 5
          max-size: 20
          queue-capacity: 100

異步配置


@Configuration
@EnableAsync
public class AsyncConfig {
    
    @Bean("mailExecutor")
    public ThreadPoolTaskExecutor mailExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(5);
        executor.setMaxPoolSize(20);
        executor.setQueueCapacity(100);
        executor.setThreadNamePrefix("mail-async-");
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.initialize();
        return executor;
    }
}

五、使用示例

1. 簡單文本郵件


@Autowired
private MailService mailService;

public void notifyAdmin(String message) {
    EmailMessage email = EmailMessage.builder()
        .to(new String[]{"admin@company.com"})
        .subject("系統告警")
        .text(message)
        .build();
    mailService.sendAsync(email);
}

2. HTML 富文本郵件


public void sendWelcomeEmail(User user) {
    String html = "

歡迎 " + user.getName() + "!

" + "

您的帳號已激活,點擊登錄

" + ""; // 內嵌圖片 EmailMessage email = EmailMessage.builder() .to(new String[]{user.getEmail()}) .subject("歡迎加入我們") .html(html) .inlineResources(List.of( InlineResource.builder() .contentId("logo") .file(new File("classpath:static/images/logo.png")) .contentType("image/png") .build() )) .build(); mailService.sendAsync(email); }

3. 帶附件郵件


public void sendReportWithAttachment(String to, File reportFile) {
    EmailMessage email = EmailMessage.builder()
        .to(new String[]{to})
        .subject("月度報表")
        .text("請查收附件中的月度報表。")
        .attachments(List.of(
            Attachment.builder()
                .filename("月度報表_" + LocalDate.now() + ".pdf")
                .file(reportFile)
                .contentType("application/pdf")
                .build()
        ))
        .build();
    mailService.sendAsync(email);
}

4. 模板郵件 (Thymeleaf/FreeMarker)


@Autowired
private TemplateEngine templateEngine; // Thymeleaf

public void sendOrderConfirmation(Order order) {
    Context ctx = new Context();
    ctx.setVariables(Map.of(
        "order", order,
        "customer", order.getCustomer(),
        "items", order.getItems(),
        "subject", "訂單確認 - " + order.getOrderNo()
    ));
    
    mailService.sendTemplate("email/order-confirmation", ctx.getVariables(), 
        order.getCustomer().getEmail());
}

六、常見問題與優化

問題原因解決
發送超時網絡/防火牆/端口檢查 465/587 端口連通性;嘗試 STARTTLS (587)
認證失敗密碼錯誤/未開啟 SMTP使用授權碼/應用專用密碼;郵箱設置開啟 SMTP
進垃圾箱內容/域名信譽/SPF/DKIM配置 SPF/DKIM/DMARC;避免敏感詞;使用企業郵箱
中文附件名亂碼RFC 2231 編碼MimeUtility.encodeText(filename, "UTF-8", "B")
大附件發送失敗郵箱大小限制 (通常 20-50MB)分卷壓縮/雲存儲鏈接/斷點續傳
連接泄漏Transport 未關閉使用 Transport.send() 靜態方法自動管理
HTML 顯示異常CSS 不兼容/圖片引用失效內聯 CSS、內嵌關鍵圖片、內聯關鍵樣式

七、安全與合規

  1. 憑證管理:配置文件加密、環境變量、Vault/密鑰管理系統
  2. 傳輸加密:強制 SSL/TLS,驗證服務器證書
  3. 發送限流:Token Bucket 算法限制每分鐘/小時發送量
  4. 內容審核:敏感詞過濾、防郵件注入(頭部注入攻擊)
  5. 退信處理:監控 Bounce 郵件,自動清理無效地址
  6. 審計日誌:記錄發送者、收件人、主題、時間、結果、IP
  7. 合規:GDPR、CAN-SPAM、中國《互聯網郵件服務管理辦法》

八、監控指標


@Timed("mail.send")
public void send(EmailMessage email) { ... }

// 關鍵指標
- mail.send.total: 發送總數
- mail.send.success: 成功數
- mail.send.failed: 失敗數
- mail.send.duration: 發送耗時
- mail.queue.size: 異步隊列積壓
- mail.connection.pool.usage: 連接池使用率

總結

JavaMail 封裝核心在於:MimeMessage 正確構建(multipart 嵌套邏輯)、異步非阻塞發送、模板引擎集成、異常分類處理、安全合規。生產環境必備:異步線程池、重試機制、限流熔斷、審計日誌、退信監控。Spring Boot `spring-boot-starter-mail` 提供良好自動配置,建議在其基礎上構建業務級 `MailService`。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved