diff options
| author | ivarlovlie <git@ivarlovlie.no> | 2022-11-29 05:15:17 +0100 |
|---|---|---|
| committer | ivarlovlie <git@ivarlovlie.no> | 2022-11-29 05:15:17 +0100 |
| commit | 55ac6f03a23eca5f5ec9ff57ff4e16e9575770c6 (patch) | |
| tree | f8bf01dbb65510a721724a2d528a9b44e449c793 /code | |
| parent | d93735b58c3174d8ad79ef5cff7787b3ec825658 (diff) | |
| download | greatoffice-55ac6f03a23eca5f5ec9ff57ff4e16e9575770c6.tar.xz greatoffice-55ac6f03a23eca5f5ec9ff57ff4e16e9575770c6.zip | |
feat: Use postmarks http api instead of smtp
Diffstat (limited to 'code')
| -rw-r--r-- | code/api/src/Data/Models/AppConfiguration.cs | 39 | ||||
| -rw-r--r-- | code/api/src/Services/MailService.cs | 144 | ||||
| -rw-r--r-- | code/api/src/Services/PasswordResetService.cs | 11 | ||||
| -rw-r--r-- | code/api/src/Services/VaultService.cs | 61 |
4 files changed, 163 insertions, 92 deletions
diff --git a/code/api/src/Data/Models/AppConfiguration.cs b/code/api/src/Data/Models/AppConfiguration.cs index e2d7fa6..2a9afc2 100644 --- a/code/api/src/Data/Models/AppConfiguration.cs +++ b/code/api/src/Data/Models/AppConfiguration.cs @@ -54,17 +54,34 @@ public class AppConfiguration /// </summary> public string QUARTZ_DB_NAME { get; set; } + /// <summary> + /// API key to use when pushing logs to SEQ + /// </summary> public string SEQ_API_KEY { get; set; } + + /// <summary> + /// Url pointing to the seq instance that processes server logs + /// </summary> public string SEQ_API_URL { get; set; } - public string SMTP_HOST { get; set; } - public string SMTP_PORT { get; set; } - public string SMTP_USER { get; set; } - public string SMTP_PASSWORD { get; set; } + + /// <summary> + /// A token used when sending email via Postmakr + /// </summary> + public string POSTMARK_TOKEN { get; set; } + + /// <summary> + /// The address to send emails from, needs to be setup as a sender in postmark + /// </summary> public string EMAIL_FROM_ADDRESS { get; set; } - public string EMAIL_FROM_DISPLAY_NAME { get; set; } - public string GITHUB_CLIENT_ID { get; set; } - public string GITHUB_CLIENT_SECRET { get; set; } + + /// <summary> + /// The absolute url to the frontend app + /// </summary> public string CANONICAL_FRONTEND_URL { get; set; } + + /// <summary> + /// A random string used to encrypt/decrypt for general purposes + /// </summary> public string APP_AES_KEY { get; set; } /// <summary> @@ -86,14 +103,8 @@ public class AppConfiguration QUARTZ_DB_PASSWORD = QUARTZ_DB_PASSWORD.Obfuscate() ?? "", SEQ_API_KEY = SEQ_API_KEY.Obfuscate() ?? "", SEQ_API_URL, - SMTP_HOST, - SMTP_PORT, - SMTP_USER = SMTP_USER.Obfuscate() ?? "", - SMTP_PASSWORD = SMTP_PASSWORD.Obfuscate() ?? "", + POSTMARK_TOKEN = POSTMARK_TOKEN.Obfuscate() ?? "", EMAIL_FROM_ADDRESS, - EMAIL_FROM_DISPLAY_NAME, - GITHUB_CLIENT_ID = GITHUB_CLIENT_ID.Obfuscate() ?? "", - GITHUB_CLIENT_SECRET = GITHUB_CLIENT_SECRET.Obfuscate() ?? "", APP_AES_KEY = APP_AES_KEY.Obfuscate() ?? "", CERT1 = CERT1().PublicKey.Oid.FriendlyName, CANONICAL_FRONTEND_URL diff --git a/code/api/src/Services/MailService.cs b/code/api/src/Services/MailService.cs index 1e565f5..6073f6e 100644 --- a/code/api/src/Services/MailService.cs +++ b/code/api/src/Services/MailService.cs @@ -3,50 +3,134 @@ namespace IOL.GreatOffice.Api.Services; public class MailService { private readonly ILogger<MailService> _logger; - private static string _emailHost; - private static int _emailPort; - private static string _emailUser; - private static string _emailPassword; + private static string _postmarkToken; + private static string _fromEmail; + private readonly HttpClient _httpClient; - public MailService(VaultService vaultService, ILogger<MailService> logger) { + public MailService(VaultService vaultService, ILogger<MailService> logger, HttpClient httpClient) { var configuration = vaultService.GetCurrentAppConfiguration(); + _postmarkToken = configuration.POSTMARK_TOKEN; + _fromEmail = configuration.EMAIL_FROM_ADDRESS; _logger = logger; - _emailHost = configuration.SMTP_HOST; - _emailPort = Convert.ToInt32(configuration.SMTP_PORT); - _emailUser = configuration.SMTP_USER; - _emailPassword = configuration.SMTP_PASSWORD; + httpClient.DefaultRequestHeaders.Add("X-Postmark-Server-Token", _postmarkToken); + _httpClient = httpClient; } /// <summary> /// Send an email. /// </summary> /// <param name="message"></param> - public void SendMail(MailMessage message) { + public async Task SendMail(PostmarkEmail message) { try { - using var smtpClient = new SmtpClient { - Host = _emailHost, - EnableSsl = _emailPort == 587, - Port = _emailPort, - Credentials = new NetworkCredential { - UserName = _emailUser, - Password = _emailPassword, + if (message.MessageStream.IsNullOrWhiteSpace()) { + message.MessageStream = "outbound"; + } + + if (message.From.IsNullOrWhiteSpace() && _fromEmail.HasValue()) { + message.From = _fromEmail; + } else { + throw new ApplicationException("Not one from-email is available"); + } + + if (message.To.IsNullOrWhiteSpace()) { + throw new ArgumentNullException(nameof(message.To), "A recipient should be specified."); + } + + if (!message.To.IsValidEmailAddress()) { + throw new ArgumentException(nameof(message.To), "To is not a valid email address"); + } + + if (message.HtmlBody.IsNullOrWhiteSpace() && message.TextBody.IsNullOrWhiteSpace()) { + throw new ArgumentNullException(nameof(message), "Both HtmlBody and TextBody is empty, nothing to send"); + } + + using var client = new HttpClient() { + DefaultRequestHeaders = { + {"X-Postmark-Server-Token", _postmarkToken}, } }; - var configurationString = JsonSerializer.Serialize(new { - smtpClient.Host, - smtpClient.EnableSsl, - smtpClient.Port, - UserName = _emailUser.HasValue() ? "**REDACTED**" : "**MISSING**", - Password = _emailPassword.HasValue() ? "**REDACTED**" : "**MISSING**", - }, - new JsonSerializerOptions { - WriteIndented = true - }); - - _logger.LogDebug("SmtpClient was instansiated with the following configuration\n" + configurationString); - smtpClient.Send(message); + // TODO: Log response if unsuccessful + await client.PostAsJsonAsync("https://api.postmarkapp.com/email", message); } catch (Exception e) { _logger.LogError(e, "An exception occured while trying to send an email"); } } + + private class PostmarkSendResponse + { + /// <summary> + /// The Message ID returned from Postmark. + /// </summary> + public Guid MessageID { get; set; } + + /// <summary> + /// The message from the API. + /// In the event of an error, this message may contain helpful text. + /// </summary> + public string Message { get; set; } + + /// <summary> + /// The time the request was received by Postmark. + /// </summary> + public DateTime SubmittedAt { get; set; } + + /// <summary> + /// The recipient of the submitted request. + /// </summary> + public string To { get; set; } + + /// <summary> + /// The error code returned from Postmark. + /// This does not map to HTTP status codes. + /// </summary> + public int ErrorCode { get; set; } + } + + public class PostmarkEmail + { + /// <summary> + /// The message subject line. + /// </summary> + public string Subject { get; set; } + + /// <summary> + /// The message body, if the message contains + /// </summary> + public string HtmlBody { get; set; } + + /// <summary> + /// The message body, if the message is plain text. + /// </summary> + public string TextBody { get; set; } + + /// <summary> + /// The message stream used to send this message. + /// </summary> + public string MessageStream { get; set; } + + /// <summary> + /// The sender's email address. + /// </summary> + public string From { get; set; } + + /// <summary> + /// Any recipients. Separate multiple recipients with a comma. + /// </summary> + public string To { get; set; } + + /// <summary> + /// Any CC recipients. Separate multiple recipients with a comma. + /// </summary> + public string Cc { get; set; } + + /// <summary> + /// Any BCC recipients. Separate multiple recipients with a comma. + /// </summary> + public string Bcc { get; set; } + + /// <summary> + /// The email address to reply to. This is optional. + /// </summary> + public string ReplyTo { get; set; } + } }
\ No newline at end of file diff --git a/code/api/src/Services/PasswordResetService.cs b/code/api/src/Services/PasswordResetService.cs index cb4bc1e..8c8e32b 100644 --- a/code/api/src/Services/PasswordResetService.cs +++ b/code/api/src/Services/PasswordResetService.cs @@ -49,16 +49,11 @@ public class PasswordResetService var request = new PasswordResetRequest(user); _database.PasswordResetRequests.Add(request); await _database.SaveChangesAsync(cancellationToken); - var emailFromAddress = _configuration.EMAIL_FROM_ADDRESS; - var emailFromDisplayName = _configuration.EMAIL_FROM_DISPLAY_NAME; var zonedExpirationDate = TimeZoneInfo.ConvertTimeBySystemTimeZoneId(request.ExpirationDate, requestTz.Id); - var message = new MailMessage { - From = new MailAddress(emailFromAddress, emailFromDisplayName), - To = { - new MailAddress(request.User.Username, request.User.DisplayName()) - }, + var message = new MailService.PostmarkEmail() { + To = request.User.Username, Subject = "Reset password - Greatoffice", - Body = @$" + TextBody = @$" Hi {user.Username} Go to the following link to set a new password. diff --git a/code/api/src/Services/VaultService.cs b/code/api/src/Services/VaultService.cs index 8139985..d4ac775 100644 --- a/code/api/src/Services/VaultService.cs +++ b/code/api/src/Services/VaultService.cs @@ -10,8 +10,7 @@ public class VaultService private readonly ILogger<VaultService> _logger; private int CACHE_TTL { get; set; } - public VaultService(HttpClient client, IConfiguration configuration, IMemoryCache cache, ILogger<VaultService> logger) - { + public VaultService(HttpClient client, IConfiguration configuration, IMemoryCache cache, ILogger<VaultService> logger) { var token = configuration.GetValue<string>(AppEnvironmentVariables.VAULT_TOKEN); var vaultUrl = configuration.GetValue<string>(AppEnvironmentVariables.VAULT_URL); CACHE_TTL = configuration.GetValue(AppEnvironmentVariables.VAULT_CACHE_TTL, 60 * 60 * 12); @@ -25,20 +24,16 @@ public class VaultService _logger = logger; } - public T Get<T>(string path) - { + public T Get<T>(string path) { var result = _cache.GetOrCreate(AppConstants.VAULT_CACHE_KEY, - cacheEntry => - { + cacheEntry => { cacheEntry.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(CACHE_TTL); var getSecretResponse = _client.GetFromJsonAsync<GetSecretResponse<T>>("/v1/kv/data/" + path).Result; - if (getSecretResponse == null) - { + if (getSecretResponse == null) { return default; } - Log.Debug("Setting new vault cache, " + new - { + Log.Debug("Setting new vault cache, " + new { PATH = path, CACHE_TTL, Data = JsonSerializer.Serialize(getSecretResponse.Data.Data) @@ -48,42 +43,35 @@ public class VaultService return result; } - public T Refresh<T>(string path) - { + public T Refresh<T>(string path) { _cache.Remove(AppConstants.VAULT_CACHE_KEY); CACHE_TTL = _configuration.GetValue(AppEnvironmentVariables.VAULT_CACHE_TTL, 60 * 60 * 12); return Get<T>(path); } - public async Task<RenewTokenResponse> RenewTokenAsync<T>(string token) - { + public async Task<RenewTokenResponse> RenewTokenAsync<T>(string token) { var response = await _client.PostAsJsonAsync("v1/auth/token/renew", - new - { + new { Token = token }); - if (response.IsSuccessStatusCode) - { + if (response.IsSuccessStatusCode) { return await response.Content.ReadFromJsonAsync<RenewTokenResponse>(); } return default; } - public AppConfiguration GetCurrentAppConfiguration() - { + public AppConfiguration GetCurrentAppConfiguration() { #if DEBUG var isInFlightMode = true; - if (isInFlightMode) - { - return new AppConfiguration() - { - EMAIL_FROM_ADDRESS = "local@dev", - EMAIL_FROM_DISPLAY_NAME = "Friendly neighbourhood developer", + if (isInFlightMode) { + return new AppConfiguration() { + EMAIL_FROM_ADDRESS = "heydev@greatoffice.life", DB_HOST = "localhost", DB_PORT = "5432", DB_NAME = "greatoffice_ivar_dev", DB_PASSWORD = "ivar123", + POSTMARK_TOKEN = "b530c311-45c7-43e5-aa48-f2c992886e51", DB_USER = "postgres", QUARTZ_DB_HOST = "localhost", QUARTZ_DB_PORT = "5432", @@ -97,8 +85,7 @@ public class VaultService var path = _configuration.GetValue<string>(AppEnvironmentVariables.MAIN_CONFIG_SHEET); var result = Get<AppConfiguration>(path); - var overwrites = new - { + var overwrites = new { DB_HOST = _configuration.GetValue("OVERWRITE_DB_HOST", string.Empty), DB_PORT = _configuration.GetValue("OVERWRITE_DB_PORT", string.Empty), DB_USER = _configuration.GetValue("OVERWRITE_DB_USER", string.Empty), @@ -106,32 +93,27 @@ public class VaultService DB_NAME = _configuration.GetValue("OVERWRITE_DB_NAME", string.Empty), }; - if (overwrites.DB_HOST.HasValue()) - { + if (overwrites.DB_HOST.HasValue()) { _logger.LogInformation("OVERWRITE_DB_HOST is specified, using it's value: {DB_HOST}", overwrites.DB_HOST); result.DB_HOST = overwrites.DB_HOST; } - if (overwrites.DB_PORT.HasValue()) - { + if (overwrites.DB_PORT.HasValue()) { _logger.LogInformation("OVERWRITE_DB_PORT is specified, using it's value: {DB_PORT}", overwrites.DB_PORT); result.DB_PORT = overwrites.DB_PORT; } - if (overwrites.DB_USER.HasValue()) - { + if (overwrites.DB_USER.HasValue()) { _logger.LogInformation("OVERWRITE_DB_USER is specified, using it's value: {DB_USER}", overwrites.DB_USER); result.DB_USER = overwrites.DB_USER; } - if (overwrites.DB_PASSWORD.HasValue()) - { + if (overwrites.DB_PASSWORD.HasValue()) { _logger.LogInformation("OVERWRITE_DB_PASSWORD is specified, using it's value: (redacted)"); result.DB_PASSWORD = overwrites.DB_PASSWORD; } - if (overwrites.DB_NAME.HasValue()) - { + if (overwrites.DB_NAME.HasValue()) { _logger.LogInformation("OVERWRITE_DB_NAME is specified, using it's value: {DB_NAME}", overwrites.DB_NAME); result.DB_NAME = overwrites.DB_NAME; } @@ -139,8 +121,7 @@ public class VaultService return result; } - public AppConfiguration RefreshCurrentAppConfiguration() - { + public AppConfiguration RefreshCurrentAppConfiguration() { var path = _configuration.GetValue<string>(AppEnvironmentVariables.MAIN_CONFIG_SHEET); return Refresh<AppConfiguration>(path); } |
