mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-02-23 01:46:18 -05:00
283 lines
11 KiB
Plaintext
283 lines
11 KiB
Plaintext
@page "/emails"
|
|
@using AliasVault.RazorComponents.Tables
|
|
@inherits MainBase
|
|
|
|
<LayoutPageTitle>Emails</LayoutPageTitle>
|
|
|
|
<PageHeader
|
|
BreadcrumbItems="@BreadcrumbItems"
|
|
Title="@(TotalRecords > 0 ? $"Emails ({TotalRecords:N0})" : "Emails")"
|
|
Description="This page shows all received mails by this AliasVault server. All email fields except 'To' are encrypted with the public key of the user and are unreadable by the server.">
|
|
<CustomActions>
|
|
<a href="email-storage-stats" class="inline-flex items-center px-3 py-2 text-sm font-medium text-gray-600 bg-gray-100 border border-gray-300 rounded-md hover:bg-gray-200 hover:text-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-600 dark:hover:bg-gray-600 dark:hover:text-gray-200 mr-3">
|
|
<svg class="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
|
<path d="M3 4a1 1 0 011-1h12a1 1 0 011 1v2a1 1 0 01-1 1H4a1 1 0 01-1-1V4zM3 10a1 1 0 011-1h6a1 1 0 011 1v6a1 1 0 01-1 1H4a1 1 0 01-1-1v-6zM14 9a1 1 0 00-1 1v6a1 1 0 001 1h2a1 1 0 001-1v-6a1 1 0 00-1-1h-2z"/>
|
|
</svg>
|
|
Email Storage Stats
|
|
</a>
|
|
<RefreshButton OnClick="() => RefreshData(CancellationToken.None)" ButtonText="Refresh" />
|
|
</CustomActions>
|
|
</PageHeader>
|
|
|
|
@if (IsInitialized)
|
|
{
|
|
<div class="px-4">
|
|
<ResponsivePaginator CurrentPage="CurrentPage" PageSize="PageSize" TotalRecords="TotalRecords" OnPageChanged="HandlePageChanged" />
|
|
<div class="mb-3">
|
|
<div class="relative">
|
|
<SearchIcon />
|
|
<input type="text" @bind-value="SearchTerm" @bind-value:event="oninput" id="search" placeholder="Search emails..." class="w-full px-4 ps-10 py-2 border rounded text-sm text-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
@if (IsLoading)
|
|
{
|
|
<LoadingIndicator />
|
|
}
|
|
else
|
|
{
|
|
<div class="overflow-x-auto px-4">
|
|
<SortableTable Columns="@_tableColumns" SortColumn="@SortColumn" SortDirection="@SortDirection" OnSortChanged="HandleSortChanged">
|
|
@foreach (var viewModel in EmailViewModelList)
|
|
{
|
|
<SortableTableRow>
|
|
<SortableTableColumn IsPrimary="true">@viewModel.Email.Id</SortableTableColumn>
|
|
<SortableTableColumn>@viewModel.Email.DateSystem.ToString("yyyy-MM-dd HH:mm")</SortableTableColumn>
|
|
<SortableTableColumn>@(viewModel.Email.FromLocal.Length > 10 ? viewModel.Email.FromLocal.Substring(0, 10) : viewModel.Email.FromLocal)@@@(viewModel.Email.FromDomain.Length > 10 ? viewModel.Email.FromDomain.Substring(0, 10) : viewModel.Email.FromDomain)</SortableTableColumn>
|
|
<SortableTableColumn>@viewModel.Email.ToLocal@@@viewModel.Email.ToDomain</SortableTableColumn>
|
|
<SortableTableColumn>
|
|
@if (viewModel.UserName.Length > 0)
|
|
{
|
|
<span class="line-clamp-1"><a href="users/@viewModel.UserId">@viewModel.UserName</a></span>
|
|
}
|
|
else
|
|
{
|
|
<span class="line-clamp-1">n/a</span>
|
|
}
|
|
</SortableTableColumn>
|
|
<SortableTableColumn>@viewModel.Email.Attachments.Count</SortableTableColumn>
|
|
</SortableTableRow>
|
|
}
|
|
</SortableTable>
|
|
</div>
|
|
}
|
|
|
|
@code {
|
|
/// <summary>
|
|
/// The search term from the query parameter.
|
|
/// </summary>
|
|
[Parameter]
|
|
[SupplyParameterFromQuery(Name = "search")]
|
|
public string? SearchTermFromQuery { get; set; }
|
|
|
|
private readonly List<TableColumn> _tableColumns = [
|
|
new TableColumn { Title = "ID", PropertyName = "Id" },
|
|
new TableColumn { Title = "Time", PropertyName = "DateSystem" },
|
|
new TableColumn { Title = "From", PropertyName = "From" },
|
|
new TableColumn { Title = "To", PropertyName = "To" },
|
|
new TableColumn { Title = "User", Sortable = false },
|
|
new TableColumn { Title = "Attachments", PropertyName = "Attachments" },
|
|
];
|
|
|
|
private List<EmailViewModel> EmailViewModelList { get; set; } = [];
|
|
private bool IsInitialized { get; set; } = false;
|
|
private bool IsLoading { get; set; } = true;
|
|
private int CurrentPage { get; set; } = 1;
|
|
private int PageSize { get; set; } = 50;
|
|
private int TotalRecords { get; set; }
|
|
private string _searchTerm = string.Empty;
|
|
private CancellationTokenSource? _searchCancellationTokenSource;
|
|
|
|
/// <summary>
|
|
/// The last search term.
|
|
/// </summary>
|
|
private string _lastSearchTerm = string.Empty;
|
|
|
|
private string SearchTerm
|
|
{
|
|
get => _searchTerm;
|
|
set
|
|
{
|
|
if (_searchTerm != value)
|
|
{
|
|
_searchTerm = value;
|
|
_searchCancellationTokenSource?.Cancel();
|
|
_searchCancellationTokenSource = new CancellationTokenSource();
|
|
_ = RefreshData(_searchCancellationTokenSource.Token);
|
|
}
|
|
}
|
|
}
|
|
|
|
private string SortColumn { get; set; } = "Id";
|
|
private SortDirection SortDirection { get; set; } = SortDirection.Descending;
|
|
private async Task HandleSortChanged((string column, SortDirection direction) sort)
|
|
{
|
|
SortColumn = sort.column;
|
|
SortDirection = sort.direction;
|
|
await RefreshData(CancellationToken.None);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
protected override async Task OnInitializedAsync()
|
|
{
|
|
await base.OnInitializedAsync();
|
|
BreadcrumbItems.Add(new BreadcrumbItem { DisplayName = "Emails" });
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
|
{
|
|
if (firstRender)
|
|
{
|
|
// Set the search term from the query parameter if it exists
|
|
if (!string.IsNullOrEmpty(SearchTermFromQuery))
|
|
{
|
|
_searchTerm = SearchTermFromQuery;
|
|
}
|
|
|
|
await RefreshData(CancellationToken.None);
|
|
}
|
|
}
|
|
|
|
private void HandlePageChanged(int newPage)
|
|
{
|
|
CurrentPage = newPage;
|
|
_ = RefreshData(CancellationToken.None);
|
|
}
|
|
|
|
private async Task RefreshData(CancellationToken cancellationToken = default)
|
|
{
|
|
try
|
|
{
|
|
IsLoading = true;
|
|
StateHasChanged();
|
|
|
|
await using var dbContext = await DbContextFactory.CreateDbContextAsync(cancellationToken);
|
|
|
|
IQueryable<Email> query = dbContext.Emails;
|
|
|
|
query = ApplySearchFilter(query);
|
|
query = ApplySort(query);
|
|
|
|
TotalRecords = await query.CountAsync(cancellationToken);
|
|
var emailList = await query
|
|
.Skip((CurrentPage - 1) * PageSize)
|
|
.Take(PageSize)
|
|
.ToListAsync(cancellationToken);
|
|
|
|
if (cancellationToken.IsCancellationRequested)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Get all usernames for the emails in the current list
|
|
var encryptionKeyIds = emailList.Select(x => x.UserEncryptionKeyId).Distinct().ToList();
|
|
var encryptionKeyUsernames = await dbContext.UserEncryptionKeys
|
|
.Where(x => encryptionKeyIds.Contains(x.Id))
|
|
.Join(dbContext.AliasVaultUsers, x => x.UserId, y => y.Id, (x, y) => new { EncryptionKeyId = x.Id, UserId = y.Id, y.UserName })
|
|
.ToListAsync(cancellationToken);
|
|
|
|
// Create new list of viewmodels
|
|
EmailViewModelList = new List<EmailViewModel>();
|
|
|
|
foreach (var email in emailList)
|
|
{
|
|
var encryptionKey = encryptionKeyUsernames.FirstOrDefault(x => x.EncryptionKeyId == email.UserEncryptionKeyId);
|
|
EmailViewModelList.Add(new EmailViewModel { Email = email, UserId = encryptionKey?.UserId ?? string.Empty, UserName = encryptionKey?.UserName ?? string.Empty });
|
|
}
|
|
|
|
IsLoading = false;
|
|
IsInitialized = true;
|
|
StateHasChanged();
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
// Expected when cancellation is requested, do nothing
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Applies a search filter to the query based on the search term.
|
|
/// </summary>
|
|
/// <param name="query">The query to filter.</param>
|
|
/// <returns>The filtered query.</returns>
|
|
private IQueryable<Email> ApplySearchFilter(IQueryable<Email> query)
|
|
{
|
|
if (SearchTerm.Length > 0)
|
|
{
|
|
// Reset page number back to 1 if the search term has changed.
|
|
if (SearchTerm != _lastSearchTerm && CurrentPage != 1)
|
|
{
|
|
CurrentPage = 1;
|
|
}
|
|
_lastSearchTerm = SearchTerm;
|
|
|
|
query = query.Where(x => EF.Functions.Like(x.To.ToLower(), "%" + SearchTerm.Trim().ToLower() + "%"));
|
|
}
|
|
|
|
return query;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Applies sorting to the query based on the sort column and direction.
|
|
/// </summary>
|
|
/// <param name="query">The query to sort.</param>
|
|
/// <returns>The sorted query.</returns>
|
|
private IQueryable<Email> ApplySort(IQueryable<Email> query)
|
|
{
|
|
// Apply sort
|
|
switch (SortColumn)
|
|
{
|
|
case "Id":
|
|
query = SortDirection == SortDirection.Ascending
|
|
? query.OrderBy(x => x.Id)
|
|
: query.OrderByDescending(x => x.Id);
|
|
break;
|
|
case "DateSystem":
|
|
query = SortDirection == SortDirection.Ascending
|
|
? query.OrderBy(x => x.DateSystem)
|
|
: query.OrderByDescending(x => x.DateSystem);
|
|
break;
|
|
case "From":
|
|
query = SortDirection == SortDirection.Ascending
|
|
? query.OrderBy(x => x.FromLocal + "@" + x.FromDomain)
|
|
: query.OrderByDescending(x => x.FromLocal + "@" + x.FromDomain);
|
|
break;
|
|
case "To":
|
|
query = SortDirection == SortDirection.Ascending
|
|
? query.OrderBy(x => x.ToLocal + "@" + x.ToDomain)
|
|
: query.OrderByDescending(x => x.ToLocal + "@" + x.ToDomain);
|
|
break;
|
|
case "Attachments":
|
|
query = SortDirection == SortDirection.Ascending
|
|
? query.OrderBy(x => x.Attachments.Count)
|
|
: query.OrderByDescending(x => x.Attachments.Count);
|
|
break;
|
|
default:
|
|
query = query.OrderByDescending(x => x.DateSystem);
|
|
break;
|
|
}
|
|
|
|
return query;
|
|
}
|
|
|
|
private sealed class EmailViewModel
|
|
{
|
|
public Email Email { get; set; } = new();
|
|
public string UserId { get; set; } = string.Empty;
|
|
public string UserName { get; set; } = string.Empty;
|
|
}
|
|
|
|
protected override void Dispose(bool disposing)
|
|
{
|
|
if (disposing)
|
|
{
|
|
_searchCancellationTokenSource?.Cancel();
|
|
_searchCancellationTokenSource?.Dispose();
|
|
}
|
|
base.Dispose(disposing);
|
|
}
|
|
}
|