mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-02-22 14:22:40 +01:00
Improve email settings validation and UX (#3891)
This commit is contained in:
parent
c2d72e71aa
commit
c89f7aaaed
26 changed files with 161 additions and 93 deletions
|
@ -1,4 +1,4 @@
|
||||||
namespace BTCPayServer.Client.Models;
|
namespace BTCPayServer.Client.Models;
|
||||||
|
|
||||||
public class EmailSettingsData
|
public class EmailSettingsData
|
||||||
{
|
{
|
||||||
|
@ -21,11 +21,6 @@ public class EmailSettingsData
|
||||||
{
|
{
|
||||||
get; set;
|
get; set;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string FromDisplay
|
|
||||||
{
|
|
||||||
get; set;
|
|
||||||
}
|
|
||||||
public string From
|
public string From
|
||||||
{
|
{
|
||||||
get; set;
|
get; set;
|
||||||
|
|
|
@ -1940,6 +1940,7 @@ retry:
|
||||||
s.Driver.FindElement(By.CssSelector("button[value=\"Save\"]")).Submit();
|
s.Driver.FindElement(By.CssSelector("button[value=\"Save\"]")).Submit();
|
||||||
s.FindAlertMessage();
|
s.FindAlertMessage();
|
||||||
s.Driver.FindElement(By.Id("Settings_Password")).SendKeys("mypassword");
|
s.Driver.FindElement(By.Id("Settings_Password")).SendKeys("mypassword");
|
||||||
|
s.Driver.FindElement(By.Id("Settings_From")).SendKeys("Firstname Lastname <email@example.com>");
|
||||||
s.Driver.FindElement(By.Id("Save")).SendKeys(Keys.Enter);
|
s.Driver.FindElement(By.Id("Save")).SendKeys(Keys.Enter);
|
||||||
Assert.Contains("Configured", s.Driver.PageSource);
|
Assert.Contains("Configured", s.Driver.PageSource);
|
||||||
s.Driver.FindElement(By.Id("Settings_Login")).SendKeys("test_fix@gmail.com");
|
s.Driver.FindElement(By.Id("Settings_Login")).SendKeys("test_fix@gmail.com");
|
||||||
|
|
|
@ -39,7 +39,7 @@ namespace BTCPayServer.Controllers.GreenField
|
||||||
{
|
{
|
||||||
return this.CreateAPIError(404, "store-not-found", "The store was not found");
|
return this.CreateAPIError(404, "store-not-found", "The store was not found");
|
||||||
}
|
}
|
||||||
if (!MailboxAddress.TryParse(request.Email, out MailboxAddress to))
|
if (!MailboxAddressValidator.TryParse(request.Email, out var to))
|
||||||
{
|
{
|
||||||
ModelState.AddModelError(nameof(request.Email), "Invalid email");
|
ModelState.AddModelError(nameof(request.Email), "Invalid email");
|
||||||
return this.CreateValidationError(ModelState);
|
return this.CreateValidationError(ModelState);
|
||||||
|
@ -72,7 +72,7 @@ namespace BTCPayServer.Controllers.GreenField
|
||||||
return StoreNotFound();
|
return StoreNotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(request.From) && !EmailValidator.IsEmail(request.From))
|
if (!string.IsNullOrEmpty(request.From) && !MailboxAddressValidator.IsMailboxAddress(request.From))
|
||||||
{
|
{
|
||||||
request.AddModelError(e => e.From,
|
request.AddModelError(e => e.From,
|
||||||
"Invalid email address", this);
|
"Invalid email address", this);
|
||||||
|
|
|
@ -118,7 +118,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||||
{
|
{
|
||||||
if (request.Email is null)
|
if (request.Email is null)
|
||||||
ModelState.AddModelError(nameof(request.Email), "Email is missing");
|
ModelState.AddModelError(nameof(request.Email), "Email is missing");
|
||||||
if (!string.IsNullOrEmpty(request.Email) && !Validation.EmailValidator.IsEmail(request.Email))
|
if (!string.IsNullOrEmpty(request.Email) && !MailboxAddressValidator.IsMailboxAddress(request.Email))
|
||||||
{
|
{
|
||||||
ModelState.AddModelError(nameof(request.Email), "Invalid email");
|
ModelState.AddModelError(nameof(request.Email), "Invalid email");
|
||||||
}
|
}
|
||||||
|
|
|
@ -233,7 +233,7 @@ namespace BTCPayServer.Controllers
|
||||||
|
|
||||||
if (entity.Metadata.BuyerEmail != null)
|
if (entity.Metadata.BuyerEmail != null)
|
||||||
{
|
{
|
||||||
if (!EmailValidator.IsEmail(entity.Metadata.BuyerEmail))
|
if (!MailboxAddressValidator.IsMailboxAddress(entity.Metadata.BuyerEmail))
|
||||||
throw new BitpayHttpException(400, "Invalid email");
|
throw new BitpayHttpException(400, "Invalid email");
|
||||||
entity.RefundMail = entity.Metadata.BuyerEmail;
|
entity.RefundMail = entity.Metadata.BuyerEmail;
|
||||||
}
|
}
|
||||||
|
|
|
@ -165,8 +165,7 @@ namespace BTCPayServer.Controllers
|
||||||
|
|
||||||
var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
|
var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
|
||||||
var callbackUrl = _linkGenerator.EmailConfirmationLink(user.Id, code, Request.Scheme, Request.Host, Request.PathBase);
|
var callbackUrl = _linkGenerator.EmailConfirmationLink(user.Id, code, Request.Scheme, Request.Host, Request.PathBase);
|
||||||
var address = new MailboxAddress(user.UserName, user.Email);
|
(await _EmailSenderFactory.GetEmailSender()).SendEmailConfirmation(user.GetMailboxAddress(), callbackUrl);
|
||||||
(await _EmailSenderFactory.GetEmailSender()).SendEmailConfirmation(address, callbackUrl);
|
|
||||||
TempData[WellKnownTempData.SuccessMessage] = "Verification email sent. Please check your email.";
|
TempData[WellKnownTempData.SuccessMessage] = "Verification email sent. Please check your email.";
|
||||||
return RedirectToAction(nameof(Index));
|
return RedirectToAction(nameof(Index));
|
||||||
}
|
}
|
||||||
|
|
|
@ -303,8 +303,7 @@ namespace BTCPayServer.Controllers
|
||||||
var code = await _UserManager.GenerateEmailConfirmationTokenAsync(user);
|
var code = await _UserManager.GenerateEmailConfirmationTokenAsync(user);
|
||||||
var callbackUrl = _linkGenerator.EmailConfirmationLink(user.Id, code, Request.Scheme, Request.Host, Request.PathBase);
|
var callbackUrl = _linkGenerator.EmailConfirmationLink(user.Id, code, Request.Scheme, Request.Host, Request.PathBase);
|
||||||
|
|
||||||
var address = new MailboxAddress(user.UserName, user.Email);
|
(await _emailSenderFactory.GetEmailSender()).SendEmailConfirmation(user.GetMailboxAddress(), callbackUrl);
|
||||||
(await _emailSenderFactory.GetEmailSender()).SendEmailConfirmation(address, callbackUrl);
|
|
||||||
|
|
||||||
TempData[WellKnownTempData.SuccessMessage] = "Verification email sent";
|
TempData[WellKnownTempData.SuccessMessage] = "Verification email sent";
|
||||||
return RedirectToAction(nameof(ListUsers));
|
return RedirectToAction(nameof(ListUsers));
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
#nullable enable
|
#nullable enable
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
@ -25,6 +26,7 @@ using BTCPayServer.Services.Stores;
|
||||||
using BTCPayServer.Storage.Models;
|
using BTCPayServer.Storage.Models;
|
||||||
using BTCPayServer.Storage.Services;
|
using BTCPayServer.Storage.Services;
|
||||||
using BTCPayServer.Storage.Services.Providers;
|
using BTCPayServer.Storage.Services.Providers;
|
||||||
|
using BTCPayServer.Validation;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
@ -1015,18 +1017,13 @@ namespace BTCPayServer.Controllers
|
||||||
var settings = await _SettingsRepository.GetSettingAsync<EmailSettings>() ?? new EmailSettings();
|
var settings = await _SettingsRepository.GetSettingAsync<EmailSettings>() ?? new EmailSettings();
|
||||||
model.Settings.Password = settings.Password;
|
model.Settings.Password = settings.Password;
|
||||||
}
|
}
|
||||||
if (!model.Settings.IsComplete())
|
model.Settings.Validate("Settings.", ModelState);
|
||||||
{
|
if (string.IsNullOrEmpty(model.TestEmail))
|
||||||
TempData[WellKnownTempData.ErrorMessage] = "Required fields missing";
|
ModelState.AddModelError(nameof(model.TestEmail), new RequiredAttribute().FormatErrorMessage(nameof(model.TestEmail)));
|
||||||
|
if (!ModelState.IsValid)
|
||||||
return View(model);
|
return View(model);
|
||||||
}
|
|
||||||
if (!MailboxAddress.TryParse(model.TestEmail, out MailboxAddress testEmail))
|
|
||||||
{
|
|
||||||
TempData[WellKnownTempData.ErrorMessage] = "Invalid test email";
|
|
||||||
return View(model);
|
|
||||||
}
|
|
||||||
using (var client = await model.Settings.CreateSmtpClient())
|
using (var client = await model.Settings.CreateSmtpClient())
|
||||||
using (var message = model.Settings.CreateMailMessage(testEmail, "BTCPay test", "BTCPay test", false))
|
using (var message = model.Settings.CreateMailMessage(MailboxAddress.Parse(model.TestEmail), "BTCPay test", "BTCPay test", false))
|
||||||
{
|
{
|
||||||
await client.SendAsync(message);
|
await client.SendAsync(message);
|
||||||
await client.DisconnectAsync(true);
|
await client.DisconnectAsync(true);
|
||||||
|
@ -1043,12 +1040,17 @@ namespace BTCPayServer.Controllers
|
||||||
{
|
{
|
||||||
var settings = await _SettingsRepository.GetSettingAsync<EmailSettings>() ?? new EmailSettings();
|
var settings = await _SettingsRepository.GetSettingAsync<EmailSettings>() ?? new EmailSettings();
|
||||||
settings.Password = null;
|
settings.Password = null;
|
||||||
await _SettingsRepository.UpdateSetting(model.Settings);
|
await _SettingsRepository.UpdateSetting(settings);
|
||||||
TempData[WellKnownTempData.SuccessMessage] = "Email server password reset";
|
TempData[WellKnownTempData.SuccessMessage] = "Email server password reset";
|
||||||
return RedirectToAction(nameof(Emails));
|
return RedirectToAction(nameof(Emails));
|
||||||
}
|
}
|
||||||
else // if (command == "Save")
|
else // if (command == "Save")
|
||||||
{
|
{
|
||||||
|
if (model.Settings.From is not null && !MailboxAddressValidator.IsMailboxAddress(model.Settings.From))
|
||||||
|
{
|
||||||
|
ModelState.AddModelError("Settings.From", "Invalid email");
|
||||||
|
return View(model);
|
||||||
|
}
|
||||||
var oldSettings = await _SettingsRepository.GetSettingAsync<EmailSettings>() ?? new EmailSettings();
|
var oldSettings = await _SettingsRepository.GetSettingAsync<EmailSettings>() ?? new EmailSettings();
|
||||||
if (new EmailsViewModel(oldSettings).PasswordSet)
|
if (new EmailsViewModel(oldSettings).PasswordSet)
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Globalization;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using BTCPayServer.Abstractions.Constants;
|
using BTCPayServer.Abstractions.Constants;
|
||||||
using BTCPayServer.Abstractions.Extensions;
|
using BTCPayServer.Abstractions.Extensions;
|
||||||
|
@ -45,7 +46,7 @@ namespace BTCPayServer.Controllers
|
||||||
if (command.StartsWith("remove", StringComparison.InvariantCultureIgnoreCase))
|
if (command.StartsWith("remove", StringComparison.InvariantCultureIgnoreCase))
|
||||||
{
|
{
|
||||||
var item = command[(command.IndexOf(":", StringComparison.InvariantCultureIgnoreCase) + 1)..];
|
var item = command[(command.IndexOf(":", StringComparison.InvariantCultureIgnoreCase) + 1)..];
|
||||||
var index = int.Parse(item);
|
var index = int.Parse(item, CultureInfo.InvariantCulture);
|
||||||
vm.Rules.RemoveAt(index);
|
vm.Rules.RemoveAt(index);
|
||||||
|
|
||||||
return View(vm);
|
return View(vm);
|
||||||
|
@ -117,18 +118,13 @@ namespace BTCPayServer.Controllers
|
||||||
{
|
{
|
||||||
model.Settings.Password = store.GetStoreBlob().EmailSettings.Password;
|
model.Settings.Password = store.GetStoreBlob().EmailSettings.Password;
|
||||||
}
|
}
|
||||||
if (!model.Settings.IsComplete())
|
model.Settings.Validate("Settings.", ModelState);
|
||||||
{
|
if (string.IsNullOrEmpty(model.TestEmail))
|
||||||
TempData[WellKnownTempData.ErrorMessage] = "Required fields missing";
|
ModelState.AddModelError(nameof(model.TestEmail), new RequiredAttribute().FormatErrorMessage(nameof(model.TestEmail)));
|
||||||
|
if (!ModelState.IsValid)
|
||||||
return View(model);
|
return View(model);
|
||||||
}
|
|
||||||
if (!MailboxAddress.TryParse(model.TestEmail, out MailboxAddress testEmail))
|
|
||||||
{
|
|
||||||
TempData[WellKnownTempData.ErrorMessage] = "Invalid test email";
|
|
||||||
return View(model);
|
|
||||||
}
|
|
||||||
using var client = await model.Settings.CreateSmtpClient();
|
using var client = await model.Settings.CreateSmtpClient();
|
||||||
var message = model.Settings.CreateMailMessage(testEmail, "BTCPay test", "BTCPay test", false);
|
var message = model.Settings.CreateMailMessage(MailboxAddress.Parse(model.TestEmail), "BTCPay test", "BTCPay test", false);
|
||||||
await client.SendAsync(message);
|
await client.SendAsync(message);
|
||||||
await client.DisconnectAsync(true);
|
await client.DisconnectAsync(true);
|
||||||
TempData[WellKnownTempData.SuccessMessage] = $"Email sent to {model.TestEmail}. Please verify you received it.";
|
TempData[WellKnownTempData.SuccessMessage] = $"Email sent to {model.TestEmail}. Please verify you received it.";
|
||||||
|
@ -150,6 +146,11 @@ namespace BTCPayServer.Controllers
|
||||||
}
|
}
|
||||||
else // if (command == "Save")
|
else // if (command == "Save")
|
||||||
{
|
{
|
||||||
|
if (model.Settings.From is not null && !MailboxAddressValidator.IsMailboxAddress(model.Settings.From))
|
||||||
|
{
|
||||||
|
ModelState.AddModelError("Settings.From", "Invalid email");
|
||||||
|
return View(model);
|
||||||
|
}
|
||||||
var storeBlob = store.GetStoreBlob();
|
var storeBlob = store.GetStoreBlob();
|
||||||
if (new EmailsViewModel(storeBlob.EmailSettings).PasswordSet && storeBlob.EmailSettings != null)
|
if (new EmailsViewModel(storeBlob.EmailSettings).PasswordSet && storeBlob.EmailSettings != null)
|
||||||
{
|
{
|
||||||
|
|
|
@ -64,7 +64,7 @@ namespace BTCPayServer.Data.Payouts.LightningLike
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
string lnurlTag = null;
|
string lnurlTag = null;
|
||||||
var lnurl = EmailValidator.IsEmail(destination)
|
var lnurl = MailboxAddressValidator.IsMailboxAddress(destination)
|
||||||
? LNURL.LNURL.ExtractUriFromInternetIdentifier(destination)
|
? LNURL.LNURL.ExtractUriFromInternetIdentifier(destination)
|
||||||
: LNURL.LNURL.Parse(destination, out lnurlTag);
|
: LNURL.LNURL.Parse(destination, out lnurlTag);
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,6 @@ namespace BTCPayServer
|
||||||
{
|
{
|
||||||
public static class ModelStateExtensions
|
public static class ModelStateExtensions
|
||||||
{
|
{
|
||||||
|
|
||||||
public static void AddModelError<TModel, TProperty>(this TModel source,
|
public static void AddModelError<TModel, TProperty>(this TModel source,
|
||||||
Expression<Func<TModel, TProperty>> ex,
|
Expression<Func<TModel, TProperty>> ex,
|
||||||
string message,
|
string message,
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using BTCPayServer.Data;
|
using BTCPayServer.Data;
|
||||||
using BTCPayServer.Services.Invoices;
|
using BTCPayServer.Services.Invoices;
|
||||||
|
@ -7,6 +8,15 @@ namespace BTCPayServer
|
||||||
{
|
{
|
||||||
public static class UserExtensions
|
public static class UserExtensions
|
||||||
{
|
{
|
||||||
|
public static MimeKit.MailboxAddress GetMailboxAddress(this ApplicationUser user)
|
||||||
|
{
|
||||||
|
if (user is null)
|
||||||
|
throw new ArgumentNullException(nameof(user));
|
||||||
|
var name = user.UserName ?? String.Empty;
|
||||||
|
if (user.Email == user.UserName)
|
||||||
|
name = String.Empty;
|
||||||
|
return new MimeKit.MailboxAddress(name, user.Email);
|
||||||
|
}
|
||||||
public static UserBlob GetBlob(this ApplicationUser user)
|
public static UserBlob GetBlob(this ApplicationUser user)
|
||||||
{
|
{
|
||||||
var result = user.Blob == null
|
var result = user.Blob == null
|
||||||
|
|
|
@ -122,7 +122,7 @@ namespace BTCPayServer.HostedServices
|
||||||
|
|
||||||
if (sendMail &&
|
if (sendMail &&
|
||||||
invoice.NotificationEmail is String e &&
|
invoice.NotificationEmail is String e &&
|
||||||
MailboxAddress.TryParse(e, out MailboxAddress notificationEmail))
|
MailboxAddressValidator.TryParse(e, out MailboxAddress notificationEmail))
|
||||||
{
|
{
|
||||||
var json = NBitcoin.JsonConverters.Serializer.ToString(notification);
|
var json = NBitcoin.JsonConverters.Serializer.ToString(notification);
|
||||||
var store = await _StoreRepository.FindStore(invoice.StoreId);
|
var store = await _StoreRepository.FindStore(invoice.StoreId);
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
@ -53,20 +53,18 @@ public class StoreEmailRuleProcessorSender : EventHostedServiceBase
|
||||||
var sender = await _emailSenderFactory.GetEmailSender(invoiceEvent.Invoice.StoreId);
|
var sender = await _emailSenderFactory.GetEmailSender(invoiceEvent.Invoice.StoreId);
|
||||||
foreach (UIStoresController.StoreEmailRule actionableRule in actionableRules)
|
foreach (UIStoresController.StoreEmailRule actionableRule in actionableRules)
|
||||||
{
|
{
|
||||||
var dest = actionableRule.To.Split(",", StringSplitOptions.RemoveEmptyEntries).Where(IsValidEmailAddress);
|
var recipients = actionableRule.To.Split(",", StringSplitOptions.RemoveEmptyEntries)
|
||||||
if (actionableRule.CustomerEmail && IsValidEmailAddress(invoiceEvent.Invoice.Metadata.BuyerEmail))
|
.Select(o => { MailboxAddressValidator.TryParse(o, out var mb); return mb; })
|
||||||
|
.Where(o => o != null)
|
||||||
|
.ToList();
|
||||||
|
if (actionableRule.CustomerEmail && MailboxAddressValidator.TryParse(invoiceEvent.Invoice.Metadata.BuyerEmail, out var bmb))
|
||||||
{
|
{
|
||||||
dest = dest.Append(invoiceEvent.Invoice.Metadata.BuyerEmail);
|
recipients.Add(bmb);
|
||||||
}
|
}
|
||||||
|
sender.SendEmail(recipients.ToArray(), null, null, actionableRule.Subject, actionableRule.Body);
|
||||||
var recipients = dest.Select(address => new MailboxAddress(address, address)).ToArray();
|
|
||||||
sender.SendEmail(recipients, null, null, actionableRule.Subject, actionableRule.Body);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool IsValidEmailAddress(string address) =>
|
|
||||||
!string.IsNullOrEmpty(address) && MailboxAddress.TryParse(address, out _);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -55,8 +55,7 @@ namespace BTCPayServer.HostedServices
|
||||||
new HostString(userRegisteredEvent.RequestUri.Host, userRegisteredEvent.RequestUri.Port),
|
new HostString(userRegisteredEvent.RequestUri.Host, userRegisteredEvent.RequestUri.Port),
|
||||||
userRegisteredEvent.RequestUri.PathAndQuery);
|
userRegisteredEvent.RequestUri.PathAndQuery);
|
||||||
userRegisteredEvent.CallbackUrlGenerated?.SetResult(new Uri(callbackUrl));
|
userRegisteredEvent.CallbackUrlGenerated?.SetResult(new Uri(callbackUrl));
|
||||||
address = new MailboxAddress(userRegisteredEvent.User.Email,
|
address = userRegisteredEvent.User.GetMailboxAddress();
|
||||||
userRegisteredEvent.User.Email);
|
|
||||||
(await _emailSenderFactory.GetEmailSender()).SendEmailConfirmation(address, callbackUrl);
|
(await _emailSenderFactory.GetEmailSender()).SendEmailConfirmation(address, callbackUrl);
|
||||||
}
|
}
|
||||||
else if (!await _userManager.HasPasswordAsync(userRegisteredEvent.User))
|
else if (!await _userManager.HasPasswordAsync(userRegisteredEvent.User))
|
||||||
|
@ -86,8 +85,7 @@ passwordSetter:
|
||||||
userPasswordResetRequestedEvent.RequestUri.Port),
|
userPasswordResetRequestedEvent.RequestUri.Port),
|
||||||
userPasswordResetRequestedEvent.RequestUri.PathAndQuery);
|
userPasswordResetRequestedEvent.RequestUri.PathAndQuery);
|
||||||
userPasswordResetRequestedEvent.CallbackUrlGenerated?.SetResult(new Uri(callbackUrl));
|
userPasswordResetRequestedEvent.CallbackUrlGenerated?.SetResult(new Uri(callbackUrl));
|
||||||
address = new MailboxAddress(userPasswordResetRequestedEvent.User.Email,
|
address = userPasswordResetRequestedEvent.User.GetMailboxAddress();
|
||||||
userPasswordResetRequestedEvent.User.Email);
|
|
||||||
(await _emailSenderFactory.GetEmailSender())
|
(await _emailSenderFactory.GetEmailSender())
|
||||||
.SendSetPasswordConfirmation(address, callbackUrl, newPassword);
|
.SendSetPasswordConfirmation(address, callbackUrl, newPassword);
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -50,7 +50,7 @@ namespace BTCPayServer.Models.InvoicingModels
|
||||||
get; set;
|
get; set;
|
||||||
}
|
}
|
||||||
|
|
||||||
[EmailAddress]
|
[MailboxAddress]
|
||||||
[DisplayName("Buyer Email")]
|
[DisplayName("Buyer Email")]
|
||||||
public string BuyerEmail
|
public string BuyerEmail
|
||||||
{
|
{
|
||||||
|
@ -76,7 +76,7 @@ namespace BTCPayServer.Models.InvoicingModels
|
||||||
get; set;
|
get; set;
|
||||||
}
|
}
|
||||||
|
|
||||||
[EmailAddress]
|
[MailboxAddress]
|
||||||
[DisplayName("Notification Email")]
|
[DisplayName("Notification Email")]
|
||||||
public string NotificationEmail
|
public string NotificationEmail
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using BTCPayServer.Validation;
|
||||||
|
|
||||||
namespace BTCPayServer.Models.InvoicingModels
|
namespace BTCPayServer.Models.InvoicingModels
|
||||||
{
|
{
|
||||||
public class UpdateCustomerModel
|
public class UpdateCustomerModel
|
||||||
{
|
{
|
||||||
[EmailAddress]
|
[MailboxAddress]
|
||||||
[Required]
|
[Required]
|
||||||
public string Email
|
public string Email
|
||||||
{
|
{
|
||||||
|
|
|
@ -5,6 +5,7 @@ using BTCPayServer.Client.Models;
|
||||||
using BTCPayServer.Data;
|
using BTCPayServer.Data;
|
||||||
using BTCPayServer.Services.Invoices;
|
using BTCPayServer.Services.Invoices;
|
||||||
using BTCPayServer.Services.Rates;
|
using BTCPayServer.Services.Rates;
|
||||||
|
using BTCPayServer.Validation;
|
||||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||||
using PaymentRequestData = BTCPayServer.Data.PaymentRequestData;
|
using PaymentRequestData = BTCPayServer.Data.PaymentRequestData;
|
||||||
|
|
||||||
|
@ -64,7 +65,7 @@ namespace BTCPayServer.Models.PaymentRequestViewModels
|
||||||
[Display(Name = "Store")]
|
[Display(Name = "Store")]
|
||||||
public SelectList Stores { get; set; }
|
public SelectList Stores { get; set; }
|
||||||
|
|
||||||
[EmailAddress]
|
[MailboxAddress]
|
||||||
public string Email { get; set; }
|
public string Email { get; set; }
|
||||||
|
|
||||||
[MaxLength(500)]
|
[MaxLength(500)]
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using BTCPayServer.Services.Mails;
|
using BTCPayServer.Services.Mails;
|
||||||
|
using BTCPayServer.Validation;
|
||||||
|
|
||||||
namespace BTCPayServer.Models.ServerViewModels
|
namespace BTCPayServer.Models.ServerViewModels
|
||||||
{
|
{
|
||||||
|
@ -19,7 +20,7 @@ namespace BTCPayServer.Models.ServerViewModels
|
||||||
get; set;
|
get; set;
|
||||||
}
|
}
|
||||||
public bool PasswordSet { get; set; }
|
public bool PasswordSet { get; set; }
|
||||||
[EmailAddress]
|
[MailboxAddressAttribute]
|
||||||
[Display(Name = "Test Email")]
|
[Display(Name = "Test Email")]
|
||||||
public string TestEmail
|
public string TestEmail
|
||||||
{
|
{
|
||||||
|
|
|
@ -3,6 +3,7 @@ using System.ComponentModel.DataAnnotations;
|
||||||
using BTCPayServer.ModelBinders;
|
using BTCPayServer.ModelBinders;
|
||||||
using BTCPayServer.Models.AppViewModels;
|
using BTCPayServer.Models.AppViewModels;
|
||||||
using BTCPayServer.Models.StoreViewModels;
|
using BTCPayServer.Models.StoreViewModels;
|
||||||
|
using BTCPayServer.Validation;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
namespace BTCPayServer.Plugins.PayButton.Models
|
namespace BTCPayServer.Plugins.PayButton.Models
|
||||||
|
@ -34,7 +35,7 @@ namespace BTCPayServer.Plugins.PayButton.Models
|
||||||
public string ServerIpn { get; set; }
|
public string ServerIpn { get; set; }
|
||||||
[Url]
|
[Url]
|
||||||
public string BrowserRedirect { get; set; }
|
public string BrowserRedirect { get; set; }
|
||||||
[EmailAddress]
|
[MailboxAddress]
|
||||||
public string NotifyEmail { get; set; }
|
public string NotifyEmail { get; set; }
|
||||||
|
|
||||||
public string StoreId { get; set; }
|
public string StoreId { get; set; }
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using BTCPayServer.Client.Models;
|
using BTCPayServer.Client.Models;
|
||||||
|
using BTCPayServer.Validation;
|
||||||
using MailKit.Net.Smtp;
|
using MailKit.Net.Smtp;
|
||||||
|
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||||
using MimeKit;
|
using MimeKit;
|
||||||
|
|
||||||
namespace BTCPayServer.Services.Mails
|
namespace BTCPayServer.Services.Mails
|
||||||
|
@ -10,7 +13,29 @@ namespace BTCPayServer.Services.Mails
|
||||||
{
|
{
|
||||||
public bool IsComplete()
|
public bool IsComplete()
|
||||||
{
|
{
|
||||||
return !string.IsNullOrWhiteSpace(Server) && Port is int;
|
return MailboxAddressValidator.IsMailboxAddress(From)
|
||||||
|
&& !string.IsNullOrWhiteSpace(Server)
|
||||||
|
&& Port is int;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Validate(string prefixKey, ModelStateDictionary modelState)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(From))
|
||||||
|
{
|
||||||
|
modelState.AddModelError($"{prefixKey}{nameof(From)}", new RequiredAttribute().FormatErrorMessage(nameof(From)));
|
||||||
|
}
|
||||||
|
if (!MailboxAddressValidator.IsMailboxAddress(From))
|
||||||
|
{
|
||||||
|
modelState.AddModelError($"{prefixKey}{nameof(From)}", MailboxAddressAttribute.ErrorMessageConst);
|
||||||
|
}
|
||||||
|
if (string.IsNullOrWhiteSpace(Server))
|
||||||
|
{
|
||||||
|
modelState.AddModelError($"{prefixKey}{nameof(Server)}", new RequiredAttribute().FormatErrorMessage(nameof(Server)));
|
||||||
|
}
|
||||||
|
if (Port is null)
|
||||||
|
{
|
||||||
|
modelState.AddModelError($"{prefixKey}{nameof(Port)}", new RequiredAttribute().FormatErrorMessage(nameof(Port)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public MimeMessage CreateMailMessage(MailboxAddress to, string subject, string message, bool isHtml) =>
|
public MimeMessage CreateMailMessage(MailboxAddress to, string subject, string message, bool isHtml) =>
|
||||||
|
@ -30,12 +55,11 @@ namespace BTCPayServer.Services.Mails
|
||||||
var mm = new MimeMessage();
|
var mm = new MimeMessage();
|
||||||
mm.Body = bodyBuilder.ToMessageBody();
|
mm.Body = bodyBuilder.ToMessageBody();
|
||||||
mm.Subject = subject;
|
mm.Subject = subject;
|
||||||
mm.From.Add(new MailboxAddress(From, !string.IsNullOrWhiteSpace(FromDisplay) ? From : FromDisplay));
|
mm.From.Add(MailboxAddressValidator.Parse(From));
|
||||||
mm.To.AddRange(to);
|
mm.To.AddRange(to);
|
||||||
mm.Cc.AddRange(cc ?? System.Array.Empty<InternetAddress>());
|
mm.Cc.AddRange(cc ?? System.Array.Empty<InternetAddress>());
|
||||||
mm.Bcc.AddRange(bcc ?? System.Array.Empty<InternetAddress>());
|
mm.Bcc.AddRange(bcc ?? System.Array.Empty<InternetAddress>());
|
||||||
return mm;
|
return mm;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<SmtpClient> CreateSmtpClient()
|
public async Task<SmtpClient> CreateSmtpClient()
|
||||||
|
|
|
@ -1,18 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
|
|
||||||
namespace BTCPayServer.Validation
|
|
||||||
{
|
|
||||||
public class EmailValidator
|
|
||||||
{
|
|
||||||
static Regex _Email;
|
|
||||||
public static bool IsEmail(string str)
|
|
||||||
{
|
|
||||||
if (String.IsNullOrWhiteSpace(str))
|
|
||||||
return false;
|
|
||||||
if (_Email == null)
|
|
||||||
_Email = new Regex("^((([a-z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\u00A0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF])+(\\.([a-z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\u00A0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF])+)*)|((\\x22)((((\\x20|\\x09)*(\\x0d\\x0a))?(\\x20|\\x09)+)?(([\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]|\\x21|[\\x23-\\x5b]|[\\x5d-\\x7e]|[\\u00A0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF])|(\\\\([\\x01-\\x09\\x0b\\x0c\\x0d-\\x7f]|[\\u00A0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF]))))*(((\\x20|\\x09)*(\\x0d\\x0a))?(\\x20|\\x09)+)?(\\x22)))@((([a-z]|\\d|[\\u00A0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF])|(([a-z]|\\d|[\\u00A0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF])([a-z]|\\d|-|\\.|_|~|[\\u00A0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF])*([a-z]|\\d|[\\u00A0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF])))\\.)+(([a-z]|[\\u00A0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF])|(([a-z]|[\\u00A0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF])([a-z]|\\d|-|\\.|_|~|[\\u00A0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF])*([a-z]|[\\u00A0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF])))\\.?$", RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture | RegexOptions.Compiled, TimeSpan.FromSeconds(2.0));
|
|
||||||
return _Email.IsMatch(str);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
25
BTCPayServer/Validation/MailboxAddressAttribute.cs
Normal file
25
BTCPayServer/Validation/MailboxAddressAttribute.cs
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Validation
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Validate address in the format "Firstname Lastname <blah@example.com>" See rfc822
|
||||||
|
/// </summary>
|
||||||
|
public class MailboxAddressAttribute : ValidationAttribute
|
||||||
|
{
|
||||||
|
public MailboxAddressAttribute()
|
||||||
|
{
|
||||||
|
ErrorMessage = ErrorMessageConst;
|
||||||
|
}
|
||||||
|
public const string ErrorMessageConst = "Invalid mailbox address. Some valid examples are: 'test@example.com' or 'Firstname Lastname <test@example.com>'";
|
||||||
|
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
|
||||||
|
{
|
||||||
|
if (value is null)
|
||||||
|
return ValidationResult.Success;
|
||||||
|
var str = value as string;
|
||||||
|
if (MailboxAddressValidator.IsMailboxAddress(str))
|
||||||
|
return ValidationResult.Success;
|
||||||
|
return new ValidationResult(ErrorMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
39
BTCPayServer/Validation/MailboxAddressValidator.cs
Normal file
39
BTCPayServer/Validation/MailboxAddressValidator.cs
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
#nullable enable
|
||||||
|
using System;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using MimeKit;
|
||||||
|
|
||||||
|
namespace BTCPayServer
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Validate address in the format "Firstname Lastname <blah@example.com>" See rfc822
|
||||||
|
/// </summary>
|
||||||
|
public class MailboxAddressValidator
|
||||||
|
{
|
||||||
|
static ParserOptions _options;
|
||||||
|
static MailboxAddressValidator()
|
||||||
|
{
|
||||||
|
_options = ParserOptions.Default.Clone();
|
||||||
|
_options.AllowAddressesWithoutDomain = false;
|
||||||
|
}
|
||||||
|
public static bool IsMailboxAddress(string? str)
|
||||||
|
{
|
||||||
|
return TryParse(str, out _);
|
||||||
|
}
|
||||||
|
public static MailboxAddress Parse(string? str)
|
||||||
|
{
|
||||||
|
if (!TryParse(str, out var mb))
|
||||||
|
throw new FormatException("Invalid mailbox address (rfc822)");
|
||||||
|
return mb;
|
||||||
|
}
|
||||||
|
public static bool TryParse(string? str, [MaybeNullWhen(false)] out MailboxAddress mailboxAddress)
|
||||||
|
{
|
||||||
|
mailboxAddress = null;
|
||||||
|
if (String.IsNullOrWhiteSpace(str))
|
||||||
|
return false;
|
||||||
|
return MailboxAddress.TryParse(_options, str, out mailboxAddress) && mailboxAddress is not null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -39,17 +39,9 @@
|
||||||
<input asp-for="Settings.Port" data-fill="port" class="form-control"/>
|
<input asp-for="Settings.Port" data-fill="port" class="form-control"/>
|
||||||
<span asp-validation-for="Settings.Port" class="text-danger"></span>
|
<span asp-validation-for="Settings.Port" class="text-danger"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
|
||||||
<label asp-for="Settings.FromDisplay" class="form-label">Sender's display name</label>
|
|
||||||
<input asp-for="Settings.FromDisplay" class="form-control"/>
|
|
||||||
<small class="form-text text-muted">
|
|
||||||
Some email providers (like Gmail) don't allow you to set your display name, so this setting may not have any effect.
|
|
||||||
</small>
|
|
||||||
<span asp-validation-for="Settings.FromDisplay" class="text-danger"></span>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label asp-for="Settings.From" class="form-label">Sender's Email Address</label>
|
<label asp-for="Settings.From" class="form-label">Sender's Email Address</label>
|
||||||
<input asp-for="Settings.From" class="form-control"/>
|
<input asp-for="Settings.From" class="form-control" placeholder="Firstname Lastname <email@example.com>" />
|
||||||
<span asp-validation-for="Settings.From" class="text-danger"></span>
|
<span asp-validation-for="Settings.From" class="text-danger"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
@ -89,7 +81,7 @@
|
||||||
To test your settings, enter an email address below.
|
To test your settings, enter an email address below.
|
||||||
</p>
|
</p>
|
||||||
<label asp-for="TestEmail" class="form-label"></label>
|
<label asp-for="TestEmail" class="form-label"></label>
|
||||||
<input asp-for="TestEmail" class="form-control" />
|
<input asp-for="TestEmail" placeholder="Firstname Lastname <email@example.com>" class="form-control" />
|
||||||
<span asp-validation-for="TestEmail" class="text-danger"></span>
|
<span asp-validation-for="TestEmail" class="text-danger"></span>
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="btn btn-secondary mt-2" name="command" value="Test" id="Test">Send Test Email</button>
|
<button type="submit" class="btn btn-secondary mt-2" name="command" value="Test" id="Test">Send Test Email</button>
|
||||||
|
|
|
@ -73,7 +73,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label asp-for="Email" class="form-label"></label>
|
<label asp-for="Email" class="form-label"></label>
|
||||||
<input type="email" asp-for="Email" class="form-control" />
|
<input type="email" asp-for="Email" placeholder="Firstname Lastname <email@example.com>" class="form-control" />
|
||||||
<span asp-validation-for="Email" class="text-danger"></span>
|
<span asp-validation-for="Email" class="text-danger"></span>
|
||||||
<p id="PaymentRequestEmailHelpBlock" class="form-text text-muted">
|
<p id="PaymentRequestEmailHelpBlock" class="form-text text-muted">
|
||||||
Receive updates for this payment request.
|
Receive updates for this payment request.
|
||||||
|
|
Loading…
Add table
Reference in a new issue