Implemented http unsubscribe for mail newsletter

This commit is contained in:
Mia Rose Winter 2024-02-13 15:35:24 +01:00
parent e8438c5050
commit 300848a0bb
Signed by: miawinter
GPG key ID: 4B6F6A83178F595E
7 changed files with 492 additions and 18 deletions

View file

@ -0,0 +1,111 @@
@page "/Email/Unsubscribe"
@using Microsoft.AspNetCore.Identity.UI.Services
@using Microsoft.EntityFrameworkCore
@using Microsoft.Extensions.Options
@using Wave.Data
@using Wave.Services
@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
<PageTitle>@(TitlePrefix + Localizer["Title"])</PageTitle>
@if (!string.IsNullOrWhiteSpace(Message)) {
<div class="alert alert-success">
<span>@Message</span>
</div>
}
<BoardComponent CenterContent="true">
<BoardCardComponent Heading="@Localizer["Title"]">
<form method="post" @formname="Unsubscribe" @onsubmit="Unsubscribe_Submit">
<AntiforgeryToken />
<input type="hidden" name="user" value="@Id" />
<input type="hidden" name="token" value="@Token" />
<input type="hidden" name="newsletter" value="@Newsletter" />
<button type="submit" class="btn btn-error w-full">@Localizer["Submit_Unsubscribe"]</button>
</form>
</BoardCardComponent>
</BoardComponent>
@code {
[CascadingParameter(Name = "TitlePrefix")]
private string TitlePrefix { get; set; } = default!;
[Parameter, SupplyParameterFromQuery(Name = "user")]
public string? Id { get; set; }
[Parameter, SupplyParameterFromQuery(Name = "token")]
public string? Token { get; set; }
[Parameter, SupplyParameterFromQuery(Name = "newsletter")]
public int? Newsletter { get; set; }
private string Message { get; set; } = string.Empty;
protected override async Task OnInitializedAsync() {
if (Id is null || Token is null || Newsletter is null) {
if (string.IsNullOrWhiteSpace(Message)) Message = Localizer["Load_Failure_Message"];
return;
}
try {
if (await GetSubscriber() is null) {
Message = Localizer["Load_Failure_Message"];
}
} catch (Exception) {
Message = Localizer["Load_Failure_Message"];
}
}
private async Task Unsubscribe_Submit() {
try {
await using var context = await ContextFactory.CreateDbContextAsync();
var subscriber = await GetSubscriber(context);
if (subscriber is null) {
Message = Localizer["Unsubscribe_Failure_Message"];
return;
}
subscriber.Unsubscribed = true;
await context.SaveChangesAsync();
Message = 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 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.");
} catch (Exception) {
Message = Localizer["Unsubscribe_Failure_Message"];
}
}
private async Task<EmailSubscriber?> GetSubscriber(ApplicationDbContext? context = null) {
if (Id is null || Token is null || Newsletter is null) {
return null;
}
var id = await TemplateService.ValidateTokensAsync(Id, Token, "unsubscribe-" + Newsletter, false);
if (id is null) {
return null;
}
if (context is null) {
await using var context1 = await ContextFactory.CreateDbContextAsync();
return context1.Set<EmailSubscriber>().FirstOrDefault(s => s.Id == id);
}
return context.Set<EmailSubscriber>().FirstOrDefault(s => s.Id == id);
}
}

View file

@ -0,0 +1,125 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 1.3
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">1.3</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1">this is my long string</data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
[base64 mime encoded serialized .NET Framework object]
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
[base64 mime encoded string representing a byte array form of the .NET Framework object]
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>1.3</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Title" xml:space="preserve">
<value>Deabonnieren</value>
</data>
<data name="Submit_Unsubscribe" xml:space="preserve">
<value>Abmelden Bestätigen</value>
</data>
<data name="Load_Failure_Message" xml:space="preserve">
<value>Fehler beim laden des Abonnenten. Ihre Token sind möglicherweise inkorrekt oder abgelaufen. Bitte suchen Sie den neuesten Abmeldelink aus ihrem neuesten Newsletter heraus. If der Fehler weiterhin besteht, kontaktieren Sie den Betreiber dieser Seite.</value>
</data>
<data name="Unsubscribe_Failure_Message" xml:space="preserve">
<value>Fehler beim Abmelden. Ihre Token sind möglicherweise inkorrekt oder abgelaufen. Bitte suchen Sie den neuesten Abmeldelink aus ihrem neuesten Newsletter heraus. If der Fehler weiterhin besteht, kontaktieren Sie den Betreiber dieser Seite.</value>
</data>
<data name="Unsubscribe_Success" xml:space="preserve">
<value>Erfolgreich abgemeldet. Einen schönen Tag noch.</value>
</data>
<data name="ConfirmEmailSubject" xml:space="preserve">
<value>Erfolgreich Abgemeldet</value>
</data>
<data name="Unsubscribe_ConfirmEmailTitle" xml:space="preserve">
<value>Sie wurden erfolgreich vom Mailnewsletter abgemeldet</value>
</data>
<data name="Unsubscribe_ConfirmEmailBody" xml:space="preserve">
<value>Sie erhalten keine weiteren E-Mail Benachrichtigungen über neue Artikel. Wir hoffen Sie waren mit unserem Service zufrieden. Falls Sie sich noch anderst entscheiden oder diese Aktion nicht von ihnen ausgeführt wurde, können Sie sich jederzeit erneut anmelden. </value>
</data>
</root>

View file

@ -0,0 +1,101 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 1.3
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">1.3</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1">this is my long string</data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
[base64 mime encoded serialized .NET Framework object]
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
[base64 mime encoded string representing a byte array form of the .NET Framework object]
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>1.3</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View file

@ -0,0 +1,125 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 1.3
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">1.3</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1">this is my long string</data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
[base64 mime encoded serialized .NET Framework object]
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
[base64 mime encoded string representing a byte array form of the .NET Framework object]
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>1.3</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Title" xml:space="preserve">
<value>Unsubscribe</value>
</data>
<data name="Submit_Unsubscribe" xml:space="preserve">
<value>Confirm Unsubscribe</value>
</data>
<data name="Load_Failure_Message" xml:space="preserve">
<value>Failed to fetch Subscriber. Your tokens may be incorrect or expired. Please look up your unsubscribe link from the newest mail your received. If the error persists, please contact the site owner.</value>
</data>
<data name="Unsubscribe_Failure_Message" xml:space="preserve">
<value>Error unsubscribing. Your tokens may be incorrect or expired. Please look up your unsubscribe link from the newest mail your received. If the error persists, please contact the site owner.</value>
</data>
<data name="Unsubscribe_Success" xml:space="preserve">
<value>Successfully unsubscribed. Have a nice day. </value>
</data>
<data name="Unsubscribe_ConfirmEmailTitle" xml:space="preserve">
<value>You have been unsubscribed from the mailing newsletter</value>
</data>
<data name="Unsubscribe_ConfirmEmailBody" xml:space="preserve">
<value>You will no longer receive mail updates about new Articles. We hope you were satisfyed with our Service. If you have changed your mind or this action was not performed by you, you can easily re-subscribe.</value>
</data>
<data name="ConfirmEmailSubject" xml:space="preserve">
<value>Succsessfully Unsubscribed</value>
</data>
</root>

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.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
@ -70,11 +71,8 @@ public class EmailBackgroundWorker(ILogger<EmailBackgroundWorker> logger, IDbCon
if (!string.IsNullOrWhiteSpace(Configuration.Username)) { if (!string.IsNullOrWhiteSpace(Configuration.Username)) {
client.Authenticate(Configuration.Username, Configuration.Password); client.Authenticate(Configuration.Username, Configuration.Password);
} }
var mjmlRenderer = new MjmlRenderer(); var host = new Uri(Customizations.AppUrl, UriKind.Absolute);
var options = new MjmlOptions {
Beautify = false
};
foreach (var newsletter in newsletters) { foreach (var newsletter in newsletters) {
Logger.LogInformation("Processing '{title}'.", newsletter.Article.Title); Logger.LogInformation("Processing '{title}'.", newsletter.Article.Title);
// set newsletter to send first, so we don't spam people // set newsletter to send first, so we don't spam people
@ -84,35 +82,42 @@ public class EmailBackgroundWorker(ILogger<EmailBackgroundWorker> logger, IDbCon
string articleLink = ArticleUtilities.GenerateArticleLink( string articleLink = ArticleUtilities.GenerateArticleLink(
newsletter.Article, new Uri(Customizations.AppUrl, UriKind.Absolute)); newsletter.Article, new Uri(Customizations.AppUrl, UriKind.Absolute));
string unsubscribeLink = new Uri(new Uri(Customizations.AppUrl, UriKind.Absolute), "/unsubscribe").AbsoluteUri;
string template = TemplateService.Process("newsletter", new Dictionary<EmailTemplateService.Constants, object?>{ string template = TemplateService.Process("newsletter", new Dictionary<EmailTemplateService.Constants, object?>{
{EmailTemplateService.Constants.BrowserLink, articleLink}, {EmailTemplateService.Constants.BrowserLink, articleLink},
{EmailTemplateService.Constants.ContentLogo, "https://blog.winter-software.com/img/logo.png"}, {EmailTemplateService.Constants.ContentLogo, "https://blog.winter-software.com/img/logo.png"},
{EmailTemplateService.Constants.ContentTitle, newsletter.Article.Title}, {EmailTemplateService.Constants.ContentTitle, newsletter.Article.Title},
{EmailTemplateService.Constants.ContentBody, newsletter.Article.BodyHtml}, {EmailTemplateService.Constants.ContentBody, newsletter.Article.BodyHtml},
{EmailTemplateService.Constants.EmailUnsubscribeLink, unsubscribeLink} {EmailTemplateService.Constants.EmailUnsubscribeLink, "[[<__UNSUBSCRIBE__>]]"}
}); });
var message = new MimeMessage { var message = new MimeMessage {
From = { sender }, From = { sender },
Subject = newsletter.Article.Title Subject = newsletter.Article.Title
}; };
var builder = new BodyBuilder {
HtmlBody = mjmlRenderer.Render(template, options).Html
};
message.Body = builder.ToMessageBody();
EmailSubscriber? last = null; EmailSubscriber? last = null;
while (context.Set<EmailSubscriber>() while (context.Set<EmailSubscriber>()
.Where(s => !s.Unsubscribed && (last == null || s.Id > last.Id)) .Where(s => (last == null || s.Id > last.Id))
.OrderBy(s => s.Id) .OrderBy(s => s.Id)
.Take(50) .Take(50)
.ToList() is { Count: > 0 } subscribers) { .ToList() is { Count: > 0 } subscribers) {
last = subscribers.Last(); last = subscribers.Last();
foreach (var subscriber in subscribers) { foreach (var subscriber in subscribers) {
(string user, string token) = TemplateService.CreateConfirmTokensAsync(subscriber.Id, "unsubscribe-" + newsletter.Id, TimeSpan.FromDays(30)).ConfigureAwait(false).GetAwaiter().GetResult();
string unsubscribeLink = new Uri(host,
$"/Email/Unsubscribe?newsletter={newsletter.Id:D}&user={WebUtility.UrlEncode(user)}&token={WebUtility.UrlEncode(token)}").AbsoluteUri;
var builder = new BodyBuilder {
HtmlBody = template.Replace("[[<__UNSUBSCRIBE__>]]", unsubscribeLink)
};
message.To.Clear(); message.To.Clear();
// TODO mailto: unsubscribe:
// List-Unsubscribe: <mailto: unsubscribe@example.com?subject=unsubscribe>, <http://www.example.com/unsubscribe.html>
message.Headers.Add(HeaderId.ListUnsubscribe, $"<{unsubscribeLink}>");
message.To.Add(new MailboxAddress(subscriber.Name, subscriber.Email)); message.To.Add(new MailboxAddress(subscriber.Name, subscriber.Email));
message.Body = builder.ToMessageBody();
client.Send(message); client.Send(message);
} }

View file

@ -29,14 +29,14 @@ public enum Constants {
return (user, token); return (user, token);
} }
public async Task<Guid?> ValidateTokensAsync(string user, string token, string role = "subscribe") { public async Task<Guid?> ValidateTokensAsync(string user, string token, string role = "subscribe", bool deleteToken = true) {
string cacheKey = role + "-" + user; string cacheKey = role + "-" + user;
byte[]? tokenInCache = await TokenCache.GetAsync(cacheKey); byte[]? tokenInCache = await TokenCache.GetAsync(cacheKey);
if (tokenInCache is null || token != Convert.ToBase64String(tokenInCache)) if (tokenInCache is null || token != Convert.ToBase64String(tokenInCache))
return null; return null;
await TokenCache.RemoveAsync(cacheKey); if (deleteToken) await TokenCache.RemoveAsync(cacheKey);
return new Guid(Convert.FromBase64String(user)); return new Guid(Convert.FromBase64String(user));
} }

View file

@ -49,11 +49,18 @@ public class SmtpEmailSender(IOptions<SmtpConfiguration> config, ILogger<SmtpEma
if (!string.IsNullOrWhiteSpace(Configuration.Username)) { if (!string.IsNullOrWhiteSpace(Configuration.Username)) {
await client.AuthenticateAsync(Configuration.Username, Configuration.Password); await client.AuthenticateAsync(Configuration.Username, Configuration.Password);
} }
await client.SendAsync(message);
try {
await client.SendAsync(message);
} catch (Exception ex) {
throw new EmailNotSendException("Failed Email send.", ex);
}
await client.DisconnectAsync(true); await client.DisconnectAsync(true);
} catch (Exception ex) { } catch (Exception ex) {
Logger.LogError(ex, "Error sending E-Mail"); Logger.LogError(ex, "Error sending E-Mail");
throw; throw;
} }
} }
} }
public class EmailNotSendException(string message, Exception exception) : ApplicationException(message, exception);