Click here to Skip to main content
15,879,535 members
Articles / Programming Languages / C#

STUN Client

Rate me:
Please Sign up or sign in to vote.
4.83/5 (36 votes)
20 Apr 2007CPOL 321.9K   14.9K   85  
STUN client C# implementation with sample application
using System;
using System.IO;
using System.Collections;
using System.Net;
using System.Net.Sockets;
using System.Text;

using LumiSoft.Net;
using LumiSoft.Net.AUTH;

namespace LumiSoft.Net.POP3.Server
{
	/// <summary>
	/// POP3 Session.
	/// </summary>
	public class POP3_Session : SocketServerSession
	{	        
		private POP3_Server            m_pServer        = null;  
		private string                 m_UserName       = "";    // Holds USER command value
		private string                 m_MD5_prefix     = "";    // Session MD5 prefix for APOP command
		private int                    m_BadCmdCount    = 0;     // Holds number of bad commands.
		private POP3_MessageCollection m_POP3_Messages  = null;
		
        /// <summary>
        /// Default constructor.
        /// </summary>
        /// <param name="sessionID">Session ID.</param>
        /// <param name="socket">Server connected socket.</param>
        /// <param name="bindInfo">BindInfo what accepted socket.</param>
        /// <param name="server">Reference to server.</param>
        internal POP3_Session(string sessionID,SocketEx socket,BindInfo bindInfo,POP3_Server server) : base(sessionID,socket,bindInfo,server)
        {
            m_pServer       = server;
            m_POP3_Messages = new POP3_MessageCollection();

            // Start session proccessing
			StartSession();
        }


		#region method StartSession

		/// <summary>
		/// Starts session.
		/// </summary>
		private void StartSession()
		{
			// Add session to session list
			m_pServer.AddSession(this);
	
			try{
				// Check if ip is allowed to connect this computer
				if(m_pServer.OnValidate_IpAddress(this.LocalEndPoint,this.RemoteEndPoint)){
                    //--- Dedicated SSL connection, switch to SSL -----------------------------------//
                    if(this.BindInfo.SSL){
                        try{
                            this.Socket.SwitchToSSL(this.BindInfo.SSL_Certificate);

                            if(this.Socket.Logger != null){
                                this.Socket.Logger.AddTextEntry("SSL negotiation completed successfully.");
                            }
                        }
                        catch(Exception x){
                            if(this.Socket.Logger != null){
                                this.Socket.Logger.AddTextEntry("SSL handshake failed ! " + x.Message);

                                EndSession();
                                return;
                            }
                        }
                    }
                    //-------------------------------------------------------------------------------//

					// Notify that server is ready
					m_MD5_prefix = "<" + Guid.NewGuid().ToString().ToLower() + ">";
                    if(m_pServer.GreetingText == ""){
					    this.Socket.WriteLine("+OK " + m_pServer.HostName + " POP3 Server ready " + m_MD5_prefix);
                    }
                    else{
                        this.Socket.WriteLine("+OK " + m_pServer.GreetingText + " " + m_MD5_prefix);
                    }

					BeginRecieveCmd();
				}
				else{
					EndSession();
				}
			}
			catch(Exception x){
				OnError(x);
			}
		}

		#endregion

		#region method EndSession

		/// <summary>
		/// Ends session, closes socket.
		/// </summary>
		private void EndSession()
		{          
			try{
				// Write logs to log file, if needed
				if(m_pServer.LogCommands){
					this.Socket.Logger.Flush();
				}

				if(this.Socket != null){
					this.Socket.Shutdown(SocketShutdown.Both);
					this.Socket.Disconnect();
					//this.Socket = null;
				}
			}
			catch{ // We don't need to check errors here, because they only may be Socket closing errors.
			}
			finally{
				m_pServer.RemoveSession(this);
			}
		}

		#endregion


        #region method Kill

        /// <summary>
        /// Kill this session.
        /// </summary>
        public override void Kill()
        {
            EndSession();
        }

        #endregion

        #region method OnSessionTimeout

        /// <summary>
		/// Is called by server when session has timed out.
		/// </summary>
		internal protected override void OnSessionTimeout()
		{
			try{
				this.Socket.WriteLine("-ERR Session timeout, closing transmission channel");
			}
			catch{
			}

			EndSession();
		}

		#endregion

		#region method OnError

		/// <summary>
		/// Is called when error occures.
		/// </summary>
		/// <param name="x"></param>
		private void OnError(Exception x)
		{
			try{
                // We must see InnerException too, SocketException may be as inner exception.
                SocketException socketException = null;
                if(x is SocketException){
                    socketException = (SocketException)x;
                }
                else if(x.InnerException != null && x.InnerException is SocketException){
                    socketException = (SocketException)x.InnerException;
                }

				if(socketException != null){
					// Client disconnected without shutting down
					if(socketException.ErrorCode == 10054 || socketException.ErrorCode == 10053){
						if(m_pServer.LogCommands){
							this.Socket.Logger.AddTextEntry("Client aborted/disconnected");
						}

						EndSession();

						// Exception handled, return
						return;
					}
				}

                m_pServer.OnSysError("",x);                
			}
			catch(Exception ex){
				m_pServer.OnSysError("",ex);
			}
		}

		#endregion


		#region method BeginRecieveCmd
		
		/// <summary>
		/// Starts recieveing command.
		/// </summary>
		private void BeginRecieveCmd()
		{
			MemoryStream strm = new MemoryStream();
			this.Socket.BeginReadLine(strm,1024,strm,new SocketCallBack(this.EndRecieveCmd));
		}

		#endregion

		#region method EndRecieveCmd

		/// <summary>
		/// Is called if command is recieved.
		/// </summary>
		/// <param name="result"></param>
		/// <param name="exception"></param>
		/// <param name="count"></param>
		/// <param name="tag"></param>
		private void EndRecieveCmd(SocketCallBackResult result,long count,Exception exception,object tag)
		{
			try{
				switch(result)
				{
					case SocketCallBackResult.Ok:
						MemoryStream strm = (MemoryStream)tag;

						string cmdLine = System.Text.Encoding.Default.GetString(strm.ToArray());

						// Exceute command
						if(SwitchCommand(cmdLine)){
							// Session end, close session
							EndSession();
						}
						break;

					case SocketCallBackResult.LengthExceeded:
						this.Socket.WriteLine("-ERR Line too long.");

						BeginRecieveCmd();
						break;

					case SocketCallBackResult.SocketClosed:
						EndSession();
						break;

					case SocketCallBackResult.Exception:
						OnError(exception);
						break;
				}
			}
			catch(Exception x){
				 OnError(x);
			}
		}

		#endregion
		
		
		#region method SwitchCommand

		/// <summary>
		/// Parses and executes POP3 commmand.
		/// </summary>
		/// <param name="POP3_commandTxt">POP3 command text.</param>
		/// <returns>Returns true,if session must be terminated.</returns>
		private bool SwitchCommand(string POP3_commandTxt)
		{
			//---- Parse command --------------------------------------------------//
			string[] cmdParts = POP3_commandTxt.TrimStart().Split(new char[]{' '});
			string POP3_command = cmdParts[0].ToUpper().Trim();
			string argsText = Core.GetArgsText(POP3_commandTxt,POP3_command);
			//---------------------------------------------------------------------//

			bool getNextCmd = true;

			switch(POP3_command)
			{
				case "USER":
					USER(argsText);
					getNextCmd = false;
					break;

				case "PASS":
					PASS(argsText);
					getNextCmd = false;
					break;
					
				case "STAT":
					STAT();
					getNextCmd = false;
					break;

				case "LIST":
					LIST(argsText);
					getNextCmd = false;
					break;

				case "RETR":					
					RETR(argsText);
					getNextCmd = false;
					break;

				case "DELE":
					DELE(argsText);
					getNextCmd = false;
					break;

				case "NOOP":
					NOOP();
					getNextCmd = false;
					break;

				case "RSET":
					RSET();
					getNextCmd = false;
					break;

				case "QUIT":
					QUIT();
					getNextCmd = false;
					return true;


				//----- Optional commands ----- //
				case "UIDL":
					UIDL(argsText);
					getNextCmd = false;
					break;

				case "APOP":
					APOP(argsText);
					getNextCmd = false;
					break;

				case "TOP":
					TOP(argsText);
					getNextCmd = false;
					break;

				case "AUTH":
					AUTH(argsText);
					getNextCmd = false;
					break;

				case "CAPA":
					CAPA(argsText);
					getNextCmd = false;
					break;

                case "STLS":
					STLS(argsText);
					getNextCmd = false;
					break;
										
				default:					
					this.Socket.WriteLine("-ERR Invalid command");

					//---- Check that maximum bad commands count isn't exceeded ---------------//
					if(m_BadCmdCount > m_pServer.MaxBadCommands-1){
						this.Socket.WriteLine("-ERR Too many bad commands, closing transmission channel");
						return true;
					}
					m_BadCmdCount++;
					//-------------------------------------------------------------------------//
					break;				
			}
			
			if(getNextCmd){
				BeginRecieveCmd();
			}
						
			return false;
		}

		#endregion


		#region method USER

		private void USER(string argsText)
		{
			/* RFC 1939 7. USER
			Arguments:
				a string identifying a mailbox (required), which is of
				significance ONLY to the server
				
			NOTE:
				If the POP3 server responds with a positive
				status indicator ("+OK"), then the client may issue
				either the PASS command to complete the authentication,
				or the QUIT command to terminate the POP3 session.
			 
			*/

			if(this.Authenticated){
				this.Socket.BeginWriteLine("-ERR You are already authenticated",new SocketCallBack(this.EndSend));
				return;
			}
			if(m_UserName.Length > 0){
				this.Socket.BeginWriteLine("-ERR username is already specified, please specify password",new SocketCallBack(this.EndSend));
				return;
			}
            if((m_pServer.SupportedAuthentications & SaslAuthTypes.Plain) == 0){
                this.Socket.BeginWriteLine("-ERR USER/PASS command disabled",new SocketCallBack(this.EndSend));
				return;
            }

			string[] param = TextUtils.SplitQuotedString(argsText,' ',true);

			// There must be only one parameter - userName
			if(argsText.Length > 0 && param.Length == 1){
				string userName = param[0];
							
				// Check if user isn't logged in already
				if(!m_pServer.IsUserLoggedIn(userName)){
					m_UserName = userName;

					// Send this line last, because it issues a new command and any assignments done
					// after this method may not become wisible to next command.
					this.Socket.BeginWriteLine("+OK User:'" + userName + "' ok",new SocketCallBack(this.EndSend));					
				}
				else{
					this.Socket.BeginWriteLine("-ERR User:'" + userName + "' already logged in",new SocketCallBack(this.EndSend));
				}
			}
			else{
				this.Socket.BeginWriteLine("-ERR Syntax error. Syntax:{USER username}",new SocketCallBack(this.EndSend));
			}
		}

		#endregion

		#region method PASS

		private void PASS(string argsText)
		{	
			/* RFC 7. PASS
			Arguments:
				a server/mailbox-specific password (required)
				
			Restrictions:
				may only be given in the AUTHORIZATION state immediately
				after a successful USER command
				
			NOTE:
				When the client issues the PASS command, the POP3 server
				uses the argument pair from the USER and PASS commands to
				determine if the client should be given access to the
				appropriate maildrop.
				
			Possible Responses:
				+OK maildrop locked and ready
				-ERR invalid password
				-ERR unable to lock maildrop
						
			*/

			if(this.Authenticated){
				this.Socket.BeginWriteLine("-ERR You are already authenticated",new SocketCallBack(this.EndSend));
				return;
			}
			if(m_UserName.Length == 0){
				this.Socket.BeginWriteLine("-ERR please specify username first",new SocketCallBack(this.EndSend));
				return;
			}            
            if((m_pServer.SupportedAuthentications & SaslAuthTypes.Plain) == 0){
                this.Socket.BeginWriteLine("-ERR USER/PASS command disabled",new SocketCallBack(this.EndSend));
				return;
            }

			string[] param = TextUtils.SplitQuotedString(argsText,' ',true);

			// There may be only one parameter - password
			if(param.Length == 1){
				string password = param[0];
									
				// Authenticate user
				AuthUser_EventArgs aArgs = m_pServer.OnAuthUser(this,m_UserName,password,"",AuthType.Plain);

                // There is custom error, return it
                if(aArgs.ErrorText != null){
                    this.Socket.BeginWriteLine("-ERR " + aArgs.ErrorText,new SocketCallBack(this.EndSend));
                    return;
                }
                
				if(aArgs.Validated){
	                this.SetUserName(m_UserName);

					// Get user messages info.
					m_pServer.OnGetMessagesInfo(this,m_POP3_Messages);

					this.Socket.BeginWriteLine("+OK Password ok",new SocketCallBack(this.EndSend));
				}
				else{						
					this.Socket.BeginWriteLine("-ERR UserName or Password is incorrect",new SocketCallBack(this.EndSend));					
					m_UserName = ""; // Reset userName !!!
				}
			}
			else{
				this.Socket.BeginWriteLine("-ERR Syntax error. Syntax:{PASS userName}",new SocketCallBack(this.EndSend));
			}
		}

		#endregion

		#region method STAT

		private void STAT()
		{	
			/* RFC 1939 5. STAT
			NOTE:
				The positive response consists of "+OK" followed by a single
				space, the number of messages in the maildrop, a single
				space, and the size of the maildrop in octets.
				
				Note that messages marked as deleted are not counted in
				either total.
			 
			Example:
				C: STAT
				S: +OK 2 320
			*/

			if(!this.Authenticated){
				this.Socket.BeginWriteLine("-ERR You must authenticate first",new SocketCallBack(this.EndSend));
				return;
			}
		
			this.Socket.BeginWriteLine("+OK " + m_POP3_Messages.Count.ToString() + " " + m_POP3_Messages.GetTotalMessagesSize(),new SocketCallBack(this.EndSend));			
		}

		#endregion

		#region method LIST

		private void LIST(string argsText)
		{	
			/* RFC 1939 5. LIST
			Arguments:
				a message-number (optional), which, if present, may NOT
				refer to a message marked as deleted
			 
			NOTE:
				If an argument was given and the POP3 server issues a
				positive response with a line containing information for
				that message.

				If no argument was given and the POP3 server issues a
				positive response, then the response given is multi-line.
				
				Note that messages marked as deleted are not listed.
			
			Examples:
				C: LIST
				S: +OK 2 messages (320 octets)
				S: 1 120				
				S: 2 200
				S: .
				...
				C: LIST 2
				S: +OK 2 200
				...
				C: LIST 3
				S: -ERR no such message, only 2 messages in maildrop
			 
			*/

			if(!this.Authenticated){
				this.Socket.BeginWriteLine("-ERR You must authenticate first",new SocketCallBack(this.EndSend));
				return;
			}

			string[] param = TextUtils.SplitQuotedString(argsText,' ',true);

			// Argument isn't specified, multiline response.
			if(argsText.Length == 0){
                StringBuilder reply = new StringBuilder();
				reply.Append("+OK " + m_POP3_Messages.Count.ToString() + " messages\r\n");

				// Send message number and size for each message
                for(int i=0;i<m_POP3_Messages.Count;i++){
                    POP3_Message message = m_POP3_Messages[i];
                    if(!message.MarkedForDelete){
                        reply.Append((i + 1).ToString() + " " + message.Size + "\r\n");
                    }
                }

				// ".<CRLF>" - means end of list
				reply.Append(".\r\n");

				this.Socket.BeginWriteLine(reply.ToString(),null,new SocketCallBack(this.EndSend));
			}
			else{
				// If parameters specified,there may be only one parameter - messageNr
				if(param.Length == 1){
					// Check if messageNr is valid
					if(Core.IsNumber(param[0])){
						int messageNr = Convert.ToInt32(param[0]);
						if(m_POP3_Messages.MessageExists(messageNr)){
							POP3_Message msg = m_POP3_Messages[messageNr - 1];

							this.Socket.BeginWriteLine("+OK " + messageNr.ToString() + " " + msg.Size,new SocketCallBack(this.EndSend));
						}
						else{
							this.Socket.BeginWriteLine("-ERR no such message, or marked for deletion",new SocketCallBack(this.EndSend));
						}
					}
					else{
						this.Socket.BeginWriteLine("-ERR message-number is invalid",new SocketCallBack(this.EndSend));
					}
				}
				else{
					this.Socket.BeginWriteLine("-ERR Syntax error. Syntax:{LIST [messageNr]}",new SocketCallBack(this.EndSend));
				}
			}
		}

		#endregion

		#region method RETR
		
		private void RETR(string argsText)
		{
			/* RFC 1939 5. RETR
			Arguments:
				a message-number (required) which may NOT refer to a
				message marked as deleted
			 
			NOTE:
				If the POP3 server issues a positive response, then the
				response given is multi-line.  After the initial +OK, the
				POP3 server sends the message corresponding to the given
				message-number, being careful to byte-stuff the termination
				character (as with all multi-line responses).
				
			Example:
				C: RETR 1
				S: +OK 120 octets
				S: <the POP3 server sends the entire message here>
				S: .
			
			*/

			if(!this.Authenticated){
				this.Socket.BeginWriteLine("-ERR You must authenticate first",new SocketCallBack(this.EndSend));
			}
	
			string[] param = TextUtils.SplitQuotedString(argsText,' ',true);

			// There must be only one parameter - messageNr
			if(argsText.Length > 0 && param.Length == 1){
				// Check if messageNr is valid
				if(Core.IsNumber(param[0])){
					int messageNr = Convert.ToInt32(param[0]);					
					if(m_POP3_Messages.MessageExists(messageNr)){
						POP3_Message msg = m_POP3_Messages[messageNr - 1];
						
                        // Raise Event, request message
						POP3_eArgs_GetMessageStream eArgs = m_pServer.OnGetMessageStream(this,msg);		
						if(eArgs.MessageExists && eArgs.MessageStream != null){
                            this.Socket.WriteLine("+OK " + eArgs.MessageSize + " octets");
                                                        
							// Send message asynchronously to client
							this.Socket.BeginWritePeriodTerminated(eArgs.MessageStream,eArgs.CloseMessageStream,null,new SocketCallBack(this.EndSend));
						}
						else{									
							this.Socket.BeginWriteLine("-ERR no such message",new SocketCallBack(this.EndSend));
						}
					}
					else{
						this.Socket.BeginWriteLine("-ERR no such message",new SocketCallBack(this.EndSend));
					}
				}
				else{
					this.Socket.BeginWriteLine("-ERR message-number is invalid",new SocketCallBack(this.EndSend));
				}
			}
			else{
				this.Socket.BeginWriteLine("-ERR Syntax error. Syntax:{RETR messageNr}",new SocketCallBack(this.EndSend));
			}
		}

		#endregion

		#region method DELE

		private void DELE(string argsText)
		{	
			/* RFC 1939 5. DELE
			Arguments:
				a message-number (required) which may NOT refer to a
				message marked as deleted
			 
			NOTE:
				The POP3 server marks the message as deleted.  Any future
				reference to the message-number associated with the message
				in a POP3 command generates an error.  The POP3 server does
				not actually delete the message until the POP3 session
				enters the UPDATE state.
			*/

			if(!this.Authenticated){
				this.Socket.BeginWriteLine("-ERR You must authenticate first",new SocketCallBack(this.EndSend));
				return;
			}

			string[] param = TextUtils.SplitQuotedString(argsText,' ',true);

			// There must be only one parameter - messageNr
			if(argsText.Length > 0 && param.Length == 1){
				// Check if messageNr is valid
				if(Core.IsNumber(param[0])){
					int nr = Convert.ToInt32(param[0]);					
					if(m_POP3_Messages.MessageExists(nr)){
						POP3_Message msg = m_POP3_Messages[nr - 1];
						msg.MarkedForDelete = true;

						this.Socket.BeginWriteLine("+OK marked for delete",new SocketCallBack(this.EndSend));
					}
					else{
						this.Socket.BeginWriteLine("-ERR no such message",new SocketCallBack(this.EndSend));
					}
				}
				else{
					this.Socket.BeginWriteLine("-ERR message-number is invalid",new SocketCallBack(this.EndSend));
				}
			}
			else{
				this.Socket.BeginWriteLine("-ERR Syntax error. Syntax:{DELE messageNr}",new SocketCallBack(this.EndSend));
			}
		}

		#endregion

		#region method NOOP

		private void NOOP()
		{
			/* RFC 1939 5. NOOP
			NOTE:
				The POP3 server does nothing, it merely replies with a
				positive response.
			*/

			if(!this.Authenticated){
				this.Socket.BeginWriteLine("-ERR You must authenticate first",new SocketCallBack(this.EndSend));
				return;
			}

			this.Socket.BeginWriteLine("+OK",new SocketCallBack(this.EndSend));
		}

		#endregion

		#region method RSET

		private void RSET()
		{
			/* RFC 1939 5. RSET
			Discussion:
				If any messages have been marked as deleted by the POP3
				server, they are unmarked.  The POP3 server then replies
				with a positive response.
			*/

			if(!this.Authenticated){
				this.Socket.BeginWriteLine("-ERR You must authenticate first",new SocketCallBack(this.EndSend));
				return;
			}

			Reset();

			// Raise SessionResetted event
			m_pServer.OnSessionResetted(this);

			this.Socket.BeginWriteLine("+OK",new SocketCallBack(this.EndSend));
		}

		#endregion

		#region method QUIT

		private void QUIT()
		{
			/* RFC 1939 6. QUIT
			NOTE:
				The POP3 server removes all messages marked as deleted
				from the maildrop and replies as to the status of this
				operation.  If there is an error, such as a resource
				shortage, encountered while removing messages, the
				maildrop may result in having some or none of the messages
				marked as deleted be removed.  In no case may the server
				remove any messages not marked as deleted.

				Whether the removal was successful or not, the server
				then releases any exclusive-access lock on the maildrop
				and closes the TCP connection.
			*/					
			Update();

			this.Socket.WriteLine("+OK POP3 server signing off");			
		}

		#endregion


		//--- Optional commands

		#region method TOP

		private void TOP(string argsText)
		{		
			/* RFC 1939 7. TOP
			Arguments:
				a message-number (required) which may NOT refer to to a
				message marked as deleted, and a non-negative number
				of lines (required)
		
			NOTE:
				If the POP3 server issues a positive response, then the
				response given is multi-line.  After the initial +OK, the
				POP3 server sends the headers of the message, the blank
				line separating the headers from the body, and then the
				number of lines of the indicated message's body, being
				careful to byte-stuff the termination character (as with
				all multi-line responses).
			
			Examples:
				C: TOP 1 10
				S: +OK
				S: <the POP3 server sends the headers of the
					message, a blank line, and the first 10 lines
					of the body of the message>
				S: .
                ...
				C: TOP 100 3
				S: -ERR no such message
			 
			*/

			if(!this.Authenticated){
				this.Socket.BeginWriteLine("-ERR You must authenticate first",new SocketCallBack(this.EndSend));
			}

			string[] param = TextUtils.SplitQuotedString(argsText,' ',true);
			
			// There must be at two parameters - messageNr and nrLines
			if(param.Length == 2){
				// Check if messageNr and nrLines is valid
				if(Core.IsNumber(param[0]) && Core.IsNumber(param[1])){
					int messageNr = Convert.ToInt32(param[0]);
					if(m_POP3_Messages.MessageExists(messageNr)){
						POP3_Message msg = m_POP3_Messages[messageNr - 1];

						byte[] lines = m_pServer.OnGetTopLines(this,msg,Convert.ToInt32(param[1]));
						if(lines != null){
                            this.Socket.WriteLine("+OK " + lines.Length + " octets");

							// Send message asynchronously to client
							this.Socket.BeginWritePeriodTerminated(new MemoryStream(lines),null,new SocketCallBack(this.EndSend));
						}
						else{
							this.Socket.BeginWriteLine("-ERR no such message",new SocketCallBack(this.EndSend));
						}
					}
					else{
						this.Socket.BeginWriteLine("-ERR no such message",new SocketCallBack(this.EndSend));
					}
				}
				else{
					this.Socket.BeginWriteLine("-ERR message-number or number of lines is invalid",new SocketCallBack(this.EndSend));
				}
			}
			else{
				this.Socket.BeginWriteLine("-ERR Syntax error. Syntax:{TOP messageNr nrLines}",new SocketCallBack(this.EndSend));
			}
		}

		#endregion

		#region method UIDL

		private void UIDL(string argsText)
		{
			/* RFC 1939 UIDL [msg]
			Arguments:
			    a message-number (optional), which, if present, may NOT
				refer to a message marked as deleted
				
			NOTE:
				If an argument was given and the POP3 server issues a positive
				response with a line containing information for that message.

				If no argument was given and the POP3 server issues a positive
				response, then the response given is multi-line.  After the
				initial +OK, for each message in the maildrop, the POP3 server
				responds with a line containing information for that message.	
				
			Examples:
				C: UIDL
				S: +OK
				S: 1 whqtswO00WBw418f9t5JxYwZ
				S: 2 QhdPYR:00WBw1Ph7x7
				S: .
				...
				C: UIDL 2
				S: +OK 2 QhdPYR:00WBw1Ph7x7
				...
				C: UIDL 3
				S: -ERR no such message
			*/

			if(!this.Authenticated){
				this.Socket.BeginWriteLine("-ERR You must authenticate first",new SocketCallBack(this.EndSend));
				return;
			}

			string[] param = TextUtils.SplitQuotedString(argsText,' ',true);

			// Argument isn't specified, multiline response.
			if(argsText.Length == 0){
                StringBuilder reply = new StringBuilder();
				reply.Append("+OK\r\n");

                // Send message number and size for each message
                for(int i=0;i<m_POP3_Messages.Count;i++){
                    POP3_Message message = m_POP3_Messages[i];
                    if(!message.MarkedForDelete){
                        reply.Append((i + 1).ToString() + " " + message.UID + "\r\n");
                    }
                }

				// ".<CRLF>" - means end of list
				reply.Append(".\r\n");

				this.Socket.BeginWriteLine(reply.ToString(),null,new SocketCallBack(this.EndSend));
			}
			else{
				// If parameters specified,there may be only one parameter - messageID
				if(param.Length == 1){
					// Check if messageNr is valid
					if(Core.IsNumber(param[0])){
						int messageNr = Convert.ToInt32(param[0]);
						if(m_POP3_Messages.MessageExists(messageNr)){
							POP3_Message msg = m_POP3_Messages[messageNr - 1];

							this.Socket.BeginWriteLine("+OK " + messageNr.ToString() + " " + msg.UID,new SocketCallBack(this.EndSend));
						}
						else{
							this.Socket.BeginWriteLine("-ERR no such message",new SocketCallBack(this.EndSend));
						}
					}
					else{
						this.Socket.BeginWriteLine("-ERR message-number is invalid",new SocketCallBack(this.EndSend));
					}
				}
				else{
					this.Socket.BeginWriteLine("-ERR Syntax error. Syntax:{UIDL [messageNr]}",new SocketCallBack(this.EndSend));
				}
			}	
		}

		#endregion

		#region method APOP

		private void APOP(string argsText)
		{
			/* RFC 1939 7. APOP
			Arguments:
				a string identifying a mailbox and a MD5 digest string
				(both required)
				
			NOTE:
				A POP3 server which implements the APOP command will
				include a timestamp in its banner greeting.  The syntax of
				the timestamp corresponds to the `msg-id' in [RFC822], and
				MUST be different each time the POP3 server issues a banner
				greeting.
				
			Examples:
				S: +OK POP3 server ready <1896.697170952@dbc.mtview.ca.us>
				C: APOP mrose c4c9334bac560ecc979e58001b3e22fb
				S: +OK maildrop has 1 message (369 octets)

				In this example, the shared  secret  is  the  string  `tan-
				staaf'.  Hence, the MD5 algorithm is applied to the string

				<1896.697170952@dbc.mtview.ca.us>tanstaaf
				 
				which produces a digest value of
		            c4c9334bac560ecc979e58001b3e22fb
			 
			*/

			if(this.Authenticated){
				this.Socket.BeginWriteLine("-ERR You are already authenticated",new SocketCallBack(this.EndSend));
				return;
			}

			string[] param = TextUtils.SplitQuotedString(argsText,' ',true);

			// There must be two params
			if(param.Length == 2){
				string userName   = param[0];
				string md5HexHash = param[1];

				// Check if user isn't logged in already
				if(m_pServer.IsUserLoggedIn(userName)){
					this.Socket.BeginWriteLine("-ERR User:'" + userName + "' already logged in",new SocketCallBack(this.EndSend));
					return;
				}

				// Authenticate user
				AuthUser_EventArgs aArgs = m_pServer.OnAuthUser(this,userName,md5HexHash,m_MD5_prefix,AuthType.APOP);
				if(aArgs.Validated){
                    this.SetUserName(userName);

					// Get user messages info.
					m_pServer.OnGetMessagesInfo(this,m_POP3_Messages);

					this.Socket.BeginWriteLine("+OK authentication was successful",new SocketCallBack(this.EndSend));
				}
				else{
					this.Socket.BeginWriteLine("-ERR authentication failed",new SocketCallBack(this.EndSend));
				}
			}
			else{
				this.Socket.BeginWriteLine("-ERR syntax error. Syntax:{APOP userName md5HexHash}",new SocketCallBack(this.EndSend));
			}
		}

		#endregion

		#region method AUTH

		private void AUTH(string argsText)
		{
			/* Rfc 1734
				
				AUTH mechanism

					Arguments:
						a string identifying an IMAP4 authentication mechanism,
						such as defined by [IMAP4-AUTH].  Any use of the string
						"imap" used in a server authentication identity in the
						definition of an authentication mechanism is replaced with
						the string "pop".
						
					Possible Responses:
						+OK maildrop locked and ready
						-ERR authentication exchange failed

					Restrictions:
						may only be given in the AUTHORIZATION state

					Discussion:
						The AUTH command indicates an authentication mechanism to
						the server.  If the server supports the requested
						authentication mechanism, it performs an authentication
						protocol exchange to authenticate and identify the user.
						Optionally, it also negotiates a protection mechanism for
						subsequent protocol interactions.  If the requested
						authentication mechanism is not supported, the server						
						should reject the AUTH command by sending a negative
						response.

						The authentication protocol exchange consists of a series
						of server challenges and client answers that are specific
						to the authentication mechanism.  A server challenge,
						otherwise known as a ready response, is a line consisting
						of a "+" character followed by a single space and a BASE64
						encoded string.  The client answer consists of a line
						containing a BASE64 encoded string.  If the client wishes
						to cancel an authentication exchange, it should issue a
						line with a single "*".  If the server receives such an
						answer, it must reject the AUTH command by sending a
						negative response.

						A protection mechanism provides integrity and privacy
						protection to the protocol session.  If a protection
						mechanism is negotiated, it is applied to all subsequent
						data sent over the connection.  The protection mechanism
						takes effect immediately following the CRLF that concludes
						the authentication exchange for the client, and the CRLF of
						the positive response for the server.  Once the protection
						mechanism is in effect, the stream of command and response
						octets is processed into buffers of ciphertext.  Each
						buffer is transferred over the connection as a stream of
						octets prepended with a four octet field in network byte
						order that represents the length of the following data.
						The maximum ciphertext buffer length is defined by the
						protection mechanism.

						The server is not required to support any particular
						authentication mechanism, nor are authentication mechanisms
						required to support any protection mechanisms.  If an AUTH
						command fails with a negative response, the session remains
						in the AUTHORIZATION state and client may try another
						authentication mechanism by issuing another AUTH command,
						or may attempt to authenticate by using the USER/PASS or
						APOP commands.  In other words, the client may request
						authentication types in decreasing order of preference,
						with the USER/PASS or APOP command as a last resort.

						Should the client successfully complete the authentication
						exchange, the POP3 server issues a positive response and
						the POP3 session enters the TRANSACTION state.
						
				Examples:
							S: +OK POP3 server ready
							C: AUTH KERBEROS_V4
							S: + AmFYig==
							C: BAcAQU5EUkVXLkNNVS5FRFUAOCAsho84kLN3/IJmrMG+25a4DT
								+nZImJjnTNHJUtxAA+o0KPKfHEcAFs9a3CL5Oebe/ydHJUwYFd
								WwuQ1MWiy6IesKvjL5rL9WjXUb9MwT9bpObYLGOKi1Qh
							S: + or//EoAADZI=
							C: DiAF5A4gA+oOIALuBkAAmw==
							S: +OK Kerberos V4 authentication successful
								...
							C: AUTH FOOBAR
							S: -ERR Unrecognized authentication type
			 
			*/
			if(this.Authenticated){
				this.Socket.BeginWriteLine("-ERR already authenticated",new SocketCallBack(this.EndSend));
				return;
			}
			
				
			//------ Parse parameters -------------------------------------//
			string userName = "";
			string password = "";
			AuthUser_EventArgs aArgs = null;

			string[] param = TextUtils.SplitQuotedString(argsText,' ',true);
			switch(param[0].ToUpper())
			{
				case "PLAIN":
					this.Socket.BeginWriteLine("-ERR Unrecognized authentication type.",new SocketCallBack(this.EndSend));
					break;

				case "LOGIN":

					#region LOGIN authentication

				    //---- AUTH = LOGIN ------------------------------
					/* Login
					C: AUTH LOGIN-MD5
					S: + VXNlcm5hbWU6
					C: username_in_base64
					S: + UGFzc3dvcmQ6
					C: password_in_base64
					
					   VXNlcm5hbWU6 base64_decoded= USERNAME
					   UGFzc3dvcmQ6 base64_decoded= PASSWORD
					*/
					// Note: all strings are base64 strings eg. VXNlcm5hbWU6 = UserName.
			
					
					// Query UserName
					this.Socket.WriteLine("+ VXNlcm5hbWU6");

					string userNameLine = this.Socket.ReadLine();
					// Encode username from base64
					if(userNameLine.Length > 0){
						userName = System.Text.Encoding.Default.GetString(Convert.FromBase64String(userNameLine));
					}
						
					// Query Password
					this.Socket.WriteLine("+ UGFzc3dvcmQ6");

					string passwordLine = this.Socket.ReadLine();
					// Encode password from base64
					if(passwordLine.Length > 0){
						password = System.Text.Encoding.Default.GetString(Convert.FromBase64String(passwordLine));
					}
						
					aArgs = m_pServer.OnAuthUser(this,userName,password,"",AuthType.Plain);

                    // There is custom error, return it
                    if(aArgs.ErrorText != null){
                        this.Socket.BeginWriteLine("-ERR " + aArgs.ErrorText,new SocketCallBack(this.EndSend));
                        return;
                    }

					if(aArgs.Validated){
						this.Socket.BeginWriteLine("+OK Authentication successful.",new SocketCallBack(this.EndSend));
						
						this.SetUserName(userName);

						// Get user messages info.
						m_pServer.OnGetMessagesInfo(this,m_POP3_Messages);
					}
					else{
						this.Socket.BeginWriteLine("-ERR Authentication failed",new SocketCallBack(this.EndSend));
					}

					#endregion

					break;

				case "CRAM-MD5":
					
					#region CRAM-MD5 authentication

					/* Cram-M5
					C: AUTH CRAM-MD5
					S: + <md5_calculation_hash_in_base64>
					C: base64(decoded:username password_hash)
					*/
					
					string md5Hash = "<" + Guid.NewGuid().ToString().ToLower() + ">";
					this.Socket.WriteLine("+ " + Convert.ToBase64String(System.Text.Encoding.ASCII.GetBytes(md5Hash)));

					string reply = this.Socket.ReadLine();

					reply = System.Text.Encoding.Default.GetString(Convert.FromBase64String(reply));
					string[] replyArgs = reply.Split(' ');
					userName = replyArgs[0];
					
					aArgs = m_pServer.OnAuthUser(this,userName,replyArgs[1],md5Hash,AuthType.CRAM_MD5);

                    // There is custom error, return it
                    if(aArgs.ErrorText != null){
                        this.Socket.BeginWriteLine("-ERR " + aArgs.ErrorText,new SocketCallBack(this.EndSend));
                        return;
                    }

					if(aArgs.Validated){
						this.Socket.BeginWriteLine("+OK Authentication successful.",new SocketCallBack(this.EndSend));
						
                        this.SetUserName(userName);

						// Get user messages info.
						m_pServer.OnGetMessagesInfo(this,m_POP3_Messages);
					}
					else{
						this.Socket.BeginWriteLine("-ERR Authentication failed",new SocketCallBack(this.EndSend));
					}

					#endregion

					break;

				case "DIGEST-MD5":

					#region DIGEST-MD5 authentication

					/* RFC 2831 AUTH DIGEST-MD5
					 * 
					 * Example:
					 * 
					 * C: AUTH DIGEST-MD5
					 * S: + base64(realm="elwood.innosoft.com",nonce="OA6MG9tEQGm2hh",qop="auth",algorithm=md5-sess)
					 * C: base64(username="chris",realm="elwood.innosoft.com",nonce="OA6MG9tEQGm2hh",
					 *    nc=00000001,cnonce="OA6MHXh6VqTrRk",digest-uri="imap/elwood.innosoft.com",
                     *    response=d388dad90d4bbd760a152321f2143af7,qop=auth)
					 * S: + base64(rspauth=ea40f60335c427b5527b84dbabcdfffd)
					 * C:
					 * S: +OK Authentication successful.
					*/

					string realm = m_pServer.HostName;
					string nonce = AuthHelper.GenerateNonce();

					this.Socket.WriteLine("+ " + AuthHelper.Base64en(AuthHelper.Create_Digest_Md5_ServerResponse(realm,nonce)));

					string clientResponse = AuthHelper.Base64de(this.Socket.ReadLine());					
					// Check that realm and nonce in client response are same as we specified
					if(clientResponse.IndexOf("realm=\"" + realm + "\"") > - 1 && clientResponse.IndexOf("nonce=\"" + nonce + "\"") > - 1){
						// Parse user name and password compare value
				//		string userName  = "";
						string passwData = "";
						string cnonce = ""; 
						foreach(string clntRespParam in clientResponse.Split(',')){
							if(clntRespParam.StartsWith("username=")){
								userName = clntRespParam.Split(new char[]{'='},2)[1].Replace("\"","");
							}
							else if(clntRespParam.StartsWith("response=")){
								passwData = clntRespParam.Split(new char[]{'='},2)[1];
							}							
							else if(clntRespParam.StartsWith("cnonce=")){
								cnonce = clntRespParam.Split(new char[]{'='},2)[1].Replace("\"","");
							}
						}

						aArgs = m_pServer.OnAuthUser(this,userName,passwData,clientResponse,AuthType.DIGEST_MD5);

                        // There is custom error, return it
                        if(aArgs.ErrorText != null){
                            this.Socket.BeginWriteLine("-ERR " + aArgs.ErrorText,new SocketCallBack(this.EndSend));
                            return;
                        }

						if(aArgs.Validated){
							// Send server computed password hash
							this.Socket.WriteLine("+ " + AuthHelper.Base64en("rspauth=" + aArgs.ReturnData));
					
							// We must got empty line here
							clientResponse = this.Socket.ReadLine();
							if(clientResponse == ""){
								this.Socket.BeginWriteLine("+OK Authentication successful.",new SocketCallBack(this.EndSend));

                                this.SetUserName(userName);
							}
							else{
								this.Socket.BeginWriteLine("-ERR Authentication failed",new SocketCallBack(this.EndSend));
							}
						}
						else{
							this.Socket.BeginWriteLine("-ERR Authentication failed",new SocketCallBack(this.EndSend));
						}
					}
					else{
						this.Socket.BeginWriteLine("-ERR Authentication failed",new SocketCallBack(this.EndSend));
					}

					#endregion

					break;

				default:
					this.Socket.BeginWriteLine("-ERR Unrecognized authentication type.",new SocketCallBack(this.EndSend));
					break;
			}
			//-----------------------------------------------------------------//
		}

		#endregion

		#region method CAPA

		private void CAPA(string argsText)
		{
			/* Rfc 2449 5.  The CAPA Command
			
				The POP3 CAPA command returns a list of capabilities supported by the
				POP3 server.  It is available in both the AUTHORIZATION and
				TRANSACTION states.

				A capability description MUST document in which states the capability
				is announced, and in which states the commands are valid.

				Capabilities available in the AUTHORIZATION state MUST be announced
				in both states.

				If a capability is announced in both states, but the argument might
				differ after authentication, this possibility MUST be stated in the
				capability description.

				(These requirements allow a client to issue only one CAPA command if
				it does not use any TRANSACTION-only capabilities, or any
				capabilities whose values may differ after authentication.)

				If the authentication step negotiates an integrity protection layer,
				the client SHOULD reissue the CAPA command after authenticating, to
				check for active down-negotiation attacks.

				Each capability may enable additional protocol commands, additional
				parameters and responses for existing commands, or describe an aspect
				of server behavior.  These details are specified in the description
				of the capability.
				
				Section 3 describes the CAPA response using [ABNF].  When a
				capability response describes an optional command, the <capa-tag>
				SHOULD be identical to the command keyword.  CAPA response tags are
				case-insensitive.

				CAPA

				Arguments:
					none

				Restrictions:
					none

				Discussion:
					An -ERR response indicates the capability command is not
					implemented and the client will have to probe for
					capabilities as before.

					An +OK response is followed by a list of capabilities, one
					per line.  Each capability name MAY be followed by a single
					space and a space-separated list of parameters.  Each
					capability line is limited to 512 octets (including the
					CRLF).  The capability list is terminated by a line
					containing a termination octet (".") and a CRLF pair.

				Possible Responses:
					+OK -ERR

					Examples:
						C: CAPA
						S: +OK Capability list follows
						S: TOP
						S: USER
						S: SASL CRAM-MD5 KERBEROS_V4
						S: RESP-CODES
						S: LOGIN-DELAY 900
						S: PIPELINING
						S: EXPIRE 60
						S: UIDL
						S: IMPLEMENTATION Shlemazle-Plotz-v302
						S: .
			*/

			string capaResponse = "";
			capaResponse += "+OK Capability list follows\r\n";
			capaResponse += "PIPELINING\r\n";
			capaResponse += "UIDL\r\n";
			capaResponse += "TOP\r\n";
            if((m_pServer.SupportedAuthentications & SaslAuthTypes.Plain) != 0){
                capaResponse += "USER\r\n";
            }
            capaResponse += "SASL";
            if((m_pServer.SupportedAuthentications & SaslAuthTypes.Cram_md5) != 0){
                capaResponse += " CRAM-MD5";
            }
            if((m_pServer.SupportedAuthentications & SaslAuthTypes.Digest_md5) != 0){
                capaResponse += " DIGEST-MD5";
            }            
            if((m_pServer.SupportedAuthentications & SaslAuthTypes.Login) != 0){
                capaResponse += " LOGIN";
            }
            capaResponse += "\r\n";
            if(!this.Socket.SSL && this.BindInfo.SSL_Certificate != null){
                capaResponse += "STLS\r\n";
            }
			capaResponse += ".\r\n";
			

			this.Socket.BeginWriteLine(capaResponse,new SocketCallBack(this.EndSend));
		}

		#endregion

        #region method STLS

        private void STLS(string argsText)
        {
            /* RFC 2595 4. POP3 STARTTLS extension.
                 Arguments: none

                 Restrictions:
                     Only permitted in AUTHORIZATION state.

                 Discussion:
                     A TLS negotiation begins immediately after the CRLF at the
                     end of the +OK response from the server.  A -ERR response
                     MAY result if a security layer is already active.  Once a
                     client issues a STLS command, it MUST NOT issue further
                     commands until a server response is seen and the TLS
                     negotiation is complete.

                     The STLS command is only permitted in AUTHORIZATION state
                     and the server remains in AUTHORIZATION state, even if
                     client credentials are supplied during the TLS negotiation.
                     The AUTH command [POP-AUTH] with the EXTERNAL mechanism
                     [SASL] MAY be used to authenticate once TLS client
                     credentials are successfully exchanged, but servers
                     supporting the STLS command are not required to support the
                     EXTERNAL mechanism.

                     Once TLS has been started, the client MUST discard cached
                     information about server capabilities and SHOULD re-issue
                     the CAPA command.  This is necessary to protect against
                     man-in-the-middle attacks which alter the capabilities list
                     prior to STLS.  The server MAY advertise different
                     capabilities after STLS.

                 Possible Responses:
                     +OK -ERR

                 Examples:
                     C: STLS
                     S: +OK Begin TLS negotiation
                     <TLS negotiation, further commands are under TLS layer>
                       ...
                     C: STLS
                     S: -ERR Command not permitted when TLS active
            */

            if(this.Authenticated){
                this.Socket.WriteLine("-ERR STLS command is only permitted in AUTHORIZATION state !");
                return;
            }

            if(this.BindInfo.SSL_Certificate == null){
                this.Socket.WriteLine("-ERR TLS not available, SSL certificate isn't specified !");
				return;
            }

            this.Socket.WriteLine("+OK Ready to start TLS");

            try{          
                this.Socket.SwitchToSSL(this.BindInfo.SSL_Certificate);

                if(m_pServer.LogCommands){
                    this.Socket.Logger.AddTextEntry("TLS negotiation completed successfully.");
                }
            }
            catch(Exception x){
                this.Socket.WriteLine("-ERR TLS handshake failed ! " + x.Message);
            }

            Reset();

            BeginRecieveCmd();
        }

        #endregion


        #region method Reset

        private void Reset()
		{		
			/* RFC 1939 5. RSET
			Discussion:
				If any messages have been marked as deleted by the POP3
				server, they are unmarked.
			*/
			m_POP3_Messages.ResetDeletedFlag();
		}

		#endregion

		#region method Update

		private void Update()
		{
			/* RFC 1939 6.
			NOTE:
				When the client issues the QUIT command from the TRANSACTION state,
				the POP3 session enters the UPDATE state.  (Note that if the client
				issues the QUIT command from the AUTHORIZATION state, the POP3
				session terminates but does NOT enter the UPDATE state.)

				If a session terminates for some reason other than a client-issued
				QUIT command, the POP3 session does NOT enter the UPDATE state and
				MUST not remove any messages from the maildrop.
			*/

			if(this.Authenticated){
                lock(m_POP3_Messages){
				    // Delete all message which are marked for deletion ---//
				    foreach(POP3_Message msg in m_POP3_Messages){
					    if(msg.MarkedForDelete){
						    m_pServer.OnDeleteMessage(this,msg);
					    }
				    }
                    //-----------------------------------------------------//
                }				
			}
		}

		#endregion


		#region method EndSend
		
		/// <summary>
		/// Is called when asynchronous send completes.
		/// </summary>
		/// <param name="result">If true, then send was successfull.</param>
		/// <param name="count">Count sended.</param>
		/// <param name="exception">Exception happend on send. NOTE: available only is result=false.</param>
		/// <param name="tag">User data.</param>
		private void EndSend(SocketCallBackResult result,long count,Exception exception,object tag)
		{
			try{
				switch(result)
				{
					case SocketCallBackResult.Ok:
						BeginRecieveCmd();
						break;

					case SocketCallBackResult.SocketClosed:
						EndSession();
						break;

					case SocketCallBackResult.Exception:
						OnError(exception);
						break;
				}
			}
			catch(Exception x){
				OnError(x);
			}
		}

		#endregion		


		#region Properties Implementation

		#endregion
		
	}
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

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


Written By
Estonia Estonia
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions