PopClient - A POP3 companion to SmtpClient






4.94/5 (53 votes)
PopClient is an asynchronous POP3 library with support for SSL and attachments
Introduction
I needed a POP3 library for a .NET project that a friend and I were working
on, and while a quick Google-search revealed several free implementations, none
of them fully fit my requirements. I wanted a reliable POP3 class with full
support for asynchronous fetching, cancellation, SSL, attachments, HTML email, and an uncomplicated and simple
interface so that calling applications wouldn't need to do hideous workarounds
to integrate it into their existing system. This article explains how this
library can be used, talks about the class implementation, and also includes a
demo WPF application that shows how a typical app might use this library.
This library's goal is to provide developers with a very simple way to read
email from POP3 servers, and with that in mind, it hides the underlying POP3
protocol from calling code. What that means is that this library is not for
those who are writing their own POP3 based code or want to extend the POP3
protocol in some way (for example writing a spam filter or an email
auto-forwarding application). In some ways this library can be considered as a
POP3 analogy to the SmtpClient
class from the System.Net.Mail
namespace and in fact it actually uses the MailMessage
class (from
the same namespace) to represent an email (although that had its disadvantages
as I will explain in this article).
Safety note about the demo app
The PopClient
class has a DeleteMailAfterPop
property that's used to specify whether mails should be left on the server or if
they should be deleted after they are fetched. Unless it's a throwaway POP
account, chances are extremely low that you'd want to delete mails when running
the demo app, and so as a precautionary measure, that property has been disabled
both in the UI and in the code, so you will not inadvertently delete any of your
email.
VS 2010 and .NET 4.0
While the code and the the demo project has been written and tested using VS 2010 and .NET 4.0, you should not have too much difficulty using this from VS 2008 and .NET 3.5. As far as I know, I have not used any 4.0 specific features in any of the code.
Using the library
All the code is in the PopClient
assembly, so you need to add a
reference to that. And all the public
classes are under the
Extra.Mail
namespace, so you may want to add a using
declaration in your code for that namespace. The PopClient
class is
the only class that you need to directly instantiate and use, and it implements
IDisposable
and it's important that you dispose it off when you are
finished with the class. That said, since the class fetches mail asynchronously,
you should not dispose off the class until either the mail fetch operation has
completed, or you have manually aborted the mail fetch operation. This basically
means that it's not the best suited class for an using
-block
(typically used for IDisposable
types). The demo app shows one
typical way that the class be be disposed, and I'll also discuss another simple
way to dispose off the instance safely. Ironically, after saying all this, the
first bit of code I am about to show you does use an using
-block,
although this is a console application and I take precautions to ensure that the
object stays alive until the POP3 operation has completed.
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();
}
}
The class is instantiated with the required POP3 settings such as host,
username, and password. I have also hooked onto several events which will be
fired asynchronously during the POP3 fetch operation. Once this is all done, a
call is made to the PopMail
method (which immediately returns), and
it's called from a try
-catch
block since the method
can throw exceptions. Although the code above only catches a
PopClientException
be aware that it can also throw an
InvalidOperationException
(the demo app handles both). And finally notice
the crafty positioning of the Console.ReadKey
call to keep the
object alive and un-disposed until the fetch is completed. The ChatXXXLog
events basically log protocol-chat used for the POP3 connection, with the
ChatCommandLog
firing for all commands we send to the server and the
ChatResponseLog
firing for all single-line server responses (for
usability reasons, I do not log multi-line responses, since that will quickly be
unmanageable when fetching large emails with bulky attachments). It's purely
optional to handle these events and their use is primarily to diagnose
connectivity issues, although you could also follow the chat log to get an idea
of how POP3 chat is executed (it's a very simple protocol, so it's doubtful that
anyone will want to do it for that purpose more than once or twice).
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);
}
The effect of those two handlers is to give you a full chat conversation (with beautiful TCP direction indicators in text). *grin*
>> 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
The WPF demo app shows a better way to show this information. The other three
events are what you really need to handle for reading the fetched emails. The
first one to fire will be the QueryPopInfoCompleted
event, which
gives you the number of emails and their total size. You can potentially use
this information to show a progress-bar or to update a summary status UI panel.
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);
}
Next, the MailPopped
event fires once for each mail that's
retrieved, so if you've handled the QueryPopInfoCompleted
event you
will know how many times this will fire (unless you cancel the operation with a
call to Cancel()
). The Class Reference section details all the
information, arguments, and properties that are available, so I will not
explain every single item in the code, although the property names are fairly
self descriptive, so anyone writing code to fetch email probably won't need to
look at the documentation.
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()));
}
}
The MailPoppedEventArgs.Message
property is of type
MailMessage
, so anyone who's used that class before will recognize the
other properties that I've used there. The developers who wrote
MailMessage
intended it primarily for the SmtpClient
class,
which meant that it does not have certain properties that you need when you are
retrieving mail, such as Uidl
, ReceivedTime
,
Size
etc. So I had to provide them though the MailPoppedEventArgs
class. I realized this only after I had written half the code and while it would
not have been terribly difficult to write my custom MailMessage
-like
class, I decided to continue using MailMessage
, largely out of my
inconsiderate need to ensure that my class remained analogous and similar to the
SmtpClient
class which used MailMessage
. Developers
like familiarity and I believe that using recognizable types will certainly help
with that. I also added an extension method to the Attachment
class
so that you won't have to mess with memory streams and file IO, and instead can
just call a nice Save(filePath)
method that will save the
attachment (the WPF demo app does make use of this). The last event that's fired
is the MailPopCompleted
event. Note that this is always fired,
whether the POP fetch completed harmoniously, or if it got user cancelled, or
even if it had to abort because of an exception. In fact it's the only way to be
notified of unexpected errors, so you should always handle this event.
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);
}
}
}
This code does not fully demonstrate how the PopClientException
can be used to determine POP3 errors (like a bad password, or a POP3 server
error), but the demo app does a better job with that.
Retrieving only new messages
- Support for retrieving only new messages was added in the November 19th 2010 update.
If you've noticed with mail clients that have an option to leave mail on the
server, they only retrieve mail that has not been previously fetched. They
achieve this by storing the UIDL values for each mail message and then not
downloading messages that match an existing UIDL. The PopClient
class now has a collection property called UidlsToIgnore
.
So prior to calling PopMail
, the calling client can
optionally populate this collection and those messages will be skipped. Changes
have been made to the library so that the count and total size reported will be
adjusted to accommodate these skipped messages. Be aware though that the index
value provided by the MailPoppedEventArgs
class will continue to represent the index value of the message in the mail
server. So you will potentially see non-contiguous indices when fetching mail if
you have opted to skip existing messages. This is not a bug and is correct
behavior. Example code showing how skip-UIDLs are added:
popClient.UidlsToIgnore.Add("717619000000008003");
popClient.UidlsToIgnore.Add("717619000000009001");
popClient.UidlsToIgnore.Add("717619000000010001");
Warning : Event handlers and thread context
One very important thing to keep in mind is that all the events except for
the MailPopCompleted
event are fired on a different thread (the
worker thread that the class internally uses for the POP3 connection). So if you
are handling UI from these event handlers, you need to take the typical
precautions that you need to take when accessing UI controls from auxiliary
threads. There are well-known methods to work around this where the events can
be made to fire on the same thread that created the PopClient
object, but I decided not to go that route for two specific reasons. The first
reason is that this forces the PopClient
class to be aware of such
UI/threading issues and I wanted to keep the class design clean which meant that
it had to be kept unaware of such frivolous side effects. A second important
reason is that whether the calling code uses Windows Forms or WPF would dictate
the need to handle thread context in distinct ways, not to mention that the
class may be used from other .NET UI frameworks which may have their own thread
related quirks. A third minor reason is that it may actually be a hazardous
approach to hide this from the caller by attempting to handle this in the
PopClient
class, since the caller will remain blissfully unaware of these
threading issues and will thus be unprepared to deal with any potential
consequences arising out of inter-thread access. And finally, as you will see
from the demo project, it's quite trivial to handle this in the calling UI code.
The demo application
The WPF demo application was written using Visual Studio 2010 and targets the
.NET 4.0 framework. I will completely refrain from discussing the XAML code here
and will instead focus on how the PopClient
class is typically used
in a UI application. Those of you who are interested in the XAML/data-binding
can go through the project source, and if you have specific questions (unlikely
given how simple the code is), please feel free to ask me through the forum for
this article. The demo project uses MVVM and all of the PopClient
code is in a single View-Model class. Every method that I discuss below will
thus be a part of this class.
The View-Model has a PopClient
field (needs to be a field, so we
can support cancellation and disposing). Note that the socket connection is not
established when you instantiate the class (not surprising since it does not
have connection info yet).
private PopClient popClient = new PopClient();
The event handlers are hooked up in the V-M constructor.
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();
}
In addition, I also handle the application's Exit
event so I can
dispose off the popClient
instance. Technically this can be
considered a superficial thing to do since the process will have terminated and
all process-specific resources would be released. But it keeps the code clean
and encourages this practice in applications where the V-M may be created and
destroyed multiple times during the life of an application (in which case, the
V-M itself should probably be an IDisposable
). It's a little easier
with WinForms since all Control
s and Form
s are
IDisposable
, and thus there's a documented well-established place to
dispose the PopClient
instance. WPF windows don't do it that way,
since the WPF approach of using weak references everywhere makes this
unnecessary. One alternate way to dispose off the PopClient
instance is in the MailPopCompleted
event handler although it is
not a very clean approach and is a bit of a hack. I say that because when the
event fires, the PopClient
instance is still in use, so for a few
microseconds you actually have a disposed object that's still executing a method
(albeit its last one). Of course since I wrote the class, I know it's safe to do
this (as of today) and there's no risk of the GC firing when it's still not done
executing a method, but if you run code profilers or memory leak analyzers, they
may complain and throw up an agitated warning message. So I wouldn't recommend
doing this unless it's in code that you are in full control of.
The following fields are used to propagate information to the UI, namely the
main window and the log window. The LogInfo
class is merely a
convenience class
I created to help with data-binding. One
important member here is the mainDispatcher
field which is used to
handle the thread context issues arising from how the events are fired on
secondary threads (and not from the main UI thread where the handlers were
originally setup from).
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>();
The RefreshCommand
property is a command object exposed by the
V-M that basically performs the mail-fetch operation.
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);
}
}
Notice how CanRefresh
delegates the call to
PopClient.IsWorking
which will return true
if a fetch
operation is current under way. I assign the various POP3 properties that are
required for a POP3 connection here, although for this particular demo project
this was actually a mistake to do this here - since it gets called every time
Refresh
is called. Originally I had planned to allow the user to
change POP settings after a fetch, but eventually I ended up showing the
settings dialog only once, at startup. Of course that's a rather insignificant
issue, but I thought I'd mention that here in case someone wonders why I did it
that way. Notice how I save the current Dispatcher
instance in the
mainDispatcher
field, this is what I'll use later to update my
ObservableCollection<>
s because they will be bound to the View and
will thus need to execute on the UI thread.
Cancelling is fairly straightforward.
public ICommand CancelCommand
{
get
{
return cancelCommand ??
(cancelCommand = new DelegateCommand(Cancel, CanCancel));
}
}
public bool CanCancel()
{
return popClient.IsWorking();
}
public void Cancel()
{
popClient.Cancel();
}
There is a potential race condition where CanCancel
returns
true
, but the fetch operation completes before Cancel
is called. But it's safe to call Cancel
even in that scenario, so
there's no need to handle that race condition.
Here's the code that's used to save an attachment (this is accessible via the context menu on attachment icons).
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);
}
}
The Save
method is an extension method I wrote on the
Attachment
class. Here's the code that brings up the Chat-Log window.
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;
}
As you can see it uses the logs
collection, which is populated
via the ChatXXXLog
event handlers.
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);
}
Notice how I use mainDispatcher
to invoke my code on the UI
thread. Here are the event handlers for the QueryPopInfoCompleted
and MailPopped
events.
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);
}
And, here's the event handler for the MailPopCompleted
event.
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();
}
The code here is a better example of how the various exceptions are handled (compared to the code I showed earlier). It demonstrates how the application can handle POP3 server errors and display those messages back to the user.
Class Reference
All public types are in the Extra.Mail
namespace.
PopClient
Class
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
Class
[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
Class
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
Class
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
Class
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
Class
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; }
}
Implementation details
This class is based on the POP3 protocol which is standardized via RFC1939 (Post Office Protocol v3).
One of the design intentions was to hide the implementation details from the
public interface and this meant that I could opt to selectively implement the
minimal protocol needed to fetch email from a POP3 server without losing out on
any POP3 functionality. The internal
class PopConnection
is used to establish a socket connection and to send/receive POP3 commands and
responses from a server. It's a very thin socket based class that uses a
TcpClient
object to communicate with a POP server.
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);
}
Here's a partial list of the SendXXX
-method implementations.
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));
}
It's pretty straightforward socket code, specially since the POP3 protocol is quite uncomplicated. The receive methods are also quite straightforward.
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);
}
Every POP3 command has an associated command object, and all command objects
implement the IPopCommand
interface.
internal interface IPopCommand
{
event EventHandler<PopClientDirectionalLogEventArgs> PopClientLog;
void Execute(params object[] arguments);
bool IsMultiLineResponse { get; }
}
There is an abstract
BasePopCommand
class
that implements some of the common stuff and makes it easy to implement command
classes.
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; }
}
Here are some of the more important method implementations.
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());
}
}
Derived classes now just need to implement ExecuteInternal
and
optionally ParseInternal
(if they need to parse the response
stream). Here's how the User
command is implemented, it's one of
the more simple ones as it does not do custom parsing on the response.
internal class UserPopCommand : BasePopCommand
{
public UserPopCommand(PopConnection popConnection)
: base(popConnection)
{
}
protected override string ExecuteInternal(object argument)
{
return PopConnection.SendUser(argument as string);
}
}
Here's a slightly more involved derived class that implements the Stat
command.
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.");
}
}
}
One of the most important commands is the Retr
command, and
here's how that's been implemented.
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;
}
}
}
Notice the use of the CDOMessageConverter
class. CDO
(Collaboration Data Objects) is a COM component that's been part of Windows OSes
since Windows 2000, and it has built in functionality to parse MIME based email
(including full support for attachments). CDO has no idea about the
MailMessage
class, so I wrote a converter that will take a MIME string,
construct a CDO message from it, and then convert the CDO message into a
MailMessage
object. It's fairly straightforward COM-interop, so I won't
go into the details. The two classes do not have a one-to-one structural
equivalence, so I had to make certain compromises, but as long as I was able to
obtain all fields required to read an email message with attachments, I was not
too concerned about superficial differences between the types.
The PopChat
class provides a single point interface from where
the PopClient
class can invoke POP commands. Here's a snipped code
listing that shows what the class does. In addition to exposing properties for
each command, it also makes sure that all commands have their log events
subscribed to via a common event handler.
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);
}
}
And finally, the actual POP chat is invoked from the BackgroundWorker
thread that's used by the PopClient
class.
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(); } }
Conclusion
If you run into specific issues with a POP server, the chat log (easily accessible via the event handlers) should tell you exactly what's going on. As always, feedback and criticism is welcome, and please feel free to post your comments through the discussion forum for this article. Although if you post something nasty or vote anything less than a 5, I have a Yaqui curse that I recently learned all ready to apply on you! *grin*
History
- November 8th, 2010 - Article first published.
- November 19th, 2010
- Default POP port was incorrectly set to the SMTP port. This is now fixed.
- Added support for skippable UIDLs. This lets you only retrieve mail that was not fetched earlier.
- New command objects added for UIDL and LIST that supplement the existing versions, except that these do an index-less full fetch.