diff --git a/src/Core/Services/Implementations/MultiServiceMailDeliveryService.cs b/src/Core/Services/Implementations/MultiServiceMailDeliveryService.cs new file mode 100644 index 0000000000..b54e59564f --- /dev/null +++ b/src/Core/Services/Implementations/MultiServiceMailDeliveryService.cs @@ -0,0 +1,47 @@ +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Bit.Core.Settings; +using Microsoft.AspNetCore.Hosting; +using System.Net.Http; +using Bit.Core.Models.Mail; + +namespace Bit.Core.Services +{ + public class MultiServiceMailDeliveryService : IMailDeliveryService + { + private readonly IMailDeliveryService _sesService; + private readonly IMailDeliveryService _postalService; + private readonly int _postalPercentage; + + private static Random _random = new Random(); + + public MultiServiceMailDeliveryService( + GlobalSettings globalSettings, + IWebHostEnvironment hostingEnvironment, + IHttpClientFactory httpClientFactory, + ILogger sesLogger, + ILogger postalLogger) + { + _sesService = new AmazonSesMailDeliveryService(globalSettings, hostingEnvironment, sesLogger); + _postalService = new PostalMailDeliveryService(globalSettings, postalLogger, hostingEnvironment, + httpClientFactory); + + // 2% by default + _postalPercentage = (globalSettings.Mail?.PostalPercentage).GetValueOrDefault(2); + } + + public async Task SendEmailAsync(MailMessage message) + { + var roll = _random.Next(0, 99); + if (roll < _postalPercentage) + { + await _postalService.SendEmailAsync(message); + } + else + { + await _sesService.SendEmailAsync(message); + } + } + } +} diff --git a/src/Core/Services/Implementations/PostalMailDeliveryService.cs b/src/Core/Services/Implementations/PostalMailDeliveryService.cs new file mode 100644 index 0000000000..8d751b9c0e --- /dev/null +++ b/src/Core/Services/Implementations/PostalMailDeliveryService.cs @@ -0,0 +1,115 @@ +using System; +using System.Threading.Tasks; +using Bit.Core.Settings; +using Microsoft.Extensions.Logging; +using System.Net.Http; +using System.Collections.Generic; +using Newtonsoft.Json; +using Microsoft.AspNetCore.Hosting; +using System.Text; + +namespace Bit.Core.Services +{ + public class PostalMailDeliveryService : IMailDeliveryService + { + private readonly GlobalSettings _globalSettings; + private readonly ILogger _logger; + private readonly IHttpClientFactory _clientFactory; + private readonly string _baseTag; + private readonly string _from; + private readonly string _reply; + + public PostalMailDeliveryService( + GlobalSettings globalSettings, + ILogger logger, + IWebHostEnvironment hostingEnvironment, + IHttpClientFactory clientFactory) + { + _globalSettings = globalSettings; + _logger = logger; + _clientFactory = clientFactory; + _baseTag = $"Env_{hostingEnvironment.EnvironmentName}-" + + $"Server_{globalSettings.ProjectName?.Replace(' ', '_')}"; + _from = $"\"{globalSettings.SiteName}\" "; + _reply = $"\"{globalSettings.SiteName}\" <{globalSettings.Mail.ReplyToEmail}>"; + } + + public async Task SendEmailAsync(Models.Mail.MailMessage message) + { + var httpClient = _clientFactory.CreateClient("PostalMailDeliveryService"); + httpClient.DefaultRequestHeaders.Add("X-Server-API-Key", _globalSettings.Mail.PostalApiKey); + + var request = new PostalRequest + { + subject = message.Subject, + from = _from, + reply_to = _reply, + html_body = message.HtmlContent, + to = new List(), + tag = _baseTag + }; + foreach (var address in message.ToEmails) + { + request.to.Add(address); + } + + if (message.BccEmails != null) + { + request.bcc = new List(); + foreach (var address in message.BccEmails) + { + request.bcc.Add(address); + } + } + + if (!string.IsNullOrWhiteSpace(message.TextContent)) + { + request.plain_body = message.TextContent; + } + + if (!string.IsNullOrWhiteSpace(message.Category)) + { + request.tag = string.Concat(request.tag, "-Cat_", message.Category); + } + + var reqJson = JsonConvert.SerializeObject(request); + var responseMessage = await httpClient.PostAsync( + $"https://{_globalSettings.Mail.PostalDomain}/api/v1/send/message", + new StringContent(reqJson, Encoding.UTF8, "application/json")); + + if (responseMessage.IsSuccessStatusCode) + { + var json = await responseMessage.Content.ReadAsStringAsync(); + var response = JsonConvert.DeserializeObject(json); + if (response.status != "success") + { + _logger.LogError("Postal send status was not successful: {0}, {1}", + response.status, response.message); + } + } + else + { + _logger.LogError("Postal send failed: {0}", responseMessage.StatusCode); + } + } + + public class PostalRequest + { + public List to { get; set; } + public List cc { get; set; } + public List bcc { get; set; } + public string tag { get; set; } + public string from { get; set; } + public string reply_to { get; set; } + public string plain_body { get; set; } + public string html_body { get; set; } + public string subject { get; set; } + } + + public class PostalResponse + { + public string status { get; set; } + public string message { get; set; } + } + } +} diff --git a/src/Core/Settings/GlobalSettings.cs b/src/Core/Settings/GlobalSettings.cs index 0509e46764..6e2ff2ccea 100644 --- a/src/Core/Settings/GlobalSettings.cs +++ b/src/Core/Settings/GlobalSettings.cs @@ -275,6 +275,9 @@ namespace Bit.Core.Settings public string ReplyToEmail { get; set; } public string AmazonConfigSetName { get; set; } public SmtpSettings Smtp { get; set; } = new SmtpSettings(); + public string PostalDomain { get; set; } + public string PostalApiKey { get; set; } + public int? PostalPercentage { get; set; } public class SmtpSettings { diff --git a/src/Core/Utilities/ServiceCollectionExtensions.cs b/src/Core/Utilities/ServiceCollectionExtensions.cs index 2cafa9b1a4..33d4652569 100644 --- a/src/Core/Utilities/ServiceCollectionExtensions.cs +++ b/src/Core/Utilities/ServiceCollectionExtensions.cs @@ -143,7 +143,13 @@ namespace Bit.Core.Utilities services.AddSingleton(); } - if (CoreHelpers.SettingHasValue(globalSettings.Amazon?.AccessKeySecret)) + var awsConfigured = CoreHelpers.SettingHasValue(globalSettings.Amazon?.AccessKeySecret); + if (!globalSettings.SelfHosted && awsConfigured && + CoreHelpers.SettingHasValue(globalSettings.Mail?.PostalApiKey)) + { + services.AddSingleton(); + } + else if (awsConfigured) { services.AddSingleton(); } @@ -505,7 +511,7 @@ namespace Bit.Core.Utilities { mvc.Services.AddTransient(); return mvc.AddViewLocalization(options => options.ResourcesPath = "Resources") - .AddDataAnnotationsLocalization(options => + .AddDataAnnotationsLocalization(options => options.DataAnnotationLocalizerProvider = (type, factory) => { var assemblyName = new AssemblyName(typeof(SharedResources).GetTypeInfo().Assembly.FullName);