Skip to content

Commit

Permalink
Plain-text-mail (#406)
Browse files Browse the repository at this point in the history
* new plain text body

* update version
  • Loading branch information
jamesmh authored Sep 26, 2024
1 parent 4beda47 commit 5a96503
Show file tree
Hide file tree
Showing 16 changed files with 378 additions and 78 deletions.
19 changes: 19 additions & 0 deletions Demo/Controllers/MailController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,25 @@ public async Task<IActionResult> RenderViewWithInlineMailable()
return Content(message, "text/html");
}

public async Task<IActionResult> WithHtmlAndPlainText()
{
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)
.Text("This is a plain text version of the email!")
);

return Ok();
}

public IActionResult QueueMail()
{
UserModel user = new UserModel()
Expand Down
71 changes: 46 additions & 25 deletions DocsV2/docs/Mailing/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ public class MyHttpApiCustomMailer : ICanSendMail
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)
public async Task SendAsync(MessageBody 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.
}
Expand Down Expand Up @@ -237,7 +237,7 @@ public async Task<IActionResult> SendMyEmail()
}
```

The non-generic version is suited to using `Html()` when passing a view model is not needed.
The non-generic version is suited to using `Html()` and/or `Text()` when passing a view model is not needed.

```csharp
public async Task<IActionResult> SendMyEmail()
Expand All @@ -253,6 +253,7 @@ public async Task<IActionResult> SendMyEmail()
.To(user)
.From("from@test.com")
.Html($"<html><body><h1>Welcome {user.Name}</h1></body></html>")
.Text($"Welcome {user.Name}")
);

return Ok();
Expand Down Expand Up @@ -301,6 +302,37 @@ You can also pass:
- `To(MailRecipient)`
- `To(IEnumerable<MailRecipient>)`

### Html

If you want to supply raw Html as your e-mail use the `Html(string html)` method:

```csharp
public override void Build()
{
this.To(this._user)
.From("from@test.com")
.Html(someHtml);
}
```

In this case, your Mailable class should use the `string` generic type: `public class MyMailable : Mailable<string>`.

### Text

If you want to add a plain text representation of your email then use `Text(string plainText)`:

```csharp
public override void Build()
{
this.To(this._user)
.From("from@test.com")
.Html(someHtml)
.Text(plainText);
}
```

`Text()` doesn't replace `Html()` or `View()`. It _adds_ a plain text representation to your email that the email client will use in the event it doesn't support HTML.

### Sender

To specify the sender of the email (different from the `From` address), use the `Sender()` method:
Expand Down Expand Up @@ -339,11 +371,9 @@ Further methods, which all accept either `IEnumerable<string>` or `IEnumerable<M
- `Bcc`
- `ReplyTo`

### Specifying Mail Templates

#### Razor Templates
### Mail Templates With `View()`

##### .NET Core 3.1+
#### .NET Core 3.1+

For a standard .NET 6+ web project, this should work out-of-the-box. If using a shared library, the following applies.

Expand All @@ -359,10 +389,14 @@ If you have a shared library with razor views inside, you'll need to make sure .
</PropertyGroup>
```

##### Razor Views
#### View

Using a razor view to send e-mails is done using the `View(string viewPath, T viewModel)` method.

:::tip
You can still use `Text(string planText)` to add an alternative plain text representation beside the HTML message that calling `View()` produces.
:::

The type of the `viewModel` parameter must match the type of your Mailable's generic type parameter. For example, a `Mailable<UserModel>` will have the method `View(string viewPath, UserModel viewModel)`. Coravel will automatically bind the model to your view so you can generate dynamic content (just like using `View()` inside your MVC controllers).

For views that do not require a view model, just inherit your Mailable from `Mailable<string>` and use `View(string viewPath)`.
Expand All @@ -379,8 +413,6 @@ public class MyMailable : Mailable<string>
}
```

##### Example

:::tip
The CLI generated a sample for you at `~/Views/Example.cshtml`.
:::
Expand Down Expand Up @@ -408,22 +440,7 @@ It might look like this (which uses a built-in template):
}
```

#### Html Template

If you want to supply raw Html as your e-mail use the `Html(string html)` method:

```csharp
public override void Build()
{
this.To(this._user)
.From("from@test.com")
.Html(someHtml);
}
```

In this case, your Mailable class should use the `string` generic type: `public class MyMailable : Mailable<string>`.

## Mail Templates
## Mail Template Components

When using razor views, there are various pieces of data that Coravel will automatically passes for you:

Expand Down Expand Up @@ -558,3 +575,7 @@ public async Task<IActionResult> RenderView()
return Content(message, "text/html");
}
```

:::tip
Since we can only render 1 mail body, using `Html()` or `View()` will take precedence over `Text()` when rendering an email with `RenderAsync()`.
:::
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.1.0</Version>
<Version>7.0.0</Version>
<Authors>James Hickey</Authors>
<Company>-</Company>
<Title>Coravel.Mailer</Title>
Expand Down
2 changes: 1 addition & 1 deletion Src/Coravel.Mailer/Mail/Interfaces/ICanSendMail.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@

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);
Task SendAsync(MessageBody message, string subject, IEnumerable<MailRecipient> to, MailRecipient from, MailRecipient replyTo, IEnumerable<MailRecipient> cc, IEnumerable<MailRecipient> bcc, IEnumerable<Attachment> attachments = null, MailRecipient sender = null);
}
49 changes: 37 additions & 12 deletions Src/Coravel.Mailer/Mail/Mailable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ namespace Coravel.Mailer.Mail
{
public class Mailable<T>
{
private static readonly string NoRenderFoundMessage = "Please use one of the available methods for specifying how to render your mail (e.g. Html() or View())";
private static readonly string NoRenderFoundMessage = "Please use one of the available methods for specifying how to render your mail (e.g. Html(), Text() or View())";

/// <summary>
/// Who the email is from.
Expand Down Expand Up @@ -50,7 +50,7 @@ public class Mailable<T>
/// <summary>
/// Raw HTML to use as email message.
/// </summary>
private string _html;
private MessageBody _messageBody;

/// <summary>
/// The MVC view to use to generate the message.
Expand Down Expand Up @@ -155,7 +155,17 @@ public Mailable<T> Attach(Attachment attachment)

public Mailable<T> Html(string html)
{
this._html = html;
this._messageBody = this._messageBody is null
? new MessageBody(html, null)
: new MessageBody(html, this._messageBody.Text);
return this;
}

public Mailable<T> Text(string plainText)
{
this._messageBody = this._messageBody is null
? new MessageBody(null, plainText)
: new MessageBody(this._messageBody.Html, plainText);
return this;
}

Expand All @@ -178,7 +188,7 @@ internal async Task SendAsync(RazorRenderer renderer, IMailer mailer)
{
this.Build();

string message = await this.BuildMessage(renderer, mailer).ConfigureAwait(false);
MessageBody message = await this.BuildMessage(renderer, mailer).ConfigureAwait(false);

await mailer.SendAsync(
message,
Expand All @@ -196,25 +206,40 @@ await mailer.SendAsync(
internal async Task<string> RenderAsync(RazorRenderer renderer, IMailer mailer)
{
this.Build();
return await this.BuildMessage(renderer, mailer).ConfigureAwait(false);
var mailMessage = await this.BuildMessage(renderer, mailer).ConfigureAwait(false);
return mailMessage.HasHtmlMessage() ? mailMessage.Html : mailMessage.Text;
}

private async Task<string> BuildMessage(RazorRenderer renderer, IMailer mailer)
private async Task<MessageBody> BuildMessage(RazorRenderer renderer, IMailer mailer)
{
this.BindDynamicProperties();

if (this._html != null)
// If View() was used, the caller can still also use Text() too, so we need to handle
// when only View() is called and when both View() and Text() are used.
if (this._viewPath is not null)
{
return this._html;
var htmlRendered = await renderer
.RenderViewToStringAsync<T>(this._viewPath, this._viewModel)
.ConfigureAwait(false);

if(this._messageBody is null)
{
this._messageBody = new MessageBody(htmlRendered, null);
}
else {
this._messageBody.Html = htmlRendered;
}

return this._messageBody;
}

if (this._viewPath != null)
// View() wasn't called, so we'll see if a message body was defined.
if (this._messageBody is not null)
{
return await renderer
.RenderViewToStringAsync<T>(this._viewPath, this._viewModel)
.ConfigureAwait(false);
return this._messageBody;
}

// No render or message body found. e.g. View(), Html() nor Text() were called.
throw new NoMailRendererFound(NoRenderFoundMessage);
}

Expand Down
4 changes: 2 additions & 2 deletions Src/Coravel.Mailer/Mail/Mailers/AssertMailer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ public class AssertMailer : IMailer
{
public class Data
{
public string message { get; set; }
public MessageBody message { get; set; }
public string subject { get; set; }
public IEnumerable<MailRecipient> to { get; set; }
public MailRecipient from { get; set; }
Expand All @@ -27,7 +27,7 @@ public AssertMailer(Action<Data> assertAction)
this._assertAction = assertAction;
}

public 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)
public Task SendAsync(MessageBody message, string subject, IEnumerable<MailRecipient> to, MailRecipient from, MailRecipient replyTo, IEnumerable<MailRecipient> cc, IEnumerable<MailRecipient> bcc, IEnumerable<Attachment> attachments, MailRecipient sender = null)
{
this._assertAction(new Data
{
Expand Down
2 changes: 1 addition & 1 deletion Src/Coravel.Mailer/Mail/Mailers/CanSendMailWrapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public Task<string> RenderAsync<T>(Mailable<T> mailable) =>
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)
public async Task SendAsync(MessageBody 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())
{
Expand Down
4 changes: 2 additions & 2 deletions Src/Coravel.Mailer/Mail/Mailers/CustomMailer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ namespace Coravel.Mailer.Mail.Mailers
{
public class CustomMailer : IMailer
{
public delegate Task SendAsyncFunc(string message, string subject, IEnumerable<MailRecipient> to, MailRecipient from, MailRecipient replyTo, IEnumerable<MailRecipient> cc, IEnumerable<MailRecipient> bcc, IEnumerable<Attachment> attachments = null, MailRecipient sender = null);
public delegate Task SendAsyncFunc(MessageBody message, string subject, IEnumerable<MailRecipient> to, MailRecipient from, MailRecipient replyTo, IEnumerable<MailRecipient> cc, IEnumerable<MailRecipient> bcc, IEnumerable<Attachment> attachments = null, MailRecipient sender = null);
private RazorRenderer _renderer;
private SendAsyncFunc _sendAsyncFunc;
private MailRecipient _globalFrom;
Expand All @@ -25,7 +25,7 @@ public Task<string> RenderAsync<T>(Mailable<T> mailable) =>
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)
public async Task SendAsync(MessageBody message, string subject, IEnumerable<MailRecipient> to, MailRecipient from, MailRecipient replyTo, IEnumerable<MailRecipient> cc, IEnumerable<MailRecipient> bcc, IEnumerable<Attachment> attachments, MailRecipient sender = null)
{
await this._sendAsyncFunc(
message, subject, to, from ?? this._globalFrom, replyTo, cc, bcc, attachments, sender: sender
Expand Down
14 changes: 12 additions & 2 deletions Src/Coravel.Mailer/Mail/Mailers/FileLogMailer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public async Task<string> RenderAsync<T>(Mailable<T> mailable)
return await mailable.RenderAsync(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 = null, MailRecipient sender = null)
public async Task SendAsync(MessageBody message, string subject, IEnumerable<MailRecipient> to, MailRecipient from, MailRecipient replyTo, IEnumerable<MailRecipient> cc, IEnumerable<MailRecipient> bcc, IEnumerable<Attachment> attachments = null, MailRecipient sender = null)
{
from = from ?? this._globalFrom;

Expand All @@ -45,7 +45,17 @@ await writer.WriteAsync($@"
Attachment: { (attachments is null ? "N/A" : string.Join(";", attachments.Select(a => a.Name))) }
---------------------------------------------
{message}
------------------
Message Type: HTML
------------------
{ (message.HasHtmlMessage() ? message.Html : "N/A") }
!!!!!!!!!!!!!!!!!!!!!!!
-----------------------
Message Type: Plan Text
-----------------------
{ (message.HasPlainTextMessage() ? message.Text : "N/A") }
").ConfigureAwait(false);
}
}
Expand Down
6 changes: 3 additions & 3 deletions Src/Coravel.Mailer/Mail/Mailers/SmtpMailer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ 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 = null, MailRecipient sender = null)
public async Task SendAsync(MessageBody message, string subject, IEnumerable<MailRecipient> to, MailRecipient from, MailRecipient replyTo, IEnumerable<MailRecipient> cc, IEnumerable<MailRecipient> bcc, IEnumerable<Attachment> attachments = null, MailRecipient sender = null)
{
var mail = new MimeMessage();

Expand Down Expand Up @@ -82,9 +82,9 @@ public async Task SendAsync(string message, string subject, IEnumerable<MailReci
}
}

private static void SetMailBody(string message, IEnumerable<Attachment> attachments, MimeMessage mail)
private static void SetMailBody(MessageBody message, IEnumerable<Attachment> attachments, MimeMessage mail)
{
var bodyBuilder = new BodyBuilder { HtmlBody = message };
var bodyBuilder = new BodyBuilder { HtmlBody = message.Html, TextBody = message.Text };
if (attachments != null)
{
foreach (var attachment in attachments)
Expand Down
22 changes: 22 additions & 0 deletions Src/Coravel.Mailer/Mail/MessageBody.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
namespace Coravel.Mailer.Mail;

public class MessageBody
{
public string Html { get; set; }
public string Text { get; set; }

public MessageBody(string? html, string? text)

Check warning on line 8 in Src/Coravel.Mailer/Mail/MessageBody.cs

View workflow job for this annotation

GitHub Actions / build_and_tests_on_dotnet_8

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check warning on line 8 in Src/Coravel.Mailer/Mail/MessageBody.cs

View workflow job for this annotation

GitHub Actions / build_and_tests_on_dotnet_8

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check warning on line 8 in Src/Coravel.Mailer/Mail/MessageBody.cs

View workflow job for this annotation

GitHub Actions / build_and_tests_on_dotnet_8

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check warning on line 8 in Src/Coravel.Mailer/Mail/MessageBody.cs

View workflow job for this annotation

GitHub Actions / build_and_tests_on_dotnet_8

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check warning on line 8 in Src/Coravel.Mailer/Mail/MessageBody.cs

View workflow job for this annotation

GitHub Actions / build_and_tests_on_dotnet_7

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check warning on line 8 in Src/Coravel.Mailer/Mail/MessageBody.cs

View workflow job for this annotation

GitHub Actions / build_and_tests_on_dotnet_7

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check warning on line 8 in Src/Coravel.Mailer/Mail/MessageBody.cs

View workflow job for this annotation

GitHub Actions / build_and_tests_on_dotnet_7

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check warning on line 8 in Src/Coravel.Mailer/Mail/MessageBody.cs

View workflow job for this annotation

GitHub Actions / build_and_tests_on_dotnet_7

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check warning on line 8 in Src/Coravel.Mailer/Mail/MessageBody.cs

View workflow job for this annotation

GitHub Actions / build_and_tests_on_dotnet_6

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check warning on line 8 in Src/Coravel.Mailer/Mail/MessageBody.cs

View workflow job for this annotation

GitHub Actions / build_and_tests_on_dotnet_6

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check warning on line 8 in Src/Coravel.Mailer/Mail/MessageBody.cs

View workflow job for this annotation

GitHub Actions / build_and_tests_on_dotnet_6

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check warning on line 8 in Src/Coravel.Mailer/Mail/MessageBody.cs

View workflow job for this annotation

GitHub Actions / build_and_tests_on_dotnet_6

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
{
if(html is null && text is null)
{
throw new System.ArgumentException("You need to supply either an HTML or plain text message - or both.");
}

this.Html = html;
this.Text = text;
}

public bool HasHtmlMessage() => this.Html != null;

public bool HasPlainTextMessage() => this.Text != null;
}
Loading

0 comments on commit 5a96503

Please sign in to comment.