diff --git a/CommunityToolkit.Aspire.slnx b/CommunityToolkit.Aspire.slnx
index a58bb4ef6..a0f60757e 100644
--- a/CommunityToolkit.Aspire.slnx
+++ b/CommunityToolkit.Aspire.slnx
@@ -159,6 +159,10 @@
+
+
+
+
@@ -274,6 +278,7 @@
+
@@ -300,6 +305,7 @@
+
@@ -350,6 +356,7 @@
+
@@ -375,6 +382,7 @@
+
diff --git a/README.md b/README.md
index 7d3a49d85..e6ab8f9c4 100644
--- a/README.md
+++ b/README.md
@@ -31,6 +31,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. |
@@ -184,6 +185,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.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/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..724788641
--- /dev/null
+++ b/examples/posta/CommunityToolkit.Aspire.Hosting.Posta.AppHost/CommunityToolkit.Aspire.Hosting.Posta.AppHost.csproj
@@ -0,0 +1,18 @@
+
+
+
+ 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..f16d08abf
--- /dev/null
+++ b/examples/posta/CommunityToolkit.Aspire.Hosting.Posta.AppHost/Program.cs
@@ -0,0 +1,11 @@
+var builder = DistributedApplication.CreateBuilder(args);
+
+var postgresPassword = builder.AddParameter("postgres-password", "posta");
+var postgres = builder.AddPostgres("postgres", password: postgresPassword);
+var database = postgres.AddDatabase("posta-db", "posta");
+var redis = builder.AddRedis("redis");
+
+builder.AddPosta("posta", database, redis)
+ .WithDataVolume();
+
+builder.Build().Run();
diff --git a/examples/posta/CommunityToolkit.Aspire.Hosting.Posta.AppHost/Properties/launchSettings.json b/examples/posta/CommunityToolkit.Aspire.Hosting.Posta.AppHost/Properties/launchSettings.json
new file mode 100644
index 000000000..0a6a02a6a
--- /dev/null
+++ b/examples/posta/CommunityToolkit.Aspire.Hosting.Posta.AppHost/Properties/launchSettings.json
@@ -0,0 +1,29 @@
+{
+ "$schema": "https://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "https": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "https://localhost:18131;http://localhost:12049",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "DOTNET_ENVIRONMENT": "Development",
+ "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:32487",
+ "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:12811"
+ }
+ },
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "http://localhost:15184",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "DOTNET_ENVIRONMENT": "Development",
+ "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19198",
+ "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:21263"
+ }
+ }
+ }
+}
diff --git a/src/CommunityToolkit.Aspire.Hosting.Posta/CommunityToolkit.Aspire.Hosting.Posta.csproj b/src/CommunityToolkit.Aspire.Hosting.Posta/CommunityToolkit.Aspire.Hosting.Posta.csproj
new file mode 100644
index 000000000..cc997daa8
--- /dev/null
+++ b/src/CommunityToolkit.Aspire.Hosting.Posta/CommunityToolkit.Aspire.Hosting.Posta.csproj
@@ -0,0 +1,19 @@
+
+
+
+ An Aspire hosting integration for Posta.
+ hosting posta email smtp delivery
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/CommunityToolkit.Aspire.Hosting.Posta/PostaContainerImageTags.cs b/src/CommunityToolkit.Aspire.Hosting.Posta/PostaContainerImageTags.cs
new file mode 100644
index 000000000..557a66428
--- /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.13.0
+ public const string Tag = "0.13.0";
+}
diff --git a/src/CommunityToolkit.Aspire.Hosting.Posta/PostaEmailVerificationOptions.cs b/src/CommunityToolkit.Aspire.Hosting.Posta/PostaEmailVerificationOptions.cs
new file mode 100644
index 000000000..41b7d0ebc
--- /dev/null
+++ b/src/CommunityToolkit.Aspire.Hosting.Posta/PostaEmailVerificationOptions.cs
@@ -0,0 +1,39 @@
+using Aspire.Hosting.ApplicationModel;
+
+namespace CommunityToolkit.Aspire.Hosting.Posta;
+
+///
+/// Parameter-based configuration options for Posta email verification.
+///
+public sealed class PostaEmailVerificationOptions
+{
+ ///
+ /// Gets or sets whether users must verify their email address before sign-in.
+ ///
+ public IResourceBuilder? Required { get; set; }
+
+ ///
+ /// Gets or sets whether permanently rejected recipients are added to the suppression list.
+ ///
+ public IResourceBuilder? AutoSuppressOnReject { get; set; }
+
+ ///
+ /// Gets or sets whether the email verification endpoint is enabled.
+ ///
+ public IResourceBuilder? Enabled { get; set; }
+
+ ///
+ /// Gets or sets how long address-level verification results are cached, in hours.
+ ///
+ public IResourceBuilder? CacheTtlHours { get; set; }
+
+ ///
+ /// Gets or sets how long domain MX lookups are cached, in hours.
+ ///
+ public IResourceBuilder? MxCacheTtlHours { get; set; }
+
+ ///
+ /// Gets or sets the per-user hourly cap on verification requests.
+ ///
+ public IResourceBuilder? RateHourly { get; set; }
+}
diff --git a/src/CommunityToolkit.Aspire.Hosting.Posta/PostaEnvironmentVariables.cs b/src/CommunityToolkit.Aspire.Hosting.Posta/PostaEnvironmentVariables.cs
new file mode 100644
index 000000000..24491fb0a
--- /dev/null
+++ b/src/CommunityToolkit.Aspire.Hosting.Posta/PostaEnvironmentVariables.cs
@@ -0,0 +1,74 @@
+namespace CommunityToolkit.Aspire.Hosting.Posta;
+
+internal static class PostaEnvironmentVariables
+{
+ public const string Port = "POSTA_PORT";
+ public const string DbUrl = "POSTA_DB_URL";
+ public const string DbHost = "POSTA_DB_HOST";
+ public const string DbPort = "POSTA_DB_PORT";
+ public const string DbUser = "POSTA_DB_USER";
+ public const string DbPassword = "POSTA_DB_PASSWORD";
+ public const string DbName = "POSTA_DB_NAME";
+ public const string DbSslMode = "POSTA_DB_SSL_MODE";
+ public const string RedisAddress = "POSTA_REDIS_ADDR";
+ public const string RedisPassword = "POSTA_REDIS_PASSWORD";
+ public const string JwtSecret = "POSTA_JWT_SECRET";
+ public const string AdminEmail = "POSTA_ADMIN_EMAIL";
+ public const string AdminPassword = "POSTA_ADMIN_PASSWORD";
+ public const string Environment = "POSTA_ENV";
+ public const string DevMode = "POSTA_DEV_MODE";
+ public const string AuthRateLimitEnabled = "POSTA_AUTH_RATE_LIMIT_ENABLED";
+ public const string RateLimitHourly = "POSTA_RATE_LIMIT_HOURLY";
+ public const string RateLimitDaily = "POSTA_RATE_LIMIT_DAILY";
+ public const string OpenApiDocs = "POSTA_OPENAPI_DOCS";
+ public const string MetricsEnabled = "POSTA_METRICS_ENABLED";
+ public const string WebDir = "POSTA_WEB_DIR";
+ public const string WebUrl = "POSTA_WEB_URL";
+ public const string ApiUrl = "POSTA_API_URL";
+ public const string CorsOrigins = "POSTA_CORS_ORIGINS";
+ public const string EmbeddedWorker = "POSTA_EMBEDDED_WORKER";
+ public const string WorkerConcurrency = "POSTA_WORKER_CONCURRENCY";
+ public const string WorkerMaxRetries = "POSTA_WORKER_MAX_RETRIES";
+ public const string WebhookMaxRetries = "POSTA_WEBHOOK_MAX_RETRIES";
+ public const string WebhookTimeoutSeconds = "POSTA_WEBHOOK_TIMEOUT_SECS";
+ public const string WebhookProxyUrl = "POSTA_WEBHOOK_PROXY_URL";
+ public const string GoogleOAuthClientId = "POSTA_GOOGLE_OAUTH_CLIENT_ID";
+ public const string GoogleOAuthClientSecret = "POSTA_GOOGLE_OAUTH_CLIENT_SECRET";
+ public const string OAuthCallbackUrl = "POSTA_OAUTH_CALLBACK_URL";
+ public const string BlobProvider = "POSTA_BLOB_PROVIDER";
+ public const string BlobS3Endpoint = "POSTA_BLOB_S3_ENDPOINT";
+ public const string BlobS3Region = "POSTA_BLOB_S3_REGION";
+ public const string BlobS3Bucket = "POSTA_BLOB_S3_BUCKET";
+ public const string BlobS3AccessKey = "POSTA_BLOB_S3_ACCESS_KEY";
+ public const string BlobS3SecretKey = "POSTA_BLOB_S3_SECRET_KEY";
+ public const string BlobS3UseSsl = "POSTA_BLOB_S3_USE_SSL";
+ public const string BlobS3PathStyle = "POSTA_BLOB_S3_PATH_STYLE";
+ public const string BlobFileSystemPath = "POSTA_BLOB_FS_PATH";
+ public const string EncryptionKey = "POSTA_ENCRYPTION_KEY";
+ public const string SystemSmtpHost = "POSTA_SYSTEM_SMTP_HOST";
+ public const string SystemSmtpPort = "POSTA_SYSTEM_SMTP_PORT";
+ public const string SystemSmtpUsername = "POSTA_SYSTEM_SMTP_USERNAME";
+ public const string SystemSmtpPassword = "POSTA_SYSTEM_SMTP_PASSWORD";
+ public const string SystemSmtpFrom = "POSTA_SYSTEM_SMTP_FROM";
+ public const string SystemSmtpEncryption = "POSTA_SYSTEM_SMTP_ENCRYPTION";
+ public const string InboundEnabled = "POSTA_INBOUND_ENABLED";
+ public const string InboundSmtpHost = "POSTA_INBOUND_SMTP_HOST";
+ public const string InboundSmtpPort = "POSTA_INBOUND_SMTP_PORT";
+ public const string InboundHostname = "POSTA_INBOUND_HOSTNAME";
+ public const string InboundMaxMessageSize = "POSTA_INBOUND_MAX_MESSAGE_SIZE";
+ public const string InboundMaxAttachSize = "POSTA_INBOUND_MAX_ATTACH_SIZE";
+ public const string InboundWebhookSecret = "POSTA_INBOUND_WEBHOOK_SECRET";
+ public const string InboundTlsMode = "POSTA_INBOUND_TLS_MODE";
+ public const string InboundTlsCertFile = "POSTA_INBOUND_TLS_CERT_FILE";
+ public const string InboundTlsKeyFile = "POSTA_INBOUND_TLS_KEY_FILE";
+ public const string InboundSmtpRateLimit = "POSTA_INBOUND_SMTP_RATE_LIMIT";
+ public const string InboundSmtpRateWindow = "POSTA_INBOUND_SMTP_RATE_WINDOW";
+ public const string EmailVerificationRequired = "POSTA_EMAIL_VERIFICATION_REQUIRED";
+ public const string AutoSuppressOnReject = "POSTA_AUTO_SUPPRESS_ON_REJECT";
+ public const string EmailVerifyEnabled = "POSTA_EMAIL_VERIFY_ENABLED";
+ public const string EmailVerifyCacheTtlHours = "POSTA_EMAIL_VERIFY_CACHE_TTL_HOURS";
+ public const string EmailVerifyMxCacheTtlHours = "POSTA_EMAIL_VERIFY_MX_CACHE_TTL_HOURS";
+ public const string EmailVerifyRateHourly = "POSTA_EMAIL_VERIFY_RATE_HOURLY";
+ public const string AllowDowngrade = "POSTA_ALLOW_DOWNGRADE";
+ public const string PlanEnforcement = "POSTA_PLAN_ENFORCEMENT";
+}
diff --git a/src/CommunityToolkit.Aspire.Hosting.Posta/PostaGoogleOAuthOptions.cs b/src/CommunityToolkit.Aspire.Hosting.Posta/PostaGoogleOAuthOptions.cs
new file mode 100644
index 000000000..f6067e293
--- /dev/null
+++ b/src/CommunityToolkit.Aspire.Hosting.Posta/PostaGoogleOAuthOptions.cs
@@ -0,0 +1,24 @@
+using Aspire.Hosting.ApplicationModel;
+
+namespace CommunityToolkit.Aspire.Hosting.Posta;
+
+///
+/// Parameter-based configuration options for Posta Google OAuth login.
+///
+public sealed class PostaGoogleOAuthOptions
+{
+ ///
+ /// Gets or sets the Google OAuth client ID for SSO login.
+ ///
+ public IResourceBuilder? ClientId { get; set; }
+
+ ///
+ /// Gets or sets the Google OAuth client secret for SSO login.
+ ///
+ public IResourceBuilder? ClientSecret { get; set; }
+
+ ///
+ /// Gets or sets the OAuth callback base URL.
+ ///
+ public IResourceBuilder? CallbackUrl { get; set; }
+}
diff --git a/src/CommunityToolkit.Aspire.Hosting.Posta/PostaHostingExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Posta/PostaHostingExtensions.cs
new file mode 100644
index 000000000..c340c346f
--- /dev/null
+++ b/src/CommunityToolkit.Aspire.Hosting.Posta/PostaHostingExtensions.cs
@@ -0,0 +1,654 @@
+using Aspire.Hosting.ApplicationModel;
+using Aspire.Hosting.Utils;
+using CommunityToolkit.Aspire.Hosting.Posta;
+
+namespace Aspire.Hosting;
+
+///
+/// Provides extension methods for adding Posta to an .
+///
+public static class PostaHostingExtensions
+{
+ private const string DataContainerPath = "/data";
+
+ ///
+ /// Adds a Posta container resource to the and configures PostgreSQL and Redis references.
+ ///
+ /// Adds a Posta container resource with PostgreSQL and Redis references
+ /// The to which the Posta resource will be added.
+ /// The name of the Posta container resource.
+ /// The PostgreSQL database resource used by Posta.
+ /// The Redis resource used by Posta.
+ /// Optional parameter used as the Posta JWT signing secret.
+ /// Optional parameter used as the initial Posta admin password.
+ /// The initial Posta admin account email.
+ /// Optional host port for the Posta HTTP API and dashboard.
+ /// A reference to the for further resource configuration.
+ [AspireExport("addPostaWithReferences", MethodName = "addPostaWithReferences")]
+ public static IResourceBuilder AddPosta(
+ this IDistributedApplicationBuilder builder,
+ [ResourceName] string name,
+ IResourceBuilder database,
+ IResourceBuilder redis,
+ IResourceBuilder? jwtSecret = null,
+ IResourceBuilder? adminPassword = null,
+ string adminEmail = "admin@example.com",
+ int? port = null)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+ ArgumentNullException.ThrowIfNull(database);
+ ArgumentNullException.ThrowIfNull(redis);
+
+ return AddPostaCore(builder, name, jwtSecret, adminPassword, adminEmail, port, options: null)
+ .WithReference(database)
+ .WithReference(redis);
+ }
+
+ ///
+ /// Adds a Posta container resource to the with PostgreSQL, Redis, and additional Posta environment configuration.
+ ///
+ /// The to which the Posta resource will be added.
+ /// The name of the Posta container resource.
+ /// The PostgreSQL database resource used by Posta.
+ /// The Redis resource used by Posta.
+ /// The Posta environment configuration.
+ /// Optional parameter used as the Posta JWT signing secret.
+ /// Optional parameter used as the initial Posta admin password.
+ /// The initial Posta admin account email.
+ /// Optional host port for the Posta HTTP API and dashboard.
+ /// A reference to the for further resource configuration.
+ [AspireExportIgnore(Reason = "PostaOptions contains parameter builders for secret values and is not supported in polyglot app hosts.")]
+ public static IResourceBuilder AddPosta(
+ this IDistributedApplicationBuilder builder,
+ [ResourceName] string name,
+ IResourceBuilder database,
+ IResourceBuilder redis,
+ PostaOptions options,
+ IResourceBuilder? jwtSecret = null,
+ IResourceBuilder? adminPassword = null,
+ string adminEmail = "admin@example.com",
+ int? port = null)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+ ArgumentNullException.ThrowIfNull(database);
+ ArgumentNullException.ThrowIfNull(redis);
+ ArgumentNullException.ThrowIfNull(options);
+
+ return AddPostaCore(builder, name, jwtSecret, adminPassword, adminEmail, port, options)
+ .WithReference(database)
+ .WithReference(redis);
+ }
+
+ ///
+ /// Adds a Posta container resource to the with PostgreSQL, Redis, and additional Posta environment configuration.
+ ///
+ /// The to which the Posta resource will be added.
+ /// The name of the Posta container resource.
+ /// The PostgreSQL database resource used by Posta.
+ /// The Redis resource used by Posta.
+ /// A delegate that configures Posta environment variables.
+ /// Optional parameter used as the Posta JWT signing secret.
+ /// Optional parameter used as the initial Posta admin password.
+ /// The initial Posta admin account email.
+ /// Optional host port for the Posta HTTP API and dashboard.
+ /// A reference to the for further resource configuration.
+ [AspireExportIgnore(Reason = "Action is not supported in polyglot app hosts. Use the options object overload instead.")]
+ public static IResourceBuilder AddPosta(
+ this IDistributedApplicationBuilder builder,
+ [ResourceName] string name,
+ IResourceBuilder database,
+ IResourceBuilder redis,
+ Action configureOptions,
+ IResourceBuilder? jwtSecret = null,
+ IResourceBuilder? adminPassword = null,
+ string adminEmail = "admin@example.com",
+ int? port = null)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+ ArgumentNullException.ThrowIfNull(database);
+ ArgumentNullException.ThrowIfNull(redis);
+ ArgumentNullException.ThrowIfNull(configureOptions);
+
+ var options = new PostaOptions();
+ configureOptions.Invoke(options);
+
+ return builder.AddPosta(name, database, redis, options, jwtSecret, adminPassword, adminEmail, port);
+ }
+
+ ///
+ /// 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.
+ ///
+ /// The Posta resource builder.
+ /// The PostgreSQL database resource used by Posta.
+ /// A reference to the for further resource configuration.
+ [AspireExport("withPostgresReference", MethodName = "withPostgresReference")]
+ public static IResourceBuilder WithReference(
+ this IResourceBuilder builder,
+ IResourceBuilder database)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+ ArgumentNullException.ThrowIfNull(database);
+
+ return builder
+ .WithEnvironment(context =>
+ {
+ var postgres = database.Resource.Parent;
+
+ Set(context, PostaEnvironmentVariables.DbHost, postgres.PrimaryEndpoint.Property(EndpointProperty.Host));
+ Set(context, PostaEnvironmentVariables.DbPort, postgres.PrimaryEndpoint.Property(EndpointProperty.Port));
+ Set(context, PostaEnvironmentVariables.DbUser, postgres.UserNameReference);
+ Set(context, PostaEnvironmentVariables.DbPassword, postgres.PasswordParameter);
+ Set(context, PostaEnvironmentVariables.DbName, database.Resource.DatabaseName);
+ Set(context, PostaEnvironmentVariables.DbSslMode, "disable");
+ })
+ .WaitFor(database);
+ }
+
+ ///
+ /// Configures the Redis server used by Posta.
+ ///
+ /// The Posta resource builder.
+ /// The Redis resource used by Posta.
+ /// Optional Redis password parameter that overrides the referenced Redis resource password.
+ /// A reference to the for further resource configuration.
+ [AspireExport("withRedisReference", MethodName = "withRedisReference")]
+ public static IResourceBuilder WithReference(
+ this IResourceBuilder builder,
+ IResourceBuilder redis,
+ IResourceBuilder? redisPassword = null)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+ ArgumentNullException.ThrowIfNull(redis);
+
+ return builder
+ .WithEnvironment(context =>
+ {
+ var redisResource = redis.Resource;
+ var redisEndpoint = redisResource.GetEndpoint("secondary");
+
+ SetIfAbsent(context, PostaEnvironmentVariables.RedisAddress, ReferenceExpression.Create($"{redisEndpoint.Property(EndpointProperty.Host)}:{redisEndpoint.Property(EndpointProperty.Port)}"));
+ if (redisPassword is not null)
+ {
+ Set(context, PostaEnvironmentVariables.RedisPassword, redisPassword.Resource);
+ }
+ else if (redisResource.PasswordParameter is not null)
+ {
+ SetIfAbsent(context, PostaEnvironmentVariables.RedisPassword, redisResource.PasswordParameter);
+ }
+ })
+ .WaitFor(redis);
+ }
+
+ ///
+ /// Configures the system SMTP server used by Posta for platform notifications.
+ ///
+ /// The Posta resource builder.
+ /// The parameter-based system SMTP configuration.
+ /// A reference to the for further resource configuration.
+ [AspireExportIgnore(Reason = "PostaSystemSmtpOptions contains parameter builders and is intended for C# AppHosts.")]
+ public static IResourceBuilder WithSystemSmtp(
+ this IResourceBuilder builder,
+ PostaSystemSmtpOptions options)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+ ArgumentNullException.ThrowIfNull(options);
+
+ return builder.WithEnvironment(context => ConfigureSystemSmtp(context, options));
+ }
+
+ ///
+ /// Configures the system SMTP server used by Posta for platform notifications.
+ ///
+ /// The Posta resource builder.
+ /// A delegate that configures the parameter-based system SMTP options.
+ /// A reference to the for further resource configuration.
+ [AspireExportIgnore(Reason = "Action is not supported in polyglot app hosts. Use the options object overload instead.")]
+ public static IResourceBuilder WithSystemSmtp(
+ this IResourceBuilder builder,
+ Action configureOptions)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+ ArgumentNullException.ThrowIfNull(configureOptions);
+
+ var options = new PostaSystemSmtpOptions();
+ configureOptions.Invoke(options);
+
+ return builder.WithSystemSmtp(options);
+ }
+
+ ///
+ /// Configures the built-in inbound SMTP receiver used by Posta.
+ ///
+ /// The Posta resource builder.
+ /// The parameter-based inbound SMTP configuration.
+ /// A reference to the for further resource configuration.
+ [AspireExportIgnore(Reason = "PostaInboundSmtpOptions contains parameter builders and is intended for C# AppHosts.")]
+ public static IResourceBuilder WithInboundSmtp(
+ this IResourceBuilder builder,
+ PostaInboundSmtpOptions options)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+ ArgumentNullException.ThrowIfNull(options);
+
+ return builder.WithEnvironment(context => ConfigureInboundSmtp(context, options));
+ }
+
+ ///
+ /// Configures the built-in inbound SMTP receiver used by Posta.
+ ///
+ /// The Posta resource builder.
+ /// A delegate that configures the parameter-based inbound SMTP options.
+ /// A reference to the for further resource configuration.
+ [AspireExportIgnore(Reason = "Action is not supported in polyglot app hosts. Use the options object overload instead.")]
+ public static IResourceBuilder WithInboundSmtp(
+ this IResourceBuilder builder,
+ Action configureOptions)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+ ArgumentNullException.ThrowIfNull(configureOptions);
+
+ var options = new PostaInboundSmtpOptions();
+ configureOptions.Invoke(options);
+
+ return builder.WithInboundSmtp(options);
+ }
+
+ ///
+ /// Configures S3-compatible attachment storage for Posta.
+ ///
+ /// The Posta resource builder.
+ /// The parameter-based S3 blob storage configuration.
+ /// A reference to the for further resource configuration.
+ [AspireExportIgnore(Reason = "PostaS3BlobStorageOptions contains parameter builders and is intended for C# AppHosts.")]
+ public static IResourceBuilder WithS3BlobStorage(
+ this IResourceBuilder builder,
+ PostaS3BlobStorageOptions options)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+ ArgumentNullException.ThrowIfNull(options);
+
+ return builder.WithEnvironment(context => ConfigureS3BlobStorage(context, options));
+ }
+
+ ///
+ /// Configures S3-compatible attachment storage for Posta.
+ ///
+ /// The Posta resource builder.
+ /// A delegate that configures the parameter-based S3 blob storage options.
+ /// A reference to the for further resource configuration.
+ [AspireExportIgnore(Reason = "Action is not supported in polyglot app hosts. Use the options object overload instead.")]
+ public static IResourceBuilder WithS3BlobStorage(
+ this IResourceBuilder builder,
+ Action configureOptions)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+ ArgumentNullException.ThrowIfNull(configureOptions);
+
+ var options = new PostaS3BlobStorageOptions();
+ configureOptions.Invoke(options);
+
+ return builder.WithS3BlobStorage(options);
+ }
+
+ ///
+ /// Configures Google OAuth login for Posta.
+ ///
+ /// The Posta resource builder.
+ /// The parameter-based Google OAuth configuration.
+ /// A reference to the for further resource configuration.
+ [AspireExportIgnore(Reason = "PostaGoogleOAuthOptions contains parameter builders and is intended for C# AppHosts.")]
+ public static IResourceBuilder WithGoogleOAuth(
+ this IResourceBuilder builder,
+ PostaGoogleOAuthOptions options)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+ ArgumentNullException.ThrowIfNull(options);
+
+ return builder.WithEnvironment(context => ConfigureGoogleOAuth(context, options));
+ }
+
+ ///
+ /// Configures Google OAuth login for Posta.
+ ///
+ /// The Posta resource builder.
+ /// A delegate that configures the parameter-based Google OAuth options.
+ /// A reference to the for further resource configuration.
+ [AspireExportIgnore(Reason = "Action is not supported in polyglot app hosts. Use the options object overload instead.")]
+ public static IResourceBuilder WithGoogleOAuth(
+ this IResourceBuilder builder,
+ Action configureOptions)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+ ArgumentNullException.ThrowIfNull(configureOptions);
+
+ var options = new PostaGoogleOAuthOptions();
+ configureOptions.Invoke(options);
+
+ return builder.WithGoogleOAuth(options);
+ }
+
+ ///
+ /// Configures email verification behavior for Posta.
+ ///
+ /// The Posta resource builder.
+ /// The parameter-based email verification configuration.
+ /// A reference to the for further resource configuration.
+ [AspireExportIgnore(Reason = "PostaEmailVerificationOptions contains parameter builders and is intended for C# AppHosts.")]
+ public static IResourceBuilder WithEmailVerification(
+ this IResourceBuilder builder,
+ PostaEmailVerificationOptions options)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+ ArgumentNullException.ThrowIfNull(options);
+
+ return builder.WithEnvironment(context => ConfigureEmailVerification(context, options));
+ }
+
+ ///
+ /// Configures email verification behavior for Posta.
+ ///
+ /// The Posta resource builder.
+ /// A delegate that configures the parameter-based email verification options.
+ /// A reference to the for further resource configuration.
+ [AspireExportIgnore(Reason = "Action is not supported in polyglot app hosts. Use the options object overload instead.")]
+ public static IResourceBuilder WithEmailVerification(
+ this IResourceBuilder builder,
+ Action configureOptions)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+ ArgumentNullException.ThrowIfNull(configureOptions);
+
+ var options = new PostaEmailVerificationOptions();
+ configureOptions.Invoke(options);
+
+ return builder.WithEmailVerification(options);
+ }
+
+ private static IResourceBuilder AddPostaCore(
+ IDistributedApplicationBuilder builder,
+ string name,
+ IResourceBuilder? jwtSecret,
+ IResourceBuilder? adminPassword,
+ string adminEmail,
+ int? port,
+ PostaOptions? options)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+ ArgumentException.ThrowIfNullOrWhiteSpace(name);
+ ArgumentException.ThrowIfNullOrWhiteSpace(adminEmail);
+
+ options ??= new PostaOptions();
+
+ var jwtSecretParameter = jwtSecret?.Resource ??
+ ParameterResourceBuilderExtensions.CreateDefaultPasswordParameter(builder, $"{name}-jwt-secret", minLower: 1, minUpper: 1, minNumeric: 1);
+ var adminPasswordParameter = adminPassword?.Resource ??
+ ParameterResourceBuilderExtensions.CreateDefaultPasswordParameter(builder, $"{name}-admin-password", minLower: 1, minUpper: 1, minNumeric: 1);
+
+ var resource = new PostaResource(name, jwtSecretParameter, adminPasswordParameter);
+
+ return builder.AddResource(resource)
+ .WithImage(PostaContainerImageTags.Image)
+ .WithImageTag(PostaContainerImageTags.Tag)
+ .WithImageRegistry(PostaContainerImageTags.Registry)
+ .WithHttpEndpoint(
+ targetPort: PostaResource.HttpEndpointPort,
+ port: port,
+ name: PostaResource.HttpEndpointName)
+ .WithEnvironment(context =>
+ {
+ Set(context, PostaEnvironmentVariables.Port, PostaResource.HttpEndpointPort);
+ SetParameter(context, PostaEnvironmentVariables.DbUrl, options.DatabaseUrl);
+ SetIfNotNull(context, PostaEnvironmentVariables.RedisAddress, options.RedisAddress);
+ if (options.RedisPassword is not null)
+ {
+ Set(context, PostaEnvironmentVariables.RedisPassword, options.RedisPassword.Resource);
+ }
+ Set(context, PostaEnvironmentVariables.JwtSecret, resource.JwtSecretParameter);
+ Set(context, PostaEnvironmentVariables.AdminEmail, adminEmail);
+ Set(context, PostaEnvironmentVariables.AdminPassword, resource.AdminPasswordParameter);
+ ConfigurePostaEnvironment(context, options);
+ })
+ .WithHttpHealthCheck(
+ path: "/healthz",
+ statusCode: 200,
+ endpointName: PostaResource.HttpEndpointName)
+ .WithHttpHealthCheck(
+ path: "/readyz",
+ statusCode: 200,
+ endpointName: PostaResource.HttpEndpointName)
+ .WithUrlForEndpoint(PostaResource.HttpEndpointName, url => url.DisplayText = "Posta Dashboard");
+ }
+
+ private static void ConfigurePostaEnvironment(EnvironmentCallbackContext context, PostaOptions options)
+ {
+ Set(context, PostaEnvironmentVariables.Environment, options.Environment);
+ Set(context, PostaEnvironmentVariables.DevMode, options.DevMode);
+ Set(context, PostaEnvironmentVariables.AuthRateLimitEnabled, options.AuthRateLimitEnabled);
+ Set(context, PostaEnvironmentVariables.RateLimitHourly, options.RateLimitHourly);
+ Set(context, PostaEnvironmentVariables.RateLimitDaily, options.RateLimitDaily);
+ Set(context, PostaEnvironmentVariables.OpenApiDocs, options.OpenApiDocs);
+ Set(context, PostaEnvironmentVariables.MetricsEnabled, options.MetricsEnabled);
+ SetIfNotNull(context, PostaEnvironmentVariables.WebDir, options.WebDir);
+ SetIfNotNull(context, PostaEnvironmentVariables.WebUrl, options.WebUrl);
+ SetIfNotNull(context, PostaEnvironmentVariables.ApiUrl, options.ApiUrl);
+ Set(context, PostaEnvironmentVariables.CorsOrigins, options.CorsOrigins);
+ Set(context, PostaEnvironmentVariables.EmbeddedWorker, options.EmbeddedWorker);
+ Set(context, PostaEnvironmentVariables.WorkerConcurrency, options.WorkerConcurrency);
+ Set(context, PostaEnvironmentVariables.WorkerMaxRetries, options.WorkerMaxRetries);
+ Set(context, PostaEnvironmentVariables.WebhookMaxRetries, options.WebhookMaxRetries);
+ Set(context, PostaEnvironmentVariables.WebhookTimeoutSeconds, options.WebhookTimeoutSeconds);
+ SetIfNotNull(context, PostaEnvironmentVariables.WebhookProxyUrl, options.WebhookProxyUrl);
+ ConfigureGoogleOAuth(context, options);
+ ConfigureBlobStorage(context, options);
+ SetParameter(context, PostaEnvironmentVariables.EncryptionKey, options.EncryptionKey);
+ ConfigureSystemSmtp(context, options);
+ ConfigureInboundSmtp(context, options);
+ ConfigureEmailVerification(context, options);
+ Set(context, PostaEnvironmentVariables.AllowDowngrade, options.AllowDowngrade);
+ Set(context, PostaEnvironmentVariables.PlanEnforcement, options.PlanEnforcement);
+ }
+
+ private static void ConfigureGoogleOAuth(EnvironmentCallbackContext context, PostaOptions options)
+ {
+ SetIfNotNull(context, PostaEnvironmentVariables.GoogleOAuthClientId, options.GoogleOAuthClientId);
+ SetParameter(context, PostaEnvironmentVariables.GoogleOAuthClientSecret, options.GoogleOAuthClientSecret);
+ SetIfNotNull(context, PostaEnvironmentVariables.OAuthCallbackUrl, options.OAuthCallbackUrl);
+ }
+
+ private static void ConfigureGoogleOAuth(EnvironmentCallbackContext context, PostaGoogleOAuthOptions options)
+ {
+ SetParameter(context, PostaEnvironmentVariables.GoogleOAuthClientId, options.ClientId);
+ SetParameter(context, PostaEnvironmentVariables.GoogleOAuthClientSecret, options.ClientSecret);
+ SetParameter(context, PostaEnvironmentVariables.OAuthCallbackUrl, options.CallbackUrl);
+ }
+
+ private static void ConfigureBlobStorage(EnvironmentCallbackContext context, PostaOptions options)
+ {
+ SetIfNotNull(context, PostaEnvironmentVariables.BlobProvider, options.BlobProvider);
+ SetIfNotNull(context, PostaEnvironmentVariables.BlobS3Endpoint, options.BlobS3Endpoint);
+ Set(context, PostaEnvironmentVariables.BlobS3Region, options.BlobS3Region);
+ SetIfNotNull(context, PostaEnvironmentVariables.BlobS3Bucket, options.BlobS3Bucket);
+ SetParameter(context, PostaEnvironmentVariables.BlobS3AccessKey, options.BlobS3AccessKey);
+ SetParameter(context, PostaEnvironmentVariables.BlobS3SecretKey, options.BlobS3SecretKey);
+ Set(context, PostaEnvironmentVariables.BlobS3UseSsl, options.BlobS3UseSsl);
+ Set(context, PostaEnvironmentVariables.BlobS3PathStyle, options.BlobS3PathStyle);
+ Set(context, PostaEnvironmentVariables.BlobFileSystemPath, options.BlobFileSystemPath);
+ }
+
+ private static void ConfigureS3BlobStorage(EnvironmentCallbackContext context, PostaS3BlobStorageOptions options)
+ {
+ Set(context, PostaEnvironmentVariables.BlobProvider, "s3");
+ SetParameter(context, PostaEnvironmentVariables.BlobS3Endpoint, options.Endpoint);
+ SetParameter(context, PostaEnvironmentVariables.BlobS3Region, options.Region);
+ SetParameter(context, PostaEnvironmentVariables.BlobS3Bucket, options.Bucket);
+ SetParameter(context, PostaEnvironmentVariables.BlobS3AccessKey, options.AccessKey);
+ SetParameter(context, PostaEnvironmentVariables.BlobS3SecretKey, options.SecretKey);
+ SetParameter(context, PostaEnvironmentVariables.BlobS3UseSsl, options.UseSsl);
+ SetParameter(context, PostaEnvironmentVariables.BlobS3PathStyle, options.PathStyle);
+ }
+
+ private static void ConfigureSystemSmtp(EnvironmentCallbackContext context, PostaOptions options)
+ {
+ SetIfNotNull(context, PostaEnvironmentVariables.SystemSmtpHost, options.SystemSmtpHost);
+ Set(context, PostaEnvironmentVariables.SystemSmtpPort, options.SystemSmtpPort);
+ SetIfNotNull(context, PostaEnvironmentVariables.SystemSmtpUsername, options.SystemSmtpUsername);
+ SetParameter(context, PostaEnvironmentVariables.SystemSmtpPassword, options.SystemSmtpPassword);
+ SetIfNotNull(context, PostaEnvironmentVariables.SystemSmtpFrom, options.SystemSmtpFrom);
+ Set(context, PostaEnvironmentVariables.SystemSmtpEncryption, options.SystemSmtpEncryption);
+ }
+
+ private static void ConfigureSystemSmtp(EnvironmentCallbackContext context, PostaSystemSmtpOptions options)
+ {
+ SetParameter(context, PostaEnvironmentVariables.SystemSmtpHost, options.Host);
+ SetParameter(context, PostaEnvironmentVariables.SystemSmtpPort, options.Port);
+ SetParameter(context, PostaEnvironmentVariables.SystemSmtpUsername, options.Username);
+ SetParameter(context, PostaEnvironmentVariables.SystemSmtpPassword, options.Password);
+ SetParameter(context, PostaEnvironmentVariables.SystemSmtpFrom, options.From);
+ SetParameter(context, PostaEnvironmentVariables.SystemSmtpEncryption, options.Encryption);
+ }
+
+ private static void ConfigureInboundSmtp(EnvironmentCallbackContext context, PostaOptions options)
+ {
+ Set(context, PostaEnvironmentVariables.InboundEnabled, options.InboundEnabled);
+ Set(context, PostaEnvironmentVariables.InboundSmtpHost, options.InboundSmtpHost);
+ Set(context, PostaEnvironmentVariables.InboundSmtpPort, options.InboundSmtpPort);
+ Set(context, PostaEnvironmentVariables.InboundHostname, options.InboundHostname);
+ Set(context, PostaEnvironmentVariables.InboundMaxMessageSize, options.InboundMaxMessageSize);
+ Set(context, PostaEnvironmentVariables.InboundMaxAttachSize, options.InboundMaxAttachSize);
+ SetParameter(context, PostaEnvironmentVariables.InboundWebhookSecret, options.InboundWebhookSecret);
+ Set(context, PostaEnvironmentVariables.InboundTlsMode, options.InboundTlsMode);
+ SetIfNotNull(context, PostaEnvironmentVariables.InboundTlsCertFile, options.InboundTlsCertFile);
+ SetIfNotNull(context, PostaEnvironmentVariables.InboundTlsKeyFile, options.InboundTlsKeyFile);
+ Set(context, PostaEnvironmentVariables.InboundSmtpRateLimit, options.InboundSmtpRateLimit);
+ Set(context, PostaEnvironmentVariables.InboundSmtpRateWindow, options.InboundSmtpRateWindow);
+ }
+
+ private static void ConfigureInboundSmtp(EnvironmentCallbackContext context, PostaInboundSmtpOptions options)
+ {
+ SetParameter(context, PostaEnvironmentVariables.InboundEnabled, options.Enabled);
+ SetParameter(context, PostaEnvironmentVariables.InboundSmtpHost, options.Host);
+ SetParameter(context, PostaEnvironmentVariables.InboundSmtpPort, options.Port);
+ SetParameter(context, PostaEnvironmentVariables.InboundHostname, options.Hostname);
+ SetParameter(context, PostaEnvironmentVariables.InboundMaxMessageSize, options.MaxMessageSize);
+ SetParameter(context, PostaEnvironmentVariables.InboundMaxAttachSize, options.MaxAttachmentSize);
+ SetParameter(context, PostaEnvironmentVariables.InboundWebhookSecret, options.WebhookSecret);
+ SetParameter(context, PostaEnvironmentVariables.InboundTlsMode, options.TlsMode);
+ SetParameter(context, PostaEnvironmentVariables.InboundTlsCertFile, options.TlsCertFile);
+ SetParameter(context, PostaEnvironmentVariables.InboundTlsKeyFile, options.TlsKeyFile);
+ SetParameter(context, PostaEnvironmentVariables.InboundSmtpRateLimit, options.RateLimit);
+ SetParameter(context, PostaEnvironmentVariables.InboundSmtpRateWindow, options.RateWindow);
+ }
+
+ private static void ConfigureEmailVerification(EnvironmentCallbackContext context, PostaOptions options)
+ {
+ Set(context, PostaEnvironmentVariables.EmailVerificationRequired, options.EmailVerificationRequired);
+ Set(context, PostaEnvironmentVariables.AutoSuppressOnReject, options.AutoSuppressOnReject);
+ Set(context, PostaEnvironmentVariables.EmailVerifyEnabled, options.EmailVerifyEnabled);
+ Set(context, PostaEnvironmentVariables.EmailVerifyCacheTtlHours, options.EmailVerifyCacheTtlHours);
+ Set(context, PostaEnvironmentVariables.EmailVerifyMxCacheTtlHours, options.EmailVerifyMxCacheTtlHours);
+ Set(context, PostaEnvironmentVariables.EmailVerifyRateHourly, options.EmailVerifyRateHourly);
+ }
+
+ private static void ConfigureEmailVerification(EnvironmentCallbackContext context, PostaEmailVerificationOptions options)
+ {
+ SetParameter(context, PostaEnvironmentVariables.EmailVerificationRequired, options.Required);
+ SetParameter(context, PostaEnvironmentVariables.AutoSuppressOnReject, options.AutoSuppressOnReject);
+ SetParameter(context, PostaEnvironmentVariables.EmailVerifyEnabled, options.Enabled);
+ SetParameter(context, PostaEnvironmentVariables.EmailVerifyCacheTtlHours, options.CacheTtlHours);
+ SetParameter(context, PostaEnvironmentVariables.EmailVerifyMxCacheTtlHours, options.MxCacheTtlHours);
+ SetParameter(context, PostaEnvironmentVariables.EmailVerifyRateHourly, options.RateHourly);
+ }
+
+ private static void Set(EnvironmentCallbackContext context, string name, string value)
+ {
+ context.EnvironmentVariables[name] = value;
+ }
+
+ private static void Set(EnvironmentCallbackContext context, string name, ParameterResource value)
+ {
+ context.EnvironmentVariables[name] = value;
+ }
+
+ private static void Set(EnvironmentCallbackContext context, string name, EndpointReferenceExpression value)
+ {
+ context.EnvironmentVariables[name] = value;
+ }
+
+ private static void Set(EnvironmentCallbackContext context, string name, ReferenceExpression value)
+ {
+ context.EnvironmentVariables[name] = value;
+ }
+
+ private static void SetIfAbsent(EnvironmentCallbackContext context, string name, ParameterResource value)
+ {
+ context.EnvironmentVariables.TryAdd(name, value);
+ }
+
+ private static void SetIfAbsent(EnvironmentCallbackContext context, string name, ReferenceExpression value)
+ {
+ context.EnvironmentVariables.TryAdd(name, value);
+ }
+
+ private static void Set(EnvironmentCallbackContext context, string name, bool value)
+ {
+ context.EnvironmentVariables[name] = value ? "true" : "false";
+ }
+
+ private static void Set(EnvironmentCallbackContext context, string name, int value)
+ {
+ context.EnvironmentVariables[name] = value.ToString(System.Globalization.CultureInfo.InvariantCulture);
+ }
+
+ private static void SetIfNotNull(EnvironmentCallbackContext context, string name, string? value)
+ {
+ if (!string.IsNullOrEmpty(value))
+ {
+ context.EnvironmentVariables[name] = value;
+ }
+ }
+
+ private static void SetParameter(EnvironmentCallbackContext context, string name, IResourceBuilder? parameter)
+ {
+ if (parameter is not null)
+ {
+ context.EnvironmentVariables[name] = parameter.Resource;
+ }
+ }
+}
diff --git a/src/CommunityToolkit.Aspire.Hosting.Posta/PostaInboundSmtpOptions.cs b/src/CommunityToolkit.Aspire.Hosting.Posta/PostaInboundSmtpOptions.cs
new file mode 100644
index 000000000..656238a44
--- /dev/null
+++ b/src/CommunityToolkit.Aspire.Hosting.Posta/PostaInboundSmtpOptions.cs
@@ -0,0 +1,69 @@
+using Aspire.Hosting.ApplicationModel;
+
+namespace CommunityToolkit.Aspire.Hosting.Posta;
+
+///
+/// Parameter-based configuration options for Posta inbound SMTP processing.
+///
+public sealed class PostaInboundSmtpOptions
+{
+ ///
+ /// Gets or sets whether inbound email processing is enabled.
+ ///
+ public IResourceBuilder? Enabled { get; set; }
+
+ ///
+ /// Gets or sets the bind address for the built-in SMTP receiver.
+ ///
+ public IResourceBuilder? Host { get; set; }
+
+ ///
+ /// Gets or sets the SMTP listener port.
+ ///
+ public IResourceBuilder? Port { get; set; }
+
+ ///
+ /// Gets or sets the hostname announced in EHLO and used as TLS SNI.
+ ///
+ public IResourceBuilder? Hostname { get; set; }
+
+ ///
+ /// Gets or sets the maximum raw inbound message size in bytes.
+ ///
+ public IResourceBuilder? MaxMessageSize { get; set; }
+
+ ///
+ /// Gets or sets the maximum inbound attachment size in bytes.
+ ///
+ public IResourceBuilder? MaxAttachmentSize { get; set; }
+
+ ///
+ /// Gets or sets the shared secret for inbound webhooks.
+ ///
+ public IResourceBuilder? WebhookSecret { get; set; }
+
+ ///
+ /// Gets or sets the inbound SMTP TLS mode: none or starttls.
+ ///
+ public IResourceBuilder? TlsMode { get; set; }
+
+ ///
+ /// Gets or sets the PEM certificate path for inbound TLS.
+ ///
+ public IResourceBuilder? TlsCertFile { get; set; }
+
+ ///
+ /// Gets or sets the PEM key path for inbound TLS.
+ ///
+ public IResourceBuilder? TlsKeyFile { get; set; }
+
+ ///
+ /// Gets or sets the per-IP SMTP session rate limit.
+ ///
+ public IResourceBuilder? RateLimit { get; set; }
+
+ ///
+ /// Gets or sets the SMTP rate-limit window in seconds.
+ ///
+ public IResourceBuilder? RateWindow { get; set; }
+}
diff --git a/src/CommunityToolkit.Aspire.Hosting.Posta/PostaOptions.cs b/src/CommunityToolkit.Aspire.Hosting.Posta/PostaOptions.cs
new file mode 100644
index 000000000..f174491eb
--- /dev/null
+++ b/src/CommunityToolkit.Aspire.Hosting.Posta/PostaOptions.cs
@@ -0,0 +1,304 @@
+using Aspire.Hosting.ApplicationModel;
+
+namespace CommunityToolkit.Aspire.Hosting.Posta;
+
+///
+/// Configuration options for the Posta container.
+///
+public sealed class PostaOptions
+{
+ ///
+ /// Gets or sets the Posta environment name.
+ ///
+ public string Environment { get; set; } = "production";
+
+ ///
+ /// Gets or sets a value indicating whether Posta stores emails without sending them.
+ ///
+ public bool DevMode { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether auth rate limiting is enabled.
+ ///
+ public bool AuthRateLimitEnabled { get; set; } = true;
+
+ ///
+ /// Gets or sets the maximum emails per hour per user.
+ ///
+ public int RateLimitHourly { get; set; } = 100;
+
+ ///
+ /// Gets or sets the maximum emails per day per user.
+ ///
+ public int RateLimitDaily { get; set; } = 1000;
+
+ ///
+ /// Gets or sets a value indicating whether Swagger UI and ReDoc are enabled.
+ ///
+ public bool OpenApiDocs { get; set; } = true;
+
+ ///
+ /// Gets or sets a value indicating whether Prometheus metrics are enabled.
+ ///
+ public bool MetricsEnabled { get; set; }
+
+ ///
+ /// Gets or sets the Vue dashboard build directory.
+ ///
+ public string? WebDir { get; set; }
+
+ ///
+ /// Gets or sets the public dashboard URL.
+ ///
+ public string? WebUrl { get; set; }
+
+ ///
+ /// Gets or sets the public API base URL advertised in the OpenAPI servers list.
+ ///
+ public string? ApiUrl { get; set; }
+
+ ///
+ /// Gets or sets the comma-separated allowed CORS origins.
+ ///
+ public string CorsOrigins { get; set; } = "*";
+
+ ///
+ /// Gets or sets a value indicating whether the worker runs in the server process.
+ ///
+ public bool EmbeddedWorker { get; set; } = true;
+
+ ///
+ /// Gets or sets the number of worker goroutines.
+ ///
+ public int WorkerConcurrency { get; set; } = 10;
+
+ ///
+ /// Gets or sets the maximum email retry attempts.
+ ///
+ public int WorkerMaxRetries { get; set; } = 5;
+
+ ///
+ /// Gets or sets the maximum webhook delivery retry attempts.
+ ///
+ public int WebhookMaxRetries { get; set; } = 3;
+
+ ///
+ /// Gets or sets the webhook HTTP request timeout in seconds.
+ ///
+ public int WebhookTimeoutSeconds { get; set; } = 10;
+
+ ///
+ /// Gets or sets the optional HTTP, HTTPS, or SOCKS5 proxy for webhook delivery.
+ ///
+ public string? WebhookProxyUrl { get; set; }
+
+ ///
+ /// Gets or sets the Google OAuth client ID for SSO login.
+ ///
+ public string? GoogleOAuthClientId { get; set; }
+
+ ///
+ /// Gets or sets the Google OAuth client secret for SSO login.
+ ///
+ public IResourceBuilder? GoogleOAuthClientSecret { get; set; }
+
+ ///
+ /// Gets or sets the OAuth callback base URL.
+ ///
+ public string? OAuthCallbackUrl { get; set; }
+
+ ///
+ /// Gets or sets the storage backend for attachments, either s3 or filesystem.
+ ///
+ public string? BlobProvider { get; set; }
+
+ ///
+ /// Gets or sets the S3-compatible endpoint.
+ ///
+ public string? BlobS3Endpoint { get; set; }
+
+ ///
+ /// Gets or sets the S3 region.
+ ///
+ public string BlobS3Region { get; set; } = "us-east-1";
+
+ ///
+ /// Gets or sets the S3 bucket name.
+ ///
+ public string? BlobS3Bucket { get; set; }
+
+ ///
+ /// Gets or sets the S3 access key.
+ ///
+ public IResourceBuilder? BlobS3AccessKey { get; set; }
+
+ ///
+ /// Gets or sets the S3 secret key.
+ ///
+ public IResourceBuilder? BlobS3SecretKey { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether S3 storage uses TLS.
+ ///
+ public bool BlobS3UseSsl { get; set; } = true;
+
+ ///
+ /// Gets or sets a value indicating whether S3 path-style addressing is used.
+ ///
+ public bool BlobS3PathStyle { get; set; }
+
+ ///
+ /// Gets or sets the filesystem storage path.
+ ///
+ public string BlobFileSystemPath { get; set; } = "/data/attachments";
+
+ ///
+ /// Gets or sets the AES-256-GCM encryption key for stored SMTP passwords.
+ ///
+ public IResourceBuilder? EncryptionKey { get; set; }
+
+ ///
+ /// Gets or sets the system SMTP server host used for platform notifications.
+ ///
+ public string? SystemSmtpHost { get; set; }
+
+ ///
+ /// Gets or sets the system SMTP server port.
+ ///
+ public int SystemSmtpPort { get; set; } = 587;
+
+ ///
+ /// Gets or sets the system SMTP username.
+ ///
+ public string? SystemSmtpUsername { get; set; }
+
+ ///
+ /// Gets or sets the system SMTP password.
+ ///
+ public IResourceBuilder? SystemSmtpPassword { get; set; }
+
+ ///
+ /// Gets or sets the system SMTP from address.
+ ///
+ public string? SystemSmtpFrom { get; set; }
+
+ ///
+ /// Gets or sets the system SMTP encryption mode: none, ssl, or starttls.
+ ///
+ public string SystemSmtpEncryption { get; set; } = "starttls";
+
+ ///
+ /// Gets or sets a value indicating whether inbound email processing is enabled.
+ ///
+ public bool InboundEnabled { get; set; }
+
+ ///
+ /// Gets or sets the bind address for the built-in SMTP receiver.
+ ///
+ public string InboundSmtpHost { get; set; } = "0.0.0.0";
+
+ ///
+ /// Gets or sets the SMTP listener port.
+ ///
+ public int InboundSmtpPort { get; set; } = 2525;
+
+ ///
+ /// Gets or sets the hostname announced in EHLO and used as TLS SNI.
+ ///
+ public string InboundHostname { get; set; } = "posta.local";
+
+ ///
+ /// Gets or sets the maximum raw inbound message size in bytes.
+ ///
+ public int InboundMaxMessageSize { get; set; } = 26214400;
+
+ ///
+ /// Gets or sets the maximum inbound attachment size in bytes.
+ ///
+ public int InboundMaxAttachSize { get; set; } = 10485760;
+
+ ///
+ /// Gets or sets the shared secret for inbound webhooks.
+ ///
+ public IResourceBuilder? InboundWebhookSecret { get; set; }
+
+ ///
+ /// Gets or sets the inbound SMTP TLS mode: none or starttls.
+ ///
+ public string InboundTlsMode { get; set; } = "none";
+
+ ///
+ /// Gets or sets the PEM certificate path for inbound TLS.
+ ///
+ public string? InboundTlsCertFile { get; set; }
+
+ ///
+ /// Gets or sets the PEM key path for inbound TLS.
+ ///
+ public string? InboundTlsKeyFile { get; set; }
+
+ ///
+ /// Gets or sets the per-IP SMTP session rate limit.
+ ///
+ public int InboundSmtpRateLimit { get; set; } = 60;
+
+ ///
+ /// Gets or sets the SMTP rate-limit window in seconds.
+ ///
+ public int InboundSmtpRateWindow { get; set; } = 60;
+
+ ///
+ /// Gets or sets a value indicating whether users must verify their email address before sign-in.
+ ///
+ public bool EmailVerificationRequired { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether permanently rejected recipients are added to the suppression list.
+ ///
+ public bool AutoSuppressOnReject { get; set; } = true;
+
+ ///
+ /// Gets or sets a value indicating whether the email verification endpoint is enabled.
+ ///
+ public bool EmailVerifyEnabled { get; set; } = true;
+
+ ///
+ /// Gets or sets how long address-level verification results are cached, in hours.
+ ///
+ public int EmailVerifyCacheTtlHours { get; set; } = 168;
+
+ ///
+ /// Gets or sets how long domain MX lookups are cached, in hours.
+ ///
+ public int EmailVerifyMxCacheTtlHours { get; set; } = 24;
+
+ ///
+ /// Gets or sets the per-user hourly cap on verification requests.
+ ///
+ public int EmailVerifyRateHourly { get; set; } = 1000;
+
+ ///
+ /// Gets or sets a value indicating whether older binaries can boot against a newer database schema.
+ ///
+ public bool AllowDowngrade { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether hosted plan limits and quotas are enforced.
+ ///
+ public bool PlanEnforcement { get; set; }
+
+ ///
+ /// Gets or sets a PostgreSQL connection string parameter that overrides individual database settings.
+ ///
+ public IResourceBuilder? DatabaseUrl { get; set; }
+
+ ///
+ /// Gets or sets a Redis password parameter that overrides the referenced Redis resource password.
+ ///
+ public IResourceBuilder? RedisPassword { get; set; }
+
+ ///
+ /// Gets or sets an external Redis address in the form host:port.
+ ///
+ public string? RedisAddress { get; set; }
+}
diff --git a/src/CommunityToolkit.Aspire.Hosting.Posta/PostaResource.cs b/src/CommunityToolkit.Aspire.Hosting.Posta/PostaResource.cs
new file mode 100644
index 000000000..7ab14ed98
--- /dev/null
+++ b/src/CommunityToolkit.Aspire.Hosting.Posta/PostaResource.cs
@@ -0,0 +1,61 @@
+namespace Aspire.Hosting.ApplicationModel;
+
+///
+/// Resource for the Posta email delivery platform.
+///
+/// The name of the resource.
+/// A parameter that contains the JWT signing secret.
+/// A parameter that contains the initial admin password.
+[AspireExport(ExposeProperties = true)]
+public sealed class PostaResource(string name, ParameterResource jwtSecret, ParameterResource adminPassword)
+ : ContainerResource(name), IResourceWithConnectionString
+{
+ internal const int HttpEndpointPort = 9000;
+ internal const string HttpEndpointName = "http";
+
+ private EndpointReference? _primaryEndpoint;
+
+ ///
+ /// Gets the primary HTTP endpoint for the Posta server.
+ ///
+ public EndpointReference PrimaryEndpoint => _primaryEndpoint ??= new(this, HttpEndpointName);
+
+ ///
+ /// Gets the host endpoint reference for the HTTP endpoint.
+ ///
+ public EndpointReferenceExpression Host => PrimaryEndpoint.Property(EndpointProperty.Host);
+
+ ///
+ /// Gets the port endpoint reference for the HTTP endpoint.
+ ///
+ public EndpointReferenceExpression Port => PrimaryEndpoint.Property(EndpointProperty.Port);
+
+ ///
+ /// Gets the parameter that contains the JWT signing secret.
+ ///
+ public ParameterResource JwtSecretParameter { get; } = jwtSecret ?? throw new ArgumentNullException(nameof(jwtSecret));
+
+ ///
+ /// Gets the parameter that contains the initial admin password.
+ ///
+ public ParameterResource AdminPasswordParameter { get; } = adminPassword ?? throw new ArgumentNullException(nameof(adminPassword));
+
+ ///
+ /// Connection string for the Posta HTTP API in the form of Endpoint=http://host:port.
+ ///
+ public ReferenceExpression ConnectionStringExpression => ReferenceExpression.Create(
+ $"Endpoint={PrimaryEndpoint.Property(EndpointProperty.Url)}");
+
+ ///
+ /// Gets the connection URI expression for the Posta HTTP API.
+ ///
+ public ReferenceExpression UriExpression => ReferenceExpression.Create($"{PrimaryEndpoint.Property(EndpointProperty.Url)}");
+
+ IEnumerable> IResourceWithConnectionString.GetConnectionProperties()
+ {
+ yield return new("Host", ReferenceExpression.Create($"{Host}"));
+ yield return new("Port", ReferenceExpression.Create($"{Port}"));
+ yield return new("Uri", UriExpression);
+ }
+}
+
diff --git a/src/CommunityToolkit.Aspire.Hosting.Posta/PostaS3BlobStorageOptions.cs b/src/CommunityToolkit.Aspire.Hosting.Posta/PostaS3BlobStorageOptions.cs
new file mode 100644
index 000000000..ac80a0bfb
--- /dev/null
+++ b/src/CommunityToolkit.Aspire.Hosting.Posta/PostaS3BlobStorageOptions.cs
@@ -0,0 +1,44 @@
+using Aspire.Hosting.ApplicationModel;
+
+namespace CommunityToolkit.Aspire.Hosting.Posta;
+
+///
+/// Parameter-based configuration options for Posta S3-compatible attachment storage.
+///
+public sealed class PostaS3BlobStorageOptions
+{
+ ///
+ /// Gets or sets the S3-compatible endpoint.
+ ///
+ public IResourceBuilder? Endpoint { get; set; }
+
+ ///
+ /// Gets or sets the S3 region.
+ ///
+ public IResourceBuilder? Region { get; set; }
+
+ ///
+ /// Gets or sets the S3 bucket name.
+ ///
+ public IResourceBuilder? Bucket { get; set; }
+
+ ///
+ /// Gets or sets the S3 access key.
+ ///
+ public IResourceBuilder? AccessKey { get; set; }
+
+ ///
+ /// Gets or sets the S3 secret key.
+ ///
+ public IResourceBuilder? SecretKey { get; set; }
+
+ ///
+ /// Gets or sets whether S3 storage uses TLS.
+ ///
+ public IResourceBuilder? UseSsl { get; set; }
+
+ ///
+ /// Gets or sets whether S3 path-style addressing is used.
+ ///
+ public IResourceBuilder? PathStyle { get; set; }
+}
diff --git a/src/CommunityToolkit.Aspire.Hosting.Posta/PostaSystemSmtpOptions.cs b/src/CommunityToolkit.Aspire.Hosting.Posta/PostaSystemSmtpOptions.cs
new file mode 100644
index 000000000..c61f285f0
--- /dev/null
+++ b/src/CommunityToolkit.Aspire.Hosting.Posta/PostaSystemSmtpOptions.cs
@@ -0,0 +1,39 @@
+using Aspire.Hosting.ApplicationModel;
+
+namespace CommunityToolkit.Aspire.Hosting.Posta;
+
+///
+/// Parameter-based configuration options for Posta system SMTP notifications.
+///
+public sealed class PostaSystemSmtpOptions
+{
+ ///
+ /// Gets or sets the system SMTP server host used for platform notifications.
+ ///
+ public IResourceBuilder? Host { get; set; }
+
+ ///
+ /// Gets or sets the system SMTP server port.
+ ///
+ public IResourceBuilder? Port { get; set; }
+
+ ///
+ /// Gets or sets the system SMTP username.
+ ///
+ public IResourceBuilder? Username { get; set; }
+
+ ///
+ /// Gets or sets the system SMTP password.
+ ///
+ public IResourceBuilder? Password { get; set; }
+
+ ///
+ /// Gets or sets the system SMTP from address.
+ ///
+ public IResourceBuilder? From { get; set; }
+
+ ///
+ /// Gets or sets the system SMTP encryption mode: none, ssl, or starttls.
+ ///
+ public IResourceBuilder? Encryption { get; set; }
+}
diff --git a/src/CommunityToolkit.Aspire.Hosting.Posta/README.md b/src/CommunityToolkit.Aspire.Hosting.Posta/README.md
new file mode 100644
index 000000000..eca50870a
--- /dev/null
+++ b/src/CommunityToolkit.Aspire.Hosting.Posta/README.md
@@ -0,0 +1,181 @@
+# Posta hosting integration
+
+Use this integration to model, configure, and orchestrate the [Posta](https://docs.goposta.dev/) self-hosted email delivery platform in an Aspire AppHost.
+
+## Getting started
+
+Install the package in your AppHost project:
+
+```bash
+aspire add CommunityToolkit.Aspire.Hosting.Posta
+```
+
+## Basic usage
+
+```csharp
+var builder = DistributedApplication.CreateBuilder(args);
+
+var postgresPassword = builder.AddParameter("postgres-password", secret: true);
+var postgres = builder.AddPostgres("postgres", password: postgresPassword);
+var database = postgres.AddDatabase("posta-db", "posta");
+var redis = builder.AddRedis("redis");
+
+var posta = builder.AddPosta("posta", database, redis);
+
+builder.AddProject("api")
+ .WithReference(posta)
+ .WaitFor(posta);
+
+builder.Build().Run();
+```
+
+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
+
+Use the `PostaOptions` callback when values are known in the AppHost and can be represented as typed literal values. Secret values inside `PostaOptions`, such as OAuth client secrets, S3 keys, SMTP passwords, encryption keys, inbound webhook secrets, `POSTA_DB_URL`, and Redis passwords, should still be passed as Aspire parameters.
+
+```csharp
+var encryptionKey = builder.AddParameter("posta-encryption-key", secret: true);
+var smtpPassword = builder.AddParameter("posta-smtp-password", secret: true);
+
+var posta = builder.AddPosta("posta", database, redis, options =>
+{
+ options.Environment = "production";
+ options.WebUrl = "https://mail.example.com";
+ options.ApiUrl = "https://mail.example.com";
+ options.CorsOrigins = "https://app.example.com";
+ options.MetricsEnabled = true;
+ options.EncryptionKey = encryptionKey;
+
+ options.SystemSmtpHost = "smtp.example.com";
+ options.SystemSmtpPort = 587;
+ options.SystemSmtpPassword = smtpPassword;
+ options.SystemSmtpFrom = "notifications@example.com";
+ options.SystemSmtpEncryption = "starttls";
+});
+```
+
+For values coming from configuration, environment variables, user secrets, or publish-time parameters, use the grouped parameter-based methods. These methods accept options objects whose properties are `IResourceBuilder?`, so they work with `AddParameter` and `AddParameterFromConfiguration`.
+
+```csharp
+var smtpHost = builder.AddParameterFromConfiguration("posta-smtp-host", "Posta:Smtp:Host");
+var smtpPort = builder.AddParameterFromConfiguration("posta-smtp-port", "Posta:Smtp:Port");
+var smtpUsername = builder.AddParameterFromConfiguration("posta-smtp-username", "Posta:Smtp:Username");
+var smtpPassword = builder.AddParameterFromConfiguration("posta-smtp-password", "Posta:Smtp:Password", secret: true);
+var smtpFrom = builder.AddParameterFromConfiguration("posta-smtp-from", "Posta:Smtp:From");
+var smtpEncryption = builder.AddParameterFromConfiguration("posta-smtp-encryption", "Posta:Smtp:Encryption");
+
+var googleClientId = builder.AddParameterFromConfiguration("posta-google-client-id", "Posta:GoogleOAuth:ClientId");
+var googleClientSecret = builder.AddParameterFromConfiguration("posta-google-client-secret", "Posta:GoogleOAuth:ClientSecret", secret: true);
+var googleCallbackUrl = builder.AddParameterFromConfiguration("posta-google-callback-url", "Posta:GoogleOAuth:CallbackUrl");
+
+var s3Endpoint = builder.AddParameterFromConfiguration("posta-s3-endpoint", "Posta:S3:Endpoint");
+var s3Region = builder.AddParameterFromConfiguration("posta-s3-region", "Posta:S3:Region");
+var s3Bucket = builder.AddParameterFromConfiguration("posta-s3-bucket", "Posta:S3:Bucket");
+var s3AccessKey = builder.AddParameterFromConfiguration("posta-s3-access-key", "Posta:S3:AccessKey", secret: true);
+var s3SecretKey = builder.AddParameterFromConfiguration("posta-s3-secret-key", "Posta:S3:SecretKey", secret: true);
+
+var posta = builder.AddPosta("posta", database, redis)
+ .WithSystemSmtp(options =>
+ {
+ options.Host = smtpHost;
+ options.Port = smtpPort;
+ options.Username = smtpUsername;
+ options.Password = smtpPassword;
+ options.From = smtpFrom;
+ options.Encryption = smtpEncryption;
+ })
+ .WithGoogleOAuth(options =>
+ {
+ options.ClientId = googleClientId;
+ options.ClientSecret = googleClientSecret;
+ options.CallbackUrl = googleCallbackUrl;
+ })
+ .WithS3BlobStorage(options =>
+ {
+ options.Endpoint = s3Endpoint;
+ options.Region = s3Region;
+ options.Bucket = s3Bucket;
+ options.AccessKey = s3AccessKey;
+ options.SecretKey = s3SecretKey;
+ });
+```
+
+You can also configure inbound SMTP and email verification through parameter-based options:
+
+```csharp
+var inboundEnabled = builder.AddParameterFromConfiguration("posta-inbound-enabled", "Posta:Inbound:Enabled");
+var inboundPort = builder.AddParameterFromConfiguration("posta-inbound-port", "Posta:Inbound:Port");
+var inboundWebhookSecret = builder.AddParameterFromConfiguration("posta-inbound-webhook-secret", "Posta:Inbound:WebhookSecret", secret: true);
+
+var emailVerificationRequired = builder.AddParameterFromConfiguration("posta-email-verification-required", "Posta:EmailVerification:Required");
+
+builder.AddPosta("posta", database, redis)
+ .WithInboundSmtp(options =>
+ {
+ options.Enabled = inboundEnabled;
+ options.Port = inboundPort;
+ options.WebhookSecret = inboundWebhookSecret;
+ })
+ .WithEmailVerification(options =>
+ {
+ options.Required = emailVerificationRequired;
+ });
+```
+
+## API overview
+
+| Method | Purpose |
+| --- | --- |
+| `AddPosta(name, database, redis)` | Adds Posta and configures PostgreSQL and Redis references. |
+| `AddPosta(name, database, redis, PostaOptions)` | Adds Posta with typed environment configuration. |
+| `AddPosta(name, database, redis, Action)` | Adds Posta with callback-based typed environment configuration. |
+| `WithReference(PostgresDatabaseResource)` | Configures the PostgreSQL database environment variables and waits for the database. |
+| `WithReference(RedisResource, redisPassword)` | Configures Redis address/password environment variables and waits for Redis. |
+| `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. |
+| `WithInboundSmtp(Action)` | Configures parameter-based inbound SMTP receiver settings with a callback. |
+| `WithS3BlobStorage(PostaS3BlobStorageOptions)` | Configures parameter-based S3-compatible attachment storage and sets the blob provider to `s3`. |
+| `WithS3BlobStorage(Action)` | Configures parameter-based S3-compatible attachment storage with a callback and sets the blob provider to `s3`. |
+| `WithGoogleOAuth(PostaGoogleOAuthOptions)` | Configures parameter-based Google OAuth login settings. |
+| `WithGoogleOAuth(Action)` | Configures parameter-based Google OAuth login settings with a callback. |
+| `WithEmailVerification(PostaEmailVerificationOptions)` | Configures parameter-based email verification settings. |
+| `WithEmailVerification(Action)` | Configures parameter-based email verification settings with a callback. |
+
+Use `PostaOptions.DatabaseUrl`, `PostaOptions.RedisAddress`, and `PostaOptions.RedisPassword` only when you need to override the generated PostgreSQL or Redis environment values.
+
+## Connection Properties
+
+The Posta resource exposes the following connection properties:
+
+| Name | Format |
+| --- | --- |
+| `Host` | Posta HTTP API host |
+| `Port` | Posta HTTP API port |
+| `Uri` | `http://{host}:{port}` |
+
+These properties become environment variables named `[RESOURCE]__HOST`, `[RESOURCE]__PORT`, and `[RESOURCE]__URI` when referenced by another resource. The connection string uses `Endpoint=http://{host}:{port}`.
+
+## Additional documentation
+
+* [Posta documentation](https://docs.goposta.dev/)
+* [Posta installation guide](https://docs.goposta.dev/docs/getting-started/installation/)
+* [Posta configuration](https://docs.goposta.dev/docs/getting-started/configuration/)
+
+## Feedback & contributing
+
+https://github.com/CommunityToolkit/Aspire
diff --git a/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.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 @@
+
+
+
+