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

.NET POP3 MIME 客户端

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.89/5 (48投票s)

2007 年 11 月 20 日

CPOL

9分钟阅读

viewsIcon

1106341

downloadIcon

6820

本文档提供了一个使用 .NET 2.0 和 C# 实现的 POP3 MIME 客户端。

引言

.NET Framework 不提供 POP3 或 MIME 支持。对于需要这些功能的您,本文档提供了对 POP3 和 MIME 的支持。本文档的目的不是深入探讨 POP3 或 MIME 的细节。相反,本文档重点介绍如何使用我提供的类,以及类内部的实现,以便您进行修改。我在开发过程中已成功地在 Yahoo 和 Gmail 上对这些类进行了测试。

用法

下面是一个代码示例片段,演示了我创建的 Pop3Client 的用法。除了 USERPASS 命令外,所有已实现的 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 方法返回。最后,显示了 DELENOOPRSETQUIT 命令的请求和响应结果。

内部

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

POP3

每个 POP3 命令都表示为一个继承自 Pop3Command 的命令类。POP3 命令都标记为内部,仅用于从 Pop3Client 类内部执行。在内部,Pop3Command 负责确保命令处于可执行状态,发送命令请求,并返回命令请求的服务器响应。每个 Pop3Command 子类负责创建最终将发送到服务器的请求消息。Pop3Command 类确实封装了 Pop3Response 对象的创建,该对象表示服务器的简单响应。对于 RETRLIST 等命令,它们具有更复杂的响应消息解析处理要求,Pop3Command 类的 CreateResponse 方法是可重写的,允许继承者创建自己的响应类型并返回,而不是标准的 Pop3Response

根据 POP3 规范,每个 POP3 命令只能在一处或多处以下状态下执行:AUTHENTICATIONTRANSACTIONUPDATE。当定义每个 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 的一些有用的辅助方法,例如 GetTransferEncodingGetContentType

/// <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 对象添加到当前 MimeEntityChildren 集合中。

/// <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 已经有很多现有的文章。下面是我在撰写本文之前审查过的几个优秀的实现。我能够利用这两篇文章中提出的想法,并尽我所能记录下我直接使用任何这些想法而没有进行重大修改的地方。

历史

  • 2008.02.08
    • 修复了 Shawn Cook 发现的 Body 属性为空的问题。
  • 2008.02.07 小错误修复
    • 当 GMail 主机将其 3 部分主机消息作为响应的第一行返回时,RetrResponse 出现问题。
  • 2008.02.05 错误修复和附加命令
    • 各种小错误修复
    • 修复了 D I Petersen 在解析 GMail 头时发现的问题。这是由于一个空/null 头引起的。
    • 当未从 POP3 服务器收到响应时,更改了处理方式。此修复解决了 zlezj 发现的,当缓冲区大小落在行终止符中间时出现的问题。
    • 更改了断开连接的逻辑,使其不影响 TcpClient,以便可以重用它进行后续的 POP3 请求。
    • 添加了 Top 命令,以支持非标准的 POP3 TOP 命令,从而能够下载消息头而不是整个消息。
  • 2007.11.20 初始发布
© . All rights reserved.