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