1
0
mirror of https://github.com/bitwarden/server synced 2025-12-06 00:03:34 +00:00

[PM-25427] Allow reading mail templates from disk (#6123)

* Allow reading mail templates from self host disk

* Update src/Core/Services/Implementations/HandlebarsMailService.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update src/Core/Services/Implementations/HandlebarsMailService.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* tweak logic

* some error handling reading templates from disk

* fix: broken test

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Andreas Coroiu <andreas.coroiu@gmail.com>
This commit is contained in:
Kyle Spearrin
2025-10-06 04:13:56 -04:00
committed by GitHub
parent d2577f670e
commit 60d701c945
3 changed files with 59 additions and 3 deletions

View File

@@ -26,6 +26,7 @@ using Bit.Core.Vault.Models.Data;
using Core.Auth.Enums;
using HandlebarsDotNet;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Logging;
namespace Bit.Core.Services;
@@ -39,6 +40,7 @@ public class HandlebarsMailService : IMailService
private readonly IMailDeliveryService _mailDeliveryService;
private readonly IMailEnqueuingService _mailEnqueuingService;
private readonly IDistributedCache _distributedCache;
private readonly ILogger<HandlebarsMailService> _logger;
private readonly Dictionary<string, HandlebarsTemplate<object, object>> _templateCache = new();
private bool _registeredHelpersAndPartials = false;
@@ -47,12 +49,14 @@ public class HandlebarsMailService : IMailService
GlobalSettings globalSettings,
IMailDeliveryService mailDeliveryService,
IMailEnqueuingService mailEnqueuingService,
IDistributedCache distributedCache)
IDistributedCache distributedCache,
ILogger<HandlebarsMailService> logger)
{
_globalSettings = globalSettings;
_mailDeliveryService = mailDeliveryService;
_mailEnqueuingService = mailEnqueuingService;
_distributedCache = distributedCache;
_logger = logger;
}
public async Task SendVerifyEmailEmailAsync(string email, Guid userId, string token)
@@ -708,6 +712,12 @@ public class HandlebarsMailService : IMailService
private async Task<string?> ReadSourceAsync(string templateName)
{
var diskSource = await ReadSourceFromDiskAsync(templateName);
if (!string.IsNullOrWhiteSpace(diskSource))
{
return diskSource;
}
var assembly = typeof(HandlebarsMailService).GetTypeInfo().Assembly;
var fullTemplateName = $"{Namespace}.{templateName}.hbs";
if (!assembly.GetManifestResourceNames().Any(f => f == fullTemplateName))
@@ -721,6 +731,42 @@ public class HandlebarsMailService : IMailService
}
}
private async Task<string?> ReadSourceFromDiskAsync(string templateName)
{
if (!_globalSettings.SelfHosted)
{
return null;
}
try
{
var templateFileSuffix = ".html";
if (templateName.EndsWith(".txt"))
{
templateFileSuffix = ".txt";
}
else if (!templateName.EndsWith(".html"))
{
// unexpected suffix
return null;
}
var suffixPosition = templateName.LastIndexOf(templateFileSuffix);
var templateNameNoSuffix = templateName.Substring(0, suffixPosition);
var templatePathNoSuffix = templateNameNoSuffix.Replace(".", "/");
var diskPath = $"{_globalSettings.MailTemplateDirectory}/{templatePathNoSuffix}{templateFileSuffix}.hbs";
var directory = Path.GetDirectoryName(diskPath);
if (Directory.Exists(directory) && File.Exists(diskPath))
{
var fileContents = await File.ReadAllTextAsync(diskPath);
return fileContents;
}
}
catch (Exception e)
{
_logger.LogError(e, "Failed to read mail template from disk.");
}
return null;
}
private async Task RegisterHelpersAndPartialsAsync()
{
if (_registeredHelpersAndPartials)

View File

@@ -8,6 +8,7 @@ namespace Bit.Core.Settings;
public class GlobalSettings : IGlobalSettings
{
private string _mailTemplateDirectory;
private string _logDirectory;
private string _licenseDirectory;
@@ -37,6 +38,11 @@ public class GlobalSettings : IGlobalSettings
get => BuildDirectory(_licenseDirectory, "/core/licenses");
set => _licenseDirectory = value;
}
public virtual string MailTemplateDirectory
{
get => BuildDirectory(_mailTemplateDirectory, "/mail-templates");
set => _mailTemplateDirectory = value;
}
public string LicenseCertificatePassword { get; set; }
public virtual string PushRelayBaseUri { get; set; }
public virtual string InternalIdentityKey { get; set; }

View File

@@ -23,6 +23,7 @@ public class HandlebarsMailServiceTests
private readonly IMailDeliveryService _mailDeliveryService;
private readonly IMailEnqueuingService _mailEnqueuingService;
private readonly IDistributedCache _distributedCache;
private readonly ILogger<HandlebarsMailService> _logger;
public HandlebarsMailServiceTests()
{
@@ -30,12 +31,14 @@ public class HandlebarsMailServiceTests
_mailDeliveryService = Substitute.For<IMailDeliveryService>();
_mailEnqueuingService = Substitute.For<IMailEnqueuingService>();
_distributedCache = Substitute.For<IDistributedCache>();
_logger = Substitute.For<ILogger<HandlebarsMailService>>();
_sut = new HandlebarsMailService(
_globalSettings,
_mailDeliveryService,
_mailEnqueuingService,
_distributedCache
_distributedCache,
_logger
);
}
@@ -217,8 +220,9 @@ public class HandlebarsMailServiceTests
var mailDeliveryService = new MailKitSmtpMailDeliveryService(globalSettings, Substitute.For<ILogger<MailKitSmtpMailDeliveryService>>());
var distributedCache = Substitute.For<IDistributedCache>();
var logger = Substitute.For<ILogger<HandlebarsMailService>>();
var handlebarsService = new HandlebarsMailService(globalSettings, mailDeliveryService, new BlockingMailEnqueuingService(), distributedCache);
var handlebarsService = new HandlebarsMailService(globalSettings, mailDeliveryService, new BlockingMailEnqueuingService(), distributedCache, logger);
var sendMethods = typeof(IMailService).GetMethods(BindingFlags.Public | BindingFlags.Instance)
.Where(m => m.Name.StartsWith("Send") && m.Name != "SendEnqueuedMailMessageAsync");