// // ImapCommand.cs // // Author: Jeffrey Stedfast // // Copyright (c) 2013-2020 .NET Foundation and Contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. // using System; using System.IO; using System.Text; using System.Threading; using System.Globalization; using System.Threading.Tasks; using System.Collections.Generic; using MimeKit; using MimeKit.IO; using MimeKit.Utils; using SslStream = MailKit.Net.SslStream; using NetworkStream = MailKit.Net.NetworkStream; namespace MailKit.Net.Imap { /// /// An IMAP continuation handler. /// /// /// All exceptions thrown by the handler are considered fatal and will /// force-disconnect the connection. If a non-fatal error occurs, set /// it on the property. /// delegate Task ImapContinuationHandler (ImapEngine engine, ImapCommand ic, string text, bool doAsync); /// /// An IMAP untagged response handler. /// /// /// Most IMAP commands return their results in untagged responses. /// delegate Task ImapUntaggedHandler (ImapEngine engine, ImapCommand ic, int index, bool doAsync); delegate void ImapCommandResetHandler (ImapCommand ic); /// /// IMAP command status. /// enum ImapCommandStatus { Created, Queued, Active, Complete, Error } enum ImapLiteralType { String, //Stream, MimeMessage } enum ImapStringType { Atom, QString, Literal, Nil } /// /// An IMAP IDLE context. /// /// /// An IMAP IDLE command does not work like normal commands. Unlike most commands, /// the IDLE command does not end until the client sends a separate "DONE" command. /// In order to facilitate this, the way this works is that the consumer of MailKit's /// IMAP APIs provides a 'doneToken' which signals to the command-processing loop to /// send the "DONE" command. Since, like every other IMAP command, it is also necessary to /// provide a means of cancelling the IDLE command, it becomes necessary to link the /// 'doneToken' and the 'cancellationToken' together. /// sealed class ImapIdleContext : IDisposable { static readonly byte[] DoneCommand = Encoding.ASCII.GetBytes ("DONE\r\n"); CancellationTokenRegistration registration; /// /// Initializes a new instance of the class. /// /// /// Creates a new . /// /// The IMAP engine. /// The done token. /// The cancellation token. public ImapIdleContext (ImapEngine engine, CancellationToken doneToken, CancellationToken cancellationToken) { CancellationToken = cancellationToken; DoneToken = doneToken; Engine = engine; } /// /// Get the engine. /// /// /// Gets the engine. /// /// The engine. public ImapEngine Engine { get; private set; } /// /// Get the cancellation token. /// /// /// Get the cancellation token. /// /// The cancellation token. public CancellationToken CancellationToken { get; private set; } /// /// Get the done token. /// /// /// Gets the done token. /// /// The done token. public CancellationToken DoneToken { get; private set; } #if false /// /// Get whether or not cancellation has been requested. /// /// /// Gets whether or not cancellation has been requested. /// /// true if cancellation has been requested; otherwise, false. public bool IsCancellationRequested { get { return CancellationToken.IsCancellationRequested; } } /// /// Get whether or not the IDLE command should be ended. /// /// /// Gets whether or not the IDLE command should be ended. /// /// true if the IDLE command should end; otherwise, false. public bool IsDoneRequested { get { return DoneToken.IsCancellationRequested; } } #endif void IdleComplete () { if (Engine.State == ImapEngineState.Idle) { try { Engine.Stream.Write (DoneCommand, 0, DoneCommand.Length, CancellationToken); Engine.Stream.Flush (CancellationToken); } catch { return; } Engine.State = ImapEngineState.Selected; } } /// /// Callback method to be used as the ImapCommand's ContinuationHandler. /// /// /// Callback method to be used as the ImapCommand's ContinuationHandler. /// /// The ImapEngine. /// The ImapCommand. /// The text. /// true if the command is being run asynchronously; otherwise, false. /// public Task ContinuationHandler (ImapEngine engine, ImapCommand ic, string text, bool doAsync) { Engine.State = ImapEngineState.Idle; registration = DoneToken.Register (IdleComplete); return Task.FromResult (true); } /// /// Releases all resource used by the object. /// /// Call when you are finished using the . The /// method leaves the in an unusable state. After /// calling , you must release all references to the /// so the garbage collector can reclaim the memory that the /// was occupying. public void Dispose () { registration.Dispose (); } } /// /// An IMAP literal object. /// /// /// The literal can be a string, byte[], Stream, or a MimeMessage. /// class ImapLiteral { public readonly ImapLiteralType Type; public readonly object Literal; readonly FormatOptions format; readonly Action update; /// /// Initializes a new instance of the class. /// /// /// Creates a new . /// /// The formatting options. /// The message. /// The progress update action. public ImapLiteral (FormatOptions options, MimeMessage message, Action action = null) { format = options.Clone (); format.NewLineFormat = NewLineFormat.Dos; update = action; Type = ImapLiteralType.MimeMessage; Literal = message; } /// /// Initializes a new instance of the class. /// /// /// Creates a new . /// /// The formatting options. /// The literal. public ImapLiteral (FormatOptions options, byte[] literal) { format = options.Clone (); format.NewLineFormat = NewLineFormat.Dos; Type = ImapLiteralType.String; Literal = literal; } /// /// Get the length of the literal, in bytes. /// /// /// Gets the length of the literal, in bytes. /// /// The length. public long Length { get { if (Type == ImapLiteralType.String) return ((byte[]) Literal).Length; using (var measure = new MeasuringStream ()) { //if (Type == ImapLiteralType.Stream) { // var stream = (Stream) Literal; // stream.CopyTo (measure, 4096); // stream.Position = 0; // return measure.Length; //} ((MimeMessage) Literal).WriteTo (format, measure); return measure.Length; } } } /// /// Write the literal to the specified stream. /// /// /// Writes the literal to the specified stream. /// /// The stream. /// Whether the literal should be written asynchronously or not. /// The cancellation token. public async Task WriteToAsync (ImapStream stream, bool doAsync, CancellationToken cancellationToken) { if (Type == ImapLiteralType.String) { var bytes = (byte[]) Literal; if (doAsync) { await stream.WriteAsync (bytes, 0, bytes.Length, cancellationToken).ConfigureAwait (false); await stream.FlushAsync (cancellationToken).ConfigureAwait (false); } else { stream.Write (bytes, 0, bytes.Length, cancellationToken); stream.Flush (cancellationToken); } return; } //if (Type == ImapLiteralType.Stream) { // var literal = (Stream) Literal; // var buf = new byte[4096]; // int nread; // if (doAsync) { // while ((nread = await literal.ReadAsync (buf, 0, buf.Length, cancellationToken).ConfigureAwait (false)) > 0) // await stream.WriteAsync (buf, 0, nread, cancellationToken).ConfigureAwait (false); // await stream.FlushAsync (cancellationToken).ConfigureAwait (false); // } else { // while ((nread = literal.Read (buf, 0, buf.Length)) > 0) // stream.Write (buf, 0, nread, cancellationToken); // stream.Flush (cancellationToken); // } // return; //} var message = (MimeMessage) Literal; using (var s = new ProgressStream (stream, update)) { if (doAsync) { await message.WriteToAsync (format, s, cancellationToken).ConfigureAwait (false); await s.FlushAsync (cancellationToken).ConfigureAwait (false); } else { message.WriteTo (format, s, cancellationToken); s.Flush (cancellationToken); } } } } /// /// A partial IMAP command. /// /// /// IMAP commands that contain literal strings are broken up into multiple parts /// in case the IMAP server does not support the LITERAL+ extension. These parts /// are then sent individually as we receive "+" responses from the server. /// class ImapCommandPart { public readonly byte[] Command; public readonly ImapLiteral Literal; public readonly bool WaitForContinuation; public ImapCommandPart (byte[] command, ImapLiteral literal, bool wait = true) { WaitForContinuation = wait; Command = command; Literal = literal; } } /// /// An IMAP command. /// class ImapCommand { static readonly byte[] UTF8LiteralTokenPrefix = Encoding.ASCII.GetBytes ("UTF8 (~{"); static readonly byte[] LiteralTokenSuffix = { (byte) '}', (byte) '\r', (byte) '\n' }; static readonly byte[] Nil = { (byte) 'N', (byte) 'I', (byte) 'L' }; static readonly byte[] NewLine = { (byte) '\r', (byte) '\n' }; static readonly byte[] LiteralTokenPrefix = { (byte) '{' }; public Dictionary UntaggedHandlers { get; private set; } public ImapContinuationHandler ContinuationHandler { get; set; } public CancellationToken CancellationToken { get; private set; } public ImapCommandStatus Status { get; internal set; } public ImapCommandResponse Response { get; internal set; } public ITransferProgress Progress { get; internal set; } public Exception Exception { get; internal set; } public readonly List RespCodes; public string ResponseText { get; internal set; } public ImapFolder Folder { get; private set; } public object UserData { get; internal set; } public bool ListReturnsSubscribed { get; internal set; } public bool Logout { get; private set; } public bool Lsub { get; internal set; } public string Tag { get; private set; } public bool Bye { get; internal set; } readonly List parts = new List (); readonly ImapEngine Engine; long totalSize, nwritten; int current; /// /// Initializes a new instance of the class. /// /// /// Creates a new . /// /// The IMAP engine that will be sending the command. /// The cancellation token. /// The IMAP folder that the command operates on. /// The formatting options. /// The command format. /// The command arguments. public ImapCommand (ImapEngine engine, CancellationToken cancellationToken, ImapFolder folder, FormatOptions options, string format, params object[] args) { UntaggedHandlers = new Dictionary (StringComparer.OrdinalIgnoreCase); Logout = format.Equals ("LOGOUT\r\n", StringComparison.Ordinal); RespCodes = new List (); CancellationToken = cancellationToken; Response = ImapCommandResponse.None; Status = ImapCommandStatus.Created; Engine = engine; Folder = folder; using (var builder = new MemoryStream ()) { byte[] buf, utf8 = new byte[8]; int argc = 0; string str; for (int i = 0; i < format.Length; i++) { if (format[i] == '%') { switch (format[++i]) { case '%': // a literal % builder.WriteByte ((byte) '%'); break; case 'd': // an integer str = ((int) args[argc++]).ToString (CultureInfo.InvariantCulture); buf = Encoding.ASCII.GetBytes (str); builder.Write (buf, 0, buf.Length); break; case 'u': // an unsigned integer str = ((uint) args[argc++]).ToString (CultureInfo.InvariantCulture); buf = Encoding.ASCII.GetBytes (str); builder.Write (buf, 0, buf.Length); break; case 's': str = (string) args[argc++]; buf = Encoding.ASCII.GetBytes (str); builder.Write (buf, 0, buf.Length); break; case 'F': // an ImapFolder var utf7 = ((ImapFolder) args[argc++]).EncodedName; AppendString (options, true, builder, utf7); break; case 'L': // a MimeMessage or a byte[] var arg = args[argc++]; ImapLiteral literal; byte[] prefix; if (arg is MimeMessage message) { prefix = options.International ? UTF8LiteralTokenPrefix : LiteralTokenPrefix; literal = new ImapLiteral (options, message, UpdateProgress); } else { literal = new ImapLiteral (options, (byte[]) arg); prefix = LiteralTokenPrefix; } var length = literal.Length; bool wait = true; builder.Write (prefix, 0, prefix.Length); buf = Encoding.ASCII.GetBytes (length.ToString (CultureInfo.InvariantCulture)); builder.Write (buf, 0, buf.Length); if (CanUseNonSynchronizedLiteral (Engine, length)) { builder.WriteByte ((byte) '+'); wait = false; } builder.Write (LiteralTokenSuffix, 0, LiteralTokenSuffix.Length); totalSize += length; parts.Add (new ImapCommandPart (builder.ToArray (), literal, wait)); builder.SetLength (0); if (prefix == UTF8LiteralTokenPrefix) builder.WriteByte ((byte) ')'); break; case 'S': // a string which may need to be quoted or made into a literal AppendString (options, true, builder, (string) args[argc++]); break; case 'Q': // similar to %S but string must be quoted at a minimum AppendString (options, false, builder, (string) args[argc++]); break; default: throw new FormatException (); } } else if (format[i] < 128) { builder.WriteByte ((byte) format[i]); } else { int nchars = char.IsSurrogate (format[i]) ? 2 : 1; int nbytes = Encoding.UTF8.GetBytes (format, i, nchars, utf8, 0); builder.Write (utf8, 0, nbytes); i += nchars - 1; } } parts.Add (new ImapCommandPart (builder.ToArray (), null)); } } /// /// Initializes a new instance of the class. /// /// /// Creates a new . /// /// The IMAP engine that will be sending the command. /// The cancellation token. /// The IMAP folder that the command operates on. /// The command format. /// The command arguments. public ImapCommand (ImapEngine engine, CancellationToken cancellationToken, ImapFolder folder, string format, params object[] args) : this (engine, cancellationToken, folder, FormatOptions.Default, format, args) { } internal static int EstimateCommandLength (ImapEngine engine, FormatOptions options, string format, params object[] args) { const int EstimatedTagLength = 10; var eoln = false; int length = 0; int argc = 0; string str; for (int i = 0; i < format.Length; i++) { if (format[i] == '%') { switch (format[++i]) { case '%': // a literal % length++; break; case 'd': // an integer str = ((int) args[argc++]).ToString (CultureInfo.InvariantCulture); length += str.Length; break; case 'u': // an unsigned integer str = ((uint) args[argc++]).ToString (CultureInfo.InvariantCulture); length += str.Length; break; case 's': str = (string) args[argc++]; length += str.Length; break; case 'F': // an ImapFolder var utf7 = ((ImapFolder) args[argc++]).EncodedName; length += EstimateStringLength (engine, options, true, utf7, out eoln); break; case 'L': // a MimeMessage or a byte[] var arg = args[argc++]; byte[] prefix; long len; if (arg is MimeMessage message) { prefix = options.International ? UTF8LiteralTokenPrefix : LiteralTokenPrefix; var literal = new ImapLiteral (options, message, null); len = literal.Length; } else { len = ((byte[]) arg).Length; prefix = LiteralTokenPrefix; } length += prefix.Length; length += Encoding.ASCII.GetByteCount (len.ToString (CultureInfo.InvariantCulture)); if (CanUseNonSynchronizedLiteral (engine, len)) length++; length += LiteralTokenSuffix.Length; if (prefix == UTF8LiteralTokenPrefix) length++; eoln = true; break; case 'S': // a string which may need to be quoted or made into a literal length += EstimateStringLength (engine, options, true, (string) args[argc++], out eoln); break; case 'Q': // similar to %S but string must be quoted at a minimum length += EstimateStringLength (engine, options, false, (string) args[argc++], out eoln); break; default: throw new FormatException (); } if (eoln) break; } else { length++; } } return length + EstimatedTagLength; } internal static int EstimateCommandLength (ImapEngine engine, string format, params object[] args) { return EstimateCommandLength (engine, FormatOptions.Default, format, args); } void UpdateProgress (int n) { nwritten += n; if (Progress != null) Progress.Report (nwritten, totalSize); } static bool IsAtom (char c) { return c < 128 && !char.IsControl (c) && "(){ \t%*\\\"]".IndexOf (c) == -1; } static bool IsQuotedSafe (ImapEngine engine, char c) { return (c < 128 || engine.UTF8Enabled) && !char.IsControl (c); } internal static ImapStringType GetStringType (ImapEngine engine, string value, bool allowAtom) { var type = allowAtom ? ImapStringType.Atom : ImapStringType.QString; if (value == null) return ImapStringType.Nil; if (value.Length == 0) return ImapStringType.QString; for (int i = 0; i < value.Length; i++) { if (!IsAtom (value[i])) { if (!IsQuotedSafe (engine, value[i])) return ImapStringType.Literal; type = ImapStringType.QString; } } return type; } static bool CanUseNonSynchronizedLiteral (ImapEngine engine, long length) { return (engine.Capabilities & ImapCapabilities.LiteralPlus) != 0 || (length <= 4096 && (engine.Capabilities & ImapCapabilities.LiteralMinus) != 0); } static int EstimateStringLength (ImapEngine engine, FormatOptions options, bool allowAtom, string value, out bool eoln) { eoln = false; switch (GetStringType (engine, value, allowAtom)) { case ImapStringType.Literal: var literal = Encoding.UTF8.GetByteCount (value); var plus = CanUseNonSynchronizedLiteral (engine, literal); int length = "{}\r\n".Length; length += literal.ToString (CultureInfo.InvariantCulture).Length; if (plus) length++; eoln = true; return length++; case ImapStringType.QString: return Encoding.UTF8.GetByteCount (MimeUtils.Quote (value)); case ImapStringType.Nil: return Nil.Length; default: return value.Length; } } void AppendString (FormatOptions options, bool allowAtom, MemoryStream builder, string value) { byte[] buf; switch (GetStringType (Engine, value, allowAtom)) { case ImapStringType.Literal: var literal = Encoding.UTF8.GetBytes (value); var plus = CanUseNonSynchronizedLiteral (Engine, literal.Length); var length = literal.Length.ToString (CultureInfo.InvariantCulture); buf = Encoding.ASCII.GetBytes (length); builder.WriteByte ((byte) '{'); builder.Write (buf, 0, buf.Length); if (plus) builder.WriteByte ((byte) '+'); builder.WriteByte ((byte) '}'); builder.WriteByte ((byte) '\r'); builder.WriteByte ((byte) '\n'); if (plus) { builder.Write (literal, 0, literal.Length); } else { parts.Add (new ImapCommandPart (builder.ToArray (), new ImapLiteral (options, literal))); builder.SetLength (0); } break; case ImapStringType.QString: buf = Encoding.UTF8.GetBytes (MimeUtils.Quote (value)); builder.Write (buf, 0, buf.Length); break; case ImapStringType.Atom: buf = Encoding.UTF8.GetBytes (value); builder.Write (buf, 0, buf.Length); break; case ImapStringType.Nil: builder.Write (Nil, 0, Nil.Length); break; } } /// /// Registers the untagged handler for the specified atom token. /// /// The atom token. /// The handler. /// /// is null. /// -or- /// is null. /// /// /// Untagged handlers must be registered before the command has been queued. /// public void RegisterUntaggedHandler (string atom, ImapUntaggedHandler handler) { if (atom == null) throw new ArgumentNullException (nameof (atom)); if (handler == null) throw new ArgumentNullException (nameof (handler)); if (Status != ImapCommandStatus.Created) throw new InvalidOperationException ("Untagged handlers must be registered before the command has been queued."); UntaggedHandlers.Add (atom, handler); } /// /// Sends the next part of the command to the server. /// /// /// The operation was canceled via the cancellation token. /// /// /// An I/O error occurred. /// /// /// An IMAP protocol error occurred. /// public async Task StepAsync (bool doAsync) { var supportsLiteralPlus = (Engine.Capabilities & ImapCapabilities.LiteralPlus) != 0; var idle = UserData as ImapIdleContext; var result = ImapCommandResponse.None; ImapToken token; // construct and write the command tag if this is the initial state if (current == 0) { Tag = string.Format (CultureInfo.InvariantCulture, "{0}{1:D8}", Engine.TagPrefix, Engine.Tag++); var buf = Encoding.ASCII.GetBytes (Tag + " "); if (doAsync) await Engine.Stream.WriteAsync (buf, 0, buf.Length, CancellationToken).ConfigureAwait (false); else Engine.Stream.Write (buf, 0, buf.Length, CancellationToken); } do { var command = parts[current].Command; if (doAsync) await Engine.Stream.WriteAsync (command, 0, command.Length, CancellationToken).ConfigureAwait (false); else Engine.Stream.Write (command, 0, command.Length, CancellationToken); // if the server doesn't support LITERAL+ (or LITERAL-), we'll need to wait // for a "+" response before writing out the any literals... if (parts[current].WaitForContinuation) break; // otherwise, we can write out any and all literal tokens we have... await parts[current].Literal.WriteToAsync (Engine.Stream, doAsync, CancellationToken).ConfigureAwait (false); if (current + 1 >= parts.Count) break; current++; } while (true); if (doAsync) await Engine.Stream.FlushAsync (CancellationToken).ConfigureAwait (false); else Engine.Stream.Flush (CancellationToken); // now we need to read the response... do { if (Engine.State == ImapEngineState.Idle) { int timeout = Timeout.Infinite; if (Engine.Stream.CanTimeout) { timeout = Engine.Stream.ReadTimeout; Engine.Stream.ReadTimeout = Timeout.Infinite; } try { token = await Engine.ReadTokenAsync (doAsync, CancellationToken).ConfigureAwait (false); } finally { if (Engine.Stream.IsConnected && Engine.Stream.CanTimeout) Engine.Stream.ReadTimeout = timeout; } } else { token = await Engine.ReadTokenAsync (doAsync, CancellationToken).ConfigureAwait (false); } if (token.Type == ImapTokenType.Atom && token.Value.ToString () == "+") { // we've gotten a continuation response from the server var text = (await Engine.ReadLineAsync (doAsync, CancellationToken).ConfigureAwait (false)).Trim (); // if we've got a Literal pending, the '+' means we can send it now... if (!supportsLiteralPlus && parts[current].Literal != null) { await parts[current].Literal.WriteToAsync (Engine.Stream, doAsync, CancellationToken).ConfigureAwait (false); break; } if (ContinuationHandler != null) { await ContinuationHandler (Engine, this, text, doAsync).ConfigureAwait (false); } else if (doAsync) { await Engine.Stream.WriteAsync (NewLine, 0, NewLine.Length, CancellationToken).ConfigureAwait (false); await Engine.Stream.FlushAsync (CancellationToken).ConfigureAwait (false); } else { Engine.Stream.Write (NewLine, 0, NewLine.Length, CancellationToken); Engine.Stream.Flush (CancellationToken); } } else if (token.Type == ImapTokenType.Asterisk) { // we got an untagged response, let the engine handle this... await Engine.ProcessUntaggedResponseAsync (doAsync, CancellationToken).ConfigureAwait (false); } else if (token.Type == ImapTokenType.Atom && (string) token.Value == Tag) { // the next token should be "OK", "NO", or "BAD" token = await Engine.ReadTokenAsync (doAsync, CancellationToken).ConfigureAwait (false); ImapEngine.AssertToken (token, ImapTokenType.Atom, "Syntax error in tagged response. Unexpected token: {0}", token); string atom = (string) token.Value; switch (atom) { case "BAD": result = ImapCommandResponse.Bad; break; case "OK": result = ImapCommandResponse.Ok; break; case "NO": result = ImapCommandResponse.No; break; default: throw ImapEngine.UnexpectedToken ("Syntax error in tagged response. Unexpected token: {0}", token); } token = await Engine.ReadTokenAsync (doAsync, CancellationToken).ConfigureAwait (false); if (token.Type == ImapTokenType.OpenBracket) { var code = await Engine.ParseResponseCodeAsync (true, doAsync, CancellationToken).ConfigureAwait (false); RespCodes.Add (code); break; } if (token.Type != ImapTokenType.Eoln) { // consume the rest of the line... var line = await Engine.ReadLineAsync (doAsync, CancellationToken).ConfigureAwait (false); ResponseText = (((string) token.Value) + line).TrimEnd (); break; } } else if (token.Type == ImapTokenType.OpenBracket) { // Note: this is a work-around for broken IMAP servers like Office365.com that // return RESP-CODES that are not preceded by "* OK " such as the example in // issue #115 (https://github.com/jstedfast/MailKit/issues/115). var code = await Engine.ParseResponseCodeAsync (false, doAsync, CancellationToken).ConfigureAwait (false); RespCodes.Add (code); } else { // no clue what we got... throw ImapEngine.UnexpectedToken ("Syntax error in response. Unexpected token: {0}", token); } } while (Status == ImapCommandStatus.Active); if (Status == ImapCommandStatus.Active) { current++; if (current >= parts.Count || result != ImapCommandResponse.None) { Status = ImapCommandStatus.Complete; Response = result; return false; } return true; } return false; } } }