-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Expand file tree
/
Copy pathHandlebarMailRenderer.cs
More file actions
145 lines (117 loc) · 5.52 KB
/
HandlebarMailRenderer.cs
File metadata and controls
145 lines (117 loc) · 5.52 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
#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
{
/// <summary>
/// Lazy-initialized Handlebars instance. Thread-safe and ensures initialization occurs only once.
/// </summary>
private readonly Lazy<Task<IHandlebars>> _handlebarsTask;
/// <summary>
/// Helper function that returns the handlebar instance.
/// </summary>
private Task<IHandlebars> GetHandlebars() => _handlebarsTask.Value;
/// <summary>
/// This dictionary is used to cache compiled templates in a thread-safe manner.
/// </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");
var txt = await CompileTemplateAsync(model, "text");
return (html, txt);
}
private async Task<string> CompileTemplateAsync(BaseMailView model, string type)
{
var templateName = $"{model.GetType().FullName}.{type}.hbs";
var assembly = model.GetType().Assembly;
// GetOrAdd is atomic - only one Lazy will be stored per templateName.
// The Lazy with ExecutionAndPublication ensures the compilation happens exactly once.
var lazyTemplate = _templateCache.GetOrAdd(
templateName,
key => new Lazy<Task<HandlebarsTemplate<object, object>>>(
() => CompileTemplateInternalAsync(assembly, key),
LazyThreadSafetyMode.ExecutionAndPublication));
var template = await lazyTemplate.Value;
return template(model);
}
private async Task<HandlebarsTemplate<object, object>> CompileTemplateInternalAsync(Assembly assembly, string templateName)
{
var source = await ReadSourceAsync(assembly, templateName);
var handlebars = await GetHandlebars();
return handlebars.Compile(source);
}
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 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();
// TODO: Do we still need layouts with MJML?
var assembly = typeof(HandlebarMailRenderer).Assembly;
// Register Full layouts
var fullHtmlLayoutSource = await ReadSourceAsync(assembly, "Bit.Core.MailTemplates.Handlebars.Layouts.Full.html.hbs");
handlebars.RegisterTemplate("FullHtmlLayout", fullHtmlLayoutSource);
var fullTextLayoutSource = await ReadSourceAsync(assembly, "Bit.Core.MailTemplates.Handlebars.Layouts.Full.text.hbs");
handlebars.RegisterTemplate("FullTextLayout", fullTextLayoutSource);
// Register TitleContactUs layouts
var titleContactUsHtmlLayoutSource = await ReadSourceAsync(assembly, "Bit.Core.MailTemplates.Handlebars.Layouts.TitleContactUs.html.hbs");
handlebars.RegisterTemplate("TitleContactUsHtmlLayout", titleContactUsHtmlLayoutSource);
var titleContactUsTextLayoutSource = await ReadSourceAsync(assembly, "Bit.Core.MailTemplates.Handlebars.Layouts.TitleContactUs.text.hbs");
handlebars.RegisterTemplate("TitleContactUsTextLayout", titleContactUsTextLayoutSource);
return handlebars;
}
}