2020-06-28 21:44:35 -05:00
using System ;
2022-06-22 05:05:32 +02:00
using System.Collections.Generic ;
using System.ComponentModel.DataAnnotations ;
2022-06-23 13:41:52 +09:00
using System.Globalization ;
2023-12-01 10:50:05 +01:00
using System.Linq ;
2019-01-06 15:53:37 +01:00
using System.Threading.Tasks ;
2022-02-21 15:46:43 +01:00
using BTCPayServer.Abstractions.Constants ;
2022-06-22 05:05:32 +02:00
using BTCPayServer.Abstractions.Extensions ;
using BTCPayServer.Abstractions.Models ;
2024-03-14 10:25:40 +01:00
using BTCPayServer.Client ;
2019-01-06 15:53:37 +01:00
using BTCPayServer.Data ;
2024-02-21 14:43:44 +01:00
using BTCPayServer.Models ;
2019-01-06 15:53:37 +01:00
using BTCPayServer.Services.Mails ;
2024-03-14 10:25:40 +01:00
using Microsoft.AspNetCore.Authorization ;
2019-01-06 15:53:37 +01:00
using Microsoft.AspNetCore.Mvc ;
2021-12-15 21:30:46 +09:00
using MimeKit ;
2019-01-06 15:53:37 +01:00
2024-04-04 11:00:18 +02:00
namespace BTCPayServer.Controllers ;
public partial class UIStoresController
2019-01-06 15:53:37 +01:00
{
2024-04-04 11:00:18 +02:00
[HttpGet("{storeId}/emails")]
public async Task < IActionResult > StoreEmails ( string storeId )
2019-01-06 15:53:37 +01:00
{
2024-04-04 11:00:18 +02:00
var store = HttpContext . GetStoreData ( ) ;
if ( store = = null )
return NotFound ( ) ;
2023-01-06 14:18:07 +01:00
2024-04-04 11:00:18 +02:00
var blob = store . GetStoreBlob ( ) ;
if ( blob . EmailSettings ? . IsComplete ( ) is not true & & ! TempData . HasStatusMessage ( ) )
{
var emailSender = await _emailSenderFactory . GetEmailSender ( store . Id ) as StoreEmailSender ;
if ( ! await IsSetupComplete ( emailSender ? . FallbackSender ) )
2022-06-22 05:05:32 +02:00
{
2024-04-04 11:00:18 +02:00
TempData . SetStatusMessageModel ( new StatusMessageModel
2022-06-22 05:05:32 +02:00
{
2024-04-04 11:00:18 +02:00
Severity = StatusMessageModel . StatusSeverity . Warning ,
Html = $"You need to configure email settings before this feature works. <a class='alert-link' href='{Url.Action(" StoreEmailSettings ", new { storeId })}'>Configure store email settings</a>."
} ) ;
2022-06-22 05:05:32 +02:00
}
2024-04-04 11:00:18 +02:00
}
2022-06-22 05:05:32 +02:00
2024-04-04 11:00:18 +02:00
var vm = new StoreEmailRuleViewModel { Rules = blob . EmailRules ? ? [ ] } ;
return View ( vm ) ;
}
[HttpPost("{storeId}/emails")]
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
public async Task < IActionResult > StoreEmails ( string storeId , StoreEmailRuleViewModel vm , string command )
{
vm . Rules ? ? = [ ] ;
int commandIndex = 0 ;
var indSep = command . Split ( ':' , StringSplitOptions . RemoveEmptyEntries ) ;
if ( indSep . Length > 1 )
{
commandIndex = int . Parse ( indSep [ 1 ] , CultureInfo . InvariantCulture ) ;
2022-06-22 05:05:32 +02:00
}
2020-06-28 17:55:27 +09:00
2024-04-04 11:00:18 +02:00
if ( command . StartsWith ( "remove" , StringComparison . InvariantCultureIgnoreCase ) )
2022-06-22 05:05:32 +02:00
{
2024-04-04 11:00:18 +02:00
vm . Rules . RemoveAt ( commandIndex ) ;
}
else if ( command = = "add" )
{
vm . Rules . Add ( new StoreEmailRule ( ) ) ;
2023-04-04 03:52:42 +02:00
2024-04-04 11:00:18 +02:00
return View ( vm ) ;
}
2023-01-06 14:18:07 +01:00
2024-04-04 11:00:18 +02:00
for ( var i = 0 ; i < vm . Rules . Count ; i + + )
{
var rule = vm . Rules [ i ] ;
2023-09-28 08:36:12 +02:00
2024-04-04 11:00:18 +02:00
if ( ! string . IsNullOrEmpty ( rule . To ) & & rule . To . Split ( ',' , StringSplitOptions . RemoveEmptyEntries )
. Any ( s = > ! MailboxAddressValidator . TryParse ( s , out _ ) ) )
2023-09-28 08:36:12 +02:00
{
2024-04-04 11:00:18 +02:00
ModelState . AddModelError ( $"{nameof(vm.Rules)}[{i}].{nameof(rule.To)}" ,
"Invalid mailbox address provided. Valid formats are: 'test@example.com' or 'Firstname Lastname <test@example.com>'" ) ;
2023-09-28 08:36:12 +02:00
}
2024-04-04 11:00:18 +02:00
else if ( ! rule . CustomerEmail & & string . IsNullOrEmpty ( rule . To ) )
ModelState . AddModelError ( $"{nameof(vm.Rules)}[{i}].{nameof(rule.To)}" ,
"Either recipient or \"Send the email to the buyer\" is required" ) ;
}
2023-12-08 11:33:29 +01:00
2024-04-04 11:00:18 +02:00
if ( ! ModelState . IsValid )
{
return View ( vm ) ;
}
2023-01-06 14:18:07 +01:00
2024-04-04 11:00:18 +02:00
var store = HttpContext . GetStoreData ( ) ;
2023-04-04 03:52:42 +02:00
2024-04-04 11:00:18 +02:00
if ( store = = null )
return NotFound ( ) ;
2023-12-01 10:50:05 +01:00
2024-04-04 11:00:18 +02:00
string message = "" ;
2023-12-01 10:50:05 +01:00
2024-04-04 11:00:18 +02:00
// update rules
var blob = store . GetStoreBlob ( ) ;
blob . EmailRules = vm . Rules ;
if ( store . SetStoreBlob ( blob ) )
{
await _storeRepo . UpdateStore ( store ) ;
message + = "Store email rules saved. " ;
}
2023-04-04 03:52:42 +02:00
2024-04-04 11:00:18 +02:00
if ( command . StartsWith ( "test" , StringComparison . InvariantCultureIgnoreCase ) )
{
try
2023-04-04 03:52:42 +02:00
{
2024-04-04 11:00:18 +02:00
var rule = vm . Rules [ commandIndex ] ;
var emailSender = await _emailSenderFactory . GetEmailSender ( store . Id ) ;
if ( await IsSetupComplete ( emailSender ) )
2023-04-04 03:52:42 +02:00
{
2024-04-04 11:00:18 +02:00
var recipients = rule . To . Split ( "," , StringSplitOptions . RemoveEmptyEntries )
. Select ( o = >
{
MailboxAddressValidator . TryParse ( o , out var mb ) ;
return mb ;
} )
. Where ( o = > o ! = null )
. ToArray ( ) ;
2024-01-15 13:30:10 +01:00
2024-04-04 11:00:18 +02:00
emailSender . SendEmail ( recipients . ToArray ( ) , null , null , $"[TEST] {rule.Subject}" , rule . Body ) ;
message + = "Test email sent — please verify you received it." ;
2023-04-04 03:52:42 +02:00
}
2024-04-04 11:00:18 +02:00
else
2023-04-04 03:52:42 +02:00
{
2024-04-04 11:00:18 +02:00
message + = "Complete the email setup to send test emails." ;
2023-04-04 03:52:42 +02:00
}
}
2024-04-04 11:00:18 +02:00
catch ( Exception ex )
2022-06-22 05:05:32 +02:00
{
2024-04-04 11:00:18 +02:00
TempData [ WellKnownTempData . ErrorMessage ] = message + "Error sending test email: " + ex . Message ;
return RedirectToAction ( "StoreEmails" , new { storeId } ) ;
2023-04-04 03:52:42 +02:00
}
2022-06-22 05:05:32 +02:00
}
2024-04-04 11:00:18 +02:00
if ( ! string . IsNullOrEmpty ( message ) )
2022-06-22 05:05:32 +02:00
{
2024-04-04 11:00:18 +02:00
TempData . SetStatusMessageModel ( new StatusMessageModel
{
Severity = StatusMessageModel . StatusSeverity . Success ,
Message = message
} ) ;
2022-06-22 05:05:32 +02:00
}
2023-01-06 14:18:07 +01:00
2024-04-04 11:00:18 +02:00
return RedirectToAction ( "StoreEmails" , new { storeId } ) ;
}
public class StoreEmailRuleViewModel
{
public List < StoreEmailRule > Rules { get ; set ; }
}
public class StoreEmailRule
{
[Required]
public string Trigger { get ; set ; }
2023-08-07 10:10:48 +03:00
2024-04-04 11:00:18 +02:00
public bool CustomerEmail { get ; set ; }
2023-08-07 10:10:48 +03:00
2023-12-08 11:33:29 +01:00
2024-04-04 11:00:18 +02:00
public string To { get ; set ; }
2023-08-07 10:10:48 +03:00
2024-04-04 11:00:18 +02:00
[Required]
public string Subject { get ; set ; }
2023-08-07 10:10:48 +03:00
2024-04-04 11:00:18 +02:00
[Required]
public string Body { get ; set ; }
}
2023-01-06 14:18:07 +01:00
2024-04-04 11:00:18 +02:00
[HttpGet("{storeId}/email-settings")]
public async Task < IActionResult > StoreEmailSettings ( string storeId )
{
var store = HttpContext . GetStoreData ( ) ;
if ( store = = null )
return NotFound ( ) ;
2024-01-26 10:28:50 +01:00
2024-04-04 11:00:18 +02:00
var blob = store . GetStoreBlob ( ) ;
var data = blob . EmailSettings ? ? new EmailSettings ( ) ;
var fallbackSettings = await _emailSenderFactory . GetEmailSender ( store . Id ) is StoreEmailSender { FallbackSender : not null } storeSender
? await storeSender . FallbackSender . GetEmailSettings ( )
: null ;
var vm = new EmailsViewModel ( data , fallbackSettings ) ;
2024-01-26 10:28:50 +01:00
2024-04-04 11:00:18 +02:00
return View ( vm ) ;
}
2020-06-28 17:55:27 +09:00
2024-04-04 11:00:18 +02:00
[HttpPost("{storeId}/email-settings")]
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
public async Task < IActionResult > StoreEmailSettings ( string storeId , EmailsViewModel model , string command , [ FromForm ] bool useCustomSMTP = false )
{
var store = HttpContext . GetStoreData ( ) ;
if ( store = = null )
return NotFound ( ) ;
2024-03-19 14:58:33 +01:00
2024-04-04 11:00:18 +02:00
ViewBag . UseCustomSMTP = useCustomSMTP ;
model . FallbackSettings = await _emailSenderFactory . GetEmailSender ( store . Id ) is StoreEmailSender { FallbackSender : not null } storeSender
? await storeSender . FallbackSender . GetEmailSettings ( )
: null ;
if ( useCustomSMTP )
{
model . Settings . Validate ( "Settings." , ModelState ) ;
}
if ( command = = "Test" )
{
try
2019-01-06 15:53:37 +01:00
{
2024-04-04 11:00:18 +02:00
if ( useCustomSMTP )
2019-01-06 15:53:37 +01:00
{
2024-04-04 11:00:18 +02:00
if ( model . PasswordSet )
2020-10-05 15:42:19 +09:00
{
2024-04-04 11:00:18 +02:00
model . Settings . Password = store . GetStoreBlob ( ) . EmailSettings . Password ;
2020-10-05 15:42:19 +09:00
}
2019-01-06 15:53:37 +01:00
}
2024-04-04 11:00:18 +02:00
if ( string . IsNullOrEmpty ( model . TestEmail ) )
ModelState . AddModelError ( nameof ( model . TestEmail ) , new RequiredAttribute ( ) . FormatErrorMessage ( nameof ( model . TestEmail ) ) ) ;
if ( ! ModelState . IsValid )
return View ( model ) ;
var settings = useCustomSMTP ? model . Settings : model . FallbackSettings ;
using var client = await settings . CreateSmtpClient ( ) ;
var message = settings . CreateMailMessage ( MailboxAddress . Parse ( model . TestEmail ) , $"{store.StoreName}: Email test" , "You received it, the BTCPay Server SMTP settings work." , false ) ;
await client . SendAsync ( message ) ;
await client . DisconnectAsync ( true ) ;
TempData [ WellKnownTempData . SuccessMessage ] = $"Email sent to {model.TestEmail}. Please verify you received it." ;
2019-01-06 15:53:37 +01:00
}
2024-04-04 11:00:18 +02:00
catch ( Exception ex )
2020-10-05 15:42:19 +09:00
{
2024-04-04 11:00:18 +02:00
TempData [ WellKnownTempData . ErrorMessage ] = "Error: " + ex . Message ;
2020-10-05 15:42:19 +09:00
}
2024-04-04 11:00:18 +02:00
return View ( model ) ;
}
if ( command = = "ResetPassword" )
{
var storeBlob = store . GetStoreBlob ( ) ;
storeBlob . EmailSettings . Password = null ;
store . SetStoreBlob ( storeBlob ) ;
await _storeRepo . UpdateStore ( store ) ;
TempData [ WellKnownTempData . SuccessMessage ] = "Email server password reset" ;
}
if ( useCustomSMTP )
{
if ( model . Settings . From is not null & & ! MailboxAddressValidator . IsMailboxAddress ( model . Settings . From ) )
2019-01-06 15:53:37 +01:00
{
2024-04-04 11:00:18 +02:00
ModelState . AddModelError ( "Settings.From" , "Invalid email" ) ;
}
if ( ! ModelState . IsValid )
return View ( model ) ;
var storeBlob = store . GetStoreBlob ( ) ;
if ( storeBlob . EmailSettings ! = null & & new EmailsViewModel ( storeBlob . EmailSettings , model . FallbackSettings ) . PasswordSet )
{
model . Settings . Password = storeBlob . EmailSettings . Password ;
2019-01-06 15:53:37 +01:00
}
2024-04-04 11:00:18 +02:00
storeBlob . EmailSettings = model . Settings ;
store . SetStoreBlob ( storeBlob ) ;
await _storeRepo . UpdateStore ( store ) ;
TempData [ WellKnownTempData . SuccessMessage ] = "Email settings modified" ;
2019-01-06 15:53:37 +01:00
}
2024-04-04 11:00:18 +02:00
return RedirectToAction ( nameof ( StoreEmailSettings ) , new { storeId } ) ;
}
2023-12-01 10:50:05 +01:00
2024-04-04 11:00:18 +02:00
private static async Task < bool > IsSetupComplete ( IEmailSender emailSender )
{
return emailSender is not null & & ( await emailSender . GetEmailSettings ( ) ) ? . IsComplete ( ) = = true ;
2019-01-06 15:53:37 +01:00
}
}