From 62beda380e887f9c889260787f1dd3a5521a429e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=9Eorvaldur=20Hafdal?= Date: Wed, 15 Jul 2026 10:31:48 +0000 Subject: [PATCH] Support RCPT ESMTP parameters --- src/SmtpServer.Tests/SmtpParserTests.cs | 35 +++++++++++++++++++ .../Protocol/ISmtpCommandFactory.cs | 9 +++++ src/SmtpServer/Protocol/RcptCommand.cs | 18 +++++++++- src/SmtpServer/Protocol/SmtpCommandFactory.cs | 8 ++++- src/SmtpServer/Protocol/SmtpParser.cs | 15 ++++++-- .../Tracing/TracingSmtpCommandVisitor.cs | 4 ++- 6 files changed, 84 insertions(+), 5 deletions(-) diff --git a/src/SmtpServer.Tests/SmtpParserTests.cs b/src/SmtpServer.Tests/SmtpParserTests.cs index 555093cd..7553cf41 100644 --- a/src/SmtpServer.Tests/SmtpParserTests.cs +++ b/src/SmtpServer.Tests/SmtpParserTests.cs @@ -244,6 +244,41 @@ public void CanMakeRcpt(string input, string user, string host) Assert.Equal(host, ((RcptCommand)command).Address.Host); } + [Fact] + public void CanMakeRcptWithParameters() + { + // arrange + var reader = CreateReader("RCPT TO: NOTIFY=SUCCESS,FAILURE ORCPT=rfc822;cain.osullivan@gmail.com"); + + // act + var result = Parser.TryMakeRcpt(ref reader, out var command, out var errorResponse); + + // assert + Assert.True(result); + Assert.True(command is RcptCommand); + var rcpt = (RcptCommand)command; + Assert.Equal("cain.osullivan", rcpt.Address.User); + Assert.Equal("gmail.com", rcpt.Address.Host); + Assert.Equal(2, rcpt.Parameters.Count); + Assert.Equal("SUCCESS,FAILURE", rcpt.Parameters["NOTIFY"]); + Assert.Equal("rfc822;cain.osullivan@gmail.com", rcpt.Parameters["ORCPT"]); + } + + [Fact] + public void CanNotMakeRcptWithInvalidParameters() + { + // arrange + var reader = CreateReader("RCPT TO: NOTIFY="); + + // act + var result = Parser.TryMakeRcpt(ref reader, out var command, out var errorResponse); + + // assert + Assert.False(result); + Assert.Null(command); + Assert.NotNull(errorResponse); + } + [Theory] [InlineData("RCPT TO:")] [InlineData("RCPT TO:")] diff --git a/src/SmtpServer/Protocol/ISmtpCommandFactory.cs b/src/SmtpServer/Protocol/ISmtpCommandFactory.cs index fa67d5f1..050ed082 100644 --- a/src/SmtpServer/Protocol/ISmtpCommandFactory.cs +++ b/src/SmtpServer/Protocol/ISmtpCommandFactory.cs @@ -38,6 +38,15 @@ public interface ISmtpCommandFactory /// The RCPT command. SmtpCommand CreateRcpt(IMailbox address); + /// + /// Create a RCPT command. + /// + /// The address that the mail is to. + /// The optional parameters for the recipient. + /// The RCPT command. + SmtpCommand CreateRcpt(IMailbox address, IReadOnlyDictionary parameters) + => CreateRcpt(address); + /// /// Create a DATA command. /// diff --git a/src/SmtpServer/Protocol/RcptCommand.cs b/src/SmtpServer/Protocol/RcptCommand.cs index 1c1bc1ba..77cc1c7d 100644 --- a/src/SmtpServer/Protocol/RcptCommand.cs +++ b/src/SmtpServer/Protocol/RcptCommand.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using SmtpServer.ComponentModel; @@ -22,9 +23,19 @@ public sealed class RcptCommand : SmtpCommand /// Constructor. /// /// The address. - public RcptCommand(IMailbox address) : base(Command) + public RcptCommand(IMailbox address) : this(address, new Dictionary()) + { + } + + /// + /// Constructor. + /// + /// The address. + /// The optional parameters for the recipient. + public RcptCommand(IMailbox address, IReadOnlyDictionary parameters) : base(Command) { Address = address; + Parameters = parameters; } /// @@ -59,5 +70,10 @@ internal override async Task ExecuteAsync(SmtpSessionContext context, Canc /// Gets the address that the mail is to. /// public IMailbox Address { get; } + + /// + /// The list of extended recipient parameters. + /// + public IReadOnlyDictionary Parameters { get; } } } diff --git a/src/SmtpServer/Protocol/SmtpCommandFactory.cs b/src/SmtpServer/Protocol/SmtpCommandFactory.cs index e0c0d75e..bf73e926 100644 --- a/src/SmtpServer/Protocol/SmtpCommandFactory.cs +++ b/src/SmtpServer/Protocol/SmtpCommandFactory.cs @@ -30,7 +30,13 @@ public virtual SmtpCommand CreateMail(IMailbox address, IReadOnlyDictionary public virtual SmtpCommand CreateRcpt(IMailbox address) { - return new RcptCommand(address); + return CreateRcpt(address, new Dictionary()); + } + + /// + public virtual SmtpCommand CreateRcpt(IMailbox address, IReadOnlyDictionary parameters) + { + return new RcptCommand(address, parameters); } /// diff --git a/src/SmtpServer/Protocol/SmtpParser.cs b/src/SmtpServer/Protocol/SmtpParser.cs index 3b57b32c..7efe3171 100644 --- a/src/SmtpServer/Protocol/SmtpParser.cs +++ b/src/SmtpServer/Protocol/SmtpParser.cs @@ -313,9 +313,20 @@ public bool TryMakeRcpt(ref TokenReader reader, out SmtpCommand command, out Smt return false; } - // TODO: support optional service extension parameters here + reader.Skip(TokenKind.Space); + + IReadOnlyDictionary parameters; + if (reader.Peek().Kind == TokenKind.None) + { + parameters = new Dictionary(); + } + else if (reader.TryMake(TryMakeMailParameters, out parameters) == false) + { + errorResponse = SmtpResponse.SyntaxError; + return false; + } - command = _smtpCommandFactory.CreateRcpt(mailbox); + command = _smtpCommandFactory.CreateRcpt(mailbox, parameters); return true; } diff --git a/src/SmtpServer/Tracing/TracingSmtpCommandVisitor.cs b/src/SmtpServer/Tracing/TracingSmtpCommandVisitor.cs index 1e2317cf..a6815e20 100644 --- a/src/SmtpServer/Tracing/TracingSmtpCommandVisitor.cs +++ b/src/SmtpServer/Tracing/TracingSmtpCommandVisitor.cs @@ -107,7 +107,9 @@ protected override void Visit(QuitCommand command) /// The command that is being visited. protected override void Visit(RcptCommand command) { - _output.WriteLine("RCPT: Address={0}", command.Address.AsAddress()); + _output.WriteLine("RCPT: Address={0} Parameters={1}", + command.Address.AsAddress(), + string.Join(",", command.Parameters.Select(kvp => $"{kvp.Key}={kvp.Value}"))); } ///