1
0
mirror of https://github.com/bitwarden/server.git synced 2025-04-05 21:18:13 -05:00

add support for postal and multi service mail delivery (#1326)

* adds suppose for postal and multi service mail delivery

* adjust tags

* dont need settings checks in multi-service
This commit is contained in:
Kyle Spearrin 2021-05-13 15:18:42 -04:00 committed by GitHub
parent 571862a7ac
commit b150f5977e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 173 additions and 2 deletions

View File

@ -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<AmazonSesMailDeliveryService> sesLogger,
ILogger<PostalMailDeliveryService> 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);
}
}
}
}

View File

@ -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<PostalMailDeliveryService> _logger;
private readonly IHttpClientFactory _clientFactory;
private readonly string _baseTag;
private readonly string _from;
private readonly string _reply;
public PostalMailDeliveryService(
GlobalSettings globalSettings,
ILogger<PostalMailDeliveryService> logger,
IWebHostEnvironment hostingEnvironment,
IHttpClientFactory clientFactory)
{
_globalSettings = globalSettings;
_logger = logger;
_clientFactory = clientFactory;
_baseTag = $"Env_{hostingEnvironment.EnvironmentName}-" +
$"Server_{globalSettings.ProjectName?.Replace(' ', '_')}";
_from = $"\"{globalSettings.SiteName}\" <no-reply@{_globalSettings.Mail.PostalDomain}>";
_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<string>(),
tag = _baseTag
};
foreach (var address in message.ToEmails)
{
request.to.Add(address);
}
if (message.BccEmails != null)
{
request.bcc = new List<string>();
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<PostalResponse>(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<string> to { get; set; }
public List<string> cc { get; set; }
public List<string> 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; }
}
}
}

View File

@ -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
{

View File

@ -143,7 +143,13 @@ namespace Bit.Core.Utilities
services.AddSingleton<IApplicationCacheService, InMemoryApplicationCacheService>();
}
if (CoreHelpers.SettingHasValue(globalSettings.Amazon?.AccessKeySecret))
var awsConfigured = CoreHelpers.SettingHasValue(globalSettings.Amazon?.AccessKeySecret);
if (!globalSettings.SelfHosted && awsConfigured &&
CoreHelpers.SettingHasValue(globalSettings.Mail?.PostalApiKey))
{
services.AddSingleton<IMailDeliveryService, MultiServiceMailDeliveryService>();
}
else if (awsConfigured)
{
services.AddSingleton<IMailDeliveryService, AmazonSesMailDeliveryService>();
}
@ -505,7 +511,7 @@ namespace Bit.Core.Utilities
{
mvc.Services.AddTransient<IViewLocalizer, I18nViewLocalizer>();
return mvc.AddViewLocalization(options => options.ResourcesPath = "Resources")
.AddDataAnnotationsLocalization(options =>
.AddDataAnnotationsLocalization(options =>
options.DataAnnotationLocalizerProvider = (type, factory) =>
{
var assemblyName = new AssemblyName(typeof(SharedResources).GetTypeInfo().Assembly.FullName);