diff options
author | vman <public@vaughnnugent.com> | 2022-11-18 15:55:22 -0500 |
---|---|---|
committer | vman <public@vaughnnugent.com> | 2022-11-18 15:55:22 -0500 |
commit | c9e17b57a5ecdeea81674de5a033a201e7802526 (patch) | |
tree | 15c331af950441351507bb716e07dbe45452cd13 /Transactional Emails/Api Endpoints/SendEndpoint.cs | |
parent | 038d86a0381b39af94b66c9bdd3da1e31cd2d8f2 (diff) |
Initial commit
Diffstat (limited to 'Transactional Emails/Api Endpoints/SendEndpoint.cs')
-rw-r--r-- | Transactional Emails/Api Endpoints/SendEndpoint.cs | 249 |
1 files changed, 249 insertions, 0 deletions
diff --git a/Transactional Emails/Api Endpoints/SendEndpoint.cs b/Transactional Emails/Api Endpoints/SendEndpoint.cs new file mode 100644 index 0000000..c19dbcf --- /dev/null +++ b/Transactional Emails/Api Endpoints/SendEndpoint.cs @@ -0,0 +1,249 @@ +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; + +#nullable enable + +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 SmtpProvider EmailService; + private readonly TransactionStore Transactions; + + private readonly Dictionary<string, FluidCache> TemplateCache; + private readonly TimeSpan TemplateCacheTimeout; + private readonly MinioClient Client; + private readonly S3Config S3Config; + + 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"); + string password = plugin.TryGetSecretAsync("smtp_password").Result ?? throw new KeyNotFoundException("Missing required 'smtp_password' in secrets"); + TimeSpan timeout = smtp["timeout_sec"].GetTimeSpan(TimeParseType.Seconds); + NetworkCredential cred = new(username, password); + //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(); + Client.WithEndpoint(S3Config.ServerAddress) + .WithCredentials(S3Config.ClientId, S3Config.ClientSecret); + + 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; + } + } +} |