feat: notification digests via SMTP
Adds a periodic inbox-summary email per opted-in user. SMTP is configured via appsettings (Smtp section); the digest is built from existing analytics (health score, top senders, recommendations) and sent by a new hourly DigestWorker, mirroring the GmailSyncWorker pattern. Frequency and send hour are configurable (Digest section). Backend: IEmailSender (SmtpEmailSender, MailKit), IDigestService, DigestWorker, new User.DigestEnabled/LastDigestSentUtc fields + migration, SettingsController for the per-user toggle and a send-now test endpoint, all scoped to the authenticated UserId. Frontend: SettingsApi + a bell/no-bell toggle button in the topbar. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
using InboxIntel.Application.Abstractions;
|
||||
using InboxIntel.Infrastructure.Configuration;
|
||||
using MailKit.Net.Smtp;
|
||||
using MailKit.Security;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using MimeKit;
|
||||
|
||||
namespace InboxIntel.Infrastructure.Notifications;
|
||||
|
||||
public class SmtpEmailSender : IEmailSender
|
||||
{
|
||||
private readonly SmtpOptions _options;
|
||||
private readonly ILogger<SmtpEmailSender> _logger;
|
||||
|
||||
public SmtpEmailSender(IOptions<SmtpOptions> options, ILogger<SmtpEmailSender> logger)
|
||||
{
|
||||
_options = options.Value;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public bool IsEnabled => _options.Enabled && !string.IsNullOrWhiteSpace(_options.Host);
|
||||
|
||||
public async Task SendAsync(string toAddress, string subject, string htmlBody, CancellationToken ct = default)
|
||||
{
|
||||
if (!IsEnabled)
|
||||
{
|
||||
_logger.LogInformation("SMTP not configured; skipping email \"{Subject}\" to {To}", subject, toAddress);
|
||||
return;
|
||||
}
|
||||
|
||||
var message = new MimeMessage();
|
||||
message.From.Add(new MailboxAddress(_options.FromName, _options.FromAddress));
|
||||
message.To.Add(MailboxAddress.Parse(toAddress));
|
||||
message.Subject = subject;
|
||||
message.Body = new BodyBuilder { HtmlBody = htmlBody }.ToMessageBody();
|
||||
|
||||
using var client = new SmtpClient();
|
||||
var socketOptions = _options.UseSsl ? SecureSocketOptions.StartTls : SecureSocketOptions.None;
|
||||
await client.ConnectAsync(_options.Host, _options.Port, socketOptions, ct);
|
||||
if (!string.IsNullOrEmpty(_options.User))
|
||||
await client.AuthenticateAsync(_options.User, _options.Password, ct);
|
||||
await client.SendAsync(message, ct);
|
||||
await client.DisconnectAsync(true, ct);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user