diff --git a/BTCPayServer.Client/Models/WebhookEventType.cs b/BTCPayServer.Client/Models/WebhookEventType.cs index e4b6e6458..147b2dd3a 100644 --- a/BTCPayServer.Client/Models/WebhookEventType.cs +++ b/BTCPayServer.Client/Models/WebhookEventType.cs @@ -1,7 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Text; - namespace BTCPayServer.Client.Models { public enum WebhookEventType diff --git a/BTCPayServer.Tests/CheckoutUITests.cs b/BTCPayServer.Tests/CheckoutUITests.cs index 2e293f860..5e8ad521d 100644 --- a/BTCPayServer.Tests/CheckoutUITests.cs +++ b/BTCPayServer.Tests/CheckoutUITests.cs @@ -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); diff --git a/BTCPayServer.Tests/GreenfieldAPITests.cs b/BTCPayServer.Tests/GreenfieldAPITests.cs index 0e34d5885..96e2f57f8 100644 --- a/BTCPayServer.Tests/GreenfieldAPITests.cs +++ b/BTCPayServer.Tests/GreenfieldAPITests.cs @@ -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)] diff --git a/BTCPayServer.Tests/SeleniumTests.cs b/BTCPayServer.Tests/SeleniumTests.cs index 6e56ed834..e0927b8e4 100644 --- a/BTCPayServer.Tests/SeleniumTests.cs +++ b/BTCPayServer.Tests/SeleniumTests.cs @@ -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)] diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs index 4ede08ec5..fa20f7560 100644 --- a/BTCPayServer.Tests/UnitTest1.cs +++ b/BTCPayServer.Tests/UnitTest1.cs @@ -2628,7 +2628,7 @@ namespace BTCPayServer.Tests Assert.Equal("admin@admin.com", (await Assert.IsType(await emailSenderFactory.GetEmailSender()).GetEmailSettings()).Login); Assert.Null(await Assert.IsType(await emailSenderFactory.GetEmailSender(acc.StoreId)).GetEmailSettings()); - Assert.IsType(await acc.GetController().Emails(acc.StoreId, new EmailsViewModel(new EmailSettings() + Assert.IsType(await acc.GetController().StoreEmailSettings(acc.StoreId, new EmailsViewModel(new EmailSettings() { From = "store@store.com", Login = "store@store.com", diff --git a/BTCPayServer/Controllers/GreenField/GreenfieldStoreEmailController.cs b/BTCPayServer/Controllers/GreenField/GreenfieldStoreEmailController.cs index d18eaba15..0e88764fd 100644 --- a/BTCPayServer/Controllers/GreenField/GreenfieldStoreEmailController.cs +++ b/BTCPayServer/Controllers/GreenField/GreenfieldStoreEmailController.cs @@ -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(); } diff --git a/BTCPayServer/Controllers/UIManageController.cs b/BTCPayServer/Controllers/UIManageController.cs index d6a83f29a..833fbb441 100644 --- a/BTCPayServer/Controllers/UIManageController.cs +++ b/BTCPayServer/Controllers/UIManageController.cs @@ -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)); } diff --git a/BTCPayServer/Controllers/UIServerController.Users.cs b/BTCPayServer/Controllers/UIServerController.Users.cs index d7c4c1761..3dc3b8672 100644 --- a/BTCPayServer/Controllers/UIServerController.Users.cs +++ b/BTCPayServer/Controllers/UIServerController.Users.cs @@ -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)); diff --git a/BTCPayServer/Controllers/UIServerController.cs b/BTCPayServer/Controllers/UIServerController.cs index abaf3f650..1c83b00b8 100644 --- a/BTCPayServer/Controllers/UIServerController.cs +++ b/BTCPayServer/Controllers/UIServerController.cs @@ -1006,7 +1006,6 @@ namespace BTCPayServer.Controllers [HttpPost] public async Task 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() ?? 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() ?? new EmailSettings(); if (new EmailsViewModel(oldSettings).PasswordSet) diff --git a/BTCPayServer/Controllers/UIStoresController.Email.cs b/BTCPayServer/Controllers/UIStoresController.Email.cs index 82f590c70..399f2f754 100644 --- a/BTCPayServer/Controllers/UIStoresController.Email.cs +++ b/BTCPayServer/Controllers/UIStoresController.Email.cs @@ -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. Configure now." + }); + } - [Route("{storeId}/emails")] - public IActionResult Emails() + var vm = new StoreEmailRuleViewModel { Rules = blob.EmailRules ?? new List() }; + return View(vm); + } + + [HttpPost("{storeId}/emails")] + public async Task StoreEmails(string storeId, StoreEmailRuleViewModel vm, string command) + { + vm.Rules ??= new List(); + 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 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 Emails(string storeId, EmailsViewModel model, string command) + [HttpPost("{storeId}/email-settings")] + public async Task 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 }); } } } diff --git a/BTCPayServer/Data/StoreBlob.cs b/BTCPayServer/Data/StoreBlob.cs index dc2808891..aa13afd78 100644 --- a/BTCPayServer/Data/StoreBlob.cs +++ b/BTCPayServer/Data/StoreBlob.cs @@ -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 EmailRules { get; set; } + public IPaymentFilter GetExcludedPaymentMethods() { #pragma warning disable CS0618 // Type or member is obsolete diff --git a/BTCPayServer/Extensions/EmailSenderExtensions.cs b/BTCPayServer/Extensions/EmailSenderExtensions.cs index 4f69bcc8a..51293801c 100644 --- a/BTCPayServer/Extensions/EmailSenderExtensions.cs +++ b/BTCPayServer/Extensions/EmailSenderExtensions.cs @@ -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: link"); } - 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.

{CallToAction(subject, HtmlEncoder.Default.Encode(link))}"; - emailSender.SendEmail(email, - subject, - $"{HEADER_HTML}{body}"); + emailSender.SendEmail(address, subject, $"{HEADER_HTML}{body}"); } } } diff --git a/BTCPayServer/HostedServices/BitpayIPNSender.cs b/BTCPayServer/HostedServices/BitpayIPNSender.cs index d1edf63db..a4b06829c 100644 --- a/BTCPayServer/HostedServices/BitpayIPNSender.cs +++ b/BTCPayServer/HostedServices/BitpayIPNSender.cs @@ -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 $"
Details
{json}
"; (await _EmailSenderFactory.GetEmailSender(invoice.StoreId)).SendEmail( - invoice.NotificationEmail, + notificationEmail, $"{storeName} Invoice Notification - ${invoice.StoreId}", emailBody); } diff --git a/BTCPayServer/HostedServices/StoreEmailRuleProcessorSender.cs b/BTCPayServer/HostedServices/StoreEmailRuleProcessorSender.cs new file mode 100644 index 000000000..1cec93f04 --- /dev/null +++ b/BTCPayServer/HostedServices/StoreEmailRuleProcessorSender.cs @@ -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 logger, + EmailSenderFactory emailSenderFactory) : base( + eventAggregator, logger) + { + _storeRepository = storeRepository; + _emailSenderFactory = emailSenderFactory; + } + + protected override void SubscribeToEvents() + { + Subscribe(); + } + + 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 _); +} diff --git a/BTCPayServer/HostedServices/UserEventHostedService.cs b/BTCPayServer/HostedServices/UserEventHostedService.cs index 1f7a42c83..f45323d68 100644 --- a/BTCPayServer/HostedServices/UserEventHostedService.cs +++ b/BTCPayServer/HostedServices/UserEventHostedService.cs @@ -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; } } diff --git a/BTCPayServer/HostedServices/WebhookSender.cs b/BTCPayServer/HostedServices/WebhookSender.cs index 0ab0da766..88a16051d 100644 --- a/BTCPayServer/HostedServices/WebhookSender.cs +++ b/BTCPayServer/HostedServices/WebhookSender.cs @@ -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) diff --git a/BTCPayServer/Hosting/BTCPayServerServices.cs b/BTCPayServer/Hosting/BTCPayServerServices.cs index 3bc391c3a..572699aba 100644 --- a/BTCPayServer/Hosting/BTCPayServerServices.cs +++ b/BTCPayServer/Hosting/BTCPayServerServices.cs @@ -332,6 +332,7 @@ namespace BTCPayServer.Hosting services.AddSingleton(o => o.GetRequiredService()); services.AddSingleton(); services.AddSingleton(o => o.GetRequiredService()); + services.AddSingleton(); services.AddHttpClient(WebhookSender.OnionNamedClient) .ConfigurePrimaryHttpMessageHandler(); diff --git a/BTCPayServer/Services/Mails/EmailSender.cs b/BTCPayServer/Services/Mails/EmailSender.cs index 3d23e9902..4f04e77ab 100644 --- a/BTCPayServer/Services/Mails/EmailSender.cs +++ b/BTCPayServer/Services/Mails/EmailSender.cs @@ -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(), Array.Empty(), 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); diff --git a/BTCPayServer/Services/Mails/EmailSettings.cs b/BTCPayServer/Services/Mails/EmailSettings.cs index 9cc258605..34c70cfa3 100644 --- a/BTCPayServer/Services/Mails/EmailSettings.cs +++ b/BTCPayServer/Services/Mails/EmailSettings.cs @@ -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()); + mm.Bcc.AddRange(bcc?? System.Array.Empty()); + return mm; + } public async Task CreateSmtpClient() diff --git a/BTCPayServer/Services/Mails/IEmailSender.cs b/BTCPayServer/Services/Mails/IEmailSender.cs index 8429b6878..ce0b9936e 100644 --- a/BTCPayServer/Services/Mails/IEmailSender.cs +++ b/BTCPayServer/Services/Mails/IEmailSender.cs @@ -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); } } diff --git a/BTCPayServer/Views/Shared/EmailsBody.cshtml b/BTCPayServer/Views/Shared/EmailsBody.cshtml index af015ae84..12220344a 100644 --- a/BTCPayServer/Views/Shared/EmailsBody.cshtml +++ b/BTCPayServer/Views/Shared/EmailsBody.cshtml @@ -1,19 +1,21 @@ @model BTCPayServer.Models.ServerViewModels.EmailsViewModel
-
-
-

@ViewData["Title"]

-