diff --git a/Directory.Packages.props b/Directory.Packages.props
index 99f45ab76..21b1373bd 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -67,6 +67,7 @@
+
diff --git a/Testcontainers.slnx b/Testcontainers.slnx
index fd3853239..24e32d2d9 100644
--- a/Testcontainers.slnx
+++ b/Testcontainers.slnx
@@ -48,6 +48,7 @@
+
@@ -116,6 +117,7 @@
+
diff --git a/docs/modules/index.md b/docs/modules/index.md
index 930012923..1b3fec695 100644
--- a/docs/modules/index.md
+++ b/docs/modules/index.md
@@ -52,6 +52,7 @@ await moduleNameContainer.StartAsync();
| Kusto emulator | `mcr.microsoft.com/azuredataexplorer/kustainer-linux:latest` | [NuGet](https://www.nuget.org/packages/Testcontainers.Kusto) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.Kusto) |
| LocalStack | `localstack/localstack:2.0` | [NuGet](https://www.nuget.org/packages/Testcontainers.LocalStack) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.LocalStack) |
| Lowkey Vault | `nagyesta/lowkey-vault:2.7.1-ubi9-minimal` | [NuGet](https://www.nuget.org/packages/Testcontainers.LowkeyVault) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.LowkeyVault) |
+| Mailpit | `axllent/mailpit:v1.30` | [NuGet](https://www.nuget.org/packages/Testcontainers.Mailpit) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.Mailpit) |
| MariaDB | `mariadb:10.10` | [NuGet](https://www.nuget.org/packages/Testcontainers.MariaDb) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.MariaDb) |
| Milvus | `milvusdb/milvus:v2.3.10` | [NuGet](https://www.nuget.org/packages/Testcontainers.Milvus) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.Milvus) |
| MinIO | `minio/minio:RELEASE.2023-01-31T02-24-19Z` | [NuGet](https://www.nuget.org/packages/Testcontainers.Minio) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.Minio) |
diff --git a/src/Testcontainers.Mailpit/.editorconfig b/src/Testcontainers.Mailpit/.editorconfig
new file mode 100644
index 000000000..6f066619d
--- /dev/null
+++ b/src/Testcontainers.Mailpit/.editorconfig
@@ -0,0 +1 @@
+root = true
\ No newline at end of file
diff --git a/src/Testcontainers.Mailpit/MailpitBuilder.cs b/src/Testcontainers.Mailpit/MailpitBuilder.cs
new file mode 100644
index 000000000..64a34f7d2
--- /dev/null
+++ b/src/Testcontainers.Mailpit/MailpitBuilder.cs
@@ -0,0 +1,143 @@
+namespace Testcontainers.Mailpit;
+
+///
+[PublicAPI]
+public sealed class MailpitBuilder
+ : ContainerBuilder
+{
+ [Obsolete("This constant is obsolete and will be removed in the future. Use the constructor with the image parameter instead: https://github.com/testcontainers/testcontainers-dotnet/discussions/1470#discussioncomment-15185721.")]
+ public const string MailpitImage = "axllent/mailpit:v1.30";
+
+ public const ushort SmtpPort = 1025;
+
+ public const ushort WebPort = 8025;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ [Obsolete("This parameterless constructor is obsolete and will be removed. Use the constructor with the image parameter instead: https://github.com/testcontainers/testcontainers-dotnet/discussions/1470#discussioncomment-15185721.")]
+ public MailpitBuilder()
+ :this(MailpitImage)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The full Docker image name, including the image repository and tag (e.g., axllent/mailpit:v1.30).
+ ///
+ /// Docker image tags available at .
+ ///
+ public MailpitBuilder(string image)
+ : this(new DockerImage(image))
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// An instance that specifies the Docker image to be used for the container builder configuration.
+ ///
+ /// Docker image tags available at .
+ ///
+ public MailpitBuilder(IImage image)
+ : this(new MailpitConfiguration())
+ {
+ DockerResourceConfiguration = Init().WithImage(image).DockerResourceConfiguration;
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The Docker resource configuration.
+ private MailpitBuilder(MailpitConfiguration resourceConfiguration)
+ : base(resourceConfiguration)
+ {
+ DockerResourceConfiguration = resourceConfiguration;
+ }
+
+ ///
+ protected override MailpitConfiguration DockerResourceConfiguration { get; }
+
+ ///
+ /// Sets the Mailpit MP_SMTP_AUTH config.
+ ///
+ /// The credentials to be used in SMTP authentication.
+ ///
+ /// When , the MP_SMTP_AUTH_ALLOW_INSECURE config is set to true to allow insecure PLAIN and LOGIN SMTP authentication.
+ /// When , a self-signed certificate is used. Its subject and issuer are CN=localhost, O=Mailpit self-signed certificate.
+ ///
+ /// A configured instance of .
+ public MailpitBuilder WithSmtpAuthCredentials(NetworkCredential credentials, bool allowInsecure)
+ {
+ if (credentials == null)
+ {
+ throw new ArgumentNullException(nameof(credentials));
+ }
+
+ if (credentials.UserName.Contains(":"))
+ {
+ throw new ArgumentException("The UserName cannot contain a colon (:) character.", nameof(credentials));
+ }
+
+ // https://mailpit.axllent.org/docs/configuration/smtp/#adding-smtp-authentication
+ var builder = Merge(DockerResourceConfiguration, new MailpitConfiguration(smtpAuthCredentials: credentials, smtpAuthAllowInsecure: allowInsecure))
+ .WithEnvironment("MP_SMTP_AUTH", $"{credentials.UserName}:{credentials.Password}");
+
+ return allowInsecure
+ ? builder
+ .WithEnvironment("MP_SMTP_AUTH_ALLOW_INSECURE", "1")
+ : builder
+ // https://mailpit.axllent.org/docs/configuration/certificates/#auto-generate-self-signed-certificates
+ .WithEnvironment("MP_SMTP_TLS_CERT", "sans:localhost")
+ .WithEnvironment("MP_SMTP_TLS_KEY", "sans:localhost");
+ }
+
+ ///
+ /// Sets the Mailpit MP_MAX_MESSAGES config.
+ /// Maximum number of messages to store. Mailpit will periodically delete the oldest messages if greater than this. Set to 0 to disable auto-deletion.
+ ///
+ /// The maximum number of messages to set.
+ /// A configured instance of .
+ public MailpitBuilder WithMaxMessages(uint maxMessages)
+ {
+ return Merge(DockerResourceConfiguration, new MailpitConfiguration(maxMessages: maxMessages))
+ .WithEnvironment("MP_MAX_MESSAGES", maxMessages.ToString());
+ }
+
+ ///
+ public override MailpitContainer Build()
+ {
+ Validate();
+ return new MailpitContainer(DockerResourceConfiguration);
+ }
+
+ ///
+ protected override MailpitBuilder Init()
+ {
+ return base.Init()
+ .WithPortBinding(SmtpPort, true)
+ .WithPortBinding(WebPort, true)
+ .WithWaitStrategy(Wait.ForUnixContainer().UntilHttpRequestIsSucceeded(request =>
+ // https://mailpit.axllent.org/docs/integration/healthcheck/
+ request.ForPort(WebPort).ForPath("/readyz")));
+ }
+
+ ///
+ protected override MailpitBuilder Clone(IResourceConfiguration resourceConfiguration)
+ {
+ return Merge(DockerResourceConfiguration, new MailpitConfiguration(resourceConfiguration));
+ }
+
+ ///
+ protected override MailpitBuilder Clone(IContainerConfiguration resourceConfiguration)
+ {
+ return Merge(DockerResourceConfiguration, new MailpitConfiguration(resourceConfiguration));
+ }
+
+ ///
+ protected override MailpitBuilder Merge(MailpitConfiguration oldValue, MailpitConfiguration newValue)
+ {
+ return new MailpitBuilder(new MailpitConfiguration(oldValue, newValue));
+ }
+}
diff --git a/src/Testcontainers.Mailpit/MailpitConfiguration.cs b/src/Testcontainers.Mailpit/MailpitConfiguration.cs
new file mode 100644
index 000000000..b8f15b760
--- /dev/null
+++ b/src/Testcontainers.Mailpit/MailpitConfiguration.cs
@@ -0,0 +1,77 @@
+namespace Testcontainers.Mailpit;
+
+///
+[PublicAPI]
+public sealed class MailpitConfiguration : ContainerConfiguration
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Username and password for SMTP authentication. The username must not contain a : character.
+ /// Typically, STARTTLS is enforced for all SMTP authentication. This option allows insecure PLAIN & LOGIN SMTP authentication.
+ /// Maximum number of messages to store. Mailpit will periodically delete the oldest messages if greater than this. Set to 0 to disable auto-deletion.
+ public MailpitConfiguration(NetworkCredential smtpAuthCredentials = null, bool smtpAuthAllowInsecure = true, uint maxMessages = 100)
+ {
+ SmtpAuthCredentials = smtpAuthCredentials;
+ SmtpAuthAllowInsecure = smtpAuthAllowInsecure;
+ MaxMessages = maxMessages;
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The Docker resource configuration.
+ public MailpitConfiguration(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 MailpitConfiguration(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 MailpitConfiguration(MailpitConfiguration resourceConfiguration)
+ : this(new MailpitConfiguration(), 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 MailpitConfiguration(MailpitConfiguration oldValue, MailpitConfiguration newValue)
+ : base(oldValue, newValue)
+ {
+ SmtpAuthCredentials = BuildConfiguration.Combine(oldValue.SmtpAuthCredentials, newValue.SmtpAuthCredentials);
+ SmtpAuthAllowInsecure = BuildConfiguration.Combine(oldValue.SmtpAuthAllowInsecure, newValue.SmtpAuthAllowInsecure);
+ MaxMessages = BuildConfiguration.Combine(oldValue.MaxMessages, newValue.MaxMessages);
+ }
+
+ ///
+ /// Username and password for SMTP authentication. The username must not contain a : character.
+ ///
+ public NetworkCredential SmtpAuthCredentials { get; }
+
+ ///
+ /// Typically, STARTTLS is enforced for all SMTP authentication. This option allows insecure PLAIN & LOGIN SMTP authentication.
+ ///
+ public bool SmtpAuthAllowInsecure { get; }
+
+ ///
+ /// Maximum number of messages to store. Mailpit will periodically delete the oldest messages if greater than this. Set to 0 to disable auto-deletion.
+ ///
+ public uint MaxMessages { get; }
+}
diff --git a/src/Testcontainers.Mailpit/MailpitContainer.cs b/src/Testcontainers.Mailpit/MailpitContainer.cs
new file mode 100644
index 000000000..c9747354c
--- /dev/null
+++ b/src/Testcontainers.Mailpit/MailpitContainer.cs
@@ -0,0 +1,28 @@
+namespace Testcontainers.Mailpit;
+
+///
+[PublicAPI]
+public sealed class MailpitContainer : DockerContainer
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The container configuration.
+ public MailpitContainer(MailpitConfiguration configuration)
+ : base(configuration)
+ {
+ }
+
+ ///
+ /// The SMTP server port.
+ ///
+ public ushort SmtpPort => GetMappedPublicPort(MailpitBuilder.SmtpPort);
+
+ ///
+ /// Gets the web server address of the user interface. Can also be used as the base URL for the REST API.
+ ///
+ public string GetWebAddress()
+ {
+ return new UriBuilder(Uri.UriSchemeHttp, Hostname, GetMappedPublicPort(MailpitBuilder.WebPort)).ToString();
+ }
+}
diff --git a/src/Testcontainers.Mailpit/Testcontainers.Mailpit.csproj b/src/Testcontainers.Mailpit/Testcontainers.Mailpit.csproj
new file mode 100644
index 000000000..6f204b739
--- /dev/null
+++ b/src/Testcontainers.Mailpit/Testcontainers.Mailpit.csproj
@@ -0,0 +1,12 @@
+
+
+ net8.0;net9.0;net10.0;netstandard2.0;netstandard2.1
+ latest
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Testcontainers.Mailpit/Usings.cs b/src/Testcontainers.Mailpit/Usings.cs
new file mode 100644
index 000000000..539e5bac3
--- /dev/null
+++ b/src/Testcontainers.Mailpit/Usings.cs
@@ -0,0 +1,8 @@
+global using System;
+global using System.Net;
+global using Docker.DotNet.Models;
+global using DotNet.Testcontainers.Builders;
+global using DotNet.Testcontainers.Configurations;
+global using DotNet.Testcontainers.Containers;
+global using DotNet.Testcontainers.Images;
+global using JetBrains.Annotations;
\ No newline at end of file
diff --git a/tests/Testcontainers.Mailpit.Tests/.editorconfig b/tests/Testcontainers.Mailpit.Tests/.editorconfig
new file mode 100644
index 000000000..6f066619d
--- /dev/null
+++ b/tests/Testcontainers.Mailpit.Tests/.editorconfig
@@ -0,0 +1 @@
+root = true
\ No newline at end of file
diff --git a/tests/Testcontainers.Mailpit.Tests/.runs-on b/tests/Testcontainers.Mailpit.Tests/.runs-on
new file mode 100644
index 000000000..d0395e498
--- /dev/null
+++ b/tests/Testcontainers.Mailpit.Tests/.runs-on
@@ -0,0 +1 @@
+ubuntu-24.04
\ No newline at end of file
diff --git a/tests/Testcontainers.Mailpit.Tests/Dockerfile b/tests/Testcontainers.Mailpit.Tests/Dockerfile
new file mode 100644
index 000000000..4ae3087e8
--- /dev/null
+++ b/tests/Testcontainers.Mailpit.Tests/Dockerfile
@@ -0,0 +1 @@
+FROM axllent/mailpit:v1.30@sha256:5a49a77c5bdbe7c5474450b4f46348d09949df3695257729c93a30369382d4f6
\ No newline at end of file
diff --git a/tests/Testcontainers.Mailpit.Tests/MailpitContainerTest.cs b/tests/Testcontainers.Mailpit.Tests/MailpitContainerTest.cs
new file mode 100644
index 000000000..bc7258729
--- /dev/null
+++ b/tests/Testcontainers.Mailpit.Tests/MailpitContainerTest.cs
@@ -0,0 +1,115 @@
+namespace Testcontainers.Mailpit;
+
+public abstract partial class MailpitContainerTest(MailpitContainerTest.MailpitFixture fixture)
+{
+ [Fact]
+ [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))]
+ public async Task MailSentAndApiReturnsSuccessful()
+ {
+ // Given
+ var from = new MailboxAddress("The Sender", "sender@mailpit-testcontainers.com");
+ var to = new MailboxAddress("The Receiver", "receiver@mailpit-testcontainers.com");
+ var message = new MimeMessage
+ {
+ From = { from },
+ To = { to },
+ Subject = "Hey there from Mailpit!",
+ Body = new TextPart { Text = "This is just a test message, it doesn't have much going on.\n\nCheers,\n\nSender" },
+ };
+
+ // When
+ var messageId = await fixture.SendMessageAsync(message, TestContext.Current.CancellationToken);
+
+ // Then
+ var response = await fixture.GetMessageAsync(messageId, TestContext.Current.CancellationToken);
+ Assert.Equal(message.Subject, response.Subject);
+ Assert.Equal(from.Address, response.From.Address);
+ Assert.Equal(from.Name, response.From.Name);
+ Assert.Equal(to.Address, response.To[0].Address);
+ Assert.Equal(to.Name, response.To[0].Name);
+ }
+
+ public partial class MailpitFixture(IMessageSink messageSink)
+ : ContainerFixture(messageSink)
+ {
+ protected override MailpitBuilder Configure()
+ => new(TestSession.GetImageFromDockerfile());
+
+ [GeneratedRegex("queued as (.+)")]
+ private static partial Regex QueuedMessage();
+
+ [CanBeNull]
+ protected virtual ICredentials Credentials => null;
+
+ public async Task SendMessageAsync(MimeMessage message, CancellationToken cancellationToken)
+ {
+ using var smtpClient = new SmtpClient();
+ smtpClient.ServerCertificateValidationCallback = (_, certificate, _, _) => certificate?.Issuer == "CN=localhost, O=Mailpit self-signed certificate";
+ await smtpClient.ConnectAsync(Container.Hostname, Container.SmtpPort, SecureSocketOptions.Auto, cancellationToken);
+ if (Credentials != null)
+ {
+ await smtpClient.AuthenticateAsync(Credentials, cancellationToken);
+ }
+
+ var result = await smtpClient.SendAsync(message, cancellationToken);
+
+ var messageId = QueuedMessage().Match(result).Groups[1].Value;
+ Assert.NotEmpty(messageId);
+ return messageId;
+ }
+
+ public async Task GetMessageAsync(string messageId, CancellationToken cancellationToken)
+ {
+ using var client = new HttpClient();
+ client.BaseAddress = new Uri(Container.GetWebAddress());
+ return await client.GetFromJsonAsync($"/api/v1/message/{messageId}", cancellationToken);
+ }
+
+ public class MailpitMessage
+ {
+ public string Subject { get; init; }
+ public Mailbox From { get; init; }
+ public Mailbox[] To { get; init; } = [];
+
+ public class Mailbox
+ {
+ public string Address { get; init; }
+ public string Name { get; init; }
+ }
+ }
+ }
+
+ public abstract class AuthenticationMailpitFixture(IMessageSink messageSink) : MailpitFixture(messageSink)
+ {
+ protected override NetworkCredential Credentials { get; } = new NetworkCredential("user", "p@ssw0rd");
+
+ protected abstract bool AllowInsecure { get; }
+
+ protected override MailpitBuilder Configure()
+ => base.Configure().WithSmtpAuthCredentials(Credentials, AllowInsecure);
+ }
+
+ [UsedImplicitly]
+ public class AuthenticationSecureMailpitFixture(IMessageSink messageSink) : AuthenticationMailpitFixture(messageSink)
+ {
+ protected override bool AllowInsecure => false;
+ }
+
+ [UsedImplicitly]
+ public class AuthenticationInsecureMailpitFixture(IMessageSink messageSink) : AuthenticationMailpitFixture(messageSink)
+ {
+ protected override bool AllowInsecure => true;
+ }
+
+ [UsedImplicitly]
+ public sealed class DefaultConfiguration(MailpitFixture fixture)
+ : MailpitContainerTest(fixture), IClassFixture;
+
+ [UsedImplicitly]
+ public sealed class SmtpAuthSecure(AuthenticationSecureMailpitFixture fixture)
+ : MailpitContainerTest(fixture), IClassFixture;
+
+ [UsedImplicitly]
+ public sealed class SmtpAuthInsecure(AuthenticationInsecureMailpitFixture fixture)
+ : MailpitContainerTest(fixture), IClassFixture;
+}
diff --git a/tests/Testcontainers.Mailpit.Tests/Testcontainers.Mailpit.Tests.csproj b/tests/Testcontainers.Mailpit.Tests/Testcontainers.Mailpit.Tests.csproj
new file mode 100644
index 000000000..a87d7d856
--- /dev/null
+++ b/tests/Testcontainers.Mailpit.Tests/Testcontainers.Mailpit.Tests.csproj
@@ -0,0 +1,25 @@
+
+
+ net10.0
+ false
+ false
+ Exe
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ PreserveNewest
+
+
+
\ No newline at end of file
diff --git a/tests/Testcontainers.Mailpit.Tests/Usings.cs b/tests/Testcontainers.Mailpit.Tests/Usings.cs
new file mode 100644
index 000000000..2013ea5ac
--- /dev/null
+++ b/tests/Testcontainers.Mailpit.Tests/Usings.cs
@@ -0,0 +1,15 @@
+global using DotNet.Testcontainers.Commons;
+global using JetBrains.Annotations;
+global using MailKit.Net.Smtp;
+global using MailKit.Security;
+global using MimeKit;
+global using System;
+global using System.Net;
+global using System.Net.Http;
+global using System.Net.Http.Json;
+global using System.Text.RegularExpressions;
+global using System.Threading;
+global using System.Threading.Tasks;
+global using Testcontainers.Xunit;
+global using Xunit;
+global using Xunit.Sdk;
\ No newline at end of file