diff --git a/Wave/Dockerfile b/Wave/Dockerfile index 3be05ed..8a81774 100644 --- a/Wave/Dockerfile +++ b/Wave/Dockerfile @@ -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 diff --git a/Wave/Program.cs b/Wave/Program.cs index 2358d34..a56e123 100644 --- a/Wave/Program.cs +++ b/Wave/Program.cs @@ -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(); builder.Services.AddSingleton(); builder.Services.AddHostedService(); diff --git a/Wave/Services/EmailBackgroundWorker.cs b/Wave/Services/EmailBackgroundWorker.cs index 0d10522..4e1e7f4 100644 --- a/Wave/Services/EmailBackgroundWorker.cs +++ b/Wave/Services/EmailBackgroundWorker.cs @@ -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 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 logger, IDbCon newsletter.Article, new Uri(Customizations.AppUrl, UriKind.Absolute)); string template = TemplateService.Process("newsletter", new Dictionary{ {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__>]]"} diff --git a/Wave/Services/EmailTemplateService.cs b/Wave/Services/EmailTemplateService.cs index 39faf81..ce9014e 100644 --- a/Wave/Services/EmailTemplateService.cs +++ b/Wave/Services/EmailTemplateService.cs @@ -4,15 +4,16 @@ namespace Wave.Services; -public partial class EmailTemplateService(ILogger logger, IDistributedCache tokenCache) { +public partial class EmailTemplateService(ILogger logger, IDistributedCache tokenCache, FileSystemService fileSystem) { public enum Constants { BrowserLink, HomeLink, ContentLogo, ContentTitle, ContentBody, EmailUnsubscribeLink } private ILogger 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 data) { var options = new MjmlOptions { Beautify = false }; - string template = $""" - - - - - - - - - Read in Browser - - - - - - - - -

[[{Constants.ContentTitle}]]

-
-
-
- - - - - - - - [[{Constants.ContentBody}]] - - - - - - - - - - - Unsubscribe - - - -
-
- """; - + 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(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 DefaultTemplates { get; } = new() { + { + "default", + $""" + + + + + + + + + + + +

[[{Constants.ContentTitle}]]

+
+
+
+ + + + + + + + [[{Constants.ContentBody}]] + + + + + + + +
+
+ """ + }, + { + "newsletter", + $""" + + + + + + + + + Read in Browser + + + + + + + + + +

[[{Constants.ContentTitle}]]

+
+
+
+ + + + + + + + [[{Constants.ContentBody}]] + + + + + + + + + + + Unsubscribe + + + +
+
+ """ + } + }; } \ No newline at end of file diff --git a/Wave/Services/FileSystemService.cs b/Wave/Services/FileSystemService.cs new file mode 100644 index 0000000..14f6bc5 --- /dev/null +++ b/Wave/Services/FileSystemService.cs @@ -0,0 +1,55 @@ +using System.Text; + +namespace Wave.Services; + +public class FileSystemService(ILogger logger) { + public const string ConfigurationDirectory = "/configuration"; + + private ILogger Logger { get; } = logger; + + public async Task 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; + } + } +} \ No newline at end of file