/// <summary> /// Starts parsing FETCH response. /// </summary> /// <param name="imap">IMAP cleint.</param> /// <param name="line">Initial FETCH response line.</param> /// <param name="callback">Callback to be called when fetch completed.</param> /// <exception cref="ArgumentNullException">Is raised when <b>imap</b>,<b>line</b> or <b>callback</b> is null reference.</exception> internal void ParseAsync(IMAP_Client imap,string line,EventHandler<EventArgs<Exception>> callback) { if(imap == null){ throw new ArgumentNullException("imap"); } if(line == null){ throw new ArgumentNullException("line"); } if(callback == null){ throw new ArgumentNullException("callback"); } /* RFC 3501 7.4.2. FETCH Response. Example: S: * 23 FETCH (FLAGS (\Seen) RFC822.SIZE 44827) */ StringReader r = new StringReader(line); // Eat '*' r.ReadWord(); // Parse seqNo m_MsgSeqNo = Convert.ToInt32(r.ReadWord()); // Eat 'FETCH' r.ReadWord(); // Eat '(', if list of fetch data-items. r.ReadToFirstChar(); if(r.StartsWith("(")){ r.ReadSpecifiedLength(1); } ParseDataItems(imap,r,callback); }
/// <summary> /// Parses rfc 2822 datetime. /// </summary> /// <param name="date">Date string.</param> /// <returns></returns> public static DateTime ParseDate(string date) { /* Rfc 2822 3.3. Date and Time Specification. date-time = [ day-of-week "," ] date FWS time [CFWS] date = day month year time = hour ":" minute [ ":" second ] FWS zone */ /* IMAP date format. date-time = date FWS time [CFWS] date = day-month-year time = hour ":" minute [ ":" second ] FWS zone */ // zone = (( "+" / "-" ) 4DIGIT) //--- Replace timezone constants -------// /* UT -0000 GMT -0000 EDT -0400 EST -0500 CDT -0500 CST -0600 MDT -0600 MST -0700 PDT -0700 PST -0800 BST +0100 British Summer Time */ date = date.ToLower(); date = date.Replace("ut","-0000"); date = date.Replace("gmt","-0000"); date = date.Replace("edt","-0400"); date = date.Replace("est","-0500"); date = date.Replace("cdt","-0500"); date = date.Replace("cst","-0600"); date = date.Replace("mdt","-0600"); date = date.Replace("mst","-0700"); date = date.Replace("pdt","-0700"); date = date.Replace("pst","-0800"); date = date.Replace("bst","+0100"); //----------------------------------------// //--- Replace month constants ---// date = date.Replace("jan","01"); date = date.Replace("feb","02"); date = date.Replace("mar","03"); date = date.Replace("apr","04"); date = date.Replace("may","05"); date = date.Replace("jun","06"); date = date.Replace("jul","07"); date = date.Replace("aug","08"); date = date.Replace("sep","09"); date = date.Replace("oct","10"); date = date.Replace("nov","11"); date = date.Replace("dec","12"); //-------------------------------// // If date contains optional "day-of-week,", remove it if(date.IndexOf(',') > -1){ date = date.Substring(date.IndexOf(',') + 1); } // Remove () from date. "Mon, 13 Oct 2003 20:50:57 +0300 (EEST)" if(date.IndexOf(" (") > -1){ date = date.Substring(0,date.IndexOf(" (")); } int year = 1900; int month = 1; int day = 1; int hour = -1; int minute = -1; int second = -1; int zoneMinutes = -1; StringReader s = new StringReader(date); //--- Pase date --------------------------------------------------------------------// try{ day = Convert.ToInt32(s.ReadWord(true,new char[]{'.','-',' '},true)); } catch{ throw new Exception("Invalid date value '" + date + "', invalid day value !"); } try{ month = Convert.ToInt32(s.ReadWord(true,new char[]{'.','-',' '},true)); } catch{ throw new Exception("Invalid date value '" + date + "', invalid month value !"); } try{ year = Convert.ToInt32(s.ReadWord(true,new char[]{'.','-',' '},true)); } catch{ throw new Exception("Invalid date value '" + date + "', invalid year value !"); } //----------------------------------------------------------------------------------// //--- Parse time -------------------------------------------------------------------// // Time is optional, so parse it if its included. if(s.Available > 0){ try{ hour = Convert.ToInt32(s.ReadWord(true,new char[]{':'},true)); } catch{ throw new Exception("Invalid date value '" + date + "', invalid hour value !"); } try{ minute = Convert.ToInt32(s.ReadWord(true,new char[]{':'},false)); } catch{ throw new Exception("Invalid date value '" + date + "', invalid minute value !"); } s.ReadToFirstChar(); if(s.StartsWith(":")){ s.ReadSpecifiedLength(1); try{ string secondString = s.ReadWord(true,new char[]{' '},true); // Milli seconds specified, remove them. if(secondString.IndexOf('.') > -1){ secondString = secondString.Substring(0,secondString.IndexOf('.')); } second = Convert.ToInt32(secondString); } catch{ throw new Exception("Invalid date value '" + date + "', invalid second value !"); } } s.ReadToFirstChar(); if(s.Available > 3){ string timezone = s.SourceString.Replace(":",""); if(timezone.StartsWith("+") || timezone.StartsWith("-")){ bool utc_add_time = timezone.StartsWith("+"); // Remove +/- sign timezone = timezone.Substring(1); // padd time zone to 4 symbol. For example 200, will be 0200. while(timezone.Length < 4){ timezone = "0" + timezone; } try{ // time zone format hours|minutes int h = Convert.ToInt32(timezone.Substring(0,2)); int m = Convert.ToInt32(timezone.Substring(2)); if(utc_add_time){ zoneMinutes = 0 - ((h * 60) + m); } else{ zoneMinutes = (h * 60) + m; } } catch{ // Just skip time zone, if can't parse } } } } //---------------------------------------------------------------------------------// // Convert time to UTC if(hour != -1 && minute != -1 && second != -1){ DateTime d = new DateTime(year,month,day,hour,minute,second).AddMinutes(zoneMinutes); return new DateTime(d.Year,d.Month,d.Day,d.Hour,d.Minute,d.Second,DateTimeKind.Utc).ToLocalTime(); } else{ return new DateTime(year,month,day); } }
private void Fetch(string cmdTag, string argsText, bool uidFetch) { /* Rfc 3501 6.4.5 FETCH Command Arguments: message set message data item names Responses: untagged responses: FETCH Result: OK - fetch completed NO - fetch error: can't fetch that data BAD - command unknown or arguments invalid The FETCH command retrieves data associated with a message in the mailbox. The data items to be fetched can be either a single atom or a parenthesized list. Most data items, identified in the formal syntax under the msg-att-static rule, are static and MUST NOT change for any particular message. Other data items, identified in the formal syntax under the msg-att-dynamic rule, MAY change, either as a result of a STORE command or due to external events. For example, if a client receives an ENVELOPE for a message when it already knows the envelope, it can safely ignore the newly transmitted envelope. There are three macros which specify commonly-used sets of data items, and can be used instead of data items. A macro must be used by itself, and not in conjunction with other macros or data items. ALL Macro equivalent to: (FLAGS INTERNALDATE RFC822.SIZE ENVELOPE) FAST Macro equivalent to: (FLAGS INTERNALDATE RFC822.SIZE) FULL Macro equivalent to: (FLAGS INTERNALDATE RFC822.SIZE ENVELOPE BODY) The currently defined data items that can be fetched are: BODY Non-extensible form of BODYSTRUCTURE. BODY[<section>]<<partial>> The text of a particular body section. The section specification is a set of zero or more part specifiers delimited by periods. A part specifier is either a part number or one of the following: HEADER, HEADER.FIELDS, HEADER.FIELDS.NOT, MIME, and TEXT. An empty section specification refers to the entire message, including the header. Every message has at least one part number. Non-[MIME-IMB] messages, and non-multipart [MIME-IMB] messages with no encapsulated message, only have a part 1. Multipart messages are assigned consecutive part numbers, as they occur in the message. If a particular part is of type message or multipart, its parts MUST be indicated by a period followed by the part number within that nested multipart part. A part of type MESSAGE/RFC822 also has nested part numbers, referring to parts of the MESSAGE part's body. The HEADER, HEADER.FIELDS, HEADER.FIELDS.NOT, and TEXT part specifiers can be the sole part specifier or can be prefixed by one or more numeric part specifiers, provided that the numeric part specifier refers to a part of type MESSAGE/RFC822. The MIME part specifier MUST be prefixed by one or more numeric part specifiers. The HEADER, HEADER.FIELDS, and HEADER.FIELDS.NOT part specifiers refer to the [RFC-2822] header of the message or of an encapsulated [MIME-IMT] MESSAGE/RFC822 message. HEADER.FIELDS and HEADER.FIELDS.NOT are followed by a list of field-name (as defined in [RFC-2822]) names, and return a subset of the header. The subset returned by HEADER.FIELDS contains only those header fields with a field-name that matches one of the names in the list; similarly, the subset returned by HEADER.FIELDS.NOT contains only the header fields with a non-matching field-name. The field-matching is case-insensitive but otherwise exact. Subsetting does not exclude the [RFC-2822] delimiting blank line between the header and the body; the blank line is included in all header fetches, except in the case of a message which has no body and no blank line. The MIME part specifier refers to the [MIME-IMB] header for this part. The TEXT part specifier refers to the text body of the message, omitting the [RFC-2822] header. Here is an example of a complex message with some of its part specifiers: HEADER ([RFC-2822] header of the message) TEXT ([RFC-2822] text body of the message) MULTIPART/MIXED 1 TEXT/PLAIN 2 APPLICATION/OCTET-STREAM 3 MESSAGE/RFC822 3.HEADER ([RFC-2822] header of the message) 3.TEXT ([RFC-2822] text body of the message) MULTIPART/MIXED 3.1 TEXT/PLAIN 3.2 APPLICATION/OCTET-STREAM 4 MULTIPART/MIXED 4.1 IMAGE/GIF 4.1.MIME ([MIME-IMB] header for the IMAGE/GIF) 4.2 MESSAGE/RFC822 4.2.HEADER ([RFC-2822] header of the message) 4.2.TEXT ([RFC-2822] text body of the message) MULTIPART/MIXED 4.2.1 TEXT/PLAIN 4.2.2 MULTIPART/ALTERNATIVE 4.2.2.1 TEXT/PLAIN 4.2.2.2 TEXT/RICHTEXT It is possible to fetch a substring of the designated text. This is done by appending an open angle bracket ("<"), the octet position of the first desired octet, a period, the maximum number of octets desired, and a close angle bracket (">") to the part specifier. If the starting octet is beyond the end of the text, an empty string is returned. Any partial fetch that attempts to read beyond the end of the text is truncated as appropriate. A partial fetch that starts at octet 0 is returned as a partial fetch, even if this truncation happened. Note: This means that BODY[]<0.2048> of a 1500-octet message will return BODY[]<0> with a literal of size 1500, not BODY[]. Note: A substring fetch of a HEADER.FIELDS or HEADER.FIELDS.NOT part specifier is calculated after subsetting the header. The \Seen flag is implicitly set; if this causes the flags to change, they SHOULD be included as part of the FETCH responses. BODY.PEEK[<section>]<<partial>> An alternate form of BODY[<section>] that does not implicitly set the \Seen flag. BODYSTRUCTURE The [MIME-IMB] body structure of the message. This is computed by the server by parsing the [MIME-IMB] header fields in the [RFC-2822] header and [MIME-IMB] headers. ENVELOPE The envelope structure of the message. This is computed by the server by parsing the [RFC-2822] header into the component parts, defaulting various fields as necessary. FLAGS The flags that are set for this message. INTERNALDATE The internal date of the message. RFC822 Functionally equivalent to BODY[], differing in the syntax of the resulting untagged FETCH data (RFC822 is returned). RFC822.HEADER Functionally equivalent to BODY.PEEK[HEADER], differing in the syntax of the resulting untagged FETCH data (RFC822.HEADER is returned). RFC822.SIZE The [RFC-2822] size of the message. RFC822.TEXT Functionally equivalent to BODY[TEXT], differing in the syntax of the resulting untagged FETCH data (RFC822.TEXT is returned). UID The unique identifier for the message. Example: C: A654 FETCH 2:4 (FLAGS BODY[HEADER.FIELDS (DATE FROM)]) S: * 2 FETCH .... S: * 3 FETCH .... S: * 4 FETCH .... S: A654 OK FETCH completed */ if (!this.IsAuthenticated) { TcpStream.WriteLine(string.Format("{0} NO Authenticate first !", cmdTag)); return; } if (SelectedMailbox.Length == 0) { this.TcpStream.WriteLine(string.Format("{0} NO Select mailbox first !", cmdTag)); return; } TcpStream.MemoryBuffer = true; // Store start time long startTime = DateTime.Now.Ticks; IMAP_MessageItems_enum messageItems = IMAP_MessageItems_enum.None; #region Parse parameters string[] args = ParseParams(argsText); if (args.Length != 2) { this.TcpStream.WriteLine(string.Format("{0} BAD Invalid arguments", cmdTag)); return; } IMAP_SequenceSet sequenceSet = new IMAP_SequenceSet(); // Just try if it can be parsed as sequence-set try { if (uidFetch) { if (m_pSelectedFolder.Messages.Count > 0) { sequenceSet.Parse(args[0], m_pSelectedFolder.Messages[m_pSelectedFolder.Messages.Count - 1].UID); } } else { sequenceSet.Parse(args[0], m_pSelectedFolder.Messages.Count); } } // This isn't valid sequnce-set value catch { this.TcpStream.WriteLine(string.Format("{0} BAD Invalid <sequnce-set> value '{1}' Syntax: {{<command-tag> FETCH <sequnce-set> (<fetch-keys>)}}!", cmdTag, args[0])); return; } // Replace macros string fetchItems = args[1].ToUpper(); fetchItems = fetchItems.Replace("ALL", "FLAGS INTERNALDATE RFC822.SIZE ENVELOPE"); fetchItems = fetchItems.Replace("FAST", "FLAGS INTERNALDATE RFC822.SIZE"); fetchItems = fetchItems.Replace("FULL", "FLAGS INTERNALDATE RFC822.SIZE ENVELOPE BODY"); // If UID FETCH and no UID, we must implicity add it, it's required if (uidFetch && fetchItems.ToUpper().IndexOf("UID") == -1) { fetchItems += " UID"; } // Start parm parsing from left to end in while loop while params parsed or bad param found ArrayList fetchFlags = new ArrayList(); StringReader argsReader = new StringReader(fetchItems.Trim()); while (argsReader.Available > 0) { argsReader.ReadToFirstChar(); #region BODYSTRUCTURE // BODYSTRUCTURE if (argsReader.StartsWith("BODYSTRUCTURE")) { argsReader.ReadSpecifiedLength("BODYSTRUCTURE".Length); fetchFlags.Add(new object[] { "BODYSTRUCTURE" }); messageItems |= IMAP_MessageItems_enum.BodyStructure; } #endregion #region BODY, BODY[<section>]<<partial>>, BODY.PEEK[<section>]<<partial>> // BODY, BODY[<section>]<<partial>>, BODY.PEEK[<section>]<<partial>> else if (argsReader.StartsWith("BODY")) { // Remove BODY argsReader.ReadSpecifiedLength("BODY".Length); bool peek = false; // BODY.PEEK if (argsReader.StartsWith(".PEEK")) { // Remove .PEEK argsReader.ReadSpecifiedLength(".PEEK".Length); peek = true; } // [<section>]<<partial>> if (argsReader.StartsWith("[")) { // Read value between [] string section = ""; try { section = argsReader.ReadParenthesized(); } catch { this.TcpStream.WriteLine(cmdTag + " BAD Invalid BODY[], closing ] parenthesize is missing !"); return; } string originalSectionValue = section; string mimePartsSpecifier = ""; string sectionType = ""; string sectionArgs = ""; /* Validate <section> Section can be: "" - entire message [MimePartsSepcifier.]HEADER - message header [MimePartsSepcifier.]HEADER.FIELDS (headerFields) - message header fields [MimePartsSepcifier.]HEADER.FIELDS.NOT (headerFields) - message header fields except requested [MimePartsSepcifier.]TEXT - message text [MimePartsSepcifier.]MIME - same as header, different response */ if (section.Length > 0) { string[] section_args = section.Split(new char[] { ' ' }, 2); section = section_args[0]; if (section_args.Length == 2) { sectionArgs = section_args[1]; } if (section.EndsWith("HEADER")) { // Remove HEADER from end section = section.Substring(0, section.Length - "HEADER".Length); sectionType = "HEADER"; messageItems |= IMAP_MessageItems_enum.Header; } else if (section.EndsWith("HEADER.FIELDS")) { // Remove HEADER.FIELDS from end section = section.Substring(0, section.Length - "HEADER.FIELDS".Length); sectionType = "HEADER.FIELDS"; messageItems |= IMAP_MessageItems_enum.Header; } else if (section.EndsWith("HEADER.FIELDS.NOT")) { // Remove HEADER.FIELDS.NOT from end section = section.Substring(0, section.Length - "HEADER.FIELDS.NOT".Length); sectionType = "HEADER.FIELDS.NOT"; messageItems |= IMAP_MessageItems_enum.Header; } else if (section.EndsWith("TEXT")) { // Remove TEXT from end section = section.Substring(0, section.Length - "TEXT".Length); sectionType = "TEXT"; messageItems |= IMAP_MessageItems_enum.Message; } else if (section.EndsWith("MIME")) { // Remove MIME from end section = section.Substring(0, section.Length - "MIME".Length); sectionType = "MIME"; messageItems = IMAP_MessageItems_enum.Header; } // Remove last ., if there is any if (section.EndsWith(".")) { section = section.Substring(0, section.Length - 1); } // MimePartsSepcifier is specified, validate it. It can contain numbers only. if (section.Length > 0) { // Now we certainly need full message, because nested mime parts wanted messageItems |= IMAP_MessageItems_enum.Message; string[] sectionParts = section.Split('.'); foreach (string sectionPart in sectionParts) { if (!Core.IsNumber(sectionPart)) { this.TcpStream.WriteLine(string.Format("{0} BAD Invalid BODY[<section>] argument. Invalid <section>: {1}", cmdTag, section)); return; } } mimePartsSpecifier = section; } } else { messageItems |= IMAP_MessageItems_enum.Message; } long startPosition = -1; long length = -1; // See if partial fetch if (argsReader.StartsWith("<")) { /* <partial> syntax: startPosition[.endPosition] */ // Read partial value between <> string partial = ""; try { partial = argsReader.ReadParenthesized(); } catch { this.TcpStream.WriteLine(string.Format("{0} BAD Invalid BODY[]<start[.length]>, closing > parenthesize is missing !", cmdTag)); return; } string[] start_length = partial.Split('.'); // Validate <partial> if (start_length.Length == 0 || start_length.Length > 2 || !Core.IsNumber(start_length[0]) || (start_length.Length == 2 && !Core.IsNumber(start_length[1]))) { this.TcpStream.WriteLine(string.Format("{0} BAD Invalid BODY[]<partial> argument. Invalid <partial>: {1}", cmdTag, partial)); return; } startPosition = Convert.ToInt64(start_length[0]); if (start_length.Length == 2) { length = Convert.ToInt64(start_length[1]); } } // object[] structure for BODY[] // fetchFlagName // isPeek // mimePartsSpecifier // originalSectionValue // sectionType // sectionArgs // startPosition // length fetchFlags.Add(new object[] { "BODY[]", peek, mimePartsSpecifier, originalSectionValue, sectionType, sectionArgs, startPosition, length }); } // BODY else { fetchFlags.Add(new object[] { "BODY" }); messageItems |= IMAP_MessageItems_enum.BodyStructure; } } #endregion #region ENVELOPE // ENVELOPE else if (argsReader.StartsWith("ENVELOPE")) { argsReader.ReadSpecifiedLength("ENVELOPE".Length); fetchFlags.Add(new object[] { "ENVELOPE" }); messageItems |= IMAP_MessageItems_enum.Envelope; } #endregion #region FLAGS // FLAGS // The flags that are set for this message. else if (argsReader.StartsWith("FLAGS")) { argsReader.ReadSpecifiedLength("FLAGS".Length); fetchFlags.Add(new object[] { "FLAGS" }); } #endregion #region INTERNALDATE // INTERNALDATE else if (argsReader.StartsWith("INTERNALDATE")) { argsReader.ReadSpecifiedLength("INTERNALDATE".Length); fetchFlags.Add(new object[] { "INTERNALDATE" }); } #endregion #region RFC822.HEADER // RFC822.HEADER else if (argsReader.StartsWith("RFC822.HEADER")) { argsReader.ReadSpecifiedLength("RFC822.HEADER".Length); fetchFlags.Add(new object[] { "RFC822.HEADER" }); messageItems |= IMAP_MessageItems_enum.Header; } #endregion #region RFC822.SIZE // RFC822.SIZE // The [RFC-2822] size of the message. else if (argsReader.StartsWith("RFC822.SIZE")) { argsReader.ReadSpecifiedLength("RFC822.SIZE".Length); fetchFlags.Add(new object[] { "RFC822.SIZE" }); } #endregion #region RFC822.TEXT // RFC822.TEXT else if (argsReader.StartsWith("RFC822.TEXT")) { argsReader.ReadSpecifiedLength("RFC822.TEXT".Length); fetchFlags.Add(new object[] { "RFC822.TEXT" }); messageItems |= IMAP_MessageItems_enum.Message; } #endregion #region RFC822 // RFC822 NOTE: RFC822 must be below RFC822.xxx or is parsed wrong ! else if (argsReader.StartsWith("RFC822")) { argsReader.ReadSpecifiedLength("RFC822".Length); fetchFlags.Add(new object[] { "RFC822" }); messageItems |= IMAP_MessageItems_enum.Message; } #endregion #region UID // UID // The unique identifier for the message. else if (argsReader.StartsWith("UID")) { argsReader.ReadSpecifiedLength("UID".Length); fetchFlags.Add(new object[] { "UID" }); } #endregion // This must be unknown fetch flag else { this.TcpStream.WriteLine(string.Format("{0} BAD Invalid fetch-items argument. Unkown part starts from: {1}", cmdTag, argsReader.SourceString)); return; } } #endregion // ToDo: ??? But non of the servers do it ? // The server should respond with a tagged BAD response to a command that uses a message // sequence number greater than the number of messages in the selected mailbox. This // includes "*" if the selected mailbox is empty. // if(m_Messages.Count == 0 || ){ // SendData(cmdTag + " BAD Sequence number greater than the number of messages in the selected mailbox !\r\n"); // return; // } // Create buffered writer, so we make less network calls. for (int i = 0; i < m_pSelectedFolder.Messages.Count; i++) { IMAP_Message msg = m_pSelectedFolder.Messages[i]; // For UID FETCH we must compare UIDs and for normal FETCH message numbers. bool sequenceSetContains = false; if (uidFetch) { sequenceSetContains = sequenceSet.Contains(msg.UID); } else { sequenceSetContains = sequenceSet.Contains(i + 1); } if (sequenceSetContains) { IMAP_eArgs_MessageItems eArgs = null; // Get message items only if they are needed. if (messageItems != IMAP_MessageItems_enum.None) { // Raise event GetMessageItems to get all neccesary message itmes eArgs = ImapServer.OnGetMessageItems(this, msg, messageItems); // Message doesn't exist any more, notify email client. if (!eArgs.MessageExists) { TcpStream.Write("* " + msg.SequenceNo + " EXPUNGE"); ImapServer.OnDeleteMessage(this, msg); m_pSelectedFolder.Messages.Remove(msg); i--; continue; } try { // Ensure that all requested items were provided. eArgs.Validate(); } catch (Exception x) { ImapServer.OnSysError(x.Message, x); this.TcpStream.WriteLine(string.Format("{0} NO Internal IMAP server component error: {1}", cmdTag, x.Message)); return; } } // Write fetch start data "* msgNo FETCH (" TcpStream.Write("* " + (i + 1) + " FETCH ("); IMAP_MessageFlags msgFlagsOr = msg.Flags; // Construct reply here, based on requested fetch items int nCount = 0; foreach (object[] fetchFlag in fetchFlags) { string fetchFlagName = (string)fetchFlag[0]; #region BODY // BODY if (fetchFlagName == "BODY") { // Sets \seen flag msg.SetFlags(msg.Flags | IMAP_MessageFlags.Seen); // BODY () TcpStream.Write("BODY " + eArgs.BodyStructure); } #endregion #region BODY[], BODY.PEEK[] // BODY[<section>]<<partial>>, BODY.PEEK[<section>]<<partial>> else if (fetchFlagName == "BODY[]") { // Force to write all buffered data. TcpStream.Flush(); // object[] structure for BODY[] // fetchFlagName // isPeek // mimePartsSpecifier // originalSectionValue // sectionType // sectionArgs // startPosition // length bool isPeek = (bool)fetchFlag[1]; string mimePartsSpecifier = (string)fetchFlag[2]; string originalSectionValue = (string)fetchFlag[3]; string sectionType = (string)fetchFlag[4]; string sectionArgs = (string)fetchFlag[5]; long startPosition = (long)fetchFlag[6]; long length = (long)fetchFlag[7]; // Difference between BODY[] and BODY.PEEK[] is that .PEEK won't set seen flag if (!isPeek) { // Sets \seen flag msg.SetFlags(msg.Flags | IMAP_MessageFlags.Seen); } /* Section value: "" - entire message HEADER - message header HEADER.FIELDS - message header fields HEADER.FIELDS.NOT - message header fields except requested TEXT - message text MIME - same as header, different response */ Stream dataStream = null; if (sectionType == "" && mimePartsSpecifier == "") { dataStream = eArgs.MessageStream; } else { Mime parser = null; try { if (eArgs.MessageStream == null) { parser = Mime.Parse(eArgs.Header); } else { parser = Mime.Parse(eArgs.MessageStream); } } // Invalid message, parsing failed catch { parser = Mime.CreateSimple(new AddressList(), new AddressList(), "BAD Message", "This is BAD message, mail server failed to parse it !", ""); } MimeEntity currentEntity = parser.MainEntity; // Specific mime entity requested, get it if (mimePartsSpecifier != "") { currentEntity = FetchHelper.GetMimeEntity(parser, mimePartsSpecifier); } if (currentEntity != null) { if (sectionType == "HEADER") { dataStream = new MemoryStream(FetchHelper.GetMimeEntityHeader(currentEntity)); } else if (sectionType == "HEADER.FIELDS") { dataStream = new MemoryStream(FetchHelper.ParseHeaderFields(sectionArgs, currentEntity)); } else if (sectionType == "HEADER.FIELDS.NOT") { dataStream = new MemoryStream(FetchHelper.ParseHeaderFieldsNot(sectionArgs, currentEntity)); } else if (sectionType == "TEXT") { try { if (currentEntity.DataEncoded != null) { dataStream = new MemoryStream(currentEntity.DataEncoded); } } catch { // This probably multipart entity, data isn't available } } else if (sectionType == "MIME") { dataStream = new MemoryStream(FetchHelper.GetMimeEntityHeader(currentEntity)); } else if (sectionType == "") { try { dataStream = new MemoryStream(currentEntity.DataEncoded); } catch { // This probably multipart entity, data isn't available } } } } // Partial fetch. Reports <origin position> in fetch reply. if (startPosition > -1) { if (dataStream == null) { this.TcpStream.Write("BODY[" + originalSectionValue + "]<" + startPosition.ToString() + "> \"\"\r\n"); } else { long lengthToSend = length; if (lengthToSend == -1) { lengthToSend = (dataStream.Length - dataStream.Position) - startPosition; } if ((lengthToSend + startPosition) > (dataStream.Length - dataStream.Position)) { lengthToSend = (dataStream.Length - dataStream.Position) - startPosition; } if (startPosition >= (dataStream.Length - dataStream.Position)) { this.TcpStream.Write("BODY[" + originalSectionValue + "]<" + startPosition.ToString() + "> \"\"\r\n"); } else { this.TcpStream.Write("BODY[" + originalSectionValue + "]<" + startPosition.ToString() + "> {" + lengthToSend + "}\r\n"); dataStream.Position += startPosition; this.TcpStream.WriteStream(dataStream, lengthToSend); } } } // Normal fetch else { if (dataStream == null) { this.TcpStream.Write("BODY[" + originalSectionValue + "] \"\"\r\n"); } else { this.TcpStream.Write("BODY[" + originalSectionValue + "] {" + (dataStream.Length - dataStream.Position) + "}\r\n"); this.TcpStream.WriteStream(dataStream); } } } #endregion #region BODYSTRUCTURE // BODYSTRUCTURE else if (fetchFlagName == "BODYSTRUCTURE") { TcpStream.Write("BODYSTRUCTURE " + eArgs.BodyStructure); } #endregion #region ENVELOPE // ENVELOPE else if (fetchFlagName == "ENVELOPE") { TcpStream.Write("ENVELOPE " + eArgs.Envelope); } #endregion #region FLAGS // FLAGS else if (fetchFlagName == "FLAGS") { TcpStream.Write("FLAGS (" + msg.FlagsString + ")"); } #endregion #region INTERNALDATE // INTERNALDATE else if (fetchFlagName == "INTERNALDATE") { // INTERNALDATE "date" TcpStream.Write("INTERNALDATE \"" + IMAP_Utils.DateTimeToString(msg.InternalDate) + "\""); } #endregion #region RFC822 // RFC822 else if (fetchFlagName == "RFC822") { // Force to write all buffered data. TcpStream.Flush(); // Sets \seen flag msg.SetFlags(msg.Flags | IMAP_MessageFlags.Seen); // RFC822 {size} // msg data this.TcpStream.Write("RFC822 {" + eArgs.MessageSize.ToString() + "}\r\n"); this.TcpStream.WriteStream(eArgs.MessageStream); } #endregion #region RFC822.HEADER // RFC822.HEADER else if (fetchFlagName == "RFC822.HEADER") { // Force to write all buffered data. TcpStream.Flush(); // RFC822.HEADER {size} // msg header data this.TcpStream.Write("RFC822.HEADER {" + eArgs.Header.Length + "}\r\n"); this.TcpStream.Write(eArgs.Header); } #endregion #region RFC822.SIZE // RFC822.SIZE else if (fetchFlagName == "RFC822.SIZE") { // RFC822.SIZE size TcpStream.Write("RFC822.SIZE " + msg.Size); } #endregion #region RFC822.TEXT // RFC822.TEXT else if (fetchFlagName == "RFC822.TEXT") { // Force to write all buffered data. TcpStream.Flush(); // Sets \seen flag msg.SetFlags(msg.Flags | IMAP_MessageFlags.Seen); //--- Find body text entity ------------------------------------// Mime parser = Mime.Parse(eArgs.MessageStream); MimeEntity bodyTextEntity = null; if (parser.MainEntity.ContentType == MediaType_enum.NotSpecified) { if (parser.MainEntity.DataEncoded != null) { bodyTextEntity = parser.MainEntity; } } else { MimeEntity[] entities = parser.MimeEntities; foreach (MimeEntity entity in entities) { if (entity.ContentType == MediaType_enum.Text_plain) { bodyTextEntity = entity; break; } } } //----------------------------------------------------------------// // RFC822.TEXT {size} // msg text byte[] data = null; if (bodyTextEntity != null) { data = bodyTextEntity.DataEncoded; } else { data = System.Text.Encoding.ASCII.GetBytes(""); } this.TcpStream.Write("RFC822.TEXT {" + data.Length + "}\r\n"); this.TcpStream.Write(data); } #endregion #region UID // UID else if (fetchFlagName == "UID") { TcpStream.Write("UID " + msg.UID); } #endregion nCount++; // Write fetch item separator data " " // We don't write it for last item if (nCount < fetchFlags.Count) { TcpStream.Write(" "); } } // Write fetch end data ")" TcpStream.Write(")\r\n"); // Free event args, close message stream, ... . if (eArgs != null) { eArgs.Dispose(); } // Set message flags here if required or changed if (((int)IMAP_MessageFlags.Recent & (int)msg.Flags) != 0 || msgFlagsOr != msg.Flags) { msg.SetFlags(msg.Flags & ~IMAP_MessageFlags.Recent); ImapServer.OnStoreMessageFlags(this, msg); } } } // Force to write all buffered data. TcpStream.Flush(); this.TcpStream.WriteLine(string.Format("{0} OK FETCH completed in {1} seconds", cmdTag, ((DateTime.Now.Ticks - startTime) / (decimal)10000000).ToString("f2"))); }
/// <summary> /// Decodes "encoded-word"'s from the specified text. For more information see RFC 2047. /// </summary> /// <param name="text">Text to decode.</param> /// <returns>Returns decoded text.</returns> public static string DecodeWords(string text) { if(text == null){ return null; } /* RFC 2047 2. Syntax of encoded-words. An 'encoded-word' is defined by the following ABNF grammar. The notation of RFC 822 is used, with the exception that white space characters MUST NOT appear between components of an 'encoded-word'. encoded-word = "=?" charset "?" encoding "?" encoded-text "?=" charset = token ; see section 3 encoding = token ; see section 4 token = 1*<Any CHAR except SPACE, CTLs, and especials> especials = "(" / ")" / "<" / ">" / "@" / "," / ";" / ":" / " <"> / "/" / "[" / "]" / "?" / "." / "=" encoded-text = 1*<Any printable ASCII character other than "?" or SPACE> ; (but see "Use of encoded-words in message headers", section 5) Both 'encoding' and 'charset' names are case-independent. Thus the charset name "ISO-8859-1" is equivalent to "iso-8859-1", and the encoding named "Q" may be spelled either "Q" or "q". An 'encoded-word' may not be more than 75 characters long, including 'charset', 'encoding', 'encoded-text', and delimiters. If it is desirable to encode more text than will fit in an 'encoded-word' of 75 characters, multiple 'encoded-word's (separated by CRLF SPACE) may be used. IMPORTANT: 'encoded-word's are designed to be recognized as 'atom's by an RFC 822 parser. As a consequence, unencoded white space characters (such as SPACE and HTAB) are FORBIDDEN within an 'encoded-word'. For example, the character sequence =?iso-8859-1?q?this is some text?= would be parsed as four 'atom's, rather than as a single 'atom' (by an RFC 822 parser) or 'encoded-word' (by a parser which understands 'encoded-words'). The correct way to encode the string "this is some text" is to encode the SPACE characters as well, e.g. =?iso-8859-1?q?this=20is=20some=20text?= */ StringReader r = new StringReader(text); StringBuilder retVal = new StringBuilder(); // We need to loop all words, if encoded word, decode it, othwerwise just append to return value. bool lastIsEncodedWord = false; while(r.Available > 0){ string whiteSpaces = r.ReadToFirstChar(); // Probably is encoded-word, we try to parse it. if(r.StartsWith("=?") && r.SourceString.IndexOf("?=") > -1){ StringBuilder encodedWord = new StringBuilder(); string decodedWord = null; try{ // NOTE: We can't read encoded word and then split !!!, we need to read each part. // Remove =? encodedWord.Append(r.ReadSpecifiedLength(2)); // Read charset string charset = r.QuotedReadToDelimiter('?'); encodedWord.Append(charset + "?"); // Read encoding string encoding = r.QuotedReadToDelimiter('?'); encodedWord.Append(encoding + "?"); // Read text string encodedText = r.QuotedReadToDelimiter('?'); encodedWord.Append(encodedText + "?"); // We must have remaining '=' here if(r.StartsWith("=")){ encodedWord.Append(r.ReadSpecifiedLength(1)); Encoding c = Encoding.GetEncoding(charset); if(encoding.ToLower() == "q"){ decodedWord = Core.QDecode(c,encodedText); } else if(encoding.ToLower() == "b"){ decodedWord = c.GetString(Core.Base64Decode(Encoding.Default.GetBytes(encodedText))); } } } catch{ // Not encoded-word or contains unknwon charset/encoding, so leave // encoded-word as is. } /* RFC 2047 6.2. When displaying a particular header field that contains multiple 'encoded-word's, any 'linear-white-space' that separates a pair of adjacent 'encoded-word's is ignored. (This is to allow the use of multiple 'encoded-word's to represent long strings of unencoded text, without having to separate 'encoded-word's where spaces occur in the unencoded text.) */ if(!lastIsEncodedWord){ retVal.Append(whiteSpaces); } // Decoding failed for that encoded-word, leave encoded-word as is. if(decodedWord == null){ retVal.Append(encodedWord.ToString()); } // We deocded encoded-word successfully. else{ retVal.Append(decodedWord); } lastIsEncodedWord = true; } // Normal word. else if(r.StartsWithWord()){ retVal.Append(whiteSpaces + r.ReadWord(false)); lastIsEncodedWord = false; } // We have some separator or parenthesize. else{ retVal.Append(whiteSpaces + r.ReadSpecifiedLength(1)); } } return retVal.ToString(); }
/// <summary> /// Reads IMAP string-literal/string/astring/nstring/utf8-quoted from string reader. /// </summary> /// <param name="reader">String reader.</param> /// <returns>Returns IMAP string.</returns> /// <exception cref="ArgumentNullException">Is raised when <b>reader</b> is null reference.</exception> public static string ReadString(StringReader reader) { if(reader == null){ throw new ArgumentNullException("reader"); } reader.ReadToFirstChar(); // We have string-literal. if(reader.SourceString.StartsWith("{")){ int literalSize = Convert.ToInt32(reader.ReadParenthesized()); // Literal has CRLF ending, skip it. reader.ReadSpecifiedLength(2); return reader.ReadSpecifiedLength(literalSize); } // utf8-quoted old rfc 5738 else if(reader.StartsWith("*\"")){ reader.ReadSpecifiedLength(1); return reader.ReadWord(); } // string/astring/nstring else{ string word = reader.ReadWord(); // nstring if(string.Equals(word,"NIL",StringComparison.InvariantCultureIgnoreCase)){ return null; } return word; } }
/// <summary> /// Gets specified message from server and stores to specified stream. /// </summary> /// <param name="uid">Message UID which to get.</param> /// <param name="storeStream">Stream where to store message.</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,not authenticated and folder not selected.</exception> public void FetchMessage(int uid, Stream storeStream) { 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."); } // Send fetch command line to server. // mwa fix for gmail change, message was being marked as read automatically without the PEEK modifier string line = GetNextCmdTag() + " UID FETCH " + uid + " BODY.PEEK[]"; int countWritten = this.TcpStream.WriteLine(line); LogAddWrite(countWritten, line); // Read un-tagged response lines while we get final response line. while (true) { line = this.ReadLine(); // We have un-tagged resposne. if (line.StartsWith("*")) { if (IsStatusResponse(line)) { ProcessStatusResponse(line); } else if (line.ToUpper().ToString().IndexOf("BODY[") > -1) { if (line.IndexOf('{') > -1) { StringReader r = new StringReader(line); while (r.Available > 0 && !r.StartsWith("{")) { r.ReadSpecifiedLength(1); } int sizeOfData = Convert.ToInt32(r.ReadParenthesized()); this.TcpStream.ReadFixedCount(storeStream, sizeOfData); LogAddRead(sizeOfData, "Readed " + sizeOfData + " bytes."); line = this.ReadLine(); } } } else { break; } } if (!RemoveCmdTag(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(); }
public byte[] GetFilters() { if (IsDisposed) { throw new ObjectDisposedException(GetType().Name); } if (!IsConnected) { throw new InvalidOperationException("You must connect first."); } if (!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."); } // Send fetch command line to server. string line = GetNextCmdTag() + " X-GET-FILTER "; int countWritten = TcpStream.WriteLine(line); LogAddWrite(countWritten, line); using (MemoryStream store = new MemoryStream()) { // Read un-tagged response lines while we get final response line. while (true) { line = ReadLine(); // We have un-tagged resposne. if (line.StartsWith("*")) { if (IsStatusResponse(line)) { ProcessStatusResponse(line); } else if (line.ToUpper().IndexOf("FILTER") > -1) { if (line.IndexOf('{') > -1) { StringReader r = new StringReader(line); while (r.Available > 0 && !r.StartsWith("{")) { r.ReadSpecifiedLength(1); } int sizeOfData = Convert.ToInt32(r.ReadParenthesized()); TcpStream.ReadFixedCount(store, sizeOfData); LogAddRead(sizeOfData, "Readed " + sizeOfData + " bytes."); line = ReadLine(); } } } else { break; } } if (!RemoveCmdTag(line).ToUpper().StartsWith("OK")) { throw new IMAP_ClientException(line); } byte[] buffer = store.GetBuffer(); if (buffer.Length>0) { return Convert.FromBase64String(Encoding.UTF8.GetString(buffer)); } else { return null; } } }
/// <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 IMAP string/astring/nstring/utf8-quoted from string reader. /// </summary> /// <param name="reader">String reader.</param> /// <returns>Returns IMAP string.</returns> /// <exception cref="ArgumentNullException">Is raised when <b>reader</b> is null reference.</exception> internal static string ReadString(StringReader reader) { if(reader == null){ throw new ArgumentNullException("reader"); } reader.ReadToFirstChar(); // utf8-quoted if(reader.StartsWith("*\"")){ reader.ReadSpecifiedLength(1); return reader.ReadWord(); } // string/astring/nstring else{ string word = reader.ReadWord(); // nstring if(string.Equals(word,"NIL",StringComparison.InvariantCultureIgnoreCase)){ return null; } return word; } }
/// <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> public IMAP_FetchItem[] FetchMessages(IMAP_SequenceSet sequence_set,IMAP_FetchItem_Flags fetchFlags,bool setSeenFlag,bool uidFetch) { if(!m_Connected){ throw new Exception("You must connect first !"); } if(!m_Authenticated){ throw new Exception("You must authenticate first !"); } if(m_SelectedFolder.Length == 0){ throw new Exception("You must select folder first !"); } List<IMAP_FetchItem> fetchItems = new List<IMAP_FetchItem>(); //--- Construct FETCH command line -----------------------------------------------------------------------// string fetchCmdLine = "a1"; 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 m_pSocket.WriteLine(fetchCmdLine); // Must get lines with * and cmdTag + OK or cmdTag BAD/NO string reply = m_pSocket.ReadLine(50000); // Read multiline response while(reply.StartsWith("*")){ // Fetch may return status response there, skip them if(IsStatusResponse(reply)){ // Read next fetch item or server response reply = m_pSocket.ReadLine(50000); continue; } 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 * reply = reply.Substring(1).TrimStart(); // Get message number no = Convert.ToInt32(reply.Substring(0,reply.IndexOf(" "))); // Get rid of FETCH and parse params. Reply:* 1 FETCH (UID 12 BODY[] ...) reply = reply.Substring(reply.IndexOf("FETCH (") + 7); StringReader r = new StringReader(reply); // 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)); MemoryStream strm = new MemoryStream(); m_pSocket.ReadSpecifiedLength(countToRead,strm); r.AppenString(TextUtils.QuoteString(System.Text.Encoding.Default.GetString(strm.ToArray()))); // Read fetch continuing line r.AppenString(m_pSocket.ReadLine(50000)); } // 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); m_pSocket.ReadSpecifiedLength(dataLength,storeStrm); data = storeStrm.ToArray(); // Read fetch continuing line r.AppenString(m_pSocket.ReadLine(50000).Trim()); } #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)); // Read next fetch item or server response reply = m_pSocket.ReadLine(50000); } // We must get OK or otherwise there is error if(!RemoveCmdTag(reply).ToUpper().StartsWith("OK")){ if(!reply.ToUpper().StartsWith("NO")){ throw new Exception("Server returned:" + reply); } } return fetchItems.ToArray(); }
/// <summary> /// Gets specified message from server and stores to specified stream. /// </summary> /// <param name="uid">Message UID which to get.</param> /// <param name="storeStream">Stream where to store message.</param> public void FetchMessage(int uid,Stream storeStream) { if(!m_Connected){ throw new Exception("You must connect first !"); } if(!m_Authenticated){ throw new Exception("You must authenticate first !"); } if(m_SelectedFolder.Length == 0){ throw new Exception("You must select folder first !"); } m_pSocket.WriteLine("a1 UID FETCH " + uid + " BODY[]"); string reply = m_pSocket.ReadLine(50000); // Read multiline response while(reply.StartsWith("*")){ // Fetch may return status response there, skip them if(IsStatusResponse(reply)){ // Read next server response reply = m_pSocket.ReadLine(50000); continue; } reply = RemoveCmdTag(reply); // We must get here: BODY[] {sizeOfData} if(reply.ToUpper().ToString().IndexOf("BODY[") > - 1){ if(reply.IndexOf('{') > -1){ StringReader r = new StringReader(reply); while(r.Available > 0 && !r.StartsWith("{")){ r.ReadSpecifiedLength(1); } int sizeOfData = Convert.ToInt32(r.ReadParenthesized()); m_pSocket.ReadSpecifiedLength(sizeOfData,storeStream); m_pSocket.ReadLine(); } } // Read next server response reply = m_pSocket.ReadLine(50000); } // We must get OK or otherwise there is error if(!RemoveCmdTag(reply).ToUpper().StartsWith("OK")){ if(!reply.ToUpper().StartsWith("NO")){ throw new Exception("Server returned:" + reply); } } }