2b19bddf7b
CI / backend (push) Successful in 52s
CI / frontend (push) Successful in 11s
Deploy Staging / deploy (push) Successful in 24s
Security / secrets (push) Successful in 4s
Security / dependencies (push) Successful in 52s
CI / backend (pull_request) Successful in 49s
CI / frontend (pull_request) Successful in 10s
Security / secrets (pull_request) Successful in 3s
Security / dependencies (pull_request) Successful in 55s
64 lines
2.4 KiB
C#
64 lines
2.4 KiB
C#
using InboxIntel.Infrastructure.Configuration;
|
|
using InboxIntel.Infrastructure.Persistence;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.Extensions.Logging;
|
|
using Microsoft.Extensions.Options;
|
|
|
|
namespace InboxIntel.Infrastructure.Retention;
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public class RetentionService
|
|
{
|
|
private readonly AppDbContext _db;
|
|
private readonly DataRetentionOptions _options;
|
|
private readonly ILogger<RetentionService> _logger;
|
|
|
|
public RetentionService(AppDbContext db, IOptions<DataRetentionOptions> options, ILogger<RetentionService> logger)
|
|
{
|
|
_db = db;
|
|
_options = options.Value;
|
|
_logger = logger;
|
|
}
|
|
|
|
/// <summary>Runs the configured purges. Returns the number of emails removed.</summary>
|
|
public async Task<int> 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<int> PurgeWhereAsync(
|
|
System.Linq.Expressions.Expression<Func<Domain.Entities.Email, bool>> 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;
|
|
}
|
|
}
|