理解 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和其他内容的详细说明