領導要求直接解析本地 `.eml` / `.msg` 郵件文件,而非通過 IMAP/POP3 連接郵箱服務器。適用於郵件歸檔、合規審計、數據遷移、離線分析等場景。
<dependency>
<groupId>com.sun.mail</groupId>
<artifactId>jakarta.mail</artifactId>
<version>2.0.1</version>
</dependency>
<!-- 解析 .msg (Outlook) 需額外依賴 -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-scratchpad</artifactId>
<version>5.2.3</version>
</dependency>
import jakarta.mail.*;
import jakarta.mail.internet.*;
import java.io.*;
import java.util.*;
public class LocalEmailParser {
public static void main(String[] args) throws Exception {
File emlFile = new File("D:/emails/sample.eml");
parseEml(emlFile);
}
public static void parseEml(File file) throws Exception {
// 1. 創建 Session (無需郵箱服務器)
Properties props = new Properties();
Session session = Session.getDefaultInstance(props, null);
// 2. 從文件輸入流構建 MimeMessage
try (FileInputStream fis = new FileInputStream(file)) {
MimeMessage message = new MimeMessage(session, fis);
// 3. 提取基本信息
System.out.println("主題: " + MimeUtility.decodeText(message.getSubject()));
System.out.println("發件人: " + message.getFrom()[0]);
System.out.println("收件人: " + Arrays.toString(message.getRecipients(Message.RecipientType.TO)));
System.out.println("抄送: " + Arrays.toString(message.getRecipients(Message.RecipientType.CC)));
System.out.println("發送時間: " + message.getSentDate());
System.out.println("Message-ID: " + message.getMessageID());
// 4. 遞歸解析內容 (處理 multipart/alternative/mixed)
parsePart(message);
}
}
private static void parsePart(Part part) throws Exception {
String contentType = part.getContentType();
System.out.println("\n--- Part Content-Type: " + contentType + " ---");
if (part.isMimeType("text/plain")) {
System.out.println("【純文本】: " + part.getContent());
}
else if (part.isMimeType("text/html")) {
String html = (String) part.getContent();
System.out.println("【HTML】長度: " + html.length());
// 可用 Jsoup 解析 HTML 提取純文本
}
else if (part.isMimeType("multipart/*")) {
Multipart mp = (Multipart) part.getContent();
for (int i = 0; i < mp.getCount(); i++) {
parsePart(mp.getBodyPart(i));
}
}
else if (Part.ATTACHMENT.equalsIgnoreCase(part.getDisposition())) {
// 附件處理
String fileName = MimeUtility.decodeText(part.getFileName());
System.out.println("【附件】: " + fileName + " 大小: " + part.getSize());
// 保存附件
// try (InputStream is = part.getInputStream(); FileOutputStream fos = new FileOutputStream(fileName)) { ... }
}
else {
// 其他類型 (inline image 等)
Object content = part.getContent();
System.out.println("【其他】類型: " + content.getClass().getName());
}
}
}
import org.apache.poi.hsmf.MAPIMessage;
import java.io.File;
public class MsgParser {
public static void parseMsg(File file) throws Exception {
MAPIMessage msg = new MAPIMessage(file);
System.out.println("主題: " + msg.getSubject());
System.out.println("發件人: " + msg.getDisplayFrom());
System.out.println("收件人: " + msg.getDisplayTo());
System.out.println("正文 (RTF): " + msg.getBody());
// 附件遍歷
for (int i = 0; i < msg.getAttachmentCount(); i++) {
MAPIMessage.Attachment att = msg.getAttachment(i);
System.out.println("附件: " + att.getLongFileName() + " 大小: " + att.getAttachmentSize());
}
}
}
| 難點 | 解決方案 |
|---|---|
| 編碼亂碼 (GB2312/UTF-8 混雜) | 統一使用 MimeUtility.decodeText() 解碼主題/文件名;正文指定 charset 讀取 |
| 嵌套 Multipath 結構複雜 | 遞歸遍歷 Multipart,按 Content-Type 分派處理 |
| 內嵌圖片 (cid:) 關聯 | 構建 Content-ID 到 Part 的映射表,HTML 渲染時替換 src |
| 大郵件內存溢出 | 使用 SharedFileInputStream 或臨時文件流,避免全量載入內存 |
| .msg 格式專有 | Apache POI-HSMF 僅支持舊版 .msg,新版需 Aspose.Email (商業) 或 LibreOffice 轉換 |
public interface EmailParser {
EmailContent parse(File file) throws ParseException;
}
public class EmailContent {
String subject, from, to, cc, bcc, messageId;
Date sentDate;
String textBody, htmlBody;
List attachments;
Map inlineImages; // cid -> Part
}
public class Attachment {
String fileName, contentType, contentId;
long size;
InputStream contentStream; // 延遲加載
}