Access email settings through the factory

This commit is contained in:
nicolas.dorier
2024-12-06 13:47:14 +09:00
parent 52a1627d81
commit 4d01e3a16a
13 changed files with 64 additions and 60 deletions

View File

@@ -14,6 +14,7 @@ using BTCPayServer.Hosting;
using BTCPayServer.Rating; using BTCPayServer.Rating;
using BTCPayServer.Services; using BTCPayServer.Services;
using BTCPayServer.Services.Invoices; using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Mails;
using BTCPayServer.Services.Rates; using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores; using BTCPayServer.Services.Stores;
using BTCPayServer.Tests.Logging; using BTCPayServer.Tests.Logging;

View File

@@ -47,6 +47,7 @@ namespace BTCPayServer.Controllers
readonly ILogger _logger; readonly ILogger _logger;
public PoliciesSettings PoliciesSettings { get; } public PoliciesSettings PoliciesSettings { get; }
public EmailSenderFactory EmailSenderFactory { get; }
public IStringLocalizer StringLocalizer { get; } public IStringLocalizer StringLocalizer { get; }
public Logs Logs { get; } public Logs Logs { get; }
@@ -62,6 +63,7 @@ namespace BTCPayServer.Controllers
Fido2Service fido2Service, Fido2Service fido2Service,
UserLoginCodeService userLoginCodeService, UserLoginCodeService userLoginCodeService,
LnurlAuthService lnurlAuthService, LnurlAuthService lnurlAuthService,
EmailSenderFactory emailSenderFactory,
LinkGenerator linkGenerator, LinkGenerator linkGenerator,
IStringLocalizer stringLocalizer, IStringLocalizer stringLocalizer,
Logs logs) Logs logs)
@@ -75,6 +77,7 @@ namespace BTCPayServer.Controllers
_btcPayServerEnvironment = btcPayServerEnvironment; _btcPayServerEnvironment = btcPayServerEnvironment;
_fido2Service = fido2Service; _fido2Service = fido2Service;
_lnurlAuthService = lnurlAuthService; _lnurlAuthService = lnurlAuthService;
EmailSenderFactory = emailSenderFactory;
_linkGenerator = linkGenerator; _linkGenerator = linkGenerator;
_userLoginCodeService = userLoginCodeService; _userLoginCodeService = userLoginCodeService;
_eventAggregator = eventAggregator; _eventAggregator = eventAggregator;
@@ -738,8 +741,7 @@ namespace BTCPayServer.Controllers
[RateLimitsFilter(ZoneLimits.ForgotPassword, Scope = RateLimitsScope.RemoteAddress)] [RateLimitsFilter(ZoneLimits.ForgotPassword, Scope = RateLimitsScope.RemoteAddress)]
public async Task<IActionResult> ForgotPassword(ForgotPasswordViewModel model) public async Task<IActionResult> ForgotPassword(ForgotPasswordViewModel model)
{ {
var settings = await _SettingsRepository.GetSettingAsync<EmailSettings>(); if (ModelState.IsValid && await EmailSenderFactory.IsComplete())
if (ModelState.IsValid && settings?.IsComplete() is true)
{ {
var user = await _userManager.FindByEmailAsync(model.Email); var user = await _userManager.FindByEmailAsync(model.Email);
if (!UserService.TryCanLogin(user, out _)) if (!UserService.TryCanLogin(user, out _))

View File

@@ -425,8 +425,7 @@ namespace BTCPayServer.Controllers
private async Task PrepareCreateUserViewData() private async Task PrepareCreateUserViewData()
{ {
var emailSettings = await _SettingsRepository.GetSettingAsync<EmailSettings>() ?? new EmailSettings(); ViewData["CanSendEmail"] = await _emailSenderFactory.IsComplete();
ViewData["CanSendEmail"] = emailSettings.IsComplete();
ViewData["AllowRequestEmailConfirmation"] = _policiesSettings.RequiresConfirmedEmail; ViewData["AllowRequestEmailConfirmation"] = _policiesSettings.RequiresConfirmedEmail;
} }
} }

View File

@@ -1199,7 +1199,7 @@ namespace BTCPayServer.Controllers
[HttpGet("server/emails")] [HttpGet("server/emails")]
public async Task<IActionResult> Emails() public async Task<IActionResult> Emails()
{ {
var email = await _SettingsRepository.GetSettingAsync<EmailSettings>() ?? new EmailSettings(); var email = await _emailSenderFactory.GetSettings() ?? new EmailSettings();
var vm = new ServerEmailsViewModel(email) var vm = new ServerEmailsViewModel(email)
{ {
EnableStoresToUseServerEmailSettings = !_policiesSettings.DisableStoresToUseServerEmailSettings EnableStoresToUseServerEmailSettings = !_policiesSettings.DisableStoresToUseServerEmailSettings
@@ -1216,7 +1216,7 @@ namespace BTCPayServer.Controllers
{ {
if (model.PasswordSet) if (model.PasswordSet)
{ {
var settings = await _SettingsRepository.GetSettingAsync<EmailSettings>() ?? new EmailSettings(); var settings = await _emailSenderFactory.GetSettings() ?? new EmailSettings();
model.Settings.Password = settings.Password; model.Settings.Password = settings.Password;
} }
model.Settings.Validate("Settings.", ModelState); model.Settings.Validate("Settings.", ModelState);
@@ -1262,7 +1262,7 @@ namespace BTCPayServer.Controllers
ModelState.AddModelError("Settings.From", StringLocalizer["Invalid email"]); ModelState.AddModelError("Settings.From", StringLocalizer["Invalid email"]);
return View(model); return View(model);
} }
var oldSettings = await _SettingsRepository.GetSettingAsync<EmailSettings>() ?? new EmailSettings(); var oldSettings = await _emailSenderFactory.GetSettings() ?? new EmailSettings();
if (new ServerEmailsViewModel(oldSettings).PasswordSet) if (new ServerEmailsViewModel(oldSettings).PasswordSet)
{ {
model.Settings.Password = oldSettings.Password; model.Settings.Password = oldSettings.Password;

View File

@@ -26,22 +26,18 @@ public partial class UIStoresController
if (store == null) if (store == null)
return NotFound(); return NotFound();
var blob = store.GetStoreBlob(); var configured = await _emailSenderFactory.IsComplete(store.Id);
if (blob.EmailSettings?.IsComplete() is not true && !TempData.HasStatusMessage()) if (!configured && !TempData.HasStatusMessage())
{ {
var emailSender = await _emailSenderFactory.GetEmailSender(store.Id) as StoreEmailSender; TempData.SetStatusMessageModel(new StatusMessageModel
if (!await IsSetupComplete(emailSender?.FallbackSender))
{ {
TempData.SetStatusMessageModel(new StatusMessageModel Severity = StatusMessageModel.StatusSeverity.Warning,
{ Html = "You need to configure email settings before this feature works." +
Severity = StatusMessageModel.StatusSeverity.Warning, $" <a class='alert-link' href='{Url.Action("StoreEmailSettings", new { storeId })}'>Configure store email settings</a>."
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 ?? [] }; var vm = new StoreEmailRuleViewModel { Rules = store.GetStoreBlob().EmailRules ?? [] };
return View(vm); return View(vm);
} }
@@ -110,8 +106,7 @@ public partial class UIStoresController
try try
{ {
var rule = vm.Rules[commandIndex]; var rule = vm.Rules[commandIndex];
var emailSender = await _emailSenderFactory.GetEmailSender(store.Id); if (await _emailSenderFactory.IsComplete(store.Id))
if (await IsSetupComplete(emailSender))
{ {
var recipients = rule.To.Split(",", StringSplitOptions.RemoveEmptyEntries) var recipients = rule.To.Split(",", StringSplitOptions.RemoveEmptyEntries)
.Select(o => .Select(o =>
@@ -121,7 +116,8 @@ public partial class UIStoresController
}) })
.Where(o => o != null) .Where(o => o != null)
.ToArray(); .ToArray();
var emailSender = await _emailSenderFactory.GetEmailSender(store.Id);
emailSender.SendEmail(recipients.ToArray(), null, null, $"[TEST] {rule.Subject}", rule.Body); emailSender.SendEmail(recipients.ToArray(), null, null, $"[TEST] {rule.Subject}", rule.Body);
message += StringLocalizer["Test email sent — please verify you received it."]; message += StringLocalizer["Test email sent — please verify you received it."];
} }
@@ -178,14 +174,12 @@ public partial class UIStoresController
if (store == null) if (store == null)
return NotFound(); return NotFound();
var blob = store.GetStoreBlob(); var emailSender = await _emailSenderFactory.GetEmailSender(store.Id);
var data = blob.EmailSettings ?? new EmailSettings(); var data = await emailSender.GetEmailSettings() ?? new EmailSettings();
var fallbackSettings = await _emailSenderFactory.GetEmailSender(store.Id) is StoreEmailSender { FallbackSender: not null } storeSender var fallbackSettings = emailSender is StoreEmailSender { FallbackSender: { } fallbackSender }
? await storeSender.FallbackSender.GetEmailSettings() ? await fallbackSender.GetEmailSettings()
: null; : null;
var vm = new EmailsViewModel(data, fallbackSettings); return View(new EmailsViewModel(data, fallbackSettings));
return View(vm);
} }
[HttpPost("{storeId}/email-settings")] [HttpPost("{storeId}/email-settings")]
@@ -262,9 +256,4 @@ public partial class UIStoresController
} }
return RedirectToAction(nameof(StoreEmailSettings), new { storeId }); return RedirectToAction(nameof(StoreEmailSettings), new { storeId });
} }
private static async Task<bool> IsSetupComplete(IEmailSender emailSender)
{
return emailSender is not null && (await emailSender.GetEmailSettings())?.IsComplete() == true;
}
} }

View File

@@ -61,7 +61,8 @@ public partial class UIStoresController
var result = await _userManager.CreateAsync(user); var result = await _userManager.CreateAsync(user);
if (result.Succeeded) if (result.Succeeded)
{ {
var tcs = new TaskCompletionSource<Uri>(); var invitationEmail = await _emailSenderFactory.IsComplete();
var tcs = new TaskCompletionSource<Uri>();
var currentUser = await _userManager.GetUserAsync(HttpContext.User); var currentUser = await _userManager.GetUserAsync(HttpContext.User);
_eventAggregator.Publish(new UserRegisteredEvent _eventAggregator.Publish(new UserRegisteredEvent
@@ -70,14 +71,13 @@ public partial class UIStoresController
Kind = UserRegisteredEventKind.Invite, Kind = UserRegisteredEventKind.Invite,
User = user, User = user,
InvitedByUser = currentUser, InvitedByUser = currentUser,
SendInvitationEmail = true, SendInvitationEmail = invitationEmail,
CallbackUrlGenerated = tcs CallbackUrlGenerated = tcs
}); });
var callbackUrl = await tcs.Task; var callbackUrl = await tcs.Task;
var settings = await _settingsRepository.GetSettingAsync<EmailSettings>() ?? new EmailSettings(); var info = invitationEmail
var info = settings.IsComplete() ? "An invitation email has been sent.<br/>You may alternatively"
? "An invitation email has been sent.<br/>You may alternatively"
: "An invitation email has not been sent, because the server does not have an email server configured.<br/> You need to"; : "An invitation email has not been sent, because the server does not have an email server configured.<br/> You need to";
successInfo = $"{info} share this link with them: <a class='alert-link' href='{callbackUrl}'>{callbackUrl}</a>"; successInfo = $"{info} share this link with them: <a class='alert-link' href='{callbackUrl}'>{callbackUrl}</a>";
} }

View File

@@ -60,7 +60,6 @@ public partial class UIStoresController : Controller
WalletFileParsers onChainWalletParsers, WalletFileParsers onChainWalletParsers,
UIUserStoresController userStoresController, UIUserStoresController userStoresController,
UriResolver uriResolver, UriResolver uriResolver,
SettingsRepository settingsRepository,
CurrencyNameTable currencyNameTable, CurrencyNameTable currencyNameTable,
IStringLocalizer stringLocalizer, IStringLocalizer stringLocalizer,
EventAggregator eventAggregator, EventAggregator eventAggregator,
@@ -88,7 +87,6 @@ public partial class UIStoresController : Controller
_onChainWalletParsers = onChainWalletParsers; _onChainWalletParsers = onChainWalletParsers;
_userStoresController = userStoresController; _userStoresController = userStoresController;
_uriResolver = uriResolver; _uriResolver = uriResolver;
_settingsRepository = settingsRepository;
_currencyNameTable = currencyNameTable; _currencyNameTable = currencyNameTable;
_eventAggregator = eventAggregator; _eventAggregator = eventAggregator;
_html = html; _html = html;
@@ -110,7 +108,6 @@ public partial class UIStoresController : Controller
private readonly TokenRepository _tokenRepository; private readonly TokenRepository _tokenRepository;
private readonly UserManager<ApplicationUser> _userManager; private readonly UserManager<ApplicationUser> _userManager;
private readonly RateFetcher _rateFactory; private readonly RateFetcher _rateFactory;
private readonly SettingsRepository _settingsRepository;
private readonly CurrencyNameTable _currencyNameTable; private readonly CurrencyNameTable _currencyNameTable;
private readonly ExplorerClientProvider _explorerProvider; private readonly ExplorerClientProvider _explorerProvider;
private readonly LanguageService _langService; private readonly LanguageService _langService;

View File

@@ -1,3 +1,4 @@
#nullable enable
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using BTCPayServer.Logging; using BTCPayServer.Logging;
@@ -28,7 +29,7 @@ namespace BTCPayServer.Services.Mails
_JobClient.Schedule(async cancellationToken => _JobClient.Schedule(async cancellationToken =>
{ {
var emailSettings = await GetEmailSettings(); var emailSettings = await GetEmailSettings();
if (emailSettings?.IsComplete() != true) if (emailSettings?.IsComplete() is not true)
{ {
Logs.Configuration.LogWarning("Should have sent email, but email settings are not configured"); Logs.Configuration.LogWarning("Should have sent email, but email settings are not configured");
return; return;
@@ -42,7 +43,7 @@ namespace BTCPayServer.Services.Mails
}, TimeSpan.Zero); }, TimeSpan.Zero);
} }
public abstract Task<EmailSettings> GetEmailSettings(); public abstract Task<EmailSettings?> GetEmailSettings();
public abstract Task<string> GetPrefixedSubject(string subject); public abstract Task<string> GetPrefixedSubject(string subject);
} }
} }

View File

@@ -1,3 +1,4 @@
#nullable enable
using System.Threading.Tasks; using System.Threading.Tasks;
using BTCPayServer.HostedServices; using BTCPayServer.HostedServices;
using BTCPayServer.Logging; using BTCPayServer.Logging;
@@ -27,7 +28,7 @@ namespace BTCPayServer.Services.Mails
_storeRepository = storeRepository; _storeRepository = storeRepository;
} }
public Task<IEmailSender> GetEmailSender(string storeId = null) public Task<IEmailSender> GetEmailSender(string? storeId = null)
{ {
var serverSender = new ServerEmailSender(_settingsRepository, _jobClient, Logs); var serverSender = new ServerEmailSender(_settingsRepository, _jobClient, Logs);
if (string.IsNullOrEmpty(storeId)) if (string.IsNullOrEmpty(storeId))
@@ -36,5 +37,16 @@ namespace BTCPayServer.Services.Mails
!PoliciesSettings.Settings.DisableStoresToUseServerEmailSettings ? serverSender : null, _jobClient, !PoliciesSettings.Settings.DisableStoresToUseServerEmailSettings ? serverSender : null, _jobClient,
storeId, Logs)); storeId, Logs));
} }
}
public async Task<bool> IsComplete(string? storeId = null)
{
var settings = await this.GetSettings(storeId);
return settings?.IsComplete() is true;
}
public async Task<EmailSettings?> GetSettings(string? storeId = null)
{
var sender = await this.GetEmailSender(storeId);
return await sender.GetEmailSettings();
}
}
} }

View File

@@ -1,3 +1,4 @@
#nullable enable
using System.Threading.Tasks; using System.Threading.Tasks;
using MimeKit; using MimeKit;
@@ -7,6 +8,6 @@ namespace BTCPayServer.Services.Mails
{ {
void SendEmail(MailboxAddress 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); void SendEmail(MailboxAddress[] email, MailboxAddress[] cc, MailboxAddress[] bcc, string subject, string message);
Task<EmailSettings> GetEmailSettings(); Task<EmailSettings?> GetEmailSettings();
} }
} }

View File

@@ -1,3 +1,4 @@
#nullable enable
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using BTCPayServer.Data; using BTCPayServer.Data;
@@ -9,7 +10,7 @@ namespace BTCPayServer.Services.Mails
class StoreEmailSender : EmailSender class StoreEmailSender : EmailSender
{ {
public StoreEmailSender(StoreRepository storeRepository, public StoreEmailSender(StoreRepository storeRepository,
EmailSender fallback, EmailSender? fallback,
IBackgroundJobClient backgroundJobClient, IBackgroundJobClient backgroundJobClient,
string storeId, string storeId,
Logs logs) : base(backgroundJobClient, logs) Logs logs) : base(backgroundJobClient, logs)
@@ -20,20 +21,22 @@ namespace BTCPayServer.Services.Mails
} }
public StoreRepository StoreRepository { get; } public StoreRepository StoreRepository { get; }
public EmailSender FallbackSender { get; } public EmailSender? FallbackSender { get; }
public string StoreId { get; } public string StoreId { get; }
public override async Task<EmailSettings> GetEmailSettings() public override async Task<EmailSettings?> GetEmailSettings()
{ {
var store = await StoreRepository.FindStore(StoreId); var store = await StoreRepository.FindStore(StoreId);
if (store is null)
return null;
var emailSettings = store.GetStoreBlob().EmailSettings; var emailSettings = store.GetStoreBlob().EmailSettings;
if (emailSettings?.IsComplete() == true) if (emailSettings?.IsComplete() is true)
{ {
return emailSettings; return emailSettings;
} }
if (FallbackSender != null) if (FallbackSender is not null)
return await FallbackSender?.GetEmailSettings(); return await FallbackSender.GetEmailSettings();
return null; return null;
} }

View File

@@ -1,11 +1,11 @@
@using BTCPayServer.Services @using BTCPayServer.Services
@using BTCPayServer.Services.Mails @using BTCPayServer.Services.Mails
@using Microsoft.AspNetCore.Mvc.TagHelpers @using Microsoft.AspNetCore.Mvc.TagHelpers
@model ForgotPasswordViewModel @model ForgotPasswordViewModel
@inject SettingsRepository SettingsRepository @inject EmailSenderFactory EmailSenderFactory
@{ @{
var isEmailConfigured = (await SettingsRepository.GetSettingAsync<EmailSettings>())?.IsComplete() is true; var isEmailConfigured = await EmailSenderFactory.IsComplete();
ViewData["Title"] = isEmailConfigured ? "Forgot your password?" : "Email Server Configuration Required"; ViewData["Title"] = isEmailConfigured ? "Forgot your password?" : "Email Server Configuration Required";
Layout = "_LayoutSignedOut"; Layout = "_LayoutSignedOut";
} }

View File

@@ -1,7 +1,7 @@
@using BTCPayServer.Services @using BTCPayServer.Services
@using BTCPayServer.Services.Mails; @using BTCPayServer.Services.Mails;
@model BTCPayServer.Services.PoliciesSettings @model BTCPayServer.Services.PoliciesSettings
@inject SettingsRepository _SettingsRepository @inject EmailSenderFactory EmailSenderFactory
@inject TransactionLinkProviders TransactionLinkProviders @inject TransactionLinkProviders TransactionLinkProviders
@{ @{
ViewData.SetActivePage(ServerNavPages.Policies); ViewData.SetActivePage(ServerNavPages.Policies);
@@ -47,10 +47,9 @@
<div class="subsettings"> <div class="subsettings">
<div class="d-flex my-3"> <div class="d-flex my-3">
@{ @{
var emailSettings = (await _SettingsRepository.GetSettingAsync<EmailSettings>()) ?? new EmailSettings();
/* The "|| Model.RequiresConfirmedEmail" check is for the case when a user had checked /* The "|| Model.RequiresConfirmedEmail" check is for the case when a user had checked
the checkbox without first configuring the e-mail settings so that they can uncheck it. */ the checkbox without first configuring the e-mail settings so that they can uncheck it. */
var isEmailConfigured = emailSettings.IsComplete() || Model.RequiresConfirmedEmail; var isEmailConfigured = await EmailSenderFactory.IsComplete() || Model.RequiresConfirmedEmail;
} }
<input asp-for="RequiresConfirmedEmail" type="checkbox" class="btcpay-toggle me-3" disabled="@(isEmailConfigured ? null : "disabled")" /> <input asp-for="RequiresConfirmedEmail" type="checkbox" class="btcpay-toggle me-3" disabled="@(isEmailConfigured ? null : "disabled")" />
<div> <div>