Implemented http unsubscribe for mail newsletter
This commit is contained in:
parent
e8438c5050
commit
300848a0bb
111
Wave/Components/Pages/EmailEdit.razor
Normal file
111
Wave/Components/Pages/EmailEdit.razor
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
125
Wave/Resources/Components/Pages/EmailEdit.de-DE.resx
Normal file
125
Wave/Resources/Components/Pages/EmailEdit.de-DE.resx
Normal 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>
|
101
Wave/Resources/Components/Pages/EmailEdit.en-GB.resx
Normal file
101
Wave/Resources/Components/Pages/EmailEdit.en-GB.resx
Normal 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>
|
125
Wave/Resources/Components/Pages/EmailEdit.resx
Normal file
125
Wave/Resources/Components/Pages/EmailEdit.resx
Normal 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>
|
|
@ -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;
|
||||||
|
@ -71,10 +72,7 @@ public class EmailBackgroundWorker(ILogger<EmailBackgroundWorker> logger, IDbCon
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -49,7 +49,12 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
await client.SendAsync(message);
|
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");
|
||||||
|
@ -57,3 +62,5 @@ public class SmtpEmailSender(IOptions<SmtpConfiguration> config, ILogger<SmtpEma
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class EmailNotSendException(string message, Exception exception) : ApplicationException(message, exception);
|
Loading…
Reference in a new issue