1
0
mirror of https://github.com/bitwarden/server synced 2025-12-27 21:53:24 +00:00

[PM-27575] Add support for loading Mailer templates from disk (#6520)

Adds support for overloading mail templates from disk.
This commit is contained in:
Oscar Hinton
2025-11-10 08:40:40 +01:00
committed by GitHub
parent 22fe50c67a
commit 7d39efe29f
3 changed files with 216 additions and 5 deletions

View File

@@ -1,7 +1,9 @@
#nullable enable
using System.Collections.Concurrent;
using System.Reflection;
using Bit.Core.Settings;
using HandlebarsDotNet;
using Microsoft.Extensions.Logging;
namespace Bit.Core.Platform.Mail.Mailer;
public class HandlebarMailRenderer : IMailRenderer
@@ -9,7 +11,7 @@ public class HandlebarMailRenderer : IMailRenderer
/// <summary>
/// Lazy-initialized Handlebars instance. Thread-safe and ensures initialization occurs only once.
/// </summary>
private readonly Lazy<Task<IHandlebars>> _handlebarsTask = new(InitializeHandlebarsAsync, LazyThreadSafetyMode.ExecutionAndPublication);
private readonly Lazy<Task<IHandlebars>> _handlebarsTask;
/// <summary>
/// Helper function that returns the handlebar instance.
@@ -21,6 +23,17 @@ public class HandlebarMailRenderer : IMailRenderer
/// </summary>
private readonly ConcurrentDictionary<string, Lazy<Task<HandlebarsTemplate<object, object>>>> _templateCache = new();
private readonly ILogger<HandlebarMailRenderer> _logger;
private readonly GlobalSettings _globalSettings;
public HandlebarMailRenderer(ILogger<HandlebarMailRenderer> logger, GlobalSettings globalSettings)
{
_logger = logger;
_globalSettings = globalSettings;
_handlebarsTask = new Lazy<Task<IHandlebars>>(InitializeHandlebarsAsync, LazyThreadSafetyMode.ExecutionAndPublication);
}
public async Task<(string html, string txt)> RenderAsync(BaseMailView model)
{
var html = await CompileTemplateAsync(model, "html");
@@ -53,19 +66,59 @@ public class HandlebarMailRenderer : IMailRenderer
return handlebars.Compile(source);
}
private static async Task<string> ReadSourceAsync(Assembly assembly, string template)
private async Task<string> ReadSourceAsync(Assembly assembly, string template)
{
if (assembly.GetManifestResourceNames().All(f => f != template))
{
throw new FileNotFoundException("Template not found: " + template);
}
var diskSource = await ReadSourceFromDiskAsync(template);
if (!string.IsNullOrWhiteSpace(diskSource))
{
return diskSource;
}
await using var s = assembly.GetManifestResourceStream(template)!;
using var sr = new StreamReader(s);
return await sr.ReadToEndAsync();
}
private static async Task<IHandlebars> InitializeHandlebarsAsync()
private async Task<string?> ReadSourceFromDiskAsync(string template)
{
if (!_globalSettings.SelfHosted)
{
return null;
}
try
{
var diskPath = Path.GetFullPath(Path.Combine(_globalSettings.MailTemplateDirectory, template));
var baseDirectory = Path.GetFullPath(_globalSettings.MailTemplateDirectory);
// Ensure the resolved path is within the configured directory
if (!diskPath.StartsWith(baseDirectory + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase) &&
!diskPath.Equals(baseDirectory, StringComparison.OrdinalIgnoreCase))
{
_logger.LogWarning("Template path traversal attempt detected: {Template}", template);
return null;
}
if (File.Exists(diskPath))
{
var fileContents = await File.ReadAllTextAsync(diskPath);
return fileContents;
}
}
catch (Exception e)
{
_logger.LogError(e, "Failed to read mail template from disk: {TemplateName}", template);
}
return null;
}
private async Task<IHandlebars> InitializeHandlebarsAsync()
{
var handlebars = Handlebars.Create();