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

理解 IMAP 邮件协议的内部:第 3 部分

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.89/5 (29投票s)

2012年6月26日

MIT

5分钟阅读

viewsIcon

121896

downloadIcon

3459

本文面向邮件协议初学者,描述了IMAP协议中的收件过程。

引言

拥有电脑或移动设备的人都使用过邮件。邮件系统是一个古老、传统且简单的协议。本文(第三部分)旨在深入探讨IMAP协议的内部机制,并展示如何使用C#来实现它。

您可以在 http://higlabo.codeplex.com/ 获取包含邮件、Twitter、Facebook、Dropbox、Windows Live 等库。

IMAP

您可以选择两种协议从邮箱接收邮件。本文将介绍IMAP协议。以下是接收邮件的基本流程。

  • 打开连接
  • Authenticate
  • 选择文件夹
  • 获取 (Fetch)
  • Logout

打开连接

请参阅本文 此处的“打开连接”部分

Authenticate

首先,您必须使用用户名和密码对邮箱进行身份验证。

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

ListResultListLineResult 的类图如下:

您可以通过调用 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 有两个属性:MessageTypeNumber

通过idle命令,您可以了解服务器上发生的情况,并向用户弹出通知窗口。

其他...

IMAP有许多规范,无法在本篇文章中全部涵盖。我专注于IMAP初学者易于入门的最重要主题。希望本文对您有所帮助。感谢您的阅读。

相关文章列表如下:

历史

  • 2012年6月25日:首次发布
  • 2012年7月6日:修改了源代码和文章
  • 2012年7月24日:修改了源代码,并添加了一些关于Idle和其他内容的详细说明
© . All rights reserved.