├── src ├── SmtpServer │ ├── AssemblyInfo.cs │ ├── Storage │ │ ├── IMessageStoreFactory.cs │ │ ├── IMailboxFilterFactory.cs │ │ ├── IMessageStore.cs │ │ ├── CompositeMailboxFilterFactory.cs │ │ ├── DelegatingMessageStoreFactory.cs │ │ ├── DelegatingMailboxFilterFactory.cs │ │ ├── MessageStore.cs │ │ ├── MailboxFilter.cs │ │ ├── IMailboxFilter.cs │ │ └── CompositeMailboxFilter.cs │ ├── StateMachine │ │ ├── SmtpStateId.cs │ │ ├── SmtpStateTransition.cs │ │ ├── SmtpState.cs │ │ ├── SmtpStateMachine.cs │ │ └── SmtpStateTable.cs │ ├── Authentication │ │ ├── IUserAuthenticatorFactory.cs │ │ ├── IUserAuthenticator.cs │ │ ├── DelegatingUserAuthenticatorFactory.cs │ │ ├── UserAuthenticator.cs │ │ └── DelegatingUserAuthenticator.cs │ ├── IO │ │ ├── PipelineException.cs │ │ ├── ByteArraySegment.cs │ │ ├── ByteArraySegmentList.cs │ │ ├── ISecurableDuplexPipe.cs │ │ ├── PipeWriterExtensions.cs │ │ └── SecurableDuplexPipe.cs │ ├── Protocol │ │ ├── AuthenticationMethod.cs │ │ ├── SmtpCommand.cs │ │ ├── NoopCommand.cs │ │ ├── RsetCommand.cs │ │ ├── StartTlsCommand.cs │ │ ├── HeloCommand.cs │ │ ├── SmtpResponseException.cs │ │ ├── QuitCommand.cs │ │ ├── ProxyCommand.cs │ │ ├── SmtpCommandFactory.cs │ │ ├── RcptCommand.cs │ │ ├── DataCommand.cs │ │ ├── ISmtpCommandFactory.cs │ │ ├── SmtpResponse.cs │ │ ├── EhloCommand.cs │ │ ├── MailCommand.cs │ │ └── SmtpCommandVisitor.cs │ ├── MaxMessageSizeHandling.cs │ ├── ICertificateFactory.cs │ ├── SessionEventArgs.cs │ ├── Net │ │ ├── IEndpointListenerFactory.cs │ │ ├── IEndpointListener.cs │ │ ├── EndPointEventArgs.cs │ │ ├── EndpointListenerFactory.cs │ │ └── EndpointListener.cs │ ├── ComponentModel │ │ ├── ISessionContextInstanceFactory.cs │ │ ├── DisposableContainer.cs │ │ └── ServiceProviderExtensions.cs │ ├── IMaxMessageSizeOptions.cs │ ├── SessionFaultedEventArgs.cs │ ├── SmtpCommandEventArgs.cs │ ├── IMessageTransaction.cs │ ├── SmtpResponseExceptionEventArgs.cs │ ├── SmtpMessageTransaction.cs │ ├── AuthenticationContext.cs │ ├── Mail │ │ ├── IMailbox.cs │ │ └── Mailbox.cs │ ├── IEndpointDefinition.cs │ ├── MaxMessageSizeOptions.cs │ ├── SmtpServer.csproj │ ├── Text │ │ ├── StringUtil.cs │ │ ├── TokenKind.cs │ │ └── Token.cs │ ├── ISmtpServerOptions.cs │ ├── ISessionContext.cs │ ├── Extensions │ │ └── TaskExtensions.cs │ ├── SmtpSessionManager.cs │ ├── SmtpSessionContext.cs │ └── Tracing │ │ └── TracingSmtpCommandVisitor.cs ├── SmtpServer.Tests │ ├── SmtpServer.Tests.csproj │ ├── SmtpServerDisposable.cs │ ├── Mocks │ │ └── MockMessageStore.cs │ ├── RawSmtpClient.cs │ ├── MailClient.cs │ ├── PipeReaderTests.cs │ └── TokenReaderTests.cs ├── SmtpServer.Benchmarks │ ├── Program.cs │ ├── Test1.eml │ ├── SmtpServer.Benchmarks.csproj │ ├── TokenizerBenchmarks.cs │ └── ThroughputBenchmarks.cs └── SmtpServer.sln ├── examples ├── WorkerService │ ├── appsettings.json │ ├── appsettings.Development.json │ ├── Properties │ │ └── launchSettings.json │ ├── WorkerService.csproj │ ├── Worker.cs │ ├── Program.cs │ └── ConsoleMessageStore.cs └── SampleApp │ ├── TaskExtensions.cs │ ├── SampleApp.csproj │ ├── Examples │ ├── SimpleExample.cs │ ├── SimpleServerExample.cs │ ├── DependencyInjectionExample.cs │ ├── CommonPortsExample.cs │ ├── ServerCancellingExample.cs │ ├── ServerShutdownExample.cs │ ├── SecureServerExample.cs │ ├── SessionTracingExample.cs │ └── SessionContextExample.cs │ ├── SampleUserAuthenticator.cs │ ├── Program.cs │ ├── SampleMessageStore.cs │ ├── SampleMailClient.cs │ └── SampleMailboxFilter.cs ├── .github ├── FUNDING.yml └── workflows │ ├── build.yml │ └── build-release.yml ├── LICENSE ├── CHANGELOG.md ├── .gitignore └── README.md /src/SmtpServer/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | [assembly: InternalsVisibleTo("SmtpServer.Tests")] 4 | [assembly: InternalsVisibleTo("SmtpServer.Benchmarks")] 5 | [assembly: InternalsVisibleTo("SampleApp")] 6 | -------------------------------------------------------------------------------- /examples/WorkerService/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/WorkerService/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/WorkerService/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "WorkerService": { 4 | "commandName": "Project", 5 | "environmentVariables": { 6 | "DOTNET_ENVIRONMENT": "Development" 7 | } 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/SmtpServer/Storage/IMessageStoreFactory.cs: -------------------------------------------------------------------------------- 1 | using SmtpServer.ComponentModel; 2 | 3 | namespace SmtpServer.Storage 4 | { 5 | /// 6 | /// Message Store Factory Interface 7 | /// 8 | public interface IMessageStoreFactory : ISessionContextInstanceFactory { } 9 | } 10 | -------------------------------------------------------------------------------- /src/SmtpServer/Storage/IMailboxFilterFactory.cs: -------------------------------------------------------------------------------- 1 | using SmtpServer.ComponentModel; 2 | 3 | namespace SmtpServer.Storage 4 | { 5 | /// 6 | /// Mailbox Filter Factory Interface 7 | /// 8 | public interface IMailboxFilterFactory : ISessionContextInstanceFactory { } 9 | } 10 | -------------------------------------------------------------------------------- /src/SmtpServer/StateMachine/SmtpStateId.cs: -------------------------------------------------------------------------------- 1 | namespace SmtpServer.StateMachine 2 | { 3 | internal enum SmtpStateId 4 | { 5 | None = 0, 6 | Initialized = 1, 7 | WaitingForMail = 2, 8 | WaitingForMailSecure = 3, 9 | WithinTransaction = 4, 10 | CanAcceptData = 5, 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/SmtpServer/Authentication/IUserAuthenticatorFactory.cs: -------------------------------------------------------------------------------- 1 | using SmtpServer.ComponentModel; 2 | 3 | namespace SmtpServer.Authentication 4 | { 5 | /// 6 | /// User Authenticator Factory Interface 7 | /// 8 | public interface IUserAuthenticatorFactory : ISessionContextInstanceFactory { } 9 | } 10 | -------------------------------------------------------------------------------- /src/SmtpServer/IO/PipelineException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SmtpServer.IO 4 | { 5 | /// 6 | /// Pipeline Exception 7 | /// 8 | public abstract class PipelineException : Exception { } 9 | 10 | /// 11 | /// Pipeline Cancelled Exception 12 | /// 13 | public sealed class PipelineCancelledException : PipelineException { } 14 | } 15 | -------------------------------------------------------------------------------- /src/SmtpServer/Protocol/AuthenticationMethod.cs: -------------------------------------------------------------------------------- 1 | namespace SmtpServer.Protocol 2 | { 3 | /// 4 | /// Authentication Method 5 | /// 6 | public enum AuthenticationMethod 7 | { 8 | /// 9 | /// Login 10 | /// 11 | Login, 12 | 13 | /// 14 | /// Plain 15 | /// 16 | Plain 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/SampleApp/TaskExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace SampleApp 5 | { 6 | public static class TaskExtensions 7 | { 8 | public static void WaitWithoutException(this Task task) 9 | { 10 | try 11 | { 12 | task.Wait(); 13 | } 14 | catch (AggregateException e) 15 | { 16 | e.Handle(exception => exception is OperationCanceledException); 17 | } 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/WorkerService/WorkerService.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | dotnet-WorkerService-1397719A-187C-45F4-8DB3-2427A449DD89 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/SmtpServer/MaxMessageSizeHandling.cs: -------------------------------------------------------------------------------- 1 | namespace SmtpServer 2 | { 3 | /// 4 | /// Choose how MaxMessageSize limit should be considered 5 | /// 6 | public enum MaxMessageSizeHandling 7 | { 8 | /// 9 | /// Use the size limit for the SIZE extension of ESMTP 10 | /// 11 | Ignore = 0, 12 | 13 | /// 14 | /// Close the session after too much data has been sent 15 | /// 16 | Strict = 1, 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/WorkerService/Worker.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using Microsoft.Extensions.Hosting; 4 | 5 | namespace WorkerService 6 | { 7 | public sealed class Worker : BackgroundService 8 | { 9 | readonly SmtpServer.SmtpServer _smtpServer; 10 | 11 | public Worker(SmtpServer.SmtpServer smtpServer) 12 | { 13 | _smtpServer = smtpServer; 14 | } 15 | 16 | protected override Task ExecuteAsync(CancellationToken stoppingToken) 17 | { 18 | return _smtpServer.StartAsync(stoppingToken); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/SmtpServer/ICertificateFactory.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Cryptography.X509Certificates; 2 | 3 | namespace SmtpServer 4 | { 5 | /// 6 | /// Certificate Factory Interface 7 | /// 8 | public interface ICertificateFactory 9 | { 10 | /// 11 | /// Returns the certificate to use for the session. 12 | /// 13 | /// The session context. 14 | /// The certificate to use when starting a TLS session. 15 | X509Certificate GetServerCertificate(ISessionContext sessionContext); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/SmtpServer/SessionEventArgs.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SmtpServer 4 | { 5 | /// 6 | /// Session EventArgs 7 | /// 8 | public class SessionEventArgs : EventArgs 9 | { 10 | /// 11 | /// Constructor. 12 | /// 13 | /// The session context. 14 | public SessionEventArgs(ISessionContext context) 15 | { 16 | Context = context; 17 | } 18 | 19 | /// 20 | /// Returns the session context. 21 | /// 22 | public ISessionContext Context { get; } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/SmtpServer/Net/IEndpointListenerFactory.cs: -------------------------------------------------------------------------------- 1 | namespace SmtpServer.Net 2 | { 3 | /// 4 | /// Endpoint Listener Factory Interface 5 | /// 6 | public interface IEndpointListenerFactory 7 | { 8 | /// 9 | /// Create an instance of an endpoint listener for the specified endpoint definition. 10 | /// 11 | /// The endpoint definition to create the listener for. 12 | /// The endpoint listener for the specified endpoint definition. 13 | IEndpointListener CreateListener(IEndpointDefinition endpointDefinition); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/SmtpServer/ComponentModel/ISessionContextInstanceFactory.cs: -------------------------------------------------------------------------------- 1 | namespace SmtpServer.ComponentModel 2 | { 3 | /// 4 | /// SessionContext Instance Factory Interface 5 | /// 6 | /// 7 | public interface ISessionContextInstanceFactory 8 | { 9 | /// 10 | /// Creates an instance of the service for the given session context. 11 | /// 12 | /// The session context. 13 | /// The service instance for the session context. 14 | TInstance CreateInstance(ISessionContext context); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/SmtpServer/IMaxMessageSizeOptions.cs: -------------------------------------------------------------------------------- 1 | namespace SmtpServer 2 | { 3 | /// 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 | /// 7 | public interface IMaxMessageSizeOptions 8 | { 9 | /// 10 | /// Gets or sets the maximum allowed message size in bytes. 11 | /// 12 | int Length { get; } 13 | 14 | /// 15 | /// Gets the handling type an oversized message. 16 | /// 17 | MaxMessageSizeHandling Handling { get; } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/SmtpServer/IO/ByteArraySegment.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers; 3 | 4 | namespace SmtpServer.IO 5 | { 6 | /// 7 | /// Byte Array Segment 8 | /// 9 | internal sealed class ByteArraySegment : ReadOnlySequenceSegment 10 | { 11 | internal ByteArraySegment(ReadOnlyMemory memory) 12 | { 13 | Memory = memory; 14 | } 15 | 16 | internal ByteArraySegment Append(ReadOnlyMemory memory) 17 | { 18 | var segment = new ByteArraySegment(memory) 19 | { 20 | RunningIndex = RunningIndex + Memory.Length 21 | }; 22 | 23 | Next = segment; 24 | 25 | return segment; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [cosullivan] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /src/SmtpServer/SessionFaultedEventArgs.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SmtpServer 4 | { 5 | /// 6 | /// Session Faulted EventArgs 7 | /// 8 | public class SessionFaultedEventArgs : SessionEventArgs 9 | { 10 | /// 11 | /// Constructor. 12 | /// 13 | /// The session context. 14 | /// The exception that occured 15 | public SessionFaultedEventArgs(ISessionContext context, Exception exception) : base(context) 16 | { 17 | Exception = exception; 18 | } 19 | 20 | /// 21 | /// Returns the exception. 22 | /// 23 | public Exception Exception { get; } 24 | } 25 | } -------------------------------------------------------------------------------- /src/SmtpServer.Tests/SmtpServer.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | all 13 | runtime; build; native; contentfiles; analyzers; buildtransitive 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/SmtpServer/SmtpCommandEventArgs.cs: -------------------------------------------------------------------------------- 1 | using SmtpServer.Protocol; 2 | 3 | namespace SmtpServer 4 | { 5 | /// 6 | /// Smtp Command EventArgs 7 | /// 8 | public sealed class SmtpCommandEventArgs : SessionEventArgs 9 | { 10 | /// 11 | /// Constructor. 12 | /// 13 | /// The session context. 14 | /// The command for the event. 15 | public SmtpCommandEventArgs(ISessionContext context, SmtpCommand command) : base(context) 16 | { 17 | Command = command; 18 | } 19 | 20 | /// 21 | /// The command for the event. 22 | /// 23 | public SmtpCommand Command { get; } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /examples/SampleApp/SampleApp.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net8.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/SmtpServer/Net/IEndpointListener.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using SmtpServer.IO; 5 | 6 | namespace SmtpServer.Net 7 | { 8 | /// 9 | /// Endpoint Listener Interface 10 | /// 11 | public interface IEndpointListener : IDisposable 12 | { 13 | /// 14 | /// Returns a securtable pipe to the endpoint. 15 | /// 16 | /// The session context that the pipe is being created for. 17 | /// The cancellation token. 18 | /// The securable pipe from the endpoint. 19 | Task GetPipeAsync(ISessionContext context, CancellationToken cancellationToken); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/SmtpServer/IMessageTransaction.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using SmtpServer.Mail; 3 | 4 | namespace SmtpServer 5 | { 6 | /// 7 | /// Message Transaction Interface 8 | /// 9 | public interface IMessageTransaction 10 | { 11 | /// 12 | /// Gets or sets the mailbox that is sending the message. 13 | /// 14 | IMailbox From { get; set; } 15 | 16 | /// 17 | /// Gets the collection of mailboxes that the message is to be delivered to. 18 | /// 19 | IList To { get; } 20 | 21 | /// 22 | /// The list of parameters that were supplied by the client. 23 | /// 24 | IReadOnlyDictionary Parameters { get; } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/SmtpServer/SmtpResponseExceptionEventArgs.cs: -------------------------------------------------------------------------------- 1 | using SmtpServer.Protocol; 2 | 3 | namespace SmtpServer 4 | { 5 | /// 6 | /// Smtp Response Exception EventArgs 7 | /// 8 | public sealed class SmtpResponseExceptionEventArgs : SessionEventArgs 9 | { 10 | /// 11 | /// Constructor. 12 | /// 13 | /// The session context. 14 | /// The exception that occured 15 | public SmtpResponseExceptionEventArgs(ISessionContext context, SmtpResponseException exception) : base(context) 16 | { 17 | Exception = exception; 18 | } 19 | 20 | /// 21 | /// Returns the exception. 22 | /// 23 | public SmtpResponseException Exception { get; } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/SmtpServer.Benchmarks/Program.cs: -------------------------------------------------------------------------------- 1 | using BenchmarkDotNet.Configs; 2 | using BenchmarkDotNet.Diagnosers; 3 | using BenchmarkDotNet.Running; 4 | 5 | namespace SmtpServer.Benchmarks 6 | { 7 | public class Program 8 | { 9 | public static void Main(string[] args) 10 | { 11 | //var summary = BenchmarkRunner.Run( 12 | // ManualConfig 13 | // .Create(DefaultConfig.Instance) 14 | // .With(ConfigOptions.DisableOptimizationsValidator)); 15 | 16 | //var summary = BenchmarkRunner.Run(); 17 | 18 | var summary = BenchmarkRunner.Run( 19 | ManualConfig 20 | .Create(DefaultConfig.Instance) 21 | .With(ConfigOptions.DisableOptimizationsValidator)); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/SmtpServer/Authentication/IUserAuthenticator.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | 4 | namespace SmtpServer.Authentication 5 | { 6 | /// 7 | /// User Authenticator Interface 8 | /// 9 | public interface IUserAuthenticator 10 | { 11 | /// 12 | /// Authenticate a user account. 13 | /// 14 | /// The session context. 15 | /// The user to authenticate. 16 | /// The password of the user. 17 | /// The cancellation token. 18 | /// true if the user is authenticated, false if not. 19 | Task AuthenticateAsync(ISessionContext context, string user, string password, CancellationToken cancellationToken); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/SmtpServer.Benchmarks/Test1.eml: -------------------------------------------------------------------------------- 1 | Message-ID: <10707123.1075851877657.JavaMail.evans@thyme> 2 | Date: Mon, 5 Apr 1999 00:33:00 -0700 (PDT) 3 | From: leo.nichols@enron.com 4 | To: larry.campbell@enron.com 5 | Subject: PCB Contractors 6 | Mime-Version: 1.0 7 | Content-Type: text/plain; charset=us-ascii 8 | Content-Transfer-Encoding: 7bit 9 | X-From: Leo F Nichols 10 | X-To: Larry Campbell 11 | X-cc: 12 | X-bcc: 13 | X-Folder: \Larry_Campbell_Nov2001_1\Notes Folders\All documents 14 | X-Origin: CAMPBELL-L 15 | X-FileName: lcampbe.nsf 16 | 17 | I know that I have more data, but can't find it what I have now is: 18 | 19 | John Woodyard 20 | Roy F. Weston 21 | 2300 Clayton Road, Suite 1550 22 | Concord CA 23 | 94529-2148 24 | (847) 918-4008 25 | (847) 918-4055 Fax 26 | 27 | Fred Decker 28 | Vector Group Inc. 29 | 1118 Ferris Road 30 | Cincinnati, OH 31 | 45102 32 | (513) 752-8988 33 | 34 | I hope that this is of assistance. 35 | 36 | Leo -------------------------------------------------------------------------------- /src/SmtpServer/StateMachine/SmtpStateTransition.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SmtpServer.StateMachine 4 | { 5 | internal sealed class SmtpStateTransition 6 | { 7 | readonly Func _canAcceptDelegate; 8 | readonly Func _transitionDelegate; 9 | 10 | internal SmtpStateTransition(Func canAcceptDelegate, Func transitionDelegate) 11 | { 12 | _canAcceptDelegate = canAcceptDelegate; 13 | _transitionDelegate = transitionDelegate; 14 | } 15 | 16 | internal bool CanAccept(SmtpSessionContext context) 17 | { 18 | return _canAcceptDelegate(context); 19 | } 20 | 21 | internal SmtpStateId Transition(SmtpSessionContext context) 22 | { 23 | return _transitionDelegate(context); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/SmtpServer.Benchmarks/SmtpServer.Benchmarks.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net8.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/SmtpServer/ComponentModel/DisposableContainer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SmtpServer.ComponentModel 4 | { 5 | internal sealed class DisposableContainer : IDisposable 6 | { 7 | /// 8 | /// Constructor. 9 | /// 10 | /// The instance to dispose. 11 | internal DisposableContainer(TInstance instance) 12 | { 13 | Instance = instance; 14 | } 15 | 16 | /// 17 | /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. 18 | /// 19 | public void Dispose() 20 | { 21 | if (Instance is IDisposable disposable) 22 | { 23 | disposable.Dispose(); 24 | } 25 | } 26 | 27 | /// 28 | /// Returns the instance. 29 | /// 30 | internal TInstance Instance { get; } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /examples/SampleApp/Examples/SimpleExample.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using SmtpServer; 4 | using SmtpServer.ComponentModel; 5 | 6 | namespace SampleApp.Examples 7 | { 8 | public static class SimpleExample 9 | { 10 | public static void Run() 11 | { 12 | var cancellationTokenSource = new CancellationTokenSource(); 13 | 14 | var options = new SmtpServerOptionsBuilder() 15 | .ServerName("SmtpServer SampleApp") 16 | .Port(9025) 17 | .Build(); 18 | 19 | var serviceProvider = new ServiceProvider(); 20 | serviceProvider.Add(new SampleMessageStore(Console.Out)); 21 | 22 | var server = new SmtpServer.SmtpServer(options, serviceProvider); 23 | var serverTask = server.StartAsync(cancellationTokenSource.Token); 24 | 25 | SampleMailClient.Send(); 26 | 27 | cancellationTokenSource.Cancel(); 28 | serverTask.WaitWithoutException(); 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /src/SmtpServer.Benchmarks/TokenizerBenchmarks.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using BenchmarkDotNet.Attributes; 6 | using SmtpServer.Text; 7 | 8 | namespace SmtpServer.Benchmarks 9 | { 10 | [MemoryDiagnoser] 11 | public class TokenizerBenchmarks 12 | { 13 | static readonly IReadOnlyList> Segments = Tokenize("ABCD:EF01:2345:6789:ABCD:EF01:2345:6789"); 14 | 15 | //[Benchmark] 16 | //public void EnumerateTokens() 17 | //{ 18 | // var tokenizer = new TokenEnumerator(new ByteArrayTokenReader(Segments)); 19 | 20 | // while (tokenizer.Peek() != Token.None) 21 | // { 22 | // tokenizer.Take(); 23 | // } 24 | //} 25 | 26 | static IReadOnlyList> Tokenize(params string[] text) 27 | { 28 | return text.Select(t => new ArraySegment(Encoding.ASCII.GetBytes(t))).ToList(); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build and Test 2 | 3 | on: 4 | workflow_dispatch: # Allow running the workflow manually from the GitHub UI 5 | push: 6 | paths: 7 | - 'src/**' 8 | - '.github/workflows/**' 9 | branches: [ master ] 10 | pull_request: 11 | paths: 12 | - 'src/**' 13 | - '.github/workflows/**' 14 | 15 | jobs: 16 | build: 17 | 18 | runs-on: ubuntu-latest 19 | 20 | permissions: 21 | contents: read 22 | #packages: write 23 | 24 | steps: 25 | - uses: actions/checkout@v5 26 | - name: Setup .NET 8.0 27 | uses: actions/setup-dotnet@v5 28 | with: 29 | dotnet-version: 8.0.x 30 | - name: Restore dependencies 31 | working-directory: ./src 32 | run: dotnet restore 33 | - name: Build 34 | working-directory: ./src 35 | run: dotnet build --configuration Release --no-restore 36 | - name: Test 37 | working-directory: ./src 38 | run: | 39 | dotnet test --configuration Release --no-restore --no-build --verbosity normal 40 | -------------------------------------------------------------------------------- /src/SmtpServer.Tests/SmtpServerDisposable.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SmtpServer.Tests 4 | { 5 | internal sealed class SmtpServerDisposable : IDisposable 6 | { 7 | readonly Action _delegate; 8 | 9 | /// 10 | /// Constructor. 11 | /// 12 | /// The SMTP server instance. 13 | /// The delegate to execute upon disposal. 14 | public SmtpServerDisposable(SmtpServer server, Action @delegate) 15 | { 16 | Server = server; 17 | 18 | _delegate = @delegate; 19 | } 20 | 21 | /// 22 | /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. 23 | /// 24 | public void Dispose() 25 | { 26 | _delegate(); 27 | } 28 | 29 | /// 30 | /// The SMTP server instance. 31 | /// 32 | public SmtpServer Server { get; } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/SmtpServer/Storage/IMessageStore.cs: -------------------------------------------------------------------------------- 1 | using System.Buffers; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using SmtpServer.Protocol; 5 | 6 | namespace SmtpServer.Storage 7 | { 8 | /// 9 | /// Message Store Interface 10 | /// 11 | public interface IMessageStore 12 | { 13 | /// 14 | /// Save the given message to the underlying storage system. 15 | /// 16 | /// The session level context. 17 | /// The SMTP message transaction to store. 18 | /// The buffer that contains the message content. 19 | /// The cancellation token. 20 | /// The response code to return that indicates the result of the message being saved. 21 | Task SaveAsync(ISessionContext context, IMessageTransaction transaction, ReadOnlySequence buffer, CancellationToken cancellationToken); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/SampleApp/SampleUserAuthenticator.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using SmtpServer; 4 | using SmtpServer.Authentication; 5 | 6 | namespace SampleApp 7 | { 8 | public sealed class SampleUserAuthenticator : UserAuthenticator 9 | { 10 | /// 11 | /// Authenticate a user account. 12 | /// 13 | /// The session context. 14 | /// The user to authenticate. 15 | /// The password of the user. 16 | /// The cancellation token. 17 | /// true if the user is authenticated, false if not. 18 | public override Task AuthenticateAsync( 19 | ISessionContext context, 20 | string user, 21 | string password, 22 | CancellationToken cancellationToken) 23 | { 24 | return Task.FromResult(user == "user" && password == "password"); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/SmtpServer/Net/EndPointEventArgs.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | 4 | namespace SmtpServer.Net 5 | { 6 | /// 7 | /// Endpoint EventArgs 8 | /// 9 | public sealed class EndpointEventArgs : EventArgs 10 | { 11 | /// 12 | /// Constructor. 13 | /// 14 | /// The endpoint definition. 15 | /// The locally bound endpoint. 16 | public EndpointEventArgs(IEndpointDefinition endpointDefinition, EndPoint localEndPoint) 17 | { 18 | EndpointDefinition = endpointDefinition; 19 | LocalEndPoint = localEndPoint; 20 | } 21 | 22 | /// 23 | /// Returns the endpoint definition. 24 | /// 25 | public IEndpointDefinition EndpointDefinition { get; } 26 | 27 | /// 28 | /// Returns the locally bound endpoint 29 | /// 30 | public EndPoint LocalEndPoint { get; } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/SmtpServer/SmtpMessageTransaction.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Collections.ObjectModel; 3 | using SmtpServer.Mail; 4 | 5 | namespace SmtpServer 6 | { 7 | /// 8 | /// Smtp Message Transaction 9 | /// 10 | internal sealed class SmtpMessageTransaction : IMessageTransaction 11 | { 12 | /// 13 | /// Reset the current transaction. 14 | /// 15 | public void Reset() 16 | { 17 | From = null; 18 | To = new Collection(); 19 | Parameters = new ReadOnlyDictionary(new Dictionary()); 20 | } 21 | 22 | /// 23 | public IMailbox From { get; set; } 24 | 25 | /// 26 | public IList To { get; set; } = new Collection(); 27 | 28 | /// 29 | public IReadOnlyDictionary Parameters { get; set; } = new ReadOnlyDictionary(new Dictionary()); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/SmtpServer/Storage/CompositeMailboxFilterFactory.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | 3 | namespace SmtpServer.Storage 4 | { 5 | internal sealed class CompositeMailboxFilterFactory : IMailboxFilterFactory 6 | { 7 | readonly IMailboxFilterFactory[] _factories; 8 | 9 | /// 10 | /// Constructor. 11 | /// 12 | /// The list of factories to run in order. 13 | public CompositeMailboxFilterFactory(params IMailboxFilterFactory[] factories) 14 | { 15 | _factories = factories; 16 | } 17 | 18 | /// 19 | /// Creates an instance of the message box filter. 20 | /// 21 | /// The session context. 22 | /// The mailbox filter for the session. 23 | public IMailboxFilter CreateInstance(ISessionContext context) 24 | { 25 | return new CompositeMailboxFilter(_factories.Select(factory => factory.CreateInstance(context)).ToArray()); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/SmtpServer/Storage/DelegatingMessageStoreFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SmtpServer.Storage 4 | { 5 | /// 6 | /// Delegating MessageStore Factory 7 | /// 8 | public sealed class DelegatingMessageStoreFactory : IMessageStoreFactory 9 | { 10 | readonly Func _delegate; 11 | 12 | /// 13 | /// Delegating MessageStore Factory 14 | /// 15 | /// 16 | public DelegatingMessageStoreFactory(Func @delegate) 17 | { 18 | _delegate = @delegate; 19 | } 20 | 21 | /// 22 | /// Creates an instance of the service for the given session context. 23 | /// 24 | /// The session context. 25 | /// The service instance for the session context. 26 | public IMessageStore CreateInstance(ISessionContext context) 27 | { 28 | return _delegate(context); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/SmtpServer/Storage/DelegatingMailboxFilterFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SmtpServer.Storage 4 | { 5 | /// 6 | /// Delegating Mailbox Filter Factory 7 | /// 8 | public sealed class DelegatingMailboxFilterFactory : IMailboxFilterFactory 9 | { 10 | readonly Func _delegate; 11 | 12 | /// 13 | /// Delegating Mailbox Filter Factory 14 | /// 15 | /// 16 | public DelegatingMailboxFilterFactory(Func @delegate) 17 | { 18 | _delegate = @delegate; 19 | } 20 | 21 | /// 22 | /// Creates an instance of the service for the given session context. 23 | /// 24 | /// The session context. 25 | /// The service instance for the session context. 26 | public IMailboxFilter CreateInstance(ISessionContext context) 27 | { 28 | return _delegate(context); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 cosullivan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/SmtpServer/Storage/MessageStore.cs: -------------------------------------------------------------------------------- 1 | using System.Buffers; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using SmtpServer.Protocol; 5 | 6 | namespace SmtpServer.Storage 7 | { 8 | /// 9 | /// Message Store 10 | /// 11 | public abstract class MessageStore : IMessageStore 12 | { 13 | /// 14 | /// Default Message Store 15 | /// 16 | public static readonly IMessageStore Default = new DefaultMessageStore(); 17 | 18 | /// 19 | public abstract Task SaveAsync(ISessionContext context, IMessageTransaction transaction, ReadOnlySequence buffer, CancellationToken cancellationToken); 20 | 21 | sealed class DefaultMessageStore : MessageStore 22 | { 23 | /// 24 | public override Task SaveAsync(ISessionContext context, IMessageTransaction transaction, ReadOnlySequence buffer, CancellationToken cancellationToken) 25 | { 26 | return Task.FromResult(SmtpResponse.Ok); 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/SmtpServer/Authentication/DelegatingUserAuthenticatorFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SmtpServer.Authentication 4 | { 5 | /// 6 | /// Delegating User Authenticator Factory 7 | /// 8 | public sealed class DelegatingUserAuthenticatorFactory : IUserAuthenticatorFactory 9 | { 10 | readonly Func _delegate; 11 | 12 | /// 13 | /// Delegating User Authenticator Factory 14 | /// 15 | /// 16 | public DelegatingUserAuthenticatorFactory(Func @delegate) 17 | { 18 | _delegate = @delegate; 19 | } 20 | 21 | /// 22 | /// Creates an instance of the service for the given session context. 23 | /// 24 | /// The session context. 25 | /// The service instance for the session context. 26 | public IUserAuthenticator CreateInstance(ISessionContext context) 27 | { 28 | return _delegate(context); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/SmtpServer/Storage/MailboxFilter.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using SmtpServer.Mail; 4 | 5 | namespace SmtpServer.Storage 6 | { 7 | /// 8 | /// Mailbox Filter 9 | /// 10 | public abstract class MailboxFilter : IMailboxFilter 11 | { 12 | /// 13 | /// Default Mailbox Filter 14 | /// 15 | public static readonly IMailboxFilter Default = new DefaultMailboxFilter(); 16 | 17 | /// 18 | public virtual Task CanAcceptFromAsync( 19 | ISessionContext context, 20 | IMailbox @from, 21 | int size, 22 | CancellationToken cancellationToken) 23 | { 24 | return Task.FromResult(true); 25 | } 26 | 27 | /// 28 | public virtual Task CanDeliverToAsync( 29 | ISessionContext context, 30 | IMailbox to, 31 | IMailbox @from, 32 | CancellationToken cancellationToken) 33 | { 34 | return Task.FromResult(true); 35 | } 36 | 37 | sealed class DefaultMailboxFilter : MailboxFilter { } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/SmtpServer/Protocol/SmtpCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | 4 | namespace SmtpServer.Protocol 5 | { 6 | /// 7 | /// Smtp Command 8 | /// 9 | public abstract class SmtpCommand 10 | { 11 | /// 12 | /// Constructor. 13 | /// 14 | /// The name of the command. 15 | protected SmtpCommand(string name) 16 | { 17 | Name = name; 18 | } 19 | 20 | /// 21 | /// Execute the command. 22 | /// 23 | /// The execution context to operate on. 24 | /// The cancellation token. 25 | /// Returns true if the command executed successfully such that the transition to the next state should occurr, false 26 | /// if the current state is to be maintained. 27 | internal abstract Task ExecuteAsync(SmtpSessionContext context, CancellationToken cancellationToken); 28 | 29 | /// 30 | /// The name of the command. 31 | /// 32 | public string Name { get; } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/SmtpServer/AuthenticationContext.cs: -------------------------------------------------------------------------------- 1 | namespace SmtpServer 2 | { 3 | /// 4 | /// Authentication Context 5 | /// 6 | public sealed class AuthenticationContext 7 | { 8 | /// 9 | /// Authentication Context in the Unauthenticated state 10 | /// 11 | public static readonly AuthenticationContext Unauthenticated = new AuthenticationContext(); 12 | 13 | /// 14 | /// Constructor. 15 | /// 16 | public AuthenticationContext() 17 | { 18 | IsAuthenticated = false; 19 | } 20 | 21 | /// 22 | /// Constructor. 23 | /// 24 | /// The name of the user that was authenticated. 25 | public AuthenticationContext(string user) 26 | { 27 | User = user; 28 | IsAuthenticated = true; 29 | } 30 | 31 | /// 32 | /// The name of the user that was authenticated. 33 | /// 34 | public string User { get; } 35 | 36 | /// 37 | /// Returns a value indicating whether or nor the current session is authenticated. 38 | /// 39 | public bool IsAuthenticated { get; } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/SmtpServer/IO/ByteArraySegmentList.cs: -------------------------------------------------------------------------------- 1 | using System.Buffers; 2 | 3 | namespace SmtpServer.IO 4 | { 5 | /// 6 | /// Byte Array Segment List 7 | /// 8 | internal sealed class ByteArraySegmentList 9 | { 10 | internal void Append(byte[] buffer) 11 | { 12 | var sequence = new ReadOnlySequence(buffer); 13 | 14 | Append(ref sequence); 15 | } 16 | 17 | internal void Append(ref ReadOnlySequence sequence) 18 | { 19 | var position = sequence.GetPosition(0); 20 | 21 | while (sequence.TryGet(ref position, out var memory)) 22 | { 23 | if (Start == null) 24 | { 25 | Start = new ByteArraySegment(memory); 26 | End = Start; 27 | } 28 | else 29 | { 30 | End = End.Append(memory); 31 | } 32 | } 33 | } 34 | 35 | internal ReadOnlySequence Build() 36 | { 37 | return new ReadOnlySequence(Start, 0, End, End.Memory.Length); 38 | } 39 | 40 | internal ByteArraySegment Start { get; private set; } 41 | 42 | internal ByteArraySegment End { get; private set; } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /examples/SampleApp/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Linq; 4 | using System.Net; 5 | using System.Net.Security; 6 | using System.Security.Cryptography.X509Certificates; 7 | using System.Text; 8 | using System.Threading; 9 | using MimeKit; 10 | using SampleApp.Examples; 11 | using SmtpServer; 12 | using SmtpServer.IO; 13 | using SmtpServer.Protocol; 14 | using SmtpServer.Tests; 15 | using SmtpServer.Text; 16 | using SmtpServer.ComponentModel; 17 | using SmtpServer.Net; 18 | using SmtpServer.Tracing; 19 | 20 | namespace SampleApp 21 | { 22 | class Program 23 | { 24 | static void Main(string[] args) 25 | { 26 | ServicePointManager.ServerCertificateValidationCallback = IgnoreCertificateValidationFailureForTestingOnly; 27 | 28 | //SimpleExample.Run(); 29 | //SimpleServerExample.Run(); 30 | //CustomEndpointListenerExample.Run(); 31 | //ServerCancellingExample.Run(); 32 | SessionTracingExample.Run(); 33 | //DependencyInjectionExample.Run(); 34 | //SecureServerExample.Run(); 35 | } 36 | 37 | static bool IgnoreCertificateValidationFailureForTestingOnly(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) 38 | { 39 | return true; 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /src/SmtpServer/Protocol/NoopCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using SmtpServer.IO; 4 | 5 | namespace SmtpServer.Protocol 6 | { 7 | /// 8 | /// Noop Command 9 | /// 10 | public sealed class NoopCommand : SmtpCommand 11 | { 12 | /// 13 | /// Smtp Noop Command 14 | /// 15 | public const string Command = "NOOP"; 16 | 17 | /// 18 | /// Constructor. 19 | /// 20 | public NoopCommand() : base(Command) { } 21 | 22 | /// 23 | /// Execute the command. 24 | /// 25 | /// The execution context to operate on. 26 | /// The cancellation token. 27 | /// Returns true if the command executed successfully such that the transition to the next state should occurr, false 28 | /// if the current state is to be maintained. 29 | internal override async Task ExecuteAsync(SmtpSessionContext context, CancellationToken cancellationToken) 30 | { 31 | await context.Pipe.Output.WriteReplyAsync(SmtpResponse.Ok, cancellationToken).ConfigureAwait(false); 32 | 33 | return true; 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /examples/WorkerService/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Microsoft.Extensions.Hosting; 4 | using SmtpServer; 5 | using SmtpServer.Storage; 6 | 7 | namespace WorkerService 8 | { 9 | public class Program 10 | { 11 | public static void Main(string[] args) 12 | { 13 | CreateHostBuilder(args).Build().Run(); 14 | } 15 | 16 | public static IHostBuilder CreateHostBuilder(string[] args) => 17 | Host.CreateDefaultBuilder(args) 18 | .ConfigureServices( 19 | (hostContext, services) => 20 | { 21 | services.AddTransient(); 22 | 23 | services.AddSingleton( 24 | provider => 25 | { 26 | var options = new SmtpServerOptionsBuilder() 27 | .ServerName("SMTP Server") 28 | .Port(9025) 29 | .Build(); 30 | 31 | return new SmtpServer.SmtpServer(options, provider.GetRequiredService()); 32 | }); 33 | 34 | services.AddHostedService(); 35 | }); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/SmtpServer/ComponentModel/ServiceProviderExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SmtpServer.ComponentModel 4 | { 5 | internal static class ServiceProviderExtensions 6 | { 7 | internal static TService GetServiceOrDefault(this IServiceProvider serviceProvider, TService @default) where TService : class 8 | { 9 | if (serviceProvider == null) 10 | { 11 | throw new ArgumentNullException(nameof(serviceProvider)); 12 | } 13 | 14 | return serviceProvider.GetService(typeof(TService)) as TService ?? @default; 15 | } 16 | 17 | internal static TService GetService(this IServiceProvider serviceProvider, ISessionContext sessionContext, TService @default) 18 | where TService : class 19 | where TServiceFactory : ISessionContextInstanceFactory 20 | { 21 | if (serviceProvider == null) 22 | { 23 | throw new ArgumentNullException(nameof(serviceProvider)); 24 | } 25 | 26 | if (serviceProvider.GetService(typeof(TServiceFactory)) is TServiceFactory sessionFactory) 27 | { 28 | return sessionFactory.CreateInstance(sessionContext); 29 | } 30 | 31 | return serviceProvider.GetServiceOrDefault(@default); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/SmtpServer/Protocol/RsetCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using SmtpServer.IO; 4 | 5 | namespace SmtpServer.Protocol 6 | { 7 | /// 8 | /// Rset Command 9 | /// 10 | public sealed class RsetCommand : SmtpCommand 11 | { 12 | /// 13 | /// Smtp Rset Command 14 | /// 15 | public const string Command = "RSET"; 16 | 17 | /// 18 | /// Constructor. 19 | /// 20 | public RsetCommand() : base(Command) { } 21 | 22 | /// 23 | /// Execute the command. 24 | /// 25 | /// The execution context to operate on. 26 | /// The cancellation token. 27 | /// Returns true if the command executed successfully such that the transition to the next state should occurr, false 28 | /// if the current state is to be maintained. 29 | internal override async Task ExecuteAsync(SmtpSessionContext context, CancellationToken cancellationToken) 30 | { 31 | context.Transaction.Reset(); 32 | 33 | await context.Pipe.Output.WriteReplyAsync(SmtpResponse.Ok, cancellationToken).ConfigureAwait(false); 34 | 35 | return true; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/SmtpServer/IO/ISecurableDuplexPipe.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO.Pipelines; 3 | using System.Security.Authentication; 4 | using System.Security.Cryptography.X509Certificates; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | namespace SmtpServer.IO 9 | { 10 | /// 11 | /// Securable Duplex Pipe Interface 12 | /// 13 | public interface ISecurableDuplexPipe : IDuplexPipe, IDisposable 14 | { 15 | /// 16 | /// Upgrade to a secure pipeline. 17 | /// 18 | /// The X509Certificate used to authenticate the server. 19 | /// The value that represents the protocol used for authentication. 20 | /// The cancellation token. 21 | /// A task that asynchronously performs the operation. 22 | Task UpgradeAsync(X509Certificate certificate, SslProtocols protocols, CancellationToken cancellationToken = default); 23 | 24 | /// 25 | /// Returns a value indicating whether or not the current pipeline is secure. 26 | /// 27 | bool IsSecure { get; } 28 | 29 | /// 30 | /// Returns the used SslProtocol of a secure pipeline 31 | /// 32 | SslProtocols SslProtocol { get; } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/SmtpServer/Mail/IMailbox.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | 4 | namespace SmtpServer.Mail 5 | { 6 | /// 7 | /// Mailbox Interface 8 | /// 9 | public interface IMailbox 10 | { 11 | /// 12 | /// Gets the user/account name. 13 | /// 14 | string User { get; } 15 | 16 | /// 17 | /// Gets the host server. 18 | /// 19 | string Host { get; } 20 | } 21 | 22 | /// 23 | /// Mailbox Extension Methods 24 | /// 25 | public static class MailboxExtensionMethods 26 | { 27 | /// 28 | /// Returns the Mailbox as an Address string. 29 | /// 30 | /// The mailbox to perform the operation on. 31 | /// The address string that represents the mailbox. 32 | public static string AsAddress(this IMailbox mailbox) 33 | { 34 | if (mailbox == null) 35 | { 36 | throw new ArgumentNullException(nameof(mailbox)); 37 | } 38 | 39 | if (string.IsNullOrWhiteSpace(mailbox.User) && string.IsNullOrWhiteSpace(mailbox.Host)) 40 | { 41 | return null; 42 | } 43 | 44 | return string.Format(CultureInfo.InvariantCulture, "{0}@{1}", mailbox.User, mailbox.Host); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/SmtpServer/Mail/Mailbox.cs: -------------------------------------------------------------------------------- 1 | namespace SmtpServer.Mail 2 | { 3 | /// 4 | /// Mailbox 5 | /// 6 | public sealed class Mailbox : IMailbox 7 | { 8 | /// 9 | /// Empty Mailbox 10 | /// 11 | public static readonly IMailbox Empty = new Mailbox(string.Empty, string.Empty); 12 | 13 | /// 14 | /// Constructor. 15 | /// 16 | /// The user/account name. 17 | /// The host server. 18 | public Mailbox(string user, string host) 19 | { 20 | User = user; 21 | Host = host; 22 | } 23 | 24 | /// 25 | /// Constructor. 26 | /// 27 | /// The email address to create the mailbox from. 28 | public Mailbox(string address) 29 | { 30 | address = address.Replace(" ", string.Empty); 31 | 32 | var index = address.IndexOf('@'); 33 | 34 | User = address.Substring(0, index); 35 | Host = address.Substring(index + 1); 36 | } 37 | 38 | /// 39 | /// Gets the user/account name. 40 | /// 41 | public string User { get; } 42 | 43 | /// 44 | /// Gets the host server. 45 | /// 46 | public string Host { get; } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/SmtpServer/IEndpointDefinition.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Security.Authentication; 4 | 5 | namespace SmtpServer 6 | { 7 | /// 8 | /// Endpoint Definition Interface 9 | /// 10 | public interface IEndpointDefinition 11 | { 12 | /// 13 | /// The IP endpoint to listen on. 14 | /// 15 | IPEndPoint Endpoint { get; } 16 | 17 | /// 18 | /// Indicates whether the endpoint is secure by default. 19 | /// 20 | bool IsSecure { get; } 21 | 22 | /// 23 | /// Gets a value indicating whether the client must authenticate in order to proceed. 24 | /// 25 | bool AuthenticationRequired { get; } 26 | 27 | /// 28 | /// Gets a value indicating whether authentication should be allowed on an unsecure session. 29 | /// 30 | bool AllowUnsecureAuthentication { get; } 31 | 32 | /// 33 | /// The timeout of an Smtp session. 34 | /// 35 | TimeSpan SessionTimeout { get; } 36 | 37 | /// 38 | /// Gets the Server Certificate factory to use when starting a TLS session. 39 | /// 40 | ICertificateFactory CertificateFactory { get; } 41 | 42 | /// 43 | /// The supported SSL protocols. 44 | /// 45 | SslProtocols SupportedSslProtocols { get; } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/SmtpServer.Tests/Mocks/MockMessageStore.cs: -------------------------------------------------------------------------------- 1 | using System.Buffers; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using MimeKit; 7 | using SmtpServer.Protocol; 8 | using SmtpServer.Storage; 9 | 10 | namespace SmtpServer.Tests.Mocks 11 | { 12 | public sealed class MockMessageStore : MessageStore 13 | { 14 | public MockMessageStore() 15 | { 16 | Messages = new List(); 17 | } 18 | 19 | public override Task SaveAsync(ISessionContext context, IMessageTransaction transaction, ReadOnlySequence buffer, CancellationToken cancellationToken) 20 | { 21 | Messages.Add(new MockMessage(transaction, buffer));; 22 | 23 | return Task.FromResult(SmtpResponse.Ok); 24 | } 25 | 26 | public List Messages { get; } 27 | } 28 | 29 | public sealed class MockMessage 30 | { 31 | public MockMessage(IMessageTransaction transaction, ReadOnlySequence buffer) 32 | { 33 | Transaction = transaction; 34 | 35 | using var stream = new MemoryStream(buffer.ToArray()); 36 | 37 | MimeMessage = MimeMessage.Load(stream); 38 | } 39 | 40 | public string Text(string charset = "utf-8") 41 | { 42 | return ((TextPart)MimeMessage.Body).GetText(charset).TrimEnd('\n', '\r'); 43 | } 44 | 45 | public IMessageTransaction Transaction { get; } 46 | 47 | public MimeMessage MimeMessage { get; } 48 | } 49 | } -------------------------------------------------------------------------------- /examples/WorkerService/ConsoleMessageStore.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers; 3 | using System.IO; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using SmtpServer; 7 | using SmtpServer.Protocol; 8 | using SmtpServer.Storage; 9 | 10 | namespace WorkerService 11 | { 12 | public sealed class ConsoleMessageStore : MessageStore 13 | { 14 | /// 15 | /// Save the given message to the underlying storage system. 16 | /// 17 | /// The session context. 18 | /// The SMTP message transaction to store. 19 | /// The buffer that contains the message content. 20 | /// The cancellation token. 21 | /// A unique identifier that represents this message in the underlying message store. 22 | public override async Task SaveAsync(ISessionContext context, IMessageTransaction transaction, ReadOnlySequence buffer, CancellationToken cancellationToken) 23 | { 24 | await using var stream = new MemoryStream(); 25 | 26 | var position = buffer.GetPosition(0); 27 | while (buffer.TryGet(ref position, out var memory)) 28 | { 29 | await stream.WriteAsync(memory, cancellationToken); 30 | } 31 | 32 | stream.Position = 0; 33 | 34 | Console.WriteLine(await new StreamReader(stream).ReadToEndAsync()); 35 | 36 | return SmtpResponse.Ok; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /examples/SampleApp/Examples/SimpleServerExample.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using SmtpServer; 4 | using SmtpServer.ComponentModel; 5 | using SmtpServer.Tracing; 6 | 7 | namespace SampleApp.Examples 8 | { 9 | public static class SimpleServerExample 10 | { 11 | public static void Run() 12 | { 13 | var cancellationTokenSource = new CancellationTokenSource(); 14 | 15 | var options = new SmtpServerOptionsBuilder() 16 | .ServerName("SmtpServer SampleApp") 17 | .Port(9025) 18 | .CommandWaitTimeout(TimeSpan.FromSeconds(100)) 19 | .Build(); 20 | 21 | var server = new SmtpServer.SmtpServer(options, ServiceProvider.Default); 22 | server.SessionCreated += OnSessionCreated; 23 | 24 | var serverTask = server.StartAsync(cancellationTokenSource.Token); 25 | 26 | Console.WriteLine("Press any key to shutdown the server."); 27 | Console.ReadKey(); 28 | 29 | cancellationTokenSource.Cancel(); 30 | serverTask.WaitWithoutException(); 31 | } 32 | 33 | static void OnSessionCreated(object sender, SessionEventArgs e) 34 | { 35 | Console.WriteLine("Session Created."); 36 | 37 | e.Context.CommandExecuting += OnCommandExecuting; 38 | } 39 | 40 | static void OnCommandExecuting(object sender, SmtpCommandEventArgs e) 41 | { 42 | Console.WriteLine("Command Executing."); 43 | 44 | new TracingSmtpCommandVisitor(Console.Out).Visit(e.Command); 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /.github/workflows/build-release.yml: -------------------------------------------------------------------------------- 1 | name: Build, Test & Publish 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*.*.*' 7 | 8 | jobs: 9 | build: 10 | 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v5 15 | - name: Setup .NET 8.0 16 | uses: actions/setup-dotnet@v5 17 | with: 18 | dotnet-version: 8.0.x 19 | - name: Extract Version from tag 20 | id: tag 21 | uses: actions/github-script@v7 22 | with: 23 | script: | 24 | const version = context.ref.replace('refs/tags/', ''); 25 | return version.slice(1); 26 | - name: Change Package Version 27 | working-directory: ./src 28 | run: | 29 | sed -i -e "s/[a-zA-Z0-9.-]*<\/Version>/${{ steps.tag.outputs.result }}<\/Version>/g" SmtpServer/SmtpServer.csproj 30 | - name: Restore dependencies 31 | working-directory: ./src 32 | run: dotnet restore 33 | - name: Build 34 | working-directory: ./src 35 | run: dotnet build --configuration Release --no-restore 36 | - name: Test 37 | working-directory: ./src 38 | run: | 39 | dotnet test --configuration Release --no-restore --no-build --verbosity normal 40 | - name: Build project and generate NuGet package 41 | run: | 42 | dotnet pack --configuration Release --output $GITHUB_WORKSPACE/out src/SmtpServer/SmtpServer.csproj 43 | - name: Push NuGet package 44 | run: | 45 | cd $GITHUB_WORKSPACE/out 46 | dotnet nuget push *.nupkg --source https://api.nuget.org/v3/index.json --api-key ${{secrets.NUGET_TOKEN}} --skip-duplicate --no-symbols 47 | -------------------------------------------------------------------------------- /src/SmtpServer/Protocol/StartTlsCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using SmtpServer.IO; 4 | 5 | namespace SmtpServer.Protocol 6 | { 7 | /// 8 | /// Start Tls Command 9 | /// 10 | public sealed class StartTlsCommand : SmtpCommand 11 | { 12 | /// 13 | /// Smtp Start Tls Command 14 | /// 15 | public const string Command = "STARTTLS"; 16 | 17 | /// 18 | /// Constructor. 19 | /// 20 | public StartTlsCommand() : base(Command) { } 21 | 22 | /// 23 | /// Execute the command. 24 | /// 25 | /// The execution context to operate on. 26 | /// The cancellation token. 27 | /// Returns true if the command executed successfully such that the transition to the next state should occurr, false 28 | /// if the current state is to be maintained. 29 | internal override async Task ExecuteAsync(SmtpSessionContext context, CancellationToken cancellationToken) 30 | { 31 | await context.Pipe.Output.WriteReplyAsync(SmtpResponse.ServiceReady, cancellationToken).ConfigureAwait(false); 32 | 33 | var certificate = context.EndpointDefinition.CertificateFactory.GetServerCertificate(context); 34 | 35 | var protocols = context.EndpointDefinition.SupportedSslProtocols; 36 | 37 | await context.Pipe.UpgradeAsync(certificate, protocols, cancellationToken).ConfigureAwait(false); 38 | 39 | return true; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/SmtpServer/Storage/IMailboxFilter.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using SmtpServer.Mail; 4 | 5 | namespace SmtpServer.Storage 6 | { 7 | /// 8 | /// Mailbox Filter Interface 9 | /// 10 | public interface IMailboxFilter 11 | { 12 | /// 13 | /// Returns a value indicating whether the given mailbox can be accepted as a sender. 14 | /// 15 | /// The session context. 16 | /// The mailbox to test. 17 | /// The estimated message size to accept. 18 | /// The cancellation token. 19 | /// Returns true if the mailbox is accepted, false if not. 20 | Task CanAcceptFromAsync( 21 | ISessionContext context, 22 | IMailbox from, 23 | int size, 24 | CancellationToken cancellationToken); 25 | 26 | /// 27 | /// Returns a value indicating whether the given mailbox can be accepted as a recipient to the given sender. 28 | /// 29 | /// The session context. 30 | /// The mailbox to test. 31 | /// The sender's mailbox. 32 | /// The cancellation token. 33 | /// Returns true if the mailbox can be delivered to, false if not. 34 | Task CanDeliverToAsync( 35 | ISessionContext context, 36 | IMailbox to, 37 | IMailbox from, 38 | CancellationToken cancellationToken); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/SmtpServer/MaxMessageSizeOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace SmtpServer 6 | { 7 | /// 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 | /// 11 | public class MaxMessageSizeOptions : IMaxMessageSizeOptions 12 | { 13 | /// 14 | /// Gets or sets the maximum allowed message size in bytes, 15 | /// as specified by the SMTP SIZE extension (RFC 1870). 16 | /// 17 | public int Length { get; set; } 18 | 19 | /// 20 | /// Gets or sets the handling strategy for messages that exceed the maximum allowed size. 21 | /// 22 | public MaxMessageSizeHandling Handling { get; set; } 23 | 24 | /// 25 | /// Initializes a new instance of the class 26 | /// with the specified handling strategy and message size limit. 27 | /// 28 | /// The strategy for handling messages that exceed the maximum allowed size. 29 | /// The maximum allowed message size in bytes. 30 | public MaxMessageSizeOptions(MaxMessageSizeHandling handling, int length) 31 | { 32 | Length = length; 33 | Handling = handling; 34 | } 35 | 36 | /// 37 | /// Initializes a new instance of the class with default values. 38 | /// 39 | public MaxMessageSizeOptions() 40 | { 41 | 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /examples/SampleApp/Examples/DependencyInjectionExample.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using SmtpServer; 4 | using SmtpServer.Protocol; 5 | 6 | namespace SampleApp.Examples 7 | { 8 | public static class DependencyInjectionExample 9 | { 10 | public static void Run() 11 | { 12 | var cancellationTokenSource = new CancellationTokenSource(); 13 | 14 | var options = new SmtpServerOptionsBuilder() 15 | .ServerName("SmtpServer SampleApp") 16 | .Port(9025) 17 | .Build(); 18 | 19 | var services = new ServiceCollection(); 20 | services.AddSingleton(options); 21 | services.AddTransient(); 22 | 23 | var server = new SmtpServer.SmtpServer(options, services.BuildServiceProvider()); 24 | 25 | var serverTask = server.StartAsync(cancellationTokenSource.Token); 26 | 27 | SampleMailClient.Send(); 28 | 29 | cancellationTokenSource.Cancel(); 30 | serverTask.WaitWithoutException(); 31 | } 32 | 33 | public sealed class CustomSmtpCommandFactory : SmtpCommandFactory 34 | { 35 | public override SmtpCommand CreateEhlo(string domainOrAddress) 36 | { 37 | return new CustomEhloCommand(domainOrAddress); 38 | } 39 | } 40 | 41 | public sealed class CustomEhloCommand : EhloCommand 42 | { 43 | public CustomEhloCommand(string domainOrAddress) : base(domainOrAddress) { } 44 | 45 | protected override string GetGreeting(ISessionContext context) 46 | { 47 | return "Good morning, Vietnam!"; 48 | } 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /src/SmtpServer/SmtpServer.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.1 5 | 8.0 6 | SmtpServer 7 | SmtpServer 8 | 10.0.1 9 | High-performance, flexible SMTP server implementation for .NET with support for ESMTP, TLS, authentication, and custom message handling. 10 | Cain O'Sullivan 11 | 2015-2023 12 | https://github.com/cosullivan/SmtpServer 13 | http://cainosullivan.com/smtpserver 14 | smtp smtpserver mailserver 15 | LICENSE 16 | True 17 | A changelog is available at https://github.com/cosullivan/SmtpServer/blob/master/CHANGELOG.md 18 | true 19 | true 20 | 21 | 22 | 23 | true 24 | 25 | 26 | 27 | true 28 | 29 | 30 | 31 | 32 | True 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /examples/SampleApp/SampleMessageStore.cs: -------------------------------------------------------------------------------- 1 | using System.Buffers; 2 | using System.IO; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using SmtpServer; 6 | using SmtpServer.Protocol; 7 | using SmtpServer.Storage; 8 | 9 | namespace SampleApp 10 | { 11 | public class SampleMessageStore : MessageStore 12 | { 13 | readonly TextWriter _writer; 14 | 15 | public SampleMessageStore(TextWriter writer) 16 | { 17 | _writer = writer; 18 | } 19 | 20 | /// 21 | /// Save the given message to the underlying storage system. 22 | /// 23 | /// The session context. 24 | /// The SMTP message transaction to store. 25 | /// The buffer that contains the message content. 26 | /// The cancellation token. 27 | /// A unique identifier that represents this message in the underlying message store. 28 | public override async Task SaveAsync(ISessionContext context, IMessageTransaction transaction, ReadOnlySequence buffer, CancellationToken cancellationToken) 29 | { 30 | await using var stream = new MemoryStream(); 31 | 32 | var position = buffer.GetPosition(0); 33 | while (buffer.TryGet(ref position, out var memory)) 34 | { 35 | stream.Write(memory.Span); 36 | } 37 | 38 | stream.Position = 0; 39 | 40 | var message = await MimeKit.MimeMessage.LoadAsync(stream, cancellationToken); 41 | 42 | _writer.WriteLine("Subject={0}", message.Subject); 43 | _writer.WriteLine("Body={0}", message.Body); 44 | 45 | return SmtpResponse.Ok; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/SmtpServer/Text/StringUtil.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers; 3 | using System.Text; 4 | 5 | namespace SmtpServer.Text 6 | { 7 | internal static class StringUtil 8 | { 9 | internal static string Create(ReadOnlySequence sequence) 10 | { 11 | return Create(sequence, Encoding.ASCII); 12 | } 13 | 14 | internal static string Create(ReadOnlySequence sequence, Encoding encoding) 15 | { 16 | if (sequence.Length == 0) 17 | { 18 | return null; 19 | } 20 | 21 | if (sequence.Length > short.MaxValue) 22 | { 23 | return null; 24 | } 25 | 26 | if (sequence.IsSingleSegment) 27 | { 28 | return encoding.GetString(sequence.First.Span); 29 | } 30 | else 31 | { 32 | Span buffer = stackalloc byte[(int)sequence.Length]; 33 | 34 | var i = 0; 35 | var position = sequence.GetPosition(0); 36 | 37 | while (sequence.TryGet(ref position, out var memory)) 38 | { 39 | var span = memory.Span; 40 | for (var j = 0; j < span.Length; i++, j++) 41 | { 42 | buffer[i] = span[j]; 43 | } 44 | } 45 | 46 | return encoding.GetString(buffer); 47 | } 48 | } 49 | 50 | internal static string Create(ref ReadOnlySpan buffer) 51 | { 52 | return Create(ref buffer, Encoding.ASCII); 53 | } 54 | 55 | internal static unsafe string Create(ref ReadOnlySpan buffer, Encoding encoding) 56 | { 57 | fixed (byte* ptr = buffer) 58 | { 59 | return encoding.GetString(ptr, buffer.Length); 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/SmtpServer/ISmtpServerOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace SmtpServer 5 | { 6 | /// 7 | /// Smtp Server Options Interface 8 | /// 9 | public interface ISmtpServerOptions 10 | { 11 | /// 12 | /// Gets the maximum message size option. 13 | /// 14 | IMaxMessageSizeOptions MaxMessageSizeOptions { get; } 15 | 16 | /// 17 | /// The maximum number of retries before quitting the session. 18 | /// 19 | int MaxRetryCount { get; } 20 | 21 | /// 22 | /// The maximum number of authentication attempts. 23 | /// 24 | int MaxAuthenticationAttempts { get; } 25 | 26 | /// 27 | /// Gets the SMTP server name. 28 | /// 29 | string ServerName { get; } 30 | 31 | /// 32 | /// Gets the collection of endpoints to listen on. 33 | /// 34 | IReadOnlyList Endpoints { get; } 35 | 36 | /// 37 | /// The timeout to use when waiting for a command from the client. 38 | /// 39 | TimeSpan CommandWaitTimeout { get; } 40 | 41 | /// 42 | /// The size of the buffer that is read from each call to the underlying network client. 43 | /// 44 | int NetworkBufferSize { get; } 45 | 46 | /// 47 | /// Gets the custom SMTP greeting message that the server sends immediately after a client connects, 48 | /// typically as the initial "220" response. The message can be dynamically generated based on the session context. 49 | /// If not set, a default greeting will be used (e.g., "220 mail.example.com ESMTP ready"). 50 | /// 51 | Func CustomSmtpGreeting { get; } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /examples/SampleApp/SampleMailClient.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using MailKit.Net.Smtp; 3 | using MimeKit; 4 | 5 | namespace SampleApp 6 | { 7 | public static class SampleMailClient 8 | { 9 | public static void Send( 10 | string from = null, 11 | string to = null, 12 | string subject = null, 13 | string user = null, 14 | string password = null, 15 | MimeEntity body = null, 16 | int count = 1, 17 | int recipients = 1, 18 | bool useSsl = false, 19 | int port = 9025) 20 | { 21 | var message = new MimeMessage(); 22 | 23 | message.From.Add(MailboxAddress.Parse(from ?? "from@sample.com")); 24 | 25 | for (var i = 0; i < recipients; i++) 26 | { 27 | message.To.Add(MailboxAddress.Parse(to ?? $"to_{i}@sample.com")); 28 | } 29 | 30 | message.Subject = subject ?? "Hello"; 31 | message.Body = body ?? new TextPart("plain") 32 | { 33 | Text = "Hello World" 34 | }; 35 | 36 | using var client = new SmtpClientEx(); 37 | 38 | client.Connect("localhost", port, useSsl); 39 | 40 | if (user != null && password != null) 41 | { 42 | client.Authenticate(user, password); 43 | } 44 | 45 | client.SendUnknownCommand("ABCD EFGH IJKL"); 46 | 47 | while (count-- > 0) 48 | { 49 | client.Send(message); 50 | } 51 | 52 | client.Disconnect(true); 53 | } 54 | 55 | internal class SmtpClientEx : SmtpClient 56 | { 57 | public SmtpResponse SendUnknownCommand(string command, CancellationToken cancellationToken = default) 58 | { 59 | return SendCommand(command, cancellationToken); 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/SmtpServer/Net/EndpointListenerFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Sockets; 3 | 4 | namespace SmtpServer.Net 5 | { 6 | /// 7 | /// Endpoint Listener Factory 8 | /// 9 | public class EndpointListenerFactory : IEndpointListenerFactory 10 | { 11 | /// 12 | /// Default Endpoint Listener Factory 13 | /// 14 | internal static readonly IEndpointListenerFactory Default = new EndpointListenerFactory(); 15 | 16 | /// 17 | /// Raised when an endpoint has been started. 18 | /// 19 | public event EventHandler EndpointStarted; 20 | 21 | /// 22 | /// Raised when an endpoint has been stopped. 23 | /// 24 | public event EventHandler EndpointStopped; 25 | 26 | /// 27 | public virtual IEndpointListener CreateListener(IEndpointDefinition endpointDefinition) 28 | { 29 | var tcpListener = new TcpListener(endpointDefinition.Endpoint); 30 | tcpListener.Start(); 31 | 32 | var endpointEventArgs = new EndpointEventArgs(endpointDefinition, tcpListener.LocalEndpoint); 33 | OnEndpointStarted(endpointEventArgs); 34 | 35 | return new EndpointListener(tcpListener, () => OnEndpointStopped(endpointEventArgs)); 36 | } 37 | 38 | /// 39 | /// Raises the EndPointStarted Event. 40 | /// 41 | /// The event data. 42 | protected virtual void OnEndpointStarted(EndpointEventArgs args) 43 | { 44 | EndpointStarted?.Invoke(this, args); 45 | } 46 | 47 | /// 48 | /// Raises the EndPointStopped Event. 49 | /// 50 | /// The event data. 51 | protected virtual void OnEndpointStopped(EndpointEventArgs args) 52 | { 53 | EndpointStopped?.Invoke(this, args); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/SmtpServer/Authentication/UserAuthenticator.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | 4 | namespace SmtpServer.Authentication 5 | { 6 | /// 7 | /// User Authenticator 8 | /// 9 | public abstract class UserAuthenticator : IUserAuthenticator 10 | { 11 | /// 12 | /// Default User Authenticator 13 | /// 14 | public static readonly IUserAuthenticator Default = new DefaultUserAuthenticator(); 15 | 16 | /// 17 | /// Authenticate a user account. 18 | /// 19 | /// The session context. 20 | /// The user to authenticate. 21 | /// The password of the user. 22 | /// The cancellation token. 23 | /// true if the user is authenticated, false if not. 24 | public abstract Task AuthenticateAsync( 25 | ISessionContext context, 26 | string user, 27 | string password, 28 | CancellationToken cancellationToken); 29 | 30 | sealed class DefaultUserAuthenticator : UserAuthenticator 31 | { 32 | /// 33 | /// Authenticate a user account. 34 | /// 35 | /// The session context. 36 | /// The user to authenticate. 37 | /// The password of the user. 38 | /// The cancellation token. 39 | /// true if the user is authenticated, false if not. 40 | public override Task AuthenticateAsync( 41 | ISessionContext context, 42 | string user, 43 | string password, 44 | CancellationToken cancellationToken) 45 | { 46 | return Task.FromResult(true); 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/SmtpServer/StateMachine/SmtpState.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | 5 | namespace SmtpServer.StateMachine 6 | { 7 | internal sealed class SmtpState : IEnumerable 8 | { 9 | internal SmtpState(SmtpStateId stateId) 10 | { 11 | StateId = stateId; 12 | } 13 | 14 | internal void Add(string command) 15 | { 16 | Transitions.Add(command, new SmtpStateTransition(context => true, context => StateId)); 17 | } 18 | 19 | internal void Add(string command, SmtpStateId state) 20 | { 21 | Transitions.Add(command, new SmtpStateTransition(context => true, context => state)); 22 | } 23 | 24 | internal void Add(string command, Func transitionDelegate) 25 | { 26 | Transitions.Add(command, new SmtpStateTransition(context => true, transitionDelegate)); 27 | } 28 | 29 | internal void Add(string command, Func canAcceptDelegate) 30 | { 31 | Transitions.Add(command, new SmtpStateTransition(canAcceptDelegate, context => StateId)); 32 | } 33 | 34 | internal void Add(string command, Func canAcceptDelegate, SmtpStateId state) 35 | { 36 | Transitions.Add(command, new SmtpStateTransition(canAcceptDelegate, context => state)); 37 | } 38 | 39 | internal void Add(string command, Func canAcceptDelegate, Func transitionDelegate) 40 | { 41 | Transitions.Add(command, new SmtpStateTransition(canAcceptDelegate, transitionDelegate)); 42 | } 43 | 44 | // this is just here for the collection initializer syntax to work 45 | IEnumerator IEnumerable.GetEnumerator() => throw new NotImplementedException(); 46 | 47 | internal SmtpStateId StateId { get; } 48 | 49 | internal IDictionary Transitions { get; } = new Dictionary(StringComparer.OrdinalIgnoreCase); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/SmtpServer/StateMachine/SmtpStateMachine.cs: -------------------------------------------------------------------------------- 1 | using SmtpServer.Protocol; 2 | using System.Linq; 3 | 4 | namespace SmtpServer.StateMachine 5 | { 6 | internal sealed class SmtpStateMachine 7 | { 8 | readonly SmtpSessionContext _context; 9 | SmtpState _state; 10 | SmtpStateTransition _transition; 11 | 12 | /// 13 | /// Constructor. 14 | /// 15 | /// The SMTP server session context. 16 | internal SmtpStateMachine(SmtpSessionContext context) 17 | { 18 | _state = SmtpStateTable.Shared[SmtpStateId.Initialized]; 19 | _context = context; 20 | } 21 | 22 | /// 23 | /// Try to accept the command given the current state. 24 | /// 25 | /// The command to accept. 26 | /// The error response to display if the command was not accepted. 27 | /// true if the command could be accepted, false if not. 28 | public bool TryAccept(SmtpCommand command, out SmtpResponse errorResponse) 29 | { 30 | errorResponse = null; 31 | 32 | if (_state.Transitions.TryGetValue(command.Name, out var transition) == false || transition.CanAccept(_context) == false) 33 | { 34 | var commands = _state.Transitions.Where(t => t.Value.CanAccept(_context)).Select(t => t.Key); 35 | 36 | errorResponse = new SmtpResponse(SmtpReplyCode.SyntaxError, $"expected {string.Join("/", commands)}"); 37 | return false; 38 | } 39 | 40 | _transition = transition; 41 | return true; 42 | } 43 | 44 | /// 45 | /// Accept the state and transition to the new state. 46 | /// 47 | /// The session context to use for accepting session based transitions. 48 | public void Transition(SmtpSessionContext context) 49 | { 50 | _state = SmtpStateTable.Shared[_transition.Transition(context)]; 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/SmtpServer/ISessionContext.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using SmtpServer.IO; 4 | 5 | namespace SmtpServer 6 | { 7 | /// 8 | /// Session Context Interface 9 | /// 10 | public interface ISessionContext 11 | { 12 | /// 13 | /// A unique Id for the Session 14 | /// 15 | public Guid SessionId { get; } 16 | 17 | /// 18 | /// Fired when a command is about to execute. 19 | /// 20 | event EventHandler CommandExecuting; 21 | 22 | /// 23 | /// Fired when a command has finished executing. 24 | /// 25 | event EventHandler CommandExecuted; 26 | 27 | /// 28 | /// Fired when a response exception has occured. 29 | /// 30 | event EventHandler ResponseException; 31 | 32 | /// 33 | /// Fired when the session has been authenticated. 34 | /// 35 | event EventHandler SessionAuthenticated; 36 | 37 | /// 38 | /// The service provider instance. 39 | /// 40 | IServiceProvider ServiceProvider { get; } 41 | 42 | /// 43 | /// Gets the options that the server was created with. 44 | /// 45 | ISmtpServerOptions ServerOptions { get; } 46 | 47 | /// 48 | /// Gets the endpoint definition. 49 | /// 50 | IEndpointDefinition EndpointDefinition { get; } 51 | 52 | /// 53 | /// Gets the pipeline to read from and write to. 54 | /// 55 | ISecurableDuplexPipe Pipe { get; } 56 | 57 | /// 58 | /// Returns the authentication context. 59 | /// 60 | AuthenticationContext Authentication { get; } 61 | 62 | /// 63 | /// Returns a set of propeties for the current session. 64 | /// 65 | IDictionary Properties { get; } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/SmtpServer/Protocol/HeloCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using SmtpServer.IO; 4 | 5 | namespace SmtpServer.Protocol 6 | { 7 | /// 8 | /// Helo Command 9 | /// 10 | public class HeloCommand : SmtpCommand 11 | { 12 | /// 13 | /// Smtp Helo Command 14 | /// 15 | public const string Command = "HELO"; 16 | 17 | /// 18 | /// Constructor. 19 | /// 20 | /// The domain name. 21 | public HeloCommand(string domainOrAddress) : base(Command) 22 | { 23 | DomainOrAddress = domainOrAddress; 24 | } 25 | 26 | /// 27 | /// Execute the command. 28 | /// 29 | /// The execution context to operate on. 30 | /// The cancellation token. 31 | /// Returns true if the command executed successfully such that the transition to the next state should occurr, false 32 | /// if the current state is to be maintained. 33 | internal override async Task ExecuteAsync(SmtpSessionContext context, CancellationToken cancellationToken) 34 | { 35 | var response = new SmtpResponse(SmtpReplyCode.Ok, GetGreeting(context)); 36 | 37 | await context.Pipe.Output.WriteReplyAsync(response, cancellationToken).ConfigureAwait(false); 38 | 39 | return true; 40 | } 41 | 42 | /// 43 | /// Returns the greeting to display to the remote host. 44 | /// 45 | /// The session context. 46 | /// The greeting text to display to the remote host. 47 | protected virtual string GetGreeting(ISessionContext context) 48 | { 49 | return $"{context.ServerOptions.ServerName} Hello {DomainOrAddress}, haven't we met before?"; 50 | } 51 | 52 | /// 53 | /// Gets the domain name. 54 | /// 55 | public string DomainOrAddress { get; } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/SmtpServer/Protocol/SmtpResponseException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace SmtpServer.Protocol 5 | { 6 | /// 7 | /// Smtp Response Exception 8 | /// 9 | public sealed class SmtpResponseException : Exception 10 | { 11 | static readonly IReadOnlyDictionary Empty = new Dictionary(); 12 | 13 | /// 14 | /// Constructor. 15 | /// 16 | /// The response to raise in the exception. 17 | public SmtpResponseException(SmtpResponse response) : this(response, false) { } 18 | 19 | /// 20 | /// Constructor. 21 | /// 22 | /// The response to raise in the exception. 23 | /// Indicates whether or not the session should terminate. 24 | public SmtpResponseException(SmtpResponse response, bool quit) : this(response, quit, Empty) { } 25 | 26 | /// 27 | /// Constructor. 28 | /// 29 | /// The response to raise in the exception. 30 | /// Indicates whether or not the session should terminate. 31 | /// The contextual properties to include as metadata for the exception. 32 | public SmtpResponseException(SmtpResponse response, bool quit, IReadOnlyDictionary properties) 33 | { 34 | Response = response; 35 | IsQuitRequested = quit; 36 | Properties = properties; 37 | } 38 | 39 | /// 40 | /// The response to return to the client. 41 | /// 42 | public SmtpResponse Response { get; } 43 | 44 | /// 45 | /// Indicates whether or not the session should terminate. 46 | /// 47 | public bool IsQuitRequested { get; } 48 | 49 | /// 50 | /// Returns a set of propeties for the current session. 51 | /// 52 | public IReadOnlyDictionary Properties { get; } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /examples/SampleApp/Examples/CommonPortsExample.cs: -------------------------------------------------------------------------------- 1 | using SmtpServer; 2 | using System.Security.Cryptography.X509Certificates; 3 | 4 | namespace SampleApp.Examples 5 | { 6 | public static class CommonPortsExample 7 | { 8 | public static void Run() 9 | { 10 | var options = new SmtpServerOptionsBuilder() 11 | .ServerName("SmtpServer SampleApp") 12 | 13 | // Port 25 is primarily used for SMTP relaying where emails are 14 | // sent from one mail server to another. Mail clients generally wont 15 | // use this port and most ISP will likely block it anyway. 16 | .Endpoint(builder => builder.Port(25).IsSecure(false)) 17 | 18 | // For a brief period in time this was a recognized port whereby 19 | // TLS was enabled by default on the connection. When connecting to 20 | // port 465 the client will upgrade its connection to SSL before 21 | // doing anything else. Port 465 is obsolete in favor of using 22 | // port 587 but it is still available by some mail servers. 23 | .Endpoint(builder => 24 | builder 25 | .Port(465) 26 | .IsSecure(true) // indicates that the client will need to upgrade to SSL upon connection 27 | .Certificate(new X509Certificate2())) // requires a valid certificate to be configured 28 | 29 | // Port 587 is the default port that should be used by modern mail 30 | // clients. When a certificate is provided, the server will advertise 31 | // that is supports the STARTTLS command which allows the client 32 | // to determine when they want to upgrade the connection to SSL. 33 | .Endpoint(builder => 34 | builder 35 | .Port(587) 36 | .AllowUnsecureAuthentication(false) // using 'false' here means that the user cant authenticate unless the connection is secure 37 | .Certificate(new X509Certificate2())) // requires a valid certificate to be configured 38 | 39 | .Build(); 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /src/SmtpServer/Text/TokenKind.cs: -------------------------------------------------------------------------------- 1 | namespace SmtpServer.Text 2 | { 3 | /// 4 | /// Token Kind 5 | /// 6 | public enum TokenKind 7 | { 8 | /// 9 | /// No token has been defined. 10 | /// 11 | None = 0, 12 | 13 | /// 14 | /// A text. 15 | /// 16 | Text, 17 | 18 | /// 19 | /// A number. 20 | /// 21 | Number, 22 | 23 | /// 24 | /// A single space character. 25 | /// 26 | Space, 27 | 28 | /// 29 | /// - 30 | /// 31 | Hyphen, 32 | 33 | /// 34 | /// . 35 | /// 36 | Period, 37 | 38 | /// 39 | /// [ 40 | /// 41 | LeftBracket, 42 | 43 | /// 44 | /// ] 45 | /// 46 | RightBracket, 47 | 48 | /// 49 | /// : 50 | /// 51 | Colon, 52 | 53 | /// 54 | /// > Greater-than sign 55 | /// 56 | GreaterThan, 57 | 58 | /// 59 | /// < Less-than sign 60 | /// 61 | LessThan = 10, 62 | 63 | /// 64 | /// , 65 | /// 66 | Comma, 67 | 68 | /// 69 | /// @ 70 | /// 71 | At, 72 | 73 | /// 74 | /// " 75 | /// 76 | Quote, 77 | 78 | /// 79 | /// = 80 | /// 81 | Equal, 82 | 83 | /// 84 | /// / 85 | /// 86 | Slash, 87 | 88 | /// 89 | /// \ 90 | /// 91 | Backslash, 92 | 93 | /// 94 | /// + 95 | /// 96 | Plus, 97 | 98 | /// 99 | /// Unknown. 100 | /// 101 | Other, 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/SmtpServer/Protocol/QuitCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using System.Net.Sockets; 4 | using SmtpServer.IO; 5 | using System.IO; 6 | 7 | namespace SmtpServer.Protocol 8 | { 9 | /// 10 | /// Quit Command 11 | /// 12 | public sealed class QuitCommand : SmtpCommand 13 | { 14 | /// 15 | /// Smtp Quit Command 16 | /// 17 | public const string Command = "QUIT"; 18 | 19 | /// 20 | /// Constructor. 21 | /// 22 | public QuitCommand() : base(Command) { } 23 | 24 | /// 25 | /// Execute the command. 26 | /// 27 | /// The execution context to operate on. 28 | /// The cancellation token. 29 | /// Returns true if the command executed successfully such that the transition to the next state should occurr, false 30 | /// if the current state is to be maintained. 31 | internal override async Task ExecuteAsync(SmtpSessionContext context, CancellationToken cancellationToken) 32 | { 33 | context.IsQuitRequested = true; 34 | 35 | try 36 | { 37 | await context.Pipe.Output.WriteReplyAsync(SmtpResponse.ServiceClosingTransmissionChannel, cancellationToken).ConfigureAwait(false); 38 | } 39 | catch (IOException ioException) 40 | { 41 | if (ioException.GetBaseException() is SocketException socketException) 42 | { 43 | // Some mail servers will send the QUIT command and then disconnect before 44 | // waiting for the 221 response from the server. This doesnt follow the spec but 45 | // we can gracefully handle this situation as in theory everything should be fine 46 | if (socketException.SocketErrorCode == SocketError.ConnectionReset) 47 | { 48 | return true; 49 | } 50 | } 51 | 52 | throw ioException; 53 | } 54 | 55 | return true; 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/SmtpServer.Tests/RawSmtpClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Net.Sockets; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace SmtpServer.Tests 8 | { 9 | internal class RawSmtpClient : IDisposable 10 | { 11 | private readonly TcpClient _tcpClient; 12 | private NetworkStream _networkStream; 13 | private readonly string _host; 14 | private readonly int _port; 15 | 16 | internal RawSmtpClient(string host, int port) 17 | { 18 | _host = host; 19 | _port = port; 20 | 21 | _tcpClient = new TcpClient(); 22 | } 23 | 24 | public void Dispose() 25 | { 26 | _networkStream?.Dispose(); 27 | _tcpClient.Dispose(); 28 | } 29 | 30 | internal async Task ConnectAsync() 31 | { 32 | await _tcpClient.ConnectAsync(new IPEndPoint(IPAddress.Parse(_host), _port)); 33 | _networkStream = _tcpClient.GetStream(); 34 | 35 | var greetingResponse = await WaitForDataAsync(); 36 | if (greetingResponse.StartsWith("220")) 37 | { 38 | return true; 39 | } 40 | 41 | return false; 42 | } 43 | 44 | internal async Task SendCommandAsync(string command) 45 | { 46 | var commandData = Encoding.UTF8.GetBytes($"{command}\r\n"); 47 | 48 | await _networkStream.WriteAsync(commandData, 0, commandData.Length); 49 | return await WaitForDataAsync(); 50 | } 51 | 52 | internal async Task SendDataAsync(string data) 53 | { 54 | var mailData = Encoding.UTF8.GetBytes(data); 55 | 56 | await _networkStream.WriteAsync(mailData, 0, mailData.Length); 57 | } 58 | 59 | internal async Task WaitForDataAsync() 60 | { 61 | var buffer = new byte[1024]; 62 | int bytesRead; 63 | 64 | while ((bytesRead = await _networkStream.ReadAsync(buffer, 0, buffer.Length)) > 0) 65 | { 66 | var receivedData = Encoding.UTF8.GetString(buffer, 0, bytesRead); 67 | return receivedData; 68 | } 69 | 70 | return null; 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/SmtpServer/Extensions/TaskExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | 5 | namespace SmtpServer 6 | { 7 | /// 8 | /// Task Extensions 9 | /// 10 | static class TaskExtensions 11 | { 12 | /// 13 | /// Configures the task to stop waiting when the cancellation has been requested. 14 | /// 15 | /// The task to wait for. 16 | /// The cancellation token to watch. 17 | /// The original task. 18 | public static async Task WithCancellation(this Task task, CancellationToken cancellationToken) 19 | { 20 | var tcs = new TaskCompletionSource(); 21 | 22 | using (cancellationToken.Register(s => ((TaskCompletionSource)s).TrySetResult(true), tcs)) 23 | { 24 | if (task != await Task.WhenAny(task, tcs.Task).ConfigureAwait(false)) 25 | { 26 | throw new OperationCanceledException(cancellationToken); 27 | } 28 | } 29 | 30 | await task.ConfigureAwait(false); 31 | } 32 | 33 | /// 34 | /// Configures the task to stop waiting when the cancellation has been requested. 35 | /// 36 | /// The return type of the task. 37 | /// The task to wait for. 38 | /// The cancellation token to watch. 39 | /// The original task. 40 | public static async Task WithCancellation(this Task task, CancellationToken cancellationToken) 41 | { 42 | var tcs = new TaskCompletionSource(); 43 | 44 | using (cancellationToken.Register(s => ((TaskCompletionSource)s).TrySetResult(true), tcs)) 45 | { 46 | if (task != await Task.WhenAny(task, tcs.Task).ConfigureAwait(false)) 47 | { 48 | throw new OperationCanceledException(cancellationToken); 49 | } 50 | } 51 | 52 | return await task.ConfigureAwait(false); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/SmtpServer/Protocol/ProxyCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | 5 | namespace SmtpServer.Protocol 6 | { 7 | /// 8 | /// Support for proxy protocol version 1 header for use with HAProxy. 9 | /// Documented at http://www.haproxy.org/download/1.8/doc/proxy-protocol.txt 10 | /// This should always (and only ever) be the first command seen on a new connection from HAProxy 11 | /// 12 | public sealed class ProxyCommand : SmtpCommand 13 | { 14 | /// 15 | /// Proxy Source Endpoint Key 16 | /// 17 | public const string ProxySourceEndpointKey = "Proxy:ProxySourceEndpoint"; 18 | 19 | /// 20 | /// Proxy Destination Endpoint Key 21 | /// 22 | public const string ProxyDestinationEndpointKey = "Proxy:ProxyDestinationEndpoint"; 23 | 24 | /// 25 | /// Smtp Proxy Command 26 | /// 27 | public const string Command = "PROXY"; 28 | 29 | /// 30 | /// Constructor. 31 | /// 32 | /// The source endpoint. 33 | /// The destination endpoint. 34 | public ProxyCommand(IPEndPoint sourceEndpoint = null, IPEndPoint destinationEndpoint = null) : base(Command) 35 | { 36 | SourceEndpoint = sourceEndpoint; 37 | DestinationEndpoint = destinationEndpoint; 38 | } 39 | 40 | /// 41 | internal override Task ExecuteAsync(SmtpSessionContext context, CancellationToken cancellationToken) 42 | { 43 | context.Properties.Add(ProxySourceEndpointKey, SourceEndpoint); 44 | context.Properties.Add(ProxyDestinationEndpointKey, DestinationEndpoint); 45 | 46 | // Do not transition smtp protocol state for these commands. 47 | return Task.FromResult(false); 48 | } 49 | 50 | /// 51 | /// The source endpoint. 52 | /// 53 | public IPEndPoint SourceEndpoint { get; } 54 | 55 | /// 56 | /// The destination endpoint. 57 | /// 58 | public IPEndPoint DestinationEndpoint { get; } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /examples/SampleApp/Examples/ServerCancellingExample.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using SmtpServer; 5 | using SmtpServer.ComponentModel; 6 | 7 | namespace SampleApp.Examples 8 | { 9 | public static class ServerCancellingExample 10 | { 11 | public static void Run() 12 | { 13 | var cancellationTokenSource = new CancellationTokenSource(); 14 | 15 | var options = new SmtpServerOptionsBuilder() 16 | .ServerName("SmtpServer SampleApp") 17 | .Port(9025) 18 | .Build(); 19 | 20 | var serviceProvider = new ServiceProvider(); 21 | serviceProvider.Add(new SampleMailboxFilter(TimeSpan.FromSeconds(5))); 22 | 23 | var server = new SmtpServer.SmtpServer(options, serviceProvider); 24 | server.SessionCreated += OnSessionCreated; 25 | server.SessionCompleted += OnSessionCompleted; 26 | server.SessionFaulted += OnSessionFaulted; 27 | server.SessionCancelled += OnSessionCancelled; 28 | 29 | var serverTask = server.StartAsync(cancellationTokenSource.Token); 30 | 31 | // ReSharper disable once MethodSupportsCancellation 32 | Task.Run(() => SampleMailClient.Send()); 33 | 34 | Console.WriteLine("Press any key to cancel the server."); 35 | Console.ReadKey(); 36 | 37 | Console.WriteLine("Forcibily cancelling the server and any active sessions"); 38 | 39 | cancellationTokenSource.Cancel(); 40 | serverTask.WaitWithoutException(); 41 | 42 | Console.WriteLine("The server has been cancelled."); 43 | } 44 | 45 | static void OnSessionCreated(object sender, SessionEventArgs e) 46 | { 47 | Console.WriteLine("Session Created."); 48 | } 49 | 50 | static void OnSessionCompleted(object sender, SessionEventArgs e) 51 | { 52 | Console.WriteLine("Session Completed"); 53 | } 54 | 55 | static void OnSessionFaulted(object sender, SessionFaultedEventArgs e) 56 | { 57 | Console.WriteLine("Session Faulted: {0}", e.Exception); 58 | } 59 | 60 | static void OnSessionCancelled(object sender, SessionEventArgs e) 61 | { 62 | Console.WriteLine("Session Cancelled"); 63 | } 64 | } 65 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## v11.1.0 4 | 5 | - Added: Configuration option to define the maximum allowed message size. 6 | - Added: Support for custom SMTP greeting messages. 7 | - Improved: Optimized protection against excessively long text segments to enhance stability and performance. 8 | 9 | ```cs 10 | var options = new SmtpServerOptionsBuilder() 11 | .ServerName("My mail server") 12 | .MaxMessageSize(5242880, MaxMessageSizeHandling.Strict) //5MB 13 | .CommandWaitTimeout(TimeSpan.FromSeconds(60)) 14 | ``` 15 | 16 | ## v11.0.0 17 | 18 | - Added: SslProtocol support to SecurableDuplexPipe. 19 | - Added: GitHub workflow for automated build and unit tests. 20 | - Added: Summary information to classes for improved code documentation. 21 | - Added: Session timeout to automatically close connections that remain open for too long. 22 | - Fixed: Missing SessionCreated event in failure scenarios. 23 | 24 | ## v10.0.1 25 | 26 | - Fixed a bug that could cause a failure to recognize commands when using a large number of recipients. 27 | 28 | ## v10.0.0 29 | 30 | - Removed MailboxFilterResult in favor of bool result. Impementations can throw SmtpResponseException for more control. 31 | - Handled servers that send the QUIT command and immediately close the connection. 32 | - Added an ICertificateFactory on the Endpoint that allows a new certificate to be created when required without having to restart the server. 33 | 34 | ## v9.1.0 35 | 36 | - Added a ResponseException event handler to the ISessionContext to enable external logging of exceptions. 37 | 38 | ## v9.0.3 39 | 40 | - Fixed a bug with the session not closing when the cancellation token was cancelled. 41 | 42 | ## v9.0.2 43 | 44 | - Fixed a performance issue whereby the server would block incoming connections whilst another connection was upgrading to SSL. 45 | 46 | ## v9.0.0 47 | 48 | - Breaking API change by removing the Certificate from the server options and adding it to the endpoint. 49 | 50 | ## v8 51 | 52 | - Version 8 contains substantial refactoring to take advantage of [System.IO.Pipelines](https://www.nuget.org/packages/System.IO.Pipelines/) in an effort to improve throughput performance and reduce memory allocations. 53 | - In addition to this there are also changes to make service resolution easier via Dependency Injection through utilizing the [IServiceProvider](https://docs.microsoft.com/en-us/dotnet/api/system.iserviceprovider) interface. 54 | -------------------------------------------------------------------------------- /src/SmtpServer/Protocol/SmtpCommandFactory.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Net; 3 | using SmtpServer.Mail; 4 | 5 | namespace SmtpServer.Protocol 6 | { 7 | /// 8 | /// Smtp Command Factory 9 | /// 10 | public class SmtpCommandFactory : ISmtpCommandFactory 11 | { 12 | /// 13 | public virtual SmtpCommand CreateHelo(string domainOrAddress) 14 | { 15 | return new HeloCommand(domainOrAddress); 16 | } 17 | 18 | /// 19 | public virtual SmtpCommand CreateEhlo(string domainOrAddress) 20 | { 21 | return new EhloCommand(domainOrAddress); 22 | } 23 | 24 | /// 25 | public virtual SmtpCommand CreateMail(IMailbox address, IReadOnlyDictionary parameters) 26 | { 27 | return new MailCommand(address, parameters); 28 | } 29 | 30 | /// 31 | public virtual SmtpCommand CreateRcpt(IMailbox address) 32 | { 33 | return new RcptCommand(address); 34 | } 35 | 36 | /// 37 | public virtual SmtpCommand CreateData() 38 | { 39 | return new DataCommand(); 40 | } 41 | 42 | /// 43 | public virtual SmtpCommand CreateQuit() 44 | { 45 | return new QuitCommand(); 46 | } 47 | 48 | /// 49 | public virtual SmtpCommand CreateNoop() 50 | { 51 | return new NoopCommand(); 52 | } 53 | 54 | /// 55 | public virtual SmtpCommand CreateRset() 56 | { 57 | return new RsetCommand(); 58 | } 59 | 60 | /// 61 | public virtual SmtpCommand CreateStartTls() 62 | { 63 | return new StartTlsCommand(); 64 | } 65 | 66 | /// 67 | public SmtpCommand CreateAuth(AuthenticationMethod method, string parameter) 68 | { 69 | return new AuthCommand(method, parameter); 70 | } 71 | 72 | /// 73 | public virtual SmtpCommand CreateProxy(IPEndPoint sourceEndpoint, IPEndPoint destinationEndpoint) 74 | { 75 | return new ProxyCommand(sourceEndpoint, destinationEndpoint); 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/SmtpServer.Benchmarks/ThroughputBenchmarks.cs: -------------------------------------------------------------------------------- 1 | using BenchmarkDotNet.Attributes; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using MimeKit; 5 | using SmtpServer.ComponentModel; 6 | using SmtpClient = MailKit.Net.Smtp.SmtpClient; 7 | 8 | namespace SmtpServer.Benchmarks 9 | { 10 | [MemoryDiagnoser] 11 | public class ThroughputBenchmarks 12 | { 13 | readonly SmtpServer _smtpServer = new SmtpServer( 14 | new SmtpServerOptionsBuilder() 15 | .Port(9025, false) 16 | .Build(), 17 | ServiceProvider.Default); 18 | 19 | readonly CancellationTokenSource _smtpServerCancellationTokenSource = new CancellationTokenSource(); 20 | 21 | readonly SmtpClient _smtpClient = new SmtpClient(); 22 | 23 | static readonly MimeMessage Message1 = MimeMessage.Load(typeof(ThroughputBenchmarks).Assembly.GetManifestResourceStream("SmtpServer.Benchmarks.Test1.eml")); 24 | static readonly MimeMessage Message2 = MimeMessage.Load(typeof(ThroughputBenchmarks).Assembly.GetManifestResourceStream("SmtpServer.Benchmarks.Test2.eml")); 25 | static readonly MimeMessage Message3 = MimeMessage.Load(typeof(ThroughputBenchmarks).Assembly.GetManifestResourceStream("SmtpServer.Benchmarks.Test3.eml")); 26 | static readonly MimeMessage Message4 = MimeMessage.Load(typeof(ThroughputBenchmarks).Assembly.GetManifestResourceStream("SmtpServer.Benchmarks.Test4.eml")); 27 | 28 | [GlobalSetup] 29 | public void SmtpServerSetup() 30 | { 31 | _smtpServer.StartAsync(_smtpServerCancellationTokenSource.Token); 32 | 33 | _smtpClient.Connect("localhost", 9025); 34 | } 35 | 36 | [GlobalCleanup] 37 | public Task SmtpServerCleanupAsync() 38 | { 39 | _smtpClient.Disconnect(true); 40 | 41 | _smtpServerCancellationTokenSource.Cancel(); 42 | 43 | return _smtpServer.ShutdownTask; 44 | } 45 | 46 | [Benchmark] 47 | public void Send1() 48 | { 49 | _smtpClient.Send(Message1); 50 | } 51 | 52 | [Benchmark] 53 | public void Send2() 54 | { 55 | _smtpClient.Send(Message2); 56 | } 57 | 58 | [Benchmark] 59 | public void Send3() 60 | { 61 | _smtpClient.Send(Message3); 62 | } 63 | 64 | [Benchmark] 65 | public void Send4() 66 | { 67 | _smtpClient.Send(Message4); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /examples/SampleApp/Examples/ServerShutdownExample.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using SmtpServer; 5 | using SmtpServer.ComponentModel; 6 | 7 | namespace SampleApp.Examples 8 | { 9 | public static class ServerShutdownExample 10 | { 11 | public static void Run() 12 | { 13 | var cancellationTokenSource = new CancellationTokenSource(); 14 | 15 | var options = new SmtpServerOptionsBuilder() 16 | .ServerName("SmtpServer SampleApp") 17 | .Port(9025) 18 | .Build(); 19 | 20 | var serviceProvider = new ServiceProvider(); 21 | serviceProvider.Add(new SampleMailboxFilter(TimeSpan.FromSeconds(2))); 22 | 23 | var server = new SmtpServer.SmtpServer(options, serviceProvider); 24 | server.SessionCreated += OnSessionCreated; 25 | server.SessionCompleted += OnSessionCompleted; 26 | server.SessionFaulted += OnSessionFaulted; 27 | server.SessionCancelled += OnSessionCancelled; 28 | 29 | var serverTask = server.StartAsync(cancellationTokenSource.Token); 30 | 31 | // ReSharper disable once MethodSupportsCancellation 32 | Task.Run(() => SampleMailClient.Send()); 33 | 34 | Console.WriteLine("Press any key to shudown the server."); 35 | Console.ReadKey(); 36 | 37 | Console.WriteLine("Gracefully shutting down the server."); 38 | server.Shutdown(); 39 | 40 | server.ShutdownTask.WaitWithoutException(); 41 | Console.WriteLine("The server is no longer accepting new connections."); 42 | 43 | Console.WriteLine("Waiting for active sessions to complete."); 44 | serverTask.WaitWithoutException(); 45 | 46 | Console.WriteLine("All active sessions are complete."); 47 | } 48 | 49 | static void OnSessionCreated(object sender, SessionEventArgs e) 50 | { 51 | Console.WriteLine("Session Created."); 52 | } 53 | 54 | static void OnSessionCompleted(object sender, SessionEventArgs e) 55 | { 56 | Console.WriteLine("Session Completed"); 57 | } 58 | 59 | static void OnSessionFaulted(object sender, SessionFaultedEventArgs e) 60 | { 61 | Console.WriteLine("Session Faulted: {0}", e.Exception); 62 | } 63 | 64 | static void OnSessionCancelled(object sender, SessionEventArgs e) 65 | { 66 | Console.WriteLine("Session Cancelled"); 67 | } 68 | } 69 | } -------------------------------------------------------------------------------- /src/SmtpServer/Protocol/RcptCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using SmtpServer.ComponentModel; 5 | using SmtpServer.IO; 6 | using SmtpServer.Mail; 7 | using SmtpServer.Storage; 8 | 9 | namespace SmtpServer.Protocol 10 | { 11 | /// 12 | /// Rcpt Command 13 | /// 14 | public sealed class RcptCommand : SmtpCommand 15 | { 16 | /// 17 | /// Smtp Rcpt Command 18 | /// 19 | public const string Command = "RCPT"; 20 | 21 | /// 22 | /// Constructor. 23 | /// 24 | /// The address. 25 | public RcptCommand(IMailbox address) : base(Command) 26 | { 27 | Address = address; 28 | } 29 | 30 | /// 31 | /// Execute the command. 32 | /// 33 | /// The execution context to operate on. 34 | /// The cancellation token. 35 | /// Returns true if the command executed successfully such that the transition to the next state should occurr, false 36 | /// if the current state is to be maintained. 37 | internal override async Task ExecuteAsync(SmtpSessionContext context, CancellationToken cancellationToken) 38 | { 39 | var mailboxFilter = context.ServiceProvider.GetService(context, MailboxFilter.Default); 40 | 41 | using var container = new DisposableContainer(mailboxFilter); 42 | 43 | switch (await container.Instance.CanDeliverToAsync(context, Address, context.Transaction.From, cancellationToken).ConfigureAwait(false)) 44 | { 45 | case true: 46 | context.Transaction.To.Add(Address); 47 | await context.Pipe.Output.WriteReplyAsync(SmtpResponse.Ok, cancellationToken).ConfigureAwait(false); 48 | return true; 49 | 50 | case false: 51 | await context.Pipe.Output.WriteReplyAsync(SmtpResponse.MailboxUnavailable, cancellationToken).ConfigureAwait(false); 52 | return false; 53 | } 54 | 55 | throw new NotSupportedException("The Acceptance state is not supported."); 56 | } 57 | 58 | /// 59 | /// Gets the address that the mail is to. 60 | /// 61 | public IMailbox Address { get; } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /examples/SampleApp/SampleMailboxFilter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using SmtpServer; 6 | using SmtpServer.Mail; 7 | using SmtpServer.Net; 8 | using SmtpServer.Storage; 9 | using SmtpServer.Protocol; 10 | 11 | namespace SampleApp 12 | { 13 | public class SampleMailboxFilter : MailboxFilter 14 | { 15 | readonly TimeSpan _delay; 16 | public SampleMailboxFilter() : this(TimeSpan.Zero) { } 17 | 18 | public SampleMailboxFilter(TimeSpan delay) 19 | { 20 | _delay = delay; 21 | } 22 | 23 | /// 24 | /// Returns a value indicating whether the given mailbox can be accepted as a sender. 25 | /// 26 | /// The session context. 27 | /// The mailbox to test. 28 | /// The estimated message size to accept. 29 | /// The cancellation token. 30 | /// Returns true if the mailbox is accepted, false if not. 31 | public override async Task CanAcceptFromAsync( 32 | ISessionContext context, 33 | IMailbox @from, 34 | int size, 35 | CancellationToken cancellationToken) 36 | { 37 | await Task.Delay(_delay, cancellationToken); 38 | 39 | if (@from == Mailbox.Empty) 40 | { 41 | throw new SmtpResponseException(SmtpResponse.MailboxNameNotAllowed); 42 | } 43 | 44 | var endpoint = (IPEndPoint)context.Properties[EndpointListener.RemoteEndPointKey]; 45 | 46 | if (endpoint.Address.Equals(IPAddress.Parse("127.0.0.1"))) 47 | { 48 | return true; 49 | } 50 | 51 | return false; 52 | } 53 | 54 | /// 55 | /// Returns a value indicating whether the given mailbox can be accepted as a recipient to the given sender. 56 | /// 57 | /// The session context. 58 | /// The mailbox to test. 59 | /// The sender's mailbox. 60 | /// The cancellation token. 61 | /// Returns true if the mailbox can be delivered to, false if not. 62 | public override async Task CanDeliverToAsync( 63 | ISessionContext context, 64 | IMailbox to, 65 | IMailbox @from, 66 | CancellationToken cancellationToken) 67 | { 68 | await Task.Delay(_delay, cancellationToken); 69 | 70 | return true; 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | Components/ 14 | Build/ 15 | 16 | # Build results 17 | [Dd]ebug/ 18 | [Dd]ebugPublic/ 19 | [Rr]elease/ 20 | [Rr]eleases/ 21 | x64/ 22 | x86/ 23 | build/ 24 | bld/ 25 | [Bb]in/ 26 | [Oo]bj/ 27 | 28 | !Build/ 29 | Build/release 30 | 31 | # Roslyn cache directories 32 | *.ide/ 33 | 34 | *_i.c 35 | *_p.c 36 | *_i.h 37 | *.ilk 38 | *.meta 39 | *.obj 40 | *.pch 41 | *.pdb 42 | *.pgc 43 | *.pgd 44 | *.rsp 45 | *.sbr 46 | *.tlb 47 | *.tli 48 | *.tlh 49 | *.tmp 50 | *.tmp_proj 51 | *.log 52 | *.vspscc 53 | *.vssscc 54 | .builds 55 | *.pidb 56 | *.svclog 57 | *.scc 58 | 59 | # ignore signing key file 60 | *.pfx 61 | 62 | # Visual Studio profiler 63 | *.psess 64 | *.vsp 65 | *.vspx 66 | 67 | # ReSharper is a .NET coding add-in 68 | _ReSharper*/ 69 | *.[Rr]e[Ss]harper 70 | *.DotSettings.user 71 | 72 | # TeamCity is a build add-in 73 | _TeamCity* 74 | 75 | # DotCover is a Code Coverage Tool 76 | *.dotCover 77 | 78 | # DocProject is a documentation generator add-in 79 | DocProject/buildhelp/ 80 | DocProject/Help/*.HxT 81 | DocProject/Help/*.HxC 82 | DocProject/Help/*.hhc 83 | DocProject/Help/*.hhk 84 | DocProject/Help/*.hhp 85 | DocProject/Help/Html2 86 | DocProject/Help/html 87 | 88 | # Click-Once directory 89 | publish/ 90 | 91 | # Publish Web Output 92 | *.[Pp]ublish.xml 93 | *.azurePubxml 94 | # TODO: Comment the next line if you want to checkin your web deploy settings 95 | # but database connection strings (with potential passwords) will be unencrypted 96 | *.pubxml 97 | *.publishproj 98 | 99 | # NuGet Packages 100 | *.nupkg 101 | # The packages folder can be ignored because of Package Restore 102 | **/packages/* 103 | # except build/, which is used as an MSBuild target. 104 | !**/packages/build/ 105 | 106 | # Windows Azure Build Output 107 | csx/ 108 | *.build.csdef 109 | 110 | # Windows Store app package directory 111 | AppPackages/ 112 | 113 | # Others 114 | *.[Cc]ache 115 | ClientBin/ 116 | [Ss]tyle[Cc]op.* 117 | ~$* 118 | *~ 119 | *.dbmdl 120 | *.dbproj.schemaview 121 | *.pfx 122 | *.publishsettings 123 | node_modules/ 124 | bower_components/ 125 | 126 | # Backup & report files from converting an old project file 127 | # to a newer Visual Studio version. Backup files are not needed, 128 | # because we have git ;-) 129 | _UpgradeReport_Files/ 130 | Backup*/ 131 | UpgradeLog*.XML 132 | UpgradeLog*.htm 133 | 134 | # Microsoft Fakes 135 | FakesAssemblies/ 136 | 137 | # Node.js Tools for Visual Studio 138 | .ntvs_analysis.dat 139 | 140 | .vs/ 141 | .vscode/ 142 | project.lock.json -------------------------------------------------------------------------------- /src/SmtpServer/Net/EndpointListener.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Sockets; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using SmtpServer.IO; 6 | 7 | namespace SmtpServer.Net 8 | { 9 | /// 10 | /// Endpoint Listener 11 | /// 12 | public sealed class EndpointListener : IEndpointListener 13 | { 14 | /// 15 | /// EndpointListener LocalEndPoint Key 16 | /// 17 | public const string LocalEndPointKey = "EndpointListener:LocalEndPoint"; 18 | 19 | /// 20 | /// EndpointListener RemoteEndPoint Key 21 | /// 22 | public const string RemoteEndPointKey = "EndpointListener:RemoteEndPoint"; 23 | 24 | readonly TcpListener _tcpListener; 25 | readonly Action _disposeAction; 26 | 27 | /// 28 | /// Constructor. 29 | /// 30 | /// The TCP listener for the endpoint. 31 | /// The action to execute when the listener has been disposed. 32 | internal EndpointListener(TcpListener tcpListener, Action disposeAction) 33 | { 34 | _tcpListener = tcpListener; 35 | _disposeAction = disposeAction; 36 | } 37 | 38 | /// 39 | /// Returns a securable pipe to the endpoint. 40 | /// 41 | /// The session context that the pipe is being created for. 42 | /// The cancellation token. 43 | /// The securable pipe from the endpoint. 44 | public async Task GetPipeAsync(ISessionContext context, CancellationToken cancellationToken) 45 | { 46 | var tcpClient = await _tcpListener.AcceptTcpClientAsync().WithCancellation(cancellationToken).ConfigureAwait(false); 47 | cancellationToken.ThrowIfCancellationRequested(); 48 | 49 | context.Properties.Add(LocalEndPointKey, _tcpListener.LocalEndpoint); 50 | context.Properties.Add(RemoteEndPointKey, tcpClient.Client.RemoteEndPoint); 51 | 52 | var stream = tcpClient.GetStream(); 53 | 54 | return new SecurableDuplexPipe(stream, () => 55 | { 56 | try 57 | { 58 | tcpClient.Close(); 59 | tcpClient.Dispose(); 60 | } 61 | catch { } 62 | }); 63 | } 64 | 65 | /// 66 | /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. 67 | /// 68 | public void Dispose() 69 | { 70 | _tcpListener.Stop(); 71 | _disposeAction(); 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/SmtpServer/Storage/CompositeMailboxFilter.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using SmtpServer.Mail; 5 | 6 | namespace SmtpServer.Storage 7 | { 8 | internal sealed class CompositeMailboxFilter : IMailboxFilter 9 | { 10 | readonly IMailboxFilter[] _filters; 11 | 12 | /// 13 | /// Constructor. 14 | /// 15 | /// The list of filters to run in order. 16 | public CompositeMailboxFilter(params IMailboxFilter[] filters) 17 | { 18 | _filters = filters; 19 | } 20 | 21 | /// 22 | /// Returns a value indicating whether the given mailbox can be accepted as a sender. 23 | /// 24 | /// The session context. 25 | /// The mailbox to test. 26 | /// The estimated message size to accept. 27 | /// The cancellation token. 28 | /// Returns true if the mailbox is accepted, false if not. 29 | public async Task CanAcceptFromAsync( 30 | ISessionContext context, 31 | IMailbox @from, 32 | int size, 33 | CancellationToken cancellationToken = default) 34 | { 35 | if (_filters == null || _filters.Any() == false) 36 | { 37 | return true; 38 | } 39 | 40 | var results = await Task.WhenAll(_filters.Select(f => f.CanAcceptFromAsync(context, @from, size, cancellationToken))).ConfigureAwait(false); 41 | 42 | return results.All(r => r == true); 43 | } 44 | 45 | /// 46 | /// Returns a value indicating whether the given mailbox can be accepted as a recipient to the given sender. 47 | /// 48 | /// The session context. 49 | /// The mailbox to test. 50 | /// The sender's mailbox. 51 | /// The cancellation token. 52 | /// Returns true if the mailbox can be delivered to, false if not. 53 | public async Task CanDeliverToAsync( 54 | ISessionContext context, 55 | IMailbox to, 56 | IMailbox @from, 57 | CancellationToken cancellationToken = default) 58 | { 59 | if (_filters == null || _filters.Any() == false) 60 | { 61 | return true; 62 | } 63 | 64 | var results = await Task.WhenAll(_filters.Select(f => f.CanDeliverToAsync(context, to, @from, cancellationToken))).ConfigureAwait(false); 65 | 66 | return results.All(r => r == true); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/SmtpServer/IO/PipeWriterExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO.Pipelines; 3 | using System.Text; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using SmtpServer.Protocol; 7 | 8 | namespace SmtpServer.IO 9 | { 10 | /// 11 | /// Pipe Writer Extensions 12 | /// 13 | public static class PipeWriterExtensions 14 | { 15 | /// 16 | /// Write a line of text to the pipe. 17 | /// 18 | /// The writer to perform the operation on. 19 | /// The text to write to the writer. 20 | internal static void WriteLine(this PipeWriter writer, string text) 21 | { 22 | if (writer == null) 23 | { 24 | throw new ArgumentNullException(nameof(writer)); 25 | } 26 | 27 | WriteLine(writer, Encoding.ASCII, text); 28 | } 29 | 30 | /// 31 | /// Write a line of text to the writer. 32 | /// 33 | /// The writer to perform the operation on. 34 | /// The encoding to use for the text. 35 | /// The text to write to the writer. 36 | static unsafe void WriteLine(this PipeWriter writer, Encoding encoding, string text) 37 | { 38 | if (writer == null) 39 | { 40 | throw new ArgumentNullException(nameof(writer)); 41 | } 42 | 43 | fixed (char* ptr = text) 44 | { 45 | var count = encoding.GetByteCount(ptr, text.Length); 46 | 47 | fixed (byte* b = writer.GetSpan(count + 2)) 48 | { 49 | encoding.GetBytes(ptr, text.Length, b, count); 50 | 51 | b[count + 0] = 13; 52 | b[count + 1] = 10; 53 | } 54 | 55 | writer.Advance(count + 2); 56 | } 57 | } 58 | 59 | /// 60 | /// Write a reply to the client. 61 | /// 62 | /// The writer to perform the operation on. 63 | /// The response to write. 64 | /// The cancellation token. 65 | /// A task which performs the operation. 66 | public static ValueTask WriteReplyAsync(this PipeWriter writer, SmtpResponse response, CancellationToken cancellationToken) 67 | { 68 | if (writer == null) 69 | { 70 | throw new ArgumentNullException(nameof(writer)); 71 | } 72 | 73 | writer.WriteLine($"{(int)response.ReplyCode} {response.Message}"); 74 | 75 | return writer.FlushAsync(cancellationToken); 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /examples/SampleApp/Examples/SecureServerExample.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Net; 4 | using System.Net.Security; 5 | using System.Security.Cryptography.X509Certificates; 6 | using System.Threading; 7 | using SmtpServer; 8 | using SmtpServer.ComponentModel; 9 | using SmtpServer.Tracing; 10 | 11 | namespace SampleApp.Examples 12 | { 13 | public static class SecureServerExample 14 | { 15 | public static void Run() 16 | { 17 | // this is important when dealing with a certificate that isnt valid 18 | ServicePointManager.ServerCertificateValidationCallback = IgnoreCertificateValidationFailureForTestingOnly; 19 | 20 | var cancellationTokenSource = new CancellationTokenSource(); 21 | 22 | var options = new SmtpServerOptionsBuilder() 23 | .ServerName("SmtpServer SampleApp") 24 | .Endpoint(builder => 25 | builder 26 | .Port(9025, true) 27 | .AllowUnsecureAuthentication(false) 28 | .Certificate(CreateCertificate())) 29 | .Build(); 30 | 31 | var serviceProvider = new ServiceProvider(); 32 | serviceProvider.Add(new SampleUserAuthenticator()); 33 | 34 | var server = new SmtpServer.SmtpServer(options, serviceProvider); 35 | server.SessionCreated += OnSessionCreated; 36 | 37 | var serverTask = server.StartAsync(cancellationTokenSource.Token); 38 | 39 | SampleMailClient.Send(user: "user", password: "password", useSsl: true); 40 | 41 | cancellationTokenSource.Cancel(); 42 | serverTask.WaitWithoutException(); 43 | } 44 | 45 | static void OnSessionCreated(object sender, SessionEventArgs e) 46 | { 47 | Console.WriteLine("Session Created."); 48 | 49 | e.Context.CommandExecuting += OnCommandExecuting; 50 | } 51 | 52 | static void OnCommandExecuting(object sender, SmtpCommandEventArgs e) 53 | { 54 | Console.WriteLine("Command Executing."); 55 | 56 | new TracingSmtpCommandVisitor(Console.Out).Visit(e.Command); 57 | } 58 | 59 | static bool IgnoreCertificateValidationFailureForTestingOnly(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) 60 | { 61 | return true; 62 | } 63 | 64 | static X509Certificate2 CreateCertificate() 65 | { 66 | // to create an X509Certificate for testing you need to run MAKECERT.EXE and then PVK2PFX.EXE 67 | // http://www.digitallycreated.net/Blog/38/using-makecert-to-create-certificates-for-development 68 | 69 | var certificate = File.ReadAllBytes(@"C:\Users\caino\Dropbox\Documents\Cain\Programming\SmtpServer\SmtpServer.pfx"); 70 | var password = File.ReadAllText(@"C:\Users\caino\Dropbox\Documents\Cain\Programming\SmtpServer\SmtpServerPassword.txt"); 71 | 72 | return new X509Certificate2(certificate, password); 73 | } 74 | } 75 | } -------------------------------------------------------------------------------- /src/SmtpServer/Protocol/DataCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using SmtpServer.ComponentModel; 5 | using SmtpServer.IO; 6 | using SmtpServer.Storage; 7 | 8 | namespace SmtpServer.Protocol 9 | { 10 | /// 11 | /// Data Command 12 | /// 13 | public sealed class DataCommand : SmtpCommand 14 | { 15 | /// 16 | /// Smtp Data Command 17 | /// 18 | public const string Command = "DATA"; 19 | 20 | /// 21 | /// Constructor. 22 | /// 23 | public DataCommand() : base(Command) { } 24 | 25 | /// 26 | /// Execute the command. 27 | /// 28 | /// The execution context to operate on. 29 | /// The cancellation token. 30 | /// Returns true if the command executed successfully such that the transition to the next state should occurr, false 31 | /// if the current state is to be maintained. 32 | internal override async Task ExecuteAsync(SmtpSessionContext context, CancellationToken cancellationToken) 33 | { 34 | if (context.Transaction.To.Count == 0) 35 | { 36 | await context.Pipe.Output.WriteReplyAsync(SmtpResponse.NoValidRecipientsGiven, cancellationToken).ConfigureAwait(false); 37 | return false; 38 | } 39 | 40 | await context.Pipe.Output.WriteReplyAsync(new SmtpResponse(SmtpReplyCode.StartMailInput, "end with ."), cancellationToken).ConfigureAwait(false); 41 | 42 | var messageStore = context.ServiceProvider.GetService(context, MessageStore.Default); 43 | 44 | try 45 | { 46 | using var container = new DisposableContainer(messageStore); 47 | 48 | SmtpResponse response = null; 49 | 50 | await context.Pipe.Input.ReadDotBlockAsync( 51 | async buffer => 52 | { 53 | // ReSharper disable once AccessToDisposedClosure 54 | response = await container.Instance.SaveAsync(context, context.Transaction, buffer, cancellationToken).ConfigureAwait(false); 55 | }, 56 | context.ServerOptions.MaxMessageSizeOptions, 57 | cancellationToken).ConfigureAwait(false); 58 | 59 | await context.Pipe.Output.WriteReplyAsync(response, cancellationToken).ConfigureAwait(false); 60 | } 61 | catch (SmtpResponseException) 62 | { 63 | return false; 64 | } 65 | catch (Exception) 66 | { 67 | await context.Pipe.Output.WriteReplyAsync(new SmtpResponse(SmtpReplyCode.TransactionFailed), cancellationToken).ConfigureAwait(false); 68 | return false; 69 | } 70 | 71 | return true; 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/SmtpServer.Tests/MailClient.cs: -------------------------------------------------------------------------------- 1 | using MailKit.Net.Smtp; 2 | using MailKit.Security; 3 | using MimeKit; 4 | using MimeKit.Text; 5 | using System.Threading; 6 | 7 | namespace SmtpServer.Tests 8 | { 9 | internal static class MailClient 10 | { 11 | public static MimeMessage Message( 12 | string from = null, 13 | string to = null, 14 | string cc = null, 15 | string bcc = null, 16 | string subject = null, 17 | string text = null, 18 | string charset = "utf-8", 19 | MimeEntity body = null) 20 | { 21 | var message = new MimeMessage(); 22 | 23 | message.From.Add(MailboxAddress.Parse(from ?? "from@sample.com")); 24 | message.To.Add(MailboxAddress.Parse(to ?? "to@sample.com")); 25 | 26 | if (cc != null) 27 | { 28 | message.Cc.Add(MailboxAddress.Parse(cc)); 29 | } 30 | 31 | if (bcc != null) 32 | { 33 | message.Bcc.Add(MailboxAddress.Parse(bcc)); 34 | } 35 | 36 | message.Subject = subject ?? "Hello"; 37 | 38 | if (body == null) 39 | { 40 | body = new TextPart(TextFormat.Plain); 41 | ((TextPart)body).SetText(charset, text ?? "Hello World"); 42 | } 43 | 44 | message.Body = body; 45 | 46 | return message; 47 | } 48 | 49 | public static SmtpClientEx Client(string host = "localhost", int port = 9025, SecureSocketOptions options = SecureSocketOptions.Auto) 50 | { 51 | var client = new SmtpClientEx(); 52 | client.ServerCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true; 53 | 54 | client.Connected += (sender, args) => 55 | { 56 | 57 | }; 58 | 59 | client.Connect("localhost", 9025, options); 60 | 61 | return client; 62 | } 63 | 64 | public static void Send( 65 | MimeMessage message = null, 66 | string user = null, 67 | string password = null) 68 | { 69 | message ??= Message(); 70 | 71 | using var client = Client(); 72 | 73 | if (user != null && password != null) 74 | { 75 | client.Authenticate(user, password); 76 | } 77 | 78 | //client.NoOp(); 79 | 80 | client.Send(message); 81 | client.Disconnect(true); 82 | } 83 | 84 | public static void NoOp(SecureSocketOptions options = SecureSocketOptions.Auto) 85 | { 86 | using var client = Client(options: options); 87 | 88 | client.NoOp(); 89 | } 90 | } 91 | 92 | internal class SmtpClientEx : SmtpClient 93 | { 94 | public SmtpResponse SendUnknownCommand(string command, CancellationToken cancellationToken = default) 95 | { 96 | return SendCommand(command, cancellationToken); 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/SmtpServer/Authentication/DelegatingUserAuthenticator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | 5 | namespace SmtpServer.Authentication 6 | { 7 | /// 8 | /// Delegating User Authenticator 9 | /// 10 | public sealed class DelegatingUserAuthenticator : UserAuthenticator 11 | { 12 | readonly Func _delegate; 13 | 14 | /// 15 | /// Constructor. 16 | /// 17 | /// THe delegate to execute for the authentication. 18 | public DelegatingUserAuthenticator(Action @delegate) : this(Wrap(@delegate)) { } 19 | 20 | /// 21 | /// Constructor. 22 | /// 23 | /// THe delegate to execute for the authentication. 24 | public DelegatingUserAuthenticator(Func @delegate) : this(Wrap(@delegate)) { } 25 | 26 | /// 27 | /// Constructor. 28 | /// 29 | /// THe delegate to execute for the authentication. 30 | public DelegatingUserAuthenticator(Func @delegate) 31 | { 32 | _delegate = @delegate; 33 | } 34 | 35 | /// 36 | /// Wrap the delegate into a function that is compatible with the signature. 37 | /// 38 | /// The delegate to wrap. 39 | /// The function that is compatible with the main signature. 40 | static Func Wrap(Func @delegate) 41 | { 42 | return (context, user, password) => @delegate(user, password); 43 | } 44 | 45 | /// 46 | /// Wrap the delegate into a function that is compatible with the signature. 47 | /// 48 | /// The delegate to wrap. 49 | /// The function that is compatible with the main signature. 50 | static Func Wrap(Action @delegate) 51 | { 52 | return (context, user, password) => 53 | { 54 | @delegate(user, password); 55 | 56 | return true; 57 | }; 58 | } 59 | 60 | /// 61 | /// Authenticate a user account. 62 | /// 63 | /// The session context. 64 | /// The user to authenticate. 65 | /// The password of the user. 66 | /// The cancellation token. 67 | /// true if the user is authenticated, false if not. 68 | public override Task AuthenticateAsync( 69 | ISessionContext context, 70 | string user, 71 | string password, 72 | CancellationToken cancellationToken) 73 | { 74 | return Task.FromResult(_delegate(context, user, password)); 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/SmtpServer.Tests/PipeReaderTests.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.IO.Pipelines; 3 | using System.Text; 4 | using System.Threading.Tasks; 5 | using SmtpServer.IO; 6 | using SmtpServer.Text; 7 | using Xunit; 8 | 9 | namespace SmtpServer.Tests 10 | { 11 | public sealed class PipeReaderExtensionTests 12 | { 13 | static PipeReader CreatePipeReader(string text) 14 | { 15 | var stream = new MemoryStream(Encoding.ASCII.GetBytes(text)); 16 | 17 | return PipeReader.Create(stream); 18 | } 19 | 20 | [Fact] 21 | // ReSharper disable once InconsistentNaming 22 | public async Task CanReadLineAndRemoveTrailingCRLF() 23 | { 24 | // arrange 25 | var reader = CreatePipeReader("abcde\r\n"); 26 | 27 | var maxMessageSizeOptions = new MaxMessageSizeOptions(); 28 | 29 | // act 30 | var line = await reader.ReadLineAsync(Encoding.ASCII, maxMessageSizeOptions).ConfigureAwait(false); 31 | 32 | // assert 33 | Assert.Equal(5, line.Length); 34 | Assert.Equal("abcde", line); 35 | } 36 | 37 | [Fact] 38 | // ReSharper disable once InconsistentNaming 39 | public async Task CanReadLinesWithInconsistentCRLF() 40 | { 41 | // arrange 42 | var reader = CreatePipeReader("ab\rcd\ne\r\n"); 43 | 44 | var maxMessageSizeOptions = new MaxMessageSizeOptions(); 45 | 46 | // act 47 | var line = await reader.ReadLineAsync(Encoding.ASCII, maxMessageSizeOptions).ConfigureAwait(false); 48 | 49 | // assert 50 | Assert.Equal(7, line.Length); 51 | Assert.Equal("ab\rcd\ne", line); 52 | } 53 | 54 | [Fact] 55 | // ReSharper disable once InconsistentNaming 56 | public async Task CanReadMultipleLines() 57 | { 58 | // arrange 59 | var reader = CreatePipeReader("abcde\r\nfghij\r\nklmno\r\n"); 60 | 61 | var maxMessageSizeOptions = new MaxMessageSizeOptions(); 62 | 63 | // act 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); 67 | 68 | // assert 69 | Assert.Equal("abcde", line1); 70 | Assert.Equal("fghij", line2); 71 | Assert.Equal("klmno", line3); 72 | } 73 | 74 | [Fact] 75 | public async Task CanReadBlockWithDotStuffingRemoved() 76 | { 77 | // arrange 78 | var reader = CreatePipeReader("abcd\r\n..1234\r\n.\r\n"); 79 | 80 | var maxMessageSizeOptions = new MaxMessageSizeOptions(); 81 | 82 | // act 83 | var text = ""; 84 | await reader.ReadDotBlockAsync( 85 | buffer => 86 | { 87 | text = StringUtil.Create(buffer); 88 | 89 | return Task.CompletedTask; 90 | }, 91 | maxMessageSizeOptions); 92 | 93 | // assert 94 | Assert.Equal("abcd\r\n.1234", text); 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/SmtpServer/Protocol/ISmtpCommandFactory.cs: -------------------------------------------------------------------------------- 1 | using SmtpServer.Mail; 2 | using System.Collections.Generic; 3 | using System.Net; 4 | 5 | namespace SmtpServer.Protocol 6 | { 7 | /// 8 | /// Smtp Command Factory Interface 9 | /// 10 | public interface ISmtpCommandFactory 11 | { 12 | /// 13 | /// Create a HELO command. 14 | /// 15 | /// The domain name or address literal. 16 | /// The HELO command. 17 | SmtpCommand CreateHelo(string domainOrAddress); 18 | 19 | /// 20 | /// Create a EHLO command. 21 | /// 22 | /// The domain name or address literal. 23 | /// The EHLO command. 24 | SmtpCommand CreateEhlo(string domainOrAddress); 25 | 26 | /// 27 | /// Create a MAIL command. 28 | /// 29 | /// The Mailbox address that the message is from. 30 | /// The optional parameters for the message. 31 | /// The MAIL command. 32 | SmtpCommand CreateMail(IMailbox address, IReadOnlyDictionary parameters); 33 | 34 | /// 35 | /// Create a RCPT command. 36 | /// 37 | /// The address that the mail is to. 38 | /// The RCPT command. 39 | SmtpCommand CreateRcpt(IMailbox address); 40 | 41 | /// 42 | /// Create a DATA command. 43 | /// 44 | /// The DATA command. 45 | SmtpCommand CreateData(); 46 | 47 | /// 48 | /// Create a QUIT command. 49 | /// 50 | /// The QUIT command. 51 | SmtpCommand CreateQuit(); 52 | 53 | /// 54 | /// Create a NOOP command. 55 | /// 56 | /// The NOOP command. 57 | SmtpCommand CreateNoop(); 58 | 59 | /// 60 | /// Create a RSET command. 61 | /// 62 | /// The RSET command. 63 | SmtpCommand CreateRset(); 64 | 65 | /// 66 | /// Create a STARTTLS command. 67 | /// 68 | /// The STARTTLS command. 69 | SmtpCommand CreateStartTls(); 70 | 71 | /// 72 | /// Create a AUTH command. 73 | /// 74 | /// The authentication method. 75 | /// The authentication parameter. 76 | /// The AUTH command. 77 | SmtpCommand CreateAuth(AuthenticationMethod method, string parameter); 78 | 79 | /// 80 | /// Create a PROXY command. 81 | /// 82 | /// The source endpoint. 83 | /// The destination endpoint. 84 | /// The PROXY command. 85 | SmtpCommand CreateProxy(IPEndPoint sourceEndpoint = null, IPEndPoint destinationEndpoint = null); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /examples/SampleApp/Examples/SessionTracingExample.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | using System.Threading; 4 | using SmtpServer; 5 | using SmtpServer.ComponentModel; 6 | using SmtpServer.Net; 7 | using SmtpServer.Tracing; 8 | 9 | namespace SampleApp.Examples 10 | { 11 | public static class SessionTracingExample 12 | { 13 | static CancellationTokenSource _cancellationTokenSource; 14 | 15 | public static void Run() 16 | { 17 | _cancellationTokenSource = new CancellationTokenSource(); 18 | 19 | var options = new SmtpServerOptionsBuilder() 20 | .ServerName("SmtpServer SampleApp") 21 | .Port(9025) 22 | .Build(); 23 | 24 | var server = new SmtpServer.SmtpServer(options, ServiceProvider.Default); 25 | 26 | server.SessionCreated += OnSessionCreated; 27 | server.SessionCompleted += OnSessionCompleted; 28 | server.SessionFaulted += OnSessionFaulted; 29 | server.SessionCancelled += OnSessionCancelled; 30 | 31 | var serverTask = server.StartAsync(_cancellationTokenSource.Token); 32 | 33 | SampleMailClient.Send(recipients: 1000); 34 | 35 | serverTask.WaitWithoutException(); 36 | } 37 | 38 | static void OnSessionFaulted(object sender, SessionFaultedEventArgs e) 39 | { 40 | Console.WriteLine("SessionFaulted: {0}", e.Exception); 41 | } 42 | 43 | static void OnSessionCancelled(object sender, SessionEventArgs e) 44 | { 45 | Console.WriteLine("SessionCancelled"); 46 | } 47 | 48 | static void OnSessionCreated(object sender, SessionEventArgs e) 49 | { 50 | e.Context.Properties.Add("SessionID", Guid.NewGuid()); 51 | 52 | e.Context.CommandExecuting += OnCommandExecuting; 53 | e.Context.CommandExecuted += OnCommandExecuted; 54 | e.Context.ResponseException += OnResponseException; 55 | } 56 | 57 | private static void OnResponseException(object sender, SmtpResponseExceptionEventArgs e) 58 | { 59 | Console.WriteLine("Response Exception"); 60 | if (e.Exception.Properties.ContainsKey("SmtpSession:Buffer")) 61 | { 62 | var buffer = e.Exception.Properties["SmtpSession:Buffer"] as byte[]; 63 | Console.WriteLine("Unrecognized Command: {0}", Encoding.UTF8.GetString(buffer)); 64 | } 65 | } 66 | 67 | static void OnCommandExecuting(object sender, SmtpCommandEventArgs e) 68 | { 69 | Console.WriteLine("Command Executing (SessionID={0})", e.Context.Properties["SessionID"]); 70 | new TracingSmtpCommandVisitor(Console.Out).Visit(e.Command); 71 | } 72 | 73 | static void OnCommandExecuted(object sender, SmtpCommandEventArgs e) 74 | { 75 | Console.WriteLine("Command Executed (SessionID={0})", e.Context.Properties["SessionID"]); 76 | new TracingSmtpCommandVisitor(Console.Out).Visit(e.Command); 77 | } 78 | 79 | static void OnSessionCompleted(object sender, SessionEventArgs e) 80 | { 81 | Console.WriteLine("SessionCompleted: {0}", e.Context.Properties[EndpointListener.RemoteEndPointKey]); 82 | 83 | e.Context.CommandExecuting -= OnCommandExecuting; 84 | e.Context.CommandExecuted -= OnCommandExecuted; 85 | e.Context.ResponseException -= OnResponseException; 86 | 87 | _cancellationTokenSource.Cancel(); 88 | } 89 | } 90 | } -------------------------------------------------------------------------------- /src/SmtpServer.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.28803.352 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SmtpServer", "SmtpServer\SmtpServer.csproj", "{0A7CFC3D-305C-4018-9052-3A7A8B5DD104}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SmtpServer.Benchmarks", "SmtpServer.Benchmarks\SmtpServer.Benchmarks.csproj", "{7AE1F3B4-2C00-4BAA-A13C-5EBD43EDE81A}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SmtpServer.Tests", "SmtpServer.Tests\SmtpServer.Tests.csproj", "{4957B054-F07E-402D-A3EC-7EBA0B3018B7}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WorkerService", "..\examples\WorkerService\WorkerService.csproj", "{EE0C474F-8404-4FB6-865F-A034B5DB77FE}" 13 | EndProject 14 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Examples", "Examples", "{6BAD2430-FA6B-4929-8BD7-66663CA02207}" 15 | EndProject 16 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SampleApp", "..\examples\SampleApp\SampleApp.csproj", "{DB671922-7280-4854-9C4F-0BA073B5F1E2}" 17 | EndProject 18 | Global 19 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 20 | Debug|Any CPU = Debug|Any CPU 21 | Release|Any CPU = Release|Any CPU 22 | EndGlobalSection 23 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 24 | {0A7CFC3D-305C-4018-9052-3A7A8B5DD104}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 25 | {0A7CFC3D-305C-4018-9052-3A7A8B5DD104}.Debug|Any CPU.Build.0 = Debug|Any CPU 26 | {0A7CFC3D-305C-4018-9052-3A7A8B5DD104}.Release|Any CPU.ActiveCfg = Release|Any CPU 27 | {0A7CFC3D-305C-4018-9052-3A7A8B5DD104}.Release|Any CPU.Build.0 = Release|Any CPU 28 | {7AE1F3B4-2C00-4BAA-A13C-5EBD43EDE81A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {7AE1F3B4-2C00-4BAA-A13C-5EBD43EDE81A}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {7AE1F3B4-2C00-4BAA-A13C-5EBD43EDE81A}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {7AE1F3B4-2C00-4BAA-A13C-5EBD43EDE81A}.Release|Any CPU.Build.0 = Release|Any CPU 32 | {4957B054-F07E-402D-A3EC-7EBA0B3018B7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {4957B054-F07E-402D-A3EC-7EBA0B3018B7}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {4957B054-F07E-402D-A3EC-7EBA0B3018B7}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {4957B054-F07E-402D-A3EC-7EBA0B3018B7}.Release|Any CPU.Build.0 = Release|Any CPU 36 | {EE0C474F-8404-4FB6-865F-A034B5DB77FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 37 | {EE0C474F-8404-4FB6-865F-A034B5DB77FE}.Debug|Any CPU.Build.0 = Debug|Any CPU 38 | {EE0C474F-8404-4FB6-865F-A034B5DB77FE}.Release|Any CPU.ActiveCfg = Release|Any CPU 39 | {EE0C474F-8404-4FB6-865F-A034B5DB77FE}.Release|Any CPU.Build.0 = Release|Any CPU 40 | {DB671922-7280-4854-9C4F-0BA073B5F1E2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 41 | {DB671922-7280-4854-9C4F-0BA073B5F1E2}.Debug|Any CPU.Build.0 = Debug|Any CPU 42 | {DB671922-7280-4854-9C4F-0BA073B5F1E2}.Release|Any CPU.ActiveCfg = Release|Any CPU 43 | {DB671922-7280-4854-9C4F-0BA073B5F1E2}.Release|Any CPU.Build.0 = Release|Any CPU 44 | EndGlobalSection 45 | GlobalSection(SolutionProperties) = preSolution 46 | HideSolutionNode = FALSE 47 | EndGlobalSection 48 | GlobalSection(NestedProjects) = preSolution 49 | {EE0C474F-8404-4FB6-865F-A034B5DB77FE} = {6BAD2430-FA6B-4929-8BD7-66663CA02207} 50 | {DB671922-7280-4854-9C4F-0BA073B5F1E2} = {6BAD2430-FA6B-4929-8BD7-66663CA02207} 51 | EndGlobalSection 52 | GlobalSection(ExtensibilityGlobals) = postSolution 53 | SolutionGuid = {3050CFB6-1836-4A06-8AA4-C9E75A9B9147} 54 | EndGlobalSection 55 | EndGlobal 56 | -------------------------------------------------------------------------------- /examples/SampleApp/Examples/SessionContextExample.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using SmtpServer; 6 | using SmtpServer.Authentication; 7 | using SmtpServer.ComponentModel; 8 | using SmtpServer.Protocol; 9 | using SmtpServer.Tracing; 10 | 11 | namespace SampleApp.Examples 12 | { 13 | public static class SessionContextExample 14 | { 15 | static CancellationTokenSource _cancellationTokenSource; 16 | 17 | public static void Run() 18 | { 19 | _cancellationTokenSource = new CancellationTokenSource(); 20 | 21 | var options = new SmtpServerOptionsBuilder() 22 | .ServerName("SmtpServer SampleApp") 23 | .Endpoint(builder => 24 | builder 25 | .AllowUnsecureAuthentication() 26 | .AuthenticationRequired() 27 | .Port(9025)) 28 | .Build(); 29 | 30 | var serviceProvider = new ServiceProvider(); 31 | serviceProvider.Add(new AuthenticationHandler()); 32 | 33 | var server = new SmtpServer.SmtpServer(options, serviceProvider); 34 | 35 | server.SessionCreated += OnSessionCreated; 36 | server.SessionCompleted += OnSessionCompleted; 37 | 38 | var serverTask = server.StartAsync(_cancellationTokenSource.Token); 39 | 40 | SampleMailClient.Send(user: "cain", password: "o'sullivan", count: 5); 41 | 42 | serverTask.WaitWithoutException(); 43 | } 44 | 45 | static void OnSessionCreated(object sender, SessionEventArgs e) 46 | { 47 | // the session context contains a Properties dictionary 48 | // which can be used to custom session context 49 | 50 | e.Context.Properties["Start"] = DateTimeOffset.Now; 51 | e.Context.Properties["Commands"] = new List(); 52 | 53 | e.Context.CommandExecuting += OnCommandExecuting; 54 | } 55 | 56 | static void OnCommandExecuting(object sender, SmtpCommandEventArgs e) 57 | { 58 | ((List)e.Context.Properties["Commands"]).Add(e.Command); 59 | } 60 | 61 | static void OnSessionCompleted(object sender, SessionEventArgs e) 62 | { 63 | e.Context.CommandExecuting -= OnCommandExecuting; 64 | 65 | Console.WriteLine("The session started at {0}.", e.Context.Properties["Start"]); 66 | Console.WriteLine(); 67 | 68 | Console.WriteLine("The user that authenticated was {0}", e.Context.Properties["User"]); 69 | Console.WriteLine(); 70 | 71 | Console.WriteLine("The following commands were executed during the session;"); 72 | Console.WriteLine(); 73 | 74 | var writer = new TracingSmtpCommandVisitor(Console.Out); 75 | 76 | foreach (var command in (List)e.Context.Properties["Commands"]) 77 | { 78 | writer.Visit(command); 79 | } 80 | 81 | _cancellationTokenSource.Cancel(); 82 | } 83 | 84 | public class AuthenticationHandler : UserAuthenticator 85 | { 86 | public override Task AuthenticateAsync( 87 | ISessionContext context, 88 | string user, 89 | string password, 90 | CancellationToken cancellationToken) 91 | { 92 | context.Properties["User"] = user; 93 | 94 | return Task.FromResult(true); 95 | } 96 | } 97 | } 98 | } -------------------------------------------------------------------------------- /src/SmtpServer/IO/SecurableDuplexPipe.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.IO.Pipelines; 4 | using System.Net.Security; 5 | using System.Security.Authentication; 6 | using System.Security.Cryptography.X509Certificates; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | 10 | namespace SmtpServer.IO 11 | { 12 | internal sealed class SecurableDuplexPipe : ISecurableDuplexPipe 13 | { 14 | readonly Action _disposeAction; 15 | Stream _stream; 16 | bool _disposed; 17 | 18 | /// 19 | /// Constructor. 20 | /// 21 | /// The stream that the pipe is reading and writing to. 22 | /// The action to execute when the stream has been disposed. 23 | internal SecurableDuplexPipe(Stream stream, Action disposeAction) 24 | { 25 | _stream = stream; 26 | _disposeAction = disposeAction; 27 | 28 | Input = PipeReader.Create(_stream); 29 | Output = PipeWriter.Create(_stream); 30 | } 31 | 32 | /// 33 | public async Task UpgradeAsync(X509Certificate certificate, SslProtocols protocols, CancellationToken cancellationToken = default) 34 | { 35 | var sslStream = new SslStream(_stream, true); 36 | 37 | try 38 | { 39 | await sslStream.AuthenticateAsServerAsync( 40 | new SslServerAuthenticationOptions 41 | { 42 | ServerCertificate = certificate, 43 | ClientCertificateRequired = false, 44 | EnabledSslProtocols = protocols, 45 | CertificateRevocationCheckMode = X509RevocationMode.Online 46 | }, 47 | cancellationToken); 48 | } 49 | catch 50 | { 51 | sslStream.Dispose(); 52 | throw; 53 | } 54 | 55 | _stream = sslStream; 56 | 57 | Input = PipeReader.Create(_stream); 58 | Output = PipeWriter.Create(_stream); 59 | } 60 | 61 | /// 62 | /// Releases the unmanaged resources used by the stream and optionally releases the managed resources. 63 | /// 64 | /// true to release both managed and unmanaged resources; false to release only unmanaged resources. 65 | void Dispose(bool disposing) 66 | { 67 | if (_disposed == false) 68 | { 69 | if (disposing) 70 | { 71 | _disposeAction(); 72 | _stream = null; 73 | } 74 | 75 | _disposed = true; 76 | } 77 | } 78 | 79 | /// 80 | /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. 81 | /// 82 | public void Dispose() 83 | { 84 | Dispose(true); 85 | } 86 | 87 | /// 88 | /// Gets the half of the duplex pipe. 89 | /// 90 | public PipeReader Input { get; private set; } 91 | 92 | /// 93 | /// Gets the half of the duplex pipe. 94 | /// 95 | public PipeWriter Output { get; private set; } 96 | 97 | /// 98 | public bool IsSecure => _stream is SslStream; 99 | 100 | /// 101 | public SslProtocols SslProtocol => (_stream as SslStream)?.SslProtocol ?? SslProtocols.None; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/SmtpServer.Tests/TokenReaderTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | using SmtpServer.IO; 4 | using SmtpServer.Text; 5 | using Xunit; 6 | 7 | namespace SmtpServer.Tests 8 | { 9 | public sealed class TokenReaderTests 10 | { 11 | static TokenReader CreateReader(params string[] values) 12 | { 13 | var segments = new ByteArraySegmentList(); 14 | 15 | foreach (var value in values) 16 | { 17 | segments.Append(Encoding.UTF8.GetBytes(value)); 18 | } 19 | 20 | return new TokenReader(segments.Build()); 21 | } 22 | 23 | [Fact] 24 | public void CanCompareAcrossMultipleSegments() 25 | { 26 | // arrange 27 | var reader = CreateReader("A", "BCD", "EF"); 28 | 29 | Span match = stackalloc char[6]; 30 | match[0] = 'A'; 31 | match[1] = 'B'; 32 | match[2] = 'C'; 33 | match[3] = 'D'; 34 | match[4] = 'E'; 35 | match[5] = 'F'; 36 | 37 | // assert 38 | Assert.True(reader.TryMake(TryMakeText, out var text)); 39 | Assert.False(text.IsSingleSegment); 40 | 41 | // assert 42 | Assert.True(text.CaseInsensitiveStringEquals(ref match)); 43 | 44 | static bool TryMakeText(ref TokenReader reader) 45 | { 46 | if (reader.Peek().Kind == TokenKind.Text) 47 | { 48 | reader.Skip(TokenKind.Text); 49 | return true; 50 | } 51 | 52 | return false; 53 | } 54 | } 55 | 56 | [Fact] 57 | public void CanTokenizeWord() 58 | { 59 | // arrange 60 | var reader = CreateReader("ABC"); 61 | 62 | // assert 63 | Assert.Equal(TokenKind.Text, reader.Peek().Kind); 64 | Assert.Equal("ABC", reader.Take().ToText()); 65 | Assert.Equal(TokenKind.None, reader.Take().Kind); 66 | } 67 | 68 | [Fact] 69 | public void CanTokenizeMultiSegmentWord() 70 | { 71 | // arrange 72 | var reader = CreateReader("ABC", "DEF"); 73 | 74 | // assert 75 | Assert.Equal(TokenKind.Text, reader.Peek().Kind); 76 | Assert.Equal("ABC", reader.Take().ToText()); 77 | Assert.Equal(TokenKind.Text, reader.Peek().Kind); 78 | Assert.Equal("DEF", reader.Take().ToText()); 79 | Assert.Equal(TokenKind.None, reader.Take().Kind); 80 | } 81 | 82 | [Fact] 83 | public void CanMakeSequences() 84 | { 85 | // arrange 86 | var reader = CreateReader("1", "AB", "CDE", "F234", "5678"); 87 | 88 | // act 89 | var made1 = reader.TryMake(TryMakeNumber, out var sequence1); 90 | var made2 = reader.TryMake(TryMakeText, out var sequence2); 91 | var made3 = reader.TryMake(TryMakeNumber, out var sequence3); 92 | 93 | // assert 94 | Assert.True(made1); 95 | Assert.Equal("1", StringUtil.Create(sequence1)); 96 | Assert.True(made2); 97 | Assert.Equal("ABCDEF", StringUtil.Create(sequence2)); 98 | Assert.True(made3); 99 | Assert.Equal("2345678", StringUtil.Create(sequence3)); 100 | 101 | static bool TryMakeText(ref TokenReader r) 102 | { 103 | if (r.Peek().Kind == TokenKind.Text) 104 | { 105 | r.Skip(TokenKind.Text); 106 | return true; 107 | } 108 | 109 | return false; 110 | } 111 | 112 | static bool TryMakeNumber(ref TokenReader r) 113 | { 114 | if (r.Peek().Kind == TokenKind.Number) 115 | { 116 | r.Skip(TokenKind.Number); 117 | return true; 118 | } 119 | 120 | return false; 121 | } 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/SmtpServer/StateMachine/SmtpStateTable.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using SmtpServer.Protocol; 5 | 6 | namespace SmtpServer.StateMachine 7 | { 8 | internal sealed class SmtpStateTable : IEnumerable 9 | { 10 | internal static readonly SmtpStateTable Shared = new SmtpStateTable 11 | { 12 | new SmtpState(SmtpStateId.Initialized) 13 | { 14 | { NoopCommand.Command }, 15 | { RsetCommand.Command }, 16 | { QuitCommand.Command }, 17 | { ProxyCommand.Command }, 18 | { HeloCommand.Command, WaitingForMailSecureWhenSecure }, 19 | { EhloCommand.Command, WaitingForMailSecureWhenSecure } 20 | }, 21 | new SmtpState(SmtpStateId.WaitingForMail) 22 | { 23 | { NoopCommand.Command }, 24 | { RsetCommand.Command }, 25 | { QuitCommand.Command }, 26 | { StartTlsCommand.Command, CanAcceptStartTls, SmtpStateId.WaitingForMailSecure }, 27 | { AuthCommand.Command, context => context.EndpointDefinition.AllowUnsecureAuthentication && context.Authentication.IsAuthenticated == false }, 28 | { HeloCommand.Command, SmtpStateId.WaitingForMail }, 29 | { EhloCommand.Command, SmtpStateId.WaitingForMail }, 30 | { MailCommand.Command, SmtpStateId.WithinTransaction } 31 | }, 32 | new SmtpState(SmtpStateId.WaitingForMailSecure) 33 | { 34 | { NoopCommand.Command }, 35 | { RsetCommand.Command }, 36 | { QuitCommand.Command }, 37 | { AuthCommand.Command, context => context.Authentication.IsAuthenticated == false }, 38 | { HeloCommand.Command, SmtpStateId.WaitingForMailSecure }, 39 | { EhloCommand.Command, SmtpStateId.WaitingForMailSecure }, 40 | { MailCommand.Command, SmtpStateId.WithinTransaction } 41 | }, 42 | new SmtpState(SmtpStateId.WithinTransaction) 43 | { 44 | { NoopCommand.Command }, 45 | { RsetCommand.Command, WaitingForMailSecureWhenSecure }, 46 | { QuitCommand.Command }, 47 | { RcptCommand.Command, SmtpStateId.CanAcceptData }, 48 | }, 49 | new SmtpState(SmtpStateId.CanAcceptData) 50 | { 51 | { NoopCommand.Command }, 52 | { RsetCommand.Command, WaitingForMailSecureWhenSecure }, 53 | { QuitCommand.Command }, 54 | { RcptCommand.Command }, 55 | { DataCommand.Command, SmtpStateId.WaitingForMail }, 56 | } 57 | }; 58 | 59 | static SmtpStateId WaitingForMailSecureWhenSecure(SmtpSessionContext context) 60 | { 61 | return context.Pipe.IsSecure ? SmtpStateId.WaitingForMailSecure : SmtpStateId.WaitingForMail; 62 | } 63 | 64 | static bool CanAcceptStartTls(SmtpSessionContext context) 65 | { 66 | return context.EndpointDefinition.CertificateFactory != null && context.Pipe.IsSecure == false; 67 | } 68 | 69 | readonly IDictionary _states = new Dictionary(); 70 | 71 | internal SmtpState this[SmtpStateId stateId] => _states[stateId]; 72 | 73 | /// 74 | /// Add the state to the table. 75 | /// 76 | /// 77 | void Add(SmtpState state) 78 | { 79 | _states.Add(state.StateId, state); 80 | } 81 | 82 | /// 83 | /// Returns an enumerator that iterates through a collection. 84 | /// 85 | /// An object that can be used to iterate through the collection. 86 | IEnumerator IEnumerable.GetEnumerator() 87 | { 88 | // this is just here for the collection initializer syntax to work 89 | throw new NotImplementedException(); 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/SmtpServer/Protocol/SmtpResponse.cs: -------------------------------------------------------------------------------- 1 | namespace SmtpServer.Protocol 2 | { 3 | /// 4 | /// Smtp Response 5 | /// 6 | public class SmtpResponse 7 | { 8 | /// 9 | /// 250 Ok 10 | /// 11 | public static readonly SmtpResponse Ok = new SmtpResponse(SmtpReplyCode.Ok, "Ok"); 12 | 13 | /// 14 | /// 220 ServiceReady 15 | /// 16 | public static readonly SmtpResponse ServiceReady = new SmtpResponse(SmtpReplyCode.ServiceReady, "ready when you are"); 17 | 18 | /// 19 | /// 550 MailboxUnavailable 20 | /// 21 | public static readonly SmtpResponse MailboxUnavailable = new SmtpResponse(SmtpReplyCode.MailboxUnavailable, "mailbox unavailable"); 22 | 23 | /// 24 | /// 553 MailboxNameNotAllowed 25 | /// 26 | public static readonly SmtpResponse MailboxNameNotAllowed = new SmtpResponse(SmtpReplyCode.MailboxNameNotAllowed, "mailbox name not allowed"); 27 | 28 | /// 29 | /// 221 ServiceClosingTransmissionChannel 30 | /// 31 | public static readonly SmtpResponse ServiceClosingTransmissionChannel = new SmtpResponse(SmtpReplyCode.ServiceClosingTransmissionChannel, "bye"); 32 | 33 | /// 34 | /// 501 SyntaxError 35 | /// 36 | public static readonly SmtpResponse SyntaxError = new SmtpResponse(SmtpReplyCode.SyntaxError, "syntax error"); 37 | 38 | /// 39 | /// 552 SizeLimitExceeded 40 | /// 41 | public static readonly SmtpResponse SizeLimitExceeded = new SmtpResponse(SmtpReplyCode.SizeLimitExceeded, "size limit exceeded"); 42 | 43 | /// 44 | /// 554 TransactionFailed 45 | /// 46 | public static readonly SmtpResponse NoValidRecipientsGiven = new SmtpResponse(SmtpReplyCode.TransactionFailed, "no valid recipients given"); 47 | 48 | /// 49 | /// 535 AuthenticationFailed 50 | /// 51 | public static readonly SmtpResponse AuthenticationFailed = new SmtpResponse(SmtpReplyCode.AuthenticationFailed, "authentication failed"); 52 | 53 | /// 54 | /// 235 AuthenticationSuccessful 55 | /// 56 | public static readonly SmtpResponse AuthenticationSuccessful = new SmtpResponse(SmtpReplyCode.AuthenticationSuccessful, "go ahead"); 57 | 58 | /// 59 | /// 554 TransactionFailed 60 | /// 61 | public static readonly SmtpResponse TransactionFailed = new SmtpResponse(SmtpReplyCode.TransactionFailed); 62 | 63 | /// 64 | /// 503 BadSequence 65 | /// 66 | public static readonly SmtpResponse BadSequence = new SmtpResponse(SmtpReplyCode.BadSequence, "bad sequence of commands"); 67 | 68 | /// 69 | /// 530 AuthenticationRequired 70 | /// 71 | public static readonly SmtpResponse AuthenticationRequired = new SmtpResponse(SmtpReplyCode.AuthenticationRequired, "authentication required"); 72 | 73 | /// 74 | /// 552 MaxMessageSizeExceeded 75 | /// 76 | public static readonly SmtpResponse MaxMessageSizeExceeded = new SmtpResponse(SmtpReplyCode.SizeLimitExceeded, "message size exceeds fixed maximium message size"); 77 | 78 | /// 79 | /// Constructor. 80 | /// 81 | /// The reply code. 82 | /// The reply message. 83 | public SmtpResponse(SmtpReplyCode replyCode, string message = null) 84 | { 85 | ReplyCode = replyCode; 86 | Message = message; 87 | } 88 | 89 | /// 90 | /// Gets the Reply Code. 91 | /// 92 | public SmtpReplyCode ReplyCode { get; } 93 | 94 | /// 95 | /// Gets the response message. 96 | /// 97 | public string Message { get; } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/SmtpServer/SmtpSessionManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Linq; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | namespace SmtpServer 8 | { 9 | internal sealed class SmtpSessionManager 10 | { 11 | readonly SmtpServer _smtpServer; 12 | readonly ConcurrentDictionary _sessions = new ConcurrentDictionary(); 13 | 14 | internal SmtpSessionManager(SmtpServer smtpServer) 15 | { 16 | _smtpServer = smtpServer; 17 | } 18 | 19 | internal void Run(SmtpSessionContext sessionContext, CancellationToken cancellationToken) 20 | { 21 | var handle = new SmtpSessionHandle(new SmtpSession(sessionContext), sessionContext); 22 | Add(handle); 23 | 24 | handle.CompletionTask = RunAsync(handle, cancellationToken).ContinueWith(task => 25 | { 26 | Remove(handle); 27 | }); 28 | } 29 | 30 | async Task RunAsync(SmtpSessionHandle handle, CancellationToken cancellationToken) 31 | { 32 | using var sessionTimeoutCancellationTokenSource = new CancellationTokenSource(handle.SessionContext.EndpointDefinition.SessionTimeout); 33 | 34 | using var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, sessionTimeoutCancellationTokenSource.Token); 35 | 36 | try 37 | { 38 | _smtpServer.OnSessionCreated(new SessionEventArgs(handle.SessionContext)); 39 | 40 | await UpgradeAsync(handle, linkedTokenSource.Token); 41 | 42 | linkedTokenSource.Token.ThrowIfCancellationRequested(); 43 | 44 | await handle.Session.RunAsync(linkedTokenSource.Token); 45 | 46 | _smtpServer.OnSessionCompleted(new SessionEventArgs(handle.SessionContext)); 47 | } 48 | catch (OperationCanceledException) 49 | { 50 | _smtpServer.OnSessionCancelled(new SessionEventArgs(handle.SessionContext)); 51 | } 52 | catch (Exception ex) 53 | { 54 | _smtpServer.OnSessionFaulted(new SessionFaultedEventArgs(handle.SessionContext, ex)); 55 | } 56 | finally 57 | { 58 | await handle.SessionContext.Pipe.Input.CompleteAsync(); 59 | 60 | handle.SessionContext.Pipe.Dispose(); 61 | } 62 | } 63 | 64 | async Task UpgradeAsync(SmtpSessionHandle handle, CancellationToken cancellationToken) 65 | { 66 | var endpoint = handle.SessionContext.EndpointDefinition; 67 | 68 | if (endpoint.IsSecure && endpoint.CertificateFactory != null) 69 | { 70 | var serverCertificate = endpoint.CertificateFactory.GetServerCertificate(handle.SessionContext); 71 | 72 | await handle.SessionContext.Pipe.UpgradeAsync(serverCertificate, endpoint.SupportedSslProtocols, cancellationToken).ConfigureAwait(false); 73 | } 74 | } 75 | 76 | internal Task WaitAsync() 77 | { 78 | var tasks = _sessions.Values.Select(session => session.CompletionTask).ToList().AsReadOnly(); 79 | return Task.WhenAll(tasks); 80 | } 81 | 82 | void Add(SmtpSessionHandle handle) 83 | { 84 | _sessions.TryAdd(handle.SessionContext.SessionId, handle); 85 | } 86 | 87 | void Remove(SmtpSessionHandle handle) 88 | { 89 | _sessions.TryRemove(handle.SessionContext.SessionId, out _); 90 | } 91 | 92 | class SmtpSessionHandle 93 | { 94 | public SmtpSessionHandle(SmtpSession session, SmtpSessionContext sessionContext) 95 | { 96 | Session = session; 97 | SessionContext = sessionContext; 98 | } 99 | 100 | public SmtpSession Session { get; } 101 | 102 | public SmtpSessionContext SessionContext { get; } 103 | 104 | public Task CompletionTask { get; set; } 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/SmtpServer/Protocol/EhloCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using SmtpServer.Authentication; 6 | using SmtpServer.IO; 7 | 8 | namespace SmtpServer.Protocol 9 | { 10 | /// 11 | /// Ehlo Command 12 | /// 13 | public class EhloCommand : SmtpCommand 14 | { 15 | /// 16 | /// Smtp Ehlo Command 17 | /// 18 | public const string Command = "EHLO"; 19 | 20 | /// 21 | /// Constructor. 22 | /// 23 | /// The domain name or address literal. 24 | public EhloCommand(string domainOrAddress) : base(Command) 25 | { 26 | DomainOrAddress = domainOrAddress; 27 | } 28 | 29 | /// 30 | /// Execute the command. 31 | /// 32 | /// The execution context to operate on. 33 | /// The cancellation token. 34 | /// Returns true if the command executed successfully such that the transition to the next state should occurr, false 35 | /// if the current state is to be maintained. 36 | internal override async Task ExecuteAsync(SmtpSessionContext context, CancellationToken cancellationToken) 37 | { 38 | var output = new[] { GetGreeting(context) }.Union(GetExtensions(context)).ToArray(); 39 | 40 | for (var i = 0; i < output.Length - 1; i++) 41 | { 42 | context.Pipe.Output.WriteLine($"250-{output[i]}"); 43 | } 44 | 45 | context.Pipe.Output.WriteLine($"250 {output[output.Length - 1]}"); 46 | 47 | await context.Pipe.Output.FlushAsync(cancellationToken).ConfigureAwait(false); 48 | 49 | return true; 50 | } 51 | 52 | /// 53 | /// Returns the greeting to display to the remote host. 54 | /// 55 | /// The session context. 56 | /// The greeting text to display to the remote host. 57 | protected virtual string GetGreeting(ISessionContext context) 58 | { 59 | return $"{context.ServerOptions.ServerName} Hello {DomainOrAddress}, haven't we met before?"; 60 | } 61 | 62 | /// 63 | /// Returns the list of extensions that are current for the context. 64 | /// 65 | /// The session context. 66 | /// The list of extensions that are current for the context. 67 | protected virtual IEnumerable GetExtensions(ISessionContext context) 68 | { 69 | yield return "PIPELINING"; 70 | yield return "8BITMIME"; 71 | yield return "SMTPUTF8"; 72 | 73 | if (context.Pipe.IsSecure == false && context.EndpointDefinition.CertificateFactory != null) 74 | { 75 | yield return "STARTTLS"; 76 | } 77 | 78 | if (context.ServerOptions.MaxMessageSizeOptions.Length > 0) 79 | { 80 | yield return $"SIZE {context.ServerOptions.MaxMessageSizeOptions.Length}"; 81 | } 82 | 83 | if (IsPlainLoginAllowed(context)) 84 | { 85 | yield return "AUTH PLAIN LOGIN"; 86 | } 87 | 88 | static bool IsPlainLoginAllowed(ISessionContext context) 89 | { 90 | if (context.ServiceProvider.GetService(typeof(IUserAuthenticatorFactory)) == null && context.ServiceProvider.GetService(typeof(IUserAuthenticator)) == null) 91 | { 92 | return false; 93 | } 94 | 95 | return context.Pipe.IsSecure || context.EndpointDefinition.AllowUnsecureAuthentication; 96 | } 97 | } 98 | 99 | /// 100 | /// Gets the domain name or address literal. 101 | /// 102 | public string DomainOrAddress { get; } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/SmtpServer/SmtpSessionContext.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using SmtpServer.IO; 4 | using SmtpServer.Protocol; 5 | 6 | namespace SmtpServer 7 | { 8 | internal sealed class SmtpSessionContext : ISessionContext 9 | { 10 | /// 11 | public Guid SessionId { get; private set; } 12 | 13 | /// 14 | public event EventHandler CommandExecuting; 15 | 16 | /// 17 | public event EventHandler CommandExecuted; 18 | 19 | /// 20 | public event EventHandler ResponseException; 21 | 22 | /// 23 | public event EventHandler SessionAuthenticated; 24 | 25 | /// 26 | /// Constructor. 27 | /// 28 | /// The service provider instance. 29 | /// The server options. 30 | /// The endpoint definition. 31 | internal SmtpSessionContext(IServiceProvider serviceProvider, ISmtpServerOptions options, IEndpointDefinition endpointDefinition) 32 | { 33 | SessionId = Guid.NewGuid(); 34 | ServiceProvider = serviceProvider; 35 | ServerOptions = options; 36 | EndpointDefinition = endpointDefinition; 37 | Transaction = new SmtpMessageTransaction(); 38 | Properties = new Dictionary(); 39 | } 40 | 41 | /// 42 | /// Raise the command executing event. 43 | /// 44 | /// The command that is executing. 45 | internal void RaiseCommandExecuting(SmtpCommand command) 46 | { 47 | CommandExecuting?.Invoke(this, new SmtpCommandEventArgs(this, command)); 48 | } 49 | 50 | /// 51 | /// Raise the command executed event. 52 | /// 53 | /// The command that was executed. 54 | internal void RaiseCommandExecuted(SmtpCommand command) 55 | { 56 | CommandExecuted?.Invoke(this, new SmtpCommandEventArgs(this, command)); 57 | } 58 | 59 | /// 60 | /// Raise the response exception event. 61 | /// 62 | /// The response exception that was raised. 63 | internal void RaiseResponseException(SmtpResponseException responseException) 64 | { 65 | ResponseException?.Invoke(this, new SmtpResponseExceptionEventArgs(this, responseException)); 66 | } 67 | 68 | /// 69 | /// Raise the session authenticated event. 70 | /// 71 | internal void RaiseSessionAuthenticated() 72 | { 73 | SessionAuthenticated?.Invoke(this, EventArgs.Empty); 74 | } 75 | 76 | /// 77 | public IServiceProvider ServiceProvider { get; } 78 | 79 | /// 80 | public ISmtpServerOptions ServerOptions { get; } 81 | 82 | /// 83 | public IEndpointDefinition EndpointDefinition { get; } 84 | 85 | /// 86 | public ISecurableDuplexPipe Pipe { get; internal set; } 87 | 88 | /// 89 | /// Gets the current transaction. 90 | /// 91 | public SmtpMessageTransaction Transaction { get; } 92 | 93 | /// 94 | public AuthenticationContext Authentication { get; internal set; } = AuthenticationContext.Unauthenticated; 95 | 96 | /// 97 | /// Returns the number of athentication attempts. 98 | /// 99 | public int AuthenticationAttempts { get; internal set; } 100 | 101 | /// 102 | /// Gets a value indicating whether a quit has been requested. 103 | /// 104 | public bool IsQuitRequested { get; internal set; } 105 | 106 | /// 107 | public IDictionary Properties { get; } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/SmtpServer/Protocol/MailCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using SmtpServer.ComponentModel; 5 | using SmtpServer.IO; 6 | using SmtpServer.Mail; 7 | using SmtpServer.Storage; 8 | 9 | namespace SmtpServer.Protocol 10 | { 11 | /// 12 | /// Mail Command 13 | /// 14 | public sealed class MailCommand : SmtpCommand 15 | { 16 | /// 17 | /// Smtp Mail Command 18 | /// 19 | public const string Command = "MAIL"; 20 | 21 | /// 22 | /// Constructor. 23 | /// 24 | /// The address. 25 | /// The list of extended (ESMTP) parameters. 26 | public MailCommand(IMailbox address, IReadOnlyDictionary parameters) : base(Command) 27 | { 28 | Address = address; 29 | Parameters = parameters; 30 | } 31 | 32 | /// 33 | /// Execute the command. 34 | /// 35 | /// The execution context to operate on. 36 | /// The cancellation token. 37 | /// Returns true if the command executed successfully such that the transition to the next state should occurr, false 38 | /// if the current state is to be maintained. 39 | internal override async Task ExecuteAsync(SmtpSessionContext context, CancellationToken cancellationToken) 40 | { 41 | if (context.EndpointDefinition.AuthenticationRequired && context.Authentication.IsAuthenticated == false) 42 | { 43 | await context.Pipe.Output.WriteReplyAsync(SmtpResponse.AuthenticationRequired, cancellationToken).ConfigureAwait(false); 44 | return false; 45 | } 46 | 47 | context.Transaction.Reset(); 48 | context.Transaction.Parameters = Parameters; 49 | 50 | // check if a size has been defined 51 | var size = GetMessageSize(); 52 | 53 | // check against the server supplied maximum 54 | if (context.ServerOptions.MaxMessageSizeOptions.Length > 0 && size > context.ServerOptions.MaxMessageSizeOptions.Length) 55 | { 56 | await context.Pipe.Output.WriteReplyAsync(SmtpResponse.SizeLimitExceeded, cancellationToken).ConfigureAwait(false); 57 | return false; 58 | } 59 | 60 | var mailboxFilter = context.ServiceProvider.GetService(context, MailboxFilter.Default); 61 | 62 | using var container = new DisposableContainer(mailboxFilter); 63 | 64 | switch (await container.Instance.CanAcceptFromAsync(context, Address, size, cancellationToken).ConfigureAwait(false)) 65 | { 66 | case true: 67 | context.Transaction.From = Address; 68 | await context.Pipe.Output.WriteReplyAsync(SmtpResponse.Ok, cancellationToken).ConfigureAwait(false); 69 | return true; 70 | 71 | case false: 72 | await context.Pipe.Output.WriteReplyAsync(SmtpResponse.MailboxUnavailable, cancellationToken).ConfigureAwait(false); 73 | return false; 74 | } 75 | 76 | throw new SmtpResponseException(SmtpResponse.TransactionFailed); 77 | } 78 | 79 | /// 80 | /// Gets the estimated message size supplied from the ESMTP command extension. 81 | /// 82 | /// The estimated message size that was supplied by the client. 83 | int GetMessageSize() 84 | { 85 | if (Parameters.TryGetValue("SIZE", out var value) == false) 86 | { 87 | return 0; 88 | } 89 | 90 | return int.TryParse(value, out var size) == false ? 0 : size; 91 | } 92 | 93 | /// 94 | /// Gets the address that the mail is from. 95 | /// 96 | public IMailbox Address { get; } 97 | 98 | /// 99 | /// The list of extended mail parameters. 100 | /// 101 | public IReadOnlyDictionary Parameters { get; } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # What is SmtpServer? 2 | 3 | [![NuGet](https://img.shields.io/nuget/v/SmtpServer.svg)](https://www.nuget.org/packages/SmtpServer/) 4 | 5 | **SmtpServer** is a lightweight yet powerful SMTP server implementation in C#. 6 | Built entirely in .NET, it leverages the Task Parallel Library (TPL) for maximum performance. 7 | 8 | ## 🆕 What's New? 9 | 10 | Check the [Changelog](https://github.com/cosullivan/SmtpServer/blob/master/CHANGELOG.md) 11 | 12 | ## ⚡ Supported ESMTP Extensions 13 | 14 | SmtpServer currently supports the following extensions: 15 | 16 | - STARTTLS 17 | - SIZE 18 | - PIPELINING 19 | - 8BITMIME 20 | - SMTPUTF8 21 | - AUTH PLAIN LOGIN 22 | 23 | ## Installation 24 | 25 | The package is available on [NuGet](https://www.nuget.org/packages/SmtpServer) 26 | ```powershell 27 | PM> install-package SmtpServer 28 | ``` 29 | 30 | ## 🚀 Getting Started 31 | 32 | Starting a basic SMTP server requires only a few lines of code: 33 | 34 | ```cs 35 | var options = new SmtpServerOptionsBuilder() 36 | .ServerName("localhost") 37 | .Port(25, 587) 38 | .Build(); 39 | 40 | var smtpServer = new SmtpServer.SmtpServer(options, ServiceProvider.Default); 41 | await smtpServer.StartAsync(CancellationToken.None); 42 | ``` 43 | 44 | ### What hooks are provided? 45 | 46 | There are three hooks that can be implemented; IMessageStore, IMailboxFilter, and IUserAuthenticator. 47 | 48 | ```cs 49 | var options = new SmtpServerOptionsBuilder() 50 | .ServerName("localhost") 51 | .Endpoint(builder => 52 | builder 53 | .Port(9025, true) 54 | .AllowUnsecureAuthentication(false) 55 | .Certificate(CreateCertificate())) 56 | .Build(); 57 | 58 | var serviceProvider = new ServiceProvider(); 59 | serviceProvider.Add(new SampleMessageStore()); 60 | serviceProvider.Add(new SampleMailboxFilter()); 61 | serviceProvider.Add(new SampleUserAuthenticator()); 62 | 63 | var smtpServer = new SmtpServer.SmtpServer(options, serviceProvider); 64 | await smtpServer.StartAsync(CancellationToken.None); 65 | 66 | // to create an X509Certificate for testing you need to run MAKECERT.EXE and then PVK2PFX.EXE 67 | // http://www.digitallycreated.net/Blog/38/using-makecert-to-create-certificates-for-development 68 | static X509Certificate2 CreateCertificate() 69 | { 70 | var certificate = File.ReadAllBytes(@"Certificate.pfx"); 71 | 72 | return new X509Certificate2(certificate, "P@ssw0rd"); 73 | } 74 | ``` 75 | 76 | ```cs 77 | public class SampleMessageStore : MessageStore 78 | { 79 | public override async Task SaveAsync(ISessionContext context, IMessageTransaction transaction, ReadOnlySequence buffer, CancellationToken cancellationToken) 80 | { 81 | await using var stream = new MemoryStream(); 82 | 83 | var position = buffer.GetPosition(0); 84 | while (buffer.TryGet(ref position, out var memory)) 85 | { 86 | await stream.WriteAsync(memory, cancellationToken); 87 | } 88 | 89 | stream.Position = 0; 90 | 91 | var message = await MimeKit.MimeMessage.LoadAsync(stream, cancellationToken); 92 | Console.WriteLine(message.TextBody); 93 | 94 | return SmtpResponse.Ok; 95 | } 96 | } 97 | ``` 98 | 99 | ```cs 100 | public class SampleMailboxFilter : IMailboxFilter, IMailboxFilterFactory 101 | { 102 | public Task CanAcceptFromAsync(ISessionContext context, IMailbox @from, int size, CancellationToken cancellationToken) 103 | { 104 | if (String.Equals(@from.Host, "test.com")) 105 | { 106 | return Task.FromResult(MailboxFilterResult.Yes); 107 | } 108 | 109 | return Task.FromResult(MailboxFilterResult.NoPermanently); 110 | } 111 | 112 | public Task CanDeliverToAsync(ISessionContext context, IMailbox to, IMailbox @from, CancellationToken token) 113 | { 114 | return Task.FromResult(MailboxFilterResult.Yes); 115 | } 116 | 117 | public IMailboxFilter CreateInstance(ISessionContext context) 118 | { 119 | return new SampleMailboxFilter(); 120 | } 121 | } 122 | ``` 123 | 124 | ```cs 125 | public class SampleUserAuthenticator : IUserAuthenticator, IUserAuthenticatorFactory 126 | { 127 | public Task AuthenticateAsync(ISessionContext context, string user, string password, CancellationToken token) 128 | { 129 | Console.WriteLine("User={0} Password={1}", user, password); 130 | 131 | return Task.FromResult(user.Length > 4); 132 | } 133 | 134 | public IUserAuthenticator CreateInstance(ISessionContext context) 135 | { 136 | return new SampleUserAuthenticator(); 137 | } 138 | } 139 | ``` 140 | -------------------------------------------------------------------------------- /src/SmtpServer/Tracing/TracingSmtpCommandVisitor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Linq; 4 | using SmtpServer.Mail; 5 | using SmtpServer.Protocol; 6 | 7 | namespace SmtpServer.Tracing 8 | { 9 | /// 10 | /// Tracing Smtp Command Visitor 11 | /// 12 | public sealed class TracingSmtpCommandVisitor : SmtpCommandVisitor 13 | { 14 | readonly TextWriter _output; 15 | 16 | /// 17 | /// Constructor. 18 | /// 19 | /// The output stream to write the command execution to. 20 | public TracingSmtpCommandVisitor(TextWriter output) 21 | { 22 | if (output == null) 23 | { 24 | throw new ArgumentException(nameof(output)); 25 | } 26 | 27 | _output = output; 28 | } 29 | 30 | /// 31 | /// Visit an AUTH command. 32 | /// 33 | /// The command that is being visited. 34 | protected override void Visit(AuthCommand command) 35 | { 36 | _output.WriteLine("AUTH: Method={0}, Parameter={1}", command.Method, command.Parameter); 37 | } 38 | 39 | /// 40 | /// Visit an DATA command. 41 | /// 42 | /// The command that is being visited. 43 | protected override void Visit(DataCommand command) 44 | { 45 | _output.WriteLine("DATA"); 46 | } 47 | 48 | /// 49 | /// Visit a HELO command. 50 | /// 51 | /// The command that is being visited. 52 | protected override void Visit(HeloCommand command) 53 | { 54 | _output.WriteLine("HELO: DomainOrAddress={0}", command.DomainOrAddress); 55 | } 56 | 57 | /// 58 | /// Visit an EHLO command. 59 | /// 60 | /// The command that is being visited. 61 | protected override void Visit(EhloCommand command) 62 | { 63 | _output.WriteLine("EHLO: DomainOrAddress={0}", command.DomainOrAddress); 64 | } 65 | 66 | /// 67 | /// Visit an MAIL command. 68 | /// 69 | /// The command that is being visited. 70 | protected override void Visit(MailCommand command) 71 | { 72 | _output.WriteLine("MAIL: Address={0} Parameters={1}", 73 | command.Address.AsAddress(), 74 | string.Join(",", command.Parameters.Select(kvp => $"{kvp.Key}={kvp.Value}"))); 75 | } 76 | 77 | /// 78 | /// Visit an NOOP command. 79 | /// 80 | /// The command that is being visited. 81 | protected override void Visit(NoopCommand command) 82 | { 83 | _output.WriteLine("NOOP"); 84 | } 85 | 86 | /// 87 | /// Visit an PROXY command. 88 | /// 89 | /// The command that is being visited. 90 | protected override void Visit(ProxyCommand command) 91 | { 92 | _output.WriteLine($"PROXY {command.SourceEndpoint} --> {command.DestinationEndpoint}"); 93 | } 94 | 95 | /// 96 | /// Visit an QUIT command. 97 | /// 98 | /// The command that is being visited. 99 | protected override void Visit(QuitCommand command) 100 | { 101 | _output.WriteLine("QUIT"); 102 | } 103 | 104 | /// 105 | /// Visit an RCPT command. 106 | /// 107 | /// The command that is being visited. 108 | protected override void Visit(RcptCommand command) 109 | { 110 | _output.WriteLine("RCPT: Address={0}", command.Address.AsAddress()); 111 | } 112 | 113 | /// 114 | /// Visit an RSET command. 115 | /// 116 | /// The command that is being visited. 117 | protected override void Visit(RsetCommand command) 118 | { 119 | _output.WriteLine("RSET"); 120 | } 121 | 122 | /// 123 | /// Visit an STARTTLS command. 124 | /// 125 | /// The command that is being visited. 126 | protected override void Visit(StartTlsCommand command) 127 | { 128 | _output.WriteLine("STARTTLS"); 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/SmtpServer/Protocol/SmtpCommandVisitor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SmtpServer.Protocol 4 | { 5 | /// 6 | /// Smtp Command Visitor 7 | /// 8 | public abstract class SmtpCommandVisitor 9 | { 10 | /// 11 | /// Visit the command. 12 | /// 13 | /// 14 | public void Visit(SmtpCommand command) 15 | { 16 | if (command is AuthCommand authCommand) 17 | { 18 | Visit(authCommand); 19 | return; 20 | } 21 | 22 | if (command is DataCommand dataCommand) 23 | { 24 | Visit(dataCommand); 25 | return; 26 | } 27 | 28 | if (command is HeloCommand heloCommand) 29 | { 30 | Visit(heloCommand); 31 | return; 32 | } 33 | 34 | if (command is EhloCommand ehloCommand) 35 | { 36 | Visit(ehloCommand); 37 | return; 38 | } 39 | 40 | if (command is MailCommand mailCommand) 41 | { 42 | Visit(mailCommand); 43 | return; 44 | } 45 | 46 | if (command is NoopCommand noopCommand) 47 | { 48 | Visit(noopCommand); 49 | return; 50 | } 51 | 52 | if (command is ProxyCommand proxyCommand) 53 | { 54 | Visit(proxyCommand); 55 | return; 56 | } 57 | 58 | if (command is QuitCommand quitCommand) 59 | { 60 | Visit(quitCommand); 61 | return; 62 | } 63 | 64 | if (command is RcptCommand rcptCommand) 65 | { 66 | Visit(rcptCommand); 67 | return; 68 | } 69 | 70 | if (command is RsetCommand rsetCommand) 71 | { 72 | Visit(rsetCommand); 73 | return; 74 | } 75 | 76 | if (command is StartTlsCommand tlsCommand) 77 | { 78 | Visit(tlsCommand); 79 | return; 80 | } 81 | 82 | throw new NotSupportedException(command.ToString()); 83 | } 84 | 85 | /// 86 | /// Visit an AUTH command. 87 | /// 88 | /// The command that is being visited. 89 | protected virtual void Visit(AuthCommand command) { } 90 | 91 | /// 92 | /// Visit an DATA command. 93 | /// 94 | /// The command that is being visited. 95 | protected virtual void Visit(DataCommand command) { } 96 | 97 | /// 98 | /// Visit a HELO command. 99 | /// 100 | /// The command that is being visited. 101 | protected virtual void Visit(HeloCommand command) { } 102 | 103 | /// 104 | /// Visit an EHLO command. 105 | /// 106 | /// The command that is being visited. 107 | protected virtual void Visit(EhloCommand command) { } 108 | 109 | /// 110 | /// Visit an MAIL command. 111 | /// 112 | /// The command that is being visited. 113 | protected virtual void Visit(MailCommand command) { } 114 | 115 | /// 116 | /// Visit an NOOP command. 117 | /// 118 | /// The command that is being visited. 119 | protected virtual void Visit(NoopCommand command) { } 120 | 121 | /// 122 | /// Visit an PROXY command. 123 | /// 124 | /// The command that is being visited. 125 | protected virtual void Visit(ProxyCommand command) { } 126 | 127 | /// 128 | /// Visit an QUIT command. 129 | /// 130 | /// The command that is being visited. 131 | protected virtual void Visit(QuitCommand command) { } 132 | 133 | /// 134 | /// Visit an RCPT command. 135 | /// 136 | /// The command that is being visited. 137 | protected virtual void Visit(RcptCommand command) { } 138 | 139 | /// 140 | /// Visit an RSET command. 141 | /// 142 | /// The command that is being visited. 143 | protected virtual void Visit(RsetCommand command) { } 144 | 145 | /// 146 | /// Visit an STARTTLS command. 147 | /// 148 | /// The command that is being visited. 149 | protected virtual void Visit(StartTlsCommand command) { } 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/SmtpServer/Text/Token.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Runtime.CompilerServices; 4 | 5 | namespace SmtpServer.Text 6 | { 7 | /// 8 | /// Token 9 | /// 10 | [DebuggerDisplay("[{Kind}] {Text}")] 11 | public readonly ref struct Token 12 | { 13 | /// 14 | /// Constructor. 15 | /// 16 | /// The token kind. 17 | /// The text that the token represents. 18 | public Token(TokenKind kind, ReadOnlySpan text = default) 19 | { 20 | Kind = kind; 21 | Text = text; 22 | } 23 | 24 | /// 25 | /// Returns a value indicating whether or not the given byte is considered a text character. 26 | /// 27 | /// The value to test. 28 | /// true if the value is considered a text character, false if not. 29 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 30 | public static bool IsText(byte value) 31 | { 32 | return IsBetween(value, 'a', 'z') || IsBetween(value, 'A', 'Z') || IsUtf8(value); 33 | } 34 | 35 | /// 36 | /// Returns a value indicating whether or not the given byte is a UTF-8 encoded character. 37 | /// 38 | /// The value to test. 39 | /// true if the value is considered a UTF-8 character, false if not. 40 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 41 | public static bool IsUtf8(byte value) 42 | { 43 | return value >= 0x80; 44 | } 45 | 46 | /// 47 | /// Returns a value indicating whether or not the given byte is considered a digit character. 48 | /// 49 | /// The value to test. 50 | /// true if the value is considered a digit character, false if not. 51 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 52 | public static bool IsNumber(byte value) 53 | { 54 | return IsBetween(value, '0', '9'); 55 | } 56 | 57 | /// 58 | /// Returns a value indicating whether or not the given byte is considered a whitespace. 59 | /// 60 | /// The value to test. 61 | /// true if the value is considered a whitespace character, false if not. 62 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 63 | public static bool IsWhiteSpace(byte value) 64 | { 65 | return value == 32 || IsBetween(value, 9, 13); 66 | } 67 | 68 | /// 69 | /// Returns a value indicating whether or not the given value is inclusively between a given range. 70 | /// 71 | /// The value to test. 72 | /// The lower value of the range. 73 | /// The higher value of the range. 74 | /// true if the value is between the range, false if not. 75 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 76 | static bool IsBetween(byte value, char low, char high) 77 | { 78 | return value >= low && value <= high; 79 | } 80 | 81 | /// 82 | /// Returns a value indicating whether or not the given value is inclusively between a given range. 83 | /// 84 | /// The value to test. 85 | /// The lower value of the range. 86 | /// The higher value of the range. 87 | /// true if the value is between the range, false if not. 88 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 89 | static bool IsBetween(byte value, byte low, byte high) 90 | { 91 | return value >= low && value <= high; 92 | } 93 | 94 | /// 95 | /// Returns the string representation of the token. 96 | /// 97 | /// The string representation of the token. 98 | public override string ToString() 99 | { 100 | return $"[{Kind}] {Text.ToString()}"; 101 | } 102 | 103 | /// 104 | /// Returns the Text selection as a string. 105 | /// 106 | /// The string that was created from the selection. 107 | public string ToText() 108 | { 109 | var text = Text; 110 | 111 | return StringUtil.Create(ref text); 112 | } 113 | 114 | /// 115 | /// Gets the token kind. 116 | /// 117 | public TokenKind Kind { get; } 118 | 119 | /// 120 | /// Returns the text representation of the token. 121 | /// 122 | public ReadOnlySpan Text { get; } 123 | } 124 | } 125 | --------------------------------------------------------------------------------