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

SMTP.SendEmail - Added AcceptAllCerts parameter #26

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Frends.SMTP.SendEmail/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Changelog

## [1.3.0] - 2024-09-30
### Added
- [Breaking] Added parameter AcceptAllCerts which allows bypassing SSL/TLS certificate validation for SMTP servers.
- Default value: false
- Warning: Enabling this option poses security risks. Only use when connecting to trusted SMTP servers with self-signed certificates.
- Usage: Enable only in controlled environments where certificate validation issues cannot be resolved through proper certificate installation.

## [1.2.1] - 2024-01-02
### Fixed
- Fixed issue with connecting to SMTP servers which do not support authentication.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Moq" Version="4.20.72" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
using NUnit.Framework;
using Moq;
using System;
using System.IO;
using System.Threading.Tasks;
using Frends.SMTP.SendEmail.Definitions;
using MailKit.Net.Smtp;
using MailKit.Security;
using System.Net.Security;
using System.Threading;

namespace Frends.SMTP.SendEmail.Tests;

Expand Down Expand Up @@ -75,7 +80,8 @@ public void EmailTestSetup()
SMTPServer = SMTPADDRESS,
Port = PORT,
UseOAuth2 = false,
SecureSocket = SecureSocketOption.None
SecureSocket = SecureSocketOption.None,
AcceptAllCerts = false
};

}
Expand All @@ -97,6 +103,19 @@ public async Task SendEmailWithPlainText()
Assert.IsTrue(result.EmailSent);
}

[Test]
public async Task EmailTestAcceptAllCertifications()
{
_options.SecureSocket = SecureSocketOption.StartTls;
_options.AcceptAllCerts = true;

var input = _input;
input.Subject = "Email test - PlainText";

var result = await SMTP.SendEmail(input, null, _options, default);
Assert.IsTrue(result.EmailSent);
}
RikuVirtanen marked this conversation as resolved.
Show resolved Hide resolved

[Test]
public async Task SendEmailWithFileAttachment()
{
Expand Down Expand Up @@ -190,4 +209,65 @@ public void TrySendingEmailWithNoFileAttachmentFoundException()
var ex = Assert.ThrowsAsync<FileNotFoundException>(async () => await SMTP.SendEmail(input, Attachments, _options, default));
Assert.AreEqual(@$"The given filepath '{attachment.FilePath}' had no matching files", ex.Message);
}

[Test]
public async Task TestAcceptAllCerts()
{
var options = new Options
{
AcceptAllCerts = true,
SMTPServer = "smtp.example.com",
Port = 587,
SecureSocket = SecureSocketOption.StartTls,
UseOAuth2 = false,
};

var mockSmtpClient = new Mock<SmtpClient> { CallBase = true };

mockSmtpClient.Setup(client => client.ConnectAsync(
It.IsAny<string>(),
It.IsAny<int>(),
It.IsAny<SecureSocketOptions>(),
It.IsAny<CancellationToken>())).Returns(Task.CompletedTask);

mockSmtpClient.Setup(client => client.AuthenticateAsync(
It.IsAny<SaslMechanism>(),
It.IsAny<CancellationToken>())).Returns(Task.CompletedTask);

await SMTP.InitializeSmtpClient(options, default, mockSmtpClient.Object);

// Verify connection parameters
mockSmtpClient.Verify(client => client.ConnectAsync(
options.SMTPServer,
options.Port,
SecureSocketOptions.StartTls,
default), Times.Once);

// Verify certificate validation behavior
Assert.IsNotNull(mockSmtpClient.Object.ServerCertificateValidationCallback);

var callback = mockSmtpClient.Object.ServerCertificateValidationCallback;

// Test various SSL policy errors
Assert.Multiple(() =>
{
Assert.IsTrue(callback.Invoke(null, null, null, SslPolicyErrors.None), "Should accept valid certificates");
Assert.IsTrue(callback.Invoke(null, null, null, SslPolicyErrors.RemoteCertificateNotAvailable), "Should accept missing certificates");
Assert.IsTrue(callback.Invoke(null, null, null, SslPolicyErrors.RemoteCertificateNameMismatch), "Should accept mismatched certificates");
Assert.IsTrue(callback.Invoke(null, null, null, SslPolicyErrors.RemoteCertificateChainErrors), "Should accept invalid certificate chains");
});

// Test with AcceptAllCerts = false
options.AcceptAllCerts = false;
await SMTP.InitializeSmtpClient(options, default, mockSmtpClient.Object);
callback = mockSmtpClient.Object.ServerCertificateValidationCallback;

Assert.Multiple(() =>
{
Assert.IsTrue(callback.Invoke(null, null, null, SslPolicyErrors.None), "Should accept valid certificates");
Assert.IsFalse(callback.Invoke(null, null, null, SslPolicyErrors.RemoteCertificateNotAvailable), "Should reject missing certificates");
Assert.IsFalse(callback.Invoke(null, null, null, SslPolicyErrors.RemoteCertificateNameMismatch), "Should reject mismatched certificates");
Assert.IsFalse(callback.Invoke(null, null, null, SslPolicyErrors.RemoteCertificateChainErrors), "Should reject invalid certificate chains");
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,15 @@ public class Options
[DefaultValue(SecureSocketOption.Auto)]
public SecureSocketOption SecureSocket { get; set; }

/// <summary>
/// WARNING: Setting AcceptAllCerts to true disables SSL/TLS certificate validation.
/// This should only be used in development/test environments with self-signed certificates.
/// Using this option in production environments poses significant security risks.
/// </summary>
/// <example>true</example>
[DefaultValue(false)]
public bool AcceptAllCerts { get; set; }

/// <summary>
/// Set this true if SMTP server expectes OAuth token.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Version>1.2.1</Version>
<Version>1.3.0</Version>
<LangVersion>latest</LangVersion>
<Authors>Frends</Authors>
<Company>Frends</Company>
Expand Down Expand Up @@ -32,4 +32,10 @@
</None>
</ItemGroup>

<ItemGroup>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
<_Parameter1>$(MSBuildProjectName).Tests</_Parameter1>
</AssemblyAttribute>
</ItemGroup>

</Project>
31 changes: 23 additions & 8 deletions Frends.SMTP.SendEmail/Frends.Smtp.SendEmail/SendEmailTask.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@
using System.Threading;
using System.Threading.Tasks;
using Frends.SMTP.SendEmail.Definitions;
using System.Runtime.CompilerServices;
using System.Net.Security;

[assembly: InternalsVisibleTo("Frends.SMTP.Tests")]
namespace Frends.SMTP.SendEmail;
/// <summary>
/// Main class of the Task.
Expand Down Expand Up @@ -96,11 +99,23 @@ public static async Task<Result> SendEmail([PropertyTab] Input input, [PropertyT
/// <summary>
/// Initializes new SmtpClient with given parameters.
/// </summary>
private static async Task<SmtpClient> InitializeSmtpClient(Options settings, CancellationToken cancellationToken)
internal static async Task<SmtpClient> InitializeSmtpClient(Options options, CancellationToken cancellationToken, SmtpClient client = null)
{
var client = new SmtpClient();
client ??= new SmtpClient();

var secureSocketOption = settings.SecureSocket switch
// Accept all certs?
if (options.AcceptAllCerts)
{
#pragma warning disable S4830 // Server certificates should be verified during SSL/TLS connections
client.ServerCertificateValidationCallback = (s, x509certificate, x590chain, sslPolicyErrors) => true;
#pragma warning restore S4830 // Server certificates should be verified during SSL/TLS connections
}
RikuVirtanen marked this conversation as resolved.
Show resolved Hide resolved
else
{
client.ServerCertificateValidationCallback = (s, x509certificate, x590chain, sslPolicyErrors) => sslPolicyErrors == SslPolicyErrors.None;
}

var secureSocketOption = options.SecureSocket switch
{
SecureSocketOption.None => SecureSocketOptions.None,
SecureSocketOption.SslOnConnect => SecureSocketOptions.SslOnConnect,
Expand All @@ -109,16 +124,16 @@ private static async Task<SmtpClient> InitializeSmtpClient(Options settings, Can
_ => SecureSocketOptions.Auto,
};

await client.ConnectAsync(settings.SMTPServer, settings.Port, secureSocketOption, cancellationToken);
await client.ConnectAsync(options.SMTPServer, options.Port, secureSocketOption, cancellationToken);

SaslMechanism mechanism;

if (settings.UseOAuth2)
mechanism = new SaslMechanismOAuth2(settings.UserName, settings.Token);
else if (string.IsNullOrEmpty(settings.Password))
if (options.UseOAuth2)
mechanism = new SaslMechanismOAuth2(options.UserName, options.Token);
else if (string.IsNullOrEmpty(options.Password))
return client;
else
mechanism = new SaslMechanismLogin(new NetworkCredential(settings.UserName, settings.Password));
mechanism = new SaslMechanismLogin(new NetworkCredential(options.UserName, options.Password));

await client.AuthenticateAsync(mechanism, cancellationToken);

Expand Down
Loading