What changed, and why it matters
This commit adds CC and BCC fields to BTCPay Server's email rules and refactors how email addresses are parsed. It is a feature addition, not a clear security fix. The new parsing logic uses a placeholder-replacement trick that could mask malformed addresses, and the validation is only partial, so there is a small chance of misdirected or failed emails. There is no evidence in the commit or supplied references that the vendor is treating this as a security issue.
Review the new AsArray() parser for robustness against malformed or adversarial email strings, ensure all failure modes produce validation errors rather than silent drops, and confirm CC/BCC values are properly encoded and authorized before sending. Treat as a normal feature commit unless additional security context emerges.
Security signals we found
New email-address parsing path introduced in StoreEmailRuleViewModel.AsArray()
Validation only catches FormatException; other parsing/validation failures may be silently ignored
Placeholder substitution with a random dummy address could hide parsing edge cases
CC/BCC fields added to email rules, increasing scope of outbound email targets
No explicit security fix language in commit title or message
Evidence from the diff
The change extends EmailRuleData with CC/BCC columns, updates the UI/ViewModels, and refactors address parsing in StoreEmailRuleViewModel.AsArray(). That method replaces template placeholders like {SomeTemplate} with a random dummy email before handing the string to System.Net.Mail.MailAddressCollection, then maps parsed display names back to placeholder syntax. Validation only catches FormatException from AsArray() and adds a model error. The EmailRuleProcessorSender now routes To/CC/BCC through a shared AddToMatchedContext helper, which renders templates and parses the resulting string. The commit also renames Recipients to To in EmailRuleMatchContext and updates trigger providers accordingly. No security relevance, CVE, or researcher attribution is stated.
Changed components
BTCPayServer.Plugins.EmailsBTCPayServer.Plugins.SubscriptionsBTCPayServer.Plugins.WebhooksBTCPayServer.Common.TextTemplateBTCPayServer.Data.EmailRuleDataInspect captured patch +442 / −177
diff --git a/BTCPayServer.Common/TextTemplate.cs b/BTCPayServer.Common/TextTemplate.cs
index faddf84..11f5e33 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<string, string> NotFoundReplacement { get; set; } = path => $"[NotFound({path})]";
+ public Func<string, string> 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() ?? $"<NotFound({initial})>";
+ return token?.ToString() ?? NotFoundReplacement(initial);
}
catch
{
- return $"<ParsingError({initial})>";
+ return ParsingErrorReplacement(initial);
}
});
}
diff --git a/BTCPayServer.Data/Data/EmailRuleData.cs b/BTCPayServer.Data/Data/EmailRuleData.cs
index cf83667..76ac044 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 0000000..3c4566a
--- /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
+ {
+ /// <inheritdoc />
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AddColumn<string[]>(
+ name: "bcc",
+ table: "email_rules",
+ type: "text[]",
+ nullable: false,
+ defaultValue: new string[0]);
+
+ migrationBuilder.AddColumn<string[]>(
+ name: "cc",
+ table: "email_rules",
+ type: "text[]",
+ nullable: false,
+ defaultValue: new string[0]);
+ }
+
+ /// <inheritdoc />
+ 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 c0f857d..4ab9fe1 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<string[]>("BCC")
+ .IsRequired()
+ .HasColumnType("text[]")
+ .HasColumnName("bcc");
+
b.Property<string>("Body")
.IsRequired()
.HasColumnType("text")
.HasColumnName("body");
+ b.Property<string[]>("CC")
+ .IsRequired()
+ .HasColumnType("text[]")
+ .HasColumnName("cc");
+
b.Property<string>("Condition")
.HasColumnType("text")
.HasColumnName("condition");
diff --git a/BTCPayServer.Tests/FastTests.cs b/BTCPayServer.Tests/FastTests.cs
index 74934c5..e9674b9 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\" <emperor@btc.pay>,{SomeTemplate} ,\"Madd,Test\" <madd@example.com>");
+ string[] expected = ["\"Nicolas, The, Great\" <emperor@btc.pay>", "{SomeTemplate}", "\"Madd,Test\" <madd@example.com>"];
+ 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 23c8745..5dc3a1b 100644
--- a/BTCPayServer/Components/MainNav/Default.cshtml
+++ b/BTCPayServer/Components/MainNav/Default.cshtml
@@ -314,7 +314,7 @@
<a layout-menu-item="@nameof(ServerNavPages.Roles)" asp-controller="UIServer" asp-action="ListRoles" text-translate="true">Roles</a>
</li>
<li class="nav-item nav-item-sub" permission="@Policies.CanModifyServerSettings">
- <a layout-menu-item="Server-@nameof(ServerNavPages.Emails)" asp-area="@EmailsPlugin.Area" asp-controller="UIServerEmail" asp-action="ServerEmailSettings" text-translate="true">Email</a>
+ <a layout-menu-item="Server-@nameof(ServerNavPages.Emails)" asp-area="@EmailsPlugin.Area" asp-controller="UIServerEmail" asp-action="ServerEmailSettings" text-translate="true">Emails</a>
</li>
<li class="nav-item nav-item-sub" permission="@Policies.CanModifyServerSettings">
<a layout-menu-item="@nameof(ServerNavPages.Services)" asp-controller="UIServer" asp-action="Services" text-translate="true">Services</a>
diff --git a/BTCPayServer/Plugins/Emails/Controllers/UIEmailRuleControllerBase.cs b/BTCPayServer/Plugins/Emails/Controllers/UIEmailRuleControllerBase.cs
index 997aead..334ac71 100644
--- a/BTCPayServer/Plugins/Emails/Controllers/UIEmailRuleControllerBase.cs
+++ b/BTCPayServer/Plugins/Emails/Controllers/UIEmailRuleControllerBase.cs
@@ -106,7 +106,9 @@ public class UIEmailRuleControllerBase(
Subject = model.Subject,
Condition = string.IsNullOrWhiteSpace(model.Condition) ? null : model.Condition,
OfferingId = model.OfferingId,
- To = model.ToAsArray()
+ To = model.AsArray(model.To),
+ CC = model.AsArray(model.CC),
+ BCC = model.AsArray(model.BCC),
};
c.SetBTCPayAdditionalData(model.AdditionalData);
ctx.EmailRules.Add(c);
@@ -143,7 +145,9 @@ public class UIEmailRuleControllerBase(
rule.Trigger = model.Trigger;
rule.SetBTCPayAdditionalData(model.AdditionalData);
- rule.To = model.ToAsArray();
+ rule.To = model.AsArray(model.To);
+ rule.CC = model.AsArray(model.CC);
+ rule.BCC = model.AsArray(model.BCC);
rule.Subject = model.Subject;
rule.Condition = model.Condition;
rule.Body = model.Body;
@@ -170,6 +174,20 @@ public class UIEmailRuleControllerBase(
protected async Task ValidateCondition(StoreEmailRuleViewModel model)
{
+ string[] modelKeys = [nameof(model.To), nameof(model.CC), nameof(model.BCC)];
+ string[] values = [model.To, model.CC, model.BCC];
+ for (int i = 0; i < modelKeys.Length; i++)
+ {
+ try
+ {
+ model.AsArray(values[i]);
+ }
+ catch (FormatException)
+ {
+ ModelState.AddModelError(modelKeys[i], StringLocalizer["Invalid email address or placeholder detected"]);
+ }
+ }
+
model.Condition = model.Condition?.Trim() ?? "";
if (model.Condition.Length == 0)
model.Condition = null;
diff --git a/BTCPayServer/Plugins/Emails/EmailsPlugin.cs b/BTCPayServer/Plugins/Emails/EmailsPlugin.cs
index 1c9ab04..5f77537 100644
--- a/BTCPayServer/Plugins/Emails/EmailsPlugin.cs
+++ b/BTCPayServer/Plugins/Emails/EmailsPlugin.cs
@@ -42,77 +42,92 @@ public class EmailsPlugin : BaseBTCPayServerPlugin
var vm = new EmailTriggerViewModel()
{
Trigger = ServerMailTriggers.PasswordReset,
- RecipientExample = "{User.MailboxAddress}",
- SubjectExample = "Update Password",
- BodyExample = CreateEmailBody($"A request has been made to reset your {{Branding.ServerName}} password. Please set your password by clicking below.<br/><br/>{CallToAction("Update Password", "{ResetLink}")}"),
+ DefaultEmail = new()
+ {
+ To = ["{User.MailboxAddress}"],
+ Subject = "Update Password",
+ Body = CreateEmailBody($"A request has been made to reset your {{Branding.ServerName}} password. Please set your password by clicking below.<br/><br/>{CallToAction("Update Password", "{ResetLink}")}"),
+ },
PlaceHolders = new()
{
new ("{ResetLink}", "The link to the password reset page")
},
- Description = "Password Reset Requested",
+ Description = "User: Password Reset Requested",
};
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}")}"),
+ DefaultEmail = new()
+ {
+ To = ["{User.MailboxAddress}"],
+ Subject = "Confirm your email address",
+ Body = 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",
+ Description = "User: Email confirmation",
};
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}}"),
+ DefaultEmail = new()
+ {
+ To = ["{User.MailboxAddress}"],
+ Subject = "Invitation to join {Branding.ServerName}",
+ Body = 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",
+ Description = "User: Invitation",
};
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}")}"),
+ DefaultEmail = new()
+ {
+ To = ["{User.MailboxAddress}"],
+ Subject = "Your account has been approved",
+ Body = 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",
+ 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}")}"),
+ DefaultEmail = new()
+ {
+ To = ["{Admin.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.<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",
+ Description = "Admin: Approval request",
};
vms.Add(vm);
var commonPlaceholders = new List<EmailTriggerViewModel.PlaceHolder>()
{
- new("{Admins.MailboxAddresses}", "The email addresses of the admins separated by a comma (eg. ,)"),
+ 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\" <john.doe@example.com>)"),
diff --git a/BTCPayServer/Plugins/Emails/HostedServices/EmailRuleProcessorSender.cs b/BTCPayServer/Plugins/Emails/HostedServices/EmailRuleProcessorSender.cs
index 1833396..84059ed 100644
--- a/BTCPayServer/Plugins/Emails/HostedServices/EmailRuleProcessorSender.cs
+++ b/BTCPayServer/Plugins/Emails/HostedServices/EmailRuleProcessorSender.cs
@@ -32,9 +32,9 @@ public class EmailRuleMatchContext(
public TriggerEvent TriggerEvent { get; } = triggerEvent;
public EmailRuleData MatchedRule { get; } = matchedRule;
- public List<MailboxAddress> Recipients { get; set; } = new();
- public List<MailboxAddress> Cc { get; set; } = new();
- public List<MailboxAddress> Bcc { get; set; } = new();
+ public List<MailboxAddress> To { get; set; } = new();
+ public List<MailboxAddress> CC { get; set; } = new();
+ public List<MailboxAddress> BCC { get; set; } = new();
}
public class StoreEmailRuleProcessorSender(
@@ -65,41 +65,45 @@ public class StoreEmailRuleProcessorSender(
var body = new TextTemplate(actionableRule.Body ?? "");
var subject = new TextTemplate(actionableRule.Subject ?? "");
- matchedContext.Recipients.AddRange(
- actionableRule.To
- .SelectMany(o =>
- {
- if (MailboxAddressValidator.TryParse(o, out var oo))
- return new[] { oo };
-
- var emails = new TextTemplate(o).Render(triggEvent.Model);
- MailAddressCollection mailCollection = new();
- try
- {
- mailCollection.Add(emails);
- }
- 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)!);
+ AddToMatchedContext(triggEvent.Model, matchedContext.To, actionableRule.To);
+ AddToMatchedContext(triggEvent.Model, matchedContext.CC, actionableRule.CC);
+ AddToMatchedContext(triggEvent.Model, matchedContext.BCC, actionableRule.BCC);
if (triggEvent.Owner is not null)
await triggEvent.Owner.BeforeSending(matchedContext);
- if (matchedContext.Recipients.Count == 0)
+ if (matchedContext.To.Count == 0)
continue;
- sender.SendEmail(matchedContext.Recipients.ToArray(), matchedContext.Cc.ToArray(), matchedContext.Bcc.ToArray(), subject.Render(triggEvent.Model), body.Render(triggEvent.Model));
+ sender.SendEmail(matchedContext.To.ToArray(), matchedContext.CC.ToArray(), matchedContext.BCC.ToArray(), subject.Render(triggEvent.Model), body.Render(triggEvent.Model));
}
}
}
}
+
+ private void AddToMatchedContext(JObject model, List<MailboxAddress> mailboxAddresses, string[] rulesAddresses)
+ {
+ mailboxAddresses.AddRange(
+ rulesAddresses
+ .SelectMany(o =>
+ {
+ var emails = new TextTemplate(o).Render(model);
+ MailAddressCollection mailCollection = new();
+ try
+ {
+ mailCollection.Add(emails);
+ }
+ catch (FormatException)
+ {
+ return Array.Empty<MailboxAddress>();
+ }
+
+ return mailCollection.Select(a =>
+ {
+ MailboxAddressValidator.TryParse(a.ToString(), out var oo);
+ return oo;
+ })
+ .Where(a => a != null)
+ .ToArray();
+ })
+ .Where(o => o != null)!);
+ }
}
diff --git a/BTCPayServer/Plugins/Emails/Views/EmailTriggerViewModel.cs b/BTCPayServer/Plugins/Emails/Views/EmailTriggerViewModel.cs
index 933cce1..f086d93 100644
--- a/BTCPayServer/Plugins/Emails/Views/EmailTriggerViewModel.cs
+++ b/BTCPayServer/Plugins/Emails/Views/EmailTriggerViewModel.cs
@@ -1,4 +1,6 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
+using Newtonsoft.Json;
namespace BTCPayServer.Plugins.Emails.Views;
@@ -9,16 +11,21 @@ public class EmailTriggerViewModel
{
public class Default
{
- public string SubjectExample { get; set; }
- public string BodyExample { get; set; }
+ public string Subject { get; set; }
+ public string Body { get; set; }
+ public string[] To { get; set; } = Array.Empty<string>();
+ [JsonProperty("cc")]
+ public string[] CC { get; set; } = Array.Empty<string>();
+ [JsonProperty("bcc")]
+ public string[] BCC { get; set; } = Array.Empty<string>();
public bool CanIncludeCustomerEmail { get; set; }
- public string RecipientExample { get; set; }
}
public string Trigger { get; set; }
public string Description { get; set; }
+ public Default DefaultEmail { get; set; }
public class PlaceHolder(string name, string description)
{
diff --git a/BTCPayServer/Plugins/Emails/Views/Shared/EmailRulesManage.cshtml b/BTCPayServer/Plugins/Emails/Views/Shared/EmailRulesManage.cshtml
index 152a2c9..be09dbb 100644
--- a/BTCPayServer/Plugins/Emails/Views/Shared/EmailRulesManage.cshtml
+++ b/BTCPayServer/Plugins/Emails/Views/Shared/EmailRulesManage.cshtml
@@ -91,12 +91,25 @@
<div class="form-text" text-translate="true">Only send email when the specified JSON Path exists</div>
</div>
+ @{
+ var placeholder = "\"John Smith\" <john.smith@example.com>, john.smith@example.com, {Placeholder}";
+ }
<div class="form-group">
- <label asp-for="To" class="form-label" text-translate="true">Recipients</label>
- <input type="text" asp-for="To" class="form-control email-rule-to" />
+ <label asp-for="To" class="form-label" text-translate="true">To</label>
+ <input type="text" asp-for="To" placeholder="@placeholder" class="form-control email-rule-to" />
<span asp-validation-for="To" class="text-danger"></span>
<div class="form-text" text-translate="true">Who to send the email to. For multiple emails, separate with a comma.</div>
</div>
+ <div class="form-group">
+ <label asp-for="CC" class="form-label" text-translate="true">CC</label>
+ <input type="text" asp-for="CC" placeholder="@placeholder" class="form-control email-rule-cc" />
+ <span asp-validation-for="CC" class="text-danger"></span>
+ </div>
+ <div class="form-group">
+ <label asp-for="BCC" class="form-label" text-translate="true">BCC</label>
+ <input type="text" asp-for="BCC" placeholder="@placeholder" class="form-control email-rule-bcc" />
+ <span asp-validation-for="BCC" class="text-danger"></span>
+ </div>
<div class="form-check mb-4 customer-email-container">
<input asp-for="AdditionalData.CustomerEmail" type="checkbox" class="form-check-input email-rule-customer-email customer-email-checkbox" />
@@ -148,16 +161,23 @@
const triggerSelect = document.querySelector('.email-rule-trigger') ?? document.querySelector('.email-rule-trigger-hidden');
const subjectInput = document.querySelector('.email-rule-subject');
- const recipientInput = document.querySelector('.email-rule-to');
+ const toInput = document.querySelector('.email-rule-to');
+ const ccInput = document.querySelector('.email-rule-cc');
+ const bccInput = document.querySelector('.email-rule-bcc');
const bodyTextarea = document.querySelector('.email-rule-body');
const placeholdersTd = document.querySelector('#placeholders');
+ function join(arr) {
+ return arr ? arr.join(', ') : '';
+ }
function applyTemplate() {
const selectedTrigger = triggerSelect.value;
if (triggersByType[selectedTrigger]) {
- subjectInput.value = triggersByType[selectedTrigger].subjectExample;
- recipientInput.value = triggersByType[selectedTrigger].recipientExample;
- var body = triggersByType[selectedTrigger].bodyExample;
+ subjectInput.value = triggersByType[selectedTrigger].defaultEmail.subject;
+ toInput.value = join(triggersByType[selectedTrigger].defaultEmail.to);
+ ccInput.value = join(triggersByType[selectedTrigger].defaultEmail.cc);
+ bccInput.value = join(triggersByType[selectedTrigger].defaultEmail.bcc);
+ var body = triggersByType[selectedTrigger].defaultEmail.body;
if ($(bodyTextarea).summernote) {
console.log(body);
@@ -166,7 +186,13 @@
} else {
bodyTextarea.value = body;
}
-
+ applyPlaceholders();
+ }
+ }
+ function applyPlaceholders()
+ {
+ const selectedTrigger = triggerSelect.value;
+ if (triggersByType[selectedTrigger]) {
placeholdersTd.innerHTML = '';
triggersByType[selectedTrigger].placeHolders.forEach(p => {
const tr = document.createElement('tr');
@@ -180,7 +206,6 @@
tr.appendChild(td2);
placeholdersTd.appendChild(tr);
});
-
}
}
@@ -205,6 +230,10 @@
{
applyTemplate();
}
+ else
+ {
+ applyPlaceholders();
+ }
toggleCustomerEmailVisibility();
}
});
diff --git a/BTCPayServer/Plugins/Emails/Views/StoreEmailRuleViewModel.cs b/BTCPayServer/Plugins/Emails/Views/StoreEmailRuleViewModel.cs
index f78f413..3c22bce 100644
--- a/BTCPayServer/Plugins/Emails/Views/StoreEmailRuleViewModel.cs
+++ b/BTCPayServer/Plugins/Emails/Views/StoreEmailRuleViewModel.cs
@@ -2,7 +2,9 @@ using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
+using System.Net.Mail;
using BTCPayServer.Data;
+using Newtonsoft.Json.Linq;
namespace BTCPayServer.Plugins.Emails.Views;
@@ -25,6 +27,8 @@ public class StoreEmailRuleViewModel
Condition = data.Condition ?? "";
Body = data.Body;
To = string.Join(",", data.To);
+ CC = string.Join(",", data.CC);
+ BCC = string.Join(",", data.BCC);
}
else
{
@@ -46,6 +50,8 @@ public class StoreEmailRuleViewModel
public EmailRuleData Data { get; set; }
public EmailRuleData.BTCPayAdditionalData AdditionalData { get; set; }
public string To { get; set; }
+ public string CC { get; set; }
+ public string BCC { get; set; }
public List<EmailTriggerViewModel> Triggers { get; set; }
public string RedirectUrl { get; set; }
@@ -54,8 +60,29 @@ public class StoreEmailRuleViewModel
public string OfferingId { get; set; }
public string StoreId { get; set; }
- public string[] ToAsArray()
- => (To ?? "").Split(',', StringSplitOptions.RemoveEmptyEntries)
- .Select(t => t.Trim())
- .ToArray();
+ public string[] AsArray(string values)
+ {
+ // This replace the placeholders with random email addresses
+ // We can't just split input by comma, because display names of people can contain commas.
+ // "John, Jr. Smith" <jon@example.com>,{User.Email},"Nicolas D." <nico@example.com>
+ values ??= "";
+
+ // We replace the placeholders with dummy email addresses
+ var template = new TextTemplate(values);
+ var dummy = $"{Random.Shared.Next()}@example.com";
+ template.NotFoundReplacement = o => $"\"{o}\" <{dummy}>";
+ values = template.Render(new JObject());
+ if (string.IsNullOrWhiteSpace(values))
+ return Array.Empty<string>();
+ // We use MailAddressCollection to parse the addresses
+ MailAddressCollection mailCollection = new();
+ mailCollection.Add(values);
+ foreach (var mail in mailCollection)
+ {
+ // Let it throw if the address is invalid
+ MailboxAddressValidator.Parse(mail.ToString());
+ }
+ // We replace the dummies with the former placeholders
+ return mailCollection.Select(a => a.Address == dummy ? $"{{{a.DisplayName}}}" : a.ToString()).ToArray();
+ }
}
diff --git a/BTCPayServer/Plugins/Subscriptions/SubscriptionsPlugin.cs b/BTCPayServer/Plugins/Subscriptions/SubscriptionsPlugin.cs
index b551d56..afce461 100644
--- a/BTCPayServer/Plugins/Subscriptions/SubscriptionsPlugin.cs
+++ b/BTCPayServer/Plugins/Subscriptions/SubscriptionsPlugin.cs
@@ -43,18 +43,17 @@ public class SubscriptionsPlugin : BaseBTCPayServerPlugin
var placeHolders = new List<EmailTriggerViewModel.PlaceHolder>()
{
- new ("{Plan.Id}", "Plan ID"),
- new ("{Plan.Name}", "Plan name"),
- new ("{Offering.Name}", "Offering name"),
- new ("{Offering.Id}", "Offering ID"),
- new ("{Offering.AppId}", "Offering app ID"),
- new ("{Offering.Metadata}*", "Offering metadata"),
- new ("{Subscriber.Phase}", "Subscriber phase"),
- new ("{Subscriber.Email}", "Subscriber email"),
- new ("{Customer.ExternalRef}", "Customer external reference"),
- new ("{Customer.Name}", "Customer name"),
- new ("{Customer.Metadata}*", "Customer metadata")
-
+ new("{Plan.Id}", "Plan ID"),
+ new("{Plan.Name}", "Plan name"),
+ new("{Offering.Name}", "Offering name"),
+ new("{Offering.Id}", "Offering ID"),
+ new("{Offering.AppId}", "Offering app ID"),
+ new("{Offering.Metadata}*", "Offering metadata"),
+ new("{Subscriber.Phase}", "Subscriber phase"),
+ new("{Subscriber.Email}", "Subscriber email"),
+ new("{Customer.ExternalRef}", "Customer external reference"),
+ new("{Customer.Name}", "Customer name"),
+ new("{Customer.Metadata}*", "Customer metadata")
}.AddStoresPlaceHolders();
var viewModels = new List<EmailTriggerViewModel>()
@@ -63,72 +62,99 @@ public class SubscriptionsPlugin : BaseBTCPayServerPlugin
{
Trigger = WebhookSubscriptionEvent.SubscriberCreated,
Description = "Subscription - New subscriber",
- SubjectExample = "Welcome {Customer.Name}!",
- BodyExample = "Hello {Customer.Name},\n\nThank you for subscribing to our service.\n\nRegards,\n{Store.Name}",
+ DefaultEmail = new()
+ {
+ Subject = "Welcome {Customer.Name}!",
+ Body = "Hello {Customer.Name},\n\nThank you for subscribing to our service.\n\nRegards,\n{Store.Name}"
+ },
PlaceHolders = placeHolders
},
new()
{
Trigger = WebhookSubscriptionEvent.SubscriberCredited,
Description = "Subscription - Subscriber credited",
- SubjectExample = "Your subscription has been credited",
- BodyExample = "Hello {Customer.Name},\n\nYour subscription has been credited.\n\nRegards,\n{Store.Name}",
+ DefaultEmail = new()
+ {
+ Subject = "Your subscription has been credited",
+ Body = "Hello {Customer.Name},\n\nYour subscription has been credited.\n\nRegards,\n{Store.Name}"
+ },
PlaceHolders = placeHolders
},
new()
{
Trigger = WebhookSubscriptionEvent.SubscriberCharged,
Description = "Subscription - Subscriber charged",
- SubjectExample = "Your subscription payment has been processed",
- BodyExample = "Hello {Customer.Name},\n\nYour subscription payment for {Plan.Name} has been processed.\n\nRegards,\n{Store.Name}",
+ DefaultEmail = new()
+ {
+ Subject = "Your subscription payment has been processed",
+ Body = "Hello {Customer.Name},\n\nYour subscription payment for {Plan.Name} has been processed.\n\nRegards,\n{Store.Name}"
+ },
PlaceHolders = placeHolders
},
new()
{
Trigger = WebhookSubscriptionEvent.SubscriberActivated,
Description = "Subscription - Subscriber activated",
- SubjectExample = "Your subscription is now active",
- BodyExample = "Hello {Customer.Name},\n\nYour subscription to {Plan.Name} is now active.\n\nRegards,\n{Store.Name}",
+ DefaultEmail = new()
+ {
+ Subject = "Your subscription is now active",
+ Body = "Hello {Customer.Name},\n\nYour subscription to {Plan.Name} is now active.\n\nRegards,\n{Store.Name}"
+ },
PlaceHolders = placeHolders
},
new()
{
Trigger = WebhookSubscriptionEvent.SubscriberPhaseChanged,
Description = "Subscription - Subscriber phase changed",
- SubjectExample = "Your subscription phase has changed",
- BodyExample = "Hello {Customer.Name},\n\nYour subscription phase has been updated to {Subscriber.Phase}.\n\nRegards,\n{Store.Name}",
+ DefaultEmail = new()
+ {
+ Subject = "Your subscription phase has changed",
+ Body = "Hello {Customer.Name},\n\nYour subscription phase has been updated to {Subscriber.Phase}.\n\nRegards,\n{Store.Name}"
+ },
PlaceHolders = placeHolders
},
new()
{
Trigger = WebhookSubscriptionEvent.SubscriberDisabled,
Description = "Subscription - Subscriber disabled",
- SubjectExample = "Your subscription has been disabled",
- BodyExample = "Hello {Customer.Name},\n\nYour subscription has been disabled.\n\nRegards,\n{Store.Name}",
+ DefaultEmail = new()
+ {
+ Subject = "Your subscription has been disabled",
+ Body = "Hello {Customer.Name},\n\nYour subscription has been disabled.\n\nRegards,\n{Store.Name}"
+ },
PlaceHolders = placeHolders
},
new()
{
Trigger = WebhookSubscriptionEvent.PaymentReminder,
Description = "Subscription - Payment reminder",
- SubjectExample = "Payment reminder for your subscription",
- BodyExample = "Hello {Customer.Name},\n\nThis is a reminder about your upcoming subscription payment.\n\nRegards,\n{Store.Name}",
+ DefaultEmail = new()
+ {
+ Subject = "Payment reminder for your subscription",
+ Body = "Hello {Customer.Name},\n\nThis is a reminder about your upcoming subscription payment.\n\nRegards,\n{Store.Name}"
+ },
PlaceHolders = placeHolders
},
new()
{
Trigger = WebhookSubscriptionEvent.PlanStarted,
Description = "Subscription - Plan started",
- SubjectExample = "Your subscription plan has started",
- BodyExample = "Hello {Customer.Name},\n\nYour subscription plan {Plan.Name} has started.\n\nRegards,\n{Store.Name}",
+ DefaultEmail = new()
+ {
+ Subject = "Your subscription plan has started",
+ Body = "Hello {Customer.Name},\n\nYour subscription plan {Plan.Name} has started.\n\nRegards,\n{Store.Name}"
+ },
PlaceHolders = placeHolders
},
new()
{
Trigger = WebhookSubscriptionEvent.SubscriberNeedUpgrade,
Description = "Subscription - Need upgrade",
- SubjectExample = "Your subscription needs to be upgraded",
- BodyExample = "Hello {Customer.Name},\n\nYour subscription needs to be upgraded to continue using our service.\n\nRegards,\n{Store.Name}",
+ DefaultEmail = new()
+ {
+ Subject = "Your subscription needs to be upgraded",
+ Body = "Hello {Customer.Name},\n\nYour subscription needs to be upgraded to continue using our service.\n\nRegards,\n{Store.Name}"
+ },
PlaceHolders = placeHolders
},
};
@@ -141,6 +167,7 @@ public class SubscriptionsAppType(
IOptions<BTCPayServerOptions> btcPayServerOptions) : AppBaseType(AppType)
{
public const string AppType = "Subscriptions";
+
public class AppConfig
{
public string OfferingId { get; set; } = null!;
diff --git a/BTCPayServer/Plugins/Webhooks/TriggerProviders/InvoiceTriggerProvider.cs b/BTCPayServer/Plugins/Webhooks/TriggerProviders/InvoiceTriggerProvider.cs
index 49eee8c..80a7e31 100644
--- a/BTCPayServer/Plugins/Webhooks/TriggerProviders/InvoiceTriggerProvider.cs
+++ b/BTCPayServer/Plugins/Webhooks/TriggerProviders/InvoiceTriggerProvider.cs
@@ -42,7 +42,7 @@ public class InvoiceTriggerProvider(LinkGenerator linkGenerator)
context.MatchedRule.GetBTCPayAdditionalData()?.CustomerEmail is true &&
MailboxAddressValidator.TryParse(email, out var mb))
{
- context.Recipients.Insert(0, mb);
+ context.To.Insert(0, mb);
}
return Task.CompletedTask;
}
diff --git a/BTCPayServer/Plugins/Webhooks/TriggerProviders/PaymentRequestTriggerProvider.cs b/BTCPayServer/Plugins/Webhooks/TriggerProviders/PaymentRequestTriggerProvider.cs
index 39aeb3a..39c6979 100644
--- a/BTCPayServer/Plugins/Webhooks/TriggerProviders/PaymentRequestTriggerProvider.cs
+++ b/BTCPayServer/Plugins/Webhooks/TriggerProviders/PaymentRequestTriggerProvider.cs
@@ -43,7 +43,7 @@ public class PaymentRequestTriggerProvider(LinkGenerator linkGenerator)
context.MatchedRule.GetBTCPayAdditionalData()?.CustomerEmail is true &&
MailboxAddressValidator.TryParse(email, out var mb))
{
- context.Recipients.Insert(0, mb);
+ context.To.Insert(0, mb);
}
return Task.CompletedTask;
}
diff --git a/BTCPayServer/Plugins/Webhooks/WebhooksPlugin.cs b/BTCPayServer/Plugins/Webhooks/WebhooksPlugin.cs
index 0f0a9d6..9e14df2 100644
--- a/BTCPayServer/Plugins/Webhooks/WebhooksPlugin.cs
+++ b/BTCPayServer/Plugins/Webhooks/WebhooksPlugin.cs
@@ -1,4 +1,4 @@
-#nullable enable
+#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
@@ -53,7 +53,7 @@ public class WebhooksPlugin : BaseBTCPayServerPlugin
AddPendingTransactionWebhooks(services);
}
- private static void AddPendingTransactionWebhooks(IServiceCollection services)
+ private static void AddPendingTransactionWebhooks(IServiceCollection services)
{
services.AddWebhookTriggerProvider<PendingTransactionTriggerProvider>();
var pendingTransactionsPlaceholders = new List<EmailTriggerViewModel.PlaceHolder>()
@@ -73,32 +73,45 @@ public class WebhooksPlugin : BaseBTCPayServerPlugin
{
Trigger = PendingTransactionTriggerProvider.PendingTransactionCreated,
Description = "Pending Transaction - Created",
- SubjectExample = "Pending Transaction {PendingTransaction.TrimmedId} Created",
- BodyExample = "Review the transaction {PendingTransaction.Id} and sign it on: {PendingTransaction.Link}",
+ DefaultEmail = new()
+ {
+ Subject = "Pending Transaction {PendingTransaction.TrimmedId} Created",
+ Body = "Review the transaction {PendingTransaction.Id} and sign it on: {PendingTransaction.Link}"
+ },
PlaceHolders = pendingTransactionsPlaceholders
},
new()
{
Trigger = PendingTransactionTriggerProvider.PendingTransactionSignatureCollected,
Description = "Pending Transaction - Signature Collected",
- SubjectExample = "Signature Collected for Pending Transaction {PendingTransaction.TrimmedId}",
- BodyExample = "So far {PendingTransaction.SignaturesCollected} signatures collected out of {PendingTransaction.SignaturesNeeded} signatures needed. ",
+ DefaultEmail = new()
+ {
+ Subject = "Signature Collected for Pending Transaction {PendingTransaction.TrimmedId}",
+ Body =
+ "So far {PendingTransaction.SignaturesCollected} signatures collected out of {PendingTransaction.SignaturesNeeded} signatures needed. "
+ },
PlaceHolders = pendingTransactionsPlaceholders
},
new()
{
Trigger = PendingTransactionTriggerProvider.PendingTransactionBroadcast,
Description = "Pending Transaction - Broadcast",
- SubjectExample = "Transaction {PendingTransaction.TrimmedId} has been Broadcast",
- BodyExample = "Transaction is visible in mempool on: https://mempool.space/tx/{PendingTransaction.Id}. ",
+ DefaultEmail = new()
+ {
+ Subject = "Transaction {PendingTransaction.TrimmedId} has been Broadcast",
+ Body = "Transaction is visible in mempool on: https://mempool.space/tx/{PendingTransaction.Id}. "
+ },
PlaceHolders = pendingTransactionsPlaceholders
},
new()
{
Trigger = PendingTransactionTriggerProvider.PendingTransactionCancelled,
Description = "Pending Transaction - Cancelled",
- SubjectExample = "Pending Transaction {PendingTransaction.TrimmedId} Cancelled",
- BodyExample = "Transaction {PendingTransaction.Id} is cancelled and signatures are no longer being collected. ",
+ DefaultEmail = new()
+ {
+ Subject = "Pending Transaction {PendingTransaction.TrimmedId} Cancelled",
+ Body = "Transaction {PendingTransaction.Id} is cancelled and signatures are no longer being collected. "
+ },
PlaceHolders = pendingTransactionsPlaceholders
}
};
@@ -130,46 +143,61 @@ public class WebhooksPlugin : BaseBTCPayServerPlugin
{
Trigger = WebhookEventType.PaymentRequestCreated,
Description = "Payment Request - Created",
- SubjectExample = "Payment Request {PaymentRequest.Id} created",
- BodyExample = "Payment Request {PaymentRequest.Id} ({PaymentRequest.Title}) created.",
- PlaceHolders = paymentRequestPlaceholders,
- CanIncludeCustomerEmail = true
+ DefaultEmail = new()
+ {
+ Subject = "Payment Request {PaymentRequest.Id} created",
+ Body = "Payment Request {PaymentRequest.Id} ({PaymentRequest.Title}) created.",
+ CanIncludeCustomerEmail = true
+ },
+ PlaceHolders = paymentRequestPlaceholders
},
new()
{
Trigger = WebhookEventType.PaymentRequestUpdated,
Description = "Payment Request - Updated",
- SubjectExample = "Payment Request {PaymentRequest.Id} updated",
- BodyExample = "Payment Request {PaymentRequest.Id} ({PaymentRequest.Title}) updated.",
- PlaceHolders = paymentRequestPlaceholders,
- CanIncludeCustomerEmail = true
+ DefaultEmail = new()
+ {
+ Subject = "Payment Request {PaymentRequest.Id} updated",
+ Body = "Payment Request {PaymentRequest.Id} ({PaymentRequest.Title}) updated.",
+ CanIncludeCustomerEmail = true
+ },
+ PlaceHolders = paymentRequestPlaceholders
},
new()
{
Trigger = WebhookEventType.PaymentRequestArchived,
Description = "Payment Request - Archived",
- SubjectExample = "Payment Request {PaymentRequest.Id} archived",
- BodyExample = "Payment Request {PaymentRequest.Id} ({PaymentRequest.Title}) archived.",
- PlaceHolders = paymentRequestPlaceholders,
- CanIncludeCustomerEmail = true
+ DefaultEmail = new()
+ {
+ Subject = "Payment Request {PaymentRequest.Id} archived",
+ Body = "Payment Request {PaymentRequest.Id} ({PaymentRequest.Title}) archived.",
+ CanIncludeCustomerEmail = true
+ },
+ PlaceHolders = paymentRequestPlaceholders
},
new()
{
Trigger = WebhookEventType.PaymentRequestStatusChanged,
Description = "Payment Request - Status Changed",
- SubjectExample = "Payment Request {PaymentRequest.Id} status changed",
- BodyExample = "Payment Request {PaymentRequest.Id} ({PaymentRequest.Title}) status changed to {PaymentRequest.Status}.",
- PlaceHolders = paymentRequestPlaceholders,
- CanIncludeCustomerEmail = true
+ DefaultEmail = new()
+ {
+ Subject = "Payment Request {PaymentRequest.Id} status changed",
+ Body = "Payment Request {PaymentRequest.Id} ({PaymentRequest.Title}) status changed to {PaymentRequest.Status}.",
+ CanIncludeCustomerEmail = true
+ },
+ PlaceHolders = paymentRequestPlaceholders
},
new()
{
Trigger = WebhookEventType.PaymentRequestCompleted,
Description = "Payment Request - Completed",
- SubjectExample = "Payment Request {PaymentRequest.Title} {PaymentRequest.ReferenceId} Completed",
- BodyExample = "The total of {PaymentRequest.Amount} {PaymentRequest.Currency} has been received and Payment Request {PaymentRequest.Id} is completed.\nReview the payment request: {PaymentRequest.Link}",
- PlaceHolders = paymentRequestPlaceholders,
- CanIncludeCustomerEmail = true
+ DefaultEmail = new()
+ {
+ Subject = "Payment Request {PaymentRequest.Title} {PaymentRequest.ReferenceId} Completed",
+ Body = "The total of {PaymentRequest.Amount} {PaymentRequest.Currency} has been received and Payment Request {PaymentRequest.Id} is completed.\nReview the payment request: {PaymentRequest.Link}",
+ CanIncludeCustomerEmail = true
+ },
+ PlaceHolders = paymentRequestPlaceholders
}
};
services.AddWebhookTriggerViewModels(paymentRequestTriggers);
@@ -192,24 +220,33 @@ public class WebhooksPlugin : BaseBTCPayServerPlugin
{
Trigger = WebhookEventType.PayoutCreated,
Description = "Payout - Created",
- SubjectExample = "Payout {Payout.Id} created",
- BodyExample = "Payout {Payout.Id} (Pull Payment Id: {Payout.PullPaymentId}) created.",
+ DefaultEmail = new()
+ {
+ Subject = "Payout {Payout.Id} created",
+ Body = "Payout {Payout.Id} (Pull Payment Id: {Payout.PullPaymentId}) created."
+ },
PlaceHolders = payoutPlaceholders
},
new()
{
Trigger = WebhookEventType.PayoutApproved,
Description = "Payout - Approved",
- SubjectExample = "Payout {Payout.Id} approved",
- BodyExample = "Payout {Payout.Id} (Pull Payment Id: {Payout.PullPaymentId}) approved.",
+ DefaultEmail = new()
+ {
+ Subject = "Payout {Payout.Id} approved",
+ Body = "Payout {Payout.Id} (Pull Payment Id: {Payout.PullPaymentId}) approved."
+ },
PlaceHolders = payoutPlaceholders
},
new()
{
Trigger = WebhookEventType.PayoutUpdated,
Description = "Payout - Updated",
- SubjectExample = "Payout {Payout.Id} updated",
- BodyExample = "Payout {Payout.Id} (Pull Payment Id: {Payout.PullPaymentId}) updated.",
+ DefaultEmail = new()
+ {
+ Subject = "Payout {Payout.Id} updated",
+ Body = "Payout {Payout.Id} (Pull Payment Id: {Payout.PullPaymentId}) updated."
+ },
PlaceHolders = payoutPlaceholders
}
};
@@ -238,82 +275,110 @@ public class WebhooksPlugin : BaseBTCPayServerPlugin
{
Trigger = WebhookEventType.InvoiceCreated,
Description = "Invoice - Created",
- SubjectExample = "Invoice {Invoice.Id} created",
- BodyExample = "Invoice {Invoice.Id} (Order Id: {Invoice.OrderId}) created.",
- PlaceHolders = invoicePlaceholders,
- CanIncludeCustomerEmail = true
+ DefaultEmail = new()
+ {
+ Subject = "Invoice {Invoice.Id} created",
+ Body = "Invoice {Invoice.Id} (Order Id: {Invoice.OrderId}) created.",
+ CanIncludeCustomerEmail = true
+ },
+ PlaceHolders = invoicePlaceholders
},
new()
{
Trigger = WebhookEventType.InvoiceReceivedPayment,
Description = "Invoice - Received Payment",
- SubjectExample = "Invoice {Invoice.Id} received payment",
- BodyExample = "Invoice {Invoice.Id} (Order Id: {Invoice.OrderId}) received payment.",
- PlaceHolders = invoicePlaceholders,
- CanIncludeCustomerEmail = true
+ DefaultEmail = new()
+ {
+ Subject = "Invoice {Invoice.Id} received payment",
+ Body = "Invoice {Invoice.Id} (Order Id: {Invoice.OrderId}) received payment.",
+ CanIncludeCustomerEmail = true
+ },
+ PlaceHolders = invoicePlaceholders
},
new()
{
Trigger = WebhookEventType.InvoiceProcessing,
Description = "Invoice - Is Processing",
- SubjectExample = "Invoice {Invoice.Id} processing",
- BodyExample = "Invoice {Invoice.Id} (Order Id: {Invoice.OrderId}) is processing.",
- PlaceHolders = invoicePlaceholders,
- CanIncludeCustomerEmail = true
+ DefaultEmail = new()
+ {
+ Subject = "Invoice {Invoice.Id} processing",
+ Body = "Invoice {Invoice.Id} (Order Id: {Invoice.OrderId}) is processing.",
+ CanIncludeCustomerEmail = true
+ },
+ PlaceHolders = invoicePlaceholders
},
new()
{
Trigger = WebhookEventType.InvoiceExpired,
Description = "Invoice - Expired",
- SubjectExample = "Invoice {Invoice.Id} expired",
- BodyExample = "Invoice {Invoice.Id} (Order Id: {Invoice.OrderId}) expired.",
- PlaceHolders = invoicePlaceholders,
- CanIncludeCustomerEmail = true
+ DefaultEmail = new()
+ {
+ Subject = "Invoice {Invoice.Id} expired",
+ Body = "Invoice {Invoice.Id} (Order Id: {Invoice.OrderId}) expired.",
+ CanIncludeCustomerEmail = true
+ },
+ PlaceHolders = invoicePlaceholders
},
new()
{
Trigger = WebhookEventType.InvoiceSettled,
Description = "Invoice - Is Settled",
- SubjectExample = "Invoice {Invoice.Id} settled",
- BodyExample = "Invoice {Invoice.Id} (Order Id: {Invoice.OrderId}) is settled.",
- PlaceHolders = invoicePlaceholders,
- CanIncludeCustomerEmail = true
+ DefaultEmail = new()
+ {
+ Subject = "Invoice {Invoice.Id} settled",
+ Body = "Invoice {Invoice.Id} (Order Id: {Invoice.OrderId}) is settled.",
+ CanIncludeCustomerEmail = true
+ },
+ PlaceHolders = invoicePlaceholders
},
new()
{
Trigger = WebhookEventType.InvoiceInvalid,
Description = "Invoice - Became Invalid",
- SubjectExample = "Invoice {Invoice.Id} invalid",
- BodyExample = "Invoice {Invoice.Id} (Order Id: {Invoice.OrderId}) invalid.",
- PlaceHolders = invoicePlaceholders,
- CanIncludeCustomerEmail = true
+ DefaultEmail = new()
+ {
+ Subject = "Invoice {Invoice.Id} invalid",
+ Body = "Invoice {Invoice.Id} (Order Id: {Invoice.OrderId}) invalid.",
+ CanIncludeCustomerEmail = true
+ },
+ PlaceHolders = invoicePlaceholders
},
new()
{
Trigger = WebhookEventType.InvoicePaymentSettled,
Description = "Invoice - Payment Settled",
- SubjectExample = "Invoice {Invoice.Id} payment settled",
- BodyExample = "Invoice {Invoice.Id} (Order Id: {Invoice.OrderId}) payment settled.",
- PlaceHolders = invoicePlaceholders,
- CanIncludeCustomerEmail = true
+ DefaultEmail = new()
+ {
+ Subject = "Invoice {Invoice.Id} payment settled",
+ Body = "Invoice {Invoice.Id} (Order Id: {Invoice.OrderId}) payment settled.",
+ CanIncludeCustomerEmail = true
+ },
+ PlaceHolders = invoicePlaceholders
},
new()
{
Trigger = WebhookEventType.InvoiceExpiredPaidPartial,
Description = "Invoice - Expired Paid Partial",
- SubjectExample = "Invoice {Invoice.Id} expired with partial payment",
- BodyExample = "Invoice {Invoice.Id} (Order Id: {Invoice.OrderId}) expired with partial payment. \nPlease review and take appropriate action: {Invoice.Link}",
+ DefaultEmail = new()
+ {
+ Subject = "Invoice {Invoice.Id} expired with partial payment",
+ Body = "Invoice {Invoice.Id} (Order Id: {Invoice.OrderId}) expired with partial payment. \nPlease review and take appropriate action: {Invoice.Link}",
+ CanIncludeCustomerEmail = true
+ },
PlaceHolders = invoicePlaceholders,
- CanIncludeCustomerEmail = true
+
},
new()
{
Trigger = WebhookEventType.InvoicePaidAfterExpiration,
Description = "Invoice - Expired Paid Late",
- SubjectExample = "Invoice {Invoice.Id} paid after expiration",
- BodyExample = "Invoice {Invoice.Id} (Order Id: {Invoice.OrderId}) paid after expiration.",
- PlaceHolders = invoicePlaceholders,
- CanIncludeCustomerEmail = true
+ DefaultEmail = new()
+ {
+ Subject = "Invoice {Invoice.Id} paid after expiration",
+ Body = "Invoice {Invoice.Id} (Order Id: {Invoice.OrderId}) paid after expiration.",
+ CanIncludeCustomerEmail = true
+ },
+ PlaceHolders = invoicePlaceholders
}
};
services.AddWebhookTriggerViewModels(emailTriggers);
Why this scored 25/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.