理解 IMAP 邮件协议的内部:第 3 部分
本文面向邮件协议初学者,描述了IMAP协议中的收件过程。
引言
拥有电脑或移动设备的人都使用过邮件。邮件系统是一个古老、传统且简单的协议。本文(第三部分)旨在深入探讨IMAP协议的内部机制,并展示如何使用C#来实现它。
您可以在 http://higlabo.codeplex.com/ 获取包含邮件、Twitter、Facebook、Dropbox、Windows Live 等库。
IMAP
您可以选择两种协议从邮箱接收邮件。本文将介绍IMAP协议。以下是接收邮件的基本流程。
- 打开连接
- Authenticate
- 选择文件夹
- 获取 (Fetch)
- Logout

打开连接
请参阅本文 此处的“打开连接”部分。
Authenticate
首先,您必须使用用户名和密码对邮箱进行身份验证。
public ImapCommandResult ExecuteLogin()
{
    if (this.EnsureOpen() == ImapConnectionState.Disconnected) 
       { throw new MailClientException(); }
    String commandText = String.Format(this.Tag + 
           " LOGIN {0} {1}", this.UserName, this.Password);
    String s = this.Execute(commandText, false);
    ImapCommandResult rs = new ImapCommandResult(this.Tag, s);
    if (rs.Status == ImapCommandResultStatus.Ok)
    {
        this._State = ImapConnectionState.Authenticated;
    }
    else
    {
        this._State = ImapConnectionState.Connected;
    }
    return rs;
}
获取文件夹列表
身份验证后,您必须选择一个文件夹才能获取实际的邮件数据。要选择一个文件夹,您需要获取邮箱中存在的文件夹列表。您可以通过向邮件服务器发送list命令来获取文件夹列表。
public ListResult ExecuteList(String folderName, Boolean recursive)
{
    this.ValidateState(ImapConnectionState.Authenticated);
    List<ListLineResult> l = new List<ListLineResult>();
    String name = "";
    Boolean noSelect = false;
    Boolean hasChildren = false;
    String rc = "%";
    if (recursive == true)
    {
        rc = "*";
    }
    String s = this.Execute(String.Format
               (this.Tag + " LIST \"{0}\" \"{1}\"", folderName, rc), false);
    foreach (Match m in RegexList.GetListFolderResult.Matches(s))
    {
        name = NamingConversion.DecodeString(m.Groups["name"].Value);
        foreach (Capture c in m.Groups["opt"].Captures)
        {
            if (c.Value.ToString() == "\\Noselect")
            {
                noSelect = true;
            }
            else if (c.Value.ToString() == "\\HasNoChildren")
            {
                hasChildren = false;
            }
            else if (c.Value.ToString() == "\\HasChildren")
            {
                hasChildren = true;
            }
        }
        l.Add(new ListLineResult(name, noSelect, hasChildren));
    }
    return new ListResult(l);
}
底层向邮件服务器发送的命令文本如下:
tag1 LIST "" "*"
服务器的响应文本如下:
* LIST (\HasNoChildren) "/" "INBOX"
* LIST (\HasNoChildren) "/" "Notes"
* LIST (\Noselect \HasChildren) "/" "[Gmail]"
* LIST (\HasNoChildren) "/" "[Gmail]/All Mail"
......
* LIST (\HasNoChildren) "/" "[Gmail]/Trash"
tag1 OK Success
ListResult 和 ListLineResult 的类图如下:


您可以通过调用 ImapClient 类中的 GetAllFolders 方法来获取所有文件夹。
MailMessage mg = null;
using (ImapClient cl = new ImapClient("imap.gmail.com"))
{
    cl.Port = 993;
    cl.Ssl = true;
    cl.UserName = "xxxxx";
    cl.Password = "yyyyy";
    var bl = cl.Authenticate();
    if (bl == true)
    {
        //Get all folder
        var l = cl.GetAllFolders();
    }
}
选择文件夹
在从服务器接收邮件之前,您必须选择邮箱中的文件夹。要选择文件夹,您需要向服务器发送select命令。以下是 ImapClient 类中 ExecuteSelect 方法的内部实现。
public SelectResult ExecuteSelect(String folderName)
{
    this.ValidateState(ImapConnectionState.Authenticated);
    String commandText = String.Format(this.Tag + " Select {0}", 
                         NamingConversion.EncodeString(folderName));
    String s = this.Execute(commandText, false);
    var rs = this.GetSelectResult(folderName, s);
    this.CurrentFolder = new ImapFolder(rs);
    return rs;
}
客户端向服务器发送以下文本。
tag1 Select INBOX
服务器向客户端返回响应文本。
* FLAGS (\Answered \Flagged \Draft \Deleted \Seen $Forwarded)
* OK [PERMANENTFLAGS (\Answered \Flagged \Draft \Deleted \Seen $Forwarded \*)] Flags permitted.
* OK [UIDVALIDITY 594544687] UIDs valid.
* 223 EXISTS
* 0 RECENT
* OK [UIDNEXT 494] Predicted next UID.
tag1 OK [READ-WRITE] INBOX selected. (Success)
此响应文本将表示为 SelectResult 类。

ImapClient 类有一个易于使用的 SelectFolder 方法。该方法返回一个 ImapFolder 对象,该对象是在 SelectFolder 方法内部从 SelectResult 对象创建的。
public ImapFolder SelectFolder(String folderName)
{
    var rs = this.ExecuteSelect(folderName);
    return new ImapFolder(rs);
}
以下是 ImapFolder 类图:

获取邮件
在选择文件夹后,您可以使用 Fetch 命令获取邮件列表。您可以通过调用 ImapClient 类中的 GetMessage 方法来获取实际的邮件消息数据。以下是接收邮件消息的示例代码:
private static void ImapMailReceive()
{
    MailMessage mg = null;
    using (ImapClient cl = new ImapClient("imap.gmail.com"))
    {
        cl.Port = 993;
        cl.Ssl = true;
        cl.UserName = "xxxxx";
        cl.Password = "yyyyy";
        var bl = cl.Authenticate();
        if (bl == true)
        {
            //Select folder
            var folder = cl.SelectFolder("[Gmail]/All Mail");
            //Get all mail from folder
            for (int i = 0; i < folder.MailCount; i++)
            {
                mg = cl.GetMessage(i + 1);
            }
        }
    }
}
您必须将大于 1 的索引传递给 GetMessage 方法。请注意,第一个值是 1,而不是零。
客户端发送以下文本:
tag1 FETCH 1 (BODY[])
服务器将返回以下文本:
* 1 FETCH (BODY[] {2370}
Delivered-To: xxxx@gmail.com
To: = <yyyyy@gmail.com>
Date: Thu, 23 Apr 2009 08:22:21 +0900
From: <xxxxx@gmail.com>
Subject: Test Mail
...Mail body data....
)
tag1 OK Success
格式与 Pop3Message.ImapClient 类提取的文本相同,并将其传递给 MailMessage 类来创建 MailMessage 对象。
仅获取邮件头
您可以像下面这样在不获取正文数据的情况下获取邮件头。这可以节省网络流量并提高性能。
using (ImapClient cl = new ImapClient("imap.xxx.com")) 
{ 
    cl.UserName = "xxx@gmail.com"; 
    cl.Password = "xxx"; 
    var authenticated = cl.TryAuthenticate(); 
    if (authenticated == true) 
    { 
        //Select folder 
        var folder = cl.SelectFolder("YourFolder"); 
        //Get all mail header 
        var mailIndex = 1;
        var headers = cl.GetHeaderCollection(mailIndex); 
    } 
}
附件、HTML邮件、.eml 文件
附件、HTML邮件、.eml 文件与POP3协议相同。请参阅本文:深入了解POP3邮件协议:第二部分。
删除邮件
以下是IMAP中的删除过程:

您将一个标志作为delete添加到您通过邮件索引指定的已选邮件中。当您发送 EXPUNGE 命令时,这些标记的邮件将被删除。
您可以通过使用 ImapClient 对象的 DeleteMail 方法来删除邮件。
protected void Button1_Click(object sender, EventArgs e)
{
    MailMessage mg = null;
    String htmlText = "";
    using (ImapClient cl = new ImapClient("imap.gmail.com"))
    {
        cl.Port = 993;
        cl.Ssl = true;
        cl.UserName = "xxxxx";
        cl.Password = "yyyyy";
        //Select folder
        var folder = cl.SelectFolder("[Gmail]/All Mail");
        cl.DeleteMail(1, 2, 3);
    }
}
由于所有过程(打开连接、身份验证、选择文件夹、expunge、注销)都将在 DeleteMail 方法内部自动执行,因此不需要身份验证。以下是 DeleteMail 方法的实现。
public Boolean DeleteMail(params Int64[] mailIndex)
{
    this.ValidateState(ImapConnectionState.Authenticated, true);
    return this.DeleteMail(this.CurrentFolder.Name, mailIndex);
}
public Boolean DeleteMail(params Int64[] mailIndex)
{
    if (this.EnsureOpen() == ImapConnectionState.Disconnected) { return false; }
    if (this.Authenticate() == false) { return false; }
    for (int i = 0; i < mailIndex.Length; i++)
    {
        var rs = this.ExecuteStore(mailIndex[i], StoreItem.FlagsAdd, @"\Deleted");
        if (rs.Status != ImapCommandResultStatus.Ok) { return false; }
    }
    this.ExecuteExpunge();
    this.ExecuteLogout();
    return true;
}
管理已读或未读
IMAP是POP3之后为了解决POP3问题而出现的新协议。IMAP有一个搜索命令,可以只接收未读邮件。因此,您不必在应用程序中管理已读状态。您需要做的就是向邮件服务器发送一个搜索命令。以下是从服务器接收未读邮件列表的示例代码。
MailMessage mg = null;
using (ImapClient cl = new ImapClient("imap.gmail.com"))
{
    cl.Port = 993;
    cl.Ssl = true;
    cl.UserName = "xxxxx";
    cl.Password = "yyyyy";
    var bl = cl.Authenticate();
    if (bl == true)
    {
        //Select folder
        var folder = cl.SelectFolder("[Gmail]/All Mail");
        //Search Unread
        var list = cl.ExecuteSearch("UNSEEN UNDELETED");
        //Get all unread mail
        for (int i = 0; i < list.MailIndexList.Count; i++)
        {
            mg = cl.GetMessage(list.MailIndexList[i]);
        }
    }
}
底层客户端发送以下文本。
tag1 SEARCH UNSEEN UNDELETED
服务器将返回以下文本给客户端。
* SEARCH 1 2 3 4 9 10 11 12 13 14 17 18 19 20 21 22 30 32 33
tag1 OK SEARCH completed (Success)
此响应文本表示为 SearchResult 类。

通过 Search 命令,您可以轻松获得邮件索引列表。与POP3相比,这非常简单。
草稿邮件
您可以通过调用 Append 命令将草稿邮件保存到邮件服务器。
MailMessage mg = null;
using (ImapClient cl = new ImapClient("imap.gmail.com"))
{
    cl.Port = 993;
    cl.Ssl = true;
    cl.UserName = "xxxxx";
    cl.Password = "yyyyy";
    var bl = cl.Authenticate();
    if (bl == true)
    {
        //Add Draft
        var smg = new SmtpMessage("xxx@gmail.com", "yyy@hotmail.com", 
            "yyy@hotmail.com", "This is a test mail.", "Hi.Is it correct??");
        cl.ExecuteAppend("GMail/Drafts", smg.GetDataText(), "\\Draft", DateTimeOffset.Now);
    }
}
您可以通过上述相同的方法获取草稿邮件。
MailMessage mg = null;
using (ImapClient cl = new ImapClient("imap.gmail.com"))
{
    cl.Port = 993;
    cl.Ssl = true;
    cl.UserName = "xxxxx";
    cl.Password = "yyyyy";
    var bl = cl.Authenticate();
    if (bl == true)
    {
        //Select folder
        var folder = cl.SelectFolder("[Gmail]/Drafts");
        //Get all mail from folder
        mg = cl.GetMessage(1);
        //Create SmtpMessage object
        var smg = mg.CreateSmtpMessage();
        //And send mail!!
    }
}
要发送邮件,请参阅本文:深入了解SMTP邮件协议:第一部分。
Subscribe (订阅)
您可以订阅您想关注的文件夹。
MailMessage mg = null;
using (ImapClient cl = new ImapClient("imap.gmail.com"))
{
    cl.Port = 993;
    cl.Ssl = true;
    cl.UserName = "xxxxx";
    cl.Password = "yyyyy";
    var bl = cl.Authenticate();
    if (bl == true)
    {                
        cl.ExecuteSubscribe("CodeProject");
        cl.ExecuteSubscribe("Codeplex");
    }
}
订阅文件夹后,您可以使用以下代码获取文件夹列表:
MailMessage mg = null;
using (ImapClient cl = new ImapClient("imap.gmail.com"))
{
    cl.Port = 993;
    cl.Ssl = true;
    cl.UserName = "xxxxx";
    cl.Password = "yyyyy";
    var bl = cl.Authenticate();
    if (bl == true)
    {                
        var rs = cl.ExecuteLsub("", false);
        foreach (var line in rs.Lines)
        {
            var folder = new ImapFolder(line);
            //Do something...
        }
    }
}
Idle 命令
IMAP有一个idle命令,可以让您从服务器接收消息(新邮件、已删除等)。
using (ImapClient cl = new ImapClient("imap.gmail.com", 993, "user name", "password"))
{
    cl.Ssl = true;
    cl.ReceiveTimeout = 10 * 60 * 1000;//10 minute
    if (cl.Authenticate() == true)
    {
        ImapFolder r = cl.SelectFolder("INBOX");
        using (var cm = cl.CreateImapIdleCommand())
        {
            cm.MessageReceived += 
            (Object o, ImapIdleCommandMessageReceivedEventArgs e) =>
            {
                foreach (var mg in e.MessageList)
                {
                    String text = String.Format("Type is {0} Number is {1}"
                       , mg.MessageType, mg.Number);
                    Console.WriteLine(text);
                }
            };
            cl.ExecuteIdle(cm);
            while (true)
            {
                var line = Console.ReadLine();
                if (line == "done")
                {
                    cl.ExecuteDone(cm);
                    break;
                }
            }
        }
    }
}
当您收到新邮件且INBOX文件夹计数发生变化时,服务器会向客户端发送文本作为响应。
* 224 EXISTS
如果您删除了邮件,服务器会向客户端发送此类文本。
* 224 EXPUNGE
* 223 EXISTS
您可以通过向 MessageReceived 事件注册事件处理程序来接收这些消息。
cm.MessageReceived += (Object o, ImapIdleCommandMessageReceivedEventArgs e) =>
{
    foreach (var mg in e.MessageList)
    {
        String text = String.Format("Type is {0} Number is {1}"
            , mg.MessageType, mg.Number);
        Console.WriteLine(text);
    }
};
ImapIdleCommandMessageReceivedEventArgs 对象有一个 MessageList 属性,它是 List<imapidlecommandmessage>。 ImapIdleCommandMessage 有两个属性:MessageType 和 Number。
 
  
 
通过idle命令,您可以了解服务器上发生的情况,并向用户弹出通知窗口。
其他...
IMAP有许多规范,无法在本篇文章中全部涵盖。我专注于IMAP初学者易于入门的最重要主题。希望本文对您有所帮助。感谢您的阅读。
相关文章列表如下:
历史
- 2012年6月25日:首次发布
- 2012年7月6日:修改了源代码和文章
- 2012年7月24日:修改了源代码,并添加了一些关于Idle和其他内容的详细说明




