You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用JavaMail API递归提取EML文件中嵌套.msg附件及子附件

我刚好处理过类似的嵌套附件提取场景,给你一套基于JavaMail API的完整实现,还搭配了Apache POI来解析.msg文件里的嵌套附件,直接就能用:

第一步:添加依赖

先把需要的Maven依赖加上(Gradle的话可以自行转换),JavaMail用来读取EML,POI-Scratchpad用来解析.msg文件:

<!-- JavaMail API 处理EML邮件 -->
<dependency>
    <groupId>com.sun.mail</groupId>
    <artifactId>javax.mail</artifactId>
    <version>1.6.2</version>
</dependency>
<!-- Apache POI 解析.msg格式附件 -->
<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi-scratchpad</artifactId>
    <version>5.2.3</version>
</dependency>
第二步:完整提取代码

下面是一个工具类,包含递归提取EML中所有附件(包括.msg里的多层嵌套附件)的逻辑,注释写得很清楚:

import org.apache.poi.hsmf.MAPIMessage;
import org.apache.poi.hsmf.exceptions.ChunkNotFoundException;

import javax.mail.*;
import javax.mail.internet.MimeBodyPart;
import javax.mail.internet.MimeMultipart;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;

public class EmlAttachmentExtractor {

    // 附件保存的根目录,可以自行修改
    private static final String SAVE_ROOT = "./extracted_eml_attachments";

    public static void main(String[] args) throws Exception {
        // 初始化保存目录
        File rootDir = new File(SAVE_ROOT);
        if (!rootDir.exists()) {
            rootDir.mkdirs();
        }

        // 替换成你的EML文件路径
        File targetEml = new File("./your_email.eml");
        Session mailSession = Session.getDefaultInstance(new Properties());
        Message emlMessage = new MimeMessage(mailSession, targetEml.toURI().toURL().openStream());

        // 启动递归提取流程
        traverseMessageParts(emlMessage, rootDir);
        System.out.println("✅ 所有附件提取完成!保存路径:" + rootDir.getAbsolutePath());
    }

    /**
     * 递归遍历邮件的所有内容部分,处理附件
     */
    private static void traverseMessageParts(Part part, File currentDir) throws MessagingException, IOException {
        // 判断当前Part是否是附件(两种情况:明确标记为ATTACHMENT,或者有文件名)
        boolean isAttachment = Part.ATTACHMENT.equalsIgnoreCase(part.getDisposition()) 
                || (part.getFileName() != null && !part.getFileName().trim().isEmpty());

        if (isAttachment) {
            String fileName = part.getFileName();
            // 保存当前附件
            saveSingleAttachment(part, currentDir, fileName);

            // 如果是.msg文件,进一步提取其内部的附件
            if (fileName.toLowerCase().endsWith(".msg")) {
                File msgFile = new File(currentDir, fileName);
                // 为当前.msg创建单独的子目录存放其附件
                File msgAttachDir = new File(currentDir, fileName.replace(".msg", "_sub_attachments"));
                extractMsgInnerAttachments(msgFile, msgAttachDir);
            }
            return;
        }

        // 如果是Multipart类型,递归处理每个子Part
        if (part.getContent() instanceof MimeMultipart) {
            MimeMultipart multipart = (MimeMultipart) part.getContent();
            for (int i = 0; i < multipart.getCount(); i++) {
                BodyPart subPart = multipart.getBodyPart(i);
                traverseMessageParts(subPart, currentDir);
            }
        }
    }

    /**
     * 保存单个附件到指定目录,处理文件名特殊字符
     */
    private static void saveSingleAttachment(Part part, File saveDir, String fileName) throws MessagingException, IOException {
        // 替换文件名中的非法字符,避免创建文件失败
        String safeFileName = fileName.replaceAll("[^a-zA-Z0-9\\.\\-_]", "_");
        File outputFile = new File(saveDir, safeFileName);

        try (InputStream in = part.getInputStream();
             FileOutputStream out = new FileOutputStream(outputFile)) {
            byte[] buffer = new byte[4096];
            int bytesRead;
            while ((bytesRead = in.read(buffer)) != -1) {
                out.write(buffer, 0, bytesRead);
            }
            System.out.println("📦 已保存附件:" + outputFile.getAbsolutePath());
        }
    }

    /**
     * 解析.msg文件,提取其中的所有附件,支持递归处理嵌套的.msg附件
     */
    private static void extractMsgInnerAttachments(File msgFile, File saveDir) throws IOException {
        if (!saveDir.exists()) {
            saveDir.mkdirs();
        }

        try (MAPIMessage msg = new MAPIMessage(msgFile.getAbsolutePath())) {
            MAPIMessage.Attachment[] attachments = msg.getAttachmentFiles();
            if (attachments == null || attachments.length == 0) {
                System.out.println("ℹ️ 该.msg文件没有内部附件:" + msgFile.getAbsolutePath());
                return;
            }

            for (MAPIMessage.Attachment attachment : attachments) {
                // 优先取长文件名,没有的话用短文件名
                String fileName = attachment.getLongFilename();
                if (fileName == null || fileName.trim().isEmpty()) {
                    fileName = attachment.getShortFilename();
                }
                String safeFileName = fileName.replaceAll("[^a-zA-Z0-9\\.\\-_]", "_");
                File outputFile = new File(saveDir, safeFileName);

                try (FileOutputStream out = new FileOutputStream(outputFile)) {
                    out.write(attachment.getAttachmentData());
                    System.out.println("📥 已提取.msg内部附件:" + outputFile.getAbsolutePath());

                    // 如果提取出的附件还是.msg,继续递归处理
                    if (safeFileName.toLowerCase().endsWith(".msg")) {
                        extractMsgInnerAttachments(outputFile, new File(saveDir, safeFileName.replace(".msg", "_sub_attachments")));
                    }
                }
            }
        } catch (ChunkNotFoundException e) {
            System.err.println("❌ 解析.msg文件失败:" + msgFile.getAbsolutePath() + ",错误信息:" + e.getMessage());
        }
    }
}
关键说明
  1. 递归逻辑:traverseMessageParts方法会遍历EML的所有内容部分,不管嵌套多少层,只要是附件就会被保存;如果是.msg附件,会调用extractMsgInnerAttachments继续提取其内部的附件。
  2. 文件名安全处理:替换了文件名中的非法字符,避免在不同操作系统下创建文件失败。
  3. .msg解析:用Apache POI的MAPIMessage处理.msg文件,支持提取其中的附件,并且同样递归处理嵌套的.msg。
  4. 可扩展性:如果不需要解析.msg里的附件,可以直接去掉POI依赖和extractMsgInnerAttachments相关代码。

内容的提问来源于stack exchange,提问作者RRM

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.15 08:43:30