This is number four in a series of posts on developing a POP3 client in C#. These are the previous ones:
- Developing a C# POP3 client: Part 01 - POP3 commands
- Developing a C# POP3 client: Part 02 - The SimplePop3Client
- Developing a C# POP3 client: Part 03 - Session States, Exception Handling, SSL
New methods for easier handling
The POP3 client works quite well for now and it could also be used in other projects. But it's handling is a bit cumbersome. Normally you don't want to handle each POP3 command and then parse it's response. So why not introduce some methods to simplify the common actions as Login() for USER() and PASS().
It also makes more sense to return a boolean value instaed of the original server response for commands like USER, PASS, NOOP, DELE and QUIT. Since there are many checks, if the response starts with "+OK" or "-ERR", I used two extensions methods which are extremely simple (I know) but very handy.
public static bool IsOK(this string s)
{
return s.StartsWith("+OK");
}
public static bool IsErr(this string s)
{
return s.StartsWith("-ERR");
}
Login methods
There are several methods for logging in to the mail server. They are really simple therefor I'll show you only the most advanced here.
public bool Login(string username, string password, bool useApop)
{
bool loggedin = false;
if (useApop && this.ApopTimestamp.IsNotNullOrEmpty()) //IsNotNullOrEmpty() is an extension method
{
// use apop login
if (APOP(username, password).IsOK())
{
loggedin = true;
}
}
else
{
// use user/pass login
if (USER(username).IsOK())
{
if (PASS(password).IsOK())
{
loggedin = true;
}
}
}
return loggedin;
}
Because we implemented the session state handling last time, we don't have to set a LoggedIn property something similar. All that is done in the standard POP3 command methods.
Messages
Getting messages is as simple as calling the RETR() method. The headers of a message can be retrieved with TOP(messageNumber, 0). The size we get with LIST(messageNumber) and the unique id with UIDL(messageNumber). DELE(messageNumber) for deleting a message from the server is also pretty easy. So there's nothing to it! But there is something to keep in mind when downloading messages.
Download a message is easy but...
As I said above this is a really simple one: RETR(messageNumber) and you get a string with the message. The only little problem is, that it's the raw text of (normally) a MIME message (MIME = Multipurpose Internet Mail Extension). And that's the really hard part of developing a useful POP3 client: parsing the MIME message. And that will be the topic of my next post in this series.
To delete or not to delete...
It may depend on the mail server's configuration but usually messages that were downloaded are deleted when QUIT command is sent. Thus I introduced a property called "DeleteMessagesAfterDownload" which indicates whether this should be done or not. To prevent the automatic deletion on the server the RSET command can be sent befor quitting. So far, so good. But with sending RSET to the server not only the state of the downloaded messages is resetted but the whole session. This means that also messages marked for deletion with the DELE command will be unmarked.
To solve this there's actually only one possiblity: Remember which messages must be deleted, do a reset, delete the messages again and then quit the session.
// ...
private List<int> deletedMessageNumbers= new List<int>;
// ...
public bool DeleteMessage(int messageNumber)
{
if (DELE(messageNumber).IsOK())
{
deletedMessageNumbers.Add(messageNumber);
return true;
}
else
return false;
}
public bool Disconnect()
{
if (!DeleteMessagesAfterDownload)
{
RSET();
foreach (int messageNumber in this.deletedMessageNumbers)
{
DELE(messageNumber);
}
}
else
{
foreach (int messageNumber in this.downloadedMessageNumbers)
{
DELE(messageNumber);
}
}
return QUIT().IsOK();
}
Mailbox stats
In this version of the Pop3Client I implemented two more properties:
The Mailbox property contains values for the numbers of messages on the server and the total size of all messages. These values are retrieved right after login using the method STAT().
Messages is a collection of MessageInfo objects which have three properties: MessageNumber, Size, UniqueID. These values are also updated immediately after the successful login. This collection is mainly useful if want to use the unique id of the messages to retrieve or delete them. And that's no bad idea because this is the only clue we have about a specific message. Messabe numbers will change after disconnecting from the server, unique ids won't. For the messages info first the LIST command is sent. After storing these values for each entry the GetUid(messageNumber) method is called which uses the UIDL <messageNumber> command. I first tried to make two collections - one with message numbers and sizes and one with message numbers and unique ids - and then to combine them. But funny enough that took twice as much time.
private void UpdateMailboxStats()
{
string response = "";
bool success = SendCommand("STAT");
if (success && (response = GetSingleLineResponse()).IsOK())
{
int numberOfMessages = -1;
int mailboxSize = -1;
string[] values = response.Split(' ');
int.TryParse(values[1], out numberOfMessages);
int.TryParse(values[2], out mailboxSize);
this.Mailbox = new MailboxInfo(numberOfMessages, mailboxSize);
return true;
}
else
{
this.Mailbox = new MailboxInfo(-1, -1);
return false;
}
}
public bool UpdateMessagesInfo()
{
bool messagesUpdated = false;
MessageInfoCollection messagesInfo = new MessageInfoCollection();
string response = "";
// get message numbers and message sizes
bool success = SendCommand("LIST");
if (success && GetSingleLineResponse().IsOK())
{
while (ProcessMultiLineResponse(out response))
{
int messageNumber = -1;
int messageSize = -1;
string[] values = response.Split(' ');
int.TryParse(values[0], out messageNumber);
int.TryParse(values[1], out messageSize);
messagesInfo.Add(new MessageInfo(messageNumber, messageSize, ""));
}
foreach (MessageInfo mi in messagesInfo)
{
mi.UniqueID = GetUid(mi.MessageNumber);
}
this.messages = messagesInfo;
messagesUpdated = true;
}
else
{
this.messages = messagesInfo;
}
return messagesUpdated;
}
public bool Login(string username, string password, bool useApop)
{
//...
if (loggedin)
{
UpdateMailboxStats();
UpdateMessagesInfo();
}
return loggedin;
}
Events
Perhaps it's useful sometime to get information about session state changes. So I introduced two events: SessionStateChanging (right before the session state is changed) and SessionStateChanged (after the session state has changed).
//...
public event Pop3EventHandler SessionStateChanging;
private void OnSessionStateChanging(Pop3EventArgs e)
{
if (SessionStateChanging != null)
SessionStateChanging(this, e);
}
public class Pop3EventArgs : EventArgs
{
public Pop3Client.Pop3SessionState SessionState { get; set; }
public Pop3EventArgs(Pop3Client.Pop3SessionState currentState)
{
this.SessionState = currentState;
}
}
//...
private void SetSessionState(Pop3SessionState state)
{
OnSessionStateChanging(new Pop3EventArgs(this.State));
this.State = state;
OnSessionStateChanged(new Pop3EventArgs(this.State));
}
Code download
Grab the code, test it, look at it and write a comment on what you think about: Pop3Client.zip (9.28 kb)
c6370bda-78ee-4df9-9023-305d04bd52a6|2|5.0