Emails on store level

This commit is contained in:
Kukks
2019-01-06 15:53:37 +01:00
committed by nicolas.dorier
parent 00aa2e4e17
commit cfb4b080d3
14 changed files with 303 additions and 53 deletions

View File

@@ -28,7 +28,7 @@ namespace BTCPayServer.Controllers
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly SignInManager<ApplicationUser> _signInManager;
private readonly IEmailSender _emailSender;
private readonly EmailSenderFactory _EmailSenderFactory;
StoreRepository storeRepository;
RoleManager<IdentityRole> _RoleManager;
SettingsRepository _SettingsRepository;
@@ -40,14 +40,14 @@ namespace BTCPayServer.Controllers
RoleManager<IdentityRole> roleManager,
StoreRepository storeRepository,
SignInManager<ApplicationUser> 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: <a href='{callbackUrl}'>link</a>");
_EmailSenderFactory.GetEmailSender().SendEmail(model.Email, "Reset Password",
$"Please reset your password by clicking here: <a href='{callbackUrl}'>link</a>");
return RedirectToAction(nameof(ForgotPasswordConfirmation));
}

View File

@@ -31,7 +31,7 @@ namespace BTCPayServer.Controllers
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly SignInManager<ApplicationUser> _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<ApplicationUser> userManager,
SignInManager<ApplicationUser> signInManager,
IEmailSender emailSender,
EmailSenderFactory emailSenderFactory,
ILogger<ManageController> 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));
}

View File

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

View File

@@ -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<string, string> WalletKeyPathRoots { get; set; } = new Dictionary<string, string>();
public EmailSettings EmailSettings { get; set; }
public IPaymentFilter GetExcludedPaymentMethods()
{
#pragma warning disable CS0618 // Type or member is obsolete

View File

@@ -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: <a href='{HtmlEncoder.Default.Encode(link)}'>link</a>");
}
}

View File

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

View File

@@ -165,7 +165,7 @@ namespace BTCPayServer.Hosting
services.AddTransient<InvoiceController>();
services.AddTransient<AppsPublicController>();
// Add application services.
services.AddTransient<IEmailSender, EmailSender>();
services.AddSingleton<EmailSenderFactory>();
// bundling
services.AddAuthorization(o => Policies.AddBTCPayPolicies(o));

View File

@@ -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<EmailSettings>() ?? 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<EmailSettings>() ?? 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<EmailSettings> GetEmailSettings();
}
}

View File

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

View File

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

View File

@@ -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<EmailSettings> GetEmailSettings()
{
return SettingsRepository.GetSettingAsync<EmailSettings>();
}
}
}

View File

@@ -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<EmailSettings> GetEmailSettings()
{
var store = await StoreRepository.FindStore(StoreId);
var emailSettings = store.GetStoreBlob().EmailSettings;
if (emailSettings?.IsComplete() == true)
{
return emailSettings;
}
return await FallbackSender.GetEmailSettings();
}
}
}

View File

@@ -0,0 +1,68 @@
@model BTCPayServer.Models.ServerViewModels.EmailsViewModel
@{
Layout = "../Shared/_NavLayout.cshtml";
ViewData.SetActivePageAndTitle(StoreNavPages.Index, "Update Store Email Settings");
}
<h4>@ViewData["Title"]</h4>
<partial name="_StatusMessage" for="StatusMessage" />
<div class="row">
<div class="col-md-6">
<div asp-validation-summary="All" class="text-danger"></div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<form method="post">
<div class="form-group">
<label asp-for="Settings.Server"></label>
<input asp-for="Settings.Server" class="form-control" />
<span asp-validation-for="Settings.Server" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Settings.Port"></label>
<input asp-for="Settings.Port" class="form-control" />
<span asp-validation-for="Settings.Port" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Settings.From"></label>
<input asp-for="Settings.From" class="form-control" />
<span asp-validation-for="Settings.From" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Settings.Login"></label>
<input asp-for="Settings.Login" class="form-control" />
<span asp-validation-for="Settings.Login" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Settings.Password"></label>
<input asp-for="Settings.Password" type="password" class="form-control" />
<span asp-validation-for="Settings.Password" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Settings.EnableSSL"></label>
<input asp-for="Settings.EnableSSL" type="checkbox" class="form-check-inline" />
</div>
<div class="form-group">
<label asp-for="TestEmail"></label>
<input asp-for="TestEmail" class="form-control" />
<span asp-validation-for="TestEmail" class="text-danger"></span>
</div>
<button type="submit" class="btn btn-primary" name="command" value="Save">Save</button>
<button type="submit" class="btn btn-primary" name="command" value="Test">Test</button>
</form>
</div>
</div>
@section Scripts {
@await Html.PartialAsync("_ValidationScriptsPartial")
}

View File

@@ -214,7 +214,29 @@
</table>
</div>
</div>
<div class="form-group">
<div class="form-group">
<h5>Services</h5>
</div>
<div class="form-group">
<table class="table table-sm table-responsive-md">
<thead>
<tr>
<th>Service</th>
<th style="text-align:right">Actions</th>
</tr>
</thead>
<tbody>
<tr>
<td>
Email
</td>
<td style="text-align:right"><a asp-action="Emails" >Modify</a></td>
</tr>
</tbody>
</table>
</div>
</div>
@if(Model.CanDelete)
{
<div class="form-group">