Skip to content

Commit 6fba0aa

Browse files
authored
Add configurable max message size enforcement (#254)
* Add configurable max message size enforcement Introduces IMaxMessageSizeOptions, MaxMessageSizeOptions, and MaxMessageSizeHandling to allow configuration of maximum allowed message size and handling strategy. Updates relevant protocol and IO classes to enforce the limit, throwing MaxMessageSizeExceededException when exceeded in strict mode. Adjusts SmtpServerOptionsBuilder and ISmtpServerOptions to use the new options, and adds a test for strict enforcement. Based on this existing pull request of @boba2fett - https://github.com/cosullivan/SmtpServer/pull/213/files#diff-3ef8e28e7d91fa5d1ac55f908d172fc69242455c3a25d0a009c79cadc71916a3 * cleanup params * Change to SmtpResponseException
1 parent 049b98c commit 6fba0aa

14 files changed

+161
-33
lines changed

Src/SmtpServer.Tests/PipeReaderTests.cs

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,10 @@ public async Task CanReadLineAndRemoveTrailingCRLF()
2424
// arrange
2525
var reader = CreatePipeReader("abcde\r\n");
2626

27+
var maxMessageSizeOptions = new MaxMessageSizeOptions();
28+
2729
// act
28-
var line = await reader.ReadLineAsync(Encoding.ASCII).ConfigureAwait(false);
30+
var line = await reader.ReadLineAsync(Encoding.ASCII, maxMessageSizeOptions).ConfigureAwait(false);
2931

3032
// assert
3133
Assert.Equal(5, line.Length);
@@ -39,8 +41,10 @@ public async Task CanReadLinesWithInconsistentCRLF()
3941
// arrange
4042
var reader = CreatePipeReader("ab\rcd\ne\r\n");
4143

44+
var maxMessageSizeOptions = new MaxMessageSizeOptions();
45+
4246
// act
43-
var line = await reader.ReadLineAsync(Encoding.ASCII).ConfigureAwait(false);
47+
var line = await reader.ReadLineAsync(Encoding.ASCII, maxMessageSizeOptions).ConfigureAwait(false);
4448

4549
// assert
4650
Assert.Equal(7, line.Length);
@@ -54,10 +58,12 @@ public async Task CanReadMultipleLines()
5458
// arrange
5559
var reader = CreatePipeReader("abcde\r\nfghij\r\nklmno\r\n");
5660

61+
var maxMessageSizeOptions = new MaxMessageSizeOptions();
62+
5763
// act
58-
var line1 = await reader.ReadLineAsync(Encoding.ASCII).ConfigureAwait(false);
59-
var line2 = await reader.ReadLineAsync(Encoding.ASCII).ConfigureAwait(false);
60-
var line3 = await reader.ReadLineAsync(Encoding.ASCII).ConfigureAwait(false);
64+
var line1 = await reader.ReadLineAsync(Encoding.ASCII, maxMessageSizeOptions).ConfigureAwait(false);
65+
var line2 = await reader.ReadLineAsync(Encoding.ASCII, maxMessageSizeOptions).ConfigureAwait(false);
66+
var line3 = await reader.ReadLineAsync(Encoding.ASCII, maxMessageSizeOptions).ConfigureAwait(false);
6167

6268
// assert
6369
Assert.Equal("abcde", line1);
@@ -71,6 +77,8 @@ public async Task CanReadBlockWithDotStuffingRemoved()
7177
// arrange
7278
var reader = CreatePipeReader("abcd\r\n..1234\r\n.\r\n");
7379

80+
var maxMessageSizeOptions = new MaxMessageSizeOptions();
81+
7482
// act
7583
var text = "";
7684
await reader.ReadDotBlockAsync(
@@ -79,7 +87,8 @@ await reader.ReadDotBlockAsync(
7987
text = StringUtil.Create(buffer);
8088

8189
return Task.CompletedTask;
82-
});
90+
},
91+
maxMessageSizeOptions);
8392

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

Src/SmtpServer.Tests/SmtpServerTests.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using MailKit;
2+
using MailKit.Net.Smtp;
23
using SmtpServer.Authentication;
34
using SmtpServer.ComponentModel;
45
using SmtpServer.Mail;
@@ -9,6 +10,7 @@
910
using System;
1011
using System.Diagnostics;
1112
using System.IO;
13+
using System.Linq;
1214
using System.Net;
1315
using System.Net.Security;
1416
using System.Net.Sockets;
@@ -162,6 +164,20 @@ public void WillTimeoutWaitingForCommand()
162164
}
163165
}
164166

167+
[Fact]
168+
public void WillTerminateDueToTooMuchData()
169+
{
170+
var maxAcceptedMailMessageSize = 50;
171+
172+
var largeMailContent = string.Concat(Enumerable.Repeat("Too long for 1024 bytes", 1000));
173+
using var mailMessage = MailClient.Message(from: "[email protected]", to: "[email protected]", text: largeMailContent);
174+
175+
using (CreateServer(c => c.MaxMessageSize(maxAcceptedMailMessageSize, MaxMessageSizeHandling.Strict)))
176+
{
177+
Assert.Throws<SmtpCommandException>(() => MailClient.Send(mailMessage));
178+
}
179+
}
180+
165181
[Fact]
166182
public async Task WillSessionTimeoutDuringMailDataTransmission()
167183
{
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
namespace SmtpServer
2+
{
3+
/// <summary>
4+
/// Defines configuration options for enforcing a maximum allowed message size according to the SMTP SIZE extension (RFC 1870).
5+
/// Includes the size limit in bytes and the handling strategy for oversized messages.
6+
/// </summary>
7+
public interface IMaxMessageSizeOptions
8+
{
9+
/// <summary>
10+
/// Gets or sets the maximum allowed message size in bytes.
11+
/// </summary>
12+
int Length { get; }
13+
14+
/// <summary>
15+
/// Gets the handling type an oversized message.
16+
/// </summary>
17+
MaxMessageSizeHandling Handling { get; }
18+
}
19+
}

Src/SmtpServer/IO/PipeReaderExtensions.cs

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.Text;
55
using System.Threading;
66
using System.Threading.Tasks;
7+
using SmtpServer.Protocol;
78
using SmtpServer.Text;
89

910
namespace SmtpServer.IO
@@ -21,9 +22,10 @@ internal static class PipeReaderExtensions
2122
/// <param name="reader">The reader to read from.</param>
2223
/// <param name="sequence">The sequence to find to terminate the read operation.</param>
2324
/// <param name="func">The callback to execute to process the buffer.</param>
25+
/// <param name="maxMessageSizeOptions">Handling of MaxMessageSize.</param>
2426
/// <param name="cancellationToken">The cancellation token.</param>
2527
/// <returns>The value that was read from the buffer.</returns>
26-
static async ValueTask ReadUntilAsync(PipeReader reader, byte[] sequence, Func<ReadOnlySequence<byte>, Task> func, CancellationToken cancellationToken)
28+
static async ValueTask ReadUntilAsync(PipeReader reader, byte[] sequence, Func<ReadOnlySequence<byte>, Task> func, IMaxMessageSizeOptions maxMessageSizeOptions, CancellationToken cancellationToken)
2729
{
2830
if (reader == null)
2931
{
@@ -35,6 +37,11 @@ static async ValueTask ReadUntilAsync(PipeReader reader, byte[] sequence, Func<R
3537

3638
while (read.IsCanceled == false && read.IsCompleted == false && read.Buffer.IsEmpty == false)
3739
{
40+
if (maxMessageSizeOptions.Handling == MaxMessageSizeHandling.Strict && read.Buffer.Length > maxMessageSizeOptions.Length)
41+
{
42+
throw new SmtpResponseException(SmtpResponse.MaxMessageSizeExceeded, true);
43+
}
44+
3845
if (read.Buffer.TryFind(sequence, ref head, out var tail))
3946
{
4047
try
@@ -60,42 +67,45 @@ static async ValueTask ReadUntilAsync(PipeReader reader, byte[] sequence, Func<R
6067
/// </summary>
6168
/// <param name="reader">The reader to read from.</param>
6269
/// <param name="func">The action to process the buffer.</param>
70+
/// <param name="maxMessageSizeOptions">Handling of MaxMessageSize.</param>
6371
/// <param name="cancellationToken">The cancellation token.</param>
6472
/// <returns>A task that can be used to wait on the operation on complete.</returns>
65-
internal static ValueTask ReadLineAsync(this PipeReader reader, Func<ReadOnlySequence<byte>, Task> func, CancellationToken cancellationToken = default)
73+
internal static ValueTask ReadLineAsync(this PipeReader reader, Func<ReadOnlySequence<byte>, Task> func, IMaxMessageSizeOptions maxMessageSizeOptions, CancellationToken cancellationToken = default)
6674
{
6775
if (reader == null)
6876
{
6977
throw new ArgumentNullException(nameof(reader));
7078
}
7179

72-
return ReadUntilAsync(reader, CRLF, func, cancellationToken);
80+
return ReadUntilAsync(reader, CRLF, func, maxMessageSizeOptions, cancellationToken);
7381
}
7482

7583
/// <summary>
7684
/// Reads a line from the reader.
7785
/// </summary>
7886
/// <param name="reader">The reader to read from.</param>
87+
/// <param name="maxMessageSizeOptions">Handling of MaxMessageSize.</param>
7988
/// <param name="cancellationToken">The cancellation token.</param>
8089
/// <returns>A task that can be used to wait on the operation on complete.</returns>
81-
internal static ValueTask<string> ReadLineAsync(this PipeReader reader, CancellationToken cancellationToken = default)
90+
internal static ValueTask<string> ReadLineAsync(this PipeReader reader, IMaxMessageSizeOptions maxMessageSizeOptions, CancellationToken cancellationToken = default)
8291
{
8392
if (reader == null)
8493
{
8594
throw new ArgumentNullException(nameof(reader));
8695
}
8796

88-
return reader.ReadLineAsync(Encoding.ASCII, cancellationToken);
97+
return reader.ReadLineAsync(Encoding.ASCII, maxMessageSizeOptions, cancellationToken);
8998
}
9099

91100
/// <summary>
92101
/// Reads a line from the reader.
93102
/// </summary>
94103
/// <param name="reader">The reader to read from.</param>
95104
/// <param name="encoding">The encoding to use when converting the input.</param>
105+
/// <param name="maxMessageSizeOptions"> Handling of MaxMessageSize</param>
96106
/// <param name="cancellationToken">The cancellation token.</param>
97107
/// <returns>A task that can be used to wait on the operation on complete.</returns>
98-
internal static async ValueTask<string> ReadLineAsync(this PipeReader reader, Encoding encoding, CancellationToken cancellationToken = default)
108+
internal static async ValueTask<string> ReadLineAsync(this PipeReader reader, Encoding encoding, IMaxMessageSizeOptions maxMessageSizeOptions, CancellationToken cancellationToken = default)
99109
{
100110
if (reader == null)
101111
{
@@ -111,6 +121,7 @@ await reader.ReadLineAsync(
111121

112122
return Task.CompletedTask;
113123
},
124+
maxMessageSizeOptions,
114125
cancellationToken);
115126

116127
return text;
@@ -121,24 +132,26 @@ await reader.ReadLineAsync(
121132
/// </summary>
122133
/// <param name="reader">The reader to read from.</param>
123134
/// <param name="func">The action to process the buffer.</param>
135+
/// <param name="maxMessageSizeOptions">Handling of MaxMessageSize.</param>
124136
/// <param name="cancellationToken">The cancellation token.</param>
125137
/// <returns>The value that was read from the buffer.</returns>
126-
internal static async ValueTask ReadDotBlockAsync(this PipeReader reader, Func<ReadOnlySequence<byte>, Task> func, CancellationToken cancellationToken = default)
138+
internal static async ValueTask ReadDotBlockAsync(this PipeReader reader, Func<ReadOnlySequence<byte>, Task> func, IMaxMessageSizeOptions maxMessageSizeOptions, CancellationToken cancellationToken = default)
127139
{
128140
if (reader == null)
129141
{
130142
throw new ArgumentNullException(nameof(reader));
131143
}
132144

133145
await ReadUntilAsync(
134-
reader,
135-
DotBlock,
146+
reader,
147+
DotBlock,
136148
buffer =>
137149
{
138150
buffer = Unstuff(buffer);
139151

140152
return func(buffer);
141-
},
153+
},
154+
maxMessageSizeOptions,
142155
cancellationToken);
143156

144157
static ReadOnlySequence<byte> Unstuff(ReadOnlySequence<byte> buffer)

Src/SmtpServer/ISmtpServerOptions.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ namespace SmtpServer
99
public interface ISmtpServerOptions
1010
{
1111
/// <summary>
12-
/// Gets the maximum size of a message.
12+
/// Gets the maximum message size option.
1313
/// </summary>
14-
int MaxMessageSize { get; }
14+
IMaxMessageSizeOptions MaxMessageSizeOptions { get; }
1515

1616
/// <summary>
1717
/// The maximum number of retries before quitting the session.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
namespace SmtpServer
2+
{
3+
/// <summary>
4+
/// Choose how MaxMessageSize limit should be considered
5+
/// </summary>
6+
public enum MaxMessageSizeHandling
7+
{
8+
/// <summary>
9+
/// Use the size limit for the SIZE extension of ESMTP
10+
/// </summary>
11+
Ignore = 0,
12+
13+
/// <summary>
14+
/// Close the session after too much data has been sent
15+
/// </summary>
16+
Strict = 1,
17+
}
18+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Text;
4+
5+
namespace SmtpServer
6+
{
7+
/// <summary>
8+
/// Represents configuration settings for enforcing a maximum message size in SMTP,
9+
/// including the size limit in bytes and the behavior when that limit is exceeded.
10+
/// </summary>
11+
public class MaxMessageSizeOptions : IMaxMessageSizeOptions
12+
{
13+
/// <summary>
14+
/// Gets or sets the maximum allowed message size in bytes,
15+
/// as specified by the SMTP SIZE extension (RFC 1870).
16+
/// </summary>
17+
public int Length { get; set; }
18+
19+
/// <summary>
20+
/// Gets or sets the handling strategy for messages that exceed the maximum allowed size.
21+
/// </summary>
22+
public MaxMessageSizeHandling Handling { get; set; }
23+
24+
/// <summary>
25+
/// Initializes a new instance of the <see cref="MaxMessageSizeOptions"/> class
26+
/// with the specified handling strategy and message size limit.
27+
/// </summary>
28+
/// <param name="handling">The strategy for handling messages that exceed the maximum allowed size.</param>
29+
/// <param name="length">The maximum allowed message size in bytes.</param>
30+
public MaxMessageSizeOptions(MaxMessageSizeHandling handling, int length)
31+
{
32+
Length = length;
33+
Handling = handling;
34+
}
35+
36+
/// <summary>
37+
/// Initializes a new instance of the <see cref="MaxMessageSizeOptions"/> class with default values.
38+
/// </summary>
39+
public MaxMessageSizeOptions()
40+
{
41+
42+
}
43+
}
44+
}

Src/SmtpServer/Protocol/AuthCommand.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ async Task<bool> TryPlainAsync(ISessionContext context, CancellationToken cancel
106106
{
107107
await context.Pipe.Output.WriteReplyAsync(new SmtpResponse(SmtpReplyCode.ContinueWithAuth, " "), cancellationToken).ConfigureAwait(false);
108108

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

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

158-
_user = await ReadBase64EncodedLineAsync(context.Pipe.Input, cancellationToken).ConfigureAwait(false);
158+
_user = await ReadBase64EncodedLineAsync(context.Pipe.Input, context.ServerOptions.MaxMessageSizeOptions, cancellationToken).ConfigureAwait(false);
159159
}
160160

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

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

166166
return true;
167167
}
@@ -172,9 +172,9 @@ async Task<bool> TryLoginAsync(ISessionContext context, CancellationToken cancel
172172
/// <param name="reader">The pipe to read from.</param>
173173
/// <param name="cancellationToken">The cancellation token.</param>
174174
/// <returns>The decoded Base64 string.</returns>
175-
static async Task<string> ReadBase64EncodedLineAsync(PipeReader reader, CancellationToken cancellationToken)
175+
static async Task<string> ReadBase64EncodedLineAsync(PipeReader reader, IMaxMessageSizeOptions maxMessageSizeOptions, CancellationToken cancellationToken)
176176
{
177-
var text = await reader.ReadLineAsync(cancellationToken);
177+
var text = await reader.ReadLineAsync(maxMessageSizeOptions, cancellationToken);
178178

179179
return text == null ? string.Empty : Encoding.UTF8.GetString(Convert.FromBase64String(text));
180180
}

Src/SmtpServer/Protocol/DataCommand.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@ await context.Pipe.Input.ReadDotBlockAsync(
5252
{
5353
// ReSharper disable once AccessToDisposedClosure
5454
response = await container.Instance.SaveAsync(context, context.Transaction, buffer, cancellationToken).ConfigureAwait(false);
55-
},
55+
},
56+
context.ServerOptions.MaxMessageSizeOptions,
5657
cancellationToken).ConfigureAwait(false);
5758

5859
await context.Pipe.Output.WriteReplyAsync(response, cancellationToken).ConfigureAwait(false);

Src/SmtpServer/Protocol/EhloCommand.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,9 +75,9 @@ protected virtual IEnumerable<string> GetExtensions(ISessionContext context)
7575
yield return "STARTTLS";
7676
}
7777

78-
if (context.ServerOptions.MaxMessageSize > 0)
78+
if (context.ServerOptions.MaxMessageSizeOptions.Length > 0)
7979
{
80-
yield return $"SIZE {context.ServerOptions.MaxMessageSize}";
80+
yield return $"SIZE {context.ServerOptions.MaxMessageSizeOptions.Length}";
8181
}
8282

8383
if (IsPlainLoginAllowed(context))

0 commit comments

Comments
 (0)