diff --git a/BTCPayServer/Controllers/AccountController.cs b/BTCPayServer/Controllers/AccountController.cs index 39e083e35..94cc11c42 100644 --- a/BTCPayServer/Controllers/AccountController.cs +++ b/BTCPayServer/Controllers/AccountController.cs @@ -28,7 +28,7 @@ namespace BTCPayServer.Controllers { private readonly UserManager _userManager; private readonly SignInManager _signInManager; - private readonly IEmailSender _emailSender; + private readonly EmailSenderFactory _EmailSenderFactory; StoreRepository storeRepository; RoleManager _RoleManager; SettingsRepository _SettingsRepository; @@ -40,14 +40,14 @@ namespace BTCPayServer.Controllers RoleManager roleManager, StoreRepository storeRepository, SignInManager signInManager, - IEmailSender emailSender, + EmailSenderFactory emailSenderFactory, SettingsRepository settingsRepository, Configuration.BTCPayServerOptions options) { this.storeRepository = storeRepository; _userManager = userManager; _signInManager = signInManager; - _emailSender = emailSender; + _EmailSenderFactory = emailSenderFactory; _RoleManager = roleManager; _SettingsRepository = settingsRepository; _Options = options; @@ -286,7 +286,8 @@ namespace BTCPayServer.Controllers var code = await _userManager.GenerateEmailConfirmationTokenAsync(user); var callbackUrl = Url.EmailConfirmationLink(user.Id, code, Request.Scheme); RegisteredUserId = user.Id; - await _emailSender.SendEmailConfirmationAsync(model.Email, callbackUrl); + + _EmailSenderFactory.GetEmailSender().SendEmailConfirmation(model.Email, callbackUrl); if (!policies.RequiresConfirmedEmail) { if(logon) @@ -446,8 +447,9 @@ namespace BTCPayServer.Controllers // visit https://go.microsoft.com/fwlink/?LinkID=532713 var code = await _userManager.GeneratePasswordResetTokenAsync(user); var callbackUrl = Url.ResetPasswordCallbackLink(user.Id, code, Request.Scheme); - await _emailSender.SendEmailAsync(model.Email, "Reset Password", - $"Please reset your password by clicking here: link"); + _EmailSenderFactory.GetEmailSender().SendEmail(model.Email, "Reset Password", + $"Please reset your password by clicking here: link"); + return RedirectToAction(nameof(ForgotPasswordConfirmation)); } diff --git a/BTCPayServer/Controllers/ManageController.cs b/BTCPayServer/Controllers/ManageController.cs index bf36f61e1..65341743f 100644 --- a/BTCPayServer/Controllers/ManageController.cs +++ b/BTCPayServer/Controllers/ManageController.cs @@ -31,7 +31,7 @@ namespace BTCPayServer.Controllers { private readonly UserManager _userManager; private readonly SignInManager _signInManager; - private readonly IEmailSender _emailSender; + private readonly EmailSenderFactory _EmailSenderFactory; private readonly ILogger _logger; private readonly UrlEncoder _urlEncoder; TokenRepository _TokenRepository; @@ -44,7 +44,7 @@ namespace BTCPayServer.Controllers public ManageController( UserManager userManager, SignInManager signInManager, - IEmailSender emailSender, + EmailSenderFactory emailSenderFactory, ILogger logger, UrlEncoder urlEncoder, TokenRepository tokenRepository, @@ -54,7 +54,7 @@ namespace BTCPayServer.Controllers { _userManager = userManager; _signInManager = signInManager; - _emailSender = emailSender; + _EmailSenderFactory = emailSenderFactory; _logger = logger; _urlEncoder = urlEncoder; _TokenRepository = tokenRepository; @@ -156,8 +156,7 @@ namespace BTCPayServer.Controllers var code = await _userManager.GenerateEmailConfirmationTokenAsync(user); var callbackUrl = Url.EmailConfirmationLink(user.Id, code, Request.Scheme); var email = user.Email; - await _emailSender.SendEmailConfirmationAsync(email, callbackUrl); - + _EmailSenderFactory.GetEmailSender().SendEmailConfirmation(email, callbackUrl); StatusMessage = "Verification email sent. Please check your email."; return RedirectToAction(nameof(Index)); } diff --git a/BTCPayServer/Controllers/StoresController.Email.cs b/BTCPayServer/Controllers/StoresController.Email.cs new file mode 100644 index 000000000..0ced7427b --- /dev/null +++ b/BTCPayServer/Controllers/StoresController.Email.cs @@ -0,0 +1,65 @@ +using System; +using System.Threading.Tasks; +using BTCPayServer.Data; +using BTCPayServer.Models.ServerViewModels; +using BTCPayServer.Models.StoreViewModels; +using BTCPayServer.Payments.Changelly; +using BTCPayServer.Services.Mails; +using Microsoft.AspNetCore.Mvc; + +namespace BTCPayServer.Controllers +{ + public partial class StoresController + { + + [Route("{storeId}/emails")] + public IActionResult Emails() + { + var store = HttpContext.GetStoreData(); + if (store == null) + return NotFound(); + var data = store.GetStoreBlob().EmailSettings ?? new EmailSettings(); + return View(new EmailsViewModel() { Settings = data }); + } + + [Route("{storeId}/emails")] + [HttpPost] + public async Task Emails(string storeId, EmailsViewModel model, string command) + { + var store = HttpContext.GetStoreData(); + if (store == null) + return NotFound(); + if (command == "Test") + { + try + { + if (!model.Settings.IsComplete()) + { + model.StatusMessage = "Error: Required fields missing"; + return View(model); + } + var client = model.Settings.CreateSmtpClient(); + await client.SendMailAsync(model.Settings.From, model.TestEmail, "BTCPay test", "BTCPay test"); + model.StatusMessage = "Email sent to " + model.TestEmail + ", please, verify you received it"; + } + catch (Exception ex) + { + model.StatusMessage = "Error: " + ex.Message; + } + return View(model); + } + else // if(command == "Save") + { + + var storeBlob = store.GetStoreBlob(); + storeBlob.EmailSettings = model.Settings; + store.SetStoreBlob(storeBlob); + await _Repo.UpdateStore(store); + StatusMessage = "Email settings modified"; + return RedirectToAction(nameof(UpdateStore), new { + storeId}); + + } + } + } +} diff --git a/BTCPayServer/Data/StoreData.cs b/BTCPayServer/Data/StoreData.cs index 953f0ecf6..18f8c2fd2 100644 --- a/BTCPayServer/Data/StoreData.cs +++ b/BTCPayServer/Data/StoreData.cs @@ -21,6 +21,7 @@ using BTCPayServer.Payments.Changelly; using BTCPayServer.Payments.CoinSwitch; using BTCPayServer.Security; using BTCPayServer.Rating; +using BTCPayServer.Services.Mails; namespace BTCPayServer.Data { @@ -403,6 +404,8 @@ namespace BTCPayServer.Data [Obsolete("Use SetWalletKeyPathRoot/GetWalletKeyPathRoot instead")] public Dictionary WalletKeyPathRoots { get; set; } = new Dictionary(); + public EmailSettings EmailSettings { get; set; } + public IPaymentFilter GetExcludedPaymentMethods() { #pragma warning disable CS0618 // Type or member is obsolete diff --git a/BTCPayServer/Extensions/EmailSenderExtensions.cs b/BTCPayServer/Extensions/EmailSenderExtensions.cs index ac07f0505..b1010b57b 100644 --- a/BTCPayServer/Extensions/EmailSenderExtensions.cs +++ b/BTCPayServer/Extensions/EmailSenderExtensions.cs @@ -10,9 +10,9 @@ namespace BTCPayServer.Services { public static class EmailSenderExtensions { - public static Task SendEmailConfirmationAsync(this IEmailSender emailSender, string email, string link) + public static void SendEmailConfirmation(this IEmailSender emailSender, string email, string link) { - return emailSender.SendEmailAsync(email, "Confirm your email", + emailSender.SendEmail(email, "Confirm your email", $"Please confirm your account by clicking this link: link"); } } diff --git a/BTCPayServer/HostedServices/InvoiceNotificationManager.cs b/BTCPayServer/HostedServices/InvoiceNotificationManager.cs index 99acf3e11..daad3e11f 100644 --- a/BTCPayServer/HostedServices/InvoiceNotificationManager.cs +++ b/BTCPayServer/HostedServices/InvoiceNotificationManager.cs @@ -46,20 +46,21 @@ namespace BTCPayServer.HostedServices EventAggregator _EventAggregator; InvoiceRepository _InvoiceRepository; BTCPayNetworkProvider _NetworkProvider; - IEmailSender _EmailSender; + private readonly EmailSenderFactory _EmailSenderFactory; public InvoiceNotificationManager( IBackgroundJobClient jobClient, EventAggregator eventAggregator, InvoiceRepository invoiceRepository, BTCPayNetworkProvider networkProvider, - IEmailSender emailSender) + ILogger logger, + EmailSenderFactory emailSenderFactory) { _JobClient = jobClient; _EventAggregator = eventAggregator; _InvoiceRepository = invoiceRepository; _NetworkProvider = networkProvider; - _EmailSender = emailSender; + _EmailSenderFactory = emailSenderFactory; } void Notify(InvoiceEntity invoice, int? eventCode = null, string name = null) @@ -76,11 +77,14 @@ namespace BTCPayServer.HostedServices invoice.StoreId }; // TODO: Consider adding info on ItemDesc and payment info (amount) - + var emailBody = NBitcoin.JsonConverters.Serializer.ToString(ipn); -#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed - _EmailSender.SendEmailAsync(invoice.NotificationEmail, $"BtcPayServer Invoice Notification - ${invoice.StoreId}", emailBody); -#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed + + _EmailSenderFactory.GetEmailSender(invoice.StoreId).SendEmail( + invoice.NotificationEmail, + $"BtcPayServer Invoice Notification - ${invoice.StoreId}", + emailBody); + } if (string.IsNullOrEmpty(invoice.NotificationURL)) return; diff --git a/BTCPayServer/Hosting/BTCPayServerServices.cs b/BTCPayServer/Hosting/BTCPayServerServices.cs index 8bd783114..403b18ef2 100644 --- a/BTCPayServer/Hosting/BTCPayServerServices.cs +++ b/BTCPayServer/Hosting/BTCPayServerServices.cs @@ -165,7 +165,7 @@ namespace BTCPayServer.Hosting services.AddTransient(); services.AddTransient(); // Add application services. - services.AddTransient(); + services.AddSingleton(); // bundling services.AddAuthorization(o => Policies.AddBTCPayPolicies(o)); diff --git a/BTCPayServer/Services/Mails/EmailSender.cs b/BTCPayServer/Services/Mails/EmailSender.cs index fd47cb1b3..801f525c8 100644 --- a/BTCPayServer/Services/Mails/EmailSender.cs +++ b/BTCPayServer/Services/Mails/EmailSender.cs @@ -1,47 +1,40 @@ using BTCPayServer.Logging; using Microsoft.Extensions.Logging; using System; -using System.Collections.Generic; -using System.Linq; using System.Net.Mail; using System.Threading.Tasks; namespace BTCPayServer.Services.Mails { - // This class is used by the application to send email for account confirmation and password reset. - // For more details see https://go.microsoft.com/fwlink/?LinkID=532713 - public class EmailSender : IEmailSender + public abstract class EmailSender : IEmailSender { IBackgroundJobClient _JobClient; - SettingsRepository _Repository; - public EmailSender(IBackgroundJobClient jobClient, SettingsRepository repository) + + public EmailSender(IBackgroundJobClient jobClient) { - if (jobClient == null) - throw new ArgumentNullException(nameof(jobClient)); - _JobClient = jobClient; - _Repository = repository; - } - public async Task SendEmailAsync(string email, string subject, string message) - { - var settings = await _Repository.GetSettingAsync() ?? new EmailSettings(); - if (!settings.IsComplete()) - { - Logs.Configuration.LogWarning("Should have sent email, but email settings are not configured"); - return; - } - _JobClient.Schedule(() => SendMailCore(email, subject, message), TimeSpan.Zero); - return; + _JobClient = jobClient ?? throw new ArgumentNullException(nameof(jobClient)); } - public async Task SendMailCore(string email, string subject, string message) + public void SendEmail(string email, string subject, string message) { - var settings = await _Repository.GetSettingAsync() ?? new EmailSettings(); - if (!settings.IsComplete()) - throw new InvalidOperationException("Email settings not configured"); - var smtp = settings.CreateSmtpClient(); - MailMessage mail = new MailMessage(settings.From, email, subject, message); - mail.IsBodyHtml = true; - await smtp.SendMailAsync(mail); + _JobClient.Schedule(async () => + { + var emailSettings = await GetEmailSettings(); + if (emailSettings?.IsComplete() != true) + { + Logs.Configuration.LogWarning("Should have sent email, but email settings are not configured"); + return; + } + var smtp = emailSettings.CreateSmtpClient(); + var mail = new MailMessage(emailSettings.From, email, subject, message) + { + IsBodyHtml = true + }; + await smtp.SendMailAsync(mail); + + }, TimeSpan.Zero); } + + public abstract Task GetEmailSettings(); } } diff --git a/BTCPayServer/Services/Mails/EmailSenderFactory.cs b/BTCPayServer/Services/Mails/EmailSenderFactory.cs new file mode 100644 index 000000000..12c288350 --- /dev/null +++ b/BTCPayServer/Services/Mails/EmailSenderFactory.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Concurrent; +using System.Threading.Tasks; +using BTCPayServer.Services.Stores; + +namespace BTCPayServer.Services.Mails +{ + public class EmailSenderFactory + { + private readonly IBackgroundJobClient _JobClient; + private readonly SettingsRepository _Repository; + private readonly StoreRepository _StoreRepository; + + public EmailSenderFactory(IBackgroundJobClient jobClient, + SettingsRepository repository, + StoreRepository storeRepository) + { + _JobClient = jobClient; + _Repository = repository; + _StoreRepository = storeRepository; + } + + public IEmailSender GetEmailSender(string storeId = null) + { + var serverSender = new ServerEmailSender(_Repository, _JobClient); + if (string.IsNullOrEmpty(storeId)) + return serverSender; + return new StoreEmailSender(_StoreRepository, serverSender, _JobClient, storeId); + } + } +} diff --git a/BTCPayServer/Services/Mails/IEmailSender.cs b/BTCPayServer/Services/Mails/IEmailSender.cs index a48025573..625886566 100644 --- a/BTCPayServer/Services/Mails/IEmailSender.cs +++ b/BTCPayServer/Services/Mails/IEmailSender.cs @@ -7,6 +7,6 @@ namespace BTCPayServer.Services.Mails { public interface IEmailSender { - Task SendEmailAsync(string email, string subject, string message); + void SendEmail(string email, string subject, string message); } } diff --git a/BTCPayServer/Services/Mails/ServerEmailSender.cs b/BTCPayServer/Services/Mails/ServerEmailSender.cs new file mode 100644 index 000000000..66d2cc0c0 --- /dev/null +++ b/BTCPayServer/Services/Mails/ServerEmailSender.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace BTCPayServer.Services.Mails +{ + class ServerEmailSender : EmailSender + { + public ServerEmailSender(SettingsRepository settingsRepository, + IBackgroundJobClient backgroundJobClient) : base(backgroundJobClient) + { + if (settingsRepository == null) + throw new ArgumentNullException(nameof(settingsRepository)); + SettingsRepository = settingsRepository; + } + + public SettingsRepository SettingsRepository { get; } + + public override Task GetEmailSettings() + { + return SettingsRepository.GetSettingAsync(); + } + } +} diff --git a/BTCPayServer/Services/Mails/StoreEmailSender.cs b/BTCPayServer/Services/Mails/StoreEmailSender.cs new file mode 100644 index 000000000..8d9751dd5 --- /dev/null +++ b/BTCPayServer/Services/Mails/StoreEmailSender.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using BTCPayServer.Services.Stores; + +namespace BTCPayServer.Services.Mails +{ + class StoreEmailSender : EmailSender + { + public StoreEmailSender(StoreRepository storeRepository, + EmailSender fallback, + IBackgroundJobClient backgroundJobClient, + string storeId) : base(backgroundJobClient) + { + if (storeId == null) + throw new ArgumentNullException(nameof(storeId)); + StoreRepository = storeRepository; + FallbackSender = fallback; + StoreId = storeId; + } + + public StoreRepository StoreRepository { get; } + public EmailSender FallbackSender { get; } + public string StoreId { get; } + + public override async Task GetEmailSettings() + { + var store = await StoreRepository.FindStore(StoreId); + var emailSettings = store.GetStoreBlob().EmailSettings; + if (emailSettings?.IsComplete() == true) + { + return emailSettings; + } + return await FallbackSender.GetEmailSettings(); + } + } +} diff --git a/BTCPayServer/Views/Stores/Emails.cshtml b/BTCPayServer/Views/Stores/Emails.cshtml new file mode 100644 index 000000000..4f16071eb --- /dev/null +++ b/BTCPayServer/Views/Stores/Emails.cshtml @@ -0,0 +1,68 @@ +@model BTCPayServer.Models.ServerViewModels.EmailsViewModel +@{ + Layout = "../Shared/_NavLayout.cshtml"; + ViewData.SetActivePageAndTitle(StoreNavPages.Index, "Update Store Email Settings"); +} + + +

@ViewData["Title"]

+ + + +
+
+
+
+
+
+
+
+
+ + + +
+
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + +
+ +
+ + + +
+ + + +
+
+
+ +@section Scripts { + @await Html.PartialAsync("_ValidationScriptsPartial") +} diff --git a/BTCPayServer/Views/Stores/UpdateStore.cshtml b/BTCPayServer/Views/Stores/UpdateStore.cshtml index ea8fa5cd7..85e38da8c 100644 --- a/BTCPayServer/Views/Stores/UpdateStore.cshtml +++ b/BTCPayServer/Views/Stores/UpdateStore.cshtml @@ -214,7 +214,29 @@ - +
+
+
Services
+
+
+ + + + + + + + + + + + + +
ServiceActions
+ Email + Modify
+
+
@if(Model.CanDelete) {