Implemented multiple email providers

This commit is contained in:
Mia Rose Winter 2024-02-18 15:06:31 +01:00
parent b86d6968d6
commit 54af1c88b6
Signed by: miawinter
GPG key ID: 4B6F6A83178F595E
7 changed files with 68 additions and 29 deletions

View file

@ -0,0 +1,9 @@
namespace Wave.Data;
public class EmailConfiguration {
public required string SenderEmail { get; init; }
public required string SenderName { get; init; }
public required string? ServiceEmail { get; init; }
public Dictionary<string, SmtpConfiguration> Smtp { get; init; } = new(StringComparer.InvariantCultureIgnoreCase);
}

View file

@ -5,8 +5,5 @@ public class SmtpConfiguration {
public required int Port { get; init; } public required int Port { get; init; }
public required string Username { get; init; } public required string Username { get; init; }
public required string Password { get; init; } public required string Password { get; init; }
public required string SenderEmail { get; init; }
public required string SenderName { get; init; }
public required string ServiceEmail { get; init; }
public bool Ssl { get; init; } = true; public bool Ssl { get; init; } = true;
} }

View file

@ -121,16 +121,40 @@
builder.Services.AddCascadingValue("TitlePrefix", builder.Services.AddCascadingValue("TitlePrefix",
sf => (sf.GetService<IOptions<Customization>>()?.Value.AppName ?? "Wave") + " - "); sf => (sf.GetService<IOptions<Customization>>()?.Value.AppName ?? "Wave") + " - ");
var smtpConfig = builder.Configuration.GetSection("Email:Smtp"); var emailConfig = builder.Configuration.GetSection("Email").Get<EmailConfiguration>();
if (smtpConfig.Exists()) { builder.Services.Configure<EmailConfiguration>(builder.Configuration.GetSection("Email"));
builder.Services.Configure<SmtpConfiguration>(smtpConfig); if (emailConfig?.Smtp.Count > 0) {
builder.Services.AddKeyedScoped<IEmailService, LiveEmailService>("live"); if (string.IsNullOrWhiteSpace(emailConfig.SenderEmail)) {
throw new ApplicationException(
"Email providers have been configured, but no SenderEmail. " +
"Please provider the sender email address used for email distribution.");
}
foreach (var smtp in emailConfig.Smtp) {
builder.Services.AddKeyedScoped<IEmailService, LiveEmailService>(smtp.Key.ToLower(), (provider, key) =>
ActivatorUtilities.CreateInstance<LiveEmailService>(provider,
provider.GetRequiredService<IOptions<EmailConfiguration>>().Value.Smtp[(string)key]));
}
if (emailConfig.Smtp.Keys.Any(k => k.Equals("live", StringComparison.CurrentCultureIgnoreCase))) {
builder.Services.AddScoped<IEmailSender, SmtpEmailSender>(); builder.Services.AddScoped<IEmailSender, SmtpEmailSender>();
builder.Services.AddScoped<IAdvancedEmailSender, SmtpEmailSender>(); builder.Services.AddScoped<IAdvancedEmailSender, SmtpEmailSender>();
builder.Services.AddScoped<IEmailSender<ApplicationUser>, SmtpEmailSender>(); builder.Services.AddScoped<IEmailSender<ApplicationUser>, SmtpEmailSender>();
} else {
builder.Services.AddSingleton<IEmailSender<ApplicationUser>, IdentityNoOpEmailSender>();
logMessages.Add("No 'live' email provider configured.");
}
if (emailConfig.Smtp.Keys.Any(k => k.Equals("bulk", StringComparison.CurrentCultureIgnoreCase))) {
builder.Services.AddScoped<NewsletterBackgroundService>();
} else if (builder.Configuration.GetSection(nameof(Features)).Get<Features>()?.EmailSubscriptions is not true) {
throw new ApplicationException(
"Email subscriptions have been enabled, but no 'bulk' email provider was configured. " +
"Disable email subscriptions or provide the mail provider for bulk sending");
}
} else { } else {
builder.Services.AddSingleton<IEmailSender<ApplicationUser>, IdentityNoOpEmailSender>(); builder.Services.AddSingleton<IEmailSender<ApplicationUser>, IdentityNoOpEmailSender>();
logMessages.Add("No Email provider configured."); logMessages.Add("No email provider configured.");
} }
builder.Services.AddScoped<EmailFactory>(); builder.Services.AddScoped<EmailFactory>();

View file

@ -1,8 +1,8 @@
namespace Wave.Services; namespace Wave.Services;
public interface IEmailService : IAsyncDisposable { public interface IEmailService : IAsyncDisposable {
ValueTask Connect(CancellationToken cancellation); ValueTask ConnectAsync(CancellationToken cancellation);
ValueTask Disconnect(CancellationToken cancellation); ValueTask DisconnectAsync(CancellationToken cancellation);
ValueTask SendEmailAsync(IEmail email); ValueTask SendEmailAsync(IEmail email);
} }

View file

@ -0,0 +1,5 @@
namespace Wave.Services;
public interface IScopedProcessingService {
ValueTask DoWork(CancellationToken cancellationToken);
}

View file

@ -6,18 +6,19 @@
namespace Wave.Services; namespace Wave.Services;
public class LiveEmailService(ILogger<LiveEmailService> logger, IOptions<SmtpConfiguration> configuration) : IEmailService { public class LiveEmailService(ILogger<LiveEmailService> logger, IOptions<EmailConfiguration> emailConfiguration, SmtpConfiguration configuration) : IEmailService {
private ILogger<LiveEmailService> Logger { get; } = logger; private ILogger<LiveEmailService> Logger { get; } = logger;
private SmtpConfiguration Configuration { get; } = configuration.Value; private EmailConfiguration EmailConfiguration { get; } = emailConfiguration.Value;
private SmtpConfiguration Configuration { get; } = configuration;
private SmtpClient? Client { get; set; } private SmtpClient? Client { get; set; }
public async ValueTask DisposeAsync() { public async ValueTask DisposeAsync() {
GC.SuppressFinalize(this); GC.SuppressFinalize(this);
await Disconnect(CancellationToken.None); await DisconnectAsync(CancellationToken.None);
} }
public async ValueTask Connect(CancellationToken cancellation) { public async ValueTask ConnectAsync(CancellationToken cancellation) {
if (Client is not null) return; if (Client is not null) return;
try { try {
@ -35,7 +36,7 @@ public class LiveEmailService(ILogger<LiveEmailService> logger, IOptions<SmtpCon
} }
} }
public async ValueTask Disconnect(CancellationToken cancellation) { public async ValueTask DisconnectAsync(CancellationToken cancellation) {
if (Client is null) return; if (Client is null) return;
await Client.DisconnectAsync(true, cancellation); await Client.DisconnectAsync(true, cancellation);
Client.Dispose(); Client.Dispose();
@ -47,7 +48,7 @@ public class LiveEmailService(ILogger<LiveEmailService> logger, IOptions<SmtpCon
if (Client is null) throw new ApplicationException("Not connected."); if (Client is null) throw new ApplicationException("Not connected.");
var message = new MimeMessage { var message = new MimeMessage {
From = { new MailboxAddress(Configuration.SenderName, Configuration.SenderEmail) }, From = { new MailboxAddress(EmailConfiguration.SenderName, EmailConfiguration.SenderEmail) },
To = { new MailboxAddress(email.ReceiverName, email.ReceiverEmail) }, To = { new MailboxAddress(email.ReceiverName, email.ReceiverEmail) },
Subject = email.Subject Subject = email.Subject
}; };
@ -55,7 +56,7 @@ public class LiveEmailService(ILogger<LiveEmailService> logger, IOptions<SmtpCon
message.Body = builder.ToMessageBody(); message.Body = builder.ToMessageBody();
foreach ((string id, string value) in email.Headers) { foreach ((string id, string value) in email.Headers) {
if (id == HeaderId.ListUnsubscribe.ToHeaderName()) { if (id == HeaderId.ListUnsubscribe.ToHeaderName()) {
message.Headers.Add(HeaderId.ListId, $"<mailto:{Configuration.ServiceEmail ?? Configuration.SenderEmail}>"); message.Headers.Add(HeaderId.ListId, $"<mailto:{EmailConfiguration.ServiceEmail ?? EmailConfiguration.SenderEmail}>");
} }
message.Headers.Add(id, value); message.Headers.Add(id, value);
} }

View file

@ -3,9 +3,10 @@
namespace Wave.Services; namespace Wave.Services;
public class SmtpEmailSender(EmailFactory email, [FromKeyedServices("live")]IEmailService emailService) : IEmailSender<ApplicationUser>, IAdvancedEmailSender, IAsyncDisposable { public class SmtpEmailSender(EmailFactory email, [FromKeyedServices("live")]IEmailService emailService, [FromKeyedServices("bulk")]IEmailService bulkEmailService) : IEmailSender<ApplicationUser>, IAdvancedEmailSender, IAsyncDisposable {
private EmailFactory Email { get; } = email; private EmailFactory Email { get; } = email;
private IEmailService EmailService { get; } = emailService; private IEmailService EmailService { get; } = emailService;
private IEmailService BulkEmailService { get; } = bulkEmailService;
#region IEmailSenderAsync<ApplicationUser> #region IEmailSenderAsync<ApplicationUser>
@ -33,33 +34,35 @@ public class SmtpEmailSender(EmailFactory email, [FromKeyedServices("live")]IEma
public async Task SendEmailAsync(string email, string? name, string subject, string htmlMessage) { public async Task SendEmailAsync(string email, string? name, string subject, string htmlMessage) {
await EmailService.Connect(CancellationToken.None); await EmailService.ConnectAsync(CancellationToken.None);
await EmailService.SendEmailAsync(await Email.CreateDefaultEmail(email, name, subject, subject, htmlMessage)); await EmailService.SendEmailAsync(await Email.CreateDefaultEmail(email, name, subject, subject, htmlMessage));
await EmailService.Disconnect(CancellationToken.None); await EmailService.DisconnectAsync(CancellationToken.None);
} }
public async Task SendDefaultMailAsync(string receiverMail, string? receiverName, string subject, string title, string bodyHtml) { public async Task SendDefaultMailAsync(string receiverMail, string? receiverName, string subject, string title, string bodyHtml) {
await EmailService.Connect(CancellationToken.None); await EmailService.ConnectAsync(CancellationToken.None);
var email = await Email.CreateDefaultEmail(receiverMail, receiverName, subject, title, bodyHtml); var email = await Email.CreateDefaultEmail(receiverMail, receiverName, subject, title, bodyHtml);
await EmailService.SendEmailAsync(email); await EmailService.SendEmailAsync(email);
await EmailService.Disconnect(CancellationToken.None); await EmailService.DisconnectAsync(CancellationToken.None);
} }
public async Task SendSubscribedMailAsync(EmailSubscriber subscriber, string subject, string title, string bodyHtml, public async Task SendSubscribedMailAsync(EmailSubscriber subscriber, string subject, string title, string bodyHtml,
string browserUrl = "", string subscribedRole = "-1") { string browserUrl = "", string subscribedRole = "-1") {
var email = await Email.CreateSubscribedEmail(subscriber, browserUrl, subject, title, bodyHtml, subscribedRole); var email = await Email.CreateSubscribedEmail(subscriber, browserUrl, subject, title, bodyHtml, subscribedRole);
await EmailService.Connect(CancellationToken.None); await BulkEmailService.ConnectAsync(CancellationToken.None);
await EmailService.SendEmailAsync(email); // TODO use bulk service await BulkEmailService.SendEmailAsync(email);
} }
public async Task SendWelcomeMailAsync(EmailSubscriber subscriber, string subject, string title, string bodyHtml, public async Task SendWelcomeMailAsync(EmailSubscriber subscriber, string subject, string title, string bodyHtml,
IEnumerable<EmailNewsletter> articles) { IEnumerable<EmailNewsletter> articles) {
var email = await Email.CreateWelcomeEmail(subscriber, articles, subject, title, bodyHtml); var email = await Email.CreateWelcomeEmail(subscriber, articles, subject, title, bodyHtml);
await EmailService.Connect(CancellationToken.None); await BulkEmailService.ConnectAsync(CancellationToken.None);
await EmailService.SendEmailAsync(email); // TODO use bulk service await BulkEmailService.SendEmailAsync(email);
} }
public async ValueTask DisposeAsync() { public async ValueTask DisposeAsync() {
GC.SuppressFinalize(this);
await EmailService.DisposeAsync(); await EmailService.DisposeAsync();
await BulkEmailService.DisposeAsync();
} }
} }