電子郵件的分析和讀取一般都通過專用的郵件軟體來實作,比如Outlook、Foxmail,但這種第三方軟體無法和開發者自己的系統整合,通過對MIME郵件格式的分析,我們可以在自己的應用程式中實作對MIME郵件所含資訊的讀取。
MIME技術規範的完整内容由RFC 2045-2049定義,包括了資訊格式、媒體類型、編碼方式等各方面的内容,這裡我們隻介紹其中的一些關鍵的格式和規範,通過了解這些格式規範,我們就可以實作以程式設計的方式從MIME郵件中提取基本的郵件資訊。
MIME郵件的基本資訊、格式資訊、編碼方式等重要内容都記錄在郵件内的各種域中,域的基本格式:{域名}:{内容},域由域名後面跟“:”再加上域的資訊内容構成,一條域在郵件中占一行或者多行,域的首行左側不能有空白字元,比如空格或者制表符,占用多行的域其後續行則必須以空白字元開頭。域的資訊内容中還可以包含屬性,屬性之間以“;”分隔,屬性的格式如下:{屬性名稱}=”{屬性值}”。
表1是一封示例郵件的内容,其中行1-5、行8都是單行的域,行6-7則是一個多行的域,并帶有一個名為charset的屬性,屬性值為us-ascii。
表1 示例電子郵件
行1 From: ”suntao” <[email protected]>
行2 To: <[email protected]>
行3 Subject: hello world
行4 Date: Mon, 9 Oct 2006 16:51:34 +0800
行5 MIME-Version: 1.0
行6 Content-Type: text/plain;
行7 charset="us-ascii"
行8 Date: Mon, 9 Oct 2006 16:48:25 +0800
行9
行10 Hello world
行11
郵件規範中定義了大量域,分别用來存儲同郵件相關的各種資訊,比如發件人的名字和郵件位址資訊存儲在From域中,收件人的郵件位址資訊存儲在To域中,開發人員可通過查詢RFC文檔得到完整的郵件域定義清單。
Content-Type域定義了郵件中所含各種内容的類型以及相關屬性。郵件所含的文本、超文本、附件等資訊都按照對應Content-Type域所指定的媒體類型、存儲位置、編碼方式等資訊存儲在郵件中。Content-Type域基本格式:Content-Type:{主類型}/{子類型}。
示例郵件中的行6-7就是一個Content-Type域,主類型為text,子類型為plain,字元集屬性為us-ascii。
表2:MIME郵件中常見的主類型
主類型
常見屬性
參數含義
text
charset
文本資訊所使用的字元集
image
name
圖像的名稱
application
應用程式的名稱
multipart
boundary
郵件分段邊界辨別
MIME郵件中各種不同類型的内容是分段存儲的,各個段的排列方式、位置資訊都通過Content-Type域的multipart類型來定義。multipart類型主要有三種子類型:mixed、alternative、related。
1.3.1 multipart類型基本格式
● multipart/mixed類型
如果一封郵件中含有附件,那郵件的Content-Type域中必須定義multipart/mixed類型,郵件通過multipart/mixed類型中定義的boundary辨別将附件内容同郵件其它内容分成不同的段。基本格式如下:
Content-Type: multipart/mixed;
boundary="{分段辨別}"
● multipart/alternative類型
MIME郵件可以傳送超文本内容,但出于相容性的考慮,一般在發送超文本格式内容的同時會同時發送一個純文字内容的副本,如果郵件中同時存在純文字和超文本内容,則郵件需要在Content-Type域中定義multipart/alternative類型,郵件通過其boundary中的分段辨別将純文字、超文本和郵件的其它内容分成不同的段。基本格式如下:
Content-Type: multipart/alternative;
● multipart/related類型
MIME郵件中除了可以攜帶各種附件外,還可以将其它内容以内嵌資源的方式存儲在郵件中。比如我們在發送html格式的郵件内容時,可能使用圖像作為html的背景,html文本會被存儲在alternative段中,而作為背景的圖像則會存儲在multipart/related類型定義的段中。基本格式如下:
Content-Type: multipart/related;
type="multipart/alternative";
1.3.2 multipart類型的boundary屬性
multipart的子類型中都定義了各自的boundary屬性,郵件使用這些boundary中定義的字元串作為辨別,将郵件内容分成不同的段,段體内的每個子段以“--”+boundary行開始,父段則以“--”+boundary+“--”行結束,不同段之間用空行分隔。
1.3.3 multipart類型的層次關系
表3:multipart子類型之間的層次關系
Multipart/mixed
Multipart/related
Multipart/alternative
純文字正文
超文本正文
内嵌資源
附件
MIME郵件通過多個Content-Type域的multipart類型将内容分成不同的段,這些段在郵件中不是線形順序排列的,而是存在一個互相包含的層次關系,multipart子類型之間的層次關系結構如表3。
MIME郵件可以傳送圖像、聲音、視訊以及附件,這些非ASCII碼的資料都是通過一定的編碼規則進行轉換後附着在郵件中進行傳遞的。編碼方式存儲在郵件的Content-Transfer-Encoding域中,一封郵件中可能有多個Content-Transfer-Encoding域,分别對應郵件不同部分内容的編碼方式。目前MIME郵件中的資料編碼普遍采用Base64編碼或Quoted-printable編碼來實作。
1.4.1 Base64編碼
Base64編碼的目的是将輸入的資料全部轉換成由64個指定ASCII字元組成的字元序列, 這64個字元由{'A'-'Z', 'a'-'z', '0'-'9', '+', '/'}構成。編碼時将需要轉換的資料每次取出6bit,然後将其轉換成十進制數字,這個數字的範圍最小為0,最大為63,然後查詢{'A'-'Z', 'a'-'z', '0'-'9', '+', '/'}構成的字典表,輸出對應位置的ASCII碼字元,這樣每3個位元組的資料内容會被轉換成4個字典中的ASCII碼字元,當轉換到資料末尾不足3個位元組時,則用“=”來填充。
1.4.2 Quoted-printable編碼
Quoted-printable編碼的目的也是将輸入的資訊轉換成可列印的ASCII碼字元,但它是根據資訊的内容來決定是否進行編碼,如果讀入的位元組處于33-60、62-126範圍内的,這些都是可直接列印的ASCII字元,則直接輸出,如果不是,則将該位元組分為兩個4bit,每個用一個16進制數字來表示,然後在前面加“=”,這樣每個需要編碼的位元組會被轉換成三個字元來表示。
從上面的分析可以看出,MIME郵件傳遞的實際是一個經過特殊編碼并以約定格式排列的字元序列,我們隻需要提取存儲在郵件各種域中的格式、位置和編碼資訊,按照根據這些資訊從字元序列中提取出對應的字元内容并對其進行反向解碼,就可以得到我們需要的有關内容。
下面給出.Net環境下,利用C#結合正規表達式從郵件中提取相關資訊的基本思路和部分代碼。
收件人、發件人、郵件主題是一封郵件的基本組成資訊,分别存郵件的From域、To域、Subject域中。開發中隻需要通過正規表達式來比對這些指定的域,然後從比對結果中取出相關資訊即可。
示例代碼:提取郵件主題
string emailContent = “……”;//emailContent中存儲的是郵件内容
pat = @"^Subject:\s*(?<title>.*)\s*\r\n";
myMatches = Regex.Matches(emailContent,pat,RegexOptions.Multiline);
foreach(Match nextMatch in myMatches)
{
GroupCollection myGroup = nextMatch.Groups;
string title = myGroup["title"].ToString();//title變量存儲From域的内容
……
}
需要注意的是上面的代碼提取的是跟随在Subject:後面的字元串,如果郵件的主題内容是中文或者其它需要編碼的地區文字,則還需要對其進行解碼。比如,如果郵件的Subject域中的資訊是“你好”,那麼提取出來的字元串會是這種形式:=?gb2312?B?xOO6ww==?=,第一個?同第二個?之間的gb2312代表标題内容所使用的字元集,第二個?和第三個?之間的B代表這部分内容采用的是base64編碼方式,如果采用Quoted-printabel編碼方式則顯示Q,第三個?和第四個?之間則是“你好”經過base64編碼後的字元串。
郵件通過multipart類型将内容分隔成不同的段,各段之間的邊界辨別由對應multipart類型的boundary屬性定義。要從郵件中提取出需要的内容,首先需要提取出郵件中的分段資訊。下面的代碼從一封郵件中提取出所有的multipart類型的名稱和boundary屬性。
示例代碼:提取multipart資訊
string pat = @"\bContent-Type:\s*(?<type>\w+/\w+);\s+(type=\S(?<subtype>\S+)\S)?\s+boundary=""(?<flag>\S+)""";
MatchCollection myMatches = Regex.Matches(emailContent,pat);
string type = myGroup["type"].ToString();//type變量存儲multipart類型的名稱
string flag = myGroup["flag"].ToString();//flag變量存儲multipart類型的boundary屬性
郵件中的附件資訊由對應的Content-Type域、Content-Transfer-Encoding域、Content-Disposition域和multipart/mixed類型定義,前三個域定義附件的類型、名稱和編碼方式,multipart/mixed則定義附件同郵件其它内容的分段辨別。基本格式如下:
--boundary分段辨別
Content-Type: application/msword;
name="readme.doc"
Content-Transfer-Encoding: base64
Content-Disposition: attachment;
filename=" readme.doc "
……
檔案内容的Base64編碼
示例代碼:提取郵件附件
//boundaryMixed代表已經提取出的multipart/mixed類型的boundary辨別
//DecodeBase64為自定義的base64解碼函數
//DecodeQuotedPrintable為自定義的quoted-printable解碼函數
string pat = @"\r\nContent-Type:\s*(?<filetype>\S*);\s*name=""(?<name>\S*)""\s*Content-Transfer-Encoding:\s*(?<encoding>\S*)\s*Content-Disposition:\s*attachment;\s*filename=""(?<filename>\S+)""\s+(?<content>[\S|\r\n]+)" + "--" + boundaryMixed;
MatchCollection myMatches = Regex.Matches(emailContent,pat,RegexOptions.Singleline);
//提取附件的類型、編碼方式、檔案名、内容資訊
string fileType = myGroup["filetype"].ToString();
string encoding = myGroup["encoding"].ToString();
string fileName = myGroup["filename"].ToString();
string content = myGroup["content"].ToString().Trim();
byte[] attachFile;
//根據附件的編碼方式對提取出的附件内容進行解碼
if(encoding == “base64”)
{
attachFile = DecodeBase64 (content);
if(encoding == “quoted-printable”)
attachFile = DecodeQuotedPrintable (content);
//将解碼後的内容寫入磁盤
FileStream fs = new FileStream("c:\\" + fileName,
FileMode.CreateNew);
BinaryWriter bw = new BinaryWriter(fs);
bw.Write(attachFile);
bw.Close();
fs.Close();
上面的程式從郵件原文中提取出附件資訊,并根據附件采用的編碼類型進行解碼,然後将解碼後的内容按照原檔案名存儲到C槽根目錄。同樣,如果附件的檔案名是中文或者其它需要編碼的文字,則首先需要對檔案名進行解碼。
本文對MIME郵件的基本格式做了分析和闡述,介紹了MIME中幾個重要的規範和定義,并給出了利用正規表達式從郵件内容中提取相關資訊的基本思路和方法。在開發中需要注意的是,郵件中所含的内容決定了郵件的具體格式,multipart類型以及對應的分段辨別隻有在有相關内容的時候才會在郵件中出現,在開發時需要具體分析。MIME的詳細技術規範可以查詢RFC的相關文檔