The elusive SSL POP3 client in C#
July 23rd, 2009
1 comment
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"; } }
