Friday 28 November 2008

POP3 client in C# ...


The other day, I was trying to develop a C# application that can retrieve emails from Gmail via POP3. I searched through various sites for a library that I could use to fetch POP3 emails. I came across a bunch of libraries (along with source code) but unfortunately, none of them worked, 'coz they all don't seem to support SSL, while Gmail POP server requires communication over a secure connection (SSL) !!

After a lot of search in Google, I came across a post in which it is explained that there is an open source project named OpenPOP.Net and that there is a class named PopClient.cs that needs to be modified as explained (the code was put on the site there). I had downloaded the sources, tried compiling OpenPOP.Net with the new class, spent some time fixing the compile time issues since the modified PopClient.cs in turn relies on a third-party library !!

Atlast I have got the sample in OpenPOP.Net working with the modified PopClient.cs and now I am able to fetch messages from Gmail :-)

For the benefit of all those developers, breaking their head trying to read emails from Gmail in C# apps (or any other POP server that requires SSL), here is the library along with one sample Windows application that you can use to test the library.

I request you to please post a comment, if you find the sources helpful and thanks to all the posts on the web that helped me compile the zip that is ready for download

57 comments:

Chris said...

Good god thank you. Was just about to give up! Genius

Pablo said...

Hi buddy,
I tried using the solution you uploaded but I cannot find where is the SSL connection or where may I define it.

Can you please provide a code example?

Thanks!
--Pablo from BA

Kiran banda said...

Hey Pablo,

I believe you have downloaded the zip file mentioned in my post. If you unpack the zip, you will find a folder named POP3Test.Please observe a method "void ReceiveMails()" in frmTest.cs to understand the usage of POP3 library :-)

Feel free to write to me incase you are stuck, shall be more than happy to help you out.

Cheers,

Pablo said...

Yes, I've seen that method but I cannot find where you define or set the connection over SSL because Gmail Pop3 works over secure connection.
Does your code support SSL? Did you add SSL to the original OpenPOP.Net?
Is possible to connect to Gmail Pop3?
Thanks from Argentina!
--Pablo

Kiran banda said...

Hi Pablo,

The code works for SSL too.. I confirm that it works for Gmail (@ port 995) :-)

Give it a try and let me know.

Cheers

had said...

Thanks a lot! I've been looking for this for hours!

techneos said...

the links you have provided for the download are broken

Kiran banda said...

Thanks techneos. I have fixed the broken links. Enjoy the download :-)

Gládiston said...

Congratulations!!! Very Good.

Sarah said...

Thank you a million times over. I have spent close to a day trying to write code (and modify other open source code) to achieve this.

It works beautifully!!!!

Well done, an excellent job. Thank you so much for sharing your hard work.

Sarah

SPAM said...

Do you know how to retrieve all messages, even messages that have been read?

SERGIO T said...

Hi There,
When I try to run the frmMain from MailMonitor project, I get this error: Cross-thread operation not valid: Control 'sbrMain' accessed from a thread other than the thread it was created on.
Any ideas what could be wrong?

Sylwester said...

Hi!
Thanks a lot for really fantastic job! It's my first more important project (POP3 client) and your source was VERY helpful. Greetings from Poland!

no name said...

Thanks for the post. Today I am gonna try out the code. If it worked fine, I will let you know.

BTW does it also download the attachments?


Thanks.

Kiran banda said...

Well, it's been quite sometime since I looked at this,but I am sure you can extend this to download attachments. Should be pretty straight-forward, let me know if you find it cumbersome :-)

Ahmed Essawy said...

great work,thank u :)

Igor said...

Your download links are broken.. :(

Kiran banda said...

Thanks for letting me know.The links are active now :-)

awachtel said...

Great stuff! Thanks for all your hard work on this!

Henrik said...

Great stuff, thanks a lot!
Is there any way of determining the status of the message, for example read/unread?

Kiran banda said...

Henrik,

To my understanding of POP3 protocol, it is not possible to determine the state of a message (read/unread). POP3 Clients have to implement the logic to track read/unread messages :-(

Gypsy said...

Hi, thanks for that... however...
1) I got a compilation error issue
regarding no overload constructore:
Error 1 No overload for method 'Connect' takes '2' arguments D:\Projects\OpenPOP_source_withGmail\OpenPOP_source\MailMonitor\Forms\frmMails.cs 272 5 MailMonitor

which is fine, coz I just add one into PopClient.cs.

public void Connect(string strHost, int intPort)
{
Connect(strHost, intPort, false);
}

2) But interesingly, still no luck... I got this error message:

"A connection attempt failed because the connected party did not properly respond after a period of time, or established connection failed because connected host has failed to respond 74.125.155.109:995"

at PopClients.cs ln 450 - the socket connection throws an exception. :(

Any idea? Thanks a lot.

Kiran banda said...

Gypsy,

Please run the project - POP3Test (set this as start up project). You can safely ignore the build errors that might arise in the project - MailMonitor (honestly, even I didn't try looking into the sources of it ;-)).

dipak said...

i have downloaded it and works fine with gmail but my problem is with gmail all maials are not showing my mailbox has more than 20000 odd messages but i am getting only 405 messages please help me to solve th eissue

Bharathi.Y said...

pls help me with this error....

A connection attempt failed because the connected party did not properly respond after a period of time, or established connection failed because connected host has failed to respond .......

Waiting for your reply....

Kiran banda said...

Hi Bharathi,

Let me know the file you are running and the server you are trying to connect to (I hope it is Gmail).

Regards,

Bharathi.Y said...

Hi Kiran,

Thanks for ur reply :-)

I am running the POP3Test file.
And the server is gmail.

Once i run the file,in the status bar "COMMUNICATION LOST" message is being displayed and after that
" OpenPOP.POP3.PopServerNotFoundException" was thrown.

I hope u will help me with this error.
Thanks ....Bharathi.

Kiran banda said...

Bharathi,

Please check the pop server details - It has to be pop.gmail.com.

Cheers

hurdad said...

Anyone try and compile this in Mono? I am getting a missing crypt32.dll runtime error.

Milan said...

Your project works great! Also I created empty project and referenced dll's from your project and now I have completly independent windows service which fetches emails from gmail! :D Thanx 100x :)
Do you know how to delete messages from gmail inbox folder? I used popClient.DeleteMessage(index) - it sends 'DELE' message to gmail, but it does not work? Bye

Milan said...

I've also set in GMail POP settings for option '2. When messages are accessed with POP' to action 'Delete gmail's copy', also tried 'Archive Gmail's copy' but at no avail... messages are still in inbox after i call delete method from app.

Kiran banda said...

Hi Milan,

I didn't try deleting mails but the issue is with POP3 Protocol. As per the protocol, when you issue a delete command, the messages are marked for deletion but not deleted from server!! You can try configuring your gmail in Thunderbird and see how it strikes off a message when you delete it. I am not sure if I am right, but I have experienced this myself.

Cheers

esi said...

hi kiran
tanks a lot for your good article.
this project recive only unread messages ,
how i can recive unread messages ?
help me please!

Kiran banda said...

ESI,

I presume it gets all the emails by default. Could you please check the message count and get back to me ?

Cheers,
Kiran Banda

rama said...

Hi Kiran,

Thanks a lot..after lot of search i got thisone working perfectly..

but i have problem with attachments.
i am able to get attachments correctly for the mail that is sent from gmail,outlook,yahoo but not getting correct attachments for mails that are sent using SMTP in my application.

Do i need to change anything while sending mail with SMTP? or am i missing something?
new to this email stuff please help me.


Thnanks
Rama

Kiran banda said...

Rama,

Trying sending an email with an attachment via smtp to your gmail or yahoo a/c and see if you can download it properly. If you can, then the Pop3 client should work too :-)

Cheers,
Kiran Banda

girish meena said...

Hi kiran,

In my application user can send mails through any mail account like yahoo, gmail MSN etc to a particular mail id like test@mydomain.com. I have to read these mails and save attachements. I did it with smtpop.it is working fine with yahoo and others who don't have SSL issue, but not reading gmail mails which are sent by users.
what should i do in this case.

I also tried your solution which works fine if i use it to read my gmail account. but in this particular case i have to read mail from id like test@mydomain.com.

So i try with sessting like
host : mail.mydomain.com
user name : test@mydomain.com
password : XYZ
Port : 110 (Which is used by smtpop)

Now i am getting this exception

Exception Details:

Org.Mentalis.Security.Ssl.Shared.SslException: The server hello message uses a protocol that was not recognized.

Please help me out.

Kiran banda said...

Hi Girish,

From what you say - it looks like you are able to read emails from Gmail and not from an account that sounds like abc@company.com !

If I am not wrong, then here is a possible reason why you are not able to read emails from your abc@company.com :

- The implementation in the sample that I have in my blog uses SSL by default. Since Gmail uses SSL too, my program is able to read emails easily.

In your case, you just have to pass "false" as a value to one of the parameters in the code that attempts to retrieve emails :-)

Try it out and let me know.

Regards,
Kiran Banda

girish meena said...
This post has been removed by the author.
girish meena said...
This post has been removed by the author.
girish meena said...
This post has been removed by the author.
Kiran banda said...

Girish,

I hope you are running Pop3Test project in the solution :-)

I tried and it works.

Let me know if you still encounter issues.

Cheers,
Kiran Banda

Rama said...

Kiran,

Now its working. i changed code little bit. i went through the raw messages that are sent using SMTP and generals mail accounts(gmail,outlook). there is a slight difference in the format i think thats why not getting attachments correctly .

it's working perfect..:)

Rama

Kiran banda said...

Thanks Rama,

If you can paste the lines that you changed, it helps those that are suffering from the same error :-)

Regards,
Kiran Banda

Rama said...

Hi,

When you send mails with out attachments, it is splitting the body into two attachments. So i changed that as well for my requirement(not changed getting messagebody for now). I am posting all the code i changed. you may find it helpful

The areas that are chaged or Added are commented with (//Changed,//Added)


***********New Message**************

private bool NewMessage(ref bool blnFinish, string strBasePath, bool blnAutoDecodeMSTNEF, string strMessage, bool blnOnlyHeader)
{
StringReader srdReader=new StringReader(strMessage);
StringBuilder sbdBuilder=new StringBuilder();
_basePath=strBasePath;
_autoDecodeMSTNEF=blnAutoDecodeMSTNEF;

_rawMessage=strMessage;

string strLine=srdReader.ReadLine();
while(Utility.IsNotNullTextEx(strLine))
{
sbdBuilder.Append(strLine + "\r\n");
ParseHeader(sbdBuilder,srdReader,ref strLine); //Changed
if(Utility.IsOrNullTextEx(strLine))
break;
else
strLine=srdReader.ReadLine();
}

_rawHeader=sbdBuilder.ToString();
string temp="";
SetAttachmentBoundry2(_rawHeader, false, ref temp); //Changed

if(_contentLength==0)
_contentLength=strMessage.Length;//_rawMessageBody.Length;

if(blnOnlyHeader==false)
{
_rawMessageBody=srdReader.ReadToEnd().Trim();

//the auto reply mail by outlook uses ms-tnef format
if((_hasAttachment==true && _attachmentboundry!=null)||MIMETypes.IsMSTNEF(_contentType))
{
set_attachments(); //Changed

if (this.Attachments.Count>0)
{
Attachment at=this.GetAttachment(0);
if(at!=null&&at.NotAttachment)
this.GetMessageBody(at.DecodeAsText());
else
{}
//in case body parts as text[0] html[1]
if(this.Attachments.Count>1&&!this.IsReport())
{
at=this.GetAttachment(1);
if(at!=null&&at.NotAttachment)
this.GetMessageBody(at.DecodeAsText());
else
{}
}
}
else
{ }
}
else
{

setBodyAttachment(); //Added
}
}

blnFinish=true;
return true;
}

Rama said...

***********ParseHeader method********
Under case "CONTENT-TYPE" code added and _hasattachment is set to false by default


private void ParseHeader(StringBuilder sbdBuilder,StringReader srdReader,ref string strLine)
{
string []array=Utility.GetHeadersValue(strLine);//Regex.Split(strLine,":");

switch(array[0].ToUpper())
{
case "TO":
_to=array[1].Split(',');
for(int i=0;i<_to.Length;i++)
{
_to[i]=Utility.DecodeLine(_to[i].Trim());
}
break;

case "CC":
_cc=array[1].Split(',');
for(int i=0;i<_cc.Length;i++)
{
_cc[i]=Utility.DecodeLine(_cc[i].Trim());
}
break;

case "BCC":
_bcc=array[1].Split(',');
for(int i=0;i<_bcc.Length;i++)
{
_bcc[i]=Utility.DecodeLine(_bcc[i].Trim());
}
break;

case "FROM":
Utility.ParseEmailAddress(array[1],ref _from,ref _fromEmail);
break;

case "REPLY-TO":
Utility.ParseEmailAddress(array[1],ref _replyTo,ref _replyToEmail);
break;

case "KEYWORDS": //ms outlook keywords
ParseStreamLines(sbdBuilder,srdReader,array[1].Trim(),ref strLine,_keywords);
break;

case "RECEIVED":
ParseStreamLines(sbdBuilder,srdReader,array[1].Trim(),ref strLine,ref _received,true);
break;

case "IMPORTANCE":
_importance=array[1].Trim();
break;

case "DISPOSITION-NOTIFICATION-TO":
_dispositionNotificationTo=array[1].Trim();
break;

case "MIME-VERSION":
_mimeVersion=array[1].Trim();
break;

case "SUBJECT":
case "THREAD-TOPIC":
string strRet=null;
for(int i=1;i').Trim('<');
break;

case "MESSAGE-ID":
_messageID=array[1].Trim().Trim('>').Trim('<');
string[] arr = Regex.Split(_messageID, "@");
_messageID = arr[0].Trim();
break;

case "DATE":
for(int i=1;i1) //here we parse all custom headers
{
string headerName=array[0].Trim();
if(headerName.ToUpper().StartsWith("X")) //every custom header starts with "X"
{
ParseStreamLines(sbdBuilder,srdReader,headerName,array[1].Trim(),ref strLine,_customHeaders);
}
}
break;
}
}


}

Rama said...

**************SetAttachmentBoundary2******************
private void SetAttachmentBoundry2(string strBuffer,bool changeHeader,ref string changedBoundary)
{
changedBoundary = "";
int indexOfAttachmentBoundry2Begin=0;
int indexOfAttachmentBoundry2End=0;
indexOfAttachmentBoundry2Begin=strBuffer.ToLower().IndexOf("Multipart/Alternative".ToLower());
//Added Rama
int indexOfAttachmentBoundry2Begin2 = 0;
indexOfAttachmentBoundry2Begin2 = strBuffer.ToLower().IndexOf("Multipart/Mixed".ToLower());
if(indexOfAttachmentBoundry2Begin!=-1 && changeHeader==false)
{
/* indexOfAttachmentBoundry2Begin=strBuffer.IndexOf("boundary=\"");
indexOfAttachmentBoundry2End=strBuffer.IndexOf("\"",indexOfAttachmentBoundry2Begin+10);
if(indexOfAttachmentBoundry2Begin!=-1&&indexOfAttachmentBoundry2End!=-1)
_attachmentboundry2=strBuffer.Substring(indexOfAttachmentBoundry2Begin+10,indexOfAttachmentBoundry2End-indexOfAttachmentBoundry2Begin-10).Trim();
*/
indexOfAttachmentBoundry2Begin=strBuffer.IndexOf("boundary=");
if(indexOfAttachmentBoundry2Begin!=-1 )
{
int p=strBuffer.IndexOf("\r\n",indexOfAttachmentBoundry2Begin);
string s=strBuffer.Substring(indexOfAttachmentBoundry2Begin+29,4);
indexOfAttachmentBoundry2End=strBuffer.IndexOf("\r\n",indexOfAttachmentBoundry2Begin+9);
if(indexOfAttachmentBoundry2End==-1)
indexOfAttachmentBoundry2End=strBuffer.Length;
_attachmentboundry2=Utility.RemoveQuote(strBuffer.Substring(indexOfAttachmentBoundry2Begin+9,indexOfAttachmentBoundry2End-indexOfAttachmentBoundry2Begin-9));
}
}
else if (indexOfAttachmentBoundry2Begin2 != -1 && changeHeader == true) //Added Rama
{
indexOfAttachmentBoundry2Begin = strBuffer.IndexOf("multipart/mixed; boundary=") + "multipart/mixed; boundary=".Length;
if (indexOfAttachmentBoundry2Begin != -1)
{
indexOfAttachmentBoundry2End = strBuffer.IndexOf("\r\n", indexOfAttachmentBoundry2Begin);
changedBoundary = strBuffer.Substring(indexOfAttachmentBoundry2Begin, indexOfAttachmentBoundry2End-indexOfAttachmentBoundry2Begin);

}
}
else if (changeHeader == true) { }
else
{
_attachmentboundry2 = _attachmentboundry;
}
}

Rama said...

***********Set_Attachments*************
private void set_attachments()
{
int indexOf_attachmentstart=0;
int indexOfAttachmentEnd=0;
bool processed=false;
string changedBoundary = "";
Attachment att=null;

SetAttachmentBoundry2(_rawMessageBody, false, ref changedBoundary); //Changed

while(!processed)
{
if(Utility.IsNotNullText(_attachmentboundry))
{
indexOf_attachmentstart=_rawMessageBody.IndexOf(_attachmentboundry,indexOf_attachmentstart)+_attachmentboundry.Length;
if(_rawMessageBody==""||indexOf_attachmentstart<0)return;

indexOfAttachmentEnd=_rawMessageBody.IndexOf(_attachmentboundry,indexOf_attachmentstart+1);
}
else
{
indexOfAttachmentEnd=-1;
}

//if(indexOfAttachmentEnd<0)return;
if(indexOfAttachmentEnd!=-1)
{
}
else if(indexOfAttachmentEnd==-1&&!processed&&_attachmentCount==0)
{
processed=true;
indexOfAttachmentEnd=_rawMessageBody.Length;
}
else
return;

if(indexOf_attachmentstart==indexOfAttachmentEnd-9)
{
indexOf_attachmentstart=0;
processed=true;
}

string strLine=_rawMessageBody.Substring(indexOf_attachmentstart,(indexOfAttachmentEnd-indexOf_attachmentstart-2));


bool isMSTNEF;
isMSTNEF=MIMETypes.IsMSTNEF(_contentType);
att=new Attachment(strLine.Trim(),_contentType,!isMSTNEF);

//Added
if (changedBoundary == "")
{
SetAttachmentBoundry2(_rawMessageBody, true, ref changedBoundary);
if (changedBoundary != "")
{
indexOf_attachmentstart = _rawMessageBody.IndexOf(changedBoundary) + changedBoundary.Length;
_attachmentboundry = changedBoundary;
}
}

//ms-tnef format might contain multiple attachments
if(MIMETypes.IsMSTNEF(att.ContentType) && AutoDecodeMSTNEF && !isMSTNEF)
{
Utility.LogError("set_attachments():found ms-tnef file");
TNEFParser tnef=new TNEFParser();
TNEFAttachment tatt=new TNEFAttachment();
Attachment attNew=null;

tnef.Verbose=false;
tnef.BasePath=this.BasePath;
//tnef.LogFilePath=this.BasePath + "OpenPOP.TNEF.log";
if (tnef.OpenTNEFStream(att.DecodedAsBytes()))
{
if(tnef.Parse())
{
for (IDictionaryEnumerator i = tnef.Attachments().GetEnumerator(); i.MoveNext();)
{
tatt=(TNEFAttachment)i.Value;
attNew=new Attachment(tatt.FileContent,tatt.FileLength ,tatt.FileName,MIMETypes.GetMimeType(tatt.FileName));
_attachmentCount++;
_attachments.Add(attNew);
}
}
else
Utility.LogError("set_attachments():ms-tnef file parse failed");
}
else
Utility.LogError("set_attachments():ms-tnef file open failed");
}
else
{
_attachmentCount++;
_attachments.Add(att);
}

indexOf_attachmentstart++;
}
}

Rama said...

**************SetBodyAttachment*****************
//Added
private void setBodyAttachment()
{
Attachment att = new Attachment(_rawMessageBody, _contentType,AttachmentBoundry2);
_attachmentCount++;
_attachments.Add(att);

}

//Added Constructor that is called in setBodyAttachment()

public Attachment(string strAttachment, string strContentType, string attBounday)
{
int begin = 0, end = 0, start = 0; ;
_contentType = strContentType;
//StringReader SR = new StringReader(strAttachment);
//string bounday = SR.ReadLine();
begin = start = strAttachment.IndexOf(attBounday);
while (end != -1)
{

end = strAttachment.IndexOf(attBounday, begin + 1);
if (end == -1) break;
else begin = end;

}
end = begin + attBounday.Length + 2;
this._rawAttachment = strAttachment.Substring(start, end - start);
_contentLength = this._rawAttachment.Length;
}

girish meena said...

Hi Kiran,

i have done all this with smtpop.net.

now i can read mail send by users from any email client like yahoo, gmail, outlook etc to a particular email id like test@mydomain.com.

Same functionality is provided by http://posterous.com/ and http://www.peopleofwalmart.com/ blog application.

Rama said...
This post has been removed by the author.
Rama said...
This post has been removed by the author.
Rama said...

Hi Kiran,

I have come across another issue.

i have got suppose 10 users with same strHost,port and useSSL as false.

if i connect and disconnect for each user individually its working but i wanted to connect only once to server(as all users having same details for mail server) and authenticate each user. when i do like this it is giving error invalid login or password.

is it compulsory to connect to mail server for each user?


Thanks
Rama

Rama said...

Hi Kiran,

I have come across another issue.

i have got suppose 10 users with same strHost,port and useSSL as false.

if i connect and disconnect for each user individually its working but i wanted to connect only once to server(as all users having same details for mail server) and authenticate each user. when i do like this it is giving error invalid login or password.

is it compulsory to connect to mail server for each user?


Thanks
Rama

Kiran banda said...

Hi Rama,

To answer your question - "Yes" we need to authenticate each time you want to perform an action on a mailbox ! I am not sure if we are getting any token kind of a thing the first time when we authenticate a user so that we could reuse it instead of authentication continually :-) For now, authentication stands as a prerequisite :-)

Rama said...

Thank you for your response.
i think i have not explained properly.

No problem authenticating user every time. but i want to connect to mail server once and retrieve all the mails for each user nad then disconnect.

Like this.

PopClient.Connect();

For 10 users
PoPClient.Authenticate(user1)
Retrieve mails()
PoPClient.Authenticate(user2)
Retrieve mails()
.
.
.

PoPClient.Disconnect();

if i do like above its giving error invalid username or password.

what i am doing presently

Connect()
Authenticate(user1)
Retrievemails() for user1
Disconnect()

Connect()
Authenticate(user2)
Retrievemails() for user2
Disconnect()

.
.
.
upto 10th user.

for each user connecting & disconnecting to mail server


Hope this makes sense

Rama

moist said...

I get the following error when trying to connect through SSL. Works fine on Win 2003 but doesn't work on Win7 x64. Any ideas?

System.IO.IOException: An I/O exception occurred. ---> Org.Mentalis.Security.SecurityException: An error occurs while communicating with the remote host.

Org.Mentalis.Security.Ssl.Shared.SslException: The pulic key should be at least 512 bits.