From c8b7f9f44f09a88bda2e70506d7055eead7ebceb Mon Sep 17 00:00:00 2001 From: Mia Winter Date: Thu, 22 Feb 2024 17:32:06 +0100 Subject: [PATCH] Implemented plain text content in newsletter emails --- Wave/Services/EmailFactory.cs | 22 +++++++++---- Wave/Services/EmailTemplateService.cs | 7 ++++- Wave/Services/IEmail.cs | 1 + Wave/Services/LiveEmailService.cs | 5 ++- Wave/Services/NewsletterBackgroundService.cs | 8 +++-- Wave/Services/SmtpEmailSender.cs | 33 +++++++++++++------- Wave/Services/StaticEmail.cs | 5 +-- 7 files changed, 56 insertions(+), 25 deletions(-) diff --git a/Wave/Services/EmailFactory.cs b/Wave/Services/EmailFactory.cs index bd76f3f..90dffbf 100644 --- a/Wave/Services/EmailFactory.cs +++ b/Wave/Services/EmailFactory.cs @@ -1,4 +1,5 @@ using System.Collections.Frozen; +using System.Globalization; using System.Net; using System.Text; using Microsoft.Extensions.Options; @@ -12,31 +13,36 @@ public class EmailFactory(IOptions customizations, EmailTemplateS private Customization Customizations { get; } = customizations.Value; private EmailTemplateService TemplateService { get; } = templateService; - public async ValueTask CreateDefaultEmail(string receiverMail, string? receiverName, string subject, string title, string bodyHtml) { + public async ValueTask CreateDefaultEmail(string receiverMail, string? receiverName, string subject, string title, string bodyHtml, string bodyPlain = "") { (string host, string logo) = GetStaticData(); string body = await TemplateService.DefaultAsync(host, logo, title, bodyHtml); - return new StaticEmail(receiverMail, receiverName, subject, title, body, FrozenDictionary.Empty); + return new StaticEmail(receiverMail, receiverName, subject, title, body, $"{title}\n\n{bodyPlain}", FrozenDictionary.Empty); } - public async ValueTask CreateSubscribedEmail(EmailSubscriber subscriber, string browserLink, string subject, string title, string bodyHtml, string role = "unknown", string? replyTo = null) { + public async ValueTask CreateSubscribedEmail(EmailSubscriber subscriber, string browserLink, string subject, string title, string bodyHtml, string bodyPlain = "", string role = "unknown", string? replyTo = null) { (string host, string logo) = GetStaticData(); string unsubscribeLink = await GetUnsubscribeLink(host, subscriber.Id, role); string body = await TemplateService.NewsletterAsync(host, browserLink, logo, title, bodyHtml, unsubscribeLink); + string footer = await TemplateService.GetPartialAsync("email-plain-footer"); + bodyPlain += "\n\n" + footer.Replace( + $"[[{EmailTemplateService.Constants.EmailUnsubscribeLink}]]", + unsubscribeLink, true, CultureInfo.InvariantCulture); var headers = new Dictionary{ {HeaderId.ListUnsubscribe.ToHeaderName(), $"<{unsubscribeLink}>"}, {HeaderId.ListUnsubscribePost.ToHeaderName(), "One-Click"} }; if (!string.IsNullOrWhiteSpace(replyTo)) headers.Add(HeaderId.ReplyTo.ToHeaderName(), replyTo); - return new StaticEmail(subscriber.Email, subscriber.Name, subject, title, body, headers.ToFrozenDictionary()); + return new StaticEmail(subscriber.Email, subscriber.Name, subject, title, body, $"{title}\n\n{bodyPlain}", headers.ToFrozenDictionary()); } - public async ValueTask CreateWelcomeEmail(EmailSubscriber subscriber, IEnumerable articles, string subject, string title, string bodyHtml) { + public async ValueTask CreateWelcomeEmail(EmailSubscriber subscriber, IEnumerable articles, string subject, string title, string bodyHtml, string bodyPlain = "") { (string host, string logo) = GetStaticData(); string articlePartial = await TemplateService.GetPartialAsync("email-article"); + string footer = await TemplateService.GetPartialAsync("email-plain-footer"); var articlesHtml = new StringBuilder(""); foreach (var n in articles) { string articleLink = ArticleUtilities.GenerateArticleLink(n.Article, new Uri(Customizations.AppUrl, UriKind.Absolute)); @@ -47,8 +53,12 @@ public class EmailFactory(IOptions customizations, EmailTemplateS string unsubscribeLink = await GetUnsubscribeLink(host, subscriber.Id, "welcome"); string body = TemplateService.Welcome(host, logo, title, bodyHtml, unsubscribeLink, articlesHtml.ToString()); + bodyPlain += "\n" + HtmlUtilities.GetPlainText(articlesHtml.ToString()); + bodyPlain += "\n\n" + footer.Replace( + $"[[{EmailTemplateService.Constants.EmailUnsubscribeLink}]]", + unsubscribeLink, true, CultureInfo.InvariantCulture); - return new StaticEmail(subscriber.Email, subscriber.Name, subject, title, body, new Dictionary{ + return new StaticEmail(subscriber.Email, subscriber.Name, subject, title, body, $"{title}\n\n{bodyPlain}", new Dictionary{ {HeaderId.ListUnsubscribe.ToHeaderName(), $"<{unsubscribeLink}>"}, {HeaderId.ListUnsubscribePost.ToHeaderName(), "One-Click"} }.ToFrozenDictionary()); diff --git a/Wave/Services/EmailTemplateService.cs b/Wave/Services/EmailTemplateService.cs index 54aa254..32ce245 100644 --- a/Wave/Services/EmailTemplateService.cs +++ b/Wave/Services/EmailTemplateService.cs @@ -97,6 +97,7 @@ public enum Constants { FileSystem.GetEmailTemplate("newsletter", DefaultTemplates["newsletter"]); FileSystem.GetEmailTemplate("welcome", DefaultTemplates["welcome"]); FileSystem.GetPartialTemplate("email-article", DefaultPartials["email-article"]); + FileSystem.GetPartialTemplate("email-plain-footer", DefaultPartials["email-plain-footer"]); } public string ApplyTokens(string template, Func replacer) { @@ -132,7 +133,7 @@ public enum Constants { return html; } - + public string Process(string templateName, Dictionary data) { string template = ApplyTokens(GetTemplate(templateName), token => data.TryGetValue(Enum.Parse(token, true), out object? v) ? v?.ToString() : null); @@ -307,6 +308,10 @@ public enum Constants { Link """ + }, + { + "email-plain-footer", + $"Unsubscribe: [[{Constants.EmailUnsubscribeLink}]]" } }; } \ No newline at end of file diff --git a/Wave/Services/IEmail.cs b/Wave/Services/IEmail.cs index dd790a8..7432b23 100644 --- a/Wave/Services/IEmail.cs +++ b/Wave/Services/IEmail.cs @@ -8,5 +8,6 @@ public interface IEmail { string Subject { get; } string Title { get; } string ContentHtml { get; } + string ContentPlain { get; } FrozenDictionary Headers { get; } } \ No newline at end of file diff --git a/Wave/Services/LiveEmailService.cs b/Wave/Services/LiveEmailService.cs index e0b9ef9..db18188 100644 --- a/Wave/Services/LiveEmailService.cs +++ b/Wave/Services/LiveEmailService.cs @@ -52,7 +52,10 @@ public class LiveEmailService(ILogger logger, IOptions lo last = subscribers.Last(); foreach (var subscriber in subscribers) { - var email = await factory.CreateSubscribedEmail(subscriber, articleLink, newsletter.Article.Title, - newsletter.Article.Title, newsletter.Article.BodyHtml, newsletter.Id.ToString(), replyTo); + var email = await factory.CreateSubscribedEmail(subscriber, articleLink, + newsletter.Article.Title, + newsletter.Article.Title, + newsletter.Article.BodyHtml, + newsletter.Article.BodyPlain, + "newsletter-" + newsletter.Id, replyTo); await client.SendEmailAsync(email); } } diff --git a/Wave/Services/SmtpEmailSender.cs b/Wave/Services/SmtpEmailSender.cs index ab25b7e..f965f3a 100644 --- a/Wave/Services/SmtpEmailSender.cs +++ b/Wave/Services/SmtpEmailSender.cs @@ -1,5 +1,7 @@ -using Microsoft.AspNetCore.Identity; +using Azure.Core; +using Microsoft.AspNetCore.Identity; using Wave.Data; +using Wave.Utilities; namespace Wave.Services; @@ -11,16 +13,19 @@ public class SmtpEmailSender(EmailFactory email, [FromKeyedServices("live")]IEma #region IEmailSenderAsync public Task SendConfirmationLinkAsync(ApplicationUser user, string email, string confirmationLink) => - SendEmailAsync(email, user.FullName, "Confirm your email", - $"Please confirm your account by clicking here."); + SendDefaultMailAsync(email, user.FullName, "Confirm your email", "Confirm your email", + $"

Please confirm your account by clicking here.

", + $"Please confirm your account by clicking here: {confirmationLink}"); public Task SendPasswordResetLinkAsync(ApplicationUser user, string email, string resetLink) => - SendEmailAsync(email, user.FullName, "Reset your password", - $"Please reset your password by clicking here."); + SendDefaultMailAsync(email, user.FullName, "Reset your password", "Reset your password", + $"

Please reset your password by clicking here.

", + $"Please reset your password by clicking here: {resetLink}"); public Task SendPasswordResetCodeAsync(ApplicationUser user, string email, string resetCode) => - SendEmailAsync(email, user.FullName, "Reset your password", - $"Please reset your password using the following code: {resetCode}"); + SendDefaultMailAsync(email, user.FullName, "Reset your password", "Reset your password", + $"

Please reset your password using the following code: {resetCode}.

", + $"Please reset your password using the following code: {resetCode}."); #endregion @@ -35,27 +40,33 @@ public class SmtpEmailSender(EmailFactory email, [FromKeyedServices("live")]IEma public async Task SendEmailAsync(string email, string? name, string subject, string htmlMessage) { await EmailService.ConnectAsync(CancellationToken.None); - await EmailService.SendEmailAsync(await Email.CreateDefaultEmail(email, name, subject, subject, htmlMessage)); + await EmailService.SendEmailAsync(await Email.CreateDefaultEmail(email, name, subject, subject, htmlMessage, HtmlUtilities.GetPlainText(htmlMessage))); await EmailService.DisconnectAsync(CancellationToken.None); } public async Task SendDefaultMailAsync(string receiverMail, string? receiverName, string subject, string title, string bodyHtml) { await EmailService.ConnectAsync(CancellationToken.None); - var email = await Email.CreateDefaultEmail(receiverMail, receiverName, subject, title, bodyHtml); + var email = await Email.CreateDefaultEmail(receiverMail, receiverName, subject, title, bodyHtml, HtmlUtilities.GetPlainText(bodyHtml)); + await EmailService.SendEmailAsync(email); + await EmailService.DisconnectAsync(CancellationToken.None); + } + public async Task SendDefaultMailAsync(string receiverMail, string? receiverName, string subject, string title, string bodyHtml, string bodyPlain) { + await EmailService.ConnectAsync(CancellationToken.None); + var email = await Email.CreateDefaultEmail(receiverMail, receiverName, subject, title, bodyHtml, bodyPlain); await EmailService.SendEmailAsync(email); await EmailService.DisconnectAsync(CancellationToken.None); } public async Task SendSubscribedMailAsync(EmailSubscriber subscriber, string subject, string title, string bodyHtml, string browserUrl = "", string subscribedRole = "-1") { - var email = await Email.CreateSubscribedEmail(subscriber, browserUrl, subject, title, bodyHtml, subscribedRole); + var email = await Email.CreateSubscribedEmail(subscriber, browserUrl, subject, title, bodyHtml, HtmlUtilities.GetPlainText(bodyHtml), subscribedRole); await BulkEmailService.ConnectAsync(CancellationToken.None); await BulkEmailService.SendEmailAsync(email); } public async Task SendWelcomeMailAsync(EmailSubscriber subscriber, string subject, string title, string bodyHtml, IEnumerable articles) { - var email = await Email.CreateWelcomeEmail(subscriber, articles, subject, title, bodyHtml); + var email = await Email.CreateWelcomeEmail(subscriber, articles, subject, title, bodyHtml, HtmlUtilities.GetPlainText(bodyHtml)); await BulkEmailService.ConnectAsync(CancellationToken.None); await BulkEmailService.SendEmailAsync(email); } diff --git a/Wave/Services/StaticEmail.cs b/Wave/Services/StaticEmail.cs index 0830285..d3e799c 100644 --- a/Wave/Services/StaticEmail.cs +++ b/Wave/Services/StaticEmail.cs @@ -1,8 +1,5 @@ using System.Collections.Frozen; -using Microsoft.Extensions.Hosting; -using Mjml.Net.Helpers; namespace Wave.Services; -public record StaticEmail(string ReceiverEmail, string? ReceiverName, string Subject, string Title, string ContentHtml, - FrozenDictionary Headers) : IEmail; \ No newline at end of file +public record StaticEmail(string ReceiverEmail, string? ReceiverName, string Subject, string Title, string ContentHtml, string ContentPlain, FrozenDictionary Headers) : IEmail; \ No newline at end of file