65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 C# 访问 Hotmail – .NET 下的 HTTPMail 客户端

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.83/5 (59投票s)

2003年3月26日

8分钟阅读

viewsIcon

573040

downloadIcon

2349

一个使用未公开的 HTTPMail 协议访问 Hotmail 的 C# 客户端库。

引言

POP 邮件协议的好处在于它是一个文档齐全的开放标准,这使得编写一个邮件客户端来从 POP 邮箱收集邮件的过程相对轻松。掌握了 POP 或 SMTP 的基本知识,就有可能编写代理服务器来执行各种有用的任务,例如过滤垃圾邮件或提供电子邮件自动回复服务。不幸的是,在尝试编写一个独立的 Hotmail(世界上最受欢迎的基于 Web 的邮件系统)客户端时,没有 POP 网关的存在很快就成了一个问题。

尽管缺乏 POP 支持,但无需使用 Web 浏览器也能连接到 Hotmail。Outlook Express 允许用户检索、删除、移动和发送消息,直接连接到标准的 Hotmail 或 MSN 邮箱。通过使用 HTTP 数据包嗅探器,可以监视 Outlook Express 和 Hotmail 之间的通信,从而确定直接邮箱连接是如何建立的。

Outlook Express 使用一种通常被称为 HTTPMail 的未公开协议,允许客户端使用一组 HTTP/1.1 扩展来访问 Hotmail。本文将介绍 HTTPMail 的一些特性,以及如何最好地使用 C# 客户端连接到 Hotmail。本文附带的示例源代码使用 COM 互操作来利用 XMLHTTP 作为传输服务。XMLHTTP 组件提供了完整的 HTTP 实现,包括身份验证以及在发送 HTTP 请求到服务器之前设置自定义标头的能力。

连接到 HTTPMail Hotmail 网关

Hotmail 邮箱的默认 HTTPMail 网关位于 http://services.msn.com/svcs/hotmail/httpmail.asp。尽管 HTTPMail 协议未公开,但它实际上是一个标准的 WebDAV 服务。由于我们使用的是 C#,可以使用 .NET 框架在 System.Net 命名空间中提供的 TCP 和 HTTP 类。由于我们处理的是 WebDAV,使用 XMLHTTP 连接到 C# 下的 Hotmail 会更简单。引用 MSXML2 组件,它提供了一个可以被直接访问的互操作程序集。请注意,在本文中的代码片段中,带有下划线的变量是指在示例代码的其他地方声明的成员字段。

  // Get the namespace.
  using MSXML2;

  ...

  // Create the object.
  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 请求。

  // Specify the server URL.
  string serverUrl = "http://services.msn.com/svcs/hotmail/httpmail.asp";

  // Build the query.
  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>";

HTTPXML 组件提供了一个 open() 方法,用于建立与 HTTP 服务器的连接。

  void open(string method, string url, bool async, string user, string password);

第一个参数指定用于打开连接的 HTTP 方法,例如 GETPOSTPUTPROPFIND。要连接到 Hotmail 网关,我们指定 PROPFIND 方法来查询邮箱。此方法和其他 HTTP 方法用于检索文件夹信息、收集邮件项和发送新邮件。请注意,open() 方法支持异步调用(默认启用),这对于图形界面邮件客户端来说是首选。由于示例代码是一个控制台应用程序,我们将此参数设置为 false。对于身份验证,我们指定用户名和密码。请注意,在 XMLHTTP 下,如果缺少这些参数且网站需要身份验证,该组件将显示登录窗口。要连接到 Hotmail 网关,我们打开连接,将 PROPFIND 请求标头设置为我们的基于 XML 的查询,然后发送带有空主体的请求。

  // Open a connection to the Hotmail server.
  xmlHttp_.open("PROPFIND", serverUrl, false, username, password);

  // Send the request.
  xmlHttp_.setRequestHeader("PROPFIND", folderQuery);
  xmlHttp_.send(null);

解析邮箱文件夹列表

发送到 services.msn.com 的请求通常会重定向几次。在负载均衡后,我们最终连接到一个空闲的 Hotmail 服务器并完成身份验证。XMLHTTP 组件会处理这种重定向和适当的身份验证。连接后,服务器还会要求我们设置各种适用于当前会话的 Cookie(同样,所有这些都由 XMLHTTP 自动处理)。在发送初始连接请求后,服务器将返回一个基于 XML 的响应。

  // Get the response.
  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 提供了快速的、仅向前访问。我们通过将 XML 字符串数据转换为字符串流来初始化 XML 读取器。

  // Initiate.
  inboxUrl_ = null;
  sendUrl_ = null;

  // Load the Xml.
  StringReader reader = new StringReader(folderList);
  XmlTextReader xml = new XmlTextReader(reader);

通过遍历每个节点来解析 XML,提取 hm:inboxhm:sendmsg 节点。

  // Read the Xml.
  while(xml.Read())
  {
    // Got an element?
    if(xml.NodeType == XmlNodeType.Element)
    {
      // Get this node.
      string name = xml.Name;

      // Got the inbox?
      if(name == "hm:inbox")
      {
        // Set folder.
        xml.Read();
        inboxUrl_ = xml.Value;
      }

      // Got the send message page?
      if(name == "hm:sendmsg")
      {
        // Set folder.
        xml.Read();
        sendUrl_ = xml.Value;
      }
    }
  }

一旦确定了该会话有效的收件箱和发件箱的 URL,就可以发送和检索电子邮件。

枚举文件夹邮件项

给定一个邮箱文件夹的 URL(例如收件箱文件夹),我们可以向该文件夹的 URL 发送一个 WebDAV 请求,以列出该文件夹内的邮件项。示例控制台应用程序定义了一个托管类型 MailItem,用于存储文件夹项的邮件信息。文件夹枚举开始时,会初始化一个 MailItems 数组。

  // Initiate.
  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>

以下 C# 代码生成 XML 查询字符串:

  // Build the query.
  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() 调用中再次提供用户名和密码。

  // Get the mail info.
  xmlHttp_.open("PROPFIND", folderUrl, false, null, null);
  xmlHttp_.send(getMailQuery);
  string folderInfo = xmlHttp_.responseText;

在成功请求后,服务器将响应一个 XML 流,其中包含文件夹中每个 MailItem 的信息。

  <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>
    ...

查看上面的 XML 片段,我们发现每个 <D:response> 节点内都包含一组标识 MailItem 的字段,包括 <D:href> 标签,该标签稍后将允许我们检索该项。我们也可以使用 System.XML.XmlTextReader 来解析此 XML 文本流。我们首先初始化流读取器。

  // Holders.
  MailItem mailItem = null;

  // Load the Xml.
  StringReader reader = new StringReader(folderInfo);
  XmlTextReader xml = new XmlTextReader(reader);

解析文件夹信息

为了在单次传递中解析 XML,我们在打开 <D:response> 元素时创建一个新的 MailItem 实例,并在到达标签末尾时存储该实例。在此期间,我们提取并设置 MailItem 字段。

  // Read the Xml.
  while(xml.Read())
  {
    // Sections.
    string name = xml.Name;
    XmlNodeType nodeType = xml.NodeType;

    // E-mail?
    if(name == "D:response")
    {
        // Start?
        if(nodeType == XmlNodeType.Element)
        {
          // Create a new mail item.
          mailItem = new MailItem();
        }

        // End?
        if(nodeType == XmlNodeType.EndElement)
        {
          // Store the last mail.
          mailItems.Add(mailItem);

          // Reset.
          mailItem = null;
        }
      }

      // Got an element?
      if(nodeType == XmlNodeType.Element)
      {
        // Mail field.
        if(name == "D:href")
        {
          // Load.
          xml.Read();
          mailItem.Url = xml.Value;
        }

        // Mail field.
        if(name == "hm:read")
        {
          // Load.
          xml.Read(); 
          mailItem.IsRead = (xml.Value == "1");
        }

        // Load other MailItem fields, etc...
      }
    }

上面的代码(示例控制台应用程序中的一部分)枚举了给定文件夹中找到的 MailItems。对于每个 MailItem,我们提取以下字段:

XML 节点 描述
D:href 用于检索此 HttpMail 项的 URL。
hm:read 如果此电子邮件已读,则设置此标志。
m:to 邮件发送给了谁。
m:from 邮件来自谁。
m:subject 邮件主题。
m:date 时间戳,格式为 [date]T[time]
D:getcontentlength 此电子邮件的大小(以字节为单位)。

示例代码读取如上所示的 XML 节点,以提取在返回的文件夹信息 XML 数据流中找到的每个邮件项的信息。

检索文件夹邮件

在枚举了文件夹中的 MailItems 后,可以使用 MailItem 的邮件 URL(适用于给定会话)来检索实际的电子邮件。这是通过向 Hotmail 服务器发送 HTTP/1.1 GET 请求到给定 URL 来完成的。示例代码中定义的 LoadMail() 函数接受一个 MailItem 实例,并返回邮箱电子邮件的内容。

  /// <summary>
  /// Loads the given mail item.
  /// </summary>
  public string LoadMail(MailItem mailItem)
  {
    // Get the Url.
    string mailUrl = mailItem.Url;

    // Open a connection to the Hotmail server.
    xmlHttp_.open("GET", mailUrl, false, null, null);

    // Send the request.
    xmlHttp_.send(null);

    // Get the response.
    string mailData = xmlHttp_.responseText;

    // Return the mail data.
    return mailData;
  }

发送新邮件

为了检索邮件,LoadMail() 方法(见上文)执行 HTTP/1.1 GET 请求。类似地,会向 sendmsg URL 发送一个 POST 请求,以便从 Hotmail 邮箱发送电子邮件。示例控制台应用程序还包含一个 SendMail() 方法,可以在连接到邮箱后调用。

  /// <summary>
  /// Sends an e-mail.
  /// </summary>
  public void SendMail(string from, string fromName,
    string to, string subject, string body)
  {
    ...
  }

我们首先设置一个引用字符串(稍后使用),并生成一个邮件时间戳。

  // Quote character.
  string quote = "\u0022";

  // Generate the time stamp.
  DateTime now = DateTime.Now;
  string timeStamp = now.ToString("ddd, dd MMM yyyy hh:mm:ss");

HTTPMail 协议遵循类似 SMTP 的通信方案(参见 RFC 821)。Outlook Express 以 MIME 格式发送邮件,但出于演示目的,我们仅发送纯文本电子邮件。

  // Build the post body.
  string postBody = null;

  // Dump mail headers.
  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";

  // Dump mail body.
  postBody += body;

要发送邮件,我们需要将 Content-Type 请求标头设置为“message/rfc821”,表明此请求的正文遵循 RFC 821。我们将生成的请求正文 POST 到连接时获得的 sendmsg URL。

  // Open the connection.
  xmlHttp_.open("POST", sendUrl_, false, null, null);

  // Send the request.
  xmlHttp_.setRequestHeader("Content-Type", "message/rfc821");
  xmlHttp_.send(postBody);

给定一个有效的目标邮箱,Hotmail 会将邮件发送到我们期望的位置。

结论

Hotmail 是世界上最大的免费、基于 Web 的电子邮件提供商。然而,唯一可以直接访问 Hotmail 的非 Web 邮件客户端是 Outlook Express。由于 HTTPMail 协议是未公开的,因此不鼓励其他供应商提供类似的服务。在本文中,我们介绍了如何使用 C# 和 XMLHTTP 组件连接到 Hotmail 邮箱,枚举收件箱邮件项,以及发送和检索电子邮件。本文附带的示例代码包含一个 .NET 程序集,证明通过 HTTPMail 连接到 Hotmail 可以像处理 POP3、IMAP4 或 SMTP 等其他邮件协议一样简单。

© . All rights reserved.