diff options
Diffstat (limited to 'lib/Emails.Transactional.Plugin/src')
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 "$(TargetDir)" "F:\Programming\vnlib\devplugins\$(TargetName)" /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; + } + } +} |