JavaMail (Jakarta Mail) 採用工廠模式與協議提供者分離設計:
Session:環境上下文,持有屬性、認證器、Provider 註冊表。Transport:發送協議抽象 (SMTP, SMTP SSL, SMTP STARTTLS)。Store:接收協議抽象 (IMAP, POP3, IMAP SSL)。Message:郵件模型 (MimeMessage 支持 MIME 多部分、附件、HTML)。public class MailSender {
private final Session session;
public MailSender(String host, int port, String user, String pass, boolean ssl) {
Properties props = new Properties();
props.put("mail.smtp.host", host);
props.put("mail.smtp.port", port);
props.put("mail.smtp.auth", "true");
if (ssl) {
props.put("mail.smtp.ssl.enable", "true");
props.put("mail.smtp.socketFactory.class", "javax.net.ssl.SSLSocketFactory");
} else {
props.put("mail.smtp.starttls.enable", "true"); // STARTTLS 升級加密
}
this.session = Session.getInstance(props, new Authenticator() {
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(user, pass);
}
});
this.session.setDebug(false); // 生產關閉
}
public void send(MailMessage mail) throws MessagingException {
MimeMessage msg = new MimeMessage(session);
msg.setFrom(new InternetAddress(mail.getFrom()));
msg.setRecipients(Message.RecipientType.TO, InternetAddress.parse(mail.getTo()));
msg.setSubject(mail.getSubject(), "UTF-8");
msg.setSentDate(new Date());
// 構建多部分內容:文本 + HTML + 附件
Multipart mp = new MimeMultipart("mixed");
// 1. 文本/HTML 替代部分
MimeMultipart alt = new MimeMultipart("alternative");
alt.addBodyPart(textPart(mail.getTextContent()));
if (mail.getHtmlContent() != null) alt.addBodyPart(htmlPart(mail.getHtmlContent()));
MimeBodyPart altPart = new MimeBodyPart();
altPart.setContent(alt);
mp.addBodyPart(altPart);
// 2. 附件
for (Attachment att : mail.getAttachments()) {
mp.addBodyPart(attachmentPart(att));
}
msg.setContent(mp);
Transport.send(msg);
}
private MimeBodyPart textPart(String text) throws MessagingException {
MimeBodyPart p = new MimeBodyPart();
p.setContent(text, "text/plain; charset=UTF-8");
return p;
}
private MimeBodyPart htmlPart(String html) throws MessagingException {
MimeBodyPart p = new MimeBodyPart();
p.setContent(html, "text/html; charset=UTF-8");
return p;
}
private MimeBodyPart attachmentPart(Attachment att) throws Exception {
MimeBodyPart p = new MimeBodyPart();
DataSource ds = new ByteArrayDataSource(att.getData(), att.getMimeType());
p.setDataHandler(new DataHandler(ds));
p.setFileName(MimeUtility.encodeText(att.getFilename(), "UTF-8", "B")); // RFC 2231 編碼
p.setDisposition(Part.ATTACHMENT);
return p;
}
}
// HTML 中引用 <img src="cid:logo">
MimeBodyPart imgPart = new MimeBodyPart();
imgPart.attachFile(new File("logo.png"));
imgPart.setContentID("<logo>"); // CID
imgPart.setDisposition(MimeBodyPart.INLINE);
related.addBodyPart(imgPart); // 放入 "related" 類型 Multipart
// 使用 FileDataSource + SharedByteArrayInputStream 或自定義 DataSource
// 關鍵:不將整個文件讀入 byte[],而是流式輸出
DataSource ds = new FileDataSource(largeFile) {
public InputStream getInputStream() throws IOException {
return new BufferedInputStream(new FileInputStream(largeFile), 8192);
}
};
Apache Commons Pool 封裝 Transport 對象池。CompletableFuture.runAsync(() -> sender.send(mail)) 或接入消息隊列 (Kafka/RocketMQ) 解耦。| 風險 | 對策 |
|---|---|
| 憑證驗證繞過 | 嚴禁 mail.smtp.ssl.trust=*,生產必須配置有效 CA 或導入信任庫 |
| 郵件注入 (Header Injection) | 所有用戶輸入 (Subject, To, Body) 經過 MimeUtility.encodeText 或白名單過濾換行符 |
| 敏感信息日誌洩露 | session.setDebug(false),生產禁用协议級日誌 |
| 中間人攻擊 | 強制 STARTTLS 或 SMTPS,設置 mail.smtp.ssl.enable=true 或 mail.smtp.starttls.required=true |
Store store = session.getStore("imaps"); // IMAP SSL
store.connect(host, user, pass);
Folder inbox = store.getFolder("INBOX");
inbox.open(Folder.READ_WRITE);
// 搜索未讀
Message[] msgs = inbox.search(new FlagTerm(new Flags(Flags.Flag.SEEN), false));
for (Message m : msgs) {
parseMessage(m); // 遞歸解析 Multipart
m.setFlag(Flags.Flag.SEEN, true); // 標記已讀
}
inbox.close(true);
store.close();
JavaMail 雖屬老牌 API,但在企業級通知、報表分發、郵件網關場景仍不可替代。掌握 MimeMultipart 類型嵌套規則 (mixed/alternative/related)、流式附件處理、安全加固與異步架構,是構建穩健郵件服務的關鍵。建議封裝為 MailService 領域服務,統一異常處理、重試策略 (指數退避)、指標監控 (發送成功率、延遲、隊列積壓)。