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

PopClient - SmtpClient 的 POP3 伴侣

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.94/5 (53投票s)

2010年11月8日

CPOL

16分钟阅读

viewsIcon

244545

downloadIcon

4963

PopClient 是一个支持 SSL 和附件的异步 POP3 库。

图 1 - POP 设置对话框

图 2 - 邮件查看器

图 3 - POP3 聊天记录

引言

我有一个 .NET 项目需要一个 POP3 库,我和一个朋友正在开发这个项目,虽然快速搜索了一下 Google,找到了几个免费的实现,但没有一个完全符合我的要求。我想要一个可靠的 POP3 类,它能够完全支持异步获取、取消、SSL、附件、HTML 电子邮件,并且接口简单易用,这样调用方应用程序就不需要进行繁琐的变通来将其集成到现有系统中。本文将解释这个库如何使用,讨论类的实现,并包含一个演示 WPF 应用程序,展示一个典型应用程序可能如何使用这个库。这个库的目标是为开发者提供一种非常简单的方式来从 POP3 服务器读取电子邮件,并考虑到这一点,它隐藏了底层 POP3 协议,不对调用代码公开。这意味着这个库不适合那些正在编写自己的基于 POP3 的代码或想扩展 POP3 协议(例如编写垃圾邮件过滤器或邮件自动转发应用程序)的人。在某些方面,这个库可以被视为 SmtpClient 类(来自 System.Net.Mail 命名空间)的 POP3 类比,事实上它实际上使用了 MailMessage 类(来自同一命名空间)来表示电子邮件(尽管正如我将在本文中所解释的,这也有其缺点)。

有关演示应用程序的安全说明

PopClient 类有一个 DeleteMailAfterPop 属性,用于指定邮件是否应保留在服务器上,或者在获取后是否应删除。除非是临时使用的 POP 帐户,否则在使用演示应用程序时,您可能极不可能想要删除邮件,因此作为预防措施,该属性在 UI 和代码中都已被禁用,这样您就不会意外删除任何电子邮件。

VS 2010 和 .NET 4.0

虽然代码和演示项目是使用 VS 2010 和 .NET 4.0 编写和测试的,但从 VS 2008 和 .NET 3.5 使用它应该不会太困难。据我所知,我在任何代码中都没有使用过 4.0 特有的功能。

使用库

所有代码都在 PopClient 程序集中,因此您需要添加一个对它的引用。所有公共类都在 Extra.Mail 命名空间下,所以您可能想在您的代码中为该命名空间添加一个 using 声明。PopClient 类是您唯一需要直接实例化和使用的类,它实现了 IDisposable,并且在您完成使用该类后,务必将其释放。话虽如此,由于该类是异步获取邮件的,因此您不应在邮件获取操作完成之前或手动中止邮件获取操作之前释放该类。这基本上意味着它不适合用于 using 块(通常用于 IDisposable 类型)。演示应用程序展示了该类的一种典型处置方式,我还会讨论另一种简单的安全处置实例的方法。讽刺的是,说了这么多之后,我将要展示的第一段代码确实使用了 using 块,尽管这是一个控制台应用程序,并且我采取了预防措施来确保对象在 POP3 操作完成之前保持活动状态。

static void Main()
{
    using (var popClient = new PopClient(host, port) { 
        Username = username, Password = password, EnableSsl = true })
    {
        popClient.MailPopCompleted += PopClient_MailPopCompleted;
        popClient.MailPopped += PopClient_MailPopped;
        popClient.QueryPopInfoCompleted += PopClient_QueryPopInfoCompleted;
        popClient.ChatCommandLog += PopClient_ChatCommandLog;
        popClient.ChatResponseLog += PopClient_ChatResponseLog;

        try
        {
            popClient.PopMail();
        }
        catch(PopClientException pce)
        {
            Console.WriteLine("PopClientException caught!");
            Console.WriteLine(
                "PopClientException.PopClientBusy : {0}", pce.PopClientBusy);
        }

        Console.ReadKey();
    }
}

该类通过所需的 POP3 设置进行实例化,例如主机、用户名和密码。我还挂接了几个事件,这些事件将在 POP3 获取操作期间异步触发。一旦所有这些都完成,就会调用 PopMail 方法(该方法会立即返回),并且是从 try-catch 块调用的,因为该方法可能会抛出异常。虽然上面的代码只捕获了 PopClientException,但请注意它也可能抛出 InvalidOperationException(演示应用程序两者都处理)。最后,请注意 Console.ReadKey 调用巧妙的定位,以保持对象在获取完成之前保持活动状态且未被释放。ChatXXXLog 事件基本上记录了用于 POP3 连接的协议聊天记录,ChatCommandLog 事件会在我们发送给服务器的所有命令时触发,ChatResponseLog 事件会在所有单行服务器响应时触发(出于可用性原因,我不记录多行响应,因为在获取带大量附件的大邮件时,这很快就会变得难以管理)。处理这些事件是可选的,其主要用途是诊断连接问题,尽管您也可以通过聊天记录来了解 POP3 聊天是如何执行的(它是一个非常简单的协议,所以怀疑没有人会为此目的进行两次以上的操作)。

static void PopClient_ChatResponseLog(object sender, PopClientLogEventArgs e)
{
    Console.WriteLine("<< {0}", e.Line);
}

static void PopClient_ChatCommandLog(object sender, PopClientLogEventArgs e)
{
    Console.WriteLine(">> {0}", e.Line);
}

这两个处理程序的执行结果是,您将获得完整的聊天对话(带有漂亮的文本 TCP 方向指示)。*微笑*

>> STAT
<< +OK 7 703466
>> LIST 1
<< +OK 1 243160
>> UIDL 1
<< +OK 1 717619000000008003
>> RETR 1
<< +OK
>> LIST 2
<< +OK 2 2
>> UIDL 2
<< +OK 2 717619000000009001
>> RETR 2
<< +OK
>> LIST 3
<< +OK 3 216991
>> UIDL 3
<< +OK 3 717619000000010001
>> RETR 3
<< +OK
>> LIST 4
<< +OK 4 12
>> UIDL 4
<< +OK 4 717619000000011001
>> RETR 4
<< +OK

WPF 演示应用程序展示了展示此信息的更好方法。其他三个事件是您真正需要处理以读取已获取邮件的事件。第一个触发的将是 QueryPopInfoCompleted 事件,它会告诉您电子邮件的数量和它们的总大小。您可以使用这些信息来显示进度条或更新摘要状态 UI 面板。

static void PopClient_QueryPopInfoCompleted(object sender, MailPopInfoFetchedEventArgs e)
{
    Console.WriteLine("Event Fired: QueryPopInfoCompleted");
    Console.WriteLine("Count: {0}, Total Size: {1} bytes", e.Count, e.Size);
}

接下来,MailPopped 事件会为检索到的每封邮件触发一次,所以如果您处理了 QueryPopInfoCompleted 事件,您就会知道它会触发多少次(除非您通过调用 Cancel() 来取消操作)。类参考部分详细介绍了所有可用的信息、参数和属性,因此我不会解释代码中的每一项,尽管属性名称已经非常具有描述性,所以任何编写代码获取电子邮件的人可能都不需要查看文档。

static void PopClient_MailPopped(object sender, MailPoppedEventArgs e)
{
    Console.WriteLine("Event Fired: MailPopped");
    Console.WriteLine("Mail No. {0}", e.Index);
    Console.WriteLine("Size: {0}", e.Size);
    Console.WriteLine("Uidl: {0}", e.Uidl);
    Console.WriteLine("Received: {0}", e.ReceivedTime);
    Console.WriteLine("From: {0}, {1}", e.Message.From.Address, e.Message.From.DisplayName);
    Console.WriteLine("To: {0}", e.Message.To);
    Console.WriteLine("Subject: {0}", e.Message.Subject);
    Console.WriteLine("Attachments: {0}", e.Message.Attachments.Count);
    
    foreach (var attachment in e.Message.Attachments)
    {
        Console.WriteLine("File: {0}", attachment.Name);
    }

    for (int i = 0; i < e.Message.Headers.Count; i++)
    {
        Console.WriteLine("{0} = {1}", 
            e.Message.Headers.GetKey(i), 
            new String(e.Message.Headers[i].Take(40).ToArray()));
    }
}

MailPoppedEventArgs.Message 属性的类型是 MailMessage,所以任何以前使用过该类的人都会认出我在那里使用的其他属性。编写 MailMessage 的开发人员最初主要将其用于 SmtpClient 类,这意味着它缺少一些您在检索邮件时需要的属性,例如 Uidl、ReceivedTime、Size 等。所以我不得不通过 MailPoppedEventArgs 类来提供它们。我在写了一半代码后才意识到这一点,虽然写一个自定义的 MailMessage 类型类并不难,但我还是决定继续使用 MailMessage,这很大程度上是出于我需要确保我的类保持与使用 MailMessage 的 SmtpClient 类相似和类比。开发者喜欢熟悉感,我相信使用可识别的类型肯定会有助于此。我还为 Attachment 类添加了一个扩展方法,这样您就不必处理内存流和文件 IO,而是可以直接调用一个方便的 Save(filePath) 方法来保存附件(WPF 演示应用程序确实使用了这个)。最后一个触发的事件是 MailPopCompleted 事件。请注意,无论 POP 获取是“和谐地”完成,还是被用户取消,甚至是由于异常而必须中止,它总是会被触发。实际上,这是通知意外错误的唯一方法,所以您应该始终处理此事件。

static void PopClient_MailPopCompleted(object sender, MailPopCompletedEventArgs e)
{
    Console.WriteLine("Event Fired: MailPopCompleted");
    Console.WriteLine("MailPopCompletedEventArgs.Aborted : {0}", e.Aborted);
    if (e.Exception != null)
    {
        Console.WriteLine(e.Exception.Message);

        PopClientException pce = e.Exception as PopClientException;
        if(pce != null)
        {                    
            Console.WriteLine("PopClientException.PopClientUserCancelled : {0}", 
                pce.PopClientUserCancelled);
        }
    }            
}

这段代码并未完全演示 PopClientException 如何用于确定 POP3 错误(例如密码错误或 POP3 服务器错误),但演示应用程序在这方面做得更好。

仅检索新消息

  • 对仅检索新消息的支持已在 2010 年 11 月 19 日的更新中添加。

如果您注意到具有在服务器上保留邮件选项的邮件客户端,它们只会检索之前未获取的邮件。它们通过存储每封邮件的 UIDL 值,然后不下载与现有 UIDL 匹配的消息来实现此目的。PopClient 类现在有一个名为 UidlsToIgnore 的集合属性。因此,在调用 PopMail 之前,调用客户端可以选择填充此集合,这些消息将被跳过。已对库进行更改,以调整计数和总大小报告以适应这些跳过的消息。但请注意,MailPoppedEventArgs 类提供的索引值将继续表示服务器上消息的索引值。因此,如果您选择跳过现有消息,在获取邮件时您可能会看到不连续的索引。这不是错误,而是正确的行为。显示如何添加跳过 UIDL 的示例代码

popClient.UidlsToIgnore.Add("717619000000008003");
popClient.UidlsToIgnore.Add("717619000000009001");
popClient.UidlsToIgnore.Add("717619000000010001");

警告:事件处理程序和线程上下文

有一件非常重要的事情需要记住,除了 MailPopCompleted 事件之外,所有事件都在不同的线程上触发(该类内部用于 POP3 连接的工作线程)。因此,如果您在这些事件处理程序中处理 UI,您需要采取典型措施来访问辅助线程中的 UI 控件。有众所周知的方法可以解决此问题,例如使事件在创建 PopClient 对象的同一线程上触发,但我决定不走这条路,原因有两个。第一个原因是,这迫使 PopClient 类意识到这种 UI/线程问题,而我希望保持类的设计简洁,这意味着它必须不知道这种琐碎的副作用。另一个重要原因是,调用代码是使用 Windows Forms 还是 WPF 将会决定如何以不同的方式处理线程上下文,更不用说该类可能被其他 .NET UI 框架使用,这些框架可能有自己的线程相关怪癖。第三个次要原因是,试图在 PopClient 类中隐藏这一点可能会很危险,因为调用者将对这些线程问题保持一无所知,因此无法应对由线程间访问可能产生的任何后果。最后,正如您将从演示项目中看到的,在调用 UI 代码中处理这个问题非常简单。

演示应用程序

图 4 - 保存附件

图 5 - 详细日志/异常处理

WPF 演示应用程序是使用 Visual Studio 2010 编写的,并且目标是 .NET 4.0 框架。我将完全避免在此讨论 XAML 代码,而将重点放在 PopClient 类如何在 UI 应用程序中典型地使用。那些对 XAML/数据绑定感兴趣的人可以查看项目源代码,如果您有具体问题(考虑到代码的简单性,不太可能),请随时通过本文的论坛向我提问。演示项目使用 MVVM,所有 PopClient 代码都放在一个 ViewModel 类中。我下面讨论的每个方法都将是该类的一部分。

ViewModel 有一个 PopClient 字段(需要是字段,这样我们才能支持取消和处置)。请注意,在实例化类时不会建立套接字连接(这并不奇怪,因为它还没有连接信息)。

private PopClient popClient = new PopClient();

事件处理程序在 V-M 构造函数中挂接。

public MainWindowViewModel()
{
    popClient.QueryPopInfoCompleted += PopClient_QueryPopInfoCompleted;
    popClient.MailPopped += PopClient_MailPopped;
    popClient.MailPopCompleted += PopClient_MailPopCompleted;
    popClient.ChatCommandLog += PopClient_ChatCommandLog;
    popClient.ChatResponseLog += PopClient_ChatResponseLog;

    Application.Current.Exit += Current_Exit;
}

void Current_Exit(object sender, ExitEventArgs e)
{
    popClient.Dispose();
}

此外,我还处理了应用程序的 Exit 事件,以便可以处置 popClient 实例。从技术上讲,这可以被认为是一件肤浅的事情,因为进程将已终止,并且所有特定于进程的资源都将被释放。但它保持了代码的整洁,并鼓励在 ViewModel 可能在应用程序生命周期中创建和销毁多次的应用程序中采用这种做法(在这种情况下,ViewModel 本身可能应该是 IDisposable)。使用 WinForms 会稍微容易一些,因为所有 Controls 和 Forms 都是 IDisposable,因此有一个有据可查且成熟的地方可以处置 PopClient 实例。WPF 窗口不是这样做的,因为 WPF 在各处使用弱引用的方法使这变得不必要。处置 PopClient 实例的另一种方法是在 MailPopCompleted 事件处理程序中,尽管这并不是一种非常简洁的方法,而且有点 hacky。我之所以这样说,是因为当事件触发时,PopClient 实例仍在被使用,所以在几微秒内,您实际上有一个已处置的对象仍然在执行方法(尽管是它的最后一次)。当然,由于我编写了该类,我知道(截至今天)这样做是安全的,并且在对象尚未完成执行方法时没有 GC 触发的风险,但如果您运行代码分析器或内存泄漏分析器,它们可能会抱怨并抛出令人恼火的警告消息。所以,除非是在您完全控制的代码中,否则我不建议这样做。

以下字段用于将信息传递到 UI,即主窗口和日志窗口。LogInfo 类仅仅是我创建的一个方便的类,用于帮助数据绑定。这里的一个重要成员是 mainDispatcher 字段,它用于处理由事件在辅助线程上触发(而不是从最初设置处理程序的 मुख्य UI 线程)引起的线程上下文问题。

private Dispatcher mainDispatcher;

private ObservableCollection<MailPoppedEventArgs> mails
  = new ObservableCollection<MailPoppedEventArgs>();

class LogInfo
{
    public string Line { get; set; }
    public bool Response { get; set; }
}

private ObservableCollection<LogInfo> logs
  = new ObservableCollection<LogInfo>();

RefreshCommand 属性是 V-M 公开的一个命令对象,它基本上执行邮件获取操作。

public ICommand RefreshCommand
{
    get
    {
        return refreshCommand ??
          (refreshCommand = new DelegateCommand(Refresh, CanRefresh));
    }
}

public bool CanRefresh()
{
    return !popClient.IsWorking();
}

public void Refresh()
{
    popClient.Host = Settings.Default.Host;
    popClient.Port = Settings.Default.Port;
    popClient.Username = Settings.Default.Username;
    popClient.Password = this.PopPassword;
    popClient.EnableSsl = Settings.Default.EnableSsl;
    popClient.DeleteMailAfterPop = false; // For demo safety!
    popClient.Timeout = Settings.Default.Timeout;

    try
    {
        mainDispatcher = Dispatcher.CurrentDispatcher;
        FetchStatusText = "Fetching...";
        mails.Clear();
        logs.Clear();
        popClient.PopMail();
    }
    catch (InvalidOperationException ex)
    {
        FetchStatusText = String.Format(
          "Connection error - {0}", ex.Message);
    }
    catch (PopClientException ex)
    {
        FetchStatusText = String.Format(
          "POP3 error - {0}", ex.Message);
    }
}

注意 CanRefresh 如何将调用委托给 PopClient.IsWorking,如果当前正在进行获取操作,它将返回 true。我在这里分配了 POP3 连接所需的各种 POP3 属性,尽管对于这个特定的演示项目来说,在这里这样做实际上是一个错误 - 因为它会在每次调用 Refresh 时被调用。最初我计划允许用户在获取后更改 POP 设置,但最终我只在启动时显示设置对话框。当然,这是一个微不足道的问题,但我想在这里提及,以防有人想知道我为什么这样做。注意我是如何将当前 Dispatcher 实例保存在 mainDispatcher 字段中的,我稍后将使用它来更新我的 ObservableCollections<>,因为它们将绑定到 View,因此需要执行 UI 线程。

取消操作非常直接。

public ICommand CancelCommand
{
    get
    {
        return cancelCommand ?? 
          (cancelCommand = new DelegateCommand(Cancel, CanCancel));
    }
}

public bool CanCancel()
{
    return popClient.IsWorking();
}

public void Cancel()
{
    popClient.Cancel();
}

存在一个潜在的竞争条件,即 CanCancel 返回 true,但在调用 Cancel 之前获取操作已完成。但在这种情况下调用 Cancel 是安全的,所以不需要处理这种竞争条件。

这是用于保存附件的代码(可通过附件图标的右键菜单访问)。

public ICommand SaveFileCommand
{
    get
    {
        return saveFileCommand ?? 
          (saveFileCommand = new DelegateCommand<Attachment>(SaveFile));
    }
}

public void SaveFile(Attachment attachment)
{
    SaveFileDialog dialog = new SaveFileDialog()
    {
        FileName = attachment.Name
    };

    if (dialog.ShowDialog().GetValueOrDefault())
    {
        attachment.Save(dialog.FileName);
    }
}

Save 方法是我编写的 Attachment 类的扩展方法。这是调出 Chat-Log 窗口的代码。

public ICommand ShowChatLogCommand
{
    get
    {
        return showChatLogCommand ?? 
          (showChatLogCommand = new DelegateCommand(
            ShowChatLog, CanShowChatLog));
    }
}

private LogWindow logWindow;

public bool CanShowChatLog()
{
    return logWindow == null;
}

public void ShowChatLog()
{
    logWindow = new LogWindow() 
      { DataContext = logs, Owner = Application.Current.MainWindow };
    logWindow.Show();
    logWindow.Closed += (s,e) => logWindow = null;
}

您可以看到它使用了 logs 集合,该集合通过 ChatXXXLog 事件处理程序填充。

void PopClient_ChatResponseLog(object sender, PopClientLogEventArgs e)
{
    mainDispatcher.Invoke((Action)(() => logs.Add(
      new LogInfo() { Line = e.Line, Response = true })), null);
}

void PopClient_ChatCommandLog(object sender, PopClientLogEventArgs e)
{
    mainDispatcher.Invoke((Action)(() => logs.Add(
      new LogInfo() { Line = e.Line })), null);
}

注意我如何使用 mainDispatcher 在 UI 线程上调用我的代码。这是 QueryPopInfoCompleted 和 MailPopped 事件的事件处理程序。

void PopClient_MailPopped(object sender, MailPoppedEventArgs e)
{
    mainDispatcher.Invoke((Action)(() => mails.Add(e)), null);
}

void PopClient_QueryPopInfoCompleted(
  object sender, MailPopInfoFetchedEventArgs e)
{
    MailStatsText = String.Format(
      "{0} mails, Size = {1}", e.Count, e.Size);
}

另外,这是 MailPopCompleted 事件的事件处理程序。

void PopClient_MailPopCompleted(object sender, MailPopCompletedEventArgs e)
{
    if (e.Aborted)
    {
        PopClientException popex = e.Exception as PopClientException;
        if (popex == null)
        {
            FetchStatusText = "Aborted!";
        }
        else
        {
            FetchStatusText = popex.PopClientUserCancelled 
              ? "User cancelled!" :  
                String.Format("POP3 error - {0}", popex.Message);
        }
    }
    else
    {
        FetchStatusText = "Done!";
    }

    CommandManager.InvalidateRequerySuggested();
}

这里的代码是处理各种异常的一个更好的例子(与我之前展示的代码相比)。它演示了应用程序如何处理 POP3 服务器错误并将这些消息显示给用户。

类参考

所有公共类型都在 Extra.Mail 命名空间下。

PopClient

public class PopClient : IDisposable
{
    // Summary:
    //     Initializes a new instance of the PopClient class.
    public PopClient();

    //
    // Summary:
    //     Initializes a new instance of the PopClient class.
    //
    // Parameters:
    //   host:
    //     The name or IP address of the host server
    public PopClient(string host);

    //
    // Summary:
    //     Initializes a new instance of the PopClient class.
    //
    // Parameters:
    //   host:
    //     The name or IP address of the host server
    //
    //   port:
    //     The port to be used
    public PopClient(string host, int port);

    // Summary:
    //     Specify whether email is deleted after fetch
    public bool DeleteMailAfterPop { get; set; }

    //
    // Summary:
    //     Specify whether the PopClient uses a Secure Sockets Layer connection
    public bool EnableSsl { get; set; }

    //
    // Summary:
    //     Gets or sets the name or IP address of the POP3 host
    public string Host { get; set; }

    //
    // Summary:
    //     Gets or sets the POP3 password
    public string Password { get; set; }

    //
    // Summary:
    //     Gets or sets the port used for the POP3 connection
    public int Port { get; set; }

    //
    // Summary:
    //     Gets or sets a value that specifies the amount of time after the connection
    //     times out.
    public int Timeout { get; set; }

    //
    // Summary:
    //     Gets or sets the POP3 username
    public string Username { get; set; }

    /// <summary>
    /// Gets or sets the collection of UIDLs that are to be ignored when doing a fetch.
    /// </summary>    
    public HashSet<string> UidlsToIgnore { get; private set; }

    // Summary:
    //     Occurs when a POP3 command is sent to the server
    public event EventHandler<PopClientLogEventArgs> ChatCommandLog;

    //
    // Summary:
    //     Occurs when a POP3 response is received from the server
    public event EventHandler<PopClientLogEventArgs> ChatResponseLog;

    //
    // Summary:
    //     Occurs when the asynchronous fetch completes
    public event EventHandler<MailPopCompletedEventArgs> MailPopCompleted;

    //
    // Summary:
    //     Occurs when a mail is fetched
    public event EventHandler<MailPoppedEventArgs> MailPopped;

    //
    // Summary:
    //     Occurs when summary info for the fetch operation is available
    public event EventHandler<MailPopInfoFetchedEventArgs> QueryPopInfoCompleted;

    // Summary:
    //     Cancels the asynchronous fetch operation
    public void Cancel();

    //
    // Summary:
    //     Releases all resources used by the PopClient class.
    public void Dispose();

    //
    // Summary:
    //     Indicates whether a fetch operation is under way
    //
    // Returns:
    //     True if a fetch is on, False otherwise
    public bool IsWorking();

    //
    // Summary:
    //     Begins an asynchronous fetch operation
    public void PopMail();
}

PopClientException

[Serializable]
public class PopClientException : Exception
{
    // Summary:
    //     Initializes a new instance of the PopClientException class.
    public PopClientException();
    
    //
    // Summary:
    //     Initializes a new instance of the PopClientException class.
    //
    // Parameters:
    //   message:
    //     The message that describes the error
    public PopClientException(string message);

    // Summary:
    //     Gets a value indicating whether the PopClient is busy.
    public bool PopClientBusy { get; }
    
    //
    // Summary:
    //     Gets a value indicating whether the fetch operation was cancelled by the
    //     user.
    public bool PopClientUserCancelled { get; }

    // Summary:
    //     Initializes a new instance of the PopClientException class.
    //
    // Parameters:
    //   info:
    //     The object that holds the serialized object data
    //
    //   context:
    //     The contextual information about the source or destination
    public override void GetObjectData(SerializationInfo info, StreamingContext context);
}

MailPopInfoFetchedEventArgs

public class MailPopInfoFetchedEventArgs : EventArgs
{
    // Summary:
    //     Instantiates a new instance of the MailPopInfoFetchedEventArgs class
    //
    // Parameters:
    //   count:
    //     The number of messages
    //
    //   size:
    //     Total size of all messages
    public MailPopInfoFetchedEventArgs(int count, int size);

    // Summary:
    //     Get the number of messages
    public int Count { get; }
    //
    // Summary:
    //     Gets the total size of all messages
    public int Size { get; }
}

MailPoppedEventArgs

public class MailPoppedEventArgs : EventArgs
{
    // Summary:
    //     Instantiates a new instance of the MailPoppedEventArgs class
    //
    // Parameters:
    //   index:
    //     The index of the message
    //
    //   message:
    //     A MailMessage that contains the fetched message
    //
    //   size:
    //     The size of the mail
    //
    //   uidl:
    //     The uidl value of the message
    //
    //   receivedTime:
    //     Time when the message was received by the server
    public MailPoppedEventArgs(
      int index, MailMessage message, int size, string uidl, DateTime receivedTime);

    // Summary:
    //     Gets the index of the message
    public int Index { get; }
    //
    // Summary:
    //     Gets the MailMessage that contains the fetched message
    public MailMessage Message { get; }
    //
    // Summary:
    //     Gets the time when the message was received by the server
    public DateTime ReceivedTime { get; }
    //
    // Summary:
    //     Gets the size of the mail
    public int Size { get; }
    //
    // Summary:
    //     Gets the uidl value of the message
    public string Uidl { get; }
}

MailPopCompletedEventArgs

public class MailPopCompletedEventArgs : EventArgs
{
    // Summary:
    //     Instantiates a new instance of the MailPopCompletedEventArgs class
    public MailPopCompletedEventArgs();
    //
    // Summary:
    //     Instantiates a new instance of the MailPopCompletedEventArgs class
    //
    // Parameters:
    //   ex:
    //     Any exception that was thrown during the asynchronous fetch
    public MailPopCompletedEventArgs(Exception ex);

    // Summary:
    //     Gets a value indicating whether the fetch operation was aborted
    public bool Aborted { get; }
    //
    // Summary:
    //     Gets any exception that was thrown during the asynchronous fetch
    public Exception Exception { get; }
}

PopClientLogEventArgs

public class PopClientLogEventArgs : EventArgs
{
    // Summary:
    //     Instantiates a new instance of the PopClientLogEventArgs class
    //
    // Parameters:
    //   line:
    //     A string representing a message log line
    public PopClientLogEventArgs(string line);

    // Summary:
    //     Gets a string representing a message log line
    public string Line { get; }
}

实现细节

此类基于 POP3 协议,该协议通过 RFC1939(Post Office Protocol v3)标准化。

设计意图之一是隐藏公共接口的实现细节,这意味着我可以选择性地实现从 POP3 服务器获取电子邮件所需的最小协议,而不会丢失任何 POP3 功能。internal 类 PopConnection 用于建立套接字连接,并从服务器发送/接收 POP3 命令和响应。它是一个非常精简的基于套接字的类,使用 TcpClient 对象与 POP 服务器进行通信。

internal class PopConnection : IDisposable
{
    private char[] endMarker;
    
    private Stream stream;
    
    private TcpClient tcpClient;

    public PopConnection(PopClient popClient);

    private bool CheckForEndOfData(StringBuilder sb);

    public void Dispose();

    protected virtual void Dispose(bool disposing);

    public string ReadMultiLineResponseString();

    public int ReadResponse(out byte[] bytes);

    public string ReadResponseString();

    private string SendCommandInternal(string command);

    public string SendDele(int messageId);

    public string SendList();

    public string SendList(int messageId);

    public string SendPass(string pass);

    public string SendQuit();

    public string SendRetr(int messageId);

    public string SendStat();

    public string SendUidl();

    public string SendUidl(int messageId);

    public string SendUser(string user);
}

以下是 SendXXX 方法实现的部分列表。

private string SendCommandInternal(string command)
{
    byte[] bytes = Encoding.UTF8.GetBytes(command);
    stream.Write(bytes, 0, bytes.Length);
    return command;
}

public string SendQuit()
{
    return SendCommandInternal("QUIT\r\n");
}

public string SendStat()
{
    return SendCommandInternal("STAT\r\n");
}

public string SendList(int messageId)
{
    return SendCommandInternal(String.Format("LIST {0}\r\n", messageId));
}

public string SendDele(int messageId)
{
    return SendCommandInternal(String.Format("DELE {0}\r\n", messageId));
}

它主要是直接的套接字代码,特别是由于 POP3 协议相当简单。接收方法也相当直接。

public string ReadResponseString()
{
    byte[] bytes;
    int count = this.ReadResponse(out bytes);
    return bytes.GetString(count);
}

public string ReadMultiLineResponseString()
{
    StringBuilder sb = new StringBuilder();

    byte[] bytes;

    do
    {
        int count = this.ReadResponse(out bytes);
        sb.Append(bytes.GetString(count));                
    }
    while( !CheckForEndOfData(sb) );

    return sb.ToString();
}

private char[] endMarker = { '\r', '\n', '.', '\r', '\n' };

private bool CheckForEndOfData(StringBuilder sb)
{
    if (sb.Length < 5)
        return false;

    char[] compare = new char[5];
    sb.CopyTo(sb.Length - 5, compare, 0, 5);

    return (endMarker as IStructuralEquatable).Equals(
        compare, EqualityComparer<char>.Default);
}

每个 POP3 命令都有一个关联的命令对象,并且所有命令对象都实现了 IPopCommand 接口。

internal interface IPopCommand
{
    event EventHandler<PopClientDirectionalLogEventArgs> PopClientLog;

    void Execute(params object[] arguments);

    bool IsMultiLineResponse { get; }
}

有一个抽象的 BasePopCommand 类,它实现了部分通用功能,并简化了命令类的实现。

internal abstract class BasePopCommand : IPopCommand
{
    private EventHandler<PopClientDirectionalLogEventArgs> PopClientLog;

    public event EventHandler<PopClientDirectionalLogEventArgs> PopClientLog;

    public BasePopCommand(PopConnection popConnection);
    private void CheckForErrorResponse();
    public void Execute(params object[] arguments);
    private void Execute(object argument);
    protected abstract string ExecuteInternal(object argument);
    protected void OnPopClientLog(string line, bool isServerResponse);
    protected virtual void ParseInternal();

    public virtual bool IsMultiLineResponse { get; }
    protected string MultiLineResponse { get; private set; }
    protected PopConnection PopConnection { get; private set; }
    protected string Response { get; private set; }
}

以下是一些更重要的方法实现。

public void Execute(params object[] arguments)
{
    this.Execute(arguments.FirstOrDefault());
}

private void Execute(object argument)
{
    this.OnPopClientLog(ExecuteInternal(argument), false);
    this.Response = this.PopConnection.ReadResponseString();
    this.OnPopClientLog(this.Response, true);
    this.CheckForErrorResponse();
    if (this.IsMultiLineResponse)
    {
        this.MultiLineResponse = 
          this.PopConnection.ReadMultiLineResponseString();
    }

    this.ParseInternal();
}

protected virtual void ParseInternal()
{            
}

private void CheckForErrorResponse()
{
    if (this.Response.StartsWith("-ERR"))
    {
        throw new PopClientException(
          new String(this.Response.Skip(4).ToArray()).Trim());
    }
}

派生类现在只需要实现 ExecuteInternal,并且可选地实现 ParseInternal(如果它们需要解析响应流)。这是 User 命令的实现方式,它是最简单的命令之一,因为它不对响应进行自定义解析。

internal class UserPopCommand : BasePopCommand
{
    public UserPopCommand(PopConnection popConnection)
        : base(popConnection)
    {
    }

    protected override string ExecuteInternal(object argument)
    {
        return PopConnection.SendUser(argument as string);
    }
}

这是一个稍微复杂一些的派生类,它实现了 Stat 命令。

internal class StatPopCommand : BasePopCommand
{
    public StatPopCommand(PopConnection popConnection)
        : base(popConnection)
    {
    }

    protected override string ExecuteInternal(object argument)
    {
        return PopConnection.SendStat();
    }

    private int count;

    public int Count
    {
        get { return count; }
    }

    private int size;

    public int Size
    {
        get { return size; }
    }

    protected override void ParseInternal()
    {
        string[] parts = this.Response.Trim().Split();

        if (parts.Length != 3 || !Int32.TryParse(parts[1], out count) 
          || !Int32.TryParse(parts[2], out size))
        {
            throw new PopClientException("Unknown STAT response from server.");
        }
    }
}

最重要的命令之一是 Retr 命令,这是它的实现方式。

internal class RetrPopCommand : BasePopCommand
{
  public MailMessage Message { get; private set; }

  public DateTime ReceivedTime  { get; private set; }

  public RetrPopCommand(PopConnection popConnection)
      : base(popConnection)
  {
  }

  protected override string ExecuteInternal(object argument)
  {
      return PopConnection.SendRetr((int)argument);
  }

  protected override void ParseInternal()
  {
      var converter = new CDOMessageConverter(this.MultiLineResponse);
      this.Message = converter.ToMailMessage();
      this.ReceivedTime = converter.ReceivedTime;
  }

  public override bool IsMultiLineResponse
  {
      get
      {
          return true;
      }
  }
}

注意 CDOMessageConverter 类的使用。CDO(Collaboration Data Objects)是一个 COM 组件,自 Windows 2000 起就包含在 Windows 操作系统中,它具有解析 MIME 电子邮件(包括对附件的完整支持)的内置功能。CDO 对 MailMessage 类一无所知,所以我写了一个转换器,它将 MIME 字符串转换为 CDO 消息,然后将 CDO 消息转换为 MailMessage 对象。这相当直接的 COM 互操作,所以我不会详细介绍。这两个类没有一对一的结构等价性,所以我不得不做一些妥协,但只要我能够获得读取包含附件的电子邮件所需的所有字段,我就不太关心类型之间的表面差异。

PopChat 类提供了一个单点接口,PopClient 类可以从该接口调用 POP 命令。这是显示该类功能的代码片段列表。除了公开每个命令的属性外,它还确保所有命令都通过一个通用事件处理程序订阅了它们的日志事件。

internal class PopChat : IDisposable
{
  //...
  
  internal NothingPopCommand Nothing { get; private set; }
  internal UserPopCommand User { get; private set; }
  internal PassPopCommand Pass { get; private set; }
  internal StatPopCommand Stat { get; private set; }
  //...

  public PopChat(PopClient client)
  {
      //...

      new IPopCommand[] 
      { 
          Nothing = new NothingPopCommand(popConnection),
          User = new UserPopCommand(popConnection),
          Pass = new PassPopCommand(popConnection),
          Stat = new StatPopCommand(popConnection),
          Retr = new RetrPopCommand(popConnection),
          Quit = new QuitPopCommand(popConnection),
          List = new ListPopCommand(popConnection),
          Uidl = new UidlPopCommand(popConnection),
          Dele = new DelePopCommand(popConnection),
          ListAll = new ListAllPopCommand(popConnection),
          UidlAll = new UidlAllPopCommand(popConnection)
      }.ToList().ForEach(pc => pc.PopClientLog += PopCommand_PopClientLog);
  }

  private void PopCommand_PopClientLog(
    object sender, PopClientDirectionalLogEventArgs e)
  {
      this.client.LogLine(e.Line, e.IsServerResponse);   
  }
}

最后,实际的 POP 聊天是从 PopClient 类使用的 BackgroundWorker 线程中调用的。

private void Worker_DoWork(object sender, DoWorkEventArgs e)
{
    using (PopChat chat = new PopChat(this))
    {
        chat.Nothing.Execute();
        chat.User.Execute(this.Username);
        chat.Pass.Execute(this.Password);
        chat.Stat.Execute();
        chat.ListAll.Execute();
        chat.UidlAll.Execute();

        int count = chat.Stat.Count;
        int size = chat.Stat.Size;

        if (this.UidlsToIgnore.Count > 0)
        {
            var skipList = chat.UidlAll.Uidls.Values.Intersect(this.UidlsToIgnore);

            count -= skipList.Count();

            foreach (var uidl in skipList)
            {
                size -= chat.ListAll.MessageSizes[chat.UidlAll.Indices[uidl]];
            }
        }

        this.OnMailPopInfoFetched(count, size);

        for (int fetchIndex = 1; fetchIndex <= chat.Stat.Count; fetchIndex++)
        {
            if (worker.CancellationPending)
            {
                e.Cancel = true;
                break;
            }

            if (!this.UidlsToIgnore.Contains(chat.UidlAll.Uidls[fetchIndex]))
            {
                chat.Retr.Execute(fetchIndex);
                this.OnMailPopped(fetchIndex, chat.Retr.Message, 
                  chat.ListAll.MessageSizes[fetchIndex], 
                  chat.UidlAll.Uidls[fetchIndex], chat.Retr.ReceivedTime);

                if (this.DeleteMailAfterPop)
                {
                    chat.Dele.Execute(fetchIndex);
                }
            }
        }

        chat.Quit.Execute();
    }
}

结论

如果您在 POP 服务器上遇到特定问题,聊天记录(可通过事件处理程序轻松访问)应该能准确地告诉您发生了什么。一如既往,欢迎提供反馈和批评,请随时通过本文的讨论论坛发表您的评论。不过,如果您发表恶意言论或评分低于 5 星,我将毫不犹豫地对您施加我最近学会的“亚基”诅咒!*微笑*

历史

  • 2010 年 11 月 8 日 - 文章首次发布。
  • 2010 年 11 月 19 日
    • 默认 POP 端口错误地设置为 SMTP 端口。现已修复。
    • 添加了对可跳过 UIDL 的支持。这允许您仅检索之前未获取的邮件。
    • 为 UIDL 和 LIST 添加了新的命令对象,作为现有版本的补充,但这些命令会进行无索引的完整获取。
© . All rights reserved.