private void AUTH(string cmdText) { // RFC 5321 3.1. if(m_SessionRejected){ WriteLine("503 Bad sequence of commands: Session rejected."); return; } /* RFC 4954 AUTH mechanism [initial-response] Arguments: mechanism: A string identifying a [SASL] authentication mechanism. initial-response: An optional initial client response. If present, this response MUST be encoded as described in Section 4 of [BASE64] or contain a single character "=". Restrictions: After an AUTH command has been successfully completed, no more AUTH commands may be issued in the same session. After a successful AUTH command completes, a server MUST reject any further AUTH commands with a 503 reply. The AUTH command is not permitted during a mail transaction. An AUTH command issued during a mail transaction MUST be rejected with a 503 reply. A server challenge is sent as a 334 reply with the text part containing the [BASE64] encoded string supplied by the SASL mechanism. This challenge MUST NOT contain any text other than the BASE64 encoded challenge. In SMTP, a server challenge that contains no data is defined as a 334 reply with no text part. Note that there is still a space following the reply code, so the complete response line is "334 ". If the client wishes to cancel the authentication exchange, it issues a line with a single "*". If the server receives such a response, it MUST reject the AUTH command by sending a 501 reply. */ if(this.IsAuthenticated){ WriteLine("503 Bad sequence of commands: you are already authenticated."); return; } if(m_pFrom != null){ WriteLine("503 Bad sequence of commands: The AUTH command is not permitted during a mail transaction."); return; } #region Parse parameters string[] arguments = cmdText.Split(' '); if(arguments.Length > 2){ WriteLine("501 Syntax error, syntax: AUTH SP mechanism [SP initial-response] CRLF"); return; } byte[] initialClientResponse = new byte[0]; if(arguments.Length == 2){ if(arguments[1] == "="){ // Skip. } else{ try{ initialClientResponse = Convert.FromBase64String(arguments[1]); } catch{ WriteLine("501 Syntax error: Parameter 'initial-response' value must be BASE64 or contain a single character '='."); return; } } } string mechanism = arguments[0]; #endregion if(!this.Authentications.ContainsKey(mechanism)){ WriteLine("501 Not supported authentication mechanism."); return; } byte[] clientResponse = initialClientResponse; AUTH_SASL_ServerMechanism auth = this.Authentications[mechanism]; auth.Reset(); while(true){ byte[] serverResponse = auth.Continue(clientResponse); // Authentication completed. if(auth.IsCompleted){ if(auth.IsAuthenticated){ m_pUser = new GenericIdentity(auth.UserName,"SASL-" + auth.Name); WriteLine("235 2.7.0 Authentication succeeded."); } else{ WriteLine("535 5.7.8 Authentication credentials invalid."); } break; } // Authentication continues. else{ // Send server challange. if(serverResponse.Length == 0){ WriteLine("334 "); } else{ WriteLine("334 " + Convert.ToBase64String(serverResponse)); } // Read client response. SmartStream.ReadLineAsyncOP readLineOP = new SmartStream.ReadLineAsyncOP(new byte[32000],SizeExceededAction.JunkAndThrowException); this.TcpStream.ReadLine(readLineOP,false); if(readLineOP.Error != null){ throw readLineOP.Error; } // Log if(this.Server.Logger != null){ this.Server.Logger.AddRead(this.ID,this.AuthenticatedUserIdentity,readLineOP.BytesInBuffer,"base64 auth-data",this.LocalEndPoint,this.RemoteEndPoint); } // Client canceled authentication. if(readLineOP.LineUtf8 == "*"){ WriteLine("501 Authentication canceled."); return; } // We have base64 client response, decode it. else{ try{ clientResponse = Convert.FromBase64String(readLineOP.LineUtf8); } catch{ WriteLine("501 Invalid client response '" + clientResponse + "'."); return; } } } } }
private void AUTH(string cmdText) { /* 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(m_SessionRejected){ WriteLine("-ERR Bad sequence of commands: Session rejected."); return; } if(this.IsAuthenticated){ this.TcpStream.WriteLine("-ERR Re-authentication error."); return; } string mechanism = cmdText; /* MS specific or someone knows where in RFC let me know about this. Empty AUTH commands causes authentication mechanisms listing. C: AUTH S: PLAIN S: . http://msdn.microsoft.com/en-us/library/cc239199.aspx */ if(string.IsNullOrEmpty(mechanism)){ StringBuilder resp = new StringBuilder(); resp.Append("+OK\r\n"); foreach(AUTH_SASL_ServerMechanism m in m_pAuthentications.Values){ resp.Append(m.Name + "\r\n"); } resp.Append(".\r\n"); WriteLine(resp.ToString()); return; } if(!this.Authentications.ContainsKey(mechanism)){ WriteLine("-ERR Not supported authentication mechanism."); return; } byte[] clientResponse = new byte[0]; AUTH_SASL_ServerMechanism auth = this.Authentications[mechanism]; auth.Reset(); while(true){ byte[] serverResponse = auth.Continue(clientResponse); // Authentication completed. if(auth.IsCompleted){ if(auth.IsAuthenticated){ m_pUser = new GenericIdentity(auth.UserName,"SASL-" + auth.Name); // Get mailbox messages. POP3_e_GetMessagesInfo eMessages = OnGetMessagesInfo(); int seqNo = 1; foreach(POP3_ServerMessage message in eMessages.Messages){ message.SequenceNumber = seqNo++; m_pMessages.Add(message.UID,message); } WriteLine("+OK Authentication succeeded."); } else{ WriteLine("-ERR Authentication credentials invalid."); } break; } // Authentication continues. else{ // Send server challange. if(serverResponse.Length == 0){ WriteLine("+ "); } else{ WriteLine("+ " + Convert.ToBase64String(serverResponse)); } // Read client response. SmartStream.ReadLineAsyncOP readLineOP = new SmartStream.ReadLineAsyncOP(new byte[32000],SizeExceededAction.JunkAndThrowException); this.TcpStream.ReadLine(readLineOP,false); if(readLineOP.Error != null){ throw readLineOP.Error; } // Log if(this.Server.Logger != null){ this.Server.Logger.AddRead(this.ID,this.AuthenticatedUserIdentity,readLineOP.BytesInBuffer,"base64 auth-data",this.LocalEndPoint,this.RemoteEndPoint); } // Client canceled authentication. if(readLineOP.LineUtf8 == "*"){ WriteLine("-ERR Authentication canceled."); return; } // We have base64 client response, decode it. else{ try{ clientResponse = Convert.FromBase64String(readLineOP.LineUtf8); } catch{ WriteLine("-ERR Invalid client response '" + clientResponse + "'."); return; } } } } }
/// <summary> /// Parses MIME header from the specified stream. /// </summary> /// <param name="stream">MIME header stream.</param> /// <param name="encoding">Headers fields reading encoding. If not sure, UTF-8 is recommended.</param> /// <exception cref="ArgumentNullException">Is raised when <b>stream</b> or <b>encoding</b> is null.</exception> public void Parse(SmartStream stream,Encoding encoding) { if(stream == null){ throw new ArgumentNullException("stream"); } if(encoding == null){ throw new ArgumentNullException("encoding"); } StringBuilder currentHeader = new StringBuilder(); SmartStream.ReadLineAsyncOP readLineOP = new SmartStream.ReadLineAsyncOP(new byte[4*1024*1024],SizeExceededAction.ThrowException); while(true){ stream.ReadLine(readLineOP,false); if(readLineOP.Error != null){ throw readLineOP.Error; } // We reached end of stream. else if(readLineOP.BytesInBuffer == 0){ if(currentHeader.Length > 0){ Add(currentHeader.ToString()); } m_IsModified = false; return; } // We got blank header terminator line. else if(readLineOP.LineBytesInBuffer == 0){ if(currentHeader.Length > 0){ Add(currentHeader.ToString()); } m_IsModified = false; return; } else{ string line = encoding.GetString(readLineOP.Buffer,0,readLineOP.BytesInBuffer); // New header field starts. if(currentHeader.Length == 0){ currentHeader.Append(line); } // Header field continues. else if(char.IsWhiteSpace(line[0])){ currentHeader.Append(line); } // Current header field closed, new starts. else{ Add(currentHeader.ToString()); currentHeader = new StringBuilder(); currentHeader.Append(line); } } } }
/// <summary> /// Starts reading incoming command from the connected client. /// </summary> private void BeginReadCmd() { if(this.IsDisposed){ return; } try{ SmartStream.ReadLineAsyncOP readLineOP = new SmartStream.ReadLineAsyncOP(new byte[32000],SizeExceededAction.JunkAndThrowException); // This event is raised only when read next coomand completes asynchronously. readLineOP.Completed += new EventHandler<EventArgs<SmartStream.ReadLineAsyncOP>>(delegate(object sender,EventArgs<SmartStream.ReadLineAsyncOP> e){ if(ProcessCmd(readLineOP)){ BeginReadCmd(); } }); // Process incoming commands while, command reading completes synchronously. while(this.TcpStream.ReadLine(readLineOP,true)){ if(!ProcessCmd(readLineOP)){ break; } } } catch(Exception x){ OnError(x); } }
/// <summary> /// Default constructor. /// </summary> /// <param name="stream">Stream from where to read body part.</param> /// <param name="boundary">Boundry ID what separates body parts.</param> /// <exception cref="ArgumentNullException">Is raised when <b>stream</b> or <b>boundary</b> is null reference.</exception> public _MultipartReader(SmartStream stream,string boundary) { if(stream == null){ throw new ArgumentNullException("stream"); } if(boundary == null){ throw new ArgumentNullException("boundary"); } m_pStream = stream; m_Boundary = boundary; m_pReadLineOP = new SmartStream.ReadLineAsyncOP(new byte[stream.LineBufferSize],SizeExceededAction.ThrowException); m_pTextPreamble = new StringBuilder(); m_pTextEpilogue = new StringBuilder(); }
private void AUTHENTICATE(string cmdTag,string cmdText) { /* RFC 3501 6.2.2. AUTHENTICATE Command. Arguments: authentication mechanism name Responses: continuation data can be requested Result: OK - authenticate completed, now in authenticated state NO - authenticate failure: unsupported authentication mechanism, credentials rejected BAD - command unknown or arguments invalid, authentication exchange cancelled The AUTHENTICATE command indicates a [SASL] authentication mechanism to the server. If the server supports the requested authentication mechanism, it performs an authentication protocol exchange to authenticate and identify the client. It MAY also negotiate an OPTIONAL security layer for subsequent protocol interactions. If the requested authentication mechanism is not supported, the server SHOULD reject the AUTHENTICATE command by sending a tagged NO response. The AUTHENTICATE command does not support the optional "initial response" feature of [SASL]. Section 5.1 of [SASL] specifies how to handle an authentication mechanism which uses an initial response. The service name specified by this protocol's profile of [SASL] is "imap". The authentication protocol exchange consists of a series of server challenges and client responses that are specific to the authentication mechanism. A server challenge consists of a command continuation request response with the "+" token followed by a BASE64 encoded string. The client response consists of a single line consisting of a BASE64 encoded string. If the client wishes to cancel an authentication exchange, it issues a line consisting of a single "*". If the server receives such a response, it MUST reject the AUTHENTICATE command by sending a tagged BAD response. If a security layer is negotiated through the [SASL] authentication exchange, it takes effect immediately following the CRLF that concludes the authentication exchange for the client, and the CRLF of the tagged OK response for the server. While client and server implementations MUST implement the AUTHENTICATE command itself, it is not required to implement any authentication mechanisms other than the PLAIN mechanism described in [IMAP-TLS]. Also, an authentication mechanism is not required to support any security layers. Note: a server implementation MUST implement a configuration in which it does NOT permit any plaintext password mechanisms, unless either the STARTTLS command has been negotiated or some other mechanism that protects the session from password snooping has been provided. Server sites SHOULD NOT use any configuration which permits a plaintext password mechanism without such a protection mechanism against password snooping. Client and server implementations SHOULD implement additional [SASL] mechanisms that do not use plaintext passwords, such the GSSAPI mechanism described in [SASL] and/or the [DIGEST-MD5] mechanism. Servers and clients can support multiple authentication mechanisms. The server SHOULD list its supported authentication mechanisms in the response to the CAPABILITY command so that the client knows which authentication mechanisms to use. A server MAY include a CAPABILITY response code in the tagged OK response of a successful AUTHENTICATE command in order to send capabilities automatically. It is unnecessary for a client to send a separate CAPABILITY command if it recognizes these automatic capabilities. This should only be done if a security layer was not negotiated by the AUTHENTICATE command, because the tagged OK response as part of an AUTHENTICATE command is not protected by encryption/integrity checking. [SASL] requires the client to re-issue a CAPABILITY command in this case. The authorization identity passed from the client to the server during the authentication exchange is interpreted by the server as the user name whose privileges the client is requesting. Example: S: * OK IMAP4rev1 Server C: A001 AUTHENTICATE GSSAPI S: + C: YIIB+wYJKoZIhvcSAQICAQBuggHqMIIB5qADAgEFoQMCAQ6iBw MFACAAAACjggEmYYIBIjCCAR6gAwIBBaESGxB1Lndhc2hpbmd0 b24uZWR1oi0wK6ADAgEDoSQwIhsEaW1hcBsac2hpdmFtcy5jYW Mud2FzaGluZ3Rvbi5lZHWjgdMwgdCgAwIBAaEDAgEDooHDBIHA cS1GSa5b+fXnPZNmXB9SjL8Ollj2SKyb+3S0iXMljen/jNkpJX AleKTz6BQPzj8duz8EtoOuNfKgweViyn/9B9bccy1uuAE2HI0y C/PHXNNU9ZrBziJ8Lm0tTNc98kUpjXnHZhsMcz5Mx2GR6dGknb I0iaGcRerMUsWOuBmKKKRmVMMdR9T3EZdpqsBd7jZCNMWotjhi vd5zovQlFqQ2Wjc2+y46vKP/iXxWIuQJuDiisyXF0Y8+5GTpAL pHDc1/pIGmMIGjoAMCAQGigZsEgZg2on5mSuxoDHEA1w9bcW9n FdFxDKpdrQhVGVRDIzcCMCTzvUboqb5KjY1NJKJsfjRQiBYBdE NKfzK+g5DlV8nrw81uOcP8NOQCLR5XkoMHC0Dr/80ziQzbNqhx O6652Npft0LQwJvenwDI13YxpwOdMXzkWZN/XrEqOWp6GCgXTB vCyLWLlWnbaUkZdEYbKHBPjd8t/1x5Yg== S: + YGgGCSqGSIb3EgECAgIAb1kwV6ADAgEFoQMCAQ+iSzBJoAMC AQGiQgRAtHTEuOP2BXb9sBYFR4SJlDZxmg39IxmRBOhXRKdDA0 uHTCOT9Bq3OsUTXUlk0CsFLoa8j+gvGDlgHuqzWHPSQg== C: S: + YDMGCSqGSIb3EgECAgIBAAD/////6jcyG4GE3KkTzBeBiVHe ceP2CWY0SR0fAQAgAAQEBAQ= C: YDMGCSqGSIb3EgECAgIBAAD/////3LQBHXTpFfZgrejpLlLImP wkhbfa2QteAQAgAG1yYwE= S: A001 OK GSSAPI authentication successful Note: The line breaks within server challenges and client responses are for editorial clarity and are not in real authenticators. */ /* RFC 4959.l7 SASL-IR 7. The following syntax specification uses the Augmented Backus-Naur Form [RFC4234] notation. [RFC3501] defines the non-terminals capability, auth-type, and base64. capability =/ "SASL-IR" authenticate = "AUTHENTICATE" SP auth-type [SP (base64 / "=")] *(CRLF base64) ;;redefine AUTHENTICATE from [RFC3501] */ if(m_SessionRejected){ m_pResponseSender.SendResponseAsync(new IMAP_r_ServerStatus(cmdTag,"NO","Bad sequence of commands: Session rejected.")); return; } if(this.IsAuthenticated){ m_pResponseSender.SendResponseAsync(new IMAP_r_ServerStatus(cmdTag,"NO","Re-authentication error.")); return; } #region Parse parameters string[] arguments = cmdText.Split(' '); if(arguments.Length > 2){ m_pResponseSender.SendResponseAsync(new IMAP_r_ServerStatus(cmdTag,"BAD","Error in arguments.")); return; } byte[] initialClientResponse = new byte[0]; if(arguments.Length == 2){ if(arguments[1] == "="){ // Skip. } else{ try{ initialClientResponse = Convert.FromBase64String(arguments[1]); } catch{ m_pResponseSender.SendResponseAsync(new IMAP_r_ServerStatus(cmdTag,"BAD","Syntax error: Parameter 'initial-response' value must be BASE64 or contain a single character '='.")); return; } } } string mechanism = arguments[0]; #endregion if(!this.Authentications.ContainsKey(mechanism)){ m_pResponseSender.SendResponseAsync(new IMAP_r_ServerStatus(cmdTag,"NO","Not supported authentication mechanism.")); return; } byte[] clientResponse = initialClientResponse; AUTH_SASL_ServerMechanism auth = this.Authentications[mechanism]; auth.Reset(); while(true){ byte[] serverResponse = auth.Continue(clientResponse); // Authentication completed. if(auth.IsCompleted){ if(auth.IsAuthenticated){ m_pUser = new GenericIdentity(auth.UserName,"SASL-" + auth.Name); m_pResponseSender.SendResponseAsync(new IMAP_r_ServerStatus(cmdTag,"OK","Authentication succeeded.")); } else{ m_pResponseSender.SendResponseAsync(new IMAP_r_ServerStatus(cmdTag,"OK","Authentication credentials invalid.")); } break; } // Authentication continues. else{ // Send server challange. if(serverResponse.Length == 0){ m_pResponseSender.SendResponseAsync(new IMAP_r_ServerStatus("+","")); } else{ m_pResponseSender.SendResponseAsync(new IMAP_r_ServerStatus("+",Convert.ToBase64String(serverResponse))); } // Read client response. SmartStream.ReadLineAsyncOP readLineOP = new SmartStream.ReadLineAsyncOP(new byte[32000],SizeExceededAction.JunkAndThrowException); this.TcpStream.ReadLine(readLineOP,false); if(readLineOP.Error != null){ throw readLineOP.Error; } // Log if(this.Server.Logger != null){ this.Server.Logger.AddRead(this.ID,this.AuthenticatedUserIdentity,readLineOP.BytesInBuffer,"base64 auth-data",this.LocalEndPoint,this.RemoteEndPoint); } // Client canceled authentication. if(readLineOP.LineUtf8 == "*"){ m_pResponseSender.SendResponseAsync(new IMAP_r_ServerStatus(cmdTag,"NO","Authentication canceled.")); return; } // We have base64 client response, decode it. else{ try{ clientResponse = Convert.FromBase64String(readLineOP.LineUtf8); } catch{ m_pResponseSender.SendResponseAsync(new IMAP_r_ServerStatus(cmdTag,"NO","Invalid client response '" + clientResponse + "'.")); return; } } } } }
private void APPEND(string cmdTag,string cmdText) { /* RFC 3501 6.3.11. APPEND Command. Arguments: mailbox name OPTIONAL flag parenthesized list OPTIONAL date/time string message literal Responses: no specific responses for this command Result: OK - append completed NO - append error: can't append to that mailbox, error in flags or date/time or message text BAD - command unknown or arguments invalid The APPEND command appends the literal argument as a new message to the end of the specified destination mailbox. This argument SHOULD be in the format of an [RFC-2822] message. 8-bit characters are permitted in the message. A server implementation that is unable to preserve 8-bit data properly MUST be able to reversibly convert 8-bit APPEND data to 7-bit using a [MIME-IMB] content transfer encoding. Note: There MAY be exceptions, e.g., draft messages, in which required [RFC-2822] header lines are omitted in the message literal argument to APPEND. The full implications of doing so MUST be understood and carefully weighed. If a flag parenthesized list is specified, the flags SHOULD be set in the resulting message; otherwise, the flag list of the resulting message is set to empty by default. In either case, the Recent flag is also set. If a date-time is specified, the internal date SHOULD be set in the resulting message; otherwise, the internal date of the resulting message is set to the current date and time by default. If the append is unsuccessful for any reason, the mailbox MUST be restored to its state before the APPEND attempt; no partial appending is permitted. If the destination mailbox does not exist, a server MUST return an error, and MUST NOT automatically create the mailbox. Unless it is certain that the destination mailbox can not be created, the server MUST send the response code "[TRYCREATE]" as the prefix of the text of the tagged NO response. This gives a hint to the client that it can attempt a CREATE command and retry the APPEND if the CREATE is successful. If the mailbox is currently selected, the normal new message actions SHOULD occur. Specifically, the server SHOULD notify the client immediately via an untagged EXISTS response. If the server does not do so, the client MAY issue a NOOP command (or failing that, a CHECK command) after one or more APPEND commands. Example: C: A003 APPEND saved-messages (\Seen) {310} S: + Ready for literal data C: Date: Mon, 7 Feb 1994 21:52:25 -0800 (PST) C: From: Fred Foobar <*****@*****.**> C: Subject: afternoon meeting C: To: [email protected] C: Message-Id: <*****@*****.**> C: MIME-Version: 1.0 C: Content-Type: TEXT/PLAIN; CHARSET=US-ASCII C: C: Hello Joe, do you think we can meet at 3:30 tomorrow? C: S: A003 OK APPEND completed Note: The APPEND command is not used for message delivery, because it does not provide a mechanism to transfer [SMTP] envelope information. */ if(!this.IsAuthenticated){ m_pResponseSender.SendResponseAsync(new IMAP_r_ServerStatus(cmdTag,"NO","Authentication required.")); return; } // Store start time long startTime = DateTime.Now.Ticks; #region Parse arguments StringReader r = new StringReader(cmdText); r.ReadToFirstChar(); string folder = null; if(r.StartsWith("\"")){ folder = IMAP_Utils.DecodeMailbox(r.ReadWord()); } else{ folder = IMAP_Utils.DecodeMailbox(r.QuotedReadToDelimiter(' ')); } r.ReadToFirstChar(); List<string> flags = new List<string>(); if(r.StartsWith("(")){ foreach(string f in r.ReadParenthesized().Split(' ')){ if(f.Length > 0 && !flags.Contains(f.Substring(1))){ flags.Add(f.Substring(1)); } } } r.ReadToFirstChar(); DateTime date = DateTime.MinValue; if(!r.StartsWith("{")){ date = IMAP_Utils.ParseDate(r.ReadWord()); } int size = Convert.ToInt32(r.ReadParenthesized()); if(r.Available > 0){ m_pResponseSender.SendResponseAsync(new IMAP_r_ServerStatus(cmdTag,"BAD","Error in arguments.")); return; } #endregion IMAP_e_Append e = OnAppend(folder,flags.ToArray(),date,size,new IMAP_r_ServerStatus(cmdTag,"OK","APPEND command completed in %exectime seconds.")); if(e.Response.IsError){ m_pResponseSender.SendResponseAsync(e.Response); } else if(e.Stream == null){ m_pResponseSender.SendResponseAsync(new IMAP_r_ServerStatus(cmdTag,"NO","Internal server error: No storage stream available.")); } else{ m_pResponseSender.SendResponseAsync(new IMAP_r_ServerStatus("+","Ready for literal data.")); // Create callback which is called when BeginReadFixedCount completes. AsyncCallback readLiteralCompletedCallback = delegate(IAsyncResult ar){ try{ this.TcpStream.EndReadFixedCount(ar); // Log. LogAddRead(size,"Readed " + size + " bytes."); // TODO: Async // Read command line terminating CRLF. SmartStream.ReadLineAsyncOP readLineOP = new SmartStream.ReadLineAsyncOP(new byte[32000],SizeExceededAction.JunkAndThrowException); this.TcpStream.ReadLine(readLineOP,false); // Read command line terminating CRLF failed. if(readLineOP.Error != null){ OnError(readLineOP.Error); } // Read command line terminating CRLF succeeded. else{ LogAddRead(readLineOP.BytesInBuffer,readLineOP.LineUtf8); // Raise Completed event. e.OnCompleted(); m_pResponseSender.SendResponseAsync(IMAP_r_ServerStatus.Parse(e.Response.ToString().TrimEnd().Replace("%exectime",((DateTime.Now.Ticks - startTime) / (decimal)10000000).ToString("f2")))); BeginReadCmd(); } } catch(Exception x){ OnError(x); } }; this.TcpStream.BeginReadFixedCount(e.Stream,size,readLiteralCompletedCallback,null); } }
/// <summary> /// Deletes specified folder. /// </summary> /// <param name="folderName">Folder name.</param> /// <exception cref="ObjectDisposedException">Is raised when this object is disposed and this method is accessed.</exception> /// <exception cref="InvalidOperationException">Is raised when IMAP client is not connected and authenticated.</exception> public void DeleteFolder(string folderName) { if (this.IsDisposed) { throw new ObjectDisposedException(this.GetType().Name); } if (!this.IsConnected) { throw new InvalidOperationException("You must connect first."); } if (!this.IsAuthenticated) { throw new InvalidOperationException("The DELETE command is only valid in authenticated state."); } string line = GetNextCmdTag() + " DELETE " + TextUtils.QuoteString(Core.Encode_IMAP_UTF7_String(folderName)); int countWritten = this.TcpStream.WriteLine(line); LogAddWrite(countWritten, line); SmartStream.ReadLineAsyncOP args = new SmartStream.ReadLineAsyncOP(new byte[32000], SizeExceededAction.JunkAndThrowException); this.TcpStream.ReadLine(args, false); if (args.Error != null) { throw args.Error; } line = args.LineUtf8; LogAddRead(args.BytesInBuffer, line); line = RemoveCmdTag(line); if (!line.ToUpper().StartsWith("OK")) { throw new IMAP_ClientException(line); } }
/// <summary> /// Fetches specifes messages specified fetch items. /// </summary> /// <param name="sequence_set">IMAP sequence-set.</param> /// <param name="fetchFlags">Specifies what data to fetch from IMAP server.</param> /// <param name="setSeenFlag">If true message seen flag is setted.</param> /// <param name="uidFetch">Specifies if sequence-set contains message UIDs or message numbers.</param> /// <returns>Returns requested fetch items.</returns> /// <exception cref="ObjectDisposedException">Is raised when this object is disposed and this method is accessed.</exception> /// <exception cref="InvalidOperationException">Is raised when IMAP client is not connected,not authenticated and folder not selected.</exception> public IMAP_FetchItem[] FetchMessages(IMAP_SequenceSet sequence_set, IMAP_FetchItem_Flags fetchFlags, bool setSeenFlag, bool uidFetch) { if (this.IsDisposed) { throw new ObjectDisposedException(this.GetType().Name); } if (!this.IsConnected) { throw new InvalidOperationException("You must connect first."); } if (!this.IsAuthenticated) { throw new InvalidOperationException("The command is only valid in authenticated state."); } if (m_SelectedFolder.Length == 0) { throw new InvalidOperationException("The command is only valid in selected state."); } List<IMAP_FetchItem> fetchItems = new List<IMAP_FetchItem>(); //--- Construct FETCH command line -----------------------------------------------------------------------// string fetchCmdLine = GetNextCmdTag(); if (uidFetch) { fetchCmdLine += " UID"; } fetchCmdLine += " FETCH " + sequence_set.ToSequenceSetString() + " (UID"; // FLAGS if ((fetchFlags & IMAP_FetchItem_Flags.MessageFlags) != 0) { fetchCmdLine += " FLAGS"; } // RFC822.SIZE if ((fetchFlags & IMAP_FetchItem_Flags.Size) != 0) { fetchCmdLine += " RFC822.SIZE"; } // INTERNALDATE if ((fetchFlags & IMAP_FetchItem_Flags.InternalDate) != 0) { fetchCmdLine += " INTERNALDATE"; } // ENVELOPE if ((fetchFlags & IMAP_FetchItem_Flags.Envelope) != 0) { fetchCmdLine += " ENVELOPE"; } // BODYSTRUCTURE if ((fetchFlags & IMAP_FetchItem_Flags.BodyStructure) != 0) { fetchCmdLine += " BODYSTRUCTURE"; } // BODY[] or BODY.PEEK[] if ((fetchFlags & IMAP_FetchItem_Flags.Message) != 0) { if (setSeenFlag) { fetchCmdLine += " BODY[]"; } else { fetchCmdLine += " BODY.PEEK[]"; } } // BODY[HEADER] or BODY.PEEK[HEADER] ---> This needed only if full message isn't requested. if ((fetchFlags & IMAP_FetchItem_Flags.Message) == 0 && (fetchFlags & IMAP_FetchItem_Flags.Header) != 0) { if (setSeenFlag) { fetchCmdLine += " BODY[HEADER]"; } else { fetchCmdLine += " BODY.PEEK[HEADER]"; } } //--------------------------------------------------------------------------------------------------------// fetchCmdLine += ")"; // Send fetch command line to server. int countWritten = this.TcpStream.WriteLine(fetchCmdLine); LogAddWrite(countWritten, fetchCmdLine); // Read un-tagged response lines while we get final response line. byte[] lineBuffer = new byte[100000]; string line = ""; while (true) { SmartStream.ReadLineAsyncOP args = new SmartStream.ReadLineAsyncOP(lineBuffer, SizeExceededAction.JunkAndThrowException); this.TcpStream.ReadLine(args, false); if (args.Error != null) { throw args.Error; } line = args.LineUtf8; LogAddRead(args.BytesInBuffer, line); // We have un-tagged resposne. if (line.StartsWith("*")) { if (IsStatusResponse(line)) { ProcessStatusResponse(line); } else { int no = 0; int uid = 0; int size = 0; byte[] data = null; IMAP_MessageFlags flags = IMAP_MessageFlags.Recent; string envelope = ""; string bodystructure = ""; string internalDate = ""; // Remove * line = RemoveCmdTag(line); // Get message number no = Convert.ToInt32(line.Substring(0, line.IndexOf(" "))); // Get rid of FETCH and parse params. Reply:* 1 FETCH (UID 12 BODY[] ...) line = line.Substring(line.IndexOf("FETCH (") + 7); StringReader r = new StringReader(line); // Loop fetch result fields while (r.Available > 0) { r.ReadToFirstChar(); // Fetch command closing ) parenthesis if (r.SourceString == ")") { break; } #region UID <value> // UID <value> else if (r.StartsWith("UID", false)) { // Remove UID word from reply r.ReadSpecifiedLength("UID".Length); r.ReadToFirstChar(); // Read <value> string word = r.ReadWord(); if (word == null) { throw new Exception("IMAP server didn't return UID <value> !"); } else { uid = Convert.ToInt32(word); } } #endregion #region RFC822.SIZE <value> // RFC822.SIZE <value> else if (r.StartsWith("RFC822.SIZE", false)) { // Remove RFC822.SIZE word from reply r.ReadSpecifiedLength("RFC822.SIZE".Length); r.ReadToFirstChar(); // Read <value> string word = r.ReadWord(); if (word == null) { throw new Exception("IMAP server didn't return RFC822.SIZE <value> !"); } else { try { size = Convert.ToInt32(word); } catch { throw new Exception("IMAP server returned invalid RFC822.SIZE <value> '" + word + "' !"); } } } #endregion #region INTERNALDATE <value> // INTERNALDATE <value> else if (r.StartsWith("INTERNALDATE", false)) { // Remove INTERNALDATE word from reply r.ReadSpecifiedLength("INTERNALDATE".Length); r.ReadToFirstChar(); // Read <value> string word = r.ReadWord(); if (word == null) { throw new Exception("IMAP server didn't return INTERNALDATE <value> !"); } else { internalDate = word; } } #endregion #region ENVELOPE (<envelope-string>) else if (r.StartsWith("ENVELOPE", false)) { // Remove ENVELOPE word from reply r.ReadSpecifiedLength("ENVELOPE".Length); r.ReadToFirstChar(); /* Handle string literals {count-to-read}<CRLF>data(length = count-to-read). (string can be quoted string or literal) Loop while get envelope,invalid response or timeout. */ while (true) { try { envelope = r.ReadParenthesized(); break; } catch (Exception x) { string s = r.ReadToEnd(); /* partial_envelope {count-to-read} Example: ENVELOPE ("Mon, 03 Apr 2006 10:10:10 GMT" {35} */ if (s.EndsWith("}")) { // Get partial envelope and append it back to reader r.AppenString(s.Substring(0, s.LastIndexOf('{'))); // Read remaining envelope and append it to reader. int countToRead = Convert.ToInt32(s.Substring(s.LastIndexOf('{') + 1, s.LastIndexOf('}') - s.LastIndexOf('{') - 1)); string reply = this.TcpStream.ReadFixedCountString(countToRead); LogAddRead(countToRead, reply); r.AppenString(TextUtils.QuoteString(reply)); // Read fetch continuing line. this.TcpStream.ReadLine(args, false); if (args.Error != null) { throw args.Error; } line = args.LineUtf8; LogAddRead(args.BytesInBuffer, line); r.AppenString(line); } // Unexpected response else { throw x; } } } } #endregion #region BODYSTRUCTURE (<bodystructure-string>) else if (r.StartsWith("BODYSTRUCTURE", false)) { // Remove BODYSTRUCTURE word from reply r.ReadSpecifiedLength("BODYSTRUCTURE".Length); r.ReadToFirstChar(); bodystructure = r.ReadParenthesized(); } #endregion #region BODY[] or BODY[HEADER] // BODY[] or BODY[HEADER] else if (r.StartsWith("BODY", false)) { if (r.StartsWith("BODY[]", false)) { // Remove BODY[] r.ReadSpecifiedLength("BODY[]".Length); } else if (r.StartsWith("BODY[HEADER]", false)) { // Remove BODY[HEADER] r.ReadSpecifiedLength("BODY[HEADER]".Length); } else { throw new Exception("Invalid FETCH response: " + r.SourceString); } r.ReadToFirstChar(); // We must now have {<size-to-read>}, or there is error if (!r.StartsWith("{")) { throw new Exception("Invalid FETCH BODY[] or BODY[HEADER] response: " + r.SourceString); } // Read <size-to-read> int dataLength = Convert.ToInt32(r.ReadParenthesized()); // Read data MemoryStream storeStrm = new MemoryStream(dataLength); this.TcpStream.ReadFixedCount(storeStrm, dataLength); LogAddRead(dataLength, "Readed " + dataLength.ToString() + " bytes."); data = storeStrm.ToArray(); // Read fetch continuing line. this.TcpStream.ReadLine(args, false); if (args.Error != null) { throw args.Error; } line = args.LineUtf8; LogAddRead(args.BytesInBuffer, line); r.AppenString(line); } #endregion #region FLAGS (<flags-list>) // FLAGS (<flags-list>) else if (r.StartsWith("FLAGS", false)) { // Remove FLAGS word from reply r.ReadSpecifiedLength("FLAGS".Length); r.ReadToFirstChar(); // Read (<flags-list>) string flagsList = r.ReadParenthesized(); if (flagsList == null) { throw new Exception("IMAP server didn't return FLAGS (<flags-list>) !"); } else { flags = IMAP_Utils.ParseMessageFlags(flagsList); } } #endregion else { throw new Exception("Not supported fetch reply: " + r.SourceString); } } fetchItems.Add(new IMAP_FetchItem(no, uid, size, data, flags, internalDate, envelope, bodystructure, fetchFlags)); } } else { break; } } if (!RemoveCmdTag(line).ToUpper().StartsWith("OK")) { throw new IMAP_ClientException(line); } return fetchItems.ToArray(); }
/// <summary> /// Writes a raw line to the imap client. /// </summary> /// <param name="data"></param> /// <returns></returns> public string WriteLine(string data) { if (this.IsDisposed) { throw new ObjectDisposedException(this.GetType().Name); } if (!this.IsConnected) { throw new InvalidOperationException("You must connect first."); } if (!this.IsAuthenticated) { throw new InvalidOperationException("The command is only valid in authenticated state."); } string line = GetNextCmdTag() + " " + data; int countWritten = this.TcpStream.WriteLine(line); LogAddWrite(countWritten, line); SmartStream.ReadLineAsyncOP args = new SmartStream.ReadLineAsyncOP(new byte[32000], SizeExceededAction.JunkAndThrowException); this.TcpStream.ReadLine(args, false); if (args.Error != null) { throw args.Error; } line = args.LineUtf8; LogAddRead(args.BytesInBuffer, line); return line; }
/// <summary> /// This method is called after TCP client has sucessfully connected. /// </summary> protected override void OnConnected() { SmartStream.ReadLineAsyncOP args = new SmartStream.ReadLineAsyncOP(new byte[32000], SizeExceededAction.JunkAndThrowException); this.TcpStream.ReadLine(args, false); if (args.Error != null) { throw args.Error; } string line = args.LineUtf8; LogAddRead(args.BytesInBuffer, line); line = RemoveCmdTag(line); if (line.ToUpper().StartsWith("OK")) { // Clear path separator, so next access will get it. m_PathSeparator = '\0'; } else { throw new Exception("Server returned: " + line); } }
/// <summary> /// Switches IMAP connection to SSL. /// </summary> /// <exception cref="ObjectDisposedException">Is raised when this object is disposed and this method is accessed.</exception> /// <exception cref="InvalidOperationException">Is raised when IMAP client is not connected or is authenticated or is already secure connection.</exception> public void StartTLS() { /* RFC 2595 3. IMAP STARTTLS extension. Example: C: a001 CAPABILITY S: * CAPABILITY IMAP4rev1 STARTTLS LOGINDISABLED S: a001 OK CAPABILITY completed C: a002 STARTTLS S: a002 OK Begin TLS negotiation now <TLS negotiation, further commands are under TLS layer> */ if (this.IsDisposed) { throw new ObjectDisposedException(this.GetType().Name); } if (!this.IsConnected) { throw new InvalidOperationException("You must connect first."); } if (this.IsAuthenticated) { throw new InvalidOperationException("The STARTTLS command is only valid in non-authenticated state !"); } if (this.IsSecureConnection) { throw new InvalidOperationException("Connection is already secure."); } string line = GetNextCmdTag() + " STARTTLS"; int countWritten = this.TcpStream.WriteLine(line); LogAddWrite(countWritten, line); SmartStream.ReadLineAsyncOP args = new SmartStream.ReadLineAsyncOP(new byte[32000], SizeExceededAction.JunkAndThrowException); this.TcpStream.ReadLine(args, false); if (args.Error != null) { throw args.Error; } line = args.LineUtf8; LogAddRead(args.BytesInBuffer, line); line = RemoveCmdTag(line); if (!line.ToUpper().StartsWith("OK")) { throw new IMAP_ClientException(line); } SwitchToSecure(); }
/// <summary> /// This method is called when TCP client has sucessfully connected. /// </summary> /// <param name="callback">Callback to be called to complete connect operation.</param> protected override void OnConnected(CompleteConnectCallback callback) { // Read POP3 server greeting response. SmartStream.ReadLineAsyncOP readGreetingOP = new SmartStream.ReadLineAsyncOP(new byte[8000],SizeExceededAction.JunkAndThrowException); readGreetingOP.Completed += delegate(object s,EventArgs<SmartStream.ReadLineAsyncOP> e){ ReadServerGreetingCompleted(readGreetingOP,callback); }; if(this.TcpStream.ReadLine(readGreetingOP,true)){ ReadServerGreetingCompleted(readGreetingOP,callback); } }
/// <summary> /// Gets files and directories in the current server directory. /// </summary> /// <param name="path">Directory or file name which listing to get. Value null means current directory will be listed.</param> /// <returns>Returns current working directory listing.</returns> /// <exception cref="ObjectDisposedException">Is raised when this object is disposed and this method is accessed.</exception> /// <exception cref="InvalidOperationException">Is raised when FTP client is not connected or FTP data connection has active read/write operation.</exception> /// <exception cref="FTP_ClientException">Is raised when FTP server returns error.</exception> public FTP_ListItem[] GetList(string path) { if(this.IsDisposed){ throw new ObjectDisposedException(this.GetType().Name); } if(!this.IsConnected){ throw new InvalidOperationException("You must connect first."); } if(m_pDataConnection.IsActive){ throw new InvalidOperationException("There is already active read/write operation on data connection."); } List<FTP_ListItem> retVal = new List<FTP_ListItem>(); // Set transfer mode SetTransferType(TransferType.Binary); if(m_TransferMode == FTP_TransferMode.Passive){ Pasv(); } else{ Port(); } // If FTP server supports MLSD command, use it to get directory listing. // MLSD is standard way to get dir listing, while LIST command isn't any strict standard. bool mlsdSupported = false; foreach(string feature in m_pExtCapabilities){ if(feature.ToLower().StartsWith("mlsd")){ mlsdSupported = true; break; } } #region MLSD if(mlsdSupported){ if(string.IsNullOrEmpty(path)){ WriteLine("MLSD"); } else{ WriteLine("MLSD " + path); } string[] response = ReadResponse(); if(!response[0].StartsWith("1")){ throw new FTP_ClientException(response[0]); } MemoryStream ms = new MemoryStream(); m_pDataConnection.ReadAll(ms); response = ReadResponse(); if(!response[0].StartsWith("2")){ throw new FTP_ClientException(response[0]); } byte[] lineBuffer = new byte[8000]; ms.Position = 0; SmartStream mlsdStream = new SmartStream(ms,true); while(true){ SmartStream.ReadLineAsyncOP args = new SmartStream.ReadLineAsyncOP(lineBuffer,SizeExceededAction.JunkAndThrowException); mlsdStream.ReadLine(args,false); if(args.Error != null){ throw args.Error; } string line = args.LineUtf8; // We reached end of stream, we readed whole list sucessfully. if(line == null){ break; } else{ string[] parameters = line.Substring(0,line.LastIndexOf(';')).Split(';'); string name = line.Substring(line.LastIndexOf(';') + 1).Trim(); string type = ""; long size = 0; DateTime modified = DateTime.MinValue; foreach(string parameter in parameters){ string[] name_value = parameter.Split('='); if(name_value[0].ToLower() == "type"){ type = name_value[1].ToLower(); } else if(name_value[0].ToLower() == "size"){ size = Convert.ToInt32(name_value[1]); } else if(name_value[0].ToLower() == "modify"){ modified = DateTime.ParseExact(name_value[1],"yyyyMMddHHmmss",System.Globalization.DateTimeFormatInfo.InvariantInfo); } else{ // Other options won't interest us, skip them. } } if(type == "dir"){ retVal.Add(new FTP_ListItem(name,0,modified,true)); } else if(type == "file"){ retVal.Add(new FTP_ListItem(name,size,modified,false)); } } } } #endregion #region LIST else{ if(string.IsNullOrEmpty(path)){ WriteLine("LIST"); } else{ WriteLine("LIST " + path); } string[] response = ReadResponse(); if(!response[0].StartsWith("1")){ throw new FTP_ClientException(response[0]); } MemoryStream ms = new MemoryStream(); m_pDataConnection.ReadAll(ms); response = ReadResponse(); if(!response[0].StartsWith("2")){ throw new FTP_ClientException(response[0]); } ms.Position = 0; SmartStream listStream = new SmartStream(ms,true); string[] winDateFormats = new string[]{"M-d-yy h:mmtt"}; string[] unixFormats = new string[]{"MMM d H:mm","MMM d yyyy"}; SmartStream.ReadLineAsyncOP args = new SmartStream.ReadLineAsyncOP(new byte[8000],SizeExceededAction.JunkAndThrowException); while(true){ listStream.ReadLine(args,false); if(args.Error != null){ throw args.Error; } else if(args.BytesInBuffer == 0){ break; } string line = args.LineUtf8; // Dedect listing. string listingType = "unix"; if(line != null){ StringReader r = new StringReader(line); DateTime modified; if(DateTime.TryParseExact(r.ReadWord() + " " + r.ReadWord(),new string[]{"MM-dd-yy hh:mmtt"},System.Globalization.DateTimeFormatInfo.InvariantInfo,System.Globalization.DateTimeStyles.None,out modified)){ listingType = "win"; } } try{ // Windows listing. if(listingType == "win"){ // MM-dd-yy hh:mm <DIR> directoryName // MM-dd-yy hh:mm size fileName StringReader r = new StringReader(line); // Read date DateTime modified = DateTime.ParseExact(r.ReadWord() + " " + r.ReadWord(),winDateFormats,System.Globalization.DateTimeFormatInfo.InvariantInfo,System.Globalization.DateTimeStyles.None); r.ReadToFirstChar(); // We have directory. if(r.StartsWith("<dir>",false)){ r.ReadSpecifiedLength(5); r.ReadToFirstChar(); retVal.Add(new FTP_ListItem(r.ReadToEnd(),0,modified,true)); } // We have file else{ // Read file size long size = Convert.ToInt64(r.ReadWord()); r.ReadToFirstChar(); retVal.Add(new FTP_ListItem(r.ReadToEnd(),size,modified,false)); } } // Unix listing else{ // "d"directoryAtttributes xx xx xx 0 MMM d HH:mm/yyyy directoryName // fileAtttributes xx xx xx fileSize MMM d HH:mm/yyyy fileName StringReader r = new StringReader(line); string attributes = r.ReadWord(); r.ReadWord(); r.ReadWord(); r.ReadWord(); long size = Convert.ToInt64(r.ReadWord()); DateTime modified = DateTime.ParseExact(r.ReadWord() + " " + r.ReadWord() + " " + r.ReadWord(),unixFormats,System.Globalization.DateTimeFormatInfo.InvariantInfo,System.Globalization.DateTimeStyles.None); r.ReadToFirstChar(); string name = r.ReadToEnd(); if(name != "." && name != ".."){ if(attributes.StartsWith("d")){ retVal.Add(new FTP_ListItem(name,0,modified,true)); } else{ retVal.Add(new FTP_ListItem(name,size,modified,false)); } } } } catch{ // Skip unknown entries. } } } #endregion return retVal.ToArray(); }
/// <summary> /// Reads line from TCP client. /// </summary> /// <returns></returns> internal string ReadLine() { SmartStream.ReadLineAsyncOP readLineOP = new SmartStream.ReadLineAsyncOP(new byte[8000],SizeExceededAction.JunkAndThrowException); this.TcpClient.TcpStream.ReadLine(readLineOP,false); return readLineOP.LineUtf8; }
/// <summary> /// Reads next continuing FETCH line and stores to fetch reader 'r'. /// </summary> /// <param name="imap">IMAP client.</param> /// <param name="r">String reader.</param> /// <param name="callback">Fetch completion callback.</param> /// <returns>Returns true if completed asynchronously or false if completed synchronously.</returns> /// <exception cref="ArgumentNullException">Is raised when <b>imap</b>,<b>r</b> or <b>callback</b> is null reference.</exception> private bool ReadNextFetchLine(IMAP_Client imap,StringReader r,EventHandler<EventArgs<Exception>> callback) { if(imap == null){ throw new ArgumentNullException("imap"); } if(r == null){ throw new ArgumentNullException("r"); } if(callback == null){ throw new ArgumentNullException("callback"); } SmartStream.ReadLineAsyncOP readLineOP = new SmartStream.ReadLineAsyncOP(new byte[64000],SizeExceededAction.JunkAndThrowException); readLineOP.Completed += delegate(object sender,EventArgs<SmartStream.ReadLineAsyncOP> e){ try{ // Read line failed. if(readLineOP.Error != null){ callback(this,new EventArgs<Exception>(readLineOP.Error)); } else{ // Log. imap.LogAddRead(readLineOP.BytesInBuffer,readLineOP.LineUtf8); // Append fetch line to fetch reader. r.AppendString(readLineOP.LineUtf8); ParseDataItems(imap,r,callback); } } catch(Exception x){ callback(this,new EventArgs<Exception>(x)); } finally{ readLineOP.Dispose(); } }; // Read line completed synchronously. if(imap.TcpStream.ReadLine(readLineOP,true)){ try{ // Read line failed. if(readLineOP.Error != null){ callback(this,new EventArgs<Exception>(readLineOP.Error)); return true; } else{ // Log. imap.LogAddRead(readLineOP.BytesInBuffer,readLineOP.LineUtf8); // Append fetch line to fetch reader. r.AppendString(readLineOP.LineUtf8); return false; } } finally{ readLineOP.Dispose(); } } return true; }
private void m_pTimerNoop_Elapsed(object sender,ElapsedEventArgs e) { try{ /* Noop Responses: +OK */ lock(this.LockSynchronizer){ m_pClient.TcpStream.WriteLine("NOOP"); SmartStream.ReadLineAsyncOP readLineOP = new SmartStream.ReadLineAsyncOP(new byte[8000],SizeExceededAction.JunkAndThrowException); m_pClient.TcpStream.ReadLine(readLineOP,false); } } catch{ } }
/// <summary> /// Reads and logs specified line from connected host. /// </summary> /// <returns>Returns readed line.</returns> protected string ReadLine() { SmartStream.ReadLineAsyncOP args = new SmartStream.ReadLineAsyncOP(new byte[32000],SizeExceededAction.JunkAndThrowException); this.TcpStream.ReadLine(args,false); if(args.Error != null){ throw args.Error; } string line = args.LineUtf8; if(args.BytesInBuffer > 0){ LogAddRead(args.BytesInBuffer,line); } else{ LogAddText("Remote host closed connection."); } return line; }
/// <summary> /// Start operation processing. /// </summary> public void Start() { /* RFC 3501. literal = "{" number "}" CRLF *CHAR8 ; Number represents the number of CHAR8s */ // TODO: Async // TODO: Limit total command size. 64k ? // If initial command line ends with literal string, read literal string and remaining command text. if(EndsWithLiteralString(m_InitialCmdLine)){ StringBuilder cmdText = new StringBuilder(); int literalSize = GetLiteralSize(m_InitialCmdLine); // Add initial command line part to command text. cmdText.Append(RemoveLiteralSpecifier(m_InitialCmdLine)); SmartStream.ReadLineAsyncOP readLineOP = new SmartStream.ReadLineAsyncOP(new byte[32000],SizeExceededAction.JunkAndThrowException); while(true){ #region Read literal string // Send "+ Continue". m_pSession.WriteLine("+ Continue."); // Read literal string. MemoryStream msLiteral = new MemoryStream(); m_pSession.TcpStream.ReadFixedCount(msLiteral,literalSize); // Log m_pSession.LogAddRead(literalSize,m_pCharset.GetString(msLiteral.ToArray())); // Add to command text as quoted string. cmdText.Append(TextUtils.QuoteString(m_pCharset.GetString(msLiteral.ToArray()))); #endregion #region Read continuing command text // Read continuing command text. m_pSession.TcpStream.ReadLine(readLineOP,false); // We have error. if(readLineOP.Error != null){ throw readLineOP.Error; } else{ string line = readLineOP.LineUtf8; // Log m_pSession.LogAddRead(readLineOP.BytesInBuffer,line); // Add command line part to command text. if(EndsWithLiteralString(line)){ cmdText.Append(RemoveLiteralSpecifier(line)); } else{ cmdText.Append(line); } // No more literal string, we are done. if(!EndsWithLiteralString(line)){ break; } else{ literalSize = GetLiteralSize(line); } } #endregion } m_CmdLine = cmdText.ToString(); } // We have no literal string, so initial cmd line is final. else{ m_CmdLine = m_InitialCmdLine; } }
/// <summary> /// Is called when DELE command sending has finished. /// </summary> /// <param name="ar">Asynchronous result.</param> private void DeleCommandSendingCompleted(IAsyncResult ar) { try{ m_pPop3Client.TcpStream.EndWrite(ar); // Read POP3 server response. SmartStream.ReadLineAsyncOP op = new SmartStream.ReadLineAsyncOP(new byte[8000],SizeExceededAction.JunkAndThrowException); op.Completed += delegate(object s,EventArgs<SmartStream.ReadLineAsyncOP> e){ DeleReadResponseCompleted(op); }; if(m_pPop3Client.TcpStream.ReadLine(op,true)){ DeleReadResponseCompleted(op); } } catch(Exception x){ m_pException = x; m_pPop3Client.LogAddException("Exception: " + x.Message,x); SetState(AsyncOP_State.Completed); } }
private bool IDLE(string cmdTag,string cmdText) { /* RFC 2177 3. IDLE Command. Arguments: none Responses: continuation data will be requested; the client sends the continuation data "DONE" to end the command Result: OK - IDLE completed after client sent "DONE" NO - failure: the server will not allow the IDLE command at this time BAD - command unknown or arguments invalid The IDLE command may be used with any IMAP4 server implementation that returns "IDLE" as one of the supported capabilities to the CAPABILITY command. If the server does not advertise the IDLE capability, the client MUST NOT use the IDLE command and must poll for mailbox updates. In particular, the client MUST continue to be able to accept unsolicited untagged responses to ANY command, as specified in the base IMAP specification. The IDLE command is sent from the client to the server when the client is ready to accept unsolicited mailbox update messages. The server requests a response to the IDLE command using the continuation ("+") response. The IDLE command remains active until the client responds to the continuation, and as long as an IDLE command is active, the server is now free to send untagged EXISTS, EXPUNGE, and other messages at any time. The IDLE command is terminated by the receipt of a "DONE" continuation from the client; such response satisfies the server's continuation request. At that point, the server MAY send any remaining queued untagged responses and then MUST immediately send the tagged response to the IDLE command and prepare to process other commands. As in the base specification, the processing of any new command may cause the sending of unsolicited untagged responses, subject to the ambiguity limitations. The client MUST NOT send a command while the server is waiting for the DONE, since the server will not be able to distinguish a command from a continuation. The server MAY consider a client inactive if it has an IDLE command running, and if such a server has an inactivity timeout it MAY log the client off implicitly at the end of its timeout period. Because of that, clients using IDLE are advised to terminate the IDLE and re-issue it at least every 29 minutes to avoid being logged off. This still allows a client to receive immediate mailbox updates even though it need only "poll" at half hour intervals. Example: C: A001 SELECT INBOX S: * FLAGS (Deleted Seen) S: * 3 EXISTS S: * 0 RECENT S: * OK [UIDVALIDITY 1] S: A001 OK SELECT completed C: A002 IDLE S: + idling ...time passes; new mail arrives... S: * 4 EXISTS C: DONE S: A002 OK IDLE terminated ...another client expunges message 2 now... C: A003 FETCH 4 ALL S: * 4 FETCH (...) S: A003 OK FETCH completed C: A004 IDLE S: * 2 EXPUNGE S: * 3 EXISTS S: + idling ...time passes; another client expunges message 3... S: * 3 EXPUNGE S: * 2 EXISTS ...time passes; new mail arrives... S: * 3 EXISTS C: DONE S: A004 OK IDLE terminated C: A005 FETCH 3 ALL S: * 3 FETCH (...) S: A005 OK FETCH completed */ if(!this.IsAuthenticated){ m_pResponseSender.SendResponseAsync(new IMAP_r_ServerStatus(cmdTag,"NO","Authentication required.")); return true; } m_pResponseSender.SendResponseAsync(new IMAP_r_ServerStatus("+","idling")); TimerEx timer = new TimerEx(30000,true); timer.Elapsed += new System.Timers.ElapsedEventHandler(delegate(object sender,System.Timers.ElapsedEventArgs e){ try{ UpdateSelectedFolderAndSendChanges(); } catch{ } }); timer.Enabled = true; // Read client response. SmartStream.ReadLineAsyncOP readLineOP = new SmartStream.ReadLineAsyncOP(new byte[32000],SizeExceededAction.JunkAndThrowException); readLineOP.Completed += new EventHandler<EventArgs<SmartStream.ReadLineAsyncOP>>(delegate(object sender,EventArgs<SmartStream.ReadLineAsyncOP> e){ try{ if(readLineOP.Error != null){ LogAddText("Error: " + readLineOP.Error.Message); timer.Dispose(); return; } // Remote host closed connection. else if(readLineOP.BytesInBuffer == 0){ LogAddText("Remote host(connected client) closed IMAP connection."); timer.Dispose(); Dispose(); return; } LogAddRead(readLineOP.BytesInBuffer,readLineOP.LineUtf8); if(string.Equals(readLineOP.LineUtf8,"DONE",StringComparison.InvariantCultureIgnoreCase)){ timer.Dispose(); m_pResponseSender.SendResponseAsync(new IMAP_r_ServerStatus(cmdTag,"OK","IDLE terminated.")); BeginReadCmd(); } else{ while(this.TcpStream.ReadLine(readLineOP,true)){ if(readLineOP.Error != null){ LogAddText("Error: " + readLineOP.Error.Message); timer.Dispose(); return; } LogAddRead(readLineOP.BytesInBuffer,readLineOP.LineUtf8); if(string.Equals(readLineOP.LineUtf8,"DONE",StringComparison.InvariantCultureIgnoreCase)){ timer.Dispose(); m_pResponseSender.SendResponseAsync(new IMAP_r_ServerStatus(cmdTag,"OK","IDLE terminated.")); BeginReadCmd(); break; } } } } catch(Exception x){ timer.Dispose(); OnError(x); } }); while(this.TcpStream.ReadLine(readLineOP,true)){ if(readLineOP.Error != null){ LogAddText("Error: " + readLineOP.Error.Message); timer.Dispose(); break; } LogAddRead(readLineOP.BytesInBuffer,readLineOP.LineUtf8); if(string.Equals(readLineOP.LineUtf8,"DONE",StringComparison.InvariantCultureIgnoreCase)){ timer.Dispose(); m_pResponseSender.SendResponseAsync(new IMAP_r_ServerStatus(cmdTag,"OK","IDLE terminated.")); BeginReadCmd(); break; } } return false; }
/// <summary> /// Parses header fields from stream. Stream position stays where header reading ends. /// </summary> /// <param name="stream">Stream from where to parse.</param> public void Parse(SmartStream stream) { /* Rfc 2822 2.2 Header Fields Header fields are lines composed of a field name, followed by a colon (":"), followed by a field body, and terminated by CRLF. A field name MUST be composed of printable US-ASCII characters (i.e., characters that have values between 33 and 126, inclusive), except colon. A field body may be composed of any US-ASCII characters, except for CR and LF. However, a field body may contain CRLF when used in header "folding" and "unfolding" as described in section 2.2.3. All field bodies MUST conform to the syntax described in sections 3 and 4 of this standard. Rfc 2822 2.2.3 Long Header Fields The process of moving from this folded multiple-line representation of a header field to its single line representation is called "unfolding". Unfolding is accomplished by simply removing any CRLF that is immediately followed by WSP. Each header field should be treated in its unfolded form for further syntactic and semantic evaluation. Example: Subject: aaaaa<CRLF> <TAB or SP>aaaaa<CRLF> */ m_pHeaderFields.Clear(); SmartStream.ReadLineAsyncOP args = new SmartStream.ReadLineAsyncOP(new byte[32000],SizeExceededAction.JunkAndThrowException); stream.ReadLine(args,false); if(args.Error != null){ throw args.Error; } string line = args.LineUtf8; while(line != null){ // End of header reached if(line == ""){ break; } // Store current header line and read next. We need to read 1 header line to ahead, // because of multiline header fields. string headerField = line; stream.ReadLine(args,false); if(args.Error != null){ throw args.Error; } line = args.LineUtf8; // See if header field is multiline. See comment above. while(line != null && (line.StartsWith("\t") || line.StartsWith(" "))){ headerField += line; stream.ReadLine(args,false); if(args.Error != null){ throw args.Error; } line = args.LineUtf8; } string[] name_value = headerField.Split(new char[]{':'},2); // There must be header field name and value, otherwise invalid header field if(name_value.Length == 2){ Add(name_value[0] + ":",name_value[1].Trim()); } } }
/// <summary> /// Reads a sequence of bytes from the current stream and advances the position within the stream by the number of bytes read. /// </summary> /// <param name="buffer">An array of bytes. When this method returns, the buffer contains the specified byte array with the values between offset and (offset + count - 1) replaced by the bytes read from the current source.</param> /// <param name="offset">The zero-based byte offset in buffer at which to begin storing the data read from the current stream.</param> /// <param name="count">The maximum number of bytes to be read from the current stream.</param> /// <returns>The total number of bytes read into the buffer. This can be less than the number of bytes requested if that many bytes are not currently available, or zero (0) if the end of the stream has been reached.</returns> /// <exception cref="ArgumentNullException">Is raised when <b>buffer</b> is null reference.</exception> /// <exception cref="ArgumentException">Is raised when any of the arguments has invalid value.</exception> /// <exception cref="NotSupportedException">Is raised when reading not supported.</exception> public override int Read(byte[] buffer,int offset,int count) { if(buffer == null){ throw new ArgumentNullException("buffer"); } if(offset < 0 || offset > buffer.Length){ throw new ArgumentException("Invalid argument 'offset' value."); } if(offset + count > buffer.Length){ throw new ArgumentException("Invalid argument 'count' value."); } if((m_AccessMode & FileAccess.Read) == 0){ throw new NotSupportedException(); } while(true){ // Read next quoted-printable line and decode it. if(m_DecodedOffset >= m_DecodedCount){ m_DecodedOffset = 0; m_DecodedCount = 0; SmartStream.ReadLineAsyncOP readLineOP = new SmartStream.ReadLineAsyncOP(new byte[32000],SizeExceededAction.ThrowException); m_pStream.ReadLine(readLineOP,false); // IO error reading line. if(readLineOP.Error != null){ throw readLineOP.Error; } // We reached end of stream. else if(readLineOP.BytesInBuffer == 0){ return 0; } // Decode quoted-printable line. else{ // Process bytes. bool softLineBreak = false; int lineLength = readLineOP.LineBytesInBuffer; for(int i=0;i<readLineOP.LineBytesInBuffer;i++){ byte b = readLineOP.Buffer[i]; // We have soft line-break. if(b == '=' && i == (lineLength - 1)){ softLineBreak = true; } // We should have =XX char. else if(b == '='){ byte b1 = readLineOP.Buffer[++i]; byte b2 = readLineOP.Buffer[++i]; m_pDecodedBuffer[m_DecodedCount++] = byte.Parse(new string(new char[]{(char)b1,(char)b2}),System.Globalization.NumberStyles.HexNumber); } // Normal char. else{ m_pDecodedBuffer[m_DecodedCount++] = b; } } if(!softLineBreak){ m_pDecodedBuffer[m_DecodedCount++] = (byte)'\r'; m_pDecodedBuffer[m_DecodedCount++] = (byte)'\n'; } } } // We some decoded data, return it. if(m_DecodedOffset < m_DecodedCount){ int countToCopy = Math.Min(count,m_DecodedCount - m_DecodedOffset); Array.Copy(m_pDecodedBuffer,m_DecodedOffset,buffer,offset,countToCopy); m_DecodedOffset += countToCopy; return countToCopy; } } }
/// <summary> /// Parses mime entity from stream. /// </summary> /// <param name="stream">Data stream from where to read data.</param> /// <param name="toBoundary">Entity data is readed to specified boundary.</param> /// <returns>Returns false if last entity. Returns true for mulipart entity, if there are more entities.</returns> internal bool Parse(SmartStream stream,string toBoundary) { // Clear header fields m_pHeader.Clear(); m_pHeaderFieldCache.Clear(); // Parse header m_pHeader.Parse(stream); // Parse entity and child entities if any (Conent-Type: multipart/xxx...) // Multipart entity if((this.ContentType & MediaType_enum.Multipart) != 0){ // There must be be boundary ID (rfc 1341 7.2.1 The Content-Type field for multipart entities requires one parameter, // "boundary", which is used to specify the encapsulation boundary.) string boundaryID = this.ContentType_Boundary; if(boundaryID == null){ // This is invalid message, just skip this mime entity } else{ // There is one or more mime entities // Find first boundary start position SmartStream.ReadLineAsyncOP args = new SmartStream.ReadLineAsyncOP(new byte[8000],SizeExceededAction.JunkAndThrowException); stream.ReadLine(args,false); if(args.Error != null){ throw args.Error; } string lineString = args.LineUtf8; while(lineString != null){ if(lineString.StartsWith("--" + boundaryID)){ break; } stream.ReadLine(args,false); if(args.Error != null){ throw args.Error; } lineString = args.LineUtf8; } // This is invalid entity, boundary start not found. Skip that entity. if(string.IsNullOrEmpty(lineString)){ return false; } // Start parsing child entities of this entity while(true){ // Parse and add child entity MimeEntity childEntity = new MimeEntity(); this.ChildEntities.Add(childEntity); // This is last entity, stop parsing if(childEntity.Parse(stream,boundaryID) == false){ break; } // else{ // There are more entities, parse them } // This entity is child of mulipart entity. // All this entity child entities are parsed, // we need to move stream position to next entity start. if(!string.IsNullOrEmpty(toBoundary)){ stream.ReadLine(args,false); if(args.Error != null){ throw args.Error; } lineString = args.LineUtf8; while(lineString != null){ if(lineString.StartsWith("--" + toBoundary)){ break; } stream.ReadLine(args,false); if(args.Error != null){ throw args.Error; } lineString = args.LineUtf8; } // Invalid boundary end, there can't be more entities if(string.IsNullOrEmpty(lineString)){ return false; } // See if last boundary or there is more. Last boundary ends with -- if(lineString.EndsWith(toBoundary + "--")){ return false; } // else{ // There are more entities return true; } } } // Singlepart entity. else{ // Boundary is specified, read data to specified boundary. if(!string.IsNullOrEmpty(toBoundary)){ MemoryStream entityData = new MemoryStream(); SmartStream.ReadLineAsyncOP readLineOP = new SmartStream.ReadLineAsyncOP(new byte[32000],SizeExceededAction.JunkAndThrowException); // Read entity data while get boundary end tag --boundaryID-- or EOS. while(true){ stream.ReadLine(readLineOP,false); if(readLineOP.Error != null){ throw readLineOP.Error; } // End of stream reached. Normally we should get boundary end tag --boundaryID--, but some x mailers won't add it, so // if we reach EOS, consider boundary closed. if(readLineOP.BytesInBuffer == 0){ // Just return data what was readed. m_EncodedData = entityData.ToArray(); return false; } // We readed a line. else{ // We have boundary start/end tag or just "--" at the beginning of line. if(readLineOP.LineBytesInBuffer >= 2 && readLineOP.Buffer[0] == '-' && readLineOP.Buffer[1] == '-'){ string lineString = readLineOP.LineUtf8; // We have boundary end tag, no more boundaries. if(lineString == "--" + toBoundary + "--"){ m_EncodedData = entityData.ToArray(); return false; } // We have new boundary start. else if(lineString == "--" + toBoundary){ m_EncodedData = entityData.ToArray(); return true; } else{ // Just skip } } // Write readed line. entityData.Write(readLineOP.Buffer,0,readLineOP.BytesInBuffer); } } } // Boundary isn't specified, read data to the stream end. else{ MemoryStream ms = new MemoryStream(); stream.ReadAll(ms); m_EncodedData = ms.ToArray(); } } return false; }
/// <summary> /// Starts operation processing. /// </summary> /// <param name="owner">Owner SMTP client.</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> internal bool Start(SMTP_Client owner) { if(owner == null){ throw new ArgumentNullException("owner"); } m_pSmtpClient = owner; try{ SmartStream.ReadLineAsyncOP op = new SmartStream.ReadLineAsyncOP(new byte[8000],SizeExceededAction.JunkAndThrowException); op.Completed += delegate(object s,EventArgs<SmartStream.ReadLineAsyncOP> e){ try{ // Response reading completed. if(!ReadLineCompleted(op)){ SetState(AsyncOP_State.Completed); OnCompletedAsync(); } // Continue response reading. else{ while(owner.TcpStream.ReadLine(op,true)){ // Response reading completed. if(!ReadLineCompleted(op)){ SetState(AsyncOP_State.Completed); OnCompletedAsync(); break; } } } } catch(Exception x){ m_pException = x; SetState(AsyncOP_State.Completed); OnCompletedAsync(); } }; while(owner.TcpStream.ReadLine(op,true)){ // Response reading completed. if(!ReadLineCompleted(op)){ SetState(AsyncOP_State.Completed); return false; } } return true; } catch(Exception x){ m_pException = x; SetState(AsyncOP_State.Completed); return false; } }
/// <summary> /// Is called when POP3 server CAPA response reading has completed. /// </summary> /// <param name="op">Asynchronous operation.</param> private void CapaReadResponseCompleted(SmartStream.ReadLineAsyncOP op) { try{ // Operation failed. if(op.Error != null){ m_pException = op.Error; m_pPop3Client.LogAddException("Exception: " + op.Error.Message,op.Error); SetState(AsyncOP_State.Completed); } // Operation succeeded. else{ // Log m_pPop3Client.LogAddRead(op.BytesInBuffer,op.LineUtf8); // Server returned success response. if(string.Equals(op.LineUtf8.Split(new char[]{' '},2)[0],"+OK",StringComparison.InvariantCultureIgnoreCase)){ // Read capa-list. SmartStream.ReadLineAsyncOP readLineOP = new SmartStream.ReadLineAsyncOP(new byte[8000],SizeExceededAction.JunkAndThrowException); readLineOP.Completed += delegate(object s,EventArgs<SmartStream.ReadLineAsyncOP> e){ try{ ReadMultiLineResponseLineCompleted(readLineOP); // Read response lines while we get terminator(.). while(this.State == AsyncOP_State.Active && m_pPop3Client.TcpStream.ReadLine(readLineOP,true)){ ReadMultiLineResponseLineCompleted(readLineOP); } } catch(Exception x){ m_pException = x; m_pPop3Client.LogAddException("Exception: " + x.Message,x); SetState(AsyncOP_State.Completed); } }; // Read response lines while we get terminator(.). while(this.State == AsyncOP_State.Active && m_pPop3Client.TcpStream.ReadLine(readLineOP,true)){ ReadMultiLineResponseLineCompleted(readLineOP); } } // Server returned error response. else{ m_pException = new POP3_ClientException(op.LineUtf8); SetState(AsyncOP_State.Completed); } } } catch(Exception x){ m_pException = x; m_pPop3Client.LogAddException("Exception: " + x.Message,x); SetState(AsyncOP_State.Completed); } op.Dispose(); }