diff options
Diffstat (limited to 'lib/Emails.Transactional.Extensions/src/TEmailConfig.cs')
-rw-r--r-- | lib/Emails.Transactional.Extensions/src/TEmailConfig.cs | 204 |
1 files changed, 204 insertions, 0 deletions
diff --git a/lib/Emails.Transactional.Extensions/src/TEmailConfig.cs b/lib/Emails.Transactional.Extensions/src/TEmailConfig.cs new file mode 100644 index 0000000..7905982 --- /dev/null +++ b/lib/Emails.Transactional.Extensions/src/TEmailConfig.cs @@ -0,0 +1,204 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: Emails.Transactional.Client.Extensions +* File: TEmailConfig.cs +* +* TEmailConfig.cs is part of Emails.Transactional.Client.Extensions which is part of the larger +* VNLib collection of libraries and utilities. +* +* Emails.Transactional.Client.Extensions is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* Emails.Transactional.Client.Extensions 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 Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System.Text; +using System.Text.Json.Serialization; + +using RestSharp; + +using VNLib.Utils.Logging; +using VNLib.Plugins; +using VNLib.Net.Rest.Client; +using VNLib.Net.Rest.Client.OAuth2; +using VNLib.Plugins.Extensions.Loading; + +namespace Emails.Transactional.Client.Extensions +{ + /// <summary> + /// Provides transactional emails based on templates and variable complilation. Should be loaded as a singleton for a plugin + /// </summary> + [ConfigurationName(EMAIL_CONFIG_KEY)] + public class TEmailConfig : TransactionalEmailConfig, IAsyncConfigurable, IDisposable + { + const string DEFAULT_USER_AGENT = "VN Transactional Email System"; + + public const string EMAIL_CONFIG_KEY = "emails"; + public const string REQUIRED_EMAIL_TEMPALTE_CONFIG_KEY = "required_email_templates"; + + public const int DEFAULT_MAX_CLIENTS = 5; + public const int DEFAULT_CLIENT_TIMEOUT_MS = 10000; + + private bool disposedValue; + + private readonly OAuth2Authenticator _authenticator; + + /// <summary> + /// A shared <see cref="VNLib.Net.Rest.Client.RestClientPool"/> for renting configuraed + /// <see cref="RestClient"/> + /// </summary> + public RestClientPool RestClientPool { get; } + /// <summary> + /// A global from email address name + /// </summary> + public string EmailFromName { get; } + /// <summary> + /// A global from email address + /// </summary> + public string EmailFromAddress { get; } + + private SecretResult? _clientId; + private SecretResult? _clientSecret; + + + /// <summary> + /// Intialzies the singleton using the <see cref="LoadingExtensions"/> + /// loading system + /// </summary> + /// <param name="plugin">The plugin loading the instance</param> + /// <param name="config">The configuration scope for the instance</param> + public TEmailConfig(PluginBase plugin, IConfigScope config) + { + //The current config scope should be the required data, so we can deserialze it + JsonConfig mailConfig = config.DeserialzeAndValidate<JsonConfig>(); + + //Get required templates + Dictionary<string, string> templates = plugin.GetConfig(REQUIRED_EMAIL_TEMPALTE_CONFIG_KEY).Deserialze<Dictionary<string, string>>(); + + EmailFromName = mailConfig.EmailFromName!; + EmailFromAddress = mailConfig.EmailFromAddress!; + + if (plugin.IsDebug()) + { + plugin.Log.Debug("Required email templates {t}", templates); + } + + //Options for auth token endpoint + RestClientOptions clientOptions = new() + { + AllowMultipleDefaultParametersWithSameName = true, + //Server supports compression + AutomaticDecompression = System.Net.DecompressionMethods.All, + PreAuthenticate = false, + Encoding = Encoding.UTF8, + MaxTimeout = mailConfig.TimeoutMs, + UserAgent = mailConfig.UserAgent, + //Server should not redirect + FollowRedirects = false + }; + + //Init Oauth authenticator + _authenticator = new(clientOptions, OAuthCredentialGet, mailConfig.TokenServerUri.ToString()); + + //Create client pool + RestClientPool = new(mailConfig.MaxClients, clientOptions, authenticator: _authenticator); + + //Init templates and service url + WithTemplates(templates) + .WithUrl(mailConfig.ServiceUri); + } + + private Credential OAuthCredentialGet() + { + _ = _clientId ?? throw new InvalidOperationException("Oauth2 client id was not loaded"); + _ = _clientSecret ?? throw new InvalidOperationException("Oauth2 client secret was not loaded"); + + //Create new credential + return Credential.Create(_clientId.Result, _clientSecret.Result); + } + + async Task IAsyncConfigurable.ConfigureServiceAsync(PluginBase plugin) + { + //Load oauth secrets + _clientId = await plugin.TryGetSecretAsync("email_client_id"); + _clientSecret = await plugin.TryGetSecretAsync("email_client_secret"); + } + + ///<inheritdoc/> + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + _clientId?.Dispose(); + _clientSecret?.Dispose(); + + _authenticator.Dispose(); + RestClientPool.Dispose(); + } + disposedValue = true; + } + } + + void IDisposable.Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + private sealed class JsonConfig : IOnConfigValidation + { + [JsonPropertyName("from_name")] + public string? EmailFromName { get; set; } + [JsonPropertyName("from_address")] + public string? EmailFromAddress { get; set; } + [JsonPropertyName("email_url")] + public string? ServiceUrl { get; set; } + [JsonPropertyName("auth_url")] + public string? AuthUrl { get; set; } + [JsonPropertyName("user_agent")] + public string UserAgent { get; set; } = DEFAULT_USER_AGENT; + [JsonPropertyName("timeout_ms")] + public int TimeoutMs { get; set; } = DEFAULT_CLIENT_TIMEOUT_MS; + [JsonPropertyName("max_clients")] + public int MaxClients { get; set; } = DEFAULT_MAX_CLIENTS; + + + [JsonIgnore] + public Uri ServiceUri => new(ServiceUrl!); + + [JsonIgnore] + public Uri TokenServerUri => new(AuthUrl!); + + void IOnConfigValidation.Validate() + { + _ = EmailFromName ?? throw new KeyNotFoundException("Missing required mail configuration key 'from_name'"); + _ = EmailFromAddress ?? throw new KeyNotFoundException("Missing required mail configuration key 'from_address'"); + _ = ServiceUri ?? throw new KeyNotFoundException("Missing required mail configuration key 'email_url'"); + _ = AuthUrl ?? throw new KeyNotFoundException("Missing required mail configuration key 'auth_url'"); + + if (MaxClients < 1) + { + throw new ArgumentOutOfRangeException("max_client", "Max clients config variable must be a non-zero positive integer"); + } + + if (TimeoutMs < 100) + { + throw new ArgumentOutOfRangeException("timeout_ms", "Client timeout should be a value greater than 100ms"); + } + } + } + } +}
\ No newline at end of file |