diff --git a/BTCPayServer.Tests/BTCPayServerTester.cs b/BTCPayServer.Tests/BTCPayServerTester.cs index d6036ae31..4c6cd1a73 100644 --- a/BTCPayServer.Tests/BTCPayServerTester.cs +++ b/BTCPayServer.Tests/BTCPayServerTester.cs @@ -14,6 +14,7 @@ using BTCPayServer.Hosting; using BTCPayServer.Rating; using BTCPayServer.Services; using BTCPayServer.Services.Invoices; +using BTCPayServer.Services.Mails; using BTCPayServer.Services.Rates; using BTCPayServer.Services.Stores; using BTCPayServer.Tests.Logging; diff --git a/BTCPayServer/Controllers/UIAccountController.cs b/BTCPayServer/Controllers/UIAccountController.cs index cac0fa637..96aaee309 100644 --- a/BTCPayServer/Controllers/UIAccountController.cs +++ b/BTCPayServer/Controllers/UIAccountController.cs @@ -47,6 +47,7 @@ namespace BTCPayServer.Controllers readonly ILogger _logger; public PoliciesSettings PoliciesSettings { get; } + public EmailSenderFactory EmailSenderFactory { get; } public IStringLocalizer StringLocalizer { get; } public Logs Logs { get; } @@ -62,6 +63,7 @@ namespace BTCPayServer.Controllers Fido2Service fido2Service, UserLoginCodeService userLoginCodeService, LnurlAuthService lnurlAuthService, + EmailSenderFactory emailSenderFactory, LinkGenerator linkGenerator, IStringLocalizer stringLocalizer, Logs logs) @@ -75,6 +77,7 @@ namespace BTCPayServer.Controllers _btcPayServerEnvironment = btcPayServerEnvironment; _fido2Service = fido2Service; _lnurlAuthService = lnurlAuthService; + EmailSenderFactory = emailSenderFactory; _linkGenerator = linkGenerator; _userLoginCodeService = userLoginCodeService; _eventAggregator = eventAggregator; @@ -738,8 +741,7 @@ namespace BTCPayServer.Controllers [RateLimitsFilter(ZoneLimits.ForgotPassword, Scope = RateLimitsScope.RemoteAddress)] public async Task ForgotPassword(ForgotPasswordViewModel model) { - var settings = await _SettingsRepository.GetSettingAsync(); - if (ModelState.IsValid && settings?.IsComplete() is true) + if (ModelState.IsValid && await EmailSenderFactory.IsComplete()) { var user = await _userManager.FindByEmailAsync(model.Email); if (!UserService.TryCanLogin(user, out _)) diff --git a/BTCPayServer/Controllers/UIServerController.Users.cs b/BTCPayServer/Controllers/UIServerController.Users.cs index 02b588d37..678cde1bb 100644 --- a/BTCPayServer/Controllers/UIServerController.Users.cs +++ b/BTCPayServer/Controllers/UIServerController.Users.cs @@ -425,8 +425,7 @@ namespace BTCPayServer.Controllers private async Task PrepareCreateUserViewData() { - var emailSettings = await _SettingsRepository.GetSettingAsync() ?? new EmailSettings(); - ViewData["CanSendEmail"] = emailSettings.IsComplete(); + ViewData["CanSendEmail"] = await _emailSenderFactory.IsComplete(); ViewData["AllowRequestEmailConfirmation"] = _policiesSettings.RequiresConfirmedEmail; } } diff --git a/BTCPayServer/Controllers/UIServerController.cs b/BTCPayServer/Controllers/UIServerController.cs index 76cfee14d..f97cfcef0 100644 --- a/BTCPayServer/Controllers/UIServerController.cs +++ b/BTCPayServer/Controllers/UIServerController.cs @@ -1199,7 +1199,7 @@ namespace BTCPayServer.Controllers [HttpGet("server/emails")] public async Task Emails() { - var email = await _SettingsRepository.GetSettingAsync() ?? new EmailSettings(); + var email = await _emailSenderFactory.GetSettings() ?? new EmailSettings(); var vm = new ServerEmailsViewModel(email) { EnableStoresToUseServerEmailSettings = !_policiesSettings.DisableStoresToUseServerEmailSettings @@ -1216,7 +1216,7 @@ namespace BTCPayServer.Controllers { if (model.PasswordSet) { - var settings = await _SettingsRepository.GetSettingAsync() ?? new EmailSettings(); + var settings = await _emailSenderFactory.GetSettings() ?? new EmailSettings(); model.Settings.Password = settings.Password; } model.Settings.Validate("Settings.", ModelState); @@ -1262,7 +1262,7 @@ namespace BTCPayServer.Controllers ModelState.AddModelError("Settings.From", StringLocalizer["Invalid email"]); return View(model); } - var oldSettings = await _SettingsRepository.GetSettingAsync() ?? new EmailSettings(); + var oldSettings = await _emailSenderFactory.GetSettings() ?? new EmailSettings(); if (new ServerEmailsViewModel(oldSettings).PasswordSet) { model.Settings.Password = oldSettings.Password; diff --git a/BTCPayServer/Controllers/UIStoresController.Email.cs b/BTCPayServer/Controllers/UIStoresController.Email.cs index e20ac04b3..b5108aa75 100644 --- a/BTCPayServer/Controllers/UIStoresController.Email.cs +++ b/BTCPayServer/Controllers/UIStoresController.Email.cs @@ -26,22 +26,18 @@ public partial class UIStoresController if (store == null) return NotFound(); - var blob = store.GetStoreBlob(); - if (blob.EmailSettings?.IsComplete() is not true && !TempData.HasStatusMessage()) + var configured = await _emailSenderFactory.IsComplete(store.Id); + if (!configured && !TempData.HasStatusMessage()) { - var emailSender = await _emailSenderFactory.GetEmailSender(store.Id) as StoreEmailSender; - if (!await IsSetupComplete(emailSender?.FallbackSender)) + TempData.SetStatusMessageModel(new StatusMessageModel { - TempData.SetStatusMessageModel(new StatusMessageModel - { - Severity = StatusMessageModel.StatusSeverity.Warning, - Html = "You need to configure email settings before this feature works." + - $" Configure store email settings." - }); - } + 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 ?? [] }; + var vm = new StoreEmailRuleViewModel { Rules = store.GetStoreBlob().EmailRules ?? [] }; return View(vm); } @@ -110,8 +106,7 @@ public partial class UIStoresController try { var rule = vm.Rules[commandIndex]; - var emailSender = await _emailSenderFactory.GetEmailSender(store.Id); - if (await IsSetupComplete(emailSender)) + if (await _emailSenderFactory.IsComplete(store.Id)) { var recipients = rule.To.Split(",", StringSplitOptions.RemoveEmptyEntries) .Select(o => @@ -121,7 +116,8 @@ public partial class UIStoresController }) .Where(o => o != null) .ToArray(); - + + var emailSender = await _emailSenderFactory.GetEmailSender(store.Id); emailSender.SendEmail(recipients.ToArray(), null, null, $"[TEST] {rule.Subject}", rule.Body); message += StringLocalizer["Test email sent — please verify you received it."]; } @@ -178,14 +174,12 @@ public partial class UIStoresController if (store == null) return NotFound(); - 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() + var emailSender = await _emailSenderFactory.GetEmailSender(store.Id); + var data = await emailSender.GetEmailSettings() ?? new EmailSettings(); + var fallbackSettings = emailSender is StoreEmailSender { FallbackSender: { } fallbackSender } + ? await fallbackSender.GetEmailSettings() : null; - var vm = new EmailsViewModel(data, fallbackSettings); - - return View(vm); + return View(new EmailsViewModel(data, fallbackSettings)); } [HttpPost("{storeId}/email-settings")] @@ -262,9 +256,4 @@ public partial class UIStoresController } return RedirectToAction(nameof(StoreEmailSettings), new { storeId }); } - - private static async Task IsSetupComplete(IEmailSender emailSender) - { - return emailSender is not null && (await emailSender.GetEmailSettings())?.IsComplete() == true; - } } diff --git a/BTCPayServer/Controllers/UIStoresController.Users.cs b/BTCPayServer/Controllers/UIStoresController.Users.cs index bcb1661fc..03fa7048c 100644 --- a/BTCPayServer/Controllers/UIStoresController.Users.cs +++ b/BTCPayServer/Controllers/UIStoresController.Users.cs @@ -61,7 +61,8 @@ public partial class UIStoresController var result = await _userManager.CreateAsync(user); if (result.Succeeded) { - var tcs = new TaskCompletionSource(); + var invitationEmail = await _emailSenderFactory.IsComplete(); + var tcs = new TaskCompletionSource(); var currentUser = await _userManager.GetUserAsync(HttpContext.User); _eventAggregator.Publish(new UserRegisteredEvent @@ -70,14 +71,13 @@ public partial class UIStoresController Kind = UserRegisteredEventKind.Invite, User = user, InvitedByUser = currentUser, - SendInvitationEmail = true, + SendInvitationEmail = invitationEmail, CallbackUrlGenerated = tcs }); var callbackUrl = await tcs.Task; - var settings = await _settingsRepository.GetSettingAsync() ?? new EmailSettings(); - var info = settings.IsComplete() - ? "An invitation email has been sent.
You may alternatively" + var info = invitationEmail + ? "An invitation email has been sent.
You may alternatively" : "An invitation email has not been sent, because the server does not have an email server configured.
You need to"; successInfo = $"{info} share this link with them: {callbackUrl}"; } diff --git a/BTCPayServer/Controllers/UIStoresController.cs b/BTCPayServer/Controllers/UIStoresController.cs index 3fe3810d6..228f0f48d 100644 --- a/BTCPayServer/Controllers/UIStoresController.cs +++ b/BTCPayServer/Controllers/UIStoresController.cs @@ -60,7 +60,6 @@ public partial class UIStoresController : Controller WalletFileParsers onChainWalletParsers, UIUserStoresController userStoresController, UriResolver uriResolver, - SettingsRepository settingsRepository, CurrencyNameTable currencyNameTable, IStringLocalizer stringLocalizer, EventAggregator eventAggregator, @@ -88,7 +87,6 @@ public partial class UIStoresController : Controller _onChainWalletParsers = onChainWalletParsers; _userStoresController = userStoresController; _uriResolver = uriResolver; - _settingsRepository = settingsRepository; _currencyNameTable = currencyNameTable; _eventAggregator = eventAggregator; _html = html; @@ -110,7 +108,6 @@ public partial class UIStoresController : Controller private readonly TokenRepository _tokenRepository; private readonly UserManager _userManager; private readonly RateFetcher _rateFactory; - private readonly SettingsRepository _settingsRepository; private readonly CurrencyNameTable _currencyNameTable; private readonly ExplorerClientProvider _explorerProvider; private readonly LanguageService _langService; diff --git a/BTCPayServer/Services/Mails/EmailSender.cs b/BTCPayServer/Services/Mails/EmailSender.cs index d5a791088..b46441748 100644 --- a/BTCPayServer/Services/Mails/EmailSender.cs +++ b/BTCPayServer/Services/Mails/EmailSender.cs @@ -1,3 +1,4 @@ +#nullable enable using System; using System.Threading.Tasks; using BTCPayServer.Logging; @@ -28,7 +29,7 @@ namespace BTCPayServer.Services.Mails _JobClient.Schedule(async cancellationToken => { 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"); return; @@ -42,7 +43,7 @@ namespace BTCPayServer.Services.Mails }, TimeSpan.Zero); } - public abstract Task GetEmailSettings(); + public abstract Task GetEmailSettings(); public abstract Task GetPrefixedSubject(string subject); } } diff --git a/BTCPayServer/Services/Mails/EmailSenderFactory.cs b/BTCPayServer/Services/Mails/EmailSenderFactory.cs index ed646f443..50b987298 100644 --- a/BTCPayServer/Services/Mails/EmailSenderFactory.cs +++ b/BTCPayServer/Services/Mails/EmailSenderFactory.cs @@ -1,3 +1,4 @@ +#nullable enable using System.Threading.Tasks; using BTCPayServer.HostedServices; using BTCPayServer.Logging; @@ -27,7 +28,7 @@ namespace BTCPayServer.Services.Mails _storeRepository = storeRepository; } - public Task GetEmailSender(string storeId = null) + public Task GetEmailSender(string? storeId = null) { var serverSender = new ServerEmailSender(_settingsRepository, _jobClient, Logs); if (string.IsNullOrEmpty(storeId)) @@ -36,5 +37,16 @@ namespace BTCPayServer.Services.Mails !PoliciesSettings.Settings.DisableStoresToUseServerEmailSettings ? serverSender : null, _jobClient, storeId, Logs)); } - } + + public async Task IsComplete(string? storeId = null) + { + var settings = await this.GetSettings(storeId); + return settings?.IsComplete() is true; + } + public async Task GetSettings(string? storeId = null) + { + var sender = await this.GetEmailSender(storeId); + return await sender.GetEmailSettings(); + } + } } diff --git a/BTCPayServer/Services/Mails/IEmailSender.cs b/BTCPayServer/Services/Mails/IEmailSender.cs index 218de362d..7f822d0b5 100644 --- a/BTCPayServer/Services/Mails/IEmailSender.cs +++ b/BTCPayServer/Services/Mails/IEmailSender.cs @@ -1,3 +1,4 @@ +#nullable enable using System.Threading.Tasks; using MimeKit; @@ -7,6 +8,6 @@ namespace BTCPayServer.Services.Mails { void SendEmail(MailboxAddress email, string subject, string message); void SendEmail(MailboxAddress[] email, MailboxAddress[] cc, MailboxAddress[] bcc, string subject, string message); - Task GetEmailSettings(); + Task GetEmailSettings(); } } diff --git a/BTCPayServer/Services/Mails/StoreEmailSender.cs b/BTCPayServer/Services/Mails/StoreEmailSender.cs index 209603667..428e6b612 100644 --- a/BTCPayServer/Services/Mails/StoreEmailSender.cs +++ b/BTCPayServer/Services/Mails/StoreEmailSender.cs @@ -1,3 +1,4 @@ +#nullable enable using System; using System.Threading.Tasks; using BTCPayServer.Data; @@ -9,7 +10,7 @@ namespace BTCPayServer.Services.Mails class StoreEmailSender : EmailSender { public StoreEmailSender(StoreRepository storeRepository, - EmailSender fallback, + EmailSender? fallback, IBackgroundJobClient backgroundJobClient, string storeId, Logs logs) : base(backgroundJobClient, logs) @@ -20,20 +21,22 @@ namespace BTCPayServer.Services.Mails } public StoreRepository StoreRepository { get; } - public EmailSender FallbackSender { get; } + public EmailSender? FallbackSender { get; } public string StoreId { get; } - public override async Task GetEmailSettings() + public override async Task GetEmailSettings() { var store = await StoreRepository.FindStore(StoreId); + if (store is null) + return null; var emailSettings = store.GetStoreBlob().EmailSettings; - if (emailSettings?.IsComplete() == true) + if (emailSettings?.IsComplete() is true) { return emailSettings; } - if (FallbackSender != null) - return await FallbackSender?.GetEmailSettings(); + if (FallbackSender is not null) + return await FallbackSender.GetEmailSettings(); return null; } diff --git a/BTCPayServer/Views/UIAccount/ForgotPassword.cshtml b/BTCPayServer/Views/UIAccount/ForgotPassword.cshtml index d1e218df8..dba8c1617 100644 --- a/BTCPayServer/Views/UIAccount/ForgotPassword.cshtml +++ b/BTCPayServer/Views/UIAccount/ForgotPassword.cshtml @@ -1,11 +1,11 @@ -@using BTCPayServer.Services +@using BTCPayServer.Services @using BTCPayServer.Services.Mails @using Microsoft.AspNetCore.Mvc.TagHelpers @model ForgotPasswordViewModel -@inject SettingsRepository SettingsRepository +@inject EmailSenderFactory EmailSenderFactory @{ - var isEmailConfigured = (await SettingsRepository.GetSettingAsync())?.IsComplete() is true; - ViewData["Title"] = isEmailConfigured ? "Forgot your password?" : "Email Server Configuration Required"; + var isEmailConfigured = await EmailSenderFactory.IsComplete(); + ViewData["Title"] = isEmailConfigured ? "Forgot your password?" : "Email Server Configuration Required"; Layout = "_LayoutSignedOut"; } diff --git a/BTCPayServer/Views/UIServer/Policies.cshtml b/BTCPayServer/Views/UIServer/Policies.cshtml index aa0317e75..15082446c 100644 --- a/BTCPayServer/Views/UIServer/Policies.cshtml +++ b/BTCPayServer/Views/UIServer/Policies.cshtml @@ -1,7 +1,7 @@ @using BTCPayServer.Services @using BTCPayServer.Services.Mails; @model BTCPayServer.Services.PoliciesSettings -@inject SettingsRepository _SettingsRepository +@inject EmailSenderFactory EmailSenderFactory @inject TransactionLinkProviders TransactionLinkProviders @{ ViewData.SetActivePage(ServerNavPages.Policies); @@ -47,10 +47,9 @@
@{ - var emailSettings = (await _SettingsRepository.GetSettingAsync()) ?? new EmailSettings(); /* 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. */ - var isEmailConfigured = emailSettings.IsComplete() || Model.RequiresConfirmedEmail; + var isEmailConfigured = await EmailSenderFactory.IsComplete() || Model.RequiresConfirmedEmail; }