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

JavaMail 完整安裝配置與常見問題解決指南

     編輯:Java 相關知識

前言:JavaMail 在現代應用中的定位

雖然即時通訊應用普及,但電子郵件仍是企業系統通知、帳單發送、密碼重設、行銷推播的核心基礎設施。JavaMail (現屬 Jakarta Mail,包名 `jakarta.mail`) 是 Java 平台發送/接收郵件的標準 API,支援 SMTP、IMAP、POP3、MULTIPART、S/MIME、DKIM 等協定。本文涵蓋從依賴引入、Session 設定、郵件發送/接收實作、SSL/TLS 加密、OAuth2 現代認證,到常見錯誤診斷與效能調優。

一、依賴管理 (Maven / Gradle)

Jakarta Mail 2.0+ (Java 8+、Jakarta EE 9+)




    com.sun.mail
    jakarta.mail
    2.0.1 



implementation("com.sun.mail:jakarta.mail:2.0.1")

舊版 JavaMail 1.6 (Java EE 8、javax.mail)



    com.sun.mail
    javax.mail
    1.6.2

相依傳遞說明

  • `jakarta.mail` 已內含 `jakarta.activation-api` (JAF),無需額外引入
  • 若使用舊版 `javax.mail`,需額外加入 `javax.activation:activation:1.1.1`

二、核心配置:Session 與 Properties

1. 基本 SMTP 發送設定


import jakarta.mail.*;
import jakarta.mail.internet.*;
import java.util.Properties;

public class MailConfig {
    public static Session createSmtpSession(String host, int port, 
                                            String username, String password,
                                            boolean useSsl, boolean useStartTls) {
        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", "30000");
        props.put("mail.smtp.writetimeout", "10000");
        
        if (useSsl) {
            // SMTPS (隱式 SSL,port 465)
            props.put("mail.smtp.ssl.enable", "true");
            props.put("mail.smtp.socketFactory.class", "javax.net.ssl.SSLSocketFactory");
            props.put("mail.smtp.socketFactory.port", port);
            props.put("mail.smtp.socketFactory.fallback", "false");
        } else if (useStartTls) {
            // STARTTLS (顯式 TLS,port 587)
            props.put("mail.smtp.starttls.enable", "true");
            props.put("mail.smtp.starttls.required", "true");
        }
        
        // 信任所有憑證 (僅測試用,生產需正確信任鏈)
        props.put("mail.smtp.ssl.trust", host);
        props.put("mail.smtp.ssl.protocols", "TLSv1.2 TLSv1.3");
        
        return Session.getInstance(props, new Authenticator() {
            @Override
            protected PasswordAuthentication getPasswordAuthentication() {
                return new PasswordAuthentication(username, password);
            }
        });
    }
}

2. 常見郵件服務商設定參考

服務商SMTP HostPort加密備註
Gmailsmtp.gmail.com587STARTTLS需開啟「低安全性應用程式存取」或使用應用程式密碼
Outlook/Office365smtp.office365.com587STARTTLS支援 OAuth2 現代認證
Yahoosmtp.mail.yahoo.com465/587SSL/STARTTLS需應用程式密碼
SendGridsmtp.sendgrid.net587STARTTLS用戶名: apikey,密碼: API Key
Mailgunsmtp.mailgun.org587STARTTLS需驗證網域
企業內部 Exchange內部主機25/587STARTTLS/無可能需網域帳號認證

3. IMAP/POP3 接收設定


public static Session createImapSession(String host, int port, 
                                        String username, String password, boolean useSsl) {
    Properties props = new Properties();
    props.put("mail.imap.host", host);
    props.put("mail.imap.port", port);
    props.put("mail.imap.auth", "true");
    props.put("mail.imap.connectiontimeout", "5000");
    props.put("mail.imap.timeout", "30000");
    
    if (useSsl) {
        props.put("mail.imap.ssl.enable", "true");
        props.put("mail.imap.socketFactory.class", "javax.net.ssl.SSLSocketFactory");
        props.put("mail.imap.socketFactory.port", port);
        props.put("mail.imap.socketFactory.fallback", "false");
    }
    
    return Session.getInstance(props, new Authenticator() {
        @Override
        protected PasswordAuthentication getPasswordAuthentication() {
            return new PasswordAuthentication(username, password);
        }
    });
}

三、郵件發送實作範例

1. 簡單文字郵件


public void sendSimpleMail(Session session, String from, String to, 
                           String subject, String text) throws MessagingException {
    MimeMessage message = new MimeMessage(session);
    message.setFrom(new InternetAddress(from));
    message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(to));
    message.setSubject(subject, "UTF-8");
    message.setText(text, "UTF-8");
    message.setSentDate(new Date());
    Transport.send(message);
}

2. HTML 郵件 + 內嵌圖片 + 附件 (MimeMultipart)


public void sendRichMail(Session session, String from, String to, 
                         String subject, String htmlContent, 
                         Map inlineImages, // CID -> 圖片資料
                         List attachments) throws MessagingException, IOException {
    
    MimeMessage message = new MimeMessage(session);
    message.setFrom(new InternetAddress(from));
    message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(to));
    message.setSubject(subject, "UTF-8");
    
    // 根容器:mixed (附件在最外層)
    MimeMultipart root = new MimeMultipart("mixed");
    
    // 內容層:related (HTML + 內嵌圖片)
    MimeMultipart related = new MimeMultipart("related");
    
    // HTML 文字部分
    MimeBodyPart htmlPart = new MimeBodyPart();
    htmlPart.setContent(htmlContent, "text/html; charset=UTF-8");
    related.addBodyPart(htmlPart);
    
    // 內嵌圖片
    for (Map.Entry entry : inlineImages.entrySet()) {
        MimeBodyPart imgPart = new MimeBodyPart();
        imgPart.setDataHandler(new DataHandler(new ByteArrayDataSource(entry.getValue(), "image/png")));
        imgPart.setHeader("Content-ID", "<" + entry.getKey() + ">"); // CID 引用
        imgPart.setDisposition(MimeBodyPart.INLINE);
        related.addBodyPart(imgPart);
    }
    
    // 將 related 包入 root
    MimeBodyPart contentWrapper = new MimeBodyPart();
    contentWrapper.setContent(related);
    root.addBodyPart(contentWrapper);
    
    // 附件
    for (Attachment att : attachments) {
        MimeBodyPart attPart = new MimeBodyPart();
        attPart.setDataHandler(new DataHandler(new ByteArrayDataSource(att.data(), att.mimeType())));
        attPart.setFileName(MimeUtility.encodeText(att.filename(), "UTF-8", "B")); // RFC 2047 編碼
        attPart.setDisposition(MimeBodyPart.ATTACHMENT);
        root.addBodyPart(attPart);
    }
    
    message.setContent(root);
    message.setSentDate(new Date());
    message.saveChanges(); // 重要:生成 Message-ID 等標頭
    Transport.send(message);
}

record Attachment(String filename, String mimeType, byte[] data) {}

四、郵件接收與解析 (IMAP)

1. 連線並搜尋未讀郵件


public List fetchUnreadMails(Session session, String folderName) 
        throws MessagingException {
    Store store = session.getStore("imap");
    store.connect();
    
    Folder folder = store.getFolder(folderName); // "INBOX"
    folder.open(Folder.READ_WRITE); // 需 READ_WRITE 才能設定 Flags
    
    // 搜尋未讀
    Flags seen = new Flags(Flags.Flag.SEEN);
    FlagTerm unseenTerm = new FlagTerm(seen, false);
    Message[] messages = folder.search(unseenTerm);
    
    // 取得前 50 封最新
    List result = new ArrayList<>();
    for (int i = messages.length - 1; i >= 0 && result.size() < 50; i--) {
        result.add(messages[i]);
    }
    return result;
}

2. 解析 MimeMessage 內容 (遞迴處理 Multipart)


public class MailParser {
    public static ParsedMail parse(Message msg) throws MessagingException, IOException {
        ParsedMail mail = new ParsedMail();
        mail.subject = MimeUtility.decodeText(msg.getSubject());
        mail.from = parseAddresses(msg.getFrom());
        mail.to = parseAddresses(msg.getRecipients(Message.RecipientType.TO));
        mail.sentDate = msg.getSentDate();
        mail.messageId = msg.getMessageID();
        
        extractContent(msg, mail);
        return mail;
    }
    
    private static void extractContent(Part part, ParsedMail mail) throws MessagingException, IOException {
        String contentType = part.getContentType();
        String disposition = part.getDisposition();
        
        if (part.isMimeType("text/plain") && Part.ATTACHMENT != disposition) {
            mail.textContent = (String) part.getContent();
        } else if (part.isMimeType("text/html") && Part.ATTACHMENT != disposition) {
            mail.htmlContent = (String) part.getContent();
        } else if (part.isMimeType("multipart/*")) {
            Multipart mp = (Multipart) part.getContent();
            for (int i = 0; i < mp.getCount(); i++) {
                extractContent(mp.getBodyPart(i), mail);
            }
        } else if (Part.ATTACHMENT.equalsIgnoreCase(disposition) || 
                   Part.INLINE.equalsIgnoreCase(disposition)) {
            // 附件或內嵌圖片
            String filename = MimeUtility.decodeText(part.getFileName());
            byte[] data = IOUtils.toByteArray(part.getInputStream());
            mail.attachments.add(new Attachment(filename, part.getContentType(), data, 
                Part.INLINE.equalsIgnoreCase(disposition)));
        }
    }
    
    private static List parseAddresses(Address[] addrs) {
        return Arrays.stream(addrs)
            .map(a -> ((InternetAddress) a).getAddress())
            .toList();
    }
}

class ParsedMail {
    String subject, textContent, htmlContent, messageId;
    Date sentDate;
    List from, to;
    List attachments = new ArrayList<>();
}
record Attachment(String filename, String mimeType, byte[] data, boolean inline) {}

五、進階主題:OAuth2 現代認證 (Gmail / Outlook)

傳統密碼認證正逐步被淘汰,Google、Microsoft 強制要求 OAuth2。JavaMail 支援 `XOAUTH2` SASL 機制。

Gmail OAuth2 範例


public static Session createGmailOAuth2Session(String accessToken, String email) {
    Properties props = new Properties();
    props.put("mail.smtp.host", "smtp.gmail.com");
    props.put("mail.smtp.port", "587");
    props.put("mail.smtp.auth", "true");
    props.put("mail.smtp.starttls.enable", "true");
    props.put("mail.smtp.sasl.enable", "true");
    props.put("mail.smtp.sasl.mechanisms", "XOAUTH2");
    
    return Session.getInstance(props, new Authenticator() {
        @Override
        protected PasswordAuthentication getPasswordAuthentication() {
            // XOAUTH2 使用 access token 作為密碼,用戶名為 email
            return new PasswordAuthentication(email, accessToken);
        }
    });
}

// 使用 Google OAuth2 取得 access token (需 Google Cloud 專案、OAuth 同意畫面)

Microsoft Graph / Outlook OAuth2

建議直接使用 Microsoft Graph Java SDK 而非 JavaMail SMTP,功能更完整 (郵件、行事曆、聯絡人、OneDrive)。

六、常見錯誤診斷與解決方案

錯誤訊息原因解決方案
535 5.7.8 Username and Password not accepted認證失敗檢查帳號/密碼、啟用應用程式密碼、確認 OAuth2 token 有效
421 4.7.0 Try again later / Connection timed out連線逾時/服務商限流增加 timeout、實作指數退避重試、檢查防火牆/Proxy
javax.net.ssl.SSLHandshakeException憑證不信任/協定不匹配匯入 CA 憑證到 truststore、設定 `mail.smtp.ssl.protocols=TLSv1.2 TLSv1.3`、確認主機名稱驗證
550 5.7.1 Relaying denied / SPF/DKIM/DMARC fail發件網域聲譽/認證失敗設定 SPF/DKIM/DMARC 記錄、使用信譽良好的發送服務 (SendGrid 等)
MessagingException: Could not connect to SMTP host網路不通/埠號錯誤telnet 測試連線、檢查安全群組/防火牆、確認 port 25/465/587
ClassNotFoundException: jakarta.activation.DataSource缺少 JAF 相依升級 jakarta.mail 2.0+ (內含) 或手動加入 jakarta.activation-api
OutOfMemoryError 發送大附件整封郵件載入 Heap使用 `FileDataSource` 串流、分塊上傳、增加 `-Xmx`

七、效能調優與生產就緒清單

連線池化


// JavaMail 預設不支援連線池,可使用 Apache Commons Pool 2 自行實作
// 或改用 Spring Boot Mail (內建連線池支援)
@Bean
public JavaMailSender javaMailSender() {
    JavaMailSenderImpl sender = new JavaMailSenderImpl();
    sender.setHost("smtp.example.com");
    sender.setPort(587);
    sender.setUsername("user");
    sender.setPassword("pass");
    sender.setJavaMailProperties(props);
    // Spring Boot 2.3+ 支援連線池
    props.put("spring.mail.properties.mail.smtp.connectionpool", "true");
    props.put("spring.mail.properties.mail.smtp.connectionpoolsize", "10");
    return sender;
}

非同步發送


@Async("mailExecutor")
public CompletableFuture sendAsync(MimeMessagePreparator preparator) {
    javaMailSender.send(preparator);
    return CompletableFuture.completedFuture(null);
}

// 執行緒池設定
@Bean
public ThreadPoolTaskExecutor mailExecutor() {
    ThreadPoolTaskExecutor exec = new ThreadPoolTaskExecutor();
    exec.setCorePoolSize(5);
    exec.setMaxPoolSize(20);
    exec.setQueueCapacity(100);
    exec.setThreadNamePrefix("mail-sender-");
    return exec;
}

監控指標

  • 發送成功率、平均延遲、佇列積壓
  • SMTP 回應碼分布 (2xx/4xx/5xx)
  • 連線池使用率、拒絕次數
  • 彈回率、投訴率 (配合郵件服務商 Webhook)

八、安全性最佳實務

  1. 憑證驗證:生產環境絕不使用 `mail.smtp.ssl.trust=*`,必須正確配置 TrustStore
  2. 認證資訊管理:密碼/Token 存放於 Vault、AWS Secrets Manager、Kubernetes Secrets,絕不硬編碼
  3. 輸入驗證:收件者地址白名單、主題/內容長度限制、防止 Header Injection (CRLF 過濾)
  4. 內容安全:HTML 郵件經過 Sanitizer (OWASP Java HTML Sanitizer) 防 XSS
  5. 速率限制:Token Bucket / Leaky Bucket 限制發送頻率,避免觸發服務商封鎖

總結

JavaMail/Jakarta Mail 是成熟穩定的郵件 API,關鍵在於正確配置 Session Properties (SSL/TLS/認證/逾時)、妥善處理 MimeMultipart 結構、遷移至 OAuth2 現代認證、建立 非同步發送與連線池 架構、並落實 安全性與監控。現代 Spring Boot 專案建議直接使用 `spring-boot-starter-mail` 簡化配置,並整合 `Micrometer` 監控發送指標。對於大量行銷郵件,建議委託專業 ESP (SendGrid、Mailgun、Amazon SES) 發送,應用層僅負責組裝內容與呼叫 HTTP API。

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