mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-05-18 13:28:12 -04:00
Add per user email limits configurable through admin (#1075)
This commit is contained in:
committed by
Leendert de Borst
parent
984f5a2c52
commit
4938129367
@@ -75,7 +75,7 @@ else
|
||||
<div class="w-full mb-4 overflow-x-auto">
|
||||
<table class="w-full text-sm text-left text-gray-500 dark:text-gray-400">
|
||||
<tbody>
|
||||
<tr class="border-b dark:border-gray-700">
|
||||
<tr class="">
|
||||
<th scope="row" class="px-4 py-3 font-medium text-gray-900 whitespace-nowrap dark:text-white">Registered at</th>
|
||||
<td class="px-4 py-3">@User.CreatedAt.ToString("yyyy-MM-dd HH:mm")</td>
|
||||
</tr>
|
||||
@@ -115,6 +115,52 @@ else
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" class="px-4 py-3 font-medium text-gray-900 whitespace-nowrap dark:text-white">Email Limits</th>
|
||||
<td class="px-4 py-3">
|
||||
@if (!IsEditingEmailLimits)
|
||||
{
|
||||
<div class="flex flex-col space-y-2">
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="text-gray-700 dark:text-gray-300">
|
||||
<strong>Max Emails:</strong> @(User.MaxEmails == 0 ? "Unlimited" : User.MaxEmails.ToString("N0"))
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="text-gray-700 dark:text-gray-300">
|
||||
<strong>Max Age:</strong> @(User.MaxEmailAgeDays == 0 ? "Unlimited" : User.MaxEmailAgeDays + " days")
|
||||
</span>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<Button Color="primary" OnClick="@(() => StartEditingEmailLimits())">Edit Limits</Button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="space-y-3 p-3 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
||||
<div>
|
||||
<label class="block mb-1 text-sm font-medium text-gray-900 dark:text-white">Max Emails</label>
|
||||
<div class="flex items-center space-x-2">
|
||||
<input type="number" min="0" @bind="EditMaxEmails" class="bg-white border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-500 focus:border-primary-500 block w-32 p-2.5 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500" />
|
||||
<span class="text-sm text-gray-500 dark:text-gray-400">(0 = unlimited)</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block mb-1 text-sm font-medium text-gray-900 dark:text-white">Max Email Age</label>
|
||||
<div class="flex items-center space-x-2">
|
||||
<input type="number" min="0" @bind="EditMaxEmailAgeDays" class="bg-white border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-500 focus:border-primary-500 block w-32 p-2.5 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500" />
|
||||
<span class="text-sm text-gray-500 dark:text-gray-400">days (0 = unlimited)</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex space-x-2 pt-2">
|
||||
<Button Color="success" OnClick="SaveEmailLimits">Save</Button>
|
||||
<Button Color="secondary" OnClick="CancelEditingEmailLimits">Cancel</Button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -135,7 +181,7 @@ else
|
||||
<div class="p-4 mb-4 bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 sm:p-6 dark:bg-gray-800">
|
||||
<div class="items-center">
|
||||
<div>
|
||||
<h3 class="mb-1 text-xl font-bold text-gray-900 dark:text-white">UserRefreshTokens (Logged in devices)</h3>
|
||||
<h3 class="mb-1 text-xl font-bold text-gray-900 dark:text-white">Logged in devices</h3>
|
||||
|
||||
<RefreshTokenTable RefreshTokenList="@RefreshTokenList" OnRevokeToken="@RevokeRefreshToken" OnRevokeAllTokens="@RevokeAllTokens" />
|
||||
</div>
|
||||
@@ -185,6 +231,9 @@ else
|
||||
private List<Vault> VaultList { get; set; } = [];
|
||||
private List<AuthLog> AuthLogList { get; set; } = [];
|
||||
private UserUsageStatistics? UserUsageStats { get; set; }
|
||||
private bool IsEditingEmailLimits { get; set; }
|
||||
private int EditMaxEmails { get; set; }
|
||||
private int EditMaxEmailAgeDays { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task OnInitializedAsync()
|
||||
@@ -427,4 +476,43 @@ Do you want to proceed with the restoration?")) {
|
||||
await RefreshData();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts editing email limits for the user.
|
||||
/// </summary>
|
||||
private void StartEditingEmailLimits()
|
||||
{
|
||||
IsEditingEmailLimits = true;
|
||||
EditMaxEmails = User!.MaxEmails;
|
||||
EditMaxEmailAgeDays = User.MaxEmailAgeDays;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cancels editing email limits.
|
||||
/// </summary>
|
||||
private void CancelEditingEmailLimits()
|
||||
{
|
||||
IsEditingEmailLimits = false;
|
||||
EditMaxEmails = 0;
|
||||
EditMaxEmailAgeDays = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves the email limits for the user.
|
||||
/// </summary>
|
||||
private async Task SaveEmailLimits()
|
||||
{
|
||||
await using var dbContext = await DbContextFactory.CreateDbContextAsync();
|
||||
User = await dbContext.AliasVaultUsers.FindAsync(Id);
|
||||
|
||||
if (User != null)
|
||||
{
|
||||
User.MaxEmails = EditMaxEmails;
|
||||
User.MaxEmailAgeDays = EditMaxEmailAgeDays;
|
||||
await dbContext.SaveChangesAsync();
|
||||
IsEditingEmailLimits = false;
|
||||
await RefreshData();
|
||||
GlobalNotificationService.AddSuccessMessage("Email limits updated successfully.", true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,16 @@ public class AliasVaultUser : IdentityUser
|
||||
/// </summary>
|
||||
public DateTime UpdatedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum number of emails for all of user's aliases. 0 means unlimited.
|
||||
/// </summary>
|
||||
public int MaxEmails { get; set; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum age of emails in days. Emails older than this will be deleted. 0 means unlimited.
|
||||
/// </summary>
|
||||
public int MaxEmailAgeDays { get; set; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the collection of vaults.
|
||||
/// </summary>
|
||||
|
||||
926
apps/server/Databases/AliasServerDb/Migrations/20250804182808_AddPerUserEmailLimits.Designer.cs
generated
Normal file
926
apps/server/Databases/AliasServerDb/Migrations/20250804182808_AddPerUserEmailLimits.Designer.cs
generated
Normal file
@@ -0,0 +1,926 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using AliasServerDb;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace AliasServerDb.Migrations
|
||||
{
|
||||
[DbContext(typeof(AliasServerDbContext))]
|
||||
[Migration("20250804182808_AddPerUserEmailLimits")]
|
||||
partial class AddPerUserEmailLimits
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "9.0.4")
|
||||
.HasAnnotation("Proxies:ChangeTracking", false)
|
||||
.HasAnnotation("Proxies:CheckEquality", false)
|
||||
.HasAnnotation("Proxies:LazyLoading", true)
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.AdminRole", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("NormalizedName")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("AdminRoles");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.AdminUser", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("AccessFailedCount")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool>("EmailConfirmed")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<DateTime?>("LastPasswordChanged")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<bool>("LockoutEnabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<DateTimeOffset?>("LockoutEnd")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("NormalizedEmail")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("NormalizedUserName")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("PhoneNumber")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool>("PhoneNumberConfirmed")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("SecurityStamp")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool>("TwoFactorEnabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("UserName")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("AdminUsers");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.AliasVaultRole", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("NormalizedName")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("AliasVaultRoles");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.AliasVaultUser", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("AccessFailedCount")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<bool>("Blocked")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool>("EmailConfirmed")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("LockoutEnabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<DateTimeOffset?>("LockoutEnd")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<int>("MaxEmailAgeDays")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("MaxEmails")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("NormalizedEmail")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("NormalizedUserName")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("PasswordChangedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("PhoneNumber")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool>("PhoneNumberConfirmed")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("SecurityStamp")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool>("TwoFactorEnabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("UserName")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("AliasVaultUsers");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.AliasVaultUserRefreshToken", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("DeviceIdentifier")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<DateTime>("ExpireDate")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("IpAddress")
|
||||
.HasMaxLength(45)
|
||||
.HasColumnType("character varying(45)");
|
||||
|
||||
b.Property<string>("PreviousTokenValue")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("AliasVaultUserRefreshTokens");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.AuthLog", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("AdditionalInfo")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<string>("Browser")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<string>("Client")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<string>("Country")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<string>("DeviceType")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<int>("EventType")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int?>("FailureReason")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("IpAddress")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<bool>("IsSuccess")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("IsSuspiciousActivity")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("OperatingSystem")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<string>("RequestPath")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<DateTime>("Timestamp")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("UserAgent")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex(new[] { "EventType" }, "IX_EventType");
|
||||
|
||||
b.HasIndex(new[] { "IpAddress" }, "IX_IpAddress");
|
||||
|
||||
b.HasIndex(new[] { "Timestamp" }, "IX_Timestamp");
|
||||
|
||||
b.HasIndex(new[] { "Username", "IsSuccess", "Timestamp" }, "IX_Username_IsSuccess_Timestamp")
|
||||
.IsDescending(false, false, true);
|
||||
|
||||
b.HasIndex(new[] { "Username", "Timestamp" }, "IX_Username_Timestamp")
|
||||
.IsDescending(false, true);
|
||||
|
||||
b.ToTable("AuthLogs");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.Email", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTime>("Date")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime>("DateSystem")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("EncryptedSymmetricKey")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("From")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("FromDomain")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("FromLocal")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("MessageHtml")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("MessagePlain")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("MessagePreview")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("MessageSource")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool>("PushNotificationSent")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("Subject")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("To")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ToDomain")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ToLocal")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<Guid>("UserEncryptionKeyId")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<bool>("Visible")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Date");
|
||||
|
||||
b.HasIndex("DateSystem");
|
||||
|
||||
b.HasIndex("PushNotificationSent");
|
||||
|
||||
b.HasIndex("ToLocal");
|
||||
|
||||
b.HasIndex("UserEncryptionKeyId");
|
||||
|
||||
b.HasIndex("Visible");
|
||||
|
||||
b.ToTable("Emails");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.EmailAttachment", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<byte[]>("Bytes")
|
||||
.IsRequired()
|
||||
.HasColumnType("bytea");
|
||||
|
||||
b.Property<DateTime>("Date")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<int>("EmailId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Filename")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("Filesize")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("MimeType")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("EmailId");
|
||||
|
||||
b.ToTable("EmailAttachments");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.Log", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Application")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<string>("Exception")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Level")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.Property<string>("LogEvent")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("LogEvent");
|
||||
|
||||
b.Property<string>("Message")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("MessageTemplate")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Properties")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("SourceContext")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<DateTime>("TimeStamp")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Application");
|
||||
|
||||
b.HasIndex("TimeStamp");
|
||||
|
||||
b.ToTable("Logs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.ServerSetting", b =>
|
||||
{
|
||||
b.Property<string>("Key")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Key");
|
||||
|
||||
b.ToTable("ServerSettings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.TaskRunnerJob", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<TimeOnly?>("EndTime")
|
||||
.HasColumnType("time without time zone");
|
||||
|
||||
b.Property<string>("ErrorMessage")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool>("IsOnDemand")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<DateTime>("RunDate")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<TimeOnly>("StartTime")
|
||||
.HasColumnType("time without time zone");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("TaskRunnerJobs");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.UserEmailClaim", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Address")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<string>("AddressDomain")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<string>("AddressLocal")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<bool>("Disabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Address")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("UserEmailClaims");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.UserEncryptionKey", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<bool>("IsPrimary")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("PublicKey")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("character varying(2000)");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("UserEncryptionKeys");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.Vault", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Client")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<int>("CredentialsCount")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("EmailClaimsCount")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("EncryptionSettings")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("EncryptionType")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("FileSize")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<long>("RevisionNumber")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("Salt")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<string>("VaultBlob")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Verifier")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("character varying(1000)");
|
||||
|
||||
b.Property<string>("Version")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("Vaults");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasVault.WorkerStatus.Database.WorkerServiceStatus", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("CurrentStatus")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<string>("DesiredStatus")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<DateTime>("Heartbeat")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("ServiceName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("WorkerServiceStatuses");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.DataProtectionKey", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("FriendlyName")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Xml")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("DataProtectionKeys");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("ClaimType")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ClaimValue")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("RoleId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("RoleClaims", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("ClaimType")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ClaimValue")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("UserClaims", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||
{
|
||||
b.Property<string>("LoginProvider")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ProviderKey")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ProviderDisplayName")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("LoginProvider", "ProviderKey");
|
||||
|
||||
b.ToTable("UserLogins", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||
{
|
||||
b.Property<string>("UserId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("RoleId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("UserId", "RoleId");
|
||||
|
||||
b.ToTable("UserRoles", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||
{
|
||||
b.Property<string>("UserId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("LoginProvider")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("UserId", "LoginProvider", "Name");
|
||||
|
||||
b.ToTable("UserTokens", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.AliasVaultUserRefreshToken", b =>
|
||||
{
|
||||
b.HasOne("AliasServerDb.AliasVaultUser", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.Email", b =>
|
||||
{
|
||||
b.HasOne("AliasServerDb.UserEncryptionKey", "EncryptionKey")
|
||||
.WithMany("Emails")
|
||||
.HasForeignKey("UserEncryptionKeyId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("EncryptionKey");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.EmailAttachment", b =>
|
||||
{
|
||||
b.HasOne("AliasServerDb.Email", "Email")
|
||||
.WithMany("Attachments")
|
||||
.HasForeignKey("EmailId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Email");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.UserEmailClaim", b =>
|
||||
{
|
||||
b.HasOne("AliasServerDb.AliasVaultUser", "User")
|
||||
.WithMany("EmailClaims")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.UserEncryptionKey", b =>
|
||||
{
|
||||
b.HasOne("AliasServerDb.AliasVaultUser", "User")
|
||||
.WithMany("EncryptionKeys")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.Vault", b =>
|
||||
{
|
||||
b.HasOne("AliasServerDb.AliasVaultUser", "User")
|
||||
.WithMany("Vaults")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.AliasVaultUser", b =>
|
||||
{
|
||||
b.Navigation("EmailClaims");
|
||||
|
||||
b.Navigation("EncryptionKeys");
|
||||
|
||||
b.Navigation("Vaults");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.Email", b =>
|
||||
{
|
||||
b.Navigation("Attachments");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.UserEncryptionKey", b =>
|
||||
{
|
||||
b.Navigation("Emails");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace AliasServerDb.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddPerUserEmailLimits : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "MaxEmailAgeDays",
|
||||
table: "AliasVaultUsers",
|
||||
type: "integer",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "MaxEmails",
|
||||
table: "AliasVaultUsers",
|
||||
type: "integer",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "MaxEmailAgeDays",
|
||||
table: "AliasVaultUsers");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "MaxEmails",
|
||||
table: "AliasVaultUsers");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ namespace AliasServerDb.Migrations
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "9.0.3")
|
||||
.HasAnnotation("ProductVersion", "9.0.4")
|
||||
.HasAnnotation("Proxies:ChangeTracking", false)
|
||||
.HasAnnotation("Proxies:CheckEquality", false)
|
||||
.HasAnnotation("Proxies:LazyLoading", true)
|
||||
@@ -147,6 +147,12 @@ namespace AliasServerDb.Migrations
|
||||
b.Property<DateTimeOffset?>("LockoutEnd")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<int>("MaxEmailAgeDays")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("MaxEmails")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("NormalizedEmail")
|
||||
.HasColumnType("text");
|
||||
|
||||
@@ -43,22 +43,70 @@ public class EmailCleanupTask : IMaintenanceTask
|
||||
public async Task ExecuteAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var settings = await _settingsService.GetAllSettingsAsync();
|
||||
if (settings.EmailRetentionDays <= 0)
|
||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
var totalEmailsDeleted = 0;
|
||||
|
||||
// First handle global retention settings
|
||||
if (settings.EmailRetentionDays > 0)
|
||||
{
|
||||
return;
|
||||
var globalCutoffDate = DateTime.UtcNow.AddDays(-settings.EmailRetentionDays);
|
||||
|
||||
// Delete the emails based on global settings
|
||||
var globalEmailsDeleted = await dbContext.Emails
|
||||
.Where(x => x.DateSystem < globalCutoffDate)
|
||||
.ExecuteDeleteAsync(cancellationToken);
|
||||
|
||||
if (globalEmailsDeleted > 0)
|
||||
{
|
||||
totalEmailsDeleted += globalEmailsDeleted;
|
||||
_logger.LogWarning(
|
||||
"Deleted {EmailCount} emails older than {Days} days (global setting)",
|
||||
globalEmailsDeleted,
|
||||
settings.EmailRetentionDays);
|
||||
}
|
||||
}
|
||||
|
||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
var cutoffDate = DateTime.UtcNow.AddDays(-settings.EmailRetentionDays);
|
||||
// Now handle per-user age limits
|
||||
var usersWithAgeLimits = await dbContext.AliasVaultUsers
|
||||
.Where(u => u.MaxEmailAgeDays > 0)
|
||||
.Select(u => new { u.Id, u.UserName, u.MaxEmailAgeDays })
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
// Delete the emails
|
||||
var emailsDeleted = await dbContext.Emails
|
||||
.Where(x => x.DateSystem < cutoffDate)
|
||||
.ExecuteDeleteAsync(cancellationToken);
|
||||
foreach (var user in usersWithAgeLimits)
|
||||
{
|
||||
var userCutoffDate = DateTime.UtcNow.AddDays(-user.MaxEmailAgeDays);
|
||||
|
||||
_logger.LogWarning(
|
||||
"Deleted {EmailCount} emails older than {Days} days",
|
||||
emailsDeleted,
|
||||
settings.EmailRetentionDays);
|
||||
// Get all email addresses for this user
|
||||
var userAddresses = await dbContext.UserEmailClaims
|
||||
.Where(c => c.UserId == user.Id)
|
||||
.Select(c => c.Address)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
if (userAddresses.Any())
|
||||
{
|
||||
// Delete emails older than user's limit
|
||||
var userEmailsDeleted = await dbContext.Emails
|
||||
.Where(e => userAddresses.Contains(e.To) && e.DateSystem < userCutoffDate)
|
||||
.ExecuteDeleteAsync(cancellationToken);
|
||||
|
||||
if (userEmailsDeleted > 0)
|
||||
{
|
||||
totalEmailsDeleted += userEmailsDeleted;
|
||||
_logger.LogWarning(
|
||||
"Deleted {EmailCount} emails older than {Days} days for user {UserName} (user-specific setting)",
|
||||
userEmailsDeleted,
|
||||
user.MaxEmailAgeDays,
|
||||
user.UserName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (totalEmailsDeleted > 0)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Total emails deleted by age cleanup: {TotalEmails}",
|
||||
totalEmailsDeleted);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,35 +43,53 @@ public class EmailQuotaCleanupTask : IMaintenanceTask
|
||||
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 })
|
||||
// Get all users with their email claims and limits
|
||||
var usersWithClaims = await (from u in dbContext.AliasVaultUsers
|
||||
join c in dbContext.UserEmailClaims on u.Id equals c.UserId
|
||||
select new { u.Id, u.UserName, u.MaxEmails, c.Address })
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
var totalEmailsDeleted = 0;
|
||||
var usersProcessed = 0;
|
||||
|
||||
// Group email claims by user
|
||||
foreach (var userGroup in userEmailClaims.GroupBy(c => c.UserId))
|
||||
// Group by user
|
||||
foreach (var userGroup in usersWithClaims.GroupBy(x => new { x.Id, x.UserName, x.MaxEmails }))
|
||||
{
|
||||
var userAddresses = userGroup.Select(c => c.Address).ToList();
|
||||
// Determine the effective limit for this user
|
||||
int effectiveLimit;
|
||||
string limitSource;
|
||||
|
||||
if (userGroup.Key.MaxEmails > 0)
|
||||
{
|
||||
// User has a specific limit
|
||||
effectiveLimit = userGroup.Key.MaxEmails;
|
||||
limitSource = "user-specific";
|
||||
}
|
||||
else if (settings.MaxEmailsPerUser > 0)
|
||||
{
|
||||
// Use global limit
|
||||
effectiveLimit = settings.MaxEmailsPerUser;
|
||||
limitSource = "global";
|
||||
}
|
||||
else
|
||||
{
|
||||
// No limits apply
|
||||
continue;
|
||||
}
|
||||
|
||||
var userAddresses = userGroup.Select(x => x.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)
|
||||
if (emailCount > effectiveLimit)
|
||||
{
|
||||
// Calculate how many emails need to be deleted
|
||||
var deleteCount = emailCount - settings.MaxEmailsPerUser;
|
||||
var deleteCount = emailCount - effectiveLimit;
|
||||
|
||||
// Delete the oldest emails - attachments will be cascade deleted
|
||||
var emailsDeleted = await dbContext.Emails
|
||||
@@ -85,18 +103,21 @@ public class EmailQuotaCleanupTask : IMaintenanceTask
|
||||
totalEmailsDeleted += emailsDeleted;
|
||||
usersProcessed++;
|
||||
_logger.LogWarning(
|
||||
"Deleted {EmailCount} emails for user {UserId} to maintain quota of {MaxEmails}",
|
||||
"Deleted {EmailCount} emails for user {Username} to maintain quota of {MaxEmails} ({LimitSource} setting)",
|
||||
emailsDeleted,
|
||||
userGroup.Key,
|
||||
settings.MaxEmailsPerUser);
|
||||
userGroup.Key.UserName,
|
||||
effectiveLimit,
|
||||
limitSource);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogWarning(
|
||||
"Deleted {TotalEmails} emails across {UserCount} users to maintain quota of {MaxEmails} max emails per user",
|
||||
totalEmailsDeleted,
|
||||
usersProcessed,
|
||||
settings.MaxEmailsPerUser);
|
||||
if (totalEmailsDeleted > 0)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Total emails deleted by quota cleanup: {TotalEmails} across {UserCount} users",
|
||||
totalEmailsDeleted,
|
||||
usersProcessed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user