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

Java 郵件發送完整實戰:Jakarta Mail (JavaMail) 核心 API、SMTP 認證、HTML 郵件、附件、異步發送與 Spring Boot 整合

     編輯:關於JSP

Java 發送郵件標準 API 為 **Jakarta Mail** (原 `javax.mail`,現 `jakarta.mail`),支援 SMTP/IMAP/POP3、SSL/TLS/STARTTLS、MIME 多部分郵件 (HTML+純文字+附件+內嵌圖片)。本文從核心 API 手寫發送器、工廠模式封裝、觀察者模式業務解耦、Spring Boot 整合、生產環境調優 (連線池、重試、監控) 全方位實戰指南。

Maven 依賴 (Jakarta Mail 2.0+ / Java 8+)

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

核心發送器實作 (手寫版,零框架依賴)

package com.example.mail;

import jakarta.mail.*;
import jakarta.mail.internet.*;
import jakarta.activation.*;
import java.util.*;
import java.io.*;

public class SimpleMailSender {
    private final Session session;
    private final String from;
    private final String fromName;

    public SimpleMailSender(String host, int port, String username, String password, 
                            String from, String fromName, boolean ssl, boolean starttls) {
        this.from = from;
        this.fromName = fromName;
        
        Properties props = new Properties();
        props.put("mail.smtp.host", host);
        props.put("mail.smtp.port", port);
        props.put("mail.smtp.auth", "true");
        props.put("mail.smtp.connectiontimeout", 5000);
        props.put("mail.smtp.timeout", 10000);
        props.put("mail.smtp.writetimeout", 10000);
        
        if (ssl) {
            props.put("mail.smtp.ssl.enable", "true");
            props.put("mail.smtp.socketFactory.class", "javax.net.ssl.SSLSocketFactory");
            props.put("mail.smtp.socketFactory.port", port);
        } else if (starttls) {
            props.put("mail.smtp.starttls.enable", "true");
            props.put("mail.smtp.starttls.required", "true");
        }
        
        // 連線池設定 (Jakarta Mail 內建簡易池)
        props.put("mail.smtp.pool.enable", "true");
        props.put("mail.smtp.pool.maxsize", "10");
        
        this.session = Session.getInstance(props, new Authenticator() {
            @Override protected PasswordAuthentication getPasswordAuthentication() {
                return new PasswordAuthentication(username, password);
            }
        });
        // 除錯模式
        // this.session.setDebug(true);
    }

    /** 發送純文字郵件 */
    public void sendText(String to, String subject, String text) throws MessagingException {
        send(to, subject, text, null, null, null);
    }

    /** 發送 HTML 郵件 (含純文字備用) */
    public void sendHtml(String to, String subject, String html, String textFallback) throws MessagingException {
        send(to, subject, textFallback, html, null, null);
    }

    /** 發送帶附件郵件 */
    public void sendWithAttachments(String to, String subject, String html, String text, 
                                    List attachments, List inlineImages) 
            throws MessagingException {
        send(to, subject, text, html, attachments, inlineImages);
    }

    /** 核心發送邏輯 */
    private void send(String to, String subject, String text, String html, 
                      List attachments, List inlineImages) 
            throws MessagingException {
        
        MimeMessage msg = new MimeMessage(session);
        msg.setFrom(new InternetAddress(from, fromName, "UTF-8"));
        msg.setRecipients(Message.RecipientType.TO, InternetAddress.parse(to));
        msg.setSubject(subject, "UTF-8");
        msg.setSentDate(new Date());

        // 建立多層 MIME 結構
        Multipart root = new MimeMultipart("mixed"); // 最外層 mixed: 附件 + related/alternative
        
        // 1. alternative: text/plain + text/html (郵件客戶端自動選擇顯示)
        Multipart alternative = new MimeMultipart("alternative");
        if (text != null) {
            MimeBodyPart textPart = new MimeBodyPart();
            textPart.setContent(text, "text/plain; charset=UTF-8");
            alternative.addBodyPart(textPart);
        }
        if (html != null) {
            MimeBodyPart htmlPart = new MimeBodyPart();
            htmlPart.setContent(html, "text/html; charset=UTF-8");
            alternative.addBodyPart(htmlPart);
        }
        // 將 alternative 包入 root
        MimeBodyPart altWrapper = new MimeBodyPart();
        altWrapper.setContent(alternative);
        root.addBodyPart(altWrapper);

        // 2. related: HTML 內嵌圖片 (cid:)
        if (inlineImages != null && !inlineImages.isEmpty()) {
            Multipart related = new MimeMultipart("related");
            // 將 alternative 移入 related
            related.addBodyPart(altWrapper); // 需重新組裝,這裡簡化:假設 html 使用 cid
            for (InlineImage img : inlineImages) {
                MimeBodyPart imgPart = new MimeBodyPart();
                imgPart.setDataHandler(new DataHandler(new ByteArrayDataSource(img.content, img.mimeType)));
                imgPart.setHeader("Content-ID", "<" + img.cid + ">"); // 
                imgPart.setDisposition(MimeBodyPart.INLINE);
                related.addBodyPart(imgPart);
            }
            // 替換 root 中的 alternative 為 related
            root.removeBodyPart(altWrapper);
            MimeBodyPart relWrapper = new MimeBodyPart();
            relWrapper.setContent(related);
            root.addBodyPart(relWrapper);
        }

        // 3. 附件
        if (attachments != null) {
            for (Attachment att : attachments) {
                MimeBodyPart attPart = new MimeBodyPart();
                attPart.setDataHandler(new DataHandler(new ByteArrayDataSource(att.content, att.mimeType)));
                attPart.setFileName(MimeUtility.encodeText(att.filename, "UTF-8", "B")); // RFC 2047 編碼中文檔名
                attPart.setDisposition(MimeBodyPart.ATTACHMENT);
                root.addBodyPart(attPart);
            }
        }

        msg.setContent(root);
        Transport.send(msg);
    }

    /** 附件資料結構 */
    public static class Attachment {
        public final String filename;
        public final String mimeType;
        public final byte[] content;
        public Attachment(String filename, String mimeType, byte[] content) {
            this.filename = filename; this.mimeType = mimeType; this.content = content;
        }
        public static Attachment fromFile(File file) throws IOException {
            return new Attachment(file.getName(), 
                Files.probeContentType(file.toPath()), 
                Files.readAllBytes(file.toPath()));
        }
    }

    /** 內嵌圖片資料結構 */
    public static class InlineImage {
        public final String cid; // 不含 <>
        public final String mimeType;
        public final byte[] content;
        public InlineImage(String cid, String mimeType, byte[] content) {
            this.cid = cid; this.mimeType = mimeType; this.content = content;
        }
    }
}

工廠模式封裝 (單例、配置外部化)

package com.example.mail;

public class MailSenderFactory {
    private static volatile SimpleMailSender instance;
    
    public static SimpleMailSender getInstance() {
        if (instance == null) {
            synchronized (MailSenderFactory.class) {
                if (instance == null) {
                    // 從配置檔/環境變數/配置中心讀取
                    String host = Config.get("mail.smtp.host", "smtp.gmail.com");
                    int port = Config.getInt("mail.smtp.port", 587);
                    String user = Config.get("mail.username");
                    String pass = Config.get("mail.password");
                    String from = Config.get("mail.from");
                    String name = Config.get("mail.from.name", "系統通知");
                    boolean ssl = Config.getBool("mail.ssl", false);
                    boolean tls = Config.getBool("mail.starttls", true);
                    instance = new SimpleMailSender(host, port, user, pass, from, name, ssl, tls);
                }
            }
        }
        return instance;
    }
}

業務解耦:觀察者模式發送價格變動通知

// 事件定義
public class PriceDropEvent {
    private final Product product;
    private final double oldPrice;
    private final double newPrice;
    // getter...
}

// 監聽器
@Component
public class PriceDropMailListener implements ApplicationListener {
    private final SimpleMailSender mailSender = MailSenderFactory.getInstance();
    private final List recipients = Arrays.asList("user1@example.com", "user2@example.com");

    @Override
    public void onApplicationEvent(PriceDropEvent event) {
        Product p = event.getProduct();
        double drop = event.getOldPrice() - event.getNewPrice();
        if (drop <= 0) return;

        String subject = "🔥 降價提醒:" + p.getName();
        String html = String.format("""
            

%s

原價:%.2f 元 → 現價:%.2f 元 (降幅 %.2f 元)

立即查看商品

""", p.getName(), event.getOldPrice(), event.getNewPrice(), drop, p.getDetailUrl()); recipients.parallelStream().forEach(to -> { try { mailSender.sendHtml(to, subject, html, "純文字備用內容..."); } catch (MessagingException e) { log.error("發送失敗: {}", to, e); // 可送入重試佇列 / 死信隊列 } }); } } // 發布事件 (Service 層) @Service public class ProductService { @Autowired ApplicationEventPublisher publisher; public void updatePrice(Long id, double newPrice) { Product p = repo.findById(id).orElseThrow(); double old = p.getPrice(); p.setPrice(newPrice); repo.save(p); if (newPrice < old) { publisher.publishEvent(new PriceDropEvent(p, old, newPrice)); } } }

Spring Boot 整合 (application.yml)

spring:
  mail:
    host: smtp.gmail.com
    port: 587
    username: your@gmail.com
    password: ${MAIL_PASSWORD}  # 環境變數/密碼管理器
    protocol: smtp
    properties:
      mail:
        smtp:
          auth: true
          starttls:
            enable: true
            required: true
          connectiontimeout: 5000
          timeout: 10000
          writetimeout: 10000
          pool:
            enable: true
            maxsize: 20
    default-encoding: UTF-8
    # 開發環境輸出到日誌而非實際發送
    # test-connection: true

使用 JavaMailSender (Spring 封裝)

@Service
@RequiredArgsConstructor
public class NotificationService {
    private final JavaMailSender mailSender;
    private final TemplateEngine templateEngine; // Thymeleaf/FreeMarker

    public void sendWelcome(User user) {
        MimeMessage msg = mailSender.createMimeMessage();
        MimeMessageHelper helper = new MimeMessageHelper(msg, true, "UTF-8");
        helper.setTo(user.getEmail());
        helper.setSubject("歡迎加入 " + appName);
        
        // 模板渲染 HTML
        Context ctx = new Context();
        ctx.setVariable("user", user);
        String html = templateEngine.process("mail/welcome", ctx);
        helper.setText(html, true); // true = HTML
        
        // 內嵌 Logo
        ClassPathResource logo = new ClassPathResource("static/images/logo.png");
        helper.addInline("logo", logo, "image/png"); // cid:logo
        
        mailSender.send(msg);
    }
}

生產環境調優與最佳實務

維度建議
連線管理啟用 `mail.smtp.pool.enable=true`,設定 `maxsize` 避免連線洩漏;Spring Boot 自動配置 `JavaMailSender` 為單例。
重試機制網路波動導致發送失敗:**不可同步重試** (阻塞執行緒)。改用 **非同步佇列**:發送請求寫入 Redis/RabbitMQ/Kafka → Worker 消費重試 (指數退避)、死信隊列告警。
異步發送`@Async` + `ThreadPoolTaskExecutor` (Spring Boot) 或 `CompletableFuture` (手寫版)。確保業務主流程不被郵件阻塞。
大量收件人**勿**放入單一 `TO`/`BCC` 超過 50-100 位 (SMTP 伺服器/反垃圾限制)。拆批發送、使用郵件列表服務 (Mailgun、SendGrid、Amazon SES) 或自建 Listserv。
信譽管理配置 **SPF、DKIM、DMARC** DNS 記錄;使用專用發信域名/子域;監控退信率、投訴率、送達率;暖機新 IP。
模板引擎Thymeleaf/FreeMarker/Velocity 分離 HTML 模板,支援國際化、片段重用、條件渲染。
附件大小單檔 < 10MB,總請求 < 25MB (Gmail/Outlook 限制)。大檔案上傳雲端儲存,郵件僅附下載連結 (簽名 URL、過期時間)。
測試開發/測試環境使用 **MailHog / FakeSMTP / GreenMail** 攔截郵件,避免誤發真實用戶;單元測試 Mock `JavaMailSender`。

常見 SMTP 服務商設定參考

服務商HostPort加密備註
Gmailsmtp.gmail.com587STARTTLS需開啟「低安全性應用程式存取」或使用應用程式密碼
Outlook/Office 365smtp.office365.com587STARTTLS支援 OAuth2 現代驗證
QQ 郵箱smtp.qq.com465/587SSL/STARTTLS需開啟 SMTP 服務生成授權碼
163/126smtp.163.com465/994SSL需開啟客戶端授權密碼
Amazon SESemail-smtp.region.amazonaws.com587/25/2587STARTTLS需驗證域名/郵箱、申請移出沙箱
SendGridsmtp.sendgrid.net587STARTTLSAPI Key 作為密碼 (用戶名 `apikey`)

除錯技巧

  • `session.setDebug(true)`:輸出完整 SMTP 交互日誌 (指令/回應/標頭/內容)。
  • 檢查 `AuthenticationFailedException`:帳號/密碼錯誤、未開啟 SMTP、需應用程式密碼、IP 被封鎖。
  • `MessagingException: Could not connect to SMTP host`:防火牆/安全組阻擋埠、DNS 解析失敗、SSL/TLS 協定不匹配 (Java 8 預設關閉 TLSv1.3,需 `-Djdk.tls.client.protocols=TLSv1.2,TLSv1.3`)。
  • 中文檔名亂碼:`MimeUtility.encodeText(filename, "UTF-8", "B")` (RFC 2047 B 編碼)。
  • 郵件進垃圾桶:檢查 SPF/DKIM/DMARC、內容關鍵字、IP 信譽、退信處理、List-Unsubscribe 標頭。
Jakarta Mail 是 Java 郵件發送基石。**核心模式**:`Session` + `Authenticator` → `MimeMessage` + `MimeMultipart(mixed/related/alternative)` → `Transport.send()`。生產環境**必做**:連線池、異步佇列重試、模板引擎、SPF/DKIM/DMARC、雲端郵件服務 (SES/SendGrid) 替代自建 SMTP。Spring Boot `spring-boot-starter-mail` 提供開箱即用整合,配合 `@Async`、`JavaMailSender`、`Thymeleaf` 即可構建企業級通知系統。
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved