From af3d8ae751d081df8b73acae2e5856472ba843c9 Mon Sep 17 00:00:00 2001 From: James Hickey Date: Tue, 24 Sep 2024 21:28:59 -0300 Subject: [PATCH] New Custom Mailer Approach And Inline Mailables (#405) * new custom mailer and inline mailables * publish new version --- Demo/Controllers/MailController.cs | 19 + Demo/Demo.csproj | 1 - DocsV2/docs/Mailing/README.md | 110 +++-- Src/Coravel.Mailer/Coravel.Mailer.csproj | 2 +- Src/Coravel.Mailer/Mail/InlineMailable.cs | 8 + Src/Coravel.Mailer/Mail/InlineMailableT.cs | 8 + .../Mail/Interfaces/ICanSendMail.cs | 8 + Src/Coravel.Mailer/Mail/Interfaces/IMailer.cs | 3 +- Src/Coravel.Mailer/Mail/Mailable.cs | 9 +- .../Mail/Mailers/CanSendMailWrapper.cs | 40 ++ Src/Coravel.Mailer/MailServiceRegistration.cs | 52 +++ .../TestMvcApp/Controllers/MailController.cs | 80 ++++ .../Tests/Mail/MailerSmokeTests.cs | 24 ++ .../Mail/CanSendMailWrapperTests.cs | 226 ++++++++++ .../MailerUnitTests/Mail/GeneralMailTests.cs | 36 ++ .../Mail/InlineMailableTests.cs | 407 ++++++++++++++++++ 16 files changed, 1002 insertions(+), 31 deletions(-) create mode 100644 Src/Coravel.Mailer/Mail/InlineMailable.cs create mode 100644 Src/Coravel.Mailer/Mail/InlineMailableT.cs create mode 100644 Src/Coravel.Mailer/Mail/Interfaces/ICanSendMail.cs create mode 100644 Src/Coravel.Mailer/Mail/Mailers/CanSendMailWrapper.cs create mode 100644 Src/UnitTests/MailerUnitTests/Mail/CanSendMailWrapperTests.cs create mode 100644 Src/UnitTests/MailerUnitTests/Mail/InlineMailableTests.cs diff --git a/Demo/Controllers/MailController.cs b/Demo/Controllers/MailController.cs index c0a55ca8..6e4df14a 100644 --- a/Demo/Controllers/MailController.cs +++ b/Demo/Controllers/MailController.cs @@ -1,6 +1,7 @@ using System; using System.Threading.Tasks; using Coravel.Mailer.Mail.Interfaces; +using Coravel.Mailer.Mail; using Microsoft.AspNetCore.Mvc; using Demo.Mailables; using Demo.Models; @@ -75,6 +76,24 @@ public async Task RenderView() return Content(message, "text/html"); } + public async Task RenderViewWithInlineMailable() + { + UserModel user = new UserModel() + { + Email = "FromUserModel@test.com", + Name = "Coravel Test Person" + }; + + string message = await this._mailer.RenderAsync( + Mailable.AsInline() + .To(user) + .From("from@test.com") + .View("~/Views/Mail/NewUser.cshtml", user) + ); + + return Content(message, "text/html"); + } + public IActionResult QueueMail() { UserModel user = new UserModel() diff --git a/Demo/Demo.csproj b/Demo/Demo.csproj index a4e4ce37..a7fc9910 100644 --- a/Demo/Demo.csproj +++ b/Demo/Demo.csproj @@ -7,7 +7,6 @@ - diff --git a/DocsV2/docs/Mailing/README.md b/DocsV2/docs/Mailing/README.md index 409ab2af..02033b60 100644 --- a/DocsV2/docs/Mailing/README.md +++ b/DocsV2/docs/Mailing/README.md @@ -43,9 +43,17 @@ This will install the Nuget package `Coravel.Mailer`, along with scaffolding som ## Config -### Configure Services +### Configure Mailer -In `Startup.ConfigureServices()`: +From `Program.cs` in newer minimal .NET configurations: + +```csharp +var builder = WebApplication.CreateBuilder(args); + +builder.AddMailer(); +``` + +Using non-web projects, you would do this inside of your `Startup.ConfigureServices()` method: ```csharp services.AddMailer(this.Configuration); // Instance of IConfiguration. @@ -89,32 +97,36 @@ Add the following keys: #### Custom Driver -The custom driver allows you to decide how you want to send e-mails (through some API call, etc.). Because it requires a closure, you need to explicitly call `AddCustomMailer()` in `ConfigureServices()`: +The custom driver allows you to decide how you want to send e-mails - using HTTP APIs, something else, etc. + +The recommended way to configure a customer mailer is by implementing `Coravel.Mailer.Mail.Interfaces.ICanSendMail`. This class will allow your custom mailer to participate in the dependency injection configuration so that you can inject things like HTTP clients, etc. + +To configure it, in `Program.cs` instead of calling `AddMailer()` you do: ```csharp -// A local function with the expected signature. -// This defines how all e-mails are sent. -async Task SendMailCustomAsync( - string message, - string subject, - IEnumerable to, - MailRecipient from, - MailRecipient replyTo, - IEnumerable cc, - IEnumerable bcc, - IEnumerable attachments = null, - MailRecipient sender = null -) -{ - // Custom logic for sending an email. -} +var builder = WebApplication.CreateBuilder(args); -services.AddCustomMailer(this.Configuration, SendMailCustomAsync); +builder.AddCustomMailer(); ``` -:::warning -Breaking changes to this method signature are more likely than other as this is the signature that the Mailer's internals use. If a new version of the Mailer causes your code to stop compiling sucessfully, it's probably this signature that needs to be updated. Luckliy, it's usually a quick change in 1 spot. -::: +Here's an example of a custom mailer: + +```csharp +public class MyHttpApiCustomMailer : ICanSendMail +{ + private readonly IHttpClient _httpClient; + + public MyHttpApiCustomMailer(IHttpClientFactory httpFactory) + { + this._httpClient = httpFactory.CreateHttpClient("MailApi"); + } + + public async Task SendAsync(string message, string subject, IEnumerable to, MailRecipient from, MailRecipient replyTo, IEnumerable cc, IEnumerable bcc, IEnumerable attachments = null, MailRecipient sender = null) + { + // Code that uses the HttpClient to send mail via an HTTP API. + } +} +``` ### Built-In View Templates @@ -161,8 +173,8 @@ In your `appsettings.json`, you may add the following global values that will po ### Creating A Mailable -Coravel uses **Mailables** to send mail. Each Mailable is a c# class that represents a specific type of e-mail -that you can send, such as "New User Sign-up", "Completed Order", etc. +Coravel uses **Mailables** to send mail. You can create a C# class that represents a specific type of e-mail +that you can send, such as "New User Sign-up", "Completed Order", etc. This approach is useful whenever you want to encapsulate the logic for this email type and can re-use it across your application (see next section for an alternative approach). :::tip If you used the Coravel CLI, it already generated a sample Mailable in your `~/Mailables` folder! @@ -198,6 +210,54 @@ All of the configuration for a Mailable is done in the `Build()` method. You can then call various methods like `To` and `From` to configure the recipients, sender, etc. +#### Inline Mailables + +You can create mailables on-the-fly if this is preferred. From the code that is trying to send an email - like a controller action or pub/sub handler - you can call either `Mailable.AsInline()` or `Mailable.AsInline()`. + +The generic version allows you to send email using `View(string viewPath, T viewModel)`: + +```csharp +public async Task SendMyEmail() +{ + UserModel user = new UserModel() + { + Email = "FromUserModel@test.com", + Name = "Coravel Test Person" + }; + + await this._mailer.SendAsync( + Mailable.AsInline() + .To(user) + .From("from@test.com") + .View("~/Views/Mail/NewUser.cshtml", user) + ); + + return Ok(); +} +``` + +The non-generic version is suited to using `Html()` when passing a view model is not needed. + +```csharp +public async Task SendMyEmail() +{ + UserModel user = new UserModel() + { + Email = "FromUserModel@test.com", + Name = "Coravel Test Person" + }; + + await this._mailer.SendAsync( + Mailable.AsInline() + .To(user) + .From("from@test.com") + .Html($"

Welcome {user.Name}

") + ); + + return Ok(); +} +``` + ### From To specify who the email is from, use the `From()` method: diff --git a/Src/Coravel.Mailer/Coravel.Mailer.csproj b/Src/Coravel.Mailer/Coravel.Mailer.csproj index 1c26f8f9..6c7531ee 100644 --- a/Src/Coravel.Mailer/Coravel.Mailer.csproj +++ b/Src/Coravel.Mailer/Coravel.Mailer.csproj @@ -4,7 +4,7 @@ .net6.0 True Coravel.Mailer - 6.0.0 + 6.1.0 James Hickey - Coravel.Mailer diff --git a/Src/Coravel.Mailer/Mail/InlineMailable.cs b/Src/Coravel.Mailer/Mail/InlineMailable.cs new file mode 100644 index 00000000..b7d63237 --- /dev/null +++ b/Src/Coravel.Mailer/Mail/InlineMailable.cs @@ -0,0 +1,8 @@ +using Coravel.Mailer.Mail; + +public class InlineMailable : Mailable +{ public override void Build() + { + // No-op. This is built inline by the caller. + } +} \ No newline at end of file diff --git a/Src/Coravel.Mailer/Mail/InlineMailableT.cs b/Src/Coravel.Mailer/Mail/InlineMailableT.cs new file mode 100644 index 00000000..3edbe9f5 --- /dev/null +++ b/Src/Coravel.Mailer/Mail/InlineMailableT.cs @@ -0,0 +1,8 @@ +using Coravel.Mailer.Mail; + +public class InlineMailable : Mailable +{ public override void Build() + { + // No-op. This is built inline by the caller. + } +} \ No newline at end of file diff --git a/Src/Coravel.Mailer/Mail/Interfaces/ICanSendMail.cs b/Src/Coravel.Mailer/Mail/Interfaces/ICanSendMail.cs new file mode 100644 index 00000000..f40ec623 --- /dev/null +++ b/Src/Coravel.Mailer/Mail/Interfaces/ICanSendMail.cs @@ -0,0 +1,8 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Coravel.Mailer.Mail; + +public interface ICanSendMail +{ + Task SendAsync(string message, string subject, IEnumerable to, MailRecipient from, MailRecipient replyTo, IEnumerable cc, IEnumerable bcc, IEnumerable attachments = null, MailRecipient sender = null); +} \ No newline at end of file diff --git a/Src/Coravel.Mailer/Mail/Interfaces/IMailer.cs b/Src/Coravel.Mailer/Mail/Interfaces/IMailer.cs index 87c52b11..98da8c6a 100644 --- a/Src/Coravel.Mailer/Mail/Interfaces/IMailer.cs +++ b/Src/Coravel.Mailer/Mail/Interfaces/IMailer.cs @@ -3,10 +3,9 @@ namespace Coravel.Mailer.Mail.Interfaces { - public interface IMailer + public interface IMailer : ICanSendMail { Task RenderAsync(Mailable mailable); Task SendAsync(Mailable mailable); - Task SendAsync(string message, string subject, IEnumerable to, MailRecipient from, MailRecipient replyTo, IEnumerable cc, IEnumerable bcc, IEnumerable attachments = null, MailRecipient sender = null); } } \ No newline at end of file diff --git a/Src/Coravel.Mailer/Mail/Mailable.cs b/Src/Coravel.Mailer/Mail/Mailable.cs index 3b8722ed..17664dc2 100644 --- a/Src/Coravel.Mailer/Mail/Mailable.cs +++ b/Src/Coravel.Mailer/Mail/Mailable.cs @@ -67,8 +67,7 @@ public class Mailable /// /// View data to pass to the view to render. /// - private T _viewModel; - + private T _viewModel; public Mailable From(MailRecipient recipient) { this._from = recipient; @@ -266,4 +265,10 @@ private void BindSubjectField() } } } + + public class Mailable + { + public static InlineMailable AsInline() => new InlineMailable(); + public static InlineMailable AsInline() => new InlineMailable(); + } } \ No newline at end of file diff --git a/Src/Coravel.Mailer/Mail/Mailers/CanSendMailWrapper.cs b/Src/Coravel.Mailer/Mail/Mailers/CanSendMailWrapper.cs new file mode 100644 index 00000000..d6cc834d --- /dev/null +++ b/Src/Coravel.Mailer/Mail/Mailers/CanSendMailWrapper.cs @@ -0,0 +1,40 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Coravel.Mailer.Mail.Interfaces; +using Coravel.Mailer.Mail.Renderers; +using Microsoft.Extensions.DependencyInjection; + +namespace Coravel.Mailer.Mail.Mailers +{ + public class CanSendMailWrapper : IMailer where TCanSendMail : ICanSendMail + { + private RazorRenderer _renderer; + private MailRecipient _globalFrom; + private IServiceScopeFactory _scopeFactory; + + public CanSendMailWrapper(RazorRenderer renderer, IServiceScopeFactory scopeFactory, MailRecipient globalFrom = null) + { + this._renderer = renderer; + this._scopeFactory = scopeFactory; + this._globalFrom = globalFrom; + } + + public Task RenderAsync(Mailable mailable) => + mailable.RenderAsync(this._renderer, this); + + public async Task SendAsync(Mailable mailable) => + await mailable.SendAsync(this._renderer, this); + + public async Task SendAsync(string message, string subject, IEnumerable to, MailRecipient from, MailRecipient replyTo, IEnumerable cc, IEnumerable bcc, IEnumerable attachments, MailRecipient sender = null) + { + await using (var scope = this._scopeFactory.CreateAsyncScope()) + { + var canSendMail = scope.ServiceProvider.GetRequiredService(); + + await canSendMail.SendAsync( + message, subject, to, from ?? this._globalFrom, replyTo, cc, bcc, attachments, sender: sender + ); + } + } + } +} \ No newline at end of file diff --git a/Src/Coravel.Mailer/MailServiceRegistration.cs b/Src/Coravel.Mailer/MailServiceRegistration.cs index b9810283..00bfa4e2 100644 --- a/Src/Coravel.Mailer/MailServiceRegistration.cs +++ b/Src/Coravel.Mailer/MailServiceRegistration.cs @@ -5,6 +5,7 @@ using Coravel.Mailer.Mail.Interfaces; using Coravel.Mailer.Mail.Mailers; using Coravel.Mailer.Mail.Renderers; +using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -31,6 +32,12 @@ public static IServiceCollection AddMailer(this IServiceCollection services, ICo return services; } + public static WebApplicationBuilder AddMailer(this WebApplicationBuilder builder) + { + builder.Services.AddMailer(builder.Configuration); + return builder; + } + /// /// Register Coravel's mailer using the File Log Mailer - which sends mail to a file. /// Useful for testing. @@ -46,6 +53,12 @@ public static IServiceCollection AddFileLogMailer(this IServiceCollection servic return services; } + public static WebApplicationBuilder AddFileLogMailer(this WebApplicationBuilder builder) + { + builder.Services.AddFileLogMailer(builder.Configuration); + return builder; + } + /// /// Register Coravel's mailer using the Smtp Mailer. /// @@ -69,6 +82,12 @@ public static IServiceCollection AddSmtpMailer(this IServiceCollection services, return services; } + public static WebApplicationBuilder AddSmtpMailer(this WebApplicationBuilder builder, RemoteCertificateValidationCallback certCallback) + { + builder.Services.AddSmtpMailer(builder.Configuration, certCallback); + return builder; + } + /// /// Register Coravel's mailer using the Smtp Mailer. /// @@ -80,6 +99,12 @@ public static IServiceCollection AddSmtpMailer(this IServiceCollection services, return services; } + public static WebApplicationBuilder AddSmtpMailer(this WebApplicationBuilder builder) + { + builder.Services.AddSmtpMailer(builder.Configuration); + return builder; + } + /// /// Register Coravel's mailer using the Custom Mailer. /// @@ -95,6 +120,33 @@ public static IServiceCollection AddCustomMailer(this IServiceCollection service return services; } + public static WebApplicationBuilder AddCustomMailer(this WebApplicationBuilder builder, CustomMailer.SendAsyncFunc sendMailAsync) + { + builder.Services.AddCustomMailer(builder.Configuration, sendMailAsync); + return builder; + } + + /// + /// Register Coravel's mailer a Custom Mailer that implements `ICanSendMail`. + /// + /// + /// + public static IServiceCollection AddCustomMailer(this IServiceCollection services, IConfiguration config) where T : ICanSendMail + { + var globalFrom = GetGlobalFromRecipient(config); + RazorRenderer renderer = RazorRendererFactory.MakeInstance(config); + services.AddSingleton(p => + new CanSendMailWrapper(renderer, p.GetRequiredService(), globalFrom) + ); + return services; + } + + public static WebApplicationBuilder AddCustomMailer(this WebApplicationBuilder builder) where T : ICanSendMail + { + builder.Services.AddCustomMailer(builder.Configuration); + return builder; + } + private static MailRecipient GetGlobalFromRecipient(IConfiguration config) { string globalFromAddress = config.GetValue("Coravel:Mail:From:Address", null); diff --git a/Src/IntegrationTests/TestMvcApp/Controllers/MailController.cs b/Src/IntegrationTests/TestMvcApp/Controllers/MailController.cs index 490f1e1c..9fa37728 100644 --- a/Src/IntegrationTests/TestMvcApp/Controllers/MailController.cs +++ b/Src/IntegrationTests/TestMvcApp/Controllers/MailController.cs @@ -1,5 +1,6 @@ using System; using System.Threading.Tasks; +using Coravel.Mailer.Mail; using Coravel.Mailer.Mail.Interfaces; using Microsoft.AspNetCore.Mvc; using TestMvcApp.Mailables; @@ -76,5 +77,84 @@ public async Task RenderView() return Content(message, "text/html"); } + + [Route("WithHtmlInlineMailable")] + public async Task WithHtmlInlineMailable() + { + UserModel user = new UserModel() + { + Email = "FromUserModel@test.com", + Name = "Coravel Test Person" + }; + + await this._mailer.SendAsync( + Mailable.AsInline() + .To(user) + .From("replyto@test.com") + .Html($"

Welcome {user.Name}

") + ); + + return Ok(); + } + + [Route("RenderHtmlInlineMailable")] + public async Task RenderHtmlInlineMailable() + { + + UserModel user = new UserModel() + { + Email = "FromUserModel@test.com", + Name = "Coravel Test Person" + }; + + string message = await this._mailer.RenderAsync( + Mailable.AsInline() + .To(user) + .From("replyto@test.com") + .Html($"

Welcome {user.Name}

") + ); + + return Content(message, "text/html"); + } + + [Route("WithHtmlInlineMailableOfT")] + public async Task WithHtmlInlineMailableOfT() + { + + UserModel user = new UserModel() + { + Email = "FromUserModel@test.com", + Name = "Coravel Test Person" + }; + + await this._mailer.SendAsync( + Mailable.AsInline() + .To(user) + .From("replyto@test.com") + .Html($"

Welcome {user.Name}

") + ); + + return Ok(); + } + + [Route("RenderHtmlInlineMailableOfT")] + public async Task RenderHtmlInlineMailableOfT() + { + + UserModel user = new UserModel() + { + Email = "FromUserModel@test.com", + Name = "Coravel Test Person" + }; + + string message = await this._mailer.RenderAsync( + Mailable.AsInline() + .To(user) + .From("replyto@test.com") + .Html($"

Welcome {user.Name}

") + ); + + return Content(message, "text/html"); + } } } \ No newline at end of file diff --git a/Src/IntegrationTests/Tests/Mail/MailerSmokeTests.cs b/Src/IntegrationTests/Tests/Mail/MailerSmokeTests.cs index b6340474..93ec2e36 100644 --- a/Src/IntegrationTests/Tests/Mail/MailerSmokeTests.cs +++ b/Src/IntegrationTests/Tests/Mail/MailerSmokeTests.cs @@ -24,6 +24,30 @@ public async Task RenderHtmlDoesntThrowTest() { var content = await this._factory.CreateClient().GetStringAsync("/Mail/RenderHtml"); Assert.False(string.IsNullOrWhiteSpace(content)); } + + [Fact] + public async Task WithHtmlInlineMailableDoesntThrowTest() { + var content = await this._factory.CreateClient().GetStringAsync("/Mail/WithHtmlInlineMailable"); + // Pass = no exceptions. + } + + [Fact] + public async Task RenderHtmlInlineMailableDoesntThrowTest() { + var content = await this._factory.CreateClient().GetStringAsync("/Mail/RenderHtmlInlineMailable"); + Assert.False(string.IsNullOrWhiteSpace(content)); + } + + [Fact] + public async Task WithHtmlInlineMailableOfTDoesntThrowTest() { + var content = await this._factory.CreateClient().GetStringAsync("/Mail/WithHtmlInlineMailableOfT"); + // Pass = no exceptions. + } + + [Fact] + public async Task RenderHtmlInlineMailableOfTDoesntThrowTest() { + var content = await this._factory.CreateClient().GetStringAsync("/Mail/RenderHtmlInlineMailableOfT"); + Assert.False(string.IsNullOrWhiteSpace(content)); + } // This actually works when running the app manually. Something about the ASP.NET Core Integration Tests // that doesn't work well here. diff --git a/Src/UnitTests/MailerUnitTests/Mail/CanSendMailWrapperTests.cs b/Src/UnitTests/MailerUnitTests/Mail/CanSendMailWrapperTests.cs new file mode 100644 index 00000000..33bdf6b4 --- /dev/null +++ b/Src/UnitTests/MailerUnitTests/Mail/CanSendMailWrapperTests.cs @@ -0,0 +1,226 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Coravel.Mailer.Mail; +using Coravel.Mailer.Mail.Mailers; +using Coravel.Mailer.Mail.Renderers; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using UnitTests.Mail.Shared.Mailables; +using Xunit; + +namespace UnitTests.Mail +{ + public class CanSendMailWrapperTests + { + public class CustomAssertMailer : ICanSendMail + { + private Action, MailRecipient, MailRecipient, IEnumerable, IEnumerable, IEnumerable, MailRecipient> _assert; + + public CustomAssertMailer(Action, MailRecipient, MailRecipient, IEnumerable, IEnumerable, IEnumerable, MailRecipient> assert) + { + this._assert = assert; + } + + public Task SendAsync(string message, string subject, IEnumerable to, MailRecipient from, MailRecipient replyTo, IEnumerable cc, IEnumerable bcc, IEnumerable attachments = null, MailRecipient sender = null) + { + this._assert(message, subject, to, from, replyTo, cc, bcc, attachments, sender); + return Task.CompletedTask; + } + } + + + [Fact] + public async Task when_sending_generic_mail_it_sends() + { + var services = new ServiceCollection(); + services.AddScoped, MailRecipient, MailRecipient, IEnumerable, IEnumerable, IEnumerable, MailRecipient>>(p => + (message, subject, to, from, replyTo, cc, bcc, attachments, sender) => + { + Assert.Equal("test", message); + Assert.Equal("from@test.com", from.Email); + Assert.Equal("from@test.com", from.Email); + Assert.Equal("to@test.com", to.First().Email); + Assert.Equal("test", message); + }); + + services.AddScoped(); + var provider = services.BuildServiceProvider(); + + var mailer = new CanSendMailWrapper( + RazorRendererFactory.MakeInstance(new ConfigurationBuilder().Build()), + provider.GetRequiredService(), + null + ); + + await mailer.SendAsync( + new GenericHtmlMailable() + .Subject("test") + .From("from@test.com") + .To("to@test.com") + .Html("test") + ); + } + + [Fact] + public async Task when_using_global_from_when_from_not_defined_it_uses_global_value() + { + var services = new ServiceCollection(); + services.AddScoped, MailRecipient, MailRecipient, IEnumerable, IEnumerable, IEnumerable, MailRecipient>>(p => + (message, subject, to, from, replyTo, cc, bcc, attachments, sender) => + { + Assert.Equal("test", message); + Assert.Equal("global@test.com", from.Email); + Assert.Equal("to@test.com", to.First().Email); + Assert.Equal("test", message); + }); + + services.AddScoped(); + var provider = services.BuildServiceProvider(); + + var mailer = new CanSendMailWrapper( + RazorRendererFactory.MakeInstance(new ConfigurationBuilder().Build()), + provider.GetRequiredService(), + new MailRecipient("global@test.com") + ); + + await mailer.SendAsync( + new GenericHtmlMailable() + .Subject("test") + // .From("from@test.com") -> test should use the global from + .To("to@test.com") + .Html("test") + ); + } + + [Fact] + public async Task when_using_global_from_when_from_is_defined_it_uses_from_value() + { + var services = new ServiceCollection(); + services.AddScoped, MailRecipient, MailRecipient, IEnumerable, IEnumerable, IEnumerable, MailRecipient>>(p => + (message, subject, to, from, replyTo, cc, bcc, attachments, sender) => + { + Assert.Equal("test", message); + Assert.Equal("from@test.com", from.Email); + Assert.Equal("to@test.com", to.First().Email); + Assert.Equal("test", message); + }); + + services.AddScoped(); + var provider = services.BuildServiceProvider(); + + var mailer = new CanSendMailWrapper( + RazorRendererFactory.MakeInstance(new ConfigurationBuilder().Build()), + provider.GetRequiredService(), + new MailRecipient("global@test.com") + ); + + await mailer.SendAsync( + new GenericHtmlMailable() + .Subject("test") + .From("from@test.com") // This should override the global from. + .To("to@test.com") + .Html("test") + ); + } + + [Fact] + public async Task when_using_render_it_renders() + { + var services = new ServiceCollection(); + services.AddScoped, MailRecipient, MailRecipient, IEnumerable, IEnumerable, IEnumerable, MailRecipient>>(p => + (message, subject, to, from, replyTo, cc, bcc, attachments, sender) => + { + + }); + + services.AddScoped(); + var provider = services.BuildServiceProvider(); + + var mailer = new CanSendMailWrapper( + RazorRendererFactory.MakeInstance(new ConfigurationBuilder().Build()), + provider.GetRequiredService(), + null + ); + + var htmlMessage = await mailer.RenderAsync( + new GenericHtmlMailable() + .Subject("test") + .From("from@test.com") + .To("to@test.com") + .Html("") + ); + + Assert.Equal("", htmlMessage); + } + + [Fact] + public async Task when_attachments_it_works() + { + var services = new ServiceCollection(); + services.AddScoped, MailRecipient, MailRecipient, IEnumerable, IEnumerable, IEnumerable, MailRecipient>>(p => + (message, subject, to, from, replyTo, cc, bcc, attachments, sender) => + { + Assert.Equal(2, attachments.Count()); + Assert.Equal("Attachment 2", attachments.Skip(1).Single().Name); + }); + + services.AddScoped(); + var provider = services.BuildServiceProvider(); + + var mailer = new CanSendMailWrapper( + RazorRendererFactory.MakeInstance(new ConfigurationBuilder().Build()), + provider.GetRequiredService(), + null + ); + + await mailer.SendAsync( + new GenericHtmlMailable() + .Subject("test") + .From("from@test.com") + .To("to@test.com") + .Html("test") + .Attach(new Attachment + { + Bytes = new byte[] { }, + Name = "Attachment 1" + }) + .Attach(new Attachment + { + Bytes = new byte[] { }, + Name = "Attachment 2" + }) + ); + } + + [Fact] + public async Task when_assigning_sender_it_is_assigned() + { + var services = new ServiceCollection(); + services.AddScoped, MailRecipient, MailRecipient, IEnumerable, IEnumerable, IEnumerable, MailRecipient>>(p => + (message, subject, to, from, replyTo, cc, bcc, attachments, sender) => + { + Assert.Equal("sender@test.com", sender.Email); + }); + + services.AddScoped(); + var provider = services.BuildServiceProvider(); + + var mailer = new CanSendMailWrapper( + RazorRendererFactory.MakeInstance(new ConfigurationBuilder().Build()), + provider.GetRequiredService(), + null + ); + + await mailer.SendAsync( + new GenericHtmlMailable() + .Subject("test") + .From("from@test.com") + .To("to@test.com") + .Sender("sender@test.com") + .Html("test") + ); + } + } +} \ No newline at end of file diff --git a/Src/UnitTests/MailerUnitTests/Mail/GeneralMailTests.cs b/Src/UnitTests/MailerUnitTests/Mail/GeneralMailTests.cs index 517d9ef0..ad79376a 100644 --- a/Src/UnitTests/MailerUnitTests/Mail/GeneralMailTests.cs +++ b/Src/UnitTests/MailerUnitTests/Mail/GeneralMailTests.cs @@ -320,5 +320,41 @@ void AssertMail(AssertMailer.Data data) await new AssertMailer(AssertMail).SendAsync(mail); } + + [Fact] + public async Task MailableHasSender() + { + void AssertMail(AssertMailer.Data data) + { + Assert.Equal("sender@test.com", data.sender.Email); + }; + + var mail = new GenericHtmlMailable() + .To("to@test.com") + .From("from@test.com") + .Subject("test") + .Sender("sender@test.com") + .Html(""); + + await new AssertMailer(AssertMail).SendAsync(mail); + } + + [Fact] + public async Task using_inline_mailable_of_T_works_in_same_file_as_using_inline_mailable() + { + void AssertMail(AssertMailer.Data data) + { + Assert.Equal("sender@test.com", data.sender.Email); + }; + + var mail = new GenericHtmlMailable() + .To("to@test.com") + .From("from@test.com") + .Subject("test") + .Sender("sender@test.com") + .Html(""); + + await new AssertMailer(AssertMail).SendAsync(mail); + } } } \ No newline at end of file diff --git a/Src/UnitTests/MailerUnitTests/Mail/InlineMailableTests.cs b/Src/UnitTests/MailerUnitTests/Mail/InlineMailableTests.cs new file mode 100644 index 00000000..d0015d1a --- /dev/null +++ b/Src/UnitTests/MailerUnitTests/Mail/InlineMailableTests.cs @@ -0,0 +1,407 @@ +using System.Linq; +using System.Threading.Tasks; +using Coravel.Mailer.Mail; +using Coravel.Mailer.Mail.Mailers; +using UnitTests.Mail.Shared.Models; +using Xunit; + +namespace UnitTests.Mail +{ + public class InlineMailableTests + { + [Fact] + public async Task MailableHasSubjectField() + { + void AssertMail(AssertMailer.Data data) + { + Assert.Equal("test", data.subject); + }; + + var mail = new InlineMailable() + .To("to@test.com") + .From("from@test.com") + .Subject("test") + .Html(""); + + await new AssertMailer(AssertMail).SendAsync(mail); + } + + [Fact] + public async Task MailableSubjectIsGeneratedFromMailableName() + { + void AssertMail(AssertMailer.Data data) + { + Assert.Equal("Inline", data.subject); + }; + + var mail = new InlineMailable() + .To("to@test.com") + .From("from@test.com") + .Html(""); + + await new AssertMailer(AssertMail).SendAsync(mail); + } + + [Fact] + public async Task CheckToAndNameFieldsAreGeneratedFromModel() + { + var user = new TestUser + { + Name = "My Name", + Email = "autoassigned@test.com" + }; + + void AssertMail(AssertMailer.Data data) + { + var model = data.to.First(); + Assert.Equal(user.Email, model.Email); + Assert.Equal(user.Name, model.Name); + }; + + await new AssertMailer(AssertMail).SendAsync( + new InlineMailable() + .To(user) + .From("from@test.com") + .Html($"Hi") + ); + } + + [Fact] + public async Task MailableHasToField_OneAddress() + { + void AssertMail(AssertMailer.Data data) + { + Assert.Equal("to@test.com", data.to.First().Email); + }; + + var mail = new InlineMailable() + .To("to@test.com") + .From("from@test.com") + .Subject("test") + .Html(""); + + await new AssertMailer(AssertMail).SendAsync(mail); + } + + [Fact] + public async Task MailableHasToField_OneMailRecipient() + { + void AssertMail(AssertMailer.Data data) + { + var recipient = data.to.First(); + Assert.Equal("to@test.com", recipient.Email); + Assert.Equal("My Name", recipient.Name); + }; + + var mail = new InlineMailable() + .To(new MailRecipient("to@test.com", "My Name")) + .From("from@test.com") + .Subject("test") + .Html(""); + + await new AssertMailer(AssertMail).SendAsync(mail); + } + + [Fact] + public async Task MailableHasToField_MultiAddress() + { + void AssertMail(AssertMailer.Data data) + { + Assert.Equal(3, data.to.Count()); + }; + + var mail = new InlineMailable() + .To(new string[] { "one@test.com", "two@test.com", "three@test.com" }) + .From("from@test.com") + .Subject("test") + .Html(""); + + await new AssertMailer(AssertMail).SendAsync(mail); + } + + [Fact] + public async Task MailableHasToField_MultiMailRecipient() + { + void AssertMail(AssertMailer.Data data) + { + Assert.Equal(3, data.to.Count()); + }; + + var mail = new InlineMailable() + .To(new MailRecipient[] { + new MailRecipient("one@test.com"), + new MailRecipient("two@test.com"), + new MailRecipient("three@test.com") + }) + .From("from@test.com") + .Subject("test") + .Html(""); + + await new AssertMailer(AssertMail).SendAsync(mail); + } + + [Fact] + public async Task MailableHasFromField_FromAddress() + { + void AssertMail(AssertMailer.Data data) + { + Assert.Equal("from@test.com", data.from.Email); + }; + + var mail = new InlineMailable() + .To("to@test.com") + .From("from@test.com") + .Subject("test") + .Html(""); + + await new AssertMailer(AssertMail).SendAsync(mail); + } + + [Fact] + public async Task MailableHasFromField_FromMailRecipient() + { + void AssertMail(AssertMailer.Data data) + { + Assert.Equal("from@test.com", data.from.Email); + Assert.Equal("From", data.from.Name); + }; + + var mail = new InlineMailable() + .To("to@test.com") + .From(new MailRecipient("from@test.com", "From")) + .Subject("test") + .Html(""); + + await new AssertMailer(AssertMail).SendAsync(mail); + } + + [Fact] + public async Task MailableHasReplyToField_FromAddress() + { + void AssertMail(AssertMailer.Data data) + { + Assert.Equal("replyTo@test.com", data.replyTo.Email); + }; + + var mail = new InlineMailable() + .To("to@test.com") + .From("from@test.com") + .Subject("test") + .ReplyTo("replyTo@test.com") + .Html(""); + + await new AssertMailer(AssertMail).SendAsync(mail); + } + + [Fact] + public async Task MailableHasReplyToField_FromMailRecipient() + { + void AssertMail(AssertMailer.Data data) + { + Assert.Equal("replyTo@test.com", data.replyTo.Email); + Assert.Equal("ReplyTo", data.replyTo.Name); + }; + + var mail = new InlineMailable() + .To("to@test.com") + .From("from@test.com") + .Subject("test") + .ReplyTo(new MailRecipient("replyTo@test.com", "ReplyTo")) + .Html(""); + + await new AssertMailer(AssertMail).SendAsync(mail); + } + + [Fact] + public async Task MailableHasCcField_FromAddresses() + { + void AssertMail(AssertMailer.Data data) + { + Assert.Equal(3, data.cc.Count()); + }; + + var mail = new InlineMailable() + .To("to@test.com") + .From("from@test.com") + .Subject("test") + .Cc(new string[] { "one@test.com", "two@test.com", "three@test.com" }) + .Html(""); + + await new AssertMailer(AssertMail).SendAsync(mail); + } + + [Fact] + public async Task MailableHasCcField_FromMailRecipients() + { + void AssertMail(AssertMailer.Data data) + { + Assert.Equal(3, data.cc.Count()); + }; + + var mail = new InlineMailable() + .To("to@test.com") + .From("from@test.com") + .Subject("test") + .Cc(new MailRecipient[] { + new MailRecipient("one@test.com"), + new MailRecipient("two@test.com"), + new MailRecipient("three@test.com") + }) + .Html(""); + + await new AssertMailer(AssertMail).SendAsync(mail); + } + + [Fact] + public async Task MailableHasBccField_FromAddresses() + { + void AssertMail(AssertMailer.Data data) + { + Assert.Equal(3, data.bcc.Count()); + }; + + var mail = new InlineMailable() + .To("to@test.com") + .From("from@test.com") + .Subject("test") + .Bcc(new string[] { "one@test.com", "two@test.com", "three@test.com" }) + .Html(""); + + await new AssertMailer(AssertMail).SendAsync(mail); + } + + [Fact] + public async Task MailableHasBccField_FromMailRecipients() + { + void AssertMail(AssertMailer.Data data) + { + Assert.Equal(3, data.bcc.Count()); + }; + + var mail = new InlineMailable() + .To("to@test.com") + .From("from@test.com") + .Subject("test") + .Bcc(new MailRecipient[] { + new MailRecipient("one@test.com"), + new MailRecipient("two@test.com"), + new MailRecipient("three@test.com") + }) + .Html(""); + + await new AssertMailer(AssertMail).SendAsync(mail); + } + + [Fact] + public async Task MailableHasAttachments() + { + void AssertMail(AssertMailer.Data data) + { + Assert.Equal(2, data.attachments.Count()); + Assert.True(data.attachments.Skip(1).Single().Name == "Attachment 2"); + }; + + var mail = new InlineMailable() + .To("to@test.com") + .From("from@test.com") + .Subject("test") + .Bcc(new MailRecipient[] { + new MailRecipient("one@test.com"), + new MailRecipient("two@test.com"), + new MailRecipient("three@test.com") + }) + .Attach(new Attachment + { + Bytes = new byte[] { }, + Name = "Attachment 1" + }) + .Attach(new Attachment + { + Bytes = new byte[] { }, + Name = "Attachment 2" + }) + .Html(""); + + await new AssertMailer(AssertMail).SendAsync(mail); + } + + [Fact] + public async Task MailableHasSender() + { + void AssertMail(AssertMailer.Data data) + { + Assert.Equal("sender@test.com", data.sender.Email); + }; + + var mail = new InlineMailable() + .To("to@test.com") + .From("from@test.com") + .Subject("test") + .Sender("sender@test.com") + .Html(""); + + await new AssertMailer(AssertMail).SendAsync(mail); + } + + [Fact] + public async Task using_inline_mailable_of_T_works_in_same_file_as_using_inline_mailable() + { + void AssertMail(AssertMailer.Data data) + { + Assert.Equal("sender@test.com", data.sender.Email); + }; + + // This generic type is used in the "View()" method. + // This test just makes sure that using this class and the non-generic version + // in the same file works/compiles fine. + var mail = new InlineMailable() + .To("to@test.com") + .From("from@test.com") + .Subject("test") + .Sender("sender@test.com") + .Html(""); + + await new AssertMailer(AssertMail).SendAsync(mail); + } + + [Fact] + public async Task inline_mailable_works_from_static_mailable_method() + { + void AssertMail(AssertMailer.Data data) + { + Assert.Equal("to@test.com", data.to.First().Email); + Assert.Equal("from@test.com", data.from.Email); + Assert.Equal("test", data.subject); + Assert.Equal("", data.message); + }; + + var mail = Mailable.AsInline() + .To("to@test.com") + .From("from@test.com") + .Subject("test") + .Html(""); + + await new AssertMailer(AssertMail).SendAsync(mail); + } + + [Fact] + public async Task inline_mailable_of_T_works_from_static_mailable_method() + { + void AssertMail(AssertMailer.Data data) + { + Assert.Equal("to@test.com", data.to.First().Email); + Assert.Equal("from@test.com", data.from.Email); + Assert.Equal("test", data.subject); + Assert.Equal("", data.message); + }; + + var mail = Mailable.AsInline() + .To("to@test.com") + .From("from@test.com") + .Subject("test") + .Html(""); + + await new AssertMailer(AssertMail).SendAsync(mail); + } + } +} \ No newline at end of file