企業級郵件通知常需同時支援 HTML 排版、內嵌圖片 (CID 引用)、多格式附件。本文基於 Jakarta Mail (原 JavaMail) 實現完整的多層次 MIME 結構構建器。
multipart/mixed (最外層:混合容器)
├── multipart/related (關聯容器:HTML + 內嵌資源)
│ ├── text/html (HTML 正文,含
)
│ └── image/jpeg (內嵌圖片,Content-ID: <logo>)
├── application/pdf (附件 1,Content-Disposition: attachment)
└── application/octet-stream (附件 2,Content-Disposition: attachment)
public MimeBodyPart buildHtmlWithInlineImage(String html, String imagePath) throws MessagingException {
MimeBodyPart htmlPart = new MimeBodyPart();
htmlPart.setContent(html, "text/html; charset=utf-8");
MimeBodyPart imagePart = new MimeBodyPart();
DataSource fds = new FileDataSource(imagePath);
imagePart.setDataHandler(new DataHandler(fds));
imagePart.setHeader("Content-ID", "<logo>"); // 必須包含尖括號
imagePart.setDisposition(MimeBodyPart.INLINE);
MimeMultipart related = new MimeMultipart("related");
related.addBodyPart(htmlPart);
related.addBodyPart(imagePart);
MimeBodyPart contentPart = new MimeBodyPart();
contentPart.setContent(related);
return contentPart;
}
public MimeBodyPart buildAttachment(String filePath) throws MessagingException {
MimeBodyPart attachment = new MimeBodyPart();
DataSource fds = new FileDataSource(filePath);
attachment.setDataHandler(new DataHandler(fds));
// MimeUtility.encodeText 解決中文文件名編碼問題
attachment.setFileName(MimeUtility.encodeText(fds.getName(), "utf-8", "B"));
attachment.setDisposition(MimeBodyPart.ATTACHMENT);
return attachment;
}
public MimeMessage createMimeMessage(Session session, MailConfig config) throws MessagingException {
MimeMessage msg = new MimeMessage(session);
msg.setFrom(new InternetAddress(config.from, config.fromName, "utf-8"));
msg.setRecipients(Message.RecipientType.TO, parseAddresses(config.to));
msg.setSubject(config.subject, "utf-8");
msg.setSentDate(new Date());
// 最外層 mixed 容器
MimeMultipart mixed = new MimeMultipart("mixed");
// 1. 添加 HTML+圖片關聯部分
mixed.addBodyPart(buildHtmlWithInlineImage(config.html, config.inlineImagePath));
// 2. 添加多個附件
for (String path : config.attachments) {
mixed.addBodyPart(buildAttachment(path));
}
msg.setContent(mixed);
msg.saveChanges(); // 重要:生成 Message-ID 等頭部
return msg;
}
// SMTP 會話配置
Properties props = new Properties();
props.put("mail.smtp.host", "smtp.exmail.qq.com");
props.put("mail.smtp.port", "465");
props.put("mail.smtp.auth", "true");
props.put("mail.smtp.ssl.enable", "true"); // SSL 直連
props.put("mail.smtp.socketFactory.class", "javax.net.ssl.SSLSocketFactory");
props.put("mail.debug", "false");
Session session = Session.getInstance(props, new Authenticator() {
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(username, authCode); // 授權碼非登錄密碼
}
});
// 發送
Transport.send(message); // 或 Transport.send(message, addresses) 指定收件人
| 問題 | 解決方案 |
|---|---|
| 中文附件名亂碼 | `MimeUtility.encodeText(name, "utf-8", "B")` (B=Base64, Q=Quoted-Printable) |
| 內嵌圖片不顯示 | Content-ID 必須含尖括號 ` |
| 大附件發送超時 | 調大 `mail.smtp.connectiontimeout`/`timeout`,或分片上傳雲存儲發鏈接 |
| SSL/TLS 握手失敗 | JDK 8+ 需導入郵件服務器證書到 cacerts,或信任所有 (測試用) |
| 群發性能差 | 使用 `Transport.sendMessage(msg, allAddresses)` 單連接發送,或異步隊列 |
<html>
<body style="font-family: 'Microsoft YaHei', sans-serif;">
<div style="border:1px solid #ddd; padding:20px; max-width:600px;">
<h2>訂單確認通知</h2>
<p>親愛的 ${userName},您的訂單 <strong>${orderNo}</strong> 已確認。</p>
<img src="cid:logo" alt="公司Logo" style="max-width:150px;">
<table>...訂單明細...</table>
</div>
</body>
</html>
建議使用 Thymeleaf/FreeMarker 渲染模板,分離業務邏輯與視圖。