Introduction
The .NET Framework does not offer POP3 or MIME support. For those of you who need it, this article provides support for both POP3 and MIME. This article's intent is not to get into the details of POP3 or MIME. Instead the article focuses on how to use the classes I've provided and finally the internals of the classes in case you'd like to do some tweaking. I've successfully tested the classes on both Yahoo and Gmail throughout development.
Usage
Below is an example snippet of code demonstrating the use of the Pop3Client
I created. With the exception of the USER
and PASS
commands, all implemented Pop3Commands
are made available directly from the Pop3Client
class. See below for a demonstration of each implemented method's usage and a brief description regarding the purpose of the method.
using (Pop3Client client = new Pop3Client(PopServer, PopPort, true, User, Pass))
{
client.Trace += new Action<string>(Console.WriteLine);
client.Authenticate();
Stat stat = client.Stat();
foreach (Pop3ListItem item in client.List())
{
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);
MimeEntity entity = client.RetrMimeEntity(item);
client.Dele(item);
}
client.Noop();
client.Rset();
client.Quit();
}
The output of the above code is displayed for a Gmail POP3 account that has only one message in its inbox. In the below screenshot, the top line is the response from the server indicating a successful connection. The following lines starting with USER
until the RETR
request line are the execution of the commands against the POP3 server and the first line of the server response. The lines following the RETR
request are the Console.WriteLine
used for some of the properties of the MailMessageEx
object returned from the RetrMailMessageEx
method. Finally, the request and response results of the DELE
, NOOP
, RSET
, and QUIT
commands are displayed.
Internals
This library supports executing POP3 requests, parsing the POP3 responses as well as parsing the mail messages returned from the RETR
requests into their MIME parts. Internally, the POP3 implementation is made up of various commands, one for each POP3 command and an additional command used to establish the connection with the server. MIME comes into play whenever the RETR
method is executed and the lines which are returned in the RetrResponse
need to be parsed into MIME parts. The MIME part of this library really is only made up of a couple classes, one to read the POP3 lines and parse them into MimeEntity
objects. And, finally a MimeEntity
class which really is a structure containing a collection of headers, some decoded content and the ToMailMessageEx
method used to convert a MimeEntity
into a MailMessageEx
.
POP3
Each POP3 command is represented as a command class inheriting from Pop3Command
. The POP3 commands are all marked internal and are intended only to be executed from within the Pop3Client
class. Internally the Pop3Command
is responsible for ensuring the command is in an executable state, sending the command request, and returning the server response from the command request. Each Pop3Command
subclass is responsible for creating the request message that will ultimately be sent to the server. The Pop3Command
class does encapsulate the creation of a Pop3Response
object representing a simple response from the server. For those commands like RETR
or LIST
which have more complex processing requirements for the parsing of the response message, the CreateResponse
method of the Pop3Command
class is overrideable allowing inheritors to create their own response type and return it instead of a standard Pop3Response
.
Each POP3 command can only be executed in one or more of the following various states based on the POP3 specification, AUTHENTICATION
, TRANSACTION
and UPDATE
. When each Pop3Command
is defined, the POP3 state(s) the command can be executed in are hardcoded into the class via the Pop3Command
classes constructor as seen below in the QuitCommand
class definition.
internal sealed class QuitCommand : Pop3Command<Pop3Response>
{
public QuitCommand(Stream stream)
: base(stream, false, Pop3State.Transaction | Pop3State.Authorization) { }
protected override byte[] CreateRequestMessage()
{
return GetRequestMessage(Pop3Commands.Quit);
}
}
Each Pop3Commands
state is validated using the classes EnsureState
method. The Pop3State
enumeration is defined using the flags attribute allowing the enumeration to be treated as a bit field that can be used in bitwise operations. See the Pop3Command.EnsureState
method below for how the Pop3State
enumeration is used:
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));
}
}
External to the Pop3Command
, the Pop3Client
class provides the current POP3 state to the command objects via the ExecuteCommand
method. The ExecuteCommand
method below is used to execute all commands to enforce consistency in how the commands are handled during execution.
private TResponse ExecuteCommand<TResponse, TCommand>(TCommand command)
where TResponse : Pop3Response where TCommand : Pop3Command<TResponse>
{
EnsureConnection();
TraceCommand<TCommand, TResponse>(command);
TResponse response = (TResponse)command.Execute(CurrentState);
EnsureResponse(response);
return response;
}
Finally the Pop3Response
is created using the CreateResponse
method of the Pop3Command
class. Below is an example from the StatCommand.CreateResponse
method illustrating this scenario returning a custom StatResponse
object to the caller.
protected override StatResponse CreateResponse(byte[] buffer)
{
Pop3Response response = Pop3Response.CreateResponse(buffer);
string[] values = response.HostMessage.Split(' ');
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);
}
If you'd like more information about POP3 please view Post Office Protocol - Version 3 containing a full explanation of the purpose of each command listed above as well as additional commands which were not implemented.
MIME
The MimeReader
class receives a string array of lines into its public constructor which make up the POP3 message. The lines are then stored within a Queue<string>
instance and processed one at a time. The MimeReader
class is responsible for parsing both multipart and singlepart MIME messages into MIME entities consisting of headers and decoded MIME content. The MimeReader
class supports parsing nested MIME entities including those of type message/rfc822. Once the MimeReader
has completed processing of the internet mail message, a MimeEntity
will be returned containing a tree structure containing the contents of the message.
The RetrCommand
overrides the CreateResponse
method and returns a RetrResponse
object containing the lines of the mail message. The Pop3Client
classes RetrMimeEntity
method provides the lines returned as part of the RetrResponse
object to a new instance of the MimeReader
class to parse the messages lines. Finally the MimeReader.CreateMimeEntity
method returns a MimeEntity
instance representing the MimeEntities
contained within the POP3 message. See below for the Pop3Client.RetrMimeEntity
method definition:
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");
}
The MimeReader
creates a new MimeEntity
object and builds a tree of MIME entities using those objects by recursively calling the CreateMimeEntity
method. This process continues until all of the lines for the entire internet mail message have been processed. Below is a snippet of code containing the CreateMimeEntity
method to show what processing takes place in order to create a new MimeEntity
.
public MimeEntity CreateMimeEntity()
{
ParseHeaders();
ProcessHeaders();
.
ParseBody();
SetDecodedContentStream();
return _entity;
}
Parsing the headers really consists of getting name value pairs until a blank line is read. The method somewhat follows the pattern defined by Peter Huber. Based on the MIME spec, we keep reading header lines until we hit the first blank line. When the blank line is encountered, the body of the MIME entity starts and is ready for processing.
private int ParseHeaders()
{
string lastHeader = string.Empty;
string line = string.Empty;
while(_lines.Count > 0 && !string.IsNullOrEmpty(_lines.Peek()))
{
line = _lines.Dequeue();
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;
}
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();
}
return _entity.Headers.Count;
}
Once the headers have been parsed from the MIME entity they need to be processed. If a header is specific to MIME processing, then the header will be assigned to a property on the MIME object. Otherwise the header will be ignored and returned in the headers NameValueCollection
on the MimeEntity
object for later processing. Some of the useful helper methods such as GetTransferEncoding
and GetContentType
the MimeReader
has are displayed below:
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;
}
}
}
Now that the headers are parsed, the body is ready to be parsed for a given MimeEntity
. Recursion takes place when new MimeReader
objects are created by the MimeReader
object while the body parsing is taking place and result in adding the MimeEntity
objects created to the Children
collection of the current MimeEntity
.
private void ParseBody()
{
if (_entity.HasBoundary)
{
while (_lines.Count > 0
&& !string.Equals(_lines.Peek(), _entity.EndBoundary))
{
if (_entity.Parent != null
&& string.Equals(_entity.Parent.StartBoundary, _lines.Peek()))
{
return;
}
if (string.Equals(_lines.Peek(), _entity.StartBoundary))
{
AddChildEntity(_entity, _lines);
}
else if (string.Equals(_entity.ContentType.MediaType,
MediaTypes.MessageRfc822, StringComparison.InvariantCultureIgnoreCase)
&& string.Equals(_entity.ContentDisposition.DispositionType,
DispositionTypeNames.Attachment,
StringComparison.InvariantCultureIgnoreCase))
{
AddChildEntity(_entity, _lines);
break;
}
else
{
_entity.EncodedMessage.Append
(string.Concat(_lines.Dequeue(), Pop3Commands.Crlf));
}
}
}
else
{
while (_lines.Count > 0)
{
_entity.EncodedMessage.Append(string.Concat
(_lines.Dequeue(), Pop3Commands.Crlf));
}
}
}
Once the body has been processed, the only remaining thing to do is write the decoded content to the Content
stream of the MimeEntity
object just prior to returning it. This is done using the SetDecodedContentStream
method.
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;
}
}
Now that the content stream is set, there isn't much more to do besides return the MimeEntity
that has been created. The object returned from this method is ready for processing. But, the MimeEntity
object returned from the CreateMimeEntity
method does not map directly to a MailMessage
. For convenience I added a method allowing a MimeEntity
to be converted into a MailMessage
. Because of the Media Type message/rfc822, I wanted those entities to be pre-parsed and made directly available and ready for use on any MimeEntity
containing a message attachment. To facilitate this, I created a class inheriting from System.Net.Mail.MailMessage
that has a List<MailMessageEx>
property containing a collection of MailMessageEx
objects which are mail message attachments to any message. The MailMessageEx
object is created by calling the MimeEntity.ToMailMessageEx
method.
Conclusion
With this overview of how to use the code and how most of the internals work, you should be well equipped to make use of these classes and make changes or additions where necessary which should allow you to easily incorporate them into your codebase. This library handles the POP3 and MIME protocols and wraps them both up in an easy to use class. MIME messages are ultimately parsed into MailMessageEx
objects so that attachments and email body text can be easily accessed.
References
There are many articles already written dealing with both POP3 and MIME. Below are a couple great implementations I reviewed prior to starting my article. I was able to make use of ideas presented in a both articles and did my best to document whenever I used any of those ideas directly without significant modification.
History
- 2008.02.08
- Fixed issue found by Shawn Cook where the
Body
property was empty.
- 2008.02.07 Minor bug fix
RetrResponse
issue whenever host GMail returns 3 part host message as first line of response.
- 2008.02.05 Bug Fixes and additional command
- Various minor bug fixes
- Fixed issue found when parsing GMail headers by D I Petersen. This was caused by an empty /
null
header. - Made changes to the way handling takes place when no response is received from the POP3 server. This fix addresses the issue found by zlezj whenever the size of the buffer lands in the middle of a line terminator.
- Changed the disconnect to leave the
TcpClient
alone so it can be reused for subsequent POP3 requests. - Added
Top
command to add support for the non-standard POP3 TOP
command providing the ability to download message headers instead of the entire message.
- 2007.11.20 Initial post