Skip to content
Merged
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
21 changes: 15 additions & 6 deletions Src/SmtpServer.Tests/PipeReaderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,10 @@
// 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);

Check warning on line 30 in Src/SmtpServer.Tests/PipeReaderTests.cs

View workflow job for this annotation

GitHub Actions / build

Test methods should not call ConfigureAwait(false), as it may bypass parallelization limits. Omit ConfigureAwait, or use ConfigureAwait(true) to avoid CA2007. (https://xunit.net/xunit.analyzers/rules/xUnit1030)

// assert
Assert.Equal(5, line.Length);
Expand All @@ -39,8 +41,10 @@
// 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);

Check warning on line 47 in Src/SmtpServer.Tests/PipeReaderTests.cs

View workflow job for this annotation

GitHub Actions / build

Test methods should not call ConfigureAwait(false), as it may bypass parallelization limits. Omit ConfigureAwait, or use ConfigureAwait(true) to avoid CA2007. (https://xunit.net/xunit.analyzers/rules/xUnit1030)

// assert
Assert.Equal(7, line.Length);
Expand All @@ -54,10 +58,12 @@
// 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);

Check warning on line 64 in Src/SmtpServer.Tests/PipeReaderTests.cs

View workflow job for this annotation

GitHub Actions / build

Test methods should not call ConfigureAwait(false), as it may bypass parallelization limits. Omit ConfigureAwait, or use ConfigureAwait(true) to avoid CA2007. (https://xunit.net/xunit.analyzers/rules/xUnit1030)
var line2 = await reader.ReadLineAsync(Encoding.ASCII, maxMessageSizeOptions).ConfigureAwait(false);

Check warning on line 65 in Src/SmtpServer.Tests/PipeReaderTests.cs

View workflow job for this annotation

GitHub Actions / build

Test methods should not call ConfigureAwait(false), as it may bypass parallelization limits. Omit ConfigureAwait, or use ConfigureAwait(true) to avoid CA2007. (https://xunit.net/xunit.analyzers/rules/xUnit1030)
var line3 = await reader.ReadLineAsync(Encoding.ASCII, maxMessageSizeOptions).ConfigureAwait(false);

Check warning on line 66 in Src/SmtpServer.Tests/PipeReaderTests.cs

View workflow job for this annotation

GitHub Actions / build

Test methods should not call ConfigureAwait(false), as it may bypass parallelization limits. Omit ConfigureAwait, or use ConfigureAwait(true) to avoid CA2007. (https://xunit.net/xunit.analyzers/rules/xUnit1030)

// assert
Assert.Equal("abcde", line1);
Expand All @@ -71,6 +77,8 @@
// arrange
var reader = CreatePipeReader("abcd\r\n..1234\r\n.\r\n");

var maxMessageSizeOptions = new MaxMessageSizeOptions();

// act
var text = "";
await reader.ReadDotBlockAsync(
Expand All @@ -79,7 +87,8 @@
text = StringUtil.Create(buffer);

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

// assert
Assert.Equal("abcd\r\n.1234", text);
Expand Down
16 changes: 16 additions & 0 deletions Src/SmtpServer.Tests/SmtpServerTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using MailKit;
using MailKit.Net.Smtp;
using SmtpServer.Authentication;
using SmtpServer.ComponentModel;
using SmtpServer.Mail;
Expand All @@ -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;
Expand Down Expand Up @@ -45,7 +47,7 @@
// assert
Assert.Single(MessageStore.Messages);
Assert.Equal("[email protected]", MessageStore.Messages[0].Transaction.From.AsAddress());
Assert.Equal(1, MessageStore.Messages[0].Transaction.To.Count);

Check warning on line 50 in Src/SmtpServer.Tests/SmtpServerTests.cs

View workflow job for this annotation

GitHub Actions / build

Do not use Assert.Equal() to check for collection size. Use Assert.Single instead. (https://xunit.net/xunit.analyzers/rules/xUnit2013)
Assert.Equal("[email protected]", MessageStore.Messages[0].Transaction.To[0].AsAddress());
}
}
Expand Down Expand Up @@ -152,16 +154,30 @@

for (var i = 0; i < 5; i++)
{
Task.Delay(TimeSpan.FromMilliseconds(250)).Wait();

Check warning on line 157 in Src/SmtpServer.Tests/SmtpServerTests.cs

View workflow job for this annotation

GitHub Actions / build

Test methods should not use blocking task operations, as they can cause deadlocks. Use an async test method and await instead. (https://xunit.net/xunit.analyzers/rules/xUnit1031)
client.NoOp();
}

Task.Delay(TimeSpan.FromSeconds(5)).Wait();

Check warning on line 161 in Src/SmtpServer.Tests/SmtpServerTests.cs

View workflow job for this annotation

GitHub Actions / build

Test methods should not use blocking task operations, as they can cause deadlocks. Use an async test method and await instead. (https://xunit.net/xunit.analyzers/rules/xUnit1031)

Assert.Throws<IOException>(() => client.NoOp());
}
}

[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: "[email protected]", to: "[email protected]", text: largeMailContent);

using (CreateServer(c => c.MaxMessageSize(maxAcceptedMailMessageSize, MaxMessageSizeHandling.Strict)))
{
Assert.Throws<SmtpCommandException>(() => MailClient.Send(mailMessage));
}
}

[Fact]
public async Task WillSessionTimeoutDuringMailDataTransmission()
{
Expand Down
19 changes: 19 additions & 0 deletions Src/SmtpServer/IMaxMessageSizeOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
namespace SmtpServer
{
/// <summary>
/// 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.
/// </summary>
public interface IMaxMessageSizeOptions
{
/// <summary>
/// Gets or sets the maximum allowed message size in bytes.
/// </summary>
int Length { get; }

/// <summary>
/// Gets the handling type an oversized message.
/// </summary>
MaxMessageSizeHandling Handling { get; }
}
}
33 changes: 23 additions & 10 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,9 +22,10 @@ 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)
{
Expand All @@ -35,6 +37,11 @@ static async ValueTask ReadUntilAsync(PipeReader reader, byte[] sequence, Func<R

while (read.IsCanceled == false && read.IsCompleted == false && read.Buffer.IsEmpty == false)
{
if (maxMessageSizeOptions.Handling == MaxMessageSizeHandling.Strict && read.Buffer.Length > maxMessageSizeOptions.Length)
{
throw new SmtpResponseException(SmtpResponse.MaxMessageSizeExceeded, true);
}

if (read.Buffer.TryFind(sequence, ref head, out var tail))
{
try
Expand All @@ -60,42 +67,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 +121,7 @@ await reader.ReadLineAsync(

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

return text;
Expand All @@ -121,24 +132,26 @@ 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)
{
throw new ArgumentNullException(nameof(reader));
}

await ReadUntilAsync(
reader,
DotBlock,
reader,
DotBlock,
buffer =>
{
buffer = Unstuff(buffer);

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

static ReadOnlySequence<byte> Unstuff(ReadOnlySequence<byte> buffer)
Expand Down
4 changes: 2 additions & 2 deletions Src/SmtpServer/ISmtpServerOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,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 Down
18 changes: 18 additions & 0 deletions Src/SmtpServer/MaxMessageSizeHandling.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
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,
}
}
44 changes: 44 additions & 0 deletions Src/SmtpServer/MaxMessageSizeOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
using System;
using System.Collections.Generic;
using System.Text;

namespace SmtpServer
{
/// <summary>
/// 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.
/// </summary>
public class MaxMessageSizeOptions : IMaxMessageSizeOptions
{
/// <summary>
/// Gets or sets the maximum allowed message size in bytes,
/// as specified by the SMTP SIZE extension (RFC 1870).
/// </summary>
public int Length { get; set; }

/// <summary>
/// Gets or sets the handling strategy for messages that exceed the maximum allowed size.
/// </summary>
public MaxMessageSizeHandling Handling { get; set; }

/// <summary>
/// Initializes a new instance of the <see cref="MaxMessageSizeOptions"/> class
/// with the specified handling strategy and message size limit.
/// </summary>
/// <param name="handling">The strategy for handling messages that exceed the maximum allowed size.</param>
/// <param name="length">The maximum allowed message size in bytes.</param>
public MaxMessageSizeOptions(MaxMessageSizeHandling handling, int length)
{
Length = length;
Handling = handling;
}

/// <summary>
/// Initializes a new instance of the <see cref="MaxMessageSizeOptions"/> class with default values.
/// </summary>
public MaxMessageSizeOptions()
{

}
}
}
10 changes: 5 additions & 5 deletions Src/SmtpServer/Protocol/AuthCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@
{
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 @@ -155,13 +155,13 @@
//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 @@ -172,9 +172,9 @@
/// <param name="reader">The pipe to read from.</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)

Check warning on line 175 in Src/SmtpServer/Protocol/AuthCommand.cs

View workflow job for this annotation

GitHub Actions / build

Parameter 'maxMessageSizeOptions' has no matching param tag in the XML comment for 'AuthCommand.ReadBase64EncodedLineAsync(PipeReader, IMaxMessageSizeOptions, CancellationToken)' (but other parameters do)
{
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
3 changes: 2 additions & 1 deletion Src/SmtpServer/Protocol/DataCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
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 @@ -75,9 +75,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 @@ -51,7 +51,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
5 changes: 5 additions & 0 deletions Src/SmtpServer/Protocol/SmtpResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@ public class SmtpResponse
/// </summary>
public static readonly SmtpResponse AuthenticationRequired = new SmtpResponse(SmtpReplyCode.AuthenticationRequired, "authentication required");

/// <summary>
/// 552 MaxMessageSizeExceeded
/// </summary>
public static readonly SmtpResponse MaxMessageSizeExceeded = new SmtpResponse(SmtpReplyCode.SizeLimitExceeded, "message size exceeds fixed maximium message size");

/// <summary>
/// Constructor.
/// </summary>
Expand Down
Loading
Loading