Changed Background mail worker to use new email api
This commit is contained in:
parent
37ffb49ca1
commit
b86d6968d6
|
@ -1,133 +1,40 @@
|
|||
using System.Net;
|
||||
using MailKit.Net.Smtp;
|
||||
using MailKit.Security;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Options;
|
||||
using MimeKit;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Wave.Data;
|
||||
using Wave.Utilities;
|
||||
|
||||
namespace Wave.Services;
|
||||
|
||||
public class EmailBackgroundWorker(ILogger<EmailBackgroundWorker> logger, IDbContextFactory<ApplicationDbContext> contextFactory, IOptions<SmtpConfiguration> config, IOptions<Customization> customizations, IOptions<Features> features, EmailTemplateService templateService) : IHostedService, IDisposable {
|
||||
public class EmailBackgroundWorker(ILogger<EmailBackgroundWorker> logger, IOptions<Features> features, EmailTemplateService templateService, IServiceProvider serviceProvider) : BackgroundService {
|
||||
private ILogger<EmailBackgroundWorker> Logger { get; } = logger;
|
||||
private IDbContextFactory<ApplicationDbContext> ContextFactory { get; } = contextFactory;
|
||||
private SmtpConfiguration Configuration { get; } = config.Value;
|
||||
private Customization Customizations { get; } = customizations.Value;
|
||||
private Features Features { get; } = features.Value;
|
||||
private EmailTemplateService TemplateService { get; } = templateService;
|
||||
private IServiceProvider ServiceProvider { get; } = serviceProvider;
|
||||
|
||||
private Timer? Timer { get; set; }
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken) {
|
||||
if (!Features.EmailSubscriptions) return Task.CompletedTask;
|
||||
|
||||
Logger.LogInformation("Background email worker starting.");
|
||||
|
||||
// we want this timer to execute every 15 minutes, at fixed times (:00, :15, :30, :45)
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
int nowMinute = now.Minute;
|
||||
int waitTime = 15 - nowMinute % 15;
|
||||
Logger.LogInformation("First distribution check will be in {waitTime} minutes, at {time}.",
|
||||
waitTime, now.AddMinutes(waitTime).LocalDateTime.ToString("u"));
|
||||
Timer = new Timer(DoWork, null, TimeSpan.FromMinutes(waitTime), TimeSpan.FromMinutes(15));
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken) {
|
||||
if (!Features.EmailSubscriptions) return;
|
||||
|
||||
TemplateService.TryCreateDefaultTemplates();
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
Logger.LogInformation("Background email worker starting.");
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken) {
|
||||
if (!Features.EmailSubscriptions) return Task.CompletedTask;
|
||||
|
||||
Logger.LogInformation("Background email worker stopping.");
|
||||
Timer?.Change(Timeout.Infinite, 0);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
Timer?.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
private void DoWork(object? _) {
|
||||
try {
|
||||
Logger.LogInformation("Checking Articles...");
|
||||
|
||||
using var context = ContextFactory.CreateDbContext();
|
||||
// we want this timer to execute every 15 minutes, at fixed times (:00, :15, :30, :45)
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var newsletters = context.Set<EmailNewsletter>()
|
||||
.Include(n => n.Article.Author)
|
||||
.Include(n => n.Article.Categories)
|
||||
.Where(n => !n.IsSend && n.DistributionDateTime <= now)
|
||||
.ToList();
|
||||
if (newsletters.Count < 1) return;
|
||||
int nowMinute = now.Minute;
|
||||
int waitTime = 15 - nowMinute % 15;
|
||||
// we always want to start a little bit later than :00:00, to make sure we actually distribute the :00:00 newsletters
|
||||
if (now.Second < 3) await Task.Delay(TimeSpan.FromSeconds(3), stoppingToken);
|
||||
Logger.LogInformation("First distribution check will be in {waitTime} minutes, at {time}.",
|
||||
waitTime, now.AddMinutes(waitTime).LocalDateTime.ToString("u"));
|
||||
await Task.Delay(TimeSpan.FromMinutes(waitTime), stoppingToken);
|
||||
|
||||
Logger.LogInformation("Processing {count} Articles...", newsletters.Count);
|
||||
|
||||
var sender = new MailboxAddress(Configuration.SenderName, Configuration.SenderEmail);
|
||||
using var client = new SmtpClient();
|
||||
client.Connect(Configuration.Host, Configuration.Port,
|
||||
Configuration.Ssl ? SecureSocketOptions.SslOnConnect : SecureSocketOptions.None);
|
||||
if (!string.IsNullOrWhiteSpace(Configuration.Username)) {
|
||||
client.Authenticate(Configuration.Username, Configuration.Password);
|
||||
}
|
||||
|
||||
var host = new Uri(Customizations.AppUrl, UriKind.Absolute);
|
||||
foreach (var newsletter in newsletters) {
|
||||
Logger.LogInformation("Processing '{title}'.", newsletter.Article.Title);
|
||||
// set newsletter to send first, so we don't spam people
|
||||
// in case something unforeseen goes wrong
|
||||
newsletter.IsSend = true;
|
||||
context.SaveChanges();
|
||||
|
||||
string articleLink = ArticleUtilities.GenerateArticleLink(newsletter.Article, new Uri(Customizations.AppUrl, UriKind.Absolute));
|
||||
string template = TemplateService.Newsletter(host.AbsoluteUri, articleLink,
|
||||
(!string.IsNullOrWhiteSpace(Customizations.LogoLink) ?
|
||||
new Uri(Customizations.LogoLink) :
|
||||
new Uri(host, "/img/logo.png")).AbsoluteUri, newsletter.Article.Title, newsletter.Article.BodyHtml,
|
||||
"[[<__UNSUBSCRIBE__>]]");
|
||||
|
||||
var message = new MimeMessage {
|
||||
From = { sender },
|
||||
Subject = newsletter.Article.Title
|
||||
};
|
||||
|
||||
EmailSubscriber? last = null;
|
||||
while (context.Set<EmailSubscriber>()
|
||||
.Where(s => (last == null || s.Id > last.Id))
|
||||
.OrderBy(s => s.Id)
|
||||
.Take(50)
|
||||
.ToList() is { Count: > 0 } subscribers) {
|
||||
last = subscribers.Last();
|
||||
|
||||
foreach (var subscriber in subscribers) {
|
||||
(string user, string token) = TemplateService.CreateConfirmTokensAsync(subscriber.Id, "unsubscribe-" + newsletter.Id, TimeSpan.FromDays(30)).ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
string unsubscribeLink = new Uri(host,
|
||||
$"/Email/Unsubscribe?newsletter={newsletter.Id:D}&user={WebUtility.UrlEncode(user)}&token={WebUtility.UrlEncode(token)}").AbsoluteUri;
|
||||
|
||||
var builder = new BodyBuilder {
|
||||
HtmlBody = template.Replace("[[<__UNSUBSCRIBE__>]]", unsubscribeLink)
|
||||
};
|
||||
|
||||
message.To.Clear();
|
||||
// TODO mailto: unsubscribe:
|
||||
// List-Unsubscribe: <mailto: unsubscribe@example.com?subject=unsubscribe>, <http://www.example.com/unsubscribe.html>
|
||||
message.Headers.Add(HeaderId.ListUnsubscribe, $"<{unsubscribeLink}>");
|
||||
message.Headers.Add(HeaderId.ListUnsubscribePost, "One-Click");
|
||||
message.To.Add(new MailboxAddress(subscriber.Name, subscriber.Email));
|
||||
message.Body = builder.ToMessageBody();
|
||||
client.Send(message);
|
||||
}
|
||||
|
||||
Task.Delay(TimeSpan.FromSeconds(10)).Wait();
|
||||
}
|
||||
}
|
||||
|
||||
client.Disconnect(true);
|
||||
Logger.LogInformation("Processing complete.");
|
||||
} catch (Exception ex) {
|
||||
Logger.LogError(ex, "Failed to distribute emails.");
|
||||
using PeriodicTimer timer = new(TimeSpan.FromMinutes(15));
|
||||
do {
|
||||
await using var scope = ServiceProvider.CreateAsyncScope();
|
||||
var service = scope.ServiceProvider.GetRequiredService<NewsletterBackgroundService>();
|
||||
await service.DoWork(stoppingToken);
|
||||
} while (await timer.WaitForNextTickAsync(stoppingToken));
|
||||
} catch (OperationCanceledException) {
|
||||
Logger.LogInformation("Background email worker stopping.");
|
||||
}
|
||||
}
|
||||
}
|
67
Wave/Services/NewsletterBackgroundService.cs
Normal file
67
Wave/Services/NewsletterBackgroundService.cs
Normal file
|
@ -0,0 +1,67 @@
|
|||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Wave.Data;
|
||||
using Wave.Utilities;
|
||||
|
||||
namespace Wave.Services;
|
||||
|
||||
public class NewsletterBackgroundService(ILogger<NewsletterBackgroundService> logger, IDbContextFactory<ApplicationDbContext> contextFactory, IServiceProvider serviceProvider, IOptions<Customization> customizations) : IScopedProcessingService {
|
||||
private ILogger<NewsletterBackgroundService> Logger { get; } = logger;
|
||||
private IDbContextFactory<ApplicationDbContext> ContextFactory { get; } = contextFactory;
|
||||
private IServiceProvider ServiceProvider { get; } = serviceProvider;
|
||||
private Customization Customizations { get; } = customizations.Value;
|
||||
|
||||
public async ValueTask DoWork(CancellationToken cancellationToken) {
|
||||
try {
|
||||
Logger.LogInformation("Checking Articles...");
|
||||
|
||||
await using var context = await ContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var newsletters = context.Set<EmailNewsletter>()
|
||||
.Include(n => n.Article.Author)
|
||||
.Include(n => n.Article.Categories)
|
||||
.Where(n => !n.IsSend && n.DistributionDateTime <= now)
|
||||
.ToList();
|
||||
if (newsletters.Count < 1) return;
|
||||
|
||||
Logger.LogInformation("Processing {count} Articles...", newsletters.Count);
|
||||
|
||||
await using var client = ServiceProvider.GetRequiredKeyedService<IEmailService>("bulk");
|
||||
await client.ConnectAsync(cancellationToken);
|
||||
var factory = ServiceProvider.GetRequiredService<EmailFactory>();
|
||||
|
||||
foreach (var newsletter in newsletters) {
|
||||
if (cancellationToken.IsCancellationRequested) {
|
||||
Logger.LogInformation("Cancellation requested, skipping processing '{title}'.", newsletter.Article.Title);
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.LogInformation("Processing '{title}'.", newsletter.Article.Title);
|
||||
// set newsletter to send first, so we don't spam people
|
||||
// in case something unforeseen goes wrong
|
||||
newsletter.IsSend = true;
|
||||
await context.SaveChangesAsync(cancellationToken);
|
||||
string articleLink = ArticleUtilities.GenerateArticleLink(newsletter.Article, new Uri(Customizations.AppUrl, UriKind.Absolute));
|
||||
|
||||
EmailSubscriber? last = null;
|
||||
while (context.Set<EmailSubscriber>()
|
||||
.Where(s => (last == null || s.Id > last.Id))
|
||||
.OrderBy(s => s.Id)
|
||||
.Take(50)
|
||||
.ToList() is { Count: > 0 } subscribers) {
|
||||
last = subscribers.Last();
|
||||
|
||||
foreach (var subscriber in subscribers) {
|
||||
var email = await factory.CreateSubscribedEmail(subscriber, articleLink, newsletter.Article.Title,
|
||||
newsletter.Article.Title, newsletter.Article.BodyHtml, newsletter.Id.ToString());
|
||||
await client.SendEmailAsync(email);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Logger.LogInformation("Processing complete.");
|
||||
} catch (Exception ex) {
|
||||
Logger.LogError(ex, "Failed to distribute emails.");
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue