邮件解析
MIME,英文全称为“Multipurpose Internet Mail Extensions”,即多用途互联网邮件扩展,是目前互联网电子邮件普遍遵循的邮件技术规范。 MIME在 RFC 822的基础上对电子邮件规范做了大量的扩展,引入了新的格式规范和编码方式,在MIME的支持下,图像、声音、动画等二进制文件都可方便的通过电子邮件来进行传递,极大地丰富了电子邮件的功能。目前互联网上使用的基本都是遵循MIME规范的电子邮件。
了解了电子邮件的规范后,接下来就是邮件的解析了。下面来看看一封邮件的结构:
简单的来看,一封邮件包含两个部分,一部分是邮件头信息,另一部分是邮件内容信息。一个Content Body还包含着一个Body Object,上图中的英文详细的对邮件的结构进行了说明。很显然邮件是一个嵌套的结构:
至于嵌套多少层?不太清楚。邮件的格式很多,每种邮件格式的嵌套情况以及正文所在部分也不尽相同。常用的邮件格式及处理方法如下:
对于其他格式邮件的处理只能死马当做活马医了。
中文乱码处理
引起中文乱码的原因:
1.由MIME编码引起的乱码。
SMTP邮件传输协议只支持传输7bit的字节流(每个字节的最高位被强制转换为0), 这就决定了SMTP协议只能传输简单的ASCII字符集的纯文本英文邮件。在发送非ASCII码的邮件内容时,为了保证信息不丢失,邮件均按照MIME规范使用Base64或OP进行编码。因此,如在获取邮件相关内容时不进行相应的解码,就会引起乱码。
其实Base64我们经常用到,例如:
迅雷专用地址:thunder://QUFodHRwOi8vd3d3LmJhaWR1LmNvbS9pbWcvc3NsbTFfbG9nby5naWZaWg==
Base64解密后:AAwww.baidu.com/img/sslm1_logo.gifZZ
其实整个过程如下:
①在地址的前后分别添加AA和ZZ。
②对新的字符串进行Base64编码。
③在上面得到的字符串前加上“thunder://”。
2.由邮件内容字符编码引起的乱码
中文邮件本身可能采用各种不同的编码方式,通常中文编码方式有:“GB2312”、“IS08859—1”等。而在Java中,默认的编码方式为Unieode编码方式。如果在获取邮件相关内容时不指定相应的编码方式,同样会引起乱码。
注:目前很多邮件服务提供商都支持用户昵称如:张三zhangsan@163.com,因此对邮件地址也需要进行Base64处理,附件的文件名样。
邮件解析类代码如下:
package edu.cie;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.text.SimpleDateFormat;
import java.util.Vector;
import javax.mail.BodyPart;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.Multipart;
import javax.mail.Part;
import javax.mail.internet.MimeUtility;
public class StrikerMessage {
private String subject="";
private String from="";
private String to="";
private String date="";
private SimpleDateFormat sdf = new SimpleDateFormat("yy年MM月dd日 HH时mm分ss秒");
private int size=0;
private String text="";
private boolean readFlag=false;
private Vector<BodyPart> bodyPart= new Vector<BodyPart>();
private Vector<String> fileName = new Vector<String>();
private static boolean haveFile = false;
final String PATH = "C://JustLook//temp//";
public StrikerMessage(Message msg){
fileName.removeAllElements();
try {
subject=msg.getSubject();
from=msg.getFrom()[0].toString();
//目前只对GBK编码处理,其他编码的处理也相当简单。需要的话自己添加吧
if(from.startsWith("=?gbk?B?")||from.startsWith("=?GBK?B?")){
String str=from.substring(8,from.indexOf("?= "));
String box=from.substring(from.indexOf('<'), from.indexOf('>'));
from=base64Decoder(str)+box;
}
date=sdf.format(msg.getSentDate()).toString();
size=msg.getSize();
} catch (MessagingException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
text = getBody(msg);
haveFile = hasAttachment(msg);
if(haveFile){
fileName = getFileName();
}
}
public String getBody(Part part){
StringBuffer sb = new StringBuffer();
sb.append(new String(""));
try{
/**
* 纯文本或者html格式的,可以直接解析掉
*/
if (part.isMimeType("text/plain") || part.isMimeType("text/html")){
sb.append(part.getContent());
}
else if (part.isMimeType("multipart/*")){
/**
* 可供选择的,一般情况下第一个是plain,第二个是html格式的
*/
if (part.isMimeType("multipart/alternative")){
Multipart mp = (Multipart) part.getContent();
int index = 0;// 兼容不正确的格式,返回第一个部分
if (mp.getCount() > 1){
index = 1;// 第2个部分为html格式的哦~
}
Part tmp = mp.getBodyPart(index);
sb.append(tmp.getContent());
}
else if (part.isMimeType("multipart/related")){
/** *//**
* related格式的,那么第一个部分包含了body,里面含有内嵌的内容的链接.
*/
Multipart mp = (Multipart) part.getContent();
Part tmp = mp.getBodyPart(0);
String bodyStr = getBody(tmp);
int count = mp.getCount();
/**
* 要把那些可能的内嵌对象都先读出来放在本地磁盘上,然后在替换相对地址为绝对地址
*/
for (int k = 1; count > 1 && k < count; k++){
Part att = mp.getBodyPart(k);
String attname = att.getFileName();
attname = MimeUtility.decodeText(attname);
double add = Math.random()*10000;
String fileName = attname.substring(0, attname.indexOf("."))+add+attname.substring(attname.indexOf("."));
try {//保存内嵌对象到本地磁盘中
File path = new File(PATH);
if(!path.exists()){
path.mkdirs();
}
File attFile = new File(PATH, fileName);
FileOutputStream fileoutput = new FileOutputStream(
attFile);
InputStream is = att.getInputStream();
BufferedOutputStream outs = new BufferedOutputStream(
fileoutput);
byte b[] = new byte[att.getSize()];
is.read(b);
outs.write(b);
outs.close();
}catch (Exception e){
System.out.println("Error occurred when to get the photos from server");
}
//替换相对地址为绝对地址
String Content_ID[] = att.getHeader("Content-ID");
if (Content_ID != null && Content_ID.length > 0){
String cid_name = Content_ID[0].replaceAll("<", "")
.replaceAll(">", "");
String cidname = "cid:"+cid_name;
String []temp = cidname.split("//$");
StringBuffer regex = new StringBuffer();
regex.append("/"");
for(int i = 0; i<temp.length; i++){
regex.append(temp[i]);
if(i!=temp.length-1)
regex.append("//$");
}
regex.append("/"");
bodyStr = bodyStr.replaceAll(regex.toString(), "file:///"+PATH+fileName);
}
}
sb.append(bodyStr);
return sb.toString();
}
else {
/**
* 其他multipart/*格式的如mixed格式,那么第一个部分包含了body,用递归解析第一个部分就可以了
*/
Multipart mp = (Multipart) part.getContent();
Part tmp = mp.getBodyPart(0);
return getBody(tmp);
}
}
else if (part.isMimeType("message/rfc822")){
return getBody((Message) part.getContent());
}
else{
/** *//**
* 否则的话,死马当成活马医,直接解析第一部分,呜呜~
*/
Object obj = part.getContent();
if (obj instanceof String){
sb.append(obj);
} else{
Multipart mp = (Multipart) obj;
Part tmp = mp.getBodyPart(0);
return getBody(tmp);
}
}
}catch(Exception e){
System.out.println("解析正文出错!");
}
return sb.toString();
}
public String getBody(Message msg){
return getBody((Part) msg);
}
public boolean hasAttachment(Message msg){
boolean flag = false;
Part part = (Part) msg;
try{
if (part.isMimeType("multipart/*")){
Multipart mp = (Multipart) part.getContent();
int count = mp.getCount();
for (int i = 0; i < count; i++){
BodyPart tmp = mp.getBodyPart(i);
if (isAttachment(tmp)){
flag = true;
bodyPart.add(tmp);
String temp=tmp.getFileName().trim();//得到未经处理的附件名字
temp =MimeUtility.decodeText(temp);
if(temp.startsWith("=?gbk?")||temp.startsWith("=?GBK?")){
String s=temp.substring(8,temp.indexOf("?=")).trim();//去到header和footer 1cXTwi50eHQ=
fileName.addElement(base64Decoder(s));//1cXTwi50eHQ=
}
else {
fileName.addElement( temp );
}
}
}
/* for(int index =0; index<fileName.size(); index++)
System.out.println("文件名:"+fileName.get(index));*/
}
} catch (Exception e){
flag = false;
}
return flag;
}
public boolean isAttachment(Part part){
boolean flag = false;
try{
String disposition = part.getDisposition();
if ((disposition != null)&&( (disposition.equals(Part.ATTACHMENT)) || (disposition
.equals(Part.INLINE)) ) )
flag = true;
} catch (Exception e){
/** *//**
* 上面的方法只是适合于附件不是中文,或者中文名较短的情况,<br>
* 附件中文名过长时就不能用了,因为javamail Api中的part.getDisposition()根本就不能得到正确的数据了<br>
* 捕获异常后,分别再详细判断处理
*/
String contentType = "";
try{
contentType = part.getContentType();
} catch (MessagingException e1){
flag = false;
}
if (contentType.startsWith("application/octet-stream")){
flag = true;
}else{
flag = false;
}
}
return flag;
}
//base64解码
private String base64Decoder(String s) throws Exception
{
sun.misc.BASE64Decoder decoder = new sun.misc.BASE64Decoder();
byte[] b=decoder.decodeBuffer(s);
return(new String(b));
}
//部分get用不到,暂时预留着。
public Vector<BodyPart> getPart() {
return bodyPart;
}
public Vector<String> getFileName() {
return fileName;
}
public String getSubject() {
return subject;
}
public String getFrom() {
return from;
}
public String getTo() {
return to;
}
public String getDate() {
return date;
}
public String getSize() {
return ""+size;
}
public String getText() {
return text;
}
public boolean isReadFlag() {
return readFlag;
}
public void setSubject(String subject) {
this.subject = subject;
}
public void setFrom(String from) {
this.from = from;
}
public void setTo(String to) {
this.to = to;
}
public void setDate(String date) {
this.date = date;
}
public void setText(String text) {
this.text = text;
}
public void setReadFlag(boolean readFlag) {
this.readFlag = readFlag;
}
public static boolean isHaveFile() {
return haveFile;
}
}
使用这个邮件解析类完成的程序,经过测试支持目前常用的邮件类型,基本满足了一个Email客户端的要求,因为个人时间关系没有将它做的很精致,对存在的问题都有了具体的解决方法或者思路,但没有实际的去处理。
存在的问题:
1.特殊邮件无法解析:因为对于其他格式邮件“死马当作活马医”,不可避免的出现问题。在后续的使用中发现,解析邮件正文为网页的邮件有问题,比如:很多网站的邮件,正文部分就是一个网页,对这类邮件无法解析。要解决这样的问题,需要了解此类邮件的结构嵌套情况,然后再采取相应的处理便可,预估计需要的时间不短,因此就没有继续去做了。
2.发件人地址乱码:邮件解析类只是对GBK中文编码的地址进行Base64解码,大多邮件服务商都是使用此类编码,但还是有一些使用GBK甚至UTF-8等编码,因此对此类邮件发件人地址会有乱码。解决办法其实不难,和处理GBK编码同理。
3.信纸显示问题:用163邮箱测试时,发现如果邮件带信纸,在显示时会有一个无法识别的标签出现,其它正常,导出为HTML页面,问题依旧存在,而QQ邮箱的信纸则不会。问题的原因不好说可能性很多,时间关系就没做过多的分析,至于解决方法也很简单,因为删除无法识别的标签对邮件无影响,直接在邮件正文中搜索此标签,删除就行了。目前对此未处理。
4.对于内嵌邮件(在邮件中插入了图片的邮件),要把那些可能的内嵌对象都先读出来放在本地磁盘上,然后再替换相对地址为绝对地址。试想如果两封邮件中存在同名的图片,就会出现覆盖同名图片的问题,未解决这个问题,在文件名的后面加上一个随机数,一次来区别。这样解决了同屏覆盖的问题,却带来了另一个问题,本地磁盘上存储了大量的同样文件,一个就够了其他的都是垃圾。暂时没有此问题的实际解决办法,大概思路是有的,解决一个问题带来另一个问题,那就向前追溯带来问题的原因,如果为每个邮件顶一个唯一标示符,就可以解决问题,我想邮件信息内部肯定有这样的标示符,然后文件以此标示符加内嵌对象名的名称存储在本地磁盘,在下载文件前查询是否已经有次文件存在。问题就解决了。个人愚见,尚未证实。
稍后带来发送多媒体邮件的问题。
注:以上邮件解析算法,借鉴优快云上某位高人的,具体出处已经忘记了,再次对高人致歉。