POP郵件協議的優點在於它是一個開放的標准,有著完善的文檔,這就使得編寫POP郵件客戶程序不那麼困難,只要掌握了POP、SMTP的基礎知識,就可以寫出代理程序來執行各種任務,例如過濾廣告和垃圾郵件,或提供e-mail自動應答服務。
Hotmail是世界上影響最廣的Web郵件系統,遺憾的是,當我們要為Hotmail編寫獨立的客戶程序(不通過浏覽器訪問的客戶程序)時,馬上就會遇到Hotmail不提供POP網關這一障礙。
雖然Hotmail不提供POP支持,但浏覽器並非訪問Hotmail的唯一途徑。例如,利用Outlook Express可以直接連接到標准的Hotmail或MSN信箱,提取、刪除、移動或發送郵件。利用HTTP包監視器,我們可以監視到Outlook Express和Hotmail的通信過程,分析出客戶程序如何連接到Hotmail信箱。
Outlook Express利用了一種通常稱為HTTPMail的未公開的協議,借助一組HTTP/1.1擴展訪問Hotmail。本文將介紹HTTPMail的一些特點以及利用C#客戶程序訪問Hotmail的過程。本文的示例程序利用COM互操作將XMLHTTP用作一種傳輸服務。XMLHTTP組件提供了一個完善的HTTP實現,除了包括認證功能,還能夠在發送HTTP請求給服務器之前設置定制的HTTP頭。
一、連接HTTPMail網關
Hotmail信箱默認的HTTPMail網關在http://services.msn.com/svcs/hotmail/httpmail.asp。HTTPMail協議實際上是一個標准的WebDAV服務,只不過尚未公開而已。在編寫C#程序時,我們可以方便地調用.NET框架在System.Net名稱空間中提供的各個TCP和HTTP類。另外,由於我們要操作WebDAV,在C#環境下利用XMLHTTP連接Hotmail最為簡便,只需引用一下MSXML2組件就可以直接訪問。注意在本文的代碼片斷中,帶有下滑線後綴的變量是示例代碼中聲明的成員域:
// 獲得名稱空間
using MSXML2;
...
// 創建對象
xmlHttp_ = new XMLHTTP();
為了連接到安全服務器,WebDAV協議要求執行HTTP/1.1驗證。HTTPMail客戶程序發出的第一個請求利用WebDAV PROPFIND方法查找一組屬性,其中包括Hotmail廣告條的URL以及信箱文件夾的位置:
<?xml version="1.0"?>
<D:propfind xmlns:D="DAV:" xmlns:h="http://schemas.microsoft.com/hotmail/"
xmlns:hm="urn:schemas:httpmail:">
<D:prop>
<h:adbar/>
<hm:contacts/>
<hm:inbox/>
<hm:outbox/>
<hm:sendmsg/>
<hm:sentitems/>
<hm:deleteditems/>
<hm:drafts/>
<hm:msgfolderroot/>
<h:maxpoll/>
<h:sig/>
</D:prop>
</D:propfind>
通過XMLHTTP發送第一個請求時,首先指定WebDAV服務器URL,然後生成XML請求的內容:
// 指定服務器的URL
string serverUrl = "http://services.msn.com/svcs/hotmail/httpmail.asp";
// 構造查詢
string folderQuery = null;
folderQuery += "<?xml version='1.0'?><D:propfind xmlns:D='DAV:' ";
folderQuery += "xmlns:h='http://schemas.microsoft.com/hotmail/' ";
folderQuery += "xmlns:hm='urn:schemas:httpmail:'><D:prop><h:adbar/>";
folderQuery += "<hm:contacts/><hm:inbox/><hm:outbox/><hm:sendmsg/>";
folderQuery += "<hm:sentitems/><hm:deleteditems/><hm:drafts/>";
folderQuery += "<hm:msgfolderroot/><h:maxpoll/><h:sig/></D:prop></D:propfind>";
XMLHTTP組件提供了一個open()方法來建立與HTTP服務器的連接:
void open(string method, string url, bool async, string user, string password);
open()方法的第一個參數指定了用來打開連接的HTTP方法,例如GET、POST、PUT或PROPFIND,通過這些HTTP方法我們可以提取文件夾信息、收集郵件或發送新郵件。為連接到Hotmail網關,我們指定用PROPFIND方法來查詢信箱。注意open()方法允許執行異步調用(默認啟用),對於帶圖形用戶界面的郵件客戶程序來說,異步調用是最理想的調用方式。由於本文的示例程序是一個控制台應用,我們把這個參數設置成false。
為了執行身份驗證,我們在open()方法中指定了用戶名字和密碼。在使用XMLHTTP組件時,如果open()方法沒有提供用戶名字和密碼參數,但網站要求執行身份驗證,XMLHTTP將顯示出一個登錄窗口。為了打開通向Hotmail網關的連接,我們把PROPFIND請求的頭設置成XML查詢的內容,消息的正文保持空白,然後發送消息:
// 打開一個通向Hotmail服務器的連接
xmlHttp_.open("PROPFIND", serverUrl, false, username, password);
// 發送請求
xmlHttp_.setRequestHeader("PROPFIND", folderQuery);
xmlHttp_.send(null);
二、分析信箱的文件夾列表
發送給services.msn.com的請求通常要經歷幾次重定向,經過服務器端的負載平衡處理,最後請求會被傳遞到一個空閒的Hotmail服務器,並執行身份驗證。在客戶端,這個重定向、執行身份驗證的交互過程由XMLHTTP組件負責處理。成功建立連接後,服務器還會要求設置一些Cookie、驗證當前會話的合法性,這部分工作同樣也由XMLHTTP組件自動處理。初始的連接請求發出之後,服務器將返回一個XML格式的應答:
// 獲得應答的內容
string folderList = xmlHttp_.responseText;
服務器返回的應答包含許多有用的信息,其中包括信箱中文件夾的URL位置,下面是一個例子:
<?xml version="1.0" encoding="Windows-1252"?>
<D:response>
...
<D:propstat>
<D:prop>
<h:adbar>AdPane=Off*...</h:adbar>
<hm:contacts>http://law15.oe.hotmail.com/...<;/hm:contacts>
<hm:inbox>http://law15.oe.hotmail.com/...<;/hm:inbox>
<hm:sendmsg>http://law15.oe.hotmail.com/...<;/hm:sendmsg>
<hm:sentitems>http://law15.oe.hotmail.com/...<;/hm:sentitems>
<hm:deleteditems>http://law15.oe.hotmail.com/...<;/hm:deleteditems>
<hm:msgfolderroot>http://law15.oe.hotmail.com/...<;/hm:msgfolderroot>
...
</D:prop>
</D:response>
</D:multistatus>
在本文的控制台示例程序中,我們感興趣的兩個文件夾是收件箱和發件箱的文件夾,它們分別用於接收和發送郵件。
在C#環境中解析XML的方法很多,由於我們肯定代碼涉及的所有XML文檔總是合法的,所以可以利用System.XML.XmlTextReader速度快的優勢。XmlTextReader是一個“只向前”的讀取器,下面把XML字符數據轉換成字符流,初始化XML讀取器:
// 初始化
inboxUrl_ = null;
sendUrl_ = null;
// 裝入XML
StringReader reader = new StringReader(folderList);
XmlTextReader xml = new XmlTextReader(reader);
遍歷各個節點,選取出hm:inbox和hm:sendmsg節點,這兩個節點分別代表收件箱和發件箱:
// 讀取XML數據
while(xml.Read())
{
// 是一個XML元素?
if(xml.NodeType == XmlNodeType.Element)
{
// 獲取該節點
string name = xml.Name;
// 該節點代表收件箱?
if(name == "hm:inbox")
{
// 保存收件箱URL
xml.Read();
inboxUrl_ = xml.Value;
}
// 該節點代表發件箱?
if(name == "hm:sendmsg")
{
// 保存發件箱URL
xml.Read();
sendUrl_ = xml.Value;
}
}
}
只有先獲取當前這次會話的合法的收件箱和發件箱URL,才可以發送和接收郵件。
三、列舉文件夾內容
得到了信箱文件夾(如收件箱)的URL之後,就可以向該文件夾的URL發送WebDAV請求列舉其內容。示例程序定義了一個托管類型MailItem,用來保存文件夾裡一項內容(即一個郵件)的信息。文件夾內容列舉從初始化一個MailItems數組開始:
// 初始化
ArrayList mailItems = new ArrayList();
為獲得郵件主題、收件人地址、發件人地址之類的郵件基本信息,我們要用到下面XML格式的WebDAV查詢:
<?xml version="1.0"?>
<D:propfind xmlns:D="DAV:" xmlns:hm="urn:schemas:httpmail:" xmlns:m="
urn:schemas:mailheader:">
<D:prop>
<D:isfolder/>
<hm:read/>
<m:hasattachment/>
<m:to/>
<m:from/>
<m:subject/>
<m:date/>
<D:getcontentlength/>
</D:prop>
</D:propfind>
生成上述XML查詢字符串的C#代碼:
// 構造查詢
string getMailQuery = null;
getMailQuery += "<?xml version='1.0'?><D:propfind xmlns:D='DAV:' ";
getMailQuery += "xmlns:hm='urn:schemas:httpmail:' ";
getMailQuery += "xmlns:m='urn:schemas:mailheader:'><D:prop><D:isfolder/>";
getMailQuery += "<hm:read/><m:hasattachment/><m:to/><m:from/><m:subject/>";
getMailQuery += "<m:date/><D:getcontentlength/></D:prop></D:propfind>";
就象前面獲取信箱文件夾清單的方式一樣,上述請求也通過XMLHTTP用PROPFIND方法發送,這次我們把請求的正文設置成查詢字符串。由於當前會話的用戶身份已經通過驗證,所以XMLHTTP open()調用中不必再提供用戶名字和密碼:
// 獲取郵件信息
xmlHttp_.open("PROPFIND", folderUrl, false, null, null);
xmlHttp_.send(getMailQuery);
string folderInfo = xmlHttp_.responseText;
如果請求成功,服務器返回的應答XML流包含了該文件夾中各個郵件的信息:
<D:multistatus>
<D:response>
<D:href>
http://sea1.oe.hotmail.com/cgi-bin/hmdata/...
</D:href>
<D:propstat>
<D:prop>
<hm:read>1</hm:read>
<m:to/>
<m:from>Mark Anderson</m:from>
<m:subject>RE: New Information</m:subject>
<m:date>2002-08-06T16:38:39</m:date>
<D:getcontentlength>1238</D:getcontentlength>
</D:prop>
<D:status>HTTP/1.1 200 OK</D:status>
</D:propstat>
</D:response>
...
觀察服務器返回的應答,我們發現每一個節點包含一組標識郵件的域,例如通過標記可提取出郵件。下面我們再次使用System.XML.XmlTextReader解析這個XML數據流,首先初始化流讀取器:
MailItem mailItem = null;
// 裝入XML
StringReader reader = new StringReader(folderInfo);
XmlTextReader xml = new XmlTextReader(reader);
四、分析郵件基本信息
為了遍歷一次就解析好整個XML文檔,我們在每次打開元素時就創建一個新的MailItem實例,一遇到標記的末尾就保存該實例,在此期間,我們提取並設置MailItem的域:
// 讀取XML數據
while(xml.Read())
{
string name = xml.Name;
XmlNodeType nodeType = xml.NodeType;
// 是一個email?
if(name == "D:response")
{
// 開始?
if(nodeType == XmlNodeType.Element)
{
// 創建一個新的MailItem
mailItem = new MailItem();
}
// 結束?
if(nodeType == XmlNodeType.EndElement)
{
// 保存email
mailItems.Add(mailItem);
// 清除變量
mailItem = null;
}
}
// 是一個元素?
if(nodeType == XmlNodeType.Element)
{
// 郵件的URL屬性
if(name == "D:href")
{
// 繼續讀取
xml.Read();
mailItem.Url = xml.Value;
}
// 郵件的“已閱讀”屬性
if(name == "hm:read")
{
// 繼續讀取
xml.Read();
mailItem.IsRead = (xml.Value == "1");
}
// 其他MailItem的屬性...
}
}
上面的代碼枚舉指定文件夾內的每一個MailItem,分別提取各個MailItem的下列屬性:
XML節點 說明
D:href 用來提取郵件的URL
hm:read 如果郵件已閱讀,則該標記被設置
m:to 收件人
m:from 發件人
m:subject 郵件主題
m:date 時間標記
D:getcontentlength 郵件的大小(字節數)
五、接收郵件
枚舉出文件夾裡面的MailItem之後,我們就可以利用MailItem的URL獲得郵件本身,只需要向Hotmail服務器發送一個HTTP/1.1 GET請求就可以了。示例代碼中的LoadMail()函數輸入一個MailItem實例作為參數,返回郵件的內容:
/// <summary>
/// 下載MailItem指定的郵件
/// </summary>
public string LoadMail(MailItem mailItem)
{
// 郵件的URL
string mailUrl = mailItem.Url;
// 打開Hotmail服務器連接
xmlHttp_.open("GET", mailUrl, false, null, null);
// 發送請求
xmlHttp_.send(null);
// 獲取應答
string mailData = xmlHttp_.responseText;
// 返回郵件數據
return mailData;
}
六、發送郵件
LoadMail()方法通過發送HTTP/1.1 GET請求獲取郵件,類似地,用Hotmail發件箱發送郵件時我們提交一個POST請求,如下面的SendMail()方法所示。
/// <summary>
/// 發送一個郵件
/// </summary>
public void SendMail(string from, string fromName,
string to, string subject, string body)
{
...
}
首先准備好後面要用到的引號字符以及郵件的時間標記:
// 引號字符
string quote = "\u0022";
// 時間標記
DateTime now = DateTime.Now;
string timeStamp = now.ToString("ddd, dd MMM yyyy hh:mm:ss");
HTTPMail協議采用與SMTP相似的通信模式。Outlook Express用MIME格式發送郵件,但為簡單計,本例我們只發送純文本的郵件:
// 構造POST請求的內容
string postBody = null;
// 郵件頭.
postBody += "MAIL FROM:<" + from + ">\r\n";
postBody += "RCPT TO:<" + to + ">\r\n";
postBody += "\r\n";
postBody += "From: " + quote + fromName + quote + " <" + from + ">\r\n";
postBody += "To: <" + to + ">\r\n";
postBody += "Subject: " + subject +"\r\n";
postBody += "Date: " + timeStamp + " -0000\n";
postBody += "\r\n";
// 郵件正文
postBody += body;
發送郵件時,我們要把Content-Type請求頭設置成message/rfc821,表示這個請求包含一個遵從RFC821的消息。最後要做的就是把郵件發送到服務器:
// 打開連接
xmlHttp_.open("POST", sendUrl_, false, null, null);
// 發送請求
xmlHttp_.setRequestHeader("Content-Type", "message/rfc821");
xmlHttp_.send(postBody);
只要目標地址正確無誤,Hotmail就會把郵件發送到目的地。
結束語:
Hotmail是世界上最大的免費Web郵件提供商。但是,Hotmail使用的HTTPMail協議是非公開的,從而為編寫直接訪問Hotmail的客戶程序帶來了困難。本文示范了如何在C#環境中利用XMLHTTP組件直接連接到Hotmail,以及如何發送和接收郵件,證明了通過HTTPMail連接Hotmail可以做到象使用POP3、IMAP4、SMTP等協議一樣簡單。