/* * Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: Emails.Transactional * File: EmailTemplateStore.cs * * EmailTemplateStore.cs is part of Emails.Transactional which is part of the larger * VNLib collection of libraries and utilities. * * Emails.Transactional is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published * by the Free Software Foundation, either version 2 of the License, * or (at your option) any later version. * * Emails.Transactional is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Emails.Transactional. If not, see http://www.gnu.org/licenses/. */ using System; using System.IO; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Collections.Generic; using Fluid; using VNLib.Utils.IO; using VNLib.Utils.Extensions; using VNLib.Utils.Memory.Caching; using VNLib.Plugins; using VNLib.Plugins.Extensions.Loading; using VNLib.Plugins.Extensions.Data.Storage; using Emails.Transactional.Mta; namespace Emails.Transactional.Templates { [ConfigurationName("templates")] internal sealed class EmailTemplateStore : ITemplateStorage { private readonly FluidParser _parser; private readonly Dictionary _templateCache; private readonly ISimpleFilesystem _filesystem; private readonly TimeSpan _cacheValidFor; public EmailTemplateStore(PluginBase plugin, IConfigScope config) { _parser = new(); _templateCache = new(StringComparer.OrdinalIgnoreCase); _cacheValidFor = config["cache_valid_for_sec"].GetTimeSpan(TimeParseType.Seconds); string fsPath = config.GetRequiredProperty("template_path", e => e.GetString()!); //Get the filesystem ISimpleFilesystem baseFs = plugin.GetOrCreateSingleton(); //Create a new scope for the base path _filesystem = baseFs.CreateNewScope(fsPath); } public Task GetTemplateAsync(string templateId, CancellationToken cancellation) { //try to get the template from the cache if (_templateCache.TryGetOrEvictRecord(templateId, out EmailTemplate? template) > 0) { return Task.FromResult(template!); } //Load the template from the store return GetTemplateFromStoreAsync(templateId, cancellation); } private async Task GetTemplateFromStoreAsync(string templateId, CancellationToken cancellation) { //memory stream for template data using VnMemoryStream templateData = new(); //remove leading slash if (templateId.StartsWith('/')) { templateId = templateId[1..]; } //Recover template data long read = await _filesystem.ReadFileAsync(templateId, templateData, cancellation); if(read <= 0) { throw new TemplateLookupFailedException($"Template {templateId} not found"); } //Rewind the stream templateData.Seek(0, SeekOrigin.Begin); //To string string templateString = Encoding.UTF8.GetString(templateData.AsSpan()); //Try to parse the template and raise exception if it fails if (!_parser.TryParse(templateString, out IFluidTemplate template, out string error)) { throw new TemplateLookupFailedException($"A template parse error occured: {error}"); } //Create new email template EmailTemplate et = new(template); //Store template in cache _templateCache.StoreRecord(templateId, et, _cacheValidFor); return et; } private sealed record class EmailTemplate(IFluidTemplate Template) : IEmailTemplate, ICacheable { /// public DateTime Expires { get; set; } /// public bool Equals(ICacheable? other) => ReferenceEquals(this, other); /// public void Evicted() { } /// public IEmailMessageData RenderTemplate(object variables) { //Create a template model TemplateContext ctx = new(variables); string html = Template.Render(ctx); return new EmailMessageData(html); } private sealed record class EmailMessageData(string Rendered) : IEmailMessageData { public string GetHtml() => Rendered; } } } }