276 lines
9.2 KiB
C#
276 lines
9.2 KiB
C#
using System.Diagnostics.CodeAnalysis;
|
|
using System.Text.RegularExpressions;
|
|
using MailKit.Net.Smtp;
|
|
using MailKit.Security;
|
|
using MimeKit;
|
|
using Newtonsoft.Json;
|
|
using Npgsql;
|
|
using PluginBuilder.Controllers.Logic;
|
|
using PluginBuilder.Util.Extensions;
|
|
using PluginBuilder.ViewModels.Admin;
|
|
|
|
namespace PluginBuilder.Services;
|
|
|
|
public class EmailService(
|
|
DBConnectionFactory connectionFactory,
|
|
AdminSettingsCache adminSettingsCache,
|
|
ILogger<EmailService> logger)
|
|
{
|
|
public const string PluginApprovedTemplate = @"
|
|
Hello Plugin Owner,
|
|
|
|
Your plugin ""{0}"" has been approved and is now listed publicly.
|
|
|
|
You can view it here:
|
|
{1}
|
|
|
|
Thank you for contributing to the BTCPay Server ecosystem!
|
|
BTCPay Server Plugin Builder Team
|
|
";
|
|
|
|
public const string PluginRejectedTemplate = @"
|
|
Hello Plugin Owner,
|
|
|
|
Your plugin ""{0}"" was reviewed, but unfortunately it has not been approved.
|
|
|
|
Reason:
|
|
{1}
|
|
|
|
You may update your submission and request a another review at any time.
|
|
|
|
Thank you,
|
|
BTCPay Server Plugin Builder Team
|
|
";
|
|
|
|
public Task<List<string>> SendEmail(string toCsvList, string subject, string messageText)
|
|
{
|
|
List<InternetAddress> toList = toCsvList.Split([","], StringSplitOptions.RemoveEmptyEntries)
|
|
.Select(InternetAddress.Parse)
|
|
.ToList();
|
|
return DeliverEmail(toList, subject, messageText);
|
|
}
|
|
|
|
protected virtual async Task<List<string>> DeliverEmail(IEnumerable<InternetAddress> toList, string subject, string messageText)
|
|
{
|
|
List<string> recipients = new();
|
|
var emailSettings = await GetEmailSettingsFromDb();
|
|
if (emailSettings == null)
|
|
throw new InvalidOperationException("Email settings not configured. Please set up email settings in the admin panel.");
|
|
|
|
var smtpClient = await CreateSmtpClient(emailSettings);
|
|
MimeMessage message = new();
|
|
message.From.Add(MailboxAddress.Parse(emailSettings.From));
|
|
message.Subject = subject;
|
|
message.Body = new TextPart("plain") { Text = messageText };
|
|
foreach (var email in toList)
|
|
{
|
|
message.To.Clear();
|
|
message.To.Add(email);
|
|
await smtpClient.SendAsync(message);
|
|
recipients.Add(email.ToString());
|
|
}
|
|
|
|
await smtpClient.DisconnectAsync(true);
|
|
return recipients;
|
|
}
|
|
|
|
public bool IsValidEmailList(string to)
|
|
{
|
|
return to.Split(',').Select(email => email.Trim())
|
|
.All(email => !string.IsNullOrWhiteSpace(email) && Regex.IsMatch(email, @"^[^@\s]+@[^@\s]+\.[^@\s]+$"));
|
|
}
|
|
|
|
public Task SendVerifyEmail(string toEmail, string verifyUrl)
|
|
{
|
|
var body = $"Please verify your account by visiting: {verifyUrl}";
|
|
|
|
return SendEmail(toEmail, "Verify your account on BTCPay Server Plugin Builder", body);
|
|
}
|
|
|
|
public async Task NotifyAdminOnNewRequestListing(NpgsqlConnection conn, PluginSlug pluginSlug, string pluginPublicUrl, string listingPageUrl)
|
|
{
|
|
var notificationSettingEmails = await conn.GetFirstPluginBuildReviewersSetting();
|
|
if (string.IsNullOrEmpty(notificationSettingEmails))
|
|
return;
|
|
|
|
var toList = notificationSettingEmails.Split(",", StringSplitOptions.RemoveEmptyEntries).Select(MailboxAddressValidator.Parse);
|
|
var body = $@"
|
|
Hello Admin,
|
|
|
|
A new plugin has just been published on the BTCPay Server Plugin Builder and is requesting lisitng to public page.
|
|
|
|
Plugin URL: {pluginPublicUrl}
|
|
|
|
Listing Page: {listingPageUrl}
|
|
|
|
Please review and list the plugin details as soon as possible.
|
|
|
|
Thank you,
|
|
BTCPay Server Plugin Builder";
|
|
try
|
|
{
|
|
await DeliverEmail(toList, "New Plugin Request Listing on BTCPay Server Plugin Builder", body);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
logger.LogWarning(ex, "Failed to notify admin about new plugin listing request for {PluginSlug}", pluginSlug);
|
|
}
|
|
}
|
|
|
|
public async Task ResetPasswordEmail(string email, string passwordResetUrl)
|
|
{
|
|
var recipient = MailboxAddressValidator.Parse(email);
|
|
var body = $@"
|
|
Hello {email},
|
|
|
|
We received a request to reset your password for your account. If you made this request, click the link below to reset your password:
|
|
|
|
{passwordResetUrl}
|
|
|
|
If you didn't request a password reset, you can safely ignore this email. Your password will remain unchanged.
|
|
|
|
Thank you,
|
|
BTCPay Server Plugin Builder";
|
|
try
|
|
{
|
|
await DeliverEmail(new[] { recipient }, "Reset your password on BTCPay Server Plugin Builder", body);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
logger.LogError(ex, "Error sending password reset email to {Email}", email);
|
|
}
|
|
}
|
|
|
|
public async Task NotifyPluginOwnerForRequestListingStatus(string email, string pluginTitle, bool isApproved, string reviewUrlOrReason)
|
|
{
|
|
string subject;
|
|
string body;
|
|
|
|
if (isApproved)
|
|
{
|
|
subject = $"{pluginTitle} Approved!";
|
|
body = string.Format(PluginApprovedTemplate, pluginTitle, reviewUrlOrReason);
|
|
}
|
|
else
|
|
{
|
|
subject = $"{pluginTitle} Not Approved";
|
|
body = string.Format(PluginRejectedTemplate, pluginTitle, reviewUrlOrReason);
|
|
}
|
|
|
|
try
|
|
{
|
|
await DeliverEmail(new[] { MailboxAddressValidator.Parse(email) }, subject, body);
|
|
}
|
|
catch (InvalidOperationException)
|
|
{
|
|
logger.LogInformation("Email settings not configured. Plugin owner {Email} will not receive {EmailSubject} email", email, subject);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
logger.LogError(ex, "ERROR prevent us sending email notification to {Email} for {EmailSubject} email", email, subject);
|
|
}
|
|
}
|
|
|
|
public async Task<EmailSettingsViewModel?> GetEmailSettingsFromDb()
|
|
{
|
|
await using var conn = await connectionFactory.Open();
|
|
var jsonEmail = await conn.SettingsGetAsync("EmailSettings");
|
|
var emailSettings = string.IsNullOrEmpty(jsonEmail)
|
|
? null
|
|
: JsonConvert.DeserializeObject<EmailSettingsViewModel>(jsonEmail);
|
|
return emailSettings;
|
|
}
|
|
|
|
public async Task SaveEmailSettingsToDatabase(EmailSettingsViewModel model)
|
|
{
|
|
await using var conn = await connectionFactory.Open();
|
|
var emailSettingsJson = JsonConvert.SerializeObject(model);
|
|
await conn.SettingsSetAsync("EmailSettings", emailSettingsJson);
|
|
await adminSettingsCache.RefreshAllVerifiedEmailSettings(conn);
|
|
}
|
|
|
|
public async Task<SmtpClient> CreateSmtpClient(EmailSettingsViewModel settings)
|
|
{
|
|
SmtpClient client = new();
|
|
using CancellationTokenSource connectCancel = new(10000);
|
|
try
|
|
{
|
|
if (settings.DisableCertificateCheck)
|
|
{
|
|
client.CheckCertificateRevocation = false;
|
|
#pragma warning disable CA5359 // Do Not Disable Certificate Validation
|
|
client.ServerCertificateValidationCallback = (s, c, h, e) => true;
|
|
#pragma warning restore CA5359 // Do Not Disable Certificate Validation
|
|
}
|
|
|
|
await client.ConnectAsync(settings.Server, settings.Port, SecureSocketOptions.Auto,
|
|
connectCancel.Token);
|
|
if ((client.Capabilities & SmtpCapabilities.Authentication) != 0)
|
|
await client.AuthenticateAsync(settings.Username ?? string.Empty, settings.Password ?? string.Empty,
|
|
connectCancel.Token);
|
|
}
|
|
catch
|
|
{
|
|
client.Dispose();
|
|
throw;
|
|
}
|
|
|
|
return client;
|
|
}
|
|
}
|
|
|
|
// public MimeMessage CreateMailMessage(MailboxAddress to, string subject, string message, bool isHtml) =>
|
|
// CreateMailMessage(new[] { to }, null, null, subject, message, isHtml);
|
|
// public MimeMessage CreateMailMessage(MailboxAddress[] to, MailboxAddress[] cc, MailboxAddress[] bcc, string subject, string message, bool isHtml)
|
|
// {
|
|
// var bodyBuilder = new BodyBuilder();
|
|
// if (isHtml)
|
|
// {
|
|
// bodyBuilder.HtmlBody = message;
|
|
// }
|
|
// else
|
|
// {
|
|
// bodyBuilder.TextBody = message;
|
|
// }
|
|
//
|
|
// var mm = new MimeMessage();
|
|
// mm.Body = bodyBuilder.ToMessageBody();
|
|
// mm.Subject = subject;
|
|
// mm.From.Add(MailboxAddressValidator.Parse(Settings.From));
|
|
// mm.To.AddRange(to);
|
|
// mm.Cc.AddRange(cc ?? System.Array.Empty<InternetAddress>());
|
|
// mm.Bcc.AddRange(bcc ?? System.Array.Empty<InternetAddress>());
|
|
// return mm;
|
|
// }
|
|
|
|
public static class MailboxAddressValidator
|
|
{
|
|
private static readonly 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;
|
|
}
|
|
}
|