1191 lines
39 KiB
C#
1191 lines
39 KiB
C#
//
|
|
// ImapStream.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.Net.Sockets;
|
|
using System.Globalization;
|
|
using System.Threading.Tasks;
|
|
|
|
using MimeKit.IO;
|
|
|
|
using Buffer = System.Buffer;
|
|
using SslStream = MailKit.Net.SslStream;
|
|
using NetworkStream = MailKit.Net.NetworkStream;
|
|
|
|
namespace MailKit.Net.Imap {
|
|
/// <summary>
|
|
/// An enumeration of the possible IMAP streaming modes.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Normal operation is done in the <see cref="ImapStreamMode.Token"/> mode,
|
|
/// but when reading literal string data, the
|
|
/// <see cref="ImapStreamMode.Literal"/> mode should be used.
|
|
/// </remarks>
|
|
enum ImapStreamMode {
|
|
/// <summary>
|
|
/// Reads 1 token at a time.
|
|
/// </summary>
|
|
Token,
|
|
|
|
/// <summary>
|
|
/// Reads literal string data.
|
|
/// </summary>
|
|
Literal
|
|
}
|
|
|
|
class ImapStream : Stream, ICancellableStream
|
|
{
|
|
public const string AtomSpecials = "(){%*\\\"\n";
|
|
public const string DefaultSpecials = "[]" + AtomSpecials;
|
|
const int ReadAheadSize = 128;
|
|
const int BlockSize = 4096;
|
|
const int PadSize = 4;
|
|
|
|
static readonly Encoding Latin1;
|
|
static readonly Encoding UTF8;
|
|
|
|
// I/O buffering
|
|
readonly byte[] input = new byte[ReadAheadSize + BlockSize + PadSize];
|
|
const int inputStart = ReadAheadSize;
|
|
int inputIndex = ReadAheadSize;
|
|
int inputEnd = ReadAheadSize;
|
|
|
|
readonly byte[] output = new byte[BlockSize];
|
|
int outputIndex;
|
|
|
|
readonly IProtocolLogger logger;
|
|
int literalDataLeft;
|
|
ImapToken nextToken;
|
|
bool disposed;
|
|
|
|
static ImapStream ()
|
|
{
|
|
UTF8 = Encoding.GetEncoding (65001, new EncoderExceptionFallback (), new DecoderExceptionFallback ());
|
|
|
|
try {
|
|
Latin1 = Encoding.GetEncoding (28591);
|
|
} catch (NotSupportedException) {
|
|
Latin1 = Encoding.GetEncoding (1252);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="MailKit.Net.Imap.ImapStream"/> class.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Creates a new <see cref="ImapStream"/>.
|
|
/// </remarks>
|
|
/// <param name="source">The underlying network stream.</param>
|
|
/// <param name="protocolLogger">The protocol logger.</param>
|
|
public ImapStream (Stream source, IProtocolLogger protocolLogger)
|
|
{
|
|
logger = protocolLogger;
|
|
IsConnected = true;
|
|
Stream = source;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get or sets the underlying network stream.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Gets or sets the underlying network stream.
|
|
/// </remarks>
|
|
/// <value>The underlying network stream.</value>
|
|
public Stream Stream {
|
|
get; internal set;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get or sets the mode used for reading.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Gets or sets the mode used for reading.
|
|
/// </remarks>
|
|
/// <value>The mode.</value>
|
|
public ImapStreamMode Mode {
|
|
get; set;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the length of the literal.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Gets the length of the literal.
|
|
/// </remarks>
|
|
/// <value>The length of the literal.</value>
|
|
public int LiteralLength {
|
|
get { return literalDataLeft; }
|
|
internal set { literalDataLeft = value; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get whether or not the stream is connected.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Gets whether or not the stream is connected.
|
|
/// </remarks>
|
|
/// <value><c>true</c> if the stream is connected; otherwise, <c>false</c>.</value>
|
|
public bool IsConnected {
|
|
get; private set;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get whether the stream supports reading.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Gets whether the stream supports reading.
|
|
/// </remarks>
|
|
/// <value><c>true</c> if the stream supports reading; otherwise, <c>false</c>.</value>
|
|
public override bool CanRead {
|
|
get { return Stream.CanRead; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get whether the stream supports writing.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Gets whether the stream supports writing.
|
|
/// </remarks>
|
|
/// <value><c>true</c> if the stream supports writing; otherwise, <c>false</c>.</value>
|
|
public override bool CanWrite {
|
|
get { return Stream.CanWrite; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get whether the stream supports seeking.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Gets whether the stream supports seeking.
|
|
/// </remarks>
|
|
/// <value><c>true</c> if the stream supports seeking; otherwise, <c>false</c>.</value>
|
|
public override bool CanSeek {
|
|
get { return false; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get whether the stream supports I/O timeouts.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Gets whether the stream supports I/O timeouts.
|
|
/// </remarks>
|
|
/// <value><c>true</c> if the stream supports I/O timeouts; otherwise, <c>false</c>.</value>
|
|
public override bool CanTimeout {
|
|
get { return Stream.CanTimeout; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get or set a value, in milliseconds, that determines how long the stream will attempt to read before timing out.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Gets or sets a value, in milliseconds, that determines how long the stream will attempt to read before timing out.
|
|
/// </remarks>
|
|
/// <returns>A value, in milliseconds, that determines how long the stream will attempt to read before timing out.</returns>
|
|
/// <value>The read timeout.</value>
|
|
public override int ReadTimeout {
|
|
get { return Stream.ReadTimeout; }
|
|
set { Stream.ReadTimeout = value; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get or set a value, in milliseconds, that determines how long the stream will attempt to write before timing out.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Gets or sets a value, in milliseconds, that determines how long the stream will attempt to write before timing out.
|
|
/// </remarks>
|
|
/// <returns>A value, in milliseconds, that determines how long the stream will attempt to write before timing out.</returns>
|
|
/// <value>The write timeout.</value>
|
|
public override int WriteTimeout {
|
|
get { return Stream.WriteTimeout; }
|
|
set { Stream.WriteTimeout = value; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get or set the position within the current stream.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Gets or sets the position within the current stream.
|
|
/// </remarks>
|
|
/// <returns>The current position within the stream.</returns>
|
|
/// <value>The position of the stream.</value>
|
|
/// <exception cref="System.IO.IOException">
|
|
/// An I/O error occurred.
|
|
/// </exception>
|
|
/// <exception cref="System.NotSupportedException">
|
|
/// The stream does not support seeking.
|
|
/// </exception>
|
|
/// <exception cref="System.ObjectDisposedException">
|
|
/// The stream has been disposed.
|
|
/// </exception>
|
|
public override long Position {
|
|
get { return Stream.Position; }
|
|
set { throw new NotSupportedException (); }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the length of the stream, in bytes.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Gets the length of the stream, in bytes.
|
|
/// </remarks>
|
|
/// <returns>A long value representing the length of the stream in bytes.</returns>
|
|
/// <value>The length of the stream.</value>
|
|
/// <exception cref="System.NotSupportedException">
|
|
/// The stream does not support seeking.
|
|
/// </exception>
|
|
/// <exception cref="System.ObjectDisposedException">
|
|
/// The stream has been disposed.
|
|
/// </exception>
|
|
public override long Length {
|
|
get { return Stream.Length; }
|
|
}
|
|
|
|
async Task<int> ReadAheadAsync (int atleast, bool doAsync, CancellationToken cancellationToken)
|
|
{
|
|
int left = inputEnd - inputIndex;
|
|
|
|
if (left >= atleast)
|
|
return left;
|
|
|
|
int start = inputStart;
|
|
int end = inputEnd;
|
|
int nread;
|
|
|
|
if (left > 0) {
|
|
int index = inputIndex;
|
|
|
|
// attempt to align the end of the remaining input with ReadAheadSize
|
|
if (index >= start) {
|
|
start -= Math.Min (ReadAheadSize, left);
|
|
Buffer.BlockCopy (input, index, input, start, left);
|
|
index = start;
|
|
start += left;
|
|
} else if (index > 0) {
|
|
int shift = Math.Min (index, end - start);
|
|
Buffer.BlockCopy (input, index, input, index - shift, left);
|
|
index -= shift;
|
|
start = index + left;
|
|
} else {
|
|
// we can't shift...
|
|
start = end;
|
|
}
|
|
|
|
inputIndex = index;
|
|
inputEnd = start;
|
|
} else {
|
|
inputIndex = start;
|
|
inputEnd = start;
|
|
}
|
|
|
|
end = input.Length - PadSize;
|
|
|
|
try {
|
|
var network = Stream as NetworkStream;
|
|
|
|
cancellationToken.ThrowIfCancellationRequested ();
|
|
|
|
if (doAsync) {
|
|
nread = await Stream.ReadAsync (input, start, end - start, cancellationToken).ConfigureAwait (false);
|
|
} else {
|
|
network?.Poll (SelectMode.SelectRead, cancellationToken);
|
|
nread = Stream.Read (input, start, end - start);
|
|
}
|
|
|
|
if (nread > 0) {
|
|
logger.LogServer (input, start, nread);
|
|
inputEnd += nread;
|
|
} else {
|
|
throw new ImapProtocolException ("The IMAP server has unexpectedly disconnected.");
|
|
}
|
|
|
|
if (network == null)
|
|
cancellationToken.ThrowIfCancellationRequested ();
|
|
} catch {
|
|
IsConnected = false;
|
|
throw;
|
|
}
|
|
|
|
return inputEnd - inputIndex;
|
|
}
|
|
|
|
static void ValidateArguments (byte[] buffer, int offset, int count)
|
|
{
|
|
if (buffer == null)
|
|
throw new ArgumentNullException (nameof (buffer));
|
|
|
|
if (offset < 0 || offset > buffer.Length)
|
|
throw new ArgumentOutOfRangeException (nameof (offset));
|
|
|
|
if (count < 0 || count > (buffer.Length - offset))
|
|
throw new ArgumentOutOfRangeException (nameof (count));
|
|
}
|
|
|
|
void CheckDisposed ()
|
|
{
|
|
if (disposed)
|
|
throw new ObjectDisposedException (nameof (ImapStream));
|
|
}
|
|
|
|
async Task<int> ReadAsync (byte[] buffer, int offset, int count, bool doAsync, CancellationToken cancellationToken)
|
|
{
|
|
CheckDisposed ();
|
|
|
|
ValidateArguments (buffer, offset, count);
|
|
|
|
if (Mode != ImapStreamMode.Literal)
|
|
return 0;
|
|
|
|
count = Math.Min (count, literalDataLeft);
|
|
|
|
int length = inputEnd - inputIndex;
|
|
int n;
|
|
|
|
if (length < count && length <= ReadAheadSize)
|
|
await ReadAheadAsync (BlockSize, doAsync, cancellationToken).ConfigureAwait (false);
|
|
|
|
length = inputEnd - inputIndex;
|
|
n = Math.Min (count, length);
|
|
|
|
Buffer.BlockCopy (input, inputIndex, buffer, offset, n);
|
|
literalDataLeft -= n;
|
|
inputIndex += n;
|
|
|
|
if (literalDataLeft == 0)
|
|
Mode = ImapStreamMode.Token;
|
|
|
|
return n;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reads a sequence of bytes from the stream and advances the position
|
|
/// within the stream by the number of bytes read.
|
|
/// </summary>
|
|
/// <returns>The total number of bytes read into the buffer. This can be less than the number of bytes requested if that many
|
|
/// bytes are not currently available, or zero (0) if the end of the stream has been reached.</returns>
|
|
/// <param name="buffer">The buffer.</param>
|
|
/// <param name="offset">The buffer offset.</param>
|
|
/// <param name="count">The number of bytes to read.</param>
|
|
/// <param name="cancellationToken">The cancellation token.</param>
|
|
/// <exception cref="System.ArgumentNullException">
|
|
/// <paramref name="buffer"/> is <c>null</c>.
|
|
/// </exception>
|
|
/// <exception cref="System.ArgumentOutOfRangeException">
|
|
/// <para><paramref name="offset"/> is less than zero or greater than the length of <paramref name="buffer"/>.</para>
|
|
/// <para>-or-</para>
|
|
/// <para>The <paramref name="buffer"/> is not large enough to contain <paramref name="count"/> bytes strting
|
|
/// at the specified <paramref name="offset"/>.</para>
|
|
/// </exception>
|
|
/// <exception cref="System.ObjectDisposedException">
|
|
/// The stream has been disposed.
|
|
/// </exception>
|
|
/// <exception cref="System.InvalidOperationException">
|
|
/// The stream is in token mode (see <see cref="ImapStreamMode.Token"/>).
|
|
/// </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 int Read (byte[] buffer, int offset, int count, CancellationToken cancellationToken)
|
|
{
|
|
return ReadAsync (buffer, offset, count, false, cancellationToken).GetAwaiter ().GetResult ();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reads a sequence of bytes from the stream and advances the position
|
|
/// within the stream by the number of bytes read.
|
|
/// </summary>
|
|
/// <returns>The total number of bytes read into the buffer. This can be less than the number of bytes requested if that many
|
|
/// bytes are not currently available, or zero (0) if the end of the stream has been reached.</returns>
|
|
/// <param name="buffer">The buffer.</param>
|
|
/// <param name="offset">The buffer offset.</param>
|
|
/// <param name="count">The number of bytes to read.</param>
|
|
/// <exception cref="System.ArgumentNullException">
|
|
/// <paramref name="buffer"/> is <c>null</c>.
|
|
/// </exception>
|
|
/// <exception cref="System.ArgumentOutOfRangeException">
|
|
/// <para><paramref name="offset"/> is less than zero or greater than the length of <paramref name="buffer"/>.</para>
|
|
/// <para>-or-</para>
|
|
/// <para>The <paramref name="buffer"/> is not large enough to contain <paramref name="count"/> bytes strting
|
|
/// at the specified <paramref name="offset"/>.</para>
|
|
/// </exception>
|
|
/// <exception cref="System.ObjectDisposedException">
|
|
/// The stream has been disposed.
|
|
/// </exception>
|
|
/// <exception cref="System.InvalidOperationException">
|
|
/// The stream is in token mode (see <see cref="ImapStreamMode.Token"/>).
|
|
/// </exception>
|
|
/// <exception cref="System.IO.IOException">
|
|
/// An I/O error occurred.
|
|
/// </exception>
|
|
public override int Read (byte[] buffer, int offset, int count)
|
|
{
|
|
return Read (buffer, offset, count, CancellationToken.None);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reads a sequence of bytes from the stream and advances the position
|
|
/// within the stream by the number of bytes read.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Reads a sequence of bytes from the stream and advances the position
|
|
/// within the stream by the number of bytes read.
|
|
/// </remarks>
|
|
/// <returns>The total number of bytes read into the buffer. This can be less than the number of bytes requested if that many
|
|
/// bytes are not currently available, or zero (0) if the end of the stream has been reached.</returns>
|
|
/// <param name="buffer">The buffer.</param>
|
|
/// <param name="offset">The buffer offset.</param>
|
|
/// <param name="count">The number of bytes to read.</param>
|
|
/// <param name="cancellationToken">The cancellation token.</param>
|
|
/// <exception cref="System.ArgumentNullException">
|
|
/// <paramref name="buffer"/> is <c>null</c>.
|
|
/// </exception>
|
|
/// <exception cref="System.ArgumentOutOfRangeException">
|
|
/// <para><paramref name="offset"/> is less than zero or greater than the length of <paramref name="buffer"/>.</para>
|
|
/// <para>-or-</para>
|
|
/// <para>The <paramref name="buffer"/> is not large enough to contain <paramref name="count"/> bytes strting
|
|
/// at the specified <paramref name="offset"/>.</para>
|
|
/// </exception>
|
|
/// <exception cref="System.ObjectDisposedException">
|
|
/// The stream has been disposed.
|
|
/// </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 override Task<int> ReadAsync (byte[] buffer, int offset, int count, CancellationToken cancellationToken)
|
|
{
|
|
return ReadAsync (buffer, offset, count, true, cancellationToken);
|
|
}
|
|
|
|
static bool IsAtom (byte c, string specials)
|
|
{
|
|
return !IsCtrl (c) && !IsWhiteSpace (c) && specials.IndexOf ((char) c) == -1;
|
|
}
|
|
|
|
static bool IsCtrl (byte c)
|
|
{
|
|
return c <= 0x1f || c == 0x7f;
|
|
}
|
|
|
|
static bool IsWhiteSpace (byte c)
|
|
{
|
|
return c == (byte) ' ' || c == (byte) '\t' || c == (byte) '\r';
|
|
}
|
|
|
|
async Task<ImapToken> ReadQuotedStringTokenAsync (bool doAsync, CancellationToken cancellationToken)
|
|
{
|
|
bool escaped = false;
|
|
|
|
// skip over the opening '"'
|
|
inputIndex++;
|
|
|
|
using (var memory = new MemoryStream ()) {
|
|
do {
|
|
while (inputIndex < inputEnd) {
|
|
if (input[inputIndex] == (byte) '"' && !escaped)
|
|
break;
|
|
|
|
if (input[inputIndex] == (byte) '\\' && !escaped) {
|
|
escaped = true;
|
|
} else {
|
|
memory.WriteByte (input[inputIndex]);
|
|
escaped = false;
|
|
}
|
|
|
|
inputIndex++;
|
|
}
|
|
|
|
if (inputIndex + 1 < inputEnd) {
|
|
// skip over closing '"'
|
|
inputIndex++;
|
|
|
|
// Note: Some IMAP servers do not properly escape double-quotes inside
|
|
// of a qstring token and so, as an attempt at working around this
|
|
// problem, check that the closing '"' character is not immediately
|
|
// followed by any character that we would expect immediately following
|
|
// a qstring token.
|
|
//
|
|
// See https://github.com/jstedfast/MailKit/issues/485 for details.
|
|
if ("]) \r\n".IndexOf ((char) input[inputIndex]) != -1)
|
|
break;
|
|
|
|
memory.WriteByte ((byte) '"');
|
|
continue;
|
|
}
|
|
|
|
await ReadAheadAsync (2, doAsync, cancellationToken).ConfigureAwait (false);
|
|
} while (true);
|
|
|
|
#if !NETSTANDARD1_3 && !NETSTANDARD1_6
|
|
var buffer = memory.GetBuffer ();
|
|
#else
|
|
var buffer = memory.ToArray ();
|
|
#endif
|
|
int length = (int) memory.Length;
|
|
|
|
return new ImapToken (ImapTokenType.QString, Encoding.UTF8.GetString (buffer, 0, length));
|
|
}
|
|
}
|
|
|
|
async Task<string> ReadAtomStringAsync (bool flag, string specials, bool doAsync, CancellationToken cancellationToken)
|
|
{
|
|
using (var memory = new MemoryStream ()) {
|
|
do {
|
|
input[inputEnd] = (byte) '\n';
|
|
|
|
if (flag && memory.Length == 0 && input[inputIndex] == (byte) '*') {
|
|
// this is a special wildcard flag
|
|
inputIndex++;
|
|
return "*";
|
|
}
|
|
|
|
while (IsAtom (input[inputIndex], specials))
|
|
memory.WriteByte (input[inputIndex++]);
|
|
|
|
if (inputIndex < inputEnd)
|
|
break;
|
|
|
|
await ReadAheadAsync (1, doAsync, cancellationToken).ConfigureAwait (false);
|
|
} while (true);
|
|
|
|
var count = (int) memory.Length;
|
|
#if !NETSTANDARD1_3 && !NETSTANDARD1_6
|
|
var buf = memory.GetBuffer ();
|
|
#else
|
|
var buf = memory.ToArray ();
|
|
#endif
|
|
|
|
try {
|
|
return UTF8.GetString (buf, 0, count);
|
|
} catch (DecoderFallbackException) {
|
|
return Latin1.GetString (buf, 0, count);
|
|
}
|
|
}
|
|
}
|
|
|
|
async Task<ImapToken> ReadAtomTokenAsync (string specials, bool doAsync, CancellationToken cancellationToken)
|
|
{
|
|
var atom = await ReadAtomStringAsync (false, specials, doAsync, cancellationToken).ConfigureAwait (false);
|
|
|
|
return atom == "NIL" ? new ImapToken (ImapTokenType.Nil, atom) : new ImapToken (ImapTokenType.Atom, atom);
|
|
}
|
|
|
|
async Task<ImapToken> ReadFlagTokenAsync (string specials, bool doAsync, CancellationToken cancellationToken)
|
|
{
|
|
inputIndex++;
|
|
|
|
var flag = "\\" + await ReadAtomStringAsync (true, specials, doAsync, cancellationToken).ConfigureAwait (false);
|
|
|
|
return new ImapToken (ImapTokenType.Flag, flag);
|
|
}
|
|
|
|
async Task<ImapToken> ReadLiteralTokenAsync (bool doAsync, CancellationToken cancellationToken)
|
|
{
|
|
var builder = new StringBuilder ();
|
|
|
|
// skip over the '{'
|
|
inputIndex++;
|
|
|
|
do {
|
|
input[inputEnd] = (byte) '}';
|
|
|
|
while (input[inputIndex] != (byte) '}' && input[inputIndex] != '+')
|
|
builder.Append ((char) input[inputIndex++]);
|
|
|
|
if (inputIndex < inputEnd)
|
|
break;
|
|
|
|
await ReadAheadAsync (1, doAsync, cancellationToken).ConfigureAwait (false);
|
|
} while (true);
|
|
|
|
if (input[inputIndex] == (byte) '+')
|
|
inputIndex++;
|
|
|
|
// technically, we need "}\r\n", but in order to be more lenient, we'll accept "}\n"
|
|
await ReadAheadAsync (2, doAsync, cancellationToken).ConfigureAwait (false);
|
|
|
|
if (input[inputIndex] != (byte) '}') {
|
|
// PROTOCOL ERROR... but maybe we can work around it?
|
|
do {
|
|
input[inputEnd] = (byte) '}';
|
|
|
|
while (input[inputIndex] != (byte) '}')
|
|
inputIndex++;
|
|
|
|
if (inputIndex < inputEnd)
|
|
break;
|
|
|
|
await ReadAheadAsync (1, doAsync, cancellationToken).ConfigureAwait (false);
|
|
} while (true);
|
|
}
|
|
|
|
// skip over the '}'
|
|
inputIndex++;
|
|
|
|
// read until we get a new line...
|
|
do {
|
|
input[inputEnd] = (byte) '\n';
|
|
|
|
while (input[inputIndex] != (byte) '\n')
|
|
inputIndex++;
|
|
|
|
if (inputIndex < inputEnd)
|
|
break;
|
|
|
|
await ReadAheadAsync (1, doAsync, cancellationToken).ConfigureAwait (false);
|
|
} while (true);
|
|
|
|
// skip over the '\n'
|
|
inputIndex++;
|
|
|
|
if (!int.TryParse (builder.ToString (), NumberStyles.None, CultureInfo.InvariantCulture, out literalDataLeft) || literalDataLeft < 0)
|
|
return new ImapToken (ImapTokenType.Error, builder.ToString ());
|
|
|
|
Mode = ImapStreamMode.Literal;
|
|
|
|
return new ImapToken (ImapTokenType.Literal, literalDataLeft);
|
|
}
|
|
|
|
internal async Task<ImapToken> ReadTokenAsync (string specials, bool doAsync, CancellationToken cancellationToken)
|
|
{
|
|
CheckDisposed ();
|
|
|
|
if (nextToken != null) {
|
|
var token = nextToken;
|
|
nextToken = null;
|
|
return token;
|
|
}
|
|
|
|
input[inputEnd] = (byte) '\n';
|
|
|
|
// skip over white space between tokens...
|
|
do {
|
|
while (IsWhiteSpace (input[inputIndex]))
|
|
inputIndex++;
|
|
|
|
if (inputIndex < inputEnd)
|
|
break;
|
|
|
|
await ReadAheadAsync (1, doAsync, cancellationToken).ConfigureAwait (false);
|
|
|
|
input[inputEnd] = (byte) '\n';
|
|
} while (true);
|
|
|
|
char c = (char) input[inputIndex];
|
|
|
|
if (c == '"')
|
|
return await ReadQuotedStringTokenAsync (doAsync, cancellationToken).ConfigureAwait (false);
|
|
|
|
if (c == '{')
|
|
return await ReadLiteralTokenAsync (doAsync, cancellationToken).ConfigureAwait (false);
|
|
|
|
if (c == '\\')
|
|
return await ReadFlagTokenAsync (specials, doAsync, cancellationToken).ConfigureAwait (false);
|
|
|
|
if (IsAtom (input[inputIndex], specials))
|
|
return await ReadAtomTokenAsync (specials, doAsync, cancellationToken).ConfigureAwait (false);
|
|
|
|
// special character token
|
|
inputIndex++;
|
|
|
|
return new ImapToken ((ImapTokenType) c, c);
|
|
}
|
|
|
|
internal Task<ImapToken> ReadTokenAsync (bool doAsync, CancellationToken cancellationToken)
|
|
{
|
|
return ReadTokenAsync (DefaultSpecials, doAsync, cancellationToken);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reads the next available token from the stream.
|
|
/// </summary>
|
|
/// <returns>The token.</returns>
|
|
/// <param name="cancellationToken">The cancellation token.</param>
|
|
/// <exception cref="System.ObjectDisposedException">
|
|
/// The stream has been disposed.
|
|
/// </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 ImapToken ReadToken (CancellationToken cancellationToken)
|
|
{
|
|
return ReadTokenAsync (false, cancellationToken).GetAwaiter ().GetResult ();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Asynchronously reads the next available token from the stream.
|
|
/// </summary>
|
|
/// <returns>The token.</returns>
|
|
/// <param name="cancellationToken">The cancellation token.</param>
|
|
/// <exception cref="System.ObjectDisposedException">
|
|
/// The stream has been disposed.
|
|
/// </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<ImapToken> ReadTokenAsync (CancellationToken cancellationToken)
|
|
{
|
|
return ReadTokenAsync (true, cancellationToken);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Ungets a token.
|
|
/// </summary>
|
|
/// <param name="token">The token.</param>
|
|
public void UngetToken (ImapToken token)
|
|
{
|
|
if (token == null)
|
|
throw new ArgumentNullException (nameof (token));
|
|
|
|
nextToken = token;
|
|
}
|
|
|
|
async Task<bool> ReadLineAsync (Stream ostream, bool doAsync, CancellationToken cancellationToken)
|
|
{
|
|
CheckDisposed ();
|
|
|
|
if (inputIndex == inputEnd)
|
|
await ReadAheadAsync (1, doAsync, cancellationToken).ConfigureAwait (false);
|
|
|
|
unsafe {
|
|
fixed (byte* inbuf = input) {
|
|
byte* start, inptr, inend;
|
|
int offset = inputIndex;
|
|
int count;
|
|
|
|
start = inbuf + inputIndex;
|
|
inend = inbuf + inputEnd;
|
|
*inend = (byte) '\n';
|
|
inptr = start;
|
|
|
|
// FIXME: use SIMD to optimize this
|
|
while (*inptr != (byte) '\n')
|
|
inptr++;
|
|
|
|
inputIndex = (int) (inptr - inbuf);
|
|
count = (int) (inptr - start);
|
|
|
|
if (inptr == inend) {
|
|
ostream.Write (input, offset, count);
|
|
return false;
|
|
}
|
|
|
|
// consume the '\n'
|
|
inputIndex++;
|
|
count++;
|
|
|
|
ostream.Write (input, offset, count);
|
|
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reads a single line of input from the stream.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This method should be called in a loop until it returns <c>true</c>.
|
|
/// </remarks>
|
|
/// <returns><c>true</c>, if reading the line is complete, <c>false</c> otherwise.</returns>
|
|
/// <param name="ostream">The output stream to write the line data into.</param>
|
|
/// <param name="cancellationToken">The cancellation token.</param>
|
|
/// <exception cref="System.ObjectDisposedException">
|
|
/// The stream has been disposed.
|
|
/// </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>
|
|
internal bool ReadLine (Stream ostream, CancellationToken cancellationToken)
|
|
{
|
|
return ReadLineAsync (ostream, false, cancellationToken).GetAwaiter ().GetResult ();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Asynchronously reads a single line of input from the stream.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This method should be called in a loop until it returns <c>true</c>.
|
|
/// </remarks>
|
|
/// <returns><c>true</c>, if reading the line is complete, <c>false</c> otherwise.</returns>
|
|
/// <param name="ostream">The output stream to write the line data into.</param>
|
|
/// <param name="cancellationToken">The cancellation token.</param>
|
|
/// <exception cref="System.ObjectDisposedException">
|
|
/// The stream has been disposed.
|
|
/// </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>
|
|
internal Task<bool> ReadLineAsync (Stream ostream, CancellationToken cancellationToken)
|
|
{
|
|
return ReadLineAsync (ostream, true, cancellationToken);
|
|
}
|
|
|
|
async Task WriteAsync (byte[] buffer, int offset, int count, bool doAsync, CancellationToken cancellationToken)
|
|
{
|
|
CheckDisposed ();
|
|
|
|
ValidateArguments (buffer, offset, count);
|
|
|
|
try {
|
|
var network = NetworkStream.Get (Stream);
|
|
int index = offset;
|
|
int left = count;
|
|
|
|
while (left > 0) {
|
|
int n = Math.Min (BlockSize - outputIndex, left);
|
|
|
|
if (outputIndex > 0 || n < BlockSize) {
|
|
// append the data to the output buffer
|
|
Buffer.BlockCopy (buffer, index, output, outputIndex, n);
|
|
outputIndex += n;
|
|
index += n;
|
|
left -= n;
|
|
}
|
|
|
|
if (outputIndex == BlockSize) {
|
|
// flush the output buffer
|
|
if (doAsync) {
|
|
await Stream.WriteAsync (output, 0, BlockSize, cancellationToken).ConfigureAwait (false);
|
|
} else {
|
|
network?.Poll (SelectMode.SelectWrite, cancellationToken);
|
|
Stream.Write (output, 0, BlockSize);
|
|
}
|
|
logger.LogClient (output, 0, BlockSize);
|
|
outputIndex = 0;
|
|
}
|
|
|
|
if (outputIndex == 0) {
|
|
// write blocks of data to the stream without buffering
|
|
while (left >= BlockSize) {
|
|
if (doAsync) {
|
|
await Stream.WriteAsync (buffer, index, BlockSize, cancellationToken).ConfigureAwait (false);
|
|
} else {
|
|
network?.Poll (SelectMode.SelectWrite, cancellationToken);
|
|
Stream.Write (buffer, index, BlockSize);
|
|
}
|
|
logger.LogClient (buffer, index, BlockSize);
|
|
index += BlockSize;
|
|
left -= BlockSize;
|
|
}
|
|
}
|
|
}
|
|
} catch (Exception ex) {
|
|
IsConnected = false;
|
|
if (!(ex is OperationCanceledException))
|
|
cancellationToken.ThrowIfCancellationRequested ();
|
|
throw;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes a sequence of bytes to the stream and advances the current
|
|
/// position within this stream by the number of bytes written.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Writes a sequence of bytes to the stream and advances the current
|
|
/// position within this stream by the number of bytes written.
|
|
/// </remarks>
|
|
/// <param name='buffer'>The buffer to write.</param>
|
|
/// <param name='offset'>The offset of the first byte to write.</param>
|
|
/// <param name='count'>The number of bytes to write.</param>
|
|
/// <param name="cancellationToken">The cancellation token.</param>
|
|
/// <exception cref="System.ArgumentNullException">
|
|
/// <paramref name="buffer"/> is <c>null</c>.
|
|
/// </exception>
|
|
/// <exception cref="System.ArgumentOutOfRangeException">
|
|
/// <para><paramref name="offset"/> is less than zero or greater than the length of <paramref name="buffer"/>.</para>
|
|
/// <para>-or-</para>
|
|
/// <para>The <paramref name="buffer"/> is not large enough to contain <paramref name="count"/> bytes strting
|
|
/// at the specified <paramref name="offset"/>.</para>
|
|
/// </exception>
|
|
/// <exception cref="System.ObjectDisposedException">
|
|
/// The stream has been disposed.
|
|
/// </exception>
|
|
/// <exception cref="System.NotSupportedException">
|
|
/// The stream does not support writing.
|
|
/// </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 void Write (byte[] buffer, int offset, int count, CancellationToken cancellationToken)
|
|
{
|
|
WriteAsync (buffer, offset, count, false, cancellationToken).GetAwaiter ().GetResult ();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes a sequence of bytes to the stream and advances the current
|
|
/// position within this stream by the number of bytes written.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Writes a sequence of bytes to the stream and advances the current
|
|
/// position within this stream by the number of bytes written.
|
|
/// </remarks>
|
|
/// <param name='buffer'>The buffer to write.</param>
|
|
/// <param name='offset'>The offset of the first byte to write.</param>
|
|
/// <param name='count'>The number of bytes to write.</param>
|
|
/// <exception cref="System.ArgumentNullException">
|
|
/// <paramref name="buffer"/> is <c>null</c>.
|
|
/// </exception>
|
|
/// <exception cref="System.ArgumentOutOfRangeException">
|
|
/// <para><paramref name="offset"/> is less than zero or greater than the length of <paramref name="buffer"/>.</para>
|
|
/// <para>-or-</para>
|
|
/// <para>The <paramref name="buffer"/> is not large enough to contain <paramref name="count"/> bytes strting
|
|
/// at the specified <paramref name="offset"/>.</para>
|
|
/// </exception>
|
|
/// <exception cref="System.ObjectDisposedException">
|
|
/// The stream has been disposed.
|
|
/// </exception>
|
|
/// <exception cref="System.NotSupportedException">
|
|
/// The stream does not support writing.
|
|
/// </exception>
|
|
/// <exception cref="System.IO.IOException">
|
|
/// An I/O error occurred.
|
|
/// </exception>
|
|
public override void Write (byte[] buffer, int offset, int count)
|
|
{
|
|
Write (buffer, offset, count, CancellationToken.None);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes a sequence of bytes to the stream and advances the current
|
|
/// position within this stream by the number of bytes written.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Writes a sequence of bytes to the stream and advances the current
|
|
/// position within this stream by the number of bytes written.
|
|
/// </remarks>
|
|
/// <returns>A task that represents the asynchronous write operation.</returns>
|
|
/// <param name='buffer'>The buffer to write.</param>
|
|
/// <param name='offset'>The offset of the first byte to write.</param>
|
|
/// <param name='count'>The number of bytes to write.</param>
|
|
/// <param name="cancellationToken">The cancellation token.</param>
|
|
/// <exception cref="System.ArgumentNullException">
|
|
/// <paramref name="buffer"/> is <c>null</c>.
|
|
/// </exception>
|
|
/// <exception cref="System.ArgumentOutOfRangeException">
|
|
/// <para><paramref name="offset"/> is less than zero or greater than the length of <paramref name="buffer"/>.</para>
|
|
/// <para>-or-</para>
|
|
/// <para>The <paramref name="buffer"/> is not large enough to contain <paramref name="count"/> bytes strting
|
|
/// at the specified <paramref name="offset"/>.</para>
|
|
/// </exception>
|
|
/// <exception cref="System.ObjectDisposedException">
|
|
/// The stream has been disposed.
|
|
/// </exception>
|
|
/// <exception cref="System.NotSupportedException">
|
|
/// The stream does not support writing.
|
|
/// </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 override Task WriteAsync (byte[] buffer, int offset, int count, CancellationToken cancellationToken)
|
|
{
|
|
return WriteAsync (buffer, offset, count, true, cancellationToken);
|
|
}
|
|
|
|
async Task FlushAsync (bool doAsync, CancellationToken cancellationToken)
|
|
{
|
|
CheckDisposed ();
|
|
|
|
if (outputIndex == 0)
|
|
return;
|
|
|
|
try {
|
|
if (doAsync) {
|
|
await Stream.WriteAsync (output, 0, outputIndex, cancellationToken).ConfigureAwait (false);
|
|
await Stream.FlushAsync (cancellationToken).ConfigureAwait (false);
|
|
} else {
|
|
var network = NetworkStream.Get (Stream);
|
|
|
|
network?.Poll (SelectMode.SelectWrite, cancellationToken);
|
|
Stream.Write (output, 0, outputIndex);
|
|
Stream.Flush ();
|
|
}
|
|
logger.LogClient (output, 0, outputIndex);
|
|
outputIndex = 0;
|
|
} catch (Exception ex) {
|
|
IsConnected = false;
|
|
if (!(ex is OperationCanceledException))
|
|
cancellationToken.ThrowIfCancellationRequested ();
|
|
throw;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Clears all output buffers for this stream and causes any buffered data to be written
|
|
/// to the underlying device.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Clears all output buffers for this stream and causes any buffered data to be written
|
|
/// to the underlying device.
|
|
/// </remarks>
|
|
/// <param name="cancellationToken">The cancellation token.</param>
|
|
/// <exception cref="System.ObjectDisposedException">
|
|
/// The stream has been disposed.
|
|
/// </exception>
|
|
/// <exception cref="System.NotSupportedException">
|
|
/// The stream does not support writing.
|
|
/// </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 void Flush (CancellationToken cancellationToken)
|
|
{
|
|
FlushAsync (false, cancellationToken).GetAwaiter ().GetResult ();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Clears all output buffers for this stream and causes any buffered data to be written
|
|
/// to the underlying device.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Clears all output buffers for this stream and causes any buffered data to be written
|
|
/// to the underlying device.
|
|
/// </remarks>
|
|
/// <exception cref="System.ObjectDisposedException">
|
|
/// The stream has been disposed.
|
|
/// </exception>
|
|
/// <exception cref="System.NotSupportedException">
|
|
/// The stream does not support writing.
|
|
/// </exception>
|
|
/// <exception cref="System.IO.IOException">
|
|
/// An I/O error occurred.
|
|
/// </exception>
|
|
public override void Flush ()
|
|
{
|
|
Flush (CancellationToken.None);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Clears all buffers for this stream and causes any buffered data to be written
|
|
/// to the underlying device.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Clears all buffers for this stream and causes any buffered data to be written
|
|
/// to the underlying device.
|
|
/// </remarks>
|
|
/// <returns>A task that represents the asynchronous flush operation.</returns>
|
|
/// <param name="cancellationToken">The cancellation token.</param>
|
|
/// <exception cref="System.ObjectDisposedException">
|
|
/// The stream has been disposed.
|
|
/// </exception>
|
|
/// <exception cref="System.NotSupportedException">
|
|
/// The stream does not support writing.
|
|
/// </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 override Task FlushAsync (CancellationToken cancellationToken)
|
|
{
|
|
return FlushAsync (true, cancellationToken);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets the position within the current stream.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// It is not possible to seek within a <see cref="ImapStream"/>.
|
|
/// </remarks>
|
|
/// <returns>The new position within the stream.</returns>
|
|
/// <param name="offset">The offset into the stream relative to the <paramref name="origin"/>.</param>
|
|
/// <param name="origin">The origin to seek from.</param>
|
|
/// <exception cref="System.NotSupportedException">
|
|
/// The stream does not support seeking.
|
|
/// </exception>
|
|
public override long Seek (long offset, SeekOrigin origin)
|
|
{
|
|
throw new NotSupportedException ();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets the length of the stream.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// It is not possible to set the length of a <see cref="ImapStream"/>.
|
|
/// </remarks>
|
|
/// <param name="value">The desired length of the stream in bytes.</param>
|
|
/// <exception cref="System.NotSupportedException">
|
|
/// The stream does not support setting the length.
|
|
/// </exception>
|
|
public override void SetLength (long value)
|
|
{
|
|
throw new NotSupportedException ();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Releases the unmanaged resources used by the <see cref="ImapStream"/> and
|
|
/// optionally releases the managed resources.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Releases the unmanaged resources used by the <see cref="ImapStream"/> and
|
|
/// optionally releases the managed resources.
|
|
/// </remarks>
|
|
/// <param name="disposing"><c>true</c> to release both managed and unmanaged resources;
|
|
/// <c>false</c> to release only the unmanaged resources.</param>
|
|
protected override void Dispose (bool disposing)
|
|
{
|
|
if (disposing && !disposed) {
|
|
IsConnected = false;
|
|
Stream.Dispose ();
|
|
}
|
|
|
|
disposed = true;
|
|
|
|
base.Dispose (disposing);
|
|
}
|
|
}
|
|
}
|