btcpayserver-plugin-builder/PluginBuilder/Services/EmailService.cs

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