OpenSim.Modules.EMail/src/MailKit/Net/Imap/ImapEngine.cs

2906 lines
102 KiB
C#

//
// ImapEngine.cs
//
// Author: Jeffrey Stedfast <jestedfa@microsoft.com>
//
// 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.Linq;
using System.Text;
using System.Threading;
using System.Diagnostics;
using System.Globalization;
using System.Threading.Tasks;
using System.Collections.Generic;
using MimeKit;
namespace MailKit.Net.Imap {
delegate ImapFolder CreateImapFolderDelegate (ImapFolderConstructorArgs args);
/// <summary>
/// The state of the <see cref="ImapEngine"/>.
/// </summary>
enum ImapEngineState {
/// <summary>
/// The ImapEngine is in the disconnected state.
/// </summary>
Disconnected,
/// <summary>
/// The ImapEngine is in the process of connecting.
/// </summary>
Connecting,
/// <summary>
/// The ImapEngine is connected but not yet authenticated.
/// </summary>
Connected,
/// <summary>
/// The ImapEngine is in the authenticated state.
/// </summary>
Authenticated,
/// <summary>
/// The ImapEngine is in the selected state.
/// </summary>
Selected,
/// <summary>
/// The ImapEngine is in the IDLE state.
/// </summary>
Idle
}
enum ImapProtocolVersion {
Unknown,
IMAP4,
IMAP4rev1
}
enum ImapUntaggedResult {
Ok,
No,
Bad,
Handled
}
enum ImapQuirksMode {
None,
Courier,
Cyrus,
Domino,
Dovecot,
Exchange,
GMail,
ProtonMail,
SmarterMail,
SunMicrosystems,
UW,
Yahoo,
Yandex
}
class ImapFolderNameComparer : IEqualityComparer<string>
{
public char DirectorySeparator;
public ImapFolderNameComparer (char directorySeparator)
{
DirectorySeparator = directorySeparator;
}
public bool Equals (string x, string y)
{
x = ImapUtils.CanonicalizeMailboxName (x, DirectorySeparator);
y = ImapUtils.CanonicalizeMailboxName (y, DirectorySeparator);
return x == y;
}
public int GetHashCode (string obj)
{
return ImapUtils.CanonicalizeMailboxName (obj, DirectorySeparator).GetHashCode ();
}
}
/// <summary>
/// An IMAP command engine.
/// </summary>
class ImapEngine : IDisposable
{
internal const string GenericUntaggedResponseSyntaxErrorFormat = "Syntax error in untagged {0} response. Unexpected token: {1}";
internal const string GenericItemSyntaxErrorFormat = "Syntax error in {0}. Unexpected token: {1}";
internal const string FetchBodySyntaxErrorFormat = "Syntax error in BODY. Unexpected token: {0}";
const string GenericResponseCodeSyntaxErrorFormat = "Syntax error in {0} response code. Unexpected token: {1}";
const string GreetingSyntaxErrorFormat = "Syntax error in IMAP server greeting. Unexpected token: {0}";
internal static readonly Encoding Latin1;
internal static readonly Encoding UTF8;
static int TagPrefixIndex;
internal readonly Dictionary<string, ImapFolder> FolderCache;
readonly CreateImapFolderDelegate createImapFolder;
readonly ImapFolderNameComparer cacheComparer;
internal ImapQuirksMode QuirksMode;
readonly List<ImapCommand> queue;
internal char TagPrefix;
ImapCommand current;
MimeParser parser;
internal int Tag;
bool disposed;
static ImapEngine ()
{
UTF8 = Encoding.GetEncoding (65001, new EncoderExceptionFallback (), new DecoderExceptionFallback ());
try {
Latin1 = Encoding.GetEncoding (28591);
} catch (NotSupportedException) {
Latin1 = Encoding.GetEncoding (1252);
}
}
public ImapEngine (CreateImapFolderDelegate createImapFolderDelegate)
{
cacheComparer = new ImapFolderNameComparer ('.');
FolderCache = new Dictionary<string, ImapFolder> (cacheComparer);
ThreadingAlgorithms = new HashSet<ThreadingAlgorithm> ();
AuthenticationMechanisms = new HashSet<string> (StringComparer.Ordinal);
CompressionAlgorithms = new HashSet<string> (StringComparer.Ordinal);
SupportedContexts = new HashSet<string> (StringComparer.Ordinal);
SupportedCharsets = new HashSet<string> (StringComparer.OrdinalIgnoreCase);
Rights = new AccessRights ();
PersonalNamespaces = new FolderNamespaceCollection ();
SharedNamespaces = new FolderNamespaceCollection ();
OtherNamespaces = new FolderNamespaceCollection ();
ProtocolVersion = ImapProtocolVersion.Unknown;
createImapFolder = createImapFolderDelegate;
Capabilities = ImapCapabilities.None;
QuirksMode = ImapQuirksMode.None;
queue = new List<ImapCommand> ();
}
/// <summary>
/// Get the authentication mechanisms supported by the IMAP server.
/// </summary>
/// <remarks>
/// The authentication mechanisms are queried durring the
/// <see cref="ConnectAsync"/> method.
/// </remarks>
/// <value>The authentication mechanisms.</value>
public HashSet<string> AuthenticationMechanisms {
get; private set;
}
/// <summary>
/// Get the compression algorithms supported by the IMAP server.
/// </summary>
/// <remarks>
/// The compression algorithms are populated by the
/// <see cref="QueryCapabilitiesAsync"/> method.
/// </remarks>
/// <value>The compression algorithms.</value>
public HashSet<string> CompressionAlgorithms {
get; private set;
}
/// <summary>
/// Get the threading algorithms supported by the IMAP server.
/// </summary>
/// <remarks>
/// The threading algorithms are populated by the
/// <see cref="QueryCapabilitiesAsync"/> method.
/// </remarks>
/// <value>The threading algorithms.</value>
public HashSet<ThreadingAlgorithm> ThreadingAlgorithms {
get; private set;
}
/// <summary>
/// Gets the append limit supported by the IMAP server.
/// </summary>
/// <remarks>
/// Gets the append limit supported by the IMAP server.
/// </remarks>
/// <value>The append limit.</value>
public uint? AppendLimit {
get; private set;
}
/// <summary>
/// Gets the I18NLEVEL supported by the IMAP server.
/// </summary>
/// <remarks>
/// Gets the I18NLEVEL supported by the IMAP server.
/// </remarks>
/// <value>The internationalization level.</value>
public int I18NLevel {
get; private set;
}
/// <summary>
/// Get the capabilities supported by the IMAP server.
/// </summary>
/// <remarks>
/// The capabilities will not be known until a successful connection
/// has been made via the <see cref="ConnectAsync"/> method.
/// </remarks>
/// <value>The capabilities.</value>
public ImapCapabilities Capabilities {
get; set;
}
/// <summary>
/// Indicates whether or not the engine is busy processing commands.
/// </summary>
/// <remarks>
/// Indicates whether or not the engine is busy processing commands.
/// </remarks>
/// <value><c>true</c> if th e engine is busy processing commands; otherwise, <c>false</c>.</value>
internal bool IsBusy {
get { return current != null; }
}
/// <summary>
/// Get the capabilities version.
/// </summary>
/// <remarks>
/// Every time the engine receives an untagged CAPABILITIES
/// response from the server, it increments this value.
/// </remarks>
/// <value>The capabilities version.</value>
public int CapabilitiesVersion {
get; private set;
}
/// <summary>
/// Get the IMAP protocol version.
/// </summary>
/// <remarks>
/// Gets the IMAP protocol version.
/// </remarks>
/// <value>The IMAP protocol version.</value>
public ImapProtocolVersion ProtocolVersion {
get; private set;
}
/// <summary>
/// Get the rights specified in the capabilities.
/// </summary>
/// <remarks>
/// Gets the rights specified in the capabilities.
/// </remarks>
/// <value>The rights.</value>
public AccessRights Rights {
get; private set;
}
/// <summary>
/// Get the supported charsets.
/// </summary>
/// <remarks>
/// Gets the supported charsets.
/// </remarks>
/// <value>The supported charsets.</value>
public HashSet<string> SupportedCharsets {
get; private set;
}
/// <summary>
/// Get the supported contexts.
/// </summary>
/// <remarks>
/// Gets the supported contexts.
/// </remarks>
/// <value>The supported contexts.</value>
public HashSet<string> SupportedContexts {
get; private set;
}
/// <summary>
/// Get whether or not the QRESYNC feature has been enabled.
/// </summary>
/// <remarks>
/// Gets whether or not the QRESYNC feature has been enabled.
/// </remarks>
/// <value><c>true</c> if the QRESYNC feature has been enabled; otherwise, <c>false</c>.</value>
public bool QResyncEnabled {
get; internal set;
}
/// <summary>
/// Get whether or not the UTF8=ACCEPT feature has been enabled.
/// </summary>
/// <remarks>
/// Gets whether or not the UTF8=ACCEPT feature has been enabled.
/// </remarks>
/// <value><c>true</c> if the UTF8=ACCEPT feature has been enabled; otherwise, <c>false</c>.</value>
public bool UTF8Enabled {
get; internal set;
}
/// <summary>
/// Get the URI of the IMAP server.
/// </summary>
/// <remarks>
/// Gets the URI of the IMAP server.
/// </remarks>
/// <value>The URI of the IMAP server.</value>
public Uri Uri {
get; internal set;
}
/// <summary>
/// Get the underlying IMAP stream.
/// </summary>
/// <remarks>
/// Gets the underlying IMAP stream.
/// </remarks>
/// <value>The IMAP stream.</value>
public ImapStream Stream {
get; private set;
}
/// <summary>
/// Get or sets the state of the engine.
/// </summary>
/// <remarks>
/// Gets or sets the state of the engine.
/// </remarks>
/// <value>The engine state.</value>
public ImapEngineState State {
get; internal set;
}
/// <summary>
/// Get whether or not the engine is currently connected to a IMAP server.
/// </summary>
/// <remarks>
/// Gets whether or not the engine is currently connected to a IMAP server.
/// </remarks>
/// <value><c>true</c> if the engine is connected; otherwise, <c>false</c>.</value>
public bool IsConnected {
get { return Stream != null && Stream.IsConnected; }
}
/// <summary>
/// Gets the personal folder namespaces.
/// </summary>
/// <remarks>
/// Gets the personal folder namespaces.
/// </remarks>
/// <value>The personal folder namespaces.</value>
public FolderNamespaceCollection PersonalNamespaces {
get; private set;
}
/// <summary>
/// Gets the shared folder namespaces.
/// </summary>
/// <remarks>
/// Gets the shared folder namespaces.
/// </remarks>
/// <value>The shared folder namespaces.</value>
public FolderNamespaceCollection SharedNamespaces {
get; private set;
}
/// <summary>
/// Gets the other folder namespaces.
/// </summary>
/// <remarks>
/// Gets the other folder namespaces.
/// </remarks>
/// <value>The other folder namespaces.</value>
public FolderNamespaceCollection OtherNamespaces {
get; private set;
}
/// <summary>
/// Gets the selected folder.
/// </summary>
/// <remarks>
/// Gets the selected folder.
/// </remarks>
/// <value>The selected folder.</value>
public ImapFolder Selected {
get; internal set;
}
/// <summary>
/// Gets a value indicating whether the engine is disposed.
/// </summary>
/// <remarks>
/// Gets a value indicating whether the engine is disposed.
/// </remarks>
/// <value><c>true</c> if the engine is disposed; otherwise, <c>false</c>.</value>
public bool IsDisposed {
get { return disposed; }
}
/// <summary>
/// Gets whether the current NOTIFY status prevents using indexes and * for referencing messages.
/// </summary>
/// <remarks>
/// Gets whether the current NOTIFY status prevents using indexes and * for referencing messages. This is the case when the client has asked for MessageNew or MessageExpunge events on the SELECTED mailbox.
/// </remarks>
/// <value><c>true</c> if the use of indexes and * is prevented; otherwise, <c>false</c>.</value>
internal bool NotifySelectedNewExpunge {
get; set;
}
#region Special Folders
/// <summary>
/// Gets the Inbox folder.
/// </summary>
/// <value>The Inbox folder.</value>
public ImapFolder Inbox {
get; private set;
}
/// <summary>
/// Gets the special folder containing an aggregate of all messages.
/// </summary>
/// <value>The folder containing all messages.</value>
public ImapFolder All {
get; private set;
}
/// <summary>
/// Gets the special archive folder.
/// </summary>
/// <value>The archive folder.</value>
public ImapFolder Archive {
get; private set;
}
/// <summary>
/// Gets the special folder containing drafts.
/// </summary>
/// <value>The drafts folder.</value>
public ImapFolder Drafts {
get; private set;
}
/// <summary>
/// Gets the special folder containing flagged messages.
/// </summary>
/// <value>The flagged folder.</value>
public ImapFolder Flagged {
get; private set;
}
/// <summary>
/// Gets the special folder containing important messages.
/// </summary>
/// <value>The important folder.</value>
public ImapFolder Important {
get; private set;
}
/// <summary>
/// Gets the special folder containing junk messages.
/// </summary>
/// <value>The junk folder.</value>
public ImapFolder Junk {
get; private set;
}
/// <summary>
/// Gets the special folder containing sent messages.
/// </summary>
/// <value>The sent.</value>
public ImapFolder Sent {
get; private set;
}
/// <summary>
/// Gets the folder containing deleted messages.
/// </summary>
/// <value>The trash folder.</value>
public ImapFolder Trash {
get; private set;
}
#endregion
internal ImapFolder CreateImapFolder (string encodedName, FolderAttributes attributes, char delim)
{
var args = new ImapFolderConstructorArgs (this, encodedName, attributes, delim);
return createImapFolder (args);
}
internal static ImapProtocolException UnexpectedToken (string format, params object[] args)
{
return new ImapProtocolException (string.Format (CultureInfo.InvariantCulture, format, args)) { UnexpectedToken = true };
}
internal static void AssertToken (ImapToken token, ImapTokenType type, string format, params object[] args)
{
if (token.Type != type)
throw UnexpectedToken (format, args);
}
internal static void AssertToken (ImapToken token, ImapTokenType type1, ImapTokenType type2, string format, params object[] args)
{
if (token.Type != type1 && token.Type != type2)
throw UnexpectedToken (format, args);
}
internal static uint ParseNumber (ImapToken token, bool nonZero, string format, params object[] args)
{
uint value;
AssertToken (token, ImapTokenType.Atom, format, args);
if (!uint.TryParse ((string) token.Value, NumberStyles.None, CultureInfo.InvariantCulture, out value) || (nonZero && value == 0))
throw UnexpectedToken (format, args);
return value;
}
internal static ulong ParseNumber64 (ImapToken token, bool nonZero, string format, params object[] args)
{
ulong value;
AssertToken (token, ImapTokenType.Atom, format, args);
if (!ulong.TryParse ((string) token.Value, NumberStyles.None, CultureInfo.InvariantCulture, out value) || (nonZero && value == 0))
throw UnexpectedToken (format, args);
return value;
}
internal static UniqueIdSet ParseUidSet (ImapToken token, uint validity, string format, params object[] args)
{
UniqueIdSet uids;
AssertToken (token, ImapTokenType.Atom, format, args);
if (!UniqueIdSet.TryParse ((string) token.Value, validity, out uids))
throw UnexpectedToken (format, args);
return uids;
}
/// <summary>
/// Sets the stream - this is only here to be used by the unit tests.
/// </summary>
/// <param name="stream">The IMAP stream.</param>
internal void SetStream (ImapStream stream)
{
Stream = stream;
}
/// <summary>
/// Takes posession of the <see cref="ImapStream"/> and reads the greeting.
/// </summary>
/// <param name="stream">The IMAP stream.</param>
/// <param name="doAsync">Whether or not asyncrhonois IO methods should be used.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <exception cref="System.OperationCanceledException">
/// The operation was canceled via the cancellation token.
/// </exception>
/// <exception cref="System.IO.IOException">
/// An I/O error occurred.
/// </exception>
/// <exception cref="ImapProtocolException">
/// An IMAP protocol error occurred.
/// </exception>
public async Task ConnectAsync (ImapStream stream, bool doAsync, CancellationToken cancellationToken)
{
TagPrefix = (char) ('A' + (TagPrefixIndex++ % 26));
ProtocolVersion = ImapProtocolVersion.Unknown;
Capabilities = ImapCapabilities.None;
AuthenticationMechanisms.Clear ();
CompressionAlgorithms.Clear ();
ThreadingAlgorithms.Clear ();
SupportedCharsets.Clear ();
SupportedContexts.Clear ();
Rights.Clear ();
State = ImapEngineState.Connecting;
QuirksMode = ImapQuirksMode.None;
SupportedCharsets.Add ("US-ASCII");
SupportedCharsets.Add ("UTF-8");
CapabilitiesVersion = 0;
QResyncEnabled = false;
UTF8Enabled = false;
AppendLimit = null;
Selected = null;
Stream = stream;
I18NLevel = 0;
Tag = 0;
try {
var token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false);
AssertToken (token, ImapTokenType.Asterisk, GreetingSyntaxErrorFormat, token);
token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false);
AssertToken (token, ImapTokenType.Atom, GreetingSyntaxErrorFormat, token);
var atom = (string) token.Value;
var text = string.Empty;
var state = State;
var bye = false;
switch (atom.ToUpperInvariant ()) {
case "BYE":
bye = true;
break;
case "PREAUTH":
state = ImapEngineState.Authenticated;
break;
case "OK":
state = ImapEngineState.Connected;
break;
default:
throw UnexpectedToken (GreetingSyntaxErrorFormat, token);
}
token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false);
if (token.Type == ImapTokenType.OpenBracket) {
var code = await ParseResponseCodeAsync (false, doAsync, cancellationToken).ConfigureAwait (false);
if (code.Type == ImapResponseCodeType.Alert) {
OnAlert (code.Message);
if (bye)
throw new ImapProtocolException (code.Message);
} else {
text = code.Message;
}
} else if (token.Type != ImapTokenType.Eoln) {
text = (string) token.Value;
text += await ReadLineAsync (doAsync, cancellationToken).ConfigureAwait (false);
text = text.TrimEnd ();
if (bye)
throw new ImapProtocolException (text);
} else if (bye) {
throw new ImapProtocolException ("The IMAP server unexpectedly refused the connection.");
}
if (text.StartsWith ("Courier-IMAP ready.", StringComparison.Ordinal))
QuirksMode = ImapQuirksMode.Courier;
else if (text.Contains (" Cyrus IMAP "))
QuirksMode = ImapQuirksMode.Cyrus;
else if (text.StartsWith ("Domino IMAP4 Server", StringComparison.Ordinal))
QuirksMode = ImapQuirksMode.Domino;
else if (text.StartsWith ("Dovecot ready.", StringComparison.Ordinal))
QuirksMode = ImapQuirksMode.Dovecot;
else if (text.StartsWith ("Microsoft Exchange Server 2007 IMAP4 service is ready", StringComparison.Ordinal))
QuirksMode = ImapQuirksMode.Exchange;
else if (text.StartsWith ("The Microsoft Exchange IMAP4 service is ready.", StringComparison.Ordinal))
QuirksMode = ImapQuirksMode.Exchange;
else if (text.StartsWith ("Gimap ready", StringComparison.Ordinal))
QuirksMode = ImapQuirksMode.GMail;
else if (text.Contains (" IMAP4rev1 2007f.") || text.Contains (" Panda IMAP "))
QuirksMode = ImapQuirksMode.UW;
else if (text.Contains ("SmarterMail"))
QuirksMode = ImapQuirksMode.SmarterMail;
else if (text.Contains ("Yandex IMAP4rev1 "))
QuirksMode = ImapQuirksMode.Yandex;
State = state;
} catch {
Disconnect ();
throw;
}
}
/// <summary>
/// Disconnects the <see cref="ImapEngine"/>.
/// </summary>
/// <remarks>
/// Disconnects the <see cref="ImapEngine"/>.
/// </remarks>
public void Disconnect ()
{
if (Selected != null) {
Selected.Reset ();
Selected.OnClosed ();
Selected = null;
}
current = null;
if (Stream != null) {
Stream.Dispose ();
Stream = null;
}
if (State != ImapEngineState.Disconnected) {
State = ImapEngineState.Disconnected;
OnDisconnected ();
}
}
internal async Task<string> ReadLineAsync (bool doAsync, CancellationToken cancellationToken)
{
using (var memory = new MemoryStream ()) {
bool complete;
byte[] buf;
int count;
do {
if (doAsync)
complete = await Stream.ReadLineAsync (memory, cancellationToken).ConfigureAwait (false);
else
complete = Stream.ReadLine (memory, cancellationToken);
} while (!complete);
count = (int) memory.Length;
#if !NETSTANDARD1_3 && !NETSTANDARD1_6
buf = memory.GetBuffer ();
#else
buf = memory.ToArray ();
#endif
try {
return UTF8.GetString (buf, 0, count);
} catch (DecoderFallbackException) {
return Latin1.GetString (buf, 0, count);
}
}
}
#if false
/// <summary>
/// Reads a single line from the <see cref="ImapStream"/>.
/// </summary>
/// <returns>The line.</returns>
/// <param name="cancellationToken">The cancellation token.</param>
/// <exception cref="System.InvalidOperationException">
/// The engine is not connected.
/// </exception>
/// <exception cref="System.OperationCanceledException">
/// The operation was canceled via the cancellation token.
/// </exception>
/// <exception cref="System.IO.IOException">
/// An I/O error occurred.
/// </exception>
/// <exception cref="ImapProtocolException">
/// An IMAP protocol error occurred.
/// </exception>
public string ReadLine (CancellationToken cancellationToken)
{
return ReadLineAsync (false, cancellationToken).GetAwaiter ().GetResult ();
}
/// <summary>
/// Asynchronously reads a single line from the <see cref="ImapStream"/>.
/// </summary>
/// <returns>The line.</returns>
/// <param name="cancellationToken">The cancellation token.</param>
/// <exception cref="System.InvalidOperationException">
/// The engine is not connected.
/// </exception>
/// <exception cref="System.OperationCanceledException">
/// The operation was canceled via the cancellation token.
/// </exception>
/// <exception cref="System.IO.IOException">
/// An I/O error occurred.
/// </exception>
/// <exception cref="ImapProtocolException">
/// An IMAP protocol error occurred.
/// </exception>
public Task<string> ReadLineAsync (CancellationToken cancellationToken)
{
return ReadLineAsync (true, cancellationToken);
}
#endif
internal Task<ImapToken> ReadTokenAsync (string specials, bool doAsync, CancellationToken cancellationToken)
{
return Stream.ReadTokenAsync (specials, doAsync, cancellationToken);
}
internal Task<ImapToken> ReadTokenAsync (bool doAsync, CancellationToken cancellationToken)
{
return Stream.ReadTokenAsync (ImapStream.DefaultSpecials, doAsync, cancellationToken);
}
internal async Task<ImapToken> PeekTokenAsync (string specials, bool doAsync, CancellationToken cancellationToken)
{
var token = await ReadTokenAsync (specials, doAsync, cancellationToken).ConfigureAwait (false);
Stream.UngetToken (token);
return token;
}
internal Task<ImapToken> PeekTokenAsync (bool doAsync, CancellationToken cancellationToken)
{
return PeekTokenAsync (ImapStream.DefaultSpecials, doAsync, cancellationToken);
}
/// <summary>
/// Reads the next token.
/// </summary>
/// <returns>The token.</returns>
/// <param name="cancellationToken">The cancellation token.</param>
/// <exception cref="System.InvalidOperationException">
/// The engine is not connected.
/// </exception>
/// <exception cref="System.OperationCanceledException">
/// The operation was canceled via the cancellation token.
/// </exception>
/// <exception cref="System.IO.IOException">
/// An I/O error occurred.
/// </exception>
/// <exception cref="ImapProtocolException">
/// An IMAP protocol error occurred.
/// </exception>
public ImapToken ReadToken (CancellationToken cancellationToken)
{
return Stream.ReadToken (cancellationToken);
}
#if false
/// <summary>
/// Asynchronously reads the next token.
/// </summary>
/// <returns>The token.</returns>
/// <param name="cancellationToken">The cancellation token.</param>
/// <exception cref="System.InvalidOperationException">
/// The engine is not connected.
/// </exception>
/// <exception cref="System.OperationCanceledException">
/// The operation was canceled via the cancellation token.
/// </exception>
/// <exception cref="System.IO.IOException">
/// An I/O error occurred.
/// </exception>
/// <exception cref="ImapProtocolException">
/// An IMAP protocol error occurred.
/// </exception>
public Task<ImapToken> ReadTokenAsync (CancellationToken cancellationToken)
{
return Stream.ReadTokenAsync (cancellationToken);
}
/// <summary>
/// Peeks at the next token.
/// </summary>
/// <returns>The next token.</returns>
/// <param name="specials">A list of characters that are not legal in bare string tokens.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <exception cref="System.InvalidOperationException">
/// The engine is not connected.
/// </exception>
/// <exception cref="System.OperationCanceledException">
/// The operation was canceled via the cancellation token.
/// </exception>
/// <exception cref="System.IO.IOException">
/// An I/O error occurred.
/// </exception>
/// <exception cref="ImapProtocolException">
/// An IMAP protocol error occurred.
/// </exception>
public ImapToken PeekToken (string specials, CancellationToken cancellationToken)
{
return PeekTokenAsync (specials, false, cancellationToken).GetAwaiter ().GetResult ();
}
/// <summary>
/// Asynchronously peeks at the next token.
/// </summary>
/// <returns>The next token.</returns>
/// <param name="specials">A list of characters that are not legal in bare string tokens.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <exception cref="System.InvalidOperationException">
/// The engine is not connected.
/// </exception>
/// <exception cref="System.OperationCanceledException">
/// The operation was canceled via the cancellation token.
/// </exception>
/// <exception cref="System.IO.IOException">
/// An I/O error occurred.
/// </exception>
/// <exception cref="ImapProtocolException">
/// An IMAP protocol error occurred.
/// </exception>
public Task<ImapToken> PeekTokenAsync (string specials, CancellationToken cancellationToken)
{
return PeekTokenAsync (specials, true, cancellationToken);
}
/// <summary>
/// Peeks at the next token.
/// </summary>
/// <returns>The next token.</returns>
/// <param name="cancellationToken">The cancellation token.</param>
/// <exception cref="System.InvalidOperationException">
/// The engine is not connected.
/// </exception>
/// <exception cref="System.OperationCanceledException">
/// The operation was canceled via the cancellation token.
/// </exception>
/// <exception cref="System.IO.IOException">
/// An I/O error occurred.
/// </exception>
/// <exception cref="ImapProtocolException">
/// An IMAP protocol error occurred.
/// </exception>
public ImapToken PeekToken (CancellationToken cancellationToken)
{
return PeekTokenAsync (false, cancellationToken).GetAwaiter ().GetResult ();
}
/// <summary>
/// Asynchronously peeks at the next token.
/// </summary>
/// <returns>The next token.</returns>
/// <param name="cancellationToken">The cancellation token.</param>
/// <exception cref="System.InvalidOperationException">
/// The engine is not connected.
/// </exception>
/// <exception cref="System.OperationCanceledException">
/// The operation was canceled via the cancellation token.
/// </exception>
/// <exception cref="System.IO.IOException">
/// An I/O error occurred.
/// </exception>
/// <exception cref="ImapProtocolException">
/// An IMAP protocol error occurred.
/// </exception>
public Task<ImapToken> PeekTokenAsync (CancellationToken cancellationToken)
{
return PeekTokenAsync (true, cancellationToken);
}
#endif
internal async Task<string> ReadLiteralAsync (bool doAsync, CancellationToken cancellationToken)
{
if (Stream.Mode != ImapStreamMode.Literal)
throw new InvalidOperationException ();
using (var memory = new MemoryStream (Stream.LiteralLength)) {
var buf = new byte[4096];
int nread;
if (doAsync) {
while ((nread = await Stream.ReadAsync (buf, 0, buf.Length, cancellationToken).ConfigureAwait (false)) > 0)
memory.Write (buf, 0, nread);
} else {
while ((nread = Stream.Read (buf, 0, buf.Length, cancellationToken)) > 0)
memory.Write (buf, 0, nread);
}
nread = (int) memory.Length;
#if !NETSTANDARD1_3 && !NETSTANDARD1_6
buf = memory.GetBuffer ();
#else
buf = memory.ToArray ();
#endif
return Latin1.GetString (buf, 0, nread);
}
}
#if false
/// <summary>
/// Reads the literal as a string.
/// </summary>
/// <returns>The literal.</returns>
/// <param name="cancellationToken">The cancellation token.</param>
/// <exception cref="System.InvalidOperationException">
/// The <see cref="Stream"/> is not in literal mode.
/// </exception>
/// <exception cref="System.OperationCanceledException">
/// The operation was canceled via the cancellation token.
/// </exception>
/// <exception cref="System.IO.IOException">
/// An I/O error occurred.
/// </exception>
public string ReadLiteral (CancellationToken cancellationToken)
{
return ReadLiteralAsync (false, cancellationToken).GetAwaiter ().GetResult ();
}
/// <summary>
/// Asynchronously reads the literal as a string.
/// </summary>
/// <returns>The literal.</returns>
/// <param name="cancellationToken">The cancellation token.</param>
/// <exception cref="System.InvalidOperationException">
/// The <see cref="Stream"/> is not in literal mode.
/// </exception>
/// <exception cref="System.OperationCanceledException">
/// The operation was canceled via the cancellation token.
/// </exception>
/// <exception cref="System.IO.IOException">
/// An I/O error occurred.
/// </exception>
public Task<string> ReadLiteralAsync (CancellationToken cancellationToken)
{
return ReadLiteralAsync (true, cancellationToken);
}
#endif
async Task SkipLineAsync (bool doAsync, CancellationToken cancellationToken)
{
ImapToken token;
do {
token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false);
if (token.Type == ImapTokenType.Literal) {
var buf = new byte[4096];
int nread;
do {
if (doAsync)
nread = await Stream.ReadAsync (buf, 0, buf.Length, cancellationToken).ConfigureAwait (false);
else
nread = Stream.Read (buf, 0, buf.Length, cancellationToken);
} while (nread > 0);
}
} while (token.Type != ImapTokenType.Eoln);
}
async Task UpdateCapabilitiesAsync (ImapTokenType sentinel, bool doAsync, CancellationToken cancellationToken)
{
// Clear the extensions except STARTTLS so that this capability stays set after a STARTTLS command.
ProtocolVersion = ImapProtocolVersion.Unknown;
Capabilities &= ImapCapabilities.StartTLS;
AuthenticationMechanisms.Clear ();
CompressionAlgorithms.Clear ();
ThreadingAlgorithms.Clear ();
SupportedContexts.Clear ();
CapabilitiesVersion++;
AppendLimit = null;
Rights.Clear ();
I18NLevel = 0;
var token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false);
while (token.Type == ImapTokenType.Atom) {
var atom = (string) token.Value;
if (atom.StartsWith ("AUTH=", StringComparison.OrdinalIgnoreCase)) {
AuthenticationMechanisms.Add (atom.Substring ("AUTH=".Length));
} else if (atom.StartsWith ("APPENDLIMIT=", StringComparison.OrdinalIgnoreCase)) {
uint limit;
if (uint.TryParse (atom.Substring ("APPENDLIMIT=".Length), NumberStyles.None, CultureInfo.InvariantCulture, out limit))
AppendLimit = limit;
Capabilities |= ImapCapabilities.AppendLimit;
} else if (atom.StartsWith ("COMPRESS=", StringComparison.OrdinalIgnoreCase)) {
CompressionAlgorithms.Add (atom.Substring ("COMPRESS=".Length));
Capabilities |= ImapCapabilities.Compress;
} else if (atom.StartsWith ("CONTEXT=", StringComparison.OrdinalIgnoreCase)) {
SupportedContexts.Add (atom.Substring ("CONTEXT=".Length));
Capabilities |= ImapCapabilities.Context;
} else if (atom.StartsWith ("I18NLEVEL=", StringComparison.OrdinalIgnoreCase)) {
int level;
int.TryParse (atom.Substring ("I18NLEVEL=".Length), NumberStyles.None, CultureInfo.InvariantCulture, out level);
I18NLevel = level;
Capabilities |= ImapCapabilities.I18NLevel;
} else if (atom.StartsWith ("RIGHTS=", StringComparison.OrdinalIgnoreCase)) {
var rights = atom.Substring ("RIGHTS=".Length);
Rights.AddRange (rights);
} else if (atom.StartsWith ("THREAD=", StringComparison.OrdinalIgnoreCase)) {
var algorithm = atom.Substring ("THREAD=".Length);
switch (algorithm.ToUpperInvariant ()) {
case "ORDEREDSUBJECT":
ThreadingAlgorithms.Add (ThreadingAlgorithm.OrderedSubject);
break;
case "REFERENCES":
ThreadingAlgorithms.Add (ThreadingAlgorithm.References);
break;
}
Capabilities |= ImapCapabilities.Thread;
} else {
switch (atom.ToUpperInvariant ()) {
case "IMAP4": Capabilities |= ImapCapabilities.IMAP4; break;
case "IMAP4REV1": Capabilities |= ImapCapabilities.IMAP4rev1; break;
case "STATUS": Capabilities |= ImapCapabilities.Status; break;
case "ACL": Capabilities |= ImapCapabilities.Acl; break;
case "QUOTA": Capabilities |= ImapCapabilities.Quota; break;
case "LITERAL+": Capabilities |= ImapCapabilities.LiteralPlus; break;
case "IDLE": Capabilities |= ImapCapabilities.Idle; break;
case "MAILBOX-REFERRALS": Capabilities |= ImapCapabilities.MailboxReferrals; break;
case "LOGIN-REFERRALS": Capabilities |= ImapCapabilities.LoginReferrals; break;
case "NAMESPACE": Capabilities |= ImapCapabilities.Namespace; break;
case "ID": Capabilities |= ImapCapabilities.Id; break;
case "CHILDREN": Capabilities |= ImapCapabilities.Children; break;
case "LOGINDISABLED": Capabilities |= ImapCapabilities.LoginDisabled; break;
case "STARTTLS": Capabilities |= ImapCapabilities.StartTLS; break;
case "MULTIAPPEND": Capabilities |= ImapCapabilities.MultiAppend; break;
case "BINARY": Capabilities |= ImapCapabilities.Binary; break;
case "UNSELECT": Capabilities |= ImapCapabilities.Unselect; break;
case "UIDPLUS": Capabilities |= ImapCapabilities.UidPlus; break;
case "CATENATE": Capabilities |= ImapCapabilities.Catenate; break;
case "CONDSTORE": Capabilities |= ImapCapabilities.CondStore; break;
case "ESEARCH": Capabilities |= ImapCapabilities.ESearch; break;
case "SASL-IR": Capabilities |= ImapCapabilities.SaslIR; break;
case "WITHIN": Capabilities |= ImapCapabilities.Within; break;
case "ENABLE": Capabilities |= ImapCapabilities.Enable; break;
case "QRESYNC": Capabilities |= ImapCapabilities.QuickResync; break;
case "SEARCHRES": Capabilities |= ImapCapabilities.SearchResults; break;
case "SORT": Capabilities |= ImapCapabilities.Sort; break;
case "ANNOTATE-EXPERIMENT-1": Capabilities |= ImapCapabilities.Annotate; break;
case "LIST-EXTENDED": Capabilities |= ImapCapabilities.ListExtended; break;
case "CONVERT": Capabilities |= ImapCapabilities.Convert; break;
case "LANGUAGE": Capabilities |= ImapCapabilities.Language; break;
case "ESORT": Capabilities |= ImapCapabilities.ESort; break;
case "METADATA": Capabilities |= ImapCapabilities.Metadata; break;
case "METADATA-SERVER": Capabilities |= ImapCapabilities.MetadataServer; break;
case "NOTIFY": Capabilities |= ImapCapabilities.Notify; break;
case "LIST-STATUS": Capabilities |= ImapCapabilities.ListStatus; break;
case "SORT=DISPLAY": Capabilities |= ImapCapabilities.SortDisplay; break;
case "CREATE-SPECIAL-USE": Capabilities |= ImapCapabilities.CreateSpecialUse; break;
case "SPECIAL-USE": Capabilities |= ImapCapabilities.SpecialUse; break;
case "SEARCH=FUZZY": Capabilities |= ImapCapabilities.FuzzySearch; break;
case "MULTISEARCH": Capabilities |= ImapCapabilities.MultiSearch; break;
case "MOVE": Capabilities |= ImapCapabilities.Move; break;
case "UTF8=ACCEPT": Capabilities |= ImapCapabilities.UTF8Accept; break;
case "UTF8=ONLY": Capabilities |= ImapCapabilities.UTF8Only; break;
case "LITERAL-": Capabilities |= ImapCapabilities.LiteralMinus; break;
case "APPENDLIMIT": Capabilities |= ImapCapabilities.AppendLimit; break;
case "UNAUTHENTICATE": Capabilities |= ImapCapabilities.Unauthenticate; break;
case "STATUS=SIZE": Capabilities |= ImapCapabilities.StatusSize; break;
case "LIST-MYRIGHTS": Capabilities |= ImapCapabilities.ListMyRights; break;
case "OBJECTID": Capabilities |= ImapCapabilities.ObjectID; break;
case "REPLACE": Capabilities |= ImapCapabilities.Replace; break;
case "XLIST": Capabilities |= ImapCapabilities.XList; break;
case "X-GM-EXT-1": Capabilities |= ImapCapabilities.GMailExt1; QuirksMode = ImapQuirksMode.GMail; break;
case "XSTOP": QuirksMode = ImapQuirksMode.ProtonMail; break;
case "X-SUN-IMAP": QuirksMode = ImapQuirksMode.SunMicrosystems; break;
case "XYMHIGHESTMODSEQ": QuirksMode = ImapQuirksMode.Yahoo; break;
}
}
token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false);
}
AssertToken (token, sentinel, GenericItemSyntaxErrorFormat, "CAPABILITIES", token);
// unget the sentinel
Stream.UngetToken (token);
if ((Capabilities & ImapCapabilities.IMAP4rev1) != 0) {
ProtocolVersion = ImapProtocolVersion.IMAP4rev1;
Capabilities |= ImapCapabilities.Status;
} else if ((Capabilities & ImapCapabilities.IMAP4) != 0) {
ProtocolVersion = ImapProtocolVersion.IMAP4;
}
if ((Capabilities & ImapCapabilities.QuickResync) != 0)
Capabilities |= ImapCapabilities.CondStore;
if ((Capabilities & ImapCapabilities.UTF8Only) != 0)
Capabilities |= ImapCapabilities.UTF8Accept;
}
async Task UpdateNamespacesAsync (bool doAsync, CancellationToken cancellationToken)
{
var namespaces = new List<FolderNamespaceCollection> {
PersonalNamespaces, OtherNamespaces, SharedNamespaces
};
ImapFolder folder;
ImapToken token;
string path;
char delim;
int n = 0;
PersonalNamespaces.Clear ();
SharedNamespaces.Clear ();
OtherNamespaces.Clear ();
token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false);
do {
if (token.Type == ImapTokenType.OpenParen) {
// parse the list of namespace pairs...
token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false);
while (token.Type == ImapTokenType.OpenParen) {
// parse the namespace pair - first token is the path
token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false);
AssertToken (token, ImapTokenType.Atom, ImapTokenType.QString, GenericUntaggedResponseSyntaxErrorFormat, "NAMESPACE", token);
path = (string) token.Value;
// second token is the directory separator
token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false);
AssertToken (token, ImapTokenType.QString, ImapTokenType.Nil, GenericUntaggedResponseSyntaxErrorFormat, "NAMESPACE", token);
var qstring = token.Type == ImapTokenType.Nil ? string.Empty : (string) token.Value;
if (qstring.Length > 0) {
delim = qstring[0];
// canonicalize the namespace path
path = path.TrimEnd (delim);
} else {
delim = '\0';
}
namespaces[n].Add (new FolderNamespace (delim, DecodeMailboxName (path)));
if (!GetCachedFolder (path, out folder)) {
folder = CreateImapFolder (path, FolderAttributes.None, delim);
CacheFolder (folder);
}
folder.UpdateIsNamespace (true);
do {
token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false);
if (token.Type == ImapTokenType.CloseParen)
break;
// NAMESPACE extension
AssertToken (token, ImapTokenType.Atom, ImapTokenType.QString, GenericUntaggedResponseSyntaxErrorFormat, "NAMESPACE", token);
token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false);
AssertToken (token, ImapTokenType.OpenParen, GenericUntaggedResponseSyntaxErrorFormat, "NAMESPACE", token);
do {
token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false);
if (token.Type == ImapTokenType.CloseParen)
break;
AssertToken (token, ImapTokenType.Atom, ImapTokenType.QString, GenericUntaggedResponseSyntaxErrorFormat, "NAMESPACE", token);
} while (true);
} while (true);
// read the next token - it should either be '(' or ')'
token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false);
}
AssertToken (token, ImapTokenType.CloseParen, GenericUntaggedResponseSyntaxErrorFormat, "NAMESPACE", token);
} else {
AssertToken (token, ImapTokenType.Nil, GenericUntaggedResponseSyntaxErrorFormat, "NAMESPACE", token);
}
token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false);
n++;
} while (n < 3);
while (token.Type != ImapTokenType.Eoln)
token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false);
}
void ProcessResponseCodes (ImapCommand ic)
{
foreach (var code in ic.RespCodes) {
switch (code.Type) {
case ImapResponseCodeType.Alert:
OnAlert (code.Message);
break;
case ImapResponseCodeType.NotificationOverflow:
OnNotificationOverflow ();
break;
}
}
}
void EmitMetadataChanged (Metadata metadata)
{
var encodedName = metadata.EncodedName;
ImapFolder folder;
if (encodedName.Length == 0) {
OnMetadataChanged (metadata);
} else if (FolderCache.TryGetValue (encodedName, out folder)) {
folder.OnMetadataChanged (metadata);
}
}
internal MetadataCollection FilterMetadata (MetadataCollection metadata, string encodedName)
{
for (int i = 0; i < metadata.Count; i++) {
if (metadata[i].EncodedName == encodedName)
continue;
EmitMetadataChanged (metadata[i]);
metadata.RemoveAt (i);
i--;
}
return metadata;
}
internal void ProcessMetadataChanges (MetadataCollection metadata)
{
for (int i = 0; i < metadata.Count; i++)
EmitMetadataChanged (metadata[i]);
}
internal static ImapResponseCodeType GetResponseCodeType (string atom)
{
switch (atom.ToUpperInvariant ()) {
case "ALERT": return ImapResponseCodeType.Alert;
case "BADCHARSET": return ImapResponseCodeType.BadCharset;
case "CAPABILITY": return ImapResponseCodeType.Capability;
case "NEWNAME": return ImapResponseCodeType.NewName;
case "PARSE": return ImapResponseCodeType.Parse;
case "PERMANENTFLAGS": return ImapResponseCodeType.PermanentFlags;
case "READ-ONLY": return ImapResponseCodeType.ReadOnly;
case "READ-WRITE": return ImapResponseCodeType.ReadWrite;
case "TRYCREATE": return ImapResponseCodeType.TryCreate;
case "UIDNEXT": return ImapResponseCodeType.UidNext;
case "UIDVALIDITY": return ImapResponseCodeType.UidValidity;
case "UNSEEN": return ImapResponseCodeType.Unseen;
case "REFERRAL": return ImapResponseCodeType.Referral;
case "UNKNOWN-CTE": return ImapResponseCodeType.UnknownCte;
case "APPENDUID": return ImapResponseCodeType.AppendUid;
case "COPYUID": return ImapResponseCodeType.CopyUid;
case "UIDNOTSTICKY": return ImapResponseCodeType.UidNotSticky;
case "URLMECH": return ImapResponseCodeType.UrlMech;
case "BADURL": return ImapResponseCodeType.BadUrl;
case "TOOBIG": return ImapResponseCodeType.TooBig;
case "HIGHESTMODSEQ": return ImapResponseCodeType.HighestModSeq;
case "MODIFIED": return ImapResponseCodeType.Modified;
case "NOMODSEQ": return ImapResponseCodeType.NoModSeq;
case "COMPRESSIONACTIVE": return ImapResponseCodeType.CompressionActive;
case "CLOSED": return ImapResponseCodeType.Closed;
case "NOTSAVED": return ImapResponseCodeType.NotSaved;
case "BADCOMPARATOR": return ImapResponseCodeType.BadComparator;
case "ANNOTATE": return ImapResponseCodeType.Annotate;
case "ANNOTATIONS": return ImapResponseCodeType.Annotations;
case "MAXCONVERTMESSAGES": return ImapResponseCodeType.MaxConvertMessages;
case "MAXCONVERTPARTS": return ImapResponseCodeType.MaxConvertParts;
case "TEMPFAIL": return ImapResponseCodeType.TempFail;
case "NOUPDATE": return ImapResponseCodeType.NoUpdate;
case "METADATA": return ImapResponseCodeType.Metadata;
case "NOTIFICATIONOVERFLOW": return ImapResponseCodeType.NotificationOverflow;
case "BADEVENT": return ImapResponseCodeType.BadEvent;
case "UNDEFINED-FILTER": return ImapResponseCodeType.UndefinedFilter;
case "UNAVAILABLE": return ImapResponseCodeType.Unavailable;
case "AUTHENTICATIONFAILED": return ImapResponseCodeType.AuthenticationFailed;
case "AUTHORIZATIONFAILED": return ImapResponseCodeType.AuthorizationFailed;
case "EXPIRED": return ImapResponseCodeType.Expired;
case "PRIVACYREQUIRED": return ImapResponseCodeType.PrivacyRequired;
case "CONTACTADMIN": return ImapResponseCodeType.ContactAdmin;
case "NOPERM": return ImapResponseCodeType.NoPerm;
case "INUSE": return ImapResponseCodeType.InUse;
case "EXPUNGEISSUED": return ImapResponseCodeType.ExpungeIssued;
case "CORRUPTION": return ImapResponseCodeType.Corruption;
case "SERVERBUG": return ImapResponseCodeType.ServerBug;
case "CLIENTBUG": return ImapResponseCodeType.ClientBug;
case "CANNOT": return ImapResponseCodeType.CanNot;
case "LIMIT": return ImapResponseCodeType.Limit;
case "OVERQUOTA": return ImapResponseCodeType.OverQuota;
case "ALREADYEXISTS": return ImapResponseCodeType.AlreadyExists;
case "NONEXISTENT": return ImapResponseCodeType.NonExistent;
case "USEATTR": return ImapResponseCodeType.UseAttr;
case "MAILBOXID": return ImapResponseCodeType.MailboxId;
default: return ImapResponseCodeType.Unknown;
}
}
/// <summary>
/// Parses the response code.
/// </summary>
/// <returns>The response code.</returns>
/// <param name="isTagged">Whether or not the resp-code is tagged vs untagged.</param>
/// <param name="doAsync">Whether or not asynchronous IO methods should be used.</param>
/// <param name="cancellationToken">The cancellation token.</param>
public async Task<ImapResponseCode> ParseResponseCodeAsync (bool isTagged, bool doAsync, CancellationToken cancellationToken)
{
uint validity = Selected != null ? Selected.UidValidity : 0;
ImapResponseCode code;
ImapToken token;
string atom;
// token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false);
//
// if (token.Type != ImapTokenType.LeftBracket) {
// Debug.WriteLine ("Expected a '[' followed by a RESP-CODE, but got: {0}", token);
// throw UnexpectedToken (token, false);
// }
token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false);
AssertToken (token, ImapTokenType.Atom, "Syntax error in response code. Unexpected token: {0}", token);
atom = (string) token.Value;
token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false);
code = ImapResponseCode.Create (GetResponseCodeType (atom));
code.IsTagged = isTagged;
switch (code.Type) {
case ImapResponseCodeType.BadCharset:
if (token.Type == ImapTokenType.OpenParen) {
token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false);
SupportedCharsets.Clear ();
while (token.Type == ImapTokenType.Atom || token.Type == ImapTokenType.QString) {
SupportedCharsets.Add ((string) token.Value);
token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false);
}
AssertToken (token, ImapTokenType.CloseParen, GenericResponseCodeSyntaxErrorFormat, "BADCHARSET", token);
token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false);
}
break;
case ImapResponseCodeType.Capability:
Stream.UngetToken (token);
await UpdateCapabilitiesAsync (ImapTokenType.CloseBracket, doAsync, cancellationToken).ConfigureAwait (false);
token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false);
break;
case ImapResponseCodeType.PermanentFlags:
var perm = (PermanentFlagsResponseCode) code;
Stream.UngetToken (token);
perm.Flags = await ImapUtils.ParseFlagsListAsync (this, "PERMANENTFLAGS", null, doAsync, cancellationToken).ConfigureAwait (false);
token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false);
break;
case ImapResponseCodeType.UidNext:
var next = (UidNextResponseCode) code;
// Note: we allow '0' here because some servers have been known to send "* OK [UIDNEXT 0]".
// The *probable* explanation here is that the folder has never been opened and/or no messages
// have ever been delivered (yet) to that mailbox and so the UIDNEXT has not (yet) been
// initialized.
//
// See https://github.com/jstedfast/MailKit/issues/1010 for an example.
var uid = ParseNumber (token, false, GenericResponseCodeSyntaxErrorFormat, "UIDNEXT", token);
next.Uid = uid > 0 ? new UniqueId (uid) : UniqueId.Invalid;
token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false);
break;
case ImapResponseCodeType.UidValidity:
var uidvalidity = (UidValidityResponseCode) code;
// Note: we allow '0' here because some servers have been known to send "* OK [UIDVALIDITY 0]".
// The *probable* explanation here is that the folder has never been opened and/or no messages
// have ever been delivered (yet) to that mailbox and so the UIDVALIDITY has not (yet) been
// initialized.
//
// See https://github.com/jstedfast/MailKit/issues/150 for an example.
uidvalidity.UidValidity = ParseNumber (token, false, GenericResponseCodeSyntaxErrorFormat, "UIDVALIDITY", token);
token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false);
break;
case ImapResponseCodeType.Unseen:
var unseen = (UnseenResponseCode) code;
// Note: we allow '0' here because some servers have been known to send "* OK [UNSEEN 0]" when the
// mailbox contains no messages.
//
// See https://github.com/jstedfast/MailKit/issues/34 for details.
var n = ParseNumber (token, false, GenericResponseCodeSyntaxErrorFormat, "UNSEEN", token);
unseen.Index = n > 0 ? (int) (n - 1) : 0;
token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false);
break;
case ImapResponseCodeType.NewName:
var rename = (NewNameResponseCode) code;
// Note: this RESP-CODE existed in rfc2060 but has been removed in rfc3501:
//
// 85) Remove NEWNAME. It can't work because mailbox names can be
// literals and can include "]". Functionality can be addressed via
// referrals.
AssertToken (token, ImapTokenType.Atom, ImapTokenType.QString, GenericResponseCodeSyntaxErrorFormat, "NEWNAME", token);
rename.OldName = (string) token.Value;
// the next token should be another atom or qstring token representing the new name of the folder
token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false);
AssertToken (token, ImapTokenType.Atom, ImapTokenType.QString, GenericResponseCodeSyntaxErrorFormat, "NEWNAME", token);
rename.NewName = (string) token.Value;
token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false);
break;
case ImapResponseCodeType.AppendUid:
var append = (AppendUidResponseCode) code;
append.UidValidity = ParseNumber (token, false, GenericResponseCodeSyntaxErrorFormat, "APPENDUID", token);
token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false);
// The MULTIAPPEND extension redefines APPENDUID's second argument to be a uid-set instead of a single uid.
append.UidSet = ParseUidSet (token, append.UidValidity, GenericResponseCodeSyntaxErrorFormat, "APPENDUID", token);
token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false);
break;
case ImapResponseCodeType.CopyUid:
var copy = (CopyUidResponseCode) code;
copy.UidValidity = ParseNumber (token, false, GenericResponseCodeSyntaxErrorFormat, "COPYUID", token);
token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false);
// Note: Outlook.com will apparently sometimes issue a [COPYUID nz_number SPACE SPACE] resp-code
// in response to a UID COPY or UID MOVE command. Likely this happens only when the source message
// didn't exist or something? See https://github.com/jstedfast/MailKit/issues/555 for details.
if (token.Type != ImapTokenType.CloseBracket) {
copy.SrcUidSet = ParseUidSet (token, validity, GenericResponseCodeSyntaxErrorFormat, "COPYUID", token);
} else {
copy.SrcUidSet = new UniqueIdSet ();
Stream.UngetToken (token);
}
token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false);
if (token.Type != ImapTokenType.CloseBracket) {
copy.DestUidSet = ParseUidSet (token, copy.UidValidity, GenericResponseCodeSyntaxErrorFormat, "COPYUID", token);
} else {
copy.DestUidSet = new UniqueIdSet ();
Stream.UngetToken (token);
}
token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false);
break;
case ImapResponseCodeType.BadUrl:
var badurl = (BadUrlResponseCode) code;
AssertToken (token, ImapTokenType.Atom, ImapTokenType.QString, GenericResponseCodeSyntaxErrorFormat, "BADURL", token);
badurl.BadUrl = (string) token.Value;
token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false);
break;
case ImapResponseCodeType.HighestModSeq:
var highest = (HighestModSeqResponseCode) code;
highest.HighestModSeq = ParseNumber64 (token, false, GenericResponseCodeSyntaxErrorFormat, "HIGHESTMODSEQ", token);
token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false);
break;
case ImapResponseCodeType.Modified:
var modified = (ModifiedResponseCode) code;
modified.UidSet = ParseUidSet (token, validity, GenericResponseCodeSyntaxErrorFormat, "MODIFIED", token);
token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false);
break;
case ImapResponseCodeType.MaxConvertMessages:
case ImapResponseCodeType.MaxConvertParts:
var maxConvert = (MaxConvertResponseCode) code;
maxConvert.MaxConvert = ParseNumber (token, false, GenericResponseCodeSyntaxErrorFormat, atom, token);
token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false);
break;
case ImapResponseCodeType.NoUpdate:
var noUpdate = (NoUpdateResponseCode) code;
AssertToken (token, ImapTokenType.Atom, ImapTokenType.QString, GenericResponseCodeSyntaxErrorFormat, "NOUPDATE", token);
noUpdate.Tag = (string) token.Value;
token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false);
break;
case ImapResponseCodeType.Annotate:
var annotate = (AnnotateResponseCode) code;
AssertToken (token, ImapTokenType.Atom, GenericResponseCodeSyntaxErrorFormat, "ANNOTATE", token);
switch (((string) token.Value).ToUpperInvariant ()) {
case "TOOBIG":
annotate.SubType = AnnotateResponseCodeSubType.TooBig;
break;
case "TOOMANY":
annotate.SubType = AnnotateResponseCodeSubType.TooMany;
break;
}
token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false);
break;
case ImapResponseCodeType.Annotations:
var annotations = (AnnotationsResponseCode) code;
AssertToken (token, ImapTokenType.Atom, GenericResponseCodeSyntaxErrorFormat, "ANNOTATIONS", token);
switch (((string) token.Value).ToUpperInvariant ()) {
case "NONE": break;
case "READ-ONLY":
annotations.Access = AnnotationAccess.ReadOnly;
break;
default:
annotations.Access = AnnotationAccess.ReadWrite;
annotations.MaxSize = ParseNumber (token, false, GenericResponseCodeSyntaxErrorFormat, "ANNOTATIONS", token);
break;
}
token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false);
if (annotations.Access != AnnotationAccess.None) {
annotations.Scopes = AnnotationScope.Both;
if (token.Type != ImapTokenType.CloseBracket) {
AssertToken (token, ImapTokenType.Atom, GenericResponseCodeSyntaxErrorFormat, "ANNOTATIONS", token);
if (((string) token.Value).Equals ("NOPRIVATE", StringComparison.OrdinalIgnoreCase))
annotations.Scopes = AnnotationScope.Shared;
token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false);
}
}
break;
case ImapResponseCodeType.Metadata:
var metadata = (MetadataResponseCode) code;
AssertToken (token, ImapTokenType.Atom, GenericResponseCodeSyntaxErrorFormat, "METADATA", token);
switch (((string) token.Value).ToUpperInvariant ()) {
case "LONGENTRIES":
metadata.SubType = MetadataResponseCodeSubType.LongEntries;
metadata.IsError = false;
token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false);
metadata.Value = ParseNumber (token, false, GenericResponseCodeSyntaxErrorFormat, "METADATA LONGENTRIES", token);
break;
case "MAXSIZE":
metadata.SubType = MetadataResponseCodeSubType.MaxSize;
token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false);
metadata.Value = ParseNumber (token, false, GenericResponseCodeSyntaxErrorFormat, "METADATA MAXSIZE", token);
break;
case "TOOMANY":
metadata.SubType = MetadataResponseCodeSubType.TooMany;
break;
case "NOPRIVATE":
metadata.SubType = MetadataResponseCodeSubType.NoPrivate;
break;
}
token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false);
break;
case ImapResponseCodeType.UndefinedFilter:
var undefined = (UndefinedFilterResponseCode) code;
AssertToken (token, ImapTokenType.Atom, GenericResponseCodeSyntaxErrorFormat, "UNDEFINED-FILTER", token);
undefined.Name = (string) token.Value;
token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false);
break;
case ImapResponseCodeType.MailboxId:
var mailboxid = (MailboxIdResponseCode) code;
AssertToken (token, ImapTokenType.OpenParen, GenericResponseCodeSyntaxErrorFormat, "MAILBOXID", token);
token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false);
AssertToken (token, ImapTokenType.Atom, GenericResponseCodeSyntaxErrorFormat, "MAILBOXID", token);
mailboxid.MailboxId = (string) token.Value;
token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false);
AssertToken (token, ImapTokenType.CloseParen, GenericResponseCodeSyntaxErrorFormat, "MAILBOXID", token);
token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false);
break;
default:
// Note: This code-path handles: [ALERT], [CLOSED], [READ-ONLY], [READ-WRITE], etc.
//if (code.Type == ImapResponseCodeType.Unknown)
// Debug.WriteLine (string.Format ("Unknown RESP-CODE encountered: {0}", atom));
// extensions are of the form: "[" atom [SPACE 1*<any TEXT_CHAR except "]">] "]"
// skip over tokens until we get to a ']'
while (token.Type != ImapTokenType.CloseBracket && token.Type != ImapTokenType.Eoln)
token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false);
break;
}
AssertToken (token, ImapTokenType.CloseBracket, "Syntax error in response code. Unexpected token: {0}", token);
code.Message = (await ReadLineAsync (doAsync, cancellationToken).ConfigureAwait (false)).Trim ();
return code;
}
async Task UpdateStatusAsync (bool doAsync, CancellationToken cancellationToken)
{
var token = await ReadTokenAsync (ImapStream.AtomSpecials, doAsync, cancellationToken).ConfigureAwait (false);
ImapFolder folder;
uint count, uid;
ulong modseq;
string name;
switch (token.Type) {
case ImapTokenType.Literal:
name = await ReadLiteralAsync (doAsync, cancellationToken).ConfigureAwait (false);
break;
case ImapTokenType.QString:
case ImapTokenType.Atom:
name = (string) token.Value;
break;
case ImapTokenType.Nil:
// Note: according to rfc3501, section 4.5, NIL is acceptable as a mailbox name.
name = "NIL";
break;
default:
throw UnexpectedToken (GenericUntaggedResponseSyntaxErrorFormat, "STATUS", token);
}
// Note: if the folder is null, then it probably means the user is using NOTIFY
// and hasn't yet requested the folder. That's ok.
GetCachedFolder (name, out folder);
token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false);
AssertToken (token, ImapTokenType.OpenParen, GenericUntaggedResponseSyntaxErrorFormat, "STATUS", token);
do {
token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false);
if (token.Type == ImapTokenType.CloseParen)
break;
AssertToken (token, ImapTokenType.Atom, GenericUntaggedResponseSyntaxErrorFormat, "STATUS", token);
var atom = (string) token.Value;
token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false);
switch (atom.ToUpperInvariant ()) {
case "HIGHESTMODSEQ":
AssertToken (token, ImapTokenType.Atom, GenericUntaggedResponseSyntaxErrorFormat, "STATUS", token);
modseq = ParseNumber64 (token, false, GenericItemSyntaxErrorFormat, atom, token);
if (folder != null)
folder.UpdateHighestModSeq (modseq);
break;
case "MESSAGES":
AssertToken (token, ImapTokenType.Atom, GenericUntaggedResponseSyntaxErrorFormat, "STATUS", token);
count = ParseNumber (token, false, GenericItemSyntaxErrorFormat, atom, token);
if (folder != null)
folder.OnExists ((int) count);
break;
case "RECENT":
AssertToken (token, ImapTokenType.Atom, GenericUntaggedResponseSyntaxErrorFormat, "STATUS", token);
count = ParseNumber (token, false, GenericItemSyntaxErrorFormat, atom, token);
if (folder != null)
folder.OnRecent ((int) count);
break;
case "UIDNEXT":
AssertToken (token, ImapTokenType.Atom, GenericUntaggedResponseSyntaxErrorFormat, "STATUS", token);
uid = ParseNumber (token, false, GenericItemSyntaxErrorFormat, atom, token);
if (folder != null)
folder.UpdateUidNext (uid > 0 ? new UniqueId (uid) : UniqueId.Invalid);
break;
case "UIDVALIDITY":
AssertToken (token, ImapTokenType.Atom, GenericUntaggedResponseSyntaxErrorFormat, "STATUS", token);
uid = ParseNumber (token, false, GenericItemSyntaxErrorFormat, atom, token);
if (folder != null)
folder.UpdateUidValidity (uid);
break;
case "UNSEEN":
AssertToken (token, ImapTokenType.Atom, GenericUntaggedResponseSyntaxErrorFormat, "STATUS", token);
count = ParseNumber (token, false, GenericItemSyntaxErrorFormat, atom, token);
if (folder != null)
folder.UpdateUnread ((int) count);
break;
case "APPENDLIMIT":
if (token.Type == ImapTokenType.Atom) {
var limit = ParseNumber (token, false, GenericItemSyntaxErrorFormat, atom, token);
if (folder != null)
folder.UpdateAppendLimit (limit);
} else {
AssertToken (token, ImapTokenType.Nil, GenericUntaggedResponseSyntaxErrorFormat, "STATUS", token);
if (folder != null)
folder.UpdateAppendLimit (null);
}
break;
case "SIZE":
AssertToken (token, ImapTokenType.Atom, GenericUntaggedResponseSyntaxErrorFormat, "STATUS", token);
var size = ParseNumber64 (token, false, GenericItemSyntaxErrorFormat, atom, token);
if (folder != null)
folder.UpdateSize (size);
break;
case "MAILBOXID":
AssertToken (token, ImapTokenType.OpenParen, GenericUntaggedResponseSyntaxErrorFormat, "STATUS", token);
token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false);
AssertToken (token, ImapTokenType.Atom, GenericItemSyntaxErrorFormat, atom, token);
if (folder != null)
folder.UpdateId ((string) token.Value);
token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false);
AssertToken (token, ImapTokenType.CloseParen, GenericUntaggedResponseSyntaxErrorFormat, "STATUS", token);
break;
}
} while (true);
token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false);
AssertToken (token, ImapTokenType.Eoln, GenericUntaggedResponseSyntaxErrorFormat, "STATUS", token);
}
/// <summary>
/// Processes an untagged response.
/// </summary>
/// <returns>The untagged response.</returns>
/// <param name="doAsync">Whether or not asynchronous IO methods should be used.</param>
/// <param name="cancellationToken">The cancellation token.</param>
internal async Task<ImapUntaggedResult> ProcessUntaggedResponseAsync (bool doAsync, CancellationToken cancellationToken)
{
var token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false);
var folder = current.Folder ?? Selected;
var result = ImapUntaggedResult.Handled;
ImapUntaggedHandler handler;
uint number;
string atom;
// Note: work around broken IMAP servers such as home.pl which sends "* [COPYUID ...]" resp-codes
// See https://github.com/jstedfast/MailKit/issues/115#issuecomment-313684616 for details.
if (token.Type == ImapTokenType.OpenBracket) {
// unget the '[' token and then pretend that we got an "OK"
Stream.UngetToken (token);
atom = "OK";
} else if (token.Type != ImapTokenType.Atom) {
// if we get anything else here, just ignore it?
Stream.UngetToken (token);
await SkipLineAsync (doAsync, cancellationToken).ConfigureAwait (false);
return result;
} else {
atom = (string) token.Value;
}
switch (atom.ToUpperInvariant ()) {
case "BYE":
token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false);
if (token.Type == ImapTokenType.OpenBracket) {
var code = await ParseResponseCodeAsync (false, doAsync, cancellationToken).ConfigureAwait (false);
current.RespCodes.Add (code);
} else {
var text = token.Value.ToString () + await ReadLineAsync (doAsync, cancellationToken).ConfigureAwait (false);
current.ResponseText = text.TrimEnd ();
}
current.Bye = true;
// Note: Yandex IMAP is broken and will continue sending untagged BYE responses until the client closes
// the connection. In order to avoid this scenario, consider this command complete as soon as we receive
// the very first untagged BYE response and do not hold out hoping for a tagged response following the
// untagged BYE.
//
// See https://github.com/jstedfast/MailKit/issues/938 for details.
if (QuirksMode == ImapQuirksMode.Yandex && !current.Logout)
current.Status = ImapCommandStatus.Complete;
break;
case "CAPABILITY":
await UpdateCapabilitiesAsync (ImapTokenType.Eoln, doAsync, cancellationToken);
// read the eoln token
await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false);
break;
case "ENABLED":
do {
token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false);
if (token.Type == ImapTokenType.Eoln)
break;
AssertToken (token, ImapTokenType.Atom, GenericUntaggedResponseSyntaxErrorFormat, atom, token);
var feature = (string) token.Value;
switch (feature.ToUpperInvariant ()) {
case "UTF8=ACCEPT": UTF8Enabled = true; break;
case "QRESYNC": QResyncEnabled = true; break;
}
} while (true);
break;
case "FLAGS":
folder.UpdateAcceptedFlags (await ImapUtils.ParseFlagsListAsync (this, atom, null, doAsync, cancellationToken).ConfigureAwait (false));
token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false);
AssertToken (token, ImapTokenType.Eoln, GenericUntaggedResponseSyntaxErrorFormat, atom, token);
break;
case "NAMESPACE":
await UpdateNamespacesAsync (doAsync, cancellationToken).ConfigureAwait (false);
break;
case "STATUS":
await UpdateStatusAsync (doAsync, cancellationToken).ConfigureAwait (false);
break;
case "OK": case "NO": case "BAD":
if (atom.Equals ("OK", StringComparison.OrdinalIgnoreCase))
result = ImapUntaggedResult.Ok;
else if (atom.Equals ("NO", StringComparison.OrdinalIgnoreCase))
result = ImapUntaggedResult.No;
else
result = ImapUntaggedResult.Bad;
token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false);
if (token.Type == ImapTokenType.OpenBracket) {
var code = await ParseResponseCodeAsync (false, doAsync, cancellationToken).ConfigureAwait (false);
current.RespCodes.Add (code);
} else if (token.Type != ImapTokenType.Eoln) {
var text = ((string) token.Value) + await ReadLineAsync (doAsync, cancellationToken).ConfigureAwait (false);
current.ResponseText = text.TrimEnd ();
}
break;
default:
if (uint.TryParse (atom, NumberStyles.None, CultureInfo.InvariantCulture, out number)) {
// we probably have something like "* 1 EXISTS"
token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false);
AssertToken (token, ImapTokenType.Atom, "Syntax error in untagged response. Unexpected token: {0}", token);
atom = (string) token.Value;
if (current.UntaggedHandlers.TryGetValue (atom, out handler)) {
// the command registered an untagged handler for this atom...
await handler (this, current, (int) number - 1, doAsync).ConfigureAwait (false);
} else if (folder != null) {
switch (atom.ToUpperInvariant ()) {
case "EXISTS":
folder.OnExists ((int) number);
break;
case "EXPUNGE":
if (number == 0)
throw UnexpectedToken ("Syntax error in untagged EXPUNGE response. Unexpected message index: 0");
folder.OnExpunge ((int) number - 1);
break;
case "FETCH":
// Apparently Courier-IMAP (2004) will reply with "* 0 FETCH ..." sometimes.
// See https://github.com/jstedfast/MailKit/issues/428 for details.
//if (number == 0)
// throw UnexpectedToken ("Syntax error in untagged FETCH response. Unexpected message index: 0");
await folder.OnFetchAsync (this, (int) number - 1, doAsync, cancellationToken).ConfigureAwait (false);
break;
case "RECENT":
folder.OnRecent ((int) number);
break;
default:
//Debug.WriteLine ("Unhandled untagged response: * {0} {1}", number, atom);
break;
}
} else {
//Debug.WriteLine ("Unhandled untagged response: * {0} {1}", number, atom);
}
await SkipLineAsync (doAsync, cancellationToken).ConfigureAwait (false);
} else if (current.UntaggedHandlers.TryGetValue (atom, out handler)) {
// the command registered an untagged handler for this atom...
await handler (this, current, -1, doAsync).ConfigureAwait (false);
await SkipLineAsync (doAsync, cancellationToken).ConfigureAwait (false);
} else if (atom.Equals ("LIST", StringComparison.OrdinalIgnoreCase)) {
// unsolicited LIST response - probably due to NOTIFY MailboxName or MailboxSubscribe event
await ImapUtils.ParseFolderListAsync (this, null, false, true, doAsync, cancellationToken).ConfigureAwait (false);
token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false);
AssertToken (token, ImapTokenType.Eoln, "Syntax error in untagged LIST response. Unexpected token: {0}", token);
} else if (atom.Equals ("METADATA", StringComparison.OrdinalIgnoreCase)) {
// unsolicited METADATA response - probably due to NOTIFY MailboxMetadataChange or ServerMetadataChange
var metadata = new MetadataCollection ();
await ImapUtils.ParseMetadataAsync (this, metadata, doAsync, cancellationToken).ConfigureAwait (false);
ProcessMetadataChanges (metadata);
token = await ReadTokenAsync (doAsync, cancellationToken).ConfigureAwait (false);
AssertToken (token, ImapTokenType.Eoln, "Syntax error in untagged LIST response. Unexpected token: {0}", token);
} else if (atom.Equals ("VANISHED", StringComparison.OrdinalIgnoreCase) && folder != null) {
await folder.OnVanishedAsync (this, doAsync, cancellationToken).ConfigureAwait (false);
await SkipLineAsync (doAsync, cancellationToken).ConfigureAwait (false);
} else {
// don't know how to handle this... eat it?
await SkipLineAsync (doAsync, cancellationToken).ConfigureAwait (false);
}
break;
}
return result;
}
/// <summary>
/// Iterate the command pipeline.
/// </summary>
async Task IterateAsync (bool doAsync)
{
lock (queue) {
if (queue.Count == 0)
throw new InvalidOperationException ("The IMAP command queue is empty.");
if (IsBusy)
throw new InvalidOperationException ("The ImapClient is currently busy processing a command in another thread. Lock the SyncRoot property to properly synchronize your threads.");
current = queue[0];
queue.RemoveAt (0);
try {
current.CancellationToken.ThrowIfCancellationRequested ();
} catch {
queue.RemoveAll (x => x.CancellationToken.IsCancellationRequested);
current = null;
throw;
}
}
current.Status = ImapCommandStatus.Active;
try {
while (await current.StepAsync (doAsync).ConfigureAwait (false)) {
// more literal data to send...
}
if (current.Bye && !current.Logout)
throw new ImapProtocolException ("Bye.");
} catch (ImapProtocolException) {
var ic = current;
Disconnect ();
if (ic.Bye) {
if (ic.RespCodes.Count > 0) {
var code = ic.RespCodes[ic.RespCodes.Count - 1];
if (code.Type == ImapResponseCodeType.Alert) {
OnAlert (code.Message);
throw new ImapProtocolException (code.Message);
}
}
if (!string.IsNullOrEmpty (ic.ResponseText))
throw new ImapProtocolException (ic.ResponseText);
}
throw;
} catch {
Disconnect ();
throw;
} finally {
current = null;
}
}
/// <summary>
/// Wait for the specified command to finish.
/// </summary>
/// <param name="ic">The IMAP command.</param>
/// <param name="doAsync">Whether or not asynchronous IO methods should be used.</param>
/// <exception cref="System.ArgumentNullException">
/// <paramref name="ic"/> is <c>null</c>.
/// </exception>
public async Task RunAsync (ImapCommand ic, bool doAsync)
{
if (ic == null)
throw new ArgumentNullException (nameof (ic));
while (ic.Status < ImapCommandStatus.Complete) {
// continue processing commands...
await IterateAsync (doAsync).ConfigureAwait (false);
}
ProcessResponseCodes (ic);
}
public IEnumerable<ImapCommand> CreateCommands (CancellationToken cancellationToken, ImapFolder folder, string format, IList<UniqueId> uids, params object[] args)
{
var vargs = new List<object> ();
int maxLength;
// we assume that uids is the first formatter (with a %s)
vargs.Add ("1");
for (int i = 0; i < args.Length; i++)
vargs.Add (args[i]);
args = vargs.ToArray ();
if (QuirksMode == ImapQuirksMode.Courier) {
// Courier IMAP's command parser allows each token to be up to 16k in size.
maxLength = 16 * 1024;
} else {
int estimated = ImapCommand.EstimateCommandLength (this, format, args);
switch (QuirksMode) {
case ImapQuirksMode.Dovecot:
// Dovecot, by default, allows commands up to 64k.
// See https://github.com/dovecot/core/blob/master/src/imap/imap-settings.c#L94
maxLength = Math.Max ((64 * 1042) - estimated, 24);
break;
case ImapQuirksMode.GMail:
// GMail seems to support command-lines up to at least 16k.
maxLength = Math.Max ((16 * 1042) - estimated, 24);
break;
case ImapQuirksMode.Yahoo:
case ImapQuirksMode.UW:
// Follow the IMAP4 Implementation Recommendations which states that clients
// *SHOULD* limit their command lengths to 1000 octets.
maxLength = Math.Max (1000 - estimated, 24);
break;
default:
// Push the boundaries of the IMAP4 Implementation Recommendations which states
// that servers *SHOULD* accept command lengths of up to 8000 octets.
maxLength = Math.Max (8000 - estimated, 24);
break;
}
}
foreach (var subset in UniqueIdSet.EnumerateSerializedSubsets (uids, maxLength)) {
args[0] = subset;
yield return new ImapCommand (this, cancellationToken, folder, format, args);
}
}
public IEnumerable<ImapCommand> QueueCommands (CancellationToken cancellationToken, ImapFolder folder, string format, IList<UniqueId> uids, params object[] args)
{
foreach (var ic in CreateCommands (cancellationToken, folder, format, uids, args)) {
QueueCommand (ic);
yield return ic;
}
}
/// <summary>
/// Queues the command.
/// </summary>
/// <returns>The command.</returns>
/// <param name="cancellationToken">The cancellation token.</param>
/// <param name="folder">The folder that the command operates on.</param>
/// <param name="options">The formatting options.</param>
/// <param name="format">The command format.</param>
/// <param name="args">The command arguments.</param>
public ImapCommand QueueCommand (CancellationToken cancellationToken, ImapFolder folder, FormatOptions options, string format, params object[] args)
{
var ic = new ImapCommand (this, cancellationToken, folder, options, format, args);
QueueCommand (ic);
return ic;
}
/// <summary>
/// Queues the command.
/// </summary>
/// <returns>The command.</returns>
/// <param name="cancellationToken">The cancellation token.</param>
/// <param name="folder">The folder that the command operates on.</param>
/// <param name="format">The command format.</param>
/// <param name="args">The command arguments.</param>
public ImapCommand QueueCommand (CancellationToken cancellationToken, ImapFolder folder, string format, params object[] args)
{
return QueueCommand (cancellationToken, folder, FormatOptions.Default, format, args);
}
/// <summary>
/// Queues the command.
/// </summary>
/// <param name="ic">The IMAP command.</param>
public void QueueCommand (ImapCommand ic)
{
lock (queue) {
ic.Status = ImapCommandStatus.Queued;
queue.Add (ic);
}
}
/// <summary>
/// Queries the capabilities.
/// </summary>
/// <returns>The command result.</returns>
/// <param name="doAsync">Whether or not asynchronous IO methods should be used.</param>
/// <param name="cancellationToken">The cancellation token.</param>
public async Task<ImapCommandResponse> QueryCapabilitiesAsync (bool doAsync, CancellationToken cancellationToken)
{
var ic = QueueCommand (cancellationToken, null, "CAPABILITY\r\n");
await RunAsync (ic, doAsync).ConfigureAwait (false);
return ic.Response;
}
/// <summary>
/// Cache the specified folder.
/// </summary>
/// <param name="folder">The folder.</param>
public void CacheFolder (ImapFolder folder)
{
if ((folder.Attributes & FolderAttributes.Inbox) != 0)
cacheComparer.DirectorySeparator = folder.DirectorySeparator;
FolderCache.Add (folder.EncodedName, folder);
}
/// <summary>
/// Gets the cached folder.
/// </summary>
/// <returns><c>true</c> if the folder was retreived from the cache; otherwise, <c>false</c>.</returns>
/// <param name="encodedName">The encoded folder name.</param>
/// <param name="folder">The cached folder.</param>
public bool GetCachedFolder (string encodedName, out ImapFolder folder)
{
return FolderCache.TryGetValue (encodedName, out folder);
}
/// <summary>
/// Looks up and sets the <see cref="MailFolder.ParentFolder"/> property of each of the folders.
/// </summary>
/// <param name="folders">The IMAP folders.</param>
/// <param name="doAsync">Whether or not asynchronous IO methods should be used.</param>
/// <param name="cancellationToken">The cancellation token.</param>
internal async Task LookupParentFoldersAsync (IEnumerable<ImapFolder> folders, bool doAsync, CancellationToken cancellationToken)
{
var list = new List<ImapFolder> (folders);
string encodedName, pattern;
ImapFolder parent;
int index;
// Note: we use a for-loop instead of foreach because we conditionally add items to the list.
for (int i = 0; i < list.Count; i++) {
var folder = list[i];
if (folder.ParentFolder != null)
continue;
// FIXME: should this search EncodedName instead of FullName?
if ((index = folder.FullName.LastIndexOf (folder.DirectorySeparator)) != -1) {
if (index == 0)
continue;
var parentName = folder.FullName.Substring (0, index);
encodedName = EncodeMailboxName (parentName);
} else {
encodedName = string.Empty;
}
if (GetCachedFolder (encodedName, out parent)) {
folder.ParentFolder = parent;
continue;
}
// Note: folder names can contain wildcards (including '*' and '%'), so replace '*' with '%'
// in order to reduce the list of folders returned by our LIST command.
pattern = encodedName.Replace ('*', '%');
var command = new StringBuilder ("LIST \"\" %S");
var returnsSubscribed = false;
if ((Capabilities & ImapCapabilities.ListExtended) != 0) {
// Try to get the \Subscribed and \HasChildren or \HasNoChildren attributes
command.Append (" RETURN (SUBSCRIBED CHILDREN)");
returnsSubscribed = true;
}
command.Append ("\r\n");
var ic = new ImapCommand (this, cancellationToken, null, command.ToString (), pattern);
ic.RegisterUntaggedHandler ("LIST", ImapUtils.ParseFolderListAsync);
ic.ListReturnsSubscribed = returnsSubscribed;
ic.UserData = new List<ImapFolder> ();
QueueCommand (ic);
await RunAsync (ic, doAsync).ConfigureAwait (false);
if (!GetCachedFolder (encodedName, out parent)) {
parent = CreateImapFolder (encodedName, FolderAttributes.NonExistent, folder.DirectorySeparator);
CacheFolder (parent);
} else if (parent.ParentFolder == null && !parent.IsNamespace) {
list.Add (parent);
}
folder.ParentFolder = parent;
}
}
/// <summary>
/// Queries the namespaces.
/// </summary>
/// <returns>The command result.</returns>
/// <param name="doAsync">Whether or not asynchronous IO methods should be used.</param>
/// <param name="cancellationToken">The cancellation token.</param>
public async Task<ImapCommandResponse> QueryNamespacesAsync (bool doAsync, CancellationToken cancellationToken)
{
ImapCommand ic;
if ((Capabilities & ImapCapabilities.Namespace) != 0) {
ic = QueueCommand (cancellationToken, null, "NAMESPACE\r\n");
await RunAsync (ic, doAsync).ConfigureAwait (false);
} else {
var list = new List<ImapFolder> ();
ic = new ImapCommand (this, cancellationToken, null, "LIST \"\" \"\"\r\n");
ic.RegisterUntaggedHandler ("LIST", ImapUtils.ParseFolderListAsync);
ic.UserData = list;
QueueCommand (ic);
await RunAsync (ic, doAsync).ConfigureAwait (false);
PersonalNamespaces.Clear ();
SharedNamespaces.Clear ();
OtherNamespaces.Clear ();
if (list.Count > 0) {
var empty = list.FirstOrDefault (x => x.EncodedName.Length == 0);
if (empty == null) {
empty = CreateImapFolder (string.Empty, FolderAttributes.None, list[0].DirectorySeparator);
CacheFolder (empty);
}
PersonalNamespaces.Add (new FolderNamespace (empty.DirectorySeparator, empty.FullName));
empty.UpdateIsNamespace (true);
}
await LookupParentFoldersAsync (list, doAsync, cancellationToken).ConfigureAwait (false);
}
return ic.Response;
}
internal static ImapFolder GetFolder (List<ImapFolder> folders, string encodedName)
{
for (int i = 0; i < folders.Count; i++) {
if (encodedName.Equals (folders[i].EncodedName, StringComparison.OrdinalIgnoreCase))
return folders[i];
}
return null;
}
/// <summary>
/// Assigns a folder as a special folder.
/// </summary>
/// <param name="folder">The special folder.</param>
public void AssignSpecialFolder (ImapFolder folder)
{
if ((folder.Attributes & FolderAttributes.All) != 0)
All = folder;
if ((folder.Attributes & FolderAttributes.Archive) != 0)
Archive = folder;
if ((folder.Attributes & FolderAttributes.Drafts) != 0)
Drafts = folder;
if ((folder.Attributes & FolderAttributes.Flagged) != 0)
Flagged = folder;
if ((folder.Attributes & FolderAttributes.Important) != 0)
Important = folder;
if ((folder.Attributes & FolderAttributes.Junk) != 0)
Junk = folder;
if ((folder.Attributes & FolderAttributes.Sent) != 0)
Sent = folder;
if ((folder.Attributes & FolderAttributes.Trash) != 0)
Trash = folder;
}
/// <summary>
/// Assigns the special folders.
/// </summary>
/// <param name="list">The list of folders.</param>
public void AssignSpecialFolders (IList<ImapFolder> list)
{
for (int i = 0; i < list.Count; i++)
AssignSpecialFolder (list[i]);
}
/// <summary>
/// Queries the special folders.
/// </summary>
/// <param name="doAsync">Whether or not asynchronous IO methods should be used.</param>
/// <param name="cancellationToken">The cancellation token.</param>
public async Task QuerySpecialFoldersAsync (bool doAsync, CancellationToken cancellationToken)
{
var command = new StringBuilder ("LIST \"\" \"INBOX\"");
var list = new List<ImapFolder> ();
var returnsSubscribed = false;
ImapFolder folder;
ImapCommand ic;
if ((Capabilities & ImapCapabilities.ListExtended) != 0) {
command.Append (" RETURN (SUBSCRIBED CHILDREN)");
returnsSubscribed = true;
}
command.Append ("\r\n");
ic = new ImapCommand (this, cancellationToken, null, command.ToString ());
ic.RegisterUntaggedHandler ("LIST", ImapUtils.ParseFolderListAsync);
ic.ListReturnsSubscribed = returnsSubscribed;
ic.UserData = list;
QueueCommand (ic);
await RunAsync (ic, doAsync).ConfigureAwait (false);
GetCachedFolder ("INBOX", out folder);
Inbox = folder;
list.Clear ();
if ((Capabilities & ImapCapabilities.SpecialUse) != 0) {
// Note: Some IMAP servers like ProtonMail respond to SPECIAL-USE LIST queries with BAD, so fall
// back to just issuing a standard LIST command and hope we get back some SPECIAL-USE attributes.
//
// See https://github.com/jstedfast/MailKit/issues/674 for dertails.
returnsSubscribed = false;
command.Clear ();
command.Append ("LIST ");
if (QuirksMode != ImapQuirksMode.ProtonMail)
command.Append ("(SPECIAL-USE) \"\" \"*\"");
else
command.Append ("\"\" \"%%\"");
if ((Capabilities & ImapCapabilities.ListExtended) != 0) {
command.Append (" RETURN (SUBSCRIBED CHILDREN)");
returnsSubscribed = true;
}
command.Append ("\r\n");
ic = new ImapCommand (this, cancellationToken, null, command.ToString ());
ic.RegisterUntaggedHandler ("LIST", ImapUtils.ParseFolderListAsync);
ic.ListReturnsSubscribed = returnsSubscribed;
ic.UserData = list;
QueueCommand (ic);
await RunAsync (ic, doAsync).ConfigureAwait (false);
await LookupParentFoldersAsync (list, doAsync, cancellationToken).ConfigureAwait (false);
AssignSpecialFolders (list);
} else if ((Capabilities & ImapCapabilities.XList) != 0) {
ic = new ImapCommand (this, cancellationToken, null, "XLIST \"\" \"*\"\r\n");
ic.RegisterUntaggedHandler ("XLIST", ImapUtils.ParseFolderListAsync);
ic.UserData = list;
QueueCommand (ic);
await RunAsync (ic, doAsync).ConfigureAwait (false);
await LookupParentFoldersAsync (list, doAsync, cancellationToken).ConfigureAwait (false);
AssignSpecialFolders (list);
}
}
/// <summary>
/// Gets the folder representing the specified quota root.
/// </summary>
/// <returns>The folder.</returns>
/// <param name="quotaRoot">The name of the quota root.</param>
/// <param name="doAsync">Whether or not asynchronous IO methods should be used.</param>
/// <param name="cancellationToken">The cancellation token.</param>
public async Task<ImapFolder> GetQuotaRootFolderAsync (string quotaRoot, bool doAsync, CancellationToken cancellationToken)
{
ImapFolder folder;
if (GetCachedFolder (quotaRoot, out folder))
return folder;
var command = new StringBuilder ("LIST \"\" %S");
var list = new List<ImapFolder> ();
var returnsSubscribed = false;
if ((Capabilities & ImapCapabilities.ListExtended) != 0) {
command.Append (" RETURN (SUBSCRIBED CHILDREN)");
returnsSubscribed = true;
}
command.Append ("\r\n");
var ic = new ImapCommand (this, cancellationToken, null, command.ToString (), quotaRoot);
ic.RegisterUntaggedHandler ("LIST", ImapUtils.ParseFolderListAsync);
ic.ListReturnsSubscribed = returnsSubscribed;
ic.UserData = list;
QueueCommand (ic);
await RunAsync (ic, doAsync).ConfigureAwait (false);
if (ic.Response != ImapCommandResponse.Ok)
throw ImapCommandException.Create ("LIST", ic);
if ((folder = GetFolder (list, quotaRoot)) == null) {
folder = CreateImapFolder (quotaRoot, FolderAttributes.NonExistent, '.');
CacheFolder (folder);
return folder;
}
await LookupParentFoldersAsync (list, doAsync, cancellationToken).ConfigureAwait (false);
return folder;
}
/// <summary>
/// Gets the folder for the specified path.
/// </summary>
/// <returns>The folder.</returns>
/// <param name="path">The folder path.</param>
/// <param name="doAsync">Whether or not asynchronous IO methods should be used.</param>
/// <param name="cancellationToken">The cancellation token.</param>
public async Task<ImapFolder> GetFolderAsync (string path, bool doAsync, CancellationToken cancellationToken)
{
var encodedName = EncodeMailboxName (path);
ImapFolder folder;
if (GetCachedFolder (encodedName, out folder))
return folder;
var command = new StringBuilder ("LIST \"\" %S");
var list = new List<ImapFolder> ();
var returnsSubscribed = false;
if ((Capabilities & ImapCapabilities.ListExtended) != 0) {
command.Append (" RETURN (SUBSCRIBED CHILDREN)");
returnsSubscribed = true;
}
command.Append ("\r\n");
var ic = new ImapCommand (this, cancellationToken, null, command.ToString (), encodedName);
ic.RegisterUntaggedHandler ("LIST", ImapUtils.ParseFolderListAsync);
ic.ListReturnsSubscribed = returnsSubscribed;
ic.UserData = list;
QueueCommand (ic);
await RunAsync (ic, doAsync).ConfigureAwait (false);
if (ic.Response != ImapCommandResponse.Ok)
throw ImapCommandException.Create ("LIST", ic);
if ((folder = GetFolder (list, encodedName)) == null)
throw new FolderNotFoundException (path);
await LookupParentFoldersAsync (list, doAsync, cancellationToken).ConfigureAwait (false);
return folder;
}
internal string GetStatusQuery (StatusItems items)
{
var flags = string.Empty;
if ((items & StatusItems.Count) != 0)
flags += "MESSAGES ";
if ((items & StatusItems.Recent) != 0)
flags += "RECENT ";
if ((items & StatusItems.UidNext) != 0)
flags += "UIDNEXT ";
if ((items & StatusItems.UidValidity) != 0)
flags += "UIDVALIDITY ";
if ((items & StatusItems.Unread) != 0)
flags += "UNSEEN ";
if ((Capabilities & ImapCapabilities.CondStore) != 0) {
if ((items & StatusItems.HighestModSeq) != 0)
flags += "HIGHESTMODSEQ ";
}
// Note: If the IMAP server specifies a limit in the CAPABILITY response, then
// it seems we cannot expect to be able to query this in a STATUS command...
if ((Capabilities & ImapCapabilities.AppendLimit) != 0 && !AppendLimit.HasValue) {
if ((items & StatusItems.AppendLimit) != 0)
flags += "APPENDLIMIT ";
}
if ((Capabilities & ImapCapabilities.StatusSize) != 0) {
if ((items & StatusItems.Size) != 0)
flags += "SIZE ";
}
if ((Capabilities & ImapCapabilities.ObjectID) != 0) {
if ((items & StatusItems.MailboxId) != 0)
flags += "MAILBOXID ";
}
return flags.TrimEnd ();
}
/// <summary>
/// Get all of the folders within the specified namespace.
/// </summary>
/// <remarks>
/// Gets all of the folders within the specified namespace.
/// </remarks>
/// <returns>The list of folders.</returns>
/// <param name="namespace">The namespace.</param>
/// <param name="items">The status items to pre-populate.</param>
/// <param name="subscribedOnly">If set to <c>true</c>, only subscribed folders will be listed.</param>
/// <param name="doAsync">Whether or not asynchronous IO methods should be used.</param>
/// <param name="cancellationToken">The cancellation token.</param>
public async Task<IList<ImapFolder>> GetFoldersAsync (FolderNamespace @namespace, StatusItems items, bool subscribedOnly, bool doAsync, CancellationToken cancellationToken)
{
var encodedName = EncodeMailboxName (@namespace.Path);
var pattern = encodedName.Length > 0 ? encodedName + @namespace.DirectorySeparator : string.Empty;
var status = items != StatusItems.None;
var list = new List<ImapFolder> ();
var command = new StringBuilder ();
var returnsSubscribed = false;
var lsub = subscribedOnly;
ImapFolder folder;
if (!GetCachedFolder (encodedName, out folder))
throw new FolderNotFoundException (@namespace.Path);
if (subscribedOnly) {
if ((Capabilities & ImapCapabilities.ListExtended) != 0) {
command.Append ("LIST (SUBSCRIBED)");
returnsSubscribed = true;
lsub = false;
} else {
command.Append ("LSUB");
}
} else {
command.Append ("LIST");
}
command.Append (" \"\" %S");
if (!lsub) {
if (items != StatusItems.None && (Capabilities & ImapCapabilities.ListStatus) != 0) {
command.Append (" RETURN (");
if ((Capabilities & ImapCapabilities.ListExtended) != 0) {
if (!subscribedOnly) {
command.Append ("SUBSCRIBED ");
returnsSubscribed = true;
}
command.Append ("CHILDREN ");
}
command.AppendFormat ("STATUS ({0})", GetStatusQuery (items));
command.Append (')');
status = false;
} else if ((Capabilities & ImapCapabilities.ListExtended) != 0) {
command.Append (" RETURN (");
if (!subscribedOnly) {
command.Append ("SUBSCRIBED ");
returnsSubscribed = true;
}
command.Append ("CHILDREN");
command.Append (')');
}
}
command.Append ("\r\n");
var ic = new ImapCommand (this, cancellationToken, null, command.ToString (), pattern + "*");
ic.RegisterUntaggedHandler (lsub ? "LSUB" : "LIST", ImapUtils.ParseFolderListAsync);
ic.ListReturnsSubscribed = returnsSubscribed;
ic.UserData = list;
ic.Lsub = lsub;
QueueCommand (ic);
await RunAsync (ic, doAsync).ConfigureAwait (false);
if (ic.Response != ImapCommandResponse.Ok)
throw ImapCommandException.Create (lsub ? "LSUB" : "LIST", ic);
await LookupParentFoldersAsync (list, doAsync, cancellationToken).ConfigureAwait (false);
if (status) {
for (int i = 0; i < list.Count; i++) {
if (list[i].Exists)
await list[i].StatusAsync (items, doAsync, false, cancellationToken).ConfigureAwait (false);
}
}
return list;
}
/// <summary>
/// Decodes the name of the mailbox.
/// </summary>
/// <returns>The mailbox name.</returns>
/// <param name="encodedName">The encoded name.</param>
public string DecodeMailboxName (string encodedName)
{
return UTF8Enabled ? encodedName : ImapEncoding.Decode (encodedName);
}
/// <summary>
/// Encodes the name of the mailbox.
/// </summary>
/// <returns>The mailbox name.</returns>
/// <param name="mailboxName">The encoded mailbox name.</param>
public string EncodeMailboxName (string mailboxName)
{
return UTF8Enabled ? mailboxName : ImapEncoding.Encode (mailboxName);
}
/// <summary>
/// Determines whether the mailbox name is valid or not.
/// </summary>
/// <returns><c>true</c> if the mailbox name is valid; otherwise, <c>false</c>.</returns>
/// <param name="mailboxName">The mailbox name.</param>
/// <param name="delim">The path delimeter.</param>
public bool IsValidMailboxName (string mailboxName, char delim)
{
// From rfc6855:
//
// Mailbox names MUST comply with the Net-Unicode Definition ([RFC5198], Section 2)
// with the specific exception that they MUST NOT contain control characters
// (U+0000-U+001F and U+0080-U+009F), a delete character (U+007F), a line separator (U+2028),
// or a paragraph separator (U+2029).
for (int i = 0; i < mailboxName.Length; i++) {
char c = mailboxName[i];
if (c <= 0x1F || (c >= 0x80 && c <= 0x9F) || c == 0x7F || c == 0x2028 || c == 0x2029 || c == delim)
return false;
}
return mailboxName.Length > 0;
}
void InitializeParser (Stream stream, bool persistent)
{
if (parser == null)
parser = new MimeParser (ParserOptions.Default, stream, persistent);
else
parser.SetStream (ParserOptions.Default, stream, persistent);
}
public async Task<HeaderList> ParseHeadersAsync (Stream stream, bool doAsync, CancellationToken cancellationToken)
{
InitializeParser (stream, false);
if (doAsync)
return await parser.ParseHeadersAsync (cancellationToken).ConfigureAwait (false);
return parser.ParseHeaders (cancellationToken);
}
public async Task<MimeMessage> ParseMessageAsync (Stream stream, bool persistent, bool doAsync, CancellationToken cancellationToken)
{
InitializeParser (stream, persistent);
if (doAsync)
return await parser.ParseMessageAsync (cancellationToken).ConfigureAwait (false);
return parser.ParseMessage (cancellationToken);
}
public async Task<MimeEntity> ParseEntityAsync (Stream stream, bool persistent, bool doAsync, CancellationToken cancellationToken)
{
InitializeParser (stream, persistent);
if (doAsync)
return await parser.ParseEntityAsync (cancellationToken).ConfigureAwait (false);
return parser.ParseEntity (cancellationToken);
}
/// <summary>
/// Occurs when the engine receives an alert message from the server.
/// </summary>
public event EventHandler<AlertEventArgs> Alert;
internal void OnAlert (string message)
{
var handler = Alert;
if (handler != null)
handler (this, new AlertEventArgs (message));
}
/// <summary>
/// Occurs when the engine receives a notification that a folder has been created.
/// </summary>
public event EventHandler<FolderCreatedEventArgs> FolderCreated;
internal void OnFolderCreated (IMailFolder folder)
{
var handler = FolderCreated;
if (handler != null)
handler (this, new FolderCreatedEventArgs (folder));
}
/// <summary>
/// Occurs when the engine receives a notification that metadata has changed.
/// </summary>
public event EventHandler<MetadataChangedEventArgs> MetadataChanged;
internal void OnMetadataChanged (Metadata metadata)
{
var handler = MetadataChanged;
if (handler != null)
handler (this, new MetadataChangedEventArgs (metadata));
}
/// <summary>
/// Occurs when the engine receives a notification overflow message from the server.
/// </summary>
public event EventHandler<EventArgs> NotificationOverflow;
internal void OnNotificationOverflow ()
{
// [NOTIFICATIONOVERFLOW] will reset to NOTIFY NONE
NotifySelectedNewExpunge = false;
var handler = NotificationOverflow;
if (handler != null)
handler (this, EventArgs.Empty);
}
public event EventHandler<EventArgs> Disconnected;
void OnDisconnected ()
{
var handler = Disconnected;
if (handler != null)
handler (this, EventArgs.Empty);
}
/// <summary>
/// Releases all resource used by the <see cref="MailKit.Net.Imap.ImapEngine"/> object.
/// </summary>
/// <remarks>Call <see cref="Dispose"/> when you are finished using the <see cref="MailKit.Net.Imap.ImapEngine"/>. The
/// <see cref="Dispose"/> method leaves the <see cref="MailKit.Net.Imap.ImapEngine"/> in an unusable state. After
/// calling <see cref="Dispose"/>, you must release all references to the <see cref="MailKit.Net.Imap.ImapEngine"/> so
/// the garbage collector can reclaim the memory that the <see cref="MailKit.Net.Imap.ImapEngine"/> was occupying.</remarks>
public void Dispose ()
{
disposed = true;
Disconnect ();
}
}
}