diff --git a/Src/SmtpServer.Tests/PipeReaderTests.cs b/Src/SmtpServer.Tests/PipeReaderTests.cs index f61ba23..933efe3 100644 --- a/Src/SmtpServer.Tests/PipeReaderTests.cs +++ b/Src/SmtpServer.Tests/PipeReaderTests.cs @@ -3,6 +3,7 @@ using System.Text; using System.Threading.Tasks; using SmtpServer.IO; +using SmtpServer.Protocol; using SmtpServer.Text; using Xunit; @@ -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); @@ -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); @@ -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); @@ -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( + async () => await reader.ReadDotBlockAsync(_ => Task.CompletedTask, new SmtpServerOptionsBuilder.MaxMessageSizeOptions(MaxMessageSizeHandling.Strict, 2)) + ); + } } } \ No newline at end of file diff --git a/Src/SmtpServer.Tests/SmtpServerTests.cs b/Src/SmtpServer.Tests/SmtpServerTests.cs index ff7a60f..58846fb 100644 --- a/Src/SmtpServer.Tests/SmtpServerTests.cs +++ b/Src/SmtpServer.Tests/SmtpServerTests.cs @@ -16,6 +16,7 @@ using SmtpServer.Protocol; using SmtpServer.Storage; using SmtpResponse = SmtpServer.Protocol.SmtpResponse; +using System.Linq; namespace SmtpServer.Tests { @@ -156,6 +157,15 @@ public void WillTimeoutWaitingForCommand() } } + [Fact] + public void WillTerminateDueToTooMuchData() + { + using (CreateServer(c => c.MaxMessageSize(2, MaxMessageSizeHandling.Strict))) + { + Assert.Throws(() => MailClient.Send(MailClient.Message(from: "test1@test.com", to: "test2@test.com", text: string.Concat(Enumerable.Repeat("Too long for 1024 bytes", 1000))))); + } + } + [Fact] public void CanReturnSmtpResponseException_DoesNotQuit() { diff --git a/Src/SmtpServer/IMaxMessageSizeOptions.cs b/Src/SmtpServer/IMaxMessageSizeOptions.cs new file mode 100644 index 0000000..7371411 --- /dev/null +++ b/Src/SmtpServer/IMaxMessageSizeOptions.cs @@ -0,0 +1,14 @@ +namespace SmtpServer +{ + public interface IMaxMessageSizeOptions + { + /// + /// Gets the maximum size of a message. + /// + int Length { get; } + /// + /// Gets the handling type an oversized message. + /// + MaxMessageSizeHandling Handling { get; } + } +} \ No newline at end of file diff --git a/Src/SmtpServer/IO/PipeReaderExtensions.cs b/Src/SmtpServer/IO/PipeReaderExtensions.cs index ea691fc..62271dc 100644 --- a/Src/SmtpServer/IO/PipeReaderExtensions.cs +++ b/Src/SmtpServer/IO/PipeReaderExtensions.cs @@ -4,6 +4,7 @@ using System.Text; using System.Threading; using System.Threading.Tasks; +using SmtpServer.Protocol; using SmtpServer.Text; namespace SmtpServer.IO @@ -21,20 +22,24 @@ internal static class PipeReaderExtensions /// The reader to read from. /// The sequence to find to terminate the read operation. /// The callback to execute to process the buffer. + /// Handling of MaxMessageSize /// The cancellation token. /// The value that was read from the buffer. - static async ValueTask ReadUntilAsync(PipeReader reader, byte[] sequence, Func, Task> func, CancellationToken cancellationToken) + static async ValueTask ReadUntilAsync(PipeReader reader, byte[] sequence, Func, 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 @@ -45,12 +50,9 @@ static async ValueTask ReadUntilAsync(PipeReader reader, byte[] sequence, Func /// The reader to read from. /// The action to process the buffer. + /// Handling of MaxMessageSize /// The cancellation token. /// A task that can be used to wait on the operation on complete. - internal static ValueTask ReadLineAsync(this PipeReader reader, Func, Task> func, CancellationToken cancellationToken = default) + internal static ValueTask ReadLineAsync(this PipeReader reader, Func, 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); } /// /// Reads a line from the reader. /// /// The reader to read from. + /// Handling of MaxMessageSize /// The cancellation token. /// A task that can be used to wait on the operation on complete. - internal static ValueTask ReadLineAsync(this PipeReader reader, CancellationToken cancellationToken = default) + internal static ValueTask 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); } /// @@ -93,9 +97,10 @@ internal static ValueTask ReadLineAsync(this PipeReader reader, Cancella /// /// The reader to read from. /// The encoding to use when converting the input. + /// Handling of MaxMessageSize /// The cancellation token. /// A task that can be used to wait on the operation on complete. - internal static async ValueTask ReadLineAsync(this PipeReader reader, Encoding encoding, CancellationToken cancellationToken = default) + internal static async ValueTask ReadLineAsync(this PipeReader reader, Encoding encoding, IMaxMessageSizeOptions maxMessageSizeOptions, CancellationToken cancellationToken = default) { if (reader == null) { @@ -111,6 +116,7 @@ await reader.ReadLineAsync( return Task.CompletedTask; }, + maxMessageSizeOptions, cancellationToken); return text; @@ -121,9 +127,10 @@ await reader.ReadLineAsync( /// /// The reader to read from. /// The action to process the buffer. + /// Handling of MaxMessageSize /// The cancellation token. /// The value that was read from the buffer. - internal static async ValueTask ReadDotBlockAsync(this PipeReader reader, Func, Task> func, CancellationToken cancellationToken = default) + internal static async ValueTask ReadDotBlockAsync(this PipeReader reader, Func, Task> func, IMaxMessageSizeOptions maxMessageSizeOptions, CancellationToken cancellationToken = default) { if (reader == null) { @@ -138,7 +145,8 @@ await ReadUntilAsync( buffer = Unstuff(buffer); return func(buffer); - }, + }, + maxMessageSizeOptions, cancellationToken); static ReadOnlySequence Unstuff(ReadOnlySequence buffer) diff --git a/Src/SmtpServer/ISmtpServerOptions.cs b/Src/SmtpServer/ISmtpServerOptions.cs index 879e575..9c0a46b 100644 --- a/Src/SmtpServer/ISmtpServerOptions.cs +++ b/Src/SmtpServer/ISmtpServerOptions.cs @@ -6,9 +6,9 @@ namespace SmtpServer public interface ISmtpServerOptions { /// - /// Gets the maximum size of a message. + /// Gets the maximum message size option. /// - int MaxMessageSize { get; } + IMaxMessageSizeOptions MaxMessageSizeOptions { get; } /// /// The maximum number of retries before quitting the session. @@ -35,6 +35,11 @@ public interface ISmtpServerOptions /// TimeSpan CommandWaitTimeout { get; } + /// + /// The timeout to use when waiting for a response from the client. + /// + TimeSpan ResponseWaitTimeout { get; } + /// /// The size of the buffer that is read from each call to the underlying network client. /// diff --git a/Src/SmtpServer/MaxMessageSizeHandling.cs b/Src/SmtpServer/MaxMessageSizeHandling.cs new file mode 100644 index 0000000..2fc3c77 --- /dev/null +++ b/Src/SmtpServer/MaxMessageSizeHandling.cs @@ -0,0 +1,17 @@ +namespace SmtpServer +{ + /// + /// Choose how MaxMessageSize limit should be considered + /// + public enum MaxMessageSizeHandling + { + /// + /// Use the size limit for the SIZE extension of ESMTP + /// + Ignore = 0, + /// + /// Close the session after too much data has been sent + /// + Strict = 1, + } +} diff --git a/Src/SmtpServer/Protocol/AuthCommand.cs b/Src/SmtpServer/Protocol/AuthCommand.cs index 1183434..b70ee71 100644 --- a/Src/SmtpServer/Protocol/AuthCommand.cs +++ b/Src/SmtpServer/Protocol/AuthCommand.cs @@ -101,7 +101,7 @@ async Task 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) @@ -150,13 +150,13 @@ async Task 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; } @@ -165,11 +165,12 @@ async Task TryLoginAsync(ISessionContext context, CancellationToken cancel /// Read a Base64 encoded line. /// /// The pipe to read from. + /// Handling of MaxMessageSize /// The cancellation token. /// The decoded Base64 string. - static async Task ReadBase64EncodedLineAsync(PipeReader reader, CancellationToken cancellationToken) + static async Task 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)); } diff --git a/Src/SmtpServer/Protocol/DataCommand.cs b/Src/SmtpServer/Protocol/DataCommand.cs index 233c18e..ceb4849 100644 --- a/Src/SmtpServer/Protocol/DataCommand.cs +++ b/Src/SmtpServer/Protocol/DataCommand.cs @@ -4,6 +4,7 @@ using SmtpServer.ComponentModel; using SmtpServer.IO; using SmtpServer.Storage; +using static SmtpServer.IO.PipeReaderExtensions; namespace SmtpServer.Protocol { @@ -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); diff --git a/Src/SmtpServer/Protocol/EhloCommand.cs b/Src/SmtpServer/Protocol/EhloCommand.cs index 485d936..7b9b476 100644 --- a/Src/SmtpServer/Protocol/EhloCommand.cs +++ b/Src/SmtpServer/Protocol/EhloCommand.cs @@ -69,9 +69,9 @@ protected virtual IEnumerable 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)) diff --git a/Src/SmtpServer/Protocol/MailCommand.cs b/Src/SmtpServer/Protocol/MailCommand.cs index d4502e5..512bf8b 100644 --- a/Src/SmtpServer/Protocol/MailCommand.cs +++ b/Src/SmtpServer/Protocol/MailCommand.cs @@ -45,7 +45,7 @@ internal override async Task 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; diff --git a/Src/SmtpServer/Protocol/MaxMessageSizeExceededException.cs b/Src/SmtpServer/Protocol/MaxMessageSizeExceededException.cs new file mode 100644 index 0000000..fa7ba6d --- /dev/null +++ b/Src/SmtpServer/Protocol/MaxMessageSizeExceededException.cs @@ -0,0 +1,9 @@ +using System; + +namespace SmtpServer.Protocol +{ + public sealed class MaxMessageSizeExceededException : Exception + { + + } +} diff --git a/Src/SmtpServer/SmtpServerOptionsBuilder.cs b/Src/SmtpServer/SmtpServerOptionsBuilder.cs index d895420..3981579 100644 --- a/Src/SmtpServer/SmtpServerOptionsBuilder.cs +++ b/Src/SmtpServer/SmtpServerOptionsBuilder.cs @@ -15,11 +15,13 @@ public ISmtpServerOptions Build() { var serverOptions = new SmtpServerOptions { + MaxMessageSizeOptions = new MaxMessageSizeOptions(), Endpoints = new List(), MaxRetryCount = 5, MaxAuthenticationAttempts = 3, NetworkBufferSize = 128, - CommandWaitTimeout = TimeSpan.FromMinutes(5) + CommandWaitTimeout = TimeSpan.FromMinutes(5), + ResponseWaitTimeout = TimeSpan.FromMinutes(5), }; _setters.ForEach(setter => setter(serverOptions)); @@ -95,11 +97,12 @@ public SmtpServerOptionsBuilder Port(int port, bool isSecure) /// /// Sets the maximum message size. /// - /// The maximum message size to allow. + /// The maximum message size to allow in bytes. + /// The handling type. /// A OptionsBuilder to continue building on. - public SmtpServerOptionsBuilder MaxMessageSize(int value) + public SmtpServerOptionsBuilder MaxMessageSize(int length, MaxMessageSizeHandling handling = MaxMessageSizeHandling.Ignore) { - _setters.Add(options => options.MaxMessageSize = value); + _setters.Add(options => options.MaxMessageSizeOptions = new MaxMessageSizeOptions(handling, length)); return this; } @@ -148,10 +151,21 @@ public SmtpServerOptionsBuilder NetworkBufferSize(int value) public SmtpServerOptionsBuilder CommandWaitTimeout(TimeSpan value) { _setters.Add(options => options.CommandWaitTimeout = value); - + return this; } + /// + /// Sets the timeout used when waiting for a command from the client. + /// + /// The timeout used when waiting for a command from the client. + /// An OptionsBuilder to continue building on. + public SmtpServerOptionsBuilder ResponseWaitTimeout(TimeSpan value) + { + _setters.Add(options => options.ResponseWaitTimeout = value); + + return this; + } #region SmtpServerOptions class SmtpServerOptions : ISmtpServerOptions @@ -159,7 +173,7 @@ class SmtpServerOptions : ISmtpServerOptions /// /// Gets or sets the maximum size of a message. /// - public int MaxMessageSize { get; set; } + public IMaxMessageSizeOptions MaxMessageSizeOptions { get; set; } /// /// The maximum number of retries before quitting the session. @@ -191,12 +205,38 @@ class SmtpServerOptions : ISmtpServerOptions /// public TimeSpan CommandWaitTimeout { get; set; } + /// + /// The timeout to use when waiting for a command from the client. + /// + public TimeSpan ResponseWaitTimeout { get; set; } + /// /// The size of the buffer that is read from each call to the underlying network client. /// public int NetworkBufferSize { get; set; } } + public class MaxMessageSizeOptions: IMaxMessageSizeOptions + { + /// + /// Gets or sets the maximum size of a message. + /// + public int Length { get; set;} + /// + /// Gets or sets the handling type an oversized message. + /// + public MaxMessageSizeHandling Handling { get; set;} + public MaxMessageSizeOptions(MaxMessageSizeHandling handling, int length) + { + Length = length; + Handling = handling; + } + public MaxMessageSizeOptions() + { + + } + } + #endregion } } \ No newline at end of file diff --git a/Src/SmtpServer/SmtpSession.cs b/Src/SmtpServer/SmtpSession.cs index 6f3a303..8d5ee7e 100644 --- a/Src/SmtpServer/SmtpSession.cs +++ b/Src/SmtpServer/SmtpSession.cs @@ -56,13 +56,15 @@ internal async Task RunAsync(CancellationToken cancellationToken) /// A task which asynchronously performs the execution. async Task ExecuteAsync(SmtpSessionContext context, CancellationToken cancellationToken) { + var responseTimeout = new CancellationTokenSource(_context.ServerOptions.ResponseWaitTimeout); + var cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(responseTimeout.Token, cancellationToken); var retries = _context.ServerOptions.MaxRetryCount; while (retries-- > 0 && context.IsQuitRequested == false && cancellationToken.IsCancellationRequested == false) { try { - var command = await ReadCommandAsync(context, cancellationToken).ConfigureAwait(false); + var command = await ReadCommandAsync(context, cancellationTokenSource.Token).ConfigureAwait(false); if (command == null) { @@ -74,13 +76,21 @@ async Task ExecuteAsync(SmtpSessionContext context, CancellationToken cancellati throw new SmtpResponseException(errorResponse); } - if (await ExecuteAsync(command, context, cancellationToken).ConfigureAwait(false)) + if (await ExecuteAsync(command, context, cancellationTokenSource.Token).ConfigureAwait(false)) { _stateMachine.Transition(context); } retries = _context.ServerOptions.MaxRetryCount; } + catch (MaxMessageSizeExceededException) + { + context.RaiseResponseException(new SmtpResponseException(SmtpResponse.SizeLimitExceeded)); + + await context.Pipe.Output.WriteReplyAsync(SmtpResponse.SizeLimitExceeded, cancellationToken).ConfigureAwait(false); + + context.IsQuitRequested = true; + } catch (SmtpResponseException responseException) when (responseException.IsQuitRequested) { context.RaiseResponseException(responseException); @@ -99,7 +109,14 @@ async Task ExecuteAsync(SmtpSessionContext context, CancellationToken cancellati } catch (OperationCanceledException) { - await context.Pipe.Output.WriteReplyAsync(new SmtpResponse(SmtpReplyCode.ServiceClosingTransmissionChannel, "The session has be cancelled."), CancellationToken.None).ConfigureAwait(false); + if (responseTimeout.IsCancellationRequested) + { + await context.Pipe.Output.WriteReplyAsync(new SmtpResponse(SmtpReplyCode.ServiceClosingTransmissionChannel, "Timeout while waiting for response."), CancellationToken.None).ConfigureAwait(false); + } + else + { + await context.Pipe.Output.WriteReplyAsync(new SmtpResponse(SmtpReplyCode.ServiceClosingTransmissionChannel, "The session has be cancelled."), CancellationToken.None).ConfigureAwait(false); + } } } } @@ -131,6 +148,7 @@ await context.Pipe.Input.ReadLineAsync( return Task.CompletedTask; }, + context.ServerOptions.MaxMessageSizeOptions, cancellationTokenSource.Token).ConfigureAwait(false); return command;