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;
}
}