Skip to content
Closed
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
18 changes: 7 additions & 11 deletions src/SmtpServer.Benchmarks/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,13 @@ public class Program
{
public static void Main(string[] args)
{
//var summary = BenchmarkRunner.Run<TokenizerBenchmarks>(
// ManualConfig
// .Create(DefaultConfig.Instance)
// .With(ConfigOptions.DisableOptimizationsValidator));

//var summary = BenchmarkRunner.Run<ThroughputBenchmarks>();

var summary = BenchmarkRunner.Run<ThroughputBenchmarks>(
ManualConfig
.Create(DefaultConfig.Instance)
.With(ConfigOptions.DisableOptimizationsValidator));
BenchmarkSwitcher
.FromAssembly(typeof(Program).Assembly)
.Run(
args,
ManualConfig
.Create(DefaultConfig.Instance)
.WithOptions(ConfigOptions.DisableOptimizationsValidator));
}
}
}
150 changes: 150 additions & 0 deletions src/SmtpServer.Benchmarks/SmtpParserBenchmarks.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
using System.Buffers;
using System.Text;
using BenchmarkDotNet.Attributes;
using SmtpServer.Protocol;
using SmtpServer.Text;

namespace SmtpServer.Benchmarks
{
[MemoryDiagnoser]
[ShortRunJob]
public class SmtpParserBenchmarks
{
static readonly SmtpResponse UnrecognizedCommand = new SmtpResponse(SmtpReplyCode.CommandNotImplemented, "Unrecognized command");

readonly SmtpParser _parser = new SmtpParser(new SmtpCommandFactory());
byte[] _buffer;

[Params(
"EHLO example.com",
"MAIL FROM:<sender@example.com> SIZE=12345 SMTPUTF8",
"RCPT TO:<recipient@example.com>",
"AUTH PLAIN Y2Fpbi5vc3VsbGl2YW5AZ21haWwuY29t",
"PROXY TCP4 192.168.1.1 192.168.1.2 1234 16789",
"ABCDE FGHIJ KLMNO")]
public string Input { get; set; }

[GlobalSetup]
public void Setup()
{
_buffer = Encoding.UTF8.GetBytes(Input);
}

[Benchmark(Baseline = true)]
public bool LegacySequentialCandidateLoop()
{
var sequence = new ReadOnlySequence<byte>(_buffer);

return LegacyTryMake(ref sequence, out _, out _);
}

[Benchmark]
public bool SinglePassVerbDispatch()
{
var sequence = new ReadOnlySequence<byte>(_buffer);

return _parser.TryMake(ref sequence, out _, out _);
}

bool LegacyTryMake(ref ReadOnlySequence<byte> buffer, out SmtpCommand command, out SmtpResponse errorResponse)
{
return TryMakeEhlo(buffer, out command, out errorResponse)
|| TryMakeHelo(buffer, out command, out errorResponse)
|| TryMakeMail(buffer, out command, out errorResponse)
|| TryMakeRcpt(buffer, out command, out errorResponse)
|| TryMakeData(buffer, out command, out errorResponse)
|| TryMakeQuit(buffer, out command, out errorResponse)
|| TryMakeRset(buffer, out command, out errorResponse)
|| TryMakeNoop(buffer, out command, out errorResponse)
|| TryMakeStartTls(buffer, out command, out errorResponse)
|| TryMakeAuth(buffer, out command, out errorResponse)
|| TryMakeProxy(buffer, out command, out errorResponse)
|| MakeUnrecognized(out command, out errorResponse);
}

bool TryMakeEhlo(ReadOnlySequence<byte> buffer, out SmtpCommand command, out SmtpResponse errorResponse)
{
var reader = new TokenReader(buffer);

return _parser.TryMakeEhlo(ref reader, out command, out errorResponse);
}

bool TryMakeHelo(ReadOnlySequence<byte> buffer, out SmtpCommand command, out SmtpResponse errorResponse)
{
var reader = new TokenReader(buffer);

return _parser.TryMakeHelo(ref reader, out command, out errorResponse);
}

bool TryMakeMail(ReadOnlySequence<byte> buffer, out SmtpCommand command, out SmtpResponse errorResponse)
{
var reader = new TokenReader(buffer);

return _parser.TryMakeMail(ref reader, out command, out errorResponse);
}

bool TryMakeRcpt(ReadOnlySequence<byte> buffer, out SmtpCommand command, out SmtpResponse errorResponse)
{
var reader = new TokenReader(buffer);

return _parser.TryMakeRcpt(ref reader, out command, out errorResponse);
}

bool TryMakeData(ReadOnlySequence<byte> buffer, out SmtpCommand command, out SmtpResponse errorResponse)
{
var reader = new TokenReader(buffer);

return _parser.TryMakeData(ref reader, out command, out errorResponse);
}

bool TryMakeQuit(ReadOnlySequence<byte> buffer, out SmtpCommand command, out SmtpResponse errorResponse)
{
var reader = new TokenReader(buffer);

return _parser.TryMakeQuit(ref reader, out command, out errorResponse);
}

bool TryMakeRset(ReadOnlySequence<byte> buffer, out SmtpCommand command, out SmtpResponse errorResponse)
{
var reader = new TokenReader(buffer);

return _parser.TryMakeRset(ref reader, out command, out errorResponse);
}

bool TryMakeNoop(ReadOnlySequence<byte> buffer, out SmtpCommand command, out SmtpResponse errorResponse)
{
var reader = new TokenReader(buffer);

return _parser.TryMakeNoop(ref reader, out command, out errorResponse);
}

bool TryMakeStartTls(ReadOnlySequence<byte> buffer, out SmtpCommand command, out SmtpResponse errorResponse)
{
var reader = new TokenReader(buffer);

return _parser.TryMakeStartTls(ref reader, out command, out errorResponse);
}

bool TryMakeAuth(ReadOnlySequence<byte> buffer, out SmtpCommand command, out SmtpResponse errorResponse)
{
var reader = new TokenReader(buffer);

return _parser.TryMakeAuth(ref reader, out command, out errorResponse);
}

bool TryMakeProxy(ReadOnlySequence<byte> buffer, out SmtpCommand command, out SmtpResponse errorResponse)
{
var reader = new TokenReader(buffer);

return _parser.TryMakeProxy(ref reader, out command, out errorResponse);
}

static bool MakeUnrecognized(out SmtpCommand command, out SmtpResponse errorResponse)
{
command = null;
errorResponse = UnrecognizedCommand;

return false;
}
}
}
27 changes: 27 additions & 0 deletions src/SmtpServer.Tests/SmtpParserTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,33 @@ public void CanMakeUnrecognized()
Assert.Equal(SmtpReplyCode.CommandNotImplemented, errorResponse.ReplyCode);
}

[Theory]
[InlineData("ehlo example.com", typeof(EhloCommand))]
[InlineData("HELO example.com", typeof(HeloCommand))]
[InlineData("MAIL FROM:<cain.osullivan@gmail.com>", typeof(MailCommand))]
[InlineData("RCPT TO:<cain.osullivan@gmail.com>", typeof(RcptCommand))]
[InlineData("DATA", typeof(DataCommand))]
[InlineData("QUIT", typeof(QuitCommand))]
[InlineData("RSET", typeof(RsetCommand))]
[InlineData("NOOP", typeof(NoopCommand))]
[InlineData("STARTTLS", typeof(StartTlsCommand))]
[InlineData("AUTH PLAIN Y2Fpbi5vc3VsbGl2YW5AZ21haWwuY29t", typeof(AuthCommand))]
[InlineData("PROXY UNKNOWN", typeof(ProxyCommand))]
public void CanMakeKnownCommandUsingTopLevelDispatch(string input, Type commandType)
{
// arrange
var buffer = Encoding.UTF8.GetBytes(input);
var sequence = new ReadOnlySequence<byte>(buffer, 0, buffer.Length);

// act
var result = Parser.TryMake(ref sequence, out var command, out var errorResponse);

// assert
Assert.True(result);
Assert.Equal(commandType, command.GetType());
Assert.Null(errorResponse);
}

[Fact]
public void CanMakeQuit()
{
Expand Down
111 changes: 96 additions & 15 deletions src/SmtpServer/Protocol/SmtpParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,25 +39,106 @@ public SmtpParser(ISmtpCommandFactory smtpCommandFactory)
/// <returns>Returns true if a command could be made, false if not.</returns>
public bool TryMake(ref ReadOnlySequence<byte> buffer, out SmtpCommand command, out SmtpResponse errorResponse)
{
return Make(buffer, TryMakeEhlo, out command, out errorResponse)
|| Make(buffer, TryMakeHelo, out command, out errorResponse)
|| Make(buffer, TryMakeMail, out command, out errorResponse)
|| Make(buffer, TryMakeRcpt, out command, out errorResponse)
|| Make(buffer, TryMakeData, out command, out errorResponse)
|| Make(buffer, TryMakeQuit, out command, out errorResponse)
|| Make(buffer, TryMakeRset, out command, out errorResponse)
|| Make(buffer, TryMakeNoop, out command, out errorResponse)
|| Make(buffer, TryMakeStartTls, out command, out errorResponse)
|| Make(buffer, TryMakeAuth, out command, out errorResponse)
|| Make(buffer, TryMakeProxy, out command, out errorResponse)
|| Make(buffer, MakeUnrecognized, out command, out errorResponse);
var reader = new TokenReader(buffer);
var verb = reader.Peek();

static bool Make(ReadOnlySequence<byte> buffer, TryMakeDelegate tryMakeDelegate, out SmtpCommand command, out SmtpResponse errorResponse)
if (verb.Kind != TokenKind.Text)
{
var reader = new TokenReader(buffer);
return MakeUnrecognized(ref reader, out command, out errorResponse);
}

if (IsVerb(verb.Text, "EHLO"))
{
return Make(ref reader, TryMakeEhlo, out command, out errorResponse);
}

if (IsVerb(verb.Text, "HELO"))
{
return Make(ref reader, TryMakeHelo, out command, out errorResponse);
}

if (IsVerb(verb.Text, "MAIL"))
{
return Make(ref reader, TryMakeMail, out command, out errorResponse);
}

if (IsVerb(verb.Text, "RCPT"))
{
return Make(ref reader, TryMakeRcpt, out command, out errorResponse);
}

if (IsVerb(verb.Text, "DATA"))
{
return Make(ref reader, TryMakeData, out command, out errorResponse);
}

if (IsVerb(verb.Text, "QUIT"))
{
return Make(ref reader, TryMakeQuit, out command, out errorResponse);
}

if (IsVerb(verb.Text, "RSET"))
{
return Make(ref reader, TryMakeRset, out command, out errorResponse);
}

if (IsVerb(verb.Text, "NOOP"))
{
return Make(ref reader, TryMakeNoop, out command, out errorResponse);
}

if (IsVerb(verb.Text, "STARTTLS"))
{
return Make(ref reader, TryMakeStartTls, out command, out errorResponse);
}

if (IsVerb(verb.Text, "AUTH"))
{
return Make(ref reader, TryMakeAuth, out command, out errorResponse);
}

return tryMakeDelegate(ref reader, out command, out errorResponse);
if (IsVerb(verb.Text, "PROXY"))
{
return Make(ref reader, TryMakeProxy, out command, out errorResponse);
}

return MakeUnrecognized(ref reader, out command, out errorResponse);

static bool Make(ref TokenReader reader, TryMakeDelegate tryMakeDelegate, out SmtpCommand command, out SmtpResponse errorResponse)
{
if (tryMakeDelegate(ref reader, out command, out errorResponse))
{
return true;
}

command = null;
errorResponse = UnrecognizedCommand;
return false;
}
}

static bool IsVerb(ReadOnlySpan<byte> verb, string expected)
{
if (verb.Length != expected.Length)
{
return false;
}

for (var i = 0; i < verb.Length; i++)
{
var ch = verb[i];
if (ch >= 'a' && ch <= 'z')
{
ch = (byte)(ch - 32);
}

if (ch != expected[i])
{
return false;
}
}

return true;
}

static bool MakeUnrecognized(ref TokenReader reader, out SmtpCommand command, out SmtpResponse errorResponse)
Expand Down