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,才可以發送和接收郵件。