Add CC and BCC to emails

This commit is contained in:
Nicolas Dorier
2025-11-08 22:42:45 +09:00
parent d7fcd55707
commit dcf60e20b9
16 changed files with 442 additions and 177 deletions

View File

@@ -10,6 +10,9 @@ namespace BTCPayServer;
public class TextTemplate(string template) public class TextTemplate(string template)
{ {
static readonly Regex _interpolationRegex = new Regex(@"\{([^}]+)\}", RegexOptions.Compiled | RegexOptions.CultureInvariant); 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) public string Render(JObject model)
{ {
model = (JObject)ToLowerCase(model); model = (JObject)ToLowerCase(model);
@@ -23,11 +26,11 @@ public class TextTemplate(string template)
try try
{ {
var token = model.SelectToken(path); var token = model.SelectToken(path);
return token?.ToString() ?? $"<NotFound({initial})>"; return token?.ToString() ?? NotFoundReplacement(initial);
} }
catch catch
{ {
return $"<ParsingError({initial})>"; return ParsingErrorReplacement(initial);
} }
}); });
} }

View File

@@ -39,6 +39,12 @@ public class EmailRuleData : BaseEntityData
[Required] [Required]
[Column("to")] [Column("to")]
public string[] To { get; set; } = null!; 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] [Required]
[Column("subject")] [Column("subject")]

View File

@@ -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");
}
}
}

View File

@@ -280,11 +280,21 @@ namespace BTCPayServer.Migrations
.HasColumnName("additional_data") .HasColumnName("additional_data")
.HasDefaultValueSql("'{}'::jsonb"); .HasDefaultValueSql("'{}'::jsonb");
b.Property<string[]>("BCC")
.IsRequired()
.HasColumnType("text[]")
.HasColumnName("bcc");
b.Property<string>("Body") b.Property<string>("Body")
.IsRequired() .IsRequired()
.HasColumnType("text") .HasColumnType("text")
.HasColumnName("body"); .HasColumnName("body");
b.Property<string[]>("CC")
.IsRequired()
.HasColumnType("text[]")
.HasColumnName("cc");
b.Property<string>("Condition") b.Property<string>("Condition")
.HasColumnType("text") .HasColumnType("text")
.HasColumnName("condition"); .HasColumnName("condition");

View File

@@ -20,6 +20,7 @@ using BTCPayServer.HostedServices;
using BTCPayServer.Hosting; using BTCPayServer.Hosting;
using BTCPayServer.JsonConverters; using BTCPayServer.JsonConverters;
using BTCPayServer.Payments; using BTCPayServer.Payments;
using BTCPayServer.Plugins.Emails.Views;
using BTCPayServer.Rating; using BTCPayServer.Rating;
using BTCPayServer.Services; using BTCPayServer.Services;
using BTCPayServer.Services.Apps; using BTCPayServer.Services.Apps;
@@ -2305,6 +2306,16 @@ bc1qfzu57kgu5jthl934f9xrdzzx8mmemx7gn07tf0grnvz504j6kzusu2v0ku
Assert.Equal("1-of-xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL-xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL-[legacy]", parsedDescriptor.AccountDerivation.ToString()); 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] [Fact]
[Trait("Altcoins", "Altcoins")] [Trait("Altcoins", "Altcoins")]
public void CanCalculateCryptoDue2() public void CanCalculateCryptoDue2()

View File

@@ -314,7 +314,7 @@
<a layout-menu-item="@nameof(ServerNavPages.Roles)" asp-controller="UIServer" asp-action="ListRoles" text-translate="true">Roles</a> <a layout-menu-item="@nameof(ServerNavPages.Roles)" asp-controller="UIServer" asp-action="ListRoles" text-translate="true">Roles</a>
</li> </li>
<li class="nav-item nav-item-sub" permission="@Policies.CanModifyServerSettings"> <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>
<li class="nav-item nav-item-sub" permission="@Policies.CanModifyServerSettings"> <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> <a layout-menu-item="@nameof(ServerNavPages.Services)" asp-controller="UIServer" asp-action="Services" text-translate="true">Services</a>

View File

@@ -106,7 +106,9 @@ public class UIEmailRuleControllerBase(
Subject = model.Subject, Subject = model.Subject,
Condition = string.IsNullOrWhiteSpace(model.Condition) ? null : model.Condition, Condition = string.IsNullOrWhiteSpace(model.Condition) ? null : model.Condition,
OfferingId = model.OfferingId, 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); c.SetBTCPayAdditionalData(model.AdditionalData);
ctx.EmailRules.Add(c); ctx.EmailRules.Add(c);
@@ -143,7 +145,9 @@ public class UIEmailRuleControllerBase(
rule.Trigger = model.Trigger; rule.Trigger = model.Trigger;
rule.SetBTCPayAdditionalData(model.AdditionalData); 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.Subject = model.Subject;
rule.Condition = model.Condition; rule.Condition = model.Condition;
rule.Body = model.Body; rule.Body = model.Body;
@@ -170,6 +174,20 @@ public class UIEmailRuleControllerBase(
protected async Task ValidateCondition(StoreEmailRuleViewModel model) 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() ?? ""; model.Condition = model.Condition?.Trim() ?? "";
if (model.Condition.Length == 0) if (model.Condition.Length == 0)
model.Condition = null; model.Condition = null;

View File

@@ -42,77 +42,92 @@ public class EmailsPlugin : BaseBTCPayServerPlugin
var vm = new EmailTriggerViewModel() var vm = new EmailTriggerViewModel()
{ {
Trigger = ServerMailTriggers.PasswordReset, Trigger = ServerMailTriggers.PasswordReset,
RecipientExample = "{User.MailboxAddress}", DefaultEmail = new()
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}")}"), 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() PlaceHolders = new()
{ {
new ("{ResetLink}", "The link to the password reset page") new ("{ResetLink}", "The link to the password reset page")
}, },
Description = "Password Reset Requested", Description = "User: Password Reset Requested",
}; };
vms.Add(vm); vms.Add(vm);
vm = new EmailTriggerViewModel() vm = new EmailTriggerViewModel()
{ {
Trigger = ServerMailTriggers.EmailConfirm, Trigger = ServerMailTriggers.EmailConfirm,
RecipientExample = "{User.MailboxAddress}", DefaultEmail = new()
SubjectExample = "Confirm your email", {
BodyExample = CreateEmailBody($"Please confirm your account.<br/><br/>{CallToAction("Confirm Email", "{ConfirmLink}")}"), To = ["{User.MailboxAddress}"],
Subject = "Confirm your email address",
Body = CreateEmailBody($"Please confirm your account.<br/><br/>{CallToAction("Confirm Email", "{ConfirmLink}")}"),
},
PlaceHolders = new() PlaceHolders = new()
{ {
new ("{ConfirmLink}", "The link used to confirm the user's email address") 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); vms.Add(vm);
vm = new EmailTriggerViewModel() vm = new EmailTriggerViewModel()
{ {
Trigger = ServerMailTriggers.InvitePending, Trigger = ServerMailTriggers.InvitePending,
RecipientExample = "{User.MailboxAddress}", DefaultEmail = new()
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}}"), 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() PlaceHolders = new()
{ {
new ("{InvitationLink}", "The link where the invited user can set up their account"), new ("{InvitationLink}", "The link where the invited user can set up their account"),
new ("{InvitationLinkQR}", "The QR code representation of the invitation link") new ("{InvitationLinkQR}", "The QR code representation of the invitation link")
}, },
Description = "User invitation email", Description = "User: Invitation",
}; };
vms.Add(vm); vms.Add(vm);
vm = new EmailTriggerViewModel() vm = new EmailTriggerViewModel()
{ {
Trigger = ServerMailTriggers.ApprovalConfirmed, Trigger = ServerMailTriggers.ApprovalConfirmed,
RecipientExample = "{User.MailboxAddress}", DefaultEmail = new()
SubjectExample = "Your account has been approved", {
BodyExample = CreateEmailBody($"Your account has been approved and you can now.<br/><br/>{CallToAction("Login here", "{LoginLink}")}"), 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() PlaceHolders = new()
{ {
new ("{LoginLink}", "The link that the user can use to login"), new ("{LoginLink}", "The link that the user can use to login"),
}, },
Description = "User account approved", Description = "User: Account approved",
}; };
vms.Add(vm); vms.Add(vm);
vm = new EmailTriggerViewModel() vm = new EmailTriggerViewModel()
{ {
Trigger = ServerMailTriggers.ApprovalRequest, Trigger = ServerMailTriggers.ApprovalRequest,
RecipientExample = "{Admin.MailboxAddresses}", DefaultEmail = new()
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}")}"), 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() PlaceHolders = new()
{ {
new ("{ApprovalLink}", "The link that the admin needs to use to approve the user"), 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); 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("{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.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

@@ -32,9 +32,9 @@ public class EmailRuleMatchContext(
public TriggerEvent TriggerEvent { get; } = triggerEvent; public TriggerEvent TriggerEvent { get; } = triggerEvent;
public EmailRuleData MatchedRule { get; } = matchedRule; public EmailRuleData MatchedRule { get; } = matchedRule;
public List<MailboxAddress> Recipients { get; set; } = new(); public List<MailboxAddress> To { get; set; } = new();
public List<MailboxAddress> Cc { get; set; } = new(); public List<MailboxAddress> CC { get; set; } = new();
public List<MailboxAddress> Bcc { get; set; } = new(); public List<MailboxAddress> BCC { get; set; } = new();
} }
public class StoreEmailRuleProcessorSender( public class StoreEmailRuleProcessorSender(
@@ -65,41 +65,45 @@ public class StoreEmailRuleProcessorSender(
var body = new TextTemplate(actionableRule.Body ?? ""); var body = new TextTemplate(actionableRule.Body ?? "");
var subject = new TextTemplate(actionableRule.Subject ?? ""); var subject = new TextTemplate(actionableRule.Subject ?? "");
matchedContext.Recipients.AddRange( AddToMatchedContext(triggEvent.Model, matchedContext.To, actionableRule.To);
actionableRule.To AddToMatchedContext(triggEvent.Model, matchedContext.CC, actionableRule.CC);
.SelectMany(o => AddToMatchedContext(triggEvent.Model, matchedContext.BCC, actionableRule.BCC);
{
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)!);
if (triggEvent.Owner is not null) if (triggEvent.Owner is not null)
await triggEvent.Owner.BeforeSending(matchedContext); await triggEvent.Owner.BeforeSending(matchedContext);
if (matchedContext.Recipients.Count == 0) if (matchedContext.To.Count == 0)
continue; 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)!);
}
} }

View File

@@ -1,4 +1,6 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using Newtonsoft.Json;
namespace BTCPayServer.Plugins.Emails.Views; namespace BTCPayServer.Plugins.Emails.Views;
@@ -9,16 +11,21 @@ public class EmailTriggerViewModel
{ {
public class Default public class Default
{ {
public string SubjectExample { get; set; } public string Subject { get; set; }
public string BodyExample { 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 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 Default DefaultEmail { get; set; }
public class PlaceHolder(string name, string description) public class PlaceHolder(string name, string description)
{ {

View File

@@ -91,12 +91,25 @@
<div class="form-text" text-translate="true">Only send email when the specified JSON Path exists</div> <div class="form-text" text-translate="true">Only send email when the specified JSON Path exists</div>
</div> </div>
@{
var placeholder = "\"John Smith\" <john.smith@example.com>, john.smith@example.com, {Placeholder}";
}
<div class="form-group"> <div class="form-group">
<label asp-for="To" class="form-label" text-translate="true">Recipients</label> <label asp-for="To" class="form-label" text-translate="true">To</label>
<input type="text" asp-for="To" class="form-control email-rule-to" /> <input type="text" asp-for="To" placeholder="@placeholder" class="form-control email-rule-to" />
<span asp-validation-for="To" class="text-danger"></span> <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 class="form-text" text-translate="true">Who to send the email to. For multiple emails, separate with a comma.</div>
</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"> <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" /> <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 triggerSelect = document.querySelector('.email-rule-trigger') ?? document.querySelector('.email-rule-trigger-hidden');
const subjectInput = document.querySelector('.email-rule-subject'); 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 bodyTextarea = document.querySelector('.email-rule-body');
const placeholdersTd = document.querySelector('#placeholders'); const placeholdersTd = document.querySelector('#placeholders');
function join(arr) {
return arr ? arr.join(', ') : '';
}
function applyTemplate() { function applyTemplate() {
const selectedTrigger = triggerSelect.value; const selectedTrigger = triggerSelect.value;
if (triggersByType[selectedTrigger]) { if (triggersByType[selectedTrigger]) {
subjectInput.value = triggersByType[selectedTrigger].subjectExample; subjectInput.value = triggersByType[selectedTrigger].defaultEmail.subject;
recipientInput.value = triggersByType[selectedTrigger].recipientExample; toInput.value = join(triggersByType[selectedTrigger].defaultEmail.to);
var body = triggersByType[selectedTrigger].bodyExample; ccInput.value = join(triggersByType[selectedTrigger].defaultEmail.cc);
bccInput.value = join(triggersByType[selectedTrigger].defaultEmail.bcc);
var body = triggersByType[selectedTrigger].defaultEmail.body;
if ($(bodyTextarea).summernote) { if ($(bodyTextarea).summernote) {
console.log(body); console.log(body);
@@ -166,7 +186,13 @@
} else { } else {
bodyTextarea.value = body; bodyTextarea.value = body;
} }
applyPlaceholders();
}
}
function applyPlaceholders()
{
const selectedTrigger = triggerSelect.value;
if (triggersByType[selectedTrigger]) {
placeholdersTd.innerHTML = ''; placeholdersTd.innerHTML = '';
triggersByType[selectedTrigger].placeHolders.forEach(p => { triggersByType[selectedTrigger].placeHolders.forEach(p => {
const tr = document.createElement('tr'); const tr = document.createElement('tr');
@@ -180,7 +206,6 @@
tr.appendChild(td2); tr.appendChild(td2);
placeholdersTd.appendChild(tr); placeholdersTd.appendChild(tr);
}); });
} }
} }
@@ -205,6 +230,10 @@
{ {
applyTemplate(); applyTemplate();
} }
else
{
applyPlaceholders();
}
toggleCustomerEmailVisibility(); toggleCustomerEmailVisibility();
} }
}); });

View File

@@ -2,7 +2,9 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Linq; using System.Linq;
using System.Net.Mail;
using BTCPayServer.Data; using BTCPayServer.Data;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Plugins.Emails.Views; namespace BTCPayServer.Plugins.Emails.Views;
@@ -25,6 +27,8 @@ public class StoreEmailRuleViewModel
Condition = data.Condition ?? ""; Condition = data.Condition ?? "";
Body = data.Body; Body = data.Body;
To = string.Join(",", data.To); To = string.Join(",", data.To);
CC = string.Join(",", data.CC);
BCC = string.Join(",", data.BCC);
} }
else else
{ {
@@ -46,6 +50,8 @@ public class StoreEmailRuleViewModel
public EmailRuleData Data { get; set; } public EmailRuleData Data { get; set; }
public EmailRuleData.BTCPayAdditionalData AdditionalData { get; set; } public EmailRuleData.BTCPayAdditionalData AdditionalData { get; set; }
public string To { get; set; } public string To { get; set; }
public string CC { get; set; }
public string BCC { get; set; }
public List<EmailTriggerViewModel> Triggers { get; set; } public List<EmailTriggerViewModel> Triggers { get; set; }
public string RedirectUrl { get; set; } public string RedirectUrl { get; set; }
@@ -54,8 +60,29 @@ public class StoreEmailRuleViewModel
public string OfferingId { get; set; } public string OfferingId { get; set; }
public string StoreId { get; set; } public string StoreId { get; set; }
public string[] ToAsArray() public string[] AsArray(string values)
=> (To ?? "").Split(',', StringSplitOptions.RemoveEmptyEntries) {
.Select(t => t.Trim()) // This replace the placeholders with random email addresses
.ToArray(); // 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();
}
} }

View File

@@ -43,18 +43,17 @@ public class SubscriptionsPlugin : BaseBTCPayServerPlugin
var placeHolders = new List<EmailTriggerViewModel.PlaceHolder>() var placeHolders = new List<EmailTriggerViewModel.PlaceHolder>()
{ {
new ("{Plan.Id}", "Plan ID"), new("{Plan.Id}", "Plan ID"),
new ("{Plan.Name}", "Plan name"), new("{Plan.Name}", "Plan name"),
new ("{Offering.Name}", "Offering name"), new("{Offering.Name}", "Offering name"),
new ("{Offering.Id}", "Offering ID"), new("{Offering.Id}", "Offering ID"),
new ("{Offering.AppId}", "Offering app ID"), new("{Offering.AppId}", "Offering app ID"),
new ("{Offering.Metadata}*", "Offering metadata"), new("{Offering.Metadata}*", "Offering metadata"),
new ("{Subscriber.Phase}", "Subscriber phase"), new("{Subscriber.Phase}", "Subscriber phase"),
new ("{Subscriber.Email}", "Subscriber email"), new("{Subscriber.Email}", "Subscriber email"),
new ("{Customer.ExternalRef}", "Customer external reference"), new("{Customer.ExternalRef}", "Customer external reference"),
new ("{Customer.Name}", "Customer name"), new("{Customer.Name}", "Customer name"),
new ("{Customer.Metadata}*", "Customer metadata") new("{Customer.Metadata}*", "Customer metadata")
}.AddStoresPlaceHolders(); }.AddStoresPlaceHolders();
var viewModels = new List<EmailTriggerViewModel>() var viewModels = new List<EmailTriggerViewModel>()
@@ -63,72 +62,99 @@ public class SubscriptionsPlugin : BaseBTCPayServerPlugin
{ {
Trigger = WebhookSubscriptionEvent.SubscriberCreated, Trigger = WebhookSubscriptionEvent.SubscriberCreated,
Description = "Subscription - New subscriber", Description = "Subscription - New subscriber",
SubjectExample = "Welcome {Customer.Name}!", DefaultEmail = new()
BodyExample = "Hello {Customer.Name},\n\nThank you for subscribing to our service.\n\nRegards,\n{Store.Name}", {
Subject = "Welcome {Customer.Name}!",
Body = "Hello {Customer.Name},\n\nThank you for subscribing to our service.\n\nRegards,\n{Store.Name}"
},
PlaceHolders = placeHolders PlaceHolders = placeHolders
}, },
new() new()
{ {
Trigger = WebhookSubscriptionEvent.SubscriberCredited, Trigger = WebhookSubscriptionEvent.SubscriberCredited,
Description = "Subscription - Subscriber credited", Description = "Subscription - Subscriber credited",
SubjectExample = "Your subscription has been credited", DefaultEmail = new()
BodyExample = "Hello {Customer.Name},\n\nYour subscription has been credited.\n\nRegards,\n{Store.Name}", {
Subject = "Your subscription has been credited",
Body = "Hello {Customer.Name},\n\nYour subscription has been credited.\n\nRegards,\n{Store.Name}"
},
PlaceHolders = placeHolders PlaceHolders = placeHolders
}, },
new() new()
{ {
Trigger = WebhookSubscriptionEvent.SubscriberCharged, Trigger = WebhookSubscriptionEvent.SubscriberCharged,
Description = "Subscription - Subscriber charged", Description = "Subscription - Subscriber charged",
SubjectExample = "Your subscription payment has been processed", DefaultEmail = new()
BodyExample = "Hello {Customer.Name},\n\nYour subscription payment for {Plan.Name} has been processed.\n\nRegards,\n{Store.Name}", {
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 PlaceHolders = placeHolders
}, },
new() new()
{ {
Trigger = WebhookSubscriptionEvent.SubscriberActivated, Trigger = WebhookSubscriptionEvent.SubscriberActivated,
Description = "Subscription - Subscriber activated", Description = "Subscription - Subscriber activated",
SubjectExample = "Your subscription is now active", DefaultEmail = new()
BodyExample = "Hello {Customer.Name},\n\nYour subscription to {Plan.Name} is now active.\n\nRegards,\n{Store.Name}", {
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 PlaceHolders = placeHolders
}, },
new() new()
{ {
Trigger = WebhookSubscriptionEvent.SubscriberPhaseChanged, Trigger = WebhookSubscriptionEvent.SubscriberPhaseChanged,
Description = "Subscription - Subscriber phase changed", Description = "Subscription - Subscriber phase changed",
SubjectExample = "Your subscription phase has changed", DefaultEmail = new()
BodyExample = "Hello {Customer.Name},\n\nYour subscription phase has been updated to {Subscriber.Phase}.\n\nRegards,\n{Store.Name}", {
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 PlaceHolders = placeHolders
}, },
new() new()
{ {
Trigger = WebhookSubscriptionEvent.SubscriberDisabled, Trigger = WebhookSubscriptionEvent.SubscriberDisabled,
Description = "Subscription - Subscriber disabled", Description = "Subscription - Subscriber disabled",
SubjectExample = "Your subscription has been disabled", DefaultEmail = new()
BodyExample = "Hello {Customer.Name},\n\nYour subscription has been disabled.\n\nRegards,\n{Store.Name}", {
Subject = "Your subscription has been disabled",
Body = "Hello {Customer.Name},\n\nYour subscription has been disabled.\n\nRegards,\n{Store.Name}"
},
PlaceHolders = placeHolders PlaceHolders = placeHolders
}, },
new() new()
{ {
Trigger = WebhookSubscriptionEvent.PaymentReminder, Trigger = WebhookSubscriptionEvent.PaymentReminder,
Description = "Subscription - Payment reminder", Description = "Subscription - Payment reminder",
SubjectExample = "Payment reminder for your subscription", DefaultEmail = new()
BodyExample = "Hello {Customer.Name},\n\nThis is a reminder about your upcoming subscription payment.\n\nRegards,\n{Store.Name}", {
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 PlaceHolders = placeHolders
}, },
new() new()
{ {
Trigger = WebhookSubscriptionEvent.PlanStarted, Trigger = WebhookSubscriptionEvent.PlanStarted,
Description = "Subscription - Plan started", Description = "Subscription - Plan started",
SubjectExample = "Your subscription plan has started", DefaultEmail = new()
BodyExample = "Hello {Customer.Name},\n\nYour subscription plan {Plan.Name} has started.\n\nRegards,\n{Store.Name}", {
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 PlaceHolders = placeHolders
}, },
new() new()
{ {
Trigger = WebhookSubscriptionEvent.SubscriberNeedUpgrade, Trigger = WebhookSubscriptionEvent.SubscriberNeedUpgrade,
Description = "Subscription - Need upgrade", Description = "Subscription - Need upgrade",
SubjectExample = "Your subscription needs to be upgraded", DefaultEmail = new()
BodyExample = "Hello {Customer.Name},\n\nYour subscription needs to be upgraded to continue using our service.\n\nRegards,\n{Store.Name}", {
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 PlaceHolders = placeHolders
}, },
}; };
@@ -141,6 +167,7 @@ public class SubscriptionsAppType(
IOptions<BTCPayServerOptions> btcPayServerOptions) : AppBaseType(AppType) IOptions<BTCPayServerOptions> btcPayServerOptions) : AppBaseType(AppType)
{ {
public const string AppType = "Subscriptions"; public const string AppType = "Subscriptions";
public class AppConfig public class AppConfig
{ {
public string OfferingId { get; set; } = null!; public string OfferingId { get; set; } = null!;

View File

@@ -42,7 +42,7 @@ public class InvoiceTriggerProvider(LinkGenerator linkGenerator)
context.MatchedRule.GetBTCPayAdditionalData()?.CustomerEmail is true && context.MatchedRule.GetBTCPayAdditionalData()?.CustomerEmail is true &&
MailboxAddressValidator.TryParse(email, out var mb)) MailboxAddressValidator.TryParse(email, out var mb))
{ {
context.Recipients.Insert(0, mb); context.To.Insert(0, mb);
} }
return Task.CompletedTask; return Task.CompletedTask;
} }

View File

@@ -43,7 +43,7 @@ public class PaymentRequestTriggerProvider(LinkGenerator linkGenerator)
context.MatchedRule.GetBTCPayAdditionalData()?.CustomerEmail is true && context.MatchedRule.GetBTCPayAdditionalData()?.CustomerEmail is true &&
MailboxAddressValidator.TryParse(email, out var mb)) MailboxAddressValidator.TryParse(email, out var mb))
{ {
context.Recipients.Insert(0, mb); context.To.Insert(0, mb);
} }
return Task.CompletedTask; return Task.CompletedTask;
} }

View File

@@ -1,4 +1,4 @@
#nullable enable #nullable enable
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
@@ -53,7 +53,7 @@ public class WebhooksPlugin : BaseBTCPayServerPlugin
AddPendingTransactionWebhooks(services); AddPendingTransactionWebhooks(services);
} }
private static void AddPendingTransactionWebhooks(IServiceCollection services) private static void AddPendingTransactionWebhooks(IServiceCollection services)
{ {
services.AddWebhookTriggerProvider<PendingTransactionTriggerProvider>(); services.AddWebhookTriggerProvider<PendingTransactionTriggerProvider>();
var pendingTransactionsPlaceholders = new List<EmailTriggerViewModel.PlaceHolder>() var pendingTransactionsPlaceholders = new List<EmailTriggerViewModel.PlaceHolder>()
@@ -73,32 +73,45 @@ public class WebhooksPlugin : BaseBTCPayServerPlugin
{ {
Trigger = PendingTransactionTriggerProvider.PendingTransactionCreated, Trigger = PendingTransactionTriggerProvider.PendingTransactionCreated,
Description = "Pending Transaction - Created", Description = "Pending Transaction - Created",
SubjectExample = "Pending Transaction {PendingTransaction.TrimmedId} Created", DefaultEmail = new()
BodyExample = "Review the transaction {PendingTransaction.Id} and sign it on: {PendingTransaction.Link}", {
Subject = "Pending Transaction {PendingTransaction.TrimmedId} Created",
Body = "Review the transaction {PendingTransaction.Id} and sign it on: {PendingTransaction.Link}"
},
PlaceHolders = pendingTransactionsPlaceholders PlaceHolders = pendingTransactionsPlaceholders
}, },
new() new()
{ {
Trigger = PendingTransactionTriggerProvider.PendingTransactionSignatureCollected, Trigger = PendingTransactionTriggerProvider.PendingTransactionSignatureCollected,
Description = "Pending Transaction - Signature Collected", Description = "Pending Transaction - Signature Collected",
SubjectExample = "Signature Collected for Pending Transaction {PendingTransaction.TrimmedId}", DefaultEmail = new()
BodyExample = "So far {PendingTransaction.SignaturesCollected} signatures collected out of {PendingTransaction.SignaturesNeeded} signatures needed. ", {
Subject = "Signature Collected for Pending Transaction {PendingTransaction.TrimmedId}",
Body =
"So far {PendingTransaction.SignaturesCollected} signatures collected out of {PendingTransaction.SignaturesNeeded} signatures needed. "
},
PlaceHolders = pendingTransactionsPlaceholders PlaceHolders = pendingTransactionsPlaceholders
}, },
new() new()
{ {
Trigger = PendingTransactionTriggerProvider.PendingTransactionBroadcast, Trigger = PendingTransactionTriggerProvider.PendingTransactionBroadcast,
Description = "Pending Transaction - Broadcast", Description = "Pending Transaction - Broadcast",
SubjectExample = "Transaction {PendingTransaction.TrimmedId} has been Broadcast", DefaultEmail = new()
BodyExample = "Transaction is visible in mempool on: https://mempool.space/tx/{PendingTransaction.Id}. ", {
Subject = "Transaction {PendingTransaction.TrimmedId} has been Broadcast",
Body = "Transaction is visible in mempool on: https://mempool.space/tx/{PendingTransaction.Id}. "
},
PlaceHolders = pendingTransactionsPlaceholders PlaceHolders = pendingTransactionsPlaceholders
}, },
new() new()
{ {
Trigger = PendingTransactionTriggerProvider.PendingTransactionCancelled, Trigger = PendingTransactionTriggerProvider.PendingTransactionCancelled,
Description = "Pending Transaction - Cancelled", Description = "Pending Transaction - Cancelled",
SubjectExample = "Pending Transaction {PendingTransaction.TrimmedId} Cancelled", DefaultEmail = new()
BodyExample = "Transaction {PendingTransaction.Id} is cancelled and signatures are no longer being collected. ", {
Subject = "Pending Transaction {PendingTransaction.TrimmedId} Cancelled",
Body = "Transaction {PendingTransaction.Id} is cancelled and signatures are no longer being collected. "
},
PlaceHolders = pendingTransactionsPlaceholders PlaceHolders = pendingTransactionsPlaceholders
} }
}; };
@@ -130,46 +143,61 @@ public class WebhooksPlugin : BaseBTCPayServerPlugin
{ {
Trigger = WebhookEventType.PaymentRequestCreated, Trigger = WebhookEventType.PaymentRequestCreated,
Description = "Payment Request - Created", Description = "Payment Request - Created",
SubjectExample = "Payment Request {PaymentRequest.Id} created", DefaultEmail = new()
BodyExample = "Payment Request {PaymentRequest.Id} ({PaymentRequest.Title}) created.", {
PlaceHolders = paymentRequestPlaceholders, Subject = "Payment Request {PaymentRequest.Id} created",
CanIncludeCustomerEmail = true Body = "Payment Request {PaymentRequest.Id} ({PaymentRequest.Title}) created.",
CanIncludeCustomerEmail = true
},
PlaceHolders = paymentRequestPlaceholders
}, },
new() new()
{ {
Trigger = WebhookEventType.PaymentRequestUpdated, Trigger = WebhookEventType.PaymentRequestUpdated,
Description = "Payment Request - Updated", Description = "Payment Request - Updated",
SubjectExample = "Payment Request {PaymentRequest.Id} updated", DefaultEmail = new()
BodyExample = "Payment Request {PaymentRequest.Id} ({PaymentRequest.Title}) updated.", {
PlaceHolders = paymentRequestPlaceholders, Subject = "Payment Request {PaymentRequest.Id} updated",
CanIncludeCustomerEmail = true Body = "Payment Request {PaymentRequest.Id} ({PaymentRequest.Title}) updated.",
CanIncludeCustomerEmail = true
},
PlaceHolders = paymentRequestPlaceholders
}, },
new() new()
{ {
Trigger = WebhookEventType.PaymentRequestArchived, Trigger = WebhookEventType.PaymentRequestArchived,
Description = "Payment Request - Archived", Description = "Payment Request - Archived",
SubjectExample = "Payment Request {PaymentRequest.Id} archived", DefaultEmail = new()
BodyExample = "Payment Request {PaymentRequest.Id} ({PaymentRequest.Title}) archived.", {
PlaceHolders = paymentRequestPlaceholders, Subject = "Payment Request {PaymentRequest.Id} archived",
CanIncludeCustomerEmail = true Body = "Payment Request {PaymentRequest.Id} ({PaymentRequest.Title}) archived.",
CanIncludeCustomerEmail = true
},
PlaceHolders = paymentRequestPlaceholders
}, },
new() new()
{ {
Trigger = WebhookEventType.PaymentRequestStatusChanged, Trigger = WebhookEventType.PaymentRequestStatusChanged,
Description = "Payment Request - Status Changed", Description = "Payment Request - Status Changed",
SubjectExample = "Payment Request {PaymentRequest.Id} status changed", DefaultEmail = new()
BodyExample = "Payment Request {PaymentRequest.Id} ({PaymentRequest.Title}) status changed to {PaymentRequest.Status}.", {
PlaceHolders = paymentRequestPlaceholders, Subject = "Payment Request {PaymentRequest.Id} status changed",
CanIncludeCustomerEmail = true Body = "Payment Request {PaymentRequest.Id} ({PaymentRequest.Title}) status changed to {PaymentRequest.Status}.",
CanIncludeCustomerEmail = true
},
PlaceHolders = paymentRequestPlaceholders
}, },
new() new()
{ {
Trigger = WebhookEventType.PaymentRequestCompleted, Trigger = WebhookEventType.PaymentRequestCompleted,
Description = "Payment Request - Completed", Description = "Payment Request - Completed",
SubjectExample = "Payment Request {PaymentRequest.Title} {PaymentRequest.ReferenceId} Completed", DefaultEmail = new()
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, Subject = "Payment Request {PaymentRequest.Title} {PaymentRequest.ReferenceId} Completed",
CanIncludeCustomerEmail = true 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); services.AddWebhookTriggerViewModels(paymentRequestTriggers);
@@ -192,24 +220,33 @@ public class WebhooksPlugin : BaseBTCPayServerPlugin
{ {
Trigger = WebhookEventType.PayoutCreated, Trigger = WebhookEventType.PayoutCreated,
Description = "Payout - Created", Description = "Payout - Created",
SubjectExample = "Payout {Payout.Id} created", DefaultEmail = new()
BodyExample = "Payout {Payout.Id} (Pull Payment Id: {Payout.PullPaymentId}) created.", {
Subject = "Payout {Payout.Id} created",
Body = "Payout {Payout.Id} (Pull Payment Id: {Payout.PullPaymentId}) created."
},
PlaceHolders = payoutPlaceholders PlaceHolders = payoutPlaceholders
}, },
new() new()
{ {
Trigger = WebhookEventType.PayoutApproved, Trigger = WebhookEventType.PayoutApproved,
Description = "Payout - Approved", Description = "Payout - Approved",
SubjectExample = "Payout {Payout.Id} approved", DefaultEmail = new()
BodyExample = "Payout {Payout.Id} (Pull Payment Id: {Payout.PullPaymentId}) approved.", {
Subject = "Payout {Payout.Id} approved",
Body = "Payout {Payout.Id} (Pull Payment Id: {Payout.PullPaymentId}) approved."
},
PlaceHolders = payoutPlaceholders PlaceHolders = payoutPlaceholders
}, },
new() new()
{ {
Trigger = WebhookEventType.PayoutUpdated, Trigger = WebhookEventType.PayoutUpdated,
Description = "Payout - Updated", Description = "Payout - Updated",
SubjectExample = "Payout {Payout.Id} updated", DefaultEmail = new()
BodyExample = "Payout {Payout.Id} (Pull Payment Id: {Payout.PullPaymentId}) updated.", {
Subject = "Payout {Payout.Id} updated",
Body = "Payout {Payout.Id} (Pull Payment Id: {Payout.PullPaymentId}) updated."
},
PlaceHolders = payoutPlaceholders PlaceHolders = payoutPlaceholders
} }
}; };
@@ -238,82 +275,110 @@ public class WebhooksPlugin : BaseBTCPayServerPlugin
{ {
Trigger = WebhookEventType.InvoiceCreated, Trigger = WebhookEventType.InvoiceCreated,
Description = "Invoice - Created", Description = "Invoice - Created",
SubjectExample = "Invoice {Invoice.Id} created", DefaultEmail = new()
BodyExample = "Invoice {Invoice.Id} (Order Id: {Invoice.OrderId}) created.", {
PlaceHolders = invoicePlaceholders, Subject = "Invoice {Invoice.Id} created",
CanIncludeCustomerEmail = true Body = "Invoice {Invoice.Id} (Order Id: {Invoice.OrderId}) created.",
CanIncludeCustomerEmail = true
},
PlaceHolders = invoicePlaceholders
}, },
new() new()
{ {
Trigger = WebhookEventType.InvoiceReceivedPayment, Trigger = WebhookEventType.InvoiceReceivedPayment,
Description = "Invoice - Received Payment", Description = "Invoice - Received Payment",
SubjectExample = "Invoice {Invoice.Id} received payment", DefaultEmail = new()
BodyExample = "Invoice {Invoice.Id} (Order Id: {Invoice.OrderId}) received payment.", {
PlaceHolders = invoicePlaceholders, Subject = "Invoice {Invoice.Id} received payment",
CanIncludeCustomerEmail = true Body = "Invoice {Invoice.Id} (Order Id: {Invoice.OrderId}) received payment.",
CanIncludeCustomerEmail = true
},
PlaceHolders = invoicePlaceholders
}, },
new() new()
{ {
Trigger = WebhookEventType.InvoiceProcessing, Trigger = WebhookEventType.InvoiceProcessing,
Description = "Invoice - Is Processing", Description = "Invoice - Is Processing",
SubjectExample = "Invoice {Invoice.Id} processing", DefaultEmail = new()
BodyExample = "Invoice {Invoice.Id} (Order Id: {Invoice.OrderId}) is processing.", {
PlaceHolders = invoicePlaceholders, Subject = "Invoice {Invoice.Id} processing",
CanIncludeCustomerEmail = true Body = "Invoice {Invoice.Id} (Order Id: {Invoice.OrderId}) is processing.",
CanIncludeCustomerEmail = true
},
PlaceHolders = invoicePlaceholders
}, },
new() new()
{ {
Trigger = WebhookEventType.InvoiceExpired, Trigger = WebhookEventType.InvoiceExpired,
Description = "Invoice - Expired", Description = "Invoice - Expired",
SubjectExample = "Invoice {Invoice.Id} expired", DefaultEmail = new()
BodyExample = "Invoice {Invoice.Id} (Order Id: {Invoice.OrderId}) expired.", {
PlaceHolders = invoicePlaceholders, Subject = "Invoice {Invoice.Id} expired",
CanIncludeCustomerEmail = true Body = "Invoice {Invoice.Id} (Order Id: {Invoice.OrderId}) expired.",
CanIncludeCustomerEmail = true
},
PlaceHolders = invoicePlaceholders
}, },
new() new()
{ {
Trigger = WebhookEventType.InvoiceSettled, Trigger = WebhookEventType.InvoiceSettled,
Description = "Invoice - Is Settled", Description = "Invoice - Is Settled",
SubjectExample = "Invoice {Invoice.Id} settled", DefaultEmail = new()
BodyExample = "Invoice {Invoice.Id} (Order Id: {Invoice.OrderId}) is settled.", {
PlaceHolders = invoicePlaceholders, Subject = "Invoice {Invoice.Id} settled",
CanIncludeCustomerEmail = true Body = "Invoice {Invoice.Id} (Order Id: {Invoice.OrderId}) is settled.",
CanIncludeCustomerEmail = true
},
PlaceHolders = invoicePlaceholders
}, },
new() new()
{ {
Trigger = WebhookEventType.InvoiceInvalid, Trigger = WebhookEventType.InvoiceInvalid,
Description = "Invoice - Became Invalid", Description = "Invoice - Became Invalid",
SubjectExample = "Invoice {Invoice.Id} invalid", DefaultEmail = new()
BodyExample = "Invoice {Invoice.Id} (Order Id: {Invoice.OrderId}) invalid.", {
PlaceHolders = invoicePlaceholders, Subject = "Invoice {Invoice.Id} invalid",
CanIncludeCustomerEmail = true Body = "Invoice {Invoice.Id} (Order Id: {Invoice.OrderId}) invalid.",
CanIncludeCustomerEmail = true
},
PlaceHolders = invoicePlaceholders
}, },
new() new()
{ {
Trigger = WebhookEventType.InvoicePaymentSettled, Trigger = WebhookEventType.InvoicePaymentSettled,
Description = "Invoice - Payment Settled", Description = "Invoice - Payment Settled",
SubjectExample = "Invoice {Invoice.Id} payment settled", DefaultEmail = new()
BodyExample = "Invoice {Invoice.Id} (Order Id: {Invoice.OrderId}) payment settled.", {
PlaceHolders = invoicePlaceholders, Subject = "Invoice {Invoice.Id} payment settled",
CanIncludeCustomerEmail = true Body = "Invoice {Invoice.Id} (Order Id: {Invoice.OrderId}) payment settled.",
CanIncludeCustomerEmail = true
},
PlaceHolders = invoicePlaceholders
}, },
new() new()
{ {
Trigger = WebhookEventType.InvoiceExpiredPaidPartial, Trigger = WebhookEventType.InvoiceExpiredPaidPartial,
Description = "Invoice - Expired Paid Partial", Description = "Invoice - Expired Paid Partial",
SubjectExample = "Invoice {Invoice.Id} expired with partial payment", DefaultEmail = new()
BodyExample = "Invoice {Invoice.Id} (Order Id: {Invoice.OrderId}) expired with partial payment. \nPlease review and take appropriate action: {Invoice.Link}", {
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, PlaceHolders = invoicePlaceholders,
CanIncludeCustomerEmail = true
}, },
new() new()
{ {
Trigger = WebhookEventType.InvoicePaidAfterExpiration, Trigger = WebhookEventType.InvoicePaidAfterExpiration,
Description = "Invoice - Expired Paid Late", Description = "Invoice - Expired Paid Late",
SubjectExample = "Invoice {Invoice.Id} paid after expiration", DefaultEmail = new()
BodyExample = "Invoice {Invoice.Id} (Order Id: {Invoice.OrderId}) paid after expiration.", {
PlaceHolders = invoicePlaceholders, Subject = "Invoice {Invoice.Id} paid after expiration",
CanIncludeCustomerEmail = true Body = "Invoice {Invoice.Id} (Order Id: {Invoice.OrderId}) paid after expiration.",
CanIncludeCustomerEmail = true
},
PlaceHolders = invoicePlaceholders
} }
}; };
services.AddWebhookTriggerViewModels(emailTriggers); services.AddWebhookTriggerViewModels(emailTriggers);