using System; using System.Collections.Generic; using System.IO; using System.Net.Security; using System.Net.Sockets; using System.Text; namespace Roblox.Common.Mail.Pop3; public class Pop3MailClient : IDisposable { protected string popServer; protected int port; private bool useSSL; private bool isAutoReconnect; private bool isTimeoutReconnect; protected int readTimeout = -1; protected string username; protected string password; protected Pop3ConnectionStateEnum pop3ConnectionState = Pop3ConnectionStateEnum.Disconnected; private TcpClient serverTcpConnection; private Stream pop3Stream; protected StreamReader pop3StreamReader; protected string CRLF = "\r\n"; protected bool isTraceRawEmail; protected StringBuilder RawEmailSB; public bool isDebug; public string PopServer => popServer; public int Port => port; public bool UseSSL => useSSL; public bool IsAutoReconnect { get { return isAutoReconnect; } set { isAutoReconnect = value; } } public int ReadTimeout { get { return readTimeout; } set { readTimeout = value; if (pop3Stream != null && pop3Stream.CanTimeout) { pop3Stream.ReadTimeout = readTimeout; } } } public string Username => username; public string Password => password; public Pop3ConnectionStateEnum Pop3ConnectionState => pop3ConnectionState; public event WarningHandler Warning; public event TraceHandler Trace; protected void CallWarning(string methodName, string response, string warningText, params object[] warningParameters) { warningText = string.Format(warningText, warningParameters); if (this.Warning != null) { this.Warning(methodName + ": " + warningText, response); } CallTrace("!! {0}", warningText); } protected void CallTrace(string text, params object[] parameters) { if (this.Trace != null) { this.Trace(DateTime.Now.ToString("hh:mm:ss ") + popServer + " " + string.Format(text, parameters)); } } protected void TraceFrom(string text, params object[] parameters) { if (this.Trace != null) { CallTrace(" " + string.Format(text, parameters)); } } protected void setPop3ConnectionState(Pop3ConnectionStateEnum State) { pop3ConnectionState = State; CallTrace(" Pop3MailClient Connection State {0} reached", State); } protected void EnsureState(Pop3ConnectionStateEnum requiredState) { if (pop3ConnectionState != requiredState) { throw new Pop3Exception("GetMailboxStats only accepted during connection state: " + requiredState.ToString() + "\n The connection to server " + popServer + " is in state " + pop3ConnectionState); } } public Pop3MailClient(string PopServer, int Port, bool useSSL, string Username, string Password) { popServer = PopServer; port = Port; this.useSSL = useSSL; username = Username; password = Password; } public void Connect() { if (pop3ConnectionState != Pop3ConnectionStateEnum.Disconnected && pop3ConnectionState != Pop3ConnectionStateEnum.Closed && !isTimeoutReconnect) { CallWarning("connect", "", "Connect command received, but connection state is: " + pop3ConnectionState); return; } try { CallTrace(" Connect at port {0}", port); serverTcpConnection = new TcpClient(popServer, port); } catch (Exception ex5) { throw new Pop3Exception("Connection to server " + popServer + ", port " + port + " failed.\nRuntime Error: " + ex5.ToString()); } if (useSSL) { try { CallTrace(" Get SSL connection"); pop3Stream = new SslStream(serverTcpConnection.GetStream(), leaveInnerStreamOpen: false); pop3Stream.ReadTimeout = readTimeout; } catch (Exception ex4) { throw new Pop3Exception("Server " + popServer + " found, but cannot get SSL data stream.\nRuntime Error: " + ex4.ToString()); } try { CallTrace(" Get SSL authentication"); ((SslStream)pop3Stream).AuthenticateAsClient(popServer); } catch (Exception ex3) { throw new Pop3Exception("Server " + popServer + " found, but problem with SSL Authentication.\nRuntime Error: " + ex3.ToString()); } } else { try { CallTrace(" Get connection without SSL"); pop3Stream = serverTcpConnection.GetStream(); pop3Stream.ReadTimeout = readTimeout; } catch (Exception ex2) { throw new Pop3Exception("Server " + popServer + " found, but cannot get data stream (without SSL).\nRuntime Error: " + ex2.ToString()); } } try { pop3StreamReader = new StreamReader(pop3Stream, Encoding.ASCII); } catch (Exception ex) { if (useSSL) { throw new Pop3Exception("Server " + popServer + " found, but cannot read from SSL stream.\nRuntime Error: " + ex.ToString()); } throw new Pop3Exception("Server " + popServer + " found, but cannot read from stream (without SSL).\nRuntime Error: " + ex.ToString()); } if (!readSingleLine(out var response)) { throw new Pop3Exception("Server " + popServer + " not ready to start AUTHORIZATION.\nMessage: " + response); } setPop3ConnectionState(Pop3ConnectionStateEnum.Authorization); if (!executeCommand("USER " + username, out response)) { throw new Pop3Exception("Server " + popServer + " doesn't accept username '" + username + "'.\nMessage: " + response); } if (!executeCommand("PASS " + password, out response)) { throw new Pop3Exception("Server " + popServer + " doesn't accept password '" + password + "' for user '" + username + "'.\nMessage: " + response); } setPop3ConnectionState(Pop3ConnectionStateEnum.Connected); } public void Disconnect() { if (pop3ConnectionState == Pop3ConnectionStateEnum.Disconnected || pop3ConnectionState == Pop3ConnectionStateEnum.Closed) { CallWarning("disconnect", "", "Disconnect received, but was already disconnected."); return; } try { if (executeCommand("QUIT", out var response)) { setPop3ConnectionState(Pop3ConnectionStateEnum.Closed); return; } CallWarning("Disconnect", response, "negative response from server while closing connection: " + response); setPop3ConnectionState(Pop3ConnectionStateEnum.Disconnected); } finally { if (pop3Stream != null) { pop3Stream.Close(); } pop3StreamReader.Close(); } } public bool DeleteEmail(int msg_number) { EnsureState(Pop3ConnectionStateEnum.Connected); if (!executeCommand("DELE " + msg_number, out var response)) { CallWarning("DeleteEmail", response, "negative response for email (Id: {0}) delete request", msg_number); return false; } return true; } public bool GetEmailIdList(out List EmailIds) { EnsureState(Pop3ConnectionStateEnum.Connected); EmailIds = new List(); if (!executeCommand("LIST", out var response)) { CallWarning("GetEmailIdList", response, "negative response for email list request"); return false; } while (readMultiLine(out response)) { if (int.TryParse(response.Split(' ')[0], out var EmailId)) { EmailIds.Add(EmailId); } else { CallWarning("GetEmailIdList", response, "first characters should be integer (EmailId)"); } } TraceFrom("{0} email ids received", EmailIds.Count); return true; } public int GetEmailSize(int msg_number) { EnsureState(Pop3ConnectionStateEnum.Connected); executeCommand("LIST " + msg_number, out var response); int EmailSize = 0; string[] responseSplit = response.Split(' '); if (responseSplit.Length < 2 || !int.TryParse(responseSplit[2], out EmailSize)) { CallWarning("GetEmailSize", response, "'+OK int int' format expected (EmailId, EmailSize)"); } return EmailSize; } public bool GetUniqueEmailIdList(out List EmailIds) { EnsureState(Pop3ConnectionStateEnum.Connected); EmailIds = new List(); if (!executeCommand("UIDL ", out var response)) { CallWarning("GetUniqueEmailIdList", response, "negative response for email list request"); return false; } while (readMultiLine(out response)) { string[] responseSplit = response.Split(' '); int EmailId; if (responseSplit.Length < 2) { CallWarning("GetUniqueEmailIdList", response, "response not in format 'int string'"); } else if (!int.TryParse(responseSplit[0], out EmailId)) { CallWarning("GetUniqueEmailIdList", response, "first charaters should be integer (Unique EmailId)"); } else { EmailIds.Add(new EmailUid(EmailId, responseSplit[1])); } } TraceFrom("{0} unique email ids received", EmailIds.Count); return true; } public bool GetUniqueEmailIdList(out SortedList EmailIds) { EnsureState(Pop3ConnectionStateEnum.Connected); EmailIds = new SortedList(); if (!executeCommand("UIDL", out var response)) { CallWarning("GetUniqueEmailIdList", response, "negative response for email list request"); return false; } while (readMultiLine(out response)) { string[] responseSplit = response.Split(' '); int EmailId; if (responseSplit.Length < 2) { CallWarning("GetUniqueEmailIdList", response, "response not in format 'int string'"); } else if (!int.TryParse(responseSplit[0], out EmailId)) { CallWarning("GetUniqueEmailIdList", response, "first charaters should be integer (Unique EmailId)"); } else { EmailIds.Add(responseSplit[1], EmailId); } } TraceFrom("{0} unique email ids received", EmailIds.Count); return true; } public int GetUniqueEmailId(EmailUid msg_number) { EnsureState(Pop3ConnectionStateEnum.Connected); executeCommand("LIST " + msg_number, out var response); int EmailSize = 0; string[] responseSplit = response.Split(' '); if (responseSplit.Length < 2 || !int.TryParse(responseSplit[2], out EmailSize)) { CallWarning("GetEmailSize", response, "'+OK int int' format expected (EmailId, EmailSize)"); } return EmailSize; } public bool NOOP() { EnsureState(Pop3ConnectionStateEnum.Connected); if (!executeCommand("NOOP", out var response)) { CallWarning("NOOP", response, "negative response for NOOP request"); return false; } return true; } public bool GetRawEmail(int MessageNo, out string EmailText) { if (!SendRetrCommand(MessageNo)) { EmailText = null; return false; } int LineCounter = 0; if (RawEmailSB == null) { RawEmailSB = new StringBuilder(100000); } else { RawEmailSB.Length = 0; } isTraceRawEmail = true; string response; while (readMultiLine(out response)) { LineCounter++; } EmailText = RawEmailSB.ToString(); TraceFrom("email with {0} lines, {1} chars received", LineCounter.ToString(), EmailText.Length); return true; } public bool UndeleteAllEmails() { EnsureState(Pop3ConnectionStateEnum.Connected); string response; return executeCommand("RSET", out response); } public bool GetMailboxStats(out int NumberOfMails, out int MailboxSize) { EnsureState(Pop3ConnectionStateEnum.Connected); NumberOfMails = 0; MailboxSize = 0; if (executeCommand("STAT", out var response)) { string[] responseParts = response.Split(' '); if (responseParts.Length < 2) { throw new Pop3Exception("Server " + popServer + " sends illegally formatted response.\nExpected format: +OK int int\nReceived response: " + response); } NumberOfMails = int.Parse(responseParts[1]); MailboxSize = int.Parse(responseParts[2]); return true; } return false; } protected bool SendRetrCommand(int MessageNo) { EnsureState(Pop3ConnectionStateEnum.Connected); if (!executeCommand("RETR " + MessageNo, out var response)) { CallWarning("GetRawEmail", response, "negative response for email (ID: {0}) request", MessageNo); return false; } return true; } private bool executeCommand(string command, out string response) { byte[] commandBytes = Encoding.ASCII.GetBytes((command + CRLF).ToCharArray()); CallTrace("Tx '{0}'", command); bool isSupressThrow = false; try { pop3Stream.Write(commandBytes, 0, commandBytes.Length); if (isDebug) { isDebug = false; throw new IOException("Test", new SocketException(10053)); } } catch (IOException ex2) { if (!executeReconnect(ex2, command, commandBytes)) { throw; } } pop3Stream.Flush(); response = null; try { response = pop3StreamReader.ReadLine(); } catch (IOException ex) { if (!executeReconnect(ex, command, commandBytes)) { throw; } response = pop3StreamReader.ReadLine(); } if (response == null) { throw new Pop3Exception("Server " + popServer + " has not responded, timeout has occured."); } CallTrace("Rx '{0}'", response); if (response.Length > 0) { return response[0] == '+'; } return false; } private bool executeReconnect(IOException ex, string command, byte[] commandBytes) { if (ex.InnerException != null && ex.InnerException is SocketException) { SocketException innerEx = (SocketException)ex.InnerException; if (innerEx.ErrorCode == 10053) { CallWarning("ExecuteCommand", "", "probably timeout occured"); if (isAutoReconnect) { isTimeoutReconnect = true; try { CallTrace(" try to auto reconnect"); Connect(); CallTrace(" reconnect successful, try to resend command"); CallTrace("Tx '{0}'", command); pop3Stream.Write(commandBytes, 0, commandBytes.Length); pop3Stream.Flush(); return true; } finally { isTimeoutReconnect = false; } } } } return false; } protected bool readSingleLine(out string response) { response = null; try { response = pop3StreamReader.ReadLine(); } catch (Exception ex) { string s = ex.Message; } if (response == null) { throw new Pop3Exception("Server " + popServer + " has not responded, timeout has occured."); } CallTrace("Rx '{0}'", response); if (response.Length > 0) { return response[0] == '+'; } return false; } protected bool readMultiLine(out string response) { response = null; response = pop3StreamReader.ReadLine(); if (response == null) { throw new Pop3Exception("Server " + popServer + " has not responded, probably timeout has occured."); } if (isTraceRawEmail) { RawEmailSB.Append(response + CRLF); } if (response.Length > 0 && response[0] == '.') { if (response == ".") { return false; } response = response.Substring(1, response.Length - 1); } return true; } public void Dispose() { Disconnect(); } }