JavaMail API 功能強大但學習曲線陡峭。本文以問題導向方式,系統講解發送純文本/附件郵件、POP3 收信列表/刪除/已讀未讀統計、以及最棘手的 Multipart 郵件 (含 HTML、圖片、附件) 遞迴解析技巧,附完整代碼範例。
Properties props = new Properties();
props.put("mail.smtp.host", smtpHost);
Session session = Session.getDefaultInstance(props, null);
MimeMessage message = new MimeMessage(session);
message.setFrom(new InternetAddress(from));
message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(to));
message.setSubject("Hello");
message.setSentDate(new Date());
message.setText("郵件正文內容");
Transport.send(message);
核心:建立多個 MimeBodyPart,組合成 MimeMultipart 設為訊息內容。
MimeBodyPart textPart = new MimeBodyPart();
textPart.setText("正文內容");
MimeBodyPart attachPart = new MimeBodyPart();
attachPart.setText("附件文字內容", "gb2312"); // 或使用 attachFile()
Multipart mp = new MimeMultipart();
mp.addBodyPart(textPart);
mp.addBodyPart(attachPart);
message.setContent(mp);
Session session = Session.getDefaultInstance(props, null);
Store store = session.getStore("pop3");
store.connect(pop3Host, username, password);
Folder inbox = store.getFolder("INBOX");
inbox.open(Folder.READ_ONLY);
Message[] messages = inbox.getMessages();
for (int i = 0; i < messages.length; i++) {
System.out.println("第 " + i + " 封: " + messages[i].getFrom()[0] + "\t" + messages[i].getSubject());
}
int total = inbox.getMessageCount();
int unread = inbox.getUnreadMessageCount();
System.out.println("總共 " + total + " 封,未讀 " + unread + " 封");
inbox.close();
store.close();
Message msg = folder.getMessage(1);
msg.setFlag(Flags.Flag.DELETED, true); // 標記刪除
// 關閉 Folder 時 expunge=true 才會真正刪除
folder.close(true);
複雜郵件結構呈樹狀:multipart/alternative (純文本+HTML 替代) 內嵌於 multipart/related (含圖片) 中。關鍵洞見:第一層 Part 常非直接內容,需遞迴遍歷至葉節點判斷 MIME 類型。


public String getPart(Part part, int partNum, int msgNum, int displayMode)
throws MessagingException, IOException {
String contentType = part.getContentType();
if (contentType == null) return "part 無效";
ContentType ct = new ContentType(contentType);
// 葉節點:直接內容
if (ct.match("text/html") || ct.match("text/plain")) {
return "" + (String)part.getContent() + "
";
}
// 附件處理
if (partNum != 0) {
String fileName = part.getFileName();
if (fileName != null) {
// 可儲存附件: storeFile(fileName, part.getInputStream());
return "附件: " + fileName + "
";
}
}
// 遞迴處理 multipart/alternative (選擇顯示模式: 1=HTML, 2=Plain)
if (part.isMimeType("multipart/alternative")) {
Multipart mp = (Multipart)part.getContent();
String htmlContent = "", plainContent = "";
for (int i = 0; i < mp.getCount(); i++) {
BodyPart bp = mp.getBodyPart(i);
if (bp.isMimeType("text/html")) htmlContent = getPart(bp, i, 3, 1);
if (bp.isMimeType("text/plain")) plainContent = getPart(bp, i, 3, 2);
}
return (displayMode == 1) ? htmlContent : plainContent;
}
// 其他 multipart 類型 (如 related/mixed) 遞迴遍歷
if (part.isMimeType("multipart/*")) {
Multipart mp = (Multipart)part.getContent();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < mp.getCount(); i++) {
sb.append(getPart(mp.getBodyPart(i), i, msgNum, displayMode));
}
return sb.toString();
}
return "";
}
public static void storeFile(String fileName, InputStream in) throws IOException {
File file = new File(fileName);
for (int i = 0; file.exists(); i++) file = new File(fileName + i); // 避免覆蓋
try (FileOutputStream fos = new FileOutputStream(file);
BufferedOutputStream bos = new BufferedOutputStream(fos);
BufferedInputStream bis = new BufferedInputStream(in)) {
byte[] buffer = new byte[1024];
int len;
while ((len = bis.read(buffer)) != -1) bos.write(buffer, 0, len);
}
}
MimeMultipart 組合豐富內容setText(text, charset) 及附件檔名編碼 (RFC 2231)FetchProfile 預取標題避免逐封解析完整內容