Skip to content

Commit

Permalink
improvement: Headers in SendEmail don't overwrite default headers any…
Browse files Browse the repository at this point in the history
…more; added ReplyTo
  • Loading branch information
TwentyFourMinutes authored and jhartmann123 committed Jan 25, 2024
1 parent 77ff217 commit cdeddf3
Show file tree
Hide file tree
Showing 5 changed files with 51 additions and 25 deletions.
4 changes: 2 additions & 2 deletions docs/Email/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,5 +47,5 @@ container.Collections.Append<IEmailAttachmentResolver, YourResolver>()

## Headers

No special Headers are sent per default, however with the `Headers` overload on `SendEmail`, `MimeKit.Header`s can be added to the email (overrides all Headers defined in `EmailOptions.DefaultHeaders`). With `EmailOptions.DefaultHeaders` default headers can be set for all emails. Predefined sets of headers can be found in `EmailHeaders`:
- `DiscourageAutoReplies` includes [`Precedence:list`](https://www.rfc-editor.org/rfc/rfc3834), [`AutoSubmitted:generated`](https://www.rfc-editor.org/rfc/rfc3834), and [`X-Auto-Response-Suppress:All`](https://learn.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-oxcmail/e489ffaf-19ed-4285-96d9-c31c42cab17f). They discourage email servers to sent auto replies.
No special Headers are sent per default, however with the `Headers` parameter on `SendEmail`, `MimeKit.Header`s can be added to the email (overrides all Headers defined in `EmailOptions.DefaultHeaders`). With `EmailOptions.DefaultHeaders` default headers can be set for all emails. Predefined sets of headers can be found in `EmailHeaders`:
- `DiscourageAutoReplies` includes [`Precedence:list`](https://www.rfc-editor.org/rfc/rfc3834#section-3.1.8), [`AutoSubmitted:generated`](https://www.rfc-editor.org/rfc/rfc3834#section-3.1.7), and [`X-Auto-Response-Suppress:All`](https://learn.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-oxcmail/e489ffaf-19ed-4285-96d9-c31c42cab17f). They discourage email servers to sent auto replies.
2 changes: 1 addition & 1 deletion src/Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project>
<PropertyGroup>
<Version>7.3.0</Version>
<Version>7.4.0</Version>
<TargetFramework>net7.0</TargetFramework>
</PropertyGroup>

Expand Down
4 changes: 2 additions & 2 deletions src/Email/src/EmailHeaders.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ public static class EmailHeaders
{
public static readonly IReadOnlyDictionary<string, string> DiscourageAutoReplies = new Dictionary<string, string>
{
// https://www.rfc-editor.org/rfc/rfc3834 RFC3834 Section 2 §1
// https://www.rfc-editor.org/rfc/rfc3834#section-3.1.8 RFC3834 Section 3.1.8
[HeaderId.Precedence.ToHeaderName()] = "list",
// https://www.rfc-editor.org/rfc/rfc3834 RFC3834 Section 2 §8
// https://www.rfc-editor.org/rfc/rfc3834#section-3.1.7 RFC3834 Section 3.1.7
[HeaderId.AutoSubmitted.ToHeaderName()] = "generated",
// https://learn.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-oxcmail/e489ffaf-19ed-4285-96d9-c31c42cab17f MS-OXCMAIL Section 2.2.3.2.14
["X-Auto-Response-Suppress"] = "All"
Expand Down
39 changes: 25 additions & 14 deletions src/Email/src/SendEmail.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,16 @@ namespace Fusonic.Extensions.Email;
/// <summary>
/// Sends an email in the background.
/// </summary>
/// <param name="Recipient">Email-address of the recipient</param>
/// <param name="RecipientDisplayName">Display name of the recipient</param>
/// <param name="Culture">Culture to render the email in</param>
/// <param name="Recipient">Email-address of the recipient.</param>
/// <param name="RecipientDisplayName">Display name of the recipient.</param>
/// <param name="Culture">Culture to render the email in.</param>
/// <param name="ViewModel">View model for the email. The model must have an [EmailView]-attribute, providing the path to the email to render (cshtml).</param>
/// <param name="SubjectKey">Subject key to get the subject of the email from the ViewLocalizer. If null, the SubjectKey from the EmailViewAttribute will be used.</param>
/// <param name="BccRecipient">Email-address of the BCC recipient. Optional.</param>
/// <param name="Attachments">Attachments for the email.</param>
/// <param name="SubjectFormatParameters">String formatting parameters for the translated subject. <code>subject = string.Format(subject, SubjectFormatParameters)</code></param>
/// <param name="Headers">Adds the specified Headers to the email and overrides all default headers from the <seealso cref="EmailOptions.DefaultHeaders"/>.</param>
/// <param name="Headers">Adds the specified headers to the email and overrides the default headers from the <seealso cref="EmailOptions.DefaultHeaders"/>.</param>
/// <param name="ReplyTo">Sets the value as the Reply-To header in the email.</param>
public record SendEmail(
string Recipient,
string RecipientDisplayName,
Expand All @@ -29,7 +30,8 @@ public record SendEmail(
string? BccRecipient = null,
Attachment[]? Attachments = null,
object[]? SubjectFormatParameters = null,
IReadOnlyDictionary<string, string>? Headers = null) : ICommand
IReadOnlyDictionary<string, string>? Headers = null,
string? ReplyTo = null) : ICommand
{
[OutOfBand]
public class Handler : AsyncRequestHandler<SendEmail>, IAsyncDisposable
Expand Down Expand Up @@ -63,21 +65,19 @@ protected override async Task Handle(SendEmail request, CancellationToken cancel
Subject = subject
};

var headers = request.Headers ?? emailOptions.DefaultHeaders;

if (headers != null)
if (!string.IsNullOrWhiteSpace(request.BccRecipient))
{
foreach (var (field, value) in headers)
{
message.Headers.Add(field, value);
}
message.Bcc.Add(new MailboxAddress(request.BccRecipient, request.BccRecipient));
}

if (!string.IsNullOrWhiteSpace(request.BccRecipient))
if (!string.IsNullOrWhiteSpace(request.ReplyTo))
{
message.Bcc.Add(new MailboxAddress(request.BccRecipient, request.BccRecipient));
message.ReplyTo.Add(new MailboxAddress(string.Empty, request.ReplyTo));
}

SetHeaders(emailOptions.DefaultHeaders);
SetHeaders(request.Headers);

try
{
await smtpClient.SendMailAsync(message);
Expand All @@ -86,6 +86,17 @@ protected override async Task Handle(SendEmail request, CancellationToken cancel
{
await DisposeStreams();
}

void SetHeaders(IReadOnlyDictionary<string, string>? headers)
{
if (headers == null)
return;

foreach (var (field, value) in headers)
{
message.Headers[field] = value;
}
}
}

private async Task<MimeEntity> GetMessageBody(string htmlBody, Attachment[]? attachments, CancellationToken cancellationToken)
Expand Down
27 changes: 21 additions & 6 deletions src/Email/test/SendEmailTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -148,25 +148,40 @@ public async Task SendEmail_DefaultHeadersAdded()
}

[Fact]
public async Task SendEmail_DefaultHeadersOverridden()
public async Task SendEmail_DefaultHeadersOverriddenIfSetTwice()
{
Fixture.SmtpServer!.ClearReceivedEmail();

var options = GetInstance<EmailOptions>();

options.DefaultHeaders = new Dictionary<string, string> { ["my-header"] = "value" };
options.DefaultHeaders = new Dictionary<string, string> { ["replaced"] = "value", ["default"] = "value" };

var model = new SendEmailTestEmailViewModel { SomeField = "Some field." };
await SendAsync(new SendEmail("[email protected]", "The Recipient", new CultureInfo("de-AT"), model, Headers: new Dictionary<string, string> { ["new-header"] = "new-value" }));
await SendAsync(new SendEmail("[email protected]", "The Recipient", new CultureInfo("de-AT"), model, Headers: new Dictionary<string, string> { ["replaced"] = "new-value" }));

Fixture.SmtpServer.ReceivedEmailCount.Should().Be(1);
var email = Fixture.SmtpServer.ReceivedEmail.Single();

email.Headers.AllKeys.Should().NotContain("my-header");
email.Headers.AllKeys.Should().Contain("new-header");
email.Headers["new-header"].Should().Be("new-value");
email.Headers.AllKeys.Should().Contain("replaced");
email.Headers["replaced"].Should().Be("new-value");
email.Headers.AllKeys.Should().Contain("default");
email.Headers["default"].Should().Be("value");
}

[Fact]
public async Task SendEmail_ReplyToAdded()
{
Fixture.SmtpServer!.ClearReceivedEmail();

var model = new SendEmailTestEmailViewModel { SomeField = "Some field." };
await SendAsync(new SendEmail("[email protected]", "The Recipient", new CultureInfo("de-AT"), model, ReplyTo: "[email protected]"));

Fixture.SmtpServer.ReceivedEmailCount.Should().Be(1);
var email = Fixture.SmtpServer.ReceivedEmail.Single();

email.Headers.AllKeys.Should().Contain("Reply-To");
email.Headers["Reply-To"].Should().Be("[email protected]");
}

[Fact]
public async Task SendEmail_InvalidBccEmailAddress_ThrowsException()
Expand Down

0 comments on commit cdeddf3

Please sign in to comment.