Implemented customizable email templates
This commit is contained in:
parent
ff3397cd90
commit
56f067d4f5
|
@ -1,11 +1,12 @@
|
|||
#See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging.
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
|
||||
RUN mkdir -p /app/files && chown app /app/files
|
||||
RUN mkdir -p /configuration && chown app /configuration
|
||||
USER app
|
||||
WORKDIR /app
|
||||
RUN mkdir ./files && chown app ./files
|
||||
VOLUME /app/files
|
||||
VOLUME /configuration
|
||||
WORKDIR /app
|
||||
EXPOSE 8080
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
|
||||
|
|
|
@ -20,11 +20,11 @@
|
|||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
builder.Configuration
|
||||
.AddJsonFile("/configuration/config.json", true, false)
|
||||
.AddYamlFile("/configuration/config.yml", true, false)
|
||||
.AddTomlFile("/configuration/config.toml", true, false)
|
||||
.AddIniFile( "/configuration/config.ini", true, false)
|
||||
.AddXmlFile( "/configuration/config.xml", true, false)
|
||||
.AddJsonFile(Path.Combine(FileSystemService.ConfigurationDirectory, "config.json"), true, false)
|
||||
.AddYamlFile(Path.Combine(FileSystemService.ConfigurationDirectory, "config.yml"), true, false)
|
||||
.AddTomlFile(Path.Combine(FileSystemService.ConfigurationDirectory, "config.toml"), true, false)
|
||||
.AddIniFile( Path.Combine(FileSystemService.ConfigurationDirectory, "config.ini"), true, false)
|
||||
.AddXmlFile( Path.Combine(FileSystemService.ConfigurationDirectory, "config.xml"), true, false)
|
||||
.AddEnvironmentVariables("WAVE_");
|
||||
|
||||
builder.Services.AddRazorComponents().AddInteractiveServerComponents();
|
||||
|
@ -128,6 +128,7 @@
|
|||
logMessages.Add("No Email provider configured.");
|
||||
}
|
||||
|
||||
builder.Services.AddSingleton<FileSystemService>();
|
||||
builder.Services.AddSingleton<EmailTemplateService>();
|
||||
builder.Services.AddHostedService<EmailBackgroundWorker>();
|
||||
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Options;
|
||||
using MimeKit;
|
||||
using Mjml.Net;
|
||||
using Wave.Data;
|
||||
using Wave.Utilities;
|
||||
|
||||
|
@ -33,6 +32,8 @@ public class EmailBackgroundWorker(ILogger<EmailBackgroundWorker> logger, IDbCon
|
|||
waitTime, now.AddMinutes(waitTime).LocalDateTime.ToString("u"));
|
||||
Timer = new Timer(DoWork, null, TimeSpan.FromMinutes(waitTime), TimeSpan.FromMinutes(15));
|
||||
|
||||
TemplateService.TryCreateDefaultTemplates();
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
|
@ -84,7 +85,10 @@ public class EmailBackgroundWorker(ILogger<EmailBackgroundWorker> logger, IDbCon
|
|||
newsletter.Article, new Uri(Customizations.AppUrl, UriKind.Absolute));
|
||||
string template = TemplateService.Process("newsletter", new Dictionary<EmailTemplateService.Constants, object?>{
|
||||
{EmailTemplateService.Constants.BrowserLink, articleLink},
|
||||
{EmailTemplateService.Constants.ContentLogo, "https://blog.winter-software.com/img/logo.png"},
|
||||
{EmailTemplateService.Constants.ContentLogo, (!string.IsNullOrWhiteSpace(Customizations.LogoLink) ?
|
||||
new Uri(Customizations.LogoLink) :
|
||||
new Uri(host, "/img/logo.png"))
|
||||
.AbsoluteUri},
|
||||
{EmailTemplateService.Constants.ContentTitle, newsletter.Article.Title},
|
||||
{EmailTemplateService.Constants.ContentBody, newsletter.Article.BodyHtml},
|
||||
{EmailTemplateService.Constants.EmailUnsubscribeLink, "[[<__UNSUBSCRIBE__>]]"}
|
||||
|
|
|
@ -4,15 +4,16 @@
|
|||
|
||||
namespace Wave.Services;
|
||||
|
||||
public partial class EmailTemplateService(ILogger<EmailTemplateService> logger, IDistributedCache tokenCache) {
|
||||
public partial class EmailTemplateService(ILogger<EmailTemplateService> logger, IDistributedCache tokenCache, FileSystemService fileSystem) {
|
||||
public enum Constants {
|
||||
BrowserLink, HomeLink, ContentLogo, ContentTitle, ContentBody, EmailUnsubscribeLink
|
||||
}
|
||||
|
||||
private ILogger<EmailTemplateService> Logger { get; } = logger;
|
||||
private IMjmlRenderer Renderer { get; } = new MjmlRenderer();
|
||||
private MjmlRenderer Renderer { get; } = new();
|
||||
private IDistributedCache TokenCache { get; } = tokenCache;
|
||||
|
||||
private FileSystemService FileSystem { get; } = fileSystem;
|
||||
|
||||
private Regex TokenMatcher { get; } = MyRegex();
|
||||
|
||||
public async Task<(string user, string token)> CreateConfirmTokensAsync(Guid subscriberId, string role = "subscribe", TimeSpan? expiration = null) {
|
||||
|
@ -50,59 +51,20 @@ public enum Constants {
|
|||
});
|
||||
}
|
||||
|
||||
public void TryCreateDefaultTemplates() {
|
||||
FileSystem.GetEmailTemplate("default", DefaultTemplates["default"]);
|
||||
FileSystem.GetEmailTemplate("newsletter", DefaultTemplates["newsletter"]);
|
||||
}
|
||||
|
||||
public string Process(string templateName, Dictionary<Constants, object?> data) {
|
||||
var options = new MjmlOptions {
|
||||
Beautify = false
|
||||
};
|
||||
|
||||
string template = $"""
|
||||
<mjml>
|
||||
<mj-head>
|
||||
<mj-preview/>
|
||||
</mj-head>
|
||||
<mj-body>
|
||||
<mj-section>
|
||||
<mj-column>
|
||||
<mj-text align="center" font-size="13px" font-family="Ubuntu,Verdana">
|
||||
<a href="[[{Constants.BrowserLink}]]">Read in Browser</a></mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-section direction="rtl" padding-bottom="15px" padding-left="0px" padding-right="0px" padding-top="15px" padding="15px 0px 15px 0px">
|
||||
<mj-column vertical-align="middle" width="33%">
|
||||
<mj-image align="center" alt="" border-radius="0" border="none" container-background-color="transparent" height="auto" padding-bottom="5px" padding-left="5px" padding-right="5px" padding-top="5px" padding="5px 5px 5px 5px" href="[[{Constants.HomeLink}]]" src="[[{Constants.ContentLogo}]]"></mj-image>
|
||||
</mj-column>
|
||||
<mj-column vertical-align="middle" width="67%">
|
||||
<mj-text font-size="13px" font-family="Ubuntu,Verdana">
|
||||
<h1>[[{Constants.ContentTitle}]]</h1>
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-section>
|
||||
<mj-column>
|
||||
<mj-divider border-width="1px"></mj-divider>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-section>
|
||||
<mj-column>
|
||||
<mj-text color="#55575d" font-size="13px" font-family="Ubuntu,Verdana">[[{Constants.ContentBody}]]</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-section>
|
||||
<mj-column>
|
||||
<mj-divider border-width="1px"></mj-divider>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-section>
|
||||
<mj-column>
|
||||
<mj-text align="center" font-size="13px" font-family="Ubuntu,Verdana">
|
||||
<a href="[[{Constants.EmailUnsubscribeLink}]]">Unsubscribe</a>
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
</mj-body>
|
||||
</mjml>
|
||||
""";
|
||||
|
||||
string template = FileSystem.GetEmailTemplate(templateName,
|
||||
DefaultTemplates.TryGetValue(templateName, out string? s) ? s : null)
|
||||
?? throw new ApplicationException("Failed to retrieve mail template " + templateName + ".");
|
||||
|
||||
template = TokenMatcher.Replace(template, t =>
|
||||
data.TryGetValue(Enum.Parse<Constants>(t.Value[2..^2], true), out object? v) ?
|
||||
v?.ToString() ?? "" :
|
||||
|
@ -121,4 +83,96 @@ public enum Constants {
|
|||
[GeneratedRegex(@"(\[\[.*?\]\])",
|
||||
RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.Singleline | RegexOptions.CultureInvariant)]
|
||||
private static partial Regex MyRegex();
|
||||
|
||||
|
||||
private Dictionary<string, string> DefaultTemplates { get; } = new() {
|
||||
{
|
||||
"default",
|
||||
$"""
|
||||
<mjml>
|
||||
<mj-head>
|
||||
<mj-preview />
|
||||
</mj-head>
|
||||
<mj-body>
|
||||
<mj-section direction="rtl" padding-bottom="5px" padding-left="0px" padding-right="0px" padding-top="15px" padding="15px 0px 5px 0px">
|
||||
<mj-column vertical-align="middle" width="33%">
|
||||
<mj-image align="center" alt="" border-radius="0" border="none" container-background-color="transparent" height="auto" padding-bottom="5px" padding-left="5px" padding-right="5px" padding-top="5px" padding="5px 5px 5px 5px" href="[[{Constants.HomeLink}]]" src="[[{Constants.ContentLogo}]]"></mj-image>
|
||||
</mj-column>
|
||||
<mj-column vertical-align="middle" width="67%">
|
||||
<mj-text font-size="13px" font-family="Ubuntu,Verdana">
|
||||
<h1>[[{Constants.ContentTitle}]]</h1>
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-section padding-top="5px" padding-bottom="5px" padding="5px 0 5px 0">
|
||||
<mj-column>
|
||||
<mj-divider border-color="#9f9f9f" border-width="1px"></mj-divider>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-section>
|
||||
<mj-column>
|
||||
<mj-text color="#55575d" font-size="13px" font-family="Ubuntu,Verdana">[[{Constants.ContentBody}]]</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-section padding-top="5px" padding-bottom="5px" padding="5px 0 5px 0">
|
||||
<mj-column>
|
||||
<mj-divider border-color="#9f9f9f" border-width="1px"></mj-divider>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
</mj-body>
|
||||
</mjml>
|
||||
"""
|
||||
},
|
||||
{
|
||||
"newsletter",
|
||||
$"""
|
||||
<mjml>
|
||||
<mj-head>
|
||||
<mj-preview />
|
||||
</mj-head>
|
||||
<mj-body>
|
||||
<mj-section>
|
||||
<mj-column>
|
||||
<mj-text align="center" font-size="13px" font-family="Ubuntu,Verdana">
|
||||
<a href="[[{Constants.BrowserLink}]]">Read in Browser</a>
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-section direction="rtl" padding-bottom="5px" padding-left="0px" padding-right="0px" padding-top="15px" padding="15px 0px 5px 0px">
|
||||
<mj-column vertical-align="middle" width="33%">
|
||||
<mj-image align="center" alt="" border-radius="0" border="none" container-background-color="transparent" height="auto" padding-bottom="5px" padding-left="5px" padding-right="5px" padding-top="5px" padding="5px 5px 5px 5px" href="[[{Constants.HomeLink}]]" src="[[{Constants.ContentLogo}]]"></mj-image>
|
||||
</mj-column>
|
||||
<mj-column vertical-align="middle" width="67%">
|
||||
<mj-text font-size="13px" font-family="Ubuntu,Verdana">
|
||||
<h1>[[{Constants.ContentTitle}]]</h1>
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-section padding-top="5px" padding-bottom="5px" padding="5px 0 5px 0">
|
||||
<mj-column>
|
||||
<mj-divider border-color="#9f9f9f" border-width="1px"></mj-divider>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-section>
|
||||
<mj-column>
|
||||
<mj-text color="#55575d" font-size="13px" font-family="Ubuntu,Verdana">[[{Constants.ContentBody}]]</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-section padding-top="5px" padding-bottom="5px" padding="5px 0 5px 0">
|
||||
<mj-column>
|
||||
<mj-divider border-color="#9f9f9f" border-width="1px"></mj-divider>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-section>
|
||||
<mj-column>
|
||||
<mj-text align="center" font-size="13px" font-family="Ubuntu,Verdana">
|
||||
<a href="[[{Constants.EmailUnsubscribeLink}]]">Unsubscribe</a>
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
</mj-body>
|
||||
</mjml>
|
||||
"""
|
||||
}
|
||||
};
|
||||
}
|
55
Wave/Services/FileSystemService.cs
Normal file
55
Wave/Services/FileSystemService.cs
Normal file
|
@ -0,0 +1,55 @@
|
|||
using System.Text;
|
||||
|
||||
namespace Wave.Services;
|
||||
|
||||
public class FileSystemService(ILogger<FileSystemService> logger) {
|
||||
public const string ConfigurationDirectory = "/configuration";
|
||||
|
||||
private ILogger<FileSystemService> Logger { get; } = logger;
|
||||
|
||||
public async Task<string?> GetEmailTemplateAsync(string name, string? defaultTemplate = null) {
|
||||
string path = Path.Combine(ConfigurationDirectory, "templates", "email", name + ".mjml");
|
||||
|
||||
if (!File.Exists(path)) {
|
||||
try {
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
|
||||
if (!string.IsNullOrWhiteSpace(defaultTemplate)) {
|
||||
await File.WriteAllTextAsync(path, defaultTemplate, Encoding.UTF8);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Logger.LogError(ex, "File system access failed trying write default E-Mail template '{template}'", name);
|
||||
return defaultTemplate;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
return await File.ReadAllTextAsync(path, Encoding.UTF8);
|
||||
} catch (Exception ex) {
|
||||
Logger.LogError(ex, "File system access failed trying to retrieve E-Mail template '{template}'", name);
|
||||
return defaultTemplate;
|
||||
}
|
||||
}
|
||||
|
||||
public string? GetEmailTemplate(string name, string? defaultTemplate = null) {
|
||||
string path = Path.Combine(ConfigurationDirectory, "templates", "email", name + ".mjml");
|
||||
|
||||
if (!File.Exists(path)) {
|
||||
try {
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
|
||||
if (!string.IsNullOrWhiteSpace(defaultTemplate)) {
|
||||
File.WriteAllText(path, defaultTemplate, Encoding.UTF8);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Logger.LogError(ex, "File system access failed trying write default E-Mail template '{template}'", name);
|
||||
return defaultTemplate;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
return File.ReadAllText(path, Encoding.UTF8);
|
||||
} catch (Exception ex) {
|
||||
Logger.LogError(ex, "File system access failed trying to retrieve E-Mail template '{template}'", name);
|
||||
return defaultTemplate;
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue