1
0
mirror of https://github.com/bitwarden/server synced 2025-12-31 07:33:43 +00:00

chore(docs): Add docs for legacy mail service

* Added docs for legacy mail service.

* Updated namespaces.

* Consolidated under Platform.Mail namespace

* Updated obsolete comment.

* Linting

* Linting

* Replaced documentation in original readme after accidental deletion.
This commit is contained in:
Todd Martin
2025-11-04 11:54:39 -05:00
committed by GitHub
parent 04ed8abf5a
commit 3668a445e5
30 changed files with 73 additions and 51 deletions

View File

@@ -1,144 +0,0 @@
#nullable enable
using Amazon;
using Amazon.SimpleEmail;
using Amazon.SimpleEmail.Model;
using Bit.Core.Models.Mail;
using Bit.Core.Settings;
using Bit.Core.Utilities;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Logging;
namespace Bit.Core.Services;
public class AmazonSesMailDeliveryService : IMailDeliveryService, IDisposable
{
private readonly GlobalSettings _globalSettings;
private readonly IWebHostEnvironment _hostingEnvironment;
private readonly ILogger<AmazonSesMailDeliveryService> _logger;
private readonly IAmazonSimpleEmailService _client;
private readonly string _source;
private readonly string _senderTag;
private readonly string? _configSetName;
public AmazonSesMailDeliveryService(
GlobalSettings globalSettings,
IWebHostEnvironment hostingEnvironment,
ILogger<AmazonSesMailDeliveryService> logger)
: this(globalSettings, hostingEnvironment, logger,
new AmazonSimpleEmailServiceClient(
globalSettings.Amazon.AccessKeyId,
globalSettings.Amazon.AccessKeySecret,
RegionEndpoint.GetBySystemName(globalSettings.Amazon.Region))
)
{
}
public AmazonSesMailDeliveryService(
GlobalSettings globalSettings,
IWebHostEnvironment hostingEnvironment,
ILogger<AmazonSesMailDeliveryService> logger,
IAmazonSimpleEmailService amazonSimpleEmailService)
{
if (string.IsNullOrWhiteSpace(globalSettings.Amazon?.AccessKeyId))
{
throw new ArgumentNullException(nameof(globalSettings.Amazon.AccessKeyId));
}
if (string.IsNullOrWhiteSpace(globalSettings.Amazon?.AccessKeySecret))
{
throw new ArgumentNullException(nameof(globalSettings.Amazon.AccessKeySecret));
}
if (string.IsNullOrWhiteSpace(globalSettings.Amazon?.Region))
{
throw new ArgumentNullException(nameof(globalSettings.Amazon.Region));
}
var replyToEmail = CoreHelpers.PunyEncode(globalSettings.Mail.ReplyToEmail);
_globalSettings = globalSettings;
_hostingEnvironment = hostingEnvironment;
_logger = logger;
_client = amazonSimpleEmailService;
_source = $"\"{globalSettings.SiteName}\" <{replyToEmail}>";
_senderTag = $"Server_{globalSettings.ProjectName?.Replace(' ', '_')}";
if (!string.IsNullOrWhiteSpace(_globalSettings.Mail.AmazonConfigSetName))
{
_configSetName = _globalSettings.Mail.AmazonConfigSetName;
}
}
public void Dispose()
{
_client?.Dispose();
}
public async Task SendEmailAsync(MailMessage message)
{
var request = new SendEmailRequest
{
ConfigurationSetName = _configSetName,
Source = _source,
Destination = new Destination
{
ToAddresses = message.ToEmails
.Select(email => CoreHelpers.PunyEncode(email))
.ToList()
},
Message = new Message
{
Subject = new Content(message.Subject),
Body = new Body
{
Html = new Content
{
Charset = "UTF-8",
Data = message.HtmlContent
},
Text = new Content
{
Charset = "UTF-8",
Data = message.TextContent
}
}
},
Tags = new List<MessageTag>
{
new MessageTag { Name = "Environment", Value = _hostingEnvironment.EnvironmentName },
new MessageTag { Name = "Sender", Value = _senderTag }
}
};
if (message.BccEmails?.Any() ?? false)
{
request.Destination.BccAddresses = message.BccEmails
.Select(email => CoreHelpers.PunyEncode(email))
.ToList();
}
if (!string.IsNullOrWhiteSpace(message.Category))
{
request.Tags.Add(new MessageTag { Name = "Category", Value = message.Category });
}
try
{
await SendAsync(request, false);
}
catch (Exception e)
{
_logger.LogWarning(e, "Failed to send email. Retrying...");
await SendAsync(request, true);
throw;
}
}
private async Task SendAsync(SendEmailRequest request, bool retry)
{
if (retry)
{
// wait and try again
await Task.Delay(2000);
}
await _client.SendEmailAsync(request);
}
}

View File

@@ -1,20 +0,0 @@
using Azure.Storage.Queues;
using Bit.Core.Models.Mail;
using Bit.Core.Settings;
using Bit.Core.Utilities;
namespace Bit.Core.Services;
public class AzureQueueMailService : AzureQueueService<IMailQueueMessage>, IMailEnqueuingService
{
public AzureQueueMailService(GlobalSettings globalSettings) : base(
new QueueClient(globalSettings.Mail.ConnectionString, "mail"),
JsonHelpers.IgnoreWritingNull)
{ }
public Task EnqueueAsync(IMailQueueMessage message, Func<IMailQueueMessage, Task> fallback) =>
CreateManyAsync(new[] { message });
public Task EnqueueManyAsync(IEnumerable<IMailQueueMessage> messages, Func<IMailQueueMessage, Task> fallback) =>
CreateManyAsync(messages);
}

View File

@@ -1,19 +0,0 @@
using Bit.Core.Models.Mail;
namespace Bit.Core.Services;
public class BlockingMailEnqueuingService : IMailEnqueuingService
{
public async Task EnqueueAsync(IMailQueueMessage message, Func<IMailQueueMessage, Task> fallback)
{
await fallback(message);
}
public async Task EnqueueManyAsync(IEnumerable<IMailQueueMessage> messages, Func<IMailQueueMessage, Task> fallback)
{
foreach (var message in messages)
{
await fallback(message);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,123 +0,0 @@
// FIXME: Update this file to be null safe and then delete the line below
#nullable disable
using Bit.Core.Settings;
using Bit.Core.Utilities;
using MailKit.Net.Smtp;
using Microsoft.Extensions.Logging;
using MimeKit;
namespace Bit.Core.Services;
public class MailKitSmtpMailDeliveryService : IMailDeliveryService
{
private readonly GlobalSettings _globalSettings;
private readonly ILogger<MailKitSmtpMailDeliveryService> _logger;
private readonly string _replyDomain;
private readonly string _replyEmail;
public MailKitSmtpMailDeliveryService(
GlobalSettings globalSettings,
ILogger<MailKitSmtpMailDeliveryService> logger)
{
if (globalSettings.Mail.Smtp?.Host == null)
{
throw new ArgumentNullException(nameof(globalSettings.Mail.Smtp.Host));
}
if (globalSettings.Mail.ReplyToEmail == null)
{
throw new InvalidOperationException("A GlobalSettings.Mail.ReplyToEmail is required to be set up.");
}
_replyEmail = CoreHelpers.PunyEncode(globalSettings.Mail.ReplyToEmail);
if (_replyEmail.Contains("@"))
{
_replyDomain = _replyEmail.Split('@')[1];
}
_globalSettings = globalSettings;
_logger = logger;
}
public async Task SendEmailAsync(Models.Mail.MailMessage message)
=> await SendEmailAsync(message, CancellationToken.None);
public async Task SendEmailAsync(Models.Mail.MailMessage message, CancellationToken cancellationToken)
{
var mimeMessage = new MimeMessage();
mimeMessage.From.Add(new MailboxAddress(_globalSettings.SiteName, _replyEmail));
mimeMessage.Subject = message.Subject;
if (!string.IsNullOrWhiteSpace(_replyDomain))
{
mimeMessage.MessageId = $"<{Guid.NewGuid()}@{_replyDomain}>";
}
foreach (var address in message.ToEmails)
{
var punyencoded = CoreHelpers.PunyEncode(address);
mimeMessage.To.Add(MailboxAddress.Parse(punyencoded));
}
if (message.BccEmails != null)
{
foreach (var address in message.BccEmails)
{
var punyencoded = CoreHelpers.PunyEncode(address);
mimeMessage.Bcc.Add(MailboxAddress.Parse(punyencoded));
}
}
var builder = new BodyBuilder();
if (!string.IsNullOrWhiteSpace(message.TextContent))
{
builder.TextBody = message.TextContent;
}
builder.HtmlBody = message.HtmlContent;
mimeMessage.Body = builder.ToMessageBody();
using (var client = new SmtpClient())
{
if (_globalSettings.Mail.Smtp.TrustServer)
{
client.ServerCertificateValidationCallback = (s, c, h, e) => true;
}
if (!_globalSettings.Mail.Smtp.StartTls && !_globalSettings.Mail.Smtp.Ssl &&
_globalSettings.Mail.Smtp.Port == 25)
{
await client.ConnectAsync(
_globalSettings.Mail.Smtp.Host,
_globalSettings.Mail.Smtp.Port,
MailKit.Security.SecureSocketOptions.None,
cancellationToken
);
}
else
{
var useSsl = _globalSettings.Mail.Smtp.Port == 587 && !_globalSettings.Mail.Smtp.SslOverride ?
false : _globalSettings.Mail.Smtp.Ssl;
await client.ConnectAsync(
_globalSettings.Mail.Smtp.Host,
_globalSettings.Mail.Smtp.Port,
useSsl,
cancellationToken
);
}
if (CoreHelpers.SettingHasValue(_globalSettings.Mail.Smtp.Username) &&
CoreHelpers.SettingHasValue(_globalSettings.Mail.Smtp.Password))
{
await client.AuthenticateAsync(
_globalSettings.Mail.Smtp.Username,
_globalSettings.Mail.Smtp.Password,
cancellationToken
);
}
await client.SendAsync(mimeMessage, cancellationToken);
await client.DisconnectAsync(true, cancellationToken);
}
}
}

View File

@@ -1,41 +0,0 @@
using Bit.Core.Models.Mail;
using Bit.Core.Settings;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Logging;
namespace Bit.Core.Services;
public class MultiServiceMailDeliveryService : IMailDeliveryService
{
private readonly IMailDeliveryService _sesService;
private readonly IMailDeliveryService _sendGridService;
private readonly int _sendGridPercentage;
private static Random _random = new Random();
public MultiServiceMailDeliveryService(
GlobalSettings globalSettings,
IWebHostEnvironment hostingEnvironment,
ILogger<AmazonSesMailDeliveryService> sesLogger,
ILogger<SendGridMailDeliveryService> sendGridLogger)
{
_sesService = new AmazonSesMailDeliveryService(globalSettings, hostingEnvironment, sesLogger);
_sendGridService = new SendGridMailDeliveryService(globalSettings, hostingEnvironment, sendGridLogger);
// disabled by default (-1)
_sendGridPercentage = (globalSettings.Mail?.SendGridPercentage).GetValueOrDefault(-1);
}
public async Task SendEmailAsync(MailMessage message)
{
var roll = _random.Next(0, 99);
if (roll < _sendGridPercentage)
{
await _sendGridService.SendEmailAsync(message);
}
else
{
await _sesService.SendEmailAsync(message);
}
}
}

View File

@@ -1,114 +0,0 @@
using Bit.Core.Models.Mail;
using Bit.Core.Settings;
using Bit.Core.Utilities;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Logging;
using SendGrid;
using SendGrid.Helpers.Mail;
namespace Bit.Core.Services;
public class SendGridMailDeliveryService : IMailDeliveryService, IDisposable
{
private readonly GlobalSettings _globalSettings;
private readonly IWebHostEnvironment _hostingEnvironment;
private readonly ILogger<SendGridMailDeliveryService> _logger;
private readonly ISendGridClient _client;
private readonly string _senderTag;
private readonly string _replyToEmail;
public SendGridMailDeliveryService(
GlobalSettings globalSettings,
IWebHostEnvironment hostingEnvironment,
ILogger<SendGridMailDeliveryService> logger)
: this(new SendGridClient(globalSettings.Mail.SendGridApiKey, globalSettings.Mail.SendGridApiHost),
globalSettings, hostingEnvironment, logger)
{
}
public void Dispose()
{
// TODO: nothing to dispose
}
public SendGridMailDeliveryService(
ISendGridClient client,
GlobalSettings globalSettings,
IWebHostEnvironment hostingEnvironment,
ILogger<SendGridMailDeliveryService> logger)
{
if (string.IsNullOrWhiteSpace(globalSettings.Mail?.SendGridApiKey))
{
throw new ArgumentNullException(nameof(globalSettings.Mail.SendGridApiKey));
}
_globalSettings = globalSettings;
_hostingEnvironment = hostingEnvironment;
_logger = logger;
_client = client;
_senderTag = $"Server_{globalSettings.ProjectName?.Replace(' ', '_')}";
_replyToEmail = CoreHelpers.PunyEncode(globalSettings.Mail.ReplyToEmail);
}
public async Task SendEmailAsync(MailMessage message)
{
var msg = new SendGridMessage();
msg.SetFrom(new EmailAddress(_replyToEmail, _globalSettings.SiteName));
msg.AddTos(message.ToEmails.Select(e => new EmailAddress(CoreHelpers.PunyEncode(e))).ToList());
if (message.BccEmails?.Any() ?? false)
{
msg.AddBccs(message.BccEmails.Select(e => new EmailAddress(CoreHelpers.PunyEncode(e))).ToList());
}
msg.SetSubject(message.Subject);
msg.AddContent(MimeType.Text, message.TextContent);
msg.AddContent(MimeType.Html, message.HtmlContent);
msg.AddCategory($"type:{message.Category}");
msg.AddCategory($"env:{_hostingEnvironment.EnvironmentName}");
msg.AddCategory($"sender:{_senderTag}");
msg.SetClickTracking(false, false);
msg.SetOpenTracking(false);
if (message.MetaData != null &&
message.MetaData.TryGetValue("SendGridBypassListManagement", out var sendGridBypassListManagement) &&
Convert.ToBoolean(sendGridBypassListManagement))
{
msg.SetBypassListManagement(true);
}
try
{
var success = await SendAsync(msg, false);
if (!success)
{
_logger.LogWarning("Failed to send email. Retrying...");
await SendAsync(msg, true);
}
}
catch (Exception e)
{
_logger.LogWarning(e, "Failed to send email (with exception). Retrying...");
await SendAsync(msg, true);
throw;
}
}
private async Task<bool> SendAsync(SendGridMessage message, bool retry)
{
if (retry)
{
// wait and try again
await Task.Delay(2000);
}
var response = await _client.SendEmailAsync(message);
if (!response.IsSuccessStatusCode)
{
var responseBody = await response.Body.ReadAsStringAsync();
_logger.LogError("SendGrid email sending failed with {0}: {1}", response.StatusCode, responseBody);
}
return response.IsSuccessStatusCode;
}
}