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 @@ + + + + false + true + + + + + + + + + diff --git a/tests/CommunityToolkit.Aspire.Hosting.Posta.Tests/ContainerResourceCreationTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Posta.Tests/ContainerResourceCreationTests.cs new file mode 100644 index 000000000..4fa5f9698 --- /dev/null +++ b/tests/CommunityToolkit.Aspire.Hosting.Posta.Tests/ContainerResourceCreationTests.cs @@ -0,0 +1,561 @@ +using Aspire.Hosting; +using CommunityToolkit.Aspire.Testing; + +namespace CommunityToolkit.Aspire.Hosting.Posta.Tests; + +public class ContainerResourceCreationTests +{ + [Fact] + public void AddPostaThrowsWhenBuilderIsNull() + { + IDistributedApplicationBuilder builder = null!; + + var exception = Assert.Throws(() => builder.AddPosta("posta", null!, null!)); + Assert.Equal("builder", exception.ParamName); + } + + [Fact] + public void AddPostaThrowsWhenNameIsNull() + { + var builder = DistributedApplication.CreateBuilder(); + var (database, redis) = AddPostaDependencies(builder); + + Assert.Throws(() => builder.AddPosta(null!, database, redis)); + } + + [Fact] + public void WithReferenceThrowsWhenDatabaseIsNull() + { + var builder = DistributedApplication.CreateBuilder(); + var (database, redis) = AddPostaDependencies(builder); + var posta = builder.AddPosta("posta", database, redis); + + Assert.Throws(() => posta.WithReference((IResourceBuilder)null!)); + } + + [Fact] + public void WithReferenceThrowsWhenRedisIsNull() + { + var builder = DistributedApplication.CreateBuilder(); + var (database, redis) = AddPostaDependencies(builder); + var posta = builder.AddPosta("posta", database, redis); + + Assert.Throws(() => posta.WithReference((IResourceBuilder)null!)); + } + + [Fact] + public void WithDataVolumeAddsVolumeMount() + { + var builder = DistributedApplication.CreateBuilder(); + var (database, redis) = AddPostaDependencies(builder); + + var posta = builder.AddPosta("posta", database, redis) + .WithDataVolume("posta-data", isReadOnly: true); + + var mount = Assert.Single(posta.Resource.Annotations.OfType()); + Assert.Equal(ContainerMountType.Volume, mount.Type); + Assert.Equal("posta-data", mount.Source); + Assert.Equal("/data", mount.Target); + Assert.True(mount.IsReadOnly); + } + + [Fact] + public void WithDataVolumeGeneratesVolumeNameWhenNotSpecified() + { + var builder = DistributedApplication.CreateBuilder(); + var (database, redis) = AddPostaDependencies(builder); + + var posta = builder.AddPosta("posta", database, redis) + .WithDataVolume(); + + var mount = Assert.Single(posta.Resource.Annotations.OfType()); + Assert.Equal(ContainerMountType.Volume, mount.Type); + Assert.False(string.IsNullOrWhiteSpace(mount.Source)); + Assert.Equal("/data", mount.Target); + } + + [Fact] + public void WithDataBindMountAddsBindMount() + { + var builder = DistributedApplication.CreateBuilder(); + var (database, redis) = AddPostaDependencies(builder); + + var posta = builder.AddPosta("posta", database, redis) + .WithDataBindMount("./posta-data", isReadOnly: true); + + var mount = Assert.Single(posta.Resource.Annotations.OfType()); + Assert.Equal(ContainerMountType.BindMount, mount.Type); + var source = Assert.IsType(mount.Source); + Assert.True(Path.IsPathFullyQualified(source)); + Assert.EndsWith("posta-data", source); + Assert.Equal("/data", mount.Target); + Assert.True(mount.IsReadOnly); + } + + [Fact] + public void WithDataMethodsValidateArguments() + { + IResourceBuilder posta = null!; + + Assert.Throws(() => posta.WithDataVolume()); + Assert.Throws(() => posta.WithDataBindMount("./posta-data")); + + var builder = DistributedApplication.CreateBuilder(); + var (database, redis) = AddPostaDependencies(builder); + posta = builder.AddPosta("posta", database, redis); + + Assert.Throws(() => posta.WithDataBindMount(null!)); + } + + [Fact] + public void WithGroupedOptionCallbacksThrowWhenConfigureOptionsIsNull() + { + var builder = DistributedApplication.CreateBuilder(); + var (database, redis) = AddPostaDependencies(builder); + var posta = builder.AddPosta("posta", database, redis); + + Assert.Throws(() => posta.WithSystemSmtp((Action)null!)); + Assert.Throws(() => posta.WithInboundSmtp((Action)null!)); + Assert.Throws(() => posta.WithS3BlobStorage((Action)null!)); + Assert.Throws(() => posta.WithGoogleOAuth((Action)null!)); + Assert.Throws(() => posta.WithEmailVerification((Action)null!)); + } + + [Fact] + public void AddPostaWithReferencesThrowsWhenDatabaseIsNull() + { + var builder = DistributedApplication.CreateBuilder(); + var redis = builder.AddRedis("redis"); + + Assert.Throws(() => builder.AddPosta("posta", null!, redis)); + } + + [Fact] + public void AddPostaWithReferencesThrowsWhenRedisIsNull() + { + var builder = DistributedApplication.CreateBuilder(); + var postgres = builder.AddPostgres("postgres"); + var database = postgres.AddDatabase("posta-db", "posta"); + + Assert.Throws(() => builder.AddPosta("posta", database, null!)); + } + + [Fact] + public void AddPostaSetsContainerDetailsOnResource() + { + var builder = DistributedApplication.CreateBuilder(); + var (database, redis) = AddPostaDependencies(builder); + + builder.AddPosta("posta", database, redis); + + using var app = builder.Build(); + var appModel = app.Services.GetRequiredService(); + + var resource = Assert.Single(appModel.Resources.OfType()); + + Assert.Equal("posta", resource.Name); + Assert.True(resource.TryGetLastAnnotation(out ContainerImageAnnotation? imageAnnotations)); + Assert.Equal(PostaContainerImageTags.Tag, imageAnnotations.Tag); + Assert.NotEqual("latest", imageAnnotations.Tag); + Assert.Equal(PostaContainerImageTags.Image, imageAnnotations.Image); + Assert.Equal(PostaContainerImageTags.Registry, imageAnnotations.Registry); + } + + [Fact] + public void AddPostaSetsEndpointDetailsOnResource() + { + var builder = DistributedApplication.CreateBuilder(); + var (database, redis) = AddPostaDependencies(builder); + + builder.AddPosta("posta", database, redis, port: 9001); + + using var app = builder.Build(); + var appModel = app.Services.GetRequiredService(); + var resource = Assert.Single(appModel.Resources.OfType()); + + var endpoint = Assert.Single(resource.Annotations.OfType(), x => x.Name == PostaResource.HttpEndpointName); + Assert.Equal("http", endpoint.UriScheme); + Assert.Equal(PostaResource.HttpEndpointPort, endpoint.TargetPort); + Assert.Equal(9001, endpoint.Port); + } + + [Fact] + public void AddPostaRegistersHealthChecks() + { + var builder = DistributedApplication.CreateBuilder(); + var (database, redis) = AddPostaDependencies(builder); + + builder.AddPosta("posta", database, redis); + + using var app = builder.Build(); + var appModel = app.Services.GetRequiredService(); + var resource = Assert.Single(appModel.Resources.OfType()); + + Assert.True(resource.TryGetAnnotationsOfType(out var annotations)); + Assert.Equal(2, annotations.Count()); + } + + [Fact] + public async Task AddPostaWithReferencesConfiguresPostgreSqlAndRedis() + { + var builder = DistributedApplication.CreateBuilder(); + var postgres = builder.AddPostgres("postgres"); + var database = postgres.AddDatabase("posta-db", "posta"); + var redis = builder.AddRedis("redis"); + + var posta = builder.AddPosta("posta", database, redis); + + Assert.True(posta.Resource.TryGetAnnotationsOfType(out var annotations)); + + var context = new EnvironmentCallbackContext( + new DistributedApplicationExecutionContext( + new DistributedApplicationExecutionContextOptions(DistributedApplicationOperation.Run))); + + foreach (var annotation in annotations) + { + await annotation.Callback(context); + } + + var env = context.EnvironmentVariables; + + Assert.True(env.ContainsKey("POSTA_DB_HOST")); + Assert.True(env.ContainsKey("POSTA_DB_PORT")); + Assert.True(env.ContainsKey("POSTA_DB_USER")); + Assert.True(env.ContainsKey("POSTA_DB_PASSWORD")); + Assert.Equal("posta", env["POSTA_DB_NAME"]); + Assert.Equal("disable", env["POSTA_DB_SSL_MODE"]); + Assert.True(env.ContainsKey("POSTA_REDIS_ADDR")); + } + + [Fact] + public async Task AddPostaConfiguresPostaEnvironmentVariables() + { + var builder = DistributedApplication.CreateBuilder(); + var postgres = builder.AddPostgres("postgres"); + var database = postgres.AddDatabase("posta-db", "posta"); + var redis = builder.AddRedis("redis"); + var databaseUrl = builder.AddParameter("posta-db-url", "postgres://example"); + var redisPassword = builder.AddParameter("posta-redis-password", "redis-secret"); + var googleSecret = builder.AddParameter("posta-google-secret", "google-secret"); + var s3AccessKey = builder.AddParameter("posta-s3-access-key", "s3-access"); + var s3SecretKey = builder.AddParameter("posta-s3-secret-key", "s3-secret"); + var encryptionKey = builder.AddParameter("posta-encryption-key", "encryption-secret"); + var smtpPassword = builder.AddParameter("posta-smtp-password", "smtp-secret"); + var inboundWebhookSecret = builder.AddParameter("posta-inbound-secret", "inbound-secret"); + + var posta = builder.AddPosta("posta", database, redis, options => + { + options.Environment = "dev"; + options.DevMode = true; + options.AuthRateLimitEnabled = false; + options.RateLimitHourly = 500; + options.RateLimitDaily = 5000; + options.OpenApiDocs = false; + options.MetricsEnabled = true; + options.WebDir = "web/dist"; + options.WebUrl = "https://posta.example.com"; + options.ApiUrl = "https://api.posta.example.com"; + options.CorsOrigins = "https://app.example.com"; + options.EmbeddedWorker = false; + options.WorkerConcurrency = 20; + options.WorkerMaxRetries = 7; + options.WebhookMaxRetries = 4; + options.WebhookTimeoutSeconds = 30; + options.WebhookProxyUrl = "http://proxy.example.com"; + options.GoogleOAuthClientId = "google-client-id"; + options.GoogleOAuthClientSecret = googleSecret; + options.OAuthCallbackUrl = "https://posta.example.com/oauth/callback"; + options.BlobProvider = "s3"; + options.BlobS3Endpoint = "https://s3.example.com"; + options.BlobS3Region = "eu-central-1"; + options.BlobS3Bucket = "posta"; + options.BlobS3AccessKey = s3AccessKey; + options.BlobS3SecretKey = s3SecretKey; + options.BlobS3UseSsl = false; + options.BlobS3PathStyle = true; + options.BlobFileSystemPath = "/posta/attachments"; + options.EncryptionKey = encryptionKey; + options.SystemSmtpHost = "smtp.example.com"; + options.SystemSmtpPort = 2525; + options.SystemSmtpUsername = "notifications@example.com"; + options.SystemSmtpPassword = smtpPassword; + options.SystemSmtpFrom = "notifications@example.com"; + options.SystemSmtpEncryption = "ssl"; + options.InboundEnabled = true; + options.InboundSmtpHost = "127.0.0.1"; + options.InboundSmtpPort = 2526; + options.InboundHostname = "mx.example.com"; + options.InboundMaxMessageSize = 12345; + options.InboundMaxAttachSize = 6789; + options.InboundWebhookSecret = inboundWebhookSecret; + options.InboundTlsMode = "starttls"; + options.InboundTlsCertFile = "/certs/fullchain.pem"; + options.InboundTlsKeyFile = "/certs/privkey.pem"; + options.InboundSmtpRateLimit = 10; + options.InboundSmtpRateWindow = 20; + options.EmailVerificationRequired = true; + options.AutoSuppressOnReject = false; + options.EmailVerifyEnabled = false; + options.EmailVerifyCacheTtlHours = 12; + options.EmailVerifyMxCacheTtlHours = 6; + options.EmailVerifyRateHourly = 42; + options.AllowDowngrade = true; + options.PlanEnforcement = true; + options.DatabaseUrl = databaseUrl; + options.RedisPassword = redisPassword; + options.RedisAddress = "redis.example.com:6379"; + }) + .WithReference(redis, redisPassword); + + Assert.True(posta.Resource.TryGetAnnotationsOfType(out var annotations)); + + var context = new EnvironmentCallbackContext( + new DistributedApplicationExecutionContext( + new DistributedApplicationExecutionContextOptions(DistributedApplicationOperation.Run))); + + foreach (var annotation in annotations) + { + await annotation.Callback(context); + } + + var env = context.EnvironmentVariables; + + Assert.Equal("9000", env["POSTA_PORT"]); + Assert.Equal("dev", env["POSTA_ENV"]); + Assert.Equal("true", env["POSTA_DEV_MODE"]); + Assert.Equal("false", env["POSTA_AUTH_RATE_LIMIT_ENABLED"]); + Assert.Equal("500", env["POSTA_RATE_LIMIT_HOURLY"]); + Assert.Equal("5000", env["POSTA_RATE_LIMIT_DAILY"]); + Assert.Equal("false", env["POSTA_OPENAPI_DOCS"]); + Assert.Equal("true", env["POSTA_METRICS_ENABLED"]); + Assert.Equal("web/dist", env["POSTA_WEB_DIR"]); + Assert.Equal("https://posta.example.com", env["POSTA_WEB_URL"]); + Assert.Equal("https://api.posta.example.com", env["POSTA_API_URL"]); + Assert.Equal("https://app.example.com", env["POSTA_CORS_ORIGINS"]); + Assert.Equal("false", env["POSTA_EMBEDDED_WORKER"]); + Assert.Equal("20", env["POSTA_WORKER_CONCURRENCY"]); + Assert.Equal("7", env["POSTA_WORKER_MAX_RETRIES"]); + Assert.Equal("4", env["POSTA_WEBHOOK_MAX_RETRIES"]); + Assert.Equal("30", env["POSTA_WEBHOOK_TIMEOUT_SECS"]); + Assert.Equal("http://proxy.example.com", env["POSTA_WEBHOOK_PROXY_URL"]); + Assert.Equal("google-client-id", env["POSTA_GOOGLE_OAUTH_CLIENT_ID"]); + Assert.Same(googleSecret.Resource, env["POSTA_GOOGLE_OAUTH_CLIENT_SECRET"]); + Assert.Equal("https://posta.example.com/oauth/callback", env["POSTA_OAUTH_CALLBACK_URL"]); + Assert.Equal("s3", env["POSTA_BLOB_PROVIDER"]); + Assert.Equal("https://s3.example.com", env["POSTA_BLOB_S3_ENDPOINT"]); + Assert.Equal("eu-central-1", env["POSTA_BLOB_S3_REGION"]); + Assert.Equal("posta", env["POSTA_BLOB_S3_BUCKET"]); + Assert.Same(s3AccessKey.Resource, env["POSTA_BLOB_S3_ACCESS_KEY"]); + Assert.Same(s3SecretKey.Resource, env["POSTA_BLOB_S3_SECRET_KEY"]); + Assert.Equal("false", env["POSTA_BLOB_S3_USE_SSL"]); + Assert.Equal("true", env["POSTA_BLOB_S3_PATH_STYLE"]); + Assert.Equal("/posta/attachments", env["POSTA_BLOB_FS_PATH"]); + Assert.Same(encryptionKey.Resource, env["POSTA_ENCRYPTION_KEY"]); + Assert.Equal("smtp.example.com", env["POSTA_SYSTEM_SMTP_HOST"]); + Assert.Equal("2525", env["POSTA_SYSTEM_SMTP_PORT"]); + Assert.Equal("notifications@example.com", env["POSTA_SYSTEM_SMTP_USERNAME"]); + Assert.Same(smtpPassword.Resource, env["POSTA_SYSTEM_SMTP_PASSWORD"]); + Assert.Equal("notifications@example.com", env["POSTA_SYSTEM_SMTP_FROM"]); + Assert.Equal("ssl", env["POSTA_SYSTEM_SMTP_ENCRYPTION"]); + Assert.Equal("true", env["POSTA_INBOUND_ENABLED"]); + Assert.Equal("127.0.0.1", env["POSTA_INBOUND_SMTP_HOST"]); + Assert.Equal("2526", env["POSTA_INBOUND_SMTP_PORT"]); + Assert.Equal("mx.example.com", env["POSTA_INBOUND_HOSTNAME"]); + Assert.Equal("12345", env["POSTA_INBOUND_MAX_MESSAGE_SIZE"]); + Assert.Equal("6789", env["POSTA_INBOUND_MAX_ATTACH_SIZE"]); + Assert.Same(inboundWebhookSecret.Resource, env["POSTA_INBOUND_WEBHOOK_SECRET"]); + Assert.Equal("starttls", env["POSTA_INBOUND_TLS_MODE"]); + Assert.Equal("/certs/fullchain.pem", env["POSTA_INBOUND_TLS_CERT_FILE"]); + Assert.Equal("/certs/privkey.pem", env["POSTA_INBOUND_TLS_KEY_FILE"]); + Assert.Equal("10", env["POSTA_INBOUND_SMTP_RATE_LIMIT"]); + Assert.Equal("20", env["POSTA_INBOUND_SMTP_RATE_WINDOW"]); + Assert.Equal("true", env["POSTA_EMAIL_VERIFICATION_REQUIRED"]); + Assert.Equal("false", env["POSTA_AUTO_SUPPRESS_ON_REJECT"]); + Assert.Equal("false", env["POSTA_EMAIL_VERIFY_ENABLED"]); + Assert.Equal("12", env["POSTA_EMAIL_VERIFY_CACHE_TTL_HOURS"]); + Assert.Equal("6", env["POSTA_EMAIL_VERIFY_MX_CACHE_TTL_HOURS"]); + Assert.Equal("42", env["POSTA_EMAIL_VERIFY_RATE_HOURLY"]); + Assert.Equal("true", env["POSTA_ALLOW_DOWNGRADE"]); + Assert.Equal("true", env["POSTA_PLAN_ENFORCEMENT"]); + Assert.Same(databaseUrl.Resource, env["POSTA_DB_URL"]); + + Assert.True(env.ContainsKey("POSTA_DB_HOST")); + Assert.True(env.ContainsKey("POSTA_DB_PORT")); + Assert.True(env.ContainsKey("POSTA_DB_USER")); + Assert.True(env.ContainsKey("POSTA_DB_PASSWORD")); + Assert.Equal("posta", env["POSTA_DB_NAME"]); + Assert.Equal("disable", env["POSTA_DB_SSL_MODE"]); + Assert.True(env.ContainsKey("POSTA_REDIS_ADDR")); + Assert.Equal("redis.example.com:6379", env["POSTA_REDIS_ADDR"]); + Assert.Same(redisPassword.Resource, env["POSTA_REDIS_PASSWORD"]); + } + + [Fact] + public async Task WithGroupedOptionsConfiguresParameterBasedEnvironmentVariables() + { + var builder = DistributedApplication.CreateBuilder(); + var (database, redis) = AddPostaDependencies(builder); + var parameters = new Dictionary>(); + + IResourceBuilder Parameter(string name, string value) + { + var parameter = builder.AddParameter($"posta-{name}", value); + parameters[name] = parameter; + return parameter; + } + + var posta = builder.AddPosta("posta", database, redis) + .WithSystemSmtp(new PostaSystemSmtpOptions + { + Host = Parameter("system-smtp-host", "smtp.example.com"), + Port = Parameter("system-smtp-port", "2525"), + Username = Parameter("system-smtp-username", "notifications@example.com"), + Password = Parameter("system-smtp-password", "smtp-secret"), + From = Parameter("system-smtp-from", "notifications@example.com"), + Encryption = Parameter("system-smtp-encryption", "ssl") + }) + .WithInboundSmtp(new PostaInboundSmtpOptions + { + Enabled = Parameter("inbound-enabled", "true"), + Host = Parameter("inbound-smtp-host", "127.0.0.1"), + Port = Parameter("inbound-smtp-port", "2526"), + Hostname = Parameter("inbound-hostname", "mx.example.com"), + MaxMessageSize = Parameter("inbound-max-message-size", "12345"), + MaxAttachmentSize = Parameter("inbound-max-attachment-size", "6789"), + WebhookSecret = Parameter("inbound-webhook-secret", "inbound-secret"), + TlsMode = Parameter("inbound-tls-mode", "starttls"), + TlsCertFile = Parameter("inbound-tls-cert-file", "/certs/fullchain.pem"), + TlsKeyFile = Parameter("inbound-tls-key-file", "/certs/privkey.pem"), + RateLimit = Parameter("inbound-smtp-rate-limit", "10"), + RateWindow = Parameter("inbound-smtp-rate-window", "20") + }) + .WithS3BlobStorage(new PostaS3BlobStorageOptions + { + Endpoint = Parameter("blob-s3-endpoint", "https://s3.example.com"), + Region = Parameter("blob-s3-region", "eu-central-1"), + Bucket = Parameter("blob-s3-bucket", "posta"), + AccessKey = Parameter("blob-s3-access-key", "s3-access"), + SecretKey = Parameter("blob-s3-secret-key", "s3-secret"), + UseSsl = Parameter("blob-s3-use-ssl", "false"), + PathStyle = Parameter("blob-s3-path-style", "true") + }) + .WithGoogleOAuth(new PostaGoogleOAuthOptions + { + ClientId = Parameter("google-oauth-client-id", "google-client-id"), + ClientSecret = Parameter("google-oauth-client-secret", "google-secret"), + CallbackUrl = Parameter("oauth-callback-url", "https://posta.example.com/oauth/callback") + }) + .WithEmailVerification(new PostaEmailVerificationOptions + { + Required = Parameter("email-verification-required", "true"), + AutoSuppressOnReject = Parameter("auto-suppress-on-reject", "false"), + Enabled = Parameter("email-verify-enabled", "false"), + CacheTtlHours = Parameter("email-verify-cache-ttl-hours", "12"), + MxCacheTtlHours = Parameter("email-verify-mx-cache-ttl-hours", "6"), + RateHourly = Parameter("email-verify-rate-hourly", "42") + }); + + Assert.True(posta.Resource.TryGetAnnotationsOfType(out var annotations)); + + var context = new EnvironmentCallbackContext( + new DistributedApplicationExecutionContext( + new DistributedApplicationExecutionContextOptions(DistributedApplicationOperation.Run))); + + foreach (var annotation in annotations) + { + await annotation.Callback(context); + } + + var env = context.EnvironmentVariables; + + Assert.Equal("s3", env["POSTA_BLOB_PROVIDER"]); + foreach (var (parameterName, environmentVariableName) in new (string ParameterName, string EnvironmentVariableName)[] + { + ("system-smtp-host", "POSTA_SYSTEM_SMTP_HOST"), + ("system-smtp-port", "POSTA_SYSTEM_SMTP_PORT"), + ("system-smtp-username", "POSTA_SYSTEM_SMTP_USERNAME"), + ("system-smtp-password", "POSTA_SYSTEM_SMTP_PASSWORD"), + ("system-smtp-from", "POSTA_SYSTEM_SMTP_FROM"), + ("system-smtp-encryption", "POSTA_SYSTEM_SMTP_ENCRYPTION"), + ("inbound-enabled", "POSTA_INBOUND_ENABLED"), + ("inbound-smtp-host", "POSTA_INBOUND_SMTP_HOST"), + ("inbound-smtp-port", "POSTA_INBOUND_SMTP_PORT"), + ("inbound-hostname", "POSTA_INBOUND_HOSTNAME"), + ("inbound-max-message-size", "POSTA_INBOUND_MAX_MESSAGE_SIZE"), + ("inbound-max-attachment-size", "POSTA_INBOUND_MAX_ATTACH_SIZE"), + ("inbound-webhook-secret", "POSTA_INBOUND_WEBHOOK_SECRET"), + ("inbound-tls-mode", "POSTA_INBOUND_TLS_MODE"), + ("inbound-tls-cert-file", "POSTA_INBOUND_TLS_CERT_FILE"), + ("inbound-tls-key-file", "POSTA_INBOUND_TLS_KEY_FILE"), + ("inbound-smtp-rate-limit", "POSTA_INBOUND_SMTP_RATE_LIMIT"), + ("inbound-smtp-rate-window", "POSTA_INBOUND_SMTP_RATE_WINDOW"), + ("blob-s3-endpoint", "POSTA_BLOB_S3_ENDPOINT"), + ("blob-s3-region", "POSTA_BLOB_S3_REGION"), + ("blob-s3-bucket", "POSTA_BLOB_S3_BUCKET"), + ("blob-s3-access-key", "POSTA_BLOB_S3_ACCESS_KEY"), + ("blob-s3-secret-key", "POSTA_BLOB_S3_SECRET_KEY"), + ("blob-s3-use-ssl", "POSTA_BLOB_S3_USE_SSL"), + ("blob-s3-path-style", "POSTA_BLOB_S3_PATH_STYLE"), + ("google-oauth-client-id", "POSTA_GOOGLE_OAUTH_CLIENT_ID"), + ("google-oauth-client-secret", "POSTA_GOOGLE_OAUTH_CLIENT_SECRET"), + ("oauth-callback-url", "POSTA_OAUTH_CALLBACK_URL"), + ("email-verification-required", "POSTA_EMAIL_VERIFICATION_REQUIRED"), + ("auto-suppress-on-reject", "POSTA_AUTO_SUPPRESS_ON_REJECT"), + ("email-verify-enabled", "POSTA_EMAIL_VERIFY_ENABLED"), + ("email-verify-cache-ttl-hours", "POSTA_EMAIL_VERIFY_CACHE_TTL_HOURS"), + ("email-verify-mx-cache-ttl-hours", "POSTA_EMAIL_VERIFY_MX_CACHE_TTL_HOURS"), + ("email-verify-rate-hourly", "POSTA_EMAIL_VERIFY_RATE_HOURLY") + }) + { + Assert.Same(parameters[parameterName].Resource, env[environmentVariableName]); + } + } + + [Fact] + public async Task WithGroupedOptionCallbacksConfigureParameterBasedEnvironmentVariables() + { + var builder = DistributedApplication.CreateBuilder(); + var (database, redis) = AddPostaDependencies(builder); + var smtpHost = builder.AddParameter("posta-smtp-host", "smtp.example.com"); + var inboundEnabled = builder.AddParameter("posta-inbound-enabled", "true"); + var s3Bucket = builder.AddParameter("posta-s3-bucket", "posta"); + var googleClientId = builder.AddParameter("posta-google-client-id", "google-client-id"); + var emailVerificationRequired = builder.AddParameter("posta-email-verification-required", "true"); + + var posta = builder.AddPosta("posta", database, redis) + .WithSystemSmtp(options => options.Host = smtpHost) + .WithInboundSmtp(options => options.Enabled = inboundEnabled) + .WithS3BlobStorage(options => options.Bucket = s3Bucket) + .WithGoogleOAuth(options => options.ClientId = googleClientId) + .WithEmailVerification(options => options.Required = emailVerificationRequired); + + Assert.True(posta.Resource.TryGetAnnotationsOfType(out var annotations)); + + var context = new EnvironmentCallbackContext( + new DistributedApplicationExecutionContext( + new DistributedApplicationExecutionContextOptions(DistributedApplicationOperation.Run))); + + foreach (var annotation in annotations) + { + await annotation.Callback(context); + } + + var env = context.EnvironmentVariables; + + Assert.Same(smtpHost.Resource, env["POSTA_SYSTEM_SMTP_HOST"]); + Assert.Same(inboundEnabled.Resource, env["POSTA_INBOUND_ENABLED"]); + Assert.Equal("s3", env["POSTA_BLOB_PROVIDER"]); + Assert.Same(s3Bucket.Resource, env["POSTA_BLOB_S3_BUCKET"]); + Assert.Same(googleClientId.Resource, env["POSTA_GOOGLE_OAUTH_CLIENT_ID"]); + Assert.Same(emailVerificationRequired.Resource, env["POSTA_EMAIL_VERIFICATION_REQUIRED"]); + } + + private static (IResourceBuilder Database, IResourceBuilder Redis) AddPostaDependencies(IDistributedApplicationBuilder builder) + { + var postgres = builder.AddPostgres("postgres"); + var database = postgres.AddDatabase("posta-db", "posta"); + var redis = builder.AddRedis("redis"); + + return (database, redis); + } +} diff --git a/tests/CommunityToolkit.Aspire.Posta.Tests/CommunityToolkit.Aspire.Posta.Tests.csproj b/tests/CommunityToolkit.Aspire.Posta.Tests/CommunityToolkit.Aspire.Posta.Tests.csproj new file mode 100644 index 000000000..6ad99e918 --- /dev/null +++ b/tests/CommunityToolkit.Aspire.Posta.Tests/CommunityToolkit.Aspire.Posta.Tests.csproj @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/tests/CommunityToolkit.Aspire.Posta.Tests/PostaClientTests.cs b/tests/CommunityToolkit.Aspire.Posta.Tests/PostaClientTests.cs new file mode 100644 index 000000000..f3c859e7c --- /dev/null +++ b/tests/CommunityToolkit.Aspire.Posta.Tests/PostaClientTests.cs @@ -0,0 +1,162 @@ +using CommunityToolkit.Aspire.Posta.Clients; +using CommunityToolkit.Aspire.Posta.Configuration; +using CommunityToolkit.Aspire.Posta.Endpoints; +using CommunityToolkit.Aspire.Posta.Models.Emails; +using CommunityToolkit.Aspire.Posta.Transport; +using System.Net.Http.Headers; +using System.Text; + +namespace CommunityToolkit.Aspire.Posta.Tests; + +public class PostaClientTests +{ + private static readonly string[] s_queryTags = ["one", "two"]; + + [Fact] + public void EndpointCatalogContainsEveryOpenApiOperation() + { + var endpoints = typeof(IPostaEndpoints).GetProperties(); + + Assert.Equal(21, endpoints.Length); + Assert.All(endpoints, property => Assert.Equal(typeof(PostaEndpoint), property.PropertyType)); + } + + [Fact] + public void SectionInterfacesContainEveryNamedOperation() + { + Type[] sections = + [ + typeof(IPostaEmailsClient), typeof(IPostaTemplatesClient), typeof(IPostaSubscriberListsClient), + typeof(IPostaHealthClient), typeof(IPostaInfoClient) + ]; + + int operationCount = sections.Sum(section => section.GetMethods().Count(method => method.DeclaringType == section)); + + Assert.Equal(21, operationCount); + } + + [Fact] + public async Task NamedOperationSerializesRequestAndDeserializesTypedResponse() + { + RecordingHandler handler = new(_ => new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent("{\"success\":true,\"data\":{\"id\":\"email-id\",\"status\":\"queued\"}}", Encoding.UTF8, "application/json") + }); + PostaTransport transport = CreateTransport(handler, apiKey: "secret-key"); + PostaClientSection client = new(transport, new PostaEndpoints()); + + SendAnEmailResponse? response = await client.SendAnEmailAsync( + new SendAnEmailRequest + { + From = "sender@example.com", + To = ["recipient@example.com"], + Subject = "Hello", + DryRun = true + }, + TestContext.Current.CancellationToken); + + Assert.NotNull(response); + Assert.True(response.Success); + Assert.Equal("email-id", response.Data?.Id); + Assert.Equal("queued", response.Data?.Status); + Assert.Equal("/api/v1/emails/send?dry_run=true", handler.Request!.RequestUri!.PathAndQuery); + Assert.Contains("\"from\":\"sender@example.com\"", handler.Body, StringComparison.Ordinal); + Assert.DoesNotContain("dry_run", handler.Body, StringComparison.Ordinal); + } + + [Fact] + public async Task SendAsyncExpandsPathAndQueryAndUsesApiKey() + { + RecordingHandler handler = new(_ => new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent("{\"status\":\"queued\"}", Encoding.UTF8, "application/json") + }); + PostaTransport transport = CreateTransport(handler, apiKey: "secret-key"); + + using var result = await transport.SendAsync( + new PostaEndpoint(HttpMethod.Post, "/api/v1/emails/{id}", PostaAuthentication.ApiKey), + new PostaRequest + { + PathParameters = new Dictionary { ["id"] = "a/b" }, + Query = new Dictionary { ["dry_run"] = true, ["tag"] = s_queryTags }, + Body = new { subject = "Hello" } + }, + TestContext.Current.CancellationToken); + + Assert.Equal("/api/v1/emails/a%2Fb?dry_run=true&tag=one&tag=two", handler.Request!.RequestUri!.PathAndQuery); + Assert.Equal(new AuthenticationHeaderValue("Bearer", "secret-key"), handler.Request.Headers.Authorization); + Assert.Contains("\"subject\":\"Hello\"", handler.Body, StringComparison.Ordinal); + Assert.Equal("queued", result!.RootElement.GetProperty("status").GetString()); + } + + [Fact] + public async Task SendAsyncDoesNotSendCredentialToAnonymousEndpoint() + { + RecordingHandler handler = new(_ => new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent("{}", Encoding.UTF8, "application/json") + }); + PostaTransport transport = CreateTransport(handler, apiKey: "must-not-leak"); + + using var result = await transport.SendAsync( + new PostaEndpoint(HttpMethod.Get, "/healthz", PostaAuthentication.None), + null, + TestContext.Current.CancellationToken); + + Assert.Null(handler.Request!.Headers.Authorization); + } + + [Fact] + public async Task SendAsyncPreservesErrorResponse() + { + RecordingHandler handler = new(_ => new HttpResponseMessage(HttpStatusCode.TooManyRequests) + { + Content = new StringContent("{\"error\":\"quota exceeded\"}", Encoding.UTF8, "application/json") + }); + PostaTransport transport = CreateTransport(handler, apiKey: "key"); + + PostaApiException exception = await Assert.ThrowsAsync(() => transport.SendAsync( + new PostaEndpoint(HttpMethod.Post, "/api/v1/emails/send", PostaAuthentication.ApiKey), + null, + TestContext.Current.CancellationToken)); + + Assert.Equal(HttpStatusCode.TooManyRequests, exception.StatusCode); + Assert.Contains("quota exceeded", exception.ResponseBody, StringComparison.Ordinal); + } + + [Fact] + public void EndpointCanBeOverridden() + { + IPostaEndpoints endpoints = new CustomEndpoints(); + + Assert.Equal("/custom/emails/send", endpoints.SendAnEmail.Path); + } + + private static PostaTransport CreateTransport(RecordingHandler handler, string? apiKey = null, string? accessToken = null) + { + HttpClient client = new(handler) { BaseAddress = new Uri("https://posta.example") }; + return new PostaTransport(client, new PostaCredentialProvider(new PostaClientSettings + { + ApiKey = apiKey, + AccessToken = accessToken + })); + } + + private sealed class CustomEndpoints : PostaEndpoints + { + public override PostaEndpoint SendAnEmail { get; } = new(HttpMethod.Post, "/custom/emails/send", PostaAuthentication.ApiKey); + } + + private sealed class RecordingHandler(Func responseFactory) : HttpMessageHandler + { + public HttpRequestMessage? Request { get; private set; } + public string? Body { get; private set; } + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + Request = request; + Body = request.Content is null ? null : await request.Content.ReadAsStringAsync(cancellationToken); + return responseFactory(request); + } + } +} \ No newline at end of file