From 3195ad86ce56df07a99f8e9ea0a7e4b2e679dc74 Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Wed, 4 Dec 2024 15:52:56 +0100 Subject: [PATCH] Add email cleanup tasks (#221) --- .../Controllers/Email/EmailController.cs | 7 +- src/Services/AliasVault.TaskRunner/Program.cs | 3 + .../Tasks/EmailCleanupTask.cs | 64 +++++++++++ .../Tasks/EmailQuotaCleanupTask.cs | 102 ++++++++++++++++++ .../Tasks/RefreshTokenCleanupTask.cs | 52 +++++++++ .../Models/ServerSettingsModel.cs | 22 ++-- 6 files changed, 234 insertions(+), 16 deletions(-) create mode 100644 src/Services/AliasVault.TaskRunner/Tasks/EmailCleanupTask.cs create mode 100644 src/Services/AliasVault.TaskRunner/Tasks/EmailQuotaCleanupTask.cs create mode 100644 src/Services/AliasVault.TaskRunner/Tasks/RefreshTokenCleanupTask.cs diff --git a/src/AliasVault.Api/Controllers/Email/EmailController.cs b/src/AliasVault.Api/Controllers/Email/EmailController.cs index 88bea4801..7ba4c5186 100644 --- a/src/AliasVault.Api/Controllers/Email/EmailController.cs +++ b/src/AliasVault.Api/Controllers/Email/EmailController.cs @@ -89,11 +89,8 @@ public class EmailController(ILogger logger, IDbContextFactory< return errorResult; } - // Delete associated attachments - context.EmailAttachments.RemoveRange(email!.Attachments); - - // Delete the email - context.Emails.Remove(email); + // Delete the email - attachments will be cascade deleted + context.Emails.Remove(email!); try { diff --git a/src/Services/AliasVault.TaskRunner/Program.cs b/src/Services/AliasVault.TaskRunner/Program.cs index b04d838b4..b669b1f91 100644 --- a/src/Services/AliasVault.TaskRunner/Program.cs +++ b/src/Services/AliasVault.TaskRunner/Program.cs @@ -41,6 +41,9 @@ builder.Services.AddSingleton(); // Define the tasks that will be executed by the TaskRunner. builder.Services.AddTransient(); +builder.Services.AddTransient(); +builder.Services.AddTransient(); +builder.Services.AddTransient(); builder.Services.AddStatusHostedService(Assembly.GetExecutingAssembly().GetName().Name!); diff --git a/src/Services/AliasVault.TaskRunner/Tasks/EmailCleanupTask.cs b/src/Services/AliasVault.TaskRunner/Tasks/EmailCleanupTask.cs new file mode 100644 index 000000000..d08c300dc --- /dev/null +++ b/src/Services/AliasVault.TaskRunner/Tasks/EmailCleanupTask.cs @@ -0,0 +1,64 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) lanedirt. All rights reserved. +// Licensed under the MIT license. See LICENSE.md file in the project root for full license information. +// +//----------------------------------------------------------------------- + +namespace AliasVault.TaskRunner.Tasks; + +using AliasServerDb; +using AliasVault.Shared.Server.Services; +using Microsoft.EntityFrameworkCore; + +/// +/// A maintenance task that deletes old emails based on server settings. +/// +public class EmailCleanupTask : IMaintenanceTask +{ + private readonly ILogger _logger; + private readonly IDbContextFactory _dbContextFactory; + private readonly ServerSettingsService _settingsService; + + /// + /// Initializes a new instance of the class. + /// + /// The logger. + /// The database context factory. + /// The settings service. + public EmailCleanupTask( + ILogger logger, + IDbContextFactory dbContextFactory, + ServerSettingsService settingsService) + { + _logger = logger; + _dbContextFactory = dbContextFactory; + _settingsService = settingsService; + } + + /// + public string Name => "Email Cleanup"; + + /// + public async Task ExecuteAsync(CancellationToken cancellationToken) + { + var settings = await _settingsService.GetAllSettingsAsync(); + if (settings.EmailRetentionDays <= 0) + { + return; + } + + await using var dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); + var cutoffDate = DateTime.UtcNow.AddDays(-settings.EmailRetentionDays); + + // Delete the emails + var emailsDeleted = await dbContext.Emails + .Where(x => x.DateSystem < cutoffDate) + .ExecuteDeleteAsync(cancellationToken); + + _logger.LogWarning( + "Deleted {EmailCount} emails older than {Days} days", + emailsDeleted, + settings.EmailRetentionDays); + } +} diff --git a/src/Services/AliasVault.TaskRunner/Tasks/EmailQuotaCleanupTask.cs b/src/Services/AliasVault.TaskRunner/Tasks/EmailQuotaCleanupTask.cs new file mode 100644 index 000000000..28d67d87b --- /dev/null +++ b/src/Services/AliasVault.TaskRunner/Tasks/EmailQuotaCleanupTask.cs @@ -0,0 +1,102 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) lanedirt. All rights reserved. +// Licensed under the MIT license. See LICENSE.md file in the project root for full license information. +// +//----------------------------------------------------------------------- + +namespace AliasVault.TaskRunner.Tasks; + +using AliasServerDb; +using AliasVault.Shared.Server.Services; +using Microsoft.EntityFrameworkCore; + +/// +/// A maintenance task that enforces email quotas by deleting oldest emails when users exceed their limit. +/// +public class EmailQuotaCleanupTask : IMaintenanceTask +{ + private readonly ILogger _logger; + private readonly IDbContextFactory _dbContextFactory; + private readonly ServerSettingsService _settingsService; + + /// + /// Initializes a new instance of the class. + /// + /// The logger. + /// The database context factory. + /// The settings service. + public EmailQuotaCleanupTask( + ILogger logger, + IDbContextFactory dbContextFactory, + ServerSettingsService settingsService) + { + _logger = logger; + _dbContextFactory = dbContextFactory; + _settingsService = settingsService; + } + + /// + public string Name => "Email Quota Cleanup"; + + /// + public async Task ExecuteAsync(CancellationToken cancellationToken) + { + var settings = await _settingsService.GetAllSettingsAsync(); + if (settings.MaxEmailsPerUser <= 0) + { + return; + } + + await using var dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); + + // Get all users with their email claims + var userEmailClaims = await dbContext.UserEmailClaims + .Select(c => new { c.UserId, c.Address }) + .ToListAsync(cancellationToken); + + var totalEmailsDeleted = 0; + var usersProcessed = 0; + + // Group email claims by user + foreach (var userGroup in userEmailClaims.GroupBy(c => c.UserId)) + { + var userAddresses = userGroup.Select(c => c.Address).ToList(); + + // Get total email count for this user + var emailCount = await dbContext.Emails + .Where(e => userAddresses.Contains(e.To)) + .CountAsync(cancellationToken); + + if (emailCount > settings.MaxEmailsPerUser) + { + // Calculate how many emails need to be deleted + var deleteCount = emailCount - settings.MaxEmailsPerUser; + + // Delete the oldest emails - attachments will be cascade deleted + var emailsDeleted = await dbContext.Emails + .Where(e => userAddresses.Contains(e.To)) + .OrderBy(e => e.DateSystem) + .Take(deleteCount) + .ExecuteDeleteAsync(cancellationToken); + + if (emailsDeleted > 0) + { + totalEmailsDeleted += emailsDeleted; + usersProcessed++; + _logger.LogWarning( + "Deleted {EmailCount} emails for user {UserId} to maintain quota of {MaxEmails}", + emailsDeleted, + userGroup.Key, + settings.MaxEmailsPerUser); + } + } + } + + _logger.LogWarning( + "Deleted {TotalEmails} emails across {UserCount} users to maintain quota of {MaxEmails} max emails per user", + totalEmailsDeleted, + usersProcessed, + settings.MaxEmailsPerUser); + } +} diff --git a/src/Services/AliasVault.TaskRunner/Tasks/RefreshTokenCleanupTask.cs b/src/Services/AliasVault.TaskRunner/Tasks/RefreshTokenCleanupTask.cs new file mode 100644 index 000000000..06a8cc2e4 --- /dev/null +++ b/src/Services/AliasVault.TaskRunner/Tasks/RefreshTokenCleanupTask.cs @@ -0,0 +1,52 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) lanedirt. All rights reserved. +// Licensed under the MIT license. See LICENSE.md file in the project root for full license information. +// +//----------------------------------------------------------------------- + +namespace AliasVault.TaskRunner.Tasks; + +using AliasServerDb; +using Microsoft.EntityFrameworkCore; + +/// +/// A maintenance task that deletes expired refresh tokens. +/// +public class RefreshTokenCleanupTask : IMaintenanceTask +{ + private readonly ILogger _logger; + private readonly IDbContextFactory _dbContextFactory; + + /// + /// Initializes a new instance of the class. + /// + /// The logger. + /// The database context factory. + public RefreshTokenCleanupTask( + ILogger logger, + IDbContextFactory dbContextFactory) + { + _logger = logger; + _dbContextFactory = dbContextFactory; + } + + /// + public string Name => "Refresh Token Cleanup"; + + /// + public async Task ExecuteAsync(CancellationToken cancellationToken) + { + await using var dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); + + var cutoffDate = DateTime.UtcNow; + var deletedCount = await dbContext.AliasVaultUserRefreshTokens + .Where(x => x.ExpireDate < cutoffDate) + .ExecuteDeleteAsync(cancellationToken); + + if (deletedCount > 0) + { + _logger.LogWarning("Deleted {Count} expired refresh tokens", deletedCount); + } + } +} diff --git a/src/Shared/AliasVault.Shared.Server/Models/ServerSettingsModel.cs b/src/Shared/AliasVault.Shared.Server/Models/ServerSettingsModel.cs index 789fd6ef8..0b2fd8f28 100644 --- a/src/Shared/AliasVault.Shared.Server/Models/ServerSettingsModel.cs +++ b/src/Shared/AliasVault.Shared.Server/Models/ServerSettingsModel.cs @@ -13,32 +13,32 @@ namespace AliasVault.Shared.Server.Models; public class ServerSettingsModel { /// - /// Gets or sets the general log retention days. + /// Gets or sets the general log retention days. Defaults to 30. /// public int GeneralLogRetentionDays { get; set; } = 30; /// - /// Gets or sets the auth log retention days. + /// Gets or sets the auth log retention days. Defaults to 30. /// - public int AuthLogRetentionDays { get; set; } = 90; + public int AuthLogRetentionDays { get; set; } = 30; /// - /// Gets or sets the email retention days. + /// Gets or sets the email retention days. Defaults to 0 (disabled). /// - public int EmailRetentionDays { get; set; } = 30; + public int EmailRetentionDays { get; set; } /// - /// Gets or sets the max emails per user. + /// Gets or sets the max emails per user. Defaults to 0 (unlimited). /// - public int MaxEmailsPerUser { get; set; } = 100; + public int MaxEmailsPerUser { get; set; } /// - /// Gets or sets the time when maintenance tasks are run (24h format). + /// Gets or sets the time when maintenance tasks are run (24h format). Defaults to 00:00. /// - public TimeOnly MaintenanceTime { get; set; } = new TimeOnly(0, 0); + public TimeOnly MaintenanceTime { get; set; } = new(0, 0); /// - /// Gets or sets the task runner days. + /// Gets or sets the task runner days. Defaults to all days of the week. /// - public List TaskRunnerDays { get; set; } = new() { 1, 2, 3, 4, 5, 6, 7 }; + public List TaskRunnerDays { get; set; } = [1, 2, 3, 4, 5, 6, 7]; }