Improved newsletter subscribe, welcome and unsubscribe Mails

This commit is contained in:
Mia Rose Winter 2024-02-28 11:42:42 +01:00
parent 461ac238ac
commit 17ab022bbe
Signed by: miawinter
GPG key ID: 4B6F6A83178F595E
6 changed files with 60 additions and 76 deletions

View file

@ -1,7 +1,6 @@
@page "/Email/Unsubscribe" @page "/Email/Unsubscribe"
@using Microsoft.AspNetCore.Identity.UI.Services @using Microsoft.AspNetCore.Identity.UI.Services
@using Microsoft.EntityFrameworkCore @using Microsoft.EntityFrameworkCore
@using Microsoft.Extensions.Options
@using Wave.Data @using Wave.Data
@using Wave.Services @using Wave.Services
@using Wave.Utilities @using Wave.Utilities
@ -9,8 +8,6 @@
@inject ILogger<EmailEdit> Logger @inject ILogger<EmailEdit> Logger
@inject IStringLocalizer<EmailEdit> Localizer @inject IStringLocalizer<EmailEdit> Localizer
@inject IDbContextFactory<ApplicationDbContext> ContextFactory @inject IDbContextFactory<ApplicationDbContext> ContextFactory
@inject IOptions<Customization> Customizations
@inject NavigationManager Navigation
@inject IEmailSender EmailSender @inject IEmailSender EmailSender
@inject EmailTemplateService TemplateService @inject EmailTemplateService TemplateService
@inject IMessageDisplay Messages @inject IMessageDisplay Messages
@ -78,13 +75,7 @@
await context.SaveChangesAsync(); await context.SaveChangesAsync();
Messages.ShowSuccess(Localizer["Unsubscribe_Success"]); Messages.ShowSuccess(Localizer["Unsubscribe_Success"]);
var customization = Customizations.Value; await EmailSender.SendEmailAsync(subscriber.Email, Localizer["ConfirmEmailSubject"], Localizer["Unsubscribe_ConfirmEmailBody"]);
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 TemplateService.ValidateTokensAsync(Id!, Token!, "unsubscribe-" + Newsletter); // delete token await TemplateService.ValidateTokensAsync(Id!, Token!, "unsubscribe-" + Newsletter); // delete token
} catch (EmailNotSendException ex) { } catch (EmailNotSendException ex) {
Logger.LogWarning(ex, "Failed to send unsubscribe confirm email. The user has been unsubscribed anyway."); Logger.LogWarning(ex, "Failed to send unsubscribe confirm email. The user has been unsubscribed anyway.");

View file

@ -4,7 +4,6 @@
@using Microsoft.Extensions.Options @using Microsoft.Extensions.Options
@using Wave.Data @using Wave.Data
@using System.ComponentModel.DataAnnotations @using System.ComponentModel.DataAnnotations
@using System.Net
@using Microsoft.EntityFrameworkCore @using Microsoft.EntityFrameworkCore
@using Wave.Services @using Wave.Services
@using Wave.Utilities @using Wave.Utilities
@ -13,10 +12,8 @@
@inject IStringLocalizer<EmailSignup> Localizer @inject IStringLocalizer<EmailSignup> Localizer
@inject IDbContextFactory<ApplicationDbContext> ContextFactory @inject IDbContextFactory<ApplicationDbContext> ContextFactory
@inject IOptions<Features> Features @inject IOptions<Features> Features
@inject IOptions<Customization> Customizations @inject IEmailService EmailService
@inject NavigationManager Navigation @inject EmailFactory Email
@inject IAdvancedEmailSender EmailSender
@inject EmailTemplateService TemplateService
@inject IMessageDisplay Messages @inject IMessageDisplay Messages
<PageTitle>@(TitlePrefix + Localizer["Title"])</PageTitle> <PageTitle>@(TitlePrefix + Localizer["Title"])</PageTitle>
@ -57,7 +54,7 @@
if (Id is null || Token is null) return; if (Id is null || Token is null) return;
try { try {
var id = await TemplateService.ValidateTokensAsync(Id, Token, deleteToken: false); var id = await Email.IsTokenValid(Id, Token);
if (id is null) { if (id is null) {
Messages.ShowError(Localizer["Failure_Message"]); Messages.ShowError(Localizer["Failure_Message"]);
@ -74,16 +71,24 @@
await context.SaveChangesAsync(); await context.SaveChangesAsync();
Messages.ShowSuccess(Localizer["Success_Message"]); Messages.ShowSuccess(Localizer["Success_Message"]);
await TemplateService.ValidateTokensAsync(Id, Token, deleteToken: true); await Email.ClearToken(Id, Token);
var articles = await context.Set<EmailNewsletter>() var articles = await context.Set<EmailNewsletter>()
.IgnoreAutoIncludes() .IgnoreAutoIncludes().IgnoreQueryFilters().Where(n => n.IsSend)
.Include(a => a.Article).ThenInclude(a => a.Author) .Include(a => a.Article).ThenInclude(a => a.Author)
.OrderByDescending(a => a.DistributionDateTime) .OrderByDescending(a => a.DistributionDateTime)
.Take(3) .Take(3)
.ToListAsync(); .ToListAsync();
await EmailSender.SendWelcomeMailAsync(subscriber, var mail = await Email.CreateWelcomeEmail(
Localizer["WelcomeEmailSubject"], Localizer["WelcomeEmailTitle"], Localizer["WelcomeEmailBody"], articles); 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) { } catch (Exception ex) {
Logger.LogError(ex, "Error trying to confirm subscriber."); Logger.LogError(ex, "Error trying to confirm subscriber.");
Messages.ShowError(Localizer["Failure_Message"]); Messages.ShowError(Localizer["Failure_Message"]);
@ -110,15 +115,19 @@
await context.SaveChangesAsync(); await context.SaveChangesAsync();
if (subscriber.Unsubscribed) { 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( await EmailService.ConnectAsync(CancellationToken.None);
$"/Email/Confirm?user={WebUtility.UrlEncode(id)}&token={WebUtility.UrlEncode(token)}").AbsoluteUri; await EmailService.SendEmailAsync(email);
string body = string.Format(Localizer["ConfirmEmailBody"], Customizations.Value.AppName) + await EmailService.DisconnectAsync(CancellationToken.None);
$"""<p style="text-align: center"><a href="{confirmLink}">{Localizer["Submit"]}</a></p>""";
await EmailSender.SendDefaultMailAsync(subscriber.Email, subscriber.Name, Localizer["ConfirmEmailSubject"],
Localizer["ConfirmEmailTitle"], body);
} }
Model = new();
} catch (Exception ex) { } catch (Exception ex) {
Logger.LogError(ex, "Failed to create subscriber/send confirmation mail."); Logger.LogError(ex, "Failed to create subscriber/send confirmation mail.");
} }

View file

@ -149,7 +149,6 @@
if (emailConfig.Smtp.Keys.Any(k => k.Equals("live", StringComparison.CurrentCultureIgnoreCase))) { if (emailConfig.Smtp.Keys.Any(k => k.Equals("live", StringComparison.CurrentCultureIgnoreCase))) {
builder.Services.AddScoped(sp => sp.GetKeyedService<IEmailService>("live")!); builder.Services.AddScoped(sp => sp.GetKeyedService<IEmailService>("live")!);
builder.Services.AddScoped<IEmailSender, SmtpEmailSender>(); builder.Services.AddScoped<IEmailSender, SmtpEmailSender>();
builder.Services.AddScoped<IAdvancedEmailSender, SmtpEmailSender>();
builder.Services.AddScoped<IEmailSender<ApplicationUser>, SmtpEmailSender>(); builder.Services.AddScoped<IEmailSender<ApplicationUser>, SmtpEmailSender>();
} else { } else {
builder.Services.AddSingleton<IEmailSender<ApplicationUser>, IdentityNoOpEmailSender>(); builder.Services.AddSingleton<IEmailSender<ApplicationUser>, IdentityNoOpEmailSender>();

View file

@ -4,6 +4,7 @@
using System.Text; using System.Text;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using MimeKit; using MimeKit;
using StackExchange.Redis;
using Wave.Data; using Wave.Data;
using Wave.Utilities; using Wave.Utilities;
@ -20,6 +21,20 @@ public class EmailFactory(IOptions<Customization> customizations, EmailTemplateS
return new StaticEmail(receiverMail, receiverName, subject, title, body, $"{title}\n\n{bodyPlain}", FrozenDictionary<string, string>.Empty); return new StaticEmail(receiverMail, receiverName, subject, title, body, $"{title}\n\n{bodyPlain}", FrozenDictionary<string, string>.Empty);
} }
public async ValueTask<IEmail> 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)
+ $"""<p style="text-align: center"><a href="{confirmLink}">{confirmLabel}</a></p>""",
string.Format(bodyPlain, Customizations.AppName) + $"\n\n{confirmLabel}: {confirmLink}");
}
public async ValueTask<IEmail> CreateSubscribedEmail(EmailSubscriber subscriber, string browserLink, string subject, string title, string bodyHtml, string bodyPlain = "", string role = "unknown", string? replyTo = null) { public async ValueTask<IEmail> 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 host, string logo) = GetStaticData();
@ -44,16 +59,19 @@ public class EmailFactory(IOptions<Customization> customizations, EmailTemplateS
string articlePartial = await TemplateService.GetPartialAsync("email-article"); string articlePartial = await TemplateService.GetPartialAsync("email-article");
string footer = await TemplateService.GetPartialAsync("email-plain-footer"); string footer = await TemplateService.GetPartialAsync("email-plain-footer");
var articlesHtml = new StringBuilder(""); var articlesHtml = new StringBuilder("");
var articlesPlain = new StringBuilder("");
foreach (var n in articles) { foreach (var n in articles) {
string articleLink = ArticleUtilities.GenerateArticleLink(n.Article, new Uri(Customizations.AppUrl, UriKind.Absolute)); string articleLink = ArticleUtilities.GenerateArticleLink(n.Article, new Uri(Customizations.AppUrl, UriKind.Absolute));
articlesHtml.AppendFormat( articlesHtml.AppendFormat(
articlePartial, articlePartial,
n.Article.Title, n.Article.Author.Name, n.Article.Body[..Math.Min(250, n.Article.Body.Length)], articleLink); 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 unsubscribeLink = await GetUnsubscribeLink(host, subscriber.Id, "welcome");
string body = TemplateService.Welcome(host, logo, title, bodyHtml, unsubscribeLink, articlesHtml.ToString()); 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( bodyPlain += "\n\n" + footer.Replace(
$"[[{EmailTemplateService.Constants.EmailUnsubscribeLink}]]", $"[[{EmailTemplateService.Constants.EmailUnsubscribeLink}]]",
unsubscribeLink, true, CultureInfo.InvariantCulture); unsubscribeLink, true, CultureInfo.InvariantCulture);
@ -64,6 +82,14 @@ public class EmailFactory(IOptions<Customization> customizations, EmailTemplateS
}.ToFrozenDictionary()); }.ToFrozenDictionary());
} }
public async ValueTask<Guid?> 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() { private (string host, string logo) GetStaticData() {
var host = new Uri(string.IsNullOrWhiteSpace(Customizations.AppUrl) ? "" : Customizations.AppUrl); // TODO get link var host = new Uri(string.IsNullOrWhiteSpace(Customizations.AppUrl) ? "" : Customizations.AppUrl); // TODO get link
string logo = !string.IsNullOrWhiteSpace(Customizations.LogoLink) string logo = !string.IsNullOrWhiteSpace(Customizations.LogoLink)

View file

@ -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<EmailNewsletter> articles);
}

View file

@ -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.Data;
using Wave.Utilities; using Wave.Utilities;
namespace Wave.Services; namespace Wave.Services;
public class SmtpEmailSender(EmailFactory email, [FromKeyedServices("live")]IEmailService emailService, [FromKeyedServices("bulk")]IEmailService bulkEmailService) : IEmailSender<ApplicationUser>, IAdvancedEmailSender, IAsyncDisposable { public class SmtpEmailSender(EmailFactory email, [FromKeyedServices("live")]IEmailService emailService, [FromKeyedServices("bulk")]IEmailService bulkEmailService) : IEmailSender<ApplicationUser>, IEmailSender, IAsyncDisposable {
private EmailFactory Email { get; } = email; private EmailFactory Email { get; } = email;
private IEmailService EmailService { get; } = emailService; private IEmailService EmailService { get; } = emailService;
private IEmailService BulkEmailService { get; } = bulkEmailService; 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}."); $"Please reset your password using the following code: {resetCode}.");
#endregion #endregion
#region IEmailSender #region IEmailSender
public Task SendEmailAsync(string email, string subject, string htmlMessage) { 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 #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) { public async Task SendDefaultMailAsync(string receiverMail, string? receiverName, string subject, string title, string bodyHtml, string bodyPlain) {
await EmailService.ConnectAsync(CancellationToken.None); await EmailService.ConnectAsync(CancellationToken.None);
var email = await Email.CreateDefaultEmail(receiverMail, receiverName, subject, title, bodyHtml, bodyPlain); 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); 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<EmailNewsletter> 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() { public async ValueTask DisposeAsync() {
GC.SuppressFinalize(this); GC.SuppressFinalize(this);
await EmailService.DisposeAsync(); await EmailService.DisposeAsync();