diff --git a/CommunityToolkit.Aspire.slnx b/CommunityToolkit.Aspire.slnx
index ed3837435..b0156934f 100644
--- a/CommunityToolkit.Aspire.slnx
+++ b/CommunityToolkit.Aspire.slnx
@@ -155,6 +155,9 @@
+
+
+
@@ -269,6 +272,7 @@
+
@@ -344,6 +348,7 @@
+
diff --git a/README.md b/README.md
index 3abed65fe..5c309b91d 100644
--- a/README.md
+++ b/README.md
@@ -30,6 +30,7 @@ This repository contains the source code for the Aspire Community Toolkit, a col
| - **Learn More**: [`Hosting.Bitwarden.SecretManager`][bitwarden-secret-manager-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Hosting.Bitwarden.SecretManager][bitwarden-secret-manager-hosting-shields]][bitwarden-secret-manager-hosting-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Hosting.Bitwarden.SecretManager][bitwarden-secret-manager-hosting-shields-preview]][bitwarden-secret-manager-hosting-nuget-preview] | A hosting integration for Bitwarden Secrets Manager projects and managed secrets. |
| - **Learn More**: [`Bitwarden.SecretManager`][bitwarden-secret-manager-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Bitwarden.SecretManager][bitwarden-secret-manager-client-shields]][bitwarden-secret-manager-client-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Bitwarden.SecretManager][bitwarden-secret-manager-client-shields-preview]][bitwarden-secret-manager-client-nuget-preview] | A client integration for authenticating and using the Bitwarden Secrets Manager SDK from Aspire applications. |
| - **Learn More**: [`Hosting.Perl`][perl-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Hosting.Perl][perl-shields]][perl-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Hosting.Perl][perl-shields-preview]][perl-nuget-preview] | A hosting integration for Perl scripts and APIs. |
+| - **Learn More**: [`Hosting.Posta`][posta-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Hosting.Posta][posta-shields]][posta-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Hosting.Posta][posta-shields-preview]][posta-nuget-preview] | An Aspire hosting integration for the [Posta](https://docs.goposta.dev/) self-hosted email delivery platform. |
| - **Learn More**: [`Hosting.Python.Extensions`][python-ext-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Python.Extensions][python-ext-shields]][python-ext-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Hosting.Python.Extensions][python-ext-shields-preview]][python-ext-nuget-preview] | An integration that contains some additional extensions for running python applications |
| - **Learn More**: [`Hosting.KurrentDB`][kurrentdb-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Hosting.KurrentDB][kurrentdb-shields]][kurrentdb-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Hosting.KurrentDB][kurrentdb-shields-preview]][kurrentdb-nuget-preview] | An Aspire hosting integration leveraging the [KurrentDB](https://www.kurrent.io) container. |
| - **Learn More**: [`KurrentDB`][kurrentdb-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.KurrentDB][kurrentdb-client-shields]][kurrentdb-client-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.KurrentDB][kurrentdb-client-shields-preview]][kurrentdb-client-nuget-preview] | An Aspire client integration for the [KurrentDB](https://github.com/kurrent-io/KurrentDB-Client-Dotnet) package. |
@@ -178,6 +179,11 @@ This project is supported by the [.NET Foundation](https://dotnetfoundation.org)
[perl-nuget]: https://nuget.org/packages/CommunityToolkit.Aspire.Hosting.Perl/
[perl-shields-preview]: https://img.shields.io/nuget/vpre/CommunityToolkit.Aspire.Hosting.Perl?label=nuget%20(preview)
[perl-nuget-preview]: https://nuget.org/packages/CommunityToolkit.Aspire.Hosting.Perl/absoluteLatest
+[posta-integration-docs]: ./src/CommunityToolkit.Aspire.Hosting.Posta/README.md
+[posta-shields]: https://img.shields.io/nuget/v/CommunityToolkit.Aspire.Hosting.Posta
+[posta-nuget]: https://nuget.org/packages/CommunityToolkit.Aspire.Hosting.Posta/
+[posta-shields-preview]: https://img.shields.io/nuget/vpre/CommunityToolkit.Aspire.Hosting.Posta?label=nuget%20(preview)
+[posta-nuget-preview]: https://nuget.org/packages/CommunityToolkit.Aspire.Hosting.Posta/absoluteLatest
[python-ext-integration-docs]: https://learn.microsoft.com/dotnet/aspire/community-toolkit/hosting-python-extensions
[python-ext-shields]: https://img.shields.io/nuget/v/CommunityToolkit.Aspire.Hosting.Python.Extensions
[python-ext-nuget]: https://nuget.org/packages/CommunityToolkit.Aspire.Hosting.Python.Extensions/
diff --git a/examples/posta/CommunityToolkit.Aspire.Hosting.Posta.AppHost/CommunityToolkit.Aspire.Hosting.Posta.AppHost.csproj b/examples/posta/CommunityToolkit.Aspire.Hosting.Posta.AppHost/CommunityToolkit.Aspire.Hosting.Posta.AppHost.csproj
new file mode 100644
index 000000000..8ab6b6f05
--- /dev/null
+++ b/examples/posta/CommunityToolkit.Aspire.Hosting.Posta.AppHost/CommunityToolkit.Aspire.Hosting.Posta.AppHost.csproj
@@ -0,0 +1,17 @@
+
+
+
+ Exe
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/posta/CommunityToolkit.Aspire.Hosting.Posta.AppHost/Program.cs b/examples/posta/CommunityToolkit.Aspire.Hosting.Posta.AppHost/Program.cs
new file mode 100644
index 000000000..50c3cce45
--- /dev/null
+++ b/examples/posta/CommunityToolkit.Aspire.Hosting.Posta.AppHost/Program.cs
@@ -0,0 +1,10 @@
+var builder = DistributedApplication.CreateBuilder(args);
+
+var postgresPassword = builder.AddParameter("postgres-password", "posta");
+var postgres = builder.AddPostgres("postgres", password: postgresPassword);
+var database = postgres.AddDatabase("posta-db", "posta");
+var redis = builder.AddRedis("redis");
+
+builder.AddPosta("posta", database, redis);
+
+builder.Build().Run();
diff --git a/examples/posta/CommunityToolkit.Aspire.Hosting.Posta.AppHost/Properties/launchSettings.json b/examples/posta/CommunityToolkit.Aspire.Hosting.Posta.AppHost/Properties/launchSettings.json
new file mode 100644
index 000000000..0a6a02a6a
--- /dev/null
+++ b/examples/posta/CommunityToolkit.Aspire.Hosting.Posta.AppHost/Properties/launchSettings.json
@@ -0,0 +1,29 @@
+{
+ "$schema": "https://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "https": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "https://localhost:18131;http://localhost:12049",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "DOTNET_ENVIRONMENT": "Development",
+ "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:32487",
+ "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:12811"
+ }
+ },
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "http://localhost:15184",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "DOTNET_ENVIRONMENT": "Development",
+ "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19198",
+ "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:21263"
+ }
+ }
+ }
+}
diff --git a/src/CommunityToolkit.Aspire.Hosting.Posta/CommunityToolkit.Aspire.Hosting.Posta.csproj b/src/CommunityToolkit.Aspire.Hosting.Posta/CommunityToolkit.Aspire.Hosting.Posta.csproj
new file mode 100644
index 000000000..cc997daa8
--- /dev/null
+++ b/src/CommunityToolkit.Aspire.Hosting.Posta/CommunityToolkit.Aspire.Hosting.Posta.csproj
@@ -0,0 +1,19 @@
+
+
+
+ An Aspire hosting integration for Posta.
+ hosting posta email smtp delivery
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/CommunityToolkit.Aspire.Hosting.Posta/PostaContainerImageTags.cs b/src/CommunityToolkit.Aspire.Hosting.Posta/PostaContainerImageTags.cs
new file mode 100644
index 000000000..8c5478d1e
--- /dev/null
+++ b/src/CommunityToolkit.Aspire.Hosting.Posta/PostaContainerImageTags.cs
@@ -0,0 +1,13 @@
+namespace CommunityToolkit.Aspire.Hosting.Posta;
+
+internal static class PostaContainerImageTags
+{
+ /// docker.io
+ public const string Registry = "docker.io";
+
+ /// jkaninda/posta
+ public const string Image = "jkaninda/posta";
+
+ /// 0.11.0
+ public const string Tag = "0.11.0";
+}
\ No newline at end of file
diff --git a/src/CommunityToolkit.Aspire.Hosting.Posta/PostaEmailVerificationOptions.cs b/src/CommunityToolkit.Aspire.Hosting.Posta/PostaEmailVerificationOptions.cs
new file mode 100644
index 000000000..41b7d0ebc
--- /dev/null
+++ b/src/CommunityToolkit.Aspire.Hosting.Posta/PostaEmailVerificationOptions.cs
@@ -0,0 +1,39 @@
+using Aspire.Hosting.ApplicationModel;
+
+namespace CommunityToolkit.Aspire.Hosting.Posta;
+
+///
+/// Parameter-based configuration options for Posta email verification.
+///
+public sealed class PostaEmailVerificationOptions
+{
+ ///
+ /// Gets or sets whether users must verify their email address before sign-in.
+ ///
+ public IResourceBuilder? Required { get; set; }
+
+ ///
+ /// Gets or sets whether permanently rejected recipients are added to the suppression list.
+ ///
+ public IResourceBuilder? AutoSuppressOnReject { get; set; }
+
+ ///
+ /// Gets or sets whether the email verification endpoint is enabled.
+ ///
+ public IResourceBuilder? Enabled { get; set; }
+
+ ///
+ /// Gets or sets how long address-level verification results are cached, in hours.
+ ///
+ public IResourceBuilder? CacheTtlHours { get; set; }
+
+ ///
+ /// Gets or sets how long domain MX lookups are cached, in hours.
+ ///
+ public IResourceBuilder? MxCacheTtlHours { get; set; }
+
+ ///
+ /// Gets or sets the per-user hourly cap on verification requests.
+ ///
+ public IResourceBuilder? RateHourly { get; set; }
+}
diff --git a/src/CommunityToolkit.Aspire.Hosting.Posta/PostaEnvironmentVariables.cs b/src/CommunityToolkit.Aspire.Hosting.Posta/PostaEnvironmentVariables.cs
new file mode 100644
index 000000000..24491fb0a
--- /dev/null
+++ b/src/CommunityToolkit.Aspire.Hosting.Posta/PostaEnvironmentVariables.cs
@@ -0,0 +1,74 @@
+namespace CommunityToolkit.Aspire.Hosting.Posta;
+
+internal static class PostaEnvironmentVariables
+{
+ public const string Port = "POSTA_PORT";
+ public const string DbUrl = "POSTA_DB_URL";
+ public const string DbHost = "POSTA_DB_HOST";
+ public const string DbPort = "POSTA_DB_PORT";
+ public const string DbUser = "POSTA_DB_USER";
+ public const string DbPassword = "POSTA_DB_PASSWORD";
+ public const string DbName = "POSTA_DB_NAME";
+ public const string DbSslMode = "POSTA_DB_SSL_MODE";
+ public const string RedisAddress = "POSTA_REDIS_ADDR";
+ public const string RedisPassword = "POSTA_REDIS_PASSWORD";
+ public const string JwtSecret = "POSTA_JWT_SECRET";
+ public const string AdminEmail = "POSTA_ADMIN_EMAIL";
+ public const string AdminPassword = "POSTA_ADMIN_PASSWORD";
+ public const string Environment = "POSTA_ENV";
+ public const string DevMode = "POSTA_DEV_MODE";
+ public const string AuthRateLimitEnabled = "POSTA_AUTH_RATE_LIMIT_ENABLED";
+ public const string RateLimitHourly = "POSTA_RATE_LIMIT_HOURLY";
+ public const string RateLimitDaily = "POSTA_RATE_LIMIT_DAILY";
+ public const string OpenApiDocs = "POSTA_OPENAPI_DOCS";
+ public const string MetricsEnabled = "POSTA_METRICS_ENABLED";
+ public const string WebDir = "POSTA_WEB_DIR";
+ public const string WebUrl = "POSTA_WEB_URL";
+ public const string ApiUrl = "POSTA_API_URL";
+ public const string CorsOrigins = "POSTA_CORS_ORIGINS";
+ public const string EmbeddedWorker = "POSTA_EMBEDDED_WORKER";
+ public const string WorkerConcurrency = "POSTA_WORKER_CONCURRENCY";
+ public const string WorkerMaxRetries = "POSTA_WORKER_MAX_RETRIES";
+ public const string WebhookMaxRetries = "POSTA_WEBHOOK_MAX_RETRIES";
+ public const string WebhookTimeoutSeconds = "POSTA_WEBHOOK_TIMEOUT_SECS";
+ public const string WebhookProxyUrl = "POSTA_WEBHOOK_PROXY_URL";
+ public const string GoogleOAuthClientId = "POSTA_GOOGLE_OAUTH_CLIENT_ID";
+ public const string GoogleOAuthClientSecret = "POSTA_GOOGLE_OAUTH_CLIENT_SECRET";
+ public const string OAuthCallbackUrl = "POSTA_OAUTH_CALLBACK_URL";
+ public const string BlobProvider = "POSTA_BLOB_PROVIDER";
+ public const string BlobS3Endpoint = "POSTA_BLOB_S3_ENDPOINT";
+ public const string BlobS3Region = "POSTA_BLOB_S3_REGION";
+ public const string BlobS3Bucket = "POSTA_BLOB_S3_BUCKET";
+ public const string BlobS3AccessKey = "POSTA_BLOB_S3_ACCESS_KEY";
+ public const string BlobS3SecretKey = "POSTA_BLOB_S3_SECRET_KEY";
+ public const string BlobS3UseSsl = "POSTA_BLOB_S3_USE_SSL";
+ public const string BlobS3PathStyle = "POSTA_BLOB_S3_PATH_STYLE";
+ public const string BlobFileSystemPath = "POSTA_BLOB_FS_PATH";
+ public const string EncryptionKey = "POSTA_ENCRYPTION_KEY";
+ public const string SystemSmtpHost = "POSTA_SYSTEM_SMTP_HOST";
+ public const string SystemSmtpPort = "POSTA_SYSTEM_SMTP_PORT";
+ public const string SystemSmtpUsername = "POSTA_SYSTEM_SMTP_USERNAME";
+ public const string SystemSmtpPassword = "POSTA_SYSTEM_SMTP_PASSWORD";
+ public const string SystemSmtpFrom = "POSTA_SYSTEM_SMTP_FROM";
+ public const string SystemSmtpEncryption = "POSTA_SYSTEM_SMTP_ENCRYPTION";
+ public const string InboundEnabled = "POSTA_INBOUND_ENABLED";
+ public const string InboundSmtpHost = "POSTA_INBOUND_SMTP_HOST";
+ public const string InboundSmtpPort = "POSTA_INBOUND_SMTP_PORT";
+ public const string InboundHostname = "POSTA_INBOUND_HOSTNAME";
+ public const string InboundMaxMessageSize = "POSTA_INBOUND_MAX_MESSAGE_SIZE";
+ public const string InboundMaxAttachSize = "POSTA_INBOUND_MAX_ATTACH_SIZE";
+ public const string InboundWebhookSecret = "POSTA_INBOUND_WEBHOOK_SECRET";
+ public const string InboundTlsMode = "POSTA_INBOUND_TLS_MODE";
+ public const string InboundTlsCertFile = "POSTA_INBOUND_TLS_CERT_FILE";
+ public const string InboundTlsKeyFile = "POSTA_INBOUND_TLS_KEY_FILE";
+ public const string InboundSmtpRateLimit = "POSTA_INBOUND_SMTP_RATE_LIMIT";
+ public const string InboundSmtpRateWindow = "POSTA_INBOUND_SMTP_RATE_WINDOW";
+ public const string EmailVerificationRequired = "POSTA_EMAIL_VERIFICATION_REQUIRED";
+ public const string AutoSuppressOnReject = "POSTA_AUTO_SUPPRESS_ON_REJECT";
+ public const string EmailVerifyEnabled = "POSTA_EMAIL_VERIFY_ENABLED";
+ public const string EmailVerifyCacheTtlHours = "POSTA_EMAIL_VERIFY_CACHE_TTL_HOURS";
+ public const string EmailVerifyMxCacheTtlHours = "POSTA_EMAIL_VERIFY_MX_CACHE_TTL_HOURS";
+ public const string EmailVerifyRateHourly = "POSTA_EMAIL_VERIFY_RATE_HOURLY";
+ public const string AllowDowngrade = "POSTA_ALLOW_DOWNGRADE";
+ public const string PlanEnforcement = "POSTA_PLAN_ENFORCEMENT";
+}
diff --git a/src/CommunityToolkit.Aspire.Hosting.Posta/PostaGoogleOAuthOptions.cs b/src/CommunityToolkit.Aspire.Hosting.Posta/PostaGoogleOAuthOptions.cs
new file mode 100644
index 000000000..f6067e293
--- /dev/null
+++ b/src/CommunityToolkit.Aspire.Hosting.Posta/PostaGoogleOAuthOptions.cs
@@ -0,0 +1,24 @@
+using Aspire.Hosting.ApplicationModel;
+
+namespace CommunityToolkit.Aspire.Hosting.Posta;
+
+///
+/// Parameter-based configuration options for Posta Google OAuth login.
+///
+public sealed class PostaGoogleOAuthOptions
+{
+ ///
+ /// Gets or sets the Google OAuth client ID for SSO login.
+ ///
+ public IResourceBuilder? ClientId { get; set; }
+
+ ///
+ /// Gets or sets the Google OAuth client secret for SSO login.
+ ///
+ public IResourceBuilder? ClientSecret { get; set; }
+
+ ///
+ /// Gets or sets the OAuth callback base URL.
+ ///
+ public IResourceBuilder? CallbackUrl { get; set; }
+}
diff --git a/src/CommunityToolkit.Aspire.Hosting.Posta/PostaHostingExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Posta/PostaHostingExtensions.cs
new file mode 100644
index 000000000..10ddc7fff
--- /dev/null
+++ b/src/CommunityToolkit.Aspire.Hosting.Posta/PostaHostingExtensions.cs
@@ -0,0 +1,614 @@
+using Aspire.Hosting.ApplicationModel;
+using CommunityToolkit.Aspire.Hosting.Posta;
+
+namespace Aspire.Hosting;
+
+///
+/// Provides extension methods for adding Posta to an .
+///
+public static class PostaHostingExtensions
+{
+ ///
+ /// Adds a Posta container resource to the and configures PostgreSQL and Redis references.
+ ///
+ /// Adds a Posta container resource with PostgreSQL and Redis references
+ /// The to which the Posta resource will be added.
+ /// The name of the Posta container resource.
+ /// The PostgreSQL database resource used by Posta.
+ /// The Redis resource used by Posta.
+ /// Optional parameter used as the Posta JWT signing secret.
+ /// Optional parameter used as the initial Posta admin password.
+ /// The initial Posta admin account email.
+ /// Optional host port for the Posta HTTP API and dashboard.
+ /// A reference to the for further resource configuration.
+ [AspireExport("addPostaWithReferences", MethodName = "addPostaWithReferences")]
+ public static IResourceBuilder AddPosta(
+ this IDistributedApplicationBuilder builder,
+ [ResourceName] string name,
+ IResourceBuilder database,
+ IResourceBuilder redis,
+ IResourceBuilder? jwtSecret = null,
+ IResourceBuilder? adminPassword = null,
+ string adminEmail = "admin@example.com",
+ int? port = null)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+ ArgumentNullException.ThrowIfNull(database);
+ ArgumentNullException.ThrowIfNull(redis);
+
+ return AddPostaCore(builder, name, jwtSecret, adminPassword, adminEmail, port, options: null)
+ .WithReference(database)
+ .WithReference(redis);
+ }
+
+ ///
+ /// Adds a Posta container resource to the with PostgreSQL, Redis, and additional Posta environment configuration.
+ ///
+ /// The to which the Posta resource will be added.
+ /// The name of the Posta container resource.
+ /// The PostgreSQL database resource used by Posta.
+ /// The Redis resource used by Posta.
+ /// The Posta environment configuration.
+ /// Optional parameter used as the Posta JWT signing secret.
+ /// Optional parameter used as the initial Posta admin password.
+ /// The initial Posta admin account email.
+ /// Optional host port for the Posta HTTP API and dashboard.
+ /// A reference to the for further resource configuration.
+ [AspireExportIgnore(Reason = "PostaOptions contains parameter builders for secret values and is not supported in polyglot app hosts.")]
+ public static IResourceBuilder AddPosta(
+ this IDistributedApplicationBuilder builder,
+ [ResourceName] string name,
+ IResourceBuilder database,
+ IResourceBuilder redis,
+ PostaOptions options,
+ IResourceBuilder? jwtSecret = null,
+ IResourceBuilder? adminPassword = null,
+ string adminEmail = "admin@example.com",
+ int? port = null)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+ ArgumentNullException.ThrowIfNull(database);
+ ArgumentNullException.ThrowIfNull(redis);
+ ArgumentNullException.ThrowIfNull(options);
+
+ return AddPostaCore(builder, name, jwtSecret, adminPassword, adminEmail, port, options)
+ .WithReference(database)
+ .WithReference(redis);
+ }
+
+ ///
+ /// Adds a Posta container resource to the with PostgreSQL, Redis, and additional Posta environment configuration.
+ ///
+ /// The to which the Posta resource will be added.
+ /// The name of the Posta container resource.
+ /// The PostgreSQL database resource used by Posta.
+ /// The Redis resource used by Posta.
+ /// A delegate that configures Posta environment variables.
+ /// Optional parameter used as the Posta JWT signing secret.
+ /// Optional parameter used as the initial Posta admin password.
+ /// The initial Posta admin account email.
+ /// Optional host port for the Posta HTTP API and dashboard.
+ /// A reference to the for further resource configuration.
+ [AspireExportIgnore(Reason = "Action is not supported in polyglot app hosts. Use the options object overload instead.")]
+ public static IResourceBuilder AddPosta(
+ this IDistributedApplicationBuilder builder,
+ [ResourceName] string name,
+ IResourceBuilder database,
+ IResourceBuilder redis,
+ Action configureOptions,
+ IResourceBuilder? jwtSecret = null,
+ IResourceBuilder? adminPassword = null,
+ string adminEmail = "admin@example.com",
+ int? port = null)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+ ArgumentNullException.ThrowIfNull(database);
+ ArgumentNullException.ThrowIfNull(redis);
+ ArgumentNullException.ThrowIfNull(configureOptions);
+
+ var options = new PostaOptions();
+ configureOptions.Invoke(options);
+
+ return builder.AddPosta(name, database, redis, options, jwtSecret, adminPassword, adminEmail, port);
+ }
+
+ ///
+ /// Configures the PostgreSQL database used by Posta.
+ ///
+ /// The Posta resource builder.
+ /// The PostgreSQL database resource used by Posta.
+ /// A reference to the for further resource configuration.
+ [AspireExport("withPostgresReference", MethodName = "withPostgresReference")]
+ public static IResourceBuilder WithReference(
+ this IResourceBuilder builder,
+ IResourceBuilder database)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+ ArgumentNullException.ThrowIfNull(database);
+
+ return builder
+ .WithEnvironment(context =>
+ {
+ var postgres = database.Resource.Parent;
+
+ Set(context, PostaEnvironmentVariables.DbHost, postgres.PrimaryEndpoint.Property(EndpointProperty.Host));
+ Set(context, PostaEnvironmentVariables.DbPort, postgres.PrimaryEndpoint.Property(EndpointProperty.Port));
+ Set(context, PostaEnvironmentVariables.DbUser, postgres.UserNameReference);
+ Set(context, PostaEnvironmentVariables.DbPassword, postgres.PasswordParameter);
+ Set(context, PostaEnvironmentVariables.DbName, database.Resource.DatabaseName);
+ Set(context, PostaEnvironmentVariables.DbSslMode, "disable");
+ })
+ .WaitFor(database);
+ }
+
+ ///
+ /// Configures the Redis server used by Posta.
+ ///
+ /// The Posta resource builder.
+ /// The Redis resource used by Posta.
+ /// Optional Redis password parameter that overrides the referenced Redis resource password.
+ /// A reference to the for further resource configuration.
+ [AspireExport("withRedisReference", MethodName = "withRedisReference")]
+ public static IResourceBuilder WithReference(
+ this IResourceBuilder builder,
+ IResourceBuilder redis,
+ IResourceBuilder? redisPassword = null)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+ ArgumentNullException.ThrowIfNull(redis);
+
+ return builder
+ .WithEnvironment(context =>
+ {
+ var redisResource = redis.Resource;
+ var redisEndpoint = redisResource.GetEndpoint("secondary");
+
+ SetIfAbsent(context, PostaEnvironmentVariables.RedisAddress, ReferenceExpression.Create($"{redisEndpoint.Property(EndpointProperty.Host)}:{redisEndpoint.Property(EndpointProperty.Port)}"));
+ if (redisPassword is not null)
+ {
+ Set(context, PostaEnvironmentVariables.RedisPassword, redisPassword.Resource);
+ }
+ else if (redisResource.PasswordParameter is not null)
+ {
+ SetIfAbsent(context, PostaEnvironmentVariables.RedisPassword, redisResource.PasswordParameter);
+ }
+ })
+ .WaitFor(redis);
+ }
+
+ ///
+ /// Configures the system SMTP server used by Posta for platform notifications.
+ ///
+ /// The Posta resource builder.
+ /// The parameter-based system SMTP configuration.
+ /// A reference to the for further resource configuration.
+ [AspireExportIgnore(Reason = "PostaSystemSmtpOptions contains parameter builders and is intended for C# AppHosts.")]
+ public static IResourceBuilder WithSystemSmtp(
+ this IResourceBuilder builder,
+ PostaSystemSmtpOptions options)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+ ArgumentNullException.ThrowIfNull(options);
+
+ return builder.WithEnvironment(context => ConfigureSystemSmtp(context, options));
+ }
+
+ ///
+ /// Configures the system SMTP server used by Posta for platform notifications.
+ ///
+ /// The Posta resource builder.
+ /// A delegate that configures the parameter-based system SMTP options.
+ /// A reference to the for further resource configuration.
+ [AspireExportIgnore(Reason = "Action is not supported in polyglot app hosts. Use the options object overload instead.")]
+ public static IResourceBuilder WithSystemSmtp(
+ this IResourceBuilder builder,
+ Action configureOptions)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+ ArgumentNullException.ThrowIfNull(configureOptions);
+
+ var options = new PostaSystemSmtpOptions();
+ configureOptions.Invoke(options);
+
+ return builder.WithSystemSmtp(options);
+ }
+
+ ///
+ /// Configures the built-in inbound SMTP receiver used by Posta.
+ ///
+ /// The Posta resource builder.
+ /// The parameter-based inbound SMTP configuration.
+ /// A reference to the for further resource configuration.
+ [AspireExportIgnore(Reason = "PostaInboundSmtpOptions contains parameter builders and is intended for C# AppHosts.")]
+ public static IResourceBuilder WithInboundSmtp(
+ this IResourceBuilder builder,
+ PostaInboundSmtpOptions options)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+ ArgumentNullException.ThrowIfNull(options);
+
+ return builder.WithEnvironment(context => ConfigureInboundSmtp(context, options));
+ }
+
+ ///
+ /// Configures the built-in inbound SMTP receiver used by Posta.
+ ///
+ /// The Posta resource builder.
+ /// A delegate that configures the parameter-based inbound SMTP options.
+ /// A reference to the for further resource configuration.
+ [AspireExportIgnore(Reason = "Action is not supported in polyglot app hosts. Use the options object overload instead.")]
+ public static IResourceBuilder WithInboundSmtp(
+ this IResourceBuilder builder,
+ Action configureOptions)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+ ArgumentNullException.ThrowIfNull(configureOptions);
+
+ var options = new PostaInboundSmtpOptions();
+ configureOptions.Invoke(options);
+
+ return builder.WithInboundSmtp(options);
+ }
+
+ ///
+ /// Configures S3-compatible attachment storage for Posta.
+ ///
+ /// The Posta resource builder.
+ /// The parameter-based S3 blob storage configuration.
+ /// A reference to the for further resource configuration.
+ [AspireExportIgnore(Reason = "PostaS3BlobStorageOptions contains parameter builders and is intended for C# AppHosts.")]
+ public static IResourceBuilder WithS3BlobStorage(
+ this IResourceBuilder builder,
+ PostaS3BlobStorageOptions options)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+ ArgumentNullException.ThrowIfNull(options);
+
+ return builder.WithEnvironment(context => ConfigureS3BlobStorage(context, options));
+ }
+
+ ///
+ /// Configures S3-compatible attachment storage for Posta.
+ ///
+ /// The Posta resource builder.
+ /// A delegate that configures the parameter-based S3 blob storage options.
+ /// A reference to the for further resource configuration.
+ [AspireExportIgnore(Reason = "Action is not supported in polyglot app hosts. Use the options object overload instead.")]
+ public static IResourceBuilder WithS3BlobStorage(
+ this IResourceBuilder builder,
+ Action configureOptions)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+ ArgumentNullException.ThrowIfNull(configureOptions);
+
+ var options = new PostaS3BlobStorageOptions();
+ configureOptions.Invoke(options);
+
+ return builder.WithS3BlobStorage(options);
+ }
+
+ ///
+ /// Configures Google OAuth login for Posta.
+ ///
+ /// The Posta resource builder.
+ /// The parameter-based Google OAuth configuration.
+ /// A reference to the for further resource configuration.
+ [AspireExportIgnore(Reason = "PostaGoogleOAuthOptions contains parameter builders and is intended for C# AppHosts.")]
+ public static IResourceBuilder WithGoogleOAuth(
+ this IResourceBuilder builder,
+ PostaGoogleOAuthOptions options)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+ ArgumentNullException.ThrowIfNull(options);
+
+ return builder.WithEnvironment(context => ConfigureGoogleOAuth(context, options));
+ }
+
+ ///
+ /// Configures Google OAuth login for Posta.
+ ///
+ /// The Posta resource builder.
+ /// A delegate that configures the parameter-based Google OAuth options.
+ /// A reference to the for further resource configuration.
+ [AspireExportIgnore(Reason = "Action is not supported in polyglot app hosts. Use the options object overload instead.")]
+ public static IResourceBuilder WithGoogleOAuth(
+ this IResourceBuilder builder,
+ Action configureOptions)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+ ArgumentNullException.ThrowIfNull(configureOptions);
+
+ var options = new PostaGoogleOAuthOptions();
+ configureOptions.Invoke(options);
+
+ return builder.WithGoogleOAuth(options);
+ }
+
+ ///
+ /// Configures email verification behavior for Posta.
+ ///
+ /// The Posta resource builder.
+ /// The parameter-based email verification configuration.
+ /// A reference to the for further resource configuration.
+ [AspireExportIgnore(Reason = "PostaEmailVerificationOptions contains parameter builders and is intended for C# AppHosts.")]
+ public static IResourceBuilder WithEmailVerification(
+ this IResourceBuilder builder,
+ PostaEmailVerificationOptions options)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+ ArgumentNullException.ThrowIfNull(options);
+
+ return builder.WithEnvironment(context => ConfigureEmailVerification(context, options));
+ }
+
+ ///
+ /// Configures email verification behavior for Posta.
+ ///
+ /// The Posta resource builder.
+ /// A delegate that configures the parameter-based email verification options.
+ /// A reference to the for further resource configuration.
+ [AspireExportIgnore(Reason = "Action is not supported in polyglot app hosts. Use the options object overload instead.")]
+ public static IResourceBuilder WithEmailVerification(
+ this IResourceBuilder builder,
+ Action configureOptions)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+ ArgumentNullException.ThrowIfNull(configureOptions);
+
+ var options = new PostaEmailVerificationOptions();
+ configureOptions.Invoke(options);
+
+ return builder.WithEmailVerification(options);
+ }
+
+ private static IResourceBuilder AddPostaCore(
+ IDistributedApplicationBuilder builder,
+ string name,
+ IResourceBuilder? jwtSecret,
+ IResourceBuilder? adminPassword,
+ string adminEmail,
+ int? port,
+ PostaOptions? options)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+ ArgumentException.ThrowIfNullOrWhiteSpace(name);
+ ArgumentException.ThrowIfNullOrWhiteSpace(adminEmail);
+
+ options ??= new PostaOptions();
+
+ var jwtSecretParameter = jwtSecret?.Resource ??
+ ParameterResourceBuilderExtensions.CreateDefaultPasswordParameter(builder, $"{name}-jwt-secret", minLower: 1, minUpper: 1, minNumeric: 1);
+ var adminPasswordParameter = adminPassword?.Resource ??
+ ParameterResourceBuilderExtensions.CreateDefaultPasswordParameter(builder, $"{name}-admin-password", minLower: 1, minUpper: 1, minNumeric: 1);
+
+ var resource = new PostaResource(name, jwtSecretParameter, adminPasswordParameter);
+
+ return builder.AddResource(resource)
+ .WithImage(PostaContainerImageTags.Image)
+ .WithImageTag(PostaContainerImageTags.Tag)
+ .WithImageRegistry(PostaContainerImageTags.Registry)
+ .WithHttpEndpoint(
+ targetPort: PostaResource.HttpEndpointPort,
+ port: port,
+ name: PostaResource.HttpEndpointName)
+ .WithEnvironment(context =>
+ {
+ Set(context, PostaEnvironmentVariables.Port, PostaResource.HttpEndpointPort);
+ SetParameter(context, PostaEnvironmentVariables.DbUrl, options.DatabaseUrl);
+ SetIfNotNull(context, PostaEnvironmentVariables.RedisAddress, options.RedisAddress);
+ if (options.RedisPassword is not null)
+ {
+ Set(context, PostaEnvironmentVariables.RedisPassword, options.RedisPassword.Resource);
+ }
+ Set(context, PostaEnvironmentVariables.JwtSecret, resource.JwtSecretParameter);
+ Set(context, PostaEnvironmentVariables.AdminEmail, adminEmail);
+ Set(context, PostaEnvironmentVariables.AdminPassword, resource.AdminPasswordParameter);
+ ConfigurePostaEnvironment(context, options);
+ })
+ .WithHttpHealthCheck(
+ path: "/healthz",
+ statusCode: 200,
+ endpointName: PostaResource.HttpEndpointName)
+ .WithHttpHealthCheck(
+ path: "/readyz",
+ statusCode: 200,
+ endpointName: PostaResource.HttpEndpointName)
+ .WithUrlForEndpoint(PostaResource.HttpEndpointName, url => url.DisplayText = "Posta Dashboard");
+ }
+
+ private static void ConfigurePostaEnvironment(EnvironmentCallbackContext context, PostaOptions options)
+ {
+ Set(context, PostaEnvironmentVariables.Environment, options.Environment);
+ Set(context, PostaEnvironmentVariables.DevMode, options.DevMode);
+ Set(context, PostaEnvironmentVariables.AuthRateLimitEnabled, options.AuthRateLimitEnabled);
+ Set(context, PostaEnvironmentVariables.RateLimitHourly, options.RateLimitHourly);
+ Set(context, PostaEnvironmentVariables.RateLimitDaily, options.RateLimitDaily);
+ Set(context, PostaEnvironmentVariables.OpenApiDocs, options.OpenApiDocs);
+ Set(context, PostaEnvironmentVariables.MetricsEnabled, options.MetricsEnabled);
+ SetIfNotNull(context, PostaEnvironmentVariables.WebDir, options.WebDir);
+ SetIfNotNull(context, PostaEnvironmentVariables.WebUrl, options.WebUrl);
+ SetIfNotNull(context, PostaEnvironmentVariables.ApiUrl, options.ApiUrl);
+ Set(context, PostaEnvironmentVariables.CorsOrigins, options.CorsOrigins);
+ Set(context, PostaEnvironmentVariables.EmbeddedWorker, options.EmbeddedWorker);
+ Set(context, PostaEnvironmentVariables.WorkerConcurrency, options.WorkerConcurrency);
+ Set(context, PostaEnvironmentVariables.WorkerMaxRetries, options.WorkerMaxRetries);
+ Set(context, PostaEnvironmentVariables.WebhookMaxRetries, options.WebhookMaxRetries);
+ Set(context, PostaEnvironmentVariables.WebhookTimeoutSeconds, options.WebhookTimeoutSeconds);
+ SetIfNotNull(context, PostaEnvironmentVariables.WebhookProxyUrl, options.WebhookProxyUrl);
+ ConfigureGoogleOAuth(context, options);
+ ConfigureBlobStorage(context, options);
+ SetParameter(context, PostaEnvironmentVariables.EncryptionKey, options.EncryptionKey);
+ ConfigureSystemSmtp(context, options);
+ ConfigureInboundSmtp(context, options);
+ ConfigureEmailVerification(context, options);
+ Set(context, PostaEnvironmentVariables.AllowDowngrade, options.AllowDowngrade);
+ Set(context, PostaEnvironmentVariables.PlanEnforcement, options.PlanEnforcement);
+ }
+
+ private static void ConfigureGoogleOAuth(EnvironmentCallbackContext context, PostaOptions options)
+ {
+ SetIfNotNull(context, PostaEnvironmentVariables.GoogleOAuthClientId, options.GoogleOAuthClientId);
+ SetParameter(context, PostaEnvironmentVariables.GoogleOAuthClientSecret, options.GoogleOAuthClientSecret);
+ SetIfNotNull(context, PostaEnvironmentVariables.OAuthCallbackUrl, options.OAuthCallbackUrl);
+ }
+
+ private static void ConfigureGoogleOAuth(EnvironmentCallbackContext context, PostaGoogleOAuthOptions options)
+ {
+ SetParameter(context, PostaEnvironmentVariables.GoogleOAuthClientId, options.ClientId);
+ SetParameter(context, PostaEnvironmentVariables.GoogleOAuthClientSecret, options.ClientSecret);
+ SetParameter(context, PostaEnvironmentVariables.OAuthCallbackUrl, options.CallbackUrl);
+ }
+
+ private static void ConfigureBlobStorage(EnvironmentCallbackContext context, PostaOptions options)
+ {
+ SetIfNotNull(context, PostaEnvironmentVariables.BlobProvider, options.BlobProvider);
+ SetIfNotNull(context, PostaEnvironmentVariables.BlobS3Endpoint, options.BlobS3Endpoint);
+ Set(context, PostaEnvironmentVariables.BlobS3Region, options.BlobS3Region);
+ SetIfNotNull(context, PostaEnvironmentVariables.BlobS3Bucket, options.BlobS3Bucket);
+ SetParameter(context, PostaEnvironmentVariables.BlobS3AccessKey, options.BlobS3AccessKey);
+ SetParameter(context, PostaEnvironmentVariables.BlobS3SecretKey, options.BlobS3SecretKey);
+ Set(context, PostaEnvironmentVariables.BlobS3UseSsl, options.BlobS3UseSsl);
+ Set(context, PostaEnvironmentVariables.BlobS3PathStyle, options.BlobS3PathStyle);
+ Set(context, PostaEnvironmentVariables.BlobFileSystemPath, options.BlobFileSystemPath);
+ }
+
+ private static void ConfigureS3BlobStorage(EnvironmentCallbackContext context, PostaS3BlobStorageOptions options)
+ {
+ Set(context, PostaEnvironmentVariables.BlobProvider, "s3");
+ SetParameter(context, PostaEnvironmentVariables.BlobS3Endpoint, options.Endpoint);
+ SetParameter(context, PostaEnvironmentVariables.BlobS3Region, options.Region);
+ SetParameter(context, PostaEnvironmentVariables.BlobS3Bucket, options.Bucket);
+ SetParameter(context, PostaEnvironmentVariables.BlobS3AccessKey, options.AccessKey);
+ SetParameter(context, PostaEnvironmentVariables.BlobS3SecretKey, options.SecretKey);
+ SetParameter(context, PostaEnvironmentVariables.BlobS3UseSsl, options.UseSsl);
+ SetParameter(context, PostaEnvironmentVariables.BlobS3PathStyle, options.PathStyle);
+ }
+
+ private static void ConfigureSystemSmtp(EnvironmentCallbackContext context, PostaOptions options)
+ {
+ SetIfNotNull(context, PostaEnvironmentVariables.SystemSmtpHost, options.SystemSmtpHost);
+ Set(context, PostaEnvironmentVariables.SystemSmtpPort, options.SystemSmtpPort);
+ SetIfNotNull(context, PostaEnvironmentVariables.SystemSmtpUsername, options.SystemSmtpUsername);
+ SetParameter(context, PostaEnvironmentVariables.SystemSmtpPassword, options.SystemSmtpPassword);
+ SetIfNotNull(context, PostaEnvironmentVariables.SystemSmtpFrom, options.SystemSmtpFrom);
+ Set(context, PostaEnvironmentVariables.SystemSmtpEncryption, options.SystemSmtpEncryption);
+ }
+
+ private static void ConfigureSystemSmtp(EnvironmentCallbackContext context, PostaSystemSmtpOptions options)
+ {
+ SetParameter(context, PostaEnvironmentVariables.SystemSmtpHost, options.Host);
+ SetParameter(context, PostaEnvironmentVariables.SystemSmtpPort, options.Port);
+ SetParameter(context, PostaEnvironmentVariables.SystemSmtpUsername, options.Username);
+ SetParameter(context, PostaEnvironmentVariables.SystemSmtpPassword, options.Password);
+ SetParameter(context, PostaEnvironmentVariables.SystemSmtpFrom, options.From);
+ SetParameter(context, PostaEnvironmentVariables.SystemSmtpEncryption, options.Encryption);
+ }
+
+ private static void ConfigureInboundSmtp(EnvironmentCallbackContext context, PostaOptions options)
+ {
+ Set(context, PostaEnvironmentVariables.InboundEnabled, options.InboundEnabled);
+ Set(context, PostaEnvironmentVariables.InboundSmtpHost, options.InboundSmtpHost);
+ Set(context, PostaEnvironmentVariables.InboundSmtpPort, options.InboundSmtpPort);
+ Set(context, PostaEnvironmentVariables.InboundHostname, options.InboundHostname);
+ Set(context, PostaEnvironmentVariables.InboundMaxMessageSize, options.InboundMaxMessageSize);
+ Set(context, PostaEnvironmentVariables.InboundMaxAttachSize, options.InboundMaxAttachSize);
+ SetParameter(context, PostaEnvironmentVariables.InboundWebhookSecret, options.InboundWebhookSecret);
+ Set(context, PostaEnvironmentVariables.InboundTlsMode, options.InboundTlsMode);
+ SetIfNotNull(context, PostaEnvironmentVariables.InboundTlsCertFile, options.InboundTlsCertFile);
+ SetIfNotNull(context, PostaEnvironmentVariables.InboundTlsKeyFile, options.InboundTlsKeyFile);
+ Set(context, PostaEnvironmentVariables.InboundSmtpRateLimit, options.InboundSmtpRateLimit);
+ Set(context, PostaEnvironmentVariables.InboundSmtpRateWindow, options.InboundSmtpRateWindow);
+ }
+
+ private static void ConfigureInboundSmtp(EnvironmentCallbackContext context, PostaInboundSmtpOptions options)
+ {
+ SetParameter(context, PostaEnvironmentVariables.InboundEnabled, options.Enabled);
+ SetParameter(context, PostaEnvironmentVariables.InboundSmtpHost, options.Host);
+ SetParameter(context, PostaEnvironmentVariables.InboundSmtpPort, options.Port);
+ SetParameter(context, PostaEnvironmentVariables.InboundHostname, options.Hostname);
+ SetParameter(context, PostaEnvironmentVariables.InboundMaxMessageSize, options.MaxMessageSize);
+ SetParameter(context, PostaEnvironmentVariables.InboundMaxAttachSize, options.MaxAttachmentSize);
+ SetParameter(context, PostaEnvironmentVariables.InboundWebhookSecret, options.WebhookSecret);
+ SetParameter(context, PostaEnvironmentVariables.InboundTlsMode, options.TlsMode);
+ SetParameter(context, PostaEnvironmentVariables.InboundTlsCertFile, options.TlsCertFile);
+ SetParameter(context, PostaEnvironmentVariables.InboundTlsKeyFile, options.TlsKeyFile);
+ SetParameter(context, PostaEnvironmentVariables.InboundSmtpRateLimit, options.RateLimit);
+ SetParameter(context, PostaEnvironmentVariables.InboundSmtpRateWindow, options.RateWindow);
+ }
+
+ private static void ConfigureEmailVerification(EnvironmentCallbackContext context, PostaOptions options)
+ {
+ Set(context, PostaEnvironmentVariables.EmailVerificationRequired, options.EmailVerificationRequired);
+ Set(context, PostaEnvironmentVariables.AutoSuppressOnReject, options.AutoSuppressOnReject);
+ Set(context, PostaEnvironmentVariables.EmailVerifyEnabled, options.EmailVerifyEnabled);
+ Set(context, PostaEnvironmentVariables.EmailVerifyCacheTtlHours, options.EmailVerifyCacheTtlHours);
+ Set(context, PostaEnvironmentVariables.EmailVerifyMxCacheTtlHours, options.EmailVerifyMxCacheTtlHours);
+ Set(context, PostaEnvironmentVariables.EmailVerifyRateHourly, options.EmailVerifyRateHourly);
+ }
+
+ private static void ConfigureEmailVerification(EnvironmentCallbackContext context, PostaEmailVerificationOptions options)
+ {
+ SetParameter(context, PostaEnvironmentVariables.EmailVerificationRequired, options.Required);
+ SetParameter(context, PostaEnvironmentVariables.AutoSuppressOnReject, options.AutoSuppressOnReject);
+ SetParameter(context, PostaEnvironmentVariables.EmailVerifyEnabled, options.Enabled);
+ SetParameter(context, PostaEnvironmentVariables.EmailVerifyCacheTtlHours, options.CacheTtlHours);
+ SetParameter(context, PostaEnvironmentVariables.EmailVerifyMxCacheTtlHours, options.MxCacheTtlHours);
+ SetParameter(context, PostaEnvironmentVariables.EmailVerifyRateHourly, options.RateHourly);
+ }
+
+ private static void Set(EnvironmentCallbackContext context, string name, string value)
+ {
+ context.EnvironmentVariables[name] = value;
+ }
+
+ private static void Set(EnvironmentCallbackContext context, string name, ParameterResource value)
+ {
+ context.EnvironmentVariables[name] = value;
+ }
+
+ private static void Set(EnvironmentCallbackContext context, string name, EndpointReferenceExpression value)
+ {
+ context.EnvironmentVariables[name] = value;
+ }
+
+ private static void Set(EnvironmentCallbackContext context, string name, ReferenceExpression value)
+ {
+ context.EnvironmentVariables[name] = value;
+ }
+
+ private static void SetIfAbsent(EnvironmentCallbackContext context, string name, ParameterResource value)
+ {
+ context.EnvironmentVariables.TryAdd(name, value);
+ }
+
+ private static void SetIfAbsent(EnvironmentCallbackContext context, string name, ReferenceExpression value)
+ {
+ context.EnvironmentVariables.TryAdd(name, value);
+ }
+
+ private static void Set(EnvironmentCallbackContext context, string name, bool value)
+ {
+ context.EnvironmentVariables[name] = value ? "true" : "false";
+ }
+
+ private static void Set(EnvironmentCallbackContext context, string name, int value)
+ {
+ context.EnvironmentVariables[name] = value.ToString(System.Globalization.CultureInfo.InvariantCulture);
+ }
+
+ private static void SetIfNotNull(EnvironmentCallbackContext context, string name, string? value)
+ {
+ if (!string.IsNullOrEmpty(value))
+ {
+ context.EnvironmentVariables[name] = value;
+ }
+ }
+
+ private static void SetParameter(EnvironmentCallbackContext context, string name, IResourceBuilder? parameter)
+ {
+ if (parameter is not null)
+ {
+ context.EnvironmentVariables[name] = parameter.Resource;
+ }
+ }
+}
diff --git a/src/CommunityToolkit.Aspire.Hosting.Posta/PostaInboundSmtpOptions.cs b/src/CommunityToolkit.Aspire.Hosting.Posta/PostaInboundSmtpOptions.cs
new file mode 100644
index 000000000..656238a44
--- /dev/null
+++ b/src/CommunityToolkit.Aspire.Hosting.Posta/PostaInboundSmtpOptions.cs
@@ -0,0 +1,69 @@
+using Aspire.Hosting.ApplicationModel;
+
+namespace CommunityToolkit.Aspire.Hosting.Posta;
+
+///
+/// Parameter-based configuration options for Posta inbound SMTP processing.
+///
+public sealed class PostaInboundSmtpOptions
+{
+ ///
+ /// Gets or sets whether inbound email processing is enabled.
+ ///
+ public IResourceBuilder? Enabled { get; set; }
+
+ ///
+ /// Gets or sets the bind address for the built-in SMTP receiver.
+ ///
+ public IResourceBuilder? Host { get; set; }
+
+ ///
+ /// Gets or sets the SMTP listener port.
+ ///
+ public IResourceBuilder? Port { get; set; }
+
+ ///
+ /// Gets or sets the hostname announced in EHLO and used as TLS SNI.
+ ///
+ public IResourceBuilder? Hostname { get; set; }
+
+ ///
+ /// Gets or sets the maximum raw inbound message size in bytes.
+ ///
+ public IResourceBuilder? MaxMessageSize { get; set; }
+
+ ///
+ /// Gets or sets the maximum inbound attachment size in bytes.
+ ///
+ public IResourceBuilder? MaxAttachmentSize { get; set; }
+
+ ///
+ /// Gets or sets the shared secret for inbound webhooks.
+ ///
+ public IResourceBuilder? WebhookSecret { get; set; }
+
+ ///
+ /// Gets or sets the inbound SMTP TLS mode: none or starttls.
+ ///
+ public IResourceBuilder? TlsMode { get; set; }
+
+ ///
+ /// Gets or sets the PEM certificate path for inbound TLS.
+ ///
+ public IResourceBuilder? TlsCertFile { get; set; }
+
+ ///
+ /// Gets or sets the PEM key path for inbound TLS.
+ ///
+ public IResourceBuilder? TlsKeyFile { get; set; }
+
+ ///
+ /// Gets or sets the per-IP SMTP session rate limit.
+ ///
+ public IResourceBuilder? RateLimit { get; set; }
+
+ ///
+ /// Gets or sets the SMTP rate-limit window in seconds.
+ ///
+ public IResourceBuilder? RateWindow { get; set; }
+}
diff --git a/src/CommunityToolkit.Aspire.Hosting.Posta/PostaOptions.cs b/src/CommunityToolkit.Aspire.Hosting.Posta/PostaOptions.cs
new file mode 100644
index 000000000..f174491eb
--- /dev/null
+++ b/src/CommunityToolkit.Aspire.Hosting.Posta/PostaOptions.cs
@@ -0,0 +1,304 @@
+using Aspire.Hosting.ApplicationModel;
+
+namespace CommunityToolkit.Aspire.Hosting.Posta;
+
+///
+/// Configuration options for the Posta container.
+///
+public sealed class PostaOptions
+{
+ ///
+ /// Gets or sets the Posta environment name.
+ ///
+ public string Environment { get; set; } = "production";
+
+ ///
+ /// Gets or sets a value indicating whether Posta stores emails without sending them.
+ ///
+ public bool DevMode { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether auth rate limiting is enabled.
+ ///
+ public bool AuthRateLimitEnabled { get; set; } = true;
+
+ ///
+ /// Gets or sets the maximum emails per hour per user.
+ ///
+ public int RateLimitHourly { get; set; } = 100;
+
+ ///
+ /// Gets or sets the maximum emails per day per user.
+ ///
+ public int RateLimitDaily { get; set; } = 1000;
+
+ ///
+ /// Gets or sets a value indicating whether Swagger UI and ReDoc are enabled.
+ ///
+ public bool OpenApiDocs { get; set; } = true;
+
+ ///
+ /// Gets or sets a value indicating whether Prometheus metrics are enabled.
+ ///
+ public bool MetricsEnabled { get; set; }
+
+ ///
+ /// Gets or sets the Vue dashboard build directory.
+ ///
+ public string? WebDir { get; set; }
+
+ ///
+ /// Gets or sets the public dashboard URL.
+ ///
+ public string? WebUrl { get; set; }
+
+ ///
+ /// Gets or sets the public API base URL advertised in the OpenAPI servers list.
+ ///
+ public string? ApiUrl { get; set; }
+
+ ///
+ /// Gets or sets the comma-separated allowed CORS origins.
+ ///
+ public string CorsOrigins { get; set; } = "*";
+
+ ///
+ /// Gets or sets a value indicating whether the worker runs in the server process.
+ ///
+ public bool EmbeddedWorker { get; set; } = true;
+
+ ///
+ /// Gets or sets the number of worker goroutines.
+ ///
+ public int WorkerConcurrency { get; set; } = 10;
+
+ ///
+ /// Gets or sets the maximum email retry attempts.
+ ///
+ public int WorkerMaxRetries { get; set; } = 5;
+
+ ///
+ /// Gets or sets the maximum webhook delivery retry attempts.
+ ///
+ public int WebhookMaxRetries { get; set; } = 3;
+
+ ///
+ /// Gets or sets the webhook HTTP request timeout in seconds.
+ ///
+ public int WebhookTimeoutSeconds { get; set; } = 10;
+
+ ///
+ /// Gets or sets the optional HTTP, HTTPS, or SOCKS5 proxy for webhook delivery.
+ ///
+ public string? WebhookProxyUrl { get; set; }
+
+ ///
+ /// Gets or sets the Google OAuth client ID for SSO login.
+ ///
+ public string? GoogleOAuthClientId { get; set; }
+
+ ///
+ /// Gets or sets the Google OAuth client secret for SSO login.
+ ///
+ public IResourceBuilder? GoogleOAuthClientSecret { get; set; }
+
+ ///
+ /// Gets or sets the OAuth callback base URL.
+ ///
+ public string? OAuthCallbackUrl { get; set; }
+
+ ///
+ /// Gets or sets the storage backend for attachments, either s3 or filesystem.
+ ///
+ public string? BlobProvider { get; set; }
+
+ ///
+ /// Gets or sets the S3-compatible endpoint.
+ ///
+ public string? BlobS3Endpoint { get; set; }
+
+ ///
+ /// Gets or sets the S3 region.
+ ///
+ public string BlobS3Region { get; set; } = "us-east-1";
+
+ ///
+ /// Gets or sets the S3 bucket name.
+ ///
+ public string? BlobS3Bucket { get; set; }
+
+ ///
+ /// Gets or sets the S3 access key.
+ ///
+ public IResourceBuilder? BlobS3AccessKey { get; set; }
+
+ ///
+ /// Gets or sets the S3 secret key.
+ ///
+ public IResourceBuilder? BlobS3SecretKey { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether S3 storage uses TLS.
+ ///
+ public bool BlobS3UseSsl { get; set; } = true;
+
+ ///
+ /// Gets or sets a value indicating whether S3 path-style addressing is used.
+ ///
+ public bool BlobS3PathStyle { get; set; }
+
+ ///
+ /// Gets or sets the filesystem storage path.
+ ///
+ public string BlobFileSystemPath { get; set; } = "/data/attachments";
+
+ ///
+ /// Gets or sets the AES-256-GCM encryption key for stored SMTP passwords.
+ ///
+ public IResourceBuilder? EncryptionKey { get; set; }
+
+ ///
+ /// Gets or sets the system SMTP server host used for platform notifications.
+ ///
+ public string? SystemSmtpHost { get; set; }
+
+ ///
+ /// Gets or sets the system SMTP server port.
+ ///
+ public int SystemSmtpPort { get; set; } = 587;
+
+ ///
+ /// Gets or sets the system SMTP username.
+ ///
+ public string? SystemSmtpUsername { get; set; }
+
+ ///
+ /// Gets or sets the system SMTP password.
+ ///
+ public IResourceBuilder? SystemSmtpPassword { get; set; }
+
+ ///
+ /// Gets or sets the system SMTP from address.
+ ///
+ public string? SystemSmtpFrom { get; set; }
+
+ ///
+ /// Gets or sets the system SMTP encryption mode: none, ssl, or starttls.
+ ///
+ public string SystemSmtpEncryption { get; set; } = "starttls";
+
+ ///
+ /// Gets or sets a value indicating whether inbound email processing is enabled.
+ ///
+ public bool InboundEnabled { get; set; }
+
+ ///
+ /// Gets or sets the bind address for the built-in SMTP receiver.
+ ///
+ public string InboundSmtpHost { get; set; } = "0.0.0.0";
+
+ ///
+ /// Gets or sets the SMTP listener port.
+ ///
+ public int InboundSmtpPort { get; set; } = 2525;
+
+ ///
+ /// Gets or sets the hostname announced in EHLO and used as TLS SNI.
+ ///
+ public string InboundHostname { get; set; } = "posta.local";
+
+ ///
+ /// Gets or sets the maximum raw inbound message size in bytes.
+ ///
+ public int InboundMaxMessageSize { get; set; } = 26214400;
+
+ ///
+ /// Gets or sets the maximum inbound attachment size in bytes.
+ ///
+ public int InboundMaxAttachSize { get; set; } = 10485760;
+
+ ///
+ /// Gets or sets the shared secret for inbound webhooks.
+ ///
+ public IResourceBuilder? InboundWebhookSecret { get; set; }
+
+ ///
+ /// Gets or sets the inbound SMTP TLS mode: none or starttls.
+ ///
+ public string InboundTlsMode { get; set; } = "none";
+
+ ///
+ /// Gets or sets the PEM certificate path for inbound TLS.
+ ///
+ public string? InboundTlsCertFile { get; set; }
+
+ ///
+ /// Gets or sets the PEM key path for inbound TLS.
+ ///
+ public string? InboundTlsKeyFile { get; set; }
+
+ ///
+ /// Gets or sets the per-IP SMTP session rate limit.
+ ///
+ public int InboundSmtpRateLimit { get; set; } = 60;
+
+ ///
+ /// Gets or sets the SMTP rate-limit window in seconds.
+ ///
+ public int InboundSmtpRateWindow { get; set; } = 60;
+
+ ///
+ /// Gets or sets a value indicating whether users must verify their email address before sign-in.
+ ///
+ public bool EmailVerificationRequired { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether permanently rejected recipients are added to the suppression list.
+ ///
+ public bool AutoSuppressOnReject { get; set; } = true;
+
+ ///
+ /// Gets or sets a value indicating whether the email verification endpoint is enabled.
+ ///
+ public bool EmailVerifyEnabled { get; set; } = true;
+
+ ///
+ /// Gets or sets how long address-level verification results are cached, in hours.
+ ///
+ public int EmailVerifyCacheTtlHours { get; set; } = 168;
+
+ ///
+ /// Gets or sets how long domain MX lookups are cached, in hours.
+ ///
+ public int EmailVerifyMxCacheTtlHours { get; set; } = 24;
+
+ ///
+ /// Gets or sets the per-user hourly cap on verification requests.
+ ///
+ public int EmailVerifyRateHourly { get; set; } = 1000;
+
+ ///
+ /// Gets or sets a value indicating whether older binaries can boot against a newer database schema.
+ ///
+ public bool AllowDowngrade { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether hosted plan limits and quotas are enforced.
+ ///
+ public bool PlanEnforcement { get; set; }
+
+ ///
+ /// Gets or sets a PostgreSQL connection string parameter that overrides individual database settings.
+ ///
+ public IResourceBuilder? DatabaseUrl { get; set; }
+
+ ///
+ /// Gets or sets a Redis password parameter that overrides the referenced Redis resource password.
+ ///
+ public IResourceBuilder? RedisPassword { get; set; }
+
+ ///
+ /// Gets or sets an external Redis address in the form host:port.
+ ///
+ public string? RedisAddress { get; set; }
+}
diff --git a/src/CommunityToolkit.Aspire.Hosting.Posta/PostaResource.cs b/src/CommunityToolkit.Aspire.Hosting.Posta/PostaResource.cs
new file mode 100644
index 000000000..7ab14ed98
--- /dev/null
+++ b/src/CommunityToolkit.Aspire.Hosting.Posta/PostaResource.cs
@@ -0,0 +1,61 @@
+namespace Aspire.Hosting.ApplicationModel;
+
+///
+/// Resource for the Posta email delivery platform.
+///
+/// The name of the resource.
+/// A parameter that contains the JWT signing secret.
+/// A parameter that contains the initial admin password.
+[AspireExport(ExposeProperties = true)]
+public sealed class PostaResource(string name, ParameterResource jwtSecret, ParameterResource adminPassword)
+ : ContainerResource(name), IResourceWithConnectionString
+{
+ internal const int HttpEndpointPort = 9000;
+ internal const string HttpEndpointName = "http";
+
+ private EndpointReference? _primaryEndpoint;
+
+ ///
+ /// Gets the primary HTTP endpoint for the Posta server.
+ ///
+ public EndpointReference PrimaryEndpoint => _primaryEndpoint ??= new(this, HttpEndpointName);
+
+ ///
+ /// Gets the host endpoint reference for the HTTP endpoint.
+ ///
+ public EndpointReferenceExpression Host => PrimaryEndpoint.Property(EndpointProperty.Host);
+
+ ///
+ /// Gets the port endpoint reference for the HTTP endpoint.
+ ///
+ public EndpointReferenceExpression Port => PrimaryEndpoint.Property(EndpointProperty.Port);
+
+ ///
+ /// Gets the parameter that contains the JWT signing secret.
+ ///
+ public ParameterResource JwtSecretParameter { get; } = jwtSecret ?? throw new ArgumentNullException(nameof(jwtSecret));
+
+ ///
+ /// Gets the parameter that contains the initial admin password.
+ ///
+ public ParameterResource AdminPasswordParameter { get; } = adminPassword ?? throw new ArgumentNullException(nameof(adminPassword));
+
+ ///
+ /// Connection string for the Posta HTTP API in the form of Endpoint=http://host:port.
+ ///
+ public ReferenceExpression ConnectionStringExpression => ReferenceExpression.Create(
+ $"Endpoint={PrimaryEndpoint.Property(EndpointProperty.Url)}");
+
+ ///
+ /// Gets the connection URI expression for the Posta HTTP API.
+ ///
+ public ReferenceExpression UriExpression => ReferenceExpression.Create($"{PrimaryEndpoint.Property(EndpointProperty.Url)}");
+
+ IEnumerable> IResourceWithConnectionString.GetConnectionProperties()
+ {
+ yield return new("Host", ReferenceExpression.Create($"{Host}"));
+ yield return new("Port", ReferenceExpression.Create($"{Port}"));
+ yield return new("Uri", UriExpression);
+ }
+}
+
diff --git a/src/CommunityToolkit.Aspire.Hosting.Posta/PostaS3BlobStorageOptions.cs b/src/CommunityToolkit.Aspire.Hosting.Posta/PostaS3BlobStorageOptions.cs
new file mode 100644
index 000000000..ac80a0bfb
--- /dev/null
+++ b/src/CommunityToolkit.Aspire.Hosting.Posta/PostaS3BlobStorageOptions.cs
@@ -0,0 +1,44 @@
+using Aspire.Hosting.ApplicationModel;
+
+namespace CommunityToolkit.Aspire.Hosting.Posta;
+
+///
+/// Parameter-based configuration options for Posta S3-compatible attachment storage.
+///
+public sealed class PostaS3BlobStorageOptions
+{
+ ///
+ /// Gets or sets the S3-compatible endpoint.
+ ///
+ public IResourceBuilder? Endpoint { get; set; }
+
+ ///
+ /// Gets or sets the S3 region.
+ ///
+ public IResourceBuilder? Region { get; set; }
+
+ ///
+ /// Gets or sets the S3 bucket name.
+ ///
+ public IResourceBuilder? Bucket { get; set; }
+
+ ///
+ /// Gets or sets the S3 access key.
+ ///
+ public IResourceBuilder? AccessKey { get; set; }
+
+ ///
+ /// Gets or sets the S3 secret key.
+ ///
+ public IResourceBuilder? SecretKey { get; set; }
+
+ ///
+ /// Gets or sets whether S3 storage uses TLS.
+ ///
+ public IResourceBuilder? UseSsl { get; set; }
+
+ ///
+ /// Gets or sets whether S3 path-style addressing is used.
+ ///
+ public IResourceBuilder? PathStyle { get; set; }
+}
diff --git a/src/CommunityToolkit.Aspire.Hosting.Posta/PostaSystemSmtpOptions.cs b/src/CommunityToolkit.Aspire.Hosting.Posta/PostaSystemSmtpOptions.cs
new file mode 100644
index 000000000..c61f285f0
--- /dev/null
+++ b/src/CommunityToolkit.Aspire.Hosting.Posta/PostaSystemSmtpOptions.cs
@@ -0,0 +1,39 @@
+using Aspire.Hosting.ApplicationModel;
+
+namespace CommunityToolkit.Aspire.Hosting.Posta;
+
+///
+/// Parameter-based configuration options for Posta system SMTP notifications.
+///
+public sealed class PostaSystemSmtpOptions
+{
+ ///
+ /// Gets or sets the system SMTP server host used for platform notifications.
+ ///
+ public IResourceBuilder? Host { get; set; }
+
+ ///
+ /// Gets or sets the system SMTP server port.
+ ///
+ public IResourceBuilder? Port { get; set; }
+
+ ///
+ /// Gets or sets the system SMTP username.
+ ///
+ public IResourceBuilder? Username { get; set; }
+
+ ///
+ /// Gets or sets the system SMTP password.
+ ///
+ public IResourceBuilder? Password { get; set; }
+
+ ///
+ /// Gets or sets the system SMTP from address.
+ ///
+ public IResourceBuilder? From { get; set; }
+
+ ///
+ /// Gets or sets the system SMTP encryption mode: none, ssl, or starttls.
+ ///
+ public IResourceBuilder? Encryption { get; set; }
+}
diff --git a/src/CommunityToolkit.Aspire.Hosting.Posta/README.md b/src/CommunityToolkit.Aspire.Hosting.Posta/README.md
new file mode 100644
index 000000000..89a6b3f66
--- /dev/null
+++ b/src/CommunityToolkit.Aspire.Hosting.Posta/README.md
@@ -0,0 +1,169 @@
+# Posta hosting integration
+
+Use this integration to model, configure, and orchestrate the [Posta](https://docs.goposta.dev/) self-hosted email delivery platform in an Aspire AppHost.
+
+## Getting started
+
+Install the package in your AppHost project:
+
+```bash
+aspire add CommunityToolkit.Aspire.Hosting.Posta
+```
+
+## Basic usage
+
+```csharp
+var builder = DistributedApplication.CreateBuilder(args);
+
+var postgresPassword = builder.AddParameter("postgres-password", secret: true);
+var postgres = builder.AddPostgres("postgres", password: postgresPassword);
+var database = postgres.AddDatabase("posta-db", "posta");
+var redis = builder.AddRedis("redis");
+
+var posta = builder.AddPosta("posta", database, redis);
+
+builder.AddProject("api")
+ .WithReference(posta)
+ .WaitFor(posta);
+
+builder.Build().Run();
+```
+
+Posta stores data in PostgreSQL, uses Redis for queueing, and runs the embedded worker in the main container by default. `AddPosta` requires PostgreSQL database and Redis resources so the container starts with the dependencies it needs.
+
+## Configuration
+
+Use the `PostaOptions` callback when values are known in the AppHost and can be represented as typed literal values. Secret values inside `PostaOptions`, such as OAuth client secrets, S3 keys, SMTP passwords, encryption keys, inbound webhook secrets, `POSTA_DB_URL`, and Redis passwords, should still be passed as Aspire parameters.
+
+```csharp
+var encryptionKey = builder.AddParameter("posta-encryption-key", secret: true);
+var smtpPassword = builder.AddParameter("posta-smtp-password", secret: true);
+
+var posta = builder.AddPosta("posta", database, redis, options =>
+{
+ options.Environment = "production";
+ options.WebUrl = "https://mail.example.com";
+ options.ApiUrl = "https://mail.example.com";
+ options.CorsOrigins = "https://app.example.com";
+ options.MetricsEnabled = true;
+ options.EncryptionKey = encryptionKey;
+
+ options.SystemSmtpHost = "smtp.example.com";
+ options.SystemSmtpPort = 587;
+ options.SystemSmtpPassword = smtpPassword;
+ options.SystemSmtpFrom = "notifications@example.com";
+ options.SystemSmtpEncryption = "starttls";
+});
+```
+
+For values coming from configuration, environment variables, user secrets, or publish-time parameters, use the grouped parameter-based methods. These methods accept options objects whose properties are `IResourceBuilder?`, so they work with `AddParameter` and `AddParameterFromConfiguration`.
+
+```csharp
+var smtpHost = builder.AddParameterFromConfiguration("posta-smtp-host", "Posta:Smtp:Host");
+var smtpPort = builder.AddParameterFromConfiguration("posta-smtp-port", "Posta:Smtp:Port");
+var smtpUsername = builder.AddParameterFromConfiguration("posta-smtp-username", "Posta:Smtp:Username");
+var smtpPassword = builder.AddParameterFromConfiguration("posta-smtp-password", "Posta:Smtp:Password", secret: true);
+var smtpFrom = builder.AddParameterFromConfiguration("posta-smtp-from", "Posta:Smtp:From");
+var smtpEncryption = builder.AddParameterFromConfiguration("posta-smtp-encryption", "Posta:Smtp:Encryption");
+
+var googleClientId = builder.AddParameterFromConfiguration("posta-google-client-id", "Posta:GoogleOAuth:ClientId");
+var googleClientSecret = builder.AddParameterFromConfiguration("posta-google-client-secret", "Posta:GoogleOAuth:ClientSecret", secret: true);
+var googleCallbackUrl = builder.AddParameterFromConfiguration("posta-google-callback-url", "Posta:GoogleOAuth:CallbackUrl");
+
+var s3Endpoint = builder.AddParameterFromConfiguration("posta-s3-endpoint", "Posta:S3:Endpoint");
+var s3Region = builder.AddParameterFromConfiguration("posta-s3-region", "Posta:S3:Region");
+var s3Bucket = builder.AddParameterFromConfiguration("posta-s3-bucket", "Posta:S3:Bucket");
+var s3AccessKey = builder.AddParameterFromConfiguration("posta-s3-access-key", "Posta:S3:AccessKey", secret: true);
+var s3SecretKey = builder.AddParameterFromConfiguration("posta-s3-secret-key", "Posta:S3:SecretKey", secret: true);
+
+var posta = builder.AddPosta("posta", database, redis)
+ .WithSystemSmtp(options =>
+ {
+ options.Host = smtpHost;
+ options.Port = smtpPort;
+ options.Username = smtpUsername;
+ options.Password = smtpPassword;
+ options.From = smtpFrom;
+ options.Encryption = smtpEncryption;
+ })
+ .WithGoogleOAuth(options =>
+ {
+ options.ClientId = googleClientId;
+ options.ClientSecret = googleClientSecret;
+ options.CallbackUrl = googleCallbackUrl;
+ })
+ .WithS3BlobStorage(options =>
+ {
+ options.Endpoint = s3Endpoint;
+ options.Region = s3Region;
+ options.Bucket = s3Bucket;
+ options.AccessKey = s3AccessKey;
+ options.SecretKey = s3SecretKey;
+ });
+```
+
+You can also configure inbound SMTP and email verification through parameter-based options:
+
+```csharp
+var inboundEnabled = builder.AddParameterFromConfiguration("posta-inbound-enabled", "Posta:Inbound:Enabled");
+var inboundPort = builder.AddParameterFromConfiguration("posta-inbound-port", "Posta:Inbound:Port");
+var inboundWebhookSecret = builder.AddParameterFromConfiguration("posta-inbound-webhook-secret", "Posta:Inbound:WebhookSecret", secret: true);
+
+var emailVerificationRequired = builder.AddParameterFromConfiguration("posta-email-verification-required", "Posta:EmailVerification:Required");
+
+builder.AddPosta("posta", database, redis)
+ .WithInboundSmtp(options =>
+ {
+ options.Enabled = inboundEnabled;
+ options.Port = inboundPort;
+ options.WebhookSecret = inboundWebhookSecret;
+ })
+ .WithEmailVerification(options =>
+ {
+ options.Required = emailVerificationRequired;
+ });
+```
+
+## API overview
+
+| Method | Purpose |
+| --- | --- |
+| `AddPosta(name, database, redis)` | Adds Posta and configures PostgreSQL and Redis references. |
+| `AddPosta(name, database, redis, PostaOptions)` | Adds Posta with typed environment configuration. |
+| `AddPosta(name, database, redis, Action)` | Adds Posta with callback-based typed environment configuration. |
+| `WithReference(PostgresDatabaseResource)` | Configures the PostgreSQL database environment variables and waits for the database. |
+| `WithReference(RedisResource, redisPassword)` | Configures Redis address/password environment variables and waits for Redis. |
+| `WithSystemSmtp(PostaSystemSmtpOptions)` | Configures parameter-based system SMTP settings. |
+| `WithSystemSmtp(Action)` | Configures parameter-based system SMTP settings with a callback. |
+| `WithInboundSmtp(PostaInboundSmtpOptions)` | Configures parameter-based inbound SMTP receiver settings. |
+| `WithInboundSmtp(Action)` | Configures parameter-based inbound SMTP receiver settings with a callback. |
+| `WithS3BlobStorage(PostaS3BlobStorageOptions)` | Configures parameter-based S3-compatible attachment storage and sets the blob provider to `s3`. |
+| `WithS3BlobStorage(Action)` | Configures parameter-based S3-compatible attachment storage with a callback and sets the blob provider to `s3`. |
+| `WithGoogleOAuth(PostaGoogleOAuthOptions)` | Configures parameter-based Google OAuth login settings. |
+| `WithGoogleOAuth(Action)` | Configures parameter-based Google OAuth login settings with a callback. |
+| `WithEmailVerification(PostaEmailVerificationOptions)` | Configures parameter-based email verification settings. |
+| `WithEmailVerification(Action)` | Configures parameter-based email verification settings with a callback. |
+
+Use `PostaOptions.DatabaseUrl`, `PostaOptions.RedisAddress`, and `PostaOptions.RedisPassword` only when you need to override the generated PostgreSQL or Redis environment values.
+
+## Connection Properties
+
+The Posta resource exposes the following connection properties:
+
+| Name | Format |
+| --- | --- |
+| `Host` | Posta HTTP API host |
+| `Port` | Posta HTTP API port |
+| `Uri` | `http://{host}:{port}` |
+
+These properties become environment variables named `[RESOURCE]__HOST`, `[RESOURCE]__PORT`, and `[RESOURCE]__URI` when referenced by another resource. The connection string uses `Endpoint=http://{host}:{port}`.
+
+## Additional documentation
+
+* [Posta documentation](https://docs.goposta.dev/)
+* [Posta installation guide](https://docs.goposta.dev/docs/getting-started/installation/)
+* [Posta configuration](https://docs.goposta.dev/docs/getting-started/configuration/)
+
+## Feedback & contributing
+
+https://github.com/CommunityToolkit/Aspire
diff --git a/tests/CommunityToolkit.Aspire.Hosting.Posta.Tests/AppHostTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Posta.Tests/AppHostTests.cs
new file mode 100644
index 000000000..ef1f4505c
--- /dev/null
+++ b/tests/CommunityToolkit.Aspire.Hosting.Posta.Tests/AppHostTests.cs
@@ -0,0 +1,26 @@
+using Aspire.Components.Common.Tests;
+using CommunityToolkit.Aspire.Testing;
+
+namespace CommunityToolkit.Aspire.Hosting.Posta.Tests;
+
+[RequiresDocker]
+public class AppHostTests(AspireIntegrationTestFixture fixture)
+ : IClassFixture>
+{
+ private const string ResourceName = "posta";
+
+ [Fact]
+ public async Task ResourceStartsAndHealthEndpointResponds()
+ {
+ await fixture.ResourceNotificationService
+ .WaitForResourceHealthyAsync(ResourceName)
+ .WaitAsync(TimeSpan.FromMinutes(3));
+
+ var httpEndpoint = fixture.GetEndpoint(ResourceName, "http");
+ using var httpClient = new HttpClient { BaseAddress = httpEndpoint };
+
+ using var response = await httpClient.GetAsync("/healthz", TestContext.Current.CancellationToken);
+
+ response.EnsureSuccessStatusCode();
+ }
+}
\ No newline at end of file
diff --git a/tests/CommunityToolkit.Aspire.Hosting.Posta.Tests/CommunityToolkit.Aspire.Hosting.Posta.Tests.csproj b/tests/CommunityToolkit.Aspire.Hosting.Posta.Tests/CommunityToolkit.Aspire.Hosting.Posta.Tests.csproj
new file mode 100644
index 000000000..49fdce0d8
--- /dev/null
+++ b/tests/CommunityToolkit.Aspire.Hosting.Posta.Tests/CommunityToolkit.Aspire.Hosting.Posta.Tests.csproj
@@ -0,0 +1,14 @@
+
+
+
+ false
+ true
+
+
+
+
+
+
+
+
+
diff --git a/tests/CommunityToolkit.Aspire.Hosting.Posta.Tests/ContainerResourceCreationTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Posta.Tests/ContainerResourceCreationTests.cs
new file mode 100644
index 000000000..f75ff0c83
--- /dev/null
+++ b/tests/CommunityToolkit.Aspire.Hosting.Posta.Tests/ContainerResourceCreationTests.cs
@@ -0,0 +1,497 @@
+using Aspire.Hosting;
+using CommunityToolkit.Aspire.Testing;
+
+namespace CommunityToolkit.Aspire.Hosting.Posta.Tests;
+
+public class ContainerResourceCreationTests
+{
+ [Fact]
+ public void AddPostaThrowsWhenBuilderIsNull()
+ {
+ IDistributedApplicationBuilder builder = null!;
+
+ var exception = Assert.Throws(() => builder.AddPosta("posta", null!, null!));
+ Assert.Equal("builder", exception.ParamName);
+ }
+
+ [Fact]
+ public void AddPostaThrowsWhenNameIsNull()
+ {
+ var builder = DistributedApplication.CreateBuilder();
+ var (database, redis) = AddPostaDependencies(builder);
+
+ Assert.Throws(() => builder.AddPosta(null!, database, redis));
+ }
+
+ [Fact]
+ public void WithReferenceThrowsWhenDatabaseIsNull()
+ {
+ var builder = DistributedApplication.CreateBuilder();
+ var (database, redis) = AddPostaDependencies(builder);
+ var posta = builder.AddPosta("posta", database, redis);
+
+ Assert.Throws(() => posta.WithReference((IResourceBuilder)null!));
+ }
+
+ [Fact]
+ public void WithReferenceThrowsWhenRedisIsNull()
+ {
+ var builder = DistributedApplication.CreateBuilder();
+ var (database, redis) = AddPostaDependencies(builder);
+ var posta = builder.AddPosta("posta", database, redis);
+
+ Assert.Throws(() => posta.WithReference((IResourceBuilder)null!));
+ }
+
+ [Fact]
+ public void WithGroupedOptionCallbacksThrowWhenConfigureOptionsIsNull()
+ {
+ var builder = DistributedApplication.CreateBuilder();
+ var (database, redis) = AddPostaDependencies(builder);
+ var posta = builder.AddPosta("posta", database, redis);
+
+ Assert.Throws(() => posta.WithSystemSmtp((Action)null!));
+ Assert.Throws(() => posta.WithInboundSmtp((Action)null!));
+ Assert.Throws(() => posta.WithS3BlobStorage((Action)null!));
+ Assert.Throws(() => posta.WithGoogleOAuth((Action)null!));
+ Assert.Throws(() => posta.WithEmailVerification((Action)null!));
+ }
+
+ [Fact]
+ public void AddPostaWithReferencesThrowsWhenDatabaseIsNull()
+ {
+ var builder = DistributedApplication.CreateBuilder();
+ var redis = builder.AddRedis("redis");
+
+ Assert.Throws(() => builder.AddPosta("posta", null!, redis));
+ }
+
+ [Fact]
+ public void AddPostaWithReferencesThrowsWhenRedisIsNull()
+ {
+ var builder = DistributedApplication.CreateBuilder();
+ var postgres = builder.AddPostgres("postgres");
+ var database = postgres.AddDatabase("posta-db", "posta");
+
+ Assert.Throws(() => builder.AddPosta("posta", database, null!));
+ }
+
+ [Fact]
+ public void AddPostaSetsContainerDetailsOnResource()
+ {
+ var builder = DistributedApplication.CreateBuilder();
+ var (database, redis) = AddPostaDependencies(builder);
+
+ builder.AddPosta("posta", database, redis);
+
+ using var app = builder.Build();
+ var appModel = app.Services.GetRequiredService();
+
+ var resource = Assert.Single(appModel.Resources.OfType());
+
+ Assert.Equal("posta", resource.Name);
+ Assert.True(resource.TryGetLastAnnotation(out ContainerImageAnnotation? imageAnnotations));
+ Assert.Equal(PostaContainerImageTags.Tag, imageAnnotations.Tag);
+ Assert.NotEqual("latest", imageAnnotations.Tag);
+ Assert.Equal(PostaContainerImageTags.Image, imageAnnotations.Image);
+ Assert.Equal(PostaContainerImageTags.Registry, imageAnnotations.Registry);
+ }
+
+ [Fact]
+ public void AddPostaSetsEndpointDetailsOnResource()
+ {
+ var builder = DistributedApplication.CreateBuilder();
+ var (database, redis) = AddPostaDependencies(builder);
+
+ builder.AddPosta("posta", database, redis, port: 9001);
+
+ using var app = builder.Build();
+ var appModel = app.Services.GetRequiredService();
+ var resource = Assert.Single(appModel.Resources.OfType());
+
+ var endpoint = Assert.Single(resource.Annotations.OfType(), x => x.Name == PostaResource.HttpEndpointName);
+ Assert.Equal("http", endpoint.UriScheme);
+ Assert.Equal(PostaResource.HttpEndpointPort, endpoint.TargetPort);
+ Assert.Equal(9001, endpoint.Port);
+ }
+
+ [Fact]
+ public void AddPostaRegistersHealthChecks()
+ {
+ var builder = DistributedApplication.CreateBuilder();
+ var (database, redis) = AddPostaDependencies(builder);
+
+ builder.AddPosta("posta", database, redis);
+
+ using var app = builder.Build();
+ var appModel = app.Services.GetRequiredService();
+ var resource = Assert.Single(appModel.Resources.OfType());
+
+ Assert.True(resource.TryGetAnnotationsOfType(out var annotations));
+ Assert.Equal(2, annotations.Count());
+ }
+
+ [Fact]
+ public async Task AddPostaWithReferencesConfiguresPostgreSqlAndRedis()
+ {
+ var builder = DistributedApplication.CreateBuilder();
+ var postgres = builder.AddPostgres("postgres");
+ var database = postgres.AddDatabase("posta-db", "posta");
+ var redis = builder.AddRedis("redis");
+
+ var posta = builder.AddPosta("posta", database, redis);
+
+ Assert.True(posta.Resource.TryGetAnnotationsOfType(out var annotations));
+
+ var context = new EnvironmentCallbackContext(
+ new DistributedApplicationExecutionContext(
+ new DistributedApplicationExecutionContextOptions(DistributedApplicationOperation.Run)));
+
+ foreach (var annotation in annotations)
+ {
+ await annotation.Callback(context);
+ }
+
+ var env = context.EnvironmentVariables;
+
+ Assert.True(env.ContainsKey("POSTA_DB_HOST"));
+ Assert.True(env.ContainsKey("POSTA_DB_PORT"));
+ Assert.True(env.ContainsKey("POSTA_DB_USER"));
+ Assert.True(env.ContainsKey("POSTA_DB_PASSWORD"));
+ Assert.Equal("posta", env["POSTA_DB_NAME"]);
+ Assert.Equal("disable", env["POSTA_DB_SSL_MODE"]);
+ Assert.True(env.ContainsKey("POSTA_REDIS_ADDR"));
+ }
+
+ [Fact]
+ public async Task AddPostaConfiguresPostaEnvironmentVariables()
+ {
+ var builder = DistributedApplication.CreateBuilder();
+ var postgres = builder.AddPostgres("postgres");
+ var database = postgres.AddDatabase("posta-db", "posta");
+ var redis = builder.AddRedis("redis");
+ var databaseUrl = builder.AddParameter("posta-db-url", "postgres://example");
+ var redisPassword = builder.AddParameter("posta-redis-password", "redis-secret");
+ var googleSecret = builder.AddParameter("posta-google-secret", "google-secret");
+ var s3AccessKey = builder.AddParameter("posta-s3-access-key", "s3-access");
+ var s3SecretKey = builder.AddParameter("posta-s3-secret-key", "s3-secret");
+ var encryptionKey = builder.AddParameter("posta-encryption-key", "encryption-secret");
+ var smtpPassword = builder.AddParameter("posta-smtp-password", "smtp-secret");
+ var inboundWebhookSecret = builder.AddParameter("posta-inbound-secret", "inbound-secret");
+
+ var posta = builder.AddPosta("posta", database, redis, options =>
+ {
+ options.Environment = "dev";
+ options.DevMode = true;
+ options.AuthRateLimitEnabled = false;
+ options.RateLimitHourly = 500;
+ options.RateLimitDaily = 5000;
+ options.OpenApiDocs = false;
+ options.MetricsEnabled = true;
+ options.WebDir = "web/dist";
+ options.WebUrl = "https://posta.example.com";
+ options.ApiUrl = "https://api.posta.example.com";
+ options.CorsOrigins = "https://app.example.com";
+ options.EmbeddedWorker = false;
+ options.WorkerConcurrency = 20;
+ options.WorkerMaxRetries = 7;
+ options.WebhookMaxRetries = 4;
+ options.WebhookTimeoutSeconds = 30;
+ options.WebhookProxyUrl = "http://proxy.example.com";
+ options.GoogleOAuthClientId = "google-client-id";
+ options.GoogleOAuthClientSecret = googleSecret;
+ options.OAuthCallbackUrl = "https://posta.example.com/oauth/callback";
+ options.BlobProvider = "s3";
+ options.BlobS3Endpoint = "https://s3.example.com";
+ options.BlobS3Region = "eu-central-1";
+ options.BlobS3Bucket = "posta";
+ options.BlobS3AccessKey = s3AccessKey;
+ options.BlobS3SecretKey = s3SecretKey;
+ options.BlobS3UseSsl = false;
+ options.BlobS3PathStyle = true;
+ options.BlobFileSystemPath = "/posta/attachments";
+ options.EncryptionKey = encryptionKey;
+ options.SystemSmtpHost = "smtp.example.com";
+ options.SystemSmtpPort = 2525;
+ options.SystemSmtpUsername = "notifications@example.com";
+ options.SystemSmtpPassword = smtpPassword;
+ options.SystemSmtpFrom = "notifications@example.com";
+ options.SystemSmtpEncryption = "ssl";
+ options.InboundEnabled = true;
+ options.InboundSmtpHost = "127.0.0.1";
+ options.InboundSmtpPort = 2526;
+ options.InboundHostname = "mx.example.com";
+ options.InboundMaxMessageSize = 12345;
+ options.InboundMaxAttachSize = 6789;
+ options.InboundWebhookSecret = inboundWebhookSecret;
+ options.InboundTlsMode = "starttls";
+ options.InboundTlsCertFile = "/certs/fullchain.pem";
+ options.InboundTlsKeyFile = "/certs/privkey.pem";
+ options.InboundSmtpRateLimit = 10;
+ options.InboundSmtpRateWindow = 20;
+ options.EmailVerificationRequired = true;
+ options.AutoSuppressOnReject = false;
+ options.EmailVerifyEnabled = false;
+ options.EmailVerifyCacheTtlHours = 12;
+ options.EmailVerifyMxCacheTtlHours = 6;
+ options.EmailVerifyRateHourly = 42;
+ options.AllowDowngrade = true;
+ options.PlanEnforcement = true;
+ options.DatabaseUrl = databaseUrl;
+ options.RedisPassword = redisPassword;
+ options.RedisAddress = "redis.example.com:6379";
+ })
+ .WithReference(redis, redisPassword);
+
+ Assert.True(posta.Resource.TryGetAnnotationsOfType(out var annotations));
+
+ var context = new EnvironmentCallbackContext(
+ new DistributedApplicationExecutionContext(
+ new DistributedApplicationExecutionContextOptions(DistributedApplicationOperation.Run)));
+
+ foreach (var annotation in annotations)
+ {
+ await annotation.Callback(context);
+ }
+
+ var env = context.EnvironmentVariables;
+
+ Assert.Equal("9000", env["POSTA_PORT"]);
+ Assert.Equal("dev", env["POSTA_ENV"]);
+ Assert.Equal("true", env["POSTA_DEV_MODE"]);
+ Assert.Equal("false", env["POSTA_AUTH_RATE_LIMIT_ENABLED"]);
+ Assert.Equal("500", env["POSTA_RATE_LIMIT_HOURLY"]);
+ Assert.Equal("5000", env["POSTA_RATE_LIMIT_DAILY"]);
+ Assert.Equal("false", env["POSTA_OPENAPI_DOCS"]);
+ Assert.Equal("true", env["POSTA_METRICS_ENABLED"]);
+ Assert.Equal("web/dist", env["POSTA_WEB_DIR"]);
+ Assert.Equal("https://posta.example.com", env["POSTA_WEB_URL"]);
+ Assert.Equal("https://api.posta.example.com", env["POSTA_API_URL"]);
+ Assert.Equal("https://app.example.com", env["POSTA_CORS_ORIGINS"]);
+ Assert.Equal("false", env["POSTA_EMBEDDED_WORKER"]);
+ Assert.Equal("20", env["POSTA_WORKER_CONCURRENCY"]);
+ Assert.Equal("7", env["POSTA_WORKER_MAX_RETRIES"]);
+ Assert.Equal("4", env["POSTA_WEBHOOK_MAX_RETRIES"]);
+ Assert.Equal("30", env["POSTA_WEBHOOK_TIMEOUT_SECS"]);
+ Assert.Equal("http://proxy.example.com", env["POSTA_WEBHOOK_PROXY_URL"]);
+ Assert.Equal("google-client-id", env["POSTA_GOOGLE_OAUTH_CLIENT_ID"]);
+ Assert.Same(googleSecret.Resource, env["POSTA_GOOGLE_OAUTH_CLIENT_SECRET"]);
+ Assert.Equal("https://posta.example.com/oauth/callback", env["POSTA_OAUTH_CALLBACK_URL"]);
+ Assert.Equal("s3", env["POSTA_BLOB_PROVIDER"]);
+ Assert.Equal("https://s3.example.com", env["POSTA_BLOB_S3_ENDPOINT"]);
+ Assert.Equal("eu-central-1", env["POSTA_BLOB_S3_REGION"]);
+ Assert.Equal("posta", env["POSTA_BLOB_S3_BUCKET"]);
+ Assert.Same(s3AccessKey.Resource, env["POSTA_BLOB_S3_ACCESS_KEY"]);
+ Assert.Same(s3SecretKey.Resource, env["POSTA_BLOB_S3_SECRET_KEY"]);
+ Assert.Equal("false", env["POSTA_BLOB_S3_USE_SSL"]);
+ Assert.Equal("true", env["POSTA_BLOB_S3_PATH_STYLE"]);
+ Assert.Equal("/posta/attachments", env["POSTA_BLOB_FS_PATH"]);
+ Assert.Same(encryptionKey.Resource, env["POSTA_ENCRYPTION_KEY"]);
+ Assert.Equal("smtp.example.com", env["POSTA_SYSTEM_SMTP_HOST"]);
+ Assert.Equal("2525", env["POSTA_SYSTEM_SMTP_PORT"]);
+ Assert.Equal("notifications@example.com", env["POSTA_SYSTEM_SMTP_USERNAME"]);
+ Assert.Same(smtpPassword.Resource, env["POSTA_SYSTEM_SMTP_PASSWORD"]);
+ Assert.Equal("notifications@example.com", env["POSTA_SYSTEM_SMTP_FROM"]);
+ Assert.Equal("ssl", env["POSTA_SYSTEM_SMTP_ENCRYPTION"]);
+ Assert.Equal("true", env["POSTA_INBOUND_ENABLED"]);
+ Assert.Equal("127.0.0.1", env["POSTA_INBOUND_SMTP_HOST"]);
+ Assert.Equal("2526", env["POSTA_INBOUND_SMTP_PORT"]);
+ Assert.Equal("mx.example.com", env["POSTA_INBOUND_HOSTNAME"]);
+ Assert.Equal("12345", env["POSTA_INBOUND_MAX_MESSAGE_SIZE"]);
+ Assert.Equal("6789", env["POSTA_INBOUND_MAX_ATTACH_SIZE"]);
+ Assert.Same(inboundWebhookSecret.Resource, env["POSTA_INBOUND_WEBHOOK_SECRET"]);
+ Assert.Equal("starttls", env["POSTA_INBOUND_TLS_MODE"]);
+ Assert.Equal("/certs/fullchain.pem", env["POSTA_INBOUND_TLS_CERT_FILE"]);
+ Assert.Equal("/certs/privkey.pem", env["POSTA_INBOUND_TLS_KEY_FILE"]);
+ Assert.Equal("10", env["POSTA_INBOUND_SMTP_RATE_LIMIT"]);
+ Assert.Equal("20", env["POSTA_INBOUND_SMTP_RATE_WINDOW"]);
+ Assert.Equal("true", env["POSTA_EMAIL_VERIFICATION_REQUIRED"]);
+ Assert.Equal("false", env["POSTA_AUTO_SUPPRESS_ON_REJECT"]);
+ Assert.Equal("false", env["POSTA_EMAIL_VERIFY_ENABLED"]);
+ Assert.Equal("12", env["POSTA_EMAIL_VERIFY_CACHE_TTL_HOURS"]);
+ Assert.Equal("6", env["POSTA_EMAIL_VERIFY_MX_CACHE_TTL_HOURS"]);
+ Assert.Equal("42", env["POSTA_EMAIL_VERIFY_RATE_HOURLY"]);
+ Assert.Equal("true", env["POSTA_ALLOW_DOWNGRADE"]);
+ Assert.Equal("true", env["POSTA_PLAN_ENFORCEMENT"]);
+ Assert.Same(databaseUrl.Resource, env["POSTA_DB_URL"]);
+
+ Assert.True(env.ContainsKey("POSTA_DB_HOST"));
+ Assert.True(env.ContainsKey("POSTA_DB_PORT"));
+ Assert.True(env.ContainsKey("POSTA_DB_USER"));
+ Assert.True(env.ContainsKey("POSTA_DB_PASSWORD"));
+ Assert.Equal("posta", env["POSTA_DB_NAME"]);
+ Assert.Equal("disable", env["POSTA_DB_SSL_MODE"]);
+ Assert.True(env.ContainsKey("POSTA_REDIS_ADDR"));
+ Assert.Equal("redis.example.com:6379", env["POSTA_REDIS_ADDR"]);
+ Assert.Same(redisPassword.Resource, env["POSTA_REDIS_PASSWORD"]);
+ }
+
+ [Fact]
+ public async Task WithGroupedOptionsConfiguresParameterBasedEnvironmentVariables()
+ {
+ var builder = DistributedApplication.CreateBuilder();
+ var (database, redis) = AddPostaDependencies(builder);
+ var parameters = new Dictionary>();
+
+ IResourceBuilder Parameter(string name, string value)
+ {
+ var parameter = builder.AddParameter($"posta-{name}", value);
+ parameters[name] = parameter;
+ return parameter;
+ }
+
+ var posta = builder.AddPosta("posta", database, redis)
+ .WithSystemSmtp(new PostaSystemSmtpOptions
+ {
+ Host = Parameter("system-smtp-host", "smtp.example.com"),
+ Port = Parameter("system-smtp-port", "2525"),
+ Username = Parameter("system-smtp-username", "notifications@example.com"),
+ Password = Parameter("system-smtp-password", "smtp-secret"),
+ From = Parameter("system-smtp-from", "notifications@example.com"),
+ Encryption = Parameter("system-smtp-encryption", "ssl")
+ })
+ .WithInboundSmtp(new PostaInboundSmtpOptions
+ {
+ Enabled = Parameter("inbound-enabled", "true"),
+ Host = Parameter("inbound-smtp-host", "127.0.0.1"),
+ Port = Parameter("inbound-smtp-port", "2526"),
+ Hostname = Parameter("inbound-hostname", "mx.example.com"),
+ MaxMessageSize = Parameter("inbound-max-message-size", "12345"),
+ MaxAttachmentSize = Parameter("inbound-max-attachment-size", "6789"),
+ WebhookSecret = Parameter("inbound-webhook-secret", "inbound-secret"),
+ TlsMode = Parameter("inbound-tls-mode", "starttls"),
+ TlsCertFile = Parameter("inbound-tls-cert-file", "/certs/fullchain.pem"),
+ TlsKeyFile = Parameter("inbound-tls-key-file", "/certs/privkey.pem"),
+ RateLimit = Parameter("inbound-smtp-rate-limit", "10"),
+ RateWindow = Parameter("inbound-smtp-rate-window", "20")
+ })
+ .WithS3BlobStorage(new PostaS3BlobStorageOptions
+ {
+ Endpoint = Parameter("blob-s3-endpoint", "https://s3.example.com"),
+ Region = Parameter("blob-s3-region", "eu-central-1"),
+ Bucket = Parameter("blob-s3-bucket", "posta"),
+ AccessKey = Parameter("blob-s3-access-key", "s3-access"),
+ SecretKey = Parameter("blob-s3-secret-key", "s3-secret"),
+ UseSsl = Parameter("blob-s3-use-ssl", "false"),
+ PathStyle = Parameter("blob-s3-path-style", "true")
+ })
+ .WithGoogleOAuth(new PostaGoogleOAuthOptions
+ {
+ ClientId = Parameter("google-oauth-client-id", "google-client-id"),
+ ClientSecret = Parameter("google-oauth-client-secret", "google-secret"),
+ CallbackUrl = Parameter("oauth-callback-url", "https://posta.example.com/oauth/callback")
+ })
+ .WithEmailVerification(new PostaEmailVerificationOptions
+ {
+ Required = Parameter("email-verification-required", "true"),
+ AutoSuppressOnReject = Parameter("auto-suppress-on-reject", "false"),
+ Enabled = Parameter("email-verify-enabled", "false"),
+ CacheTtlHours = Parameter("email-verify-cache-ttl-hours", "12"),
+ MxCacheTtlHours = Parameter("email-verify-mx-cache-ttl-hours", "6"),
+ RateHourly = Parameter("email-verify-rate-hourly", "42")
+ });
+
+ Assert.True(posta.Resource.TryGetAnnotationsOfType(out var annotations));
+
+ var context = new EnvironmentCallbackContext(
+ new DistributedApplicationExecutionContext(
+ new DistributedApplicationExecutionContextOptions(DistributedApplicationOperation.Run)));
+
+ foreach (var annotation in annotations)
+ {
+ await annotation.Callback(context);
+ }
+
+ var env = context.EnvironmentVariables;
+
+ Assert.Equal("s3", env["POSTA_BLOB_PROVIDER"]);
+ foreach (var (parameterName, environmentVariableName) in new (string ParameterName, string EnvironmentVariableName)[]
+ {
+ ("system-smtp-host", "POSTA_SYSTEM_SMTP_HOST"),
+ ("system-smtp-port", "POSTA_SYSTEM_SMTP_PORT"),
+ ("system-smtp-username", "POSTA_SYSTEM_SMTP_USERNAME"),
+ ("system-smtp-password", "POSTA_SYSTEM_SMTP_PASSWORD"),
+ ("system-smtp-from", "POSTA_SYSTEM_SMTP_FROM"),
+ ("system-smtp-encryption", "POSTA_SYSTEM_SMTP_ENCRYPTION"),
+ ("inbound-enabled", "POSTA_INBOUND_ENABLED"),
+ ("inbound-smtp-host", "POSTA_INBOUND_SMTP_HOST"),
+ ("inbound-smtp-port", "POSTA_INBOUND_SMTP_PORT"),
+ ("inbound-hostname", "POSTA_INBOUND_HOSTNAME"),
+ ("inbound-max-message-size", "POSTA_INBOUND_MAX_MESSAGE_SIZE"),
+ ("inbound-max-attachment-size", "POSTA_INBOUND_MAX_ATTACH_SIZE"),
+ ("inbound-webhook-secret", "POSTA_INBOUND_WEBHOOK_SECRET"),
+ ("inbound-tls-mode", "POSTA_INBOUND_TLS_MODE"),
+ ("inbound-tls-cert-file", "POSTA_INBOUND_TLS_CERT_FILE"),
+ ("inbound-tls-key-file", "POSTA_INBOUND_TLS_KEY_FILE"),
+ ("inbound-smtp-rate-limit", "POSTA_INBOUND_SMTP_RATE_LIMIT"),
+ ("inbound-smtp-rate-window", "POSTA_INBOUND_SMTP_RATE_WINDOW"),
+ ("blob-s3-endpoint", "POSTA_BLOB_S3_ENDPOINT"),
+ ("blob-s3-region", "POSTA_BLOB_S3_REGION"),
+ ("blob-s3-bucket", "POSTA_BLOB_S3_BUCKET"),
+ ("blob-s3-access-key", "POSTA_BLOB_S3_ACCESS_KEY"),
+ ("blob-s3-secret-key", "POSTA_BLOB_S3_SECRET_KEY"),
+ ("blob-s3-use-ssl", "POSTA_BLOB_S3_USE_SSL"),
+ ("blob-s3-path-style", "POSTA_BLOB_S3_PATH_STYLE"),
+ ("google-oauth-client-id", "POSTA_GOOGLE_OAUTH_CLIENT_ID"),
+ ("google-oauth-client-secret", "POSTA_GOOGLE_OAUTH_CLIENT_SECRET"),
+ ("oauth-callback-url", "POSTA_OAUTH_CALLBACK_URL"),
+ ("email-verification-required", "POSTA_EMAIL_VERIFICATION_REQUIRED"),
+ ("auto-suppress-on-reject", "POSTA_AUTO_SUPPRESS_ON_REJECT"),
+ ("email-verify-enabled", "POSTA_EMAIL_VERIFY_ENABLED"),
+ ("email-verify-cache-ttl-hours", "POSTA_EMAIL_VERIFY_CACHE_TTL_HOURS"),
+ ("email-verify-mx-cache-ttl-hours", "POSTA_EMAIL_VERIFY_MX_CACHE_TTL_HOURS"),
+ ("email-verify-rate-hourly", "POSTA_EMAIL_VERIFY_RATE_HOURLY")
+ })
+ {
+ Assert.Same(parameters[parameterName].Resource, env[environmentVariableName]);
+ }
+ }
+
+ [Fact]
+ public async Task WithGroupedOptionCallbacksConfigureParameterBasedEnvironmentVariables()
+ {
+ var builder = DistributedApplication.CreateBuilder();
+ var (database, redis) = AddPostaDependencies(builder);
+ var smtpHost = builder.AddParameter("posta-smtp-host", "smtp.example.com");
+ var inboundEnabled = builder.AddParameter("posta-inbound-enabled", "true");
+ var s3Bucket = builder.AddParameter("posta-s3-bucket", "posta");
+ var googleClientId = builder.AddParameter("posta-google-client-id", "google-client-id");
+ var emailVerificationRequired = builder.AddParameter("posta-email-verification-required", "true");
+
+ var posta = builder.AddPosta("posta", database, redis)
+ .WithSystemSmtp(options => options.Host = smtpHost)
+ .WithInboundSmtp(options => options.Enabled = inboundEnabled)
+ .WithS3BlobStorage(options => options.Bucket = s3Bucket)
+ .WithGoogleOAuth(options => options.ClientId = googleClientId)
+ .WithEmailVerification(options => options.Required = emailVerificationRequired);
+
+ Assert.True(posta.Resource.TryGetAnnotationsOfType(out var annotations));
+
+ var context = new EnvironmentCallbackContext(
+ new DistributedApplicationExecutionContext(
+ new DistributedApplicationExecutionContextOptions(DistributedApplicationOperation.Run)));
+
+ foreach (var annotation in annotations)
+ {
+ await annotation.Callback(context);
+ }
+
+ var env = context.EnvironmentVariables;
+
+ Assert.Same(smtpHost.Resource, env["POSTA_SYSTEM_SMTP_HOST"]);
+ Assert.Same(inboundEnabled.Resource, env["POSTA_INBOUND_ENABLED"]);
+ Assert.Equal("s3", env["POSTA_BLOB_PROVIDER"]);
+ Assert.Same(s3Bucket.Resource, env["POSTA_BLOB_S3_BUCKET"]);
+ Assert.Same(googleClientId.Resource, env["POSTA_GOOGLE_OAUTH_CLIENT_ID"]);
+ Assert.Same(emailVerificationRequired.Resource, env["POSTA_EMAIL_VERIFICATION_REQUIRED"]);
+ }
+
+ private static (IResourceBuilder Database, IResourceBuilder Redis) AddPostaDependencies(IDistributedApplicationBuilder builder)
+ {
+ var postgres = builder.AddPostgres("postgres");
+ var database = postgres.AddDatabase("posta-db", "posta");
+ var redis = builder.AddRedis("redis");
+
+ return (database, redis);
+ }
+}