多用途互联网邮件扩展(MIME)

多用途互联网邮件扩展(MIME)

 

目录:

一、RFC822:电子邮件的基本框架

    1.1电子邮件的结构

    1.2源地址

    1.3目的地址

    1.4日期

    1.5可选头段

    1.6用户自定义段

    1.7例子和解释

二、MIME概述

三、编码转换

    3.1 quoted-printable

    3.2  Base64

    3.3  7bit、8bit和binary

四、类型/子类型

    4.1文本类型

    4.2图象、声音和视频类型

    4.3程序数据类型

    4.4 分块类型(Multipart)

    4.5  Message类型

五、邮件头段中的字符编码

六、邮件附件

七、聚合邮件(aggregatedmessage)

    7.1 Multipart/Related

    7.2  Content-ID

    7.3 Content-Location

    7.4  多层聚合

 

 

 


    本章介绍多用途互联网邮件扩展(MIME)和RFC822的基本内容,为便于叙述,使用了一些语法描述,这些语法描述仅是为了说明概念,并非严格的语法,读者若要了解准确的语法结构,请查阅相应的RFC文档。

 

MIME是对RFC822的补充和扩展,RFC822的文件标题是“ARPA互联网文字信息的格式标准”,是1982年颁布的,这个文件制定了电子邮件的基本框架并一直沿用至今。RFC822存在一些不足,主要有两个方面,一是邮件只能使用ASCII字符,二是邮件内容只能是文字,不能是图象、声音等多媒体数据。MIME在RFC822的基础上增加了很多新内容,使得电子邮件不仅可以使用各种文字符号,也可以包含多媒体数据。

   MIME虽然是电子邮件技术标准,但它是在网络上传输多媒体数据的一个基本技术,在其它场合也经常被应用。例如,HTTP协议所传送的数据就采用了MIME的格式。又如,当我们在IE上将一张网页存为单一文件格式(*.mht)时,这种文件也是MIME格式的文件。

    MIME的内容由如下5个RFC文件描述:

1. RFC2045,Multipurpose Internet Mail Extensions:Part 1

2. RFC2046, Part 2: Media Types

3. RFC2047, Part 3: Message HeaderExtensions for Non-ASCII Text

4. RFC2048, Part 4: RegistrationProcedures

5. RFC2049, Part 5: ConformanceCriteria and Examples

让我们先从RFC822讲起。

一、RFC822:电子邮件的基本框架

1.1 电子邮件的结构

如同传统信件,电子邮件也是由信封和信件内容两部分所构成。但是电子邮件不是写在纸上的东西,而是由计算机编辑、传送和显示的一串文字字符。一封电子邮件如同我们在Windows“记事本”中看到的纯文本内容,但是其中的字符是机器“写”上去的,而不是用户输入、编辑的。

 

释疑:你可能不同意这种说法,因为邮件内容,包括收件人地址、转发地址,肯定必须由用户输入。但是我们这里讨论的是在机器中传送、存储的电子邮件,用户一般通过某种电子邮件应用程序(如Outlook Express,FoxMail等)收发、编写电子邮件,这些程序按照用户输入的信息自动产生符合RFC822及MIME标准的邮件,然后才发送出去。

 

一封电子邮件的内容是纯文本的,其中开头若干行是信件“信封”,然后是一个空行(即只有回车换行字符的一行),其后是信件正文。“信封”、信件正文都是一行行的文字,邮件中的第一个空行是“信封”部分的终结标志,其后是邮件正文,邮件正文可以包含空行,也可以没有空行,这无关紧要。RFC822规定,所有的字符都必须是ASCII字符,或者更准确地说,必须是ASCII编码的字符。注意RFC822是指定字符编码的,符合RFC822标准的电子邮件,其中所有的字符只能使用ASCII编码,不能是其它编码。

RFC822对于信件正文没有什么规定,只是要求必须由ASCII字符组成,而“信封”上的信息则是要由机器处理的,因而有着严格的语法规定。“信封”的正式名称是邮件头(header),邮件头由若干“头段”(headerfield)组成,每一头段的格式为:

    field-name : [field-body] CRLF

(注:在本章中,当描述语法格式时,我们用斜体字表示一个语法成分的名称,用方括号表示可选项,即,包含在方括号中的语法成分可用也可不用,用粗体表示必须照样书写的字符。)

其中field-name是段名,RFC822定义了常用的段名及其语义,应用程序也可以定义自己的段名及其语义,但不得与已定义的段名同名。field-body是段体,即段的具体内容。段名和段体用一个冒号分隔,最后用CRLF(回车换行)结束整个段。

    以下是一些邮件头段的例子:

    From: George Jones <Group@Host>
    Sender: Secy@Host

    Reply-To: Secy@Host

其中每一行为一个头段,其中“From”、“Sender”和“Reply-To”是段名,段名不区分大小写,每行冒号右边的文字是段体,段体的语法、含义是有规定的。

    一个头段逻辑上就是一行,但是当文字较长时,也允许换行。换行可在任何一个空格位置进行,但是要求换行之后在新行开头至少必须有一个空格或Tab,表示本行是接续上一行的。这种换行称为“折叠”。一个头段可以折叠为若干行,应用程序进行语法分析时,应去掉折叠,将每一段都整理为一行,以便分析。

    例如:Received: from m15-40.126.com (220.181.15.40)CRLF by localhost with SMTP;

    文本编辑窗口显示出来的效果:

    Received: from m15-40.126.com(220.181.15.40)

 by localhost with SMTP;

   RFC822定义的头段按其功能可分为源地址、目的地址、日期和可选段四大部分,以下分别介绍各个部分。

1.2 源地址

描述信件来源的段名有From、Sender、Return-path、Received和Reply-To,它们的语法如下:

    From:mailbox

    Sender:mailbox

    Return-path:< [route] local-part@domain >

    Reply-To: mailbox

    Received:[fromdomain][by domain][via atom][with atom]

[id msg-id][for local-part@domain]

; date-time

From用来指出发信人是谁,段体mailbox是发信人的邮箱地址,mailbox可以采用两种形式之一,一种是local-part@domain,其中local-part是用户帐号名称,domain是邮件服务器的域名。另一种形式是发信人名称后面再加上尖括号括起来的邮件地址,例如

  From:  GeorgeJones <Group@Host>

    邮箱地址中的local-part一般不能使用圆括号、尖括号、方括号、逗号、分号、冒号、@、斜杠和空格这些字符,如果要使用这些字符,则整个local-part部分必须用双引号括起来。

    如果某些程序使得信件不是从写信人的邮箱寄出,而是从另一个邮箱寄出,则必须用Sender再加上From,这时Sender的段体是寄出的邮箱地址,而From的段体是写信人的邮箱地址。

    一封电子邮件可能要经过若干次转发才能到达目的地。最后的转发站(转发邮件的计算机)可以用Return-path指出回送邮件给发信人的路由。Return-path段体中的可选项route指出路由,其形式为

   @domain_1, @domain_2, ... ..., @domian_n

其中各个domain_i是邮件服务器的域名。段体中最后的local-part@domain则是发信人的邮箱地址。

   Reply-To则是由最初发出邮件的机器填写的头段,它直接指出信件的回复地址。

    当邮件经过多次中转转发时,每一个转发站都必须在邮件开头加上Received头段,说明邮件何时从何地来,往何处去。Received段体中的可选项用来记录这些信息:

from domain说明邮件从何处来,domain是发来邮件的主机IP地址、域名或主机名;

by domain指出邮件接收者,domain是接收邮件的主机IP地址、域名或主机名;

via atom指出邮件的物理传输路径,例如通过互联网或电话网,atom是这种物理路径的名称。

with atom指出邮件的传输协议,例如SMTP、ESMTP等,atom是协议名称。

id msg-id一些转发站可能会将邮件排队,msg-id指出邮件的内部ID。

for local-part@domain指出邮件的目的地,local-part@domain是邮箱地址。

Received段最后要加上分号和时间戳date-time,记录邮件的接收时间。时间戳的格式将在1.4节详述。

    一些邮件系统允许邮件接收者将接收到的邮件转发给他人,并保留原有的邮件头。这种情况下为了与原来邮件头中的头段相区别,新加上的头段的段名前要加上Resent-,即Resent-From、 Resent-Sender或 Resent-Reply-To,它们的作用与From、Sender和Reply-To相同。

    在邮件头中各段的段体中,可以插入注释。注释用一对圆括号括起来,内容可以是任何字符串。注释是给人看的,在语法上没有其它意义。

    例1:以下是一封电子邮件邮件头中的片断:

Received: from unknown (HELO mail.scnu.edu.cn)(unknown@202.116.32.11)

 by 202.116.32.4 with SMTP;Fri, 09 Feb 2007 14:22:51 +0800

Received: from m15-40.126.com (220.181.15.40)

  by localhost with SMTP; 9Feb 2007 06:32:51 -0000

Received: from 192.168.208.33 by webmail-app40 (Coremail) ;

Fri, 9 Feb 2007 14:30:58 +0800 (CST)

From: someone@126.com

从这些头段可以知道,邮件来自someone@126.com,首先被主机webmail-app40收到,时间是2007年2月9日,星期五,当地时间14时30分58秒,当时邮件来自主机192.168.208.33。

然后邮件被主机localhost接收到,邮件来自主机m15-40.126.com,邮件通过SMTP协议接收,接收到邮件的时间是2007年2月9日,格林威治标准时间6时32分51秒。

最后邮件被主机202.116.32.4收到,邮件来自未知主机unknown,邮件通过SMTP协议接收,接收到邮件的时间是2007年2月9日,当地时间14时22分51秒。

1.3目的地址

描述邮件目的地址的头段有To、cc和bcc,它们的语法如下:

    To:address

    cc:address

    bcc:address

其中address是一个或多个邮箱地址,在多个邮箱地址的情况下,邮箱地址彼此用逗号分隔。

To用来指出邮件的第一接收者,cc用来指出邮件的第二接收者。当邮件同时抄送给多人时,需要用到cc,其段体指出抄送的收件人。bcc是英文Blind Carbon Copy的缩写,意思是隐藏地将邮件同时抄送给其他人,而不让收件人知道。当邮件头包含bcc段时,该段的内容将不会传送给第一和第二收件人。至于bcc段中的收件人,他们可能看到bcc段的内容,也可能看不到,这取决于传送邮件的系统。

当邮件被转发时,可用Resent-To、Resent-cc或Resent-bcc指出邮件的接收者和抄送接收者,它们的语法语义与To、cc和bcc分别对应。

 

释疑:注意抄送和转发的区别。抄送是在发送邮件时同时发送给多人,转发是收信人收到邮件后,再传送给其他人。

1.4 日期

Date头段用来指出邮件的创建日期,其语法是

     Date: date-time

其中date-time指出日期时间,其语法为

        [day , ] date time

其中可选项day表示星期几,这一项后面要加逗号。date表示日期(年、月、日),time表示时间(时、分、秒)。

    星期几用英文缩写表示,即Mon、Tue、Wed、Thu、Fri、Sat或Sun。

    日期的格式为dd monthyyyy,其中dd是表示“日”的两位数字,month是月份,用3个字母的英文缩写,即Jan,Feb,Mar,Apr等等,yyyy是年份,用4位数字表示。

    时间的格式是时分秒加时差。时分秒的格式是hh:mm:ss,分别用两位数字表示时、分和秒,采用24小时记时制。日期时间必须用当地时间,但邮件接收者和沿途的转发站未必在同一时区,因此最后加上“+hhmm”或“-hhmm”的时差部分,说明当地时间与格林威治标准时间相差(增加或减少)几小时几分钟。

例如,Fri, 02 Mar 2007 22:13:00 +0800表示2007年3月2日,星期五,22时13分0秒,当地时间比格林威治标准时间多8小时0分。

    以上介绍的日期事件格式其实是RFC1123制定的格式。RFC822所制定的日期格式中,年份只用两位数字,这显然将导致“千年虫”问题。RFC1123修正了RFC822的规定,年份改用4位数字。另外,RFC822的时间格式可以不用数字时差,而改用北美地区的时区符号说明当地时间,例如“EST”、“EDT”等,RFC1123建议采用数字时差。

    除了Date段,Received段最后的时间戳也使用这种日期时间格式。

当邮件被转发时,可添加Resent-Date以说明日期时间,其用法与Date相同。

1.5可选头段

     可选头段包括Message-ID、Resent-Message-ID、In-Reply-To、References、 Keywords、 Subject、Comments和Encrypted。

Message-ID用来为邮件指定一个唯一标识,其语法为

         Message-ID: msg-id

其中msg-id的形式为<local-part@domian>,此处local-part是一串作为邮件ID的字符串,domian是指定这个ID的主机域名。ID的唯一性由指定ID的主机负责。当一封邮件经过若干邮件服务器传送时,这些邮件服务器可以各自为这封邮件指定不同的ID,以便服务器内部管理邮件。

    当邮件被转发时,可用Resent-Message-ID指定ID,其用法和Message-ID相同。

   In-Reply-To用来指出当前邮件是对哪一封邮件的答复,其语法为

        In-Reply-To:  *( phrase / msg-id )

    (注: 语法中的斜杠“/”表示“或者”,“*”表示可重复任意次)

其中phrase是一串字符串,msg-id是邮件ID,两者可以单独使用,也可以混合使用,目的是标识所答复的邮件。例如:

   In-Reply-to: <000601c87ead$elcd28a0$0200a8c0@c220>

   References头段用来指出与当前邮件相关的有哪些邮件。例如,当我们利用电子邮件讨论某个问题时,可能来来往往会有很多封邮件,这时References可用来指出当前邮件涉及到哪些邮件。References的语法是:

        References:  *( phrase / msg-id )

例如:

    References: <000601c87ead$elcd28a0$0200a8c0@c220>

     <15662676.1412391204620030868.JavaMail.coremail@bj163app125.163.com>

   Keywords头段用来指出当前邮件中的关键词,其语法是:

        Keywords: #phrase

    (注:“#”表示其后的语法项可多次出现,彼此之间用逗号分隔)

其中phrase是关键词,可以列出多个关键词,彼此之间用逗号分隔。

    Subject头段用来指出邮件的标题,其语法是

        Subject: text

其中text是任意的文字。

    Comments头段是注释,其语法为

        Comments: text

注意Comments是整个段作为注释,在其它段中也可以插入注释,插入的注释内容只要用圆括号括起来即可。

   Encrypted头段用来通知邮件接收者如何解密。邮件正文可以加密,接收方为了解密,可能需要知道发送方使用何种加密手段。Encrypted的语法是

        Encrypted:wd1, wd2

其中wd1指出加密程序的名称,wd2用来帮助接收者选择密钥,例如,指出内部密钥表的索引。

    邮件正文可以加密,但是邮件头不能加密,因为邮件头中的信息是传送邮件的主机必须知道的,如果加密,邮件将无法传送。

   Encrypted已经被PEM(Privacy Enhanced Messaging)淘汰,有兴趣的读者可查阅RFC1421、RFC1422、RFC1423和RFC1424。

1.6用户自定义段

    以上介绍了RFC822所定义的头段,以后还可能有新的RFC文件来定义一些新头段,新头段的段名必须在NIC(NetworkInformation Center, SRI International, Menlo Park, California)登记。登记的新头段段名不允许以“X-”或“x-”开头。

    用户可以自由使用自己定义的头段,但这种段的段名不能与RFC822所定义的段名相同,也不能与将来登记的新头段段名相同,否则可能被排斥。为此,用户自定义的段名可用“X-”或“x-”开头,因为RFC822已承诺新增加的段名不会以这两个字母开头。

1.7例子和解释

例2:以下是一封垃圾邮件的全文,略去信件正文。虽然内容是垃圾,但语法格式是正确的(否则无法传播),废物利用,权当示例:

Received: (eyou send program); Mon, 26 Feb2007 03:35:50 +0800

Message-ID:<372432150.27375@scnu.edu.cn>

Received: from unknown (HELO downboy.com)(unknown@83.7.213.189)

 by202.116.32.4 with SMTP; Mon, 26 Feb 2007 03:35:50 +0800

Received: from domy5ly6i69dod([89.131.200.82])

       by 83.7.213.189 (7.81.9/7.81.9) with SMTP id 8K7gQ4BTGPRFCJ;

       Sun, 25 Feb 200720:44:10 +0100

Message-ID: <001801c7591d$b39b46d0$00deed84@domy5ly6i69dod>

From: "Belinda May"<teatomic@downboy.com>

To: "somebody" <somebody@scnu.edu.cn>

Subject: expenditure

Date: Sun, 25 Feb 2007 20:42:34 +0100

 

Some garbage text here ... ..., omitted.

从邮件头可以看到,邮件创建于2007-2-25,20:42:34,当地时间比格林威治时间多1小时。邮件标题是“expenditure”,收件人地址是somebody@scnu.edu.cn,发信人地址是teatomic@downboy.com。主机domy5ly6i69dod为邮件指定了一个ID:001801c7591d$b39b46d0$00deed84。

邮件最先被IP地址为83.7.213.189的主机收到,时间是2007-2-25,20:44:10,当地时间比格林威治时间多1小时。信件来自名称为domy5ly6i69dod的主机,通过SMTP协议接收。信件在该主机上的id是8K7gQ4BTGPRFCJ。

随后邮件被IP地址为202.116.32.4的主机接收到,时间是2007-2-26,03:35:50,当地时间比格林威治时间多8小时。信件来自未知的主机,通过SMTP协议接收。主机scnu.edu.cn为邮件指定了ID:372432150.27375。

最后信件被最终转发程序接收,时间是2007-2-26,03:35:50,当地时间比格林威治时间多8小时。

    信件中有一个空行,空行之前为邮件头,之后是信件正文。

二、MIME概述

RFC822制定了电子邮件的基本框架,这个框架一直被沿用至今。但是其中的缺陷也是明显的,主要有两个问题:

1)       邮件只能使用英文字符,不能使用其它文字符号;

2)       邮件正文只能是文字,不能包含图象、声音等多媒体数据。

为解决这些问题,IETF的研究人员提出了扩充的办法,称为“多用途互联网邮件扩展”,英文缩写为MIME。先是RFC1521、RFC1522提出了实现的方法,后来RFC2045~RFC2049又做了一些修改。

    在具体介绍MIME之前,还必须说明一个问题。RFC822和MIME只是规定电子邮件的格式,网络上电子邮件的传送方法则由SMTP协议规定,SMTP对邮件的限制也影响到MIME的设计,这种限制主要有两点:

1)       电子邮件包括邮件头和邮件正文,都必须是以行为单位的文本,邮件在两部机器之间传送时,是一行一行地传送的;

2)       每一行文本不能超过1000个字符(包含结尾的CR、LF两个字符)。

由于设计MIME时,这个世界上已经有很多邮件系统在运行了,这些系统只按照RFC822和SMTP的规范运作,MIME必须使得扩展之后的邮件仍然能在这些系统上传送,尽管这样做使得一些做法显得很累赘,但是就像RFC2045中所说的,“兼容比优雅更重要”。

    因此,MIME在使得邮件可以使用多种文字和多媒体数据的同时,必须保证邮件传送系统所看到的邮件仍然是一行行的文字,每一行文字不超过1000个字符,并且所有的字符都是合法的ASCII字符(并不是所有的ASCII字符都可以出现在邮件中的任何地方,详见RFC822,我们不详细讨论)。这就是为什么MIME要采用很多编码转换算法的原因。

   MIME的扩充办法一是增加新的头段,二是允许邮件正文分块(Multy Parts)。新增加的头段中关键的两个是Content-Type和Content-Transfer-Encoding,Content-Type用来说明邮件的正文是文字、多媒体数据还是其它数据,以及文字编码、多媒体数据格式等进一步的详细说明;Content-Transfer-Encoding则用来说明所采用的传送编码,这种编码使得非ASCII字符数据转换成ASCII字符数据,以便通过邮件传送系统。邮件正文分块使得同一封邮件中可以同时包含文字、声音、图象等多种类型的数据,每种数据各自成为一块,块与块之间互不交叉,每一块内部可以用头段Content-Type和Content-Transfer-Encoding说明块内数据的类型和传送编码。

   MIME事实上并未更改RFC822的规定。MIME主要是规定邮件正文的格式,而RFC822对于邮件正文并未做任何规定(除了必须是ASCII字符之外),因此MIME和RFC822是互不相交的两个规范(即彼此之间没有重叠的部分)。

    MIME增加的头段如下:

1.MIME-Version

头段MIME-Version用来说明当前的MIME版本,其语法是:

          MIME-Version: x.x

其中x是一位十进制数字,x.x是版本号。以后可能会有新的MIME版本,支持新版本的邮件可以用这个头段说明所支持的版本号,以便不支持新版本的系统采取回避措施。目前的版本号是1.0,并且至今还没有新的MIME版本出现,因此现在的MIME邮件都是使用头段

          MIME-Version: 1.0

这个头段是强制使用的,所有按MIME规范编写的邮件都必须加上这个头段。

    2.Content-ID

头段Conten-ID用来为信件内容加上唯一的ID标识,其语法为:

          Content-ID: msg-id

其中msg-id的形式为<local-part@domian>,此处local-part是一串作为邮件ID的字符串,domian是指定这个ID的主机域名。ID的唯一性由指定ID的主机负责。由于域名是唯一的,所以这样构造的ID将是全球唯一的。

    与RFC822的Message-ID不同,Content-ID不是电子邮件的ID,而是电子邮件分块数据中某一块数据的ID,本章7.2节将详细解释这个头段的作用。。

    3.Content-Description

头段Content-Description为信件内容加上文字描述信息,其语法是

          Content-Description: text

其中text是任意的文字。对于多媒体数据,加上文字描述信息有时是有帮助的。

4.Content-Transfer-Encoding

图象、声音等多媒体数据是二进制数据,并且以字节为单位,对于程序来说,这种数据无非是一串8位的二进制数据,即8位位组数据流(octet stream)。这种数据是无法通过执行RFC822的邮件系统的,必须进行编码转换,使之成为一行行的ASCII字符进行传送,然后在接收方进行反转换从而恢复数据。编码转换的算法必须是公开的,并且必须在邮件中指出所用的算法,这样接收方才能知道如何进行反转换。头段Content-Transfer-Encoding就是用来指出这种编码转换算法的,其语法为:

Content-Transfer-Encoding:mechanism

mechanism为“7bit”、“8bit”、“binary”、“quoted-printable”或“base64”其中之一。本章第三节将详细介绍这些编码转换算法。

    5.Content-Type

头段Content-Type可说明邮件数据的类型,其语法为:

        Content-Type: type/subtype*(; parameter)

其中type是数据类型,subtype是子类型。段体最后还可以使用参数parameter,进一步说明类型信息。可以使用多个参数,每个参数必须以分号开头。typesubtypeparameter都由MIME指定。

   MIME指定的数据类型可分为两大类,一类指定数据的多媒体类型,另一类是复合类型。属于多媒体类型的type/subtype

    text/plain, text/richtext

    image/jpeg, image/gif

    audio/basic

    video/mpeg

    application/octet-stream,application/PostScript

属于复合类型的type/subtype

    message/rfc822, message/partial

    multipart/mixed, multipart/alternative,multipart/parallel, multipart/digest

本章第四节将详细介绍这些“类型/子类型”及其使用的参数。

    类型和子类型是可以扩展的,但必须在IANA登记。用户也可以定义自己私有的类型和子类型,但这种类型的名称必须用“x-”或“X-”开头,以免与IANA登记的名称混淆。

三、编码转换

3.1  quoted-printable

quoted-printable(简称QP)是一种编码转换算法,可以将8位位组数据流转换为ASCII字符文本数据,转换之后每行不超过76个字符。对于所转换8位位组中的ASCII字符,QP保持不变,仅对其中非ASCII的数据进行转换。QP主要用来转换那些主要是ASCII字符、只包含少量非ASCII字符的数据。通常这样的数据是以英文文字为主的文本数据。

 

释疑:对于计算机,字符和多媒体数据一样,都是一些二进制数,并且无论是ASCII字符还是其它文字字符,或者多媒体数据,一般都是以8位位组(字节)为单位存储和传送的。计算机判别一个字节是否为ASCII字符的方法是:如果这个字节对应的数值在0~127之间,就是ASCII字符,否则即为其它数据。另外,RFC822规定,当机器遇到连续的两个字符CR(ASCII码为13)和LF(ASCII码为10)时,就认为是一行的结束。此处CR是回车字符的名称,LF是换行字符的名称。

 

QP的转换算法如下:

(i)如下字符不必转换,但如有必要,也可按(ii)的方法转换:

     ! " # $ % &' ( ) * + , - . / : ; < > ? @ [ \ ] ^ _ ` { | } ~

     英文字母a~z,A~Z

     阿拉伯数字0~9

     以上字符对应的ASCII码为33~60,62~126。

(ii)其它8位位组都可以转换为“=hh”形式,其中hh是该位组对应的两个16进制数字。例如,10001100转换为“=8C”。注意这样转换之后一个位组转换成了3个字符,并且都是ASCII字符,在机器中,这3个字符将对应3个字节,每个字节存放一个字符对应的ASCII码。16进制数字中出现的英文字母一律用大写,不许用小写。由于“=”现在有了特定的语法含义,所以原8位位组中出现的“=”必须转换为“=3D”(注意(i)列出的字符没有“=”)。

(iii)如果所转换的8位位组数据流是文本数据,则其中的CRLF应保持不变。对于非文本数据,CR、LF应分别转换为“=0D”和“=0A”。这是因为,非文本数据出现的CR或LF并没有回车换行的含义,只是该位组数值上恰好等于13和10而已。

    (iv)Tab和空格可以不转换,但它们不能出现在一行的末尾。因为有些邮件系统会自动在每行末尾添加空格,这些空格最后会被删除掉。如果行末确实需要保留Tab和空格,则应该将它们转换为“=hh”的形式。

    (iiv)最后,检查每行字符的个数,不能超过76个字符,若超过,则应该插入“软回车”使之分行。“软回车”是字符“=”加上CRLF。

 

例1:下面是一行既有英文又有汉字的文本,其中字符个数超过76个,字符编码为GBK:

QPexample: 中英文混合文本, wehave a line which length exceeds 76 characters, so a soft line break must beused.

按QP算法转换后的结果如下:

QP example:=D6=D0=D3=A2=CE=C4=BB=EC=BA=CF=CE=C4=B1=BE, we have a line =

which length exceeds 76 characters, so asoft line break must be used.

其中所有的汉字都被转换为“=hh”的编码,英文字母和标点符号则不转换。注意一个汉字的编码为两个字节,转换之后成为6个字符。转换之后该行的字符个数超过76个,因此在适当的位置插入了软回车。

 

QP转换是容易反转换的,只需要删除所有的软回车,然后将所有形如“=hh”的字符恢复为8位位组即可。

3.2  Base64

Base64是另一种转换算法,主要用来转换多媒体数据或非英文文本。首先,待转换的数据被视为一串字节(8位位组)数据流,将这一串数据划分为3个字节一组(暂时假设数据流的字节个数恰为3的整数倍),每组24位二进制数,再划分为6位一组,共4组。6位二进制数的数值范围是0~63,每6位二进制数依照其数值由按表一转换为一个字符。例如,对于二进制字节

10110111 01011100 11010001

划分为6位一组:101101 110101 110011 010001

各组对应的数值为:45 53 51 17

按照表一转换为3个字符:t1zR

表一中的字符全部属于ASCII字符,因此这样转换之后,任意的8位位组就变成了ASCII字符数据。

    如果待转换的数据字节数并非刚好是3的整数倍,那么从第一个字节开始,每3个字节划为一组,最后只有两种情况:一是剩下两个字节,二是剩下一个字节。对于第一种情况,在剩下的16位二进制数的后面加上两个0,使之成为18位二进制数,然后划分为3个6位组,按表一转换为3个字符,最后再加上一个垫底字符“=”。对于第二种情况,在剩下的8位二进制数的末尾加上4个0,使之成为12位二进制数,然后划分为两个6位组,按表一转换为2个字符,最后加上再加上两个垫底字符“=”。

表一:

    数值字符     数值 字符     数值 字符     数值 字符     数值 字符     数值 字符

      0   A          12  M         24  Y          36  k           48  w         60  8

      1  B          13  N         25  Z           37  l           49  x           61  9

      2   C          14  O          26  a           38  m         50  y           62  +

      3  D          15  P          27  b           39  n           51  z          63  /

      4  E          16  Q          28  c          40  o           52  0     (垫底)=

      5   F           17  R          29  d          41  p           53  1       

      6   G          18  S           30  e          42  q           54  2

      7  H          19  T          31  f          43  r           55  3

      8  I           20  U          32  g          44  s           56  4

      9   J           21  V         33  h           45  t            57  5

     10  K          22  W                34  i           46  u           58  6

      11  L          23  X          35  j            47  v          59  7

 

    转换的结果是一串字符流,最后在这串字符流中的适当位置插入CRLF,使之划分为若干行,每行不超过76个字符。一般的做法是每76个字符划分为一行,最后一行可以少于76个字符。

    之所以要在末尾加上垫底字符,是为了反解码时知道最后多加了几个0。Base64的反解码是很简单的,待转换数据是一串字符流,首先删去其中所有的CRLF,然后每个字符按表一转换为对应的二进制数,垫底字符不转换。最后若字符流末尾有一个垫底字符,则删去二进制数最后的两个0,若末尾有两个垫底字符,则删去二进制数的最后的4个0。

    Base64将每3个字节转换为4个字符,每个字符在机器中是用一个字节的ASCII码存放的,因此Base64实际上将3个字节变为4个字节,数据容量比原来增加了三分之一。

3.3  7bit、8bit和binary

如果传送编码被说明为Content-Transfer-Encoding:7bit,则意味着邮件正文都是ASCII字符。7bit是Content-Transfer-Encoding的默认值,即,没有指定传送编码就认为其编码是7bit。

    如果传送编码被说明为Content-Transfer-Encoding:8bit,则意味着邮件正文是文字文本,但使用的字符不是ASCII字符,而是使用8位编码或多个8位编码的字符。例如,中文邮件就可以使用这种编码。这种邮件的内容数据不再转换,就是原来的字符编码。但是,这种编码的邮件未必能通过所有的邮件传送系统,因为按照RFC822,邮件字符只能是7位的ASCII编码。支持ESMTP(RFC1652)的系统可以传送这种邮件,目前大多数邮件系统都支持ESMTP。

    说明为Content-Transfer-Encoding: binary的邮件正文并非真的是纯粹的二进制数据,纯粹的二进制数据是无法通过邮件系统传送的,说明为binary其实只是意味着,邮件中的数据已经经过处理了,照原样即可。例如,对于分块的邮件正文,如果每一块都已经用Content-Transfer-Encoding说明了所用的转换编码,则邮件正文总体的编码就可以说明是binary。

 

    例2:分块编码的邮件。

 
 

1

 

2

 

3

 

4

 

5

 

6

 

7

 

8

 

9

 

10

 

11

 

12

 

13

 

14

 

15

 

16

 

17

 

18

 

19

 

20

 

21

 

22

 

23

 
 
 

Date: Sat, 6 Jan 2007 15:01:00 +0800 (CST)

 

From: <foo@sohu.com>

 

To: <somebody@scnu.edu.cn>

 

Subject: a multipart mail

 

Mime-Version: 1.0

 

Content-Type: multipart/mixed;

 

    boundary="=_Part_2997_30302587.1168066860810"

 

Content-Transfer-Encoding: binary

 

 

 

--=_Part_2997_30302587.1168066860810

 

Content-Type: text/plain; charset="GB2312"

 

Content-Transfer-Encoding: 8bit

 

 

 

  此处为一些中文文本。

 

 

 

--=_Part_2997_30302587.1168066860810

 

Content-Type: application/octet-stream;

 

Content-Transfer-Encoding: base64

 

 

 

UmFyIRoHAM+QcwAADQAAAAAAAADhi3QgglIAmwIAADgHAAACDTxIq2CDUz

 

UdMzIAIAAAANf30rXTNhVkKM5p+KpUs91buvJjiymwtCch3f3fzy

 

 

 

--=_Part_2997_30302587.1168066860810--

 

在这个例子中,第1行到第8行为邮件头,从第10行到23行为邮件正文,邮件正文被分为两块,第11到15行为第一块,第17到22行为第二块,第10、16、23行为块的分界符。分块的方法将在后面介绍,现在我们只关心转换编码。从例中可以看到,每一块都有自己的头段,这些头段指出块内数据的类型和编码,块内头段和块内数据之间用一个空行隔开。

    第一块的转换编码是8bit,其内容是一段中文文字。第二块的转换编码是base64,其数据经base64转换之后的两行字符。由于块内已说明了转换编码,所以整个邮件正文的编码可以说明为binary(第8行)。

四、类型/子类型

本节介绍Content-Type的语法

    Content-Type: type/subtype*(; parameter)

所使用的类型(type)、子类型(subtype)和参数(parameter)。

4.1文本类型

MIME中定义的文本类型及其子类型只有text/plain和text/richtext。

当邮件数据用Conten-Type说明为text/plain时,表示这些数据是纯文本数据,即没有字体、色彩、大小这些修饰成分的文本数据,这种数据中每个字节都是字符编码。纯文本数据中的文字如果不是英文,则必须用参数charset说明字符编码。例如,对于简体中文数据,可以用

Content-Type: text/plain; charset=“GB2312”

说明编码。注意charset事实上是指字符编码而不是字符集。参数charset的语法是

          charset="encoding-name"

其中encoding-name是字符编码的名称,这种名称必须是在IANA登记的名称。MIME规范本身只定义了US-ASCII和ISO-8859-1, ISO-8859-2, ...,ISO-8859-10这些名称,它们分别表示英文和欧洲文字字符的编码。现在在IANA登记的字符编码已经很多了,例如,GB2312,Big5,UTF-16BE,UTF-16LE,UTF-16,UTF-8等,详细情况可查阅IANA文件。

    无论邮件文字是何种编码,文本中的回车换行一定必须是ASCII码字符CRLF。这是因为SMTP是逐行传送邮件内容的,必须有统一的换行标志。

    如果文本类型说明为text/richtext,则文本数据将是RichText格式的数据,这种数据也是文本数据,但不是纯文本,其中包含了字体、色彩、大小等修饰信息,修饰信息是一些字符串构成的指令,所以RichText格式的数据仍是文本数据。Windows的word文档可以转存为RichText格式。

    目前在IANA登记的MIME文本子类型已增加了很多,例如,

text/css:样式表文本数据

text/html:HTML数据

text/xml: XML数据

    文本数据无论采用何种子类型,都必须保证每行字符不超过998个(不包含CRLF)。

4.2图象、声音和视频类型

MIME定义的图象数据类型有image/gif和image/jpeg,前者说明数据是gif格式的图象数据,后者说明数据是jpeg格式的图象数据。

声音数据媒体类型是audio,MIME为这种类型只定义了一种子类型basic。说明为audio/basic的数据是单声道、取样频率8000Hz、取样精度8位、按μ-law编码的声音数据。

视频类型为video,MIME只定义了一种子类型mpeg,说明为image/mpeg的数据是采用MPEG压缩技术的视频数据。

    所有这些类型的子类型都可以扩展的,但必须在IANA登记。目前已登记的例子有:

image/png,image/tiff,

audio/mpeg,audio/mp4,

   video/dv,video/mp4,video/raw等。

4.3程序数据类型

MIME定义了一种称为application的数据类型,这种类型的数据或者是需要某些应用程序处理的数据,或者是无法归类为其它媒体类型的数据。application有两种子类型:octet-stream和PostScript。

说明为application/octet-stream的数据是一串8位位组二进制数。这种类型可以用如下两个参数做进一步的说明:

    type=category     padding=number

其中category是一串字符,用来指出数据的一般类型。使用这个参数的目的是为阅读邮件的人提供信息,并非为应用程序自动处理。application/octet-stream数据是以8位为一组的二进制数,如果所传送的二进制数的位数并非8的整数倍,则必须在末尾补上若干位。参数PADDING用来指出补上的位数,number就是指出位数的数字。

说明为application/PostScript的数据是PostScript程序。PostScript是一种打印语言。

4.4分块类型(Multipart)

4.4.1 multipart/*的语法

当邮件数据包含几种不同类型的数据时,必须将数据分块,每一块为一种类型的数据,并在块内说明数据类型。这时整个邮件正文的类型就必须说明为分块类型Multipart。分块类型的子类型在MIME中定义的有mixed,alternate, digest, parallel四种,各种子类型的语法都是一样的,如下所示:

     Content-type: multipart/subtype;boundary="boundary_string"

其中subtype为mixed, alternate, digest, parallel之一,或以后登记的扩展子类型。参数boundary指出分界字符串是什么。对于分块的邮件正文,块与块之间用一行字符作为分界线,boundary_string就是这一串字符。boundary_string由邮件编写软件设定,要求不超过70个字符,并且只能使用如下ASCII字符:

      a~z,A~Z,0~9,' ( ) + _ , - ./ : = ?

也可以使用空格,但空格不能出现在末尾。

    显然,分界字符串不能与邮件的内容混淆。邮件编写软件必须保证所指定的分界字符串不会在邮件内容中出现,因此,经常选择一些古怪的字符组合作为分界字符串。

    分块邮件的第一块以一行分界字符串,开头再加两个连字符“-”开始。例如,如果邮件头中指定

     Content-type:multipart/mixed; boundary="sample boundary"

则邮件中第一次出现的如下一行将开始第一块:

     --sample boundary

以后每两块之间都用同样的一行作为边界线,最后一块的末尾也要用边界字符串,同样也是在开头加上两个连字符,但是末尾还要再加上两个连字符。仍如上例,结束分界线将是

     --sample boundary--

    分块邮件的结构仍遵从RFC822的框架,只是邮件正文被划分为若干块。具体地说,就是邮件开头是邮件头,邮件头中包含RFC822定义的头段,也可以包含MIME头段。邮件头与邮件正文用一个空行分隔,邮件正文被划分为若干块,块与块之间用分界字符串隔开。每一块的内部可分为块头和块内正文(part body)两部分,块头由MIME头段组成,这些头段说明块内数据的类型和转换编码。块头也可以包含RFC822定义的头段,但是这种头段在块内是没有意义的,可以忽略。块内正文是块中的数据,块内正文和块头之间同样用一个空行分隔。

    对于分块的邮件正文,在第一行分界字符串之前也可以写入文字,这些文字称为“导言”(preamble)。在邮件正文最后的一行分界字符串后面,也可以写入文字,这些文字称为“结语”(epilogue)。导言和结语并非必要,但有时也有作用。例如,当一封MIME邮件被一个只知道RFC822的邮件软件接收到时,虽然这个软件不知道如何处理其中的MIME数据,但它至少可以将导言和结语显示给用户看。

    例3:分块邮件的结构示例。

From: Some one <somebody@21cn.com>
To: Others <foo@sohu.com>
Date: Sun, 21 Mar 2006 23:56:48 +0800 
Subject: Sample message
MIME-Version: 1.0
Content-type: multipart/mixed; boundary="simple boundary"
 
Here is the preamble. 
 
--simple boundary
Content-type: text/plain; charset=GB2312
Content-Transfer-Encoding: 8bit
 
此处为第一块的数据。
 
--simple boundary
Content-type: text/plain; charset=us-ascii
 
Here is the second part.
 
--simple boundary--
 
This is the epilogue. 

 

分块是可以嵌套的。也就是说,一封分块邮件内的某块,也可以是multipart类型(在块头中用Content-type说明),即,其块内正文又划分为若干块。要注意这种情况下内外层的分界字符串必须互不相同。

    分块邮件也可以只有一块,这时虽然看起来似乎没有必要分块,但有时也有用处。例如,块内为图象或声音这些多媒体数据,我们可以在导言或结语部分附加一些文字说明。

说明为multipart的邮件正文,包括嵌套分块的块内正文,其转换编码只能是7bit、8bit或binary,不能是QP或Base64。这是因为边界字符串和块内头段都必须是ASCII编码,不能转换。

 

4.4.2multipart/mixed

说明为multipart/mixed的分块邮件,块与块之间相互独立。这是分块邮件中最一般的情况,也是默认的类型。默认的意思是:如果系统不知道multipart/subtype中的子类型是什么,就作为mixed处理。

 

4.4.3multipart/alternative

说明为multipart/alternative的分块邮件,各块中存放的都是同一信息,只不过采用不同的形式。例如,同样一段中文文字,将它放在分块邮件的两个块中,一块采用8bit编码,另一块采用base64编码,整个邮件正文的类型则说明为multipart/alternative。用户邮件软件接收到multipart/alternative类型的分块邮件,将选择其中一块显示给用户看。如果用户邮件软件不能处理8位编码,它可以选择base64编码的那一块。

multipart/alternative的作用就是为同样的信息提供不同的形式,以便邮件系统可以选择其中一种可理解的形式进行处理。对于这种邮件,编写系统必须把最希望被采用的那一块放在最后,最不希望被采用的那一块放在最前面,中间各块的希望程度从后往前依次递减。接收邮件的系统则必须按从后到前的优先次序选择进行处理的块,或者用交互的方式询问用户处理哪一块。

 

4.4.4multipart/digest

这种分块类型用来将多封RFC822的邮件合并在一封邮件中传送。说明为multipart/digest的邮件,其中每一块就是一封符合RFC822规范的邮件,包括邮件头和邮件正文。每一块内的数据类型默认为message/rfc822(这种类型将在稍后介绍)。

 

4.4.5multipart/parallel

如果分块电子邮件的类型被说明为multipart/parallel,则其意图是在某种硬件或软件系统上并行地显示其中各块的内容。例如,其中一块的数据类型是image/jpeg,另一块的数据类型是audio/basic,然后希望接收邮件的系统在显示图象的同时播放音乐。

    邮件的编写软件设计者必须注意,很多邮件系统都不能并行地显示数据。

4.5  Message类型

在实际应用中经常需要将一封邮件封装在另一封邮件中,message类型可用于此种目的。当邮件正文或分块邮件中某一块的正文包含另一封邮件时,则正文内容可说明为message类型。

   MIME为message类型定义了三种子类型:rfc822、partial和external-body

 

4.5.1 message/rfc822

如果邮件正文的类型被说明为message/rfc822,则正文本身就是一封符合RFC822语法的邮件。MIME邮件也是符合RFC822语法的,因此这种正文也可以是MIME邮件。

    说明为message/rfc822的正文既然是符合RFC822语法的邮件,当然包含邮件头。MIME规定,对于这种正文,正文中出现的邮件头中必须有头段From、Subject或Date,三者至少要有一个。

    说明为message/rfc822的正文,其编码只能是7bit、8bit或binary,不能是其它编码。

 

4.5.2 message/partial

当传送一封包含大量数据的邮件时,沿途的转发站可能无法处理这种大数据量的邮件,它们只能将邮件拆分为几个片断(fragment)分开传送,然后在接收端再重新组装起来。数据类型message/partial可用来说明这种邮件片断,即,message/partial类型的数据是一封邮件被拆分之后其中的一个片断。

    由于拆分必须处理邮件正文,如果一封邮件的正文编码为8bit或binary,而中途某个转发站只能处理7bit的邮件,则这封邮件将无法传送。因此,限制这种被拆分的邮件只能是7bit编码。另外,拆分必须以行为单位,即,不能在一行的中间拆分。

 

释疑:限制使用7bit编码并不妨碍使用大容量的多媒体数据。因为我们可以使用分块邮件,在块内使用base64编码,这样编码之后的数据全部都是7bit的ASCII字符数据,整个邮件正文的编码可以说明为7bit。

 

用Content-type说明数据类型为message/partial时,必须使用三个参数:id、number和total。id是被拆分邮件的唯一标识,拆分之后的邮件片断可以依据id判别各自属于哪一封邮件。number是一个整数,指出当前邮件片断是拆分之后的第几个片断,即片断编号,这种编号从1数起(而不是从0数起)。total也是一个整数,它指出总共有几个片断。邮件的最后一个片断必须有total这个参数,其它片断total是可选的。

    以下是一个例子,其中只列出各个邮件片断中的MIME头段:

    第一个邮件片断:
Content-Type: Message/Partial; number=1; total=3;
                id="oc=jpbe0M2Yt4s@thumper.bellcore.com"
    第二个邮件片断:
    Content-Type: Message/Partial;
                id="oc=jpbe0M2Yt4s@thumper.bellcore.com";
                number=2
    最后一个邮件片断:
  Content-Type: Message/Partial; number=3; total=3;
                id="oc=jpbe0M2Yt4s@thumper.bellcore.com"

 

4.5.3  message/external-body

如果邮件正文的类型被说明为message/external-body,则这封邮件将包含另一封邮件,但所包含的邮件并不在当前邮件中,而是必须从外部获取,当前邮件只包含获取外部邮件所需的信息。注意这种类型的邮件并不是等待收到邮件的用户自己去获取外部数据(例如,在邮件中嵌入一个链接,让用户去点击),而是要求接收邮件的系统自动按照当前邮件中提供的信息去获取外部邮件。

    一封message/external-body类型的邮件,其邮件正文虽然不包含外部邮件的正文,但是必须包含外部邮件的一些头段,特别是,必须包含外部邮件的Content-ID头段,以便邮件接收系统可以按照这个唯一标识去获取外部邮件。

    例4:典型的message/external-body类型的邮件结构如下:

 

 
 

1

 

2

 

3

 

4

 

5

 

6

 

7

 

8

 

9

 

10     

 
 
Content-type: message/external-body;
                access-type=local-file;
                name="/u/nsb/Me.jpeg"
 
Content-type: image/jpeg
Content-ID: <id42@guppylake.bellcore.com>
Content-Transfer-Encoding: binary
 
Here is the phantom body
 

 

 

 

其中1~3行为当前邮件的邮件头,5~10行为当前邮件的邮件正文,其中5~7行是所包含的外部邮件邮件头中的头段,第9行开始是所谓的幻影部分(phantombody),这一部分一般只是提供一些辅助信息,但是当外部邮件必须从某个邮件服务器获取时,这一部分可用来存放命令,我们将在随后介绍。

    用头段Content-type说明邮件正文的类型为message/external-body时,必须使用一些参数,其中必不可少的一个是access-type,根据这个参数的取值,可能又需要更多的参数,但无论如何,如下参数是共用的:

 

释疑:参数的书写格式是param=value,等号左边的param是参数名,等号右边的value是参数值,程序“书写”参数值时可以在两端加双引号,也可以不加,这与参数值中使用的字符有关,此处不详细讨论。

 

1)       access-type。这个参数的参数值指出获取外部数据的方法,MIME已定义的参数值有“ftp”、“anon-ftp”、“tftp”、“local-file”和“mail-server”。稍后我们将介绍这些参数值的含义。可以定义这个参数新的参数值,但是必须在IANA登记。这个参数是必须有的。

2)       expiration。这个参数用来说明外部数据的可访问期限,超过这个期限外部数据可能就不存在了。expiration的参数值采用RFC822中规定的日期时间格式。这个参数是可选的。

3)       size。这个参数的参数值是一个整数,即外部数据的大小,以字节为单位。这个大小是数据未编码之前的大小,例如,数据用base64编码,则size的值应该是未编码之前的数据大小。这个参数是可选的。

4)       permission。这个参数说明数据的读写访问权限,MIME定义的参数值有“read”和“read-write”。这个参数是可选的。

当access-type的参数值为ftp或tftp时,外部数据将作为一个文件从某个ftp服务器或tftp服务器下载,接收数据的邮件系统必须执行ftp协议或tftp协议进行下载。这种情况Content-type头段中还必须使用如下参数:

•       name。其参数值即为要下载文件的文件名。这个参数是必须的。

•       site。其参数值为服务器的域名,此处不能使用别名。这个参数是必须的。

•       directory。其参数值为服务器上存放name所指定文件的目录名,这个参数是可选的。

•       mode。指定下载文件时的工作模式,对于tftp,参数值为“NETASCII”、“OCTET”或“MAIL”;对于ftp,参数值为“ASCII”、“EBCDIC”、“IMAGE”或“LOCALn”。关于这些模式的含义,读者可参阅ftp协议(RFC959)和tftp协议(RFC783)。这个参数是可选的,若没有这个参数,则tftp的模式默认为“NETASCII”,ftp的模式默认为“ASCII”。

注意参数中并没有包含登录服务器时必须提供的用户名和密码,这是为了安全起见。邮件系统可以通过询问用户的方式获得用户名和密码。

    若access-type的参数值为anon-ftp,则表示外部数据将用匿名ftp的方式获得,这种方式登录服务器时不需要用户名和密码。需要的参数与ftp完全相同。

    若access-type的参数值为local-file,则表示外部数据可从本地机器获得,这时需要如下额外的参数:

•       name。包含外部数据的文件名及其路径。这个参数是必须的。

•       site。本地机器的域名。这个参数是可选的。

    若access-type的参数值为mail-server,则表示外部数据可从某个邮件服务器获得,这时需要如下额外的参数:

•       server。形如local-part@domian的邮箱地址。这个参数是必须的。

•       subject。邮件标题。有些邮件服务器按邮件标题建立索引,可按照标题检索邮件。这个参数是可选的。

除此之外,mail-server方式还要在幻影部分包含一些命令,这些命令将在获取外部数据时发送给邮件服务器。MIME没有规定命令的格式。

   message/external-body类型存在一些安全隐患。这种方式事实上使得邮件接收系统按照邮件的内容执行一些操作,恶意邮件有可能利用这种方式诱骗邮件系统去做一些本来不应该做的事情。

 

五、邮件头段中的字符编码

前面几节所介绍的Content-type和Content-trasfer-encoding用来说明邮件正文(或者分块邮件中块的正文)的数据类型和编码方法,而对于头段本身,也需要有一种办法让用户可以使用非ASCII字符。例如,对于一封中文邮件,我们自然希望邮件标题也用中文,这就需要在头段Subject中使用非ASCII字符。

每个头段都由段名和段体两部分组成。段名是已经指定的,并且都是ASCII字符,需要考虑的是如何在段体中使用其它字符。必须指出,并非所有的段体都可以使用非ASCII字符,也并非段体中任何部分都可以使用非ASCII字符。例如,Date头段的段体除了插入的注释部分,其它位置都必须是ASCII字符,因为RFC822已经规定了日期时间的格式,这种格式是不能改变的。又如From、To、cc等头段,其段体的一般形式为

    phrase <loacal-part@domian>

其中phrase部分可以使用其它字符,而loacal-part@domian部分只能是ASCII字符,因为邮件帐户名和域名规定必须由ASCII字符组成。一般说来,只有段体中那些可以让用户自由填写的部分有可能使用其它文字,其它部分只能按照RFC822和MIME的规定。

    对于段体中可以使用其它文字的部分,RFC2047提出了编码的方法,这种方法同样是将其它文字转换为ASCII字符,因为如果不这样做而直接使用其它文字字符,邮件就可能通不过只知道RFC822的系统。

   RFC2047提出的编码方法使用所谓的“编码文本”(encoded-word)来替换非ASCII字符的文本,编码文本的语法为

       =? charset ? encoding? encoded-text ?=

编码文本以“=?”开头,以“?=”结尾,中间的两个问号将三个语法成分隔开,这三个语法成分的语义如下:

•       charset是被转换文本中字符编码的名称,必须是已经在IANA登记的名称。

•       encoding是转换编码算法,RFC2047只定义了两种可用的算法,就是QP和base64,此处分别用字母“Q”和“B”表示这两种算法。

•       encoded-text则是转换之后的文本。

    例如文本“中文标题”,原字符编码为GB2312,选择base64转换编码,按上述算法将转换为

=?gb2312?B?1tDOxLHqzOI=?=

    要求每行编码文本不超过75个字符,若超过则要折叠,即在适当位置插入CRLF和空格使之成为多行,并且除了第一行,后续各行开头要有空格(参见RFC822关于头段折叠的要求)。

    如果采用QP编码,则被转换的文本中的ASCII字符将会保留原状。但RFC2047要求转换之后的编码文本中不能出现空格,这样做是为了使之成为一个整体的RFC822语法成分,以免RFC822的语法分析器产生误解。因此,被转换文本中若出现空格,则必须转换为“=20”。或者,将被转换文本中所有的空格都换成下划线“_”,这样既起到空格的作用,又保持可读性。

    段体中可以用一对圆括号插入注释,注释内容可以采用编码文本,但两端的原括号不能作为被转换注释的一部分,必须保留原状。

 

六、邮件附件

MIME(RFC2045~RFC2049)并没有提出在邮件中附带文件的方法,提出这种方法的是RFC2183,这个文件提出一个新的MIME头段Content-Disposition,可用来说明邮件中所包含数据的处理方式。

Content-Disposition的语法是

     Content-Disposition: type*(; param)

其中type指定所含数据的处理方式,必须是inline或者attachment。param是一些可选的参数。

    这个头段象其它MIME头段一样,可用在邮件的邮件头说明邮件正文数据的处理方式,也可以放在分块邮件的某一块中说明块内数据的处理方式。

    当type为inline时,所说明的数据为在线显示方式,邮件用户程序在接收到邮件后应该将这种数据和邮件其它内容一起显示出来,而不是另外作为一个文件处理。

type为attachment时所说明的数据将被作为附件处理,邮件用户程序应该询问用户是否保存附件以及保存到什么地方。这种类型需要如下几个可选的参数:

•       filename:附件的文件名。这是邮件发送方建议的文件名,接收方保存附件时可作为参考。文件名中不应该包含磁盘目录路径,若出现这种路径,接收方应该忽略它。

•       creation-date:附件中文件的创建日期。参数值必须使用RFC822规定的日期时间格式,并且必须使用数字表示的时区。

•       modification-date:附件中文件最后一次被修改的日期时间,格式同上。

•       read-date:附件中文件最后一次被阅读的日期时间,格式同上。

•       size:附件中文件的大致大小,以字节为单位。

参数filename的参数值可能需要使用非ASCII字符(例如,中文文件名),可以采用RFC2047的编码文本方法(见本章第五节),也可以采用RFC2184提出的方法。

RFC2184提出的方法主要是为了对参数值中的非ASCII字符进行编码转换,同时也提出了处理长串参数值的方法。

当参数值很长时,可以采用RFC822中的头段折叠的办法,但这种方法使用起来有一些限制,例如,必须有空格以便在空格处换行,但参数值中未必有空格。为此,RFC2184提出如下语法,假设有参数param:

      param=very longstring value

其参数值是很长的一串字符,则可将之分成若干段,分别表示为

      param*0=1st line;

     param*1=2nd line;

     param*2=3rd line

即,将很长的参数分成若干段,每段仍保留“; param=value”的形式,但是在参数名和等号之间插入一个星号“*”和一个数字,数字表示分段编号,编号从0数起。按照MIME语法,每一段相当于一个参数,并不违反原来的语法。邮件接收系统要复原这些参数段,只需将同样参数名的各行按行号排列,将等号右边的字符串连接起来就可恢复原来的参数值。

    对于参数值中的非ASCII字符,采用的是URI那种转义的方法,即,将一个8位的编码用对应的两个16进制字符表示(A~F用大写字母),前面加上百分号。但是用这样的方法必须说明字符编码,因此,使用如下语法:

       param *= charset' language' encoded-string

其中param是参数名,参数名和等号之间插入一个星号“*”,表示参数值是经过编码转换的文字,等号之后charset的是字符编码名称,例如iso-8859-1、GB2312等,然后是一个单引号,后面的是关于所用语言的解释信息,接着又是一个单引号,最后是转换之后的字符串。

例如,参数

filename=互联网技术.doc

按上述方法转换之后成为

       filename*=GB2312'hz'%BB%A5%C1%AA%CD%F8%BC%BC%CA%F5.doc

注意其中的ASCII字符是不转换的。

    作为邮件附件的数据,除了用Content-Disposition说明其为附件数据之外,还必须用MIME头段Content-type和Content-transfer-encoding说明数据类型和传送编码。

    例5:带有附件的电子邮件:

From: someone<someone@126.com>

To: others<others@163.com>

Date: Sat, 16 Feb2008 15:02:24 +0800

Subject:=?gb2312?B?z+DGrA==?=

Content-Type:multipart/mixed;

         boundary="_004_29815604740571203145344048JavaMailcoremailbj126app38126_"

MIME-Version: 1.0

 

--_004_29815604740571203145344048JavaMailcoremailbj126app38126_

Content-Type:text/plain; charset="gb2312"

Content-Transfer-Encoding:base64

 

DQoNCg0KDQoNCg0KX19fX19fX19(此处为base64编码的中文,略)

 

--_004_29815604740571203145344048JavaMailcoremailbj126app38126_

Content-Type:application/octet-stream; name="2008_02_09.rar"

Content-Description:2008_02_09.rar

Content-Disposition:attachment; filename="2008_02_09.rar"; size=4316635;

         creation-date="Sat, 16 Feb 200807:33:05 GMT";

         modification-date="Sat, 16 Feb2008 07:33:05 GMT"

Content-Transfer-Encoding:base64

 

UmFyIRoHAM+QcwAADQAAA(此处为base64编码的二进制数据,略)

 

--_004_29815604740571203145344048JavaMailcoremailbj126app38126_--

 

七、聚合邮件(aggregatedmessage)

假如我们发送一封包含网页(html文档)的电子邮件,并且希望接收方的用户邮件软件能够显示这张网页,那么邮件就必须有聚合资源的机制。我们知道,html文档中往往包含图象、视频片断、Applet等数据资源,这些数据资源并不是包含在html文档中的,而是存放在Web服务器上,html用URI指出获取这些资源的方法。例如,在html中,图象元素的语法是<img src=URI >,其中的URI是图象资源的URI,通常是http命名方案的URI,浏览器据此可获得图象数据并显示在网页窗口上。

又如Applet的语法是<Applet codebase=URI  code=class_file>,其中的URI指出Applet代码的存放位置,class_file指出此处要执行的类文件名。浏览器获得html文档之后,必须根据这些信息从网络上获取代码,然后才能执行这些代码。

我们将包含图象、Applet等资源的html文档称为根数据(root resource),将其中包含的资源称为附属数据(subsidiary resource)。并非只有网页才包含附属数据,其它格式的文档(例如XML、pdf文档)也有这种情况。聚合邮件是用来包含这类文档的邮件。

    不能要求用户邮件软件也像浏览器那样通过网络获取附属数据,因为用户经常要在脱机的环境中阅读已下载到本地的邮件。因此,必须在邮件中同时包含根数据和附属数据。我们可以利用分块邮件,将根数据和各种附属数据分别存放在各自的块中。分块邮件的每一块都可以指定块内数据类型和传送编码,完全可以包含各种类型的附属资源。但是这种邮件内各块是存在引用关系的,必须由根数据块引用附属数据块中的数据,因此必须指出哪个数据块是根数据,哪些是附属数据,以及根数据如何引用附属数据。MIME已定义的分块类型Multipart/*并没有反映这种关系,为此,RFC2387提出一种新的分块类型Multipart/Related,RFC2392提出一种命名方案为mid和cid的URI,用来标识被引用的附属数据块,而RFC2557则定义一个新的MIME头段Content-Location来标识数据块。

7.1Multipart/Related

Multipart/Related是一种分块数据类型,用在头段Content-Type中,其语法是

            Content-type: Multipart/Related *(; params)

其中params是参数。可以使用多个参数,每个参数的格式都是“参数名 = 参数值”的形式。Multipart/Related类型使用的参数有:

•       boundary:块分界字符串,所有的Multipart/*类型都必须有这个参数,详见4.4.1节。

•       type:参数值是MIME媒体类型,例如text/html,text/plain等。这个参数用来指出根数据的类型。这个参数是必有的。虽然根数据块内也可以用头段Content-type指出媒体类型,但是这个参数使得用户邮件软件一开始就可以知道根数据块的类型。

•       start:参数值是标识根数据块的URI,其作用是指出当前邮件中哪一块是根数据块。这个参数是可选的,如果没有这个参数,则默认邮件中第一块为根数据块。

•       strat-info:为邮件应用程序提供进一步的信息,例如一些可执行的命令。这个参数是可选的,其参数值由应用程序自行解释。

本章第六节提到头段Content-Disposition,这个头段可用来指出块内数据是inline还是attachment。当邮件正文为Multipart/Related时,所有的块都应该是inline,因此头段Content-Disposition在此处是没有意义的。但是仍然允许在块内使用这个头段,因为那些不知道Multipart/Related类型的软件将会将邮件作为Multipart/mixed处理,这时Content-Disposition可以告诉它们数据的处理方式。

   支持Multipart/Related类型的软件则必须忽略块内的Content-Disposition头段。

7.2  Content-ID

为了让根数据引用附属数据,可以在附属数据块中用头段Content-ID指定一个URI,然后在根数据中按照这个URI引用数据。

    例如,如果我们在邮件中包含一个html文档,则其中可能包含了引用附属数据的URI,例如

         <imgsrc="http://www.foo.com/images/bar.gif" >

其中的http://www.foo.com/images/bar.gif本身就是一个URI,但这种URI并不是指向聚合邮件内的数据,而是指向网络上的某个位置(头段Content-Location可利用这种URI,稍后介绍)。使用Content-ID的方法是改写根数据中的URI,使之指向邮件内的某一块。

    这种URI有两种命名方案:cid和mid。cid用来标识邮件内的一个数据块,mid则用来标识一封邮件及其中的某一块。

    cid的语法是:

       cid:localpart@domain

简单地说,就是“cid:”后面再加上一个“邮箱地址”,不过其中的localpart并非用户名,而是作为ID用的标识字符串,这串ID字符的唯一性由设计它的主机负责,domain是该主机的域名。由于域名是唯一的,localpart在域名对应的主机上也是唯一的,所以这种URI是全球唯一的。

    当用cid标识数据块时,块内用头段Content-ID指出该块的cid,但要去掉开头的“cid:”并且loacalpart@domian要用一对尖括号括起来,而根数据中对应的cid则使用原状。

    例6:使用cid的聚合邮件。

From:foo1@bar.net

To:foo2@bar.net

MIME-Version:1.0

Content-type:multipart/related; boundary="simple-example";

                type=text/html

 

-- simple-example

Content-type:text/html; charset="GB2312"

Content-Transfer-Encoding:8bit

 

   此处为html内容,其中包括一幅图象,其中的URI已设为cid:

       <img src="cid:foo*id@bar.net">

 

--simple-example

Content-ID:< foo*id@bar.net >

Content-type:image/gif

Content-Transfer-Encoding:base64

 

UmFyIRoHAM+QcwAADQAAAAAAAADhi3QgglIAmwIAAD....

 

--simple-example--

 

    命名方案mid的URI语法为:

       mid:localpart@domian [ /cid:localpart2@domain2 ]

其中mid:后面的localpart@domain部分是邮件ID标识。可选部分是一个cid,mid如果不包含这个可选部分,则标识一封邮件,如果包含cid,则标识指定邮件中的某一数据块。

    为邮件指定mid的方法是在邮件头加上头段Message-ID,其段体为一个mid,但是象Content-ID使用cid一样,去掉开头的“mid:”,其余部分用尖括号括起来。例如,如果我们为例6的邮件指定一个ID:mid:mail.id@bar.net,则可在邮件头加上

      Message-ID: < mail.id@bar.net>

如果其它邮件引用这封邮件中的image/gif数据,则它们可以用如下mid:

      mid:mail.id@bar.net / cid:foo*id@bar.net

    最后要指出,使用mid和cid时,其中的localpart@domain部分必须遵守RFC822的规定,但是整个URI又必须遵守统一资源定位符的语法规定,这有时可能会有矛盾。特别是,RFC822允许使用的字符有些在URI中是不能使用的,解决的办法是用URI字符转义的方法(即%hh的表示方法)。

7.3 Content-Location

如果一个聚合邮件中的根数据和附属数据是从网上复制下来的,则使用cid或mid意味着必须修改根数据中所有对附属数据的URI引用。例如,修改html中img元素的属性src,修改Applet元素的属性codebase,等等。这不仅麻烦,而且有时候是不允许的,因为这样做破坏了原文档的完整性,如果文档有校验和的话,将会被认为是无效文档。

新增加的头段Content-Location可以解决这个问题。在聚合邮件中,根数据不必修改,所有的附属数据各自成为一块,在这些附属数据块的块头加上头段Content-Location,其段体就是根数据中引用它的URI。

    例7:使用Content-Location的聚合邮件。

From: foo1@bar.net

To: foo2@bar.net

MIME-Version: 1.0

Content-type: multipart/related; boundary="simple-example";

                type=text/html

 

-- simple-example

Content-type:text/html; charset="GB2312"

Content-Transfer-Encoding:8bit

 

   此处为html内容,其中包括一幅图象:

       <img src="http://www.bar.net/images/logo.gif">

 

--simple-example

Content-Location:http://www.bar.net/images/logo.gif

Content-type:image/gif

Content-Transfer-Encoding:base64

 

UmFyIRoHAM+QcwAADQAAAAAAAADhi3QgglIAmwIAAD....

 

--simple-example--

 

这种做法非常简单,但是,如果根数据中引用附属数据的URI是相对URI,事情就会有点麻烦。相对URI是相对于基底URI的,因此,必须确定基底URI。确定的方法有:

•       依据根数据中给出的基底URI。例如,html可以用Base元素指定基底URI。

•       检查邮件头中是否有字段Content-Location。这个字段可以在邮件头中使用,可用来指定基底URI。

•       如果聚合数据是从网上通过http协议获得的,则发起http连接时使用的URI可作为基底URI。

•       如果以上方法都不行,则用“thismessage:/”作为基底URI。这种URI仅限于在聚合邮件内引用数据。

另外,头段Centent-Location必须遵守RFC822的语法,特别是,只能使用ASCII字符。但是URI有可能出现非ASCII字符。例如,中文网站上的网页URI往往含有汉字,这种URI不能直接用在Centent-Location中,必须使用RFC2047提出的方法(参见本章第五节)。

7.4  多层聚合

如果Multipart/Related类型的分块邮件中,其中某一块又是Multipart/Related类型,即出现分块嵌套,则称为多层聚合。例如,邮件的根数据是使用框架的html文档:

<frameset rows="50%,50%">

   <frame src="doc1.htm"name=a1 >

   <framesrc="doc2.htm" name=a2 >

</frameset>

则其中将包含两个html文档,这两个作为附属数据的html文档可能又包含图象等附属数据,这样的文档构造成聚合邮件就会产生分块嵌套。

对于多层聚合,同一层内各块之间的引用是允许的,但隔层引用只能是内层引用直接外层,不允许外层引用内层。例8可以说明这个问题,这个例子来自RFC2557,此处为层次加上标号并且画了一个示意图。

 

例8:多层聚合的相互引用。

 
 

邮件头

 

 

 

 

 

 

 

A层

 

 

 

 

 

   A.1

 

 

 

 

 

 

 

 

 

      ①

 

 

 

 

 

 

 

 

 

 

 

 

 

      ②

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

   A.2

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

   A.3

 

  B层

 

 

 

 

 

    B.1

 

 

 

 

 

 

 

 

 

    ③

 

 

 

 

 

 

 

 

 

      ④

 

 

 

 

 

 

 

    B.2

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

   A.4

 

  C层

 

 

 

 

 

    C.1

 

 

 

 

 

 

 

 

 

      ⑤

 

 

 

 

 

 

 

 

 

      ⑥

 

 

 

 

 

 

 

    C.2

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 
 
 

From:  foo1@bar.net

 

To: foo2@bar.net

 

Subject: A simple  example

 

Mime-Version: 1.0

 

Content-Type:  multipart/related; boundary="boundary-example-1";

 

              type="text/html"

 

 

 

--boundary-example-1

 

Content-Type:  text/html;charset="US-ASCII"

 

Content-ID:  <foo3@foo1@bar.net>

 

 

 

  The image reference below will be resolved  with the image in the next body part.

 

  <IMG  SRC="http://www.ietf.cnri.reston.va.us/images/ietflogo.gif"

 

      ALT="IETF logo with white background">

 

 

 

  The image reference below cannot be resolved  within this

 

  MIME message, since it contains a reference  from an outside

 

  body part to an inside body part, which is  not supported

 

  by this standard.

 

  <IMG SRC=images/ietflogo2e.gif"

 

    ALT="IETF logo with transparent  background">

 

 

 

  The anchor reference immediately below will  be resolved with

 

  the nested text/html body part below:

 

  <A  HREF="http://www.ietf.cnri.reston.va.us/more-info>

 

     More info</A>

 

 

 

   The anchor reference immediately below  will be resolved with

 

   the nested text/html body part below:

 

   <A  HREF="http://www.ietf.cnri.reston.va.us/even-more-info>

 

      Even more info</A>

 

 

 

--boundary-example-1

 

Content-Location:  http://www.ietf.cnri.reston.va.us/images/ietflogo.gif

 

Content-Type:  IMAGE/GIF

 

Content-Transfer-Encoding:  BASE64

 

 

 

R0lGODlhGAGgAPEAAP/ZRaCgoAAAACH+PUNvcHlyaWdodCAoQykgMTk5

 

NSBJRVRGLiBVbmF1dGhvcml6ZWQgZHVwbGljYXRpb24gcHJvaGliaXRlZC4A

 

etc...

 

 

 

--boundary-example-1

 

Content-Location:  http://www.ietf.cnri.reston.va.us/more-info

 

Content-Type:  multipart/related; boundary="boundary-example-2";

 

                 type="text/html"

 

--boundary-example-2

 

Content-Type:  text/html;charset="US-ASCII"

 

Content-ID:  <foo4@foo1@bar.net>

 

 

 

  The image reference below will be resolved  with the image

 

  in the surrounding multipart/related above.

 

  <IMG SRC="images/ietflogo.gif"

 

  ALT="IETF logo with white  background">

 

 

 

  The image reference below will be resolved  with the image

 

  inside the current nested multipart/related  below.

 

  <IMG SRC=images/ietflogo2e.gif"

 

      ALT="IETF logo with transparent  background">

 

 

 

--boundary-example-2

 

Content-Location:  http:images/ietflogo2e.gif

 

Content-Type:  IMAGE/GIF

 

Content-Transfer-Encoding:  BASE64

 

 

 

R0lGODlhGAGgANX/ACkpKTExMTk5OUJCQkpKSlJSUlpaWmNjY2tra3Nzc3t7e4

 

SEhIyMjJSUlJycnKWlpa2trbW1tcDAwM7Ozv/eQnNzjHNzlGtrjGNjhFpae1pa

 

etc...

 

 

 

--boundary-example-2--

 

--boundary-example-1

 

Content-Location:  http://www.ietf.cnri.reston.va.us/even-more-info

 

Content-Type:  multipart/related; boundary="boundary-example-3";

 

      type="text/html"

 

--boundary-example-3

 

Content-Type:  text/html;charset="US-ASCII"

 

Content-ID:  <4@foo@bar.net>

 

 

 

   The image reference below will be resolved  with the image

 

   inside the current nested  multipart/related below.

 

   <IMG SRC=images/ietflogo2d.gif" ALT="IETF  logo with shadows">

 

 

 

   The image reference below cannot be  resolved according to

 

   this standard since references between  parallel multipart/

 

   related structures are not supported.

 

   <IMG SRC=images/ietflogo2e.gif"

 

   ALT="IETF logo with transparent  background">

 

 

 

--boundary-example-3

 

Content-Location:  http:images/ietflogo2d.gif

 

Content-Type:  IMAGE/GIF

 

Content-Transfer-Encoding:  BASE64

 

 

 

R0lGODlhGAGgANX/AMDAwCkpKTExMTk5OUJCQkpKSlJSUlpaWmNjY2tra3Nz

 

c3t7e4SEhIyMjJSUlJycnKWlpa2trbW1tb29vcbGxs7OztbW1t7e3ufn5+/v

 

etc...

 

 

 

--boundary-example-3--

 

--boundary-example-1--

 

 

例8中各层相互引用的情况可用图1说明:

                                                                             

   
    
    
    

A层

    
    
                  
   
    
    
    

A.1

    
    
                  
   
    
    
    

A.2

    
    
                  
   
    
    
    

B层(A.3)

    
    
                  
   
    
    
    

B.1

    
    
                  
   
    
    
    

B.2

    
    
                  
   
    
    
    

C层(A.4)

    
    
                  
   
    
    
    

C.1

    
    
                  
   
    
    
    

C.2

    
    
                                                                                                                                                                                                           
   
    
    
    

    
    
                  
   
    
    
    

    
    
                  
   
    
    
    

    
    
                  
   
    
    
    

    
    
                  
   
    
    
    

    
    
                  
   
    
    
    

    
    
            

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 


                   图1:

在图1中:

     引用①是允许的,这是同层引用;

     引用②是不允许的,这是外层引用内层;

     引用③是允许的,这是内层引用相邻外层;

     引用④是允许的,这是同层引用;

     引用⑤是允许的,这是同层引用;

     引用⑥是不允许的,这是平行层隔层引用。

 

 

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值