From 17ab022bbe111777c6d4b6a180d4bbcbbaa68329 Mon Sep 17 00:00:00 2001 From: Mia Winter Date: Wed, 28 Feb 2024 11:42:42 +0100 Subject: [PATCH] Improved newsletter subscribe, welcome and unsubscribe Mails --- Wave/Components/Pages/EmailEdit.razor | 11 +----- Wave/Components/Pages/EmailSignup.razor | 45 +++++++++++++++---------- Wave/Program.cs | 1 - Wave/Services/EmailFactory.cs | 28 ++++++++++++++- Wave/Services/IAdvancedEmailSender.cs | 14 -------- Wave/Services/SmtpEmailSender.cs | 37 +++----------------- 6 files changed, 60 insertions(+), 76 deletions(-) delete mode 100644 Wave/Services/IAdvancedEmailSender.cs diff --git a/Wave/Components/Pages/EmailEdit.razor b/Wave/Components/Pages/EmailEdit.razor index e54495b..90a4cc9 100644 --- a/Wave/Components/Pages/EmailEdit.razor +++ b/Wave/Components/Pages/EmailEdit.razor @@ -1,7 +1,6 @@ @page "/Email/Unsubscribe" @using Microsoft.AspNetCore.Identity.UI.Services @using Microsoft.EntityFrameworkCore -@using Microsoft.Extensions.Options @using Wave.Data @using Wave.Services @using Wave.Utilities @@ -9,8 +8,6 @@ @inject ILogger Logger @inject IStringLocalizer Localizer @inject IDbContextFactory ContextFactory -@inject IOptions Customizations -@inject NavigationManager Navigation @inject IEmailSender EmailSender @inject EmailTemplateService TemplateService @inject IMessageDisplay Messages @@ -78,13 +75,7 @@ await context.SaveChangesAsync(); Messages.ShowSuccess(Localizer["Unsubscribe_Success"]); - var customization = Customizations.Value; - string body = TemplateService.Default( - Navigation.BaseUri, - !string.IsNullOrWhiteSpace(customization.LogoLink) ? customization.LogoLink : Navigation.ToAbsoluteUri("/img/logo.png").AbsoluteUri, - Localizer["Unsubscribe_ConfirmEmailTitle"], - Localizer["Unsubscribe_ConfirmEmailBody"]); - await EmailSender.SendEmailAsync(subscriber.Email, Localizer["ConfirmEmailSubject"], body); + await EmailSender.SendEmailAsync(subscriber.Email, Localizer["ConfirmEmailSubject"], Localizer["Unsubscribe_ConfirmEmailBody"]); await TemplateService.ValidateTokensAsync(Id!, Token!, "unsubscribe-" + Newsletter); // delete token } catch (EmailNotSendException ex) { Logger.LogWarning(ex, "Failed to send unsubscribe confirm email. The user has been unsubscribed anyway."); diff --git a/Wave/Components/Pages/EmailSignup.razor b/Wave/Components/Pages/EmailSignup.razor index 71947b7..1fbce3f 100644 --- a/Wave/Components/Pages/EmailSignup.razor +++ b/Wave/Components/Pages/EmailSignup.razor @@ -4,7 +4,6 @@ @using Microsoft.Extensions.Options @using Wave.Data @using System.ComponentModel.DataAnnotations -@using System.Net @using Microsoft.EntityFrameworkCore @using Wave.Services @using Wave.Utilities @@ -13,10 +12,8 @@ @inject IStringLocalizer Localizer @inject IDbContextFactory ContextFactory @inject IOptions Features -@inject IOptions Customizations -@inject NavigationManager Navigation -@inject IAdvancedEmailSender EmailSender -@inject EmailTemplateService TemplateService +@inject IEmailService EmailService +@inject EmailFactory Email @inject IMessageDisplay Messages @(TitlePrefix + Localizer["Title"]) @@ -57,7 +54,7 @@ if (Id is null || Token is null) return; try { - var id = await TemplateService.ValidateTokensAsync(Id, Token, deleteToken: false); + var id = await Email.IsTokenValid(Id, Token); if (id is null) { Messages.ShowError(Localizer["Failure_Message"]); @@ -74,16 +71,24 @@ await context.SaveChangesAsync(); Messages.ShowSuccess(Localizer["Success_Message"]); - await TemplateService.ValidateTokensAsync(Id, Token, deleteToken: true); - + await Email.ClearToken(Id, Token); + var articles = await context.Set() - .IgnoreAutoIncludes() + .IgnoreAutoIncludes().IgnoreQueryFilters().Where(n => n.IsSend) .Include(a => a.Article).ThenInclude(a => a.Author) .OrderByDescending(a => a.DistributionDateTime) .Take(3) .ToListAsync(); - await EmailSender.SendWelcomeMailAsync(subscriber, - Localizer["WelcomeEmailSubject"], Localizer["WelcomeEmailTitle"], Localizer["WelcomeEmailBody"], articles); + var mail = await Email.CreateWelcomeEmail( + subscriber, articles, + Localizer["WelcomeEmailSubject"], + Localizer["WelcomeEmailTitle"], + Localizer["WelcomeEmailBody"], + Localizer["WelcomeEmailBody"]); + + await EmailService.ConnectAsync(CancellationToken.None); + await EmailService.SendEmailAsync(mail); + await EmailService.DisconnectAsync(CancellationToken.None); } catch (Exception ex) { Logger.LogError(ex, "Error trying to confirm subscriber."); Messages.ShowError(Localizer["Failure_Message"]); @@ -110,15 +115,19 @@ await context.SaveChangesAsync(); if (subscriber.Unsubscribed) { - (string id, string token) = await TemplateService.CreateConfirmTokensAsync(subscriber.Id); + var email = await Email.CreateConfirmationEmail(subscriber, + Localizer["ConfirmEmailSubject"], + Localizer["ConfirmEmailTitle"], + Localizer["ConfirmEmailBody"], + Localizer["ConfirmEmailBody"], + Localizer["Submit"]); - string confirmLink = Navigation.ToAbsoluteUri( - $"/Email/Confirm?user={WebUtility.UrlEncode(id)}&token={WebUtility.UrlEncode(token)}").AbsoluteUri; - string body = string.Format(Localizer["ConfirmEmailBody"], Customizations.Value.AppName) + - $"""

{Localizer["Submit"]}

"""; - await EmailSender.SendDefaultMailAsync(subscriber.Email, subscriber.Name, Localizer["ConfirmEmailSubject"], - Localizer["ConfirmEmailTitle"], body); + await EmailService.ConnectAsync(CancellationToken.None); + await EmailService.SendEmailAsync(email); + await EmailService.DisconnectAsync(CancellationToken.None); } + + Model = new(); } catch (Exception ex) { Logger.LogError(ex, "Failed to create subscriber/send confirmation mail."); } diff --git a/Wave/Program.cs b/Wave/Program.cs index 36c56f5..c09ec2d 100644 --- a/Wave/Program.cs +++ b/Wave/Program.cs @@ -149,7 +149,6 @@ if (emailConfig.Smtp.Keys.Any(k => k.Equals("live", StringComparison.CurrentCultureIgnoreCase))) { builder.Services.AddScoped(sp => sp.GetKeyedService("live")!); builder.Services.AddScoped(); - builder.Services.AddScoped(); builder.Services.AddScoped, SmtpEmailSender>(); } else { builder.Services.AddSingleton, IdentityNoOpEmailSender>(); diff --git a/Wave/Services/EmailFactory.cs b/Wave/Services/EmailFactory.cs index 90dffbf..ffd2072 100644 --- a/Wave/Services/EmailFactory.cs +++ b/Wave/Services/EmailFactory.cs @@ -4,6 +4,7 @@ using System.Text; using Microsoft.Extensions.Options; using MimeKit; +using StackExchange.Redis; using Wave.Data; using Wave.Utilities; @@ -20,6 +21,20 @@ public class EmailFactory(IOptions customizations, EmailTemplateS return new StaticEmail(receiverMail, receiverName, subject, title, body, $"{title}\n\n{bodyPlain}", FrozenDictionary.Empty); } + public async ValueTask CreateConfirmationEmail(EmailSubscriber subscriber, string subject, string title, string bodyHtml, string bodyPlain = "", string confirmLabel = "confirm") { + (string user, string token) = await TemplateService.CreateConfirmTokensAsync(subscriber.Id); + (string? host, string _) = GetStaticData(); + string confirmLink = new Uri( + new Uri(host, UriKind.Absolute), + new Uri($"/Email/Confirm?user={WebUtility.UrlEncode(user)}&token={WebUtility.UrlEncode(token)}", UriKind.Relative)) + .AbsoluteUri; + + return await CreateDefaultEmail(subscriber.Email, subscriber.Name, subject, title, + string.Format(bodyHtml, Customizations.AppName) + + $"""

{confirmLabel}

""", + string.Format(bodyPlain, Customizations.AppName) + $"\n\n{confirmLabel}: {confirmLink}"); + } + 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(); @@ -44,16 +59,19 @@ public class EmailFactory(IOptions customizations, EmailTemplateS string articlePartial = await TemplateService.GetPartialAsync("email-article"); string footer = await TemplateService.GetPartialAsync("email-plain-footer"); var articlesHtml = new StringBuilder(""); + var articlesPlain = new StringBuilder(""); foreach (var n in articles) { string articleLink = ArticleUtilities.GenerateArticleLink(n.Article, new Uri(Customizations.AppUrl, UriKind.Absolute)); articlesHtml.AppendFormat( articlePartial, n.Article.Title, n.Article.Author.Name, n.Article.Body[..Math.Min(250, n.Article.Body.Length)], articleLink); + articlesPlain.AppendFormat("{0}\n\n{1}\n{2}\n{3}", + n.Article.Title, n.Article.Author.Name, n.Article.Body[..Math.Min(250, n.Article.Body.Length)], articleLink); } 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\n" + articlesPlain; bodyPlain += "\n\n" + footer.Replace( $"[[{EmailTemplateService.Constants.EmailUnsubscribeLink}]]", unsubscribeLink, true, CultureInfo.InvariantCulture); @@ -64,6 +82,14 @@ public class EmailFactory(IOptions customizations, EmailTemplateS }.ToFrozenDictionary()); } + public async ValueTask IsTokenValid(string id, string token) { + return await TemplateService.ValidateTokensAsync(id, token, deleteToken: false); + } + + public async ValueTask ClearToken(string id, string token) { + await TemplateService.ValidateTokensAsync(id, token, deleteToken: true); + } + private (string host, string logo) GetStaticData() { var host = new Uri(string.IsNullOrWhiteSpace(Customizations.AppUrl) ? "" : Customizations.AppUrl); // TODO get link string logo = !string.IsNullOrWhiteSpace(Customizations.LogoLink) diff --git a/Wave/Services/IAdvancedEmailSender.cs b/Wave/Services/IAdvancedEmailSender.cs deleted file mode 100644 index afee6a2..0000000 --- a/Wave/Services/IAdvancedEmailSender.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Microsoft.AspNetCore.Identity.UI.Services; -using Wave.Data; - -namespace Wave.Services; - -[Obsolete] -public interface IAdvancedEmailSender : IEmailSender { - Task SendEmailAsync(string email, string? name, string subject, string htmlMessage); - Task SendDefaultMailAsync(string receiverMail, string? receiverName, string subject, string title, string bodyHtml); - Task SendSubscribedMailAsync(EmailSubscriber subscriber, string subject, string title, string bodyHtml, - string browserUrl = "", string subscribedRole = "-1"); - Task SendWelcomeMailAsync(EmailSubscriber subscriber, string subject, string title, string bodyHtml, - IEnumerable articles); -} \ No newline at end of file diff --git a/Wave/Services/SmtpEmailSender.cs b/Wave/Services/SmtpEmailSender.cs index f965f3a..2f068b0 100644 --- a/Wave/Services/SmtpEmailSender.cs +++ b/Wave/Services/SmtpEmailSender.cs @@ -1,11 +1,11 @@ -using Azure.Core; -using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity.UI.Services; using Wave.Data; using Wave.Utilities; namespace Wave.Services; -public class SmtpEmailSender(EmailFactory email, [FromKeyedServices("live")]IEmailService emailService, [FromKeyedServices("bulk")]IEmailService bulkEmailService) : IEmailSender, IAdvancedEmailSender, IAsyncDisposable { +public class SmtpEmailSender(EmailFactory email, [FromKeyedServices("live")]IEmailService emailService, [FromKeyedServices("bulk")]IEmailService bulkEmailService) : IEmailSender, IEmailSender, IAsyncDisposable { private EmailFactory Email { get; } = email; private IEmailService EmailService { get; } = emailService; private IEmailService BulkEmailService { get; } = bulkEmailService; @@ -28,28 +28,15 @@ public class SmtpEmailSender(EmailFactory email, [FromKeyedServices("live")]IEma $"Please reset your password using the following code: {resetCode}."); #endregion - + #region IEmailSender public Task SendEmailAsync(string email, string subject, string htmlMessage) { - return SendEmailAsync(email, null, subject, htmlMessage); + return SendDefaultMailAsync(email, null, subject, subject, htmlMessage, HtmlUtilities.GetPlainText(htmlMessage)); } #endregion - - 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, 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, 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); @@ -57,20 +44,6 @@ public class SmtpEmailSender(EmailFactory email, [FromKeyedServices("live")]IEma 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, 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, HtmlUtilities.GetPlainText(bodyHtml)); - await BulkEmailService.ConnectAsync(CancellationToken.None); - await BulkEmailService.SendEmailAsync(email); - } - public async ValueTask DisposeAsync() { GC.SuppressFinalize(this); await EmailService.DisposeAsync();