diff --git a/BTCPayServer.Tests/SeleniumTests.cs b/BTCPayServer.Tests/SeleniumTests.cs index e2a1a253b..59b13888c 100644 --- a/BTCPayServer.Tests/SeleniumTests.cs +++ b/BTCPayServer.Tests/SeleniumTests.cs @@ -468,8 +468,16 @@ namespace BTCPayServer.Tests s.RegisterNewUser(true); s.CreateNewStore(); + // Ensure empty server settings + s.Driver.Navigate().GoToUrl(s.Link("/server/emails")); + s.Driver.FindElement(By.Id("Settings_Login")).Clear(); + s.Driver.FindElement(By.Id("Settings_Password")).Clear(); + s.Driver.FindElement(By.Id("Settings_From")).Clear(); + s.Driver.FindElement(By.Id("Save")).Submit(); + // Store Emails without server fallback s.GoToStore(StoreNavPages.Emails); + s.Driver.ElementDoesNotExist(By.Id("UseCustomSMTP")); s.Driver.FindElement(By.Id("ConfigureEmailRules")).Click(); Assert.Contains("You need to configure email settings before this feature works", s.Driver.PageSource); @@ -481,13 +489,16 @@ namespace BTCPayServer.Tests s.FindAlertMessage(); } CanSetupEmailCore(s); - + // Store Emails with server fallback s.GoToStore(StoreNavPages.Emails); + Assert.False(s.Driver.FindElement(By.Id("UseCustomSMTP")).Selected); s.Driver.FindElement(By.Id("ConfigureEmailRules")).Click(); - Assert.Contains("Emails will be sent with the email settings of the server", s.Driver.PageSource); + Assert.DoesNotContain("You need to configure email settings before this feature works", s.Driver.PageSource); s.GoToStore(StoreNavPages.Emails); + s.Driver.FindElement(By.Id("UseCustomSMTP")).Click(); + Thread.Sleep(250); CanSetupEmailCore(s); // Store Email Rules @@ -495,7 +506,6 @@ namespace BTCPayServer.Tests 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); - Assert.DoesNotContain("Emails will be sent with the email settings of the server", s.Driver.PageSource); s.Driver.FindElement(By.Id("CreateEmailRule")).Click(); var select = new SelectElement(s.Driver.FindElement(By.Id("Rules_0__Trigger"))); @@ -506,6 +516,9 @@ namespace BTCPayServer.Tests s.Driver.FindElement(By.ClassName("note-editable")).SendKeys("Your invoice is settled"); s.Driver.FindElement(By.Id("SaveEmailRules")).Click(); Assert.Contains("Store email rules saved", s.FindAlertMessage().Text); + + s.GoToStore(StoreNavPages.Emails); + Assert.True(s.Driver.FindElement(By.Id("UseCustomSMTP")).Selected); } [Fact(Timeout = TestTimeout)] @@ -3159,15 +3172,13 @@ retry: private static void CanSetupEmailCore(SeleniumTester s) { + s.Driver.ScrollTo(By.Id("QuickFillDropdownToggle")); s.Driver.FindElement(By.Id("QuickFillDropdownToggle")).Click(); s.Driver.FindElement(By.CssSelector("#quick-fill .dropdown-menu .dropdown-item:first-child")).Click(); s.Driver.FindElement(By.Id("Settings_Login")).Clear(); s.Driver.FindElement(By.Id("Settings_Login")).SendKeys("test@gmail.com"); - s.Driver.FindElement(By.CssSelector("button[value=\"Save\"]")).Submit(); - s.FindAlertMessage(); s.Driver.FindElement(By.Id("Settings_Password")).Clear(); s.Driver.FindElement(By.Id("Settings_Password")).SendKeys("mypassword"); - s.Driver.FindElement(By.Id("Settings_From")).Clear(); s.Driver.FindElement(By.Id("Settings_From")).SendKeys("Firstname Lastname "); s.Driver.FindElement(By.Id("Save")).SendKeys(Keys.Enter); diff --git a/BTCPayServer/Controllers/UIStoresController.Email.cs b/BTCPayServer/Controllers/UIStoresController.Email.cs index 94e65ba4f..e1a1520aa 100644 --- a/BTCPayServer/Controllers/UIStoresController.Email.cs +++ b/BTCPayServer/Controllers/UIStoresController.Email.cs @@ -27,22 +27,20 @@ namespace BTCPayServer.Controllers return NotFound(); var blob = store.GetStoreBlob(); - var storeSetupComplete = blob.EmailSettings?.IsComplete() is true; - if (!storeSetupComplete && !TempData.HasStatusMessage()) + if (blob.EmailSettings?.IsComplete() is not true && !TempData.HasStatusMessage()) { var emailSender = await _emailSenderFactory.GetEmailSender(store.Id) as StoreEmailSender; - var hasServerFallback = await IsSetupComplete(emailSender?.FallbackSender); - var message = hasServerFallback - ? "Emails will be sent with the email settings of the server" - : "You need to configure email settings before this feature works"; - TempData.SetStatusMessageModel(new StatusMessageModel + if (!await IsSetupComplete(emailSender?.FallbackSender)) { - Severity = hasServerFallback ? StatusMessageModel.StatusSeverity.Info : StatusMessageModel.StatusSeverity.Warning, - Html = $"{message}. Configure store email settings." - }); + TempData.SetStatusMessageModel(new StatusMessageModel + { + Severity = StatusMessageModel.StatusSeverity.Warning, + Html = $"You need to configure email settings before this feature works. Configure store email settings." + }); + } } - var vm = new StoreEmailRuleViewModel { Rules = blob.EmailRules ?? new List() }; + var vm = new StoreEmailRuleViewModel { Rules = blob.EmailRules ?? [] }; return View(vm); } @@ -172,13 +170,20 @@ namespace BTCPayServer.Controllers } [HttpGet("{storeId}/email-settings")] - public IActionResult StoreEmailSettings() + public async Task StoreEmailSettings(string storeId) { var store = HttpContext.GetStoreData(); if (store == null) return NotFound(); - var data = store.GetStoreBlob().EmailSettings ?? new EmailSettings(); - return View(new EmailsViewModel(data)); + + var blob = store.GetStoreBlob(); + var data = blob.EmailSettings ?? new EmailSettings(); + var fallbackSettings = await _emailSenderFactory.GetEmailSender(store.Id) is StoreEmailSender { FallbackSender: not null } storeSender + ? await storeSender.FallbackSender.GetEmailSettings() + : null; + var vm = new EmailsViewModel(data, fallbackSettings); + + return View(vm); } [HttpPost("{storeId}/email-settings")] @@ -187,7 +192,13 @@ namespace BTCPayServer.Controllers var store = HttpContext.GetStoreData(); if (store == null) return NotFound(); - + + var emailSender = await _emailSenderFactory.GetEmailSender(store.Id) as StoreEmailSender; + var fallbackSettings = await _emailSenderFactory.GetEmailSender(store.Id) is StoreEmailSender { FallbackSender: not null } storeSender + ? await storeSender.FallbackSender.GetEmailSettings() + : null; + model.FallbackSettings = fallbackSettings; + if (command == "Test") { try @@ -230,7 +241,7 @@ namespace BTCPayServer.Controllers return View(model); } var storeBlob = store.GetStoreBlob(); - if (new EmailsViewModel(storeBlob.EmailSettings).PasswordSet && storeBlob.EmailSettings != null) + if (storeBlob.EmailSettings != null && new EmailsViewModel(storeBlob.EmailSettings, fallbackSettings).PasswordSet) { model.Settings.Password = storeBlob.EmailSettings.Password; } diff --git a/BTCPayServer/Models/ServerViewModels/EmailsViewModel.cs b/BTCPayServer/Models/ServerViewModels/EmailsViewModel.cs index 63805b4ee..c2f4ad35b 100644 --- a/BTCPayServer/Models/ServerViewModels/EmailsViewModel.cs +++ b/BTCPayServer/Models/ServerViewModels/EmailsViewModel.cs @@ -6,25 +6,27 @@ namespace BTCPayServer.Models.ServerViewModels { public class EmailsViewModel { + public EmailSettings Settings { get; set; } + public EmailSettings FallbackSettings { get; set; } + public bool PasswordSet { get; set; } + + [MailboxAddress] + [Display(Name = "Test Email")] + public string TestEmail { get; set; } + public EmailsViewModel() { - } - public EmailsViewModel(EmailSettings settings) + + public EmailsViewModel(EmailSettings settings, EmailSettings fallbackSettings = null) { Settings = settings; + FallbackSettings = fallbackSettings; PasswordSet = !string.IsNullOrEmpty(settings?.Password); } - public EmailSettings Settings - { - get; set; - } - public bool PasswordSet { get; set; } - [MailboxAddressAttribute] - [Display(Name = "Test Email")] - public string TestEmail - { - get; set; - } + + public bool IsSetup() => Settings?.IsComplete() is true; + public bool IsFallbackSetup() => FallbackSettings?.IsComplete() is true; + public bool UsesFallback() => IsFallbackSetup() && Settings == FallbackSettings; } } diff --git a/BTCPayServer/Services/PoliciesSettings.cs b/BTCPayServer/Services/PoliciesSettings.cs index 9317c84e4..3eb858d23 100644 --- a/BTCPayServer/Services/PoliciesSettings.cs +++ b/BTCPayServer/Services/PoliciesSettings.cs @@ -30,6 +30,14 @@ namespace BTCPayServer.Services public bool DisableInstantNotifications { get; set; } [Display(Name = "Disable stores from using the server's email settings as backup")] public bool DisableStoresToUseServerEmailSettings { get; set; } + [JsonIgnore] + [Display(Name = "Allow stores to use the server's SMTP email settings as a default")] + public bool EnableStoresToUseServerEmailSettings + { + get => !DisableStoresToUseServerEmailSettings; + set { DisableStoresToUseServerEmailSettings = !value; } + } + [Display(Name = "Disable non-admins access to the user creation API endpoint")] public bool DisableNonAdminCreateUserApi { get; set; } diff --git a/BTCPayServer/Views/Shared/EmailsBody.cshtml b/BTCPayServer/Views/Shared/EmailsBody.cshtml index fcc599507..593659727 100644 --- a/BTCPayServer/Views/Shared/EmailsBody.cshtml +++ b/BTCPayServer/Views/Shared/EmailsBody.cshtml @@ -1,27 +1,5 @@ @model BTCPayServer.Models.ServerViewModels.EmailsViewModel -
-
-
-

Email Server

-
- -
-
-
-
-
@@ -30,8 +8,22 @@
}
- - +
+ + +
+
@@ -53,7 +45,6 @@
@if (!Model.PasswordSet) { - diff --git a/BTCPayServer/Views/UIServer/Emails.cshtml b/BTCPayServer/Views/UIServer/Emails.cshtml index e8ad13664..edf7797f4 100644 --- a/BTCPayServer/Views/UIServer/Emails.cshtml +++ b/BTCPayServer/Views/UIServer/Emails.cshtml @@ -3,6 +3,7 @@ ViewData.SetActivePage(ServerNavPages.Emails, "Emails"); } +

Email Server

@section PageFootContent { diff --git a/BTCPayServer/Views/UIServer/Policies.cshtml b/BTCPayServer/Views/UIServer/Policies.cshtml index 2348ecb0a..f5489aa80 100644 --- a/BTCPayServer/Views/UIServer/Policies.cshtml +++ b/BTCPayServer/Views/UIServer/Policies.cshtml @@ -113,6 +113,18 @@
+
+

Email Settings

+
+ + + + + + +
+
+

Notification Settings

@@ -123,14 +135,6 @@
-
- - - - - - -
diff --git a/BTCPayServer/Views/UIStores/StoreEmailSettings.cshtml b/BTCPayServer/Views/UIStores/StoreEmailSettings.cshtml index 862bb0784..c657dbb49 100644 --- a/BTCPayServer/Views/UIStores/StoreEmailSettings.cshtml +++ b/BTCPayServer/Views/UIStores/StoreEmailSettings.cshtml @@ -2,6 +2,7 @@ @{ Layout = "../Shared/_NavLayout.cshtml"; ViewData.SetActivePage(StoreNavPages.Emails, "Emails", Context.GetStoreData().Id); + var hasCustomSettings = Model.IsSetup() && !Model.UsesFallback(); }
@@ -19,7 +20,25 @@
- +

Email Server

+@if (Model.IsFallbackSetup()) +{ + + +
+ +
+} +else +{ + +} @section PageFootContent { diff --git a/BTCPayServer/Views/UIStores/StoreEmails.cshtml b/BTCPayServer/Views/UIStores/StoreEmails.cshtml index ff597ff49..35a952fb3 100644 --- a/BTCPayServer/Views/UIStores/StoreEmails.cshtml +++ b/BTCPayServer/Views/UIStores/StoreEmails.cshtml @@ -117,8 +117,7 @@ {Payout.Metadata}* - * These fields are JSON objects. You can access properties within them using this syntax. One example is {Invoice.Metadata.itemCode} - + * These fields are JSON objects. You can access properties within them using this syntax. One example is {Invoice.Metadata.itemCode}