mirror of
https://github.com/bitwarden/server.git
synced 2025-06-15 07:20:49 -05:00
Implement Mailer
This commit is contained in:
parent
7e875bfd8d
commit
7ae6424bc3
@ -1,24 +0,0 @@
|
|||||||
{{#>FullHtmlLayout}}
|
|
||||||
<table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
|
||||||
<tr style="margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
|
||||||
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: left;" valign="top" align="center">
|
|
||||||
Verify this email address for your Bitwarden account by clicking the link below.
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr style="margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
|
||||||
<td class="content-block last" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none; text-align: left;" valign="top" align="center">
|
|
||||||
If you did not request to verify a Bitwarden account, you can safely ignore this email.
|
|
||||||
<br style="margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;" />
|
|
||||||
<br style="margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;" />
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr style="margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
|
||||||
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;" valign="top" align="center">
|
|
||||||
<a href="{{{Url}}}" clicktracking=off target="_blank" style="color: #ffffff; text-decoration: none; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; background-color: #175DDC; border-color: #175DDC; border-style: solid; border-width: 10px 20px; margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
|
||||||
Verify Email Address Now
|
|
||||||
</a>
|
|
||||||
<br style="margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;" />
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
{{/FullHtmlLayout}}
|
|
@ -1,8 +0,0 @@
|
|||||||
{{#>BasicTextLayout}}
|
|
||||||
Verify this email address for your Bitwarden account by clicking the link below.
|
|
||||||
|
|
||||||
If you did not request to verify a Bitwarden account, you can safely ignore this email.
|
|
||||||
|
|
||||||
{{{Url}}}
|
|
||||||
|
|
||||||
{{/BasicTextLayout}}
|
|
54
src/Core/Platform/Services/BaseMail.cs
Normal file
54
src/Core/Platform/Services/BaseMail.cs
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
namespace Bit.Core.Platform.Services;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// BaseMail describes a model for emails. It contains metadata about the email such as recipients,
|
||||||
|
/// subject, and an optional category for processing at the upstream email delivery service.
|
||||||
|
///
|
||||||
|
/// Each BaseMail must have a view model that inherits from BaseMailView. The view model is used to
|
||||||
|
/// generate the text part and HTML body.
|
||||||
|
/// </summary>
|
||||||
|
public abstract class BaseMail<TView> where TView : BaseMailView
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Email recipients.
|
||||||
|
/// </summary>
|
||||||
|
public required IEnumerable<string> ToEmails { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The subject of the email.
|
||||||
|
/// </summary>
|
||||||
|
public abstract string Subject { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// An optional category for processing at the upstream email delivery service.
|
||||||
|
/// </summary>
|
||||||
|
public string? Category { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Allows you to override ignore the suppression list for this email.
|
||||||
|
///
|
||||||
|
/// Warning: This should be used with caution, valid reasons are primarily account recovery, email OTP.
|
||||||
|
/// </summary>
|
||||||
|
public virtual bool IgnoreSuppressList { get; } = false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// View model for the email body.
|
||||||
|
/// </summary>
|
||||||
|
public required TView View { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Each MailView consists of two body parts: a text part and an HTML part and the filename must be
|
||||||
|
/// relative to the viewmodel and match the following pattern:
|
||||||
|
/// - `{ClassName}.html.hbs` for the HTML part
|
||||||
|
/// - `{ClassName}.txt.hbs` for the text part
|
||||||
|
/// </summary>
|
||||||
|
public abstract class BaseMailView
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Current year.
|
||||||
|
/// </summary>
|
||||||
|
public string CurrentYear => DateTime.UtcNow.Year.ToString();
|
||||||
|
}
|
98
src/Core/Platform/Services/HandlebarMailRenderer.cs
Normal file
98
src/Core/Platform/Services/HandlebarMailRenderer.cs
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
#nullable enable
|
||||||
|
using System.Reflection;
|
||||||
|
using HandlebarsDotNet;
|
||||||
|
|
||||||
|
namespace Bit.Core.Platform.Services;
|
||||||
|
|
||||||
|
public class HandlebarMailRenderer : IMailRenderer
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// This field holds the handlebars instance.
|
||||||
|
///
|
||||||
|
/// This should never be used directly, rather use the GetHandlebars() method to ensure that it is initialized properly.
|
||||||
|
/// </summary>
|
||||||
|
private IHandlebars? _handlebars;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This task is used to ensure that the handlebars instance is initialized only once.
|
||||||
|
/// </summary>
|
||||||
|
private Task<IHandlebars>? _initTask;
|
||||||
|
/// <summary>
|
||||||
|
/// This lock is used to ensure that the handlebars instance is initialized only once,
|
||||||
|
/// even if multiple threads call GetHandlebars() at the same time.
|
||||||
|
/// </summary>
|
||||||
|
private readonly object _initLock = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This dictionary is used to cache compiled templates.
|
||||||
|
/// </summary>
|
||||||
|
private readonly Dictionary<string, HandlebarsTemplate<object, object>> _templateCache = new();
|
||||||
|
|
||||||
|
public async Task<(string html, string? txt)> RenderAsync(BaseMailView model)
|
||||||
|
{
|
||||||
|
var html = await CompileTemplateAsync(model, "html");
|
||||||
|
var txt = await CompileTemplateAsync(model, "txt");
|
||||||
|
|
||||||
|
return (html, txt);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<string> CompileTemplateAsync(BaseMailView model, string type)
|
||||||
|
{
|
||||||
|
var handlebars = await GetHandlebars();
|
||||||
|
|
||||||
|
var templateName = $"{model.GetType().FullName}.{type}.hbs";
|
||||||
|
|
||||||
|
if (!_templateCache.TryGetValue(templateName, out var template))
|
||||||
|
{
|
||||||
|
var assembly = model.GetType().Assembly;
|
||||||
|
var source = await ReadSourceAsync(assembly, templateName);
|
||||||
|
template = handlebars.Compile(source);
|
||||||
|
_templateCache.Add(templateName, template);
|
||||||
|
}
|
||||||
|
|
||||||
|
return template(model);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<string> ReadSourceAsync(Assembly assembly, string template)
|
||||||
|
{
|
||||||
|
if (assembly.GetManifestResourceNames().All(f => f != template))
|
||||||
|
{
|
||||||
|
throw new FileNotFoundException("Template not found: " + template);
|
||||||
|
}
|
||||||
|
|
||||||
|
await using var s = assembly.GetManifestResourceStream(template)!;
|
||||||
|
using var sr = new StreamReader(s);
|
||||||
|
return await sr.ReadToEndAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Helper function that returns the handlebar instance, initializing it if necessary.
|
||||||
|
///
|
||||||
|
/// Protects against initializing the same instance multiple times in parallel.
|
||||||
|
/// </summary>
|
||||||
|
private Task<IHandlebars> GetHandlebars()
|
||||||
|
{
|
||||||
|
if (_handlebars != null)
|
||||||
|
{
|
||||||
|
return Task.FromResult(_handlebars);
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_initLock)
|
||||||
|
{
|
||||||
|
_initTask ??= InitializeHandlebarsAsync();
|
||||||
|
return _initTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<IHandlebars> InitializeHandlebarsAsync()
|
||||||
|
{
|
||||||
|
var handlebars = Handlebars.Create();
|
||||||
|
|
||||||
|
var assembly = typeof(HandlebarMailRenderer).Assembly;
|
||||||
|
var layoutSource = await ReadSourceAsync(assembly, "Bit.Core.MailTemplates.Handlebars.Layouts.Full.html.hbs");
|
||||||
|
handlebars.RegisterTemplate("FullHtmlLayout", layoutSource);
|
||||||
|
|
||||||
|
_handlebars = handlebars;
|
||||||
|
return handlebars;
|
||||||
|
}
|
||||||
|
}
|
@ -1,30 +0,0 @@
|
|||||||
namespace Bit.Core.Platform.Services;
|
|
||||||
|
|
||||||
#nullable enable
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// IMail describes a view model for emails. Any propery in the view model are available for usage
|
|
||||||
/// in the email templates.
|
|
||||||
///
|
|
||||||
/// Each Mail consists of two body parts: a text part and an HTML part and the filename must be
|
|
||||||
/// relative to the viewmodel and match the following pattern:
|
|
||||||
/// - `{ClassName}.html.hbs` for the HTML part
|
|
||||||
/// - `{ClassName}.txt.hbs` for the text part
|
|
||||||
/// </summary>
|
|
||||||
public abstract class BaseMailModel2
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The subject of the email.
|
|
||||||
/// </summary>
|
|
||||||
public abstract string Subject { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// An optional category for processing at the upstream email delivery service.
|
|
||||||
/// </summary>
|
|
||||||
public string? Category { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Current year.
|
|
||||||
/// </summary>
|
|
||||||
public string CurrentYear => DateTime.UtcNow.Year.ToString();
|
|
||||||
}
|
|
7
src/Core/Platform/Services/IMailRenderer.cs
Normal file
7
src/Core/Platform/Services/IMailRenderer.cs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
#nullable enable
|
||||||
|
namespace Bit.Core.Platform.Services;
|
||||||
|
|
||||||
|
public interface IMailRenderer
|
||||||
|
{
|
||||||
|
Task<(string html, string? txt)> RenderAsync(BaseMailView model);
|
||||||
|
}
|
@ -8,16 +8,8 @@
|
|||||||
public interface IMailer
|
public interface IMailer
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Sends an email message to the specified recipient.
|
/// Sends an email message.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="message"></param>
|
/// <param name="message"></param>
|
||||||
/// <param name="recipient">Recipient email</param>
|
public Task SendEmail<T>(BaseMail<T> message) where T : BaseMailView;
|
||||||
public Task SendEmail(BaseMailModel2 message, string recipient);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Sends multiple emails message to the specified recipients.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="message"></param>
|
|
||||||
/// <param name="recipients">Recipient emails</param>
|
|
||||||
public void SendEmails(BaseMailModel2 message, string[] recipients);
|
|
||||||
}
|
}
|
||||||
|
@ -1,129 +1,28 @@
|
|||||||
using System.Reflection;
|
using Bit.Core.Models.Mail;
|
||||||
using HandlebarsDotNet;
|
using Bit.Core.Services;
|
||||||
|
|
||||||
namespace Bit.Core.Platform.Services;
|
namespace Bit.Core.Platform.Services;
|
||||||
|
|
||||||
#nullable enable
|
#nullable enable
|
||||||
|
|
||||||
public class Mailer : IMailer
|
public class Mailer(IMailRenderer renderer, IMailDeliveryService mailDeliveryService)
|
||||||
|
: IMailer
|
||||||
{
|
{
|
||||||
private readonly IMailRenderer _renderer;
|
public async Task SendEmail<T>(BaseMail<T> message) where T : BaseMailView
|
||||||
|
|
||||||
public Mailer(IMailRenderer renderer)
|
|
||||||
{
|
{
|
||||||
_renderer = renderer;
|
var content = await renderer.RenderAsync(message.View);
|
||||||
}
|
|
||||||
|
|
||||||
public async Task SendEmail(BaseMailModel2 message, string recipient)
|
var metadata = new Dictionary<string, object>();
|
||||||
|
|
||||||
|
var mailMessage = new MailMessage
|
||||||
{
|
{
|
||||||
var htmlContent = await _renderer.RenderAsync(message);
|
ToEmails = message.ToEmails,
|
||||||
|
Subject = message.Subject,
|
||||||
|
MetaData = metadata,
|
||||||
|
HtmlContent = content.html,
|
||||||
|
TextContent = content.txt,
|
||||||
|
};
|
||||||
|
|
||||||
Console.WriteLine(htmlContent);
|
await mailDeliveryService.SendEmailAsync(mailMessage);
|
||||||
}
|
|
||||||
|
|
||||||
public void SendEmails(BaseMailModel2 message, string[] recipients) => throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface IMailRenderer
|
|
||||||
{
|
|
||||||
Task<(string html, string? txt)> RenderAsync(BaseMailModel2 model);
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum TemplateType
|
|
||||||
{
|
|
||||||
Html,
|
|
||||||
Txt
|
|
||||||
}
|
|
||||||
|
|
||||||
public class HandlebarMailRenderer : IMailRenderer
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// This field holds the handlebars instance.
|
|
||||||
///
|
|
||||||
/// This should never be used directly, rather use the GetHandlebars() method to ensure that it is initialized properly.
|
|
||||||
/// </summary>
|
|
||||||
private IHandlebars? _handlebars;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// This task is used to ensure that the handlebars instance is initialized only once.
|
|
||||||
/// </summary>
|
|
||||||
private Task<IHandlebars>? _initTask;
|
|
||||||
/// <summary>
|
|
||||||
/// This lock is used to ensure that the handlebars instance is initialized only once,
|
|
||||||
/// even if multiple threads call GetHandlebars() at the same time.
|
|
||||||
/// </summary>
|
|
||||||
private readonly object _initLock = new();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// This dictionary is used to cache compiled templates.
|
|
||||||
/// </summary>
|
|
||||||
private readonly Dictionary<string, HandlebarsTemplate<object, object>> _templateCache = new();
|
|
||||||
|
|
||||||
public async Task<(string html, string? txt)> RenderAsync(BaseMailModel2 model)
|
|
||||||
{
|
|
||||||
var handlebars = await GetHandlebars();
|
|
||||||
|
|
||||||
var html = await CompileTemplateAsync(handlebars, model, TemplateType.Html);
|
|
||||||
var txt = await CompileTemplateAsync(handlebars, model, TemplateType.Txt);
|
|
||||||
|
|
||||||
return (html, txt);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<string> CompileTemplateAsync(IHandlebars handlebars, BaseMailModel2 model, TemplateType type)
|
|
||||||
{
|
|
||||||
var templateName = $"{model.GetType().FullName}.{type.ToString().ToLower()}.hbs";
|
|
||||||
|
|
||||||
if (!_templateCache.TryGetValue(templateName, out var template))
|
|
||||||
{
|
|
||||||
var assembly = model.GetType().Assembly;
|
|
||||||
var source = await ReadSourceAsync(assembly, templateName);
|
|
||||||
template = Handlebars.Compile(source);
|
|
||||||
_templateCache.Add(templateName, template);
|
|
||||||
}
|
|
||||||
|
|
||||||
return template(model);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task<string> ReadSourceAsync(Assembly assembly, string template)
|
|
||||||
{
|
|
||||||
if (assembly.GetManifestResourceNames().All(f => f != template))
|
|
||||||
{
|
|
||||||
throw new FileNotFoundException("Template not found: " + template);
|
|
||||||
}
|
|
||||||
|
|
||||||
await using var s = assembly.GetManifestResourceStream(template)!;
|
|
||||||
using var sr = new StreamReader(s);
|
|
||||||
return await sr.ReadToEndAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Helper function that returns the handlebar instance, initializing it if necessary.
|
|
||||||
///
|
|
||||||
/// Protects against initializing the same instance multiple times in parallel.
|
|
||||||
/// </summary>
|
|
||||||
private Task<IHandlebars> GetHandlebars()
|
|
||||||
{
|
|
||||||
if (_handlebars != null)
|
|
||||||
{
|
|
||||||
return Task.FromResult(_handlebars);
|
|
||||||
}
|
|
||||||
|
|
||||||
lock (_initLock)
|
|
||||||
{
|
|
||||||
_initTask ??= InitializeHandlebarsAsync();
|
|
||||||
return _initTask;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<IHandlebars> InitializeHandlebarsAsync()
|
|
||||||
{
|
|
||||||
var handlebars = Handlebars.Create();
|
|
||||||
|
|
||||||
var assembly = typeof(HandlebarMailRenderer).Assembly;
|
|
||||||
var layoutSource = await ReadSourceAsync(assembly, "Bit.Core.MailTemplates.Handlebars.Layouts.Full.html.hbs");
|
|
||||||
handlebars.RegisterTemplate("FullHtmlLayout", layoutSource);
|
|
||||||
|
|
||||||
_handlebars = handlebars;
|
|
||||||
return handlebars;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,6 +28,9 @@
|
|||||||
<None Remove="Utilities\data\embeddedResource.txt" />
|
<None Remove="Utilities\data\embeddedResource.txt" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<!-- Email templates uses .hbs extension, they must be included for emails to work -->
|
||||||
|
<EmbeddedResource Include="**\*.hbs" />
|
||||||
|
|
||||||
<EmbeddedResource Include="Utilities\data\embeddedResource.txt" />
|
<EmbeddedResource Include="Utilities\data\embeddedResource.txt" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
using Bit.Core.Auth.UserFeatures.Registration.VerifyEmail;
|
using Bit.Core.Models.Mail;
|
||||||
using Bit.Core.Platform.Services;
|
using Bit.Core.Platform.Services;
|
||||||
|
using Bit.Core.Services;
|
||||||
|
using Bit.Core.Test.Platform.TestMail;
|
||||||
|
using NSubstitute;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Bit.Core.Test.Platform;
|
namespace Bit.Core.Test.Platform;
|
||||||
@ -9,15 +12,28 @@ public class MailerTest
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task SendEmailAsync()
|
public async Task SendEmailAsync()
|
||||||
{
|
{
|
||||||
var mailer = new Mailer(new HandlebarMailRenderer());
|
var deliveryService = Substitute.For<IMailDeliveryService>();
|
||||||
|
var mailer = new Mailer(new HandlebarMailRenderer(), deliveryService);
|
||||||
|
|
||||||
var mail = new VerifyEmail
|
var mail = new TestMail.TestMail()
|
||||||
{
|
{
|
||||||
Token = "test-token",
|
ToEmails = ["test@bw.com"],
|
||||||
Email = "test@bitwarden.com",
|
View = new TestMailView() { Token = "", Email = "", WebVaultUrl = "" }
|
||||||
WebVaultUrl = "https://vault.bitwarden.com"
|
|
||||||
};
|
};
|
||||||
|
|
||||||
await mailer.SendEmail(mail, "test@bitwarden.com");
|
MailMessage? sentMessage = null;
|
||||||
|
await deliveryService.SendEmailAsync(Arg.Do<MailMessage>(message =>
|
||||||
|
sentMessage = message
|
||||||
|
));
|
||||||
|
|
||||||
|
await mailer.SendEmail(mail);
|
||||||
|
|
||||||
|
Assert.NotNull(sentMessage);
|
||||||
|
Assert.Contains("test@bw.com", sentMessage.ToEmails);
|
||||||
|
Assert.Equal("Test Email", sentMessage.Subject);
|
||||||
|
Assert.Equivalent("Test Email\n\n/redirect-connector.html#finish-signup?token=&email=&fromEmail=true\n",
|
||||||
|
sentMessage.TextContent);
|
||||||
|
Assert.Equivalent("Test <b>Email</b>\n\n<a href=\"/redirect-connector.html#finish-signup?token=&email=&fromEmail=true\">Test</a>\n",
|
||||||
|
sentMessage.HtmlContent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,24 +1,24 @@
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
using Bit.Core.Platform.Services;
|
using Bit.Core.Platform.Services;
|
||||||
|
|
||||||
namespace Bit.Core.Auth.UserFeatures.Registration.VerifyEmail;
|
namespace Bit.Core.Test.Platform.TestMail;
|
||||||
|
|
||||||
public class VerifyEmail() : BaseMailModel2
|
public class TestMailView : BaseMailView
|
||||||
{
|
{
|
||||||
public override string Subject { get; set; } = "Verify Your Email";
|
|
||||||
|
|
||||||
public required string Token { get; init; }
|
public required string Token { get; init; }
|
||||||
public required string Email { get; init; }
|
public required string Email { get; init; }
|
||||||
public required string WebVaultUrl { get; init; }
|
public required string WebVaultUrl { get; init; }
|
||||||
|
|
||||||
public string Url
|
public string Url =>
|
||||||
{
|
string.Format(
|
||||||
get => string.Format(
|
|
||||||
"{0}/redirect-connector.html#finish-signup?token={1}&email={2}&fromEmail=true",
|
"{0}/redirect-connector.html#finish-signup?token={1}&email={2}&fromEmail=true",
|
||||||
WebVaultUrl,
|
WebVaultUrl,
|
||||||
WebUtility.UrlEncode(Token),
|
WebUtility.UrlEncode(Token),
|
||||||
WebUtility.UrlEncode(Email)
|
WebUtility.UrlEncode(Email)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class TestMail : BaseMail<TestMailView>
|
||||||
|
{
|
||||||
|
public override string Subject { get; } = "Test Email";
|
||||||
}
|
}
|
3
test/Core.Test/Platform/TestMail/TestMailView.html.hbs
Normal file
3
test/Core.Test/Platform/TestMail/TestMailView.html.hbs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
Test <b>Email</b>
|
||||||
|
|
||||||
|
<a href="{{{Url}}}">Test</a>
|
3
test/Core.Test/Platform/TestMail/TestMailView.txt.hbs
Normal file
3
test/Core.Test/Platform/TestMail/TestMailView.txt.hbs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
Test Email
|
||||||
|
|
||||||
|
{{{Url}}}
|
Loading…
x
Reference in New Issue
Block a user