From c9b95b20e71e26a1f4489ff2141e4174dd3fc1c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20T=C3=BCnte?= Date: Wed, 8 Nov 2023 12:16:22 +0100 Subject: [PATCH 1/3] Add option to strictly disallow large messages --- Src/SmtpServer.Tests/PipeReaderTests.cs | 30 ++++++++++++---- Src/SmtpServer/IMaxMessageSizeOptions.cs | 14 ++++++++ Src/SmtpServer/IO/PipeReaderExtensions.cs | 32 ++++++++++------- Src/SmtpServer/ISmtpServerOptions.cs | 4 +-- Src/SmtpServer/MaxMessageSizeHandling.cs | 17 ++++++++++ Src/SmtpServer/Protocol/AuthCommand.cs | 11 +++--- Src/SmtpServer/Protocol/DataCommand.cs | 8 ++++- Src/SmtpServer/Protocol/EhloCommand.cs | 4 +-- Src/SmtpServer/Protocol/MailCommand.cs | 2 +- .../MaxMessageSizeExceededException.cs | 9 +++++ Src/SmtpServer/SmtpServerOptionsBuilder.cs | 34 +++++++++++++++---- Src/SmtpServer/SmtpSession.cs | 9 +++++ 12 files changed, 139 insertions(+), 35 deletions(-) create mode 100644 Src/SmtpServer/IMaxMessageSizeOptions.cs create mode 100644 Src/SmtpServer/MaxMessageSizeHandling.cs create mode 100644 Src/SmtpServer/Protocol/MaxMessageSizeExceededException.cs 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/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..1756efb 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. 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..f3b3912 100644 --- a/Src/SmtpServer/SmtpServerOptionsBuilder.cs +++ b/Src/SmtpServer/SmtpServerOptionsBuilder.cs @@ -15,6 +15,7 @@ public ISmtpServerOptions Build() { var serverOptions = new SmtpServerOptions { + MaxMessageSizeOptions = new MaxMessageSizeOptions(), Endpoints = new List(), MaxRetryCount = 5, MaxAuthenticationAttempts = 3, @@ -95,11 +96,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 +150,9 @@ public SmtpServerOptionsBuilder NetworkBufferSize(int value) public SmtpServerOptionsBuilder CommandWaitTimeout(TimeSpan value) { _setters.Add(options => options.CommandWaitTimeout = value); - + return this; } - #region SmtpServerOptions class SmtpServerOptions : ISmtpServerOptions @@ -159,7 +160,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. @@ -197,6 +198,27 @@ class SmtpServerOptions : ISmtpServerOptions 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..643dca7 100644 --- a/Src/SmtpServer/SmtpSession.cs +++ b/Src/SmtpServer/SmtpSession.cs @@ -81,6 +81,14 @@ async Task ExecuteAsync(SmtpSessionContext context, CancellationToken cancellati 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); @@ -131,6 +139,7 @@ await context.Pipe.Input.ReadLineAsync( return Task.CompletedTask; }, + context.ServerOptions.MaxMessageSizeOptions, cancellationTokenSource.Token).ConfigureAwait(false); return command; From f51e1fcdfdfda3ddad399f05edfde9b741560e6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20T=C3=BCnte?= Date: Wed, 8 Nov 2023 13:55:55 +0100 Subject: [PATCH 2/3] Add Timeout for reading response --- Src/SmtpServer/ISmtpServerOptions.cs | 5 +++++ Src/SmtpServer/SmtpServerOptionsBuilder.cs | 20 +++++++++++++++++++- Src/SmtpServer/SmtpSession.cs | 15 ++++++++++++--- 3 files changed, 36 insertions(+), 4 deletions(-) diff --git a/Src/SmtpServer/ISmtpServerOptions.cs b/Src/SmtpServer/ISmtpServerOptions.cs index 1756efb..9c0a46b 100644 --- a/Src/SmtpServer/ISmtpServerOptions.cs +++ b/Src/SmtpServer/ISmtpServerOptions.cs @@ -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/SmtpServerOptionsBuilder.cs b/Src/SmtpServer/SmtpServerOptionsBuilder.cs index f3b3912..3981579 100644 --- a/Src/SmtpServer/SmtpServerOptionsBuilder.cs +++ b/Src/SmtpServer/SmtpServerOptionsBuilder.cs @@ -20,7 +20,8 @@ public ISmtpServerOptions Build() MaxRetryCount = 5, MaxAuthenticationAttempts = 3, NetworkBufferSize = 128, - CommandWaitTimeout = TimeSpan.FromMinutes(5) + CommandWaitTimeout = TimeSpan.FromMinutes(5), + ResponseWaitTimeout = TimeSpan.FromMinutes(5), }; _setters.ForEach(setter => setter(serverOptions)); @@ -153,6 +154,18 @@ public SmtpServerOptionsBuilder CommandWaitTimeout(TimeSpan 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 @@ -192,6 +205,11 @@ 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. /// diff --git a/Src/SmtpServer/SmtpSession.cs b/Src/SmtpServer/SmtpSession.cs index 643dca7..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,7 +76,7 @@ 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); } @@ -107,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); + } } } } From 306fc4a08b7e30d0b2a70956462ca7edfc9bfe5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20T=C3=BCnte?= Date: Wed, 3 Jan 2024 12:19:08 +0100 Subject: [PATCH 3/3] Add Test for MaxMessageSize --- Src/SmtpServer.Tests/SmtpServerTests.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) 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() {