.NET POP3 MIME 客户端
本文档提供了一个使用 .NET 2.0 和 C# 实现的 POP3 MIME 客户端。
引言
.NET Framework 不提供 POP3 或 MIME 支持。对于需要这些功能的您,本文档提供了对 POP3 和 MIME 的支持。本文档的目的不是深入探讨 POP3 或 MIME 的细节。相反,本文档重点介绍如何使用我提供的类,以及类内部的实现,以便您进行修改。我在开发过程中已成功地在 Yahoo 和 Gmail 上对这些类进行了测试。
用法
下面是一个代码示例片段,演示了我创建的 Pop3Client
的用法。除了 USER
和 PASS
命令外,所有已实现的 Pop3Commands
都直接从 Pop3Client
类中可用。请参见下文,了解每个已实现方法的用法以及该方法用途的简要说明。
using (Pop3Client client = new Pop3Client(PopServer, PopPort, true, User, Pass))
{
/*Peter Huber implemented a Trace event in his Pop3Client as well,
I used his idea as a model for my Trace event.
Pretty much the same functionality with a little different implementation
as the events ultimately raised from the Pop3Client class are initiated
from the internal command objects and not the Pop3Client class.*/
client.Trace += new Action<string>(Console.WriteLine);
/* The Authenticate method establishes connection and executes
both the USER and PASS commands using
the username and password provided to the Pop3Client constructor.*/
client.Authenticate();
/*The Stat method executes a STAT command against the pop3 server and
returns a Stat object as a result.*/
Stat stat = client.Stat();
/*As seen below, the list of items in a POP3 inbox can be retrieved
using the List method of the Pop3Client.
The List method also has an overload to get a single Pop3ListItem*/
foreach (Pop3ListItem item in client.List())
{
/*The MimeEntity returned from the RetrMimeEntity method is converted into a
MailMessageEx within the RetrMailMessageEx method. The MailMessageEx class
inherits System.Net.Mail.MailMessage and adds a few properties related to
MIME and the Internet Mail Protocol. One important property added to the
MailMessageEx class is the Children property containing a
List<MailMessageEx> parsed MIME entity attachments whose Media Type
is message/rfc822.*/
MailMessageEx message = client.RetrMailMessageEx(item);
Console.WriteLine("Children.Count: {0}",
message.Children.Count);
Console.WriteLine("message-id: {0}",
message.MessageId);
Console.WriteLine("subject: {0}",
message.Subject);
Console.WriteLine("Attachments.Count: {0}",
message.Attachments.Count);
/*Consumers of the Pop3Client class can get a tree of MimeEntities as they
were originally parsed from the POP3 message they can simply call
the RetrMimeEntity method of the Pop3Client class and
work with the MimeEntity object directly.*/
MimeEntity entity = client.RetrMimeEntity(item);
/*The Dele method executes the DELE command on the POP3 server the Pop3Client is
connected to.*/
client.Dele(item);
}
/*The Noop method executes the NOOP command on the POP3 server the Pop3Client is
connected to.*/
client.Noop();
/*The Rset method executes the RSET command on the POP3 server the Pop3Client is
connected to.*/
client.Rset();
/*The Quit method executes the QUIT command on the POP# server the Pop3Client is
connected to.*/
client.Quit();
}
上方代码的输出显示了一个仅包含一封邮件的 Gmail POP3 帐户的收件箱。在下面的屏幕截图中,第一行是服务器响应,表示连接成功。接下来的以 USER
开头直到 RETR
请求行之间的行是针对 POP3 服务器执行的命令及其服务器响应的第一行。RETR
请求之后的行是用于 MailMessageEx
对象的一些属性的 Console.WriteLine
,该对象由 RetrMailMessageEx
方法返回。最后,显示了 DELE
、NOOP
、RSET
和 QUIT
命令的请求和响应结果。

内部
该库支持执行 POP3 请求,解析 POP3 响应,以及将 RETR
请求返回的邮件解析成 MIME 部分。在内部,POP3 实现由各种命令组成,每个 POP3 命令一个,还有一个用于与服务器建立连接的附加命令。每当执行 RETR
方法并且 RetrResponse
中返回的行需要被解析为 MIME 部分时,MIME 就派上用场了。该库的 MIME 部分实际上只由几个类组成,一个用于读取 POP3 行并将其解析为 MimeEntity
对象。最后,一个 MimeEntity
类,它实际上是一个包含头集合、一些解码内容以及用于将 MimeEntity
转换为 MailMessageEx
的 ToMailMessageEx
方法的结构。
POP3

每个 POP3 命令都表示为一个继承自 Pop3Command
的命令类。POP3 命令都标记为内部,仅用于从 Pop3Client
类内部执行。在内部,Pop3Command
负责确保命令处于可执行状态,发送命令请求,并返回命令请求的服务器响应。每个 Pop3Command
子类负责创建最终将发送到服务器的请求消息。Pop3Command
类确实封装了 Pop3Response
对象的创建,该对象表示服务器的简单响应。对于 RETR
或 LIST
等命令,它们具有更复杂的响应消息解析处理要求,Pop3Command
类的 CreateResponse
方法是可重写的,允许继承者创建自己的响应类型并返回,而不是标准的 Pop3Response
。
根据 POP3 规范,每个 POP3 命令只能在一处或多处以下状态下执行:AUTHENTICATION
、TRANSACTION
和 UPDATE
。当定义每个 Pop3Command
时,该命令可以在哪些 POP3 状态下执行已通过 Pop3Command
类的构造函数硬编码到类中,如下面的 QuitCommand
类定义所示。
/// <summary>
/// This class represents the Pop3 QUIT command.
/// </summary>
internal sealed class QuitCommand : Pop3Command<Pop3Response>
{
/// <summary>
/// Initializes a new instance of the <see cref="QuitCommand"> class.
/// </summary>
/// <param name="stream">The stream.</param>
public QuitCommand(Stream stream)
: base(stream, false, Pop3State.Transaction | Pop3State.Authorization) { }
/// <summary>
/// Creates the Quit request message.
/// </summary>
/// <returns>
/// The byte[] containing the QUIT request message.
/// </returns>
protected override byte[] CreateRequestMessage()
{
return GetRequestMessage(Pop3Commands.Quit);
}
}
每个 Pop3Commands
的状态都使用类的 EnsureState
方法进行验证。Pop3State
枚举使用 flags 属性定义,允许将该枚举视为位字段,可用于按位运算。请参阅下面的 Pop3Command.EnsureState
方法,了解 Pop3State
枚举的使用方法。
/// <summary>
/// Ensures the state of the POP3.
/// </summary>
/// <param name="currentState">State of the current.</param>
protected void EnsurePop3State(Pop3State currentState)
{
if (!((currentState & ValidExecuteState) == currentState))
{
throw new Pop3Exception(string.Format("This command is being executed" +
" in an invalid execution state. Current:{0}, Valid:{1}",
currentState, ValidExecuteState));
}
}
在 Pop3Command
外部,Pop3Client
类通过 ExecuteCommand
方法将当前的 POP3 状态提供给命令对象。下面的 ExecuteCommand
方法用于执行所有命令,以确保命令在执行过程中得到一致的处理。
/// <summary>
/// Provides a common way to execute all commands. This method
/// validates the connection, traces the command and finally
/// validates the response message for a -ERR response.
/// </summary>
/// <param name="command">The command.</param>
/// <returns>The Pop3Response for the provided command</returns>
/// <exception cref="Pop3Exception">If the HostMessage does not start with '+OK'.
/// </exception>
/// <exception cref="Pop3Exception">If the client is no longer connected.</exception>
private TResponse ExecuteCommand<TResponse, TCommand>(TCommand command)
where TResponse : Pop3Response where TCommand : Pop3Command<TResponse>
{
//Ensures the TcpClient is still connected prior to executing the command.
EnsureConnection();
/*Adds an anonymous delegate to handle the commands Trace event in order to
provided tracing.*/
TraceCommand<TCommand, TResponse>(command);
//Executes the command providing the current POP3 state.
TResponse response = (TResponse)command.Execute(CurrentState);
//Ensures the Pop3Response started with '+OK'.
EnsureResponse(response);
return response;
}
最后,使用 Pop3Command
类的 CreateResponse
方法创建 Pop3Response
。下面是 StatCommand.CreateResponse
方法的一个示例,说明了这种情况,它向调用者返回一个自定义的 StatResponse
对象。
protected override StatResponse CreateResponse(byte[] buffer)
{
Pop3Response response = Pop3Response.CreateResponse(buffer);
string[] values = response.HostMessage.Split(' ');
//should consist of '+OK', 'messagecount', 'octets'
if (values.Length < 3)
{
throw new Pop3Exception(string.Concat("Invalid response message: ",
response.HostMessage));
}
int messageCount = Convert.ToInt32(values[1]);
long octets = Convert.ToInt64(values[2]);
return new StatResponse(response, messageCount, octets);
}
如果您想了解更多关于 POP3 的信息,请参阅 Post Office Protocol - Version 3,其中包含对上述每个命令的完整解释,以及未实现的附加命令。
MIME
MimeReader
类在其公共构造函数中接收一个字符串数组,这些字符串构成了 POP3 消息。然后,这些行存储在 Queue<string>
实例中,并逐个处理。MimeReader
类负责将多部分和单部分 MIME 消息解析成由头和解码的 MIME 内容组成的 MIME 实体。MimeReader
类支持解析嵌套的 MIME 实体,包括 message/rfc822 类型的实体。一旦 MimeReader
完成对互联网邮件消息的处理,就会返回一个 MimeEntity
,其中包含一个树形结构,该结构包含消息的内容。
RetrCommand
重写了 CreateResponse
方法,并返回一个 RetrResponse
对象,该对象包含邮件消息的行。Pop3Client
类的 RetrMimeEntity
方法将 RetrResponse
对象中返回的行提供给 MimeReader
类的一个新实例,以解析消息行。最后,MimeReader.CreateMimeEntity
方法返回一个 MimeEntity
实例,该实例代表 POP3 消息中包含的 MimeEntities
。请参见下面的 Pop3Client.RetrMimeEntity
方法定义。
/// <summary>
/// Retrs the specified message.
/// </summary>
/// <param name="item">The item.</param>
/// <returns>A MimeEntity for the requested Pop3 Mail Item.</returns>
public MimeEntity RetrMimeEntity(Pop3ListItem item)
{
if (item == null)
{
throw new ArgumentNullException("item");
}
if (item.MessageId < 1)
{
throw new ArgumentOutOfRangeException("item.MessageId");
}
RetrResponse response;
using (RetrCommand command = new RetrCommand(_clientStream, item.MessageId))
{
response = ExecuteCommand<RetrResponse, RetrCommand>(command);
}
if (response != null)
{
MimeReader reader = new MimeReader(response.MessageLines);
return reader.CreateMimeEntity();
}
throw new Pop3Exception("Unable to get RetrResponse. Response object null");
}
MimeReader
创建一个新的 MimeEntity
对象,并通过递归调用 CreateMimeEntity
方法来构建由这些对象组成的 MIME 实体树。这个过程一直持续到处理完整个互联网邮件消息的所有行。下面是一个包含 CreateMimeEntity
方法的代码片段,以展示为了创建新的 MimeEntity
所进行的处理。
/// <summary>
/// Creates the MIME entity.
/// </summary>
/// <returns>A mime entity containing 0 or more children
/// representing the mime message.</returns>
public MimeEntity CreateMimeEntity()
{
//Removes the headers from the mime entity for later processing.
ParseHeaders();
/*Processes the headers previously removed and sets any MIME
specific properties like ContentType or ContentDisposition.*/
ProcessHeaders();
/*Parses the MimeEntities body. This method causes new
MimeReader objects to be created for each new Mime Entity found within the POP3
messages lines until all of the lines have been removed from the queue.*/.
ParseBody();
/*Decodes the content stream based on the entities
ContentTransferEncoding and sets the Content stream of the MimeEntity.*/
SetDecodedContentStream();
/*Returns the MimeEntity that was created in the constructor
of the class and that now has all of its child mime parts parsed
into mime entities.*/
return _entity;
}
解析头实际上就是获取键值对,直到读到一个空行。该方法在一定程度上遵循了 Peter Huber 定义的模式。根据 MIME 规范,我们一直读取头行,直到遇到第一个空行。当遇到空行时,MIME 实体的正文就开始了,并准备好进行处理。
/// <summary>
/// Parse headers into _entity.Headers NameValueCollection.
/// </summary></span>
private int ParseHeaders()
{
string lastHeader = string.Empty;
string line = string.Empty;
// the first empty line is the end of the headers.
while(_lines.Count > 0 && !string.IsNullOrEmpty(_lines.Peek()))
{
line = _lines.Dequeue();
/*if a header line starts with a space or tab then it
is a continuation of the previous line.*/
if (line.StartsWith(" ")
|| line.StartsWith(Convert.ToString('\t')))
{
_entity.Headers[lastHeader]
= string.Concat(_entity.Headers[lastHeader], line);
continue;
}
int separatorIndex = line.IndexOf(':');
if (separatorIndex < 0)
{
System.Diagnostics.Debug.WriteLine("Invalid header:{0}", line);
continue;
} //This is an invalid header field. Ignore this line.
string headerName = line.Substring(0, separatorIndex);
string headerValue
= line.Substring(separatorIndex + 1).Trim(HeaderWhitespaceChars);
_entity.Headers.Add(headerName.ToLower(), headerValue);
lastHeader = headerName;
}
if (_lines.Count > 0)
{
_lines.Dequeue();
} //remove closing header CRLF.
return _entity.Headers.Count;
}
一旦 MIME 实体中的头被解析,就需要对它们进行处理。如果一个头是 MIME 处理特有的,那么该头将被分配给 MIME 对象上的一个属性。否则,该头将被忽略,并在 MimeEntity
对象的头 NameValueCollection
中返回以供后续处理。下面展示了 MimeReader
的一些有用的辅助方法,例如 GetTransferEncoding
和 GetContentType
。
/// <summary>
/// Processes mime specific headers.
/// </summary>
/// <returns>A mime entity with mime specific headers parsed.</returns>
private void ProcessHeaders()
{
foreach (string key in _entity.Headers.AllKeys)
{
switch (key)
{
case "content-description":
_entity.ContentDescription = _entity.Headers[key];
break;
case "content-disposition":
_entity.ContentDisposition
= new ContentDisposition(_entity.Headers[key]);
break;
case "content-id":
_entity.ContentId = _entity.Headers[key];
break;
case "content-transfer-encoding":
_entity.TransferEncoding = _entity.Headers[key];
_entity.ContentTransferEncoding
= MimeReader.GetTransferEncoding(_entity.Headers[key]);
break;
case "content-type":
_entity.SetContentType(MimeReader.GetContentType(_entity.Headers[key]));
break;
case "mime-version":
_entity.MimeVersion = _entity.Headers[key];
break;
}
}
}
现在头已经解析完毕,就可以解析给定 MimeEntity
的正文了。当 MimeReader
对象在正文解析过程中创建新的 MimeReader
对象时,就会发生递归,从而将创建的 MimeEntity
对象添加到当前 MimeEntity
的 Children
集合中。
/// <summary>
/// Parses the body.
/// </summary>
private void ParseBody()
{
if (_entity.HasBoundary)
{
while (_lines.Count > 0
&& !string.Equals(_lines.Peek(), _entity.EndBoundary))
{
/*Check to verify the current line is not the same as the
parent starting boundary. If it is the same as the parent
starting boundary this indicates existence of a new child
entity. Return and process the next child.*/
if (_entity.Parent != null
&& string.Equals(_entity.Parent.StartBoundary, _lines.Peek()))
{
return;
}
if (string.Equals(_lines.Peek(), _entity.StartBoundary))
{
AddChildEntity(_entity, _lines);
} //Parse a new child mime part.
else if (string.Equals(_entity.ContentType.MediaType,
MediaTypes.MessageRfc822, StringComparison.InvariantCultureIgnoreCase)
&& string.Equals(_entity.ContentDisposition.DispositionType,
DispositionTypeNames.Attachment,
StringComparison.InvariantCultureIgnoreCase))
{
/*If the content type is message/rfc822 the
stop condition to parse headers has already been encountered.
But, a content type of message/rfc822 would
have the message headers immediately following the mime
headers so we need to parse the headers for the attached message.*/
AddChildEntity(_entity, _lines);
break;
}
else
{
_entity.EncodedMessage.Append
(string.Concat(_lines.Dequeue(), Pop3Commands.Crlf));
} //Append the message content.
}
} //Parse a multipart message.
else
{
while (_lines.Count > 0)
{
_entity.EncodedMessage.Append(string.Concat
(_lines.Dequeue(), Pop3Commands.Crlf));
}
} //Parse a single part message.
}
正文处理完成后,唯一剩下的事情就是在返回 MimeEntity
对象之前,将其解码内容写入 Content
流。这是使用 SetDecodedContentStream
方法完成的。
private void SetDecodedContentStream()
{
switch (_entity.ContentTransferEncoding)
{
case System.Net.Mime.TransferEncoding.Base64:
_entity.Content
= new MemoryStream(Convert.FromBase64String
(_entity.EncodedMessage.ToString()), false);
break;
case System.Net.Mime.TransferEncoding.QuotedPrintable:
_entity.Content
= new MemoryStream(GetBytes(QuotedPrintableEncoding.Decode
(_entity.EncodedMessage.ToString())), false);
break;
case System.Net.Mime.TransferEncoding.SevenBit:
default:
_entity.Content
= new MemoryStream(GetBytes(_entity.EncodedMessage.ToString()),
false);
break;
}
}
现在内容流已设置,除了返回创建的 MimeEntity
之外,没有太多事情要做。此方法返回的对象已准备好进行处理。但是,CreateMimeEntity
方法返回的 MimeEntity
对象并不直接映射到 MailMessage
。为了方便起见,我添加了一个方法,允许将 MimeEntity
转换为 MailMessage
。由于媒体类型 message/rfc822,我希望这些实体被预先解析,并直接提供给任何包含邮件附件的 MimeEntity
使用。为了实现这一点,我创建了一个继承自 System.Net.Mail.MailMessage
的类,该类具有一个 List<MailMessageEx>
属性,其中包含一个 MailMessageEx
对象集合,这些对象是任何邮件的邮件附件。MailMessageEx
对象是通过调用 MimeEntity.ToMailMessageEx
方法创建的。
结论
通过对如何使用代码以及大部分内部工作原理的概述,您应该已经能够充分利用这些类,并根据需要进行修改或添加,这将使您能够轻松地将它们集成到您的代码库中。该库处理 POP3 和 MIME 协议,并将它们封装在一个易于使用的类中。MIME 消息最终被解析为 MailMessageEx
对象,以便可以轻松访问附件和电子邮件正文文本。
参考文献
关于 POP3 和 MIME 已经有很多现有的文章。下面是我在撰写本文之前审查过的几个优秀的实现。我能够利用这两篇文章中提出的想法,并尽我所能记录下我直接使用任何这些想法而没有进行重大修改的地方。
- Desmond McCarter 的 C# .NET POP3 客户端
- Peter Huber 的支持完整 MIME 的 POP3 邮件客户端 (.NET 2.0)
- Bill Gearhart 的 QuotedPrintable 类
- Post Office Protocol - Version 3
- Multipurpose Internet Mail Extensions (MIME) Part 1: Format of Internet Message Bodies
- Multipurpose Internet Mail Extensions (MIME) Part 2: Media Types
- Multipurpose Internet Mail Extensions (MIME) Part 3: Message Header Extensions for Non-ASCII Text
- Multipurpose Internet Mail Extensions (MIME) Part 4: Registration Procedures
- Multipurpose Internet Mail Extensions (MIME) Part 5: Conformance Criteria and Examples
历史
- 2008.02.08
- 修复了 Shawn Cook 发现的
Body
属性为空的问题。
- 修复了 Shawn Cook 发现的
- 2008.02.07 小错误修复
- 当 GMail 主机将其 3 部分主机消息作为响应的第一行返回时,
RetrResponse
出现问题。
- 当 GMail 主机将其 3 部分主机消息作为响应的第一行返回时,
- 2008.02.05 错误修复和附加命令
- 各种小错误修复
- 修复了 D I Petersen 在解析 GMail 头时发现的问题。这是由于一个空/
null
头引起的。 - 当未从 POP3 服务器收到响应时,更改了处理方式。此修复解决了 zlezj 发现的,当缓冲区大小落在行终止符中间时出现的问题。
- 更改了断开连接的逻辑,使其不影响
TcpClient
,以便可以重用它进行后续的 POP3 请求。 - 添加了
Top
命令,以支持非标准的 POP3TOP
命令,从而能够下载消息头而不是整个消息。
- 2007.11.20 初始发布