Skip to content

Commit

Permalink
Store Email Settings: Improve configuration (#5629)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
dennisreimann committed Jan 26, 2024
1 parent 2111b67 commit b174977
Show file tree
Hide file tree
Showing 9 changed files with 117 additions and 71 deletions.
23 changes: 17 additions & 6 deletions BTCPayServer.Tests/SeleniumTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -468,8 +468,16 @@ public async Task CanSetupEmailServer()
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);

Expand All @@ -481,21 +489,23 @@ public async Task CanSetupEmailServer()
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
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);
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")));
Expand All @@ -506,6 +516,9 @@ public async Task CanSetupEmailServer()
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)]
Expand Down Expand Up @@ -3159,15 +3172,13 @@ private static void CanBrowseContent(SeleniumTester s)

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);
Expand Down
43 changes: 27 additions & 16 deletions BTCPayServer/Controllers/UIStoresController.Email.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,22 +27,20 @@ public async Task<IActionResult> StoreEmails(string storeId)
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);
}

Expand Down Expand Up @@ -172,13 +170,20 @@ public class StoreEmailRule
}

[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")]
Expand All @@ -187,7 +192,13 @@ public async Task<IActionResult> StoreEmailSettings(string storeId, EmailsViewMo
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
Expand Down Expand Up @@ -230,7 +241,7 @@ public async Task<IActionResult> StoreEmailSettings(string storeId, EmailsViewMo
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;
}
Expand Down
28 changes: 15 additions & 13 deletions BTCPayServer/Models/ServerViewModels/EmailsViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
8 changes: 8 additions & 0 deletions BTCPayServer/Services/PoliciesSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,14 @@ public class PoliciesSettings
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; }

Expand Down
41 changes: 16 additions & 25 deletions BTCPayServer/Views/Shared/EmailsBody.cshtml
Original file line number Diff line number Diff line change
@@ -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">
Expand All @@ -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">
Expand All @@ -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>
Expand Down
1 change: 1 addition & 0 deletions BTCPayServer/Views/UIServer/Emails.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
ViewData.SetActivePage(ServerNavPages.Emails, "Emails");
}

<h3 class="mb-4">Email Server</h3>
<partial name="EmailsBody" model="Model" />

@section PageFootContent {
Expand Down
20 changes: 12 additions & 8 deletions BTCPayServer/Views/UIServer/Policies.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -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">
Expand All @@ -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">
Expand Down
21 changes: 20 additions & 1 deletion BTCPayServer/Views/UIStores/StoreEmailSettings.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -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">
Expand All @@ -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" />
Expand Down
3 changes: 1 addition & 2 deletions BTCPayServer/Views/UIStores/StoreEmails.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down

0 comments on commit b174977

Please sign in to comment.