5,442,984 members and growing! (18,638 online)
Email Password   helpLost your password?
General Programming » Internet / Network » Email     Intermediate License: The Code Project Open License (CPOL)

A Library for downloading Mails from a POP3 Server

By m@u

Mail downloading with C#.
C#, Windows, .NET 1.1, .NETVisual Studio, VS.NET2003, Dev

Posted: 5 Jun 2005
Updated: 14 Mar 2007
Views: 77,978
Bookmarked: 122 times
Announcements
Want a new Job?



Search    
Advanced Search
Sitemap
36 votes for this Article.
Popularity: 7.15 Rating: 4.59 out of 5
0 votes, 0.0%
1
0 votes, 0.0%
2
2 votes, 5.6%
3
3 votes, 8.3%
4
31 votes, 86.1%
5

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; // 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;
    }
}

BeginWait starts the wait thread. With WaitThread.Join we block the current thread until the line is downloaded completely:

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);
}

StartLFRead reads the data from the server and pulses the Wait object:

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...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();
        // 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 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)
{
    // 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 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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

m@u



Occupation: Software Developer
Location: Switzerland Switzerland

Other popular Internet / Network articles:

Article Top
Sign Up to vote for this article
You must Sign In to use this message board.
FAQ FAQ Noise ToleranceSearch Search Messages 
 Layout  Per page   
 Msgs 1 to 25 of 104 (Total in Forum: 104) (Refresh)FirstPrevNext
Subject  Author Date 
QuestionTimeout on server...memberigor_dev3:37 26 Sep '07  
AnswerRe: Timeout on server...memberm@u3:48 26 Sep '07  
GeneralRe: Timeout on server...memberigor_dev4:30 26 Sep '07  
GeneralRe: Timeout on server...memberm@u4:52 26 Sep '07  
GeneralRe: Timeout on server...memberigor_dev5:14 26 Sep '07  
GeneralRe: Timeout on server...memberigor_dev6:32 26 Sep '07  
GeneralRe: Timeout on server...memberm@u22:29 26 Sep '07  
GeneralRe: Timeout on server...memberigor_dev1:44 27 Sep '07  
GeneralRe: Timeout on server...memberigor_dev6:24 27 Sep '07  
GeneralRe: Timeout on server...memberigor_dev6:29 27 Sep '07  
GeneralRe: Timeout on server...memberm@u3:24 28 Sep '07  
GeneralExecuteCommand COMMENT!!!memberigor_dev4:18 28 Sep '07  
GeneralRe: ExecuteCommand COMMENT!!!memberm@u4:34 28 Sep '07  
GeneralRe: ExecuteCommand COMMENT IN GETMAIL TOmemberigor_dev4:46 28 Sep '07  
GeneralRe: ExecuteCommand COMMENT IN GETMAIL TOmemberm@u6:50 28 Sep '07  
AnswerRe: Timeout on server...memberMember 36687757:02 16 Jun '08  
QuestionHow to decode 8bit of chinese? [modified]memberXu Min0:17 4 Sep '07  
GeneralTHX!!!memberDsh4h6:01 1 Sep '07  
QuestionQuotedPrintable Class Question!!memberXu Min17:03 30 Aug '07  
QuestionHow to list mail again?? [modified]memberXu Min16:13 29 Aug '07  
AnswerRe: How to list mail again??memberXu Min16:22 30 Aug '07  
QuestionHelp to delete emails from the servermemberterryharris16:36 26 Jul '07