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

SMTP Client with SSL/TLS

By , , 6 May 2013
 

Introduction

I needed to send emails in a product written in C++, so I searched the Internet and found a great article: SMTP Client written by Jakub Piwowarczyk. However, many of my clients use SMTP servers that require secure connection (TLS or SSL), and the SMTP Client does not support it. So I had to add SSL/TLS support to the CSmtp class from SMTP Client before I could use it in my product. As I was new to SSL/OpenSSL, it did take me quite some time to learn how to use it properly, and to make the code work with several popular SMTP servers. I have also seen people searching the internet looking for a C++ implementation of SMTP/SSL/TLS, but just could not find one. So I decided to share the one I wrote, in the hope that it will save people who are not familiar with SSL some time.

Note that this article does not cover the details of SMTP. Please go to the original article SMTP Client if you need to know more about SMTP.

Background

There are 2 kinds of secure connections for SMTP, one is SSL and the other is TLS. Some SMTP servers support only one kind and some support both. Generally speaking, the port for SSL is 465, and the port for TLS is 587, but this is not always the case. In addition to the ports being different, SMTP/SSL is different than SMTP/TLS in that, SMTP/SSL negotiates an encrypted connection directly after the underlying TCP connection has been established, while SMTP/TLS requires that the client send a STARTLS command to the server before they negotiate an encrypted connection.

The steps involved in SMTP/SSL are as follows:

  1. The client connects to the server using TCP.
  2. The client negotiates an encrypted connection with the server.
  3. The server sends a welcome message using the encrypted connection to the client.
  4. The client sends a EHLO command using the encrypted connection to the server.
  5. The server responds to the EHLO command using the encrypted connection.

The steps involved in SMTP/TLS are as follows:

  1. The client connects to the server using TCP.
  2. The server sends a welcome message using the un-encrypted connection to the client.
  3. The client sends a EHLO command using the un-encrypted connection to the server.
  4. The server responds to the EHLO command using the un-encrypted connection.
  5. The client sends a STARTTLS command using the un-encrypted connection to the server.
  6. The server responds to the STARTTLS command using the un-encrypted connection.
  7. The client negotiates an encrypted connection with the server.
  8. The client sends a EHLO command using the encrypted connection to the server.
  9. The server responds to the EHLO command using the encrypted connection.

Using the Code

I have used openssl (http://www.openssl.org) in the sample code. The directory "openssl-0.9.8l" in the sample code contains all the necessary header files and the two pre-built static openssl libraries. If you would also like to use this version of openssl in your code, be sure to copy the entire "openssl-0.9.8l" directory to the root directory of your project and add "openssl-0.9.8l\inc32" to "Additional Include Directories", and also add "openssl-0.9.8l\out32" to "Additional Library Directories".

If you would like to build your own openssl, please refer to http://www.openssl.org for detailed instructions.

#define test_gmail_tls
    CSmtp mail;
#if defined(test_gmail_tls)
    mail.SetSMTPServer("smtp.gmail.com",587);
    mail.SetSecurityType(USE_TLS);
#elif defined(test_gmail_ssl)
    mail.SetSMTPServer("smtp.gmail.com",465);
    mail.SetSecurityType(USE_SSL);
#elif defined(test_hotmail_TLS)
    mail.SetSMTPServer("smtp.live.com",25);
    mail.SetSecurityType(USE_TLS);
#elif defined(test_aol_tls)
    mail.SetSMTPServer("smtp.aol.com",587);
    mail.SetSecurityType(USE_TLS);
#elif defined(test_yahoo_ssl)
    mail.SetSMTPServer("plus.smtp.mail.yahoo.com",465);
    mail.SetSecurityType(USE_SSL);
#endif
    mail.SetLogin("***");
    mail.SetPassword("***");
    mail.SetSenderName("User");
    // ......
    mail.Send();

If you use a non-privileged user account to test Yahoo, the mail will fail to send. And the error message returned by the Yahoo SMTP server is "530 Access denied: Free users cannot access this server".

Notes

  • The code does not verify the server's identity, that is, it does not check the server's certificate. This is usually not a big problem if we make sure we feed the program with correct server addresses. However, it is still worth mentioning that there is the chance that we are talking to an impersonator if we don't check the certificate.
  • You are not allowed to use the code in this article for spamming.

References

History

  • Rev 2.2, 2013/05/06

Thanks to everyone for the effective crowdsourcing!  Please keep up the improvements to our library!

  • Added fix contributed GKarRacer for the improper checking of the MsgBody.size() before working with a line of the message body.
  • Moved memory allocation and checking if all attachments can be opened to before the MAIL command is issued so that if there is an issue with one of them, sending the email can be gracefully aborted without the email being sent without the attachments.
  • Changed all the sprintf commands to snprintf to add greater security.  #define'd snprintf to sprintf_s for MS Visual C.  Also changed most strcpy calls to snprintf since the MS Visual C version of that function strcpy_s has the arguments re-ordered so it wouldn't be possible to use it without affecting portability.
  • Added fix contributed by jcyangzh about a possible infinite loop in the SayQuit function.
  • Added fixes contributed by sbrytskyy required to make the AUTH PLAIN login work properly.
  • Rev 2.1, 2012/11/06
  • Rev 2.0, 2011/06/23
    • Added the m_bAuthenticate member variable to be able to disable authentication even though it may be supported by the server. It defaults to true so if it is not set, the library will act as it would have before the addition.
    • Added the ability to pass the security type, m_type, the new m_Authenticate flag, the login and password into the ConnectRemoteServer function. If these new arguments are not included in the call, the function will work as it did before.
    • Added the ability to pass the new m_Authenticate flag into the SetSMTPServer function. If not provided, the function will act as it would before the addition.
    • Added fixes contributed by Martin Kjallman
    • Added fixes contributed by Karpov Andrey
    • Added fixes contributed by Jakub Piwowarczyk
  • Rev 1.9, 2010/08/19
    • Added PLAIN, CRAM-MD5 and DIGESTMD5 authorization.
    • Added a DisconnectRemoteServer() function and reconfigured the Send() function such that if you have already called the ConnectRemoveServer() function, it will use the existing connection and leave the connection open. This allows you to call ConnectRemoteServer(), then send multiple messages on the same connection. If this approach is used, you have to call DisconnectRemoteServer() to close the connection. If you call Send() without calling ConnectRemoteServer(), it will close the connection after sending. This change should be fully backward compatible.
  • Rev 1.8, 2010/08/09
  • 2010/08/03
    • Modified Introduction
  • 2010/08/02
    • Added note
  • 2010/08/01
    • Initial post

License

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

About the Authors

David Johns
Web Developer
United States United States
Member
No Biography provided

John_Tang
Software Developer
China China
Member
No Biography provided

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
QuestionSevere failure with attachments that are too large (greater than 5MB limit)memberGKarRacer11 May '13 - 16:55 
If you attempt to send an attachment that is greater than the default limit of 5MB (say 6MB, for example), the program waits indefinitely until the server times out and closes the connection and then proceeds to crash.
 
Here's the sequence:
 
1. Start sending attachment.
2. Notice that the file is too large, throw exception MSG_TOO_BIG
3. Catch exception and call DisconnectRemoteServer.
4. Send QUIT command.
5. Waits for response.
6. Server times out waiting for message data.
7. My server responded with: 421 SMTP incoming data timeout - closing connection.
8. Server closes connection abruptly.
9. Select returns with response received in step 7 (421...)
10. Checks response.
11. 421 is not a valid response for QUIT.
12. Throws exception
13. Program aborts because it is a new exception is thrown that is unhandled (at least in the example program).
 
The underlying problem here (at least in this case) is that the QUIT command is not valid because it is currently inside the message data portion. The message data must be ended first before the server can start accepting commands again. I suggest sending the end of message line before throwing for the failed attachment. Also perhaps it's better to check the file's validity before even putting out the MIME header data for it.
 
The secondary problem is the unhandled exception. I suggest wrapping the call to DisconnectRemoteServer (from inside the Send catch handler) with its own try/catch block. These exceptions are then trapped and ignored. Unfortunately I'm not sure what to do about the original exception. I've tried different methods, but in each one I've tried the program aborts when rethrowing the original exception.
 
Hopefully this makes sense. There may be other failure points inside the message data section as well that have this issue, but this is what I found so far.
AnswerRe: Severe failure with attachments that are too large (greater than 5MB limit)memberDavid Johns12 May '13 - 10:53 
GKarRacer,
Which version of the project are you using? This is one of the primary issues resolved in version 2.2 of the code - at least I think. I just want to make sure if you are seeing an issue with it that you are on the latest version of the project.
 
Thanks,
David
GeneralRe: Severe failure with attachments that are too large (greater than 5MB limit)memberGKarRacer12 May '13 - 16:32 
I have version 2.1. I had downloaded it fairly recently and didn't realize there was an update out already. I will check it out. Thanks.
GeneralRe: Severe failure with attachments that are too large (greater than 5MB limit)memberGKarRacer12 May '13 - 17:21 
I took a quick look at 2.2. I haven't had a chance to run it yet, but the attachment handling does look better. Thanks.
 
There is still an issue, however, with exceptions occurring within DisconnectRemoteServer. If an exception occurs while calling this, the program most certainly will crash due to nested exceptions or worse by throwing from within the destructor. I think for safety all calls to DisconnectRemoteServer need to have its own try/catch block wrapper and these exceptions should not be forwarded onto the caller. The destructor for CSmtp especially needs this.
 
To get around the double exception problem in the Send function I mentioned earlier, here's a suggestion: Don't call DisconnectRemoteServer from within Send's catch handler. Let the destructor handle it during the normal stack unwind from the original exception. Make sure, of course, to wrap DisconnectRemoteServer in the destructor within its own try/catch block. If you do this Send doesn't even need its own try/catch block.
 
Come to think of it instead of always wrapping DisconnectRemoteServer, you could simply have the DisconnectRemoteServer function have its own internal try/catch block that eats all exceptions.
 
I think ConnectRemoteServer also may have the same issue as Send.
 
What do you think?
Questionretreiving file size for attachmentsmemberGKarRacer11 May '13 - 15:11 
When sending attachments you read the entire file to calculate the file size. This seems highly inefficient. There are, of course, various operating system specific ways to get this information, but even with straight C++ you can do the following:
 
fseek(hFile, 0, SEEK_END);
TotalSize = ftell(hFile);
fseek(hFile, 0, SEEK_SET);
 
The only problem with this is if the total size of the file is greater than 4GB (clearly too big for an attachment, but you know those crazy users...). With VS, at least, you can use _ftelli64, but I don't know if there's a cross platform equivalent. You're current code also will fail with excessively sized files. Admittedly though, this is unexpected input.
AnswerRe: retreiving file size for attachmentsmemberGKarRacer11 May '13 - 15:15 
Sorry, the second line should be:
 
FileSize = ftell(hFile);
TotalSize += FileSize;
 

or simply:
 
TotalSize += ftell(hFile);
GeneralRe: retreiving file size for attachmentsmemberDavid Johns12 May '13 - 10:36 
GKarRacer,
 
Great point. Also, in the process of putting this in, I realized a couple of things that aren't good about rev 2.2. Now it reads the attachments one time just to check for total size and throws an error there if it is too large. However, in doing so:
1. Like you mentioned, it reads the whole file in the process just to find out how large it is.
2. It doesn't close the files so they will be left locked.
3. It still throws an error in the middle of sending so that if the file size changes between when it checks the first time and when it sends them, it could still end up in limbo.
 
To fix these problems I've made the following changes.
 
Starting on line 533:
		//Check that any attachments specified can be opened
		TotalSize = 0;
		for(FileId=0;FileId<Attachments.size();FileId++)
		{
			snprintf(FileName, 255, Attachments[FileId].c_str());
 
			// opening the file:
			hFile = fopen(FileName,"rb");
			if(hFile == NULL)
				throw ECSmtp(ECSmtp::FILE_NOT_EXIST);
			
			// checking file size:
			fseek(hFile, 0, SEEK_END);
			FileSize = ftell(hFile);
			TotalSize += FileSize;
 
			// sending the file:
			if(TotalSize/1024 > MSG_SIZE_IN_MB*1024)
				throw ECSmtp(ECSmtp::MSG_TOO_BIG);
 
			fclose(hFile);
		}
 
and then after making that change, starting on line 647:
			// get file size:
			fseek(hFile, 0, SEEK_END);
			FileSize = ftell(hFile);
			fseek (hFile,0,SEEK_SET);
 
			MsgPart = 0;
			for(i=0;i<FileSize/54+1;i++)
			{
				res = fread(FileBuf,sizeof(char),54,hFile);
				MsgPart ? strcat(SendBuf,base64_encode(reinterpret_cast<const unsigned char*>(FileBuf),res).c_str())
							: strcpy(SendBuf,base64_encode(reinterpret_cast<const unsigned char*>(FileBuf),res).c_str());
				strcat(SendBuf,"\r\n");
				MsgPart += res + 2;
				if(MsgPart >= BUFFER_SIZE/2)
				{ // sending part of the message
					MsgPart = 0;
					SendData(pEntry); // FileBuf, FileName, fclose(hFile);
				}
			}
			if(MsgPart)
			{
				SendData(pEntry); // FileBuf, FileName, fclose(hFile);
			}
			fclose(hFile);

Questionhow to set a smtp proxy?memberASERERTA@#@s8 May '13 - 8:01 
how to send email through a proxy?
does it have the setproxy function?
AnswerRe: how to set a smtp proxy?memberDavid Johns12 May '13 - 10:52 
ASERERTA@#@s,
Sorry, this capability is not currently supported. If you want to develop it and post back we could look into adding it.
 
Thanks,
David
GeneralMy vote of 5memberASERERTA@#@s8 May '13 - 7:54 
excellent !

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

Permalink | Advertise | Privacy | Mobile
Web02 | 2.6.130523.1 | Last Updated 6 May 2013
Article Copyright 2010 by David Johns, John_Tang
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid