├── 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 | [](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 |
--------------------------------------------------------------------------------