Implemented welcome email on signup, Improved unsubscribe mail handling

This commit is contained in:
Mia Rose Winter 2024-02-13 23:41:14 +01:00
parent 84986065bf
commit 735f564a64
Signed by: miawinter
GPG key ID: 4B6F6A83178F595E
7 changed files with 106 additions and 40 deletions

View file

@ -7,6 +7,7 @@
@using System.Net @using System.Net
@using Microsoft.EntityFrameworkCore @using Microsoft.EntityFrameworkCore
@using Wave.Services @using Wave.Services
@using Wave.Utilities
@inject ILogger<EmailSignup> Logger @inject ILogger<EmailSignup> Logger
@inject IStringLocalizer<EmailSignup> Localizer @inject IStringLocalizer<EmailSignup> Localizer
@ -63,7 +64,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); var id = await TemplateService.ValidateTokensAsync(Id, Token, deleteToken: false);
if (id is null) { if (id is null) {
Message = Localizer["Failure_Message"]; Message = Localizer["Failure_Message"];
@ -78,11 +79,30 @@
} }
subscriber.Unsubscribed = false; subscriber.Unsubscribed = false;
await context.SaveChangesAsync(); await context.SaveChangesAsync();
await TemplateService.ValidateTokensAsync(Id, Token, deleteToken: true);
Message = Localizer["Success_Message"]; Message = Localizer["Success_Message"];
await TemplateService.ValidateTokensAsync(Id, Token, deleteToken: true);
var articles = await context.Set<EmailNewsletter>()
.IgnoreAutoIncludes()
.Include(a => a.Article).ThenInclude(a => a.Author)
.OrderByDescending(a => a.DistributionDateTime)
.Take(3)
.ToListAsync();
string body = $"<p>{Localizer["WelcomeEmailBody"]}</p>\n";
foreach (var n in articles) {
string articleLink = ArticleUtilities.GenerateArticleLink(n.Article, new Uri(Customizations.Value.AppUrl, UriKind.Absolute));
body = body +
"<div style=\"padding: 10px; background: #333; color: #fff; margin-bottom: 10px\">" +
$"<h3>{n.Article.Title}</h3>" +
$"<small>{n.Article.Author.Name}</small>" +
$"<p>{n.Article.Body[..Math.Min(100, n.Article.Body.Length)]}...</p>" +
$"<a href=\"{articleLink}\">Link</a>" +
"</div>";
}
await EmailSender.SendSubscribedMailAsync(subscriber,
Localizer["WelcomeEmailSubject"], Localizer["WelcomeEmailTitle"], body, subscribedRole: "welcome");
} catch (Exception ex) { } catch (Exception ex) {
Logger.LogError(ex, "Error trying to confirm subscriber."); Logger.LogError(ex, "Error trying to confirm subscriber.");
Message = Localizer["Failure_Message"]; Message = Localizer["Failure_Message"];

View file

@ -15,7 +15,7 @@ public class ApplicationUser : IdentityUser {
public string Biography { get; set; } = string.Empty; public string Biography { get; set; } = string.Empty;
public string BiographyHtml { get; set; } = string.Empty; public string BiographyHtml { get; set; } = string.Empty;
public string Name => FullName ?? UserName ?? "Anon"; public string Name => FullName ?? "Guest Author";
public IList<Article> Articles { get; set; } = []; public IList<Article> Articles { get; set; } = [];
public IList<UserLink> Links { get; set; } = []; public IList<UserLink> Links { get; set; } = [];

View file

@ -128,4 +128,13 @@
<data name="Success_Message" xml:space="preserve"> <data name="Success_Message" xml:space="preserve">
<value>Ihre E-Mail wurde erfolgreich bestätigt. Sie werden nun in zukunft Benachrichtigungen zu neuen Artikeln erhalten.</value> <value>Ihre E-Mail wurde erfolgreich bestätigt. Sie werden nun in zukunft Benachrichtigungen zu neuen Artikeln erhalten.</value>
</data> </data>
<data name="WelcomeEmailTitle" xml:space="preserve">
<value>Sie sind nun Abonniert!</value>
</data>
<data name="WelcomeEmailSubject" xml:space="preserve">
<value>Ihr Newsletter-Abonnement wurde Bestätigt</value>
</data>
<data name="WelcomeEmailBody" xml:space="preserve">
<value>Sie werden von nun an über neue Artikel informiert. Schauen Sie doch mal in der folgenden Sektion was Sie vielleicht verpasst haben. Sie können sich jeder Zeit in diesem oder zukünftigen E-Mails abmelden über den Link den Sie am ende finden.</value>
</data>
</root> </root>

View file

@ -134,4 +134,13 @@
<data name="Success_Message" xml:space="preserve"> <data name="Success_Message" xml:space="preserve">
<value>Your Email has been confirmed successfully. You will not receive notifications about future articles.</value> <value>Your Email has been confirmed successfully. You will not receive notifications about future articles.</value>
</data> </data>
<data name="WelcomeEmailSubject" xml:space="preserve">
<value>Your Newsletter Subscription has been Confirmed</value>
</data>
<data name="WelcomeEmailTitle" xml:space="preserve">
<value>You are now subscribed!</value>
</data>
<data name="WelcomeEmailBody" xml:space="preserve">
<value>You are now being notified about new articles coming out. Have a look at the following section for what you might have missed. You can always unsubscribe using the link at the bottom of this or future Emails.</value>
</data>
</root> </root>

View file

@ -114,6 +114,7 @@ public class EmailBackgroundWorker(ILogger<EmailBackgroundWorker> logger, IDbCon
// TODO mailto: unsubscribe: // TODO mailto: unsubscribe:
// List-Unsubscribe: <mailto: unsubscribe@example.com?subject=unsubscribe>, <http://www.example.com/unsubscribe.html> // List-Unsubscribe: <mailto: unsubscribe@example.com?subject=unsubscribe>, <http://www.example.com/unsubscribe.html>
message.Headers.Add(HeaderId.ListUnsubscribe, $"<{unsubscribeLink}>"); message.Headers.Add(HeaderId.ListUnsubscribe, $"<{unsubscribeLink}>");
message.Headers.Add(HeaderId.ListUnsubscribePost, "One-Click");
message.To.Add(new MailboxAddress(subscriber.Name, subscriber.Email)); message.To.Add(new MailboxAddress(subscriber.Name, subscriber.Email));
message.Body = builder.ToMessageBody(); message.Body = builder.ToMessageBody();
client.Send(message); client.Send(message);

View file

@ -1,8 +1,11 @@
using Microsoft.AspNetCore.Identity.UI.Services; using Microsoft.AspNetCore.Identity.UI.Services;
using Wave.Data;
namespace Wave.Services; namespace Wave.Services;
public interface IAdvancedEmailSender : IEmailSender { public interface IAdvancedEmailSender : IEmailSender {
Task SendEmailAsync(string email, string? name, string subject, string htmlMessage); Task SendEmailAsync(string email, string? name, string subject, string htmlMessage);
Task SendDefaultMailAsync(string receiverMail, string? receiverName, string subject, string title, string bodyHtml); 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");
} }

View file

@ -1,4 +1,5 @@
using MailKit.Net.Smtp; using System.Net;
using MailKit.Net.Smtp;
using MailKit.Security; using MailKit.Security;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
@ -10,45 +11,50 @@ namespace Wave.Services;
public class SmtpEmailSender(ILogger<SmtpEmailSender> logger, IOptions<SmtpConfiguration> config, IOptions<Customization> customizations, EmailTemplateService templateService) : IEmailSender<ApplicationUser>, IAdvancedEmailSender { public class SmtpEmailSender(ILogger<SmtpEmailSender> logger, IOptions<SmtpConfiguration> config, IOptions<Customization> customizations, EmailTemplateService templateService) : IEmailSender<ApplicationUser>, IAdvancedEmailSender {
private ILogger<SmtpEmailSender> Logger { get; } = logger; private ILogger<SmtpEmailSender> Logger { get; } = logger;
private SmtpConfiguration Configuration { get; } = config.Value; private SmtpConfiguration Configuration { get; } = config.Value;
private Customization Customizations { get; } = customizations.Value; private Customization Customizations { get; } = customizations.Value;
private EmailTemplateService TemplateService { get; } = templateService; private EmailTemplateService TemplateService { get; } = templateService;
public Task SendConfirmationLinkAsync(ApplicationUser user, string email, string confirmationLink) => public Task SendConfirmationLinkAsync(ApplicationUser user, string email, string confirmationLink) =>
SendEmailAsync(email, "Confirm your email", SendEmailAsync(email, "Confirm your email",
$"Please confirm your account by <a href='{confirmationLink}'>clicking here</a>."); $"Please confirm your account by <a href='{confirmationLink}'>clicking here</a>.");
public Task SendPasswordResetLinkAsync(ApplicationUser user, string email, string resetLink) => public Task SendPasswordResetLinkAsync(ApplicationUser user, string email, string resetLink) =>
SendEmailAsync(email, "Reset your password", SendEmailAsync(email, "Reset your password",
$"Please reset your password by <a href='{resetLink}'>clicking here</a>."); $"Please reset your password by <a href='{resetLink}'>clicking here</a>.");
public Task SendPasswordResetCodeAsync(ApplicationUser user, string email, string resetCode) => public Task SendPasswordResetCodeAsync(ApplicationUser user, string email, string resetCode) =>
SendEmailAsync(email, "Reset your password", SendEmailAsync(email, "Reset your password",
$"Please reset your password using the following code: {resetCode}"); $"Please reset your password using the following code: {resetCode}");
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 SendEmailAsync(email, null, subject, htmlMessage);
} }
public async Task SendEmailAsync(string email, string? name, string subject, string htmlMessage) { public Task SendEmailAsync(string email, string? name, string subject, string htmlMessage)
try { => SendEmailAsync(email, name, subject, htmlMessage, []);
var message = new MimeMessage { public async Task SendEmailAsync(string email, string? name, string subject, string htmlMessage, params Header[] header) {
From = {new MailboxAddress(Configuration.SenderName, Configuration.SenderEmail)}, try {
To = { new MailboxAddress(name, email) }, var message = new MimeMessage {
Subject = subject From = {new MailboxAddress(Configuration.SenderName, Configuration.SenderEmail)},
}; To = { new MailboxAddress(name, email) },
Subject = subject
};
var builder = new BodyBuilder { var builder = new BodyBuilder {
HtmlBody = htmlMessage HtmlBody = htmlMessage
}; };
message.Body = builder.ToMessageBody(); message.Body = builder.ToMessageBody();
foreach (var h in header) {
message.Headers.Add(h);
}
using var client = new SmtpClient(); using var client = new SmtpClient();
await client.ConnectAsync(Configuration.Host, Configuration.Port, await client.ConnectAsync(Configuration.Host, Configuration.Port,
Configuration.Ssl ? SecureSocketOptions.SslOnConnect : SecureSocketOptions.None); Configuration.Ssl ? SecureSocketOptions.SslOnConnect : SecureSocketOptions.None);
if (!string.IsNullOrWhiteSpace(Configuration.Username)) { if (!string.IsNullOrWhiteSpace(Configuration.Username)) {
await client.AuthenticateAsync(Configuration.Username, Configuration.Password); await client.AuthenticateAsync(Configuration.Username, Configuration.Password);
} }
@ -57,22 +63,40 @@ public class SmtpEmailSender(ILogger<SmtpEmailSender> logger, IOptions<SmtpConfi
} catch (Exception ex) { } catch (Exception ex) {
throw new EmailNotSendException("Failed Email send.", ex); throw new EmailNotSendException("Failed Email send.", ex);
} }
await client.DisconnectAsync(true); await client.DisconnectAsync(true);
Logger.LogInformation("Successfully send mail to {email} (subject: {subject}).", email, subject); Logger.LogInformation("Successfully send mail to {email} (subject: {subject}).", email, subject);
} catch (Exception ex) { } catch (Exception ex) {
Logger.LogError(ex, "Error sending E-Mail"); Logger.LogError(ex, "Error sending E-Mail");
throw; throw;
} }
} }
public Task SendDefaultMailAsync(string receiverMail, string? receiverName, string subject, string title, string bodyHtml) { public Task SendDefaultMailAsync(string receiverMail, string? receiverName, string subject, string title, string bodyHtml) {
var host = new Uri(string.IsNullOrWhiteSpace(Customizations.AppUrl) ? "" : Customizations.AppUrl); 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)
? Customizations.LogoLink ? Customizations.LogoLink
: new Uri(host, "/img/logo.png").AbsoluteUri; : new Uri(host, "/img/logo.png").AbsoluteUri;
string body = TemplateService.Default(host.AbsoluteUri, logo, title, bodyHtml); string body = TemplateService.Default(host.AbsoluteUri, logo, title, bodyHtml);
return SendEmailAsync(receiverMail, receiverName, subject, body); return SendEmailAsync(receiverMail, receiverName, subject, body);
} }
public async Task SendSubscribedMailAsync(EmailSubscriber subscriber, string subject, string title, string bodyHtml,
string browserUrl = "", string subscribedRole = "-1") {
(string user, string token) = await TemplateService
.CreateConfirmTokensAsync(subscriber.Id, "unsubscribe-"+subscribedRole, TimeSpan.FromDays(30));
var host = new Uri(string.IsNullOrWhiteSpace(Customizations.AppUrl) ? "" : Customizations.AppUrl); // TODO get link
browserUrl = string.IsNullOrWhiteSpace(browserUrl) ? host.AbsoluteUri : browserUrl; // TODO find better solution
string logo = !string.IsNullOrWhiteSpace(Customizations.LogoLink)
? Customizations.LogoLink
: new Uri(host, "/img/logo.png").AbsoluteUri;
string unsubscribeLink = new Uri(host,
$"/Email/Unsubscribe?newsletter={subscribedRole}&user={WebUtility.UrlEncode(user)}&token={WebUtility.UrlEncode(token)}").AbsoluteUri;
string body = TemplateService.Newsletter(host.AbsoluteUri, browserUrl, logo, title, bodyHtml, unsubscribeLink);
await SendEmailAsync(subscriber.Email, subscriber.Name, subject, body,
new Header(HeaderId.ListUnsubscribe, $"<{unsubscribeLink}>"),
new Header(HeaderId.ListUnsubscribePost, "One-Click"));
}
} }
public class EmailNotSendException(string message, Exception exception) : ApplicationException(message, exception); public class EmailNotSendException(string message, Exception exception) : ApplicationException(message, exception);