mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-02-20 13:34:37 +01:00
Store Emails (#3611)
* Store Emails * fix test * Update email rules layout * Cleanups * Test cleanups * Add back comments * Update view; add test * Show email rules link even if email settings aren't completed * Validate email addresses * No redirect, display warning * Fix test * Refactoring: Change email argument types to MailAddress * Test fix * Refactoring: Use MailboxAddress * Parse emails properly in controllers and backend Co-authored-by: Dennis Reimann <mail@dennisreimann.de> Co-authored-by: nicolas.dorier <nicolas.dorier@gmail.com>
This commit is contained in:
parent
af93cf2adb
commit
c2d72e71aa
30 changed files with 429 additions and 120 deletions
|
@ -1,7 +1,3 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace BTCPayServer.Client.Models
|
||||
{
|
||||
public enum WebhookEventType
|
||||
|
|
|
@ -22,13 +22,12 @@ namespace BTCPayServer.Tests
|
|||
[Fact(Timeout = TestTimeout)]
|
||||
public async Task CanHandleRefundEmailForm()
|
||||
{
|
||||
|
||||
using var s = CreateSeleniumTester();
|
||||
await s.StartAsync();
|
||||
s.GoToRegister();
|
||||
s.RegisterNewUser();
|
||||
s.CreateNewStore();
|
||||
s.AddDerivationScheme("BTC");
|
||||
s.AddDerivationScheme();
|
||||
s.GoToStore(StoreNavPages.CheckoutAppearance);
|
||||
s.Driver.FindElement(By.Id("RequiresRefundEmail")).Click();
|
||||
s.Driver.FindElement(By.Name("command")).Click();
|
||||
|
@ -70,14 +69,13 @@ namespace BTCPayServer.Tests
|
|||
[Fact(Timeout = TestTimeout)]
|
||||
public async Task CanHandleRefundEmailForm2()
|
||||
{
|
||||
|
||||
using var s = CreateSeleniumTester();
|
||||
// Prepare user account and store
|
||||
await s.StartAsync();
|
||||
s.GoToRegister();
|
||||
s.RegisterNewUser();
|
||||
s.CreateNewStore();
|
||||
s.AddDerivationScheme("BTC");
|
||||
s.AddDerivationScheme();
|
||||
|
||||
// Now create an invoice that requires a refund email
|
||||
var invoice = s.CreateInvoice(100, "USD", "", null, true);
|
||||
|
@ -135,7 +133,7 @@ namespace BTCPayServer.Tests
|
|||
s.GoToRegister();
|
||||
s.RegisterNewUser();
|
||||
s.CreateNewStore();
|
||||
s.AddDerivationScheme("BTC");
|
||||
s.AddDerivationScheme();
|
||||
|
||||
var invoiceId = s.CreateInvoice();
|
||||
s.GoToInvoiceCheckout(invoiceId);
|
||||
|
@ -166,7 +164,7 @@ namespace BTCPayServer.Tests
|
|||
s.RegisterNewUser(true);
|
||||
s.CreateNewStore();
|
||||
s.AddLightningNode();
|
||||
s.AddDerivationScheme("BTC");
|
||||
s.AddDerivationScheme();
|
||||
|
||||
var invoiceId = s.CreateInvoice(defaultPaymentMethod: "BTC_LightningLike");
|
||||
s.GoToInvoiceCheckout(invoiceId);
|
||||
|
|
|
@ -2353,10 +2353,8 @@ namespace BTCPayServer.Tests
|
|||
await AssertPermissionError(Policies.CanModifyStoreSettings, async () => await client.RemoveStoreUser(user.StoreId, user.UserId));
|
||||
|
||||
await AssertAPIError("store-user-role-orphaned", async () => await user2Client.RemoveStoreUser(user.StoreId, user2.UserId));
|
||||
|
||||
}
|
||||
|
||||
|
||||
[Fact(Timeout = 60 * 2 * 1000)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task StoreEmailTests()
|
||||
|
@ -2369,7 +2367,7 @@ namespace BTCPayServer.Tests
|
|||
await adminClient.UpdateStoreEmailSettings(admin.StoreId,
|
||||
new EmailSettingsData());
|
||||
|
||||
var data = new EmailSettingsData()
|
||||
var data = new EmailSettingsData
|
||||
{
|
||||
From = "admin@admin.com",
|
||||
Login = "admin@admin.com",
|
||||
|
@ -2382,11 +2380,10 @@ namespace BTCPayServer.Tests
|
|||
Assert.Equal(JsonConvert.SerializeObject(s), JsonConvert.SerializeObject(data));
|
||||
await AssertValidationError(new[] { nameof(EmailSettingsData.From) },
|
||||
async () => await adminClient.UpdateStoreEmailSettings(admin.StoreId,
|
||||
new EmailSettingsData() { From = "ass" }));
|
||||
|
||||
new EmailSettingsData { From = "invalid" }));
|
||||
|
||||
await adminClient.SendEmail(admin.StoreId,
|
||||
new SendEmailRequest() { Body = "lol", Subject = "subj", Email = "sdasdas" });
|
||||
new SendEmailRequest { Body = "lol", Subject = "subj", Email = "to@example.org" });
|
||||
}
|
||||
|
||||
[Fact(Timeout = 60 * 2 * 1000)]
|
||||
|
|
|
@ -319,6 +319,8 @@ namespace BTCPayServer.Tests
|
|||
using var s = CreateSeleniumTester();
|
||||
await s.StartAsync();
|
||||
s.RegisterNewUser(true);
|
||||
|
||||
// Server Emails
|
||||
s.Driver.Navigate().GoToUrl(s.Link("/server/emails"));
|
||||
if (s.Driver.PageSource.Contains("Configured"))
|
||||
{
|
||||
|
@ -327,8 +329,30 @@ namespace BTCPayServer.Tests
|
|||
}
|
||||
CanSetupEmailCore(s);
|
||||
s.CreateNewStore();
|
||||
s.GoToUrl($"stores/{s.StoreId}/emails");
|
||||
|
||||
// Store Emails
|
||||
s.GoToStore(StoreNavPages.Emails);
|
||||
s.Driver.FindElement(By.Id("ConfigureEmailRules")).Click();
|
||||
Assert.Contains("You need to configure email settings before this feature works", s.Driver.PageSource);
|
||||
|
||||
s.GoToStore(StoreNavPages.Emails);
|
||||
CanSetupEmailCore(s);
|
||||
|
||||
// Store Email Rules
|
||||
s.Driver.FindElement(By.Id("ConfigureEmailRules")).Click();
|
||||
Assert.Contains("There are no rules yet.", s.Driver.PageSource);
|
||||
Assert.DoesNotContain("id=\"SaveEmailRules\"", s.Driver.PageSource);
|
||||
Assert.DoesNotContain("You need to configure email settings before this feature works", s.Driver.PageSource);
|
||||
|
||||
s.Driver.FindElement(By.Id("CreateEmailRule")).Click();
|
||||
var select = new SelectElement(s.Driver.FindElement(By.Id("Rules_0__Trigger")));
|
||||
select.SelectByText("InvoiceSettled", true);
|
||||
s.Driver.FindElement(By.Id("Rules_0__To")).SendKeys("test@gmail.com");
|
||||
s.Driver.FindElement(By.Id("Rules_0__CustomerEmail")).Click();
|
||||
s.Driver.FindElement(By.Id("Rules_0__Subject")).SendKeys("Thanks!");
|
||||
s.Driver.FindElement(By.Id("Rules_0__Body")).SendKeys("Your invoice is settled");
|
||||
s.Driver.FindElement(By.Id("SaveEmailRules")).Click();
|
||||
Assert.Contains("Store email rules saved", s.FindAlertMessage().Text);
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
|
|
|
@ -2628,7 +2628,7 @@ namespace BTCPayServer.Tests
|
|||
Assert.Equal("admin@admin.com", (await Assert.IsType<ServerEmailSender>(await emailSenderFactory.GetEmailSender()).GetEmailSettings()).Login);
|
||||
Assert.Null(await Assert.IsType<StoreEmailSender>(await emailSenderFactory.GetEmailSender(acc.StoreId)).GetEmailSettings());
|
||||
|
||||
Assert.IsType<RedirectToActionResult>(await acc.GetController<UIStoresController>().Emails(acc.StoreId, new EmailsViewModel(new EmailSettings()
|
||||
Assert.IsType<RedirectToActionResult>(await acc.GetController<UIStoresController>().StoreEmailSettings(acc.StoreId, new EmailsViewModel(new EmailSettings()
|
||||
{
|
||||
From = "store@store.com",
|
||||
Login = "store@store.com",
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
#nullable enable
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
|
@ -12,6 +11,7 @@ using BTCPayServer.Validation;
|
|||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using MimeKit;
|
||||
|
||||
namespace BTCPayServer.Controllers.GreenField
|
||||
{
|
||||
|
@ -39,13 +39,17 @@ namespace BTCPayServer.Controllers.GreenField
|
|||
{
|
||||
return this.CreateAPIError(404, "store-not-found", "The store was not found");
|
||||
}
|
||||
if (!MailboxAddress.TryParse(request.Email, out MailboxAddress to))
|
||||
{
|
||||
ModelState.AddModelError(nameof(request.Email), "Invalid email");
|
||||
return this.CreateValidationError(ModelState);
|
||||
}
|
||||
var emailSender = await _emailSenderFactory.GetEmailSender(storeId);
|
||||
if (emailSender is null )
|
||||
if (emailSender is null)
|
||||
{
|
||||
return this.CreateAPIError(404,"smtp-not-configured", "Store does not have an SMTP server configured.");
|
||||
}
|
||||
|
||||
emailSender.SendEmail(request.Email, request.Subject, request.Body);
|
||||
emailSender.SendEmail(to, request.Subject, request.Body);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@ using Microsoft.AspNetCore.Identity;
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MimeKit;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
|
@ -164,8 +165,8 @@ namespace BTCPayServer.Controllers
|
|||
|
||||
var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
|
||||
var callbackUrl = _linkGenerator.EmailConfirmationLink(user.Id, code, Request.Scheme, Request.Host, Request.PathBase);
|
||||
var email = user.Email;
|
||||
(await _EmailSenderFactory.GetEmailSender()).SendEmailConfirmation(email, callbackUrl);
|
||||
var address = new MailboxAddress(user.UserName, user.Email);
|
||||
(await _EmailSenderFactory.GetEmailSender()).SendEmailConfirmation(address, callbackUrl);
|
||||
TempData[WellKnownTempData.SuccessMessage] = "Verification email sent. Please check your email.";
|
||||
return RedirectToAction(nameof(Index));
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ using Microsoft.AspNetCore.Identity;
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using MimeKit;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
|
@ -302,7 +303,8 @@ namespace BTCPayServer.Controllers
|
|||
var code = await _UserManager.GenerateEmailConfirmationTokenAsync(user);
|
||||
var callbackUrl = _linkGenerator.EmailConfirmationLink(user.Id, code, Request.Scheme, Request.Host, Request.PathBase);
|
||||
|
||||
(await _emailSenderFactory.GetEmailSender()).SendEmailConfirmation(user.Email, callbackUrl);
|
||||
var address = new MailboxAddress(user.UserName, user.Email);
|
||||
(await _emailSenderFactory.GetEmailSender()).SendEmailConfirmation(address, callbackUrl);
|
||||
|
||||
TempData[WellKnownTempData.SuccessMessage] = "Verification email sent";
|
||||
return RedirectToAction(nameof(ListUsers));
|
||||
|
|
|
@ -1006,7 +1006,6 @@ namespace BTCPayServer.Controllers
|
|||
[HttpPost]
|
||||
public async Task<IActionResult> Emails(EmailsViewModel model, string command)
|
||||
{
|
||||
|
||||
if (command == "Test")
|
||||
{
|
||||
try
|
||||
|
@ -1021,13 +1020,18 @@ namespace BTCPayServer.Controllers
|
|||
TempData[WellKnownTempData.ErrorMessage] = "Required fields missing";
|
||||
return View(model);
|
||||
}
|
||||
if (!MailboxAddress.TryParse(model.TestEmail, out MailboxAddress testEmail))
|
||||
{
|
||||
TempData[WellKnownTempData.ErrorMessage] = "Invalid test email";
|
||||
return View(model);
|
||||
}
|
||||
using (var client = await model.Settings.CreateSmtpClient())
|
||||
using (var message = model.Settings.CreateMailMessage(new MailboxAddress(model.TestEmail, model.TestEmail), "BTCPay test", "BTCPay test", false))
|
||||
using (var message = model.Settings.CreateMailMessage(testEmail, "BTCPay test", "BTCPay test", false))
|
||||
{
|
||||
await client.SendAsync(message);
|
||||
await client.DisconnectAsync(true);
|
||||
}
|
||||
TempData[WellKnownTempData.SuccessMessage] = "Email sent to " + model.TestEmail + ", please, verify you received it";
|
||||
TempData[WellKnownTempData.SuccessMessage] = $"Email sent to {model.TestEmail}. Please verify you received it.";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
@ -1035,7 +1039,7 @@ namespace BTCPayServer.Controllers
|
|||
}
|
||||
return View(model);
|
||||
}
|
||||
else if (command == "ResetPassword")
|
||||
if (command == "ResetPassword")
|
||||
{
|
||||
var settings = await _SettingsRepository.GetSettingAsync<EmailSettings>() ?? new EmailSettings();
|
||||
settings.Password = null;
|
||||
|
@ -1043,7 +1047,7 @@ namespace BTCPayServer.Controllers
|
|||
TempData[WellKnownTempData.SuccessMessage] = "Email server password reset";
|
||||
return RedirectToAction(nameof(Emails));
|
||||
}
|
||||
else // if(command == "Save")
|
||||
else // if (command == "Save")
|
||||
{
|
||||
var oldSettings = await _SettingsRepository.GetSettingAsync<EmailSettings>() ?? new EmailSettings();
|
||||
if (new EmailsViewModel(oldSettings).PasswordSet)
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
using System;
|
||||
using System.Net.Mail;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Abstractions.Models;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models.ServerViewModels;
|
||||
using BTCPayServer.Services.Mails;
|
||||
|
@ -12,9 +16,84 @@ namespace BTCPayServer.Controllers
|
|||
{
|
||||
public partial class UIStoresController
|
||||
{
|
||||
[HttpGet("{storeId}/emails")]
|
||||
public IActionResult StoreEmails(string storeId)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
|
||||
var blob = store.GetStoreBlob();
|
||||
var data = blob.EmailSettings;
|
||||
if (data?.IsComplete() is not true)
|
||||
{
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Warning,
|
||||
Html = $"You need to configure email settings before this feature works. <a class='alert-link' href='{Url.Action("StoreEmailSettings", new {storeId})}'>Configure now</a>."
|
||||
});
|
||||
}
|
||||
|
||||
[Route("{storeId}/emails")]
|
||||
public IActionResult Emails()
|
||||
var vm = new StoreEmailRuleViewModel { Rules = blob.EmailRules ?? new List<StoreEmailRule>() };
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
[HttpPost("{storeId}/emails")]
|
||||
public async Task<IActionResult> StoreEmails(string storeId, StoreEmailRuleViewModel vm, string command)
|
||||
{
|
||||
vm.Rules ??= new List<StoreEmailRule>();
|
||||
if (command.StartsWith("remove", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
var item = command[(command.IndexOf(":", StringComparison.InvariantCultureIgnoreCase) + 1)..];
|
||||
var index = int.Parse(item);
|
||||
vm.Rules.RemoveAt(index);
|
||||
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
if (command == "add")
|
||||
{
|
||||
vm.Rules.Add(new StoreEmailRule());
|
||||
|
||||
return View(vm);
|
||||
}
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
var blob = store.GetStoreBlob();
|
||||
blob.EmailRules = vm.Rules;
|
||||
store.SetStoreBlob(blob);
|
||||
await _Repo.UpdateStore(store);
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Success,
|
||||
Message = "Store email rules saved"
|
||||
});
|
||||
return RedirectToAction("StoreEmails", new {storeId});
|
||||
}
|
||||
|
||||
public class StoreEmailRuleViewModel
|
||||
{
|
||||
public List<StoreEmailRule> Rules { get; set; }
|
||||
}
|
||||
|
||||
public class StoreEmailRule
|
||||
{
|
||||
[Required]
|
||||
public WebhookEventType Trigger { get; set; }
|
||||
public bool CustomerEmail { get; set; }
|
||||
public string To { get; set; }
|
||||
public string Body { get; set; }
|
||||
public string Subject { get; set; }
|
||||
}
|
||||
|
||||
[HttpGet("{storeId}/email-settings")]
|
||||
public IActionResult StoreEmailSettings()
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
|
@ -23,13 +102,13 @@ namespace BTCPayServer.Controllers
|
|||
return View(new EmailsViewModel(data));
|
||||
}
|
||||
|
||||
[Route("{storeId}/emails")]
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> Emails(string storeId, EmailsViewModel model, string command)
|
||||
[HttpPost("{storeId}/email-settings")]
|
||||
public async Task<IActionResult> StoreEmailSettings(string storeId, EmailsViewModel model, string command)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
|
||||
if (command == "Test")
|
||||
{
|
||||
try
|
||||
|
@ -43,11 +122,16 @@ namespace BTCPayServer.Controllers
|
|||
TempData[WellKnownTempData.ErrorMessage] = "Required fields missing";
|
||||
return View(model);
|
||||
}
|
||||
if (!MailboxAddress.TryParse(model.TestEmail, out MailboxAddress testEmail))
|
||||
{
|
||||
TempData[WellKnownTempData.ErrorMessage] = "Invalid test email";
|
||||
return View(model);
|
||||
}
|
||||
using var client = await model.Settings.CreateSmtpClient();
|
||||
var message = model.Settings.CreateMailMessage(new MailboxAddress(model.TestEmail, model.TestEmail), "BTCPay test", "BTCPay test", false);
|
||||
var message = model.Settings.CreateMailMessage(testEmail, "BTCPay test", "BTCPay test", false);
|
||||
await client.SendAsync(message);
|
||||
await client.DisconnectAsync(true);
|
||||
TempData[WellKnownTempData.SuccessMessage] = "Email sent to " + model.TestEmail + ", please, verify you received it";
|
||||
TempData[WellKnownTempData.SuccessMessage] = $"Email sent to {model.TestEmail}. Please verify you received it.";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
@ -55,23 +139,19 @@ namespace BTCPayServer.Controllers
|
|||
}
|
||||
return View(model);
|
||||
}
|
||||
else if (command == "ResetPassword")
|
||||
if (command == "ResetPassword")
|
||||
{
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
storeBlob.EmailSettings.Password = null;
|
||||
store.SetStoreBlob(storeBlob);
|
||||
await _Repo.UpdateStore(store);
|
||||
TempData[WellKnownTempData.SuccessMessage] = "Email server password reset";
|
||||
return RedirectToAction(nameof(Emails), new
|
||||
{
|
||||
storeId
|
||||
});
|
||||
return RedirectToAction(nameof(StoreEmailSettings), new { storeId });
|
||||
}
|
||||
else // if(command == "Save")
|
||||
else // if (command == "Save")
|
||||
{
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
var oldPassword = storeBlob.EmailSettings?.Password;
|
||||
if (new EmailsViewModel(storeBlob.EmailSettings).PasswordSet)
|
||||
if (new EmailsViewModel(storeBlob.EmailSettings).PasswordSet && storeBlob.EmailSettings != null)
|
||||
{
|
||||
model.Settings.Password = storeBlob.EmailSettings.Password;
|
||||
}
|
||||
|
@ -79,10 +159,7 @@ namespace BTCPayServer.Controllers
|
|||
store.SetStoreBlob(storeBlob);
|
||||
await _Repo.UpdateStore(store);
|
||||
TempData[WellKnownTempData.SuccessMessage] = "Email settings modified";
|
||||
return RedirectToAction(nameof(Emails), new
|
||||
{
|
||||
storeId
|
||||
});
|
||||
return RedirectToAction(nameof(StoreEmailSettings), new { storeId });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ using System.Linq;
|
|||
using System.Text;
|
||||
using BTCPayServer.Client.JsonConverters;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.JsonConverters;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Rating;
|
||||
|
@ -179,6 +180,8 @@ namespace BTCPayServer.Data
|
|||
[JsonConverter(typeof(TimeSpanJsonConverter.Days))]
|
||||
public TimeSpan RefundBOLT11Expiration { get; set; }
|
||||
|
||||
public List<UIStoresController.StoreEmailRule> EmailRules { get; set; }
|
||||
|
||||
public IPaymentFilter GetExcludedPaymentMethods()
|
||||
{
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
using System.Text.Encodings.Web;
|
||||
using BTCPayServer.Services.Mails;
|
||||
using MimeKit;
|
||||
|
||||
namespace BTCPayServer.Services
|
||||
{
|
||||
|
@ -16,19 +17,17 @@ namespace BTCPayServer.Services
|
|||
return button;
|
||||
}
|
||||
|
||||
public static void SendEmailConfirmation(this IEmailSender emailSender, string email, string link)
|
||||
public static void SendEmailConfirmation(this IEmailSender emailSender, MailboxAddress address, string link)
|
||||
{
|
||||
emailSender.SendEmail(email, "Confirm your email",
|
||||
emailSender.SendEmail(address, "Confirm your email",
|
||||
$"Please confirm your account by clicking this link: <a href='{HtmlEncoder.Default.Encode(link)}'>link</a>");
|
||||
}
|
||||
|
||||
public static void SendSetPasswordConfirmation(this IEmailSender emailSender, string email, string link, bool newPassword)
|
||||
public static void SendSetPasswordConfirmation(this IEmailSender emailSender, MailboxAddress address, string link, bool newPassword)
|
||||
{
|
||||
var subject = $"{(newPassword ? "Set" : "Update")} Password";
|
||||
var body = $"A request has been made to {(newPassword ? "set" : "update")} your BTCPay Server password. Please confirm your password by clicking below. <br/><br/> {CallToAction(subject, HtmlEncoder.Default.Encode(link))}";
|
||||
emailSender.SendEmail(email,
|
||||
subject,
|
||||
$"<html><body style='{BODY_STYLE}'>{HEADER_HTML}{body}</body></html>");
|
||||
emailSender.SendEmail(address, subject, $"<html><body style='{BODY_STYLE}'>{HEADER_HTML}{body}</body></html>");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,16 +5,14 @@ using System.Net.Http;
|
|||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Mails;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using MimeKit;
|
||||
using NBitpayClient;
|
||||
using NBXplorer;
|
||||
using Newtonsoft.Json;
|
||||
|
@ -122,7 +120,9 @@ namespace BTCPayServer.HostedServices
|
|||
#pragma warning restore CS0618
|
||||
}
|
||||
|
||||
if (sendMail && !String.IsNullOrEmpty(invoice.NotificationEmail))
|
||||
if (sendMail &&
|
||||
invoice.NotificationEmail is String e &&
|
||||
MailboxAddress.TryParse(e, out MailboxAddress notificationEmail))
|
||||
{
|
||||
var json = NBitcoin.JsonConverters.Serializer.ToString(notification);
|
||||
var store = await _StoreRepository.FindStore(invoice.StoreId);
|
||||
|
@ -134,7 +134,7 @@ namespace BTCPayServer.HostedServices
|
|||
$"<br><details><summary>Details</summary><pre>{json}</pre></details>";
|
||||
|
||||
(await _EmailSenderFactory.GetEmailSender(invoice.StoreId)).SendEmail(
|
||||
invoice.NotificationEmail,
|
||||
notificationEmail,
|
||||
$"{storeName} Invoice Notification - ${invoice.StoreId}",
|
||||
emailBody);
|
||||
}
|
||||
|
|
72
BTCPayServer/HostedServices/StoreEmailRuleProcessorSender.cs
Normal file
72
BTCPayServer/HostedServices/StoreEmailRuleProcessorSender.cs
Normal file
|
@ -0,0 +1,72 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.Services.Mails;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MimeKit;
|
||||
|
||||
namespace BTCPayServer.HostedServices;
|
||||
|
||||
public class StoreEmailRuleProcessorSender : EventHostedServiceBase
|
||||
{
|
||||
private readonly StoreRepository _storeRepository;
|
||||
private readonly EmailSenderFactory _emailSenderFactory;
|
||||
|
||||
public StoreEmailRuleProcessorSender(StoreRepository storeRepository, EventAggregator eventAggregator,
|
||||
ILogger<InvoiceEventSaverService> logger,
|
||||
EmailSenderFactory emailSenderFactory) : base(
|
||||
eventAggregator, logger)
|
||||
{
|
||||
_storeRepository = storeRepository;
|
||||
_emailSenderFactory = emailSenderFactory;
|
||||
}
|
||||
|
||||
protected override void SubscribeToEvents()
|
||||
{
|
||||
Subscribe<InvoiceEvent>();
|
||||
}
|
||||
|
||||
protected override async Task ProcessEvent(object evt, CancellationToken cancellationToken)
|
||||
{
|
||||
if (evt is InvoiceEvent invoiceEvent)
|
||||
{
|
||||
var type = WebhookSender.GetWebhookEvent(invoiceEvent);
|
||||
if (type is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var store = await _storeRepository.FindStore(invoiceEvent.Invoice.StoreId);
|
||||
|
||||
|
||||
var blob = store.GetStoreBlob();
|
||||
if (blob.EmailRules?.Any() is true)
|
||||
{
|
||||
var actionableRules = blob.EmailRules.Where(rule => rule.Trigger == type.Type).ToList();
|
||||
if (actionableRules.Any())
|
||||
{
|
||||
var sender = await _emailSenderFactory.GetEmailSender(invoiceEvent.Invoice.StoreId);
|
||||
foreach (UIStoresController.StoreEmailRule actionableRule in actionableRules)
|
||||
{
|
||||
var dest = actionableRule.To.Split(",", StringSplitOptions.RemoveEmptyEntries).Where(IsValidEmailAddress);
|
||||
if (actionableRule.CustomerEmail && IsValidEmailAddress(invoiceEvent.Invoice.Metadata.BuyerEmail))
|
||||
{
|
||||
dest = dest.Append(invoiceEvent.Invoice.Metadata.BuyerEmail);
|
||||
}
|
||||
|
||||
var recipients = dest.Select(address => new MailboxAddress(address, address)).ToArray();
|
||||
sender.SendEmail(recipients, null, null, actionableRule.Subject, actionableRule.Body);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsValidEmailAddress(string address) =>
|
||||
!string.IsNullOrEmpty(address) && MailboxAddress.TryParse(address, out _);
|
||||
}
|
|
@ -11,6 +11,7 @@ using Microsoft.AspNetCore.Identity;
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MimeKit;
|
||||
|
||||
namespace BTCPayServer.HostedServices
|
||||
{
|
||||
|
@ -39,6 +40,7 @@ namespace BTCPayServer.HostedServices
|
|||
{
|
||||
string code;
|
||||
string callbackUrl;
|
||||
MailboxAddress address;
|
||||
UserPasswordResetRequestedEvent userPasswordResetRequestedEvent;
|
||||
switch (evt)
|
||||
{
|
||||
|
@ -53,8 +55,9 @@ namespace BTCPayServer.HostedServices
|
|||
new HostString(userRegisteredEvent.RequestUri.Host, userRegisteredEvent.RequestUri.Port),
|
||||
userRegisteredEvent.RequestUri.PathAndQuery);
|
||||
userRegisteredEvent.CallbackUrlGenerated?.SetResult(new Uri(callbackUrl));
|
||||
(await _emailSenderFactory.GetEmailSender())
|
||||
.SendEmailConfirmation(userRegisteredEvent.User.Email, callbackUrl);
|
||||
address = new MailboxAddress(userRegisteredEvent.User.Email,
|
||||
userRegisteredEvent.User.Email);
|
||||
(await _emailSenderFactory.GetEmailSender()).SendEmailConfirmation(address, callbackUrl);
|
||||
}
|
||||
else if (!await _userManager.HasPasswordAsync(userRegisteredEvent.User))
|
||||
{
|
||||
|
@ -83,9 +86,10 @@ passwordSetter:
|
|||
userPasswordResetRequestedEvent.RequestUri.Port),
|
||||
userPasswordResetRequestedEvent.RequestUri.PathAndQuery);
|
||||
userPasswordResetRequestedEvent.CallbackUrlGenerated?.SetResult(new Uri(callbackUrl));
|
||||
address = new MailboxAddress(userPasswordResetRequestedEvent.User.Email,
|
||||
userPasswordResetRequestedEvent.User.Email);
|
||||
(await _emailSenderFactory.GetEmailSender())
|
||||
.SendSetPasswordConfirmation(userPasswordResetRequestedEvent.User.Email, callbackUrl,
|
||||
newPassword);
|
||||
.SendSetPasswordConfirmation(address, callbackUrl, newPassword);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -169,7 +169,7 @@ namespace BTCPayServer.HostedServices
|
|||
_processingQueue.Enqueue(context.WebhookId, (cancellationToken) => Process(context, cancellationToken));
|
||||
}
|
||||
|
||||
private WebhookInvoiceEvent GetWebhookEvent(WebhookEventType webhookEventType)
|
||||
public static WebhookInvoiceEvent GetWebhookEvent(WebhookEventType webhookEventType)
|
||||
{
|
||||
switch (webhookEventType)
|
||||
{
|
||||
|
@ -192,7 +192,7 @@ namespace BTCPayServer.HostedServices
|
|||
}
|
||||
}
|
||||
|
||||
private WebhookInvoiceEvent? GetWebhookEvent(InvoiceEvent invoiceEvent)
|
||||
public static WebhookInvoiceEvent? GetWebhookEvent(InvoiceEvent invoiceEvent)
|
||||
{
|
||||
var eventCode = invoiceEvent.EventCode;
|
||||
switch (eventCode)
|
||||
|
|
|
@ -332,6 +332,7 @@ namespace BTCPayServer.Hosting
|
|||
services.AddSingleton<IHostedService, HostedServices.CheckConfigurationHostedService>(o => o.GetRequiredService<CheckConfigurationHostedService>());
|
||||
services.AddSingleton<HostedServices.WebhookSender>();
|
||||
services.AddSingleton<IHostedService, WebhookSender>(o => o.GetRequiredService<WebhookSender>());
|
||||
services.AddSingleton<IHostedService, StoreEmailRuleProcessorSender>();
|
||||
services.AddHttpClient(WebhookSender.OnionNamedClient)
|
||||
.ConfigurePrimaryHttpMessageHandler<Socks5HttpClientHandler>();
|
||||
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
using System;
|
||||
using System.Net.Mail;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Logging;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MimeKit;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer.Services.Mails
|
||||
{
|
||||
|
@ -20,8 +19,13 @@ namespace BTCPayServer.Services.Mails
|
|||
_JobClient = jobClient ?? throw new ArgumentNullException(nameof(jobClient));
|
||||
}
|
||||
|
||||
public void SendEmail(string email, string subject, string message)
|
||||
public void SendEmail(MailboxAddress email, string subject, string message)
|
||||
{
|
||||
SendEmail(new[] {email}, Array.Empty<MailboxAddress>(), Array.Empty<MailboxAddress>(), subject, message);
|
||||
}
|
||||
|
||||
public void SendEmail(MailboxAddress[] email, MailboxAddress[] cc, MailboxAddress[] bcc, string subject, string message)
|
||||
{
|
||||
_JobClient.Schedule(async (cancellationToken) =>
|
||||
{
|
||||
var emailSettings = await GetEmailSettings();
|
||||
|
@ -30,8 +34,9 @@ namespace BTCPayServer.Services.Mails
|
|||
Logs.Configuration.LogWarning("Should have sent email, but email settings are not configured");
|
||||
return;
|
||||
}
|
||||
|
||||
using var smtp = await emailSettings.CreateSmtpClient();
|
||||
var mail = emailSettings.CreateMailMessage(new MailboxAddress(email, email), subject, message, true);
|
||||
var mail = emailSettings.CreateMailMessage(email, cc, bcc, subject, message, true);
|
||||
await smtp.SendAsync(mail, cancellationToken);
|
||||
await smtp.DisconnectAsync(true, cancellationToken);
|
||||
}, TimeSpan.Zero);
|
||||
|
|
|
@ -13,7 +13,9 @@ namespace BTCPayServer.Services.Mails
|
|||
return !string.IsNullOrWhiteSpace(Server) && Port is int;
|
||||
}
|
||||
|
||||
public MimeMessage CreateMailMessage(MailboxAddress to, string subject, string message, bool isHtml)
|
||||
public MimeMessage CreateMailMessage(MailboxAddress to, string subject, string message, bool isHtml) =>
|
||||
CreateMailMessage(new[] {to}, null, null, subject, message, isHtml);
|
||||
public MimeMessage CreateMailMessage(MailboxAddress[] to, MailboxAddress[] cc, MailboxAddress[] bcc, string subject, string message, bool isHtml)
|
||||
{
|
||||
var bodyBuilder = new BodyBuilder();
|
||||
if (isHtml)
|
||||
|
@ -25,11 +27,15 @@ namespace BTCPayServer.Services.Mails
|
|||
bodyBuilder.TextBody = message;
|
||||
}
|
||||
|
||||
return new MimeMessage(
|
||||
from: new[] { new MailboxAddress(From, !string.IsNullOrWhiteSpace(FromDisplay) ? From : FromDisplay) },
|
||||
to: new[] { to },
|
||||
subject,
|
||||
bodyBuilder.ToMessageBody());
|
||||
var mm = new MimeMessage();
|
||||
mm.Body = bodyBuilder.ToMessageBody();
|
||||
mm.Subject = subject;
|
||||
mm.From.Add(new MailboxAddress(From, !string.IsNullOrWhiteSpace(FromDisplay) ? From : FromDisplay));
|
||||
mm.To.AddRange(to);
|
||||
mm.Cc.AddRange(cc?? System.Array.Empty<InternetAddress>());
|
||||
mm.Bcc.AddRange(bcc?? System.Array.Empty<InternetAddress>());
|
||||
return mm;
|
||||
|
||||
}
|
||||
|
||||
public async Task<SmtpClient> CreateSmtpClient()
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
using MimeKit;
|
||||
|
||||
namespace BTCPayServer.Services.Mails
|
||||
{
|
||||
public interface IEmailSender
|
||||
{
|
||||
void SendEmail(string email, string subject, string message);
|
||||
void SendEmail(MailboxAddress email, string subject, string message);
|
||||
void SendEmail(MailboxAddress[] email, MailboxAddress[] cc, MailboxAddress[] bcc, string subject, string message);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,19 +1,21 @@
|
|||
@model BTCPayServer.Models.ServerViewModels.EmailsViewModel
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xl-8 col-xxl-constrain">
|
||||
<div class="d-flex align-items-center justify-content-between mb-4">
|
||||
<h3 class="mb-0">@ViewData["Title"]</h3>
|
||||
<div class="dropdown only-for-js" id="quick-fill">
|
||||
<button class="btn btn-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false" id="QuickFillDropdownToggle">
|
||||
Quick Fill
|
||||
</button>
|
||||
<div class="dropdown-menu" aria-labelledby="QuickFillDropdownToggle">
|
||||
<a class="dropdown-item" href="" data-server="smtp.gmail.com" data-port="587">Gmail.com</a>
|
||||
<a class="dropdown-item" href="" data-server="mail.yahoo.com" data-port="587">Yahoo.com</a>
|
||||
<a class="dropdown-item" href="" data-server="smtp.mailgun.org" data-port="587">Mailgun</a>
|
||||
<a class="dropdown-item" href="" data-server="smtp.office365.com" data-port="587">Office365</a>
|
||||
<a class="dropdown-item" href="" data-server="smtp.sendgrid.net" data-port="587">SendGrid</a>
|
||||
<div class="col-xl-10 col-xxl-constrain">
|
||||
<div class="d-flex flex-wrap gap-3 align-items-center justify-content-between mt-n1 mb-4">
|
||||
<h3 class="mb-0">Email Server</h3>
|
||||
<div class="d-flex">
|
||||
<div class="dropdown only-for-js" id="quick-fill">
|
||||
<button class="btn btn-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false" id="QuickFillDropdownToggle">
|
||||
Quick Fill
|
||||
</button>
|
||||
<div class="dropdown-menu" aria-labelledby="QuickFillDropdownToggle">
|
||||
<a class="dropdown-item" href="" data-server="smtp.gmail.com" data-port="587">Gmail.com</a>
|
||||
<a class="dropdown-item" href="" data-server="mail.yahoo.com" data-port="587">Yahoo.com</a>
|
||||
<a class="dropdown-item" href="" data-server="smtp.mailgun.org" data-port="587">Mailgun</a>
|
||||
<a class="dropdown-item" href="" data-server="smtp.office365.com" data-port="587">Office365</a>
|
||||
<a class="dropdown-item" href="" data-server="smtp.sendgrid.net" data-port="587">SendGrid</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -22,7 +24,7 @@
|
|||
|
||||
<form method="post" autocomplete="off">
|
||||
<div class="row">
|
||||
<div class="col-xl-8 col-xxl-constrain">
|
||||
<div class="col-xl-10 col-xxl-constrain">
|
||||
@if (!ViewContext.ModelState.IsValid)
|
||||
{
|
||||
<div asp-validation-summary="All" class="text-danger"></div>
|
||||
|
|
|
@ -2,5 +2,5 @@
|
|||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close">
|
||||
<vc:icon symbol="close" />
|
||||
</button>
|
||||
The Email settings have not been configured on this server or store yet. Setting this field will not send emails until then. <a asp-action="Emails" asp-controller="UIStores" asp-route-storeId="@Model" class="alert-link">Configure store email settings</a>
|
||||
The Email settings have not been configured on this server or store yet. Setting this field will not send emails until then. <a asp-action="StoreEmailSettings" asp-controller="UIStores" asp-route-storeId="@Model" class="alert-link">Configure store email settings</a>
|
||||
</div>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
@model EmailsViewModel
|
||||
@{
|
||||
ViewData.SetActivePage(ServerNavPages.Emails, "Email Server");
|
||||
ViewData.SetActivePage(ServerNavPages.Emails, "Emails");
|
||||
}
|
||||
|
||||
<partial name="EmailsBody" model="Model" />
|
||||
|
|
|
@ -87,30 +87,6 @@
|
|||
|
||||
<button name="command" type="submit" class="btn btn-primary mt-2" value="Save" id="Save">Save</button>
|
||||
</form>
|
||||
|
||||
<h3 class="mt-5 mb-3">Services</h3>
|
||||
<div class="table-responsive-md">
|
||||
<table class="table table-hover mt-1 mb-5">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Service</th>
|
||||
<th class="text-end w-100px">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
Email
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<a asp-action="Emails" asp-route-storeId="@Context.GetRouteValue("storeId")">
|
||||
Setup
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@if (Model.CanDelete)
|
||||
{
|
||||
<h3 class="mt-5 mb-3">Additional Actions</h3>
|
||||
|
|
26
BTCPayServer/Views/UIStores/StoreEmailSettings.cshtml
Normal file
26
BTCPayServer/Views/UIStores/StoreEmailSettings.cshtml
Normal file
|
@ -0,0 +1,26 @@
|
|||
@model BTCPayServer.Models.ServerViewModels.EmailsViewModel
|
||||
@{
|
||||
Layout = "../Shared/_NavLayout.cshtml";
|
||||
ViewData.SetActivePage(StoreNavPages.Emails, "Emails", Context.GetStoreData().Id);
|
||||
}
|
||||
|
||||
<div class="row mb-4">
|
||||
<div class="col-xl-10 col-xxl-constrain">
|
||||
<div class="d-flex flex-wrap gap-3 align-items-center justify-content-between mt-n1 mb-4">
|
||||
<h3 class="mb-0">Email Rules</h3>
|
||||
<a class="btn btn-secondary" asp-action="StoreEmails" asp-controller="UIStores" asp-route-storeId="@Context.GetStoreData().Id" id="ConfigureEmailRules">
|
||||
Configure
|
||||
</a>
|
||||
</div>
|
||||
<p>
|
||||
<a asp-action="StoreEmails" asp-controller="UIStores" asp-route-storeId="@Context.GetStoreData().Id">Email rules</a>
|
||||
allow BTCPay Server to send customized emails from your store based on events.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<partial name="EmailsBody" model="Model" />
|
||||
|
||||
@section PageFootContent {
|
||||
<partial name="_ValidationScriptsPartial" />
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
@model BTCPayServer.Models.ServerViewModels.EmailsViewModel
|
||||
@{
|
||||
Layout = "../Shared/_NavLayout.cshtml";
|
||||
ViewData.SetActivePage(StoreNavPages.General, "Email Server", Context.GetStoreData().Id);
|
||||
ViewData.SetActivePage(StoreNavPages.Emails, "Email Server", Context.GetStoreData().Id);
|
||||
}
|
||||
|
||||
<partial name="EmailsBody" model="Model" />
|
|
@ -0,0 +1,27 @@
|
|||
@using Microsoft.AspNetCore.Server.Kestrel.Core
|
||||
@model BTCPayServer.Models.ServerViewModels.EmailsViewModel
|
||||
@{
|
||||
Layout = "../Shared/_NavLayout.cshtml";
|
||||
ViewData.SetActivePage(StoreNavPages.Emails, "Emails", Context.GetStoreData().Id);
|
||||
}
|
||||
|
||||
<div class="row mb-4">
|
||||
<div class="col-xl-10 col-xxl-constrain">
|
||||
<div class="d-flex flex-wrap gap-3 align-items-center justify-content-between mt-n1 mb-4">
|
||||
<h3 class="mb-0">Email Rules</h3>
|
||||
<a class="btn btn-secondary" asp-action="StoreEmails" asp-controller="UIStores" asp-route-storeId="@Context.GetStoreData().Id" id="ConfigureEmailRules">
|
||||
Configure
|
||||
</a>
|
||||
</div>
|
||||
<p>
|
||||
<a asp-action="StoreEmails" asp-controller="UIStores" asp-route-storeId="@Context.GetStoreData().Id">Email rules</a>
|
||||
allow BTCPay Server to send customized emails from your store based on events.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<partial name="EmailsBody" model="Model" />
|
||||
|
||||
@section PageFootContent {
|
||||
<partial name="_ValidationScriptsPartial" />
|
||||
}
|
80
BTCPayServer/Views/UIStores/StoreEmails.cshtml
Normal file
80
BTCPayServer/Views/UIStores/StoreEmails.cshtml
Normal file
|
@ -0,0 +1,80 @@
|
|||
@using BTCPayServer.Client.Models
|
||||
@model BTCPayServer.Controllers.UIStoresController.StoreEmailRuleViewModel
|
||||
|
||||
@{
|
||||
Layout = "../Shared/_NavLayout.cshtml";
|
||||
ViewData.SetActivePage(StoreNavPages.Emails, "Email Rules", Context.GetStoreData().Id);
|
||||
}
|
||||
<form class="row" asp-action="StoreEmails" asp-route-storeId="@Context.GetStoreData().Id">
|
||||
<div class="col-xxl-constrain">
|
||||
<div class="d-flex align-items-center justify-content-between mt-n1 mb-3">
|
||||
<h3 class="mb-0">@ViewData["Title"]</h3>
|
||||
<div class="d-flex gap-3">
|
||||
@if (Model.Rules.Any())
|
||||
{
|
||||
<button class="btn btn-primary" name="command" type="submit" value="save" id="SaveEmailRules">
|
||||
Save
|
||||
</button>
|
||||
}
|
||||
<button class="btn btn-primary" name="command" type="submit" value="add" id="CreateEmailRule">
|
||||
<span class="fa fa-plus"></span>
|
||||
Create
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="mb-0">Email rules allow BTCPay Server to send customized emails from your store based on events.</p>
|
||||
|
||||
@if (Model.Rules.Any())
|
||||
{
|
||||
<ul class="list-group list-group-flush">
|
||||
@for (var index = 0; index < Model.Rules.Count; index++)
|
||||
{
|
||||
<li class="list-group-item py-4 px-0">
|
||||
<div class="form-group">
|
||||
<div class="d-flex align-items-center justify-content-between gap-3">
|
||||
<label asp-for="Rules[index].Trigger" class="form-label" data-required></label>
|
||||
<button name="command" type="submit" value="remove:@index" class="d-inline-block btn text-danger btn-link p-0">
|
||||
<span class="fa fa-times"></span> Remove this email rule
|
||||
</button>
|
||||
</div>
|
||||
<select asp-for="Rules[index].Trigger" asp-items="@Html.GetEnumSelectList<WebhookEventType>()" class="form-select" required></select>
|
||||
<small class="form-text text-muted">Choose what event sends the email.</small>
|
||||
<span asp-validation-for="Rules[index].Trigger" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Rules[index].To" class="form-label">Recipients</label>
|
||||
<input type="text" asp-for="Rules[index].To" class="form-control"/>
|
||||
<small class="form-text text-muted">Who to send the email to. For multiple emails, separate with a comma.</small>
|
||||
<span asp-validation-for="Rules[index].To" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-check mb-4">
|
||||
<input asp-for="Rules[index].CustomerEmail" type="checkbox" class="form-check-input" />
|
||||
<label asp-for="Rules[index].CustomerEmail" class="form-check-label">Send the email to the buyer, if email was provided to the invoice</label>
|
||||
<span asp-validation-for="Rules[index].CustomerEmail" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Rules[index].Subject" class="form-label" ></label>
|
||||
<input type="text" asp-for="Rules[index].Subject" class="form-control"/>
|
||||
<span asp-validation-for="Rules[index].Subject" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Rules[index].Body" class="form-label" ></label>
|
||||
<textarea asp-for="Rules[index].Body" class="form-control" rows="4"></textarea>
|
||||
<span asp-validation-for="Rules[index].Body" class="text-danger"></span>
|
||||
</div>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p class="text-secondary mt-3">
|
||||
There are no rules yet.
|
||||
</p>
|
||||
}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@section PageFootContent {
|
||||
<partial name="_ValidationScriptsPartial"/>
|
||||
}
|
|
@ -20,6 +20,7 @@ namespace BTCPayServer.Views.Stores
|
|||
PullPayments,
|
||||
Payouts,
|
||||
[Obsolete("Use StoreNavPages.Plugins instead")]
|
||||
Integrations
|
||||
Integrations,
|
||||
Emails
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
<a permission="@Policies.CanModifyStoreSettings" id="SectionNav-@(nameof(StoreNavPages.Plugins))" class="nav-link @ViewData.IsActivePage(StoreNavPages.Plugins) || @ViewData.IsActivePage(StoreNavPages.Integrations)" asp-controller="UIStores" asp-action="Plugins" asp-route-storeId="@storeId">Plugins</a>
|
||||
<a permission="@Policies.CanModifyStoreSettings" id="SectionNav-@(nameof(StoreNavPages.Webhooks))" class="nav-link @ViewData.IsActivePage(StoreNavPages.Webhooks)" asp-controller="UIStores" asp-action="Webhooks" asp-route-storeId="@storeId">Webhooks</a>
|
||||
<a permission="@Policies.CanModifyStoreSettings" id="SectionNav-PayoutProcessors" class="nav-link @ViewData.IsActivePage("PayoutProcessors")" asp-controller="UIPayoutProcessors" asp-action="ConfigureStorePayoutProcessors" asp-route-storeId="@storeId">Payout Processors</a>
|
||||
<a permission="@Policies.CanModifyStoreSettings" id="SectionNav-@(nameof(StoreNavPages.Emails))" class="nav-link @ViewData.IsActivePage(StoreNavPages.Emails)" asp-controller="UIStores" asp-action="StoreEmailSettings" asp-route-storeId="@storeId">Emails</a>
|
||||
<vc:ui-extension-point location="store-nav" model="@Model"/>
|
||||
</div>
|
||||
</nav>
|
||||
|
|
Loading…
Add table
Reference in a new issue