Introduction
For the project I'm working on, I needed a library that could download emails from a POP-server, extract the attachments, and do some tasks depending on the sender. I decided to do it in two steps. The first step involves downloading of the emails and wrapping them in an object that returns the data you need. The second step would be a service that takes the POP-mail-objects and performs tasks with them depending on a rule defined by me. This article is about the first part, downloading and wrapping of an email so that another application can work with it in an easy way.
Background
Before I could start, I had to find answers to these essential questions:
- I can decode Base64, but what if the encoding is Quoted Printable?
- How an email is typically formatted? (How does the MIME - format work?)
- How can I talk to a POP-server?
- How to connect to a POP-Server that only supports Secure-Pop?
After I had found answers to these questions, in RFC 1521 (MIME) by using Telnet (POP-server) and on the ASP emporium (Quoted Printable class), I finally started with my library. And by using the SecLib from Mentalis.Org, i was also able to implement SecurePop.
The library
The most important part of the library is the class POPHandler. This class logs on to the server, retrieves the list of mails, loads them down and finally deletes them:
public class PopHandler
{
private NetworkTalker Talker;
private Thread WaitThread; private AsyncState State; private ArrayList MailList;
private object tLock = new object();
All the commands on the server are executed in the same way. Let's take a look at the constructor, where we log on to the server, to understand how it works:
public PopHandler(string Servername, int Port,
string UserName, string Password, bool useSecurity)
{
Talker = new NetworkTalker(Servername,Port,useSecurity);
Initialize(UserName, Password);
}
private void initialize(string UserName, string Password)
{
State = new AsyncState();
State.Client = Talker;
State.Stream = Talker.GetStream();
BeginWait();
StartLFRead();
WaitToEnd();
if (State.Data.StartsWith("+"))
{
State.Data = "";
LogOn(UserName, Password);
}
else
{
Exception up = new Exception("Error! Connect to Server Failed!" +
State.Data);
State.Data = "";
throw up;
}
}
BeginWait starts the wait thread. With WaitThread.Join we block the current thread until the line is downloaded completely:
private void BeginWait()
{
WaitThread = new Thread(new ThreadStart(Waiter));
WaitThread.Start();
System.Threading.Thread.Sleep(100);
}
private void Waiter()
{
Monitor.Enter(tLock);
Monitor.Wait(tLock);
Monitor.Exit(tLock);
}
StartLFRead reads the data from the server and pulses the Wait object:
private void StartLFRead()
{
State.Stream.BeginRead(State.Buffer,0,State.Buffer.Length,
new AsyncCallback(ReadToCRLF),State);
}
private void ReadToCRLF(IAsyncResult Result)
{
try
{
AsyncState stt = (AsyncState)Result.AsyncState;
int dl = stt.Stream.EndRead(Result);
if (dl != 0)
{
stt.Data +=
System.Text.Encoding.Default.GetString(stt.Buffer,0,dl);
if (stt.Data.EndsWith("\r\n"))
{
Monitor.Enter(tLock);
Monitor.Pulse(tLock);
Monitor.Exit(tLock);
}
else
{
stt.Stream.BeginRead(stt.Buffer,0,State.Buffer.Length,
new AsyncCallback(ReadToCRLF),stt);
}
}
else
{
Monitor.Enter(tLock);
Monitor.Pulse(tLock);
Monitor.Exit(tLock);
stt.Stream.Close();
stt.Client.Close();
stt.Writer.Close();
}
}
catch
{
Monitor.Enter(tLock);
Monitor.Pulse(tLock);
Monitor.Exit(tLock);
}
}
Now, there's only one method left...LogOn():
private void LogOn(string UserName, string Password)
{
ExecuteCommand(string.Format("USER {0}", UserName));
ExecuteCommand(string.Format("PASS {0}",Password));
}
LogOn calls a method that works similar to the piece of code we've seen in the constructor. It calls an async method, and blocks the calling thread until the async operation is finished:
private string ExecuteCommand(string Command)
{
return ExecuteCommand(Command,true);
}
private string ExecuteCommand(string Command,bool ClearAtEnd)
{
bool ServerError = false;
string RetVal = "";
try
{
BeginWait();
State.Writer.WriteLine(Command);
StartLFRead(); State.Writer.Flush();
WaitThread.Join(); RetVal = State.Data;
if (State.Data.StartsWith("-"))
{
Exception up = new Exception(State.Data);
State.Data = "";
ServerError = true;
throw up;
}
if (ClearAtEnd)
{
State.Data = "";
}
}
catch
{
if (!ServerError)
{
Monitor.Enter(tLock);
Monitor.Pulse(tLock);
Monitor.Exit(tLock);
}
throw;
}
return RetVal;
}
The code for downloading the email is almost the same. The only difference is that it downloads 500 byte fragments during each async call, and it waits for "\r\n.\r\n" instead of "\r\n".
Now, we can get the source code of an email with this class. But, maybe you don't feel like parsing MIME in your application. If you download an email with POPHandler, you will get a POP-Mail-Object back that wraps the email and splits it into several parts and decodes them:
public PopMail(string SourceCode)
{
boundaries = new ArrayList();
source = SourceCode;
SourceCode = SourceCode.Replace("\r","");
src = SourceCode.Split('\n');
ParseMail();
}
private void ParseMail()
{
bool Hdr = true; Headers = new ArrayList();
AddHead();
for (int i = 0; i< src.Length-2; i++)
{
if (!Hdr)
{
if (!(GetBoundary(src[i].ToLower()) != -1 & mpmessage))
{
head.AppendSourceLine(src[i]);
}
else
{
if (src[i].ToLower() == "--"+ ((Boundary)
boundaries[GetBoundary(src[i].ToLower())]).Text.ToLower())
{
head.LockParagraph();
AddHead();
i = GetNextNotEmptyLine(i);
Hdr = true;
}
else if (src[i].ToLower() == "--"+ ((Boundary)
boundaries[GetBoundary(src[i].ToLower())]).Text.ToLower()+"--")
{
if (((Boundary)
boundaries[GetBoundary(src[i].ToLower())]).Type ==
BoundaryTypes.Multipart_Mixed)
{
break;
}
else
{
head.LockParagraph();
boundaries.RemoveAt(GetBoundary(src[i].ToLower()));
i = GetNextNotEmptyLine(i);
}
}
}
}
else
{
if (src[i] != "")
{
if (!char.IsWhiteSpace(src[i][0]))
{
if (!src[i].ToLower().StartsWith("subject:"))
{
string[] prp = ExtractProperty(src[i]);
head.AddProperty(prp[0],ParseSubject(prp[1]));
}
else
{
head.AddProperty("subject",
ParseSubject(ExtractProperty(src[i])[1]));
}
}
else
{
head[head.PropertyCount-1].AddSubValue(src[i].Trim());
}
}
else
{
Hdr = false;
try
{
if (head["content-type"].Value.ToLower().StartsWith(
"multipart"))
{
mpmessage = true;
MPProperty mp = head["content-type"];
for (int a = 0; a< mp.SubItemCount; a++)
{
if (mp[a].ToLower().StartsWith("boundary"))
{
int start = mp[a].IndexOf("\"",0);
int end = mp[a].IndexOf("\"",start+1);
BoundaryTypes tt =
(mp.Value == "multipart/mixed")?
BoundaryTypes.Multipart_Mixed:
BoundaryTypes.Multipart_Alternative;
boundaries.Add(new
Boundary(mp[a].Substring(start+1,
end-(start+1)),tt));
break;
}
}
}
}
catch
{
mpmessage = false;
}
}
}
}
head.LockParagraph(); }
That's the most important part of mail parsing. The source code in the zip is not well documented, but in most cases you will be able to see immediately what I'm trying to do.
Using the code
The usage of the code is quite simple:
private void blah()
{
PopHandler foo =
new PopHandler("YourServer",110,"YourName","YourPass");
string[] list = foo.GetList();
for (int i = 0; i< list.Length; i++)
{
Console.WriteLine("Getting Mail {0}: {1}",i+1,list[i]);
PopMail Mail = foo.GetMail(i);
Console.WriteLine(
@"Sender: {0} Subject: {1} Number of Parts: {2}",
Mail[0]["From"],Mail[0]["Subject"], Mail.ParagraphCount);
for (int a = 1; a < Mail.ParagraphCount; a++)
{
MPProperty CT;
MPProperty CD;
try
{
CT = Mail[a]["Content-Type"];
}
catch
{
Console.WriteLine("No Content-Type");
CT = null;
}
try
{
CD = Mail[a]["Content-Disposition"];
}
catch
{
Console.WriteLine("no Content-Disposition");
CD = null;
}
if (CD != null)
{
if (CD.Value.ToLower() == "attachment")
{
System.IO.FileStream FS =
new System.IO.FileStream(string.Format(@"C:\{0}",
GetFilename(CD)),System.IO.FileMode.Create);
FS.Write(Mail[a].DecodedData,0,
Mail[a].DecodedData.Length);
FS.Close();
}
}
else
{
if (CT != null)
{
if (GetFilename(CT) != "")
{
System.IO.FileStream FS =
new System.IO.FileStream(string.Format(@"C:\{0}",
GetFilename(CT)),System.IO.FileMode.Create);
FS.Write(Mail[a].DecodedData,0,
Mail[a].DecodedData.Length);
FS.Close();
}
}
}
}
}
}
private string GetFilename(MPProperty Prop)
{
string[] Splitted;
string RetVal = "";
for (int i= 0; i< Prop.SubItemCount; i++)
{
Splitted = Prop[i].Split('=');
if (Splitted.Length == 2)
{
if (Splitted[0].ToLower() ==
"filename" | Splitted[0].ToLower() == "name")
{
RetVal = Splitted[1];
}
}
}
return RetVal;
}
This example extracts all the attachments of a mail and saves them at c:\ + name of attachment.
Credits
- Special thanks go to Bill Gerhart for his great Quoted Printable class.
- and "Thank you" to rokma95 for fixing bug in the
GetList() method.
- msaguiar for his fix of the Problems with empty Fields in Mail-Parsing
Updates
- 09/14/2005
- Update of the library: The library will not hang anymore while executing commands or downloading mails. Now, there's a demo application available.
- 10/07/2005
- Update of the library: The library is now able to get the list of mails on all servers.
- 05/04/2006
- Update of the library and the demo project: It's now possible to download only the header of the e-mail. Some bugs are also fixed.
- 07/14/2007
- Fixed some Bugs
- Implemented
SecurePop