Click here to Skip to main content
Click here to Skip to main content

A Library for downloading Mails from a POP3 Server

, 14 Mar 2007
Rate this:
Please Sign up or sign in to vote.
Mail downloading with C#.

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
Software Developer
Switzerland Switzerland
No Biography provided

Comments and Discussions

 
QuestionUpdate: new Bugfixed Version PinmemberThomas Haller4-Oct-12 3:35 
Questionsome contribution [modified] PinmemberThomas Haller27-Jun-11 5:07 
QuestionHow to Read folder lik "junk" or any customized folder on mailserver? Pin Pin Pinmemberjymitra19-Jul-10 21:47 
AnswerRe: How to Read folder lik "junk" or any customized folder on mailserver? Pin Pin Pinmemberm@u19-Jul-10 22:34 
GeneralRe: How to Read folder lik "junk" or any customized folder on mailserver? Pinmemberjymitra19-Jul-10 23:11 
hello
thnk u very much for this information ,if u hv any reference post or code reference ?if u hv n can share then email me ..please..
Generaldownloading attachments from forwarded emails Pinmembermaciaszek28-Nov-09 1:49 
GeneralDeleting mails Pinmemberryanc23-Nov-09 22:17 
Generalproblem while building the project with HMAC PinmemberRamsin2-Jul-09 14:20 
GeneralRe: problem while building the project with HMAC Pinmemberm@u5-Jul-09 21:47 
QuestionTimeout on server... Pinmemberigor_dev26-Sep-07 2:37 
AnswerRe: Timeout on server... Pinmemberm@u26-Sep-07 2:48 
GeneralRe: Timeout on server... Pinmemberigor_dev26-Sep-07 3:30 
GeneralRe: Timeout on server... Pinmemberm@u26-Sep-07 3:52 
GeneralRe: Timeout on server... Pinmemberigor_dev26-Sep-07 4:14 
GeneralRe: Timeout on server... Pinmemberigor_dev26-Sep-07 5:32 
GeneralRe: Timeout on server... Pinmemberm@u26-Sep-07 21:29 
GeneralRe: Timeout on server... Pinmemberigor_dev27-Sep-07 0:44 
GeneralRe: Timeout on server... Pinmemberigor_dev27-Sep-07 5:24 
GeneralRe: Timeout on server... Pinmemberigor_dev27-Sep-07 5:29 
GeneralRe: Timeout on server... Pinmemberm@u28-Sep-07 2:24 
GeneralExecuteCommand COMMENT!!! Pinmemberigor_dev28-Sep-07 3:18 
GeneralRe: ExecuteCommand COMMENT!!! Pinmemberm@u28-Sep-07 3:34 
GeneralRe: ExecuteCommand COMMENT IN GETMAIL TO Pinmemberigor_dev28-Sep-07 3:46 
GeneralRe: ExecuteCommand COMMENT IN GETMAIL TO Pinmemberm@u28-Sep-07 5:50 
AnswerRe: Timeout on server... PinmemberMember 366877516-Jun-08 6:02 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web04 | 2.8.140709.1 | Last Updated 15 Mar 2007
Article Copyright 2005 by m@u
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid