Java解析eml邮件格式文件


placeholder image
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是解析邮件数据文件的实现,参考如下图所示:

image.png

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富文本段落和本地图片文件,参考如下图所示:

image

(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;
    }
}

解析结果


image.png

(解析的JSON结果)

image.png

(HTML段落另存为文件)

其它说明

(1)eml格式文件是纯文本的文件,可使用记事本、Notepad++等工具打开,所以当看到它的内容时也可以按需进行自定义解析实现;

(2)实际应用中肯定比这个要复杂的多,本篇只是一个富含多个知识细节的示例,工作中实际处理要复杂的多;

(3)某些场景下的附件名称需要特殊转码,实际的附件名称在邮件的源文件中被分割为了多段,需要合并后转码;

(4)邮件中包含回复邮件、转发邮件等多次邮件来往,需要特殊处理;

(5)邮件的内容部分包含多种内容类型,需要提供多种的解析适配程序(如:某些邮件的签名处有图片签名,混迹在内容区域,需要先解析富文本再解析二进制内容体等);

(6)本篇文章代码仅供参考,切勿直接使用,按内容类型来解析的实现应该是基于工厂模式进行的多种解析适配,示例工程源码见:源码下载.zip

(7)【20240219补充】在实际应用中每天需要解析几百封eml格式的邮件,也陆陆续续的报出来了一些问题,部分问题参考如下:

A:收件人过多(几百个收件人)会出问题,因为默认的解析内部实现配置的参数不够大导致,所以一些参数需要自定义,比如默认设置的最大HeaderLength为10000字节,超过则将报错;

B:附件乱码问题,各种各样的邮件也会有不一样的文件,关于附件名称的提取是按规则提取编码再转码;

C:一些系统退信的邮件,再解析时未能正常解析出所需要的数据,但实际应用中并未深入摸索,因为它们不属于实际有效的邮件文件;


 点赞


 发表评论

当前回复:作者

 评论列表


留言区