Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Resilience Features #213

Open
wants to merge 3 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 24 additions & 6 deletions Src/SmtpServer.Tests/PipeReaderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Text;
using System.Threading.Tasks;
using SmtpServer.IO;
using SmtpServer.Protocol;
using SmtpServer.Text;
using Xunit;

Expand All @@ -25,7 +26,7 @@ public async void CanReadLineAndRemoveTrailingCRLF()
var reader = CreatePipeReader("abcde\r\n");

// act
var line = await reader.ReadLineAsync(Encoding.ASCII).ConfigureAwait(false);
var line = await reader.ReadLineAsync(Encoding.ASCII, new SmtpServerOptionsBuilder.MaxMessageSizeOptions()).ConfigureAwait(false);

// assert
Assert.Equal(5, line.Length);
Expand All @@ -40,7 +41,7 @@ public async void CanReadLinesWithInconsistentCRLF()
var reader = CreatePipeReader("ab\rcd\ne\r\n");

// act
var line = await reader.ReadLineAsync(Encoding.ASCII).ConfigureAwait(false);
var line = await reader.ReadLineAsync(Encoding.ASCII, new SmtpServerOptionsBuilder.MaxMessageSizeOptions()).ConfigureAwait(false);

// assert
Assert.Equal(7, line.Length);
Expand All @@ -55,9 +56,9 @@ public async void CanReadMultipleLines()
var reader = CreatePipeReader("abcde\r\nfghij\r\nklmno\r\n");

// act
var line1 = await reader.ReadLineAsync(Encoding.ASCII).ConfigureAwait(false);
var line2 = await reader.ReadLineAsync(Encoding.ASCII).ConfigureAwait(false);
var line3 = await reader.ReadLineAsync(Encoding.ASCII).ConfigureAwait(false);
var line1 = await reader.ReadLineAsync(Encoding.ASCII, new SmtpServerOptionsBuilder.MaxMessageSizeOptions()).ConfigureAwait(false);
var line2 = await reader.ReadLineAsync(Encoding.ASCII, new SmtpServerOptionsBuilder.MaxMessageSizeOptions()).ConfigureAwait(false);
var line3 = await reader.ReadLineAsync(Encoding.ASCII, new SmtpServerOptionsBuilder.MaxMessageSizeOptions()).ConfigureAwait(false);

// assert
Assert.Equal("abcde", line1);
Expand All @@ -79,10 +80,27 @@ await reader.ReadDotBlockAsync(
text = StringUtil.Create(buffer);

return Task.CompletedTask;
});
}, new SmtpServerOptionsBuilder.MaxMessageSizeOptions());

// assert
Assert.Equal("abcd\r\n.1234", text);
}

[Fact]
public async void ThrowsNoExceptionOnIgnore()
{
var reader = CreatePipeReader("abcd\r\n..1234\r\n.\r\n");

await reader.ReadDotBlockAsync(_ => Task.CompletedTask, new SmtpServerOptionsBuilder.MaxMessageSizeOptions(MaxMessageSizeHandling.Ignore, 2));
}
[Fact]
public async void ThrowsExceptionOnStrict()
{
var reader = CreatePipeReader("abcd\r\n..1234\r\n.\r\n");

await Assert.ThrowsAsync<MaxMessageSizeExceededException>(
async () => await reader.ReadDotBlockAsync(_ => Task.CompletedTask, new SmtpServerOptionsBuilder.MaxMessageSizeOptions(MaxMessageSizeHandling.Strict, 2))
);
}
}
}
10 changes: 10 additions & 0 deletions Src/SmtpServer.Tests/SmtpServerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
using SmtpServer.Protocol;
using SmtpServer.Storage;
using SmtpResponse = SmtpServer.Protocol.SmtpResponse;
using System.Linq;

namespace SmtpServer.Tests
{
Expand Down Expand Up @@ -156,6 +157,15 @@ public void WillTimeoutWaitingForCommand()
}
}

[Fact]
public void WillTerminateDueToTooMuchData()
{
using (CreateServer(c => c.MaxMessageSize(2, MaxMessageSizeHandling.Strict)))
{
Assert.Throws<IOException>(() => MailClient.Send(MailClient.Message(from: "[email protected]", to: "[email protected]", text: string.Concat(Enumerable.Repeat("Too long for 1024 bytes", 1000)))));
}
}

[Fact]
public void CanReturnSmtpResponseException_DoesNotQuit()
{
Expand Down
14 changes: 14 additions & 0 deletions Src/SmtpServer/IMaxMessageSizeOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace SmtpServer
{
public interface IMaxMessageSizeOptions
{
/// <summary>
/// Gets the maximum size of a message.
/// </summary>
int Length { get; }
/// <summary>
/// Gets the handling type an oversized message.
/// </summary>
MaxMessageSizeHandling Handling { get; }
}
}
32 changes: 20 additions & 12 deletions Src/SmtpServer/IO/PipeReaderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using SmtpServer.Protocol;
using SmtpServer.Text;

namespace SmtpServer.IO
Expand All @@ -21,20 +22,24 @@ internal static class PipeReaderExtensions
/// <param name="reader">The reader to read from.</param>
/// <param name="sequence">The sequence to find to terminate the read operation.</param>
/// <param name="func">The callback to execute to process the buffer.</param>
/// <param name="maxMessageSizeOptions"> Handling of MaxMessageSize</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The value that was read from the buffer.</returns>
static async ValueTask ReadUntilAsync(PipeReader reader, byte[] sequence, Func<ReadOnlySequence<byte>, Task> func, CancellationToken cancellationToken)
static async ValueTask ReadUntilAsync(PipeReader reader, byte[] sequence, Func<ReadOnlySequence<byte>, Task> func, IMaxMessageSizeOptions maxMessageSizeOptions, CancellationToken cancellationToken)
{
if (reader == null)
{
throw new ArgumentNullException(nameof(reader));
}

var read = await reader.ReadAsync(cancellationToken);
var head = read.Buffer.Start;

while (read.IsCanceled == false && read.IsCompleted == false && read.Buffer.IsEmpty == false)
{
if (maxMessageSizeOptions.Handling == MaxMessageSizeHandling.Strict && read.Buffer.Length > maxMessageSizeOptions.Length)
{
throw new MaxMessageSizeExceededException();
}
if (read.Buffer.TryFind(sequence, ref head, out var tail))
{
try
Expand All @@ -45,12 +50,9 @@ static async ValueTask ReadUntilAsync(PipeReader reader, byte[] sequence, Func<R
{
reader.AdvanceTo(tail);
}

return;
}

reader.AdvanceTo(read.Buffer.Start, read.Buffer.End);

read = await reader.ReadAsync(cancellationToken);
}
}
Expand All @@ -60,42 +62,45 @@ static async ValueTask ReadUntilAsync(PipeReader reader, byte[] sequence, Func<R
/// </summary>
/// <param name="reader">The reader to read from.</param>
/// <param name="func">The action to process the buffer.</param>
/// <param name="maxMessageSizeOptions"> Handling of MaxMessageSize</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A task that can be used to wait on the operation on complete.</returns>
internal static ValueTask ReadLineAsync(this PipeReader reader, Func<ReadOnlySequence<byte>, Task> func, CancellationToken cancellationToken = default)
internal static ValueTask ReadLineAsync(this PipeReader reader, Func<ReadOnlySequence<byte>, Task> func, IMaxMessageSizeOptions maxMessageSizeOptions, CancellationToken cancellationToken = default)
{
if (reader == null)
{
throw new ArgumentNullException(nameof(reader));
}

return ReadUntilAsync(reader, CRLF, func, cancellationToken);
return ReadUntilAsync(reader, CRLF, func, maxMessageSizeOptions, cancellationToken);
}

/// <summary>
/// Reads a line from the reader.
/// </summary>
/// <param name="reader">The reader to read from.</param>
/// <param name="maxMessageSizeOptions"> Handling of MaxMessageSize</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A task that can be used to wait on the operation on complete.</returns>
internal static ValueTask<string> ReadLineAsync(this PipeReader reader, CancellationToken cancellationToken = default)
internal static ValueTask<string> ReadLineAsync(this PipeReader reader, IMaxMessageSizeOptions maxMessageSizeOptions, CancellationToken cancellationToken = default)
{
if (reader == null)
{
throw new ArgumentNullException(nameof(reader));
}

return reader.ReadLineAsync(Encoding.ASCII, cancellationToken);
return reader.ReadLineAsync(Encoding.ASCII, maxMessageSizeOptions, cancellationToken);
}

/// <summary>
/// Reads a line from the reader.
/// </summary>
/// <param name="reader">The reader to read from.</param>
/// <param name="encoding">The encoding to use when converting the input.</param>
/// <param name="maxMessageSizeOptions"> Handling of MaxMessageSize</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A task that can be used to wait on the operation on complete.</returns>
internal static async ValueTask<string> ReadLineAsync(this PipeReader reader, Encoding encoding, CancellationToken cancellationToken = default)
internal static async ValueTask<string> ReadLineAsync(this PipeReader reader, Encoding encoding, IMaxMessageSizeOptions maxMessageSizeOptions, CancellationToken cancellationToken = default)
{
if (reader == null)
{
Expand All @@ -111,6 +116,7 @@ await reader.ReadLineAsync(

return Task.CompletedTask;
},
maxMessageSizeOptions,
cancellationToken);

return text;
Expand All @@ -121,9 +127,10 @@ await reader.ReadLineAsync(
/// </summary>
/// <param name="reader">The reader to read from.</param>
/// <param name="func">The action to process the buffer.</param>
/// <param name="maxMessageSizeOptions"> Handling of MaxMessageSize</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The value that was read from the buffer.</returns>
internal static async ValueTask ReadDotBlockAsync(this PipeReader reader, Func<ReadOnlySequence<byte>, Task> func, CancellationToken cancellationToken = default)
internal static async ValueTask ReadDotBlockAsync(this PipeReader reader, Func<ReadOnlySequence<byte>, Task> func, IMaxMessageSizeOptions maxMessageSizeOptions, CancellationToken cancellationToken = default)
{
if (reader == null)
{
Expand All @@ -138,7 +145,8 @@ await ReadUntilAsync(
buffer = Unstuff(buffer);

return func(buffer);
},
},
maxMessageSizeOptions,
cancellationToken);

static ReadOnlySequence<byte> Unstuff(ReadOnlySequence<byte> buffer)
Expand Down
9 changes: 7 additions & 2 deletions Src/SmtpServer/ISmtpServerOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ namespace SmtpServer
public interface ISmtpServerOptions
{
/// <summary>
/// Gets the maximum size of a message.
/// Gets the maximum message size option.
/// </summary>
int MaxMessageSize { get; }
IMaxMessageSizeOptions MaxMessageSizeOptions { get; }

/// <summary>
/// The maximum number of retries before quitting the session.
Expand All @@ -35,6 +35,11 @@ public interface ISmtpServerOptions
/// </summary>
TimeSpan CommandWaitTimeout { get; }

/// <summary>
/// The timeout to use when waiting for a response from the client.
/// </summary>
TimeSpan ResponseWaitTimeout { get; }

/// <summary>
/// The size of the buffer that is read from each call to the underlying network client.
/// </summary>
Expand Down
17 changes: 17 additions & 0 deletions Src/SmtpServer/MaxMessageSizeHandling.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
namespace SmtpServer
{
/// <summary>
/// Choose how MaxMessageSize limit should be considered
/// </summary>
public enum MaxMessageSizeHandling
{
/// <summary>
/// Use the size limit for the SIZE extension of ESMTP
/// </summary>
Ignore = 0,
/// <summary>
/// Close the session after too much data has been sent
/// </summary>
Strict = 1,
}
}
11 changes: 6 additions & 5 deletions Src/SmtpServer/Protocol/AuthCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ async Task<bool> TryPlainAsync(ISessionContext context, CancellationToken cancel
{
await context.Pipe.Output.WriteReplyAsync(new SmtpResponse(SmtpReplyCode.ContinueWithAuth, " "), cancellationToken).ConfigureAwait(false);

authentication = await context.Pipe.Input.ReadLineAsync(Encoding.ASCII, cancellationToken).ConfigureAwait(false);
authentication = await context.Pipe.Input.ReadLineAsync(Encoding.ASCII, context.ServerOptions.MaxMessageSizeOptions, cancellationToken).ConfigureAwait(false);
}

if (TryExtractFromBase64(authentication) == false)
Expand Down Expand Up @@ -150,13 +150,13 @@ async Task<bool> TryLoginAsync(ISessionContext context, CancellationToken cancel
//Username = VXNlcm5hbWU6 (base64)
await context.Pipe.Output.WriteReplyAsync(new SmtpResponse(SmtpReplyCode.ContinueWithAuth, "VXNlcm5hbWU6"), cancellationToken).ConfigureAwait(false);

_user = await ReadBase64EncodedLineAsync(context.Pipe.Input, cancellationToken).ConfigureAwait(false);
_user = await ReadBase64EncodedLineAsync(context.Pipe.Input, context.ServerOptions.MaxMessageSizeOptions, cancellationToken).ConfigureAwait(false);
}

//Password = UGFzc3dvcmQ6 (base64)
await context.Pipe.Output.WriteReplyAsync(new SmtpResponse(SmtpReplyCode.ContinueWithAuth, "UGFzc3dvcmQ6"), cancellationToken).ConfigureAwait(false);

_password = await ReadBase64EncodedLineAsync(context.Pipe.Input, cancellationToken).ConfigureAwait(false);
_password = await ReadBase64EncodedLineAsync(context.Pipe.Input, context.ServerOptions.MaxMessageSizeOptions, cancellationToken).ConfigureAwait(false);

return true;
}
Expand All @@ -165,11 +165,12 @@ async Task<bool> TryLoginAsync(ISessionContext context, CancellationToken cancel
/// Read a Base64 encoded line.
/// </summary>
/// <param name="reader">The pipe to read from.</param>
/// <param name="maxMessageSizeOptions"> Handling of MaxMessageSize</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The decoded Base64 string.</returns>
static async Task<string> ReadBase64EncodedLineAsync(PipeReader reader, CancellationToken cancellationToken)
static async Task<string> ReadBase64EncodedLineAsync(PipeReader reader, IMaxMessageSizeOptions maxMessageSizeOptions, CancellationToken cancellationToken)
{
var text = await reader.ReadLineAsync(cancellationToken);
var text = await reader.ReadLineAsync(maxMessageSizeOptions, cancellationToken);

return text == null ? string.Empty : Encoding.UTF8.GetString(Convert.FromBase64String(text));
}
Expand Down
8 changes: 7 additions & 1 deletion Src/SmtpServer/Protocol/DataCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using SmtpServer.ComponentModel;
using SmtpServer.IO;
using SmtpServer.Storage;
using static SmtpServer.IO.PipeReaderExtensions;

namespace SmtpServer.Protocol
{
Expand Down Expand Up @@ -46,11 +47,16 @@ await context.Pipe.Input.ReadDotBlockAsync(
{
// ReSharper disable once AccessToDisposedClosure
response = await container.Instance.SaveAsync(context, context.Transaction, buffer, cancellationToken).ConfigureAwait(false);
},
},
context.ServerOptions.MaxMessageSizeOptions,
cancellationToken).ConfigureAwait(false);

await context.Pipe.Output.WriteReplyAsync(response, cancellationToken).ConfigureAwait(false);
}
catch (MaxMessageSizeExceededException)
{
await context.Pipe.Output.WriteReplyAsync(SmtpResponse.SizeLimitExceeded, cancellationToken).ConfigureAwait(false);
}
catch (Exception)
{
await context.Pipe.Output.WriteReplyAsync(new SmtpResponse(SmtpReplyCode.TransactionFailed), cancellationToken).ConfigureAwait(false);
Expand Down
4 changes: 2 additions & 2 deletions Src/SmtpServer/Protocol/EhloCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,9 @@ protected virtual IEnumerable<string> GetExtensions(ISessionContext context)
yield return "STARTTLS";
}

if (context.ServerOptions.MaxMessageSize > 0)
if (context.ServerOptions.MaxMessageSizeOptions.Length > 0)
{
yield return $"SIZE {context.ServerOptions.MaxMessageSize}";
yield return $"SIZE {context.ServerOptions.MaxMessageSizeOptions.Length}";
}

if (IsPlainLoginAllowed(context))
Expand Down
2 changes: 1 addition & 1 deletion Src/SmtpServer/Protocol/MailCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ internal override async Task<bool> ExecuteAsync(SmtpSessionContext context, Canc
var size = GetMessageSize();

// check against the server supplied maximum
if (context.ServerOptions.MaxMessageSize > 0 && size > context.ServerOptions.MaxMessageSize)
if (context.ServerOptions.MaxMessageSizeOptions.Length > 0 && size > context.ServerOptions.MaxMessageSizeOptions.Length)
{
await context.Pipe.Output.WriteReplyAsync(SmtpResponse.SizeLimitExceeded, cancellationToken).ConfigureAwait(false);
return false;
Expand Down
9 changes: 9 additions & 0 deletions Src/SmtpServer/Protocol/MaxMessageSizeExceededException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using System;

namespace SmtpServer.Protocol
{
public sealed class MaxMessageSizeExceededException : Exception
{

}
}
Loading