Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implemented smtp client certificate validation and introduced 2 new events if smtp server fails to bind to a socket #214

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
Open
3 changes: 2 additions & 1 deletion Examples/SampleApp/Examples/CustomEndpointListenerExample.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.IO;
using System.IO.Pipelines;
using System.Net.Security;
using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;
using System.Threading;
Expand Down Expand Up @@ -80,7 +81,7 @@ public CustomSecurableDuplexPipe(ISecurableDuplexPipe securableDuplexPipe)
_securableDuplexPipe = securableDuplexPipe;
}

public Task UpgradeAsync(X509Certificate certificate, SslProtocols protocols, CancellationToken cancellationToken = default)
public Task UpgradeAsync(X509Certificate certificate, SslProtocols protocols, CancellationToken cancellationToken = default, RemoteCertificateValidationCallback remoteCertificateValidationCallback = null)
{
return _securableDuplexPipe.UpgradeAsync(certificate, protocols, cancellationToken);
}
Expand Down
10 changes: 6 additions & 4 deletions Examples/SampleApp/Examples/SecureServerExample.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@

namespace SampleApp.Examples
{
public static class SecureServerExample
public static class UnsecureServerExample
{
public static void Run()
{
// this is important when dealing with a certificate that isnt valid
// this is important when dealing with a certificate that isn't valid
ServicePointManager.ServerCertificateValidationCallback = IgnoreCertificateValidationFailureForTestingOnly;

var cancellationTokenSource = new CancellationTokenSource();
Expand All @@ -23,7 +23,9 @@ public static void Run()
.ServerName("SmtpServer SampleApp")
.Endpoint(builder =>
builder
.Port(9025, true)
.Port(9025)
.IsSecure(true)
.AuthenticationRequired(true)
.AllowUnsecureAuthentication(false)
.Certificate(CreateCertificate()))
.Build();
Expand All @@ -33,7 +35,7 @@ public static void Run()

var server = new SmtpServer.SmtpServer(options, serviceProvider);
server.SessionCreated += OnSessionCreated;

var serverTask = server.StartAsync(cancellationTokenSource.Token);

SampleMailClient.Send(user: "user", password: "password", useSsl: true);
Expand Down
79 changes: 79 additions & 0 deletions Examples/SampleApp/Examples/UnsecureServerExample.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
using System;
using System.IO;
using System.Net;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
using System.Threading;
using SmtpServer;
using SmtpServer.ComponentModel;
using SmtpServer.Tracing;

namespace SampleApp.Examples
{
public static class SecureServerExample
{
public static void Run()
{
// this is important when dealing with a certificate that isn't valid
ServicePointManager.ServerCertificateValidationCallback = IgnoreCertificateValidationFailureForTestingOnly;

var cancellationTokenSource = new CancellationTokenSource();

var options = new SmtpServerOptionsBuilder()
.ServerName("SmtpServer SampleApp")
.Endpoint(builder =>
builder
.Port(9025)
.IsSecure(false)
.AuthenticationRequired(true)
.AllowUnsecureAuthentication(false)
.Certificate(CreateCertificate()))
.Build();

var serviceProvider = new ServiceProvider();
serviceProvider.Add(new SampleUserAuthenticator());
// Client certificate validation will only be performed if the endpoint is not secure by default.
serviceProvider.Add(new SampleClientCertificateValidator());

var server = new SmtpServer.SmtpServer(options, serviceProvider);
server.SessionCreated += OnSessionCreated;

var serverTask = server.StartAsync(cancellationTokenSource.Token);

SampleMailClient.Send(user: "user", password: "password", useSsl: true);

cancellationTokenSource.Cancel();
serverTask.WaitWithoutException();
}

static void OnSessionCreated(object sender, SessionEventArgs e)
{
Console.WriteLine("Session Created.");

e.Context.CommandExecuting += OnCommandExecuting;
}

static void OnCommandExecuting(object sender, SmtpCommandEventArgs e)
{
Console.WriteLine("Command Executing.");

new TracingSmtpCommandVisitor(Console.Out).Visit(e.Command);
}

static bool IgnoreCertificateValidationFailureForTestingOnly(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
{
return true;
}

static X509Certificate2 CreateCertificate()
{
// to create an X509Certificate for testing you need to run MAKECERT.EXE and then PVK2PFX.EXE
// http://www.digitallycreated.net/Blog/38/using-makecert-to-create-certificates-for-development

var certificate = File.ReadAllBytes(@"C:\Users\caino\Dropbox\Documents\Cain\Programming\SmtpServer\SmtpServer.pfx");
var password = File.ReadAllText(@"C:\Users\caino\Dropbox\Documents\Cain\Programming\SmtpServer\SmtpServerPassword.txt");

return new X509Certificate2(certificate, password);
}
}
}
16 changes: 16 additions & 0 deletions Examples/SampleApp/SampleClientCertificateValidator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using System.Net.Security;
using SmtpServer.Authentication;

namespace SampleApp
{
public sealed class SampleClientCertificateValidator : ClientCertificateValidator
{
public override RemoteCertificateValidationCallback RemoteClientCertificateValidationCallback { get; set; } =
(sender, certificate, chain, sslPolicyErrors) =>
{
// Provide your custom client certificate validation logic here.
// Return true if valid; otherwise, false.
return true;
};
}
}
23 changes: 21 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,15 @@ await smtpServer.StartAsync(CancellationToken.None);

# What hooks are provided?

There are three hooks that can be implemented; IMessageStore, IMailboxFilter, and IUserAuthenticator.
There are four hooks that can be implemented; IMessageStore, IMailboxFilter, IUserAuthenticator and IClientCertificateValidator.

```cs
var options = new SmtpServerOptionsBuilder()
.ServerName("localhost")
.Endpoint(builder =>
builder
.Port(9025, true)
.Port(9025)
.IsSecure(true)
.AllowUnsecureAuthentication(false)
.Certificate(CreateCertificate()))
.Build();
Expand All @@ -52,6 +53,7 @@ var serviceProvider = new ServiceProvider();
serviceProvider.Add(new SampleMessageStore());
serviceProvider.Add(new SampleMailboxFilter());
serviceProvider.Add(new SampleUserAuthenticator());
serviceProvider.Add(new SampleClientCertificateValidator());

var smtpServer = new SmtpServer.SmtpServer(options, serviceProvider);
await smtpServer.StartAsync(CancellationToken.None);
Expand Down Expand Up @@ -130,3 +132,20 @@ public class SampleUserAuthenticator : IUserAuthenticator, IUserAuthenticatorFac
}
}
```

```cs
public sealed class SampleClientCertificateValidator : IClientCertificateValidator, IClientCertificateValidatorFactory
{
public RemoteCertificateValidationCallback RemoteClientCertificateValidationCallback { get; set; } =
(sender, certificate, chain, sslPolicyErrors) =>
{
// Provide your custom client certificate validation logic here.
// Return true if valid; otherwise, false.
return true;
};

public IClientCertificateValidator CreateInstance(ISessionContext context)
{
return new SampleClientCertificateValidator();
}
```
33 changes: 21 additions & 12 deletions Src/SmtpServer.Tests/MailClient.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System.Threading;
using System;
using System.Threading;
using MailKit;
using MailKit.Net.Smtp;
using MailKit.Security;
using MimeKit;
Expand Down Expand Up @@ -46,16 +48,23 @@ public static MimeMessage Message(
return message;
}

public static SmtpClientEx Client(string host = "localhost", int port = 9025, SecureSocketOptions options = SecureSocketOptions.Auto)
public static SmtpClientEx Client()
{
var client = new SmtpClientEx();

client.Connected += (sender, args) =>
{
return new SmtpClientEx();
}

};
public static SmtpClientEx Connect(this SmtpClientEx client,
EventHandler<ConnectedEventArgs> onConnected = null,
string host = "localhost",
int port = 9025,
SecureSocketOptions options = SecureSocketOptions.Auto)
{
if (onConnected is not null)
{
client.Connected += onConnected;
}

client.Connect("localhost", 9025, options);
client.Connect(host, port, options);

return client;
}
Expand All @@ -67,7 +76,7 @@ public static void Send(
{
message ??= Message();

using var client = Client();
using var client = Client().Connect();

if (user != null && password != null)
{
Expand All @@ -82,17 +91,17 @@ public static void Send(

public static void NoOp(SecureSocketOptions options = SecureSocketOptions.Auto)
{
using var client = Client(options: options);
using var client = Client().Connect(options: options);

client.NoOp();
client.NoOp();
}
}

internal class SmtpClientEx : SmtpClient
{
public SmtpResponse SendUnknownCommand(string command, CancellationToken cancellationToken = default)
{
return SendCommand(command, cancellationToken);
return SendCommand(command, cancellationToken);
}
}
}
Loading