From ec696371b0c47c089353bfc12633c02ebf7d3866 Mon Sep 17 00:00:00 2001 From: axi Date: Wed, 8 Jul 2026 14:01:32 +0300 Subject: [PATCH 01/28] Add Posta with PostgreSQL and Redis integration - Introduced `CommunityToolkit.Aspire.Hosting.Posta` project for integrating Posta with PostgreSQL and Redis. - Added extension methods for configuring Posta containers, health checks, and resource dependencies. - Created test projects for validating Posta container configuration and health checks. - Added example projects under `examples/posta` showcasing Posta integration with PostgreSQL and Redis. - Updated solution file and package references to include the new Posta project. --- CommunityToolkit.Aspire.slnx | 7 +- README.md | 6 + ...oolkit.Aspire.Hosting.Posta.AppHost.csproj | 17 ++ .../Program.cs | 12 + ...mmunityToolkit.Aspire.Hosting.Posta.csproj | 19 ++ .../PostaResource.cs | 61 +++++ .../README.md | 63 +++++ .../AppHostTests.cs | 26 ++ ...yToolkit.Aspire.Hosting.Posta.Tests.csproj | 14 + .../ContainerResourceCreationTests.cs | 257 ++++++++++++++++++ 10 files changed, 481 insertions(+), 1 deletion(-) create mode 100644 examples/posta/CommunityToolkit.Aspire.Hosting.Posta.AppHost/CommunityToolkit.Aspire.Hosting.Posta.AppHost.csproj create mode 100644 examples/posta/CommunityToolkit.Aspire.Hosting.Posta.AppHost/Program.cs create mode 100644 src/CommunityToolkit.Aspire.Hosting.Posta/CommunityToolkit.Aspire.Hosting.Posta.csproj create mode 100644 src/CommunityToolkit.Aspire.Hosting.Posta/PostaResource.cs create mode 100644 src/CommunityToolkit.Aspire.Hosting.Posta/README.md create mode 100644 tests/CommunityToolkit.Aspire.Hosting.Posta.Tests/AppHostTests.cs create mode 100644 tests/CommunityToolkit.Aspire.Hosting.Posta.Tests/CommunityToolkit.Aspire.Hosting.Posta.Tests.csproj create mode 100644 tests/CommunityToolkit.Aspire.Hosting.Posta.Tests/ContainerResourceCreationTests.cs diff --git a/CommunityToolkit.Aspire.slnx b/CommunityToolkit.Aspire.slnx index f05008d80..3c8374f7c 100644 --- a/CommunityToolkit.Aspire.slnx +++ b/CommunityToolkit.Aspire.slnx @@ -152,6 +152,9 @@ + + + @@ -265,6 +268,7 @@ + @@ -339,6 +343,7 @@ + @@ -379,4 +384,4 @@ - \ No newline at end of file + 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..d514c965a --- /dev/null +++ b/examples/posta/CommunityToolkit.Aspire.Hosting.Posta.AppHost/Program.cs @@ -0,0 +1,12 @@ +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") + .WithReference(database) + .WithReference(redis); + +builder.Build().Run(); 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/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/README.md b/src/CommunityToolkit.Aspire.Hosting.Posta/README.md new file mode 100644 index 000000000..27e16b022 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.Posta/README.md @@ -0,0 +1,63 @@ +# 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 +``` + +## Usage example + +```csharp +var builder = DistributedApplication.CreateBuilder(args); + +var postgres = builder.AddPostgres("postgres"); +var database = postgres.AddDatabase("posta-db", "posta"); +var redis = builder.AddRedis("redis"); + +var posta = builder.AddPosta("posta", options => + { + options.Environment = "production"; + options.WebUrl = "https://posta.example.com"; + options.CorsOrigins = "https://app.example.com"; + options.MetricsEnabled = true; + }) + .WithReference(database) + .WithReference(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. Use `WithReference` to connect Posta to Aspire-managed PostgreSQL and Redis resources. For externally managed infrastructure, configure `PostaOptions.DatabaseUrl`, `PostaOptions.RedisAddress`, and `PostaOptions.RedisPassword`. + +Use the `PostaOptions` callback to configure Posta environment variables such as rate limits, web URLs, OAuth, blob storage, system SMTP, inbound email, email verification, and advanced switches. Secret values such as SMTP passwords, S3 keys, OAuth client secrets, encryption keys, inbound webhook secrets, `POSTA_DB_URL`, and Redis password are represented as Aspire parameters. + +## 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..5e445b6ad --- /dev/null +++ b/tests/CommunityToolkit.Aspire.Hosting.Posta.Tests/ContainerResourceCreationTests.cs @@ -0,0 +1,257 @@ +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")); + Assert.Equal("builder", exception.ParamName); + } + + [Fact] + public void AddPostaThrowsWhenNameIsNull() + { + var builder = DistributedApplication.CreateBuilder(); + + Assert.Throws(() => builder.AddPosta(null!)); + } + + [Fact] + public void WithReferenceThrowsWhenDatabaseIsNull() + { + var builder = DistributedApplication.CreateBuilder(); + var posta = builder.AddPosta("posta"); + + Assert.Throws(() => posta.WithReference((IResourceBuilder)null!)); + } + + [Fact] + public void WithReferenceThrowsWhenRedisIsNull() + { + var builder = DistributedApplication.CreateBuilder(); + var posta = builder.AddPosta("posta"); + + Assert.Throws(() => posta.WithReference((IResourceBuilder)null!)); + } + + [Fact] + public void AddPostaSetsContainerDetailsOnResource() + { + var builder = DistributedApplication.CreateBuilder(); + + builder.AddPosta("posta"); + + 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(); + + builder.AddPosta("posta", 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(); + + builder.AddPosta("posta"); + + 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 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", 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(database) + .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.NotEqual("redis.example.com:6379", env["POSTA_REDIS_ADDR"].ToString()); + Assert.Same(redisPassword.Resource, env["POSTA_REDIS_PASSWORD"]); + } +} \ No newline at end of file From aebb0b449acc194ac044614c28731690f45a1250 Mon Sep 17 00:00:00 2001 From: axi Date: Wed, 8 Jul 2026 14:02:01 +0300 Subject: [PATCH 02/28] Add Posta with hosting extensions - Introduced `CommunityToolkit.Aspire.Hosting.Posta` project for integrating Posta as a containerized resource. - Added extension methods for configuring Posta containers, including support for PostgreSQL, Redis, and advanced environment options. - Defined Posta resource options, container image tags, and health check configurations. --- .../PostaContainerImageTags.cs | 13 + .../PostaHostingExtensions.cs | 271 ++++++++++++++++ .../PostaOptions.cs | 304 ++++++++++++++++++ 3 files changed, 588 insertions(+) create mode 100644 src/CommunityToolkit.Aspire.Hosting.Posta/PostaContainerImageTags.cs create mode 100644 src/CommunityToolkit.Aspire.Hosting.Posta/PostaHostingExtensions.cs create mode 100644 src/CommunityToolkit.Aspire.Hosting.Posta/PostaOptions.cs 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/PostaHostingExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Posta/PostaHostingExtensions.cs new file mode 100644 index 000000000..af2552344 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.Posta/PostaHostingExtensions.cs @@ -0,0 +1,271 @@ +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 . + /// + /// Adds a Posta container resource + /// The to which the Posta resource will be added. + /// The name of the Posta container resource. + /// 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] + public static IResourceBuilder AddPosta( + this IDistributedApplicationBuilder builder, + [ResourceName] string name, + IResourceBuilder? jwtSecret = null, + IResourceBuilder? adminPassword = null, + string adminEmail = "admin@example.com", + int? port = null) + { + return AddPostaCore(builder, name, jwtSecret, adminPassword, adminEmail, port, configureOptions: null); + } + + /// + /// Adds a Posta container resource to the with additional Posta environment configuration. + /// + /// The to which the Posta resource will be added. + /// The name of the Posta container resource. + /// 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 parameter-based overload instead.")] + public static IResourceBuilder AddPosta( + this IDistributedApplicationBuilder builder, + [ResourceName] string name, + Action configureOptions, + IResourceBuilder? jwtSecret = null, + IResourceBuilder? adminPassword = null, + string adminEmail = "admin@example.com", + int? port = null) + { + ArgumentNullException.ThrowIfNull(configureOptions); + + return AddPostaCore(builder, name, jwtSecret, adminPassword, adminEmail, port, configureOptions); + } + + /// + /// 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; + + context.EnvironmentVariables["POSTA_DB_HOST"] = postgres.PrimaryEndpoint.Property(EndpointProperty.Host); + context.EnvironmentVariables["POSTA_DB_PORT"] = postgres.PrimaryEndpoint.Property(EndpointProperty.Port); + context.EnvironmentVariables["POSTA_DB_USER"] = postgres.UserNameReference; + context.EnvironmentVariables["POSTA_DB_PASSWORD"] = postgres.PasswordParameter; + context.EnvironmentVariables["POSTA_DB_NAME"] = database.Resource.DatabaseName; + context.EnvironmentVariables["POSTA_DB_SSL_MODE"] = "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"); + + context.EnvironmentVariables["POSTA_REDIS_ADDR"] = ReferenceExpression.Create($"{redisEndpoint.Property(EndpointProperty.Host)}:{redisEndpoint.Property(EndpointProperty.Port)}"); + if (redisPassword is not null) + { + context.EnvironmentVariables["POSTA_REDIS_PASSWORD"] = redisPassword.Resource; + } + else if (redisResource.PasswordParameter is not null) + { + context.EnvironmentVariables["POSTA_REDIS_PASSWORD"] = redisResource.PasswordParameter; + } + }) + .WaitFor(redis); + } + + private static IResourceBuilder AddPostaCore( + IDistributedApplicationBuilder builder, + string name, + IResourceBuilder? jwtSecret, + IResourceBuilder? adminPassword, + string adminEmail, + int? port, + Action? configureOptions) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentException.ThrowIfNullOrWhiteSpace(name); + ArgumentException.ThrowIfNullOrWhiteSpace(adminEmail); + + var options = new PostaOptions(); + configureOptions?.Invoke(options); + + 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 => + { + context.EnvironmentVariables["POSTA_PORT"] = PostaResource.HttpEndpointPort.ToString(); + SetParameter(context, "POSTA_DB_URL", options.DatabaseUrl); + SetIfNotNull(context, "POSTA_REDIS_ADDR", options.RedisAddress); + if (options.RedisPassword is not null) + { + context.EnvironmentVariables["POSTA_REDIS_PASSWORD"] = options.RedisPassword.Resource; + } + context.EnvironmentVariables["POSTA_JWT_SECRET"] = resource.JwtSecretParameter; + context.EnvironmentVariables["POSTA_ADMIN_EMAIL"] = adminEmail; + context.EnvironmentVariables["POSTA_ADMIN_PASSWORD"] = 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, "POSTA_ENV", options.Environment); + Set(context, "POSTA_DEV_MODE", options.DevMode); + Set(context, "POSTA_AUTH_RATE_LIMIT_ENABLED", options.AuthRateLimitEnabled); + Set(context, "POSTA_RATE_LIMIT_HOURLY", options.RateLimitHourly); + Set(context, "POSTA_RATE_LIMIT_DAILY", options.RateLimitDaily); + Set(context, "POSTA_OPENAPI_DOCS", options.OpenApiDocs); + Set(context, "POSTA_METRICS_ENABLED", options.MetricsEnabled); + SetIfNotNull(context, "POSTA_WEB_DIR", options.WebDir); + SetIfNotNull(context, "POSTA_WEB_URL", options.WebUrl); + SetIfNotNull(context, "POSTA_API_URL", options.ApiUrl); + Set(context, "POSTA_CORS_ORIGINS", options.CorsOrigins); + Set(context, "POSTA_EMBEDDED_WORKER", options.EmbeddedWorker); + Set(context, "POSTA_WORKER_CONCURRENCY", options.WorkerConcurrency); + Set(context, "POSTA_WORKER_MAX_RETRIES", options.WorkerMaxRetries); + Set(context, "POSTA_WEBHOOK_MAX_RETRIES", options.WebhookMaxRetries); + Set(context, "POSTA_WEBHOOK_TIMEOUT_SECS", options.WebhookTimeoutSeconds); + SetIfNotNull(context, "POSTA_WEBHOOK_PROXY_URL", options.WebhookProxyUrl); + SetIfNotNull(context, "POSTA_GOOGLE_OAUTH_CLIENT_ID", options.GoogleOAuthClientId); + SetParameter(context, "POSTA_GOOGLE_OAUTH_CLIENT_SECRET", options.GoogleOAuthClientSecret); + SetIfNotNull(context, "POSTA_OAUTH_CALLBACK_URL", options.OAuthCallbackUrl); + SetIfNotNull(context, "POSTA_BLOB_PROVIDER", options.BlobProvider); + SetIfNotNull(context, "POSTA_BLOB_S3_ENDPOINT", options.BlobS3Endpoint); + Set(context, "POSTA_BLOB_S3_REGION", options.BlobS3Region); + SetIfNotNull(context, "POSTA_BLOB_S3_BUCKET", options.BlobS3Bucket); + SetParameter(context, "POSTA_BLOB_S3_ACCESS_KEY", options.BlobS3AccessKey); + SetParameter(context, "POSTA_BLOB_S3_SECRET_KEY", options.BlobS3SecretKey); + Set(context, "POSTA_BLOB_S3_USE_SSL", options.BlobS3UseSsl); + Set(context, "POSTA_BLOB_S3_PATH_STYLE", options.BlobS3PathStyle); + Set(context, "POSTA_BLOB_FS_PATH", options.BlobFileSystemPath); + SetParameter(context, "POSTA_ENCRYPTION_KEY", options.EncryptionKey); + SetIfNotNull(context, "POSTA_SYSTEM_SMTP_HOST", options.SystemSmtpHost); + Set(context, "POSTA_SYSTEM_SMTP_PORT", options.SystemSmtpPort); + SetIfNotNull(context, "POSTA_SYSTEM_SMTP_USERNAME", options.SystemSmtpUsername); + SetParameter(context, "POSTA_SYSTEM_SMTP_PASSWORD", options.SystemSmtpPassword); + SetIfNotNull(context, "POSTA_SYSTEM_SMTP_FROM", options.SystemSmtpFrom); + Set(context, "POSTA_SYSTEM_SMTP_ENCRYPTION", options.SystemSmtpEncryption); + Set(context, "POSTA_INBOUND_ENABLED", options.InboundEnabled); + Set(context, "POSTA_INBOUND_SMTP_HOST", options.InboundSmtpHost); + Set(context, "POSTA_INBOUND_SMTP_PORT", options.InboundSmtpPort); + Set(context, "POSTA_INBOUND_HOSTNAME", options.InboundHostname); + Set(context, "POSTA_INBOUND_MAX_MESSAGE_SIZE", options.InboundMaxMessageSize); + Set(context, "POSTA_INBOUND_MAX_ATTACH_SIZE", options.InboundMaxAttachSize); + SetParameter(context, "POSTA_INBOUND_WEBHOOK_SECRET", options.InboundWebhookSecret); + Set(context, "POSTA_INBOUND_TLS_MODE", options.InboundTlsMode); + SetIfNotNull(context, "POSTA_INBOUND_TLS_CERT_FILE", options.InboundTlsCertFile); + SetIfNotNull(context, "POSTA_INBOUND_TLS_KEY_FILE", options.InboundTlsKeyFile); + Set(context, "POSTA_INBOUND_SMTP_RATE_LIMIT", options.InboundSmtpRateLimit); + Set(context, "POSTA_INBOUND_SMTP_RATE_WINDOW", options.InboundSmtpRateWindow); + Set(context, "POSTA_EMAIL_VERIFICATION_REQUIRED", options.EmailVerificationRequired); + Set(context, "POSTA_AUTO_SUPPRESS_ON_REJECT", options.AutoSuppressOnReject); + Set(context, "POSTA_EMAIL_VERIFY_ENABLED", options.EmailVerifyEnabled); + Set(context, "POSTA_EMAIL_VERIFY_CACHE_TTL_HOURS", options.EmailVerifyCacheTtlHours); + Set(context, "POSTA_EMAIL_VERIFY_MX_CACHE_TTL_HOURS", options.EmailVerifyMxCacheTtlHours); + Set(context, "POSTA_EMAIL_VERIFY_RATE_HOURLY", options.EmailVerifyRateHourly); + Set(context, "POSTA_ALLOW_DOWNGRADE", options.AllowDowngrade); + Set(context, "POSTA_PLAN_ENFORCEMENT", options.PlanEnforcement); + } + + private static void Set(EnvironmentCallbackContext context, string name, string value) + { + context.EnvironmentVariables[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/PostaOptions.cs b/src/CommunityToolkit.Aspire.Hosting.Posta/PostaOptions.cs new file mode 100644 index 000000000..6ef26516a --- /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; } +} \ No newline at end of file From 31bac3ccdfa79a097ebbb4c0bc34a7bfadc55614 Mon Sep 17 00:00:00 2001 From: axi Date: Wed, 8 Jul 2026 15:59:04 +0300 Subject: [PATCH 03/28] Simplify Posta integration by directly passing database and Redis resources to `AddPosta`. Refine README to reflect updated API and usage. --- src/CommunityToolkit.Aspire.Hosting.Posta/README.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/CommunityToolkit.Aspire.Hosting.Posta/README.md b/src/CommunityToolkit.Aspire.Hosting.Posta/README.md index 27e16b022..db82005ad 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Posta/README.md +++ b/src/CommunityToolkit.Aspire.Hosting.Posta/README.md @@ -19,15 +19,13 @@ var postgres = builder.AddPostgres("postgres"); var database = postgres.AddDatabase("posta-db", "posta"); var redis = builder.AddRedis("redis"); -var posta = builder.AddPosta("posta", options => +var posta = builder.AddPosta("posta", database, redis, options => { options.Environment = "production"; options.WebUrl = "https://posta.example.com"; options.CorsOrigins = "https://app.example.com"; options.MetricsEnabled = true; - }) - .WithReference(database) - .WithReference(redis); + }); builder.AddProject("api") .WithReference(posta) @@ -36,7 +34,7 @@ builder.AddProject("api") builder.Build().Run(); ``` -Posta stores data in PostgreSQL, uses Redis for queueing, and runs the embedded worker in the main container by default. Use `WithReference` to connect Posta to Aspire-managed PostgreSQL and Redis resources. For externally managed infrastructure, configure `PostaOptions.DatabaseUrl`, `PostaOptions.RedisAddress`, and `PostaOptions.RedisPassword`. +Posta stores data in PostgreSQL, uses Redis for queueing, and runs the embedded worker in the main container by default. Pass Aspire-managed PostgreSQL and Redis resources to `AddPosta`, or use `WithReference` to attach them fluently. For externally managed infrastructure, configure `PostaOptions.DatabaseUrl`, `PostaOptions.RedisAddress`, and `PostaOptions.RedisPassword`. Use the `PostaOptions` callback to configure Posta environment variables such as rate limits, web URLs, OAuth, blob storage, system SMTP, inbound email, email verification, and advanced switches. Secret values such as SMTP passwords, S3 keys, OAuth client secrets, encryption keys, inbound webhook secrets, `POSTA_DB_URL`, and Redis password are represented as Aspire parameters. From e59b9aeb0257e1cdd9805905059c7b2285aba88c Mon Sep 17 00:00:00 2001 From: axi Date: Wed, 8 Jul 2026 15:59:13 +0300 Subject: [PATCH 04/28] Add comprehensive tests for `AddPosta` with database and Redis references - Introduced tests to validate `AddPosta` behavior with null database and Redis inputs, ensuring proper exception handling. - Added test to confirm PostgreSQL and Redis configuration via environment variables. --- .../ContainerResourceCreationTests.cs | 53 ++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/tests/CommunityToolkit.Aspire.Hosting.Posta.Tests/ContainerResourceCreationTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Posta.Tests/ContainerResourceCreationTests.cs index 5e445b6ad..19ee38960 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.Posta.Tests/ContainerResourceCreationTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.Posta.Tests/ContainerResourceCreationTests.cs @@ -40,6 +40,25 @@ public void WithReferenceThrowsWhenRedisIsNull() Assert.Throws(() => posta.WithReference((IResourceBuilder)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() { @@ -92,6 +111,38 @@ public void AddPostaRegistersHealthChecks() 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() { @@ -254,4 +305,4 @@ public async Task AddPostaConfiguresPostaEnvironmentVariables() Assert.NotEqual("redis.example.com:6379", env["POSTA_REDIS_ADDR"].ToString()); Assert.Same(redisPassword.Resource, env["POSTA_REDIS_PASSWORD"]); } -} \ No newline at end of file +} From aed268bbea5d4edadf36c09cc98ee8cc82b89294 Mon Sep 17 00:00:00 2001 From: axi Date: Wed, 8 Jul 2026 15:59:20 +0300 Subject: [PATCH 05/28] Simplify `AddPosta` API by directly passing database and Redis references. --- .../CommunityToolkit.Aspire.Hosting.Posta.AppHost/Program.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/examples/posta/CommunityToolkit.Aspire.Hosting.Posta.AppHost/Program.cs b/examples/posta/CommunityToolkit.Aspire.Hosting.Posta.AppHost/Program.cs index d514c965a..50c3cce45 100644 --- a/examples/posta/CommunityToolkit.Aspire.Hosting.Posta.AppHost/Program.cs +++ b/examples/posta/CommunityToolkit.Aspire.Hosting.Posta.AppHost/Program.cs @@ -5,8 +5,6 @@ var database = postgres.AddDatabase("posta-db", "posta"); var redis = builder.AddRedis("redis"); -builder.AddPosta("posta") - .WithReference(database) - .WithReference(redis); +builder.AddPosta("posta", database, redis); builder.Build().Run(); From 63d69007cb297824f7b87e849d0387448990dec2 Mon Sep 17 00:00:00 2001 From: axi Date: Wed, 8 Jul 2026 15:59:26 +0300 Subject: [PATCH 06/28] Add null checks for database, Redis, and configuration options in `AddPosta` methods. Extend methods to support chained references. --- .../PostaHostingExtensions.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/CommunityToolkit.Aspire.Hosting.Posta/PostaHostingExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Posta/PostaHostingExtensions.cs index af2552344..8452a9dc7 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Posta/PostaHostingExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Posta/PostaHostingExtensions.cs @@ -31,6 +31,15 @@ public static IResourceBuilder AddPosta( return AddPostaCore(builder, name, jwtSecret, adminPassword, adminEmail, port, configureOptions: null); } + { + ArgumentNullException.ThrowIfNull(database); + ArgumentNullException.ThrowIfNull(redis); + + return AddPostaCore(builder, name, jwtSecret, adminPassword, adminEmail, port, configureOptions: null) + .WithReference(database) + .WithReference(redis); + } + /// /// Adds a Posta container resource to the with additional Posta environment configuration. /// @@ -57,6 +66,16 @@ public static IResourceBuilder AddPosta( return AddPostaCore(builder, name, jwtSecret, adminPassword, adminEmail, port, configureOptions); } + { + ArgumentNullException.ThrowIfNull(database); + ArgumentNullException.ThrowIfNull(redis); + ArgumentNullException.ThrowIfNull(configureOptions); + + return AddPostaCore(builder, name, jwtSecret, adminPassword, adminEmail, port, configureOptions) + .WithReference(database) + .WithReference(redis); + } + /// /// Configures the PostgreSQL database used by Posta. /// From cc5353d08bd924fd6199e77fc7f6018ff514b86c Mon Sep 17 00:00:00 2001 From: axi Date: Wed, 8 Jul 2026 15:59:31 +0300 Subject: [PATCH 07/28] Add `AddPosta` overloads for chaining PostgreSQL and Redis references with improved configuration options. --- .../PostaHostingExtensions.cs | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/src/CommunityToolkit.Aspire.Hosting.Posta/PostaHostingExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Posta/PostaHostingExtensions.cs index 8452a9dc7..fb8b8d222 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Posta/PostaHostingExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Posta/PostaHostingExtensions.cs @@ -31,6 +31,29 @@ public static IResourceBuilder AddPosta( return AddPostaCore(builder, name, jwtSecret, adminPassword, adminEmail, port, configureOptions: null); } + /// + /// 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(database); ArgumentNullException.ThrowIfNull(redis); @@ -66,6 +89,30 @@ public static IResourceBuilder AddPosta( return AddPostaCore(builder, name, jwtSecret, adminPassword, adminEmail, port, configureOptions); } + /// + /// 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 parameter-based 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(database); ArgumentNullException.ThrowIfNull(redis); From dfc273089b944a3eb430d9cc562519ddf80eff8b Mon Sep 17 00:00:00 2001 From: axi Date: Wed, 8 Jul 2026 16:07:59 +0300 Subject: [PATCH 08/28] Refactor `AddPosta` tests to include database and Redis dependencies. Remove redundant overloads in `PostaHostingExtensions`. --- .../PostaHostingExtensions.cs | 57 +------------------ .../ContainerResourceCreationTests.cs | 38 ++++++++----- 2 files changed, 25 insertions(+), 70 deletions(-) diff --git a/src/CommunityToolkit.Aspire.Hosting.Posta/PostaHostingExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Posta/PostaHostingExtensions.cs index fb8b8d222..e10588dec 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Posta/PostaHostingExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Posta/PostaHostingExtensions.cs @@ -8,29 +8,6 @@ namespace Aspire.Hosting; /// public static class PostaHostingExtensions { - /// - /// Adds a Posta container resource to the . - /// - /// Adds a Posta container resource - /// The to which the Posta resource will be added. - /// The name of the Posta container resource. - /// 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] - public static IResourceBuilder AddPosta( - this IDistributedApplicationBuilder builder, - [ResourceName] string name, - IResourceBuilder? jwtSecret = null, - IResourceBuilder? adminPassword = null, - string adminEmail = "admin@example.com", - int? port = null) - { - return AddPostaCore(builder, name, jwtSecret, adminPassword, adminEmail, port, configureOptions: null); - } - /// /// Adds a Posta container resource to the and configures PostgreSQL and Redis references. /// @@ -55,6 +32,7 @@ public static IResourceBuilder AddPosta( string adminEmail = "admin@example.com", int? port = null) { + ArgumentNullException.ThrowIfNull(builder); ArgumentNullException.ThrowIfNull(database); ArgumentNullException.ThrowIfNull(redis); @@ -63,32 +41,6 @@ public static IResourceBuilder AddPosta( .WithReference(redis); } - /// - /// Adds a Posta container resource to the with additional Posta environment configuration. - /// - /// The to which the Posta resource will be added. - /// The name of the Posta container resource. - /// 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 parameter-based overload instead.")] - public static IResourceBuilder AddPosta( - this IDistributedApplicationBuilder builder, - [ResourceName] string name, - Action configureOptions, - IResourceBuilder? jwtSecret = null, - IResourceBuilder? adminPassword = null, - string adminEmail = "admin@example.com", - int? port = null) - { - ArgumentNullException.ThrowIfNull(configureOptions); - - return AddPostaCore(builder, name, jwtSecret, adminPassword, adminEmail, port, configureOptions); - } - /// /// Adds a Posta container resource to the with PostgreSQL, Redis, and additional Posta environment configuration. /// @@ -114,6 +66,7 @@ public static IResourceBuilder AddPosta( string adminEmail = "admin@example.com", int? port = null) { + ArgumentNullException.ThrowIfNull(builder); ArgumentNullException.ThrowIfNull(database); ArgumentNullException.ThrowIfNull(redis); ArgumentNullException.ThrowIfNull(configureOptions); @@ -221,12 +174,6 @@ private static IResourceBuilder AddPostaCore( .WithEnvironment(context => { context.EnvironmentVariables["POSTA_PORT"] = PostaResource.HttpEndpointPort.ToString(); - SetParameter(context, "POSTA_DB_URL", options.DatabaseUrl); - SetIfNotNull(context, "POSTA_REDIS_ADDR", options.RedisAddress); - if (options.RedisPassword is not null) - { - context.EnvironmentVariables["POSTA_REDIS_PASSWORD"] = options.RedisPassword.Resource; - } context.EnvironmentVariables["POSTA_JWT_SECRET"] = resource.JwtSecretParameter; context.EnvironmentVariables["POSTA_ADMIN_EMAIL"] = adminEmail; context.EnvironmentVariables["POSTA_ADMIN_PASSWORD"] = resource.AdminPasswordParameter; diff --git a/tests/CommunityToolkit.Aspire.Hosting.Posta.Tests/ContainerResourceCreationTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Posta.Tests/ContainerResourceCreationTests.cs index 19ee38960..697cc5f10 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.Posta.Tests/ContainerResourceCreationTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.Posta.Tests/ContainerResourceCreationTests.cs @@ -10,7 +10,7 @@ public void AddPostaThrowsWhenBuilderIsNull() { IDistributedApplicationBuilder builder = null!; - var exception = Assert.Throws(() => builder.AddPosta("posta")); + var exception = Assert.Throws(() => builder.AddPosta("posta", null!, null!)); Assert.Equal("builder", exception.ParamName); } @@ -18,15 +18,17 @@ public void AddPostaThrowsWhenBuilderIsNull() public void AddPostaThrowsWhenNameIsNull() { var builder = DistributedApplication.CreateBuilder(); + var (database, redis) = AddPostaDependencies(builder); - Assert.Throws(() => builder.AddPosta(null!)); + Assert.Throws(() => builder.AddPosta(null!, database, redis)); } [Fact] public void WithReferenceThrowsWhenDatabaseIsNull() { var builder = DistributedApplication.CreateBuilder(); - var posta = builder.AddPosta("posta"); + var (database, redis) = AddPostaDependencies(builder); + var posta = builder.AddPosta("posta", database, redis); Assert.Throws(() => posta.WithReference((IResourceBuilder)null!)); } @@ -35,7 +37,8 @@ public void WithReferenceThrowsWhenDatabaseIsNull() public void WithReferenceThrowsWhenRedisIsNull() { var builder = DistributedApplication.CreateBuilder(); - var posta = builder.AddPosta("posta"); + var (database, redis) = AddPostaDependencies(builder); + var posta = builder.AddPosta("posta", database, redis); Assert.Throws(() => posta.WithReference((IResourceBuilder)null!)); } @@ -63,8 +66,9 @@ public void AddPostaWithReferencesThrowsWhenRedisIsNull() public void AddPostaSetsContainerDetailsOnResource() { var builder = DistributedApplication.CreateBuilder(); + var (database, redis) = AddPostaDependencies(builder); - builder.AddPosta("posta"); + builder.AddPosta("posta", database, redis); using var app = builder.Build(); var appModel = app.Services.GetRequiredService(); @@ -83,8 +87,9 @@ public void AddPostaSetsContainerDetailsOnResource() public void AddPostaSetsEndpointDetailsOnResource() { var builder = DistributedApplication.CreateBuilder(); + var (database, redis) = AddPostaDependencies(builder); - builder.AddPosta("posta", port: 9001); + builder.AddPosta("posta", database, redis, port: 9001); using var app = builder.Build(); var appModel = app.Services.GetRequiredService(); @@ -100,8 +105,9 @@ public void AddPostaSetsEndpointDetailsOnResource() public void AddPostaRegistersHealthChecks() { var builder = DistributedApplication.CreateBuilder(); + var (database, redis) = AddPostaDependencies(builder); - builder.AddPosta("posta"); + builder.AddPosta("posta", database, redis); using var app = builder.Build(); var appModel = app.Services.GetRequiredService(); @@ -150,7 +156,6 @@ public async Task AddPostaConfiguresPostaEnvironmentVariables() 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"); @@ -159,7 +164,7 @@ public async Task AddPostaConfiguresPostaEnvironmentVariables() var smtpPassword = builder.AddParameter("posta-smtp-password", "smtp-secret"); var inboundWebhookSecret = builder.AddParameter("posta-inbound-secret", "inbound-secret"); - var posta = builder.AddPosta("posta", options => + var posta = builder.AddPosta("posta", database, redis, options => { options.Environment = "dev"; options.DevMode = true; @@ -217,11 +222,7 @@ public async Task AddPostaConfiguresPostaEnvironmentVariables() options.EmailVerifyRateHourly = 42; options.AllowDowngrade = true; options.PlanEnforcement = true; - options.DatabaseUrl = databaseUrl; - options.RedisPassword = redisPassword; - options.RedisAddress = "redis.example.com:6379"; }) - .WithReference(database) .WithReference(redis, redisPassword); Assert.True(posta.Resource.TryGetAnnotationsOfType(out var annotations)); @@ -294,7 +295,6 @@ public async Task AddPostaConfiguresPostaEnvironmentVariables() 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")); @@ -302,7 +302,15 @@ public async Task AddPostaConfiguresPostaEnvironmentVariables() Assert.Equal("posta", env["POSTA_DB_NAME"]); Assert.Equal("disable", env["POSTA_DB_SSL_MODE"]); Assert.True(env.ContainsKey("POSTA_REDIS_ADDR")); - Assert.NotEqual("redis.example.com:6379", env["POSTA_REDIS_ADDR"].ToString()); Assert.Same(redisPassword.Resource, env["POSTA_REDIS_PASSWORD"]); } + + 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); + } } From 83641b7254b10d6ca96d6d29a470ab98117e116a Mon Sep 17 00:00:00 2001 From: axi Date: Wed, 8 Jul 2026 16:14:38 +0300 Subject: [PATCH 09/28] Enhance `AddPosta` tests with support for overriding `DatabaseUrl` and `RedisAddress`; refine README to clarify environment configuration usage. --- src/CommunityToolkit.Aspire.Hosting.Posta/README.md | 2 +- .../ContainerResourceCreationTests.cs | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/CommunityToolkit.Aspire.Hosting.Posta/README.md b/src/CommunityToolkit.Aspire.Hosting.Posta/README.md index db82005ad..b298dd76b 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Posta/README.md +++ b/src/CommunityToolkit.Aspire.Hosting.Posta/README.md @@ -34,7 +34,7 @@ builder.AddProject("api") builder.Build().Run(); ``` -Posta stores data in PostgreSQL, uses Redis for queueing, and runs the embedded worker in the main container by default. Pass Aspire-managed PostgreSQL and Redis resources to `AddPosta`, or use `WithReference` to attach them fluently. For externally managed infrastructure, configure `PostaOptions.DatabaseUrl`, `PostaOptions.RedisAddress`, and `PostaOptions.RedisPassword`. +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. Use `PostaOptions.DatabaseUrl`, `PostaOptions.RedisAddress`, and `PostaOptions.RedisPassword` only when you need to override the generated Posta environment values. Use the `PostaOptions` callback to configure Posta environment variables such as rate limits, web URLs, OAuth, blob storage, system SMTP, inbound email, email verification, and advanced switches. Secret values such as SMTP passwords, S3 keys, OAuth client secrets, encryption keys, inbound webhook secrets, `POSTA_DB_URL`, and Redis password are represented as Aspire parameters. diff --git a/tests/CommunityToolkit.Aspire.Hosting.Posta.Tests/ContainerResourceCreationTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Posta.Tests/ContainerResourceCreationTests.cs index 697cc5f10..be6545eb3 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.Posta.Tests/ContainerResourceCreationTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.Posta.Tests/ContainerResourceCreationTests.cs @@ -156,6 +156,7 @@ public async Task AddPostaConfiguresPostaEnvironmentVariables() 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"); @@ -222,6 +223,9 @@ public async Task AddPostaConfiguresPostaEnvironmentVariables() options.EmailVerifyRateHourly = 42; options.AllowDowngrade = true; options.PlanEnforcement = true; + options.DatabaseUrl = databaseUrl; + options.RedisPassword = redisPassword; + options.RedisAddress = "redis.example.com:6379"; }) .WithReference(redis, redisPassword); @@ -295,6 +299,7 @@ public async Task AddPostaConfiguresPostaEnvironmentVariables() 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")); @@ -302,6 +307,7 @@ public async Task AddPostaConfiguresPostaEnvironmentVariables() Assert.Equal("posta", env["POSTA_DB_NAME"]); Assert.Equal("disable", env["POSTA_DB_SSL_MODE"]); Assert.True(env.ContainsKey("POSTA_REDIS_ADDR")); + Assert.NotEqual("redis.example.com:6379", env["POSTA_REDIS_ADDR"].ToString()); Assert.Same(redisPassword.Resource, env["POSTA_REDIS_PASSWORD"]); } From 423af759b50ce2b4750eefe9d01d817f0a49de10 Mon Sep 17 00:00:00 2001 From: axi Date: Wed, 8 Jul 2026 16:14:45 +0300 Subject: [PATCH 10/28] Add overloads to `Set` method in `PostaHostingExtensions` for handling additional expression types --- .../PostaHostingExtensions.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/CommunityToolkit.Aspire.Hosting.Posta/PostaHostingExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Posta/PostaHostingExtensions.cs index e10588dec..7934535e9 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Posta/PostaHostingExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Posta/PostaHostingExtensions.cs @@ -255,6 +255,21 @@ private static void Set(EnvironmentCallbackContext context, string name, string 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 Set(EnvironmentCallbackContext context, string name, bool value) { context.EnvironmentVariables[name] = value ? "true" : "false"; From fd0be69bc49ded3719a42f0ad57ecb20f914bf45 Mon Sep 17 00:00:00 2001 From: axi Date: Wed, 8 Jul 2026 16:14:53 +0300 Subject: [PATCH 11/28] Refactor `PostaHostingExtensions` to replace direct environment variable assignments with `Set` and `SetIfNotNull` methods for improved readability and consistency. --- .../PostaHostingExtensions.cs | 29 ++++++++++--------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/src/CommunityToolkit.Aspire.Hosting.Posta/PostaHostingExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Posta/PostaHostingExtensions.cs index 7934535e9..7d296c512 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Posta/PostaHostingExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Posta/PostaHostingExtensions.cs @@ -95,12 +95,12 @@ public static IResourceBuilder WithReference( { var postgres = database.Resource.Parent; - context.EnvironmentVariables["POSTA_DB_HOST"] = postgres.PrimaryEndpoint.Property(EndpointProperty.Host); - context.EnvironmentVariables["POSTA_DB_PORT"] = postgres.PrimaryEndpoint.Property(EndpointProperty.Port); - context.EnvironmentVariables["POSTA_DB_USER"] = postgres.UserNameReference; - context.EnvironmentVariables["POSTA_DB_PASSWORD"] = postgres.PasswordParameter; - context.EnvironmentVariables["POSTA_DB_NAME"] = database.Resource.DatabaseName; - context.EnvironmentVariables["POSTA_DB_SSL_MODE"] = "disable"; + Set(context, "POSTA_DB_HOST", postgres.PrimaryEndpoint.Property(EndpointProperty.Host)); + Set(context, "POSTA_DB_PORT", postgres.PrimaryEndpoint.Property(EndpointProperty.Port)); + Set(context, "POSTA_DB_USER", postgres.UserNameReference); + Set(context, "POSTA_DB_PASSWORD", postgres.PasswordParameter); + Set(context, "POSTA_DB_NAME", database.Resource.DatabaseName); + Set(context, "POSTA_DB_SSL_MODE", "disable"); }) .WaitFor(database); } @@ -127,14 +127,14 @@ public static IResourceBuilder WithReference( var redisResource = redis.Resource; var redisEndpoint = redisResource.GetEndpoint("secondary"); - context.EnvironmentVariables["POSTA_REDIS_ADDR"] = ReferenceExpression.Create($"{redisEndpoint.Property(EndpointProperty.Host)}:{redisEndpoint.Property(EndpointProperty.Port)}"); + Set(context, "POSTA_REDIS_ADDR", ReferenceExpression.Create($"{redisEndpoint.Property(EndpointProperty.Host)}:{redisEndpoint.Property(EndpointProperty.Port)}")); if (redisPassword is not null) { - context.EnvironmentVariables["POSTA_REDIS_PASSWORD"] = redisPassword.Resource; + Set(context, "POSTA_REDIS_PASSWORD", redisPassword.Resource); } else if (redisResource.PasswordParameter is not null) { - context.EnvironmentVariables["POSTA_REDIS_PASSWORD"] = redisResource.PasswordParameter; + Set(context, "POSTA_REDIS_PASSWORD", redisResource.PasswordParameter); } }) .WaitFor(redis); @@ -173,10 +173,13 @@ private static IResourceBuilder AddPostaCore( name: PostaResource.HttpEndpointName) .WithEnvironment(context => { - context.EnvironmentVariables["POSTA_PORT"] = PostaResource.HttpEndpointPort.ToString(); - context.EnvironmentVariables["POSTA_JWT_SECRET"] = resource.JwtSecretParameter; - context.EnvironmentVariables["POSTA_ADMIN_EMAIL"] = adminEmail; - context.EnvironmentVariables["POSTA_ADMIN_PASSWORD"] = resource.AdminPasswordParameter; + Set(context, "POSTA_PORT", PostaResource.HttpEndpointPort); + SetParameter(context, "POSTA_DB_URL", options.DatabaseUrl); + SetIfNotNull(context, "POSTA_REDIS_ADDR", options.RedisAddress); + if (options.RedisPassword is not null) + Set(context, "POSTA_JWT_SECRET", resource.JwtSecretParameter); + Set(context, "POSTA_ADMIN_EMAIL", adminEmail); + Set(context, "POSTA_ADMIN_PASSWORD", resource.AdminPasswordParameter); ConfigurePostaEnvironment(context, options); }) .WithHttpHealthCheck( From 0115e17546b282174edef26d35e7818bc9412218 Mon Sep 17 00:00:00 2001 From: axi Date: Wed, 8 Jul 2026 16:15:01 +0300 Subject: [PATCH 12/28] Inject `POSTA_REDIS_PASSWORD` configuration into environment setup for Redis authentication support. --- .../PostaHostingExtensions.cs | 3 +++ src/CommunityToolkit.Aspire.Hosting.Posta/PostaOptions.cs | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/CommunityToolkit.Aspire.Hosting.Posta/PostaHostingExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Posta/PostaHostingExtensions.cs index 7d296c512..90c3e6a26 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Posta/PostaHostingExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Posta/PostaHostingExtensions.cs @@ -177,6 +177,9 @@ private static IResourceBuilder AddPostaCore( SetParameter(context, "POSTA_DB_URL", options.DatabaseUrl); SetIfNotNull(context, "POSTA_REDIS_ADDR", options.RedisAddress); if (options.RedisPassword is not null) + { + Set(context, "POSTA_REDIS_PASSWORD", options.RedisPassword.Resource); + } Set(context, "POSTA_JWT_SECRET", resource.JwtSecretParameter); Set(context, "POSTA_ADMIN_EMAIL", adminEmail); Set(context, "POSTA_ADMIN_PASSWORD", resource.AdminPasswordParameter); diff --git a/src/CommunityToolkit.Aspire.Hosting.Posta/PostaOptions.cs b/src/CommunityToolkit.Aspire.Hosting.Posta/PostaOptions.cs index 6ef26516a..f174491eb 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Posta/PostaOptions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Posta/PostaOptions.cs @@ -301,4 +301,4 @@ public sealed class PostaOptions /// Gets or sets an external Redis address in the form host:port. /// public string? RedisAddress { get; set; } -} \ No newline at end of file +} From 9daab4537c70c12b1b14bed1dac66ec896a2dd7d Mon Sep 17 00:00:00 2001 From: axi Date: Thu, 9 Jul 2026 01:34:45 +0300 Subject: [PATCH 13/28] Refactor README to use Aspire parameters for `PostaOptions` configuration values, ensuring flexibility and enhanced security. --- src/CommunityToolkit.Aspire.Hosting.Posta/README.md | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/CommunityToolkit.Aspire.Hosting.Posta/README.md b/src/CommunityToolkit.Aspire.Hosting.Posta/README.md index b298dd76b..d442aa0d9 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Posta/README.md +++ b/src/CommunityToolkit.Aspire.Hosting.Posta/README.md @@ -19,12 +19,15 @@ var postgres = builder.AddPostgres("postgres"); var database = postgres.AddDatabase("posta-db", "posta"); var redis = builder.AddRedis("redis"); +var postaWebUrl = builder.AddParameter("posta-web-url"); +var postaCorsOrigins = builder.AddParameter("posta-cors-origins"); +var postaMetricsEnabled = builder.AddParameter("posta-metrics-enabled"); + var posta = builder.AddPosta("posta", database, redis, options => { - options.Environment = "production"; - options.WebUrl = "https://posta.example.com"; - options.CorsOrigins = "https://app.example.com"; - options.MetricsEnabled = true; + options.WebUrl = postaWebUrl; + options.CorsOrigins = postaCorsOrigins; + options.MetricsEnabled = postaMetricsEnabled; }); builder.AddProject("api") @@ -36,7 +39,7 @@ 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. Use `PostaOptions.DatabaseUrl`, `PostaOptions.RedisAddress`, and `PostaOptions.RedisPassword` only when you need to override the generated Posta environment values. -Use the `PostaOptions` callback to configure Posta environment variables such as rate limits, web URLs, OAuth, blob storage, system SMTP, inbound email, email verification, and advanced switches. Secret values such as SMTP passwords, S3 keys, OAuth client secrets, encryption keys, inbound webhook secrets, `POSTA_DB_URL`, and Redis password are represented as Aspire parameters. +Use the `PostaOptions` callback to configure Posta environment variables such as rate limits, web URLs, OAuth, blob storage, system SMTP, inbound email, email verification, and advanced switches. `PostaOptions` values are Aspire parameter builders so values can come from configuration, environment variables, user secrets, or publish-time parameters instead of being materialized in the AppHost model. Sensitive values such as SMTP passwords, S3 keys, OAuth client secrets, encryption keys, inbound webhook secrets, `POSTA_DB_URL`, and Redis password should be created as secret parameters. ## Connection Properties From f0655405ca057565f0928d78c75f3fbae0c4134b Mon Sep 17 00:00:00 2001 From: axi Date: Thu, 9 Jul 2026 01:34:50 +0300 Subject: [PATCH 14/28] Refactor `AddPosta` test configuration to use parameterized resources and improve environment variable assertions. --- .../ContainerResourceCreationTests.cs | 266 +++++++++--------- 1 file changed, 139 insertions(+), 127 deletions(-) diff --git a/tests/CommunityToolkit.Aspire.Hosting.Posta.Tests/ContainerResourceCreationTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Posta.Tests/ContainerResourceCreationTests.cs index be6545eb3..e8478951b 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.Posta.Tests/ContainerResourceCreationTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.Posta.Tests/ContainerResourceCreationTests.cs @@ -156,78 +156,78 @@ public async Task AddPostaConfiguresPostaEnvironmentVariables() 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 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, 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"; + options.Environment = Parameter("environment", "dev"); + options.DevMode = Parameter("dev-mode", "true"); + options.AuthRateLimitEnabled = Parameter("auth-rate-limit-enabled", "false"); + options.RateLimitHourly = Parameter("rate-limit-hourly", "500"); + options.RateLimitDaily = Parameter("rate-limit-daily", "5000"); + options.OpenApiDocs = Parameter("openapi-docs", "false"); + options.MetricsEnabled = Parameter("metrics-enabled", "true"); + options.WebDir = Parameter("web-dir", "web/dist"); + options.WebUrl = Parameter("web-url", "https://posta.example.com"); + options.ApiUrl = Parameter("api-url", "https://api.posta.example.com"); + options.CorsOrigins = Parameter("cors-origins", "https://app.example.com"); + options.EmbeddedWorker = Parameter("embedded-worker", "false"); + options.WorkerConcurrency = Parameter("worker-concurrency", "20"); + options.WorkerMaxRetries = Parameter("worker-max-retries", "7"); + options.WebhookMaxRetries = Parameter("webhook-max-retries", "4"); + options.WebhookTimeoutSeconds = Parameter("webhook-timeout-seconds", "30"); + options.WebhookProxyUrl = Parameter("webhook-proxy-url", "http://proxy.example.com"); + options.GoogleOAuthClientId = Parameter("google-oauth-client-id", "google-client-id"); + options.GoogleOAuthClientSecret = Parameter("google-oauth-client-secret", "google-secret"); + options.OAuthCallbackUrl = Parameter("oauth-callback-url", "https://posta.example.com/oauth/callback"); + options.BlobProvider = Parameter("blob-provider", "s3"); + options.BlobS3Endpoint = Parameter("blob-s3-endpoint", "https://s3.example.com"); + options.BlobS3Region = Parameter("blob-s3-region", "eu-central-1"); + options.BlobS3Bucket = Parameter("blob-s3-bucket", "posta"); + options.BlobS3AccessKey = Parameter("blob-s3-access-key", "s3-access"); + options.BlobS3SecretKey = Parameter("blob-s3-secret-key", "s3-secret"); + options.BlobS3UseSsl = Parameter("blob-s3-use-ssl", "false"); + options.BlobS3PathStyle = Parameter("blob-s3-path-style", "true"); + options.BlobFileSystemPath = Parameter("blob-file-system-path", "/posta/attachments"); + options.EncryptionKey = Parameter("encryption-key", "encryption-secret"); + options.SystemSmtpHost = Parameter("system-smtp-host", "smtp.example.com"); + options.SystemSmtpPort = Parameter("system-smtp-port", "2525"); + options.SystemSmtpUsername = Parameter("system-smtp-username", "notifications@example.com"); + options.SystemSmtpPassword = Parameter("system-smtp-password", "smtp-secret"); + options.SystemSmtpFrom = Parameter("system-smtp-from", "notifications@example.com"); + options.SystemSmtpEncryption = Parameter("system-smtp-encryption", "ssl"); + options.InboundEnabled = Parameter("inbound-enabled", "true"); + options.InboundSmtpHost = Parameter("inbound-smtp-host", "127.0.0.1"); + options.InboundSmtpPort = Parameter("inbound-smtp-port", "2526"); + options.InboundHostname = Parameter("inbound-hostname", "mx.example.com"); + options.InboundMaxMessageSize = Parameter("inbound-max-message-size", "12345"); + options.InboundMaxAttachSize = Parameter("inbound-max-attach-size", "6789"); + options.InboundWebhookSecret = Parameter("inbound-webhook-secret", "inbound-secret"); + options.InboundTlsMode = Parameter("inbound-tls-mode", "starttls"); + options.InboundTlsCertFile = Parameter("inbound-tls-cert-file", "/certs/fullchain.pem"); + options.InboundTlsKeyFile = Parameter("inbound-tls-key-file", "/certs/privkey.pem"); + options.InboundSmtpRateLimit = Parameter("inbound-smtp-rate-limit", "10"); + options.InboundSmtpRateWindow = Parameter("inbound-smtp-rate-window", "20"); + options.EmailVerificationRequired = Parameter("email-verification-required", "true"); + options.AutoSuppressOnReject = Parameter("auto-suppress-on-reject", "false"); + options.EmailVerifyEnabled = Parameter("email-verify-enabled", "false"); + options.EmailVerifyCacheTtlHours = Parameter("email-verify-cache-ttl-hours", "12"); + options.EmailVerifyMxCacheTtlHours = Parameter("email-verify-mx-cache-ttl-hours", "6"); + options.EmailVerifyRateHourly = Parameter("email-verify-rate-hourly", "42"); + options.AllowDowngrade = Parameter("allow-downgrade", "true"); + options.PlanEnforcement = Parameter("plan-enforcement", "true"); + options.DatabaseUrl = Parameter("db-url", "postgres://example"); + options.RedisPassword = Parameter("redis-password", "redis-secret"); + options.RedisAddress = Parameter("redis-address", "redis.example.com:6379"); }) - .WithReference(redis, redisPassword); + .WithReference(redis, parameters["redis-password"]); Assert.True(posta.Resource.TryGetAnnotationsOfType(out var annotations)); @@ -243,63 +243,70 @@ public async Task AddPostaConfiguresPostaEnvironmentVariables() 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"]); + foreach (var (parameterName, environmentVariableName) in new (string ParameterName, string EnvironmentVariableName)[] + { + ("environment", "POSTA_ENV"), + ("dev-mode", "POSTA_DEV_MODE"), + ("auth-rate-limit-enabled", "POSTA_AUTH_RATE_LIMIT_ENABLED"), + ("rate-limit-hourly", "POSTA_RATE_LIMIT_HOURLY"), + ("rate-limit-daily", "POSTA_RATE_LIMIT_DAILY"), + ("openapi-docs", "POSTA_OPENAPI_DOCS"), + ("metrics-enabled", "POSTA_METRICS_ENABLED"), + ("web-dir", "POSTA_WEB_DIR"), + ("web-url", "POSTA_WEB_URL"), + ("api-url", "POSTA_API_URL"), + ("cors-origins", "POSTA_CORS_ORIGINS"), + ("embedded-worker", "POSTA_EMBEDDED_WORKER"), + ("worker-concurrency", "POSTA_WORKER_CONCURRENCY"), + ("worker-max-retries", "POSTA_WORKER_MAX_RETRIES"), + ("webhook-max-retries", "POSTA_WEBHOOK_MAX_RETRIES"), + ("webhook-timeout-seconds", "POSTA_WEBHOOK_TIMEOUT_SECS"), + ("webhook-proxy-url", "POSTA_WEBHOOK_PROXY_URL"), + ("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"), + ("blob-provider", "POSTA_BLOB_PROVIDER"), + ("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"), + ("blob-file-system-path", "POSTA_BLOB_FS_PATH"), + ("encryption-key", "POSTA_ENCRYPTION_KEY"), + ("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-attach-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"), + ("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"), + ("allow-downgrade", "POSTA_ALLOW_DOWNGRADE"), + ("plan-enforcement", "POSTA_PLAN_ENFORCEMENT"), + ("db-url", "POSTA_DB_URL") + }) + { + AssertParameter(parameterName, environmentVariableName); + } + Assert.True(env.ContainsKey("POSTA_DB_HOST")); Assert.True(env.ContainsKey("POSTA_DB_PORT")); Assert.True(env.ContainsKey("POSTA_DB_USER")); @@ -307,8 +314,13 @@ public async Task AddPostaConfiguresPostaEnvironmentVariables() Assert.Equal("posta", env["POSTA_DB_NAME"]); Assert.Equal("disable", env["POSTA_DB_SSL_MODE"]); Assert.True(env.ContainsKey("POSTA_REDIS_ADDR")); - Assert.NotEqual("redis.example.com:6379", env["POSTA_REDIS_ADDR"].ToString()); - Assert.Same(redisPassword.Resource, env["POSTA_REDIS_PASSWORD"]); + AssertParameter("redis-address", "POSTA_REDIS_ADDR"); + AssertParameter("redis-password", "POSTA_REDIS_PASSWORD"); + + void AssertParameter(string parameterName, string environmentVariableName) + { + Assert.Same(parameters[parameterName].Resource, env[environmentVariableName]); + } } private static (IResourceBuilder Database, IResourceBuilder Redis) AddPostaDependencies(IDistributedApplicationBuilder builder) From eb0a90f8e0cc6b5dd7c5fa5a4783414ccc04d7c0 Mon Sep 17 00:00:00 2001 From: axi Date: Thu, 9 Jul 2026 01:35:02 +0300 Subject: [PATCH 15/28] Add parameter-based `AddPosta` overload with `PostaOptions` and improve method null-checks and chaining logic. --- .../PostaHostingExtensions.cs | 46 +++++++++++++++++-- 1 file changed, 41 insertions(+), 5 deletions(-) diff --git a/src/CommunityToolkit.Aspire.Hosting.Posta/PostaHostingExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Posta/PostaHostingExtensions.cs index 90c3e6a26..d80a255d1 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Posta/PostaHostingExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Posta/PostaHostingExtensions.cs @@ -36,7 +36,42 @@ public static IResourceBuilder AddPosta( ArgumentNullException.ThrowIfNull(database); ArgumentNullException.ThrowIfNull(redis); - return AddPostaCore(builder, name, jwtSecret, adminPassword, adminEmail, port, configureOptions: null) + 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 parameter-based 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 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); } @@ -54,7 +89,7 @@ public static IResourceBuilder AddPosta( /// 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 parameter-based overload instead.")] + [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, @@ -71,9 +106,10 @@ public static IResourceBuilder AddPosta( ArgumentNullException.ThrowIfNull(redis); ArgumentNullException.ThrowIfNull(configureOptions); - return AddPostaCore(builder, name, jwtSecret, adminPassword, adminEmail, port, configureOptions) - .WithReference(database) - .WithReference(redis); + var options = new PostaOptions(); + configureOptions.Invoke(options); + + return builder.AddPosta(name, database, redis, options, jwtSecret, adminPassword, adminEmail, port); } /// From eb2705a6e7547f5cfa5c30fcc08f18c8f7e49487 Mon Sep 17 00:00:00 2001 From: axi Date: Thu, 9 Jul 2026 01:35:14 +0300 Subject: [PATCH 16/28] Refactor `PostaHostingExtensions` to improve environment variable setup by replacing `SetIfNotNull` with `SetParameter`, adding default values, and introducing `SetIfAbsent` to prevent overwrites. --- .../PostaHostingExtensions.cs | 135 ++++++++++-------- 1 file changed, 73 insertions(+), 62 deletions(-) diff --git a/src/CommunityToolkit.Aspire.Hosting.Posta/PostaHostingExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Posta/PostaHostingExtensions.cs index d80a255d1..896a73120 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Posta/PostaHostingExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Posta/PostaHostingExtensions.cs @@ -163,14 +163,14 @@ public static IResourceBuilder WithReference( var redisResource = redis.Resource; var redisEndpoint = redisResource.GetEndpoint("secondary"); - Set(context, "POSTA_REDIS_ADDR", ReferenceExpression.Create($"{redisEndpoint.Property(EndpointProperty.Host)}:{redisEndpoint.Property(EndpointProperty.Port)}")); + SetIfAbsent(context, "POSTA_REDIS_ADDR", ReferenceExpression.Create($"{redisEndpoint.Property(EndpointProperty.Host)}:{redisEndpoint.Property(EndpointProperty.Port)}")); if (redisPassword is not null) { Set(context, "POSTA_REDIS_PASSWORD", redisPassword.Resource); } else if (redisResource.PasswordParameter is not null) { - Set(context, "POSTA_REDIS_PASSWORD", redisResource.PasswordParameter); + SetIfAbsent(context, "POSTA_REDIS_PASSWORD", redisResource.PasswordParameter); } }) .WaitFor(redis); @@ -183,14 +183,13 @@ private static IResourceBuilder AddPostaCore( IResourceBuilder? adminPassword, string adminEmail, int? port, - Action? configureOptions) + PostaOptions? options) { ArgumentNullException.ThrowIfNull(builder); ArgumentException.ThrowIfNullOrWhiteSpace(name); ArgumentException.ThrowIfNullOrWhiteSpace(adminEmail); - var options = new PostaOptions(); - configureOptions?.Invoke(options); + options ??= new PostaOptions(); var jwtSecretParameter = jwtSecret?.Resource ?? ParameterResourceBuilderExtensions.CreateDefaultPasswordParameter(builder, $"{name}-jwt-secret", minLower: 1, minUpper: 1, minNumeric: 1); @@ -211,7 +210,7 @@ private static IResourceBuilder AddPostaCore( { Set(context, "POSTA_PORT", PostaResource.HttpEndpointPort); SetParameter(context, "POSTA_DB_URL", options.DatabaseUrl); - SetIfNotNull(context, "POSTA_REDIS_ADDR", options.RedisAddress); + SetParameter(context, "POSTA_REDIS_ADDR", options.RedisAddress); if (options.RedisPassword is not null) { Set(context, "POSTA_REDIS_PASSWORD", options.RedisPassword.Resource); @@ -234,62 +233,62 @@ private static IResourceBuilder AddPostaCore( private static void ConfigurePostaEnvironment(EnvironmentCallbackContext context, PostaOptions options) { - Set(context, "POSTA_ENV", options.Environment); - Set(context, "POSTA_DEV_MODE", options.DevMode); - Set(context, "POSTA_AUTH_RATE_LIMIT_ENABLED", options.AuthRateLimitEnabled); - Set(context, "POSTA_RATE_LIMIT_HOURLY", options.RateLimitHourly); - Set(context, "POSTA_RATE_LIMIT_DAILY", options.RateLimitDaily); - Set(context, "POSTA_OPENAPI_DOCS", options.OpenApiDocs); - Set(context, "POSTA_METRICS_ENABLED", options.MetricsEnabled); - SetIfNotNull(context, "POSTA_WEB_DIR", options.WebDir); - SetIfNotNull(context, "POSTA_WEB_URL", options.WebUrl); - SetIfNotNull(context, "POSTA_API_URL", options.ApiUrl); - Set(context, "POSTA_CORS_ORIGINS", options.CorsOrigins); - Set(context, "POSTA_EMBEDDED_WORKER", options.EmbeddedWorker); - Set(context, "POSTA_WORKER_CONCURRENCY", options.WorkerConcurrency); - Set(context, "POSTA_WORKER_MAX_RETRIES", options.WorkerMaxRetries); - Set(context, "POSTA_WEBHOOK_MAX_RETRIES", options.WebhookMaxRetries); - Set(context, "POSTA_WEBHOOK_TIMEOUT_SECS", options.WebhookTimeoutSeconds); - SetIfNotNull(context, "POSTA_WEBHOOK_PROXY_URL", options.WebhookProxyUrl); - SetIfNotNull(context, "POSTA_GOOGLE_OAUTH_CLIENT_ID", options.GoogleOAuthClientId); + SetParameter(context, "POSTA_ENV", options.Environment, "production"); + SetParameter(context, "POSTA_DEV_MODE", options.DevMode, "false"); + SetParameter(context, "POSTA_AUTH_RATE_LIMIT_ENABLED", options.AuthRateLimitEnabled, "true"); + SetParameter(context, "POSTA_RATE_LIMIT_HOURLY", options.RateLimitHourly, "100"); + SetParameter(context, "POSTA_RATE_LIMIT_DAILY", options.RateLimitDaily, "1000"); + SetParameter(context, "POSTA_OPENAPI_DOCS", options.OpenApiDocs, "true"); + SetParameter(context, "POSTA_METRICS_ENABLED", options.MetricsEnabled, "false"); + SetParameter(context, "POSTA_WEB_DIR", options.WebDir); + SetParameter(context, "POSTA_WEB_URL", options.WebUrl); + SetParameter(context, "POSTA_API_URL", options.ApiUrl); + SetParameter(context, "POSTA_CORS_ORIGINS", options.CorsOrigins, "*"); + SetParameter(context, "POSTA_EMBEDDED_WORKER", options.EmbeddedWorker, "true"); + SetParameter(context, "POSTA_WORKER_CONCURRENCY", options.WorkerConcurrency, "10"); + SetParameter(context, "POSTA_WORKER_MAX_RETRIES", options.WorkerMaxRetries, "5"); + SetParameter(context, "POSTA_WEBHOOK_MAX_RETRIES", options.WebhookMaxRetries, "3"); + SetParameter(context, "POSTA_WEBHOOK_TIMEOUT_SECS", options.WebhookTimeoutSeconds, "10"); + SetParameter(context, "POSTA_WEBHOOK_PROXY_URL", options.WebhookProxyUrl); + SetParameter(context, "POSTA_GOOGLE_OAUTH_CLIENT_ID", options.GoogleOAuthClientId); SetParameter(context, "POSTA_GOOGLE_OAUTH_CLIENT_SECRET", options.GoogleOAuthClientSecret); - SetIfNotNull(context, "POSTA_OAUTH_CALLBACK_URL", options.OAuthCallbackUrl); - SetIfNotNull(context, "POSTA_BLOB_PROVIDER", options.BlobProvider); - SetIfNotNull(context, "POSTA_BLOB_S3_ENDPOINT", options.BlobS3Endpoint); - Set(context, "POSTA_BLOB_S3_REGION", options.BlobS3Region); - SetIfNotNull(context, "POSTA_BLOB_S3_BUCKET", options.BlobS3Bucket); + SetParameter(context, "POSTA_OAUTH_CALLBACK_URL", options.OAuthCallbackUrl); + SetParameter(context, "POSTA_BLOB_PROVIDER", options.BlobProvider); + SetParameter(context, "POSTA_BLOB_S3_ENDPOINT", options.BlobS3Endpoint); + SetParameter(context, "POSTA_BLOB_S3_REGION", options.BlobS3Region, "us-east-1"); + SetParameter(context, "POSTA_BLOB_S3_BUCKET", options.BlobS3Bucket); SetParameter(context, "POSTA_BLOB_S3_ACCESS_KEY", options.BlobS3AccessKey); SetParameter(context, "POSTA_BLOB_S3_SECRET_KEY", options.BlobS3SecretKey); - Set(context, "POSTA_BLOB_S3_USE_SSL", options.BlobS3UseSsl); - Set(context, "POSTA_BLOB_S3_PATH_STYLE", options.BlobS3PathStyle); - Set(context, "POSTA_BLOB_FS_PATH", options.BlobFileSystemPath); + SetParameter(context, "POSTA_BLOB_S3_USE_SSL", options.BlobS3UseSsl, "true"); + SetParameter(context, "POSTA_BLOB_S3_PATH_STYLE", options.BlobS3PathStyle, "false"); + SetParameter(context, "POSTA_BLOB_FS_PATH", options.BlobFileSystemPath, "/data/attachments"); SetParameter(context, "POSTA_ENCRYPTION_KEY", options.EncryptionKey); - SetIfNotNull(context, "POSTA_SYSTEM_SMTP_HOST", options.SystemSmtpHost); - Set(context, "POSTA_SYSTEM_SMTP_PORT", options.SystemSmtpPort); - SetIfNotNull(context, "POSTA_SYSTEM_SMTP_USERNAME", options.SystemSmtpUsername); + SetParameter(context, "POSTA_SYSTEM_SMTP_HOST", options.SystemSmtpHost); + SetParameter(context, "POSTA_SYSTEM_SMTP_PORT", options.SystemSmtpPort, "587"); + SetParameter(context, "POSTA_SYSTEM_SMTP_USERNAME", options.SystemSmtpUsername); SetParameter(context, "POSTA_SYSTEM_SMTP_PASSWORD", options.SystemSmtpPassword); - SetIfNotNull(context, "POSTA_SYSTEM_SMTP_FROM", options.SystemSmtpFrom); - Set(context, "POSTA_SYSTEM_SMTP_ENCRYPTION", options.SystemSmtpEncryption); - Set(context, "POSTA_INBOUND_ENABLED", options.InboundEnabled); - Set(context, "POSTA_INBOUND_SMTP_HOST", options.InboundSmtpHost); - Set(context, "POSTA_INBOUND_SMTP_PORT", options.InboundSmtpPort); - Set(context, "POSTA_INBOUND_HOSTNAME", options.InboundHostname); - Set(context, "POSTA_INBOUND_MAX_MESSAGE_SIZE", options.InboundMaxMessageSize); - Set(context, "POSTA_INBOUND_MAX_ATTACH_SIZE", options.InboundMaxAttachSize); + SetParameter(context, "POSTA_SYSTEM_SMTP_FROM", options.SystemSmtpFrom); + SetParameter(context, "POSTA_SYSTEM_SMTP_ENCRYPTION", options.SystemSmtpEncryption, "starttls"); + SetParameter(context, "POSTA_INBOUND_ENABLED", options.InboundEnabled, "false"); + SetParameter(context, "POSTA_INBOUND_SMTP_HOST", options.InboundSmtpHost, "0.0.0.0"); + SetParameter(context, "POSTA_INBOUND_SMTP_PORT", options.InboundSmtpPort, "2525"); + SetParameter(context, "POSTA_INBOUND_HOSTNAME", options.InboundHostname, "posta.local"); + SetParameter(context, "POSTA_INBOUND_MAX_MESSAGE_SIZE", options.InboundMaxMessageSize, "26214400"); + SetParameter(context, "POSTA_INBOUND_MAX_ATTACH_SIZE", options.InboundMaxAttachSize, "10485760"); SetParameter(context, "POSTA_INBOUND_WEBHOOK_SECRET", options.InboundWebhookSecret); - Set(context, "POSTA_INBOUND_TLS_MODE", options.InboundTlsMode); - SetIfNotNull(context, "POSTA_INBOUND_TLS_CERT_FILE", options.InboundTlsCertFile); - SetIfNotNull(context, "POSTA_INBOUND_TLS_KEY_FILE", options.InboundTlsKeyFile); - Set(context, "POSTA_INBOUND_SMTP_RATE_LIMIT", options.InboundSmtpRateLimit); - Set(context, "POSTA_INBOUND_SMTP_RATE_WINDOW", options.InboundSmtpRateWindow); - Set(context, "POSTA_EMAIL_VERIFICATION_REQUIRED", options.EmailVerificationRequired); - Set(context, "POSTA_AUTO_SUPPRESS_ON_REJECT", options.AutoSuppressOnReject); - Set(context, "POSTA_EMAIL_VERIFY_ENABLED", options.EmailVerifyEnabled); - Set(context, "POSTA_EMAIL_VERIFY_CACHE_TTL_HOURS", options.EmailVerifyCacheTtlHours); - Set(context, "POSTA_EMAIL_VERIFY_MX_CACHE_TTL_HOURS", options.EmailVerifyMxCacheTtlHours); - Set(context, "POSTA_EMAIL_VERIFY_RATE_HOURLY", options.EmailVerifyRateHourly); - Set(context, "POSTA_ALLOW_DOWNGRADE", options.AllowDowngrade); - Set(context, "POSTA_PLAN_ENFORCEMENT", options.PlanEnforcement); + SetParameter(context, "POSTA_INBOUND_TLS_MODE", options.InboundTlsMode, "none"); + SetParameter(context, "POSTA_INBOUND_TLS_CERT_FILE", options.InboundTlsCertFile); + SetParameter(context, "POSTA_INBOUND_TLS_KEY_FILE", options.InboundTlsKeyFile); + SetParameter(context, "POSTA_INBOUND_SMTP_RATE_LIMIT", options.InboundSmtpRateLimit, "60"); + SetParameter(context, "POSTA_INBOUND_SMTP_RATE_WINDOW", options.InboundSmtpRateWindow, "60"); + SetParameter(context, "POSTA_EMAIL_VERIFICATION_REQUIRED", options.EmailVerificationRequired, "false"); + SetParameter(context, "POSTA_AUTO_SUPPRESS_ON_REJECT", options.AutoSuppressOnReject, "true"); + SetParameter(context, "POSTA_EMAIL_VERIFY_ENABLED", options.EmailVerifyEnabled, "true"); + SetParameter(context, "POSTA_EMAIL_VERIFY_CACHE_TTL_HOURS", options.EmailVerifyCacheTtlHours, "168"); + SetParameter(context, "POSTA_EMAIL_VERIFY_MX_CACHE_TTL_HOURS", options.EmailVerifyMxCacheTtlHours, "24"); + SetParameter(context, "POSTA_EMAIL_VERIFY_RATE_HOURLY", options.EmailVerifyRateHourly, "1000"); + SetParameter(context, "POSTA_ALLOW_DOWNGRADE", options.AllowDowngrade, "false"); + SetParameter(context, "POSTA_PLAN_ENFORCEMENT", options.PlanEnforcement, "false"); } private static void Set(EnvironmentCallbackContext context, string name, string value) @@ -312,9 +311,14 @@ private static void Set(EnvironmentCallbackContext context, string name, Referen context.EnvironmentVariables[name] = value; } - private static void Set(EnvironmentCallbackContext context, string name, bool value) + private static void SetIfAbsent(EnvironmentCallbackContext context, string name, ParameterResource value) { - context.EnvironmentVariables[name] = value ? "true" : "false"; + 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, int value) @@ -322,20 +326,27 @@ private static void Set(EnvironmentCallbackContext context, string name, int val context.EnvironmentVariables[name] = value.ToString(System.Globalization.CultureInfo.InvariantCulture); } - private static void SetIfNotNull(EnvironmentCallbackContext context, string name, string? value) + private static void SetParameter(EnvironmentCallbackContext context, string name, IResourceBuilder? parameter) { - if (!string.IsNullOrEmpty(value)) + if (parameter is not null) { - context.EnvironmentVariables[name] = value; + context.EnvironmentVariables[name] = parameter.Resource; } } - private static void SetParameter(EnvironmentCallbackContext context, string name, IResourceBuilder? parameter) + private static void SetParameter( + EnvironmentCallbackContext context, + string name, + IResourceBuilder? parameter, + string defaultValue) { if (parameter is not null) { context.EnvironmentVariables[name] = parameter.Resource; + return; } + + context.EnvironmentVariables[name] = defaultValue; } } From 74b634e7e48d17cad6214e15d4c19999ab98e188 Mon Sep 17 00:00:00 2001 From: axi Date: Thu, 9 Jul 2026 01:35:23 +0300 Subject: [PATCH 17/28] Refactor `PostaOptions` to replace primitive types with parameterized resource builders for improved flexibility and configurability. --- .../PostaOptions.cs | 130 +++++++++--------- 1 file changed, 65 insertions(+), 65 deletions(-) diff --git a/src/CommunityToolkit.Aspire.Hosting.Posta/PostaOptions.cs b/src/CommunityToolkit.Aspire.Hosting.Posta/PostaOptions.cs index f174491eb..f18cd443c 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Posta/PostaOptions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Posta/PostaOptions.cs @@ -3,99 +3,99 @@ namespace CommunityToolkit.Aspire.Hosting.Posta; /// -/// Configuration options for the Posta container. +/// Parameter-based configuration overrides for the Posta container. /// public sealed class PostaOptions { /// /// Gets or sets the Posta environment name. /// - public string Environment { get; set; } = "production"; + public IResourceBuilder? Environment { get; set; } /// - /// Gets or sets a value indicating whether Posta stores emails without sending them. + /// Gets or sets whether Posta stores emails without sending them. /// - public bool DevMode { get; set; } + public IResourceBuilder? DevMode { get; set; } /// - /// Gets or sets a value indicating whether auth rate limiting is enabled. + /// Gets or sets whether auth rate limiting is enabled. /// - public bool AuthRateLimitEnabled { get; set; } = true; + public IResourceBuilder? AuthRateLimitEnabled { get; set; } /// /// Gets or sets the maximum emails per hour per user. /// - public int RateLimitHourly { get; set; } = 100; + public IResourceBuilder? RateLimitHourly { get; set; } /// /// Gets or sets the maximum emails per day per user. /// - public int RateLimitDaily { get; set; } = 1000; + public IResourceBuilder? RateLimitDaily { get; set; } /// - /// Gets or sets a value indicating whether Swagger UI and ReDoc are enabled. + /// Gets or sets whether Swagger UI and ReDoc are enabled. /// - public bool OpenApiDocs { get; set; } = true; + public IResourceBuilder? OpenApiDocs { get; set; } /// - /// Gets or sets a value indicating whether Prometheus metrics are enabled. + /// Gets or sets whether Prometheus metrics are enabled. /// - public bool MetricsEnabled { get; set; } + public IResourceBuilder? MetricsEnabled { get; set; } /// /// Gets or sets the Vue dashboard build directory. /// - public string? WebDir { get; set; } + public IResourceBuilder? WebDir { get; set; } /// /// Gets or sets the public dashboard URL. /// - public string? WebUrl { get; set; } + public IResourceBuilder? WebUrl { get; set; } /// /// Gets or sets the public API base URL advertised in the OpenAPI servers list. /// - public string? ApiUrl { get; set; } + public IResourceBuilder? ApiUrl { get; set; } /// /// Gets or sets the comma-separated allowed CORS origins. /// - public string CorsOrigins { get; set; } = "*"; + public IResourceBuilder? CorsOrigins { get; set; } /// - /// Gets or sets a value indicating whether the worker runs in the server process. + /// Gets or sets whether the worker runs in the server process. /// - public bool EmbeddedWorker { get; set; } = true; + public IResourceBuilder? EmbeddedWorker { get; set; } /// /// Gets or sets the number of worker goroutines. /// - public int WorkerConcurrency { get; set; } = 10; + public IResourceBuilder? WorkerConcurrency { get; set; } /// /// Gets or sets the maximum email retry attempts. /// - public int WorkerMaxRetries { get; set; } = 5; + public IResourceBuilder? WorkerMaxRetries { get; set; } /// /// Gets or sets the maximum webhook delivery retry attempts. /// - public int WebhookMaxRetries { get; set; } = 3; + public IResourceBuilder? WebhookMaxRetries { get; set; } /// /// Gets or sets the webhook HTTP request timeout in seconds. /// - public int WebhookTimeoutSeconds { get; set; } = 10; + public IResourceBuilder? WebhookTimeoutSeconds { get; set; } /// /// Gets or sets the optional HTTP, HTTPS, or SOCKS5 proxy for webhook delivery. /// - public string? WebhookProxyUrl { get; set; } + public IResourceBuilder? WebhookProxyUrl { get; set; } /// /// Gets or sets the Google OAuth client ID for SSO login. /// - public string? GoogleOAuthClientId { get; set; } + public IResourceBuilder? GoogleOAuthClientId { get; set; } /// /// Gets or sets the Google OAuth client secret for SSO login. @@ -105,27 +105,27 @@ public sealed class PostaOptions /// /// Gets or sets the OAuth callback base URL. /// - public string? OAuthCallbackUrl { get; set; } + public IResourceBuilder? OAuthCallbackUrl { get; set; } /// /// Gets or sets the storage backend for attachments, either s3 or filesystem. /// - public string? BlobProvider { get; set; } + public IResourceBuilder? BlobProvider { get; set; } /// /// Gets or sets the S3-compatible endpoint. /// - public string? BlobS3Endpoint { get; set; } + public IResourceBuilder? BlobS3Endpoint { get; set; } /// /// Gets or sets the S3 region. /// - public string BlobS3Region { get; set; } = "us-east-1"; + public IResourceBuilder? BlobS3Region { get; set; } /// /// Gets or sets the S3 bucket name. /// - public string? BlobS3Bucket { get; set; } + public IResourceBuilder? BlobS3Bucket { get; set; } /// /// Gets or sets the S3 access key. @@ -138,19 +138,19 @@ public sealed class PostaOptions public IResourceBuilder? BlobS3SecretKey { get; set; } /// - /// Gets or sets a value indicating whether S3 storage uses TLS. + /// Gets or sets whether S3 storage uses TLS. /// - public bool BlobS3UseSsl { get; set; } = true; + public IResourceBuilder? BlobS3UseSsl { get; set; } /// - /// Gets or sets a value indicating whether S3 path-style addressing is used. + /// Gets or sets whether S3 path-style addressing is used. /// - public bool BlobS3PathStyle { get; set; } + public IResourceBuilder? BlobS3PathStyle { get; set; } /// /// Gets or sets the filesystem storage path. /// - public string BlobFileSystemPath { get; set; } = "/data/attachments"; + public IResourceBuilder? BlobFileSystemPath { get; set; } /// /// Gets or sets the AES-256-GCM encryption key for stored SMTP passwords. @@ -160,17 +160,17 @@ public sealed class PostaOptions /// /// Gets or sets the system SMTP server host used for platform notifications. /// - public string? SystemSmtpHost { get; set; } + public IResourceBuilder? SystemSmtpHost { get; set; } /// /// Gets or sets the system SMTP server port. /// - public int SystemSmtpPort { get; set; } = 587; + public IResourceBuilder? SystemSmtpPort { get; set; } /// /// Gets or sets the system SMTP username. /// - public string? SystemSmtpUsername { get; set; } + public IResourceBuilder? SystemSmtpUsername { get; set; } /// /// Gets or sets the system SMTP password. @@ -180,42 +180,42 @@ public sealed class PostaOptions /// /// Gets or sets the system SMTP from address. /// - public string? SystemSmtpFrom { get; set; } + public IResourceBuilder? SystemSmtpFrom { get; set; } /// /// Gets or sets the system SMTP encryption mode: none, ssl, or starttls. /// - public string SystemSmtpEncryption { get; set; } = "starttls"; + public IResourceBuilder? SystemSmtpEncryption { get; set; } /// - /// Gets or sets a value indicating whether inbound email processing is enabled. + /// Gets or sets whether inbound email processing is enabled. /// - public bool InboundEnabled { get; set; } + public IResourceBuilder? InboundEnabled { get; set; } /// /// Gets or sets the bind address for the built-in SMTP receiver. /// - public string InboundSmtpHost { get; set; } = "0.0.0.0"; + public IResourceBuilder? InboundSmtpHost { get; set; } /// /// Gets or sets the SMTP listener port. /// - public int InboundSmtpPort { get; set; } = 2525; + public IResourceBuilder? InboundSmtpPort { get; set; } /// /// Gets or sets the hostname announced in EHLO and used as TLS SNI. /// - public string InboundHostname { get; set; } = "posta.local"; + public IResourceBuilder? InboundHostname { get; set; } /// /// Gets or sets the maximum raw inbound message size in bytes. /// - public int InboundMaxMessageSize { get; set; } = 26214400; + public IResourceBuilder? InboundMaxMessageSize { get; set; } /// /// Gets or sets the maximum inbound attachment size in bytes. /// - public int InboundMaxAttachSize { get; set; } = 10485760; + public IResourceBuilder? InboundMaxAttachSize { get; set; } /// /// Gets or sets the shared secret for inbound webhooks. @@ -225,67 +225,67 @@ public sealed class PostaOptions /// /// Gets or sets the inbound SMTP TLS mode: none or starttls. /// - public string InboundTlsMode { get; set; } = "none"; + public IResourceBuilder? InboundTlsMode { get; set; } /// /// Gets or sets the PEM certificate path for inbound TLS. /// - public string? InboundTlsCertFile { get; set; } + public IResourceBuilder? InboundTlsCertFile { get; set; } /// /// Gets or sets the PEM key path for inbound TLS. /// - public string? InboundTlsKeyFile { get; set; } + public IResourceBuilder? InboundTlsKeyFile { get; set; } /// /// Gets or sets the per-IP SMTP session rate limit. /// - public int InboundSmtpRateLimit { get; set; } = 60; + public IResourceBuilder? InboundSmtpRateLimit { get; set; } /// /// Gets or sets the SMTP rate-limit window in seconds. /// - public int InboundSmtpRateWindow { get; set; } = 60; + public IResourceBuilder? InboundSmtpRateWindow { get; set; } /// - /// Gets or sets a value indicating whether users must verify their email address before sign-in. + /// Gets or sets whether users must verify their email address before sign-in. /// - public bool EmailVerificationRequired { get; set; } + public IResourceBuilder? EmailVerificationRequired { get; set; } /// - /// Gets or sets a value indicating whether permanently rejected recipients are added to the suppression list. + /// Gets or sets whether permanently rejected recipients are added to the suppression list. /// - public bool AutoSuppressOnReject { get; set; } = true; + public IResourceBuilder? AutoSuppressOnReject { get; set; } /// - /// Gets or sets a value indicating whether the email verification endpoint is enabled. + /// Gets or sets whether the email verification endpoint is enabled. /// - public bool EmailVerifyEnabled { get; set; } = true; + public IResourceBuilder? EmailVerifyEnabled { get; set; } /// /// Gets or sets how long address-level verification results are cached, in hours. /// - public int EmailVerifyCacheTtlHours { get; set; } = 168; + public IResourceBuilder? EmailVerifyCacheTtlHours { get; set; } /// /// Gets or sets how long domain MX lookups are cached, in hours. /// - public int EmailVerifyMxCacheTtlHours { get; set; } = 24; + public IResourceBuilder? EmailVerifyMxCacheTtlHours { get; set; } /// /// Gets or sets the per-user hourly cap on verification requests. /// - public int EmailVerifyRateHourly { get; set; } = 1000; + public IResourceBuilder? EmailVerifyRateHourly { get; set; } /// - /// Gets or sets a value indicating whether older binaries can boot against a newer database schema. + /// Gets or sets whether older binaries can boot against a newer database schema. /// - public bool AllowDowngrade { get; set; } + public IResourceBuilder? AllowDowngrade { get; set; } /// - /// Gets or sets a value indicating whether hosted plan limits and quotas are enforced. + /// Gets or sets whether hosted plan limits and quotas are enforced. /// - public bool PlanEnforcement { get; set; } + public IResourceBuilder? PlanEnforcement { get; set; } /// /// Gets or sets a PostgreSQL connection string parameter that overrides individual database settings. @@ -300,5 +300,5 @@ public sealed class PostaOptions /// /// Gets or sets an external Redis address in the form host:port. /// - public string? RedisAddress { get; set; } + public IResourceBuilder? RedisAddress { get; set; } } From b777259e46d7fe18ccb360e5cd02e26a8d12c36c Mon Sep 17 00:00:00 2001 From: axi Date: Thu, 9 Jul 2026 13:20:53 +0300 Subject: [PATCH 18/28] Add Posta configuration options and environment variable constants - Introduced `PostaEmailVerificationOptions`, `PostaGoogleOAuthOptions`, `PostaInboundSmtpOptions`, `PostaS3BlobStorageOptions`, and `PostaSystemSmtpOptions` for parameterized configuration. - Added `PostaEnvironmentVariables` to define constants for environment variable keys. - Created `launchSettings.json` example for local hosting configuration. --- .../Properties/launchSettings.json | 29 ++++++++ .../PostaEmailVerificationOptions.cs | 39 ++++++++++ .../PostaEnvironmentVariables.cs | 74 +++++++++++++++++++ .../PostaGoogleOAuthOptions.cs | 24 ++++++ .../PostaInboundSmtpOptions.cs | 69 +++++++++++++++++ .../PostaS3BlobStorageOptions.cs | 44 +++++++++++ .../PostaSystemSmtpOptions.cs | 39 ++++++++++ 7 files changed, 318 insertions(+) create mode 100644 examples/posta/CommunityToolkit.Aspire.Hosting.Posta.AppHost/Properties/launchSettings.json create mode 100644 src/CommunityToolkit.Aspire.Hosting.Posta/PostaEmailVerificationOptions.cs create mode 100644 src/CommunityToolkit.Aspire.Hosting.Posta/PostaEnvironmentVariables.cs create mode 100644 src/CommunityToolkit.Aspire.Hosting.Posta/PostaGoogleOAuthOptions.cs create mode 100644 src/CommunityToolkit.Aspire.Hosting.Posta/PostaInboundSmtpOptions.cs create mode 100644 src/CommunityToolkit.Aspire.Hosting.Posta/PostaS3BlobStorageOptions.cs create mode 100644 src/CommunityToolkit.Aspire.Hosting.Posta/PostaSystemSmtpOptions.cs 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/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/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/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; } +} From acdadd13e3227a4298eaac9cfcfa03146a604274 Mon Sep 17 00:00:00 2001 From: axi Date: Thu, 9 Jul 2026 13:21:04 +0300 Subject: [PATCH 19/28] Refactor `AddPosta` tests to improve parameterized resource configuration, enhance environment variable assertions, and streamline option setup. --- .../ContainerResourceCreationTests.cs | 338 ++++++++++++------ 1 file changed, 224 insertions(+), 114 deletions(-) diff --git a/tests/CommunityToolkit.Aspire.Hosting.Posta.Tests/ContainerResourceCreationTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Posta.Tests/ContainerResourceCreationTests.cs index e8478951b..18973ae85 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.Posta.Tests/ContainerResourceCreationTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.Posta.Tests/ContainerResourceCreationTests.cs @@ -156,6 +156,167 @@ public async Task AddPostaConfiguresPostaEnvironmentVariables() 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) @@ -165,69 +326,56 @@ IResourceBuilder Parameter(string name, string value) return parameter; } - var posta = builder.AddPosta("posta", database, redis, options => - { - options.Environment = Parameter("environment", "dev"); - options.DevMode = Parameter("dev-mode", "true"); - options.AuthRateLimitEnabled = Parameter("auth-rate-limit-enabled", "false"); - options.RateLimitHourly = Parameter("rate-limit-hourly", "500"); - options.RateLimitDaily = Parameter("rate-limit-daily", "5000"); - options.OpenApiDocs = Parameter("openapi-docs", "false"); - options.MetricsEnabled = Parameter("metrics-enabled", "true"); - options.WebDir = Parameter("web-dir", "web/dist"); - options.WebUrl = Parameter("web-url", "https://posta.example.com"); - options.ApiUrl = Parameter("api-url", "https://api.posta.example.com"); - options.CorsOrigins = Parameter("cors-origins", "https://app.example.com"); - options.EmbeddedWorker = Parameter("embedded-worker", "false"); - options.WorkerConcurrency = Parameter("worker-concurrency", "20"); - options.WorkerMaxRetries = Parameter("worker-max-retries", "7"); - options.WebhookMaxRetries = Parameter("webhook-max-retries", "4"); - options.WebhookTimeoutSeconds = Parameter("webhook-timeout-seconds", "30"); - options.WebhookProxyUrl = Parameter("webhook-proxy-url", "http://proxy.example.com"); - options.GoogleOAuthClientId = Parameter("google-oauth-client-id", "google-client-id"); - options.GoogleOAuthClientSecret = Parameter("google-oauth-client-secret", "google-secret"); - options.OAuthCallbackUrl = Parameter("oauth-callback-url", "https://posta.example.com/oauth/callback"); - options.BlobProvider = Parameter("blob-provider", "s3"); - options.BlobS3Endpoint = Parameter("blob-s3-endpoint", "https://s3.example.com"); - options.BlobS3Region = Parameter("blob-s3-region", "eu-central-1"); - options.BlobS3Bucket = Parameter("blob-s3-bucket", "posta"); - options.BlobS3AccessKey = Parameter("blob-s3-access-key", "s3-access"); - options.BlobS3SecretKey = Parameter("blob-s3-secret-key", "s3-secret"); - options.BlobS3UseSsl = Parameter("blob-s3-use-ssl", "false"); - options.BlobS3PathStyle = Parameter("blob-s3-path-style", "true"); - options.BlobFileSystemPath = Parameter("blob-file-system-path", "/posta/attachments"); - options.EncryptionKey = Parameter("encryption-key", "encryption-secret"); - options.SystemSmtpHost = Parameter("system-smtp-host", "smtp.example.com"); - options.SystemSmtpPort = Parameter("system-smtp-port", "2525"); - options.SystemSmtpUsername = Parameter("system-smtp-username", "notifications@example.com"); - options.SystemSmtpPassword = Parameter("system-smtp-password", "smtp-secret"); - options.SystemSmtpFrom = Parameter("system-smtp-from", "notifications@example.com"); - options.SystemSmtpEncryption = Parameter("system-smtp-encryption", "ssl"); - options.InboundEnabled = Parameter("inbound-enabled", "true"); - options.InboundSmtpHost = Parameter("inbound-smtp-host", "127.0.0.1"); - options.InboundSmtpPort = Parameter("inbound-smtp-port", "2526"); - options.InboundHostname = Parameter("inbound-hostname", "mx.example.com"); - options.InboundMaxMessageSize = Parameter("inbound-max-message-size", "12345"); - options.InboundMaxAttachSize = Parameter("inbound-max-attach-size", "6789"); - options.InboundWebhookSecret = Parameter("inbound-webhook-secret", "inbound-secret"); - options.InboundTlsMode = Parameter("inbound-tls-mode", "starttls"); - options.InboundTlsCertFile = Parameter("inbound-tls-cert-file", "/certs/fullchain.pem"); - options.InboundTlsKeyFile = Parameter("inbound-tls-key-file", "/certs/privkey.pem"); - options.InboundSmtpRateLimit = Parameter("inbound-smtp-rate-limit", "10"); - options.InboundSmtpRateWindow = Parameter("inbound-smtp-rate-window", "20"); - options.EmailVerificationRequired = Parameter("email-verification-required", "true"); - options.AutoSuppressOnReject = Parameter("auto-suppress-on-reject", "false"); - options.EmailVerifyEnabled = Parameter("email-verify-enabled", "false"); - options.EmailVerifyCacheTtlHours = Parameter("email-verify-cache-ttl-hours", "12"); - options.EmailVerifyMxCacheTtlHours = Parameter("email-verify-mx-cache-ttl-hours", "6"); - options.EmailVerifyRateHourly = Parameter("email-verify-rate-hourly", "42"); - options.AllowDowngrade = Parameter("allow-downgrade", "true"); - options.PlanEnforcement = Parameter("plan-enforcement", "true"); - options.DatabaseUrl = Parameter("db-url", "postgres://example"); - options.RedisPassword = Parameter("redis-password", "redis-secret"); - options.RedisAddress = Parameter("redis-address", "redis.example.com:6379"); - }) - .WithReference(redis, parameters["redis-password"]); + 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)); @@ -242,39 +390,9 @@ IResourceBuilder Parameter(string name, string value) var env = context.EnvironmentVariables; - Assert.Equal("9000", env["POSTA_PORT"]); + Assert.Equal("s3", env["POSTA_BLOB_PROVIDER"]); foreach (var (parameterName, environmentVariableName) in new (string ParameterName, string EnvironmentVariableName)[] { - ("environment", "POSTA_ENV"), - ("dev-mode", "POSTA_DEV_MODE"), - ("auth-rate-limit-enabled", "POSTA_AUTH_RATE_LIMIT_ENABLED"), - ("rate-limit-hourly", "POSTA_RATE_LIMIT_HOURLY"), - ("rate-limit-daily", "POSTA_RATE_LIMIT_DAILY"), - ("openapi-docs", "POSTA_OPENAPI_DOCS"), - ("metrics-enabled", "POSTA_METRICS_ENABLED"), - ("web-dir", "POSTA_WEB_DIR"), - ("web-url", "POSTA_WEB_URL"), - ("api-url", "POSTA_API_URL"), - ("cors-origins", "POSTA_CORS_ORIGINS"), - ("embedded-worker", "POSTA_EMBEDDED_WORKER"), - ("worker-concurrency", "POSTA_WORKER_CONCURRENCY"), - ("worker-max-retries", "POSTA_WORKER_MAX_RETRIES"), - ("webhook-max-retries", "POSTA_WEBHOOK_MAX_RETRIES"), - ("webhook-timeout-seconds", "POSTA_WEBHOOK_TIMEOUT_SECS"), - ("webhook-proxy-url", "POSTA_WEBHOOK_PROXY_URL"), - ("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"), - ("blob-provider", "POSTA_BLOB_PROVIDER"), - ("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"), - ("blob-file-system-path", "POSTA_BLOB_FS_PATH"), - ("encryption-key", "POSTA_ENCRYPTION_KEY"), ("system-smtp-host", "POSTA_SYSTEM_SMTP_HOST"), ("system-smtp-port", "POSTA_SYSTEM_SMTP_PORT"), ("system-smtp-username", "POSTA_SYSTEM_SMTP_USERNAME"), @@ -286,38 +404,30 @@ IResourceBuilder Parameter(string name, string value) ("inbound-smtp-port", "POSTA_INBOUND_SMTP_PORT"), ("inbound-hostname", "POSTA_INBOUND_HOSTNAME"), ("inbound-max-message-size", "POSTA_INBOUND_MAX_MESSAGE_SIZE"), - ("inbound-max-attach-size", "POSTA_INBOUND_MAX_ATTACH_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"), - ("allow-downgrade", "POSTA_ALLOW_DOWNGRADE"), - ("plan-enforcement", "POSTA_PLAN_ENFORCEMENT"), - ("db-url", "POSTA_DB_URL") + ("email-verify-rate-hourly", "POSTA_EMAIL_VERIFY_RATE_HOURLY") }) - { - AssertParameter(parameterName, environmentVariableName); - } - - 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")); - AssertParameter("redis-address", "POSTA_REDIS_ADDR"); - AssertParameter("redis-password", "POSTA_REDIS_PASSWORD"); - - void AssertParameter(string parameterName, string environmentVariableName) { Assert.Same(parameters[parameterName].Resource, env[environmentVariableName]); } From 47e238e0f91d7cb542bb3af73b69fc65cafb4827 Mon Sep 17 00:00:00 2001 From: axi Date: Thu, 9 Jul 2026 13:21:12 +0300 Subject: [PATCH 20/28] Refactor `AddPosta` and README to enhance parameterized configuration, improve secret handling, and streamline examples. --- .../PostaHostingExtensions.cs | 4 +- .../README.md | 128 ++++++++++++++++-- 2 files changed, 116 insertions(+), 16 deletions(-) diff --git a/src/CommunityToolkit.Aspire.Hosting.Posta/PostaHostingExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Posta/PostaHostingExtensions.cs index 896a73120..5ed6f80ec 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Posta/PostaHostingExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Posta/PostaHostingExtensions.cs @@ -48,13 +48,13 @@ public static IResourceBuilder AddPosta( /// The name of the Posta container resource. /// The PostgreSQL database resource used by Posta. /// The Redis resource used by Posta. - /// The parameter-based Posta environment configuration. + /// 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 and is not supported in polyglot app hosts.")] + [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, diff --git a/src/CommunityToolkit.Aspire.Hosting.Posta/README.md b/src/CommunityToolkit.Aspire.Hosting.Posta/README.md index d442aa0d9..8cdea6732 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Posta/README.md +++ b/src/CommunityToolkit.Aspire.Hosting.Posta/README.md @@ -10,25 +10,17 @@ Install the package in your AppHost project: aspire add CommunityToolkit.Aspire.Hosting.Posta ``` -## Usage example +## Basic usage ```csharp var builder = DistributedApplication.CreateBuilder(args); -var postgres = builder.AddPostgres("postgres"); +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 postaWebUrl = builder.AddParameter("posta-web-url"); -var postaCorsOrigins = builder.AddParameter("posta-cors-origins"); -var postaMetricsEnabled = builder.AddParameter("posta-metrics-enabled"); - -var posta = builder.AddPosta("posta", database, redis, options => - { - options.WebUrl = postaWebUrl; - options.CorsOrigins = postaCorsOrigins; - options.MetricsEnabled = postaMetricsEnabled; - }); +var posta = builder.AddPosta("posta", database, redis); builder.AddProject("api") .WithReference(posta) @@ -37,9 +29,117 @@ builder.AddProject("api") 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. Use `PostaOptions.DatabaseUrl`, `PostaOptions.RedisAddress`, and `PostaOptions.RedisPassword` only when you need to override the generated Posta environment values. +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(new PostaSystemSmtpOptions + { + Host = smtpHost, + Port = smtpPort, + Username = smtpUsername, + Password = smtpPassword, + From = smtpFrom, + Encryption = smtpEncryption + }) + .WithGoogleOAuth(new PostaGoogleOAuthOptions + { + ClientId = googleClientId, + ClientSecret = googleClientSecret, + CallbackUrl = googleCallbackUrl + }) + .WithS3BlobStorage(new PostaS3BlobStorageOptions + { + Endpoint = s3Endpoint, + Region = s3Region, + Bucket = s3Bucket, + AccessKey = s3AccessKey, + 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"); -Use the `PostaOptions` callback to configure Posta environment variables such as rate limits, web URLs, OAuth, blob storage, system SMTP, inbound email, email verification, and advanced switches. `PostaOptions` values are Aspire parameter builders so values can come from configuration, environment variables, user secrets, or publish-time parameters instead of being materialized in the AppHost model. Sensitive values such as SMTP passwords, S3 keys, OAuth client secrets, encryption keys, inbound webhook secrets, `POSTA_DB_URL`, and Redis password should be created as secret parameters. +builder.AddPosta("posta", database, redis) + .WithInboundSmtp(new PostaInboundSmtpOptions + { + Enabled = inboundEnabled, + Port = inboundPort, + WebhookSecret = inboundWebhookSecret + }) + .WithEmailVerification(new PostaEmailVerificationOptions + { + 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. | +| `WithInboundSmtp(PostaInboundSmtpOptions)` | Configures parameter-based inbound SMTP receiver settings. | +| `WithS3BlobStorage(PostaS3BlobStorageOptions)` | Configures parameter-based S3-compatible attachment storage and sets the blob provider to `s3`. | +| `WithGoogleOAuth(PostaGoogleOAuthOptions)` | Configures parameter-based Google OAuth login settings. | +| `WithEmailVerification(PostaEmailVerificationOptions)` | Configures parameter-based email verification settings. | + +Use `PostaOptions.DatabaseUrl`, `PostaOptions.RedisAddress`, and `PostaOptions.RedisPassword` only when you need to override the generated PostgreSQL or Redis environment values. ## Connection Properties From b6382a3d927bba8a57bc72643553211216d4ef9d Mon Sep 17 00:00:00 2001 From: axi Date: Thu, 9 Jul 2026 13:21:18 +0300 Subject: [PATCH 21/28] Refactor `PostaHostingExtensions` to use `PostaEnvironmentVariables` constants, improve environment variable handling, and modularize configuration setup. --- .../PostaHostingExtensions.cs | 245 ++++++++++++------ 1 file changed, 161 insertions(+), 84 deletions(-) diff --git a/src/CommunityToolkit.Aspire.Hosting.Posta/PostaHostingExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Posta/PostaHostingExtensions.cs index 5ed6f80ec..da2499413 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Posta/PostaHostingExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Posta/PostaHostingExtensions.cs @@ -131,12 +131,12 @@ public static IResourceBuilder WithReference( { var postgres = database.Resource.Parent; - Set(context, "POSTA_DB_HOST", postgres.PrimaryEndpoint.Property(EndpointProperty.Host)); - Set(context, "POSTA_DB_PORT", postgres.PrimaryEndpoint.Property(EndpointProperty.Port)); - Set(context, "POSTA_DB_USER", postgres.UserNameReference); - Set(context, "POSTA_DB_PASSWORD", postgres.PasswordParameter); - Set(context, "POSTA_DB_NAME", database.Resource.DatabaseName); - Set(context, "POSTA_DB_SSL_MODE", "disable"); + 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); } @@ -163,14 +163,14 @@ public static IResourceBuilder WithReference( var redisResource = redis.Resource; var redisEndpoint = redisResource.GetEndpoint("secondary"); - SetIfAbsent(context, "POSTA_REDIS_ADDR", ReferenceExpression.Create($"{redisEndpoint.Property(EndpointProperty.Host)}:{redisEndpoint.Property(EndpointProperty.Port)}")); + SetIfAbsent(context, PostaEnvironmentVariables.RedisAddress, ReferenceExpression.Create($"{redisEndpoint.Property(EndpointProperty.Host)}:{redisEndpoint.Property(EndpointProperty.Port)}")); if (redisPassword is not null) { - Set(context, "POSTA_REDIS_PASSWORD", redisPassword.Resource); + Set(context, PostaEnvironmentVariables.RedisPassword, redisPassword.Resource); } else if (redisResource.PasswordParameter is not null) { - SetIfAbsent(context, "POSTA_REDIS_PASSWORD", redisResource.PasswordParameter); + SetIfAbsent(context, PostaEnvironmentVariables.RedisPassword, redisResource.PasswordParameter); } }) .WaitFor(redis); @@ -208,16 +208,16 @@ private static IResourceBuilder AddPostaCore( name: PostaResource.HttpEndpointName) .WithEnvironment(context => { - Set(context, "POSTA_PORT", PostaResource.HttpEndpointPort); - SetParameter(context, "POSTA_DB_URL", options.DatabaseUrl); - SetParameter(context, "POSTA_REDIS_ADDR", options.RedisAddress); + 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, "POSTA_REDIS_PASSWORD", options.RedisPassword.Resource); + Set(context, PostaEnvironmentVariables.RedisPassword, options.RedisPassword.Resource); } - Set(context, "POSTA_JWT_SECRET", resource.JwtSecretParameter); - Set(context, "POSTA_ADMIN_EMAIL", adminEmail); - Set(context, "POSTA_ADMIN_PASSWORD", resource.AdminPasswordParameter); + Set(context, PostaEnvironmentVariables.JwtSecret, resource.JwtSecretParameter); + Set(context, PostaEnvironmentVariables.AdminEmail, adminEmail); + Set(context, PostaEnvironmentVariables.AdminPassword, resource.AdminPasswordParameter); ConfigurePostaEnvironment(context, options); }) .WithHttpHealthCheck( @@ -233,62 +233,142 @@ private static IResourceBuilder AddPostaCore( private static void ConfigurePostaEnvironment(EnvironmentCallbackContext context, PostaOptions options) { - SetParameter(context, "POSTA_ENV", options.Environment, "production"); - SetParameter(context, "POSTA_DEV_MODE", options.DevMode, "false"); - SetParameter(context, "POSTA_AUTH_RATE_LIMIT_ENABLED", options.AuthRateLimitEnabled, "true"); - SetParameter(context, "POSTA_RATE_LIMIT_HOURLY", options.RateLimitHourly, "100"); - SetParameter(context, "POSTA_RATE_LIMIT_DAILY", options.RateLimitDaily, "1000"); - SetParameter(context, "POSTA_OPENAPI_DOCS", options.OpenApiDocs, "true"); - SetParameter(context, "POSTA_METRICS_ENABLED", options.MetricsEnabled, "false"); - SetParameter(context, "POSTA_WEB_DIR", options.WebDir); - SetParameter(context, "POSTA_WEB_URL", options.WebUrl); - SetParameter(context, "POSTA_API_URL", options.ApiUrl); - SetParameter(context, "POSTA_CORS_ORIGINS", options.CorsOrigins, "*"); - SetParameter(context, "POSTA_EMBEDDED_WORKER", options.EmbeddedWorker, "true"); - SetParameter(context, "POSTA_WORKER_CONCURRENCY", options.WorkerConcurrency, "10"); - SetParameter(context, "POSTA_WORKER_MAX_RETRIES", options.WorkerMaxRetries, "5"); - SetParameter(context, "POSTA_WEBHOOK_MAX_RETRIES", options.WebhookMaxRetries, "3"); - SetParameter(context, "POSTA_WEBHOOK_TIMEOUT_SECS", options.WebhookTimeoutSeconds, "10"); - SetParameter(context, "POSTA_WEBHOOK_PROXY_URL", options.WebhookProxyUrl); - SetParameter(context, "POSTA_GOOGLE_OAUTH_CLIENT_ID", options.GoogleOAuthClientId); - SetParameter(context, "POSTA_GOOGLE_OAUTH_CLIENT_SECRET", options.GoogleOAuthClientSecret); - SetParameter(context, "POSTA_OAUTH_CALLBACK_URL", options.OAuthCallbackUrl); - SetParameter(context, "POSTA_BLOB_PROVIDER", options.BlobProvider); - SetParameter(context, "POSTA_BLOB_S3_ENDPOINT", options.BlobS3Endpoint); - SetParameter(context, "POSTA_BLOB_S3_REGION", options.BlobS3Region, "us-east-1"); - SetParameter(context, "POSTA_BLOB_S3_BUCKET", options.BlobS3Bucket); - SetParameter(context, "POSTA_BLOB_S3_ACCESS_KEY", options.BlobS3AccessKey); - SetParameter(context, "POSTA_BLOB_S3_SECRET_KEY", options.BlobS3SecretKey); - SetParameter(context, "POSTA_BLOB_S3_USE_SSL", options.BlobS3UseSsl, "true"); - SetParameter(context, "POSTA_BLOB_S3_PATH_STYLE", options.BlobS3PathStyle, "false"); - SetParameter(context, "POSTA_BLOB_FS_PATH", options.BlobFileSystemPath, "/data/attachments"); - SetParameter(context, "POSTA_ENCRYPTION_KEY", options.EncryptionKey); - SetParameter(context, "POSTA_SYSTEM_SMTP_HOST", options.SystemSmtpHost); - SetParameter(context, "POSTA_SYSTEM_SMTP_PORT", options.SystemSmtpPort, "587"); - SetParameter(context, "POSTA_SYSTEM_SMTP_USERNAME", options.SystemSmtpUsername); - SetParameter(context, "POSTA_SYSTEM_SMTP_PASSWORD", options.SystemSmtpPassword); - SetParameter(context, "POSTA_SYSTEM_SMTP_FROM", options.SystemSmtpFrom); - SetParameter(context, "POSTA_SYSTEM_SMTP_ENCRYPTION", options.SystemSmtpEncryption, "starttls"); - SetParameter(context, "POSTA_INBOUND_ENABLED", options.InboundEnabled, "false"); - SetParameter(context, "POSTA_INBOUND_SMTP_HOST", options.InboundSmtpHost, "0.0.0.0"); - SetParameter(context, "POSTA_INBOUND_SMTP_PORT", options.InboundSmtpPort, "2525"); - SetParameter(context, "POSTA_INBOUND_HOSTNAME", options.InboundHostname, "posta.local"); - SetParameter(context, "POSTA_INBOUND_MAX_MESSAGE_SIZE", options.InboundMaxMessageSize, "26214400"); - SetParameter(context, "POSTA_INBOUND_MAX_ATTACH_SIZE", options.InboundMaxAttachSize, "10485760"); - SetParameter(context, "POSTA_INBOUND_WEBHOOK_SECRET", options.InboundWebhookSecret); - SetParameter(context, "POSTA_INBOUND_TLS_MODE", options.InboundTlsMode, "none"); - SetParameter(context, "POSTA_INBOUND_TLS_CERT_FILE", options.InboundTlsCertFile); - SetParameter(context, "POSTA_INBOUND_TLS_KEY_FILE", options.InboundTlsKeyFile); - SetParameter(context, "POSTA_INBOUND_SMTP_RATE_LIMIT", options.InboundSmtpRateLimit, "60"); - SetParameter(context, "POSTA_INBOUND_SMTP_RATE_WINDOW", options.InboundSmtpRateWindow, "60"); - SetParameter(context, "POSTA_EMAIL_VERIFICATION_REQUIRED", options.EmailVerificationRequired, "false"); - SetParameter(context, "POSTA_AUTO_SUPPRESS_ON_REJECT", options.AutoSuppressOnReject, "true"); - SetParameter(context, "POSTA_EMAIL_VERIFY_ENABLED", options.EmailVerifyEnabled, "true"); - SetParameter(context, "POSTA_EMAIL_VERIFY_CACHE_TTL_HOURS", options.EmailVerifyCacheTtlHours, "168"); - SetParameter(context, "POSTA_EMAIL_VERIFY_MX_CACHE_TTL_HOURS", options.EmailVerifyMxCacheTtlHours, "24"); - SetParameter(context, "POSTA_EMAIL_VERIFY_RATE_HOURLY", options.EmailVerifyRateHourly, "1000"); - SetParameter(context, "POSTA_ALLOW_DOWNGRADE", options.AllowDowngrade, "false"); - SetParameter(context, "POSTA_PLAN_ENFORCEMENT", options.PlanEnforcement, "false"); + 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) @@ -321,32 +401,29 @@ private static void SetIfAbsent(EnvironmentCallbackContext context, string name, 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 SetParameter(EnvironmentCallbackContext context, string name, IResourceBuilder? parameter) + private static void SetIfNotNull(EnvironmentCallbackContext context, string name, string? value) { - if (parameter is not null) + if (!string.IsNullOrEmpty(value)) { - context.EnvironmentVariables[name] = parameter.Resource; + context.EnvironmentVariables[name] = value; } } - private static void SetParameter( - EnvironmentCallbackContext context, - string name, - IResourceBuilder? parameter, - string defaultValue) + private static void SetParameter(EnvironmentCallbackContext context, string name, IResourceBuilder? parameter) { if (parameter is not null) { context.EnvironmentVariables[name] = parameter.Resource; - return; } - - context.EnvironmentVariables[name] = defaultValue; } } - From bd9cdafea49d57e4a5ac5fe195d2c68396618d52 Mon Sep 17 00:00:00 2001 From: axi Date: Thu, 9 Jul 2026 13:21:25 +0300 Subject: [PATCH 22/28] Refactor `PostaOptions` to replace `IResourceBuilder` with primitive types, add defaults, and improve XML summaries for configuration properties. --- .../PostaOptions.cs | 130 +++++++++--------- 1 file changed, 65 insertions(+), 65 deletions(-) diff --git a/src/CommunityToolkit.Aspire.Hosting.Posta/PostaOptions.cs b/src/CommunityToolkit.Aspire.Hosting.Posta/PostaOptions.cs index f18cd443c..f174491eb 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Posta/PostaOptions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Posta/PostaOptions.cs @@ -3,99 +3,99 @@ namespace CommunityToolkit.Aspire.Hosting.Posta; /// -/// Parameter-based configuration overrides for the Posta container. +/// Configuration options for the Posta container. /// public sealed class PostaOptions { /// /// Gets or sets the Posta environment name. /// - public IResourceBuilder? Environment { get; set; } + public string Environment { get; set; } = "production"; /// - /// Gets or sets whether Posta stores emails without sending them. + /// Gets or sets a value indicating whether Posta stores emails without sending them. /// - public IResourceBuilder? DevMode { get; set; } + public bool DevMode { get; set; } /// - /// Gets or sets whether auth rate limiting is enabled. + /// Gets or sets a value indicating whether auth rate limiting is enabled. /// - public IResourceBuilder? AuthRateLimitEnabled { get; set; } + public bool AuthRateLimitEnabled { get; set; } = true; /// /// Gets or sets the maximum emails per hour per user. /// - public IResourceBuilder? RateLimitHourly { get; set; } + public int RateLimitHourly { get; set; } = 100; /// /// Gets or sets the maximum emails per day per user. /// - public IResourceBuilder? RateLimitDaily { get; set; } + public int RateLimitDaily { get; set; } = 1000; /// - /// Gets or sets whether Swagger UI and ReDoc are enabled. + /// Gets or sets a value indicating whether Swagger UI and ReDoc are enabled. /// - public IResourceBuilder? OpenApiDocs { get; set; } + public bool OpenApiDocs { get; set; } = true; /// - /// Gets or sets whether Prometheus metrics are enabled. + /// Gets or sets a value indicating whether Prometheus metrics are enabled. /// - public IResourceBuilder? MetricsEnabled { get; set; } + public bool MetricsEnabled { get; set; } /// /// Gets or sets the Vue dashboard build directory. /// - public IResourceBuilder? WebDir { get; set; } + public string? WebDir { get; set; } /// /// Gets or sets the public dashboard URL. /// - public IResourceBuilder? WebUrl { get; set; } + public string? WebUrl { get; set; } /// /// Gets or sets the public API base URL advertised in the OpenAPI servers list. /// - public IResourceBuilder? ApiUrl { get; set; } + public string? ApiUrl { get; set; } /// /// Gets or sets the comma-separated allowed CORS origins. /// - public IResourceBuilder? CorsOrigins { get; set; } + public string CorsOrigins { get; set; } = "*"; /// - /// Gets or sets whether the worker runs in the server process. + /// Gets or sets a value indicating whether the worker runs in the server process. /// - public IResourceBuilder? EmbeddedWorker { get; set; } + public bool EmbeddedWorker { get; set; } = true; /// /// Gets or sets the number of worker goroutines. /// - public IResourceBuilder? WorkerConcurrency { get; set; } + public int WorkerConcurrency { get; set; } = 10; /// /// Gets or sets the maximum email retry attempts. /// - public IResourceBuilder? WorkerMaxRetries { get; set; } + public int WorkerMaxRetries { get; set; } = 5; /// /// Gets or sets the maximum webhook delivery retry attempts. /// - public IResourceBuilder? WebhookMaxRetries { get; set; } + public int WebhookMaxRetries { get; set; } = 3; /// /// Gets or sets the webhook HTTP request timeout in seconds. /// - public IResourceBuilder? WebhookTimeoutSeconds { get; set; } + public int WebhookTimeoutSeconds { get; set; } = 10; /// /// Gets or sets the optional HTTP, HTTPS, or SOCKS5 proxy for webhook delivery. /// - public IResourceBuilder? WebhookProxyUrl { get; set; } + public string? WebhookProxyUrl { get; set; } /// /// Gets or sets the Google OAuth client ID for SSO login. /// - public IResourceBuilder? GoogleOAuthClientId { get; set; } + public string? GoogleOAuthClientId { get; set; } /// /// Gets or sets the Google OAuth client secret for SSO login. @@ -105,27 +105,27 @@ public sealed class PostaOptions /// /// Gets or sets the OAuth callback base URL. /// - public IResourceBuilder? OAuthCallbackUrl { get; set; } + public string? OAuthCallbackUrl { get; set; } /// /// Gets or sets the storage backend for attachments, either s3 or filesystem. /// - public IResourceBuilder? BlobProvider { get; set; } + public string? BlobProvider { get; set; } /// /// Gets or sets the S3-compatible endpoint. /// - public IResourceBuilder? BlobS3Endpoint { get; set; } + public string? BlobS3Endpoint { get; set; } /// /// Gets or sets the S3 region. /// - public IResourceBuilder? BlobS3Region { get; set; } + public string BlobS3Region { get; set; } = "us-east-1"; /// /// Gets or sets the S3 bucket name. /// - public IResourceBuilder? BlobS3Bucket { get; set; } + public string? BlobS3Bucket { get; set; } /// /// Gets or sets the S3 access key. @@ -138,19 +138,19 @@ public sealed class PostaOptions public IResourceBuilder? BlobS3SecretKey { get; set; } /// - /// Gets or sets whether S3 storage uses TLS. + /// Gets or sets a value indicating whether S3 storage uses TLS. /// - public IResourceBuilder? BlobS3UseSsl { get; set; } + public bool BlobS3UseSsl { get; set; } = true; /// - /// Gets or sets whether S3 path-style addressing is used. + /// Gets or sets a value indicating whether S3 path-style addressing is used. /// - public IResourceBuilder? BlobS3PathStyle { get; set; } + public bool BlobS3PathStyle { get; set; } /// /// Gets or sets the filesystem storage path. /// - public IResourceBuilder? BlobFileSystemPath { get; set; } + public string BlobFileSystemPath { get; set; } = "/data/attachments"; /// /// Gets or sets the AES-256-GCM encryption key for stored SMTP passwords. @@ -160,17 +160,17 @@ public sealed class PostaOptions /// /// Gets or sets the system SMTP server host used for platform notifications. /// - public IResourceBuilder? SystemSmtpHost { get; set; } + public string? SystemSmtpHost { get; set; } /// /// Gets or sets the system SMTP server port. /// - public IResourceBuilder? SystemSmtpPort { get; set; } + public int SystemSmtpPort { get; set; } = 587; /// /// Gets or sets the system SMTP username. /// - public IResourceBuilder? SystemSmtpUsername { get; set; } + public string? SystemSmtpUsername { get; set; } /// /// Gets or sets the system SMTP password. @@ -180,42 +180,42 @@ public sealed class PostaOptions /// /// Gets or sets the system SMTP from address. /// - public IResourceBuilder? SystemSmtpFrom { get; set; } + public string? SystemSmtpFrom { get; set; } /// /// Gets or sets the system SMTP encryption mode: none, ssl, or starttls. /// - public IResourceBuilder? SystemSmtpEncryption { get; set; } + public string SystemSmtpEncryption { get; set; } = "starttls"; /// - /// Gets or sets whether inbound email processing is enabled. + /// Gets or sets a value indicating whether inbound email processing is enabled. /// - public IResourceBuilder? InboundEnabled { get; set; } + public bool InboundEnabled { get; set; } /// /// Gets or sets the bind address for the built-in SMTP receiver. /// - public IResourceBuilder? InboundSmtpHost { get; set; } + public string InboundSmtpHost { get; set; } = "0.0.0.0"; /// /// Gets or sets the SMTP listener port. /// - public IResourceBuilder? InboundSmtpPort { get; set; } + public int InboundSmtpPort { get; set; } = 2525; /// /// Gets or sets the hostname announced in EHLO and used as TLS SNI. /// - public IResourceBuilder? InboundHostname { get; set; } + public string InboundHostname { get; set; } = "posta.local"; /// /// Gets or sets the maximum raw inbound message size in bytes. /// - public IResourceBuilder? InboundMaxMessageSize { get; set; } + public int InboundMaxMessageSize { get; set; } = 26214400; /// /// Gets or sets the maximum inbound attachment size in bytes. /// - public IResourceBuilder? InboundMaxAttachSize { get; set; } + public int InboundMaxAttachSize { get; set; } = 10485760; /// /// Gets or sets the shared secret for inbound webhooks. @@ -225,67 +225,67 @@ public sealed class PostaOptions /// /// Gets or sets the inbound SMTP TLS mode: none or starttls. /// - public IResourceBuilder? InboundTlsMode { get; set; } + public string InboundTlsMode { get; set; } = "none"; /// /// Gets or sets the PEM certificate path for inbound TLS. /// - public IResourceBuilder? InboundTlsCertFile { get; set; } + public string? InboundTlsCertFile { get; set; } /// /// Gets or sets the PEM key path for inbound TLS. /// - public IResourceBuilder? InboundTlsKeyFile { get; set; } + public string? InboundTlsKeyFile { get; set; } /// /// Gets or sets the per-IP SMTP session rate limit. /// - public IResourceBuilder? InboundSmtpRateLimit { get; set; } + public int InboundSmtpRateLimit { get; set; } = 60; /// /// Gets or sets the SMTP rate-limit window in seconds. /// - public IResourceBuilder? InboundSmtpRateWindow { get; set; } + public int InboundSmtpRateWindow { get; set; } = 60; /// - /// Gets or sets whether users must verify their email address before sign-in. + /// Gets or sets a value indicating whether users must verify their email address before sign-in. /// - public IResourceBuilder? EmailVerificationRequired { get; set; } + public bool EmailVerificationRequired { get; set; } /// - /// Gets or sets whether permanently rejected recipients are added to the suppression list. + /// Gets or sets a value indicating whether permanently rejected recipients are added to the suppression list. /// - public IResourceBuilder? AutoSuppressOnReject { get; set; } + public bool AutoSuppressOnReject { get; set; } = true; /// - /// Gets or sets whether the email verification endpoint is enabled. + /// Gets or sets a value indicating whether the email verification endpoint is enabled. /// - public IResourceBuilder? EmailVerifyEnabled { get; set; } + public bool EmailVerifyEnabled { get; set; } = true; /// /// Gets or sets how long address-level verification results are cached, in hours. /// - public IResourceBuilder? EmailVerifyCacheTtlHours { get; set; } + public int EmailVerifyCacheTtlHours { get; set; } = 168; /// /// Gets or sets how long domain MX lookups are cached, in hours. /// - public IResourceBuilder? EmailVerifyMxCacheTtlHours { get; set; } + public int EmailVerifyMxCacheTtlHours { get; set; } = 24; /// /// Gets or sets the per-user hourly cap on verification requests. /// - public IResourceBuilder? EmailVerifyRateHourly { get; set; } + public int EmailVerifyRateHourly { get; set; } = 1000; /// - /// Gets or sets whether older binaries can boot against a newer database schema. + /// Gets or sets a value indicating whether older binaries can boot against a newer database schema. /// - public IResourceBuilder? AllowDowngrade { get; set; } + public bool AllowDowngrade { get; set; } /// - /// Gets or sets whether hosted plan limits and quotas are enforced. + /// Gets or sets a value indicating whether hosted plan limits and quotas are enforced. /// - public IResourceBuilder? PlanEnforcement { get; set; } + public bool PlanEnforcement { get; set; } /// /// Gets or sets a PostgreSQL connection string parameter that overrides individual database settings. @@ -300,5 +300,5 @@ public sealed class PostaOptions /// /// Gets or sets an external Redis address in the form host:port. /// - public IResourceBuilder? RedisAddress { get; set; } + public string? RedisAddress { get; set; } } From d069e6ebb8567d64289b1b3704d9cd02c7c9ebed Mon Sep 17 00:00:00 2001 From: axi Date: Thu, 9 Jul 2026 13:21:31 +0300 Subject: [PATCH 23/28] Add extension methods to configure Posta resources for SMTP, OAuth, S3 storage, and email verification --- .../PostaHostingExtensions.cs | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/src/CommunityToolkit.Aspire.Hosting.Posta/PostaHostingExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Posta/PostaHostingExtensions.cs index da2499413..b1d6978d0 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Posta/PostaHostingExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Posta/PostaHostingExtensions.cs @@ -176,6 +176,91 @@ public static IResourceBuilder WithReference( .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 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 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 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 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)); + } + private static IResourceBuilder AddPostaCore( IDistributedApplicationBuilder builder, string name, From 64b542b3ebed78663bedcadb76023d5e8f30d2d2 Mon Sep 17 00:00:00 2001 From: axi Date: Thu, 9 Jul 2026 13:28:28 +0300 Subject: [PATCH 24/28] Add action-based overloads to configure Posta resources for SMTP, OAuth, S3 storage, and email verification --- .../PostaHostingExtensions.cs | 100 ++++++++++++++++++ .../README.md | 51 +++++---- .../ContainerResourceCreationTests.cs | 53 ++++++++++ 3 files changed, 181 insertions(+), 23 deletions(-) diff --git a/src/CommunityToolkit.Aspire.Hosting.Posta/PostaHostingExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Posta/PostaHostingExtensions.cs index b1d6978d0..10ddc7fff 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Posta/PostaHostingExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Posta/PostaHostingExtensions.cs @@ -193,6 +193,26 @@ public static IResourceBuilder WithSystemSmtp( 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. /// @@ -210,6 +230,26 @@ public static IResourceBuilder WithInboundSmtp( 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. /// @@ -227,6 +267,26 @@ public static IResourceBuilder WithS3BlobStorage( 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. /// @@ -244,6 +304,26 @@ public static IResourceBuilder WithGoogleOAuth( 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. /// @@ -261,6 +341,26 @@ public static IResourceBuilder WithEmailVerification( 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, diff --git a/src/CommunityToolkit.Aspire.Hosting.Posta/README.md b/src/CommunityToolkit.Aspire.Hosting.Posta/README.md index 8cdea6732..89a6b3f66 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Posta/README.md +++ b/src/CommunityToolkit.Aspire.Hosting.Posta/README.md @@ -77,28 +77,28 @@ var s3AccessKey = builder.AddParameterFromConfiguration("posta-s3-access-key", " var s3SecretKey = builder.AddParameterFromConfiguration("posta-s3-secret-key", "Posta:S3:SecretKey", secret: true); var posta = builder.AddPosta("posta", database, redis) - .WithSystemSmtp(new PostaSystemSmtpOptions + .WithSystemSmtp(options => { - Host = smtpHost, - Port = smtpPort, - Username = smtpUsername, - Password = smtpPassword, - From = smtpFrom, - Encryption = smtpEncryption + options.Host = smtpHost; + options.Port = smtpPort; + options.Username = smtpUsername; + options.Password = smtpPassword; + options.From = smtpFrom; + options.Encryption = smtpEncryption; }) - .WithGoogleOAuth(new PostaGoogleOAuthOptions + .WithGoogleOAuth(options => { - ClientId = googleClientId, - ClientSecret = googleClientSecret, - CallbackUrl = googleCallbackUrl + options.ClientId = googleClientId; + options.ClientSecret = googleClientSecret; + options.CallbackUrl = googleCallbackUrl; }) - .WithS3BlobStorage(new PostaS3BlobStorageOptions + .WithS3BlobStorage(options => { - Endpoint = s3Endpoint, - Region = s3Region, - Bucket = s3Bucket, - AccessKey = s3AccessKey, - SecretKey = s3SecretKey + options.Endpoint = s3Endpoint; + options.Region = s3Region; + options.Bucket = s3Bucket; + options.AccessKey = s3AccessKey; + options.SecretKey = s3SecretKey; }); ``` @@ -112,15 +112,15 @@ var inboundWebhookSecret = builder.AddParameterFromConfiguration("posta-inbound- var emailVerificationRequired = builder.AddParameterFromConfiguration("posta-email-verification-required", "Posta:EmailVerification:Required"); builder.AddPosta("posta", database, redis) - .WithInboundSmtp(new PostaInboundSmtpOptions + .WithInboundSmtp(options => { - Enabled = inboundEnabled, - Port = inboundPort, - WebhookSecret = inboundWebhookSecret + options.Enabled = inboundEnabled; + options.Port = inboundPort; + options.WebhookSecret = inboundWebhookSecret; }) - .WithEmailVerification(new PostaEmailVerificationOptions + .WithEmailVerification(options => { - Required = emailVerificationRequired + options.Required = emailVerificationRequired; }); ``` @@ -134,10 +134,15 @@ builder.AddPosta("posta", database, redis) | `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. diff --git a/tests/CommunityToolkit.Aspire.Hosting.Posta.Tests/ContainerResourceCreationTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Posta.Tests/ContainerResourceCreationTests.cs index 18973ae85..f75ff0c83 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.Posta.Tests/ContainerResourceCreationTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.Posta.Tests/ContainerResourceCreationTests.cs @@ -43,6 +43,20 @@ public void WithReferenceThrowsWhenRedisIsNull() 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() { @@ -433,6 +447,45 @@ IResourceBuilder Parameter(string name, string value) } } + [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"); From b5d7dff620579f54aa69f8fc3fbaef816a7febcc Mon Sep 17 00:00:00 2001 From: axi Date: Mon, 13 Jul 2026 01:45:51 +0300 Subject: [PATCH 25/28] Add initial implementation of Posta client models and project structure - Introduced core model definitions for Posta API, including support for templates, emails, and applications. - Added project structure, including example, source, and test projects. - Configured dependency requirements, project references, and test configurations. --- ...kit.Aspire.Hosting.Posta.ApiService.csproj | 7 + .../Program.cs | 21 ++ .../Clients/IPostaClient.cs | 22 ++ .../Clients/IPostaSectionClient.cs | 18 + .../Clients/PostaClient.cs | 22 ++ .../Clients/PostaClientBuilderExtensions.cs | 138 +++++++ .../Clients/PostaClientSection.cs | 16 + .../Clients/PostaOperations.cs | 348 ++++++++++++++++++ .../Clients/PostaSectionClient.cs | 36 ++ .../CommunityToolkit.Aspire.Posta.csproj | 19 + .../Configuration/IPostaCredentialProvider.cs | 10 + .../Configuration/PostaClientSettings.cs | 17 + .../Configuration/PostaCredentialProvider.cs | 18 + .../Endpoints/IPostaEndpoints.cs | 50 +++ .../Endpoints/PostaEndpoint.cs | 27 ++ .../Endpoints/PostaEndpoints.cs | 50 +++ .../Emails/GetEmailDeliveryStatusRequest.cs | 12 + .../Emails/GetEmailDeliveryStatusResponse.cs | 16 + .../Models/Emails/GetEmailDetailsRequest.cs | 12 + .../Models/Emails/GetEmailDetailsResponse.cs | 16 + .../Emails/PreviewEmailFromTemplateRequest.cs | 25 ++ .../PreviewEmailFromTemplateResponse.cs | 16 + .../Models/Emails/RetryFailedEmailRequest.cs | 12 + .../Models/Emails/RetryFailedEmailResponse.cs | 16 + .../Models/Emails/SendAnEmailRequest.cs | 57 +++ .../Models/Emails/SendAnEmailResponse.cs | 16 + .../Models/Emails/SendBatchEmailsRequest.cs | 36 ++ .../Models/Emails/SendBatchEmailsResponse.cs | 16 + .../Emails/SendEmailUsingTemplateRequest.cs | 45 +++ .../Emails/SendEmailUsingTemplateResponse.cs | 16 + .../Emails/VerifyAnEmailAddressRequest.cs | 16 + .../Emails/VerifyAnEmailAddressResponse.cs | 16 + .../Models/Health/LivenessProbeResponse.cs | 12 + .../Models/Health/ReadinessProbeResponse.cs | 20 + .../Models/Info/ApplicationInfoResponse.cs | 16 + .../Shared/ApplicationInfoResponseData.cs | 24 ++ .../Shared/CreateTemplateResponseData.cs | 64 ++++ ...CreateTemplateResponseDataActiveVersion.cs | 40 ++ ...ponseDataActiveVersionLocalizationsItem.cs | 44 +++ ...lateResponseDataActiveVersionStylesheet.cs | 36 ++ .../CreateTemplateResponseDataCreatedBy.cs | 16 + .../CreateTemplateResponseDataLastEditedBy.cs | 16 + .../GetEmailDeliveryStatusResponseData.cs | 32 ++ .../Shared/GetEmailDetailsResponseData.cs | 112 ++++++ .../Models/Shared/GetTemplateResponseData.cs | 64 ++++ .../GetTemplateResponseDataActiveVersion.cs | 40 ++ ...ponseDataActiveVersionLocalizationsItem.cs | 44 +++ ...lateResponseDataActiveVersionStylesheet.cs | 36 ++ .../GetTemplateResponseDataCreatedBy.cs | 16 + .../GetTemplateResponseDataLastEditedBy.cs | 16 + .../Shared/ListTemplatesResponseDataItem.cs | 64 ++++ ...tTemplatesResponseDataItemActiveVersion.cs | 40 ++ ...eDataItemActiveVersionLocalizationsItem.cs | 44 +++ ...ResponseDataItemActiveVersionStylesheet.cs | 36 ++ .../ListTemplatesResponseDataItemCreatedBy.cs | 16 + ...stTemplatesResponseDataItemLastEditedBy.cs | 16 + .../Shared/ListTemplatesResponsePageable.cs | 28 ++ .../PreviewEmailFromTemplateResponseData.cs | 20 + .../Shared/PreviewTemplateResponseData.cs | 20 + .../ReSubscribeAnEmailToAListResponseData.cs | 36 ++ .../Shared/RetryFailedEmailResponseData.cs | 16 + .../SendAnEmailRequestAttachmentsItem.cs | 24 ++ .../Shared/SendAnEmailRequestUnsubscribe.cs | 24 ++ .../Models/Shared/SendAnEmailResponseData.cs | 16 + .../SendBatchEmailsRequestRecipientsItem.cs | 21 ++ .../SendBatchEmailsRequestUnsubscribe.cs | 24 ++ .../Shared/SendBatchEmailsResponseData.cs | 28 ++ .../SendBatchEmailsResponseDataResultsItem.cs | 24 ++ ...mailUsingTemplateRequestAttachmentsItem.cs | 24 ++ ...endEmailUsingTemplateRequestUnsubscribe.cs | 24 ++ .../SendEmailUsingTemplateResponseData.cs | 16 + .../Shared/SendTestEmailResponseData.cs | 16 + .../SubscribeAnEmailToAListResponseData.cs | 36 ++ ...UnsubscribeAnEmailFromAListResponseData.cs | 36 ++ .../Shared/UpdateTemplateResponseData.cs | 64 ++++ ...UpdateTemplateResponseDataActiveVersion.cs | 40 ++ ...ponseDataActiveVersionLocalizationsItem.cs | 44 +++ ...lateResponseDataActiveVersionStylesheet.cs | 36 ++ .../UpdateTemplateResponseDataCreatedBy.cs | 16 + .../UpdateTemplateResponseDataLastEditedBy.cs | 16 + .../VerifyAnEmailAddressResponseData.cs | 48 +++ .../VerifyAnEmailAddressResponseDataChecks.cs | 28 ++ .../ReSubscribeAnEmailToAListRequest.cs | 16 + .../ReSubscribeAnEmailToAListResponse.cs | 16 + .../SubscribeAnEmailToAListRequest.cs | 20 + .../SubscribeAnEmailToAListResponse.cs | 16 + .../UnsubscribeAnEmailFromAListRequest.cs | 20 + .../UnsubscribeAnEmailFromAListResponse.cs | 16 + .../Models/Templates/CreateTemplateRequest.cs | 24 ++ .../Templates/CreateTemplateResponse.cs | 16 + .../Models/Templates/DeleteTemplateRequest.cs | 12 + .../Templates/DeleteTemplateResponse.cs | 6 + .../Models/Templates/GetTemplateRequest.cs | 12 + .../Models/Templates/GetTemplateResponse.cs | 16 + .../Models/Templates/ListTemplatesRequest.cs | 20 + .../Models/Templates/ListTemplatesResponse.cs | 20 + .../Templates/PreviewTemplateRequest.cs | 29 ++ .../Templates/PreviewTemplateResponse.cs | 16 + .../Models/Templates/SendTestEmailRequest.cs | 29 ++ .../Models/Templates/SendTestEmailResponse.cs | 16 + .../Models/Templates/UpdateTemplateRequest.cs | 28 ++ .../Templates/UpdateTemplateResponse.cs | 16 + src/CommunityToolkit.Aspire.Posta/README.md | 87 +++++ .../Transport/PostaApiException.cs | 16 + .../Transport/PostaRequest.cs | 23 ++ .../Transport/PostaStreamResponse.cs | 35 ++ .../Transport/PostaTransport.cs | 164 +++++++++ ...CommunityToolkit.Aspire.Posta.Tests.csproj | 12 + .../PostaClientTests.cs | 162 ++++++++ 109 files changed, 3614 insertions(+) create mode 100644 examples/posta/CommunityToolkit.Aspire.Hosting.Posta.ApiService/CommunityToolkit.Aspire.Hosting.Posta.ApiService.csproj create mode 100644 examples/posta/CommunityToolkit.Aspire.Hosting.Posta.ApiService/Program.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Clients/IPostaClient.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Clients/IPostaSectionClient.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Clients/PostaClient.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Clients/PostaClientBuilderExtensions.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Clients/PostaClientSection.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Clients/PostaOperations.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Clients/PostaSectionClient.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/CommunityToolkit.Aspire.Posta.csproj create mode 100644 src/CommunityToolkit.Aspire.Posta/Configuration/IPostaCredentialProvider.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Configuration/PostaClientSettings.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Configuration/PostaCredentialProvider.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Endpoints/IPostaEndpoints.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Endpoints/PostaEndpoint.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Endpoints/PostaEndpoints.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Models/Emails/GetEmailDeliveryStatusRequest.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Models/Emails/GetEmailDeliveryStatusResponse.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Models/Emails/GetEmailDetailsRequest.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Models/Emails/GetEmailDetailsResponse.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Models/Emails/PreviewEmailFromTemplateRequest.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Models/Emails/PreviewEmailFromTemplateResponse.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Models/Emails/RetryFailedEmailRequest.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Models/Emails/RetryFailedEmailResponse.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Models/Emails/SendAnEmailRequest.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Models/Emails/SendAnEmailResponse.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Models/Emails/SendBatchEmailsRequest.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Models/Emails/SendBatchEmailsResponse.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Models/Emails/SendEmailUsingTemplateRequest.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Models/Emails/SendEmailUsingTemplateResponse.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Models/Emails/VerifyAnEmailAddressRequest.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Models/Emails/VerifyAnEmailAddressResponse.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Models/Health/LivenessProbeResponse.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Models/Health/ReadinessProbeResponse.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Models/Info/ApplicationInfoResponse.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Models/Shared/ApplicationInfoResponseData.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Models/Shared/CreateTemplateResponseData.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Models/Shared/CreateTemplateResponseDataActiveVersion.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Models/Shared/CreateTemplateResponseDataActiveVersionLocalizationsItem.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Models/Shared/CreateTemplateResponseDataActiveVersionStylesheet.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Models/Shared/CreateTemplateResponseDataCreatedBy.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Models/Shared/CreateTemplateResponseDataLastEditedBy.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Models/Shared/GetEmailDeliveryStatusResponseData.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Models/Shared/GetEmailDetailsResponseData.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Models/Shared/GetTemplateResponseData.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Models/Shared/GetTemplateResponseDataActiveVersion.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Models/Shared/GetTemplateResponseDataActiveVersionLocalizationsItem.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Models/Shared/GetTemplateResponseDataActiveVersionStylesheet.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Models/Shared/GetTemplateResponseDataCreatedBy.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Models/Shared/GetTemplateResponseDataLastEditedBy.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Models/Shared/ListTemplatesResponseDataItem.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Models/Shared/ListTemplatesResponseDataItemActiveVersion.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Models/Shared/ListTemplatesResponseDataItemActiveVersionLocalizationsItem.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Models/Shared/ListTemplatesResponseDataItemActiveVersionStylesheet.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Models/Shared/ListTemplatesResponseDataItemCreatedBy.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Models/Shared/ListTemplatesResponseDataItemLastEditedBy.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Models/Shared/ListTemplatesResponsePageable.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Models/Shared/PreviewEmailFromTemplateResponseData.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Models/Shared/PreviewTemplateResponseData.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Models/Shared/ReSubscribeAnEmailToAListResponseData.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Models/Shared/RetryFailedEmailResponseData.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Models/Shared/SendAnEmailRequestAttachmentsItem.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Models/Shared/SendAnEmailRequestUnsubscribe.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Models/Shared/SendAnEmailResponseData.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Models/Shared/SendBatchEmailsRequestRecipientsItem.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Models/Shared/SendBatchEmailsRequestUnsubscribe.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Models/Shared/SendBatchEmailsResponseData.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Models/Shared/SendBatchEmailsResponseDataResultsItem.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Models/Shared/SendEmailUsingTemplateRequestAttachmentsItem.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Models/Shared/SendEmailUsingTemplateRequestUnsubscribe.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Models/Shared/SendEmailUsingTemplateResponseData.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Models/Shared/SendTestEmailResponseData.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Models/Shared/SubscribeAnEmailToAListResponseData.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Models/Shared/UnsubscribeAnEmailFromAListResponseData.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Models/Shared/UpdateTemplateResponseData.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Models/Shared/UpdateTemplateResponseDataActiveVersion.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Models/Shared/UpdateTemplateResponseDataActiveVersionLocalizationsItem.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Models/Shared/UpdateTemplateResponseDataActiveVersionStylesheet.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Models/Shared/UpdateTemplateResponseDataCreatedBy.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Models/Shared/UpdateTemplateResponseDataLastEditedBy.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Models/Shared/VerifyAnEmailAddressResponseData.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Models/Shared/VerifyAnEmailAddressResponseDataChecks.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Models/SubscriberLists/ReSubscribeAnEmailToAListRequest.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Models/SubscriberLists/ReSubscribeAnEmailToAListResponse.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Models/SubscriberLists/SubscribeAnEmailToAListRequest.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Models/SubscriberLists/SubscribeAnEmailToAListResponse.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Models/SubscriberLists/UnsubscribeAnEmailFromAListRequest.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Models/SubscriberLists/UnsubscribeAnEmailFromAListResponse.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Models/Templates/CreateTemplateRequest.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Models/Templates/CreateTemplateResponse.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Models/Templates/DeleteTemplateRequest.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Models/Templates/DeleteTemplateResponse.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Models/Templates/GetTemplateRequest.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Models/Templates/GetTemplateResponse.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Models/Templates/ListTemplatesRequest.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Models/Templates/ListTemplatesResponse.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Models/Templates/PreviewTemplateRequest.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Models/Templates/PreviewTemplateResponse.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Models/Templates/SendTestEmailRequest.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Models/Templates/SendTestEmailResponse.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Models/Templates/UpdateTemplateRequest.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Models/Templates/UpdateTemplateResponse.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/README.md create mode 100644 src/CommunityToolkit.Aspire.Posta/Transport/PostaApiException.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Transport/PostaRequest.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Transport/PostaStreamResponse.cs create mode 100644 src/CommunityToolkit.Aspire.Posta/Transport/PostaTransport.cs create mode 100644 tests/CommunityToolkit.Aspire.Posta.Tests/CommunityToolkit.Aspire.Posta.Tests.csproj create mode 100644 tests/CommunityToolkit.Aspire.Posta.Tests/PostaClientTests.cs diff --git a/examples/posta/CommunityToolkit.Aspire.Hosting.Posta.ApiService/CommunityToolkit.Aspire.Hosting.Posta.ApiService.csproj b/examples/posta/CommunityToolkit.Aspire.Hosting.Posta.ApiService/CommunityToolkit.Aspire.Hosting.Posta.ApiService.csproj new file mode 100644 index 000000000..3bba1706c --- /dev/null +++ b/examples/posta/CommunityToolkit.Aspire.Hosting.Posta.ApiService/CommunityToolkit.Aspire.Hosting.Posta.ApiService.csproj @@ -0,0 +1,7 @@ + + + + + + + diff --git a/examples/posta/CommunityToolkit.Aspire.Hosting.Posta.ApiService/Program.cs b/examples/posta/CommunityToolkit.Aspire.Hosting.Posta.ApiService/Program.cs new file mode 100644 index 000000000..b070a20bb --- /dev/null +++ b/examples/posta/CommunityToolkit.Aspire.Hosting.Posta.ApiService/Program.cs @@ -0,0 +1,21 @@ +using CommunityToolkit.Aspire.Posta.Clients; +using CommunityToolkit.Aspire.Posta.Models.Info; + +WebApplicationBuilder builder = WebApplication.CreateBuilder(args); + +builder.AddPostaClient("posta"); + +WebApplication app = builder.Build(); + +app.MapGet("/", () => Results.Ok(new { Name = "Posta client example" })); + +app.MapGet("/posta/info", async (IPostaClient posta, CancellationToken cancellationToken) => +{ + ApplicationInfoResponse? response = await posta.Info.ApplicationInfoAsync(cancellationToken: cancellationToken); + + return response is null ? Results.NoContent() : Results.Ok(response); +}); + +app.MapGet("/health", () => Results.Ok()); + +app.Run(); \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Clients/IPostaClient.cs b/src/CommunityToolkit.Aspire.Posta/Clients/IPostaClient.cs new file mode 100644 index 000000000..c3aa69ff3 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Clients/IPostaClient.cs @@ -0,0 +1,22 @@ +using CommunityToolkit.Aspire.Posta.Endpoints; + +namespace CommunityToolkit.Aspire.Posta.Clients; + +/// +/// Provides access to all Posta API sections and endpoint definitions. +/// +public interface IPostaClient +{ + /// Gets the overridable endpoint catalog. + IPostaEndpoints Endpoints { get; } + /// Gets email operations. + IPostaEmailsClient Emails { get; } + /// Gets template operations. + IPostaTemplatesClient Templates { get; } + /// Gets subscriber-list operations. + IPostaSubscriberListsClient SubscriberLists { get; } + /// Gets health operations. + IPostaHealthClient Health { get; } + /// Gets server information operations. + IPostaInfoClient Info { get; } +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Clients/IPostaSectionClient.cs b/src/CommunityToolkit.Aspire.Posta/Clients/IPostaSectionClient.cs new file mode 100644 index 000000000..ff5e0eef2 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Clients/IPostaSectionClient.cs @@ -0,0 +1,18 @@ +using CommunityToolkit.Aspire.Posta.Endpoints; +using CommunityToolkit.Aspire.Posta.Transport; +using System.Text.Json; + +namespace CommunityToolkit.Aspire.Posta.Clients; + +/// Common operations exposed by each Posta API section. +public interface IPostaSectionClient +{ + /// Invokes an endpoint and deserializes its JSON response. + Task SendAsync(PostaEndpoint endpoint, PostaRequest? request = null, CancellationToken cancellationToken = default); + + /// Invokes an endpoint and returns its JSON response. + Task SendAsync(PostaEndpoint endpoint, PostaRequest? request = null, CancellationToken cancellationToken = default); + + /// Invokes an endpoint and returns a streaming response. Disposing the result disposes the HTTP response. + Task SendStreamAsync(PostaEndpoint endpoint, PostaRequest? request = null, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Clients/PostaClient.cs b/src/CommunityToolkit.Aspire.Posta/Clients/PostaClient.cs new file mode 100644 index 000000000..d98337a1f --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Clients/PostaClient.cs @@ -0,0 +1,22 @@ +using CommunityToolkit.Aspire.Posta.Endpoints; +using CommunityToolkit.Aspire.Posta.Transport; + +namespace CommunityToolkit.Aspire.Posta.Clients; + +internal sealed class PostaClient : IPostaClient +{ + private readonly PostaClientSection _section; + + internal PostaClient(PostaTransport transport, IPostaEndpoints endpoints) + { + _section = new PostaClientSection(transport, endpoints); + Endpoints = endpoints; + } + + public IPostaEndpoints Endpoints { get; } + public IPostaEmailsClient Emails => _section; + public IPostaTemplatesClient Templates => _section; + public IPostaSubscriberListsClient SubscriberLists => _section; + public IPostaHealthClient Health => _section; + public IPostaInfoClient Info => _section; +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Clients/PostaClientBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Posta/Clients/PostaClientBuilderExtensions.cs new file mode 100644 index 000000000..ad1498c94 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Clients/PostaClientBuilderExtensions.cs @@ -0,0 +1,138 @@ +using CommunityToolkit.Aspire.Posta.Clients; +using CommunityToolkit.Aspire.Posta.Configuration; +using CommunityToolkit.Aspire.Posta.Endpoints; +using CommunityToolkit.Aspire.Posta.Transport; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +#pragma warning disable IDE0130 +namespace Microsoft.Extensions.Hosting; +#pragma warning restore IDE0130 + +/// Extension methods for registering a Posta API client. +public static class AspirePostaExtensions +{ + private const string DefaultConfigurationSection = "Aspire:Posta:Client"; + + /// Registers a keyed using Aspire service discovery or a configured endpoint. + /// The application builder. + /// The Posta resource or connection string name. + /// An optional settings callback applied last. + /// An optional configuration section name. + public static void AddPostaClient( + this IHostApplicationBuilder builder, + string connectionName, + Action? configureSettings = null, + string? configurationSectionName = null) + { + AddPostaClientCore(builder, connectionName, connectionName, registerDefault: true, configureSettings, configurationSectionName); + } + + /// Registers a keyed whose service key is the connection name. + /// The application builder. + /// The Posta resource or connection string name and service key. + /// An optional settings callback applied last. + /// An optional configuration section name. + public static void AddKeyedPostaClient( + this IHostApplicationBuilder builder, + string connectionName, + Action? configureSettings = null, + string? configurationSectionName = null) + { + AddPostaClientCore(builder, connectionName, connectionName, registerDefault: false, configureSettings, configurationSectionName); + } + + /// Registers a keyed using a custom service key. + /// The application builder. + /// The dependency injection service key. + /// The Posta resource or connection string name. + /// An optional settings callback applied last. + /// An optional configuration section name. + public static void AddKeyedPostaClient( + this IHostApplicationBuilder builder, + object serviceKey, + string connectionName, + Action? configureSettings = null, + string? configurationSectionName = null) + { + ArgumentNullException.ThrowIfNull(serviceKey); + AddPostaClientCore(builder, serviceKey, connectionName, registerDefault: false, configureSettings, configurationSectionName); + } + + private static void AddPostaClientCore( + IHostApplicationBuilder builder, + object serviceKey, + string connectionName, + bool registerDefault, + Action? configureSettings, + string? configurationSectionName) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentException.ThrowIfNullOrWhiteSpace(connectionName); + + PostaClientSettings settings = new(); + string sectionName = configurationSectionName ?? DefaultConfigurationSection; + builder.Configuration.GetSection(sectionName).Bind(settings); + builder.Configuration.GetSection($"{sectionName}:{connectionName}").Bind(settings); + ApplyConnectionString(settings, builder.Configuration.GetConnectionString(connectionName)); + configureSettings?.Invoke(settings); + + Uri endpoint = settings.Endpoint ?? new Uri($"http://{connectionName}", UriKind.Absolute); + builder.Services.AddHttpClient($"Posta:{connectionName}", client => + { + client.BaseAddress = endpoint; + client.Timeout = settings.Timeout; + }).AddServiceDiscovery(); + + builder.Services.TryAddSingleton(); + builder.Services.AddKeyedSingleton(serviceKey, (services, _) => + { + IPostaCredentialProvider credentialProvider = services.GetService() ?? new PostaCredentialProvider(settings); + HttpClient httpClient = services.GetRequiredService().CreateClient($"Posta:{connectionName}"); + return new PostaClient(new PostaTransport(httpClient, credentialProvider), services.GetRequiredService()); + }); + if (registerDefault) + { + builder.Services.TryAddSingleton(services => services.GetRequiredKeyedService(serviceKey)); + } + } + + private static void ApplyConnectionString(PostaClientSettings settings, string? connectionString) + { + if (string.IsNullOrWhiteSpace(connectionString)) + { + return; + } + + if (Uri.TryCreate(connectionString, UriKind.Absolute, out Uri? uri)) + { + settings.Endpoint = uri; + return; + } + + foreach (string part in connectionString.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) + { + int separator = part.IndexOf('='); + if (separator <= 0) + { + continue; + } + + string name = part[..separator]; + string value = part[(separator + 1)..]; + if (name.Equals("Endpoint", StringComparison.OrdinalIgnoreCase) && Uri.TryCreate(value, UriKind.Absolute, out uri)) + { + settings.Endpoint = uri; + } + else if (name.Equals("ApiKey", StringComparison.OrdinalIgnoreCase)) + { + settings.ApiKey = value; + } + else if (name.Equals("AccessToken", StringComparison.OrdinalIgnoreCase)) + { + settings.AccessToken = value; + } + } + } +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Clients/PostaClientSection.cs b/src/CommunityToolkit.Aspire.Posta/Clients/PostaClientSection.cs new file mode 100644 index 000000000..c018e4d75 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Clients/PostaClientSection.cs @@ -0,0 +1,16 @@ +using CommunityToolkit.Aspire.Posta.Endpoints; +using CommunityToolkit.Aspire.Posta.Transport; + +namespace CommunityToolkit.Aspire.Posta.Clients; + +internal sealed partial class PostaClientSection : PostaSectionClient, + IPostaEmailsClient, IPostaTemplatesClient, IPostaSubscriberListsClient, IPostaHealthClient, IPostaInfoClient +{ + private readonly IPostaEndpoints _endpoints; + + internal PostaClientSection(PostaTransport transport, IPostaEndpoints endpoints) + : base(transport) + { + _endpoints = endpoints; + } +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Clients/PostaOperations.cs b/src/CommunityToolkit.Aspire.Posta/Clients/PostaOperations.cs new file mode 100644 index 000000000..e28617092 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Clients/PostaOperations.cs @@ -0,0 +1,348 @@ +using CommunityToolkit.Aspire.Posta.Endpoints; +using CommunityToolkit.Aspire.Posta.Transport; +using Emails = CommunityToolkit.Aspire.Posta.Models.Emails; +using Health = CommunityToolkit.Aspire.Posta.Models.Health; +using Info = CommunityToolkit.Aspire.Posta.Models.Info; +using SubscriberLists = CommunityToolkit.Aspire.Posta.Models.SubscriberLists; +using Templates = CommunityToolkit.Aspire.Posta.Models.Templates; + +namespace CommunityToolkit.Aspire.Posta.Clients; + +/// Provides Posta emails operations. +public interface IPostaEmailsClient : IPostaSectionClient +{ + /// Invokes the SendBatchEmails operation. + Task SendBatchEmailsAsync(Emails.SendBatchEmailsRequest request, CancellationToken cancellationToken = default); + + /// Invokes the PreviewEmailFromTemplate operation. + Task PreviewEmailFromTemplateAsync(Emails.PreviewEmailFromTemplateRequest request, CancellationToken cancellationToken = default); + + /// Invokes the SendAnEmail operation. + Task SendAnEmailAsync(Emails.SendAnEmailRequest request, CancellationToken cancellationToken = default); + + /// Invokes the SendEmailUsingTemplate operation. + Task SendEmailUsingTemplateAsync(Emails.SendEmailUsingTemplateRequest request, CancellationToken cancellationToken = default); + + /// Invokes the VerifyAnEmailAddress operation. + Task VerifyAnEmailAddressAsync(Emails.VerifyAnEmailAddressRequest request, CancellationToken cancellationToken = default); + + /// Invokes the GetEmailDetails operation. + Task GetEmailDetailsAsync(Emails.GetEmailDetailsRequest request, CancellationToken cancellationToken = default); + + /// Invokes the RetryFailedEmail operation. + Task RetryFailedEmailAsync(Emails.RetryFailedEmailRequest request, CancellationToken cancellationToken = default); + + /// Invokes the GetEmailDeliveryStatus operation. + Task GetEmailDeliveryStatusAsync(Emails.GetEmailDeliveryStatusRequest request, CancellationToken cancellationToken = default); + +} + +/// Provides Posta templates operations. +public interface IPostaTemplatesClient : IPostaSectionClient +{ + /// Invokes the ListTemplates operation. + Task ListTemplatesAsync(Templates.ListTemplatesRequest? request = null, CancellationToken cancellationToken = default); + + /// Invokes the CreateTemplate operation. + Task CreateTemplateAsync(Templates.CreateTemplateRequest request, CancellationToken cancellationToken = default); + + /// Invokes the PreviewTemplate operation. + Task PreviewTemplateAsync(Templates.PreviewTemplateRequest request, CancellationToken cancellationToken = default); + + /// Invokes the DeleteTemplate operation. + Task DeleteTemplateAsync(Templates.DeleteTemplateRequest request, CancellationToken cancellationToken = default); + + /// Invokes the GetTemplate operation. + Task GetTemplateAsync(Templates.GetTemplateRequest request, CancellationToken cancellationToken = default); + + /// Invokes the UpdateTemplate operation. + Task UpdateTemplateAsync(Templates.UpdateTemplateRequest request, CancellationToken cancellationToken = default); + + /// Invokes the SendTestEmail operation. + Task SendTestEmailAsync(Templates.SendTestEmailRequest request, CancellationToken cancellationToken = default); + +} + +/// Provides Posta subscriberlists operations. +public interface IPostaSubscriberListsClient : IPostaSectionClient +{ + /// Invokes the SubscribeAnEmailToAList operation. + Task SubscribeAnEmailToAListAsync(SubscriberLists.SubscribeAnEmailToAListRequest request, CancellationToken cancellationToken = default); + + /// Invokes the ReSubscribeAnEmailToAList operation. + Task ReSubscribeAnEmailToAListAsync(SubscriberLists.ReSubscribeAnEmailToAListRequest request, CancellationToken cancellationToken = default); + + /// Invokes the UnsubscribeAnEmailFromAList operation. + Task UnsubscribeAnEmailFromAListAsync(SubscriberLists.UnsubscribeAnEmailFromAListRequest request, CancellationToken cancellationToken = default); + +} + +/// Provides Posta health operations. +public interface IPostaHealthClient : IPostaSectionClient +{ + /// Invokes the LivenessProbe operation. + Task LivenessProbeAsync(CancellationToken cancellationToken = default); + + /// Invokes the ReadinessProbe operation. + Task ReadinessProbeAsync(CancellationToken cancellationToken = default); + +} + +/// Provides Posta info operations. +public interface IPostaInfoClient : IPostaSectionClient +{ + /// Invokes the ApplicationInfo operation. + Task ApplicationInfoAsync(CancellationToken cancellationToken = default); + +} + +internal sealed partial class PostaClientSection +{ + public Task SendBatchEmailsAsync(Emails.SendBatchEmailsRequest request, CancellationToken cancellationToken = default) + { + PostaRequest postaRequest = new() + { + Body = request, + Query = new Dictionary + { + ["dry_run"] = request?.DryRun, + }, + }; + + return SendAsync(_endpoints.SendBatchEmails, postaRequest, cancellationToken); + } + + public Task PreviewEmailFromTemplateAsync(Emails.PreviewEmailFromTemplateRequest request, CancellationToken cancellationToken = default) + { + PostaRequest postaRequest = new() + { + Body = request, + }; + + return SendAsync(_endpoints.PreviewEmailFromTemplate, postaRequest, cancellationToken); + } + + public Task SendAnEmailAsync(Emails.SendAnEmailRequest request, CancellationToken cancellationToken = default) + { + PostaRequest postaRequest = new() + { + Body = request, + Query = new Dictionary + { + ["dry_run"] = request?.DryRun, + }, + }; + + return SendAsync(_endpoints.SendAnEmail, postaRequest, cancellationToken); + } + + public Task SendEmailUsingTemplateAsync(Emails.SendEmailUsingTemplateRequest request, CancellationToken cancellationToken = default) + { + PostaRequest postaRequest = new() + { + Body = request, + Query = new Dictionary + { + ["dry_run"] = request?.DryRun, + }, + }; + + return SendAsync(_endpoints.SendEmailUsingTemplate, postaRequest, cancellationToken); + } + + public Task VerifyAnEmailAddressAsync(Emails.VerifyAnEmailAddressRequest request, CancellationToken cancellationToken = default) + { + PostaRequest postaRequest = new() + { + Body = request, + Query = new Dictionary + { + ["fresh"] = request?.Fresh, + }, + }; + + return SendAsync(_endpoints.VerifyAnEmailAddress, postaRequest, cancellationToken); + } + + public Task GetEmailDetailsAsync(Emails.GetEmailDetailsRequest request, CancellationToken cancellationToken = default) + { + PostaRequest postaRequest = new() + { + PathParameters = new Dictionary + { + ["id"] = request?.Id, + }, + }; + + return SendAsync(_endpoints.GetEmailDetails, postaRequest, cancellationToken); + } + + public Task RetryFailedEmailAsync(Emails.RetryFailedEmailRequest request, CancellationToken cancellationToken = default) + { + PostaRequest postaRequest = new() + { + PathParameters = new Dictionary + { + ["id"] = request?.Id, + }, + }; + + return SendAsync(_endpoints.RetryFailedEmail, postaRequest, cancellationToken); + } + + public Task GetEmailDeliveryStatusAsync(Emails.GetEmailDeliveryStatusRequest request, CancellationToken cancellationToken = default) + { + PostaRequest postaRequest = new() + { + PathParameters = new Dictionary + { + ["id"] = request?.Id, + }, + }; + + return SendAsync(_endpoints.GetEmailDeliveryStatus, postaRequest, cancellationToken); + } + + public Task ListTemplatesAsync(Templates.ListTemplatesRequest? request = null, CancellationToken cancellationToken = default) + { + PostaRequest postaRequest = new() + { + Query = new Dictionary + { + ["page"] = request?.Page, + ["size"] = request?.Size, + ["search"] = request?.Search, + }, + }; + + return SendAsync(_endpoints.ListTemplates, postaRequest, cancellationToken); + } + + public Task CreateTemplateAsync(Templates.CreateTemplateRequest request, CancellationToken cancellationToken = default) + { + PostaRequest postaRequest = new() + { + Body = request, + }; + + return SendAsync(_endpoints.CreateTemplate, postaRequest, cancellationToken); + } + + public Task PreviewTemplateAsync(Templates.PreviewTemplateRequest request, CancellationToken cancellationToken = default) + { + PostaRequest postaRequest = new() + { + Body = request, + }; + + return SendAsync(_endpoints.PreviewTemplate, postaRequest, cancellationToken); + } + + public Task DeleteTemplateAsync(Templates.DeleteTemplateRequest request, CancellationToken cancellationToken = default) + { + PostaRequest postaRequest = new() + { + PathParameters = new Dictionary + { + ["id"] = request?.Id, + }, + }; + + return SendAsync(_endpoints.DeleteTemplate, postaRequest, cancellationToken); + } + + public Task GetTemplateAsync(Templates.GetTemplateRequest request, CancellationToken cancellationToken = default) + { + PostaRequest postaRequest = new() + { + PathParameters = new Dictionary + { + ["id"] = request?.Id, + }, + }; + + return SendAsync(_endpoints.GetTemplate, postaRequest, cancellationToken); + } + + public Task UpdateTemplateAsync(Templates.UpdateTemplateRequest request, CancellationToken cancellationToken = default) + { + PostaRequest postaRequest = new() + { + Body = request, + PathParameters = new Dictionary + { + ["id"] = request?.Id, + }, + }; + + return SendAsync(_endpoints.UpdateTemplate, postaRequest, cancellationToken); + } + + public Task SendTestEmailAsync(Templates.SendTestEmailRequest request, CancellationToken cancellationToken = default) + { + PostaRequest postaRequest = new() + { + Body = request, + PathParameters = new Dictionary + { + ["id"] = request?.Id, + }, + }; + + return SendAsync(_endpoints.SendTestEmail, postaRequest, cancellationToken); + } + + public Task SubscribeAnEmailToAListAsync(SubscriberLists.SubscribeAnEmailToAListRequest request, CancellationToken cancellationToken = default) + { + PostaRequest postaRequest = new() + { + Body = request, + }; + + return SendAsync(_endpoints.SubscribeAnEmailToAList, postaRequest, cancellationToken); + } + + public Task ReSubscribeAnEmailToAListAsync(SubscriberLists.ReSubscribeAnEmailToAListRequest request, CancellationToken cancellationToken = default) + { + PostaRequest postaRequest = new() + { + Body = request, + PathParameters = new Dictionary + { + ["id"] = request?.Id, + }, + }; + + return SendAsync(_endpoints.ReSubscribeAnEmailToAList, postaRequest, cancellationToken); + } + + public Task UnsubscribeAnEmailFromAListAsync(SubscriberLists.UnsubscribeAnEmailFromAListRequest request, CancellationToken cancellationToken = default) + { + PostaRequest postaRequest = new() + { + Body = request, + PathParameters = new Dictionary + { + ["id"] = request?.Id, + }, + }; + + return SendAsync(_endpoints.UnsubscribeAnEmailFromAList, postaRequest, cancellationToken); + } + + public Task LivenessProbeAsync(CancellationToken cancellationToken = default) + { + return SendAsync(_endpoints.LivenessProbe, null, cancellationToken); + } + + public Task ReadinessProbeAsync(CancellationToken cancellationToken = default) + { + return SendAsync(_endpoints.ReadinessProbe, null, cancellationToken); + } + + public Task ApplicationInfoAsync(CancellationToken cancellationToken = default) + { + return SendAsync(_endpoints.ApplicationInfo, null, cancellationToken); + } + +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Clients/PostaSectionClient.cs b/src/CommunityToolkit.Aspire.Posta/Clients/PostaSectionClient.cs new file mode 100644 index 000000000..9bcbda3cb --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Clients/PostaSectionClient.cs @@ -0,0 +1,36 @@ +using CommunityToolkit.Aspire.Posta.Endpoints; +using CommunityToolkit.Aspire.Posta.Transport; +using System.Text.Json; + +namespace CommunityToolkit.Aspire.Posta.Clients; + +/// +/// Base implementation used by Posta API section clients. +/// +public abstract class PostaSectionClient : IPostaSectionClient +{ + private readonly PostaTransport _transport; + + internal PostaSectionClient(PostaTransport transport) + { + _transport = transport; + } + + /// + public Task SendAsync(PostaEndpoint endpoint, PostaRequest? request = null, CancellationToken cancellationToken = default) + { + return _transport.SendAsync(endpoint, request, cancellationToken); + } + + /// + public Task SendAsync(PostaEndpoint endpoint, PostaRequest? request = null, CancellationToken cancellationToken = default) + { + return _transport.SendAsync(endpoint, request, cancellationToken); + } + + /// + public Task SendStreamAsync(PostaEndpoint endpoint, PostaRequest? request = null, CancellationToken cancellationToken = default) + { + return _transport.SendStreamAsync(endpoint, request, cancellationToken); + } +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/CommunityToolkit.Aspire.Posta.csproj b/src/CommunityToolkit.Aspire.Posta/CommunityToolkit.Aspire.Posta.csproj new file mode 100644 index 000000000..9999650eb --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/CommunityToolkit.Aspire.Posta.csproj @@ -0,0 +1,19 @@ + + + + An Aspire client integration for the Posta email delivery platform. + aspire posta email client + + + + + + + + + + + + + + diff --git a/src/CommunityToolkit.Aspire.Posta/Configuration/IPostaCredentialProvider.cs b/src/CommunityToolkit.Aspire.Posta/Configuration/IPostaCredentialProvider.cs new file mode 100644 index 000000000..bfdf7dca3 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Configuration/IPostaCredentialProvider.cs @@ -0,0 +1,10 @@ +using CommunityToolkit.Aspire.Posta.Endpoints; + +namespace CommunityToolkit.Aspire.Posta.Configuration; + +/// Resolves bearer credentials for Posta requests. +public interface IPostaCredentialProvider +{ + /// Returns the credential for the requested authentication mode. + ValueTask GetCredentialAsync(PostaAuthentication authentication, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Configuration/PostaClientSettings.cs b/src/CommunityToolkit.Aspire.Posta/Configuration/PostaClientSettings.cs new file mode 100644 index 000000000..ef6066bd6 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Configuration/PostaClientSettings.cs @@ -0,0 +1,17 @@ +namespace CommunityToolkit.Aspire.Posta.Configuration; + +/// Configuration used by the Posta HTTP client. +public sealed class PostaClientSettings +{ + /// Gets or sets the Posta server base address. + public Uri? Endpoint { get; set; } + + /// Gets or sets a long-lived Posta API key. + public string? ApiKey { get; set; } + + /// Gets or sets a JWT issued by the Posta login endpoint. + public string? AccessToken { get; set; } + + /// Gets or sets the request timeout. + public TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(100); +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Configuration/PostaCredentialProvider.cs b/src/CommunityToolkit.Aspire.Posta/Configuration/PostaCredentialProvider.cs new file mode 100644 index 000000000..b56f42863 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Configuration/PostaCredentialProvider.cs @@ -0,0 +1,18 @@ +using CommunityToolkit.Aspire.Posta.Endpoints; + +namespace CommunityToolkit.Aspire.Posta.Configuration; + +internal sealed class PostaCredentialProvider(PostaClientSettings settings) : IPostaCredentialProvider +{ + public ValueTask GetCredentialAsync(PostaAuthentication authentication, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + return ValueTask.FromResult(authentication switch + { + PostaAuthentication.None => null, + PostaAuthentication.ApiKey => settings.ApiKey, + PostaAuthentication.AccessToken => settings.AccessToken, + _ => throw new ArgumentOutOfRangeException(nameof(authentication)) + }); + } +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Endpoints/IPostaEndpoints.cs b/src/CommunityToolkit.Aspire.Posta/Endpoints/IPostaEndpoints.cs new file mode 100644 index 000000000..6e92de5ce --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Endpoints/IPostaEndpoints.cs @@ -0,0 +1,50 @@ +namespace CommunityToolkit.Aspire.Posta.Endpoints; + +/// +/// Defines the application-facing Posta API endpoints supported by this client. +/// +public interface IPostaEndpoints +{ + /// Gets the send email endpoint. + PostaEndpoint SendAnEmail { get; } + /// Gets the batch email endpoint. + PostaEndpoint SendBatchEmails { get; } + /// Gets the send-template endpoint. + PostaEndpoint SendEmailUsingTemplate { get; } + /// Gets the email preview endpoint. + PostaEndpoint PreviewEmailFromTemplate { get; } + /// Gets the email verification endpoint. + PostaEndpoint VerifyAnEmailAddress { get; } + /// Gets the email details endpoint. + PostaEndpoint GetEmailDetails { get; } + /// Gets the email status endpoint. + PostaEndpoint GetEmailDeliveryStatus { get; } + /// Gets the email retry endpoint. + PostaEndpoint RetryFailedEmail { get; } + /// Gets the templates collection endpoint. + PostaEndpoint ListTemplates { get; } + /// Gets the create-template endpoint. + PostaEndpoint CreateTemplate { get; } + /// Gets the template details endpoint. + PostaEndpoint GetTemplate { get; } + /// Gets the update-template endpoint. + PostaEndpoint UpdateTemplate { get; } + /// Gets the delete-template endpoint. + PostaEndpoint DeleteTemplate { get; } + /// Gets the template preview endpoint. + PostaEndpoint PreviewTemplate { get; } + /// Gets the template test-send endpoint. + PostaEndpoint SendTestEmail { get; } + /// Gets the subscriber-list subscription endpoint. + PostaEndpoint SubscribeAnEmailToAList { get; } + /// Gets the subscriber-list unsubscribe endpoint. + PostaEndpoint UnsubscribeAnEmailFromAList { get; } + /// Gets the subscriber-list resubscribe endpoint. + PostaEndpoint ReSubscribeAnEmailToAList { get; } + /// Gets the liveness endpoint. + PostaEndpoint LivenessProbe { get; } + /// Gets the readiness endpoint. + PostaEndpoint ReadinessProbe { get; } + /// Gets the application information endpoint. + PostaEndpoint ApplicationInfo { get; } +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Endpoints/PostaEndpoint.cs b/src/CommunityToolkit.Aspire.Posta/Endpoints/PostaEndpoint.cs new file mode 100644 index 000000000..b421f3fdc --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Endpoints/PostaEndpoint.cs @@ -0,0 +1,27 @@ +namespace CommunityToolkit.Aspire.Posta.Endpoints; + +/// Describes a Posta API operation. +/// The HTTP method. +/// The relative path template. +/// The authentication required by the operation. +/// Whether the published OpenAPI contract is complete enough for the default transport. +/// An optional implementation note. +public sealed record PostaEndpoint( + HttpMethod Method, + string Path, + PostaAuthentication Authentication, + bool IsImplemented = true, + string? Documentation = null); + +/// Authentication modes used by Posta. +public enum PostaAuthentication +{ + /// No authentication is sent. + None, + + /// A long-lived API key is sent as a bearer credential. + ApiKey, + + /// A JWT access token is sent as a bearer credential. + AccessToken +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Endpoints/PostaEndpoints.cs b/src/CommunityToolkit.Aspire.Posta/Endpoints/PostaEndpoints.cs new file mode 100644 index 000000000..8000a8d66 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Endpoints/PostaEndpoints.cs @@ -0,0 +1,50 @@ +namespace CommunityToolkit.Aspire.Posta.Endpoints; + +/// +/// Provides the default application-facing Posta API endpoint definitions. +/// +public class PostaEndpoints : IPostaEndpoints +{ + /// + public virtual PostaEndpoint SendAnEmail { get; } = new(HttpMethod.Post, "/api/v1/emails/send", PostaAuthentication.ApiKey); + /// + public virtual PostaEndpoint SendBatchEmails { get; } = new(HttpMethod.Post, "/api/v1/emails/batch", PostaAuthentication.ApiKey); + /// + public virtual PostaEndpoint SendEmailUsingTemplate { get; } = new(HttpMethod.Post, "/api/v1/emails/send-template", PostaAuthentication.ApiKey); + /// + public virtual PostaEndpoint PreviewEmailFromTemplate { get; } = new(HttpMethod.Post, "/api/v1/emails/preview", PostaAuthentication.ApiKey); + /// + public virtual PostaEndpoint VerifyAnEmailAddress { get; } = new(HttpMethod.Post, "/api/v1/emails/verify", PostaAuthentication.ApiKey); + /// + public virtual PostaEndpoint GetEmailDetails { get; } = new(HttpMethod.Get, "/api/v1/emails/{id}", PostaAuthentication.ApiKey); + /// + public virtual PostaEndpoint GetEmailDeliveryStatus { get; } = new(HttpMethod.Get, "/api/v1/emails/{id}/status", PostaAuthentication.ApiKey); + /// + public virtual PostaEndpoint RetryFailedEmail { get; } = new(HttpMethod.Post, "/api/v1/emails/{id}/retry", PostaAuthentication.ApiKey); + /// + public virtual PostaEndpoint ListTemplates { get; } = new(HttpMethod.Get, "/api/v1/workspaces/current/templates", PostaAuthentication.AccessToken); + /// + public virtual PostaEndpoint CreateTemplate { get; } = new(HttpMethod.Post, "/api/v1/workspaces/current/templates", PostaAuthentication.AccessToken); + /// + public virtual PostaEndpoint GetTemplate { get; } = new(HttpMethod.Get, "/api/v1/workspaces/current/templates/{id}", PostaAuthentication.AccessToken); + /// + public virtual PostaEndpoint UpdateTemplate { get; } = new(HttpMethod.Put, "/api/v1/workspaces/current/templates/{id}", PostaAuthentication.AccessToken); + /// + public virtual PostaEndpoint DeleteTemplate { get; } = new(HttpMethod.Delete, "/api/v1/workspaces/current/templates/{id}", PostaAuthentication.AccessToken); + /// + public virtual PostaEndpoint PreviewTemplate { get; } = new(HttpMethod.Post, "/api/v1/workspaces/current/templates/preview", PostaAuthentication.AccessToken); + /// + public virtual PostaEndpoint SendTestEmail { get; } = new(HttpMethod.Post, "/api/v1/workspaces/current/templates/{id}/send-test", PostaAuthentication.AccessToken); + /// + public virtual PostaEndpoint SubscribeAnEmailToAList { get; } = new(HttpMethod.Post, "/api/v1/subscriber-lists/subscribe", PostaAuthentication.ApiKey); + /// + public virtual PostaEndpoint UnsubscribeAnEmailFromAList { get; } = new(HttpMethod.Post, "/api/v1/subscriber-lists/{id}/unsubscribe", PostaAuthentication.ApiKey); + /// + public virtual PostaEndpoint ReSubscribeAnEmailToAList { get; } = new(HttpMethod.Post, "/api/v1/subscriber-lists/{id}/resubscribe", PostaAuthentication.ApiKey); + /// + public virtual PostaEndpoint LivenessProbe { get; } = new(HttpMethod.Get, "/healthz", PostaAuthentication.None); + /// + public virtual PostaEndpoint ReadinessProbe { get; } = new(HttpMethod.Get, "/readyz", PostaAuthentication.None); + /// + public virtual PostaEndpoint ApplicationInfo { get; } = new(HttpMethod.Get, "/api/v1/info", PostaAuthentication.None); +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Models/Emails/GetEmailDeliveryStatusRequest.cs b/src/CommunityToolkit.Aspire.Posta/Models/Emails/GetEmailDeliveryStatusRequest.cs new file mode 100644 index 000000000..bea3487a7 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Models/Emails/GetEmailDeliveryStatusRequest.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace CommunityToolkit.Aspire.Posta.Models.Emails; + +/// Represents the GetEmailDeliveryStatusRequest payload. +public class GetEmailDeliveryStatusRequest +{ + /// Gets or sets the id path parameter. + [JsonIgnore] + public required string Id { get; set; } + +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Models/Emails/GetEmailDeliveryStatusResponse.cs b/src/CommunityToolkit.Aspire.Posta/Models/Emails/GetEmailDeliveryStatusResponse.cs new file mode 100644 index 000000000..ed388a7e5 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Models/Emails/GetEmailDeliveryStatusResponse.cs @@ -0,0 +1,16 @@ +using System.Text.Json.Serialization; + +namespace CommunityToolkit.Aspire.Posta.Models.Emails; + +/// Represents the GetEmailDeliveryStatusResponse payload. +public class GetEmailDeliveryStatusResponse +{ + /// Gets or sets data. + [JsonPropertyName("data")] + public Shared.GetEmailDeliveryStatusResponseData? Data { get; set; } + + /// Gets or sets success. + [JsonPropertyName("success")] + public bool? Success { get; set; } + +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Models/Emails/GetEmailDetailsRequest.cs b/src/CommunityToolkit.Aspire.Posta/Models/Emails/GetEmailDetailsRequest.cs new file mode 100644 index 000000000..d314686b8 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Models/Emails/GetEmailDetailsRequest.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace CommunityToolkit.Aspire.Posta.Models.Emails; + +/// Represents the GetEmailDetailsRequest payload. +public class GetEmailDetailsRequest +{ + /// Gets or sets the id path parameter. + [JsonIgnore] + public required string Id { get; set; } + +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Models/Emails/GetEmailDetailsResponse.cs b/src/CommunityToolkit.Aspire.Posta/Models/Emails/GetEmailDetailsResponse.cs new file mode 100644 index 000000000..f336b66f0 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Models/Emails/GetEmailDetailsResponse.cs @@ -0,0 +1,16 @@ +using System.Text.Json.Serialization; + +namespace CommunityToolkit.Aspire.Posta.Models.Emails; + +/// Represents the GetEmailDetailsResponse payload. +public class GetEmailDetailsResponse +{ + /// Gets or sets data. + [JsonPropertyName("data")] + public Shared.GetEmailDetailsResponseData? Data { get; set; } + + /// Gets or sets success. + [JsonPropertyName("success")] + public bool? Success { get; set; } + +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Models/Emails/PreviewEmailFromTemplateRequest.cs b/src/CommunityToolkit.Aspire.Posta/Models/Emails/PreviewEmailFromTemplateRequest.cs new file mode 100644 index 000000000..07f4e0693 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Models/Emails/PreviewEmailFromTemplateRequest.cs @@ -0,0 +1,25 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace CommunityToolkit.Aspire.Posta.Models.Emails; + +/// Represents the PreviewEmailFromTemplateRequest payload. +public class PreviewEmailFromTemplateRequest +{ + /// Gets or sets language. + [JsonPropertyName("language")] + public string? Language { get; set; } + + /// Gets or sets template. + [JsonPropertyName("template")] + public string? Template { get; set; } + + /// Gets or sets template_data. + [JsonPropertyName("template_data")] + public IReadOnlyDictionary? TemplateData { get; set; } + + /// Gets or sets template_id. + [JsonPropertyName("template_id")] + public int? TemplateId { get; set; } + +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Models/Emails/PreviewEmailFromTemplateResponse.cs b/src/CommunityToolkit.Aspire.Posta/Models/Emails/PreviewEmailFromTemplateResponse.cs new file mode 100644 index 000000000..77795acdb --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Models/Emails/PreviewEmailFromTemplateResponse.cs @@ -0,0 +1,16 @@ +using System.Text.Json.Serialization; + +namespace CommunityToolkit.Aspire.Posta.Models.Emails; + +/// Represents the PreviewEmailFromTemplateResponse payload. +public class PreviewEmailFromTemplateResponse +{ + /// Gets or sets data. + [JsonPropertyName("data")] + public Shared.PreviewEmailFromTemplateResponseData? Data { get; set; } + + /// Gets or sets success. + [JsonPropertyName("success")] + public bool? Success { get; set; } + +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Models/Emails/RetryFailedEmailRequest.cs b/src/CommunityToolkit.Aspire.Posta/Models/Emails/RetryFailedEmailRequest.cs new file mode 100644 index 000000000..74dc7b5d6 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Models/Emails/RetryFailedEmailRequest.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace CommunityToolkit.Aspire.Posta.Models.Emails; + +/// Represents the RetryFailedEmailRequest payload. +public class RetryFailedEmailRequest +{ + /// Gets or sets the id path parameter. + [JsonIgnore] + public required string Id { get; set; } + +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Models/Emails/RetryFailedEmailResponse.cs b/src/CommunityToolkit.Aspire.Posta/Models/Emails/RetryFailedEmailResponse.cs new file mode 100644 index 000000000..3d7d6d5b2 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Models/Emails/RetryFailedEmailResponse.cs @@ -0,0 +1,16 @@ +using System.Text.Json.Serialization; + +namespace CommunityToolkit.Aspire.Posta.Models.Emails; + +/// Represents the RetryFailedEmailResponse payload. +public class RetryFailedEmailResponse +{ + /// Gets or sets data. + [JsonPropertyName("data")] + public Shared.RetryFailedEmailResponseData? Data { get; set; } + + /// Gets or sets success. + [JsonPropertyName("success")] + public bool? Success { get; set; } + +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Models/Emails/SendAnEmailRequest.cs b/src/CommunityToolkit.Aspire.Posta/Models/Emails/SendAnEmailRequest.cs new file mode 100644 index 000000000..b02c55d28 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Models/Emails/SendAnEmailRequest.cs @@ -0,0 +1,57 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace CommunityToolkit.Aspire.Posta.Models.Emails; + +/// Represents the SendAnEmailRequest payload. +public class SendAnEmailRequest +{ + /// Gets or sets attachments. + [JsonPropertyName("attachments")] + public IReadOnlyList? Attachments { get; set; } + + /// Gets or sets from. + [JsonPropertyName("from")] + public required string From { get; set; } + + /// Gets or sets headers. + [JsonPropertyName("headers")] + public IReadOnlyDictionary? Headers { get; set; } + + /// Gets or sets html. + [JsonPropertyName("html")] + public string? Html { get; set; } + + /// Gets or sets list_unsubscribe_post. + [JsonPropertyName("list_unsubscribe_post")] + public bool? ListUnsubscribePost { get; set; } + + /// Gets or sets list_unsubscribe_url. + [JsonPropertyName("list_unsubscribe_url")] + public string? ListUnsubscribeUrl { get; set; } + + /// Gets or sets send_at. + [JsonPropertyName("send_at")] + public DateTimeOffset? SendAt { get; set; } + + /// Gets or sets subject. + [JsonPropertyName("subject")] + public required string Subject { get; set; } + + /// Gets or sets text. + [JsonPropertyName("text")] + public string? Text { get; set; } + + /// Gets or sets to. + [JsonPropertyName("to")] + public required IReadOnlyList To { get; set; } + + /// Gets or sets unsubscribe. + [JsonPropertyName("unsubscribe")] + public Shared.SendAnEmailRequestUnsubscribe? Unsubscribe { get; set; } + + /// Gets or sets the dry_run query parameter. + [JsonIgnore] + public bool? DryRun { get; set; } + +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Models/Emails/SendAnEmailResponse.cs b/src/CommunityToolkit.Aspire.Posta/Models/Emails/SendAnEmailResponse.cs new file mode 100644 index 000000000..09fd9e514 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Models/Emails/SendAnEmailResponse.cs @@ -0,0 +1,16 @@ +using System.Text.Json.Serialization; + +namespace CommunityToolkit.Aspire.Posta.Models.Emails; + +/// Represents the SendAnEmailResponse payload. +public class SendAnEmailResponse +{ + /// Gets or sets data. + [JsonPropertyName("data")] + public Shared.SendAnEmailResponseData? Data { get; set; } + + /// Gets or sets success. + [JsonPropertyName("success")] + public bool? Success { get; set; } + +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Models/Emails/SendBatchEmailsRequest.cs b/src/CommunityToolkit.Aspire.Posta/Models/Emails/SendBatchEmailsRequest.cs new file mode 100644 index 000000000..f88c65ea4 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Models/Emails/SendBatchEmailsRequest.cs @@ -0,0 +1,36 @@ +using System.Text.Json.Serialization; + +namespace CommunityToolkit.Aspire.Posta.Models.Emails; + +/// Represents the SendBatchEmailsRequest payload. +public class SendBatchEmailsRequest +{ + /// Gets or sets from. + [JsonPropertyName("from")] + public string? From { get; set; } + + /// Gets or sets language. + [JsonPropertyName("language")] + public string? Language { get; set; } + + /// Gets or sets recipients. + [JsonPropertyName("recipients")] + public required IReadOnlyList Recipients { get; set; } + + /// Gets or sets template. + [JsonPropertyName("template")] + public string? Template { get; set; } + + /// Gets or sets template_id. + [JsonPropertyName("template_id")] + public int? TemplateId { get; set; } + + /// Gets or sets unsubscribe. + [JsonPropertyName("unsubscribe")] + public Shared.SendBatchEmailsRequestUnsubscribe? Unsubscribe { get; set; } + + /// Gets or sets the dry_run query parameter. + [JsonIgnore] + public bool? DryRun { get; set; } + +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Models/Emails/SendBatchEmailsResponse.cs b/src/CommunityToolkit.Aspire.Posta/Models/Emails/SendBatchEmailsResponse.cs new file mode 100644 index 000000000..94688162e --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Models/Emails/SendBatchEmailsResponse.cs @@ -0,0 +1,16 @@ +using System.Text.Json.Serialization; + +namespace CommunityToolkit.Aspire.Posta.Models.Emails; + +/// Represents the SendBatchEmailsResponse payload. +public class SendBatchEmailsResponse +{ + /// Gets or sets data. + [JsonPropertyName("data")] + public Shared.SendBatchEmailsResponseData? Data { get; set; } + + /// Gets or sets success. + [JsonPropertyName("success")] + public bool? Success { get; set; } + +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Models/Emails/SendEmailUsingTemplateRequest.cs b/src/CommunityToolkit.Aspire.Posta/Models/Emails/SendEmailUsingTemplateRequest.cs new file mode 100644 index 000000000..7b0e85b85 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Models/Emails/SendEmailUsingTemplateRequest.cs @@ -0,0 +1,45 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace CommunityToolkit.Aspire.Posta.Models.Emails; + +/// Represents the SendEmailUsingTemplateRequest payload. +public class SendEmailUsingTemplateRequest +{ + /// Gets or sets attachments. + [JsonPropertyName("attachments")] + public IReadOnlyList? Attachments { get; set; } + + /// Gets or sets from. + [JsonPropertyName("from")] + public string? From { get; set; } + + /// Gets or sets language. + [JsonPropertyName("language")] + public string? Language { get; set; } + + /// Gets or sets template. + [JsonPropertyName("template")] + public string? Template { get; set; } + + /// Gets or sets template_data. + [JsonPropertyName("template_data")] + public IReadOnlyDictionary? TemplateData { get; set; } + + /// Gets or sets template_id. + [JsonPropertyName("template_id")] + public int? TemplateId { get; set; } + + /// Gets or sets to. + [JsonPropertyName("to")] + public required IReadOnlyList To { get; set; } + + /// Gets or sets unsubscribe. + [JsonPropertyName("unsubscribe")] + public Shared.SendEmailUsingTemplateRequestUnsubscribe? Unsubscribe { get; set; } + + /// Gets or sets the dry_run query parameter. + [JsonIgnore] + public bool? DryRun { get; set; } + +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Models/Emails/SendEmailUsingTemplateResponse.cs b/src/CommunityToolkit.Aspire.Posta/Models/Emails/SendEmailUsingTemplateResponse.cs new file mode 100644 index 000000000..cf3238b67 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Models/Emails/SendEmailUsingTemplateResponse.cs @@ -0,0 +1,16 @@ +using System.Text.Json.Serialization; + +namespace CommunityToolkit.Aspire.Posta.Models.Emails; + +/// Represents the SendEmailUsingTemplateResponse payload. +public class SendEmailUsingTemplateResponse +{ + /// Gets or sets data. + [JsonPropertyName("data")] + public Shared.SendEmailUsingTemplateResponseData? Data { get; set; } + + /// Gets or sets success. + [JsonPropertyName("success")] + public bool? Success { get; set; } + +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Models/Emails/VerifyAnEmailAddressRequest.cs b/src/CommunityToolkit.Aspire.Posta/Models/Emails/VerifyAnEmailAddressRequest.cs new file mode 100644 index 000000000..a77ae59fd --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Models/Emails/VerifyAnEmailAddressRequest.cs @@ -0,0 +1,16 @@ +using System.Text.Json.Serialization; + +namespace CommunityToolkit.Aspire.Posta.Models.Emails; + +/// Represents the VerifyAnEmailAddressRequest payload. +public class VerifyAnEmailAddressRequest +{ + /// Gets or sets email. + [JsonPropertyName("email")] + public required string Email { get; set; } + + /// Gets or sets the fresh query parameter. + [JsonIgnore] + public bool? Fresh { get; set; } + +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Models/Emails/VerifyAnEmailAddressResponse.cs b/src/CommunityToolkit.Aspire.Posta/Models/Emails/VerifyAnEmailAddressResponse.cs new file mode 100644 index 000000000..3cb41594d --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Models/Emails/VerifyAnEmailAddressResponse.cs @@ -0,0 +1,16 @@ +using System.Text.Json.Serialization; + +namespace CommunityToolkit.Aspire.Posta.Models.Emails; + +/// Represents the VerifyAnEmailAddressResponse payload. +public class VerifyAnEmailAddressResponse +{ + /// Gets or sets data. + [JsonPropertyName("data")] + public Shared.VerifyAnEmailAddressResponseData? Data { get; set; } + + /// Gets or sets success. + [JsonPropertyName("success")] + public bool? Success { get; set; } + +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Models/Health/LivenessProbeResponse.cs b/src/CommunityToolkit.Aspire.Posta/Models/Health/LivenessProbeResponse.cs new file mode 100644 index 000000000..827644190 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Models/Health/LivenessProbeResponse.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace CommunityToolkit.Aspire.Posta.Models.Health; + +/// Represents the LivenessProbeResponse payload. +public class LivenessProbeResponse +{ + /// Gets or sets status. + [JsonPropertyName("status")] + public string? Status { get; set; } + +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Models/Health/ReadinessProbeResponse.cs b/src/CommunityToolkit.Aspire.Posta/Models/Health/ReadinessProbeResponse.cs new file mode 100644 index 000000000..658046880 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Models/Health/ReadinessProbeResponse.cs @@ -0,0 +1,20 @@ +using System.Text.Json.Serialization; + +namespace CommunityToolkit.Aspire.Posta.Models.Health; + +/// Represents the ReadinessProbeResponse payload. +public class ReadinessProbeResponse +{ + /// Gets or sets database. + [JsonPropertyName("database")] + public string? Database { get; set; } + + /// Gets or sets redis. + [JsonPropertyName("redis")] + public string? Redis { get; set; } + + /// Gets or sets status. + [JsonPropertyName("status")] + public string? Status { get; set; } + +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Models/Info/ApplicationInfoResponse.cs b/src/CommunityToolkit.Aspire.Posta/Models/Info/ApplicationInfoResponse.cs new file mode 100644 index 000000000..ff710a0ea --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Models/Info/ApplicationInfoResponse.cs @@ -0,0 +1,16 @@ +using System.Text.Json.Serialization; + +namespace CommunityToolkit.Aspire.Posta.Models.Info; + +/// Represents the ApplicationInfoResponse payload. +public class ApplicationInfoResponse +{ + /// Gets or sets data. + [JsonPropertyName("data")] + public Shared.ApplicationInfoResponseData? Data { get; set; } + + /// Gets or sets success. + [JsonPropertyName("success")] + public bool? Success { get; set; } + +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Models/Shared/ApplicationInfoResponseData.cs b/src/CommunityToolkit.Aspire.Posta/Models/Shared/ApplicationInfoResponseData.cs new file mode 100644 index 000000000..9abc6a52c --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Models/Shared/ApplicationInfoResponseData.cs @@ -0,0 +1,24 @@ +using System.Text.Json.Serialization; + +namespace CommunityToolkit.Aspire.Posta.Models.Shared; + +/// Represents the ApplicationInfoResponseData payload. +public class ApplicationInfoResponseData +{ + /// Gets or sets commit_id. + [JsonPropertyName("commit_id")] + public string? CommitId { get; set; } + + /// Gets or sets name. + [JsonPropertyName("name")] + public string? Name { get; set; } + + /// Gets or sets openapi_docs. + [JsonPropertyName("openapi_docs")] + public bool? OpenapiDocs { get; set; } + + /// Gets or sets version. + [JsonPropertyName("version")] + public string? Version { get; set; } + +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Models/Shared/CreateTemplateResponseData.cs b/src/CommunityToolkit.Aspire.Posta/Models/Shared/CreateTemplateResponseData.cs new file mode 100644 index 000000000..5bea2f899 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Models/Shared/CreateTemplateResponseData.cs @@ -0,0 +1,64 @@ +using System.Text.Json.Serialization; + +namespace CommunityToolkit.Aspire.Posta.Models.Shared; + +/// Represents the CreateTemplateResponseData payload. +public class CreateTemplateResponseData +{ + /// Gets or sets active_version. + [JsonPropertyName("active_version")] + public CreateTemplateResponseDataActiveVersion? ActiveVersion { get; set; } + + /// Gets or sets active_version_id. + [JsonPropertyName("active_version_id")] + public int? ActiveVersionId { get; set; } + + /// Gets or sets created_at. + [JsonPropertyName("created_at")] + public DateTimeOffset? CreatedAt { get; set; } + + /// Gets or sets created_by. + [JsonPropertyName("created_by")] + public CreateTemplateResponseDataCreatedBy? CreatedBy { get; set; } + + /// Gets or sets default_language. + [JsonPropertyName("default_language")] + public string? DefaultLanguage { get; set; } + + /// Gets or sets description. + [JsonPropertyName("description")] + public string? Description { get; set; } + + /// Gets or sets id. + [JsonPropertyName("id")] + public int? Id { get; set; } + + /// Gets or sets last_edited_by. + [JsonPropertyName("last_edited_by")] + public CreateTemplateResponseDataLastEditedBy? LastEditedBy { get; set; } + + /// Gets or sets last_edited_by_id. + [JsonPropertyName("last_edited_by_id")] + public int? LastEditedById { get; set; } + + /// Gets or sets name. + [JsonPropertyName("name")] + public string? Name { get; set; } + + /// Gets or sets sample_data. + [JsonPropertyName("sample_data")] + public string? SampleData { get; set; } + + /// Gets or sets updated_at. + [JsonPropertyName("updated_at")] + public DateTimeOffset? UpdatedAt { get; set; } + + /// Gets or sets user_id. + [JsonPropertyName("user_id")] + public int? UserId { get; set; } + + /// Gets or sets workspace_id. + [JsonPropertyName("workspace_id")] + public int? WorkspaceId { get; set; } + +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Models/Shared/CreateTemplateResponseDataActiveVersion.cs b/src/CommunityToolkit.Aspire.Posta/Models/Shared/CreateTemplateResponseDataActiveVersion.cs new file mode 100644 index 000000000..ef8e7b3fe --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Models/Shared/CreateTemplateResponseDataActiveVersion.cs @@ -0,0 +1,40 @@ +using System.Text.Json.Serialization; + +namespace CommunityToolkit.Aspire.Posta.Models.Shared; + +/// Represents the CreateTemplateResponseDataActiveVersion payload. +public class CreateTemplateResponseDataActiveVersion +{ + /// Gets or sets created_at. + [JsonPropertyName("created_at")] + public DateTimeOffset? CreatedAt { get; set; } + + /// Gets or sets id. + [JsonPropertyName("id")] + public int? Id { get; set; } + + /// Gets or sets localizations. + [JsonPropertyName("localizations")] + public IReadOnlyList? Localizations { get; set; } + + /// Gets or sets sample_data. + [JsonPropertyName("sample_data")] + public string? SampleData { get; set; } + + /// Gets or sets stylesheet. + [JsonPropertyName("stylesheet")] + public CreateTemplateResponseDataActiveVersionStylesheet? Stylesheet { get; set; } + + /// Gets or sets stylesheet_id. + [JsonPropertyName("stylesheet_id")] + public int? StylesheetId { get; set; } + + /// Gets or sets template_id. + [JsonPropertyName("template_id")] + public int? TemplateId { get; set; } + + /// Gets or sets version. + [JsonPropertyName("version")] + public int? Version { get; set; } + +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Models/Shared/CreateTemplateResponseDataActiveVersionLocalizationsItem.cs b/src/CommunityToolkit.Aspire.Posta/Models/Shared/CreateTemplateResponseDataActiveVersionLocalizationsItem.cs new file mode 100644 index 000000000..a9c82571f --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Models/Shared/CreateTemplateResponseDataActiveVersionLocalizationsItem.cs @@ -0,0 +1,44 @@ +using System.Text.Json.Serialization; + +namespace CommunityToolkit.Aspire.Posta.Models.Shared; + +/// Represents the CreateTemplateResponseDataActiveVersionLocalizationsItem payload. +public class CreateTemplateResponseDataActiveVersionLocalizationsItem +{ + /// Gets or sets builder_json. + [JsonPropertyName("builder_json")] + public string? BuilderJson { get; set; } + + /// Gets or sets created_at. + [JsonPropertyName("created_at")] + public DateTimeOffset? CreatedAt { get; set; } + + /// Gets or sets html_template. + [JsonPropertyName("html_template")] + public string? HtmlTemplate { get; set; } + + /// Gets or sets id. + [JsonPropertyName("id")] + public int? Id { get; set; } + + /// Gets or sets language. + [JsonPropertyName("language")] + public string? Language { get; set; } + + /// Gets or sets subject_template. + [JsonPropertyName("subject_template")] + public string? SubjectTemplate { get; set; } + + /// Gets or sets text_template. + [JsonPropertyName("text_template")] + public string? TextTemplate { get; set; } + + /// Gets or sets updated_at. + [JsonPropertyName("updated_at")] + public DateTimeOffset? UpdatedAt { get; set; } + + /// Gets or sets version_id. + [JsonPropertyName("version_id")] + public int? VersionId { get; set; } + +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Models/Shared/CreateTemplateResponseDataActiveVersionStylesheet.cs b/src/CommunityToolkit.Aspire.Posta/Models/Shared/CreateTemplateResponseDataActiveVersionStylesheet.cs new file mode 100644 index 000000000..854f1d1d2 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Models/Shared/CreateTemplateResponseDataActiveVersionStylesheet.cs @@ -0,0 +1,36 @@ +using System.Text.Json.Serialization; + +namespace CommunityToolkit.Aspire.Posta.Models.Shared; + +/// Represents the CreateTemplateResponseDataActiveVersionStylesheet payload. +public class CreateTemplateResponseDataActiveVersionStylesheet +{ + /// Gets or sets created_at. + [JsonPropertyName("created_at")] + public DateTimeOffset? CreatedAt { get; set; } + + /// Gets or sets css. + [JsonPropertyName("css")] + public string? Css { get; set; } + + /// Gets or sets id. + [JsonPropertyName("id")] + public int? Id { get; set; } + + /// Gets or sets name. + [JsonPropertyName("name")] + public string? Name { get; set; } + + /// Gets or sets updated_at. + [JsonPropertyName("updated_at")] + public DateTimeOffset? UpdatedAt { get; set; } + + /// Gets or sets user_id. + [JsonPropertyName("user_id")] + public int? UserId { get; set; } + + /// Gets or sets workspace_id. + [JsonPropertyName("workspace_id")] + public int? WorkspaceId { get; set; } + +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Models/Shared/CreateTemplateResponseDataCreatedBy.cs b/src/CommunityToolkit.Aspire.Posta/Models/Shared/CreateTemplateResponseDataCreatedBy.cs new file mode 100644 index 000000000..b69be98e5 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Models/Shared/CreateTemplateResponseDataCreatedBy.cs @@ -0,0 +1,16 @@ +using System.Text.Json.Serialization; + +namespace CommunityToolkit.Aspire.Posta.Models.Shared; + +/// Represents the CreateTemplateResponseDataCreatedBy payload. +public class CreateTemplateResponseDataCreatedBy +{ + /// Gets or sets id. + [JsonPropertyName("id")] + public int? Id { get; set; } + + /// Gets or sets name. + [JsonPropertyName("name")] + public string? Name { get; set; } + +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Models/Shared/CreateTemplateResponseDataLastEditedBy.cs b/src/CommunityToolkit.Aspire.Posta/Models/Shared/CreateTemplateResponseDataLastEditedBy.cs new file mode 100644 index 000000000..ae556f6d2 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Models/Shared/CreateTemplateResponseDataLastEditedBy.cs @@ -0,0 +1,16 @@ +using System.Text.Json.Serialization; + +namespace CommunityToolkit.Aspire.Posta.Models.Shared; + +/// Represents the CreateTemplateResponseDataLastEditedBy payload. +public class CreateTemplateResponseDataLastEditedBy +{ + /// Gets or sets id. + [JsonPropertyName("id")] + public int? Id { get; set; } + + /// Gets or sets name. + [JsonPropertyName("name")] + public string? Name { get; set; } + +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Models/Shared/GetEmailDeliveryStatusResponseData.cs b/src/CommunityToolkit.Aspire.Posta/Models/Shared/GetEmailDeliveryStatusResponseData.cs new file mode 100644 index 000000000..7ef1ed8fd --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Models/Shared/GetEmailDeliveryStatusResponseData.cs @@ -0,0 +1,32 @@ +using System.Text.Json.Serialization; + +namespace CommunityToolkit.Aspire.Posta.Models.Shared; + +/// Represents the GetEmailDeliveryStatusResponseData payload. +public class GetEmailDeliveryStatusResponseData +{ + /// Gets or sets created_at. + [JsonPropertyName("created_at")] + public string? CreatedAt { get; set; } + + /// Gets or sets error_message. + [JsonPropertyName("error_message")] + public string? ErrorMessage { get; set; } + + /// Gets or sets id. + [JsonPropertyName("id")] + public string? Id { get; set; } + + /// Gets or sets retry_count. + [JsonPropertyName("retry_count")] + public int? RetryCount { get; set; } + + /// Gets or sets sent_at. + [JsonPropertyName("sent_at")] + public string? SentAt { get; set; } + + /// Gets or sets status. + [JsonPropertyName("status")] + public string? Status { get; set; } + +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Models/Shared/GetEmailDetailsResponseData.cs b/src/CommunityToolkit.Aspire.Posta/Models/Shared/GetEmailDetailsResponseData.cs new file mode 100644 index 000000000..305bcfbdd --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Models/Shared/GetEmailDetailsResponseData.cs @@ -0,0 +1,112 @@ +using System.Text.Json.Serialization; + +namespace CommunityToolkit.Aspire.Posta.Models.Shared; + +/// Represents the GetEmailDetailsResponseData payload. +public class GetEmailDetailsResponseData +{ + /// Gets or sets api_key_id. + [JsonPropertyName("api_key_id")] + public int? ApiKeyId { get; set; } + + /// Gets or sets api_key_name. + [JsonPropertyName("api_key_name")] + public string? ApiKeyName { get; set; } + + /// Gets or sets attachments_json. + [JsonPropertyName("attachments_json")] + public string? AttachmentsJson { get; set; } + + /// Gets or sets created_at. + [JsonPropertyName("created_at")] + public DateTimeOffset? CreatedAt { get; set; } + + /// Gets or sets error_message. + [JsonPropertyName("error_message")] + public string? ErrorMessage { get; set; } + + /// Gets or sets headers_json. + [JsonPropertyName("headers_json")] + public string? HeadersJson { get; set; } + + /// Gets or sets html_body. + [JsonPropertyName("html_body")] + public string? HtmlBody { get; set; } + + /// Gets or sets id. + [JsonPropertyName("id")] + public int? Id { get; set; } + + /// Gets or sets list_unsubscribe_mailto. + [JsonPropertyName("list_unsubscribe_mailto")] + public string? ListUnsubscribeMailto { get; set; } + + /// Gets or sets list_unsubscribe_post. + [JsonPropertyName("list_unsubscribe_post")] + public bool? ListUnsubscribePost { get; set; } + + /// Gets or sets list_unsubscribe_url. + [JsonPropertyName("list_unsubscribe_url")] + public string? ListUnsubscribeUrl { get; set; } + + /// Gets or sets provider. + [JsonPropertyName("provider")] + public string? Provider { get; set; } + + /// Gets or sets recipients. + [JsonPropertyName("recipients")] + public IReadOnlyList? Recipients { get; set; } + + /// Gets or sets retry_count. + [JsonPropertyName("retry_count")] + public int? RetryCount { get; set; } + + /// Gets or sets scheduled_at. + [JsonPropertyName("scheduled_at")] + public DateTimeOffset? ScheduledAt { get; set; } + + /// Gets or sets sender. + [JsonPropertyName("sender")] + public string? Sender { get; set; } + + /// Gets or sets sent_at. + [JsonPropertyName("sent_at")] + public DateTimeOffset? SentAt { get; set; } + + /// Gets or sets smtp_hostname. + [JsonPropertyName("smtp_hostname")] + public string? SmtpHostname { get; set; } + + /// Gets or sets status. + [JsonPropertyName("status")] + public string? Status { get; set; } + + /// Gets or sets subject. + [JsonPropertyName("subject")] + public string? Subject { get; set; } + + /// Gets or sets template_name. + [JsonPropertyName("template_name")] + public string? TemplateName { get; set; } + + /// Gets or sets text_body. + [JsonPropertyName("text_body")] + public string? TextBody { get; set; } + + /// Gets or sets unsubscribe_list_id. + [JsonPropertyName("unsubscribe_list_id")] + public int? UnsubscribeListId { get; set; } + + /// Gets or sets user_id. + [JsonPropertyName("user_id")] + public int? UserId { get; set; } + + /// Gets or sets uuid. + [JsonPropertyName("uuid")] + public string? Uuid { get; set; } + + /// Gets or sets workspace_id. + [JsonPropertyName("workspace_id")] + public int? WorkspaceId { get; set; } + +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Models/Shared/GetTemplateResponseData.cs b/src/CommunityToolkit.Aspire.Posta/Models/Shared/GetTemplateResponseData.cs new file mode 100644 index 000000000..d64c14df3 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Models/Shared/GetTemplateResponseData.cs @@ -0,0 +1,64 @@ +using System.Text.Json.Serialization; + +namespace CommunityToolkit.Aspire.Posta.Models.Shared; + +/// Represents the GetTemplateResponseData payload. +public class GetTemplateResponseData +{ + /// Gets or sets active_version. + [JsonPropertyName("active_version")] + public GetTemplateResponseDataActiveVersion? ActiveVersion { get; set; } + + /// Gets or sets active_version_id. + [JsonPropertyName("active_version_id")] + public int? ActiveVersionId { get; set; } + + /// Gets or sets created_at. + [JsonPropertyName("created_at")] + public DateTimeOffset? CreatedAt { get; set; } + + /// Gets or sets created_by. + [JsonPropertyName("created_by")] + public GetTemplateResponseDataCreatedBy? CreatedBy { get; set; } + + /// Gets or sets default_language. + [JsonPropertyName("default_language")] + public string? DefaultLanguage { get; set; } + + /// Gets or sets description. + [JsonPropertyName("description")] + public string? Description { get; set; } + + /// Gets or sets id. + [JsonPropertyName("id")] + public int? Id { get; set; } + + /// Gets or sets last_edited_by. + [JsonPropertyName("last_edited_by")] + public GetTemplateResponseDataLastEditedBy? LastEditedBy { get; set; } + + /// Gets or sets last_edited_by_id. + [JsonPropertyName("last_edited_by_id")] + public int? LastEditedById { get; set; } + + /// Gets or sets name. + [JsonPropertyName("name")] + public string? Name { get; set; } + + /// Gets or sets sample_data. + [JsonPropertyName("sample_data")] + public string? SampleData { get; set; } + + /// Gets or sets updated_at. + [JsonPropertyName("updated_at")] + public DateTimeOffset? UpdatedAt { get; set; } + + /// Gets or sets user_id. + [JsonPropertyName("user_id")] + public int? UserId { get; set; } + + /// Gets or sets workspace_id. + [JsonPropertyName("workspace_id")] + public int? WorkspaceId { get; set; } + +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Models/Shared/GetTemplateResponseDataActiveVersion.cs b/src/CommunityToolkit.Aspire.Posta/Models/Shared/GetTemplateResponseDataActiveVersion.cs new file mode 100644 index 000000000..a43d0fc4f --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Models/Shared/GetTemplateResponseDataActiveVersion.cs @@ -0,0 +1,40 @@ +using System.Text.Json.Serialization; + +namespace CommunityToolkit.Aspire.Posta.Models.Shared; + +/// Represents the GetTemplateResponseDataActiveVersion payload. +public class GetTemplateResponseDataActiveVersion +{ + /// Gets or sets created_at. + [JsonPropertyName("created_at")] + public DateTimeOffset? CreatedAt { get; set; } + + /// Gets or sets id. + [JsonPropertyName("id")] + public int? Id { get; set; } + + /// Gets or sets localizations. + [JsonPropertyName("localizations")] + public IReadOnlyList? Localizations { get; set; } + + /// Gets or sets sample_data. + [JsonPropertyName("sample_data")] + public string? SampleData { get; set; } + + /// Gets or sets stylesheet. + [JsonPropertyName("stylesheet")] + public GetTemplateResponseDataActiveVersionStylesheet? Stylesheet { get; set; } + + /// Gets or sets stylesheet_id. + [JsonPropertyName("stylesheet_id")] + public int? StylesheetId { get; set; } + + /// Gets or sets template_id. + [JsonPropertyName("template_id")] + public int? TemplateId { get; set; } + + /// Gets or sets version. + [JsonPropertyName("version")] + public int? Version { get; set; } + +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Models/Shared/GetTemplateResponseDataActiveVersionLocalizationsItem.cs b/src/CommunityToolkit.Aspire.Posta/Models/Shared/GetTemplateResponseDataActiveVersionLocalizationsItem.cs new file mode 100644 index 000000000..1c870c9ba --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Models/Shared/GetTemplateResponseDataActiveVersionLocalizationsItem.cs @@ -0,0 +1,44 @@ +using System.Text.Json.Serialization; + +namespace CommunityToolkit.Aspire.Posta.Models.Shared; + +/// Represents the GetTemplateResponseDataActiveVersionLocalizationsItem payload. +public class GetTemplateResponseDataActiveVersionLocalizationsItem +{ + /// Gets or sets builder_json. + [JsonPropertyName("builder_json")] + public string? BuilderJson { get; set; } + + /// Gets or sets created_at. + [JsonPropertyName("created_at")] + public DateTimeOffset? CreatedAt { get; set; } + + /// Gets or sets html_template. + [JsonPropertyName("html_template")] + public string? HtmlTemplate { get; set; } + + /// Gets or sets id. + [JsonPropertyName("id")] + public int? Id { get; set; } + + /// Gets or sets language. + [JsonPropertyName("language")] + public string? Language { get; set; } + + /// Gets or sets subject_template. + [JsonPropertyName("subject_template")] + public string? SubjectTemplate { get; set; } + + /// Gets or sets text_template. + [JsonPropertyName("text_template")] + public string? TextTemplate { get; set; } + + /// Gets or sets updated_at. + [JsonPropertyName("updated_at")] + public DateTimeOffset? UpdatedAt { get; set; } + + /// Gets or sets version_id. + [JsonPropertyName("version_id")] + public int? VersionId { get; set; } + +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Models/Shared/GetTemplateResponseDataActiveVersionStylesheet.cs b/src/CommunityToolkit.Aspire.Posta/Models/Shared/GetTemplateResponseDataActiveVersionStylesheet.cs new file mode 100644 index 000000000..8ac83d4ea --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Models/Shared/GetTemplateResponseDataActiveVersionStylesheet.cs @@ -0,0 +1,36 @@ +using System.Text.Json.Serialization; + +namespace CommunityToolkit.Aspire.Posta.Models.Shared; + +/// Represents the GetTemplateResponseDataActiveVersionStylesheet payload. +public class GetTemplateResponseDataActiveVersionStylesheet +{ + /// Gets or sets created_at. + [JsonPropertyName("created_at")] + public DateTimeOffset? CreatedAt { get; set; } + + /// Gets or sets css. + [JsonPropertyName("css")] + public string? Css { get; set; } + + /// Gets or sets id. + [JsonPropertyName("id")] + public int? Id { get; set; } + + /// Gets or sets name. + [JsonPropertyName("name")] + public string? Name { get; set; } + + /// Gets or sets updated_at. + [JsonPropertyName("updated_at")] + public DateTimeOffset? UpdatedAt { get; set; } + + /// Gets or sets user_id. + [JsonPropertyName("user_id")] + public int? UserId { get; set; } + + /// Gets or sets workspace_id. + [JsonPropertyName("workspace_id")] + public int? WorkspaceId { get; set; } + +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Models/Shared/GetTemplateResponseDataCreatedBy.cs b/src/CommunityToolkit.Aspire.Posta/Models/Shared/GetTemplateResponseDataCreatedBy.cs new file mode 100644 index 000000000..1a2fe9266 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Models/Shared/GetTemplateResponseDataCreatedBy.cs @@ -0,0 +1,16 @@ +using System.Text.Json.Serialization; + +namespace CommunityToolkit.Aspire.Posta.Models.Shared; + +/// Represents the GetTemplateResponseDataCreatedBy payload. +public class GetTemplateResponseDataCreatedBy +{ + /// Gets or sets id. + [JsonPropertyName("id")] + public int? Id { get; set; } + + /// Gets or sets name. + [JsonPropertyName("name")] + public string? Name { get; set; } + +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Models/Shared/GetTemplateResponseDataLastEditedBy.cs b/src/CommunityToolkit.Aspire.Posta/Models/Shared/GetTemplateResponseDataLastEditedBy.cs new file mode 100644 index 000000000..ad02e1105 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Models/Shared/GetTemplateResponseDataLastEditedBy.cs @@ -0,0 +1,16 @@ +using System.Text.Json.Serialization; + +namespace CommunityToolkit.Aspire.Posta.Models.Shared; + +/// Represents the GetTemplateResponseDataLastEditedBy payload. +public class GetTemplateResponseDataLastEditedBy +{ + /// Gets or sets id. + [JsonPropertyName("id")] + public int? Id { get; set; } + + /// Gets or sets name. + [JsonPropertyName("name")] + public string? Name { get; set; } + +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Models/Shared/ListTemplatesResponseDataItem.cs b/src/CommunityToolkit.Aspire.Posta/Models/Shared/ListTemplatesResponseDataItem.cs new file mode 100644 index 000000000..487f30dab --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Models/Shared/ListTemplatesResponseDataItem.cs @@ -0,0 +1,64 @@ +using System.Text.Json.Serialization; + +namespace CommunityToolkit.Aspire.Posta.Models.Shared; + +/// Represents the ListTemplatesResponseDataItem payload. +public class ListTemplatesResponseDataItem +{ + /// Gets or sets active_version. + [JsonPropertyName("active_version")] + public ListTemplatesResponseDataItemActiveVersion? ActiveVersion { get; set; } + + /// Gets or sets active_version_id. + [JsonPropertyName("active_version_id")] + public int? ActiveVersionId { get; set; } + + /// Gets or sets created_at. + [JsonPropertyName("created_at")] + public DateTimeOffset? CreatedAt { get; set; } + + /// Gets or sets created_by. + [JsonPropertyName("created_by")] + public ListTemplatesResponseDataItemCreatedBy? CreatedBy { get; set; } + + /// Gets or sets default_language. + [JsonPropertyName("default_language")] + public string? DefaultLanguage { get; set; } + + /// Gets or sets description. + [JsonPropertyName("description")] + public string? Description { get; set; } + + /// Gets or sets id. + [JsonPropertyName("id")] + public int? Id { get; set; } + + /// Gets or sets last_edited_by. + [JsonPropertyName("last_edited_by")] + public ListTemplatesResponseDataItemLastEditedBy? LastEditedBy { get; set; } + + /// Gets or sets last_edited_by_id. + [JsonPropertyName("last_edited_by_id")] + public int? LastEditedById { get; set; } + + /// Gets or sets name. + [JsonPropertyName("name")] + public string? Name { get; set; } + + /// Gets or sets sample_data. + [JsonPropertyName("sample_data")] + public string? SampleData { get; set; } + + /// Gets or sets updated_at. + [JsonPropertyName("updated_at")] + public DateTimeOffset? UpdatedAt { get; set; } + + /// Gets or sets user_id. + [JsonPropertyName("user_id")] + public int? UserId { get; set; } + + /// Gets or sets workspace_id. + [JsonPropertyName("workspace_id")] + public int? WorkspaceId { get; set; } + +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Models/Shared/ListTemplatesResponseDataItemActiveVersion.cs b/src/CommunityToolkit.Aspire.Posta/Models/Shared/ListTemplatesResponseDataItemActiveVersion.cs new file mode 100644 index 000000000..457b7229c --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Models/Shared/ListTemplatesResponseDataItemActiveVersion.cs @@ -0,0 +1,40 @@ +using System.Text.Json.Serialization; + +namespace CommunityToolkit.Aspire.Posta.Models.Shared; + +/// Represents the ListTemplatesResponseDataItemActiveVersion payload. +public class ListTemplatesResponseDataItemActiveVersion +{ + /// Gets or sets created_at. + [JsonPropertyName("created_at")] + public DateTimeOffset? CreatedAt { get; set; } + + /// Gets or sets id. + [JsonPropertyName("id")] + public int? Id { get; set; } + + /// Gets or sets localizations. + [JsonPropertyName("localizations")] + public IReadOnlyList? Localizations { get; set; } + + /// Gets or sets sample_data. + [JsonPropertyName("sample_data")] + public string? SampleData { get; set; } + + /// Gets or sets stylesheet. + [JsonPropertyName("stylesheet")] + public ListTemplatesResponseDataItemActiveVersionStylesheet? Stylesheet { get; set; } + + /// Gets or sets stylesheet_id. + [JsonPropertyName("stylesheet_id")] + public int? StylesheetId { get; set; } + + /// Gets or sets template_id. + [JsonPropertyName("template_id")] + public int? TemplateId { get; set; } + + /// Gets or sets version. + [JsonPropertyName("version")] + public int? Version { get; set; } + +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Models/Shared/ListTemplatesResponseDataItemActiveVersionLocalizationsItem.cs b/src/CommunityToolkit.Aspire.Posta/Models/Shared/ListTemplatesResponseDataItemActiveVersionLocalizationsItem.cs new file mode 100644 index 000000000..d2f8394c5 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Models/Shared/ListTemplatesResponseDataItemActiveVersionLocalizationsItem.cs @@ -0,0 +1,44 @@ +using System.Text.Json.Serialization; + +namespace CommunityToolkit.Aspire.Posta.Models.Shared; + +/// Represents the ListTemplatesResponseDataItemActiveVersionLocalizationsItem payload. +public class ListTemplatesResponseDataItemActiveVersionLocalizationsItem +{ + /// Gets or sets builder_json. + [JsonPropertyName("builder_json")] + public string? BuilderJson { get; set; } + + /// Gets or sets created_at. + [JsonPropertyName("created_at")] + public DateTimeOffset? CreatedAt { get; set; } + + /// Gets or sets html_template. + [JsonPropertyName("html_template")] + public string? HtmlTemplate { get; set; } + + /// Gets or sets id. + [JsonPropertyName("id")] + public int? Id { get; set; } + + /// Gets or sets language. + [JsonPropertyName("language")] + public string? Language { get; set; } + + /// Gets or sets subject_template. + [JsonPropertyName("subject_template")] + public string? SubjectTemplate { get; set; } + + /// Gets or sets text_template. + [JsonPropertyName("text_template")] + public string? TextTemplate { get; set; } + + /// Gets or sets updated_at. + [JsonPropertyName("updated_at")] + public DateTimeOffset? UpdatedAt { get; set; } + + /// Gets or sets version_id. + [JsonPropertyName("version_id")] + public int? VersionId { get; set; } + +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Models/Shared/ListTemplatesResponseDataItemActiveVersionStylesheet.cs b/src/CommunityToolkit.Aspire.Posta/Models/Shared/ListTemplatesResponseDataItemActiveVersionStylesheet.cs new file mode 100644 index 000000000..8439fdb92 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Models/Shared/ListTemplatesResponseDataItemActiveVersionStylesheet.cs @@ -0,0 +1,36 @@ +using System.Text.Json.Serialization; + +namespace CommunityToolkit.Aspire.Posta.Models.Shared; + +/// Represents the ListTemplatesResponseDataItemActiveVersionStylesheet payload. +public class ListTemplatesResponseDataItemActiveVersionStylesheet +{ + /// Gets or sets created_at. + [JsonPropertyName("created_at")] + public DateTimeOffset? CreatedAt { get; set; } + + /// Gets or sets css. + [JsonPropertyName("css")] + public string? Css { get; set; } + + /// Gets or sets id. + [JsonPropertyName("id")] + public int? Id { get; set; } + + /// Gets or sets name. + [JsonPropertyName("name")] + public string? Name { get; set; } + + /// Gets or sets updated_at. + [JsonPropertyName("updated_at")] + public DateTimeOffset? UpdatedAt { get; set; } + + /// Gets or sets user_id. + [JsonPropertyName("user_id")] + public int? UserId { get; set; } + + /// Gets or sets workspace_id. + [JsonPropertyName("workspace_id")] + public int? WorkspaceId { get; set; } + +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Models/Shared/ListTemplatesResponseDataItemCreatedBy.cs b/src/CommunityToolkit.Aspire.Posta/Models/Shared/ListTemplatesResponseDataItemCreatedBy.cs new file mode 100644 index 000000000..224c48e1a --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Models/Shared/ListTemplatesResponseDataItemCreatedBy.cs @@ -0,0 +1,16 @@ +using System.Text.Json.Serialization; + +namespace CommunityToolkit.Aspire.Posta.Models.Shared; + +/// Represents the ListTemplatesResponseDataItemCreatedBy payload. +public class ListTemplatesResponseDataItemCreatedBy +{ + /// Gets or sets id. + [JsonPropertyName("id")] + public int? Id { get; set; } + + /// Gets or sets name. + [JsonPropertyName("name")] + public string? Name { get; set; } + +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Models/Shared/ListTemplatesResponseDataItemLastEditedBy.cs b/src/CommunityToolkit.Aspire.Posta/Models/Shared/ListTemplatesResponseDataItemLastEditedBy.cs new file mode 100644 index 000000000..fcfc2ea0e --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Models/Shared/ListTemplatesResponseDataItemLastEditedBy.cs @@ -0,0 +1,16 @@ +using System.Text.Json.Serialization; + +namespace CommunityToolkit.Aspire.Posta.Models.Shared; + +/// Represents the ListTemplatesResponseDataItemLastEditedBy payload. +public class ListTemplatesResponseDataItemLastEditedBy +{ + /// Gets or sets id. + [JsonPropertyName("id")] + public int? Id { get; set; } + + /// Gets or sets name. + [JsonPropertyName("name")] + public string? Name { get; set; } + +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Models/Shared/ListTemplatesResponsePageable.cs b/src/CommunityToolkit.Aspire.Posta/Models/Shared/ListTemplatesResponsePageable.cs new file mode 100644 index 000000000..d15631e83 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Models/Shared/ListTemplatesResponsePageable.cs @@ -0,0 +1,28 @@ +using System.Text.Json.Serialization; + +namespace CommunityToolkit.Aspire.Posta.Models.Shared; + +/// Represents the ListTemplatesResponsePageable payload. +public class ListTemplatesResponsePageable +{ + /// Gets or sets current_page. + [JsonPropertyName("current_page")] + public int? CurrentPage { get; set; } + + /// Gets or sets empty. + [JsonPropertyName("empty")] + public bool? Empty { get; set; } + + /// Gets or sets size. + [JsonPropertyName("size")] + public int? Size { get; set; } + + /// Gets or sets total_elements. + [JsonPropertyName("total_elements")] + public long? TotalElements { get; set; } + + /// Gets or sets total_pages. + [JsonPropertyName("total_pages")] + public int? TotalPages { get; set; } + +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Models/Shared/PreviewEmailFromTemplateResponseData.cs b/src/CommunityToolkit.Aspire.Posta/Models/Shared/PreviewEmailFromTemplateResponseData.cs new file mode 100644 index 000000000..3d4697445 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Models/Shared/PreviewEmailFromTemplateResponseData.cs @@ -0,0 +1,20 @@ +using System.Text.Json.Serialization; + +namespace CommunityToolkit.Aspire.Posta.Models.Shared; + +/// Represents the PreviewEmailFromTemplateResponseData payload. +public class PreviewEmailFromTemplateResponseData +{ + /// Gets or sets html. + [JsonPropertyName("html")] + public string? Html { get; set; } + + /// Gets or sets subject. + [JsonPropertyName("subject")] + public string? Subject { get; set; } + + /// Gets or sets text. + [JsonPropertyName("text")] + public string? Text { get; set; } + +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Models/Shared/PreviewTemplateResponseData.cs b/src/CommunityToolkit.Aspire.Posta/Models/Shared/PreviewTemplateResponseData.cs new file mode 100644 index 000000000..65f58fe02 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Models/Shared/PreviewTemplateResponseData.cs @@ -0,0 +1,20 @@ +using System.Text.Json.Serialization; + +namespace CommunityToolkit.Aspire.Posta.Models.Shared; + +/// Represents the PreviewTemplateResponseData payload. +public class PreviewTemplateResponseData +{ + /// Gets or sets html. + [JsonPropertyName("html")] + public string? Html { get; set; } + + /// Gets or sets subject. + [JsonPropertyName("subject")] + public string? Subject { get; set; } + + /// Gets or sets text. + [JsonPropertyName("text")] + public string? Text { get; set; } + +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Models/Shared/ReSubscribeAnEmailToAListResponseData.cs b/src/CommunityToolkit.Aspire.Posta/Models/Shared/ReSubscribeAnEmailToAListResponseData.cs new file mode 100644 index 000000000..1aab4151c --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Models/Shared/ReSubscribeAnEmailToAListResponseData.cs @@ -0,0 +1,36 @@ +using System.Text.Json.Serialization; + +namespace CommunityToolkit.Aspire.Posta.Models.Shared; + +/// Represents the ReSubscribeAnEmailToAListResponseData payload. +public class ReSubscribeAnEmailToAListResponseData +{ + /// Gets or sets action. + [JsonPropertyName("action")] + public string? Action { get; set; } + + /// Gets or sets email. + [JsonPropertyName("email")] + public string? Email { get; set; } + + /// Gets or sets list_created. + [JsonPropertyName("list_created")] + public bool? ListCreated { get; set; } + + /// Gets or sets list_id. + [JsonPropertyName("list_id")] + public int? ListId { get; set; } + + /// Gets or sets member_added. + [JsonPropertyName("member_added")] + public bool? MemberAdded { get; set; } + + /// Gets or sets subscriber_created. + [JsonPropertyName("subscriber_created")] + public bool? SubscriberCreated { get; set; } + + /// Gets or sets subscriber_id. + [JsonPropertyName("subscriber_id")] + public int? SubscriberId { get; set; } + +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Models/Shared/RetryFailedEmailResponseData.cs b/src/CommunityToolkit.Aspire.Posta/Models/Shared/RetryFailedEmailResponseData.cs new file mode 100644 index 000000000..dce054d25 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Models/Shared/RetryFailedEmailResponseData.cs @@ -0,0 +1,16 @@ +using System.Text.Json.Serialization; + +namespace CommunityToolkit.Aspire.Posta.Models.Shared; + +/// Represents the RetryFailedEmailResponseData payload. +public class RetryFailedEmailResponseData +{ + /// Gets or sets id. + [JsonPropertyName("id")] + public string? Id { get; set; } + + /// Gets or sets status. + [JsonPropertyName("status")] + public string? Status { get; set; } + +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Models/Shared/SendAnEmailRequestAttachmentsItem.cs b/src/CommunityToolkit.Aspire.Posta/Models/Shared/SendAnEmailRequestAttachmentsItem.cs new file mode 100644 index 000000000..76e5715e7 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Models/Shared/SendAnEmailRequestAttachmentsItem.cs @@ -0,0 +1,24 @@ +using System.Text.Json.Serialization; + +namespace CommunityToolkit.Aspire.Posta.Models.Shared; + +/// Represents the SendAnEmailRequestAttachmentsItem payload. +public class SendAnEmailRequestAttachmentsItem +{ + /// Gets or sets content. + [JsonPropertyName("content")] + public string? Content { get; set; } + + /// Gets or sets content_type. + [JsonPropertyName("content_type")] + public string? ContentType { get; set; } + + /// Gets or sets filename. + [JsonPropertyName("filename")] + public string? Filename { get; set; } + + /// Gets or sets storage_key. + [JsonPropertyName("storage_key")] + public string? StorageKey { get; set; } + +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Models/Shared/SendAnEmailRequestUnsubscribe.cs b/src/CommunityToolkit.Aspire.Posta/Models/Shared/SendAnEmailRequestUnsubscribe.cs new file mode 100644 index 000000000..0b83cd0bb --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Models/Shared/SendAnEmailRequestUnsubscribe.cs @@ -0,0 +1,24 @@ +using System.Text.Json.Serialization; + +namespace CommunityToolkit.Aspire.Posta.Models.Shared; + +/// Represents the SendAnEmailRequestUnsubscribe payload. +public class SendAnEmailRequestUnsubscribe +{ + /// Gets or sets list_id. + [JsonPropertyName("list_id")] + public int? ListId { get; set; } + + /// Gets or sets mailto. + [JsonPropertyName("mailto")] + public string? Mailto { get; set; } + + /// Gets or sets one_click. + [JsonPropertyName("one_click")] + public bool? OneClick { get; set; } + + /// Gets or sets url. + [JsonPropertyName("url")] + public string? Url { get; set; } + +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Models/Shared/SendAnEmailResponseData.cs b/src/CommunityToolkit.Aspire.Posta/Models/Shared/SendAnEmailResponseData.cs new file mode 100644 index 000000000..f2494f61e --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Models/Shared/SendAnEmailResponseData.cs @@ -0,0 +1,16 @@ +using System.Text.Json.Serialization; + +namespace CommunityToolkit.Aspire.Posta.Models.Shared; + +/// Represents the SendAnEmailResponseData payload. +public class SendAnEmailResponseData +{ + /// Gets or sets id. + [JsonPropertyName("id")] + public string? Id { get; set; } + + /// Gets or sets status. + [JsonPropertyName("status")] + public string? Status { get; set; } + +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Models/Shared/SendBatchEmailsRequestRecipientsItem.cs b/src/CommunityToolkit.Aspire.Posta/Models/Shared/SendBatchEmailsRequestRecipientsItem.cs new file mode 100644 index 000000000..ed5af35da --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Models/Shared/SendBatchEmailsRequestRecipientsItem.cs @@ -0,0 +1,21 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace CommunityToolkit.Aspire.Posta.Models.Shared; + +/// Represents the SendBatchEmailsRequestRecipientsItem payload. +public class SendBatchEmailsRequestRecipientsItem +{ + /// Gets or sets email. + [JsonPropertyName("email")] + public required string Email { get; set; } + + /// Gets or sets language. + [JsonPropertyName("language")] + public string? Language { get; set; } + + /// Gets or sets template_data. + [JsonPropertyName("template_data")] + public IReadOnlyDictionary? TemplateData { get; set; } + +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Models/Shared/SendBatchEmailsRequestUnsubscribe.cs b/src/CommunityToolkit.Aspire.Posta/Models/Shared/SendBatchEmailsRequestUnsubscribe.cs new file mode 100644 index 000000000..0734b0e03 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Models/Shared/SendBatchEmailsRequestUnsubscribe.cs @@ -0,0 +1,24 @@ +using System.Text.Json.Serialization; + +namespace CommunityToolkit.Aspire.Posta.Models.Shared; + +/// Represents the SendBatchEmailsRequestUnsubscribe payload. +public class SendBatchEmailsRequestUnsubscribe +{ + /// Gets or sets list_id. + [JsonPropertyName("list_id")] + public int? ListId { get; set; } + + /// Gets or sets mailto. + [JsonPropertyName("mailto")] + public string? Mailto { get; set; } + + /// Gets or sets one_click. + [JsonPropertyName("one_click")] + public bool? OneClick { get; set; } + + /// Gets or sets url. + [JsonPropertyName("url")] + public string? Url { get; set; } + +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Models/Shared/SendBatchEmailsResponseData.cs b/src/CommunityToolkit.Aspire.Posta/Models/Shared/SendBatchEmailsResponseData.cs new file mode 100644 index 000000000..cdbb5df2a --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Models/Shared/SendBatchEmailsResponseData.cs @@ -0,0 +1,28 @@ +using System.Text.Json.Serialization; + +namespace CommunityToolkit.Aspire.Posta.Models.Shared; + +/// Represents the SendBatchEmailsResponseData payload. +public class SendBatchEmailsResponseData +{ + /// Gets or sets failed. + [JsonPropertyName("failed")] + public int? Failed { get; set; } + + /// Gets or sets results. + [JsonPropertyName("results")] + public IReadOnlyList? Results { get; set; } + + /// Gets or sets sent. + [JsonPropertyName("sent")] + public int? Sent { get; set; } + + /// Gets or sets skipped. + [JsonPropertyName("skipped")] + public int? Skipped { get; set; } + + /// Gets or sets total. + [JsonPropertyName("total")] + public int? Total { get; set; } + +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Models/Shared/SendBatchEmailsResponseDataResultsItem.cs b/src/CommunityToolkit.Aspire.Posta/Models/Shared/SendBatchEmailsResponseDataResultsItem.cs new file mode 100644 index 000000000..5297a2c7f --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Models/Shared/SendBatchEmailsResponseDataResultsItem.cs @@ -0,0 +1,24 @@ +using System.Text.Json.Serialization; + +namespace CommunityToolkit.Aspire.Posta.Models.Shared; + +/// Represents the SendBatchEmailsResponseDataResultsItem payload. +public class SendBatchEmailsResponseDataResultsItem +{ + /// Gets or sets email. + [JsonPropertyName("email")] + public string? Email { get; set; } + + /// Gets or sets error. + [JsonPropertyName("error")] + public string? Error { get; set; } + + /// Gets or sets id. + [JsonPropertyName("id")] + public string? Id { get; set; } + + /// Gets or sets status. + [JsonPropertyName("status")] + public string? Status { get; set; } + +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Models/Shared/SendEmailUsingTemplateRequestAttachmentsItem.cs b/src/CommunityToolkit.Aspire.Posta/Models/Shared/SendEmailUsingTemplateRequestAttachmentsItem.cs new file mode 100644 index 000000000..a7a033367 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Models/Shared/SendEmailUsingTemplateRequestAttachmentsItem.cs @@ -0,0 +1,24 @@ +using System.Text.Json.Serialization; + +namespace CommunityToolkit.Aspire.Posta.Models.Shared; + +/// Represents the SendEmailUsingTemplateRequestAttachmentsItem payload. +public class SendEmailUsingTemplateRequestAttachmentsItem +{ + /// Gets or sets content. + [JsonPropertyName("content")] + public string? Content { get; set; } + + /// Gets or sets content_type. + [JsonPropertyName("content_type")] + public string? ContentType { get; set; } + + /// Gets or sets filename. + [JsonPropertyName("filename")] + public string? Filename { get; set; } + + /// Gets or sets storage_key. + [JsonPropertyName("storage_key")] + public string? StorageKey { get; set; } + +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Models/Shared/SendEmailUsingTemplateRequestUnsubscribe.cs b/src/CommunityToolkit.Aspire.Posta/Models/Shared/SendEmailUsingTemplateRequestUnsubscribe.cs new file mode 100644 index 000000000..290b039c2 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Models/Shared/SendEmailUsingTemplateRequestUnsubscribe.cs @@ -0,0 +1,24 @@ +using System.Text.Json.Serialization; + +namespace CommunityToolkit.Aspire.Posta.Models.Shared; + +/// Represents the SendEmailUsingTemplateRequestUnsubscribe payload. +public class SendEmailUsingTemplateRequestUnsubscribe +{ + /// Gets or sets list_id. + [JsonPropertyName("list_id")] + public int? ListId { get; set; } + + /// Gets or sets mailto. + [JsonPropertyName("mailto")] + public string? Mailto { get; set; } + + /// Gets or sets one_click. + [JsonPropertyName("one_click")] + public bool? OneClick { get; set; } + + /// Gets or sets url. + [JsonPropertyName("url")] + public string? Url { get; set; } + +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Models/Shared/SendEmailUsingTemplateResponseData.cs b/src/CommunityToolkit.Aspire.Posta/Models/Shared/SendEmailUsingTemplateResponseData.cs new file mode 100644 index 000000000..807bf4607 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Models/Shared/SendEmailUsingTemplateResponseData.cs @@ -0,0 +1,16 @@ +using System.Text.Json.Serialization; + +namespace CommunityToolkit.Aspire.Posta.Models.Shared; + +/// Represents the SendEmailUsingTemplateResponseData payload. +public class SendEmailUsingTemplateResponseData +{ + /// Gets or sets id. + [JsonPropertyName("id")] + public string? Id { get; set; } + + /// Gets or sets status. + [JsonPropertyName("status")] + public string? Status { get; set; } + +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Models/Shared/SendTestEmailResponseData.cs b/src/CommunityToolkit.Aspire.Posta/Models/Shared/SendTestEmailResponseData.cs new file mode 100644 index 000000000..d5de7f98f --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Models/Shared/SendTestEmailResponseData.cs @@ -0,0 +1,16 @@ +using System.Text.Json.Serialization; + +namespace CommunityToolkit.Aspire.Posta.Models.Shared; + +/// Represents the SendTestEmailResponseData payload. +public class SendTestEmailResponseData +{ + /// Gets or sets id. + [JsonPropertyName("id")] + public string? Id { get; set; } + + /// Gets or sets status. + [JsonPropertyName("status")] + public string? Status { get; set; } + +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Models/Shared/SubscribeAnEmailToAListResponseData.cs b/src/CommunityToolkit.Aspire.Posta/Models/Shared/SubscribeAnEmailToAListResponseData.cs new file mode 100644 index 000000000..dc24a8264 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Models/Shared/SubscribeAnEmailToAListResponseData.cs @@ -0,0 +1,36 @@ +using System.Text.Json.Serialization; + +namespace CommunityToolkit.Aspire.Posta.Models.Shared; + +/// Represents the SubscribeAnEmailToAListResponseData payload. +public class SubscribeAnEmailToAListResponseData +{ + /// Gets or sets action. + [JsonPropertyName("action")] + public string? Action { get; set; } + + /// Gets or sets email. + [JsonPropertyName("email")] + public string? Email { get; set; } + + /// Gets or sets list_created. + [JsonPropertyName("list_created")] + public bool? ListCreated { get; set; } + + /// Gets or sets list_id. + [JsonPropertyName("list_id")] + public int? ListId { get; set; } + + /// Gets or sets member_added. + [JsonPropertyName("member_added")] + public bool? MemberAdded { get; set; } + + /// Gets or sets subscriber_created. + [JsonPropertyName("subscriber_created")] + public bool? SubscriberCreated { get; set; } + + /// Gets or sets subscriber_id. + [JsonPropertyName("subscriber_id")] + public int? SubscriberId { get; set; } + +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Models/Shared/UnsubscribeAnEmailFromAListResponseData.cs b/src/CommunityToolkit.Aspire.Posta/Models/Shared/UnsubscribeAnEmailFromAListResponseData.cs new file mode 100644 index 000000000..dd46b2163 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Models/Shared/UnsubscribeAnEmailFromAListResponseData.cs @@ -0,0 +1,36 @@ +using System.Text.Json.Serialization; + +namespace CommunityToolkit.Aspire.Posta.Models.Shared; + +/// Represents the UnsubscribeAnEmailFromAListResponseData payload. +public class UnsubscribeAnEmailFromAListResponseData +{ + /// Gets or sets action. + [JsonPropertyName("action")] + public string? Action { get; set; } + + /// Gets or sets email. + [JsonPropertyName("email")] + public string? Email { get; set; } + + /// Gets or sets list_created. + [JsonPropertyName("list_created")] + public bool? ListCreated { get; set; } + + /// Gets or sets list_id. + [JsonPropertyName("list_id")] + public int? ListId { get; set; } + + /// Gets or sets member_added. + [JsonPropertyName("member_added")] + public bool? MemberAdded { get; set; } + + /// Gets or sets subscriber_created. + [JsonPropertyName("subscriber_created")] + public bool? SubscriberCreated { get; set; } + + /// Gets or sets subscriber_id. + [JsonPropertyName("subscriber_id")] + public int? SubscriberId { get; set; } + +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Models/Shared/UpdateTemplateResponseData.cs b/src/CommunityToolkit.Aspire.Posta/Models/Shared/UpdateTemplateResponseData.cs new file mode 100644 index 000000000..40095ccd2 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Models/Shared/UpdateTemplateResponseData.cs @@ -0,0 +1,64 @@ +using System.Text.Json.Serialization; + +namespace CommunityToolkit.Aspire.Posta.Models.Shared; + +/// Represents the UpdateTemplateResponseData payload. +public class UpdateTemplateResponseData +{ + /// Gets or sets active_version. + [JsonPropertyName("active_version")] + public UpdateTemplateResponseDataActiveVersion? ActiveVersion { get; set; } + + /// Gets or sets active_version_id. + [JsonPropertyName("active_version_id")] + public int? ActiveVersionId { get; set; } + + /// Gets or sets created_at. + [JsonPropertyName("created_at")] + public DateTimeOffset? CreatedAt { get; set; } + + /// Gets or sets created_by. + [JsonPropertyName("created_by")] + public UpdateTemplateResponseDataCreatedBy? CreatedBy { get; set; } + + /// Gets or sets default_language. + [JsonPropertyName("default_language")] + public string? DefaultLanguage { get; set; } + + /// Gets or sets description. + [JsonPropertyName("description")] + public string? Description { get; set; } + + /// Gets or sets id. + [JsonPropertyName("id")] + public int? Id { get; set; } + + /// Gets or sets last_edited_by. + [JsonPropertyName("last_edited_by")] + public UpdateTemplateResponseDataLastEditedBy? LastEditedBy { get; set; } + + /// Gets or sets last_edited_by_id. + [JsonPropertyName("last_edited_by_id")] + public int? LastEditedById { get; set; } + + /// Gets or sets name. + [JsonPropertyName("name")] + public string? Name { get; set; } + + /// Gets or sets sample_data. + [JsonPropertyName("sample_data")] + public string? SampleData { get; set; } + + /// Gets or sets updated_at. + [JsonPropertyName("updated_at")] + public DateTimeOffset? UpdatedAt { get; set; } + + /// Gets or sets user_id. + [JsonPropertyName("user_id")] + public int? UserId { get; set; } + + /// Gets or sets workspace_id. + [JsonPropertyName("workspace_id")] + public int? WorkspaceId { get; set; } + +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Models/Shared/UpdateTemplateResponseDataActiveVersion.cs b/src/CommunityToolkit.Aspire.Posta/Models/Shared/UpdateTemplateResponseDataActiveVersion.cs new file mode 100644 index 000000000..577b1fa30 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Models/Shared/UpdateTemplateResponseDataActiveVersion.cs @@ -0,0 +1,40 @@ +using System.Text.Json.Serialization; + +namespace CommunityToolkit.Aspire.Posta.Models.Shared; + +/// Represents the UpdateTemplateResponseDataActiveVersion payload. +public class UpdateTemplateResponseDataActiveVersion +{ + /// Gets or sets created_at. + [JsonPropertyName("created_at")] + public DateTimeOffset? CreatedAt { get; set; } + + /// Gets or sets id. + [JsonPropertyName("id")] + public int? Id { get; set; } + + /// Gets or sets localizations. + [JsonPropertyName("localizations")] + public IReadOnlyList? Localizations { get; set; } + + /// Gets or sets sample_data. + [JsonPropertyName("sample_data")] + public string? SampleData { get; set; } + + /// Gets or sets stylesheet. + [JsonPropertyName("stylesheet")] + public UpdateTemplateResponseDataActiveVersionStylesheet? Stylesheet { get; set; } + + /// Gets or sets stylesheet_id. + [JsonPropertyName("stylesheet_id")] + public int? StylesheetId { get; set; } + + /// Gets or sets template_id. + [JsonPropertyName("template_id")] + public int? TemplateId { get; set; } + + /// Gets or sets version. + [JsonPropertyName("version")] + public int? Version { get; set; } + +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Models/Shared/UpdateTemplateResponseDataActiveVersionLocalizationsItem.cs b/src/CommunityToolkit.Aspire.Posta/Models/Shared/UpdateTemplateResponseDataActiveVersionLocalizationsItem.cs new file mode 100644 index 000000000..3871549cc --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Models/Shared/UpdateTemplateResponseDataActiveVersionLocalizationsItem.cs @@ -0,0 +1,44 @@ +using System.Text.Json.Serialization; + +namespace CommunityToolkit.Aspire.Posta.Models.Shared; + +/// Represents the UpdateTemplateResponseDataActiveVersionLocalizationsItem payload. +public class UpdateTemplateResponseDataActiveVersionLocalizationsItem +{ + /// Gets or sets builder_json. + [JsonPropertyName("builder_json")] + public string? BuilderJson { get; set; } + + /// Gets or sets created_at. + [JsonPropertyName("created_at")] + public DateTimeOffset? CreatedAt { get; set; } + + /// Gets or sets html_template. + [JsonPropertyName("html_template")] + public string? HtmlTemplate { get; set; } + + /// Gets or sets id. + [JsonPropertyName("id")] + public int? Id { get; set; } + + /// Gets or sets language. + [JsonPropertyName("language")] + public string? Language { get; set; } + + /// Gets or sets subject_template. + [JsonPropertyName("subject_template")] + public string? SubjectTemplate { get; set; } + + /// Gets or sets text_template. + [JsonPropertyName("text_template")] + public string? TextTemplate { get; set; } + + /// Gets or sets updated_at. + [JsonPropertyName("updated_at")] + public DateTimeOffset? UpdatedAt { get; set; } + + /// Gets or sets version_id. + [JsonPropertyName("version_id")] + public int? VersionId { get; set; } + +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Models/Shared/UpdateTemplateResponseDataActiveVersionStylesheet.cs b/src/CommunityToolkit.Aspire.Posta/Models/Shared/UpdateTemplateResponseDataActiveVersionStylesheet.cs new file mode 100644 index 000000000..5535edfee --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Models/Shared/UpdateTemplateResponseDataActiveVersionStylesheet.cs @@ -0,0 +1,36 @@ +using System.Text.Json.Serialization; + +namespace CommunityToolkit.Aspire.Posta.Models.Shared; + +/// Represents the UpdateTemplateResponseDataActiveVersionStylesheet payload. +public class UpdateTemplateResponseDataActiveVersionStylesheet +{ + /// Gets or sets created_at. + [JsonPropertyName("created_at")] + public DateTimeOffset? CreatedAt { get; set; } + + /// Gets or sets css. + [JsonPropertyName("css")] + public string? Css { get; set; } + + /// Gets or sets id. + [JsonPropertyName("id")] + public int? Id { get; set; } + + /// Gets or sets name. + [JsonPropertyName("name")] + public string? Name { get; set; } + + /// Gets or sets updated_at. + [JsonPropertyName("updated_at")] + public DateTimeOffset? UpdatedAt { get; set; } + + /// Gets or sets user_id. + [JsonPropertyName("user_id")] + public int? UserId { get; set; } + + /// Gets or sets workspace_id. + [JsonPropertyName("workspace_id")] + public int? WorkspaceId { get; set; } + +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Models/Shared/UpdateTemplateResponseDataCreatedBy.cs b/src/CommunityToolkit.Aspire.Posta/Models/Shared/UpdateTemplateResponseDataCreatedBy.cs new file mode 100644 index 000000000..fc42cdb1f --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Models/Shared/UpdateTemplateResponseDataCreatedBy.cs @@ -0,0 +1,16 @@ +using System.Text.Json.Serialization; + +namespace CommunityToolkit.Aspire.Posta.Models.Shared; + +/// Represents the UpdateTemplateResponseDataCreatedBy payload. +public class UpdateTemplateResponseDataCreatedBy +{ + /// Gets or sets id. + [JsonPropertyName("id")] + public int? Id { get; set; } + + /// Gets or sets name. + [JsonPropertyName("name")] + public string? Name { get; set; } + +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Models/Shared/UpdateTemplateResponseDataLastEditedBy.cs b/src/CommunityToolkit.Aspire.Posta/Models/Shared/UpdateTemplateResponseDataLastEditedBy.cs new file mode 100644 index 000000000..f92085122 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Models/Shared/UpdateTemplateResponseDataLastEditedBy.cs @@ -0,0 +1,16 @@ +using System.Text.Json.Serialization; + +namespace CommunityToolkit.Aspire.Posta.Models.Shared; + +/// Represents the UpdateTemplateResponseDataLastEditedBy payload. +public class UpdateTemplateResponseDataLastEditedBy +{ + /// Gets or sets id. + [JsonPropertyName("id")] + public int? Id { get; set; } + + /// Gets or sets name. + [JsonPropertyName("name")] + public string? Name { get; set; } + +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Models/Shared/VerifyAnEmailAddressResponseData.cs b/src/CommunityToolkit.Aspire.Posta/Models/Shared/VerifyAnEmailAddressResponseData.cs new file mode 100644 index 000000000..431b04811 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Models/Shared/VerifyAnEmailAddressResponseData.cs @@ -0,0 +1,48 @@ +using System.Text.Json.Serialization; + +namespace CommunityToolkit.Aspire.Posta.Models.Shared; + +/// Represents the VerifyAnEmailAddressResponseData payload. +public class VerifyAnEmailAddressResponseData +{ + /// Gets or sets cached. + [JsonPropertyName("cached")] + public bool? Cached { get; set; } + + /// Gets or sets checked_at. + [JsonPropertyName("checked_at")] + public DateTimeOffset? CheckedAt { get; set; } + + /// Gets or sets checks. + [JsonPropertyName("checks")] + public VerifyAnEmailAddressResponseDataChecks? Checks { get; set; } + + /// Gets or sets email. + [JsonPropertyName("email")] + public string? Email { get; set; } + + /// Gets or sets mailbox_verified. + [JsonPropertyName("mailbox_verified")] + public bool? MailboxVerified { get; set; } + + /// Gets or sets previously_bounced. + [JsonPropertyName("previously_bounced")] + public bool? PreviouslyBounced { get; set; } + + /// Gets or sets reason. + [JsonPropertyName("reason")] + public string? Reason { get; set; } + + /// Gets or sets score. + [JsonPropertyName("score")] + public int? Score { get; set; } + + /// Gets or sets status. + [JsonPropertyName("status")] + public string? Status { get; set; } + + /// Gets or sets suppressed. + [JsonPropertyName("suppressed")] + public bool? Suppressed { get; set; } + +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Models/Shared/VerifyAnEmailAddressResponseDataChecks.cs b/src/CommunityToolkit.Aspire.Posta/Models/Shared/VerifyAnEmailAddressResponseDataChecks.cs new file mode 100644 index 000000000..6b01475f9 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Models/Shared/VerifyAnEmailAddressResponseDataChecks.cs @@ -0,0 +1,28 @@ +using System.Text.Json.Serialization; + +namespace CommunityToolkit.Aspire.Posta.Models.Shared; + +/// Represents the VerifyAnEmailAddressResponseDataChecks payload. +public class VerifyAnEmailAddressResponseDataChecks +{ + /// Gets or sets disposable. + [JsonPropertyName("disposable")] + public bool? Disposable { get; set; } + + /// Gets or sets mx. + [JsonPropertyName("mx")] + public bool? Mx { get; set; } + + /// Gets or sets role_account. + [JsonPropertyName("role_account")] + public bool? RoleAccount { get; set; } + + /// Gets or sets smtp. + [JsonPropertyName("smtp")] + public string? Smtp { get; set; } + + /// Gets or sets syntax. + [JsonPropertyName("syntax")] + public bool? Syntax { get; set; } + +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Models/SubscriberLists/ReSubscribeAnEmailToAListRequest.cs b/src/CommunityToolkit.Aspire.Posta/Models/SubscriberLists/ReSubscribeAnEmailToAListRequest.cs new file mode 100644 index 000000000..9c95c3956 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Models/SubscriberLists/ReSubscribeAnEmailToAListRequest.cs @@ -0,0 +1,16 @@ +using System.Text.Json.Serialization; + +namespace CommunityToolkit.Aspire.Posta.Models.SubscriberLists; + +/// Represents the ReSubscribeAnEmailToAListRequest payload. +public class ReSubscribeAnEmailToAListRequest +{ + /// Gets or sets email. + [JsonPropertyName("email")] + public required string Email { get; set; } + + /// Gets or sets the id path parameter. + [JsonIgnore] + public required int Id { get; set; } + +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Models/SubscriberLists/ReSubscribeAnEmailToAListResponse.cs b/src/CommunityToolkit.Aspire.Posta/Models/SubscriberLists/ReSubscribeAnEmailToAListResponse.cs new file mode 100644 index 000000000..8dd7f2959 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Models/SubscriberLists/ReSubscribeAnEmailToAListResponse.cs @@ -0,0 +1,16 @@ +using System.Text.Json.Serialization; + +namespace CommunityToolkit.Aspire.Posta.Models.SubscriberLists; + +/// Represents the ReSubscribeAnEmailToAListResponse payload. +public class ReSubscribeAnEmailToAListResponse +{ + /// Gets or sets data. + [JsonPropertyName("data")] + public Shared.ReSubscribeAnEmailToAListResponseData? Data { get; set; } + + /// Gets or sets success. + [JsonPropertyName("success")] + public bool? Success { get; set; } + +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Models/SubscriberLists/SubscribeAnEmailToAListRequest.cs b/src/CommunityToolkit.Aspire.Posta/Models/SubscriberLists/SubscribeAnEmailToAListRequest.cs new file mode 100644 index 000000000..0589b070c --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Models/SubscriberLists/SubscribeAnEmailToAListRequest.cs @@ -0,0 +1,20 @@ +using System.Text.Json.Serialization; + +namespace CommunityToolkit.Aspire.Posta.Models.SubscriberLists; + +/// Represents the SubscribeAnEmailToAListRequest payload. +public class SubscribeAnEmailToAListRequest +{ + /// Gets or sets email. + [JsonPropertyName("email")] + public required string Email { get; set; } + + /// Gets or sets list. + [JsonPropertyName("list")] + public required string List { get; set; } + + /// Gets or sets name. + [JsonPropertyName("name")] + public string? Name { get; set; } + +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Models/SubscriberLists/SubscribeAnEmailToAListResponse.cs b/src/CommunityToolkit.Aspire.Posta/Models/SubscriberLists/SubscribeAnEmailToAListResponse.cs new file mode 100644 index 000000000..23d3fc216 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Models/SubscriberLists/SubscribeAnEmailToAListResponse.cs @@ -0,0 +1,16 @@ +using System.Text.Json.Serialization; + +namespace CommunityToolkit.Aspire.Posta.Models.SubscriberLists; + +/// Represents the SubscribeAnEmailToAListResponse payload. +public class SubscribeAnEmailToAListResponse +{ + /// Gets or sets data. + [JsonPropertyName("data")] + public Shared.SubscribeAnEmailToAListResponseData? Data { get; set; } + + /// Gets or sets success. + [JsonPropertyName("success")] + public bool? Success { get; set; } + +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Models/SubscriberLists/UnsubscribeAnEmailFromAListRequest.cs b/src/CommunityToolkit.Aspire.Posta/Models/SubscriberLists/UnsubscribeAnEmailFromAListRequest.cs new file mode 100644 index 000000000..06a665a38 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Models/SubscriberLists/UnsubscribeAnEmailFromAListRequest.cs @@ -0,0 +1,20 @@ +using System.Text.Json.Serialization; + +namespace CommunityToolkit.Aspire.Posta.Models.SubscriberLists; + +/// Represents the UnsubscribeAnEmailFromAListRequest payload. +public class UnsubscribeAnEmailFromAListRequest +{ + /// Gets or sets email. + [JsonPropertyName("email")] + public required string Email { get; set; } + + /// Gets or sets reason. + [JsonPropertyName("reason")] + public string? Reason { get; set; } + + /// Gets or sets the id path parameter. + [JsonIgnore] + public required int Id { get; set; } + +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Models/SubscriberLists/UnsubscribeAnEmailFromAListResponse.cs b/src/CommunityToolkit.Aspire.Posta/Models/SubscriberLists/UnsubscribeAnEmailFromAListResponse.cs new file mode 100644 index 000000000..9e4ba336c --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Models/SubscriberLists/UnsubscribeAnEmailFromAListResponse.cs @@ -0,0 +1,16 @@ +using System.Text.Json.Serialization; + +namespace CommunityToolkit.Aspire.Posta.Models.SubscriberLists; + +/// Represents the UnsubscribeAnEmailFromAListResponse payload. +public class UnsubscribeAnEmailFromAListResponse +{ + /// Gets or sets data. + [JsonPropertyName("data")] + public Shared.UnsubscribeAnEmailFromAListResponseData? Data { get; set; } + + /// Gets or sets success. + [JsonPropertyName("success")] + public bool? Success { get; set; } + +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Models/Templates/CreateTemplateRequest.cs b/src/CommunityToolkit.Aspire.Posta/Models/Templates/CreateTemplateRequest.cs new file mode 100644 index 000000000..b48772048 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Models/Templates/CreateTemplateRequest.cs @@ -0,0 +1,24 @@ +using System.Text.Json.Serialization; + +namespace CommunityToolkit.Aspire.Posta.Models.Templates; + +/// Represents the CreateTemplateRequest payload. +public class CreateTemplateRequest +{ + /// Gets or sets default_language. + [JsonPropertyName("default_language")] + public string? DefaultLanguage { get; set; } + + /// Gets or sets description. + [JsonPropertyName("description")] + public string? Description { get; set; } + + /// Gets or sets name. + [JsonPropertyName("name")] + public required string Name { get; set; } + + /// Gets or sets sample_data. + [JsonPropertyName("sample_data")] + public string? SampleData { get; set; } + +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Models/Templates/CreateTemplateResponse.cs b/src/CommunityToolkit.Aspire.Posta/Models/Templates/CreateTemplateResponse.cs new file mode 100644 index 000000000..a5c485a7f --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Models/Templates/CreateTemplateResponse.cs @@ -0,0 +1,16 @@ +using System.Text.Json.Serialization; + +namespace CommunityToolkit.Aspire.Posta.Models.Templates; + +/// Represents the CreateTemplateResponse payload. +public class CreateTemplateResponse +{ + /// Gets or sets data. + [JsonPropertyName("data")] + public Shared.CreateTemplateResponseData? Data { get; set; } + + /// Gets or sets success. + [JsonPropertyName("success")] + public bool? Success { get; set; } + +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Models/Templates/DeleteTemplateRequest.cs b/src/CommunityToolkit.Aspire.Posta/Models/Templates/DeleteTemplateRequest.cs new file mode 100644 index 000000000..14f0b591c --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Models/Templates/DeleteTemplateRequest.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace CommunityToolkit.Aspire.Posta.Models.Templates; + +/// Represents the DeleteTemplateRequest payload. +public class DeleteTemplateRequest +{ + /// Gets or sets the id path parameter. + [JsonIgnore] + public required int Id { get; set; } + +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Models/Templates/DeleteTemplateResponse.cs b/src/CommunityToolkit.Aspire.Posta/Models/Templates/DeleteTemplateResponse.cs new file mode 100644 index 000000000..b5a0ebba9 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Models/Templates/DeleteTemplateResponse.cs @@ -0,0 +1,6 @@ +namespace CommunityToolkit.Aspire.Posta.Models.Templates; + +/// Represents the DeleteTemplateResponse payload. +public class DeleteTemplateResponse +{ +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Models/Templates/GetTemplateRequest.cs b/src/CommunityToolkit.Aspire.Posta/Models/Templates/GetTemplateRequest.cs new file mode 100644 index 000000000..5eedebccd --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Models/Templates/GetTemplateRequest.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace CommunityToolkit.Aspire.Posta.Models.Templates; + +/// Represents the GetTemplateRequest payload. +public class GetTemplateRequest +{ + /// Gets or sets the id path parameter. + [JsonIgnore] + public required int Id { get; set; } + +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Models/Templates/GetTemplateResponse.cs b/src/CommunityToolkit.Aspire.Posta/Models/Templates/GetTemplateResponse.cs new file mode 100644 index 000000000..4efcadae3 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Models/Templates/GetTemplateResponse.cs @@ -0,0 +1,16 @@ +using System.Text.Json.Serialization; + +namespace CommunityToolkit.Aspire.Posta.Models.Templates; + +/// Represents the GetTemplateResponse payload. +public class GetTemplateResponse +{ + /// Gets or sets data. + [JsonPropertyName("data")] + public Shared.GetTemplateResponseData? Data { get; set; } + + /// Gets or sets success. + [JsonPropertyName("success")] + public bool? Success { get; set; } + +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Models/Templates/ListTemplatesRequest.cs b/src/CommunityToolkit.Aspire.Posta/Models/Templates/ListTemplatesRequest.cs new file mode 100644 index 000000000..294a9bf14 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Models/Templates/ListTemplatesRequest.cs @@ -0,0 +1,20 @@ +using System.Text.Json.Serialization; + +namespace CommunityToolkit.Aspire.Posta.Models.Templates; + +/// Represents the ListTemplatesRequest payload. +public class ListTemplatesRequest +{ + /// Gets or sets the page query parameter. + [JsonIgnore] + public int? Page { get; set; } + + /// Gets or sets the size query parameter. + [JsonIgnore] + public int? Size { get; set; } + + /// Gets or sets the search query parameter. + [JsonIgnore] + public string? Search { get; set; } + +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Models/Templates/ListTemplatesResponse.cs b/src/CommunityToolkit.Aspire.Posta/Models/Templates/ListTemplatesResponse.cs new file mode 100644 index 000000000..7cbf38b52 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Models/Templates/ListTemplatesResponse.cs @@ -0,0 +1,20 @@ +using System.Text.Json.Serialization; + +namespace CommunityToolkit.Aspire.Posta.Models.Templates; + +/// Represents the ListTemplatesResponse payload. +public class ListTemplatesResponse +{ + /// Gets or sets data. + [JsonPropertyName("data")] + public IReadOnlyList? Data { get; set; } + + /// Gets or sets pageable. + [JsonPropertyName("pageable")] + public Shared.ListTemplatesResponsePageable? Pageable { get; set; } + + /// Gets or sets success. + [JsonPropertyName("success")] + public bool? Success { get; set; } + +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Models/Templates/PreviewTemplateRequest.cs b/src/CommunityToolkit.Aspire.Posta/Models/Templates/PreviewTemplateRequest.cs new file mode 100644 index 000000000..be050e374 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Models/Templates/PreviewTemplateRequest.cs @@ -0,0 +1,29 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace CommunityToolkit.Aspire.Posta.Models.Templates; + +/// Represents the PreviewTemplateRequest payload. +public class PreviewTemplateRequest +{ + /// Gets or sets html_template. + [JsonPropertyName("html_template")] + public string? HtmlTemplate { get; set; } + + /// Gets or sets stylesheet_id. + [JsonPropertyName("stylesheet_id")] + public int? StylesheetId { get; set; } + + /// Gets or sets subject_template. + [JsonPropertyName("subject_template")] + public required string SubjectTemplate { get; set; } + + /// Gets or sets template_data. + [JsonPropertyName("template_data")] + public IReadOnlyDictionary? TemplateData { get; set; } + + /// Gets or sets text_template. + [JsonPropertyName("text_template")] + public string? TextTemplate { get; set; } + +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Models/Templates/PreviewTemplateResponse.cs b/src/CommunityToolkit.Aspire.Posta/Models/Templates/PreviewTemplateResponse.cs new file mode 100644 index 000000000..5b8e3f95d --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Models/Templates/PreviewTemplateResponse.cs @@ -0,0 +1,16 @@ +using System.Text.Json.Serialization; + +namespace CommunityToolkit.Aspire.Posta.Models.Templates; + +/// Represents the PreviewTemplateResponse payload. +public class PreviewTemplateResponse +{ + /// Gets or sets data. + [JsonPropertyName("data")] + public Shared.PreviewTemplateResponseData? Data { get; set; } + + /// Gets or sets success. + [JsonPropertyName("success")] + public bool? Success { get; set; } + +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Models/Templates/SendTestEmailRequest.cs b/src/CommunityToolkit.Aspire.Posta/Models/Templates/SendTestEmailRequest.cs new file mode 100644 index 000000000..7a05febe2 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Models/Templates/SendTestEmailRequest.cs @@ -0,0 +1,29 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace CommunityToolkit.Aspire.Posta.Models.Templates; + +/// Represents the SendTestEmailRequest payload. +public class SendTestEmailRequest +{ + /// Gets or sets from. + [JsonPropertyName("from")] + public string? From { get; set; } + + /// Gets or sets language. + [JsonPropertyName("language")] + public string? Language { get; set; } + + /// Gets or sets template_data. + [JsonPropertyName("template_data")] + public IReadOnlyDictionary? TemplateData { get; set; } + + /// Gets or sets to. + [JsonPropertyName("to")] + public required IReadOnlyList To { get; set; } + + /// Gets or sets the id path parameter. + [JsonIgnore] + public required int Id { get; set; } + +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Models/Templates/SendTestEmailResponse.cs b/src/CommunityToolkit.Aspire.Posta/Models/Templates/SendTestEmailResponse.cs new file mode 100644 index 000000000..8ca23b16c --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Models/Templates/SendTestEmailResponse.cs @@ -0,0 +1,16 @@ +using System.Text.Json.Serialization; + +namespace CommunityToolkit.Aspire.Posta.Models.Templates; + +/// Represents the SendTestEmailResponse payload. +public class SendTestEmailResponse +{ + /// Gets or sets data. + [JsonPropertyName("data")] + public Shared.SendTestEmailResponseData? Data { get; set; } + + /// Gets or sets success. + [JsonPropertyName("success")] + public bool? Success { get; set; } + +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Models/Templates/UpdateTemplateRequest.cs b/src/CommunityToolkit.Aspire.Posta/Models/Templates/UpdateTemplateRequest.cs new file mode 100644 index 000000000..a5d8d3fc2 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Models/Templates/UpdateTemplateRequest.cs @@ -0,0 +1,28 @@ +using System.Text.Json.Serialization; + +namespace CommunityToolkit.Aspire.Posta.Models.Templates; + +/// Represents the UpdateTemplateRequest payload. +public class UpdateTemplateRequest +{ + /// Gets or sets default_language. + [JsonPropertyName("default_language")] + public string? DefaultLanguage { get; set; } + + /// Gets or sets description. + [JsonPropertyName("description")] + public string? Description { get; set; } + + /// Gets or sets name. + [JsonPropertyName("name")] + public string? Name { get; set; } + + /// Gets or sets sample_data. + [JsonPropertyName("sample_data")] + public string? SampleData { get; set; } + + /// Gets or sets the id path parameter. + [JsonIgnore] + public required int Id { get; set; } + +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Models/Templates/UpdateTemplateResponse.cs b/src/CommunityToolkit.Aspire.Posta/Models/Templates/UpdateTemplateResponse.cs new file mode 100644 index 000000000..79c9994fe --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Models/Templates/UpdateTemplateResponse.cs @@ -0,0 +1,16 @@ +using System.Text.Json.Serialization; + +namespace CommunityToolkit.Aspire.Posta.Models.Templates; + +/// Represents the UpdateTemplateResponse payload. +public class UpdateTemplateResponse +{ + /// Gets or sets data. + [JsonPropertyName("data")] + public Shared.UpdateTemplateResponseData? Data { get; set; } + + /// Gets or sets success. + [JsonPropertyName("success")] + public bool? Success { get; set; } + +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/README.md b/src/CommunityToolkit.Aspire.Posta/README.md new file mode 100644 index 000000000..fe56fc6d8 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/README.md @@ -0,0 +1,87 @@ +# Posta client integration + +Use this integration to call the Posta HTTP API through an `HttpClient` configured with Aspire service discovery. + +## Getting started + +```shell +dotnet add package CommunityToolkit.Aspire.Posta +``` + +Register the client in the consuming project: + +```csharp +builder.AddPostaClient("posta", settings => +{ + settings.ApiKey = builder.Configuration["Posta:ApiKey"]; +}); +``` + +The hosting resource exposes an `Endpoint=...` connection string, so an AppHost can use `.WithReference(posta)`. + +## Calling an API section + +The client intentionally covers the application-facing email, template, subscriber-list, health, and information APIs. These areas are exposed through separate interfaces. + +```csharp +using CommunityToolkit.Aspire.Posta.Models.Emails; +using CommunityToolkit.Aspire.Posta.Clients; + +public sealed class MailService(IPostaClient posta) +{ + public Task SendAsync(CancellationToken cancellationToken) => + posta.Emails.SendAnEmailAsync( + new SendAnEmailRequest + { + From = "Acme ", + To = ["user@example.com"], + Subject = "Hello", + Html = "

Hello!

" + }, + cancellationToken); +} +``` + +Path and query parameters are supplied separately and are escaped by the client: + +```csharp +await posta.Emails.GetEmailDetailsAsync( + new GetEmailDetailsRequest + { + Id = emailId + }, + cancellationToken); +``` + +API-key and JWT operations select the appropriate configured credential automatically. A dynamic application-wide implementation can replace `CommunityToolkit.Aspire.Posta.Configuration.IPostaCredentialProvider` in DI. + +For multiple Posta instances, register keyed clients with `AddKeyedPostaClient(...)` and resolve them through `GetRequiredKeyedService(key)`. + +## Request and response models + +Models are grouped by API area under `CommunityToolkit.Aspire.Posta.Models`, for example `Models.Emails`, `Models.Templates`, and `Models.SubscriberLists`. Reusable nested schemas are under `Models.Shared`. JSON property names, required members, path parameters, and query parameters follow Posta's OpenAPI document. + +Administrative, OAuth, workspace-management, analytics, campaign, and other uncommon APIs are not duplicated in this package. They can still be called through `IPostaSectionClient.SendAsync` with a custom `PostaEndpoint` and `PostaRequest`. + +## Overriding endpoints + +Paths are not embedded in the section clients. Replace `IPostaEndpoints`, or derive from `PostaEndpoints` and override only the changed operation: + +```csharp +public sealed class CustomPostaEndpoints : PostaEndpoints +{ + public override PostaEndpoint SendAnEmail { get; } = + new(HttpMethod.Post, "/custom/v1/emails/send", PostaAuthentication.ApiKey); +} + +builder.Services.AddSingleton(); +``` + +## Additional documentation + +- [Posta API reference](https://app.goposta.dev/docs) +- [Posta OpenAPI document](https://app.goposta.dev/openapi.json) + +## Feedback & contributing + +Submit issues and pull requests through the CommunityToolkit.Aspire repository. diff --git a/src/CommunityToolkit.Aspire.Posta/Transport/PostaApiException.cs b/src/CommunityToolkit.Aspire.Posta/Transport/PostaApiException.cs new file mode 100644 index 000000000..09523571b --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Transport/PostaApiException.cs @@ -0,0 +1,16 @@ +using System.Net; + +namespace CommunityToolkit.Aspire.Posta.Transport; + +/// Represents a non-successful response returned by Posta. +public sealed class PostaApiException : HttpRequestException +{ + internal PostaApiException(HttpStatusCode statusCode, string? responseBody, string message) + : base(message, null, statusCode) + { + ResponseBody = responseBody; + } + + /// Gets the response body, when one was returned. + public string? ResponseBody { get; } +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Transport/PostaRequest.cs b/src/CommunityToolkit.Aspire.Posta/Transport/PostaRequest.cs new file mode 100644 index 000000000..8ba590bd8 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Transport/PostaRequest.cs @@ -0,0 +1,23 @@ +namespace CommunityToolkit.Aspire.Posta.Transport; + +/// Arguments for invoking a Posta endpoint. +public sealed class PostaRequest +{ + /// Gets path parameter values without braces, for example id. + public IReadOnlyDictionary PathParameters { get; init; } = new Dictionary(); + + /// Gets query string values. Collections are emitted as repeated keys. + public IReadOnlyDictionary Query { get; init; } = new Dictionary(); + + /// Gets the object serialized as JSON. + public object? Body { get; init; } + + /// Gets custom HTTP content for streaming or multipart requests. + public HttpContent? Content { get; init; } + + /// Gets a per-request bearer credential overriding configured credentials. + public string? BearerToken { get; init; } + + /// Gets additional request headers. + public IReadOnlyDictionary Headers { get; init; } = new Dictionary(); +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Transport/PostaStreamResponse.cs b/src/CommunityToolkit.Aspire.Posta/Transport/PostaStreamResponse.cs new file mode 100644 index 000000000..240ddb602 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Transport/PostaStreamResponse.cs @@ -0,0 +1,35 @@ +using System.Net; +using System.Net.Http.Headers; + +namespace CommunityToolkit.Aspire.Posta.Transport; + +/// Owns a streaming Posta response and its content stream. +public sealed class PostaStreamResponse : IDisposable, IAsyncDisposable +{ + private readonly HttpResponseMessage _response; + + internal PostaStreamResponse(HttpResponseMessage response, Stream stream) + { + _response = response; + Stream = stream; + } + + /// Gets the response content stream. + public Stream Stream { get; } + + /// Gets the response status code. + public HttpStatusCode StatusCode => _response.StatusCode; + + /// Gets response content headers. + public HttpContentHeaders Headers => _response.Content.Headers; + + /// + public void Dispose() => _response.Dispose(); + + /// + public ValueTask DisposeAsync() + { + _response.Dispose(); + return ValueTask.CompletedTask; + } +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Posta/Transport/PostaTransport.cs b/src/CommunityToolkit.Aspire.Posta/Transport/PostaTransport.cs new file mode 100644 index 000000000..ad3525531 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Posta/Transport/PostaTransport.cs @@ -0,0 +1,164 @@ +using CommunityToolkit.Aspire.Posta.Configuration; +using CommunityToolkit.Aspire.Posta.Endpoints; +using System.Globalization; +using System.Net.Http.Headers; +using System.Net.Http.Json; +using System.Text.Json; + +namespace CommunityToolkit.Aspire.Posta.Transport; + +internal sealed class PostaTransport(HttpClient httpClient, IPostaCredentialProvider credentialProvider) +{ + private static readonly JsonSerializerOptions s_jsonOptions = new(JsonSerializerDefaults.Web); + + public async Task SendAsync(PostaEndpoint endpoint, PostaRequest? request, CancellationToken cancellationToken) + { + using HttpResponseMessage response = await SendCoreAsync(endpoint, request, HttpCompletionOption.ResponseContentRead, cancellationToken).ConfigureAwait(false); + if (response.Content.Headers.ContentLength == 0 || response.StatusCode == System.Net.HttpStatusCode.NoContent) + { + return default; + } + + return await response.Content.ReadFromJsonAsync(s_jsonOptions, cancellationToken).ConfigureAwait(false); + } + + public async Task SendAsync(PostaEndpoint endpoint, PostaRequest? request, CancellationToken cancellationToken) + { + using HttpResponseMessage response = await SendCoreAsync(endpoint, request, HttpCompletionOption.ResponseContentRead, cancellationToken).ConfigureAwait(false); + if (response.Content.Headers.ContentLength == 0 || response.StatusCode == System.Net.HttpStatusCode.NoContent) + { + return null; + } + + await using Stream stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); + return await JsonDocument.ParseAsync(stream, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + public async Task SendStreamAsync(PostaEndpoint endpoint, PostaRequest? request, CancellationToken cancellationToken) + { + HttpResponseMessage response = await SendCoreAsync(endpoint, request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + try + { + Stream stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); + return new PostaStreamResponse(response, stream); + } + catch + { + response.Dispose(); + throw; + } + } + + private async Task SendCoreAsync(PostaEndpoint endpoint, PostaRequest? request, HttpCompletionOption completionOption, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(endpoint); + if (!endpoint.IsImplemented) + { + throw new NotSupportedException($"The default Posta client cannot invoke '{endpoint.Path}': {endpoint.Documentation}"); + } + + request ??= new PostaRequest(); + string path = ExpandPath(endpoint.Path, request.PathParameters); + path = AddQueryString(path, request.Query); + + using HttpRequestMessage message = new(endpoint.Method, path); + if (request.Content is not null && request.Body is not null) + { + throw new ArgumentException("Specify either Body or Content, not both.", nameof(request)); + } + + if (request.Content is not null) + { + message.Content = request.Content; + } + else if (request.Body is not null) + { + message.Content = JsonContent.Create(request.Body, options: s_jsonOptions); + } + + string? credential = request.BearerToken ?? await credentialProvider.GetCredentialAsync(endpoint.Authentication, cancellationToken).ConfigureAwait(false); + if (endpoint.Authentication != PostaAuthentication.None) + { + if (string.IsNullOrWhiteSpace(credential)) + { + throw new InvalidOperationException($"A credential for {endpoint.Authentication} is required by '{endpoint.Path}'."); + } + + message.Headers.Authorization = new AuthenticationHeaderValue("Bearer", credential); + } + + foreach ((string name, string value) in request.Headers) + { + if (!message.Headers.TryAddWithoutValidation(name, value)) + { + message.Content?.Headers.TryAddWithoutValidation(name, value); + } + } + + HttpResponseMessage response = await httpClient.SendAsync(message, completionOption, cancellationToken).ConfigureAwait(false); + if (response.IsSuccessStatusCode) + { + return response; + } + + string? responseBody = response.Content is null ? null : await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); + var statusCode = response.StatusCode; + response.Dispose(); + throw new PostaApiException(statusCode, responseBody, $"Posta returned HTTP {(int)statusCode} ({statusCode}) for {endpoint.Method} {path}."); + } + + private static string ExpandPath(string template, IReadOnlyDictionary values) + { + string result = template; + foreach ((string name, object? value) in values) + { + result = result.Replace("{" + name + "}", Uri.EscapeDataString(ConvertToString(value)), StringComparison.Ordinal); + } + + if (result.Contains('{', StringComparison.Ordinal)) + { + throw new ArgumentException($"Not all path parameters were supplied for '{template}'.", nameof(values)); + } + + return result; + } + + private static string AddQueryString(string path, IReadOnlyDictionary query) + { + List values = []; + foreach ((string name, object? value) in query) + { + if (value is null) + { + continue; + } + + if (value is System.Collections.IEnumerable enumerable and not string) + { + foreach (object? item in enumerable) + { + if (item is not null) + { + values.Add($"{Uri.EscapeDataString(name)}={Uri.EscapeDataString(ConvertToString(item))}"); + } + } + } + else + { + values.Add($"{Uri.EscapeDataString(name)}={Uri.EscapeDataString(ConvertToString(value))}"); + } + } + + return values.Count == 0 ? path : $"{path}?{string.Join('&', values)}"; + } + + private static string ConvertToString(object? value) => value switch + { + null => string.Empty, + bool boolean => boolean ? "true" : "false", + DateTimeOffset dateTimeOffset => dateTimeOffset.ToString("O", CultureInfo.InvariantCulture), + DateTime dateTime => dateTime.ToString("O", CultureInfo.InvariantCulture), + IFormattable formattable => formattable.ToString(null, CultureInfo.InvariantCulture), + _ => value.ToString() ?? string.Empty + }; +} \ No newline at end of file diff --git a/tests/CommunityToolkit.Aspire.Posta.Tests/CommunityToolkit.Aspire.Posta.Tests.csproj b/tests/CommunityToolkit.Aspire.Posta.Tests/CommunityToolkit.Aspire.Posta.Tests.csproj new file mode 100644 index 000000000..6ad99e918 --- /dev/null +++ b/tests/CommunityToolkit.Aspire.Posta.Tests/CommunityToolkit.Aspire.Posta.Tests.csproj @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/tests/CommunityToolkit.Aspire.Posta.Tests/PostaClientTests.cs b/tests/CommunityToolkit.Aspire.Posta.Tests/PostaClientTests.cs new file mode 100644 index 000000000..f3c859e7c --- /dev/null +++ b/tests/CommunityToolkit.Aspire.Posta.Tests/PostaClientTests.cs @@ -0,0 +1,162 @@ +using CommunityToolkit.Aspire.Posta.Clients; +using CommunityToolkit.Aspire.Posta.Configuration; +using CommunityToolkit.Aspire.Posta.Endpoints; +using CommunityToolkit.Aspire.Posta.Models.Emails; +using CommunityToolkit.Aspire.Posta.Transport; +using System.Net.Http.Headers; +using System.Text; + +namespace CommunityToolkit.Aspire.Posta.Tests; + +public class PostaClientTests +{ + private static readonly string[] s_queryTags = ["one", "two"]; + + [Fact] + public void EndpointCatalogContainsEveryOpenApiOperation() + { + var endpoints = typeof(IPostaEndpoints).GetProperties(); + + Assert.Equal(21, endpoints.Length); + Assert.All(endpoints, property => Assert.Equal(typeof(PostaEndpoint), property.PropertyType)); + } + + [Fact] + public void SectionInterfacesContainEveryNamedOperation() + { + Type[] sections = + [ + typeof(IPostaEmailsClient), typeof(IPostaTemplatesClient), typeof(IPostaSubscriberListsClient), + typeof(IPostaHealthClient), typeof(IPostaInfoClient) + ]; + + int operationCount = sections.Sum(section => section.GetMethods().Count(method => method.DeclaringType == section)); + + Assert.Equal(21, operationCount); + } + + [Fact] + public async Task NamedOperationSerializesRequestAndDeserializesTypedResponse() + { + RecordingHandler handler = new(_ => new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent("{\"success\":true,\"data\":{\"id\":\"email-id\",\"status\":\"queued\"}}", Encoding.UTF8, "application/json") + }); + PostaTransport transport = CreateTransport(handler, apiKey: "secret-key"); + PostaClientSection client = new(transport, new PostaEndpoints()); + + SendAnEmailResponse? response = await client.SendAnEmailAsync( + new SendAnEmailRequest + { + From = "sender@example.com", + To = ["recipient@example.com"], + Subject = "Hello", + DryRun = true + }, + TestContext.Current.CancellationToken); + + Assert.NotNull(response); + Assert.True(response.Success); + Assert.Equal("email-id", response.Data?.Id); + Assert.Equal("queued", response.Data?.Status); + Assert.Equal("/api/v1/emails/send?dry_run=true", handler.Request!.RequestUri!.PathAndQuery); + Assert.Contains("\"from\":\"sender@example.com\"", handler.Body, StringComparison.Ordinal); + Assert.DoesNotContain("dry_run", handler.Body, StringComparison.Ordinal); + } + + [Fact] + public async Task SendAsyncExpandsPathAndQueryAndUsesApiKey() + { + RecordingHandler handler = new(_ => new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent("{\"status\":\"queued\"}", Encoding.UTF8, "application/json") + }); + PostaTransport transport = CreateTransport(handler, apiKey: "secret-key"); + + using var result = await transport.SendAsync( + new PostaEndpoint(HttpMethod.Post, "/api/v1/emails/{id}", PostaAuthentication.ApiKey), + new PostaRequest + { + PathParameters = new Dictionary { ["id"] = "a/b" }, + Query = new Dictionary { ["dry_run"] = true, ["tag"] = s_queryTags }, + Body = new { subject = "Hello" } + }, + TestContext.Current.CancellationToken); + + Assert.Equal("/api/v1/emails/a%2Fb?dry_run=true&tag=one&tag=two", handler.Request!.RequestUri!.PathAndQuery); + Assert.Equal(new AuthenticationHeaderValue("Bearer", "secret-key"), handler.Request.Headers.Authorization); + Assert.Contains("\"subject\":\"Hello\"", handler.Body, StringComparison.Ordinal); + Assert.Equal("queued", result!.RootElement.GetProperty("status").GetString()); + } + + [Fact] + public async Task SendAsyncDoesNotSendCredentialToAnonymousEndpoint() + { + RecordingHandler handler = new(_ => new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent("{}", Encoding.UTF8, "application/json") + }); + PostaTransport transport = CreateTransport(handler, apiKey: "must-not-leak"); + + using var result = await transport.SendAsync( + new PostaEndpoint(HttpMethod.Get, "/healthz", PostaAuthentication.None), + null, + TestContext.Current.CancellationToken); + + Assert.Null(handler.Request!.Headers.Authorization); + } + + [Fact] + public async Task SendAsyncPreservesErrorResponse() + { + RecordingHandler handler = new(_ => new HttpResponseMessage(HttpStatusCode.TooManyRequests) + { + Content = new StringContent("{\"error\":\"quota exceeded\"}", Encoding.UTF8, "application/json") + }); + PostaTransport transport = CreateTransport(handler, apiKey: "key"); + + PostaApiException exception = await Assert.ThrowsAsync(() => transport.SendAsync( + new PostaEndpoint(HttpMethod.Post, "/api/v1/emails/send", PostaAuthentication.ApiKey), + null, + TestContext.Current.CancellationToken)); + + Assert.Equal(HttpStatusCode.TooManyRequests, exception.StatusCode); + Assert.Contains("quota exceeded", exception.ResponseBody, StringComparison.Ordinal); + } + + [Fact] + public void EndpointCanBeOverridden() + { + IPostaEndpoints endpoints = new CustomEndpoints(); + + Assert.Equal("/custom/emails/send", endpoints.SendAnEmail.Path); + } + + private static PostaTransport CreateTransport(RecordingHandler handler, string? apiKey = null, string? accessToken = null) + { + HttpClient client = new(handler) { BaseAddress = new Uri("https://posta.example") }; + return new PostaTransport(client, new PostaCredentialProvider(new PostaClientSettings + { + ApiKey = apiKey, + AccessToken = accessToken + })); + } + + private sealed class CustomEndpoints : PostaEndpoints + { + public override PostaEndpoint SendAnEmail { get; } = new(HttpMethod.Post, "/custom/emails/send", PostaAuthentication.ApiKey); + } + + private sealed class RecordingHandler(Func responseFactory) : HttpMessageHandler + { + public HttpRequestMessage? Request { get; private set; } + public string? Body { get; private set; } + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + Request = request; + Body = request.Content is null ? null : await request.Content.ReadAsStringAsync(cancellationToken); + return responseFactory(request); + } + } +} \ No newline at end of file From 927109c89257b61c6185bd0ad3a2e038ad5a99f0 Mon Sep 17 00:00:00 2001 From: axi Date: Mon, 13 Jul 2026 01:45:57 +0300 Subject: [PATCH 26/28] Update solution file and project references to include Posta ApiService and Posta test projects --- CommunityToolkit.Aspire.slnx | 3 +++ .../CommunityToolkit.Aspire.Hosting.Posta.AppHost.csproj | 1 + 2 files changed, 4 insertions(+) diff --git a/CommunityToolkit.Aspire.slnx b/CommunityToolkit.Aspire.slnx index b0156934f..5809c9ec2 100644 --- a/CommunityToolkit.Aspire.slnx +++ b/CommunityToolkit.Aspire.slnx @@ -157,6 +157,7 @@ + @@ -299,6 +300,7 @@ + @@ -374,6 +376,7 @@ + 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 index 8ab6b6f05..724788641 100644 --- 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 @@ -12,6 +12,7 @@ + From 9283b4817daea6d2a53c4a7e8cc7d4fee2598462 Mon Sep 17 00:00:00 2001 From: axi Date: Thu, 23 Jul 2026 00:29:51 +0300 Subject: [PATCH 27/28] Add support for volume and bind mount configuration for Posta attachments - Introduced `WithDataVolume` and `WithDataBindMount` extension methods to configure filesystem-backed attachment storage. - Updated `README.md` with examples and details for volume/bind mount usage. - Added unit tests to validate volume and bind mount behavior. --- .../Program.cs | 3 +- .../PostaHostingExtensions.cs | 40 ++++++++++++ .../README.md | 12 ++++ .../ContainerResourceCreationTests.cs | 64 +++++++++++++++++++ 4 files changed, 118 insertions(+), 1 deletion(-) diff --git a/examples/posta/CommunityToolkit.Aspire.Hosting.Posta.AppHost/Program.cs b/examples/posta/CommunityToolkit.Aspire.Hosting.Posta.AppHost/Program.cs index 50c3cce45..f16d08abf 100644 --- a/examples/posta/CommunityToolkit.Aspire.Hosting.Posta.AppHost/Program.cs +++ b/examples/posta/CommunityToolkit.Aspire.Hosting.Posta.AppHost/Program.cs @@ -5,6 +5,7 @@ var database = postgres.AddDatabase("posta-db", "posta"); var redis = builder.AddRedis("redis"); -builder.AddPosta("posta", database, redis); +builder.AddPosta("posta", database, redis) + .WithDataVolume(); builder.Build().Run(); diff --git a/src/CommunityToolkit.Aspire.Hosting.Posta/PostaHostingExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Posta/PostaHostingExtensions.cs index 10ddc7fff..c340c346f 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Posta/PostaHostingExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Posta/PostaHostingExtensions.cs @@ -1,4 +1,5 @@ using Aspire.Hosting.ApplicationModel; +using Aspire.Hosting.Utils; using CommunityToolkit.Aspire.Hosting.Posta; namespace Aspire.Hosting; @@ -8,6 +9,8 @@ namespace Aspire.Hosting; ///
public static class PostaHostingExtensions { + private const string DataContainerPath = "/data"; + /// /// Adds a Posta container resource to the and configures PostgreSQL and Redis references. /// @@ -112,6 +115,43 @@ public static IResourceBuilder AddPosta( return builder.AddPosta(name, database, redis, options, jwtSecret, adminPassword, adminEmail, port); } + /// + /// Adds a named volume for Posta's filesystem-backed attachment data. + /// + /// The Posta resource builder. + /// The name of the volume. Defaults to an auto-generated name based on the application and resource names. + /// Indicates whether the volume should be read-only. + /// A reference to the for further resource configuration. + [AspireExport] + public static IResourceBuilder WithDataVolume( + this IResourceBuilder builder, + string? name = null, + bool isReadOnly = false) + { + ArgumentNullException.ThrowIfNull(builder); + + return builder.WithVolume(name ?? VolumeNameGenerator.Generate(builder, "data"), DataContainerPath, isReadOnly); + } + + /// + /// Adds a bind mount for Posta's filesystem-backed attachment data. + /// + /// The Posta resource builder. + /// The source directory on the host to mount into the container. + /// Indicates whether the bind mount should be read-only. + /// A reference to the for further resource configuration. + [AspireExport] + public static IResourceBuilder WithDataBindMount( + this IResourceBuilder builder, + string source, + bool isReadOnly = false) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(source); + + return builder.WithBindMount(source, DataContainerPath, isReadOnly); + } + /// /// Configures the PostgreSQL database used by Posta. /// diff --git a/src/CommunityToolkit.Aspire.Hosting.Posta/README.md b/src/CommunityToolkit.Aspire.Hosting.Posta/README.md index 89a6b3f66..eca50870a 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Posta/README.md +++ b/src/CommunityToolkit.Aspire.Hosting.Posta/README.md @@ -29,6 +29,16 @@ builder.AddProject("api") builder.Build().Run(); ``` +To persist filesystem-backed attachments across container restarts, add a named volume or a host bind mount: + +```csharp +var posta = builder.AddPosta("posta", database, redis) + .WithDataVolume(); + +// Alternatively: +// .WithDataBindMount("./data/posta"); +``` + 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 @@ -133,6 +143,8 @@ builder.AddPosta("posta", database, redis) | `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. | +| `WithDataVolume(name, isReadOnly)` | Adds a named volume at `/data` for filesystem-backed attachments. | +| `WithDataBindMount(source, isReadOnly)` | Adds a host bind mount at `/data` for filesystem-backed attachments. | | `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. | diff --git a/tests/CommunityToolkit.Aspire.Hosting.Posta.Tests/ContainerResourceCreationTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Posta.Tests/ContainerResourceCreationTests.cs index f75ff0c83..4fa5f9698 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.Posta.Tests/ContainerResourceCreationTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.Posta.Tests/ContainerResourceCreationTests.cs @@ -43,6 +43,70 @@ public void WithReferenceThrowsWhenRedisIsNull() Assert.Throws(() => posta.WithReference((IResourceBuilder)null!)); } + [Fact] + public void WithDataVolumeAddsVolumeMount() + { + var builder = DistributedApplication.CreateBuilder(); + var (database, redis) = AddPostaDependencies(builder); + + var posta = builder.AddPosta("posta", database, redis) + .WithDataVolume("posta-data", isReadOnly: true); + + var mount = Assert.Single(posta.Resource.Annotations.OfType()); + Assert.Equal(ContainerMountType.Volume, mount.Type); + Assert.Equal("posta-data", mount.Source); + Assert.Equal("/data", mount.Target); + Assert.True(mount.IsReadOnly); + } + + [Fact] + public void WithDataVolumeGeneratesVolumeNameWhenNotSpecified() + { + var builder = DistributedApplication.CreateBuilder(); + var (database, redis) = AddPostaDependencies(builder); + + var posta = builder.AddPosta("posta", database, redis) + .WithDataVolume(); + + var mount = Assert.Single(posta.Resource.Annotations.OfType()); + Assert.Equal(ContainerMountType.Volume, mount.Type); + Assert.False(string.IsNullOrWhiteSpace(mount.Source)); + Assert.Equal("/data", mount.Target); + } + + [Fact] + public void WithDataBindMountAddsBindMount() + { + var builder = DistributedApplication.CreateBuilder(); + var (database, redis) = AddPostaDependencies(builder); + + var posta = builder.AddPosta("posta", database, redis) + .WithDataBindMount("./posta-data", isReadOnly: true); + + var mount = Assert.Single(posta.Resource.Annotations.OfType()); + Assert.Equal(ContainerMountType.BindMount, mount.Type); + var source = Assert.IsType(mount.Source); + Assert.True(Path.IsPathFullyQualified(source)); + Assert.EndsWith("posta-data", source); + Assert.Equal("/data", mount.Target); + Assert.True(mount.IsReadOnly); + } + + [Fact] + public void WithDataMethodsValidateArguments() + { + IResourceBuilder posta = null!; + + Assert.Throws(() => posta.WithDataVolume()); + Assert.Throws(() => posta.WithDataBindMount("./posta-data")); + + var builder = DistributedApplication.CreateBuilder(); + var (database, redis) = AddPostaDependencies(builder); + posta = builder.AddPosta("posta", database, redis); + + Assert.Throws(() => posta.WithDataBindMount(null!)); + } + [Fact] public void WithGroupedOptionCallbacksThrowWhenConfigureOptionsIsNull() { From c642130493a1a82601e4fea569b2ae3b15c28753 Mon Sep 17 00:00:00 2001 From: axi Date: Thu, 23 Jul 2026 22:45:47 +0300 Subject: [PATCH 28/28] Update Posta container image to 0.13.0 --- .../PostaContainerImageTags.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/CommunityToolkit.Aspire.Hosting.Posta/PostaContainerImageTags.cs b/src/CommunityToolkit.Aspire.Hosting.Posta/PostaContainerImageTags.cs index 8c5478d1e..557a66428 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Posta/PostaContainerImageTags.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Posta/PostaContainerImageTags.cs @@ -8,6 +8,6 @@ internal static class PostaContainerImageTags /// jkaninda/posta public const string Image = "jkaninda/posta"; - /// 0.11.0 - public const string Tag = "0.11.0"; -} \ No newline at end of file + /// 0.13.0 + public const string Tag = "0.13.0"; +}