using InboxIntel.Infrastructure.Configuration; using InboxIntel.Infrastructure.Persistence; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; namespace InboxIntel.Infrastructure.Retention; /// /// AUDIT H-3 (retention): purges locally stored email data past the configured age so the /// local mailbox copy is not kept forever by default-of-omission. This deletes ONLY the /// local Postgres rows — the user's actual Gmail is never touched. Both knobs default to 0 /// (disabled) so existing deployments are unchanged until the operator opts in. /// public class RetentionService { private readonly AppDbContext _db; private readonly DataRetentionOptions _options; private readonly ILogger _logger; public RetentionService(AppDbContext db, IOptions options, ILogger logger) { _db = db; _options = options.Value; _logger = logger; } /// Runs the configured purges. Returns the number of emails removed. public async Task PurgeAsync(CancellationToken ct = default) { var removed = 0; var now = DateTimeOffset.UtcNow; if (_options.PurgeTrashedAfterDays > 0) { var cutoff = now.AddDays(-_options.PurgeTrashedAfterDays); removed += await PurgeWhereAsync(e => e.IsTrashed && e.SentAtUtc < cutoff, ct); } if (_options.PurgeAllAfterDays > 0) { var cutoff = now.AddDays(-_options.PurgeAllAfterDays); removed += await PurgeWhereAsync(e => e.SentAtUtc < cutoff, ct); } if (removed > 0) _logger.LogInformation("Retention purge removed {Count} locally stored emails", removed); return removed; } private async Task PurgeWhereAsync( System.Linq.Expressions.Expression> predicate, CancellationToken ct) { // RemoveRange (not ExecuteDelete) so dependent rows (labels/attachments) cascade via // the model on every provider, and the InMemory test provider works identically. var doomed = await _db.Emails.Where(predicate).ToListAsync(ct); if (doomed.Count == 0) return 0; _db.Emails.RemoveRange(doomed); await _db.SaveChangesAsync(ct); return doomed.Count; } }