Store Email Settings: Improve configuration (#5629)

* Store Email Settings: Improve configuration

This works with the existing settings and provides better guidance about the different store email cases. Closes #5623.

* Split email and notification settings
This commit is contained in:
d11n 2024-01-26 10:28:50 +01:00 committed by GitHub
parent 2111b67e2c
commit b174977bc7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 117 additions and 71 deletions

View file

@ -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 <email@example.com>");
s.Driver.FindElement(By.Id("Save")).SendKeys(Keys.Enter);

View file

@ -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}. <a class='alert-link' href='{Url.Action("StoreEmailSettings", new { storeId })}'>Configure store email settings</a>."
});
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 store email settings</a>."
});
}
}
var vm = new StoreEmailRuleViewModel { Rules = blob.EmailRules ?? new List<StoreEmailRule>() };
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<IActionResult> 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;
}

View file

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

View file

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

View file

@ -1,27 +1,5 @@
@model BTCPayServer.Models.ServerViewModels.EmailsViewModel
<div class="row">
<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>
</div>
</div>
<form method="post" autocomplete="off">
<div class="row">
<div class="col-xl-10 col-xxl-constrain">
@ -30,8 +8,22 @@
<div asp-validation-summary="All"></div>
}
<div class="form-group">
<label asp-for="Settings.Server" class="form-label">SMTP Server</label>
<input asp-for="Settings.Server" data-fill="server" class="form-control"/>
<div class="d-flex flex-wrap gap-2 align-items-center justify-content-between">
<label asp-for="Settings.Server" class="form-label">SMTP Server</label>
<div class="dropdown only-for-js mt-n2" id="quick-fill">
<button class="btn btn-link p-0 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>
<input asp-for="Settings.Server" data-fill="server" class="form-control" />
<span asp-validation-for="Settings.Server" class="text-danger"></span>
</div>
<div class="form-group">
@ -53,7 +45,6 @@
<div class="form-group">
@if (!Model.PasswordSet)
{
<label asp-for="Settings.Password" class="form-label"></label>
<input asp-for="Settings.Password" type="password" class="form-control"/>
<span asp-validation-for="Settings.Password" class="text-danger"></span>

View file

@ -3,6 +3,7 @@
ViewData.SetActivePage(ServerNavPages.Emails, "Emails");
}
<h3 class="mb-4">Email Server</h3>
<partial name="EmailsBody" model="Model" />
@section PageFootContent {

View file

@ -113,6 +113,18 @@
</div>
</div>
<div class="form-group mb-5">
<h4 class="mb-3">Email Settings</h4>
<div class="form-check my-3">
<input asp-for="EnableStoresToUseServerEmailSettings" type="checkbox" class="form-check-input"/>
<label asp-for="EnableStoresToUseServerEmailSettings" class="form-check-label"></label>
<a href="https://docs.btcpayserver.org/Notifications/#server-emails" target="_blank" rel="noreferrer noopener">
<vc:icon symbol="info" />
</a>
<span asp-validation-for="EnableStoresToUseServerEmailSettings" class="text-danger"></span>
</div>
</div>
<div class="form-group mb-5">
<h4 class="mb-3">Notification Settings</h4>
<div class="form-check my-3">
@ -123,14 +135,6 @@
</a>
<span asp-validation-for="DisableInstantNotifications" class="text-danger"></span>
</div>
<div class="form-check my-3">
<input asp-for="DisableStoresToUseServerEmailSettings" type="checkbox" class="form-check-input"/>
<label asp-for="DisableStoresToUseServerEmailSettings" class="form-check-label"></label>
<a href="https://docs.btcpayserver.org/Notifications/#server-emails" target="_blank" rel="noreferrer noopener">
<vc:icon symbol="info" />
</a>
<span asp-validation-for="DisableStoresToUseServerEmailSettings" class="text-danger"></span>
</div>
</div>
<div class="form-group mb-5">

View file

@ -2,6 +2,7 @@
@{
Layout = "../Shared/_NavLayout.cshtml";
ViewData.SetActivePage(StoreNavPages.Emails, "Emails", Context.GetStoreData().Id);
var hasCustomSettings = Model.IsSetup() && !Model.UsesFallback();
}
<div class="row mb-4">
@ -19,7 +20,25 @@
</div>
</div>
<partial name="EmailsBody" model="Model" />
<h3 class="mb-4">Email Server</h3>
@if (Model.IsFallbackSetup())
{
<label class="d-flex align-items-center mb-4">
<input type="checkbox" id="UseCustomSMTP" checked="@hasCustomSettings" class="btcpay-toggle me-3" data-bs-toggle="collapse" data-bs-target="#SmtpSettings" aria-expanded="@hasCustomSettings" aria-controls="SmtpSettings" />
<div>
<span>Use custom SMTP settings for this store</span>
<div class="form-text">Otherwise, the server's SMTP settings will be used to send emails.</div>
</div>
</label>
<div class="checkout-settings collapse @(hasCustomSettings ? "show" : "")" id="SmtpSettings">
<partial name="EmailsBody" model="Model" />
</div>
}
else
{
<partial name="EmailsBody" model="Model" />
}
@section PageFootContent {
<partial name="_ValidationScriptsPartial" />

View file

@ -117,8 +117,7 @@
<code>{Payout.Metadata}*</code>
</td>
</tr>
<tr><td colspan="2">* These fields are JSON objects. You can access properties within them using <a href="https://www.newtonsoft.com/json/help/html/SelectToken.htm#SelectTokenJSONPath" rel="noreferrer noopener" target="_blank">this syntax</a>. One example is <code>{Invoice.Metadata.itemCode}</code>
</td></tr>
<tr><td colspan="2">* These fields are JSON objects. You can access properties within them using <a href="https://www.newtonsoft.com/json/help/html/SelectToken.htm#SelectTokenJSONPath" rel="noreferrer noopener" target="_blank">this syntax</a>. One example is <code>{Invoice.Metadata.itemCode}</code></td></tr>
</table>
</div>
</div>