Java解析eml邮件格式文件
admin 发布于:2023-02-11 11:44:03
阅读:loading
关于邮件的需求总是以邮件发送或接收为主,之前涉及的技术选型有Java Mail、Apache Commons Email、Spring Mail,由于工作上的需要对eml格式的文件进行解析,随了解了一下使用Java来解析eml格式文件的实现,所谓的eml格式是微软公司在Outlook中所使用的一种遵循RFC822及其后续扩展的文件格式,并成为各类电子邮件软件的通用格式(本地电子邮件文件存储的文件格式),它的来源是电子邮件的英文E-mail的缩写形式,可以用outlook邮箱打开,也可以用各种本地邮箱客户端打开,如Foxmail、Notes等。
经过数番资料的百科发现可以使用Java Mail、Mime4J(Apache James子项目模块)的解析为主,Apache James有一个基于一组丰富的现代高效组件的模块化体系结构,它最终提供了运行在JVM上的完整、稳定、安全和可扩展的邮件服务器集成。James由内部项目(Server、Mailet、Mailbox、Protocols、MPT)和外部项目(Hupa、Mime4J、jSieve、jSPF、jDKIM)组成,其中Mime4J是解析邮件数据文件的实现,参考如下图所示:
Apache James Mime4J提供了一个解析器,用于普通RFC822和MIME中的电子邮件流格式。解析器使用回调机制报告解析事件,例如实体标头的开始、身体等。如果您熟悉SAX公司XML解析器接口开始使用mime4j应该没有问题。Mime4j还可以用于构建电子邮件使用消息类,使用此工具mime4j自动处理对字段和正文进行解码,并使用大型临时文件附件。
(1)使用QQ邮箱编辑一封邮件发送出去,并到处为本地eml文件,该邮件中的信息(可提取出来的参数)包含有以下几处:
A. 邮件标题:
B. 邮件内容,内容部分可能是纯文本和富文本,富文本包含HTML文件、内容区域的本地图片等;
C. 收件人,可以是多个收件人,收件人区分昵称与实际的邮箱地址;
D. 抄送人,与收件人一致;
E. 密送人,与收件人一致;
F. 附件,可以有多个附件文件;
G. 发送时间,是否存在时区问题;
H. 邮件大小,邮件文件的大小;
I. Message-ID 邮件唯一ID标识;
(2)邮件内容专门构建的略复杂,收件人和抄送人均为多人;邮件为多个;邮件内容包含HTML富文本段落和本地图片文件,参考如下图所示:
(3)导入maven依赖(2023年一月初发布了0.8.9版本),经过坐标的依赖实践,发现导入apache-mime4j-examples坐标可以直接把依赖的几个模块直接给导入,实际应用中需要考虑依赖其它模块,按需排除emamples和commons-logging依赖,参考坐标如下:
<!-- https://mvnrepository.com/artifact/org.apache.james/apache-mime4j-examples -->
<dependency>
<groupId>org.apache.james</groupId>
<artifactId>apache-mime4j-examples</artifactId>
<version>0.8.9</version>
</dependency>
(4)解析实现示例:
package cn.chendd.eml;
/**
* Eml文件解析数据对象
*
* @author chendd
* @date 2023/2/11 21:40
*/
@Data
public class EmlEntry {
/**
* 原始message对象
*/
@JSONField(serialize = false)
private Message message;
/**
* 消息ID
*/
private String messageId;
/**
* 邮件主题
*/
private String subject;
/**
* 纯文本邮件内容
*/
private String textContent;
/**
* 富文本邮件内容
*/
private String htmlContent;
/**
* 邮件附件
*/
private List<MutableTriple<String , Long , InputStream>> attachments = Lists.newArrayList();
/**
* 发件人
*/
private String from;
/**
* 收件人
*/
private List<Pair<String , String>> to;
/**
* 抄送人
*/
private List<Pair<String , String>> cc;
/**
* 密送人
*/
private List<Pair<String , String>> bcc;
/**
* 邮件时间
*/
private String dateTime;
}
package cn.chendd.eml;
/**
* 基本的eml文件解析示例
*
* @author chendd
* @date 2023/2/11 19:26
*/
public class EmlBasicTest {
public static void main(String[] args) {
try (InputStream inputStream = EmlBasicTest.class.getResourceAsStream("/Java解析Eml格式文件示例.eml")) {
Message message = Message.Builder.of(inputStream).build();
EmlEntry entry = new EmlEntry();
entry.setMessage(message);
entry.setMessageId(message.getMessageId());
entry.setSubject(message.getSubject());
entry.setFrom(address2String(message.getFrom()));
entry.setTo(address2List(message.getTo()));
entry.setCc(address2List(message.getCc()));
entry.setBcc(address2List(message.getBcc()));
TimeZone timeZone = TimeZone.getTimeZone(ZoneId.of("GMT"));
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
sdf.setTimeZone(timeZone);
entry.setDateTime(sdf.format(message.getDate()));
MultipartImpl body = (MultipartImpl) message.getBody();
List<Entity> bodyParts = body.getBodyParts();
//邮件附件和内容
outputContentAndAttachments(bodyParts , entry);
System.out.println(JSON.toJSONString(entry , true));
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 递归处理邮件附件(附件区域附件、内容中的base64图片附件)、邮件内容(纯文本、html富文本)
* @param bodyParts 邮件内容体
* @param entry 数据对象
* @throws IOException 异常处理
*/
private static void outputContentAndAttachments(List<Entity> bodyParts , EmlEntry entry) throws IOException {
for (Entity bodyPart : bodyParts) {
Body bodyContent = bodyPart.getBody();
String dispositionType = bodyPart.getDispositionType();
if (ContentDispositionField.DISPOSITION_TYPE_ATTACHMENT.equals(dispositionType)) {
//正常的附件文件
BinaryBody binaryBody = (BinaryBody) bodyContent;
entry.getAttachments().add(MutableTriple.of(bodyPart.getFilename() , binaryBody.size() , binaryBody.getInputStream()));
continue;
}
if (bodyContent instanceof TextBody) {
//纯文本内容
TextBody textBody = (TextBody) bodyContent;
ContentTypeFieldLenientImpl contentType = (ContentTypeFieldLenientImpl) bodyPart.getHeader().getField(HttpHeaders.CONTENT_TYPE);
String mimeType = contentType.getMimeType();
//可动态获取内容的编码,按编码转换
if (MediaType.PLAIN_TEXT_UTF_8.toString().startsWith(mimeType)) {
entry.setTextContent(IOUtils.toString(textBody.getReader()));
}
if (MediaType.HTML_UTF_8.toString().startsWith(mimeType)) {
entry.setHtmlContent(IOUtils.toString(textBody.getReader()));
}
} else if (bodyContent instanceof Multipart) {
MultipartImpl multipart = (MultipartImpl) bodyContent;
outputContentAndAttachments(multipart.getBodyParts() , entry);
} else if (bodyContent instanceof BinaryBody) {
BinaryBody binaryBody = (BinaryBody) bodyContent;
outputContentInAttachment(bodyPart.getHeader(), binaryBody, entry);
} else {
System.err.println("【是否还存在未覆盖到的其它内容类型场景】?");
}
}
}
/**
* 处理内容中的图片附件
*
* @param header 附件头信息对象
* @param binaryBody 附件对象
* @param entry 解析数据对象
*/
private static void outputContentInAttachment(Header header, BinaryBody binaryBody, EmlEntry entry) throws IOException {
Field contentIdField = header.getField(FieldName.CONTENT_ID);
Field typeField = header.getField(FieldName.CONTENT_TYPE);
if (typeField instanceof ContentTypeField) {
ContentTypeField contentTypeField = (ContentTypeField) typeField;
if (contentTypeField.getMediaType().startsWith(MediaType.ANY_IMAGE_TYPE.type())) {
try (InputStream inputStream = binaryBody.getInputStream()) {
String base64 = Base64.getEncoder().encodeToString(IOUtils.toByteArray(inputStream));
String cid = StringUtils.substringBetween(contentIdField.getBody(), "<", ">");
String content = StringUtils.replace(entry.getHtmlContent(),
"cid:" + cid, "data:" + contentTypeField.getMimeType() + ";base64," + base64);
entry.setHtmlContent(content);
}
}
}
}
/**
* 转换邮件联系人至String
* @param addressList 邮件联系人
* @return String数据
*/
private static String address2String(MailboxList addressList) {
if (addressList == null) {
return StringUtils.EMPTY;
}
for (Address address : addressList) {
return address.toString();
}
return StringUtils.EMPTY;
}
/**
* 转换邮件联系人至list集合
* @param addressList 邮件联系人
* @return list集合
*/
private static List<Pair<String , String>> address2List(AddressList addressList) {
List<Pair<String , String>> list = Lists.newArrayList();
if (addressList == null) {
return list;
}
for (Address address : addressList) {
Mailbox mailbox = (Mailbox) address;
list.add(Pair.of(mailbox.getName() , mailbox.getAddress()));
}
return list;
}
}
(解析的JSON结果)
(HTML段落另存为文件)
(1)eml格式文件是纯文本的文件,可使用记事本、Notepad++等工具打开,所以当看到它的内容时也可以按需进行自定义解析实现;
(2)实际应用中肯定比这个要复杂的多,本篇只是一个富含多个知识细节的示例,工作中实际处理要复杂的多;
(3)某些场景下的附件名称需要特殊转码,实际的附件名称在邮件的源文件中被分割为了多段,需要合并后转码;
(4)邮件中包含回复邮件、转发邮件等多次邮件来往,需要特殊处理;
(5)邮件的内容部分包含多种内容类型,需要提供多种的解析适配程序(如:某些邮件的签名处有图片签名,混迹在内容区域,需要先解析富文本再解析二进制内容体等);
(6)本篇文章代码仅供参考,切勿直接使用,按内容类型来解析的实现应该是基于工厂模式进行的多种解析适配,示例工程源码见:源码下载.zip;
(7)【20240219补充】在实际应用中每天需要解析几百封eml格式的邮件,也陆陆续续的报出来了一些问题,部分问题参考如下:
A:收件人过多(几百个收件人)会出问题,因为默认的解析内部实现配置的参数不够大导致,所以一些参数需要自定义,比如默认设置的最大HeaderLength为10000字节,超过则将报错;
B:附件乱码问题,各种各样的邮件也会有不一样的文件,关于附件名称的提取是按规则提取编码再转码;
C:一些系统退信的邮件,再解析时未能正常解析出所需要的数据,但实际应用中并未深入摸索,因为它们不属于实际有效的邮件文件;
点赞