diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml
index 87dc5d3b1..c92d62fd6 100644
--- a/.github/workflows/cicd.yml
+++ b/.github/workflows/cicd.yml
@@ -87,6 +87,7 @@ jobs:
{ name: "Testcontainers.Redpanda", runs-on: "ubuntu-22.04" },
{ name: "Testcontainers.ServiceBus", runs-on: "ubuntu-22.04" },
{ name: "Testcontainers.Sftp", runs-on: "ubuntu-22.04" },
+ { name: "Testcontainers.Smtp4Dev", runs-on: "ubuntu-22.04" },
{ name: "Testcontainers.Weaviate", runs-on: "ubuntu-22.04" },
{ name: "Testcontainers.WebDriver", runs-on: "ubuntu-22.04" },
{ name: "Testcontainers.Xunit", runs-on: "ubuntu-22.04" }
diff --git a/Testcontainers.sln b/Testcontainers.sln
index 14af32b18..f0cbf9fff 100644
--- a/Testcontainers.sln
+++ b/Testcontainers.sln
@@ -105,6 +105,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.ServiceBus",
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Sftp", "src\Testcontainers.Sftp\Testcontainers.Sftp.csproj", "{7D5C6816-0DD2-4E13-A585-033B5D3C80D5}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Smtp4Dev", "src\Testcontainers.Smtp4Dev\Testcontainers.Smtp4Dev.csproj", "{DA635A41-3448-4DF3-8A1E-D3CF9C7F4B70}"
+EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Weaviate", "src\Testcontainers.Weaviate\Testcontainers.Weaviate.csproj", "{68F8600D-24E9-4E03-9E25-5F6EB338EAC1}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.WebDriver", "src\Testcontainers.WebDriver\Testcontainers.WebDriver.csproj", "{64A87DE5-29B0-4A54-9E74-560484D8C7C0}"
@@ -225,6 +227,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.ServiceBus.T
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Sftp.Tests", "tests\Testcontainers.Sftp.Tests\Testcontainers.Sftp.Tests.csproj", "{B73C3CC0-9F16-4B34-92BE-6EC0853912C5}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Smtp4Dev.Tests", "tests\Testcontainers.Smtp4Dev.Tests\Testcontainers.Smtp4Dev.Tests.csproj", "{F7387519-8EB0-4B87-B817-A09CA8CE369A}"
+EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Tests", "tests\Testcontainers.Tests\Testcontainers.Tests.csproj", "{27CDB869-A150-4593-958F-6F26E5391E7C}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Weaviate.Tests", "tests\Testcontainers.Weaviate.Tests\Testcontainers.Weaviate.Tests.csproj", "{DDB41BC8-5826-4D97-9C5F-001151E3FFD6}"
@@ -682,6 +686,14 @@ Global
{E901DF14-6F05-4FC2-825A-3055FAD33561}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E901DF14-6F05-4FC2-825A-3055FAD33561}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E901DF14-6F05-4FC2-825A-3055FAD33561}.Release|Any CPU.Build.0 = Release|Any CPU
+ {DA635A41-3448-4DF3-8A1E-D3CF9C7F4B70}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {DA635A41-3448-4DF3-8A1E-D3CF9C7F4B70}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {DA635A41-3448-4DF3-8A1E-D3CF9C7F4B70}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {DA635A41-3448-4DF3-8A1E-D3CF9C7F4B70}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F7387519-8EB0-4B87-B817-A09CA8CE369A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F7387519-8EB0-4B87-B817-A09CA8CE369A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F7387519-8EB0-4B87-B817-A09CA8CE369A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F7387519-8EB0-4B87-B817-A09CA8CE369A}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{5365F780-0E6C-41F0-B1B9-7DC34368F80C} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
@@ -794,5 +806,7 @@ Global
{DDB41BC8-5826-4D97-9C5F-001151E3FFD6} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
{EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
{E901DF14-6F05-4FC2-825A-3055FAD33561} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
+ {DA635A41-3448-4DF3-8A1E-D3CF9C7F4B70} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
+ {F7387519-8EB0-4B87-B817-A09CA8CE369A} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
EndGlobalSection
EndGlobal
diff --git a/src/Testcontainers.Smtp4Dev/.editorconfig b/src/Testcontainers.Smtp4Dev/.editorconfig
new file mode 100644
index 000000000..6f066619d
--- /dev/null
+++ b/src/Testcontainers.Smtp4Dev/.editorconfig
@@ -0,0 +1 @@
+root = true
\ No newline at end of file
diff --git a/src/Testcontainers.Smtp4Dev/Smtp4DevBuilder.cs b/src/Testcontainers.Smtp4Dev/Smtp4DevBuilder.cs
new file mode 100644
index 000000000..8c3da3ea4
--- /dev/null
+++ b/src/Testcontainers.Smtp4Dev/Smtp4DevBuilder.cs
@@ -0,0 +1,160 @@
+using TestContainers.Smtp4Dev;
+
+namespace Testcontainers.Smtp4Dev;
+
+///
+[PublicAPI]
+public sealed class Smtp4DevBuilder : ContainerBuilder
+{
+ public const string Smtp4DevImage = "rnwood/smtp4dev:latest";
+
+ public const ushort WebInterfacePort = 80;
+ public const ushort SmtpPort = 25;
+ public const ushort ImapPort = 143;
+
+ public const bool DefaultLockSettings = false;
+ public const string DefaultBasePath = "/";
+ public const string DefaultDatabase = "database.db";
+ public const ushort DefaultNumberOfMessagesToKeep = 100;
+ public const ushort DefaultNumberOfSessionsToKeep = 100;
+ public const bool DefaultDisableMessageSanitisation = false;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public Smtp4DevBuilder()
+ : this(new Smtp4DevConfiguration())
+ {
+ DockerResourceConfiguration = Init().DockerResourceConfiguration;
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The Docker resource configuration.
+ private Smtp4DevBuilder(Smtp4DevConfiguration resourceConfiguration)
+ : base(resourceConfiguration)
+ {
+ DockerResourceConfiguration = resourceConfiguration;
+ }
+
+ ///
+ protected override Smtp4DevConfiguration DockerResourceConfiguration { get; }
+
+ ///
+ /// Sets whether settings can be changed by user via web interface.
+ ///
+ /// Locks settings form being changed by user via web interface.
+ /// A configured instance of .
+ public Smtp4DevBuilder WithLockSettings(bool lockSettings)
+ {
+ return Merge(DockerResourceConfiguration, new Smtp4DevConfiguration(lockSettings: lockSettings))
+ .WithEnvironment("LockSettings", lockSettings.ToString());
+ }
+
+ ///
+ /// Sets the virtual path from web server root where SMTP4DEV web interface will be hosted. e.g. "/" or
+ /// "/smtp4dev".
+ ///
+ /// Locks settings form being changed by user via web interface.
+ /// A configured instance of .
+ public Smtp4DevBuilder WithBasePath(string basePath)
+ {
+ return Merge(DockerResourceConfiguration, new Smtp4DevConfiguration(basePath: basePath))
+ .WithEnvironment("BasePath", basePath);
+ }
+
+ ///
+ /// Sets the path where the database will be stored relative to APPDATA env var on Windows or XDG_CONFIG_HOME
+ /// on non-Windows. Specify "" to use an in memory database.
+ ///
+ ///
+ /// The path where the database will be stored relative to APPDATA env var on Windows or XDG_CONFIG_HOME
+ /// on non-Windows. Specify "" to use an in memory database.
+ ///
+ /// A configured instance of .
+ public Smtp4DevBuilder WithDatabase(string database)
+ {
+ return Merge(DockerResourceConfiguration, new Smtp4DevConfiguration(database: database))
+ .WithEnvironment("Latabase", database);
+ }
+
+ ///
+ /// Sets the number of messages to keep per mailbox.
+ ///
+ /// The number of messages to keep per mailbox.
+ /// A configured instance of .
+ public Smtp4DevBuilder WithNumberOfMessagesToKeep(int numberOfMessagesToKeep)
+ {
+ return Merge(DockerResourceConfiguration,
+ new Smtp4DevConfiguration(numberOfMessagesToKeep: numberOfMessagesToKeep))
+ .WithEnvironment("NumberOfMessagesToKeep", numberOfMessagesToKeep.ToString());
+ }
+
+ ///
+ /// Sets the number of sessions to keep.
+ ///
+ /// The number of sessions to keep.
+ /// A configured instance of .
+ public Smtp4DevBuilder WithNumberOfSessionsToKeep(int numberOfSessionsToKeep)
+ {
+ return Merge(DockerResourceConfiguration,
+ new Smtp4DevConfiguration(numberOfSessionsToKeep: numberOfSessionsToKeep))
+ .WithEnvironment("NumberOfSessionsToKeep", numberOfSessionsToKeep.ToString());
+ }
+
+ ///
+ /// Sets whether message HTML sanitisation should be enabled. Dangerous if your messages are not generated by you
+ /// and not reflective of how messages might render in most email clients.
+ ///
+ ///
+ /// Whether message HTML sanitisation should be enabled. Dangerous if your messages are not generated by you
+ /// and not reflective of how messages might render in most email clients.
+ ///
+ /// A configured instance of .
+ public Smtp4DevBuilder WithDisableMessageSanitisations(bool disableMessageSanitisations)
+ {
+ return Merge(DockerResourceConfiguration,
+ new Smtp4DevConfiguration(disableMessageSanitisations: disableMessageSanitisations))
+ .WithEnvironment("DisableMessageSanitisations", disableMessageSanitisations.ToString());
+ }
+
+ ///
+ public override Smtp4DevContainer Build()
+ {
+ Validate();
+ return new Smtp4DevContainer(DockerResourceConfiguration);
+ }
+
+ ///
+ protected override Smtp4DevBuilder Init()
+ {
+ return base.Init()
+ .WithImage(Smtp4DevImage)
+ .WithPortBinding(SmtpPort, true)
+ .WithPortBinding(ImapPort, true)
+ .WithPortBinding(WebInterfacePort, true)
+ .WithWaitStrategy(Wait.ForUnixContainer()
+ .UntilMessageIsLogged("Now listening on: .+")
+ .UntilMessageIsLogged("SMTP Server is listening on port")
+ .UntilMessageIsLogged("IMAP Server is listening on port") );
+ }
+
+ ///
+ protected override Smtp4DevBuilder Clone(IResourceConfiguration resourceConfiguration)
+ {
+ return Merge(DockerResourceConfiguration, new Smtp4DevConfiguration(resourceConfiguration));
+ }
+
+ ///
+ protected override Smtp4DevBuilder Clone(IContainerConfiguration resourceConfiguration)
+ {
+ return Merge(DockerResourceConfiguration, new Smtp4DevConfiguration(resourceConfiguration));
+ }
+
+ ///
+ protected override Smtp4DevBuilder Merge(Smtp4DevConfiguration oldValue, Smtp4DevConfiguration newValue)
+ {
+ return new Smtp4DevBuilder(new Smtp4DevConfiguration(oldValue, newValue));
+ }
+}
\ No newline at end of file
diff --git a/src/Testcontainers.Smtp4Dev/Smtp4DevConfiguration.cs b/src/Testcontainers.Smtp4Dev/Smtp4DevConfiguration.cs
new file mode 100644
index 000000000..5e33daf70
--- /dev/null
+++ b/src/Testcontainers.Smtp4Dev/Smtp4DevConfiguration.cs
@@ -0,0 +1,118 @@
+namespace TestContainers.Smtp4Dev;
+
+///
+[PublicAPI]
+public class Smtp4DevConfiguration : ContainerConfiguration
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Locks settings form being changed by user via web interface.
+ ///
+ /// Specifies the virtual path from web server root where SMTP4DEV web interface will be hosted. e.g. "/" or
+ /// "/smtp4dev".
+ ///
+ ///
+ /// Specifies the path where the database will be stored relative to APPDATA env var on Windows or XDG_CONFIG_HOME
+ /// on non-Windows. Specify "" to use an in memory database.
+ ///
+ /// Specifies the number of messages to keep per mailbox.
+ /// Specifies the number of sessions to keep.
+ ///
+ /// Disables message HTML sanitisation. Dangerous if your messages are not generated by you and not reflective of
+ /// how messages might render in most email clients.
+ ///
+ public Smtp4DevConfiguration(
+ bool? lockSettings = null,
+ string basePath = null,
+ string database = null,
+ int? numberOfMessagesToKeep = null,
+ int? numberOfSessionsToKeep = null,
+ bool? disableMessageSanitisations = null)
+ {
+ LockSettings = lockSettings;
+ BasePath = basePath;
+ Database = database;
+ NumberOfMessagesToKeep = numberOfMessagesToKeep;
+ NumberOfSessionsToKeep = numberOfSessionsToKeep;
+ DisableMessageSanitisations = disableMessageSanitisations;
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The Docker resource configuration.
+ public Smtp4DevConfiguration(IResourceConfiguration resourceConfiguration)
+ : base(resourceConfiguration)
+ {
+ // Passes the configuration upwards to the base implementations to create an updated immutable copy.
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The Docker resource configuration.
+ public Smtp4DevConfiguration(IContainerConfiguration resourceConfiguration)
+ : base(resourceConfiguration)
+ {
+ // Passes the configuration upwards to the base implementations to create an updated immutable copy.
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The Docker resource configuration.
+ public Smtp4DevConfiguration(Smtp4DevConfiguration resourceConfiguration)
+ : this(new Smtp4DevConfiguration(), resourceConfiguration)
+ {
+ // Passes the configuration upwards to the base implementations to create an updated immutable copy.
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The old Docker resource configuration.
+ /// The new Docker resource configuration.
+ public Smtp4DevConfiguration(Smtp4DevConfiguration oldValue, Smtp4DevConfiguration newValue)
+ : base(oldValue, newValue)
+ {
+ LockSettings = BuildConfiguration.Combine(oldValue.LockSettings, newValue.LockSettings);
+ BasePath = BuildConfiguration.Combine(oldValue.BasePath, newValue.BasePath);
+ Database = BuildConfiguration.Combine(oldValue.Database, newValue.Database);
+ NumberOfMessagesToKeep = BuildConfiguration.Combine(oldValue.NumberOfMessagesToKeep, newValue.NumberOfMessagesToKeep);
+ NumberOfSessionsToKeep = BuildConfiguration.Combine(oldValue.NumberOfSessionsToKeep, newValue.NumberOfSessionsToKeep);
+ DisableMessageSanitisations = BuildConfiguration.Combine(oldValue.DisableMessageSanitisations, newValue.DisableMessageSanitisations);
+ }
+
+ ///
+ /// Gets whether settings can be changed by user via web interface.
+ ///
+ public bool? LockSettings { get; }
+
+ ///
+ /// Gets the virtual path from web server root where SMTP4DEV web interface will be hosted. e.g. "/" or
+ /// "/smtp4dev".
+ ///
+ public string BasePath { get; }
+
+ ///
+ /// Gets the path where the database will be stored relative to APPDATA env var on Windows or XDG_CONFIG_HOME
+ /// on non-Windows. Specify "" to use an in memory database.
+ ///
+ public string Database { get; }
+
+ ///
+ /// Gets the number of messages to keep per mailbox.
+ ///
+ public int? NumberOfMessagesToKeep { get; }
+
+ ///
+ /// Gets the number of sessions to keep.
+ ///
+ public int? NumberOfSessionsToKeep { get; }
+
+ ///
+ /// Gets whether message HTML sanitisation is disabled.
+ ///
+ public bool? DisableMessageSanitisations { get; }
+}
\ No newline at end of file
diff --git a/src/Testcontainers.Smtp4Dev/Smtp4DevContainer.cs b/src/Testcontainers.Smtp4Dev/Smtp4DevContainer.cs
new file mode 100644
index 000000000..e8d9fbae7
--- /dev/null
+++ b/src/Testcontainers.Smtp4Dev/Smtp4DevContainer.cs
@@ -0,0 +1,15 @@
+namespace TestContainers.Smtp4Dev;
+
+///
+[PublicAPI]
+public sealed class Smtp4DevContainer : DockerContainer
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The container configuration.
+ public Smtp4DevContainer(Smtp4DevConfiguration configuration)
+ : base(configuration)
+ {
+ }
+}
\ No newline at end of file
diff --git a/src/Testcontainers.Smtp4Dev/Testcontainers.Smtp4Dev.csproj b/src/Testcontainers.Smtp4Dev/Testcontainers.Smtp4Dev.csproj
new file mode 100644
index 000000000..de9b9e234
--- /dev/null
+++ b/src/Testcontainers.Smtp4Dev/Testcontainers.Smtp4Dev.csproj
@@ -0,0 +1,13 @@
+
+
+ net8.0;net9.0;netstandard2.0;netstandard2.1
+ latest
+ TestContainers.Smtp4Dev
+
+
+
+
+
+
+
+
diff --git a/src/Testcontainers.Smtp4Dev/Usings.cs b/src/Testcontainers.Smtp4Dev/Usings.cs
new file mode 100644
index 000000000..fa3a104a1
--- /dev/null
+++ b/src/Testcontainers.Smtp4Dev/Usings.cs
@@ -0,0 +1,6 @@
+global using Docker.DotNet.Models;
+global using DotNet.Testcontainers;
+global using DotNet.Testcontainers.Builders;
+global using DotNet.Testcontainers.Configurations;
+global using DotNet.Testcontainers.Containers;
+global using JetBrains.Annotations;
\ No newline at end of file
diff --git a/tests/Testcontainers.Smtp4Dev.Tests/.editorconfig b/tests/Testcontainers.Smtp4Dev.Tests/.editorconfig
new file mode 100644
index 000000000..6f066619d
--- /dev/null
+++ b/tests/Testcontainers.Smtp4Dev.Tests/.editorconfig
@@ -0,0 +1 @@
+root = true
\ No newline at end of file
diff --git a/tests/Testcontainers.Smtp4Dev.Tests/Smtp4DevContainerTest.cs b/tests/Testcontainers.Smtp4Dev.Tests/Smtp4DevContainerTest.cs
new file mode 100644
index 000000000..14dc9ed05
--- /dev/null
+++ b/tests/Testcontainers.Smtp4Dev.Tests/Smtp4DevContainerTest.cs
@@ -0,0 +1,61 @@
+using System.Collections.Generic;
+using System.Net.Http;
+using System.Net.Http.Json;
+using System.Net.Mail;
+using TestContainers.Smtp4Dev;
+
+namespace Testcontainers.Smtp4Dev;
+
+public sealed class Smtp4DevContainerTest : IAsyncLifetime
+{
+ private readonly Smtp4DevContainer _smtp4DevContainer = new Smtp4DevBuilder().Build();
+
+ public Task InitializeAsync()
+ {
+ return _smtp4DevContainer.StartAsync();
+ }
+
+ public Task DisposeAsync()
+ {
+ return _smtp4DevContainer.DisposeAsync().AsTask();
+ }
+
+ [Fact]
+ [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))]
+ public async Task IsConnectedReturnsTrue()
+ {
+ // Given
+ var host = _smtp4DevContainer.Hostname;
+
+ var smtpPort = _smtp4DevContainer.GetMappedPublicPort(Smtp4DevBuilder.SmtpPort);
+ var httpPort = _smtp4DevContainer.GetMappedPublicPort(Smtp4DevBuilder.WebInterfacePort);
+
+ const string senderEmail = "sender@example.com";
+ const string receiverEmail = "receiver@example.com";
+ const string subject = "Test mail";
+ const string body = "This is a test mail";
+
+ using var smtpClient = new SmtpClient(host, smtpPort);
+ using var httpClient = new HttpClient();
+
+ // When
+ await smtpClient.SendMailAsync(new MailMessage
+ {
+ From = new MailAddress(senderEmail),
+ To = { new MailAddress(receiverEmail) },
+ Subject = subject,
+ Body = body,
+ });
+
+ // Then
+ var result = await httpClient.GetFromJsonAsync($"http://{host}:{httpPort}/api/Messages");
+
+ Assert.Contains(result.Results, message => message.From == senderEmail &&
+ message.To.Length == 1 && message.To[0] == receiverEmail &&
+ message.Subject == subject);
+ }
+
+ public record PagedMessageResult(List Results);
+
+ public record Message(string From, string[] To, string Subject);
+}
\ No newline at end of file
diff --git a/tests/Testcontainers.Smtp4Dev.Tests/Testcontainers.Smtp4Dev.Tests.csproj b/tests/Testcontainers.Smtp4Dev.Tests/Testcontainers.Smtp4Dev.Tests.csproj
new file mode 100644
index 000000000..faf10e23e
--- /dev/null
+++ b/tests/Testcontainers.Smtp4Dev.Tests/Testcontainers.Smtp4Dev.Tests.csproj
@@ -0,0 +1,18 @@
+
+
+ net9.0
+ false
+ false
+ TestContainers.Smtp4Dev.Tests
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/Testcontainers.Smtp4Dev.Tests/Usings.cs b/tests/Testcontainers.Smtp4Dev.Tests/Usings.cs
new file mode 100644
index 000000000..db5ab3498
--- /dev/null
+++ b/tests/Testcontainers.Smtp4Dev.Tests/Usings.cs
@@ -0,0 +1,4 @@
+global using System.Threading;
+global using System.Threading.Tasks;
+global using DotNet.Testcontainers.Commons;
+global using Xunit;