0163165beb
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>
66 lines
2.5 KiB
C#
66 lines
2.5 KiB
C#
using System.Text;
|
|
using InboxIntel.Application.Abstractions;
|
|
using InboxIntel.Infrastructure.Persistence;
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
namespace InboxIntel.Infrastructure.Notifications;
|
|
|
|
/// <summary>Builds an HTML inbox-summary email and sends it via <see cref="IEmailSender"/>.</summary>
|
|
public class DigestService : IDigestService
|
|
{
|
|
private readonly AppDbContext _db;
|
|
private readonly IAnalyticsService _analytics;
|
|
private readonly IEmailSender _emailSender;
|
|
|
|
public DigestService(AppDbContext db, IAnalyticsService analytics, IEmailSender emailSender)
|
|
{
|
|
_db = db;
|
|
_analytics = analytics;
|
|
_emailSender = emailSender;
|
|
}
|
|
|
|
public async Task SendDigestAsync(Guid userId, CancellationToken ct = default)
|
|
{
|
|
var user = await _db.Users.FirstOrDefaultAsync(u => u.Id == userId, ct);
|
|
if (user is null || string.IsNullOrWhiteSpace(user.Email)) return;
|
|
|
|
var dashboard = await _analytics.GetDashboardAsync(userId, ct);
|
|
var html = BuildHtml(dashboard);
|
|
|
|
await _emailSender.SendAsync(user.Email, "Your InboxIntel digest", html, ct);
|
|
|
|
user.LastDigestSentUtc = DateTimeOffset.UtcNow;
|
|
await _db.SaveChangesAsync(ct);
|
|
}
|
|
|
|
private static string BuildHtml(Application.DTOs.DashboardSummaryDto d)
|
|
{
|
|
var sb = new StringBuilder();
|
|
sb.Append("<div style=\"font-family:sans-serif;max-width:480px\">");
|
|
sb.Append("<h2>Your inbox this week</h2>");
|
|
sb.Append($"<p><strong>{d.Health.Score}/100</strong> inbox health (grade {d.Health.Grade})</p>");
|
|
sb.Append($"<p>{d.UnreadEmails:N0} unread of {d.TotalEmails:N0} total emails</p>");
|
|
if (d.Health.SafeToUnsubscribeCount > 0)
|
|
sb.Append($"<p>{d.Health.SafeToUnsubscribeCount} senders are safe to unsubscribe from.</p>");
|
|
|
|
if (d.TopSenders.Count > 0)
|
|
{
|
|
sb.Append("<h3>Top senders</h3><ul>");
|
|
foreach (var s in d.TopSenders.Take(5))
|
|
sb.Append($"<li>{System.Net.WebUtility.HtmlEncode(s.DisplayName ?? s.Address)} — {s.EmailCount:N0} emails</li>");
|
|
sb.Append("</ul>");
|
|
}
|
|
|
|
if (d.Health.Recommendations.Count > 0)
|
|
{
|
|
sb.Append("<h3>Recommendations</h3><ul>");
|
|
foreach (var r in d.Health.Recommendations)
|
|
sb.Append($"<li>{System.Net.WebUtility.HtmlEncode(r)}</li>");
|
|
sb.Append("</ul>");
|
|
}
|
|
|
|
sb.Append("</div>");
|
|
return sb.ToString();
|
|
}
|
|
}
|