Skip to content

Commit

Permalink
New Custom Mailer Approach And Inline Mailables (#405)
Browse files Browse the repository at this point in the history
* new custom mailer and inline mailables

* publish new version
  • Loading branch information
jamesmh authored Sep 25, 2024
1 parent bf8749f commit af3d8ae
Show file tree
Hide file tree
Showing 16 changed files with 1,002 additions and 31 deletions.
19 changes: 19 additions & 0 deletions Demo/Controllers/MailController.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -75,6 +76,24 @@ public async Task<IActionResult> RenderView()
return Content(message, "text/html");
}

public async Task<IActionResult> RenderViewWithInlineMailable()
{
UserModel user = new UserModel()
{
Email = "FromUserModel@test.com",
Name = "Coravel Test Person"
};

string message = await this._mailer.RenderAsync(
Mailable.AsInline<UserModel>()
.To(user)
.From("from@test.com")
.View("~/Views/Mail/NewUser.cshtml", user)
);

return Content(message, "text/html");
}

public IActionResult QueueMail()
{
UserModel user = new UserModel()
Expand Down
1 change: 0 additions & 1 deletion Demo/Demo.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.1.1" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="6.0.27" />
<!-- <PackageReference Include="Coravel" Version="3.3.0" /> -->
</ItemGroup>
Expand Down
110 changes: 85 additions & 25 deletions DocsV2/docs/Mailing/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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<MailRecipient> to,
MailRecipient from,
MailRecipient replyTo,
IEnumerable<MailRecipient> cc,
IEnumerable<MailRecipient> bcc,
IEnumerable<Attachment> attachments = null,
MailRecipient sender = null
)
{
// Custom logic for sending an email.
}
var builder = WebApplication.CreateBuilder(args);

services.AddCustomMailer(this.Configuration, SendMailCustomAsync);
builder.AddCustomMailer<MyHttpApiCustomMailer>();
```

:::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<MailRecipient> to, MailRecipient from, MailRecipient replyTo, IEnumerable<MailRecipient> cc, IEnumerable<MailRecipient> bcc, IEnumerable<Attachment> attachments = null, MailRecipient sender = null)
{
// Code that uses the HttpClient to send mail via an HTTP API.
}
}
```

### Built-In View Templates

Expand Down Expand Up @@ -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!
Expand Down Expand Up @@ -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<T>()`.

The generic version allows you to send email using `View(string viewPath, T viewModel)`:

```csharp
public async Task<IActionResult> SendMyEmail()
{
UserModel user = new UserModel()
{
Email = "FromUserModel@test.com",
Name = "Coravel Test Person"
};

await this._mailer.SendAsync(
Mailable.AsInline<UserModel>()
.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<IActionResult> 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($"<html><body><h1>Welcome {user.Name}</h1></body></html>")
);

return Ok();
}
```

### From

To specify who the email is from, use the `From()` method:
Expand Down
2 changes: 1 addition & 1 deletion Src/Coravel.Mailer/Coravel.Mailer.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<TargetFramework>.net6.0</TargetFramework>
<AddRazorSupportForMvc>True</AddRazorSupportForMvc>
<PackageId>Coravel.Mailer</PackageId>
<Version>6.0.0</Version>
<Version>6.1.0</Version>
<Authors>James Hickey</Authors>
<Company>-</Company>
<Title>Coravel.Mailer</Title>
Expand Down
8 changes: 8 additions & 0 deletions Src/Coravel.Mailer/Mail/InlineMailable.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using Coravel.Mailer.Mail;

public class InlineMailable : Mailable<object>
{ public override void Build()
{
// No-op. This is built inline by the caller.
}
}
8 changes: 8 additions & 0 deletions Src/Coravel.Mailer/Mail/InlineMailableT.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using Coravel.Mailer.Mail;

public class InlineMailable<T> : Mailable<T>
{ public override void Build()
{
// No-op. This is built inline by the caller.
}
}
8 changes: 8 additions & 0 deletions Src/Coravel.Mailer/Mail/Interfaces/ICanSendMail.cs
Original file line number Diff line number Diff line change
@@ -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<MailRecipient> to, MailRecipient from, MailRecipient replyTo, IEnumerable<MailRecipient> cc, IEnumerable<MailRecipient> bcc, IEnumerable<Attachment> attachments = null, MailRecipient sender = null);
}
3 changes: 1 addition & 2 deletions Src/Coravel.Mailer/Mail/Interfaces/IMailer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,9 @@

namespace Coravel.Mailer.Mail.Interfaces
{
public interface IMailer
public interface IMailer : ICanSendMail
{
Task<string> RenderAsync<T>(Mailable<T> mailable);
Task SendAsync<T>(Mailable<T> mailable);
Task SendAsync(string message, string subject, IEnumerable<MailRecipient> to, MailRecipient from, MailRecipient replyTo, IEnumerable<MailRecipient> cc, IEnumerable<MailRecipient> bcc, IEnumerable<Attachment> attachments = null, MailRecipient sender = null);
}
}
9 changes: 7 additions & 2 deletions Src/Coravel.Mailer/Mail/Mailable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,7 @@ public class Mailable<T>
/// <summary>
/// View data to pass to the view to render.
/// </summary>
private T _viewModel;

private T _viewModel;
public Mailable<T> From(MailRecipient recipient)
{
this._from = recipient;
Expand Down Expand Up @@ -266,4 +265,10 @@ private void BindSubjectField()
}
}
}

public class Mailable
{
public static InlineMailable AsInline() => new InlineMailable();
public static InlineMailable<T> AsInline<T>() => new InlineMailable<T>();
}
}
40 changes: 40 additions & 0 deletions Src/Coravel.Mailer/Mail/Mailers/CanSendMailWrapper.cs
Original file line number Diff line number Diff line change
@@ -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<TCanSendMail> : 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<string> RenderAsync<T>(Mailable<T> mailable) =>
mailable.RenderAsync(this._renderer, this);

public async Task SendAsync<T>(Mailable<T> mailable) =>
await mailable.SendAsync(this._renderer, this);

public async Task SendAsync(string message, string subject, IEnumerable<MailRecipient> to, MailRecipient from, MailRecipient replyTo, IEnumerable<MailRecipient> cc, IEnumerable<MailRecipient> bcc, IEnumerable<Attachment> attachments, MailRecipient sender = null)
{
await using (var scope = this._scopeFactory.CreateAsyncScope())
{
var canSendMail = scope.ServiceProvider.GetRequiredService<TCanSendMail>();

await canSendMail.SendAsync(
message, subject, to, from ?? this._globalFrom, replyTo, cc, bcc, attachments, sender: sender
);
}
}
}
}
52 changes: 52 additions & 0 deletions Src/Coravel.Mailer/MailServiceRegistration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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;
}

/// <summary>
/// Register Coravel's mailer using the File Log Mailer - which sends mail to a file.
/// Useful for testing.
Expand All @@ -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;
}

/// <summary>
/// Register Coravel's mailer using the Smtp Mailer.
/// </summary>
Expand All @@ -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;
}

/// <summary>
/// Register Coravel's mailer using the Smtp Mailer.
/// </summary>
Expand All @@ -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;
}

/// <summary>
/// Register Coravel's mailer using the Custom Mailer.
/// </summary>
Expand All @@ -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;
}

/// <summary>
/// Register Coravel's mailer a Custom Mailer that implements `ICanSendMail`.
/// </summary>
/// <param name="services"></param>
/// <param name="config"></param>
public static IServiceCollection AddCustomMailer<T>(this IServiceCollection services, IConfiguration config) where T : ICanSendMail
{
var globalFrom = GetGlobalFromRecipient(config);
RazorRenderer renderer = RazorRendererFactory.MakeInstance(config);
services.AddSingleton<IMailer>(p =>
new CanSendMailWrapper<T>(renderer, p.GetRequiredService<IServiceScopeFactory>(), globalFrom)
);
return services;
}

public static WebApplicationBuilder AddCustomMailer<T>(this WebApplicationBuilder builder) where T : ICanSendMail
{
builder.Services.AddCustomMailer<T>(builder.Configuration);
return builder;
}

private static MailRecipient GetGlobalFromRecipient(IConfiguration config)
{
string globalFromAddress = config.GetValue<string>("Coravel:Mail:From:Address", null);
Expand Down
Loading

0 comments on commit af3d8ae

Please sign in to comment.