/// <summary> /// Filters sender. /// </summary> /// <param name="from">Sender.</param> /// <param name="api">Reference to server API.</param> /// <param name="session">Reference to SMTP session.</param> /// <param name="errorText">Filtering error text what is returned to client. ASCII text, 100 chars maximum.</param> /// <returns>Returns true if sender is ok or false if rejected.</returns> public bool Filter(string from,IMailServerApi api,SMTP_Session session,out string errorText) { errorText = ""; string ip = session.RemoteEndPoint.Address.ToString(); Dns_Client dns = new Dns_Client(); bool ok = false; // Don't check PTR for authenticated session and LAN IP ranges if(session.Authenticated || ip.StartsWith("127.0.0.1") || ip.StartsWith("10.") || ip.StartsWith("192.168")){ return true; } DnsServerResponse reponse = dns.Query(ip,QTYPE.PTR); if(reponse.ResponseCode == RCODE.NO_ERROR){ foreach(PTR_Record rec in reponse.GetPTRRecords()){ if(rec.DomainName.ToLower() == session.EhloName.ToLower()){ ok = true; break; } } } if(!ok){ errorText = "Bad EHLO/HELO name, you must have valid DNS PTR record for your EHLO name and IP."; } return ok; }
/// <summary> /// Default constructor. /// </summary> /// <param name="session">Reference to calling SMTP session.</param> /// <param name="errorText">Gets errors what happened on storing message or null if no errors.</param> /// <param name="messageStream">Gets message stream where messages was stored. Stream postions is End of Stream, where message storing ended.</param> public MessageStoringCompleted_eArgs(SMTP_Session session,string errorText,Stream messageStream) { m_pSession = session; m_ErrorText = errorText; m_pMessageStream = messageStream; m_pCustomReply = new SmtpServerReply(); }
/// <summary> /// Default constructor. /// </summary> /// <param name="session">Reference to pop3 session.</param> /// <param name="userName">Username.</param> /// <param name="passwData">Password data.</param> /// <param name="data">Authentication specific data(as tag).</param> /// <param name="authType">Authentication type.</param> public AuthUser_EventArgs(SMTP_Session session,string userName,string passwData,string data,AuthType authType) { m_pSession = session; m_UserName = userName; m_PasswData = passwData; m_Data = data; m_AuthType = authType; }
/// <summary> /// Default constructor. /// </summary> /// <param name="session">Owner SMTP server session.</param> /// <exception cref="ArgumentNullException">Is raised when <b>session</b> is null reference.</exception> public SMTP_e_Message(SMTP_Session session) { if(session == null){ throw new ArgumentNullException("session"); } m_pSession = session; }
/// <summary> /// Default constructor. /// </summary> /// <param name="session">Owner SMTP server session.</param> /// <param name="reply">SMTP server reply.</param> /// <exception cref="ArgumentNullException">Is raised when <b>session</b> or <b>reply</b> is null reference.</exception> public SMTP_e_Started(SMTP_Session session,SMTP_Reply reply) { if(session == null){ throw new ArgumentNullException("session"); } if(reply == null){ throw new ArgumentNullException("reply"); } m_pSession = session; m_pReply = reply; }
/// <summary> /// Filters message. /// </summary> /// <param name="messageStream">Message stream which to filter.</param> /// <param name="filteredStream">Filtered stream.</param> /// <param name="sender">Senders email address.</param> /// <param name="recipients">Recipients email addresses.</param> /// <param name="api">Access to server API.</param> /// <param name="session">Reference to SMTP session.</param> /// <param name="errorText">Filtering error text what is returned to client. ASCII text, 500 chars maximum.</param> public FilterResult Filter(Stream messageStream,out Stream filteredStream,string sender,string[] recipients,IMailServerApi api,SMTP_Session session,out string errorText) { errorText = ""; filteredStream = messageStream; try{ messageStream.Position = 0; // long pos = messageStream.Position; string headers = MimeUtils.ParseHeaders(messageStream).ToLower(); // messageStream.Position = pos; //--- Check required header fields ---------// bool headersOk = true; if(headers.IndexOf("from:") == -1){ errorText = "Required From: header field is missing !"; headersOk = false; } else if(headers.IndexOf("to:") == -1){ errorText = "Required To: header field is missing !"; headersOk = false; } else if(headers.IndexOf("subject:") == -1){ errorText = "Required Subject: header field is missing !"; headersOk = false; } //------------------------------------------// // Check invalid <CR> or <LF> in headers. Header may not contain <CR> without <LF>, // <CRLF> must be in pairs. if(headers.Replace("\r\n","").IndexOf("\r") > -1 || headers.Replace("\r\n","").IndexOf("\n") > -1){ errorText = "Message contains invalid <CR> or <LF> combinations !"; headersOk = false; } //-------------------------------------------------------------------------------// if(!headersOk){ return FilterResult.Error; } } catch{ } // Reset stream position messageStream.Position = 0; return FilterResult.Store; }
/// <summary> /// Default constructor. /// </summary> /// <param name="session">Owner SMTP server session.</param> /// <param name="to">RCPT TO: value.</param> /// <param name="reply">SMTP server reply.</param> /// <exception cref="ArgumentNullException">Is raised when <b>session</b>, <b>to</b> or <b>reply</b> is null reference.</exception> public SMTP_e_RcptTo(SMTP_Session session,SMTP_RcptTo to,SMTP_Reply reply) { if(session == null){ throw new ArgumentNullException("session"); } if(to == null){ throw new ArgumentNullException("from"); } if(reply == null){ throw new ArgumentNullException("reply"); } m_pSession = session; m_pRcptTo = to; m_pReply = reply; }
/// <summary> /// Default constructor. /// </summary> /// <param name="session">Owner SMTP server session.</param> /// <param name="stream">Message stream.</param> /// <param name="reply">SMTP server reply.</param> /// <exception cref="ArgumentNullException">Is raised when <b>session</b>, <b>stream</b> or <b>reply</b> is null reference.</exception> public SMTP_e_MessageStored(SMTP_Session session,Stream stream,SMTP_Reply reply) { if(session == null){ throw new ArgumentNullException("session"); } if(stream == null){ throw new ArgumentNullException("stream"); } if(reply == null){ throw new ArgumentNullException("reply"); } m_pSession = session; m_pStream = stream; m_pReply = reply; }
/// <summary> /// Default constructor. /// </summary> /// <param name="session">Owner SMTP server session.</param> /// <param name="from">MAIL FROM: value.</param> /// <param name="reply">SMTP server reply.</param> /// <exception cref="ArgumentNullException">Is raised when <b>session</b>, <b>from</b> or <b>reply</b> is null reference.</exception> public SMTP_e_MailFrom(SMTP_Session session,SMTP_MailFrom from,SMTP_Reply reply) { if(session == null){ throw new ArgumentNullException("session"); } if(from == null){ throw new ArgumentNullException("from"); } if(reply == null){ throw new ArgumentNullException("reply"); } m_pSession = session; m_pMailFrom = from; m_pReply = reply; }
/// <summary> /// Default constructor. /// </summary> /// <param name="session">Owner SMTP server session.</param> /// <param name="domain">Ehlo/Helo domain name.</param> /// <param name="reply">SMTP server reply.</param> /// <exception cref="ArgumentNullException">Is raised when <b>session</b>, <b>domain</b> or <b>reply</b> is null reference.</exception> /// <exception cref="ArgumentException">Is raised when any of the arguments has invalid value.</exception> public SMTP_e_Ehlo(SMTP_Session session,string domain,SMTP_Reply reply) { if(session == null){ throw new ArgumentNullException("session"); } if(domain == null){ throw new ArgumentNullException("domain"); } if(domain == string.Empty){ throw new ArgumentException("Argument 'domain' value must be sepcified.","domain"); } if(reply == null){ throw new ArgumentNullException("reply"); } m_pSession = session; m_Domain = domain; m_pReply = reply; }
/// <summary> /// Filters sender. /// </summary> /// <param name="from">Sender.</param> /// <param name="api">Reference to server API.</param> /// <param name="session">Reference to SMTP session.</param> /// <param name="errorText">Filtering error text what is returned to client. ASCII text, 100 chars maximum.</param> /// <returns>Returns true if sender is ok or false if rejected.</returns> public bool Filter(string from, IMailServerApi api, SMTP_Session session, out string errorText) { errorText = null; bool ok = false; // Don't check authenticated users or LAN IP if (session.IsAuthenticated || IsPrivateIP(session.RemoteEndPoint.Address)) { return true; } try { using (new TransactionScope(TransactionScopeOption.Suppress)) { var d = new NKDC(ApplicationConnectionString, null); if (!(from o in d.ContactEmailsViews where o.Email == @from select o).Any()) { errorText = "You must be a registered user to use the email support service."; WriteFilterLog("Sender:" + from + " IP:" + session.RemoteEndPoint.Address.ToString() + " unregistered.\r\n"); return false; } else { return true; } } } catch (Exception ex) { WriteFilterLog(string.Format("Sender:{0} IP:{1} caused exception.\r\nEX:{2}{3}\r\n__\r\n", from, session.RemoteEndPoint.Address, ex, ex.Message)); } return ok; }
/// <summary> /// Raises event ValidateMailboxSize. /// </summary> /// <param name="session"></param> /// <param name="eAddress"></param> /// <param name="messageSize"></param> /// <returns></returns> internal bool Validate_MailBoxSize(SMTP_Session session,string eAddress,long messageSize) { ValidateMailboxSize_EventArgs oArgs = new ValidateMailboxSize_EventArgs(session,eAddress,messageSize); if(this.ValidateMailboxSize != null){ this.ValidateMailboxSize(this,oArgs); } return oArgs.IsValid; }
/// <summary> /// Raises event ValidateMailTo. /// </summary> /// <param name="session"></param> /// <param name="forward_path"></param> /// <param name="email"></param> /// <param name="authenticated"></param> /// <returns></returns> internal ValidateRecipient_EventArgs OnValidate_MailTo(SMTP_Session session,string forward_path,string email,bool authenticated) { ValidateRecipient_EventArgs oArg = new ValidateRecipient_EventArgs(session,email,authenticated); if(this.ValidateMailTo != null){ this.ValidateMailTo(this, oArg); } return oArg; }
/// <summary> /// Raises event ValidateMailFrom. /// </summary> /// <param name="session"></param> /// <param name="reverse_path"></param> /// <param name="email"></param> /// <returns></returns> internal ValidateSender_EventArgs OnValidate_MailFrom(SMTP_Session session,string reverse_path,string email) { ValidateSender_EventArgs oArg = new ValidateSender_EventArgs(session,email); if(this.ValidateMailFrom != null){ this.ValidateMailFrom(this, oArg); } return oArg; }
/// <summary> /// Raises event AuthUser. /// </summary> /// <param name="session">Reference to current smtp session.</param> /// <param name="userName">User name.</param> /// <param name="passwordData">Password compare data,it depends of authentication type.</param> /// <param name="data">For md5 eg. md5 calculation hash.It depends of authentication type.</param> /// <param name="authType">Authentication type.</param> /// <returns></returns> internal AuthUser_EventArgs OnAuthUser(SMTP_Session session,string userName,string passwordData,string data,AuthType authType) { AuthUser_EventArgs oArgs = new AuthUser_EventArgs(session,userName,passwordData,data,authType); if(this.AuthUser != null){ this.AuthUser(this,oArgs); } return oArgs; }
/// <summary> /// Raises event ValidateIP event. /// </summary> /// <param name="session">Reference to current smtp session.</param> internal ValidateIP_EventArgs OnValidate_IpAddress(SMTP_Session session) { ValidateIP_EventArgs oArg = new ValidateIP_EventArgs(session.LocalEndPoint,session.RemoteEndPoint); if(this.ValidateIPAddress != null){ this.ValidateIPAddress(this, oArg); } session.Tag = oArg.SessionTag; return oArg; }
/// <summary> /// Is called when SMTP server "final" response sending has completed. /// </summary> private void SendFinalResponseCompleted(SMTP_Session.SendResponseAsyncOP op) { if(op.Error != null){ m_pException = op.Error; } SetState(AsyncOP_State.Completed); op.Dispose(); }
/// <summary> /// Filters message. /// </summary> /// <param name="messageStream">Message stream which to filter.</param> /// <param name="filteredStream">Filtered stream.</param> /// <param name="sender">Senders email address.</param> /// <param name="recipients">Recipients email addresses.</param> /// <param name="api">Access to server API.</param> /// <param name="session">Reference to SMTP session.</param> /// <param name="errorText">Filtering error text what is returned to client. ASCII text, 500 chars maximum.</param> public FilterResult Filter(Stream messageStream,out Stream filteredStream,string sender,string[] recipients,IMailServerApi api,SMTP_Session session,out string errorText) { errorText = ""; messageStream.Position = 0; filteredStream = messageStream; // we don't change message content, just return same stream try{ //--- Load data ----------------------- DataSet ds = new DataSet(); DataTable dt = ds.Tables.Add("KewWords"); dt.Columns.Add("Cost",typeof(int)); dt.Columns.Add("KeyWord"); dt = ds.Tables.Add("ContentMd5"); dt.Columns.Add("Description"); dt.Columns.Add("EntryMd5Value"); ds.ReadXml(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) + "\\lsSpam_db.xml"); //--- Do mime parts data md5 hash compare ---------------- ArrayList entries = new ArrayList(); Mime parser = Mime.Parse(messageStream); System.Security.Cryptography.MD5 md5 = System.Security.Cryptography.MD5.Create(); foreach(MimeEntity ent in parser.MimeEntities){ if(ent.Data != null){ string md5Hash = Convert.ToBase64String(md5.ComputeHash(ent.Data)); foreach(DataRow dr in ds.Tables["ContentMd5"].Rows){ // Message contains blocked content(attachment,...) if(dr["EntryMd5Value"].ToString() == md5Hash){ WriteFilterLog(DateTime.Now.ToString() + " From:" + sender + " Subject:\"" + parser.MainEntity.Subject + "\" Contained blocked content:hash=" + md5Hash + "\r\n"); return FilterResult.DontStore; } } } } byte[] topLines = new byte[2000]; if(messageStream.Length < 2000){ topLines = new byte[messageStream.Length]; } messageStream.Read(topLines,0,topLines.Length); string lines = System.Text.Encoding.ASCII.GetString(topLines).ToLower(); //--- Try spam keywords ----------- int totalCost = 0; string keyWords = ""; DataView dv = ds.Tables["KewWords"].DefaultView; dv.Sort = "Cost DESC"; foreach(DataRowView drV in dv){ if(lines.IndexOf(drV.Row["KeyWord"].ToString().ToLower()) > -1){ totalCost += Convert.ToInt32(drV.Row["Cost"]); keyWords += drV.Row["KeyWord"].ToString() + " cost:" + drV.Row["Cost"].ToString() + " "; // Check that total cost isn't exceeded if(totalCost > 99){ errorText = "Message was blocked by server and considered as SPAM !"; WriteFilterLog(DateTime.Now.ToString() + " From:" + sender + " Blocked KeyWords: " + keyWords + "\r\n"); return FilterResult.Error; } } } //--------------------------------- // Reset stream position messageStream.Position = 0; return FilterResult.Store; } catch(Exception x){ return FilterResult.DontStore; } }
/// <summary> /// Checks if specified message matches to specified criteria. /// </summary> /// <param name="matchExpression">Match expression.</param> /// <param name="mailFrom">SMTP MAIL FROM: command email value.</param> /// <param name="rcptTo">SMTP RCPT TO: command email values.</param> /// <param name="smtpSession">SMTP current session.</param> /// <param name="mime">Message to match.</param> /// <param name="messageSize">Message size in bytes.</param> /// <returns>Returns true if message matches to specified criteria.</returns> public bool Match(string matchExpression,string mailFrom,string[] rcptTo,SMTP_Session smtpSession,Mail_Message mime,int messageSize) { LumiSoft.Net.StringReader r = new LumiSoft.Net.StringReader(matchExpression); return Match(false,r,mailFrom,rcptTo,smtpSession,mime,messageSize); }
/// <summary> /// Default constructor. /// </summary> /// <param name="session">Reference to smtp session.</param> /// <param name="mailTo">Recipient email address.</param> /// <param name="authenticated">Specifies if connected user is authenticated.</param> public ValidateRecipient_EventArgs(SMTP_Session session,string mailTo,bool authenticated) { m_pSession = session; m_MailTo = mailTo; m_Authenticated = authenticated; }
/// <summary> /// Starts operation processing. /// </summary> /// <param name="owner">Owner SMTP session.</param> /// <returns>Returns true if asynchronous operation in progress or false if operation completed synchronously.</returns> /// <exception cref="ArgumentNullException">Is raised when <b>owner</b> is null reference.</exception> public bool Start(SMTP_Session owner) { if(owner == null){ throw new ArgumentNullException("owner"); } m_pSession = owner; SetState(AsyncOP_State.Active); try{ // Build SMTP response. StringBuilder response = new StringBuilder(); foreach(SMTP_t_ReplyLine replyLine in m_pReplyLines){ response.Append(replyLine.ToString()); } byte[] buffer = Encoding.UTF8.GetBytes(response.ToString()); // Log m_pSession.LogAddWrite(buffer.Length,response.ToString()); // Start response sending. m_pSession.TcpStream.BeginWrite(buffer,0,buffer.Length,this.ResponseSendingCompleted,null); } catch(Exception x){ m_pException = x; m_pSession.LogAddException("Exception: " + m_pException.Message,m_pException); SetState(AsyncOP_State.Completed); } // Set flag rise CompletedAsync event flag. The event is raised when async op completes. // If already completed sync, that flag has no effect. lock(m_pLock){ m_RiseCompleted = true; return m_State == AsyncOP_State.Active; } }
/// <summary> /// Cleans up any resource being used. /// </summary> public void Dispose() { if(m_State == AsyncOP_State.Disposed){ return; } SetState(AsyncOP_State.Disposed); m_pException = null; m_pSession = null; this.CompletedAsync = null; }
/// <summary> /// Default constructor. /// </summary> /// <param name="session">Reference to smtp session.</param> /// <param name="eAddress">Email address of recipient.</param> /// <param name="messageSize">Message size.</param> public ValidateMailboxSize_EventArgs(SMTP_Session session,string eAddress,long messageSize) { m_pSession = session; m_eAddress = eAddress; m_MsgSize = messageSize; }
/// <summary> /// Filters sender. /// </summary> /// <param name="from">Sender.</param> /// <param name="api">Reference to server API.</param> /// <param name="session">Reference to SMTP session.</param> /// <param name="errorText">Filtering error text what is returned to client. ASCII text, 100 chars maximum.</param> /// <returns>Returns true if sender is ok or false if rejected.</returns> public bool Filter(string from,IMailServerApi api,SMTP_Session session,out string errorText) { errorText = null; bool ok = true; // Don't check authenticated users or LAN IP if(session.IsAuthenticated || IsPrivateIP(session.RemoteEndPoint.Address)){ return true; } try{ //--- Load data ----------------------- DataSet ds = new DataSet(); ds.Tables.Add("General"); ds.Tables["General"].Columns.Add("CheckHelo"); ds.Tables["General"].Columns.Add("LogRejections"); ds.Tables.Add("BlackListSettings"); ds.Tables["BlackListSettings"].Columns.Add("ErrorText"); ds.Tables.Add("BlackList"); ds.Tables["BlackList"].Columns.Add("IP"); ds.Tables.Add("Servers"); ds.Tables["Servers"].Columns.Add("Cost"); ds.Tables["Servers"].Columns.Add("Server"); ds.Tables["Servers"].Columns.Add("DefaultRejectionText"); ds.ReadXml(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) + "\\lsDNSBL_Filter_db.xml"); bool logRejections = false; #region General if(ds.Tables["General"].Rows.Count == 1){ if(Convert.ToBoolean(ds.Tables["General"].Rows[0]["CheckHelo"])){ DnsServerResponse response = Dns_Client.Static.Query(session.EhloHost,DNS_QType.A); // If dns server connection errors, don't block. if(response.ConnectionOk && response.ResponseCode != DNS_RCode.SERVER_FAILURE){ bool found = false; foreach(DNS_rr_A a in response.GetARecords()){ if(session.RemoteEndPoint.Address.Equals(a.IP)){ found = true; break; } } if(!found){ errorText = "Not valid DNS EHLO/HELO name for your IP '" + session.EhloHost + "' !"; return false; } } } logRejections = ConvertEx.ToBoolean(ds.Tables["General"].Rows[0]["LogRejections"]); } #endregion #region Balck List foreach(DataRow dr in ds.Tables["BlackList"].Rows){ if(IsAstericMatch(dr["IP"].ToString(),session.RemoteEndPoint.Address.ToString())){ errorText = ds.Tables["BlackListSettings"].Rows[0]["ErrorText"].ToString(); return false; } } #endregion #region DNSBL foreach(DataRow dr in ds.Tables["Servers"].Rows){ DnsServerResponse dnsResponse = Dns_Client.Static.Query(ReverseIP(session.RemoteEndPoint.Address) + "." + dr["Server"].ToString(),DNS_QType.ANY); DNS_rr_A[] recs = dnsResponse.GetARecords(); if(recs.Length > 0){ if(logRejections){ WriteFilterLog("Sender:" + from + " IP:" + session.RemoteEndPoint.Address.ToString() + " blocked\r\n"); } errorText = dr["DefaultRejectionText"].ToString(); // Server provided return text, use it if(dnsResponse.GetTXTRecords().Length > 0){ errorText = dnsResponse.GetTXTRecords()[0].Text; } if(errorText == ""){ errorText = "You are in '" + dr["Server"].ToString() + "' rejection list !"; } return false; } } #endregion } catch{ } return ok; }
/// <summary> /// Is called when SMTP server 354 response sending has completed. /// </summary> /// <param name="op">Asynchronous operation.</param> private void Send354ResponseCompleted(SMTP_Session.SendResponseAsyncOP op) { try{ // RFC 5321.4.4 trace info. byte[] recevived = m_pSession.CreateReceivedHeader(); m_pSession.m_pMessageStream.Write(recevived,0,recevived.Length); // Create asynchronous read period-terminated opeartion. SmartStream.ReadPeriodTerminatedAsyncOP readPeriodTermOP = new SmartStream.ReadPeriodTerminatedAsyncOP( m_pSession.m_pMessageStream, m_pSession.Server.MaxMessageSize, SizeExceededAction.JunkAndThrowException ); // This event is raised only if read period-terminated opeartion completes asynchronously. readPeriodTermOP.Completed += new EventHandler<EventArgs<SmartStream.ReadPeriodTerminatedAsyncOP>>(delegate(object sender,EventArgs<SmartStream.ReadPeriodTerminatedAsyncOP> e){ MessageReadingCompleted(readPeriodTermOP); }); // Read period-terminated completed synchronously. if(m_pSession.TcpStream.ReadPeriodTerminated(readPeriodTermOP,true)){ MessageReadingCompleted(readPeriodTermOP); } } catch(Exception x){ m_pException = x; m_pSession.LogAddException("Exception: " + m_pException.Message,m_pException); SetState(AsyncOP_State.Completed); } op.Dispose(); }
/// <summary> /// Raises event GetMessageStoreStream. /// </summary> /// <param name="session">Reference to calling SMTP session.</param> /// <returns></returns> internal GetMessageStoreStream_eArgs OnGetMessageStoreStream(SMTP_Session session) { GetMessageStoreStream_eArgs eArgs = new GetMessageStoreStream_eArgs(session); if(this.GetMessageStoreStream != null){ this.GetMessageStoreStream(this,eArgs); } return eArgs; }
/// <summary> /// Raises event MessageStoringCompleted. /// </summary> /// <param name="session">Reference to calling SMTP session.</param> /// <param name="errorText">Null if no errors, otherwise conatians error text. If errors happened that means that messageStream is incomplete.</param> /// <param name="messageStream">Stream where message was stored.</param> internal MessageStoringCompleted_eArgs OnMessageStoringCompleted(SMTP_Session session,string errorText,Stream messageStream) { MessageStoringCompleted_eArgs eArgs = new MessageStoringCompleted_eArgs(session,errorText,messageStream); if(this.MessageStoringCompleted != null){ this.MessageStoringCompleted(this,eArgs); } return eArgs; }
/// <summary> /// Checks if specified message matches to specified criteria. /// </summary> /// <param name="syntaxCheckOnly">Specifies if syntax check is only done. If true no matching is done.</param> /// <param name="r">Match expression reader what contains match expression.</param> /// <param name="mailFrom">SMTP MAIL FROM: command email value.</param> /// <param name="rcptTo">SMTP RCPT TO: command email values.</param> /// <param name="smtpSession">SMTP current session.</param> /// <param name="mime">Message to match.</param> /// <param name="messageSize">Message size in bytes.</param> /// <returns>Returns true if message matches to specified criteria.</returns> private bool Match(bool syntaxCheckOnly,LumiSoft.Net.StringReader r,string mailFrom,string[] rcptTo,SMTP_Session smtpSession,Mail_Message mime,int messageSize) { /* Possible keywords order At first there can be NOT,parethesized or matcher After NOT, parethesized or matcher After matcher, AND or OR After OR, NOT,parethesized or matcher After AND, NOT,parethesized or matcher After parethesized, NOT or matcher */ PossibleClauseItem possibleClauseItems = PossibleClauseItem.Parenthesizes | PossibleClauseItem.NOT | PossibleClauseItem.Matcher; bool lastMatchValue = false; // Empty string passed r.ReadToFirstChar(); if(r.Available == 0){ throw new Exception("Invalid syntax: '" + ClauseItemsToString(possibleClauseItems) + "' expected !"); } // Parse while there are expressions or get error while(r.Available > 0){ r.ReadToFirstChar(); // Syntax check must consider that there is alwas match !!! if(syntaxCheckOnly){ lastMatchValue = true; } #region () Groupped matchers // () Groupped matchers if(r.StartsWith("(")){ lastMatchValue = Match(syntaxCheckOnly,new LumiSoft.Net.StringReader(r.ReadParenthesized()),mailFrom,rcptTo,smtpSession,mime,messageSize); possibleClauseItems = PossibleClauseItem.Parenthesizes | PossibleClauseItem.Matcher | PossibleClauseItem.NOT; } #endregion #region AND clause // AND clause else if(r.StartsWith("and",false)){ // See if AND allowed if((possibleClauseItems & PossibleClauseItem.AND) == 0){ throw new Exception("Invalid syntax: '" + ClauseItemsToString(possibleClauseItems) + "' expected !"); } // Last match value is false, no need to check next conditions if(!lastMatchValue){ return false; } // Remove AND r.ReadWord(); r.ReadToFirstChar(); lastMatchValue = Match(syntaxCheckOnly,r,mailFrom,rcptTo,smtpSession,mime,messageSize); possibleClauseItems = PossibleClauseItem.Parenthesizes | PossibleClauseItem.Matcher | PossibleClauseItem.NOT; } #endregion #region OR clause // OR clause else if(r.StartsWith("or",false)){ // See if OR allowed if((possibleClauseItems & PossibleClauseItem.OR) == 0){ throw new Exception("Invalid syntax: '" + ClauseItemsToString(possibleClauseItems) + "' expected !"); } // Remove OR r.ReadWord(); r.ReadToFirstChar(); // Last match value is false, then we need to check next condition. // Otherwise OR is matched already, just eat next matcher. if(lastMatchValue){ // Skip next clause Match(syntaxCheckOnly,r,mailFrom,rcptTo,smtpSession,mime,messageSize); } else{ lastMatchValue = Match(syntaxCheckOnly,r,mailFrom,rcptTo,smtpSession,mime,messageSize); } possibleClauseItems = PossibleClauseItem.Parenthesizes | PossibleClauseItem.Matcher | PossibleClauseItem.NOT; } #endregion #region NOT clause // NOT clause else if(r.StartsWith("not",false)){ // See if NOT allowed if((possibleClauseItems & PossibleClauseItem.NOT) == 0){ throw new Exception("Invalid syntax: '" + ClauseItemsToString(possibleClauseItems) + "' expected !"); } // Remove NOT r.ReadWord(); r.ReadToFirstChar(); // Just reverse match result value lastMatchValue = !Match(syntaxCheckOnly,r,mailFrom,rcptTo,smtpSession,mime,messageSize); possibleClauseItems = PossibleClauseItem.Parenthesizes | PossibleClauseItem.Matcher; } #endregion else{ // See if matcher allowed if((possibleClauseItems & PossibleClauseItem.Matcher) == 0){ throw new Exception("Invalid syntax: '" + ClauseItemsToString(possibleClauseItems) + "' expected ! \r\n\r\n Near: '" + r.OriginalString.Substring(0,r.Position) + "'"); } // 1) matchsource // 2) keyword // Read match source string word = r.ReadWord(); if(word == null){ throw new Exception("Invalid syntax: matcher is missing !"); } word = word.ToLower(); string[] matchSourceValues = new string[]{}; #region smtp.mail_from // SMTP command MAIL FROM: value. // smtp.mail_from if(word == "smtp.mail_from"){ if(!syntaxCheckOnly){ matchSourceValues = new string[]{mailFrom}; } } #endregion #region smtp.rcpt_to // SMTP command RCPT TO: values. // smtp.mail_to else if(word == "smtp.rcpt_to"){ if(!syntaxCheckOnly){ matchSourceValues = rcptTo; } } #endregion #region smtp.ehlo // SMTP command EHLO/HELO: value. // smtp.ehlo else if(word == "smtp.ehlo"){ if(!syntaxCheckOnly){ matchSourceValues = new string[]{smtpSession.EhloHost}; } } #endregion #region smtp.authenticated // Specifies if SMTP session is authenticated. // smtp.authenticated else if(word == "smtp.authenticated"){ if(!syntaxCheckOnly){ if(smtpSession != null){ matchSourceValues = new string[]{smtpSession.IsAuthenticated.ToString()}; } } } #endregion #region smtp.user // SMTP authenticated user name. Empy string "" if not authenticated. // smtp.user else if(word == "smtp.user"){ if(!syntaxCheckOnly){ if(smtpSession != null && smtpSession.AuthenticatedUserIdentity != null){ matchSourceValues = new string[]{smtpSession.AuthenticatedUserIdentity.Name}; } } } #endregion #region smtp.remote_ip // SMTP session connected client IP address. // smtp.remote_ip else if(word == "smtp.remote_ip"){ if(!syntaxCheckOnly){ if(smtpSession != null){ matchSourceValues = new string[]{smtpSession.RemoteEndPoint.Address.ToString()}; } } } #endregion #region message.size // Message size in bytes. // message.size else if(word == "message.size"){ if(!syntaxCheckOnly){ matchSourceValues = new string[]{messageSize.ToString()}; } } #endregion #region message.header <SP> "HeaderFieldName:" // Message main header header field. If multiple header fields, then all are checked. // message.header <SP> "HeaderFieldName:" else if(word == "message.header"){ string headerFieldName = r.ReadWord(); if(headerFieldName == null){ throw new Exception("Match source MainHeaderField HeaderFieldName is missing ! Syntax:{MainHeaderField <SP> \"HeaderFieldName:\"}"); } if(!syntaxCheckOnly){ if(mime.Header.Contains(headerFieldName)){ MIME_h[] fields = mime.Header[headerFieldName]; matchSourceValues = new string[fields.Length]; for(int i=0;i<matchSourceValues.Length;i++){ matchSourceValues[i] = fields[i].ValueToString(); } } } } #endregion #region message.all_headers <SP> "HeaderFieldName:" // Any mime entity header header field. If multiple header fields, then all are checked. // message.all_headers <SP> "HeaderFieldName:" else if(word == "message.all_headers"){ string headerFieldName = r.ReadWord(); if(headerFieldName == null){ throw new Exception("Match source MainHeaderField HeaderFieldName is missing ! Syntax:{MainHeaderField <SP> \"HeaderFieldName:\"}"); } if(!syntaxCheckOnly){ List<string> values = new List<string>(); foreach(MIME_Entity entity in mime.AllEntities){ if(entity.Header.Contains(headerFieldName)){ MIME_h[] fields = entity.Header[headerFieldName]; for(int i=0;i<fields.Length;i++){ values.Add(fields[i].ValueToString()); } } } matchSourceValues = values.ToArray(); } } #endregion #region message.body_text // Message body text. // message.body_text else if(word == "message.body_text"){ if(!syntaxCheckOnly){ matchSourceValues = new string[]{mime.BodyText}; } } #endregion #region message.body_html // Message body html. // message.body_html else if(word == "message.body_html"){ if(!syntaxCheckOnly){ matchSourceValues = new string[]{mime.BodyHtmlText}; } } #endregion #region message.content_md5 // Message any mime entity decoded data MD5 hash. // message.content_md5 else if(word == "message.content_md5"){ if(!syntaxCheckOnly){ List<string> values = new List<string>(); foreach(MIME_Entity entity in mime.AllEntities){ try{ if(entity.Body is MIME_b_SinglepartBase){ byte[] data = ((MIME_b_SinglepartBase)entity.Body).Data; if(data != null){ System.Security.Cryptography.MD5CryptoServiceProvider md5 = new System.Security.Cryptography.MD5CryptoServiceProvider(); values.Add(System.Text.Encoding.Default.GetString(md5.ComputeHash(data))); } } } catch{ // Message data parsing failed, just skip that entity md5 } } matchSourceValues = values.ToArray(); } } #endregion #region sys.date_time // System current date time. Format: yyyy.MM.dd HH:mm:ss. // sys.date_time else if(word == "sys.date_time"){ if(!syntaxCheckOnly){ matchSourceValues = new string[]{DateTime.Now.ToString("dd.MM.yyyy HH:mm:ss")}; } } #endregion #region sys.date // System current date. Format: yyyy.MM.dd. // sys.date else if(word == "sys.date"){ if(!syntaxCheckOnly){ matchSourceValues = new string[]{DateTime.Today.ToString("dd.MM.yyyy")}; } } #endregion #region sys.time // System current time. Format: HH:mm:ss. // sys.time else if(word == "sys.time"){ if(!syntaxCheckOnly){ matchSourceValues = new string[]{DateTime.Now.ToString("HH:mm:ss")}; } } #endregion #region sys.day_of_week // Day of week. Days: sunday,monday,tuesday,wednesday,thursday,friday,saturday. // sys.day_of_week else if(word == "sys.day_of_week"){ if(!syntaxCheckOnly){ matchSourceValues = new string[]{DateTime.Today.DayOfWeek.ToString()}; } } #endregion /* // Day of month. Format: 1 - 31. If no so much days in month, then replaced with month max days. // sys.day_of_month else if(word == "sys.day_of_month"){ } */ #region sys.day_of_year // Month of year. Format: 1 - 12. // sys.day_of_year else if(word == "sys.day_of_year"){ if(!syntaxCheckOnly){ matchSourceValues = new string[]{DateTime.Today.ToString("M")}; } } #endregion #region Unknown // Unknown else{ throw new Exception("Unknown match source '" + word + "' !"); } #endregion /* If we reach so far, then we have valid match sorce and compare value. Just do compare. */ // Reset lastMatch result lastMatchValue = false; // Read matcher word = r.ReadWord(true,new char[]{' '},true); if(word == null){ throw new Exception("Invalid syntax: operator is missing ! \r\n\r\n Near: '" + r.OriginalString.Substring(0,r.Position) + "'"); } word = word.ToLower(); #region * <SP> "astericPattern" // * <SP> "astericPattern" if(word == "*"){ string val = r.ReadWord(); if(val == null){ throw new Exception("Invalid syntax: <SP> \"value\" is missing !"); } val = val.ToLower(); if(!syntaxCheckOnly){ // We check matchSourceValues when first is found foreach(string matchSourceValue in matchSourceValues){ if(SCore.IsAstericMatch(val,matchSourceValue.ToLower())){ lastMatchValue = true; break; } } } } #endregion #region !* <SP> "astericPattern" // !* <SP> "astericPattern" else if(word == "!*"){ string val = r.ReadWord(); if(val == null){ throw new Exception("Invalid syntax: <SP> \"value\" is missing !"); } val = val.ToLower(); if(!syntaxCheckOnly){ // We check matchSourceValues when first is found foreach(string matchSourceValue in matchSourceValues){ if(SCore.IsAstericMatch(val,matchSourceValue.ToLower())){ lastMatchValue = false; break; } } } } #endregion #region == <SP> "value" // == <SP> "value" else if(word == "=="){ string val = r.ReadWord(); if(val == null){ throw new Exception("Invalid syntax: <SP> \"value\" is missing !"); } val = val.ToLower(); if(!syntaxCheckOnly){ // We check matchSourceValues when first is found foreach(string matchSourceValue in matchSourceValues){ if(val == matchSourceValue.ToLower()){ lastMatchValue = true; break; } } } } #endregion #region != <SP> "value" // != <SP> "value" else if(word == "!="){ string val = r.ReadWord(); if(val == null){ throw new Exception("Invalid syntax: <SP> \"value\" is missing !"); } val = val.ToLower(); if(!syntaxCheckOnly){ // We check matchSourceValues when first is found, then already value equals foreach(string matchSourceValue in matchSourceValues){ if(val == matchSourceValue.ToLower()){ lastMatchValue = false; break; } lastMatchValue = true; } } } #endregion #region >= <SP> "value" // >= <SP> "value" else if(word == ">="){ string val = r.ReadWord(); if(val == null){ throw new Exception("Invalid syntax: <SP> \"value\" is missing !"); } val = val.ToLower(); if(!syntaxCheckOnly){ // We check matchSourceValues when first is found foreach(string matchSourceValue in matchSourceValues){ if(matchSourceValue.ToLower().CompareTo(val) >= 0){ lastMatchValue = true; break; } } } } #endregion #region <= <SP> "value" // <= <SP> "value" else if(word == "<="){ string val = r.ReadWord(); if(val == null){ throw new Exception("Invalid syntax: <SP> \"value\" is missing !"); } val = val.ToLower(); if(!syntaxCheckOnly){ // We check matchSourceValues when first is found foreach(string matchSourceValue in matchSourceValues){ if(matchSourceValue.ToLower().CompareTo(val) <= 0){ lastMatchValue = true; break; } } } } #endregion #region > <SP> "value" // > <SP> "value" else if(word == ">"){ string val = r.ReadWord(); if(val == null){ throw new Exception("Invalid syntax: <SP> \"value\" is missing !"); } val = val.ToLower(); if(!syntaxCheckOnly){ // We check matchSourceValues when first is found foreach(string matchSourceValue in matchSourceValues){ if(matchSourceValue.ToLower().CompareTo(val) > 0){ lastMatchValue = true; break; } } } } #endregion #region < <SP> "value" // < <SP> "value" else if(word == "<"){ string val = r.ReadWord(); if(val == null){ throw new Exception("Invalid syntax: <SP> \"value\" is missing !"); } val = val.ToLower(); if(!syntaxCheckOnly){ // We check matchSourceValues when first is found foreach(string matchSourceValue in matchSourceValues){ if(matchSourceValue.ToLower().CompareTo(val) < 0){ lastMatchValue = true; break; } } } } #endregion #region regex <SP> "value" // Regex <SP> "value" else if(word == "regex"){ string val = r.ReadWord(); if(val == null){ throw new Exception("Invalid syntax: <SP> \"value\" is missing !"); } val = val.ToLower(); if(!syntaxCheckOnly){ // We check matchSourceValues when first is found foreach(string matchSourceValue in matchSourceValues){ if(Regex.IsMatch(val,matchSourceValue.ToLower())){ lastMatchValue = true; break; } } } } #endregion #region Unknown // Unknown else{ throw new Exception("Unknown keword '" + word + "' !"); } #endregion possibleClauseItems = PossibleClauseItem.AND | PossibleClauseItem.OR; } } return lastMatchValue; }
/// <summary> /// Initialize and start new session here. Session isn't added to session list automatically, /// session must add itself to server session list by calling AddSession(). /// </summary> /// <param name="socket">Connected client socket.</param> /// <param name="bindInfo">BindInfo what accepted socket.</param> protected override void InitNewSession(Socket socket,IPBindInfo bindInfo) { // Check maximum conncurent connections from 1 IP. if(m_MaxConnectionsPerIP > 0){ lock(this.Sessions){ int nSessions = 0; foreach(SocketServerSession s in this.Sessions){ IPEndPoint ipEndpoint = s.RemoteEndPoint; if(ipEndpoint != null){ if(ipEndpoint.Address.Equals(((IPEndPoint)socket.RemoteEndPoint).Address)){ nSessions++; } } // Maimum allowed exceeded if(nSessions >= m_MaxConnectionsPerIP){ socket.Send(System.Text.Encoding.ASCII.GetBytes("421 Maximum connections from your IP address is exceeded, try again later !\r\n")); socket.Shutdown(SocketShutdown.Both); socket.Close(); return; } } } } string sessionID = Guid.NewGuid().ToString(); SocketEx socketEx = new SocketEx(socket); if(LogCommands){ socketEx.Logger = new SocketLogger(socket,this.SessionLog); socketEx.Logger.SessionID = sessionID; } SMTP_Session session = new SMTP_Session(sessionID,socketEx,bindInfo,this); }
/// <summary> /// Starts operation processing. /// </summary> /// <param name="owner">Owner SMTP session.</param> /// <param name="cmdText">SMTP client command text.</param> /// <returns>Returns true if asynchronous operation in progress or false if operation completed synchronously.</returns> /// <exception cref="ArgumentNullException">Is raised when <b>owner</b> is null reference.</exception> public bool Start(SMTP_Session owner,string cmdText) { if(owner == null){ throw new ArgumentNullException("owner"); } m_pSession = owner; m_StartTime = DateTime.Now; SetState(AsyncOP_State.Active); try{ /* RFC 5321 4.1.1.4. The receiver normally sends a 354 response to DATA, and then treats the lines (strings ending in <CRLF> sequences, as described in Section 2.3.7) following the command as mail data from the sender. This command causes the mail data to be appended to the mail data buffer. The mail data may contain any of the 128 ASCII character codes, although experience has indicated that use of control characters other than SP, HT, CR, and LF may cause problems and SHOULD be avoided when possible. The custom of accepting lines ending only in <LF>, as a concession to non-conforming behavior on the part of some UNIX systems, has proven to cause more interoperability problems than it solves, and SMTP server systems MUST NOT do this, even in the name of improved robustness. In particular, the sequence "<LF>.<LF>" (bare line feeds, without carriage returns) MUST NOT be treated as equivalent to <CRLF>.<CRLF> as the end of mail data indication. Receipt of the end of mail data indication requires the server to process the stored mail transaction information. This processing consumes the information in the reverse-path buffer, the forward-path buffer, and the mail data buffer, and on the completion of this command these buffers are cleared. If the processing is successful, the receiver MUST send an OK reply. If the processing fails, the receiver MUST send a failure reply. The SMTP model does not allow for partial failures at this point: either the message is accepted by the server for delivery and a positive response is returned or it is not accepted and a failure reply is returned. In sending a positive "250 OK" completion reply to the end of data indication, the receiver takes full responsibility for the message (see Section 6.1). Errors that are diagnosed subsequently MUST be reported in a mail message, as discussed in Section 4.4. When the SMTP server accepts a message either for relaying or for final delivery, it inserts a trace record (also referred to interchangeably as a "time stamp line" or "Received" line) at the top of the mail data. This trace record indicates the identity of the host that sent the message, the identity of the host that received the message (and is inserting this time stamp), and the date and time the message was received. Relayed messages will have multiple time stamp lines. Details for formation of these lines, including their syntax, is specified in Section 4.4. */ // RFC 5321 3.1. if(m_pSession.m_SessionRejected){ SendFinalResponse(new SMTP_t_ReplyLine(503,"Bad sequence of commands: Session rejected.",true)); } // RFC 5321 4.1.4. else if(string.IsNullOrEmpty(m_pSession.m_EhloHost)){ SendFinalResponse(new SMTP_t_ReplyLine(503,"Bad sequence of commands: Send EHLO/HELO first.",true)); } // RFC 5321 4.1.4. else if(m_pSession.m_pFrom == null){ SendFinalResponse(new SMTP_t_ReplyLine(503,"Bad sequence of commands: Send 'MAIL FROM:' first.",true)); } // RFC 5321 4.1.4. else if(m_pSession.m_pTo.Count == 0){ SendFinalResponse(new SMTP_t_ReplyLine(503,"Bad sequence of commands: Send 'RCPT TO:' first.",true)); } else if(!string.IsNullOrEmpty(cmdText)){ SendFinalResponse(new SMTP_t_ReplyLine(500,"Command line syntax error.",true)); } else{ // Get message store stream. m_pSession.m_pMessageStream = m_pSession.OnGetMessageStream(); if(m_pSession.m_pMessageStream == null){ m_pSession.m_pMessageStream = new MemoryStreamEx(32000); } // Send "354 Start mail input; end with <CRLF>.<CRLF>". SMTP_Session.SendResponseAsyncOP sendResponseOP = new SendResponseAsyncOP(new SMTP_t_ReplyLine(354,"Start mail input; end with <CRLF>.<CRLF>",true)); sendResponseOP.CompletedAsync += delegate(object sender,EventArgs<SendResponseAsyncOP> e){ Send354ResponseCompleted(sendResponseOP); }; if(!m_pSession.SendResponseAsync(sendResponseOP)){ Send354ResponseCompleted(sendResponseOP); } } } catch(Exception x){ m_pException = x; m_pSession.LogAddException("Exception: " + m_pException.Message,m_pException); SetState(AsyncOP_State.Completed); } // Set flag rise CompletedAsync event flag. The event is raised when async op completes. // If already completed sync, that flag has no effect. lock(m_pLock){ m_RiseCompleted = true; return m_State == AsyncOP_State.Active; } }