diff --git a/Src/SmtpServer.Tests/PipeReaderTests.cs b/Src/SmtpServer.Tests/PipeReaderTests.cs index e6c4b76..b9a085a 100644 --- a/Src/SmtpServer.Tests/PipeReaderTests.cs +++ b/Src/SmtpServer.Tests/PipeReaderTests.cs @@ -24,8 +24,10 @@ public async Task CanReadLineAndRemoveTrailingCRLF() // arrange var reader = CreatePipeReader("abcde\r\n"); + var maxMessageSizeOptions = new MaxMessageSizeOptions(); + // act - var line = await reader.ReadLineAsync(Encoding.ASCII).ConfigureAwait(false); + var line = await reader.ReadLineAsync(Encoding.ASCII, maxMessageSizeOptions).ConfigureAwait(false); // assert Assert.Equal(5, line.Length); @@ -39,8 +41,10 @@ public async Task CanReadLinesWithInconsistentCRLF() // arrange var reader = CreatePipeReader("ab\rcd\ne\r\n"); + var maxMessageSizeOptions = new MaxMessageSizeOptions(); + // act - var line = await reader.ReadLineAsync(Encoding.ASCII).ConfigureAwait(false); + var line = await reader.ReadLineAsync(Encoding.ASCII, maxMessageSizeOptions).ConfigureAwait(false); // assert Assert.Equal(7, line.Length); @@ -54,10 +58,12 @@ public async Task CanReadMultipleLines() // arrange var reader = CreatePipeReader("abcde\r\nfghij\r\nklmno\r\n"); + var maxMessageSizeOptions = new MaxMessageSizeOptions(); + // 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, maxMessageSizeOptions).ConfigureAwait(false); + var line2 = await reader.ReadLineAsync(Encoding.ASCII, maxMessageSizeOptions).ConfigureAwait(false); + var line3 = await reader.ReadLineAsync(Encoding.ASCII, maxMessageSizeOptions).ConfigureAwait(false); // assert Assert.Equal("abcde", line1); @@ -71,6 +77,8 @@ public async Task CanReadBlockWithDotStuffingRemoved() // arrange var reader = CreatePipeReader("abcd\r\n..1234\r\n.\r\n"); + var maxMessageSizeOptions = new MaxMessageSizeOptions(); + // act var text = ""; await reader.ReadDotBlockAsync( @@ -79,7 +87,8 @@ await reader.ReadDotBlockAsync( text = StringUtil.Create(buffer); return Task.CompletedTask; - }); + }, + maxMessageSizeOptions); // assert Assert.Equal("abcd\r\n.1234", text); diff --git a/Src/SmtpServer.Tests/SmtpServerTests.cs b/Src/SmtpServer.Tests/SmtpServerTests.cs index 300bf03..292cdaa 100644 --- a/Src/SmtpServer.Tests/SmtpServerTests.cs +++ b/Src/SmtpServer.Tests/SmtpServerTests.cs @@ -1,4 +1,5 @@ using MailKit; +using MailKit.Net.Smtp; using SmtpServer.Authentication; using SmtpServer.ComponentModel; using SmtpServer.Mail; @@ -9,6 +10,7 @@ using System; using System.Diagnostics; using System.IO; +using System.Linq; using System.Net; using System.Net.Security; using System.Net.Sockets; @@ -162,6 +164,20 @@ public void WillTimeoutWaitingForCommand() } } + [Fact] + public void WillTerminateDueToTooMuchData() + { + var maxAcceptedMailMessageSize = 50; + + var largeMailContent = string.Concat(Enumerable.Repeat("Too long for 1024 bytes", 1000)); + using var mailMessage = MailClient.Message(from: "test1@test.com", to: "test2@test.com", text: largeMailContent); + + using (CreateServer(c => c.MaxMessageSize(maxAcceptedMailMessageSize, MaxMessageSizeHandling.Strict))) + { + Assert.Throws(() => MailClient.Send(mailMessage)); + } + } + [Fact] public async Task WillSessionTimeoutDuringMailDataTransmission() { diff --git a/Src/SmtpServer/IMaxMessageSizeOptions.cs b/Src/SmtpServer/IMaxMessageSizeOptions.cs new file mode 100644 index 0000000..a8630e9 --- /dev/null +++ b/Src/SmtpServer/IMaxMessageSizeOptions.cs @@ -0,0 +1,19 @@ +namespace SmtpServer +{ + /// + /// Defines configuration options for enforcing a maximum allowed message size according to the SMTP SIZE extension (RFC 1870). + /// Includes the size limit in bytes and the handling strategy for oversized messages. + /// + public interface IMaxMessageSizeOptions + { + /// + /// Gets or sets the maximum allowed message size in bytes. + /// + int Length { get; } + + /// + /// Gets the handling type an oversized message. + /// + MaxMessageSizeHandling Handling { get; } + } +} diff --git a/Src/SmtpServer/IO/PipeReaderExtensions.cs b/Src/SmtpServer/IO/PipeReaderExtensions.cs index d88427d..c0c4f47 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,9 +22,10 @@ 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) { @@ -35,6 +37,11 @@ static async ValueTask ReadUntilAsync(PipeReader reader, byte[] sequence, Func maxMessageSizeOptions.Length) + { + throw new SmtpResponseException(SmtpResponse.MaxMessageSizeExceeded, true); + } + if (read.Buffer.TryFind(sequence, ref head, out var tail)) { try @@ -60,32 +67,34 @@ 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 +102,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 +121,7 @@ await reader.ReadLineAsync( return Task.CompletedTask; }, + maxMessageSizeOptions, cancellationToken); return text; @@ -121,9 +132,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) { @@ -131,14 +143,15 @@ internal static async ValueTask ReadDotBlockAsync(this PipeReader reader, Func { 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 f253484..b0c1daa 100644 --- a/Src/SmtpServer/ISmtpServerOptions.cs +++ b/Src/SmtpServer/ISmtpServerOptions.cs @@ -9,9 +9,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..fdb6652 --- /dev/null +++ b/Src/SmtpServer/MaxMessageSizeHandling.cs @@ -0,0 +1,18 @@ +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/MaxMessageSizeOptions.cs b/Src/SmtpServer/MaxMessageSizeOptions.cs new file mode 100644 index 0000000..5e8cbb8 --- /dev/null +++ b/Src/SmtpServer/MaxMessageSizeOptions.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace SmtpServer +{ + /// + /// Represents configuration settings for enforcing a maximum message size in SMTP, + /// including the size limit in bytes and the behavior when that limit is exceeded. + /// + public class MaxMessageSizeOptions : IMaxMessageSizeOptions + { + /// + /// Gets or sets the maximum allowed message size in bytes, + /// as specified by the SMTP SIZE extension (RFC 1870). + /// + public int Length { get; set; } + + /// + /// Gets or sets the handling strategy for messages that exceed the maximum allowed size. + /// + public MaxMessageSizeHandling Handling { get; set; } + + /// + /// Initializes a new instance of the class + /// with the specified handling strategy and message size limit. + /// + /// The strategy for handling messages that exceed the maximum allowed size. + /// The maximum allowed message size in bytes. + public MaxMessageSizeOptions(MaxMessageSizeHandling handling, int length) + { + Length = length; + Handling = handling; + } + + /// + /// Initializes a new instance of the class with default values. + /// + public MaxMessageSizeOptions() + { + + } + } +} diff --git a/Src/SmtpServer/Protocol/AuthCommand.cs b/Src/SmtpServer/Protocol/AuthCommand.cs index 6fb980f..0c84e31 100644 --- a/Src/SmtpServer/Protocol/AuthCommand.cs +++ b/Src/SmtpServer/Protocol/AuthCommand.cs @@ -106,7 +106,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) @@ -155,13 +155,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; } @@ -172,9 +172,9 @@ async Task TryLoginAsync(ISessionContext context, CancellationToken cancel /// The pipe to read from. /// 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 3397b1c..0092370 100644 --- a/Src/SmtpServer/Protocol/DataCommand.cs +++ b/Src/SmtpServer/Protocol/DataCommand.cs @@ -52,7 +52,8 @@ 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); diff --git a/Src/SmtpServer/Protocol/EhloCommand.cs b/Src/SmtpServer/Protocol/EhloCommand.cs index a9a483e..e797506 100644 --- a/Src/SmtpServer/Protocol/EhloCommand.cs +++ b/Src/SmtpServer/Protocol/EhloCommand.cs @@ -75,9 +75,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 9ec7dc2..45feadc 100644 --- a/Src/SmtpServer/Protocol/MailCommand.cs +++ b/Src/SmtpServer/Protocol/MailCommand.cs @@ -51,7 +51,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/SmtpResponse.cs b/Src/SmtpServer/Protocol/SmtpResponse.cs index d25867f..17487c9 100644 --- a/Src/SmtpServer/Protocol/SmtpResponse.cs +++ b/Src/SmtpServer/Protocol/SmtpResponse.cs @@ -70,6 +70,11 @@ public class SmtpResponse /// public static readonly SmtpResponse AuthenticationRequired = new SmtpResponse(SmtpReplyCode.AuthenticationRequired, "authentication required"); + /// + /// 552 MaxMessageSizeExceeded + /// + public static readonly SmtpResponse MaxMessageSizeExceeded = new SmtpResponse(SmtpReplyCode.SizeLimitExceeded, "message size exceeds fixed maximium message size"); + /// /// Constructor. /// diff --git a/Src/SmtpServer/SmtpServerOptionsBuilder.cs b/Src/SmtpServer/SmtpServerOptionsBuilder.cs index 322a565..307ff8f 100644 --- a/Src/SmtpServer/SmtpServerOptionsBuilder.cs +++ b/Src/SmtpServer/SmtpServerOptionsBuilder.cs @@ -18,11 +18,12 @@ 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), }; _setters.ForEach(setter => setter(serverOptions)); @@ -98,11 +99,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; } @@ -160,9 +162,9 @@ public SmtpServerOptionsBuilder CommandWaitTimeout(TimeSpan value) class SmtpServerOptions : ISmtpServerOptions { /// - /// Gets or sets the maximum size of a message. + /// Gets or sets the maximum message size option. /// - public int MaxMessageSize { get; set; } + public IMaxMessageSizeOptions MaxMessageSizeOptions { get; set; } /// /// The maximum number of retries before quitting the session. diff --git a/Src/SmtpServer/SmtpSession.cs b/Src/SmtpServer/SmtpSession.cs index 9daaec0..292b7f5 100644 --- a/Src/SmtpServer/SmtpSession.cs +++ b/Src/SmtpServer/SmtpSession.cs @@ -131,6 +131,7 @@ await context.Pipe.Input.ReadLineAsync( return Task.CompletedTask; }, + context.ServerOptions.MaxMessageSizeOptions, cancellationTokenSource.Token).ConfigureAwait(false); return command;