|
|||||||||||||||||||||
|
|||||||||||||||||||||
|
Announcements
Want a new Job?
Chapters
Services
Feature Zones
|
IntroductionFor 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. BackgroundBefore I could start, I had to find answers to these essential questions:
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 libraryThe most important part of the library is the class public class PopHandler
{
private NetworkTalker Talker; // A Connection-Wrapper - Object that
// opens a Normal or an SSL Connection
// depending on Constructor Parameters
private Thread WaitThread; // This Thread blocks the Calling thread
// until an async Task is done
private AsyncState State; // State objecect for Async Tasks
private ArrayList MailList; // List of available Mails
// the object, WaitThread is waiting for.
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)
{
// Connect to the Server First.
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;
}
}
private void BeginWait()
{
// Start the Thread..
WaitThread = new Thread(new ThreadStart(Waiter));
WaitThread.Start();
System.Threading.Thread.Sleep(100);
}
private void Waiter()
{
// Wait until someone pulses the tLock - Object
Monitor.Enter(tLock);
Monitor.Wait(tLock);
Monitor.Exit(tLock);
}
private void StartLFRead()
{
//Start the aync Function for Reading Data..
State.Stream.BeginRead(State.Buffer,0,State.Buffer.Length,
new AsyncCallback(ReadToCRLF),State);
}
private void ReadToCRLF(IAsyncResult Result)
{
try
{
AsyncState stt = (AsyncState)Result.AsyncState;
// Get the Lenght of Data that was received
int dl = stt.Stream.EndRead(Result);
if (dl != 0)
{
// Convert the bytes to a string and
// add it to the buffered Data in the
// Async State object.
stt.Data +=
System.Text.Encoding.Default.GetString(stt.Buffer,0,dl);
// if the line is complete inform the calling
// thread by pulsing the object
// otherwise read again..
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
{
// 0 bytes normally meansy loss of connection.
// in this case we should inform the calling thread.
Monitor.Enter(tLock);
Monitor.Pulse(tLock);
Monitor.Exit(tLock);
stt.Stream.Close();
stt.Client.Close();
stt.Writer.Close();
}
}
catch
{
// if an error occured return to the calling thread.
Monitor.Enter(tLock);
Monitor.Pulse(tLock);
Monitor.Exit(tLock);
}
}
Now, there's only one method left... private void LogOn(string UserName, string Password)
{
ExecuteCommand(string.Format("USER {0}", UserName));
ExecuteCommand(string.Format("PASS {0}",Password));
}
private string ExecuteCommand(string Command)
{
return ExecuteCommand(Command,true);
}
private string ExecuteCommand(string Command,bool ClearAtEnd)
{
bool ServerError = false;
string RetVal = "";
try
{
BeginWait();
// Send the command to the Server
State.Writer.WriteLine(Command);
StartLFRead(); // Setup the Read - function
State.Writer.Flush();
WaitThread.Join(); // Wait for the Data...
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 public PopMail(string SourceCode)
{
// The user of this class should be able to parse the mime
// by himself if he wants to.. so we save the unchanged
// source of the E-Mail as well.
boundaries = new ArrayList();
source = SourceCode;
//Remove all "\r"'s and split the Mail - Data
//into lines by splitting with "\n"
SourceCode = SourceCode.Replace("\r","");
src = SourceCode.Split('\n');
ParseMail();
}
private void ParseMail()
{
bool Hdr = true; // at the beginning of a Mail there
// must be a Header.. so Hdr is true..
Headers = new ArrayList();
AddHead();
// the last 2 Lines are ".", ""
for (int i = 0; i< src.Length-2; i++)
{
if (!Hdr)
{
// if the current Line does not mark a Boundary,
// we add it to the Header - MailPart
if (!(GetBoundary(src[i].ToLower()) != -1 & mpmessage))
{
head.AppendSourceLine(src[i]);
}
else
{
// if the Current line is a Begin - Boundary,
if (src[i].ToLower() == "--"+ ((Boundary)
boundaries[GetBoundary(src[i].ToLower())]).Text.ToLower())
{
// we lock the Mail - Head and Add a new one,
// and move to the next Not Empty Line.
head.LockParagraph();
AddHead();
i = GetNextNotEmptyLine(i);
Hdr = true;
}
else if (src[i].ToLower() == "--"+ ((Boundary)
boundaries[GetBoundary(src[i].ToLower())]).Text.ToLower()+"--")
{
// if the Current line is an End - Boundary,
if (((Boundary)
boundaries[GetBoundary(src[i].ToLower())]).Type ==
BoundaryTypes.Multipart_Mixed)
{
// we check first, if it's the
// End-Of-Multipart-Message Boundary, which would mean
// we're done.
break;
}
else
{
// if we're not done, we lock the current head,
// remove the open boundary..
// and move to the next not empty line.
head.LockParagraph();
//AddHead();
boundaries.RemoveAt(GetBoundary(src[i].ToLower()));
i = GetNextNotEmptyLine(i);
//Hdr = true;
}
}
}
}
else
{
//normally there's a blank line between the
//header and the body of a MIME - Part..
if (src[i] != "")
{
// if the current line starts with a whitespace
// (normally tab or space), it's a subitem
// of the previous header...
if (!char.IsWhiteSpace(src[i][0]))
{
// otherwise it's a normal header
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
{
//after the complete head is parsed,
//look if it's a multipart message
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(); //lock the current head.
}
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 codeThe 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
Updates
| ||||||||||||||||||||