using System.Collections.Generic; using BTCPayServer.Abstractions.Models; using BTCPayServer.Client; using BTCPayServer.Data; using BTCPayServer.Plugins.Emails.Controllers; using BTCPayServer.Plugins.Emails.HostedServices; using BTCPayServer.Plugins.Emails.Views; using BTCPayServer.Plugins.GlobalSearch; using BTCPayServer.Services; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; namespace BTCPayServer.Plugins.Emails; public class EmailsPlugin : BaseBTCPayServerPlugin { public const string Area = "Emails"; public override string Identifier => "BTCPayServer.Plugins.Emails"; public override string Name => "Emails"; public override string Description => "Allows you to send emails to your customers!"; public override void Execute(IServiceCollection services) { services.AddSingleton(); services.AddSingleton(); services.AddTransient(); services.AddSingleton(); services.AddMigration(); services.AddMigration("20251223_emailsettingsmigration", """ INSERT INTO "Settings" ("Id", "Value") SELECT 'BTCPayServer.Plugins.Emails.Services.EmailSettings', "Value" FROM "Settings" WHERE "Id" = 'BTCPayServer.Services.Mails.EmailSettings' ON CONFLICT ("Id") DO NOTHING; DELETE FROM "Settings" WHERE "Id" = 'BTCPayServer.Services.Mails.EmailSettings'; """); services.AddSingleton(); services.AddSingleton(); services.AddDefaultTranslations(ServerTransformer.TranslatedStrings); services.AddSingleton(); services.AddSingleton(); services.AddDefaultTranslations(StoreTransformer.TranslatedStrings); ConfigureEmailSearch(services); RegisterServerEmailTriggers(services); } private static void ConfigureEmailSearch(IServiceCollection services) { services.AddStaticSearch(new ActionResultItemViewModel() { RequiredPolicy = Policies.CanViewStoreSettings, Title = "View store's email rules", Action = nameof(UIStoreEmailRulesController.StoreEmailRulesList), Controller = "UIStoreEmailRules", Values = ctx => new { area = Area, storeId = ctx.Store!.Id }, Category = "Store", Keywords = ["Email", "Rules"] }); services.AddStaticSearch(new ActionResultItemViewModel() { RequiredPolicy = Policies.CanModifyServerSettings, Title = "View server's email rules", Action = nameof(UIServerEmailRulesController.ServerEmailRulesList), Controller = "UIServerEmailRules", Values = ctx => new { area = Area }, Category = "Server", Keywords = ["Email", "Rules"] }); services.AddStaticSearch(new ActionResultItemViewModel() { RequiredPolicy = Policies.CanViewStoreSettings, Title = "Configure store's email settings", Action = nameof(UIStoresEmailController.StoreEmailSettings), Controller = "UIStoresEmail", Values = ctx => new { area = Area, storeId = ctx.Store!.Id }, Category = "Store", Keywords = ["Email", "Settings"] }); services.AddStaticSearch(new ActionResultItemViewModel() { RequiredPolicy = Policies.CanModifyServerSettings, Title = "Configure server's email settings", Action = nameof(UIServerEmailController.ServerEmailSettings), Controller = "UIServerEmail", Values = ctx => new { area = Area }, Category = "Server", Keywords = ["Email", "Settings"] }); } private static string BODY_STYLE = "font-family: Open Sans, Helvetica Neue,Arial,sans-serif; font-color: #292929;"; private static string HEADER_HTML = "

{Server.Name}


"; private static string BUTTON_HTML = "{button_description}"; public static string CallToAction(string actionName, string actionLink) { var button = $"{BUTTON_HTML}".Replace("{button_description}", actionName, System.StringComparison.InvariantCulture); return button.Replace("{button_link}", actionLink, System.StringComparison.InvariantCulture); } public static string CreateEmailBody(string body) => $"{HEADER_HTML}{body}"; public static string CreateEmail(string body, string actionName = null, string actionLink = null) { if (actionName is null || actionLink is null) return CreateEmailBody(body); return CreateEmailBody($"{body}

{CallToAction(actionName, actionLink)}"); } private void RegisterServerEmailTriggers(IServiceCollection services) { List vms = new(); var vm = new EmailTriggerViewModel() { Trigger = ServerMailTriggers.PasswordReset, DefaultEmail = new() { To = ["{User.MailboxAddress}"], Subject = "Update Password", Body = CreateEmailBody($"A request has been made to reset your {{Server.Name}} password. Please set your password by clicking below.

{CallToAction("Update Password", "{ResetLink}")}"), }, PlaceHolders = new() { new ("{ResetLink}", "The link to the password reset page") }, Description = "User: Password Reset Requested", }; vms.Add(vm); vm = new EmailTriggerViewModel() { Trigger = ServerMailTriggers.EmailConfirm, DefaultEmail = new() { To = ["{User.MailboxAddress}"], Subject = "Confirm your email address", Body = CreateEmailBody($"Please confirm your account.

{CallToAction("Confirm Email", "{ConfirmLink}")}"), }, PlaceHolders = new() { new ("{ConfirmLink}", "The link used to confirm the user's email address") }, Description = "User: Email confirmation", }; vms.Add(vm); vm = new EmailTriggerViewModel() { Trigger = ServerMailTriggers.InvitePending, DefaultEmail = new() { To = ["{User.MailboxAddress}"], Subject = "Invitation to join {Server.Name}", Body = CreateEmailBody($"

Please complete your account setup by clicking this link.

You can also use the BTCPay Server app and scan this QR code when connecting:

{{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", }; vms.Add(vm); vm = new EmailTriggerViewModel() { Trigger = ServerMailTriggers.ApprovalConfirmed, DefaultEmail = new() { To = ["{User.MailboxAddress}"], Subject = "Your account has been approved", Body = CreateEmailBody($"Your account has been approved and you can now log in.

{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, DefaultEmail = new() { To = ["{Admins.MailboxAddresses}"], Subject = "Approval request to access the server for {User.Email}", Body = CreateEmailBody($"A new user ({{User.MailboxAddress}}), is awaiting approval to access the server.

{CallToAction("Approve", "{ApprovalLink}")}"), }, PlaceHolders = new() { new ("{ApprovalLink}", "The link that the admin needs to use to approve the user"), }, Description = "Admin: Approval request", }; vms.Add(vm); var commonPlaceholders = new List() { new("{Admins.MailboxAddresses}", "The email addresses of the admins separated by a comma"), 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.MailboxAddress}", "The formatted mailbox address to use when sending an email. (eg. \"John Doe\" )") }; foreach (var v in vms) { v.ServerTrigger = true; v.PlaceHolders.InsertRange(0, commonPlaceholders); services.AddSingleton(v); } } }