From 776688a18b15ff8f561889819b7b3041b0030fce Mon Sep 17 00:00:00 2001 From: Arno Noordover Date: Tue, 4 Feb 2025 00:09:11 +0100 Subject: [PATCH] #240 add papercut smtp hosting (#423) * #240 add papercut smtp hosting * #240 Deleted api tag from papercut * #240 set endpointname for papercut unittest * #240 try to implement health * #240 changed unittest on papercut to call papercut directly * #240 changed README.md's * #240 revert changes in non-papercut places * #240 revert non-papercut changes * #240: update Papercut SMTP integration and tests * #240 changed README.md to reflect the new connectionstring convention --------- Co-authored-by: Aaron Powell --- CommunityToolkit.Aspire.sln | 38 ++++++ ...Aspire.Hosting.PapercutSmtp.AppHost.csproj | 23 ++++ .../Program.cs | 12 ++ .../appsettings.json | 9 ++ ...re.Hosting.PapercutSmtp.SendMailApi.csproj | 16 +++ ...pire.Hosting.PapercutSmtp.SendMailApi.http | 13 ++ .../MailData.cs | 15 +++ .../Program.cs | 44 +++++++ .../appsettings.json | 9 ++ ...osting.PapercutSmtp.ServiceDefaults.csproj | 21 ++++ .../Extensions.cs | 118 ++++++++++++++++++ ...Toolkit.Aspire.Hosting.PapercutSmtp.csproj | 15 +++ .../PapercutSmtpContainerImageTags.cs | 8 ++ .../PapercutSmtpContainerResource.cs | 21 ++++ .../PapercutSmtpHostingExtension.cs | 42 +++++++ .../PublicAPI.Shipped.txt | 1 + .../PublicAPI.Unshipped.txt | 6 + .../README.md | 39 ++++++ .../AppHostTests.cs | 66 ++++++++++ ...t.Aspire.Hosting.PapercutSmtp.Tests.csproj | 14 +++ .../ContainerResourceCreationTests.cs | 42 +++++++ 21 files changed, 572 insertions(+) create mode 100644 examples/papercut/CommunityToolkit.Aspire.Hosting.PapercutSmtp.AppHost/CommunityToolkit.Aspire.Hosting.PapercutSmtp.AppHost.csproj create mode 100644 examples/papercut/CommunityToolkit.Aspire.Hosting.PapercutSmtp.AppHost/Program.cs create mode 100644 examples/papercut/CommunityToolkit.Aspire.Hosting.PapercutSmtp.AppHost/appsettings.json create mode 100644 examples/papercut/CommunityToolkit.Aspire.Hosting.PapercutSmtp.SendMailApi/CommunityToolkit.Aspire.Hosting.PapercutSmtp.SendMailApi.csproj create mode 100644 examples/papercut/CommunityToolkit.Aspire.Hosting.PapercutSmtp.SendMailApi/CommunityToolkit.Aspire.Hosting.PapercutSmtp.SendMailApi.http create mode 100644 examples/papercut/CommunityToolkit.Aspire.Hosting.PapercutSmtp.SendMailApi/MailData.cs create mode 100644 examples/papercut/CommunityToolkit.Aspire.Hosting.PapercutSmtp.SendMailApi/Program.cs create mode 100644 examples/papercut/CommunityToolkit.Aspire.Hosting.PapercutSmtp.SendMailApi/appsettings.json create mode 100644 examples/papercut/CommunityToolkit.Aspire.Hosting.PapercutSmtp.ServiceDefaults/CommunityToolkit.Aspire.Hosting.PapercutSmtp.ServiceDefaults.csproj create mode 100644 examples/papercut/CommunityToolkit.Aspire.Hosting.PapercutSmtp.ServiceDefaults/Extensions.cs create mode 100644 src/CommunityToolkit.Aspire.Hosting.PapercutSmtp/CommunityToolkit.Aspire.Hosting.PapercutSmtp.csproj create mode 100644 src/CommunityToolkit.Aspire.Hosting.PapercutSmtp/PapercutSmtpContainerImageTags.cs create mode 100644 src/CommunityToolkit.Aspire.Hosting.PapercutSmtp/PapercutSmtpContainerResource.cs create mode 100644 src/CommunityToolkit.Aspire.Hosting.PapercutSmtp/PapercutSmtpHostingExtension.cs create mode 100644 src/CommunityToolkit.Aspire.Hosting.PapercutSmtp/PublicAPI.Shipped.txt create mode 100644 src/CommunityToolkit.Aspire.Hosting.PapercutSmtp/PublicAPI.Unshipped.txt create mode 100644 src/CommunityToolkit.Aspire.Hosting.PapercutSmtp/README.md create mode 100644 tests/CommunityToolkit.Aspire.Hosting.PapercutSmtp.Tests/AppHostTests.cs create mode 100644 tests/CommunityToolkit.Aspire.Hosting.PapercutSmtp.Tests/CommunityToolkit.Aspire.Hosting.PapercutSmtp.Tests.csproj create mode 100644 tests/CommunityToolkit.Aspire.Hosting.PapercutSmtp.Tests/ContainerResourceCreationTests.cs diff --git a/CommunityToolkit.Aspire.sln b/CommunityToolkit.Aspire.sln index ca519b7a..283241b0 100644 --- a/CommunityToolkit.Aspire.sln +++ b/CommunityToolkit.Aspire.sln @@ -233,6 +233,18 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Aspire.Mic EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Aspire.Microsoft.EntityFrameworkCore.Sqlite.Tests", "tests\CommunityToolkit.Aspire.Microsoft.EntityFrameworkCore.Sqlite.Tests\CommunityToolkit.Aspire.Microsoft.EntityFrameworkCore.Sqlite.Tests.csproj", "{52846E18-99D1-4040-AF5F-17FC69198BCE}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Aspire.Hosting.PapercutSmtp", "src\CommunityToolkit.Aspire.Hosting.PapercutSmtp\CommunityToolkit.Aspire.Hosting.PapercutSmtp.csproj", "{E267907F-4467-4504-9947-2A5A6940DE9B}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "papercut", "papercut", "{2B0503E5-4FD5-4ED5-8AFA-8FC9609FCEC1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Aspire.Hosting.PapercutSmtp.AppHost", "examples\papercut\CommunityToolkit.Aspire.Hosting.PapercutSmtp.AppHost\CommunityToolkit.Aspire.Hosting.PapercutSmtp.AppHost.csproj", "{9772004D-0905-421A-98C6-DAE1C35080B3}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Aspire.Hosting.PapercutSmtp.ServiceDefaults", "examples\papercut\CommunityToolkit.Aspire.Hosting.PapercutSmtp.ServiceDefaults\CommunityToolkit.Aspire.Hosting.PapercutSmtp.ServiceDefaults.csproj", "{449B3661-A6F2-46D3-8EC9-B515362A72C1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Aspire.Hosting.PapercutSmtp.SendMailApi", "examples\papercut\CommunityToolkit.Aspire.Hosting.PapercutSmtp.SendMailApi\CommunityToolkit.Aspire.Hosting.PapercutSmtp.SendMailApi.csproj", "{70A77931-7D25-4CBF-AE55-F5A3B932E9F8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Aspire.Hosting.PapercutSmtp.Tests", "tests\CommunityToolkit.Aspire.Hosting.PapercutSmtp.Tests\CommunityToolkit.Aspire.Hosting.PapercutSmtp.Tests.csproj", "{80CCC017-3821-4F7D-902C-BB71DE875F58}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Aspire.Hosting.Dapr", "src\CommunityToolkit.Aspire.Hosting.Dapr\CommunityToolkit.Aspire.Hosting.Dapr.csproj", "{2165F65B-83F2-4269-8781-86AB6ACF043D}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Aspire.Hosting.Dapr.Tests", "tests\CommunityToolkit.Aspire.Hosting.Dapr.Tests\CommunityToolkit.Aspire.Hosting.Dapr.Tests.csproj", "{B2384D1A-DD13-4D03-B8FE-B194DEF71A0C}" @@ -643,6 +655,26 @@ Global {52846E18-99D1-4040-AF5F-17FC69198BCE}.Debug|Any CPU.Build.0 = Debug|Any CPU {52846E18-99D1-4040-AF5F-17FC69198BCE}.Release|Any CPU.ActiveCfg = Release|Any CPU {52846E18-99D1-4040-AF5F-17FC69198BCE}.Release|Any CPU.Build.0 = Release|Any CPU + {E267907F-4467-4504-9947-2A5A6940DE9B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E267907F-4467-4504-9947-2A5A6940DE9B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E267907F-4467-4504-9947-2A5A6940DE9B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E267907F-4467-4504-9947-2A5A6940DE9B}.Release|Any CPU.Build.0 = Release|Any CPU + {9772004D-0905-421A-98C6-DAE1C35080B3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9772004D-0905-421A-98C6-DAE1C35080B3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9772004D-0905-421A-98C6-DAE1C35080B3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9772004D-0905-421A-98C6-DAE1C35080B3}.Release|Any CPU.Build.0 = Release|Any CPU + {449B3661-A6F2-46D3-8EC9-B515362A72C1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {449B3661-A6F2-46D3-8EC9-B515362A72C1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {449B3661-A6F2-46D3-8EC9-B515362A72C1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {449B3661-A6F2-46D3-8EC9-B515362A72C1}.Release|Any CPU.Build.0 = Release|Any CPU + {70A77931-7D25-4CBF-AE55-F5A3B932E9F8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {70A77931-7D25-4CBF-AE55-F5A3B932E9F8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {70A77931-7D25-4CBF-AE55-F5A3B932E9F8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {70A77931-7D25-4CBF-AE55-F5A3B932E9F8}.Release|Any CPU.Build.0 = Release|Any CPU + {80CCC017-3821-4F7D-902C-BB71DE875F58}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {80CCC017-3821-4F7D-902C-BB71DE875F58}.Debug|Any CPU.Build.0 = Debug|Any CPU + {80CCC017-3821-4F7D-902C-BB71DE875F58}.Release|Any CPU.ActiveCfg = Release|Any CPU + {80CCC017-3821-4F7D-902C-BB71DE875F58}.Release|Any CPU.Build.0 = Release|Any CPU {2165F65B-83F2-4269-8781-86AB6ACF043D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {2165F65B-83F2-4269-8781-86AB6ACF043D}.Debug|Any CPU.Build.0 = Debug|Any CPU {2165F65B-83F2-4269-8781-86AB6ACF043D}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -816,6 +848,12 @@ Global {0E6EBCFB-DEF5-496C-95AF-00884826CFC8} = {899F0713-7FC6-4750-BAFC-AC650B35B453} {861FE61C-90EE-49B0-BCC8-8417C293CC21} = {899F0713-7FC6-4750-BAFC-AC650B35B453} {52846E18-99D1-4040-AF5F-17FC69198BCE} = {899F0713-7FC6-4750-BAFC-AC650B35B453} + {E267907F-4467-4504-9947-2A5A6940DE9B} = {414151D4-7009-4E78-A5C6-D99EBD1E67D1} + {2B0503E5-4FD5-4ED5-8AFA-8FC9609FCEC1} = {8519CC01-1370-47C8-AD94-B0F326B1563F} + {9772004D-0905-421A-98C6-DAE1C35080B3} = {2B0503E5-4FD5-4ED5-8AFA-8FC9609FCEC1} + {449B3661-A6F2-46D3-8EC9-B515362A72C1} = {2B0503E5-4FD5-4ED5-8AFA-8FC9609FCEC1} + {70A77931-7D25-4CBF-AE55-F5A3B932E9F8} = {2B0503E5-4FD5-4ED5-8AFA-8FC9609FCEC1} + {80CCC017-3821-4F7D-902C-BB71DE875F58} = {899F0713-7FC6-4750-BAFC-AC650B35B453} {2165F65B-83F2-4269-8781-86AB6ACF043D} = {414151D4-7009-4E78-A5C6-D99EBD1E67D1} {B2384D1A-DD13-4D03-B8FE-B194DEF71A0C} = {899F0713-7FC6-4750-BAFC-AC650B35B453} {E3C2B4B7-B3B0-4E7F-A975-A6C7FD926792} = {8519CC01-1370-47C8-AD94-B0F326B1563F} diff --git a/examples/papercut/CommunityToolkit.Aspire.Hosting.PapercutSmtp.AppHost/CommunityToolkit.Aspire.Hosting.PapercutSmtp.AppHost.csproj b/examples/papercut/CommunityToolkit.Aspire.Hosting.PapercutSmtp.AppHost/CommunityToolkit.Aspire.Hosting.PapercutSmtp.AppHost.csproj new file mode 100644 index 00000000..94a3e378 --- /dev/null +++ b/examples/papercut/CommunityToolkit.Aspire.Hosting.PapercutSmtp.AppHost/CommunityToolkit.Aspire.Hosting.PapercutSmtp.AppHost.csproj @@ -0,0 +1,23 @@ + + + + + + Exe + enable + enable + true + f60c6ce9-5628-467c-a6fc-2fc7938b38ad + + + + + + + + + + + + + diff --git a/examples/papercut/CommunityToolkit.Aspire.Hosting.PapercutSmtp.AppHost/Program.cs b/examples/papercut/CommunityToolkit.Aspire.Hosting.PapercutSmtp.AppHost/Program.cs new file mode 100644 index 00000000..af8922b9 --- /dev/null +++ b/examples/papercut/CommunityToolkit.Aspire.Hosting.PapercutSmtp.AppHost/Program.cs @@ -0,0 +1,12 @@ +using Aspire.Hosting; +using Projects; + +var builder = DistributedApplication.CreateBuilder(args); + +var papercut = builder.AddPapercutSmtp("papercut"); + +var sendmail = builder.AddProject("sendmail") + .WithReference(papercut) + .WaitFor(papercut); + +builder.Build().Run(); diff --git a/examples/papercut/CommunityToolkit.Aspire.Hosting.PapercutSmtp.AppHost/appsettings.json b/examples/papercut/CommunityToolkit.Aspire.Hosting.PapercutSmtp.AppHost/appsettings.json new file mode 100644 index 00000000..31c092aa --- /dev/null +++ b/examples/papercut/CommunityToolkit.Aspire.Hosting.PapercutSmtp.AppHost/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Aspire.Hosting.Dcp": "Warning" + } + } +} diff --git a/examples/papercut/CommunityToolkit.Aspire.Hosting.PapercutSmtp.SendMailApi/CommunityToolkit.Aspire.Hosting.PapercutSmtp.SendMailApi.csproj b/examples/papercut/CommunityToolkit.Aspire.Hosting.PapercutSmtp.SendMailApi/CommunityToolkit.Aspire.Hosting.PapercutSmtp.SendMailApi.csproj new file mode 100644 index 00000000..dcd020c2 --- /dev/null +++ b/examples/papercut/CommunityToolkit.Aspire.Hosting.PapercutSmtp.SendMailApi/CommunityToolkit.Aspire.Hosting.PapercutSmtp.SendMailApi.csproj @@ -0,0 +1,16 @@ + + + + enable + enable + + + + + + + + + + + diff --git a/examples/papercut/CommunityToolkit.Aspire.Hosting.PapercutSmtp.SendMailApi/CommunityToolkit.Aspire.Hosting.PapercutSmtp.SendMailApi.http b/examples/papercut/CommunityToolkit.Aspire.Hosting.PapercutSmtp.SendMailApi/CommunityToolkit.Aspire.Hosting.PapercutSmtp.SendMailApi.http new file mode 100644 index 00000000..00c5725b --- /dev/null +++ b/examples/papercut/CommunityToolkit.Aspire.Hosting.PapercutSmtp.SendMailApi/CommunityToolkit.Aspire.Hosting.PapercutSmtp.SendMailApi.http @@ -0,0 +1,13 @@ +@CommunityToolkit.Aspire.Hosting.PapercutSmtp.SendMailApi_HostAddress = http://localhost:5075 + +POST http://localhost:5075/send +Accept: application/json +Content-Type: application/json + +{ + "From": "test@test.nl", + "To": "toTest@test.nl", + "Body": "Hello World", + "Subject": "Test" +} +### diff --git a/examples/papercut/CommunityToolkit.Aspire.Hosting.PapercutSmtp.SendMailApi/MailData.cs b/examples/papercut/CommunityToolkit.Aspire.Hosting.PapercutSmtp.SendMailApi/MailData.cs new file mode 100644 index 00000000..f3b88edc --- /dev/null +++ b/examples/papercut/CommunityToolkit.Aspire.Hosting.PapercutSmtp.SendMailApi/MailData.cs @@ -0,0 +1,15 @@ +using System.ComponentModel.DataAnnotations; + +namespace CommunityToolkit.Aspire.Hosting.PapercutSmtp.SendMailApi; + +public class MailData +{ + [Required] + public required string From { get; set; } + [Required] + public required string To { get; set; } + [Required] + public required string Body { get; set; } + [Required] + public required string Subject { get; set; } +} \ No newline at end of file diff --git a/examples/papercut/CommunityToolkit.Aspire.Hosting.PapercutSmtp.SendMailApi/Program.cs b/examples/papercut/CommunityToolkit.Aspire.Hosting.PapercutSmtp.SendMailApi/Program.cs new file mode 100644 index 00000000..eb5e9f5e --- /dev/null +++ b/examples/papercut/CommunityToolkit.Aspire.Hosting.PapercutSmtp.SendMailApi/Program.cs @@ -0,0 +1,44 @@ +using CommunityToolkit.Aspire.Hosting.PapercutSmtp.SendMailApi; +using Microsoft.AspNetCore.Mvc; +using System.Data.Common; +using System.Net.Mail; + +WebApplicationBuilder builder = WebApplication.CreateBuilder(args); + +string? papercutConnectionString = builder.Configuration.GetConnectionString("papercut"); +DbConnectionStringBuilder connectionBuilder = new() +{ + ConnectionString = papercutConnectionString +}; + +Uri endpoint = new(connectionBuilder["Endpoint"].ToString()!, UriKind.Absolute); +builder.Services.AddScoped(_ => new SmtpClient(endpoint.Host, endpoint.Port)); +builder.AddServiceDefaults(); +WebApplication app = builder.Build(); + +app.MapPost("/send", ([FromBody]MailData mailData, [FromServices] SmtpClient smtpClient) => + { + MailMessage myMail = CreateMailMessage(mailData); + smtpClient.Send(myMail); + }) +.WithName("SendMail"); + +app.MapGet("/health", () => "OK"); + +app.MapDefaultEndpoints(); +app.Run(); +return; + +MailMessage CreateMailMessage(MailData mailData) +{ + MailAddress from = new(mailData.From, "TestFromName"); + MailAddress to = new(mailData.To, "TestToName"); + MailMessage mailMessage = new(from, to); + MailAddress replyTo = new(mailData.From); + mailMessage.ReplyToList.Add(replyTo); + mailMessage.Subject = mailData.Subject; + mailMessage.SubjectEncoding = System.Text.Encoding.UTF8; + mailMessage.Body = mailData.Body; + mailMessage.BodyEncoding = System.Text.Encoding.UTF8; + return mailMessage; +} \ No newline at end of file diff --git a/examples/papercut/CommunityToolkit.Aspire.Hosting.PapercutSmtp.SendMailApi/appsettings.json b/examples/papercut/CommunityToolkit.Aspire.Hosting.PapercutSmtp.SendMailApi/appsettings.json new file mode 100644 index 00000000..10f68b8c --- /dev/null +++ b/examples/papercut/CommunityToolkit.Aspire.Hosting.PapercutSmtp.SendMailApi/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/examples/papercut/CommunityToolkit.Aspire.Hosting.PapercutSmtp.ServiceDefaults/CommunityToolkit.Aspire.Hosting.PapercutSmtp.ServiceDefaults.csproj b/examples/papercut/CommunityToolkit.Aspire.Hosting.PapercutSmtp.ServiceDefaults/CommunityToolkit.Aspire.Hosting.PapercutSmtp.ServiceDefaults.csproj new file mode 100644 index 00000000..caa6344d --- /dev/null +++ b/examples/papercut/CommunityToolkit.Aspire.Hosting.PapercutSmtp.ServiceDefaults/CommunityToolkit.Aspire.Hosting.PapercutSmtp.ServiceDefaults.csproj @@ -0,0 +1,21 @@ + + + + enable + enable + true + + + + + + + + + + + + + + + diff --git a/examples/papercut/CommunityToolkit.Aspire.Hosting.PapercutSmtp.ServiceDefaults/Extensions.cs b/examples/papercut/CommunityToolkit.Aspire.Hosting.PapercutSmtp.ServiceDefaults/Extensions.cs new file mode 100644 index 00000000..8cf2bbd1 --- /dev/null +++ b/examples/papercut/CommunityToolkit.Aspire.Hosting.PapercutSmtp.ServiceDefaults/Extensions.cs @@ -0,0 +1,118 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Logging; +using OpenTelemetry; +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; + +namespace Microsoft.Extensions.Hosting; + +// Adds common .NET Aspire services: service discovery, resilience, health checks, and OpenTelemetry. +// This project should be referenced by each service project in your solution. +// To learn more about using this project, see https://aka.ms/dotnet/aspire/service-defaults +public static class Extensions +{ + public static TBuilder AddServiceDefaults(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + builder.ConfigureOpenTelemetry(); + + builder.AddDefaultHealthChecks(); + + builder.Services.AddServiceDiscovery(); + + builder.Services.ConfigureHttpClientDefaults(http => + { + // Turn on resilience by default + http.AddStandardResilienceHandler(); + + // Turn on service discovery by default + http.AddServiceDiscovery(); + }); + + // Uncomment the following to restrict the allowed schemes for service discovery. + // builder.Services.Configure(options => + // { + // options.AllowedSchemes = ["https"]; + // }); + + return builder; + } + + public static TBuilder ConfigureOpenTelemetry(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + builder.Logging.AddOpenTelemetry(logging => + { + logging.IncludeFormattedMessage = true; + logging.IncludeScopes = true; + }); + + builder.Services.AddOpenTelemetry() + .WithMetrics(metrics => + { + metrics.AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .AddRuntimeInstrumentation(); + }) + .WithTracing(tracing => + { + tracing.AddSource(builder.Environment.ApplicationName) + .AddAspNetCoreInstrumentation() + // Uncomment the following line to enable gRPC instrumentation (requires the OpenTelemetry.Instrumentation.GrpcNetClient package) + //.AddGrpcClientInstrumentation() + .AddHttpClientInstrumentation(); + }); + + builder.AddOpenTelemetryExporters(); + + return builder; + } + + private static TBuilder AddOpenTelemetryExporters(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]); + + if (useOtlpExporter) + { + builder.Services.AddOpenTelemetry().UseOtlpExporter(); + } + + // Uncomment the following lines to enable the Azure Monitor exporter (requires the Azure.Monitor.OpenTelemetry.AspNetCore package) + //if (!string.IsNullOrEmpty(builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"])) + //{ + // builder.Services.AddOpenTelemetry() + // .UseAzureMonitor(); + //} + + return builder; + } + + public static TBuilder AddDefaultHealthChecks(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + builder.Services.AddHealthChecks() + // Add a default liveness check to ensure app is responsive + .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]); + + return builder; + } + + public static WebApplication MapDefaultEndpoints(this WebApplication app) + { + // Adding health checks endpoints to applications in non-development environments has security implications. + // See https://aka.ms/dotnet/aspire/healthchecks for details before enabling these endpoints in non-development environments. + if (app.Environment.IsDevelopment()) + { + // All health checks must pass for app to be considered ready to accept traffic after starting + app.MapHealthChecks("/health"); + + // Only health checks tagged with the "live" tag must pass for app to be considered alive + app.MapHealthChecks("/alive", new HealthCheckOptions + { + Predicate = r => r.Tags.Contains("live") + }); + } + + return app; + } +} diff --git a/src/CommunityToolkit.Aspire.Hosting.PapercutSmtp/CommunityToolkit.Aspire.Hosting.PapercutSmtp.csproj b/src/CommunityToolkit.Aspire.Hosting.PapercutSmtp/CommunityToolkit.Aspire.Hosting.PapercutSmtp.csproj new file mode 100644 index 00000000..2059dbe2 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.PapercutSmtp/CommunityToolkit.Aspire.Hosting.PapercutSmtp.csproj @@ -0,0 +1,15 @@ + + + + An Aspire component leveraging Papercut SMTP container. + papercut smtp hosting + + + + + + + + + + diff --git a/src/CommunityToolkit.Aspire.Hosting.PapercutSmtp/PapercutSmtpContainerImageTags.cs b/src/CommunityToolkit.Aspire.Hosting.PapercutSmtp/PapercutSmtpContainerImageTags.cs new file mode 100644 index 00000000..9a1e4733 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.PapercutSmtp/PapercutSmtpContainerImageTags.cs @@ -0,0 +1,8 @@ +namespace CommunityToolkit.Aspire.Hosting.PapercutSmtp; + +internal static class PapercutSmtpContainerImageTags +{ + public const string Registry = "docker.io"; + public const string Image = "changemakerstudiosus/papercut-smtp"; + public const string Tag = "7.0.0-rc1"; +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Hosting.PapercutSmtp/PapercutSmtpContainerResource.cs b/src/CommunityToolkit.Aspire.Hosting.PapercutSmtp/PapercutSmtpContainerResource.cs new file mode 100644 index 00000000..398254eb --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.PapercutSmtp/PapercutSmtpContainerResource.cs @@ -0,0 +1,21 @@ +namespace Aspire.Hosting.ApplicationModel; + +/// +/// Resource for the Papercut SMTP server. +/// +/// +public class PapercutSmtpContainerResource(string name) : ContainerResource(name), IResourceWithConnectionString +{ + internal const int HttpEndpointPort = 80; + internal const int SmtpEndpointPort = 25; + internal const string HttpEndpointName = "http"; + internal const string SmtpEndpointName = "smtp"; + private EndpointReference? _smtpEndpoint; + private EndpointReference SmtpEndpoint => _smtpEndpoint ??= new EndpointReference(this, SmtpEndpointName); + + /// + /// ConnectionString for the Papercut SMTP server in the form of smtp://host:port. + /// + public ReferenceExpression ConnectionStringExpression => ReferenceExpression.Create( + $"Endpoint={SmtpEndpoint.Scheme}://{SmtpEndpoint.Property(EndpointProperty.Host)}:{SmtpEndpoint.Property(EndpointProperty.Port)}"); +} diff --git a/src/CommunityToolkit.Aspire.Hosting.PapercutSmtp/PapercutSmtpHostingExtension.cs b/src/CommunityToolkit.Aspire.Hosting.PapercutSmtp/PapercutSmtpHostingExtension.cs new file mode 100644 index 00000000..ad7aa84f --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.PapercutSmtp/PapercutSmtpHostingExtension.cs @@ -0,0 +1,42 @@ +using Aspire.Hosting.ApplicationModel; +using CommunityToolkit.Aspire.Hosting.PapercutSmtp; + +namespace Aspire.Hosting; + +/// +/// Provides extension methods for adding DataApiBuilder api to an . +/// +public static class PapercutSmtpHostingExtension +{ + /// + /// Adds Papercut SMTP to the application model. + /// + /// The to add the resource to. + /// The name of the resource. + /// The HTTP portnumber for the web-console to the Papercut SMTP container. + /// The SMTP portnumber for the Papercut SMTP Conteainer + /// A reference to the . + public static IResourceBuilder AddPapercutSmtp(this IDistributedApplicationBuilder builder, + [ResourceName] string name, + int? httpPort = null, + int? smtpPort = null) + { + ArgumentNullException.ThrowIfNull("Service name must be specified.", nameof(name)); + PapercutSmtpContainerResource resource = new(name); + + IResourceBuilder rb = builder.AddResource(resource) + .WithImage(PapercutSmtpContainerImageTags.Image) + .WithImageTag(PapercutSmtpContainerImageTags.Tag) + .WithImageRegistry(PapercutSmtpContainerImageTags.Registry) + .WithEndpoint(targetPort: PapercutSmtpContainerResource.SmtpEndpointPort, + port: smtpPort, + name: PapercutSmtpContainerResource.SmtpEndpointName, + scheme: "smtp") + .WithHttpEndpoint(targetPort: PapercutSmtpContainerResource.HttpEndpointPort, + port: httpPort, + name: PapercutSmtpContainerResource.HttpEndpointName) + .WithHttpHealthCheck("/health", httpPort, PapercutSmtpContainerResource.HttpEndpointName); + + return rb; + } +} diff --git a/src/CommunityToolkit.Aspire.Hosting.PapercutSmtp/PublicAPI.Shipped.txt b/src/CommunityToolkit.Aspire.Hosting.PapercutSmtp/PublicAPI.Shipped.txt new file mode 100644 index 00000000..7dc5c581 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.PapercutSmtp/PublicAPI.Shipped.txt @@ -0,0 +1 @@ +#nullable enable diff --git a/src/CommunityToolkit.Aspire.Hosting.PapercutSmtp/PublicAPI.Unshipped.txt b/src/CommunityToolkit.Aspire.Hosting.PapercutSmtp/PublicAPI.Unshipped.txt new file mode 100644 index 00000000..b16a5e3c --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.PapercutSmtp/PublicAPI.Unshipped.txt @@ -0,0 +1,6 @@ +#nullable enable +Aspire.Hosting.ApplicationModel.PapercutSmtpContainerResource +Aspire.Hosting.ApplicationModel.PapercutSmtpContainerResource.ConnectionStringExpression.get -> Aspire.Hosting.ApplicationModel.ReferenceExpression! +Aspire.Hosting.ApplicationModel.PapercutSmtpContainerResource.PapercutSmtpContainerResource(string! name) -> void +Aspire.Hosting.PapercutSmtpHostingExtension +static Aspire.Hosting.PapercutSmtpHostingExtension.AddPapercutSmtp(this Aspire.Hosting.IDistributedApplicationBuilder! builder, string! name, int? httpPort = null, int? smtpPort = null) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! diff --git a/src/CommunityToolkit.Aspire.Hosting.PapercutSmtp/README.md b/src/CommunityToolkit.Aspire.Hosting.PapercutSmtp/README.md new file mode 100644 index 00000000..bf4f949c --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.PapercutSmtp/README.md @@ -0,0 +1,39 @@ +# CommunityToolkit.Hosting.PapercutStmp + +## Overview + +This .NET Aspire Integration runs [Papercut SMTP](https://github.com/ChangemakerStudios/Papercut-SMTP) in a container. + + +## Usage + +The Papercut SMTP integration exposes a connection string with the format `endpoint=smtp://:`. +This connection string can be used to with a DbConnectionStringBuilder to get the smtp endpoint. + +### Example 1: Add Papercut SMTP with generated ports + +```csharp +var builder = DistributedApplication.CreateBuilder(args); + +var papercut = builder.AddPapercutSmtp("papercut"); + +var xyz = builder.AddProject("application") + .WithReference(papercut) + .WaitFor(papercut); + +builder.Build().Run(); +``` + +### Example 2: Add Papercut SMTP with user-defined ports + +```csharp +var builder = DistributedApplication.CreateBuilder(args); + +var papercut = builder.AddPapercutSmtp("papercut", 80, 25); + +var xyz = builder.AddProject("application") + .WithReference(papercut) + .WaitFor(papercut); + +builder.Build().Run(); +``` diff --git a/tests/CommunityToolkit.Aspire.Hosting.PapercutSmtp.Tests/AppHostTests.cs b/tests/CommunityToolkit.Aspire.Hosting.PapercutSmtp.Tests/AppHostTests.cs new file mode 100644 index 00000000..b5ad16fc --- /dev/null +++ b/tests/CommunityToolkit.Aspire.Hosting.PapercutSmtp.Tests/AppHostTests.cs @@ -0,0 +1,66 @@ +using Aspire.Components.Common.Tests; +using CommunityToolkit.Aspire.Testing; +using System.Data.Common; +using System.Net.Http.Json; +using System.Net.Mail; +using System.Text.Json; + +namespace CommunityToolkit.Aspire.Hosting.PapercutSmtp.Tests; + +[RequiresDocker] +public class AppHostTests(AspireIntegrationTestFixture fixture) : IClassFixture> +{ + private const string ResourceName = "papercut"; + + [Fact] + public async Task ResourceStartsAndMailShouldBeReceived() + { + await fixture.ResourceNotificationService.WaitForResourceHealthyAsync(ResourceName).WaitAsync(TimeSpan.FromMinutes(2)); + SmtpClient smtpClient = await CreateSmtpClient(); + MailMessage myMail = CreateMailMessage(); + HttpClient httpClient = CreateHttpClientForPapercut(); + + Exception? exception = Record.Exception(() => smtpClient.Send(myMail)); + + Assert.Null(exception); + HttpResponseMessage response = await httpClient.GetAsync("/api/Messages"); + response.EnsureSuccessStatusCode(); + + JsonDocument? responseDocument = await response.Content.ReadFromJsonAsync(); + Assert.NotNull(responseDocument); + int totalMessageCount = responseDocument.RootElement.GetProperty("totalMessageCount").GetInt32(); + Assert.Equal(1, totalMessageCount); + } + + private HttpClient CreateHttpClientForPapercut() + { + var httpEndpoint = fixture.GetEndpoint(ResourceName, "http"); + return new HttpClient { BaseAddress = httpEndpoint }; + } + + private static MailMessage CreateMailMessage() + { + MailAddress from = new("test@test.com", "TestFromName"); + MailAddress to = new("to@test.com", "TestToName"); + MailMessage myMail = new(from, to); + MailAddress replyTo = new("reply@test.com"); + myMail.ReplyToList.Add(replyTo); + myMail.Subject = "Subject"; + myMail.SubjectEncoding = System.Text.Encoding.UTF8; + myMail.Body = "Hello world!"; + myMail.BodyEncoding = System.Text.Encoding.UTF8; + return myMail; + } + + private async Task CreateSmtpClient() + { + string? connectionString = await fixture.GetConnectionString(ResourceName); + DbConnectionStringBuilder connectionBuilder = new() + { + ConnectionString = connectionString + }; + Uri connectionUri = new(connectionBuilder["Endpoint"].ToString()!, UriKind.Absolute); + SmtpClient smtpClient = new(connectionUri.Host, connectionUri.Port); + return smtpClient; + } +} \ No newline at end of file diff --git a/tests/CommunityToolkit.Aspire.Hosting.PapercutSmtp.Tests/CommunityToolkit.Aspire.Hosting.PapercutSmtp.Tests.csproj b/tests/CommunityToolkit.Aspire.Hosting.PapercutSmtp.Tests/CommunityToolkit.Aspire.Hosting.PapercutSmtp.Tests.csproj new file mode 100644 index 00000000..a14aa410 --- /dev/null +++ b/tests/CommunityToolkit.Aspire.Hosting.PapercutSmtp.Tests/CommunityToolkit.Aspire.Hosting.PapercutSmtp.Tests.csproj @@ -0,0 +1,14 @@ + + + + false + true + + + + + + + + + diff --git a/tests/CommunityToolkit.Aspire.Hosting.PapercutSmtp.Tests/ContainerResourceCreationTests.cs b/tests/CommunityToolkit.Aspire.Hosting.PapercutSmtp.Tests/ContainerResourceCreationTests.cs new file mode 100644 index 00000000..8979255e --- /dev/null +++ b/tests/CommunityToolkit.Aspire.Hosting.PapercutSmtp.Tests/ContainerResourceCreationTests.cs @@ -0,0 +1,42 @@ +using Aspire.Hosting; + +namespace CommunityToolkit.Aspire.Hosting.PapercutSmtp.Tests; + +public class ContainerResourceCreationTests +{ + [Fact] + public void AddPapercutSmtpBuilderShouldNotBeNull() + { + IDistributedApplicationBuilder builder = null!; + Assert.Throws(() => builder.AddPapercutSmtp("papercut")); + } + + [Fact] + public void AddPapercutSmtpBuilderNameShouldNotBeNullOrWhiteSpace() + { + IDistributedApplicationBuilder builder = DistributedApplication.CreateBuilder(); + + Assert.Throws(() => builder.AddPapercutSmtp(null!)); + } + + [Fact] + public void AddPapercutSmtpBuilderContainerDetailsSetOnResource() + { + IDistributedApplicationBuilder builder = DistributedApplication.CreateBuilder(); + + builder.AddPapercutSmtp("papercut"); + + using var app = builder.Build(); + var appModel = app.Services.GetRequiredService(); + + var resource = appModel.Resources.OfType().SingleOrDefault(); + + Assert.NotNull(resource); + Assert.Equal("papercut", resource.Name); + + Assert.True(resource.TryGetLastAnnotation(out ContainerImageAnnotation? imageAnnotations)); + Assert.Equal(PapercutSmtpContainerImageTags.Tag, imageAnnotations.Tag); + Assert.Equal(PapercutSmtpContainerImageTags.Image, imageAnnotations.Image); + Assert.Equal(PapercutSmtpContainerImageTags.Registry, imageAnnotations.Registry); + } +}