Add invite and confirmation emails

This commit is contained in:
Nicolas Dorier
2025-11-07 13:09:58 +09:00
parent 88d3e7ad55
commit d7fcd55707
17 changed files with 183 additions and 136 deletions

View File

@@ -220,7 +220,7 @@ namespace BTCPayServer.Controllers
} }
var callbackUrl = await _callbackGenerator.ForEmailConfirmation(user, Request); var callbackUrl = await _callbackGenerator.ForEmailConfirmation(user, Request);
(await _EmailSenderFactory.GetEmailSender()).SendEmailConfirmation(user.GetMailboxAddress(), callbackUrl); _eventAggregator.Publish(new UserEvent.ConfirmationEmailRequested(user, callbackUrl));
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Verification email sent. Please check your email."].Value; TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Verification email sent. Please check your email."].Value;
return RedirectToAction(nameof(Index)); return RedirectToAction(nameof(Index));
} }

View File

@@ -405,9 +405,7 @@ namespace BTCPayServer.Controllers
} }
var callbackUrl = await _callbackGenerator.ForEmailConfirmation(user, Request); var callbackUrl = await _callbackGenerator.ForEmailConfirmation(user, Request);
_eventAggregator.Publish(new UserEvent.ConfirmationEmailRequested(user, callbackUrl));
(await _emailSenderFactory.GetEmailSender()).SendEmailConfirmation(user.GetMailboxAddress(), callbackUrl);
TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Verification email sent"].Value; TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Verification email sent"].Value;
return RedirectToAction(nameof(ListUsers)); return RedirectToAction(nameof(ListUsers));
} }

View File

@@ -24,6 +24,12 @@ public class UserEvent(ApplicationUser user)
{ {
public string ResetLink { get; } = resetLink; public string ResetLink { get; } = resetLink;
} }
public class ConfirmationEmailRequested(ApplicationUser user, string confirmLink) : UserEvent(user)
{
public string ConfirmLink { get; } = confirmLink;
}
public class Registered(ApplicationUser user, string approvalLink, string confirmationEmail) : UserEvent(user) public class Registered(ApplicationUser user, string approvalLink, string confirmationEmail) : UserEvent(user)
{ {
public string ApprovalLink { get; } = approvalLink; public string ApprovalLink { get; } = approvalLink;

View File

@@ -9,56 +9,16 @@ namespace BTCPayServer.Services
{ {
private static string BODY_STYLE = "font-family: Open Sans, Helvetica Neue,Arial,sans-serif; font-color: #292929;"; private static string BODY_STYLE = "font-family: Open Sans, Helvetica Neue,Arial,sans-serif; font-color: #292929;";
private static string HEADER_HTML = "<h1 style='font-size:1.2rem'>BTCPay Server</h1><br/>"; private static string HEADER_HTML = "<h1 style='font-size:1.2rem'>BTCPay Server</h1><br/>";
private static string BUTTON_HTML = "<a href='{button_link}' type='submit' style='min-width: 2em;min-height: 20px;text-decoration-line: none;cursor: pointer;display: inline-block;font-weight: 400;color: #fff;text-align: center;vertical-align: middle;user-select: none;background-color: #51b13e;border-color: #51b13e;border: 1px solid transparent;padding: 0.375rem 0.75rem;font-size: 1rem;line-height: 1.5;border-radius: 0.25rem;transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;'>{button_description}</a>";
private static string CallToAction(string actionName, string actionLink)
{
var button = $"{BUTTON_HTML}".Replace("{button_description}", actionName, System.StringComparison.InvariantCulture);
return button.Replace("{button_link}", HtmlEncoder.Default.Encode(actionLink), System.StringComparison.InvariantCulture);
}
private static string CreateEmailBody(string body) private static string CreateEmailBody(string body)
{ {
return $"<html><body style='{BODY_STYLE}'>{HEADER_HTML}{body}</body></html>"; return $"<html><body style='{BODY_STYLE}'>{HEADER_HTML}{body}</body></html>";
} }
public static void SendEmailConfirmation(this IEmailSender emailSender, MailboxAddress address, string link)
{
emailSender.SendEmail(address, "Confirm your email", CreateEmailBody(
$"Please confirm your account.<br/><br/>{CallToAction("Confirm Email", link)}"));
}
public static void SendApprovalConfirmation(this IEmailSender emailSender, MailboxAddress address, string link)
{
emailSender.SendEmail(address, "Your account has been approved", CreateEmailBody(
$"Your account has been approved and you can now <a href='{HtmlEncoder.Default.Encode(link)}'>login here</a>."));
}
public static void SendInvitation(this IEmailSender emailSender, MailboxAddress address, string link)
{
emailSender.SendEmail(address, "Invitation", CreateEmailBody(
$"<p>Please complete your account setup by clicking <a href='{HtmlEncoder.Default.Encode(link)}'>this link</a>.</p><p>You can also use the BTCPay Server app and scan this QR code when connecting:</p>{GetQrCodeImg(link)}"));
}
public static void SendNewUserInfo(this IEmailSender emailSender, MailboxAddress address, string newUserInfo, string link)
{
emailSender.SendEmail(address, newUserInfo, CreateEmailBody(
$"{newUserInfo}. You can verify and approve the account here: <a href='{HtmlEncoder.Default.Encode(link)}'>User details</a>"));
}
public static void SendUserInviteAcceptedInfo(this IEmailSender emailSender, MailboxAddress address, string userInfo, string link) public static void SendUserInviteAcceptedInfo(this IEmailSender emailSender, MailboxAddress address, string userInfo, string link)
{ {
emailSender.SendEmail(address, userInfo, CreateEmailBody( emailSender.SendEmail(address, userInfo, CreateEmailBody(
$"{userInfo}. You can view the store users here: <a href='{HtmlEncoder.Default.Encode(link)}'>Store users</a>")); $"{userInfo}. You can view the store users here: <a href='{HtmlEncoder.Default.Encode(link)}'>Store users</a>"));
} }
private static string GetQrCodeImg(string data)
{
using var qrGenerator = new QRCodeGenerator();
using var qrCodeData = qrGenerator.CreateQrCode(data, QRCodeGenerator.ECCLevel.Q);
using var qrCode = new Base64QRCode(qrCodeData);
var base64 = qrCode.GetGraphic(20);
return $"<img src='data:image/png;base64,{base64}' alt='{data}' width='320' height='320'/>";
}
} }
} }

View File

@@ -436,7 +436,6 @@ o.GetRequiredService<IEnumerable<IPaymentLinkExtension>>().ToDictionary(o => o.P
services.AddSingleton<IHostedService, AppInventoryUpdaterHostedService>(); services.AddSingleton<IHostedService, AppInventoryUpdaterHostedService>();
services.AddSingleton<IHostedService, TransactionLabelMarkerHostedService>(); services.AddSingleton<IHostedService, TransactionLabelMarkerHostedService>();
services.AddSingleton<IHostedService, OnChainRateTrackerHostedService>(); services.AddSingleton<IHostedService, OnChainRateTrackerHostedService>();
services.AddSingleton<IHostedService, UserEventHostedService>();
services.AddSingleton<IHostedService, DynamicDnsHostedService>(); services.AddSingleton<IHostedService, DynamicDnsHostedService>();
services.AddSingleton<PaymentRequestStreamer>(); services.AddSingleton<PaymentRequestStreamer>();
services.AddSingleton<IHostedService>(s => s.GetRequiredService<PaymentRequestStreamer>()); services.AddSingleton<IHostedService>(s => s.GetRequiredService<PaymentRequestStreamer>());

View File

@@ -1,5 +1,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using BTCPayServer.Abstractions.Models; using BTCPayServer.Abstractions.Models;
using BTCPayServer.Plugins.Emails.HostedServices;
using BTCPayServer.Plugins.Emails.Views; using BTCPayServer.Plugins.Emails.Views;
using BTCPayServer.Plugins.Webhooks; using BTCPayServer.Plugins.Webhooks;
using BTCPayServer.Services; using BTCPayServer.Services;
@@ -19,6 +20,7 @@ public class EmailsPlugin : BaseBTCPayServerPlugin
{ {
services.AddSingleton<IDefaultTranslationProvider, EmailsTranslationProvider>(); services.AddSingleton<IDefaultTranslationProvider, EmailsTranslationProvider>();
services.AddSingleton<IHostedService, StoreEmailRuleProcessorSender>(); services.AddSingleton<IHostedService, StoreEmailRuleProcessorSender>();
services.AddSingleton<IHostedService, UserEventHostedService>();
RegisterServerEmailTriggers(services); RegisterServerEmailTriggers(services);
} }
private static string BODY_STYLE = "font-family: Open Sans, Helvetica Neue,Arial,sans-serif; font-color: #292929;"; private static string BODY_STYLE = "font-family: Open Sans, Helvetica Neue,Arial,sans-serif; font-color: #292929;";
@@ -50,8 +52,67 @@ public class EmailsPlugin : BaseBTCPayServerPlugin
Description = "Password Reset Requested", Description = "Password Reset Requested",
}; };
vms.Add(vm); vms.Add(vm);
vm = new EmailTriggerViewModel()
{
Trigger = ServerMailTriggers.EmailConfirm,
RecipientExample = "{User.MailboxAddress}",
SubjectExample = "Confirm your email",
BodyExample = CreateEmailBody($"Please confirm your account.<br/><br/>{CallToAction("Confirm Email", "{ConfirmLink}")}"),
PlaceHolders = new()
{
new ("{ConfirmLink}", "The link used to confirm the user's email address")
},
Description = "Confirm new user's email",
};
vms.Add(vm);
vm = new EmailTriggerViewModel()
{
Trigger = ServerMailTriggers.InvitePending,
RecipientExample = "{User.MailboxAddress}",
SubjectExample = "Invitation to join {Branding.ServerName}",
BodyExample = CreateEmailBody($"<p>Please complete your account setup by clicking <a href='{{InvitationLink}}'>this link</a>.</p><p>You can also use the BTCPay Server app and scan this QR code when connecting:</p>{{InvitationLinkQR}}"),
PlaceHolders = new()
{
new ("{InvitationLink}", "The link where the invited user can set up their account"),
new ("{InvitationLinkQR}", "The QR code representation of the invitation link")
},
Description = "User invitation email",
};
vms.Add(vm);
vm = new EmailTriggerViewModel()
{
Trigger = ServerMailTriggers.ApprovalConfirmed,
RecipientExample = "{User.MailboxAddress}",
SubjectExample = "Your account has been approved",
BodyExample = CreateEmailBody($"Your account has been approved and you can now.<br/><br/>{CallToAction("Login here", "{LoginLink}")}"),
PlaceHolders = new()
{
new ("{LoginLink}", "The link that the user can use to login"),
},
Description = "User account approved",
};
vms.Add(vm);
vm = new EmailTriggerViewModel()
{
Trigger = ServerMailTriggers.ApprovalRequest,
RecipientExample = "{Admin.MailboxAddresses}",
SubjectExample = "Approval request to access the server for {User.Email}",
BodyExample = CreateEmailBody($"A new user ({{User.MailboxAddress}}), is awaiting approval to access the server.<br/><br/>{CallToAction("Approve", "{ApprovalLink}")}"),
PlaceHolders = new()
{
new ("{ApprovalLink}", "The link that the admin needs to use to approve the user"),
},
Description = "Approval request to administrators",
};
vms.Add(vm);
var commonPlaceholders = new List<EmailTriggerViewModel.PlaceHolder>() var commonPlaceholders = new List<EmailTriggerViewModel.PlaceHolder>()
{ {
new("{Admins.MailboxAddresses}", "The email addresses of the admins separated by a comma (eg. ,)"),
new("{User.Name}", "The name of the user (eg. John Doe)"), new("{User.Name}", "The name of the user (eg. John Doe)"),
new("{User.Email}", "The email of the user (eg. john.doe@example.com)"), new("{User.Email}", "The email of the user (eg. john.doe@example.com)"),
new("{User.MailboxAddress}", "The formatted mailbox address to use when sending an email. (eg. \"John Doe\" <john.doe@example.com>)"), new("{User.MailboxAddress}", "The formatted mailbox address to use when sending an email. (eg. \"John Doe\" <john.doe@example.com>)"),

View File

@@ -1,6 +1,8 @@
#nullable enable #nullable enable
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Net.Mail;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using BTCPayServer.Data; using BTCPayServer.Data;
@@ -10,7 +12,7 @@ using Microsoft.Extensions.Logging;
using MimeKit; using MimeKit;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
namespace BTCPayServer.Plugins.Emails; namespace BTCPayServer.Plugins.Emails.HostedServices;
public interface ITriggerOwner public interface ITriggerOwner
{ {
@@ -65,13 +67,29 @@ public class StoreEmailRuleProcessorSender(
var subject = new TextTemplate(actionableRule.Subject ?? ""); var subject = new TextTemplate(actionableRule.Subject ?? "");
matchedContext.Recipients.AddRange( matchedContext.Recipients.AddRange(
actionableRule.To actionableRule.To
.Select(o => .SelectMany(o =>
{ {
if (!MailboxAddressValidator.TryParse(o, out var oo)) if (MailboxAddressValidator.TryParse(o, out var oo))
return new[] { oo };
var emails = new TextTemplate(o).Render(triggEvent.Model);
MailAddressCollection mailCollection = new();
try
{ {
MailboxAddressValidator.TryParse(new TextTemplate(o).Render(triggEvent.Model), out oo); mailCollection.Add(emails);
} }
return oo; catch (FormatException)
{
return Array.Empty<MailboxAddress>();
}
return mailCollection.Select(a =>
{
MailboxAddressValidator.TryParse(a.ToString(), out oo);
return oo;
})
.Where(a => a != null)
.ToArray();
}) })
.Where(o => o != null)!); .Where(o => o != null)!);

View File

@@ -1,10 +1,12 @@
#nullable enable
using System.Linq;
using System.Text.Encodings.Web; using System.Text.Encodings.Web;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using BTCPayServer.Data; using BTCPayServer.Data;
using BTCPayServer.Events; using BTCPayServer.Events;
using BTCPayServer.HostedServices;
using BTCPayServer.Logging; using BTCPayServer.Logging;
using BTCPayServer.Plugins.Emails;
using BTCPayServer.Services; using BTCPayServer.Services;
using BTCPayServer.Services.Mails; using BTCPayServer.Services.Mails;
using BTCPayServer.Services.Notifications; using BTCPayServer.Services.Notifications;
@@ -13,13 +15,13 @@ using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using QRCoder;
namespace BTCPayServer.HostedServices; namespace BTCPayServer.Plugins.Emails.HostedServices;
public class UserEventHostedService( public class UserEventHostedService(
EventAggregator eventAggregator, EventAggregator eventAggregator,
UserManager<ApplicationUser> userManager, UserManager<ApplicationUser> userManager,
CallbackGenerator callbackGenerator,
ISettingsAccessor<ServerSettings> serverSettings, ISettingsAccessor<ServerSettings> serverSettings,
EmailSenderFactory emailSenderFactory, EmailSenderFactory emailSenderFactory,
NotificationSender notificationSender, NotificationSender notificationSender,
@@ -28,62 +30,64 @@ public class UserEventHostedService(
: EventHostedServiceBase(eventAggregator, logs) : EventHostedServiceBase(eventAggregator, logs)
{ {
public UserManager<ApplicationUser> UserManager { get; } = userManager; public UserManager<ApplicationUser> UserManager { get; } = userManager;
public CallbackGenerator CallbackGenerator { get; } = callbackGenerator;
protected override void SubscribeToEvents() protected override void SubscribeToEvents()
{ {
Subscribe<UserEvent.Registered>(); SubscribeAny<UserEvent>();
Subscribe<UserEvent.Invited>(); }
Subscribe<UserEvent.Approved>();
Subscribe<UserEvent.ConfirmedEmail>(); public static string GetQrCodeImg(string data)
Subscribe<UserEvent.PasswordResetRequested>(); {
Subscribe<UserEvent.InviteAccepted>(); using var qrGenerator = new QRCodeGenerator();
using var qrCodeData = qrGenerator.CreateQrCode(data, QRCodeGenerator.ECCLevel.Q);
using var qrCode = new Base64QRCode(qrCodeData);
var base64 = qrCode.GetGraphic(20);
return $"<img src='data:image/png;base64,{base64}' alt='{data}' width='320' height='320'/>";
} }
protected override async Task ProcessEvent(object evt, CancellationToken cancellationToken) protected override async Task ProcessEvent(object evt, CancellationToken cancellationToken)
{ {
ApplicationUser user = (evt as UserEvent).User; var user = (evt as UserEvent)?.User;
IEmailSender emailSender; if (user is null) return;
switch (evt) switch (evt)
{ {
case UserEvent.Registered ev: case UserEvent.Registered ev:
// can be either a self-registration or by invite from another user var requiresApproval = user is { RequiresApproval: true, Approved: false };
var type = await UserManager.IsInRoleAsync(user, Roles.ServerAdmin) ? "admin" : "user"; var requiresEmailConfirmation = user is { RequiresEmailConfirmation: true, EmailConfirmed: false };
var info = ev switch
{
UserEvent.Invited { InvitedByUser: { } invitedBy } => $"invited by {invitedBy.Email}",
UserEvent.Invited => "invited",
_ => "registered"
};
var requiresApproval = user.RequiresApproval && !user.Approved;
var requiresEmailConfirmation = user.RequiresEmailConfirmation && !user.EmailConfirmed;
// log registration info
var newUserInfo = $"New {type} {user.Email} {info}";
Logs.PayServer.LogInformation(newUserInfo);
// send notification if the user does not require email confirmation. // send notification if the user does not require email confirmation.
// inform admins only about qualified users and not annoy them with bot registrations. // inform admins only about qualified users and not annoy them with bot registrations.
if (requiresApproval && !requiresEmailConfirmation) if (requiresApproval && !requiresEmailConfirmation)
{ {
await NotifyAdminsAboutUserRequiringApproval(user, ev.ApprovalLink, newUserInfo); await NotifyAdminsAboutUserRequiringApproval(user, ev.ApprovalLink);
} }
// set callback result and send email to user // set callback result and send email to user
emailSender = await emailSenderFactory.GetEmailSender();
if (ev is UserEvent.Invited invited) if (ev is UserEvent.Invited invited)
{ {
if (invited.SendInvitationEmail) if (invited.SendInvitationEmail)
emailSender.SendInvitation(user.GetMailboxAddress(), invited.InvitationLink); EventAggregator.Publish(await CreateTriggerEvent(ServerMailTriggers.InvitePending,
new JObject()
{
["InvitationLink"] = HtmlEncoder.Default.Encode(invited.InvitationLink),
["InvitationLinkQR"] = GetQrCodeImg(invited.InvitationLink)
}, user));
} }
else if (requiresEmailConfirmation) else if (requiresEmailConfirmation)
{ {
emailSender.SendEmailConfirmation(user.GetMailboxAddress(), ev.ConfirmationEmailLink); EventAggregator.Publish(new UserEvent.ConfirmationEmailRequested(user, ev.ConfirmationEmailLink));
} }
break; break;
case UserEvent.ConfirmationEmailRequested confReq:
EventAggregator.Publish(await CreateTriggerEvent(ServerMailTriggers.EmailConfirm,
new JObject()
{
["ConfirmLink"] = HtmlEncoder.Default.Encode(confReq.ConfirmLink)
}, user));
break;
case UserEvent.PasswordResetRequested pwResetEvent: case UserEvent.PasswordResetRequested pwResetEvent:
EventAggregator.Publish(CreateTriggerEvent(ServerMailTriggers.PasswordReset, EventAggregator.Publish(await CreateTriggerEvent(ServerMailTriggers.PasswordReset,
new JObject() new JObject()
{ {
["ResetLink"] = HtmlEncoder.Default.Encode(pwResetEvent.ResetLink) ["ResetLink"] = HtmlEncoder.Default.Encode(pwResetEvent.ResetLink)
@@ -92,15 +96,16 @@ public class UserEventHostedService(
case UserEvent.Approved approvedEvent: case UserEvent.Approved approvedEvent:
if (!user.Approved) break; if (!user.Approved) break;
emailSender = await emailSenderFactory.GetEmailSender(); EventAggregator.Publish(await CreateTriggerEvent(ServerMailTriggers.ApprovalConfirmed,
emailSender.SendApprovalConfirmation(user.GetMailboxAddress(), approvedEvent.LoginLink); new JObject()
{
["LoginLink"] = approvedEvent.LoginLink
}, user));
break; break;
case UserEvent.ConfirmedEmail confirmedEvent: case UserEvent.ConfirmedEmail confirmedEvent when user is { RequiresApproval: true, Approved: false, EmailConfirmed: true }:
if (!user.EmailConfirmed) break; await NotifyAdminsAboutUserRequiringApproval(user, confirmedEvent.ApprovalLink);
var confirmedUserInfo = $"User {user.Email} confirmed their email address";
Logs.PayServer.LogInformation(confirmedUserInfo);
await NotifyAdminsAboutUserRequiringApproval(user, confirmedEvent.ApprovalLink, confirmedUserInfo);
break; break;
case UserEvent.InviteAccepted inviteAcceptedEvent: case UserEvent.InviteAccepted inviteAcceptedEvent:
@@ -110,8 +115,24 @@ public class UserEventHostedService(
} }
} }
private TriggerEvent CreateTriggerEvent(string trigger, JObject model, ApplicationUser user) private async Task NotifyAdminsAboutUserRequiringApproval(ApplicationUser user, string approvalLink)
{ {
await notificationSender.SendNotification(new AdminScope(), new NewUserRequiresApprovalNotification(user));
EventAggregator.Publish(await CreateTriggerEvent(ServerMailTriggers.ApprovalRequest,
new JObject()
{
["ApprovalLink"] = approvalLink
}, user));
}
private async Task<TriggerEvent> CreateTriggerEvent(string trigger, JObject model, ApplicationUser user)
{
var admins = await UserManager.GetUsersInRoleAsync(Roles.ServerAdmin);
var adminMailboxes = string.Join(", ", admins.Select(a => a.GetMailboxAddress().ToString()).ToArray());
model["Admins"] = new JObject()
{
["MailboxAddresses"] = adminMailboxes,
};
model["User"] = new JObject() model["User"] = new JObject()
{ {
["Name"] = user.UserName, ["Name"] = user.UserName,
@@ -126,21 +147,6 @@ public class UserEventHostedService(
var evt = new TriggerEvent(null, trigger, model, null); var evt = new TriggerEvent(null, trigger, model, null);
return evt; return evt;
} }
private async Task NotifyAdminsAboutUserRequiringApproval(ApplicationUser user, string approvalLink, string newUserInfo)
{
if (!user.RequiresApproval || user.Approved) return;
// notification
await notificationSender.SendNotification(new AdminScope(), new NewUserRequiresApprovalNotification(user));
// email
var admins = await UserManager.GetUsersInRoleAsync(Roles.ServerAdmin);
var emailSender = await emailSenderFactory.GetEmailSender();
foreach (var admin in admins)
{
emailSender.SendNewUserInfo(admin.GetMailboxAddress(), newUserInfo, approvalLink);
}
}
private async Task NotifyAboutUserAcceptingInvite(ApplicationUser user, string storeUsersLink) private async Task NotifyAboutUserAcceptingInvite(ApplicationUser user, string storeUsersLink)
{ {
var stores = await storeRepository.GetStoresByUserId(user.Id); var stores = await storeRepository.GetStoresByUserId(user.Id);

View File

@@ -8,4 +8,5 @@ public class ServerMailTriggers
public const string ApprovalConfirmed = "SRV-ApprovalConfirmed"; public const string ApprovalConfirmed = "SRV-ApprovalConfirmed";
public const string ApprovalPending = "SRV-ApprovalPending"; public const string ApprovalPending = "SRV-ApprovalPending";
public const string EmailConfirm = "SRV-EmailConfirmation"; public const string EmailConfirm = "SRV-EmailConfirmation";
public const string ApprovalRequest = "SRV-ApprovalRequest";
} }

View File

@@ -7,11 +7,18 @@ namespace BTCPayServer.Plugins.Emails.Views;
/// </summary> /// </summary>
public class EmailTriggerViewModel public class EmailTriggerViewModel
{ {
public class Default
{
public string SubjectExample { get; set; }
public string BodyExample { get; set; }
public bool CanIncludeCustomerEmail { get; set; }
public string RecipientExample { get; set; }
}
public string Trigger { get; set; } public string Trigger { get; set; }
public string Description { get; set; } public string Description { get; set; }
public string SubjectExample { get; set; }
public string BodyExample { get; set; }
public bool CanIncludeCustomerEmail { get; set; }
public class PlaceHolder(string name, string description) public class PlaceHolder(string name, string description)
{ {
@@ -21,5 +28,4 @@ public class EmailTriggerViewModel
public List<PlaceHolder> PlaceHolders { get; set; } = new(); public List<PlaceHolder> PlaceHolders { get; set; } = new();
public bool ServerTrigger { get; set; } public bool ServerTrigger { get; set; }
public string RecipientExample { get; set; }
} }

View File

@@ -152,31 +152,19 @@
const bodyTextarea = document.querySelector('.email-rule-body'); const bodyTextarea = document.querySelector('.email-rule-body');
const placeholdersTd = document.querySelector('#placeholders'); const placeholdersTd = document.querySelector('#placeholders');
const isEmptyOrDefault = (value, type) => {
const val = value.replace(/<.*?>/gi, '').trim();
if (!val) return true;
return Object.values(triggersByType).some(t => t[type] === val);
};
function applyTemplate() { function applyTemplate() {
const selectedTrigger = triggerSelect.value; const selectedTrigger = triggerSelect.value;
console.log(selectedTrigger);
if (triggersByType[selectedTrigger]) { if (triggersByType[selectedTrigger]) {
if (isEmptyOrDefault(subjectInput.value, 'subjectExample')) { subjectInput.value = triggersByType[selectedTrigger].subjectExample;
subjectInput.value = triggersByType[selectedTrigger].subjectExample; recipientInput.value = triggersByType[selectedTrigger].recipientExample;
}
if (isEmptyOrDefault(recipientInput.value, 'recipientExample')) {
recipientInput.value = triggersByType[selectedTrigger].recipientExample;
}
var body = triggersByType[selectedTrigger].bodyExample; var body = triggersByType[selectedTrigger].bodyExample;
if (isEmptyOrDefault(bodyTextarea.value, 'bodyExample')) { if ($(bodyTextarea).summernote) {
if ($(bodyTextarea).summernote) { console.log(body);
$(bodyTextarea).summernote('reset'); $(bodyTextarea).summernote('reset');
$(bodyTextarea).summernote('code', body.replace(/\n/g, '<br/>')); $(bodyTextarea).summernote('code', body.replace(/\n/g, '<br/>'));
} else { } else {
bodyTextarea.value = body; bodyTextarea.value = body;
}
} }
placeholdersTd.innerHTML = ''; placeholdersTd.innerHTML = '';
@@ -213,7 +201,10 @@
// Apply template on page load if a trigger is selected // Apply template on page load if a trigger is selected
if (triggerSelect.value) { if (triggerSelect.value) {
applyTemplate(); if (@Safe.Json(!isEdit))
{
applyTemplate();
}
toggleCustomerEmailVisibility(); toggleCustomerEmailVisibility();
} }
}); });

View File

@@ -7,6 +7,7 @@ using BTCPayServer.Client.Models;
using BTCPayServer.Data; using BTCPayServer.Data;
using BTCPayServer.HostedServices; using BTCPayServer.HostedServices;
using BTCPayServer.Plugins.Emails; using BTCPayServer.Plugins.Emails;
using BTCPayServer.Plugins.Emails.HostedServices;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Newtonsoft.Json; using Newtonsoft.Json;
using StoreData = BTCPayServer.Data.StoreData; using StoreData = BTCPayServer.Data.StoreData;

View File

@@ -4,6 +4,7 @@ using BTCPayServer.Client.Models;
using BTCPayServer.Controllers.Greenfield; using BTCPayServer.Controllers.Greenfield;
using BTCPayServer.Events; using BTCPayServer.Events;
using BTCPayServer.Plugins.Emails; using BTCPayServer.Plugins.Emails;
using BTCPayServer.Plugins.Emails.HostedServices;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;

View File

@@ -6,6 +6,7 @@ using BTCPayServer.Client.Models;
using BTCPayServer.Data; using BTCPayServer.Data;
using BTCPayServer.HostedServices; using BTCPayServer.HostedServices;
using BTCPayServer.Plugins.Emails; using BTCPayServer.Plugins.Emails;
using BTCPayServer.Plugins.Emails.HostedServices;
using BTCPayServer.Services.PaymentRequests; using BTCPayServer.Services.PaymentRequests;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing;

View File

@@ -3,6 +3,7 @@ using System.Threading.Tasks;
using BTCPayServer.Client.Models; using BTCPayServer.Client.Models;
using BTCPayServer.HostedServices; using BTCPayServer.HostedServices;
using BTCPayServer.Plugins.Emails; using BTCPayServer.Plugins.Emails;
using BTCPayServer.Plugins.Emails.HostedServices;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using StoreData = BTCPayServer.Data.StoreData; using StoreData = BTCPayServer.Data.StoreData;

View File

@@ -48,10 +48,7 @@ namespace BTCPayServer.Services.Notifications
} }
await db.SaveChangesAsync(); await db.SaveChangesAsync();
} }
foreach (string user in users) _notificationManager.InvalidateNotificationCache(users);
{
_notificationManager.InvalidateNotificationCache(user);
}
} }
public BaseNotification GetBaseNotification(NotificationData notificationData) public BaseNotification GetBaseNotification(NotificationData notificationData)

View File

@@ -8,7 +8,7 @@ using MimeKit;
namespace BTCPayServer namespace BTCPayServer
{ {
/// <summary> /// <summary>
/// Validate address in the format "Firstname Lastname <blah@example.com>" See rfc822 /// Validate address in the format "Firstname Lastname <blah@example.com>" See rfc5322
/// </summary> /// </summary>
public class MailboxAddressValidator public class MailboxAddressValidator
{ {
@@ -25,7 +25,7 @@ namespace BTCPayServer
public static MailboxAddress Parse(string? str) public static MailboxAddress Parse(string? str)
{ {
if (!TryParse(str, out var mb)) if (!TryParse(str, out var mb))
throw new FormatException("Invalid mailbox address (rfc822)"); throw new FormatException("Invalid mailbox address (rfc5322)");
return mb; return mb;
} }
public static bool TryParse(string? str, [MaybeNullWhen(false)] out MailboxAddress mailboxAddress) public static bool TryParse(string? str, [MaybeNullWhen(false)] out MailboxAddress mailboxAddress)