aboutsummaryrefslogtreecommitdiff
path: root/lib/Emails.Transactional.Plugin/src
diff options
context:
space:
mode:
authorLibravatar vnugent <public@vaughnnugent.com>2023-01-12 17:47:40 -0500
committerLibravatar vnugent <public@vaughnnugent.com>2023-01-12 17:47:40 -0500
commitd797953c74798252d7153a20e788ed034c71b0ae (patch)
treebb295b619a4c0ed13d18691063ddaebd3961faf5 /lib/Emails.Transactional.Plugin/src
parentd8ef5d21416c4a9deaa5cae7d3c8a11fae6a15f7 (diff)
Large project reorder and consolidation
Diffstat (limited to 'lib/Emails.Transactional.Plugin/src')
-rw-r--r--lib/Emails.Transactional.Plugin/src/Api Endpoints/SendEndpoint.cs286
-rw-r--r--lib/Emails.Transactional.Plugin/src/EmailDbCtx.cs40
-rw-r--r--lib/Emails.Transactional.Plugin/src/EmailTransaction.cs162
-rw-r--r--lib/Emails.Transactional.Plugin/src/SmtpProvider.cs118
-rw-r--r--lib/Emails.Transactional.Plugin/src/TEmailEntryPoint.cs88
-rw-r--r--lib/Emails.Transactional.Plugin/src/Transactional Emails.csproj64
-rw-r--r--lib/Emails.Transactional.Plugin/src/Transactions/EmailTransactionValidator.cs133
-rw-r--r--lib/Emails.Transactional.Plugin/src/Transactions/TransactionResult.cs45
-rw-r--r--lib/Emails.Transactional.Plugin/src/Transactions/TransactionStore.cs74
9 files changed, 1010 insertions, 0 deletions
diff --git a/lib/Emails.Transactional.Plugin/src/Api Endpoints/SendEndpoint.cs b/lib/Emails.Transactional.Plugin/src/Api Endpoints/SendEndpoint.cs
new file mode 100644
index 0000000..9dd7f6a
--- /dev/null
+++ b/lib/Emails.Transactional.Plugin/src/Api Endpoints/SendEndpoint.cs
@@ -0,0 +1,286 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: Transactional Emails
+* File: SendEndpoint.cs
+*
+* SendEndpoint.cs is part of Transactional Emails which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* Transactional Emails 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.
+*
+* Transactional Emails 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 Transactional Emails. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.IO;
+using System.Net;
+using System.Text;
+using System.Text.Json;
+using System.Threading.Tasks;
+using System.Collections.Generic;
+
+using Fluid;
+
+using Minio;
+using Minio.DataModel;
+using Minio.Exceptions;
+
+using MimeKit.Text;
+
+using VNLib.Utils.Extensions;
+using VNLib.Utils.Logging;
+using VNLib.Utils.Memory.Caching;
+using VNLib.Plugins;
+using VNLib.Plugins.Essentials;
+using VNLib.Plugins.Essentials.Extensions;
+using VNLib.Plugins.Essentials.Oauth;
+using VNLib.Plugins.Extensions.Validation;
+using VNLib.Plugins.Extensions.Loading;
+using VNLib.Plugins.Extensions.Loading.Sql;
+
+using Emails.Transactional.Transactions;
+using static Emails.Transactional.TEmailEntryPoint;
+
+namespace Emails.Transactional.Endpoints
+{
+ [ConfigurationName("transaction_endpoint")]
+ internal class SendEndpoint : O2EndpointBase
+ {
+ public const string OAUTH2_USER_SCOPE_PERMISSION = "email:send";
+
+ private class FluidCache : ICacheable
+ {
+ public FluidCache(IFluidTemplate temp)
+ {
+ this.Template = temp;
+ }
+
+ public IFluidTemplate Template { get; }
+
+ DateTime ICacheable.Expires { get; set; }
+
+ bool IEquatable<ICacheable>.Equals(ICacheable? other)
+ {
+ return ReferenceEquals(Template, (other as FluidCache)?.Template);
+ }
+
+ void ICacheable.Evicted()
+ {}
+ }
+
+ private static readonly EmailTransactionValidator Validator = new();
+ private static readonly FluidParser Parser = new();
+
+ private readonly string FromAddress;
+ private readonly string? BaseObjectPath;
+
+ private readonly TransactionStore Transactions;
+
+ private readonly Dictionary<string, FluidCache> TemplateCache;
+ private readonly TimeSpan TemplateCacheTimeout;
+ private readonly MinioClient Client;
+ private readonly S3Config S3Config;
+
+ private SmtpProvider? EmailService;
+
+ public SendEndpoint(PluginBase plugin, IReadOnlyDictionary<string, JsonElement> config)
+ {
+ string? path = config["path"].GetString();
+ FromAddress = config["from_address"].GetString() ?? throw new KeyNotFoundException("Missing required key 'from_address' in 'transaction_endpoint'");
+ TemplateCacheTimeout = config["cache_valid_sec"].GetTimeSpan(TimeParseType.Seconds);
+ BaseObjectPath = config["base_object_path"].GetString();
+
+ InitPathAndLog(path, plugin.Log);
+
+ //Smtp setup
+ {
+ IReadOnlyDictionary<string, JsonElement> smtp = plugin.GetConfig("smtp");
+ Uri serverUri = new(smtp["server_address"].GetString()!);
+ string username = smtp["username"].GetString() ?? throw new KeyNotFoundException("Missing required key 'usename' in 'smtp' config");
+ TimeSpan timeout = smtp["timeout_sec"].GetTimeSpan(TimeParseType.Seconds);
+
+ //Load SMTP
+ _ = plugin.DeferTask(async () =>
+ {
+ using SecretResult? password = await plugin.TryGetSecretAsync("smtp_password") ?? throw new KeyNotFoundException("Missing required 'smtp_password' in secrets");
+ //Copy the secre to the network credential
+ NetworkCredential cred = new(username, password.Result.ToString());
+ //Init email service
+ EmailService = new(serverUri, cred, timeout);
+ });
+ }
+
+ //S3 minio setup
+ {
+ //Init minio s3 client
+ S3Config = plugin.TryGetS3Config() ?? throw new KeyNotFoundException("Missing required 's3_config' configuration variable");
+ //Init minio from config
+ Client = new();
+
+ //Load the client when the secret finishes loading
+ _ = plugin.DeferTask(async () =>
+ {
+ using SecretResult? secret = await plugin.TryGetSecretAsync("s3_secret") ?? throw new KeyNotFoundException("Missing required s3 client secret in config");
+
+ Client.WithEndpoint(S3Config.ServerAddress)
+ .WithCredentials(S3Config.ClientId, secret.Result.ToString());
+
+ if (S3Config.UseSsl == true)
+ {
+ Client.WithSSL();
+ }
+ //Accept optional region
+ if (!string.IsNullOrWhiteSpace(S3Config.Region))
+ {
+ Client.WithRegion(S3Config.Region);
+ }
+ //Build client
+ Client.Build();
+ //If the plugin is in debug mode, log requests to logger
+ if (plugin.IsDebug())
+ {
+ Client.SetTraceOn(new ReqLogger(Log));
+ }
+ });
+ }
+
+ //Load transactions
+ Transactions = new(plugin.GetContextOptions());
+
+ TemplateCache = new(20, StringComparer.OrdinalIgnoreCase);
+ }
+
+ protected override async ValueTask<VfReturnType> PostAsync(HttpEntity entity)
+ {
+ //Make sure the user has the required scope to send an email
+ if (!entity.Session.HasScope(OAUTH2_USER_SCOPE_PERMISSION))
+ {
+ entity.CloseResponseError(HttpStatusCode.Forbidden, ErrorType.InvalidScope, "Your account does not have the required permissions scope");
+ return VfReturnType.VirtualSkip;
+ }
+
+ //Get the transaction request from the client
+ EmailTransaction? transaction = await entity.GetJsonFromFileAsync<EmailTransaction>();
+ if(transaction == null)
+ {
+ entity.CloseResponseError(HttpStatusCode.BadRequest, ErrorType.InvalidRequest, "No request body was received");
+ return VfReturnType.VirtualSkip;
+ }
+
+ TransactionResult webm = new()
+ {
+ Success = false
+ };
+ //Set a default from name/address if not set by user
+ transaction.From ??= FromAddress;
+ transaction.FromName ??= FromAddress;
+ //Specify user-id
+ transaction.UserId = entity.Session.UserID;
+ //validate the form
+ if (!Validator.Validate(transaction, webm))
+ {
+ //return errors
+ entity.CloseResponseJson(HttpStatusCode.UnprocessableEntity, webm);
+ return VfReturnType.VirtualSkip;
+ }
+
+ IFluidTemplate fTemp;
+
+ //See if the template is in cache
+ if (TemplateCache.TryGetOrEvictRecord(transaction.TemplateId!, out FluidCache? cache) == 1)
+ {
+ fTemp = cache.Template;
+ }
+ //Record was evicted or not found
+ else
+ {
+ string? templateData = null;
+ try
+ {
+ //Combine base obj path
+ string objPath = string.Concat(BaseObjectPath, transaction.TemplateId);
+ //Recover the template from the store
+ GetObjectArgs args = new();
+ args.WithBucket(S3Config.BaseBucket)
+ .WithObject(objPath)
+ .WithCallbackStream((stream) =>
+ {
+ //Read template from stream
+ using StreamReader reader = new(stream, Encoding.UTF8, detectEncodingFromByteOrderMarks: false, leaveOpen: true);
+ templateData = reader.ReadToEnd();
+ });
+ //get the template object
+ ObjectStat status = await Client.GetObjectAsync(args, entity.EventCancellation);
+
+ }
+ catch(ObjectNotFoundException)
+ {
+ entity.CloseResponseError(HttpStatusCode.NotFound, ErrorType.InvalidRequest, "The requested template does not exist");
+ return VfReturnType.VirtualSkip;
+ }
+ catch(MinioException me)
+ {
+ Log.Error(me);
+ return VfReturnType.Error;
+ }
+
+ //Make sure template data was found
+ if (string.IsNullOrWhiteSpace(templateData))
+ {
+ entity.CloseResponseError(HttpStatusCode.NotFound, ErrorType.InvalidRequest, "The requested template does not exist");
+ return VfReturnType.VirtualSkip;
+ }
+
+ //Try to parse the template
+ if (!Parser.TryParse(templateData, out fTemp, out string error))
+ {
+ Log.Error(error, "Liquid template parsing error");
+ return VfReturnType.Error;
+ }
+
+ //Cache the new template
+ TemplateCache.StoreRecord(transaction.TemplateId!, new FluidCache(fTemp), TemplateCacheTimeout);
+ }
+
+ //Allways add the template name to the model
+ transaction.Variables!["template_name"] = transaction.TemplateId!;
+
+ //Create a template model
+ TemplateContext ctx = new(transaction.Variables);
+ //render the template
+ string rendered = fTemp.Render(ctx);
+ try
+ {
+ //Send the template
+ _ = await EmailService.SendAsync(transaction, rendered, TextFormat.Html, entity.EventCancellation);
+ //Set success flag
+ webm.Success = true;
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex);
+ //Write an error status message to the transaction store
+ transaction.Result = ex.Message;
+ }
+ //Write transaction to db (we need to return the transaction id)
+ _ = await Transactions.AddOrUpdateAsync(transaction);
+ //Store the results object
+ webm.SmtpStatus = transaction.Result;
+ webm.TransactionId = transaction.Id;
+ //Return the response object
+ entity.CloseResponse(webm);
+ return VfReturnType.VirtualSkip;
+ }
+ }
+}
diff --git a/lib/Emails.Transactional.Plugin/src/EmailDbCtx.cs b/lib/Emails.Transactional.Plugin/src/EmailDbCtx.cs
new file mode 100644
index 0000000..cd865d8
--- /dev/null
+++ b/lib/Emails.Transactional.Plugin/src/EmailDbCtx.cs
@@ -0,0 +1,40 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: Transactional Emails
+* File: EmailDbCtx.cs
+*
+* EmailDbCtx.cs is part of Transactional Emails which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* Transactional Emails 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.
+*
+* Transactional Emails 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 Transactional Emails. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+
+using Microsoft.EntityFrameworkCore;
+
+using VNLib.Plugins.Extensions.Data;
+
+namespace Emails.Transactional
+{
+ internal class EmailDbCtx : TransactionalDbContext
+ {
+ public DbSet<EmailTransaction> EmailTransactions { get; set; }
+
+ public EmailDbCtx(DbContextOptions options) : base(options)
+ {}
+ }
+}
diff --git a/lib/Emails.Transactional.Plugin/src/EmailTransaction.cs b/lib/Emails.Transactional.Plugin/src/EmailTransaction.cs
new file mode 100644
index 0000000..bb7b4a8
--- /dev/null
+++ b/lib/Emails.Transactional.Plugin/src/EmailTransaction.cs
@@ -0,0 +1,162 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: Transactional Emails
+* File: EmailTransaction.cs
+*
+* EmailTransaction.cs is part of Transactional Emails which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* Transactional Emails 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.
+*
+* Transactional Emails 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 Transactional Emails. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+using VNLib.Plugins.Essentials;
+using VNLib.Plugins.Extensions.Data;
+using VNLib.Plugins.Extensions.Data.Abstractions;
+
+#nullable enable
+
+namespace Emails.Transactional
+{
+ /// <summary>
+ /// Represents an email transaction request and its status reflected
+ /// in the database
+ /// </summary>
+ internal class EmailTransaction : DbModelBase, IUserEntity
+ {
+ ///<inheritdoc/>
+ [Key]
+ [JsonPropertyName("id")]
+ public override string Id { get; set; }
+ ///<inheritdoc/>
+ [JsonIgnore]
+ public override DateTime Created { get; set; }
+ ///<inheritdoc/>
+ [JsonIgnore]
+ public override DateTime LastModified { get; set; }
+
+ /// <summary>
+ /// The sever side user-id that send the email
+ /// </summary>
+ [JsonIgnore]
+ public string? UserId { get; set; }
+
+ //To address
+ [JsonIgnore]
+ public string? To
+ {
+ //Use json to serialize/deserialze the to addresses
+ get => JsonSerializer.Serialize(ToAddresses, Statics.SR_OPTIONS);
+ set => ToAddresses = JsonSerializer.Deserialize<Dictionary<string, string>>(value!, Statics.SR_OPTIONS);
+ }
+
+ /// <summary>
+ /// A dictionary of to email address and name pairs
+ /// </summary>
+ [NotMapped]
+ [JsonPropertyName("to")]
+ public Dictionary<string, string>? ToAddresses { get; set; }
+
+ //From
+ /// <summary>
+ /// The from email address
+ /// </summary>
+ [JsonPropertyName("from")]
+ public string? From { get; set; }
+
+ /// <summary>
+ /// The optional from name (not mapped in db)
+ /// </summary>
+ [NotMapped]
+ [JsonPropertyName("from_name")]
+ public string? FromName { get; set; }
+ /// <summary>
+ /// The email subject
+ /// </summary>
+ [JsonPropertyName("subject")]
+ public string? Subject { get; set; }
+
+ //CC names
+ //ccs are stored in the db as a json serialized string
+ [JsonIgnore]
+ public string Ccs
+ {
+ //Use json to serialize/deserialze the to addresses
+ get => JsonSerializer.Serialize(CcAddresses, Statics.SR_OPTIONS);
+ set => CcAddresses = JsonSerializer.Deserialize<Dictionary<string, string>>(value, Statics.SR_OPTIONS);
+ }
+
+ [JsonPropertyName("cc")]
+ [NotMapped]
+ public Dictionary<string, string>? CcAddresses { get; set; }
+
+ //BCC Names
+
+ //bccs are stored in the db as a json serialized string
+ [JsonIgnore]
+ public string Bccs
+ {
+ //Store bccs as a comma separated list of addresses
+ get => JsonSerializer.Serialize(BccAddresses, Statics.SR_OPTIONS);
+ set => BccAddresses = JsonSerializer.Deserialize<Dictionary<string, string>>(value, Statics.SR_OPTIONS);
+ }
+
+ /// <summary>
+ /// A dictionary of bcc addresses and names
+ /// </summary>
+ [JsonPropertyName("bcc")]
+ [NotMapped]
+ public Dictionary<string, string>? BccAddresses { get; set; }
+
+ /// <summary>
+ /// The replyto email address
+ /// </summary>
+ [JsonPropertyName("reply_to")]
+ public string? ReplyTo { get; set; }
+ /// <summary>
+ /// Optional reply-to name for the address (not stored in db)
+ /// </summary>
+ [NotMapped]
+ [JsonPropertyName("reply_to_name")]
+ public string? ReplyToName { get; set; }
+
+ /// <summary>
+ /// The object id of the template to send
+ /// </summary>
+ [JsonPropertyName("template_id")]
+ public string? TemplateId { get; set; }
+
+ /// <summary>
+ /// The result of the STMP transaction
+ /// </summary>
+ [JsonIgnore]
+ public string? Result { get; set; }
+
+ /// <summary>
+ /// Variables requested from the client to embed in the template
+ /// during processing. These are not stored in the database
+ /// </summary>
+ [NotMapped]
+ [JsonPropertyName("variables")]
+ public Dictionary<string, string>? Variables { get; set; }
+ }
+}
diff --git a/lib/Emails.Transactional.Plugin/src/SmtpProvider.cs b/lib/Emails.Transactional.Plugin/src/SmtpProvider.cs
new file mode 100644
index 0000000..b2423ef
--- /dev/null
+++ b/lib/Emails.Transactional.Plugin/src/SmtpProvider.cs
@@ -0,0 +1,118 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: Transactional Emails
+* File: SmtpProvider.cs
+*
+* SmtpProvider.cs is part of Transactional Emails which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* Transactional Emails 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.
+*
+* Transactional Emails 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 Transactional Emails. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.Net;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Collections.Generic;
+
+using MailKit.Net.Smtp;
+
+using MimeKit;
+using MimeKit.Text;
+
+namespace Emails.Transactional
+{
+ internal sealed class SmtpProvider
+ {
+ private readonly Uri ServerAddress;
+ private readonly ICredentials ServerCreds;
+ private readonly TimeSpan Timeout;
+
+ public SmtpProvider(Uri ServerAddress, ICredentials ServerCreds, TimeSpan Timeout)
+ {
+ this.ServerCreds = ServerCreds;
+ this.ServerAddress = ServerAddress;
+ this.Timeout = Timeout;
+ }
+
+ /// <summary>
+ /// Opens a connection to the configured SMTP server and sends the specified email
+ /// request transaction on the server. When the operation completes, the transaction's
+ /// result property is populated with the result of the operation.
+ /// </summary>
+ /// <param name="transaction">The transaction data</param>
+ /// <param name="messageBody">The content of the message to send</param>
+ /// <param name="dataFromat">The format of the body content</param>
+ /// <returns>A task that resolves the status of the operation</returns>
+ public async Task<string> SendAsync(EmailTransaction transaction, string messageBody, TextFormat dataFromat, CancellationToken cancellation)
+ {
+ //Configured a new message
+ using MimeMessage message = new()
+ {
+ Date = DateTime.UtcNow,
+ Subject = transaction.Subject
+ };
+ //From address is the stored from address
+ message.From.Add(new MailboxAddress(transaction.FromName, transaction.From));
+
+ //Add to email addresses
+ foreach (KeyValuePair<string, string> tos in transaction.ToAddresses)
+ {
+ message.To.Add(new MailboxAddress(tos.Value, tos.Key));
+ }
+ //Add ccs
+ if (transaction.CcAddresses != null)
+ {
+ foreach (KeyValuePair<string, string> ccs in transaction.CcAddresses)
+ {
+ message.Cc.Add(new MailboxAddress(ccs.Value, ccs.Key));
+ }
+ }
+ //Add bccs
+ if (transaction.BccAddresses != null)
+ {
+ foreach (KeyValuePair<string, string> bccs in transaction.BccAddresses)
+ {
+ message.Bcc.Add(new MailboxAddress(bccs.Value, bccs.Key));
+ }
+ }
+
+ //Use html format since we expect to be reading html templates
+ using TextPart body = new(dataFromat)
+ {
+ IsAttachment = false,
+ Text = messageBody
+ };
+ //Set message body
+ message.Body = body;
+ //Open a new mail client
+ using SmtpClient client = new();
+ //Set timeout for senting messages
+ client.Timeout = (int)Timeout.TotalMilliseconds;
+ //Connect to server
+ await client.ConnectAsync(ServerAddress, cancellation);
+ //Aithenticate
+ await client.AuthenticateAsync(ServerCreds, cancellation);
+ //Send the email
+ string result = await client.SendAsync(message, cancellation);
+ //Disconnect from the server
+ await client.DisconnectAsync(true, CancellationToken.None);
+ //Update the transaction
+ transaction.Result = result;
+ return result;
+ }
+ }
+}
diff --git a/lib/Emails.Transactional.Plugin/src/TEmailEntryPoint.cs b/lib/Emails.Transactional.Plugin/src/TEmailEntryPoint.cs
new file mode 100644
index 0000000..c95ca08
--- /dev/null
+++ b/lib/Emails.Transactional.Plugin/src/TEmailEntryPoint.cs
@@ -0,0 +1,88 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: Transactional Emails
+* File: TEmailEntryPoint.cs
+*
+* TEmailEntryPoint.cs is part of Transactional Emails which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* Transactional Emails 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.
+*
+* Transactional Emails 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 Transactional Emails. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.Threading.Tasks;
+using System.Collections.Generic;
+
+using Emails.Transactional.Endpoints;
+
+using Minio;
+using Minio.DataModel.Tracing;
+
+using VNLib.Utils.Logging;
+using VNLib.Plugins;
+using VNLib.Plugins.Extensions.Loading.Routing;
+
+namespace Emails.Transactional
+{
+ public class TEmailEntryPoint : PluginBase
+ {
+ public override string PluginName => "Emails.Transactional";
+
+ internal class ReqLogger : IRequestLogger
+ {
+ private readonly ILogProvider logProvider;
+
+ public ReqLogger(ILogProvider log)
+ {
+ logProvider = log;
+ }
+
+ public void LogRequest(RequestToLog requestToLog, ResponseToLog responseToLog, double durationMs)
+ {
+ logProvider.Debug("S3 result\n{method} {uri} HTTP {ms}ms\nHTTP {status} {message}\n{content}",
+ requestToLog.method, requestToLog.resource, durationMs,
+ responseToLog.statusCode, responseToLog.errorMessage, responseToLog.content
+ );
+ }
+ }
+
+ protected override void OnLoad()
+ {
+ try
+ {
+ //Route send oauth endpoint
+ this.Route<SendEndpoint>();
+
+ Log.Information("Plugin loaded");
+ }
+ catch (KeyNotFoundException kne)
+ {
+ Log.Warn("Missing required configuration keys {err}", kne.Message);
+ }
+ }
+
+
+ protected override void OnUnLoad()
+ {
+ Log.Information("Plugin unloaded");
+ }
+
+ protected override void ProcessHostCommand(string cmd)
+ {
+ throw new NotImplementedException();
+ }
+ }
+}
diff --git a/lib/Emails.Transactional.Plugin/src/Transactional Emails.csproj b/lib/Emails.Transactional.Plugin/src/Transactional Emails.csproj
new file mode 100644
index 0000000..7795d7d
--- /dev/null
+++ b/lib/Emails.Transactional.Plugin/src/Transactional Emails.csproj
@@ -0,0 +1,64 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>net6.0</TargetFramework>
+ <RootNamespace>Emails.Transactional</RootNamespace>
+ <AssemblyName>TransactionalEmails</AssemblyName>
+
+ <Nullable>enable</Nullable>
+ <Authors>Vaughn Nugent</Authors>
+ <Copyright>Copyright © 2022 Vaughn Nugent</Copyright>
+ <Version>1.0.0.1</Version>
+ <PackageProjectUrl>https://www.vaughnnugent.com/resources</PackageProjectUrl>
+ <SignAssembly>True</SignAssembly>
+ <AssemblyOriginatorKeyFile>\\vaughnnugent.com\Internal\Folder Redirection\vman\Documents\Programming\Software\StrongNameingKey.snk</AssemblyOriginatorKeyFile>
+ </PropertyGroup>
+
+ <!-- Resolve nuget dll files and store them in the output dir -->
+ <PropertyGroup>
+ <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="ErrorProne.NET.CoreAnalyzers" Version="0.1.2">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+ </PackageReference>
+ <PackageReference Include="ErrorProne.NET.Structs" Version="0.1.2">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+ </PackageReference>
+ <PackageReference Include="Fluid.Core" Version="2.2.16" />
+ <PackageReference Include="MailKit" Version="3.4.2" />
+ <PackageReference Include="Minio" Version="4.0.6" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <None Update="TransactionalEmails.json">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </None>
+ </ItemGroup>
+
+ <ItemGroup>
+ <Service Include="{508349b6-6b84-4df5-91f0-309beebad82d}" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <Folder Include="Endpoints\" />
+ <Folder Include="Meta\" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\..\..\..\..\core\lib\Plugins.Essentials\src\VNLib.Plugins.Essentials.csproj" />
+ <ProjectReference Include="..\..\..\..\..\core\lib\Plugins.PluginBase\src\VNLib.Plugins.PluginBase.csproj" />
+ <ProjectReference Include="..\..\..\..\Extensions\lib\VNLib.Plugins.Extensions.Data\src\VNLib.Plugins.Extensions.Data.csproj" />
+ <ProjectReference Include="..\..\..\..\Extensions\lib\VNLib.Plugins.Extensions.Loading.Sql\src\VNLib.Plugins.Extensions.Loading.Sql.csproj" />
+ <ProjectReference Include="..\..\..\..\Extensions\lib\VNLib.Plugins.Extensions.Loading\src\VNLib.Plugins.Extensions.Loading.csproj" />
+ <ProjectReference Include="..\..\..\..\Extensions\lib\VNLib.Plugins.Extensions.Validation\src\VNLib.Plugins.Extensions.Validation.csproj" />
+ </ItemGroup>
+
+ <Target Name="PostBuild" AfterTargets="PostBuildEvent">
+ <Exec Command="start xcopy &quot;$(TargetDir)&quot; &quot;F:\Programming\vnlib\devplugins\$(TargetName)&quot; /E /Y /R" />
+ </Target>
+
+</Project>
diff --git a/lib/Emails.Transactional.Plugin/src/Transactions/EmailTransactionValidator.cs b/lib/Emails.Transactional.Plugin/src/Transactions/EmailTransactionValidator.cs
new file mode 100644
index 0000000..5786efb
--- /dev/null
+++ b/lib/Emails.Transactional.Plugin/src/Transactions/EmailTransactionValidator.cs
@@ -0,0 +1,133 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: Transactional Emails
+* File: EmailTransactionValidator.cs
+*
+* EmailTransactionValidator.cs is part of Transactional Emails which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* Transactional Emails 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.
+*
+* Transactional Emails 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 Transactional Emails. If not, see http://www.gnu.org/licenses/.
+*/
+
+using FluentValidation;
+
+using VNLib.Plugins.Extensions.Validation;
+
+
+namespace Emails.Transactional.Transactions
+{
+ internal class EmailTransactionValidator : AbstractValidator<EmailTransaction>
+ {
+ public const int MAX_SUBJECT_LEN = 50;
+ public EmailTransactionValidator()
+ {
+ //Catch to make sure user-id is set
+ RuleFor(static t => t.UserId)
+ .NotEmpty();
+ //validate from addres/name
+ RuleFor(static t => t.FromName)
+ .NotEmpty()
+ .WithName("from_name")
+ .AlphaNumericOnly()
+ .WithName("from_name");
+ RuleFor(static t => t.From)
+ .NotEmpty()
+ .EmailAddress()
+ .WithName("from");
+
+ //Rule names must be their json propery name, so clients can properly log errors
+ //must include a template id
+ RuleFor(static t => t.TemplateId)
+ .NotEmpty()
+ .MaximumLength(200)
+ .WithName("template_id");
+
+ //From address must not be empty
+ RuleFor(static t => t.From)
+ .NotEmpty()
+ .EmailAddress();
+ //Subject is required alpha num
+ RuleFor(static t => t.Subject)
+ .NotEmpty()
+ .AlphaNumericOnly()
+ .MaximumLength(MAX_SUBJECT_LEN)
+ .WithName("subject");
+
+ //To address must not be empty, and must be valid email addresses
+ RuleFor(static t => t.ToAddresses)
+ .NotEmpty()
+ .ChildRules(static to => {
+ //Check keys (address)
+ to.RuleForEach(static b => b.Keys)
+ .NotEmpty()
+ .EmailAddress()
+ .WithName("to");
+
+ //Check values (names)
+ to.RuleForEach(static b => b.Values)
+ .NotEmpty()
+ .AlphaNumericOnly()
+ .WithName("to_name");
+ })
+ .WithName("to");
+ //Check bcc addresses, allowed to be empty
+ RuleFor(static t => t.BccAddresses)
+ .ChildRules(static bcc => {
+ //Check keys (address)
+ bcc.RuleForEach(static b => b.Keys)
+ .NotEmpty()
+ .EmailAddress()
+ .WithName("bcc");
+
+ //Check values (names)
+ bcc.RuleForEach(static b => b.Values)
+ .NotEmpty()
+ .AlphaNumericOnly()
+ .WithName("bcc_names");
+ })
+ .When(static t => t.BccAddresses != null)
+ .WithName("bcc");
+ //Check cc, also allowed to be empty
+ RuleFor(static t => t.CcAddresses)
+ .ChildRules(static cc => {
+ //Check keys (names)
+ cc.RuleForEach(static b => b.Keys)
+ .NotEmpty()
+ .EmailAddress()
+ .WithName("cc");
+
+ //Check values (addresses)
+ cc.RuleForEach(static b => b.Values)
+ .NotEmpty()
+ .AlphaNumericOnly()
+ .WithName("cc_names");
+ })
+ .When(static t => t.BccAddresses != null)
+ .WithName("cc");
+
+ RuleFor(static t => t.ReplyTo)
+ //Allow the reply to email to be empty, if its not, then validate the address
+ .EmailAddress()
+ .When(static t => t.BccAddresses != null)
+ .WithName("reply_to");
+
+ //Make sure a variable table is defined
+ RuleFor(static t => t.Variables)
+ .NotNull()
+ .WithName("variables");
+ }
+ }
+}
diff --git a/lib/Emails.Transactional.Plugin/src/Transactions/TransactionResult.cs b/lib/Emails.Transactional.Plugin/src/Transactions/TransactionResult.cs
new file mode 100644
index 0000000..da97499
--- /dev/null
+++ b/lib/Emails.Transactional.Plugin/src/Transactions/TransactionResult.cs
@@ -0,0 +1,45 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: Transactional Emails
+* File: TransactionResult.cs
+*
+* TransactionResult.cs is part of Transactional Emails which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* Transactional Emails 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.
+*
+* Transactional Emails 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 Transactional Emails. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System.Text.Json.Serialization;
+
+using VNLib.Plugins.Extensions.Validation;
+
+namespace Emails.Transactional.Transactions
+{
+ public class TransactionResult : ValErrWebMessage
+ {
+ [JsonPropertyName("transaction_id")]
+ public string? TransactionId { get; set; }
+
+ [JsonPropertyName("smtp_status")]
+ public string? SmtpStatus { get; set; }
+
+ [JsonPropertyName("error_code")]
+ public string? ErrorCode { get; set; }
+
+ [JsonPropertyName("error_description")]
+ public string? ErrorDescription { get; set; }
+ }
+}
diff --git a/lib/Emails.Transactional.Plugin/src/Transactions/TransactionStore.cs b/lib/Emails.Transactional.Plugin/src/Transactions/TransactionStore.cs
new file mode 100644
index 0000000..a6bd4b9
--- /dev/null
+++ b/lib/Emails.Transactional.Plugin/src/Transactions/TransactionStore.cs
@@ -0,0 +1,74 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: Transactional Emails
+* File: TransactionStore.cs
+*
+* TransactionStore.cs is part of Transactional Emails which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* Transactional Emails 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.
+*
+* Transactional Emails 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 Transactional Emails. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.Linq;
+
+using Microsoft.EntityFrameworkCore;
+
+using VNLib.Plugins.Extensions.Data;
+
+namespace Emails.Transactional.Transactions
+{
+
+ internal class TransactionStore : DbStore<EmailTransaction>
+ {
+ private readonly DbContextOptions Options;
+
+ public TransactionStore(DbContextOptions options)
+ {
+ Options = options;
+ }
+
+ public override TransactionalDbContext NewContext() => new EmailDbCtx(Options);
+
+ public override string RecordIdBuilder => Guid.NewGuid().ToString("N");
+
+ protected override void OnRecordUpdate(EmailTransaction newRecord, EmailTransaction oldRecord)
+ {
+ oldRecord.LastModified = DateTime.UtcNow;
+ }
+
+ protected override IQueryable<EmailTransaction> GetCollectionQueryBuilder(TransactionalDbContext context, params string[] constraints)
+ {
+ string userId = constraints[0];
+ //Get the last transactions for the specifed user
+ EmailDbCtx ctx = context as EmailDbCtx;
+ return from trans in ctx.EmailTransactions
+ where trans.UserId == userId
+ orderby trans.LastModified descending
+ select trans;
+ }
+
+ protected override IQueryable<EmailTransaction> GetSingleQueryBuilder(TransactionalDbContext context, params string[] constraints)
+ {
+ string transactionid = constraints[0];
+ EmailDbCtx ctx = context as EmailDbCtx;
+ //Selet the exact transaction from its id
+ return from trans in ctx.EmailTransactions
+ where trans.Id == transactionid
+ select trans;
+ }
+ }
+}