From c89f7aaaeddc5e07b9749656f8e05cdbb0f07245 Mon Sep 17 00:00:00 2001 From: Nicolas Dorier Date: Thu, 23 Jun 2022 13:41:52 +0900 Subject: [PATCH] Improve email settings validation and UX (#3891) --- .../Models/EmailSettingsData.cs | 7 +--- BTCPayServer.Tests/SeleniumTests.cs | 1 + .../GreenfieldStoreEmailController.cs | 4 +- .../GreenField/GreenfieldUsersController.cs | 2 +- .../Controllers/UIInvoiceController.cs | 2 +- .../Controllers/UIManageController.cs | 3 +- .../Controllers/UIServerController.Users.cs | 3 +- .../Controllers/UIServerController.cs | 24 ++++++------ .../Controllers/UIStoresController.Email.cs | 23 +++++------ .../LightningLikePayoutHandler.cs | 2 +- .../Extensions/ModelStateExtensions.cs | 1 - BTCPayServer/Extensions/UserExtensions.cs | 10 +++++ .../HostedServices/BitpayIPNSender.cs | 2 +- .../StoreEmailRuleProcessorSender.cs | 18 ++++----- .../HostedServices/UserEventHostedService.cs | 6 +-- .../InvoicingModels/CreateInvoiceModel.cs | 4 +- .../InvoicingModels/UpdateCustomerModel.cs | 3 +- .../ListPaymentRequestsViewModel.cs | 3 +- .../ServerViewModels/EmailsViewModel.cs | 3 +- .../PayButton/Models/PayButtonViewModel.cs | 3 +- BTCPayServer/Services/Mails/EmailSettings.cs | 34 +++++++++++++--- BTCPayServer/Validation/EmailValidator.cs | 18 --------- .../Validation/MailboxAddressAttribute.cs | 25 ++++++++++++ .../Validation/MailboxAddressValidator.cs | 39 +++++++++++++++++++ BTCPayServer/Views/Shared/EmailsBody.cshtml | 12 +----- .../EditPaymentRequest.cshtml | 2 +- 26 files changed, 161 insertions(+), 93 deletions(-) delete mode 100644 BTCPayServer/Validation/EmailValidator.cs create mode 100644 BTCPayServer/Validation/MailboxAddressAttribute.cs create mode 100644 BTCPayServer/Validation/MailboxAddressValidator.cs diff --git a/BTCPayServer.Client/Models/EmailSettingsData.cs b/BTCPayServer.Client/Models/EmailSettingsData.cs index a587dfd43..3822f28fc 100644 --- a/BTCPayServer.Client/Models/EmailSettingsData.cs +++ b/BTCPayServer.Client/Models/EmailSettingsData.cs @@ -1,4 +1,4 @@ -namespace BTCPayServer.Client.Models; +namespace BTCPayServer.Client.Models; public class EmailSettingsData { @@ -21,11 +21,6 @@ public class EmailSettingsData { get; set; } - - public string FromDisplay - { - get; set; - } public string From { get; set; diff --git a/BTCPayServer.Tests/SeleniumTests.cs b/BTCPayServer.Tests/SeleniumTests.cs index e0927b8e4..30d7bf073 100644 --- a/BTCPayServer.Tests/SeleniumTests.cs +++ b/BTCPayServer.Tests/SeleniumTests.cs @@ -1940,6 +1940,7 @@ retry: s.Driver.FindElement(By.CssSelector("button[value=\"Save\"]")).Submit(); s.FindAlertMessage(); s.Driver.FindElement(By.Id("Settings_Password")).SendKeys("mypassword"); + s.Driver.FindElement(By.Id("Settings_From")).SendKeys("Firstname Lastname "); s.Driver.FindElement(By.Id("Save")).SendKeys(Keys.Enter); Assert.Contains("Configured", s.Driver.PageSource); s.Driver.FindElement(By.Id("Settings_Login")).SendKeys("test_fix@gmail.com"); diff --git a/BTCPayServer/Controllers/GreenField/GreenfieldStoreEmailController.cs b/BTCPayServer/Controllers/GreenField/GreenfieldStoreEmailController.cs index 0e88764fd..321521eb6 100644 --- a/BTCPayServer/Controllers/GreenField/GreenfieldStoreEmailController.cs +++ b/BTCPayServer/Controllers/GreenField/GreenfieldStoreEmailController.cs @@ -39,7 +39,7 @@ namespace BTCPayServer.Controllers.GreenField { 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"); return this.CreateValidationError(ModelState); @@ -72,7 +72,7 @@ namespace BTCPayServer.Controllers.GreenField 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, "Invalid email address", this); diff --git a/BTCPayServer/Controllers/GreenField/GreenfieldUsersController.cs b/BTCPayServer/Controllers/GreenField/GreenfieldUsersController.cs index f0236fa4a..7dc5c1c78 100644 --- a/BTCPayServer/Controllers/GreenField/GreenfieldUsersController.cs +++ b/BTCPayServer/Controllers/GreenField/GreenfieldUsersController.cs @@ -118,7 +118,7 @@ namespace BTCPayServer.Controllers.Greenfield { if (request.Email is null) 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"); } diff --git a/BTCPayServer/Controllers/UIInvoiceController.cs b/BTCPayServer/Controllers/UIInvoiceController.cs index 6c10795df..b1360da23 100644 --- a/BTCPayServer/Controllers/UIInvoiceController.cs +++ b/BTCPayServer/Controllers/UIInvoiceController.cs @@ -233,7 +233,7 @@ namespace BTCPayServer.Controllers if (entity.Metadata.BuyerEmail != null) { - if (!EmailValidator.IsEmail(entity.Metadata.BuyerEmail)) + if (!MailboxAddressValidator.IsMailboxAddress(entity.Metadata.BuyerEmail)) throw new BitpayHttpException(400, "Invalid email"); entity.RefundMail = entity.Metadata.BuyerEmail; } diff --git a/BTCPayServer/Controllers/UIManageController.cs b/BTCPayServer/Controllers/UIManageController.cs index 833fbb441..d88776510 100644 --- a/BTCPayServer/Controllers/UIManageController.cs +++ b/BTCPayServer/Controllers/UIManageController.cs @@ -165,8 +165,7 @@ namespace BTCPayServer.Controllers var code = await _userManager.GenerateEmailConfirmationTokenAsync(user); 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(address, callbackUrl); + (await _EmailSenderFactory.GetEmailSender()).SendEmailConfirmation(user.GetMailboxAddress(), callbackUrl); TempData[WellKnownTempData.SuccessMessage] = "Verification email sent. Please check your email."; return RedirectToAction(nameof(Index)); } diff --git a/BTCPayServer/Controllers/UIServerController.Users.cs b/BTCPayServer/Controllers/UIServerController.Users.cs index 3dc3b8672..ccdbc090a 100644 --- a/BTCPayServer/Controllers/UIServerController.Users.cs +++ b/BTCPayServer/Controllers/UIServerController.Users.cs @@ -303,8 +303,7 @@ namespace BTCPayServer.Controllers var code = await _UserManager.GenerateEmailConfirmationTokenAsync(user); 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(address, callbackUrl); + (await _emailSenderFactory.GetEmailSender()).SendEmailConfirmation(user.GetMailboxAddress(), callbackUrl); TempData[WellKnownTempData.SuccessMessage] = "Verification email sent"; return RedirectToAction(nameof(ListUsers)); diff --git a/BTCPayServer/Controllers/UIServerController.cs b/BTCPayServer/Controllers/UIServerController.cs index 1c83b00b8..94a2330ae 100644 --- a/BTCPayServer/Controllers/UIServerController.cs +++ b/BTCPayServer/Controllers/UIServerController.cs @@ -1,6 +1,7 @@ #nullable enable using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; @@ -25,6 +26,7 @@ using BTCPayServer.Services.Stores; using BTCPayServer.Storage.Models; using BTCPayServer.Storage.Services; using BTCPayServer.Storage.Services.Providers; +using BTCPayServer.Validation; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; @@ -1015,18 +1017,13 @@ namespace BTCPayServer.Controllers var settings = await _SettingsRepository.GetSettingAsync() ?? new EmailSettings(); model.Settings.Password = settings.Password; } - if (!model.Settings.IsComplete()) - { - TempData[WellKnownTempData.ErrorMessage] = "Required fields missing"; + model.Settings.Validate("Settings.", ModelState); + if (string.IsNullOrEmpty(model.TestEmail)) + ModelState.AddModelError(nameof(model.TestEmail), new RequiredAttribute().FormatErrorMessage(nameof(model.TestEmail))); + if (!ModelState.IsValid) 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 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.DisconnectAsync(true); @@ -1043,12 +1040,17 @@ namespace BTCPayServer.Controllers { var settings = await _SettingsRepository.GetSettingAsync() ?? new EmailSettings(); settings.Password = null; - await _SettingsRepository.UpdateSetting(model.Settings); + await _SettingsRepository.UpdateSetting(settings); TempData[WellKnownTempData.SuccessMessage] = "Email server password reset"; return RedirectToAction(nameof(Emails)); } 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() ?? new EmailSettings(); if (new EmailsViewModel(oldSettings).PasswordSet) { diff --git a/BTCPayServer/Controllers/UIStoresController.Email.cs b/BTCPayServer/Controllers/UIStoresController.Email.cs index 399f2f754..f84c2a69a 100644 --- a/BTCPayServer/Controllers/UIStoresController.Email.cs +++ b/BTCPayServer/Controllers/UIStoresController.Email.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; +using System.Globalization; using System.Threading.Tasks; using BTCPayServer.Abstractions.Constants; using BTCPayServer.Abstractions.Extensions; @@ -45,7 +46,7 @@ namespace BTCPayServer.Controllers if (command.StartsWith("remove", StringComparison.InvariantCultureIgnoreCase)) { var item = command[(command.IndexOf(":", StringComparison.InvariantCultureIgnoreCase) + 1)..]; - var index = int.Parse(item); + var index = int.Parse(item, CultureInfo.InvariantCulture); vm.Rules.RemoveAt(index); return View(vm); @@ -117,18 +118,13 @@ namespace BTCPayServer.Controllers { model.Settings.Password = store.GetStoreBlob().EmailSettings.Password; } - if (!model.Settings.IsComplete()) - { - TempData[WellKnownTempData.ErrorMessage] = "Required fields missing"; + model.Settings.Validate("Settings.", ModelState); + if (string.IsNullOrEmpty(model.TestEmail)) + ModelState.AddModelError(nameof(model.TestEmail), new RequiredAttribute().FormatErrorMessage(nameof(model.TestEmail))); + if (!ModelState.IsValid) 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(); - 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.DisconnectAsync(true); TempData[WellKnownTempData.SuccessMessage] = $"Email sent to {model.TestEmail}. Please verify you received it."; @@ -150,6 +146,11 @@ namespace BTCPayServer.Controllers } 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(); if (new EmailsViewModel(storeBlob.EmailSettings).PasswordSet && storeBlob.EmailSettings != null) { diff --git a/BTCPayServer/Data/Payouts/LightningLike/LightningLikePayoutHandler.cs b/BTCPayServer/Data/Payouts/LightningLike/LightningLikePayoutHandler.cs index 051045716..bc8062b80 100644 --- a/BTCPayServer/Data/Payouts/LightningLike/LightningLikePayoutHandler.cs +++ b/BTCPayServer/Data/Payouts/LightningLike/LightningLikePayoutHandler.cs @@ -64,7 +64,7 @@ namespace BTCPayServer.Data.Payouts.LightningLike try { string lnurlTag = null; - var lnurl = EmailValidator.IsEmail(destination) + var lnurl = MailboxAddressValidator.IsMailboxAddress(destination) ? LNURL.LNURL.ExtractUriFromInternetIdentifier(destination) : LNURL.LNURL.Parse(destination, out lnurlTag); diff --git a/BTCPayServer/Extensions/ModelStateExtensions.cs b/BTCPayServer/Extensions/ModelStateExtensions.cs index 80215e0e0..7a0f17952 100644 --- a/BTCPayServer/Extensions/ModelStateExtensions.cs +++ b/BTCPayServer/Extensions/ModelStateExtensions.cs @@ -7,7 +7,6 @@ namespace BTCPayServer { public static class ModelStateExtensions { - public static void AddModelError(this TModel source, Expression> ex, string message, diff --git a/BTCPayServer/Extensions/UserExtensions.cs b/BTCPayServer/Extensions/UserExtensions.cs index 8b652b775..138abb983 100644 --- a/BTCPayServer/Extensions/UserExtensions.cs +++ b/BTCPayServer/Extensions/UserExtensions.cs @@ -1,3 +1,4 @@ +using System; using System.Linq; using BTCPayServer.Data; using BTCPayServer.Services.Invoices; @@ -7,6 +8,15 @@ namespace BTCPayServer { 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) { var result = user.Blob == null diff --git a/BTCPayServer/HostedServices/BitpayIPNSender.cs b/BTCPayServer/HostedServices/BitpayIPNSender.cs index a4b06829c..f2c04fb55 100644 --- a/BTCPayServer/HostedServices/BitpayIPNSender.cs +++ b/BTCPayServer/HostedServices/BitpayIPNSender.cs @@ -122,7 +122,7 @@ namespace BTCPayServer.HostedServices if (sendMail && 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 store = await _StoreRepository.FindStore(invoice.StoreId); diff --git a/BTCPayServer/HostedServices/StoreEmailRuleProcessorSender.cs b/BTCPayServer/HostedServices/StoreEmailRuleProcessorSender.cs index 1cec93f04..60cb24759 100644 --- a/BTCPayServer/HostedServices/StoreEmailRuleProcessorSender.cs +++ b/BTCPayServer/HostedServices/StoreEmailRuleProcessorSender.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -53,20 +53,18 @@ public class StoreEmailRuleProcessorSender : EventHostedServiceBase var sender = await _emailSenderFactory.GetEmailSender(invoiceEvent.Invoice.StoreId); foreach (UIStoresController.StoreEmailRule actionableRule in actionableRules) { - var dest = actionableRule.To.Split(",", StringSplitOptions.RemoveEmptyEntries).Where(IsValidEmailAddress); - if (actionableRule.CustomerEmail && IsValidEmailAddress(invoiceEvent.Invoice.Metadata.BuyerEmail)) + var recipients = actionableRule.To.Split(",", StringSplitOptions.RemoveEmptyEntries) + .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); } - - var recipients = dest.Select(address => new MailboxAddress(address, address)).ToArray(); - sender.SendEmail(recipients, null, null, actionableRule.Subject, actionableRule.Body); + sender.SendEmail(recipients.ToArray(), null, null, actionableRule.Subject, actionableRule.Body); } } } } } - - private bool IsValidEmailAddress(string address) => - !string.IsNullOrEmpty(address) && MailboxAddress.TryParse(address, out _); } diff --git a/BTCPayServer/HostedServices/UserEventHostedService.cs b/BTCPayServer/HostedServices/UserEventHostedService.cs index f45323d68..0311d27f6 100644 --- a/BTCPayServer/HostedServices/UserEventHostedService.cs +++ b/BTCPayServer/HostedServices/UserEventHostedService.cs @@ -55,8 +55,7 @@ namespace BTCPayServer.HostedServices new HostString(userRegisteredEvent.RequestUri.Host, userRegisteredEvent.RequestUri.Port), userRegisteredEvent.RequestUri.PathAndQuery); userRegisteredEvent.CallbackUrlGenerated?.SetResult(new Uri(callbackUrl)); - address = new MailboxAddress(userRegisteredEvent.User.Email, - userRegisteredEvent.User.Email); + address = userRegisteredEvent.User.GetMailboxAddress(); (await _emailSenderFactory.GetEmailSender()).SendEmailConfirmation(address, callbackUrl); } else if (!await _userManager.HasPasswordAsync(userRegisteredEvent.User)) @@ -86,8 +85,7 @@ passwordSetter: userPasswordResetRequestedEvent.RequestUri.Port), userPasswordResetRequestedEvent.RequestUri.PathAndQuery); userPasswordResetRequestedEvent.CallbackUrlGenerated?.SetResult(new Uri(callbackUrl)); - address = new MailboxAddress(userPasswordResetRequestedEvent.User.Email, - userPasswordResetRequestedEvent.User.Email); + address = userPasswordResetRequestedEvent.User.GetMailboxAddress(); (await _emailSenderFactory.GetEmailSender()) .SendSetPasswordConfirmation(address, callbackUrl, newPassword); break; diff --git a/BTCPayServer/Models/InvoicingModels/CreateInvoiceModel.cs b/BTCPayServer/Models/InvoicingModels/CreateInvoiceModel.cs index 7306bd156..e568104a2 100644 --- a/BTCPayServer/Models/InvoicingModels/CreateInvoiceModel.cs +++ b/BTCPayServer/Models/InvoicingModels/CreateInvoiceModel.cs @@ -50,7 +50,7 @@ namespace BTCPayServer.Models.InvoicingModels get; set; } - [EmailAddress] + [MailboxAddress] [DisplayName("Buyer Email")] public string BuyerEmail { @@ -76,7 +76,7 @@ namespace BTCPayServer.Models.InvoicingModels get; set; } - [EmailAddress] + [MailboxAddress] [DisplayName("Notification Email")] public string NotificationEmail { diff --git a/BTCPayServer/Models/InvoicingModels/UpdateCustomerModel.cs b/BTCPayServer/Models/InvoicingModels/UpdateCustomerModel.cs index f819fed40..14c86873d 100644 --- a/BTCPayServer/Models/InvoicingModels/UpdateCustomerModel.cs +++ b/BTCPayServer/Models/InvoicingModels/UpdateCustomerModel.cs @@ -1,10 +1,11 @@ using System.ComponentModel.DataAnnotations; +using BTCPayServer.Validation; namespace BTCPayServer.Models.InvoicingModels { public class UpdateCustomerModel { - [EmailAddress] + [MailboxAddress] [Required] public string Email { diff --git a/BTCPayServer/Models/PaymentRequestViewModels/ListPaymentRequestsViewModel.cs b/BTCPayServer/Models/PaymentRequestViewModels/ListPaymentRequestsViewModel.cs index ce7f53a43..09de19b23 100644 --- a/BTCPayServer/Models/PaymentRequestViewModels/ListPaymentRequestsViewModel.cs +++ b/BTCPayServer/Models/PaymentRequestViewModels/ListPaymentRequestsViewModel.cs @@ -5,6 +5,7 @@ using BTCPayServer.Client.Models; using BTCPayServer.Data; using BTCPayServer.Services.Invoices; using BTCPayServer.Services.Rates; +using BTCPayServer.Validation; using Microsoft.AspNetCore.Mvc.Rendering; using PaymentRequestData = BTCPayServer.Data.PaymentRequestData; @@ -64,7 +65,7 @@ namespace BTCPayServer.Models.PaymentRequestViewModels [Display(Name = "Store")] public SelectList Stores { get; set; } - [EmailAddress] + [MailboxAddress] public string Email { get; set; } [MaxLength(500)] diff --git a/BTCPayServer/Models/ServerViewModels/EmailsViewModel.cs b/BTCPayServer/Models/ServerViewModels/EmailsViewModel.cs index 29c90df51..63805b4ee 100644 --- a/BTCPayServer/Models/ServerViewModels/EmailsViewModel.cs +++ b/BTCPayServer/Models/ServerViewModels/EmailsViewModel.cs @@ -1,5 +1,6 @@ using System.ComponentModel.DataAnnotations; using BTCPayServer.Services.Mails; +using BTCPayServer.Validation; namespace BTCPayServer.Models.ServerViewModels { @@ -19,7 +20,7 @@ namespace BTCPayServer.Models.ServerViewModels get; set; } public bool PasswordSet { get; set; } - [EmailAddress] + [MailboxAddressAttribute] [Display(Name = "Test Email")] public string TestEmail { diff --git a/BTCPayServer/Plugins/PayButton/Models/PayButtonViewModel.cs b/BTCPayServer/Plugins/PayButton/Models/PayButtonViewModel.cs index 380dd39f3..a8a4d74ac 100644 --- a/BTCPayServer/Plugins/PayButton/Models/PayButtonViewModel.cs +++ b/BTCPayServer/Plugins/PayButton/Models/PayButtonViewModel.cs @@ -3,6 +3,7 @@ using System.ComponentModel.DataAnnotations; using BTCPayServer.ModelBinders; using BTCPayServer.Models.AppViewModels; using BTCPayServer.Models.StoreViewModels; +using BTCPayServer.Validation; using Microsoft.AspNetCore.Mvc; namespace BTCPayServer.Plugins.PayButton.Models @@ -34,7 +35,7 @@ namespace BTCPayServer.Plugins.PayButton.Models public string ServerIpn { get; set; } [Url] public string BrowserRedirect { get; set; } - [EmailAddress] + [MailboxAddress] public string NotifyEmail { get; set; } public string StoreId { get; set; } diff --git a/BTCPayServer/Services/Mails/EmailSettings.cs b/BTCPayServer/Services/Mails/EmailSettings.cs index 34c70cfa3..d24fee3d4 100644 --- a/BTCPayServer/Services/Mails/EmailSettings.cs +++ b/BTCPayServer/Services/Mails/EmailSettings.cs @@ -1,7 +1,10 @@ +using System.ComponentModel.DataAnnotations; using System.Threading; using System.Threading.Tasks; using BTCPayServer.Client.Models; +using BTCPayServer.Validation; using MailKit.Net.Smtp; +using Microsoft.AspNetCore.Mvc.ModelBinding; using MimeKit; namespace BTCPayServer.Services.Mails @@ -10,7 +13,29 @@ namespace BTCPayServer.Services.Mails { 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) => @@ -30,12 +55,11 @@ namespace BTCPayServer.Services.Mails var mm = new MimeMessage(); mm.Body = bodyBuilder.ToMessageBody(); 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.Cc.AddRange(cc?? System.Array.Empty()); - mm.Bcc.AddRange(bcc?? System.Array.Empty()); + mm.Cc.AddRange(cc ?? System.Array.Empty()); + mm.Bcc.AddRange(bcc ?? System.Array.Empty()); return mm; - } public async Task CreateSmtpClient() diff --git a/BTCPayServer/Validation/EmailValidator.cs b/BTCPayServer/Validation/EmailValidator.cs deleted file mode 100644 index 7f51a500f..000000000 --- a/BTCPayServer/Validation/EmailValidator.cs +++ /dev/null @@ -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); - } - } -} diff --git a/BTCPayServer/Validation/MailboxAddressAttribute.cs b/BTCPayServer/Validation/MailboxAddressAttribute.cs new file mode 100644 index 000000000..1bca78e28 --- /dev/null +++ b/BTCPayServer/Validation/MailboxAddressAttribute.cs @@ -0,0 +1,25 @@ +using System.ComponentModel.DataAnnotations; + +namespace BTCPayServer.Validation +{ + /// + /// Validate address in the format "Firstname Lastname " See rfc822 + /// + 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 '"; + 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); + } + } +} diff --git a/BTCPayServer/Validation/MailboxAddressValidator.cs b/BTCPayServer/Validation/MailboxAddressValidator.cs new file mode 100644 index 000000000..4424be4d6 --- /dev/null +++ b/BTCPayServer/Validation/MailboxAddressValidator.cs @@ -0,0 +1,39 @@ +#nullable enable +using System; +using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; +using System.Text.RegularExpressions; +using MimeKit; + +namespace BTCPayServer +{ + /// + /// Validate address in the format "Firstname Lastname " See rfc822 + /// + 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; + } + } +} diff --git a/BTCPayServer/Views/Shared/EmailsBody.cshtml b/BTCPayServer/Views/Shared/EmailsBody.cshtml index 12220344a..5008b1c6e 100644 --- a/BTCPayServer/Views/Shared/EmailsBody.cshtml +++ b/BTCPayServer/Views/Shared/EmailsBody.cshtml @@ -39,17 +39,9 @@ -
- - - - Some email providers (like Gmail) don't allow you to set your display name, so this setting may not have any effect. - - -
- +
@@ -89,7 +81,7 @@ To test your settings, enter an email address below.

- +
diff --git a/BTCPayServer/Views/UIPaymentRequest/EditPaymentRequest.cshtml b/BTCPayServer/Views/UIPaymentRequest/EditPaymentRequest.cshtml index eb2824089..445f6697f 100644 --- a/BTCPayServer/Views/UIPaymentRequest/EditPaymentRequest.cshtml +++ b/BTCPayServer/Views/UIPaymentRequest/EditPaymentRequest.cshtml @@ -73,7 +73,7 @@
- +

Receive updates for this payment request.