mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2025-12-17 05:54:26 +01:00
Add CC and BCC to emails
This commit is contained in:
@@ -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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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")]
|
||||||
|
|||||||
43
BTCPayServer.Data/Migrations/20251107131717_emailccbcc.cs
Normal file
43
BTCPayServer.Data/Migrations/20251107131717_emailccbcc.cs
Normal 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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");
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>)"),
|
||||||
|
|||||||
@@ -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,14 +65,27 @@ 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);
|
||||||
|
AddToMatchedContext(triggEvent.Model, matchedContext.BCC, actionableRule.BCC);
|
||||||
|
|
||||||
|
if (triggEvent.Owner is not null)
|
||||||
|
await triggEvent.Owner.BeforeSending(matchedContext);
|
||||||
|
if (matchedContext.To.Count == 0)
|
||||||
|
continue;
|
||||||
|
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 =>
|
.SelectMany(o =>
|
||||||
{
|
{
|
||||||
if (MailboxAddressValidator.TryParse(o, out var oo))
|
var emails = new TextTemplate(o).Render(model);
|
||||||
return new[] { oo };
|
|
||||||
|
|
||||||
var emails = new TextTemplate(o).Render(triggEvent.Model);
|
|
||||||
MailAddressCollection mailCollection = new();
|
MailAddressCollection mailCollection = new();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -85,21 +98,12 @@ public class StoreEmailRuleProcessorSender(
|
|||||||
|
|
||||||
return mailCollection.Select(a =>
|
return mailCollection.Select(a =>
|
||||||
{
|
{
|
||||||
MailboxAddressValidator.TryParse(a.ToString(), out oo);
|
MailboxAddressValidator.TryParse(a.ToString(), out var oo);
|
||||||
return oo;
|
return oo;
|
||||||
})
|
})
|
||||||
.Where(a => a != null)
|
.Where(a => a != null)
|
||||||
.ToArray();
|
.ToArray();
|
||||||
})
|
})
|
||||||
.Where(o => o != null)!);
|
.Where(o => o != null)!);
|
||||||
|
|
||||||
if (triggEvent.Owner is not null)
|
|
||||||
await triggEvent.Owner.BeforeSending(matchedContext);
|
|
||||||
if (matchedContext.Recipients.Count == 0)
|
|
||||||
continue;
|
|
||||||
sender.SendEmail(matchedContext.Recipients.ToArray(), matchedContext.Cc.ToArray(), matchedContext.Bcc.ToArray(), subject.Render(triggEvent.Model), body.Render(triggEvent.Model));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,7 +54,6 @@ public class SubscriptionsPlugin : BaseBTCPayServerPlugin
|
|||||||
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!;
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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",
|
||||||
|
Body = "Payment Request {PaymentRequest.Id} ({PaymentRequest.Title}) created.",
|
||||||
CanIncludeCustomerEmail = true
|
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",
|
||||||
|
Body = "Payment Request {PaymentRequest.Id} ({PaymentRequest.Title}) updated.",
|
||||||
CanIncludeCustomerEmail = true
|
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",
|
||||||
|
Body = "Payment Request {PaymentRequest.Id} ({PaymentRequest.Title}) archived.",
|
||||||
CanIncludeCustomerEmail = true
|
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",
|
||||||
|
Body = "Payment Request {PaymentRequest.Id} ({PaymentRequest.Title}) status changed to {PaymentRequest.Status}.",
|
||||||
CanIncludeCustomerEmail = true
|
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",
|
||||||
|
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
|
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",
|
||||||
|
Body = "Invoice {Invoice.Id} (Order Id: {Invoice.OrderId}) created.",
|
||||||
CanIncludeCustomerEmail = true
|
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",
|
||||||
|
Body = "Invoice {Invoice.Id} (Order Id: {Invoice.OrderId}) received payment.",
|
||||||
CanIncludeCustomerEmail = true
|
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",
|
||||||
|
Body = "Invoice {Invoice.Id} (Order Id: {Invoice.OrderId}) is processing.",
|
||||||
CanIncludeCustomerEmail = true
|
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",
|
||||||
|
Body = "Invoice {Invoice.Id} (Order Id: {Invoice.OrderId}) expired.",
|
||||||
CanIncludeCustomerEmail = true
|
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",
|
||||||
|
Body = "Invoice {Invoice.Id} (Order Id: {Invoice.OrderId}) is settled.",
|
||||||
CanIncludeCustomerEmail = true
|
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",
|
||||||
|
Body = "Invoice {Invoice.Id} (Order Id: {Invoice.OrderId}) invalid.",
|
||||||
CanIncludeCustomerEmail = true
|
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",
|
||||||
|
Body = "Invoice {Invoice.Id} (Order Id: {Invoice.OrderId}) payment settled.",
|
||||||
CanIncludeCustomerEmail = true
|
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}",
|
{
|
||||||
PlaceHolders = invoicePlaceholders,
|
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
|
CanIncludeCustomerEmail = true
|
||||||
},
|
},
|
||||||
|
PlaceHolders = invoicePlaceholders,
|
||||||
|
|
||||||
|
},
|
||||||
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",
|
||||||
|
Body = "Invoice {Invoice.Id} (Order Id: {Invoice.OrderId}) paid after expiration.",
|
||||||
CanIncludeCustomerEmail = true
|
CanIncludeCustomerEmail = true
|
||||||
|
},
|
||||||
|
PlaceHolders = invoicePlaceholders
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
services.AddWebhookTriggerViewModels(emailTriggers);
|
services.AddWebhookTriggerViewModels(emailTriggers);
|
||||||
|
|||||||
Reference in New Issue
Block a user