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"
@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<EmailEdit> Logger
@inject IStringLocalizer<EmailEdit> Localizer
@inject IDbContextFactory<ApplicationDbContext> ContextFactory
@inject IOptions<Customization> 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.");

View file

@ -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<EmailSignup> Localizer
@inject IDbContextFactory<ApplicationDbContext> ContextFactory
@inject IOptions<Features> Features
@inject IOptions<Customization> Customizations
@inject NavigationManager Navigation
@inject IAdvancedEmailSender EmailSender
@inject EmailTemplateService TemplateService
@inject IEmailService EmailService
@inject EmailFactory Email
@inject IMessageDisplay Messages
<PageTitle>@(TitlePrefix + Localizer["Title"])</PageTitle>
@ -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<EmailNewsletter>()
.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) +
$"""<p style="text-align: center"><a href="{confirmLink}">{Localizer["Submit"]}</a></p>""";
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.");
}

View file

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

View file

@ -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<Customization> customizations, EmailTemplateS
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) {
(string host, string logo) = GetStaticData();
@ -44,16 +59,19 @@ public class EmailFactory(IOptions<Customization> 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<Customization> customizations, EmailTemplateS
}.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() {
var host = new Uri(string.IsNullOrWhiteSpace(Customizations.AppUrl) ? "" : Customizations.AppUrl); // TODO get link
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.Utilities;
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 IEmailService EmailService { get; } = emailService;
private IEmailService BulkEmailService { get; } = bulkEmailService;
@ -32,24 +32,11 @@ public class SmtpEmailSender(EmailFactory email, [FromKeyedServices("live")]IEma
#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<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() {
GC.SuppressFinalize(this);
await EmailService.DisposeAsync();