文 英
(北京工业大学计算机学院 北京市朝阳区平乐园100号 100022)
摘 要 本文介绍了面向对象的邮件解析器的设计与实现原理。通过对MIME邮件结构进行简单介绍,在此基础上提出了与之相适应的邮件解析器的设计及实现方案。
关键词 面向对象;邮件;MIME;解析器;解码
0 引言
电子邮件已成为人们日常生活中通信、交流的重要手段之一,但垃圾邮件问题也日益严峻。垃圾邮件的泛滥不仅实质性地影响着个人和企业,也是对互联网营销的滥用,更是对互联网开放和自由精神的亵渎。垃圾邮件已经成为继病毒之后的第二大互联网公害,反垃圾邮件也就成了亟待解决的问题。目前最行之有效的反垃圾邮件技术是垃圾邮件过滤技术。
邮件过滤根据处理邮件时位置(以邮件队列为界)的不同,可分为前过滤和后过滤两种。前过滤是指在接收邮件的过程中(邮件还未进入MTA的邮件队列),根据邮件的发送特征和信封信息来判断该邮件是否是垃圾邮件;后过滤是指在邮件完全接收完毕后(邮件已进入MTA的邮件队列),基于对邮件内容的分析来判断邮件是否是垃圾邮件。而在对邮件进行后过滤之前,首先要对原始邮件进行分解和解码,本文设计的邮件解析器正是为了实现这些功能,即把接收到的原始邮件分解成邮件头、邮件正文和邮件附件,然后对邮件正文、附件名、附件内容等一一进行解码,为后过滤做准备工作。
1 邮件的结构
为了设计邮件解析器,首先必须了解邮件的格式结构。
1.1 邮件格式的发展
为了方便信息传输,人们规范了Internet邮件消息的结构形式。RFC 822是最早的一个标准,即标准ARPA互联网文本消息格式(standard for the format of ARPA internet text messages)。RFC822规定一个邮件由邮件头和邮件体两大部分组成。邮件头与邮件体之间以空行进行分隔,邮件头中不允许出现空行。邮件头包含与传输、投递邮件有关的基本信息,邮件体可分为邮件正文和邮件附件。
使用RFC822只能发送基本的ASCII码文本信息,邮件内容如果要包括二进制文件、声音和动画等,实现起来非常困难。MIME扩充了在RFC 822中定义的邮件格式,提供了一种可以在邮件中附加多种不同编码文件的方法,弥补了原来信息格式的不足,使得可以通过Internet邮件系统传送丰富的信息内容。
1.2 MIME邮件结构
MIME邮件也是由邮件头和邮件体两大部分组成。对照RFC822, MIME在Internet E-mail报文中增加了五个新头域,即MIME-Version, Content-Type, Content-Transfer-Encoding, Content-ID和Content-Description。
MIME-Version用来声明Internet消息主体所使用格式的版本号。Content-Type用来描述相应主体中数据的原始类型。Content-Transfer-Encoding指明了在传输主体时采用了哪种编码方式及必须用哪种解码方式将数据解码成它的原始状态。Content-ID和Content-Description头域是可选的,前者的值可被用来在多处上下文中唯一的确定MIME实体;后者允许用户增加关于主体的描述性信息。
其中multipart媒体类型,是MIME邮件的精髓。若邮件的“Content-Type”字段值为multipart,则邮件体被分为多个体部分(bodypart),每个体部分由头域、空行、体组成。因此,一个体部分在语法上类似于RFC822中的邮件(message),但是在意义上是不同的:①在体部分中实际上不需要头域,因此一个以空行开始的体部分是允许的,它被解释成缺省值的头域。②RFC822邮件和体部分的区别是微妙的、不重要的。但是它们两个的区别必须被邮件解析器程序理解。③相似的语法便于邮件到体部分的转化,反之亦然。
常见的multipart类型有三种:multipart/mixed, multipart/related和 multipart/alternative。它们之间的层次关系可以归纳如图1所示。
如果在邮件中要添加附件,必须定义multipart/mixed段;如果存在内嵌资源,至少要定义multipart/related段;如果纯文本与超文本共存,至少要定义multipart/alternative段。
总而言之,一个或多个不同的数据段合并在一个单一的体(body)中,实体的头域必须指定multipart类型。multipart诸类型的共同特征是,在段头指定“boundary”参数字符串,段体内的每个子段以此串定界。所有的子段都以“--”+boundary行开始,父段则以“--”+boundary+“--”行结束。段与段之间也以空行分隔。MIME邮件的整个结构其实就是递归定义的。
图1 MIME格式邮件层次图
2 邮件解析器的设计与实现
我们采用面向对象的分析和设计方法,根据邮件解析器的主要功能,首先将系统划分为分解和解码这两个相对独立的主题。然后结合邮件的结构特点,分别分析出这两大主题中的对象,再对各个对象分别进行设计。
2.1 邮件分解概述
首先,对接收到的原始邮件,从头开始读取数据进行分析,以遇到的第一个空行为依据把该邮件划分为两大部分:即邮件头和邮件体;然后,开始分析邮件头,前面有诸如From, To, ...等字段,如果读到了Mime-Version则表明为MIME格式的邮件,否则是其它格式的邮件。
对于MIME格式的邮件,继续分析邮件头,直到读到了Content-Type,如果该字段的值是multipart/mixed则表示该邮件有附件,否则就是只有邮件正文了。如果有附件的话,紧接着会有boundary,取得该边界值,保留之;接下来,在邮件体中查找boundary,以此boundary边界值为依据,将邮件体划分为多个体部分,依次是正文,第一个附件编码数据,第二个附件编码数据……。
2.2 解码概述
①对实体的解码,根据实体头域Content-Type字段值的类型可分成两种情况。
实体头域的Content-Type字段的值属于离散媒体类型,说明实体已经是原子实体,即此实体的主体已不可再划分成更小的解码单位了。这种情况解码比较简单。直接以空行为依据,把实体划分为实体头和实体主体,然后根据实体头域的Content-Transfer-Encoding字段所指明的编码方式,对实体主体进行解码。
实体头域的Content-Type字段的值属于复合媒体类型。说明此实体的主体是由多个体部分组成的。这种情况下可以递归地处理实体,对该实体主体中包含的每一个体部分进行解析、解码。下面是解析含有多个体部分的实体主体的函数MailEntityParse。
BOOL MailEntityParse(MailEntity *body)
{ for(int i=0; i<实体主体中包含的体部分的总数;i++)
{ 取出第 i个体部分;
如果此体部分的主体中内嵌多个体部分,则递归调用MailEntityParse函数;
对实体主体进行解码;
}
Return TRUE;
}
②对附件名的解码:分析附件名的编码字串,从中分别提取表示字符集、编码类型和附件名这三个字串,依据编码类型对提取出的附件名编码字串进行解码。
2.3 类的设计
设计了apartMail类,用来分解邮件,把邮件分解成邮件头、邮件正文、多个附件。设计了decodeMime类,用来解码。
2.3.1 类apartMail
class apartMail
{
private:
char *raw; //邮件原文
int rawLen; //邮件大小
char *header, *body; //邮件头和邮件体
char *contentType; //邮件头中Content-Type字段的值
char *boundary; //用来划分邮件正文和附件的依据,boundary字段的值
int attachmentCount; //附件个数
char *mimeParts[ ]; //分解后的邮件,依次存放正文,附件1,附件2…
public:
apartMail( ) //构造函数
~apartMail( ) //析构函数
void separateHeadBody( ) //将邮件分解成邮件头和邮件体
BOOL IsMimeMail( ) //判断是否是MIME格式邮件
BOOL hasAttachment( ) //判断是否有附件
void splitBody( ) //分离正文,附件
char *getTextMail( ) //获取邮件正文段
char *getAttachment( int n ) //获取邮件的第n个附件
friend void strupper( char *s ) //友元,把串s全部转换成大写
}
2.3.2 类decodeMime
class decodeMime
{
private:
char *raw; //待解码的实体
int rawLen; //实体大小
char *header; //实体头
char *body; //实体主体
char *contentType; //Content-Type字段的值
char *charset; //属性charset的值
char *name; //属性name的值
char *encoding; //Content-Transfer-Encoding字段的值
char *attachmentNameCharset; //附件名字符集
char *attachmentNameEncoding; //附件名编码类型
char *realName; //真正的附件名
public:
decodeMime( ) //构造函数
~decodeMime( ) //析构函数
void separateHeadBody( ) //划分实体头和实体主体
void setContentType( ) //设置Content-Type字段行
BOOL IsComposite ( ) //判断顶层媒体类型是否是复合媒体类型
void setEncoding( ) //设置实体主体的编码类型
int base64Decode( char *s ) //base64编码数据解码
int QPDecode ( char *s ) //quoted-printable编码数据解码
BOOL MailEntityParse() //实体主体的解码
void displayMailEntity() //输出解码后的实体主体
void decodeAttachmentName( ) //解码附件名
void displayAttachmentAttribute( ) //输出附件名,类型,附件大小
friend void strupper( char *s ) //友元,把串s全部转换成大写
}
3 结束语
本文在对MIME邮件协议标准进行研究的基础上,采用面向对象的分析和设计方法,将系统划分两大主体,若干对象,设计了一个面向对象的邮件解析器,将原始邮件分解为邮件正文和附件,并对正文、附件名、附件进行解码,为邮件后过滤做好了充分的准备工作。
在开发此邮件解析器时,我们从网上找到了各种解码算法实现的共享源代码,稍加改造、测试就能为我所用,大大减少了工作量。充分利用现有的、共享的资源,不仅减少了人力、物力的重复浪费,而且极大地提高了开发效率。
参考文献
1 RFC 822 Standard for the format of ARPA Internet text messages
2 RFC 2045 多用途Internet邮件扩展(MIME) 第一部分:Internet信息体格式
3 RFC 2046 多用途Internet邮件扩展(MIME) 第二部分:媒体类型
4 RFC 2047 MIME Part Three: Message Header Extensions for Non-ASCII Text
5刘宗田,袁兆山.Bruce Eckel.C++编程思想.第二版,机械工业出版社
3 RFC 2046 多用途Internet邮件扩展(MIME) 第二部分:媒体类型
4 RFC 2047 MIME Part Three: Message Header Extensions for Non-ASCII Text
5刘宗田,袁兆山.Bruce Eckel.C++编程思想.第二版,机械工业出版社