Home > Geek Tips > The elusive SSL POP3 client in C#

The elusive SSL POP3 client in C#

When writing the code for my GMailPush application, I searched high and low for a C# managed POP3 client hidden in the framework somewhere. After all, C# has a managed SMTP client… so why not POP3?

In my research, I found several examples of what people were doing, but nothing was already in the framework. The examples I saw were informative, but most of them were too bloated and felt “dirty” to me. And so I sought out to create my own using the knowledge that I gained.

Without further adieu, I present my SslPopClient class:

using System;
using System.Collections.Generic;
using System.Net.Sockets;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
using System.IO;
 
namespace GMailPush
{
	public class SslPopClient
	{
		/// <summary>
		/// Stores the host and port we'll use to connect to
		/// </summary>
		/// <param name="host">The hostname of the pop server</param>
		/// <param name="port">The port the pop server listens on</param>
		public SslPopClient( string host, int port )
		{
			// Store the connection information for when the user logs in (see Login())
			_host = host;
			_port = port;
		}
 
		/// <summary>
		/// Deconstructor to clean up the client connection
		/// </summary>
		~SslPopClient()
		{
			// Try to clean up the connection before closing
			this.Disconnect();
		}
 
		/// <summary>
		/// Logs into the pop server using the USER and PASS commands
		/// </summary>
		/// <param name="username">The username to connect to the server</param>
		/// <param name="password">The password to connect to the server</param>
		public void Login( string username, string password )
		{
			// Create a new socket and ssl stream
			_client = new TcpClient( _host, _port );
			_sslStream = new SslStream( _client.GetStream(), false, new RemoteCertificateValidationCallback( ValidateServerCertificate ), new LocalCertificateSelectionCallback( SelectLocalCertificate ) );
 
			// Authenticate over the ssl stream
			_sslStream.AuthenticateAsClient( _host );
 
			// Initialize our reader and writer with the ssl stream
			_reader = new StreamReader( _sslStream );
			_writer = new StreamWriter( _sslStream );
			_writer.AutoFlush = true;
 
			// Check to see whether our connection was successful
			string response = _reader.ReadLine();
			if ( !response.StartsWith( _success ) )
				throw new Exception( "Server responded with error" );
 
			// If we got her, we're connected!
			this.Connected = true;
 
			// Now we try to authenticate with USER and PASS
			if ( !this.User( username ) )
				throw new Exception( "Username not accepted" );
			if ( !this.Pass( password ) )
				throw new Exception( "Password not accepted" );
		}
 
		/// <summary>
		/// Safely disconnects from the pop server
		/// </summary>
		public void Disconnect()
		{
			if ( this.Connected )
			{
				// Gracefully exit the pop3 server
				this.Quit();
 
				// Turn the connected flag off so we can't perform anything else
				this.Connected = false;
 
				// Clean up our reader and writer
				_reader.Close();
				_reader.Dispose();
				_writer.Close();
				_writer.Dispose();
				_sslStream.Close();
				_sslStream.Dispose();
 
				// Clean up our socket
				_client.Close();
				_client = null;
			}
		}
 
		/// <summary>
		/// Sends the USER command to the pop server
		/// </summary>
		/// <param name="username">The username to connect to the server</param>
		/// <returns>True for success, false for failure</returns>
		private bool User( string username )
		{
			// Don't continue if we're not connected to the server
			if ( !this.Connected )
				return false;
 
			// Return the results of the USER command
			return this.SendCommand( "USER {0}", username );
		}
 
		/// <summary>
		/// Sends the PASS command to the pop server
		/// </summary>
		/// <param name="password">The password used to connect to the server</param>
		/// <returns>True for success, false for failure</returns>
		private bool Pass( string password )
		{
			// Don't continue if we're not connected to the server
			if ( !this.Connected )
				return false;
 
			// Return the results of the PASS command
			return this.SendCommand( "PASS {0}", password );
		}
 
		/// <summary>
		/// Sends the QUIT command to the pop server
		/// </summary>
		/// <returns>True for success, false for failure</returns>
		private bool Quit()
		{
			// Don't continue if we're not connected to the server
			if ( !this.Connected )
				return false;
 
			// Just send the QUIT command by itself
			return ( this.SendCommand( "QUIT" ) );
		}
 
		/// <summary>
		/// Sends the STAT command on the pop server
		/// </summary>
		/// <param name="messageCount">An output variable that contains the number of messages</param>
		/// <param name="messageSize">An output variable that contains the size of the messages</param>
		/// <returns>True for success, false for failure</returns>
		private bool Stat( out int messageCount, out int messageSize )
		{
			// Initialize our output variables
			messageCount = -1;
			messageSize = -1;
 
			// Don't continue if we're not connected to the server
			if ( !this.Connected )
				return false;
 
			// Send the STAT command
			if ( !this.SendCommand( "STAT" ) )
				return false;
 
			// Get the response parameters (there should be 2)
			string[] responseRarameters = _lastResponse.Split( ' ' );
			if ( responseRarameters.Length != 3 )
				return false;
 
			// Save the response values to our output variables
			string firstParameter = responseRarameters[1];
			string secondParameter = responseRarameters[2];
			int.TryParse( firstParameter, out messageCount );
			int.TryParse( secondParameter, out messageSize );
 
			// If we got this far, success!
			return true;
		}
 
		/// <summary>
		/// Sends the TOP command to the pop server
		/// </summary>
		/// <param name="messageId">The message to call TOP for</param>
		/// <param name="numLines">The number of lines to receive</param>
		/// <param name="header">An output variable containing the headers</param>
		/// <param name="lines">An output variable containing the lines retrieved</param>
		/// <returns>True for success, false for failure</returns>
		private bool Top( int messageId, int numLines, out List<string> header, out List<string> lines )
		{
			// Initialize our output variables
			header = new List<string>();
			lines = new List<string>();
 
			// Don't continue if we're not connected to a server
			if ( !this.Connected )
				return false;
 
			// Send the TOP command
			if ( !this.SendCommand( "TOP {0} {1}", messageId, numLines ) )
				return false;
 
			// Read the received data until the terminating character "."
			string response = _reader.ReadLine();
			while ( response != "." )
			{
				// Add each header to the output variable
				header.Add( response );
				response = _reader.ReadLine();
			}
 
			// Add all the remaining lines to the output variable
			for ( int i = 0; i < numLines; i++ )
				lines.Add( _reader.ReadLine() );
 
			// If we got this far, success!
			return true;
		}
 
		/// <summary>
		/// Sends a command to the pop server
		/// </summary>
		/// <param name="command">The command to send to the server</param>
		/// <param name="commandParams">Any parameters used in the command string</param>
		/// <returns>True for success, false for failure</returns>
		private bool SendCommand( string command, params object[] commandParams )
		{
			if ( _writer == null || _reader == null )
				return false;
 
			// Send the command over the ssl stream to the server
			_writer.WriteLine( string.Format( command, commandParams ) );
 
			// Read in the last response
			_lastResponse = _reader.ReadLine();
 
			if ( _lastResponse == null )
				return false;
 
			// See whether it was a success message or not
			return _lastResponse.StartsWith( _success );
		}
 
		/// <summary>
		/// Validates the certificates from the server
		/// </summary>
		/// <remarks>
		/// Found on the MSDN forums here: 
		/// http://social.msdn.microsoft.com/Forums/en-US/vbgeneral/thread/7a674196-1422-4937-90cd-628c6e3ae3d1
		/// </remarks>
		/// <param name="sender"></param>
		/// <param name="certificate"></param>
		/// <param name="chain"></param>
		/// <param name="sslPolicyErrors"></param>
		/// <returns></returns>
		private bool ValidateServerCertificate( object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors )
		{
			// Success
			if ( sslPolicyErrors == SslPolicyErrors.None )
				return true;
 
			// Failure
			return false;
		}
 
		/// <summary>
		/// Selects the appropritate certificate to use
		/// </summary>
		/// <remarks>
		/// Found on the MSDN forums here: 
		/// http://social.msdn.microsoft.com/Forums/en-US/vbgeneral/thread/7a674196-1422-4937-90cd-628c6e3ae3d1
		/// </remarks>
		/// <param name="sender"></param>
		/// <param name="targetHost"></param>
		/// <param name="localCertificates"></param>
		/// <param name="remoteCertificate"></param>
		/// <param name="acceptableIssuers"></param>
		/// <returns></returns>
		private X509Certificate SelectLocalCertificate( object sender, string targetHost, X509CertificateCollection localCertificates, X509Certificate remoteCertificate, string[] acceptableIssuers )
		{
			// Check to see if there are any acceptable issuers and local certificates
			if ( acceptableIssuers != null && acceptableIssuers.Length > 0 && localCertificates != null && localCertificates.Count > 0 )
			{
				// Loop through the certificates
				foreach ( X509Certificate certificate in localCertificates )
				{
					// Check to see if this cert contains an acceptable issuer
					if ( Array.IndexOf( acceptableIssuers, certificate.Issuer ) != -1 )
						return certificate;
				}
			}
 
			// If there were no acceptable issuers, just return the first certificate found
			if ( localCertificates != null && localCertificates.Count > 0 )
				return localCertificates[0];
 
			// Nothing was found
			return null;
		}
 
		/// <summary>
		/// A flag to see whether the client is connected to the server or not
		/// </summary>
		public bool Connected { get; set; }
 
		/// <summary>
		/// The hostname the client will connect to
		/// </summary>
		private string _host;
		/// <summary>
		/// The port the client will connect on
		/// </summary>
		private int _port;
		/// <summary>
		/// The socket that will handle the transmission to the server
		/// </summary>
		private TcpClient _client;
		/// <summary>
		/// A layer to handle SSL communication with the server
		/// </summary>
		private SslStream _sslStream;
		/// <summary>
		/// Reads data from the SSL stream
		/// </summary>
		private StreamReader _reader;
		/// <summary>
		/// Writes data to the SSL stream
		/// </summary>
		private StreamWriter _writer;
		/// <summary>
		/// A reference to the last response received from the server
		/// </summary>
		private string _lastResponse;
		/// <summary>
		/// A constant that represents the successful response from the pop server
		/// </summary>
		private const string _success = "+OK";
	}
}

Categories: Geek Tips Tags: ,
  1. George
    March 5th, 2010 at 05:50 | #1

    Nice class, funnily enough I had a similar journey recently and came up with an almost identical class after searching around…I wish i’d found this post 1st!

  1. No trackbacks yet.

Spam protection by WP Captcha-Free