前言
JavaMail 是 Sun/Oracle 提供的郵件處理標準 API,支援 SMTP/POP3/IMAP 等協議。本文系統講解郵件系統架構、核心協議原理、JavaMail 核心類體系、發送接收實戰代碼、附件處理、HTML 郵件、異步發送及生產環境優化。
郵件系統架構與核心協議
1. 核心組件
- MUA (Mail User Agent):用戶代理 (Outlook、Foxmail、手機郵件客戶端)
- MTA (Mail Transfer Agent):郵件傳輸代理 (Postfix、Sendmail、Exchange) - 處理郵件路由轉發
- MDA (Mail Delivery Agent):郵件投遞代理 - 將郵件投入用戶郵箱
- 郵箱:郵件存儲容器 (Maildir、mbox、數據庫)
2. 郵件流轉流程
發件人 MUA
↓ SMTP (提交)
發件方 MTA (發件郵件服務器)
↓ SMTP (轉發, 可能經過多級 MTA)
收件方 MTA (收件郵件服務器)
↓ MDA (投遞)
收件人郵箱
↓ POP3/IMAP (讀取)
收件人 MUA
3. 核心協議對比
| 協議 | 全稱 | 端口 | 用途 | 特點 |
| SMTP |
Simple Mail Transfer Protocol |
25/465/587 |
發送/轉發 |
單向、推送模式、需認證 |
| POP3 |
Post Office Protocol v3 |
110/995 |
接收/下載 |
下載到本地、默認刪除服務器、離線讀取 |
| IMAP |
Internet Message Access Protocol |
143/993 |
接收/同步 |
服務器同步、多設備一致、支援文件夾、旗標 |
4. SSL/TLS 加密端口
- SMTP:465 (隱式 SSL)、587 (STARTTLS 顯式 TLS)
- POP3:995 (SSL)
- IMAP:993 (SSL)
JavaMail 核心 API 體系
1. 四大核心類
| 類別 | 角色 | 核心方法 |
Session |
環境上下文、配置管理、連接工廠 |
getInstance(), getTransport(), getStore() |
Message |
郵件對象 (MimeMessage) |
setFrom(), setRecipients(), setSubject(), setContent() |
Transport |
發送服務 (SMTP) |
connect(), sendMessage(), close() |
Store |
接收服務 (POP3/IMAP) |
connect(), getFolder(), close() |
2. MimeMessage 結構
MimeMessage
├── Headers (From, To, Subject, Date, Content-Type...)
├── Content
│ ├── Simple: text/plain
│ ├── HTML: text/html
│ ├── Attachment: multipart/mixed
│ │ ├── Part 1: text/html (正文)
│ │ ├── Part 2: application/pdf (附件1)
│ │ └── Part 3: image/png (附件2)
│ └── Inline Image: multipart/related
│ ├── Part 1: text/html (引用 cid:xxx)
│ └── Part 2: image/png (Content-ID: <xxx>)
└── ...
發送郵件實戰
1. 基礎文本郵件
public class MailSender {
private final Session session;
private final String username;
private final String password;
public MailSender(String host, int port, String username, String password, boolean ssl) {
Properties props = new Properties();
props.put("mail.transport.protocol", "smtp");
props.put("mail.smtp.host", host);
props.put("mail.smtp.port", port);
props.put("mail.smtp.auth", "true");
if (ssl) {
props.put("mail.smtp.socketFactory.class", "javax.net.ssl.SSLSocketFactory");
props.put("mail.smtp.socketFactory.port", port);
} else {
props.put("mail.smtp.starttls.enable", "true"); // STARTTLS
}
this.session = Session.getInstance(props);
this.session.setDebug(true); // 開發調試
this.username = username;
this.password = password;
}
public void sendText(String from, String to, String subject, String text) throws MessagingException {
MimeMessage msg = new MimeMessage(session);
msg.setFrom(new InternetAddress(from));
msg.setRecipient(Message.RecipientType.TO, new InternetAddress(to));
msg.setSubject(subject, "UTF-8");
msg.setText(text, "UTF-8");
msg.setSentDate(new Date());
send(msg);
}
private void send(MimeMessage msg) throws MessagingException {
try (Transport transport = session.getTransport()) {
transport.connect(username, password);
transport.sendMessage(msg, msg.getAllRecipients());
}
}
}
2. HTML 郵件與內嵌圖片
public void sendHtmlWithInlineImage(String from, String to, String subject,
String htmlContent, Map<String, File> inlineImages)
throws MessagingException {
MimeMessage msg = new MimeMessage(session);
msg.setFrom(new InternetAddress(from));
msg.setRecipients(Message.RecipientType.TO, InternetAddress.parse(to));
msg.setSubject(subject, "UTF-8");
// 根容器:related (正文 + 內嵌資源)
MimeMultipart root = new MimeMultipart("related");
// 正文部分
MimeBodyPart htmlPart = new MimeBodyPart();
htmlPart.setContent(htmlContent, "text/html;charset=UTF-8");
root.addBodyPart(htmlPart);
// 內嵌圖片
for (Map.Entry<String, File> entry : inlineImages.entrySet()) {
MimeBodyPart imgPart = new MimeBodyPart();
imgPart.attachFile(entry.getValue());
imgPart.setContentID("<" + entry.getKey() + ">"); // CID
imgPart.setDisposition(MimeBodyPart.INLINE);
root.addBodyPart(imgPart);
}
msg.setContent(root);
send(msg);
}
// HTML 中引用:
對應 Content-ID:
3. 附件郵件 (混合模式)
public void sendWithAttachments(String from, String to, String subject,
String htmlContent, List<File> attachments)
throws MessagingException {
MimeMessage msg = new MimeMessage(session);
msg.setFrom(new InternetAddress(from));
msg.setRecipients(Message.RecipientType.TO, InternetAddress.parse(to));
msg.setSubject(subject, "UTF-8");
// 外層:mixed (正文 + 附件)
MimeMultipart mixed = new MimeMultipart("mixed");
// 內層:alternative (純文本 + HTML) 或 related (HTML + 內嵌圖)
MimeMultipart alternative = new MimeMultipart("alternative");
MimeBodyPart textPart = new MimeBodyPart();
textPart.setText("請使用支持 HTML 的郵件客戶端查看", "UTF-8");
alternative.addBodyPart(textPart);
MimeBodyPart htmlPart = new MimeBodyPart();
htmlPart.setContent(htmlContent, "text/html;charset=UTF-8");
alternative.addBodyPart(htmlPart);
// 將 alternative 加入 mixed
MimeBodyPart contentPart = new MimeBodyPart();
contentPart.setContent(alternative);
mixed.addBodyPart(contentPart);
// 添加附件
for (File file : attachments) {
MimeBodyPart attachPart = new MimeBodyPart();
attachPart.attachFile(file);
attachPart.setFileName(MimeUtility.encodeText(file.getName(), "UTF-8", "B"));
mixed.addBodyPart(attachPart);
}
msg.setContent(mixed);
send(msg);
}
4. 群發與抄送
msg.setRecipients(Message.RecipientType.TO, InternetAddress.parse("a@x.com, b@x.com"));
msg.setRecipients(Message.RecipientType.CC, InternetAddress.parse("cc@x.com"));
msg.setRecipients(Message.RecipientType.BCC, InternetAddress.parse("bcc@x.com")); // 密送
接收郵件實戰
1. POP3 接收
public List<MailMessage> receivePop3(String host, int port, boolean ssl,
String username, String password) throws MessagingException {
Properties props = new Properties();
props.put("mail.store.protocol", "pop3");
props.put("mail.pop3.host", host);
props.put("mail.pop3.port", port);
if (ssl) {
props.put("mail.pop3.socketFactory.class", "javax.net.ssl.SSLSocketFactory");
}
Session session = Session.getInstance(props);
List<MailMessage> mails = new ArrayList<>();
try (Store store = session.getStore("pop3")) {
store.connect(username, password);
Folder inbox = store.getFolder("INBOX");
inbox.open(Folder.READ_ONLY);
Message[] messages = inbox.getMessages();
for (int i = messages.length - 1; i >= Math.max(0, messages.length - 20); i--) {
mails.add(parseMessage(messages[i]));
}
inbox.close(false);
}
return mails;
}
2. IMAP 接收 (支援搜索、標記)
public List<MailMessage> receiveImap(String host, int port, boolean ssl,
String username, String password, String folderName)
throws MessagingException {
Properties props = new Properties();
props.put("mail.store.protocol", "imap");
props.put("mail.imap.host", host);
props.put("mail.imap.port", port);
if (ssl) props.put("mail.imap.socketFactory.class", "javax.net.ssl.SSLSocketFactory");
Session session = Session.getInstance(props);
try (Store store = session.getStore("imap")) {
store.connect(username, password);
Folder folder = store.getFolder(folderName);
folder.open(Folder.READ_WRITE); // 可寫入標記
// 搜索未讀郵件
FlagTerm unreadTerm = new FlagTerm(new Flags(Flags.Flag.SEEN), false);
Message[] messages = folder.search(unreadTerm);
List<MailMessage> mails = new ArrayList<>();
for (Message msg : messages) {
mails.add(parseMessage(msg));
// 標記為已讀
msg.setFlag(Flags.Flag.SEEN, true);
}
folder.close(false);
return mails;
}
}
3. 郵件解析通用方法
private MailMessage parseMessage(Message msg) throws MessagingException, IOException {
MailMessage mail = new MailMessage();
mail.setSubject(MimeUtility.decodeText(msg.getSubject()));
mail.setFrom(InternetAddress.toString(msg.getFrom()));
mail.setSentDate(msg.getSentDate());
mail.setContent(parsePart(msg));
return mail;
}
private String parsePart(Part part) throws MessagingException, IOException {
if (part.isMimeType("text/plain")) {
return (String) part.getContent();
} else if (part.isMimeType("text/html")) {
return (String) part.getContent();
} else if (part.isMimeType("multipart/*")) {
Multipart mp = (Multipart) part.getContent();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < mp.getCount(); i++) {
sb.append(parsePart(mp.getBodyPart(i)));
}
return sb.toString();
}
return "";
}
生產環境優化與最佳實踐
1. 連接池管理
// 使用 Apache Commons Pool 2 管理 Transport/Store 連接
public class MailSessionPool {
private final GenericObjectPool transportPool;
private final GenericObjectPool storePool;
// 初始化池配置...
public Transport borrowTransport() throws Exception {
return transportPool.borrowObject();
}
public void returnTransport(Transport transport) {
transportPool.returnObject(transport);
}
}
2. 異步發送 (生產者-消費者)
@Service
public class AsyncMailService {
private final BlockingQueue queue = new LinkedBlockingQueue<>(10000);
private final ExecutorService senderPool = Executors.newFixedThreadPool(4);
@PostConstruct
public void init() {
for (int i = 0; i < 4; i++) {
senderPool.submit(this::consume);
}
}
public void sendAsync(MailTask task) {
queue.offer(task); // 非阻塞
}
private void consume() {
while (true) {
try {
MailTask task = queue.take();
mailSender.send(task.getMessage());
} catch (Exception e) {
log.error("郵件發送失敗", e);
// 重試邏輯、死信隊列...
}
}
}
}
3. 重試與熔斷
@Retryable(maxAttempts = 3, backoff = @Backoff(delay = 1000, multiplier = 2))
public void sendWithRetry(MimeMessage msg) throws MessagingException {
try (Transport transport = session.getTransport()) {
transport.connect(username, password);
transport.sendMessage(msg, msg.getAllRecipients());
} catch (MessagingException e) {
// 記錄失敗、觸發熔斷...
throw e;
}
}
4. 安全配置清單
- ✅ 必須使用 SSL/TLS (465/587/993/995)
- ✅ 使用授權碼而非登錄密碼 (Gmail、QQ、163 等)
- ✅ 驗證發件人域名 SPF/DKIM/DMARC 記錄
- ✅ 限制發送頻率,避免被判定為垃圾郵件
- ✅ 敏感信息 (密碼/授權碼) 存放在配置中心/密保箱,不入代碼倉庫
常見問題排查
| 異常 | 原因 | 解決 |
| AuthenticationFailedException |
密碼錯誤/未開啟 SMTP/需授權碼 |
檢查郵箱設置、使用授權碼、開啟 SMTP 服務 |
| MessagingException: Could not connect to SMTP host |
網絡不通/端口錯誤/防火牆 |
telnet host port 測試、檢查 SSL/TLS 端口 |
| javax.mail.SendFailedException: Invalid Addresses |
郵箱格式錯誤/編碼問題 |
使用 InternetAddress.parse()、MimeUtility.encodeText() |
| 附件中文名亂碼 |
文件名編碼缺失 |
MimeUtility.encodeText(filename, "UTF-8", "B") |
| 大附件發送失敗 |
超過郵件服務器限制 (通常 25-50MB) |
分卷壓縮、雲存儲鏈接、分片上傳 |
總結
- Session:線程安全,全局單例,配置協議/主機/端口/認證
- MimeMessage:構建郵件結構,支援文本/HTML/附件/內嵌圖片/多部分
- Transport:SMTP 發送,連接認證發送關閉
- Store/Folder:POP3/IMAP 接收,支援搜索/標記/文件夾管理
- 生產級:連接池、異步隊列、重試熔斷、監控告警、安全合規