From dcf60e20b9f770f865219a013d065367c4aadc52 Mon Sep 17 00:00:00 2001 From: Nicolas Dorier Date: Sat, 8 Nov 2025 22:42:45 +0900 Subject: [PATCH] Add CC and BCC to emails --- BTCPayServer.Common/TextTemplate.cs | 7 +- BTCPayServer.Data/Data/EmailRuleData.cs | 6 + .../Migrations/20251107131717_emailccbcc.cs | 43 ++++ .../ApplicationDbContextModelSnapshot.cs | 10 + BTCPayServer.Tests/FastTests.cs | 11 + .../Components/MainNav/Default.cshtml | 2 +- .../Controllers/UIEmailRuleControllerBase.cs | 22 +- BTCPayServer/Plugins/Emails/EmailsPlugin.cs | 57 +++-- .../EmailRuleProcessorSender.cs | 68 +++--- .../Emails/Views/EmailTriggerViewModel.cs | 15 +- .../Views/Shared/EmailRulesManage.cshtml | 45 +++- .../Emails/Views/StoreEmailRuleViewModel.cs | 35 ++- .../Subscriptions/SubscriptionsPlugin.cs | 87 +++++--- .../InvoiceTriggerProvider.cs | 2 +- .../PaymentRequestTriggerProvider.cs | 2 +- .../Plugins/Webhooks/WebhooksPlugin.cs | 207 ++++++++++++------ 16 files changed, 442 insertions(+), 177 deletions(-) create mode 100644 BTCPayServer.Data/Migrations/20251107131717_emailccbcc.cs diff --git a/BTCPayServer.Common/TextTemplate.cs b/BTCPayServer.Common/TextTemplate.cs index faddf8408..11f5e33be 100644 --- a/BTCPayServer.Common/TextTemplate.cs +++ b/BTCPayServer.Common/TextTemplate.cs @@ -10,6 +10,9 @@ namespace BTCPayServer; public class TextTemplate(string template) { static readonly Regex _interpolationRegex = new Regex(@"\{([^}]+)\}", RegexOptions.Compiled | RegexOptions.CultureInvariant); + public Func NotFoundReplacement { get; set; } = path => $"[NotFound({path})]"; + public Func ParsingErrorReplacement { get; set; } = path => $"[ParsingError({path})]"; + public string Render(JObject model) { model = (JObject)ToLowerCase(model); @@ -23,11 +26,11 @@ public class TextTemplate(string template) try { var token = model.SelectToken(path); - return token?.ToString() ?? $""; + return token?.ToString() ?? NotFoundReplacement(initial); } catch { - return $""; + return ParsingErrorReplacement(initial); } }); } diff --git a/BTCPayServer.Data/Data/EmailRuleData.cs b/BTCPayServer.Data/Data/EmailRuleData.cs index cf836672d..76ac04468 100644 --- a/BTCPayServer.Data/Data/EmailRuleData.cs +++ b/BTCPayServer.Data/Data/EmailRuleData.cs @@ -39,6 +39,12 @@ public class EmailRuleData : BaseEntityData [Required] [Column("to")] public string[] To { get; set; } = null!; + [Required] + [Column("cc")] + public string[] CC { get; set; } = null!; + [Required] + [Column("bcc")] + public string[] BCC { get; set; } = null!; [Required] [Column("subject")] diff --git a/BTCPayServer.Data/Migrations/20251107131717_emailccbcc.cs b/BTCPayServer.Data/Migrations/20251107131717_emailccbcc.cs new file mode 100644 index 000000000..3c4566a46 --- /dev/null +++ b/BTCPayServer.Data/Migrations/20251107131717_emailccbcc.cs @@ -0,0 +1,43 @@ +using BTCPayServer.Data; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace BTCPayServer.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20251107131717_emailccbcc")] + public partial class emailccbcc : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "bcc", + table: "email_rules", + type: "text[]", + nullable: false, + defaultValue: new string[0]); + + migrationBuilder.AddColumn( + name: "cc", + table: "email_rules", + type: "text[]", + nullable: false, + defaultValue: new string[0]); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "bcc", + table: "email_rules"); + + migrationBuilder.DropColumn( + name: "cc", + table: "email_rules"); + } + } +} diff --git a/BTCPayServer.Data/Migrations/ApplicationDbContextModelSnapshot.cs b/BTCPayServer.Data/Migrations/ApplicationDbContextModelSnapshot.cs index c0f857dab..4ab9fe105 100644 --- a/BTCPayServer.Data/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/BTCPayServer.Data/Migrations/ApplicationDbContextModelSnapshot.cs @@ -280,11 +280,21 @@ namespace BTCPayServer.Migrations .HasColumnName("additional_data") .HasDefaultValueSql("'{}'::jsonb"); + b.Property("BCC") + .IsRequired() + .HasColumnType("text[]") + .HasColumnName("bcc"); + b.Property("Body") .IsRequired() .HasColumnType("text") .HasColumnName("body"); + b.Property("CC") + .IsRequired() + .HasColumnType("text[]") + .HasColumnName("cc"); + b.Property("Condition") .HasColumnType("text") .HasColumnName("condition"); diff --git a/BTCPayServer.Tests/FastTests.cs b/BTCPayServer.Tests/FastTests.cs index 74934c591..e9674b965 100644 --- a/BTCPayServer.Tests/FastTests.cs +++ b/BTCPayServer.Tests/FastTests.cs @@ -20,6 +20,7 @@ using BTCPayServer.HostedServices; using BTCPayServer.Hosting; using BTCPayServer.JsonConverters; using BTCPayServer.Payments; +using BTCPayServer.Plugins.Emails.Views; using BTCPayServer.Rating; using BTCPayServer.Services; using BTCPayServer.Services.Apps; @@ -2305,6 +2306,16 @@ bc1qfzu57kgu5jthl934f9xrdzzx8mmemx7gn07tf0grnvz504j6kzusu2v0ku Assert.Equal("1-of-xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL-xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL-[legacy]", parsedDescriptor.AccountDerivation.ToString()); } + [Fact] + [Trait("FastTest", "FastTest")] + public void CanParseEmailDestination() + { + var vm = new StoreEmailRuleViewModel(); + var actual = vm.AsArray("\"Nicolas, The, Great\" ,{SomeTemplate} ,\"Madd,Test\" "); + string[] expected = ["\"Nicolas, The, Great\" ", "{SomeTemplate}", "\"Madd,Test\" "]; + Assert.Equal(expected, actual); + } + [Fact] [Trait("Altcoins", "Altcoins")] public void CanCalculateCryptoDue2() diff --git a/BTCPayServer/Components/MainNav/Default.cshtml b/BTCPayServer/Components/MainNav/Default.cshtml index 23c874541..5dc3a1b58 100644 --- a/BTCPayServer/Components/MainNav/Default.cshtml +++ b/BTCPayServer/Components/MainNav/Default.cshtml @@ -314,7 +314,7 @@ Roles