mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-04-04 23:05:19 -04:00
748 lines
28 KiB
Plaintext
748 lines
28 KiB
Plaintext
@page "/emails"
|
|
@using System.Net
|
|
@using System.Text
|
|
@using System.Text.Json
|
|
@using AliasVault.Client.Main.Pages.Emails.Models
|
|
@using AliasVault.Shared.Models.Spamok
|
|
@using AliasVault.Shared.Models.WebApi
|
|
@using AliasVault.Shared.Models.WebApi.Email
|
|
@using AliasVault.Client.Main.Components
|
|
@using AliasVault.Client.Main.Components.Email
|
|
@using AliasVault.Client.Main.Services
|
|
@inherits MainBase
|
|
@inject HttpClient HttpClient
|
|
@inject ILogger<Home> Logger
|
|
@inject MinDurationLoadingService LoadingService
|
|
@using Microsoft.Extensions.Localization
|
|
@implements IAsyncDisposable
|
|
|
|
<LayoutPageTitle>@Localizer["PageTitle"]</LayoutPageTitle>
|
|
|
|
@if (EmailModalVisible)
|
|
{
|
|
<EmailModal Email="EmailModalEmail" IsSpamOk="false" OnClose="CloseEmailModal" OnEmailDeleted="RefreshData" />
|
|
}
|
|
|
|
<PageHeader
|
|
BreadcrumbItems="@BreadcrumbItems"
|
|
Title="@Localizer["PageTitle"]"
|
|
Description="@Localizer["PageDescription"]">
|
|
<CustomActions>
|
|
@if (DbService.Settings.AutoEmailRefresh)
|
|
{
|
|
<div class="w-3 h-3 mr-2 rounded-full bg-primary-300 border-2 border-primary-100 animate-pulse" title="@Localizer["AutoRefreshEnabledTooltip"]"></div>
|
|
}
|
|
<RefreshButton OnClick="RefreshData" ButtonText="@Localizer["RefreshButton"]" />
|
|
</CustomActions>
|
|
</PageHeader>
|
|
|
|
@if (IsLoading)
|
|
{
|
|
<div class="px-4">
|
|
<!-- Mobile Skeleton -->
|
|
<div class="block lg:hidden mt-6">
|
|
<div class="bg-white border rounded-lg dark:bg-gray-800 dark:border-gray-700 overflow-hidden">
|
|
<ul class="divide-y divide-gray-200 dark:divide-gray-600">
|
|
@for (int i = 0; i < 5; i++)
|
|
{
|
|
<EmailRowSkeleton />
|
|
}
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Desktop Skeleton -->
|
|
<div class="hidden lg:flex mt-6 h-[calc(100vh-300px)] min-h-[600px]">
|
|
<!-- Left Sidebar Skeleton -->
|
|
<div class="w-1/4 bg-white border rounded-l-lg dark:bg-gray-800 dark:border-gray-700 overflow-hidden">
|
|
<div class="h-full overflow-y-auto">
|
|
<ul class="divide-y divide-gray-200 dark:divide-gray-600">
|
|
@for (int i = 0; i < 5; i++)
|
|
{
|
|
<EmailRowSkeleton />
|
|
}
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Right Panel Skeleton -->
|
|
<div class="w-3/4">
|
|
<EmailPreviewSkeleton />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
else if (NoEmailClaims)
|
|
{
|
|
<div class="p-4 mx-4 mt-4 bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 dark:bg-gray-800">
|
|
<div class="px-4 py-2 text-gray-400 rounded">
|
|
<p class="text-gray-500 dark:text-gray-400">@Localizer["NoEmailClaimsMessage"]</p>
|
|
</div>
|
|
</div>
|
|
}
|
|
else
|
|
{
|
|
<div class="px-4">
|
|
<!-- Mobile Layout (sm and down) - Modal behavior with traditional pagination -->
|
|
<div class="block lg:hidden mt-6">
|
|
<ResponsivePaginator CurrentPage="CurrentPage" PageSize="PageSize" TotalRecords="TotalRecords" OnPageChanged="HandlePageChanged"/>
|
|
|
|
<div class="bg-white border rounded-lg dark:bg-gray-800 dark:border-gray-700 overflow-hidden mt-4">
|
|
<ul class="divide-y divide-gray-200 dark:divide-gray-600">
|
|
@if (EmailList.Count == 0)
|
|
{
|
|
<li class="p-4 text-center text-gray-500 dark:text-gray-300">
|
|
@Localizer["NoEmailsReceivedMessage"]
|
|
</li>
|
|
}
|
|
else
|
|
{
|
|
@foreach (var email in EmailList)
|
|
{
|
|
<EmailRow
|
|
Email="email"
|
|
OnEmailClick="ShowAliasVaultEmailInModal"
|
|
IsSelected="false"
|
|
IsNewEmail="@(NewEmailIds.Contains(email.Id))" />
|
|
}
|
|
}
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Desktop Layout (lg and up) - Sidebar and Preview with Load More -->
|
|
<div class="hidden lg:flex mt-6 rounded-lg overflow-hidden">
|
|
@if (EmailList.Count == 0)
|
|
{
|
|
<!-- Single row message for desktop when no emails -->
|
|
<div class="w-full bg-white border rounded-lg dark:bg-gray-800 dark:border-gray-700 overflow-hidden">
|
|
<div class="p-4 text-center text-gray-500 dark:text-gray-300">
|
|
@Localizer["NoEmailsReceivedMessage"]
|
|
</div>
|
|
</div>
|
|
}
|
|
else
|
|
{
|
|
<div class="w-full h-[calc(100vh-300px)] min-h-[600px] flex rounded-lg overflow-hidden">
|
|
<!-- Left Sidebar - Email List -->
|
|
<div class="w-1/4 bg-white border border-r-0 dark:bg-gray-800 dark:border-gray-700 flex flex-col">
|
|
<div class="flex-1 overflow-y-auto" id="email-list-container">
|
|
<ul>
|
|
@foreach (var email in EmailList)
|
|
{
|
|
<EmailRow
|
|
Email="email"
|
|
OnEmailClick="SelectEmailForPreview"
|
|
IsSelected="@(SelectedEmailId == email.Id)"
|
|
IsNewEmail="@(NewEmailIds.Contains(email.Id))" />
|
|
}
|
|
<!-- Load More Button for Desktop -->
|
|
@if (HasMoreEmails && EmailList.Count > 0)
|
|
{
|
|
<li class="border-t border-gray-200 dark:border-gray-600 p-3 bg-gray-50 dark:bg-gray-700">
|
|
<button @onclick="LoadMoreEmails"
|
|
disabled="@IsLoadingMore"
|
|
class="w-full px-4 py-2 text-sm font-medium text-primary-600 bg-primary-50 border border-primary-200 rounded-md hover:bg-primary-100 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed dark:text-primary-400 dark:bg-primary-900/20 dark:border-primary-800 dark:hover:bg-primary-900/30">
|
|
@if (IsLoadingMore)
|
|
{
|
|
<span class="flex items-center justify-center">
|
|
<svg class="animate-spin -ml-1 mr-2 h-4 w-4" fill="none" viewBox="0 0 24 24">
|
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
</svg>
|
|
@Localizer["LoadingText"]
|
|
</span>
|
|
}
|
|
else
|
|
{
|
|
<span>@string.Format(Localizer["LoadMoreButtonText"], TotalRecords - EmailList.Count)</span>
|
|
}
|
|
</button>
|
|
</li>
|
|
}
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Right Panel - Email Preview -->
|
|
<div class="w-3/4">
|
|
<EmailPreview
|
|
Email="SelectedEmail"
|
|
IsSpamOk="false"
|
|
OnEmailDeleted="HandleEmailDeleted"
|
|
CredentialId="@GetSelectedEmailCredentialId()"
|
|
CredentialName="@GetSelectedEmailCredentialName()"
|
|
OnCredentialClick="NavigateToCredential" />
|
|
</div>
|
|
</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
@code {
|
|
private IStringLocalizer Localizer => LocalizerFactory.Create("Pages.Main.Emails.Home", "AliasVault.Client");
|
|
private List<MailListViewModel> EmailList { get; set; } = [];
|
|
private bool IsLoading => LoadingService.IsLoading("emails");
|
|
private int CurrentPage { get; set; } = 1;
|
|
private int PageSize { get; set; } = 50;
|
|
private int TotalRecords { get; set; }
|
|
private bool EmailModalVisible { get; set; }
|
|
private bool NoEmailClaims { get; set; }
|
|
private EmailApiModel EmailModalEmail { get; set; } = new();
|
|
private int? SelectedEmailId { get; set; }
|
|
private EmailApiModel? SelectedEmail { get; set; }
|
|
private bool IsLoadingMore { get; set; }
|
|
private bool HasMoreEmails => TotalRecords > EmailList.Count;
|
|
|
|
// Auto-refresh related properties
|
|
private const int ACTIVE_TAB_REFRESH_INTERVAL = 2000; // 2 seconds
|
|
private CancellationTokenSource? _pollingCts;
|
|
private DotNetObjectReference<Home>? _dotNetRef;
|
|
private bool _isPageVisible = true;
|
|
private HashSet<int> NewEmailIds { get; set; } = new();
|
|
private HashSet<int> _knownEmailIds = new();
|
|
|
|
/// <summary>
|
|
/// Callback invoked by JavaScript when the page visibility changes. This is used to start/stop the polling for new emails.
|
|
/// </summary>
|
|
/// <param name="isVisible">Indicates whether the page is visible or not.</param>
|
|
[JSInvokable]
|
|
public void OnVisibilityChange(bool isVisible)
|
|
{
|
|
_isPageVisible = isVisible;
|
|
|
|
if (isVisible && DbService.Settings.AutoEmailRefresh)
|
|
{
|
|
// Start polling if visible and auto-refresh is enabled
|
|
StartPolling();
|
|
}
|
|
else
|
|
{
|
|
// Stop polling if hidden
|
|
StopPolling();
|
|
}
|
|
|
|
// If becoming visible, do an immediate refresh
|
|
if (isVisible)
|
|
{
|
|
_ = CheckForNewEmails();
|
|
}
|
|
}
|
|
|
|
private void StartPolling()
|
|
{
|
|
// If already polling, no need to start again
|
|
if (_pollingCts != null) {
|
|
return;
|
|
}
|
|
|
|
_pollingCts = new CancellationTokenSource();
|
|
|
|
// Start polling task
|
|
_ = PollForNewEmails(_pollingCts.Token);
|
|
}
|
|
|
|
private void StopPolling()
|
|
{
|
|
if (_pollingCts != null)
|
|
{
|
|
_pollingCts.Cancel();
|
|
_pollingCts.Dispose();
|
|
_pollingCts = null;
|
|
}
|
|
}
|
|
|
|
private async Task PollForNewEmails(CancellationToken cancellationToken)
|
|
{
|
|
try
|
|
{
|
|
while (!cancellationToken.IsCancellationRequested)
|
|
{
|
|
await CheckForNewEmails();
|
|
await Task.Delay(ACTIVE_TAB_REFRESH_INTERVAL, cancellationToken);
|
|
}
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
// Normal cancellation, ignore.
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.LogError(ex, "Error in email refresh polling");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Load emails from the server with the given parameters.
|
|
/// </summary>
|
|
private async Task<(List<MailListViewModel> Emails, int TotalRecords, int CurrentPage, int PageSize)?> LoadEmailsFromServerAsync(int page, int pageSize, List<string> emailClaimList)
|
|
{
|
|
if (emailClaimList.Count == 0)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var requestModel = new MailboxBulkRequest
|
|
{
|
|
Page = page,
|
|
PageSize = pageSize,
|
|
Addresses = emailClaimList,
|
|
};
|
|
|
|
var request = new HttpRequestMessage(HttpMethod.Post, $"v1/EmailBox/bulk");
|
|
request.Content = new StringContent(JsonSerializer.Serialize(requestModel), Encoding.UTF8, "application/json");
|
|
|
|
try
|
|
{
|
|
var response = await HttpClient.SendAsync(request);
|
|
if (response.IsSuccessStatusCode)
|
|
{
|
|
var mailbox = await response.Content.ReadFromJsonAsync<MailboxBulkResponse>();
|
|
if (mailbox?.Mails != null)
|
|
{
|
|
var context = await DbService.GetDbContextAsync();
|
|
var credentialLookup = await context.Items
|
|
.Include(x => x.FieldValues)
|
|
.Where(x => !x.IsDeleted && x.DeletedAt == null)
|
|
.Where(x => x.FieldValues.Any(fv => fv.FieldKey == AliasClientDb.Models.FieldKey.LoginEmail && fv.Value != null && !fv.IsDeleted))
|
|
.Select(x => new {
|
|
x.Id,
|
|
x.Name,
|
|
Email = x.FieldValues.FirstOrDefault(fv => fv.FieldKey == AliasClientDb.Models.FieldKey.LoginEmail && !fv.IsDeleted)!.Value!.ToLower()
|
|
})
|
|
.GroupBy(x => x.Email)
|
|
.ToDictionaryAsync(
|
|
g => g.Key,
|
|
g => new { Id = g.First().Id, ServiceName = g.First().Name ?? "Unknown" }
|
|
);
|
|
|
|
List<MailboxEmailApiModel> decryptedEmailList;
|
|
try
|
|
{
|
|
decryptedEmailList = await EmailService.DecryptEmailList(mailbox.Mails);
|
|
}
|
|
catch (InvalidOperationException ex) when (ex.Message.Contains("Sequence contains no matching element"))
|
|
{
|
|
// Handle case where encryption keys are not available for some emails
|
|
Logger.LogWarning(ex, "Failed to decrypt some emails due to missing encryption keys");
|
|
return null;
|
|
}
|
|
|
|
var emails = decryptedEmailList.Select(email =>
|
|
{
|
|
var toEmail = email.ToLocal + "@" + email.ToDomain;
|
|
var credentialInfo = credentialLookup.TryGetValue(toEmail.ToLower(), out var info)
|
|
? info
|
|
: new { Id = Guid.Empty, ServiceName = "Unknown" };
|
|
|
|
return new MailListViewModel
|
|
{
|
|
Id = email.Id,
|
|
Date = email.DateSystem,
|
|
FromName = email.FromDisplay,
|
|
FromEmail = email.FromLocal + "@" + email.FromDomain,
|
|
ToEmail = toEmail,
|
|
Subject = email.Subject,
|
|
MessagePreview = email.MessagePreview,
|
|
CredentialId = credentialInfo.Id,
|
|
CredentialName = credentialInfo.ServiceName,
|
|
HasAttachments = email.HasAttachments,
|
|
};
|
|
}).ToList();
|
|
|
|
return (emails, mailbox.TotalRecords, mailbox.CurrentPage, mailbox.PageSize);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
var errorContent = await response.Content.ReadAsStringAsync();
|
|
var errorResponse = JsonSerializer.Deserialize<ApiErrorResponse>(errorContent);
|
|
switch (response.StatusCode)
|
|
{
|
|
case HttpStatusCode.BadRequest:
|
|
if (errorResponse != null)
|
|
{
|
|
switch (errorResponse.Code)
|
|
{
|
|
case "CLAIM_DOES_NOT_EXIST":
|
|
GlobalNotificationService.AddErrorMessage(Localizer["ClaimDoesNotExistError"], true);
|
|
break;
|
|
default:
|
|
throw new ArgumentException(errorResponse.Message);
|
|
}
|
|
}
|
|
break;
|
|
case HttpStatusCode.Unauthorized:
|
|
throw new UnauthorizedAccessException(errorResponse?.Message);
|
|
default:
|
|
throw new WebException(errorResponse?.Message);
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
GlobalNotificationService.AddErrorMessage(ex.Message, true);
|
|
Logger.LogError(ex, "An error occurred while loading emails from server");
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Check for new emails without disrupting the current view.
|
|
/// </summary>
|
|
private async Task CheckForNewEmails()
|
|
{
|
|
if (!_isPageVisible || !DbService.Settings.AutoEmailRefresh)
|
|
{
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
var emailClaimList = await DbService.GetEmailClaimListAsync();
|
|
var result = await LoadEmailsFromServerAsync(1, 5, emailClaimList);
|
|
|
|
if (result.HasValue)
|
|
{
|
|
var (newEmails, _, _, _) = result.Value;
|
|
|
|
// Check for new emails
|
|
var newEmailIds = newEmails.Where(email => !_knownEmailIds.Contains(email.Id)).Select(email => email.Id).ToList();
|
|
|
|
// Update the known email IDs
|
|
foreach (var email in newEmails)
|
|
{
|
|
_knownEmailIds.Add(email.Id);
|
|
}
|
|
|
|
// Add new emails to the list and mark them as new
|
|
if (newEmailIds.Count > 0)
|
|
{
|
|
// Add new emails to the beginning of the list
|
|
var emailsToAdd = newEmails.Where(e => newEmailIds.Contains(e.Id)).ToList();
|
|
EmailList.InsertRange(0, emailsToAdd);
|
|
|
|
// Update total records
|
|
TotalRecords += emailsToAdd.Count;
|
|
|
|
// Mark emails as new
|
|
NewEmailIds.UnionWith(newEmailIds);
|
|
|
|
// Remove new email indicators after 30 seconds
|
|
_ = Task.Delay(30000).ContinueWith(_ =>
|
|
{
|
|
NewEmailIds.ExceptWith(newEmailIds);
|
|
InvokeAsync(StateHasChanged);
|
|
});
|
|
|
|
StateHasChanged();
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.LogError(ex, "An error occurred while checking for new emails");
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
protected override async Task OnInitializedAsync()
|
|
{
|
|
await base.OnInitializedAsync();
|
|
|
|
// Create a single object reference for JS interop
|
|
_dotNetRef = DotNetObjectReference.Create(this);
|
|
await JsInteropService.RegisterVisibilityCallback(_dotNetRef);
|
|
|
|
// Only start polling if auto-refresh is enabled and page is visible
|
|
if (DbService.Settings.AutoEmailRefresh && _isPageVisible)
|
|
{
|
|
StartPolling();
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async ValueTask DisposeAsync()
|
|
{
|
|
// Stop polling
|
|
StopPolling();
|
|
|
|
// Unregister the visibility callback using the same reference
|
|
if (_dotNetRef != null)
|
|
{
|
|
await JsInteropService.UnregisterVisibilityCallback(_dotNetRef);
|
|
_dotNetRef.Dispose();
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
|
{
|
|
await base.OnAfterRenderAsync(firstRender);
|
|
|
|
if (firstRender)
|
|
{
|
|
await RefreshData();
|
|
}
|
|
}
|
|
|
|
private void HandlePageChanged(int newPage)
|
|
{
|
|
CurrentPage = newPage;
|
|
_ = RefreshData();
|
|
}
|
|
|
|
private async Task RefreshData()
|
|
{
|
|
LoadingService.StartLoading("emails", 300, StateHasChanged);
|
|
NoEmailClaims = false;
|
|
CloseEmailModal();
|
|
|
|
// Clear selected email when refreshing
|
|
SelectedEmailId = null;
|
|
SelectedEmail = null;
|
|
|
|
// Reset pagination for fresh load
|
|
CurrentPage = 1;
|
|
EmailList.Clear();
|
|
NewEmailIds.Clear();
|
|
_knownEmailIds.Clear();
|
|
|
|
var emailClaimList = await DbService.GetEmailClaimListAsync();
|
|
|
|
if (emailClaimList.Count == 0)
|
|
{
|
|
LoadingService.FinishLoading("emails", StateHasChanged);
|
|
NoEmailClaims = true;
|
|
return;
|
|
}
|
|
|
|
var result = await LoadEmailsFromServerAsync(CurrentPage, PageSize, emailClaimList);
|
|
|
|
if (result.HasValue)
|
|
{
|
|
var (emails, totalRecords, currentPage, pageSize) = result.Value;
|
|
await UpdateMailboxEmails(emails, totalRecords, currentPage, pageSize, false);
|
|
}
|
|
|
|
LoadingService.FinishLoading("emails", StateHasChanged);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Update the local mailbox emails.
|
|
/// </summary>
|
|
private async Task UpdateMailboxEmails(List<MailListViewModel> emails, int totalRecords, int currentPage, int pageSize, bool appendToList = false)
|
|
{
|
|
if (appendToList)
|
|
{
|
|
EmailList.AddRange(emails);
|
|
}
|
|
else
|
|
{
|
|
EmailList = emails;
|
|
// Initialize known email IDs for auto-refresh - don't mark existing emails as new
|
|
_knownEmailIds = new HashSet<int>(emails.Select(e => e.Id));
|
|
// Clear any existing new email indicators since this is the initial load
|
|
NewEmailIds.Clear();
|
|
}
|
|
|
|
CurrentPage = currentPage;
|
|
PageSize = pageSize;
|
|
TotalRecords = totalRecords;
|
|
|
|
// Auto-select first email on desktop layout if none is selected and emails exist (only for initial load)
|
|
if (!appendToList && EmailList.Count > 0 && SelectedEmailId == null)
|
|
{
|
|
var firstEmail = EmailList[0];
|
|
SelectedEmailId = firstEmail.Id;
|
|
await LoadSelectedEmailForPreview(firstEmail.Id);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Load recent emails from AliasVault.
|
|
/// </summary>
|
|
private async Task ShowAliasVaultEmailInModal(int emailId)
|
|
{
|
|
// Remove new email mark when email is clicked
|
|
NewEmailIds.Remove(emailId);
|
|
|
|
EmailApiModel? mail = await HttpClient.GetFromJsonAsync<EmailApiModel>($"v1/Email/{emailId}");
|
|
if (mail != null)
|
|
{
|
|
// Decrypt the email content locally.
|
|
var context = await DbService.GetDbContextAsync();
|
|
var privateKey = await context.EncryptionKeys.FirstOrDefaultAsync(x => x.PublicKey == mail.EncryptionKey);
|
|
if (privateKey is not null)
|
|
{
|
|
mail = await EmailService.DecryptEmail(mail);
|
|
}
|
|
|
|
EmailModalEmail = mail;
|
|
EmailModalVisible = true;
|
|
StateHasChanged();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Close the email modal.
|
|
/// </summary>
|
|
private void CloseEmailModal()
|
|
{
|
|
EmailModalVisible = false;
|
|
StateHasChanged();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Navigate to the credential page.
|
|
/// </summary>
|
|
private void NavigateToCredential(Guid credentialId)
|
|
{
|
|
NavigationManager.NavigateTo($"/items/{credentialId}");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Select an email for preview (desktop layout).
|
|
/// </summary>
|
|
private async Task SelectEmailForPreview(int emailId)
|
|
{
|
|
// Remove new email mark when email is clicked
|
|
NewEmailIds.Remove(emailId);
|
|
|
|
SelectedEmailId = emailId;
|
|
await LoadSelectedEmailForPreview(emailId);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Load the full email data for preview.
|
|
/// </summary>
|
|
private async Task LoadSelectedEmailForPreview(int emailId)
|
|
{
|
|
try
|
|
{
|
|
EmailApiModel? mail = await HttpClient.GetFromJsonAsync<EmailApiModel>($"v1/Email/{emailId}");
|
|
if (mail != null)
|
|
{
|
|
// Decrypt the email content locally.
|
|
var context = await DbService.GetDbContextAsync();
|
|
var privateKey = await context.EncryptionKeys.FirstOrDefaultAsync(x => x.PublicKey == mail.EncryptionKey);
|
|
if (privateKey is not null)
|
|
{
|
|
mail = await EmailService.DecryptEmail(mail);
|
|
}
|
|
|
|
SelectedEmail = mail;
|
|
StateHasChanged();
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
GlobalNotificationService.AddErrorMessage(string.Format(Localizer["LoadEmailsFailedMessage"], ex.Message), true);
|
|
Logger.LogError(ex, "An error occurred while loading email for preview");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handle email deletion from preview panel.
|
|
/// </summary>
|
|
private async Task HandleEmailDeleted(int emailId)
|
|
{
|
|
// Remove the deleted email from the list
|
|
var deletedEmailIndex = EmailList.FindIndex(e => e.Id == emailId);
|
|
EmailList.RemoveAll(e => e.Id == emailId);
|
|
|
|
// Remove from tracking sets
|
|
NewEmailIds.Remove(emailId);
|
|
_knownEmailIds.Remove(emailId);
|
|
|
|
// Update total records
|
|
TotalRecords = Math.Max(0, TotalRecords - 1);
|
|
|
|
// Handle selection logic
|
|
if (SelectedEmailId == emailId)
|
|
{
|
|
SelectedEmailId = null;
|
|
SelectedEmail = null;
|
|
|
|
// Try to select the next email in the list
|
|
if (EmailList.Count > 0)
|
|
{
|
|
// If we deleted the first email, select the new first email
|
|
if (deletedEmailIndex == 0)
|
|
{
|
|
var nextEmail = EmailList[0];
|
|
SelectedEmailId = nextEmail.Id;
|
|
await LoadSelectedEmailForPreview(nextEmail.Id);
|
|
}
|
|
// If we deleted an email in the middle, select the email at the same index (or the last one if index is out of bounds)
|
|
else if (deletedEmailIndex > 0)
|
|
{
|
|
var newIndex = Math.Min(deletedEmailIndex, EmailList.Count - 1);
|
|
var nextEmail = EmailList[newIndex];
|
|
SelectedEmailId = nextEmail.Id;
|
|
await LoadSelectedEmailForPreview(nextEmail.Id);
|
|
}
|
|
}
|
|
}
|
|
|
|
StateHasChanged();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the credential ID for the currently selected email.
|
|
/// </summary>
|
|
private Guid GetSelectedEmailCredentialId()
|
|
{
|
|
if (SelectedEmailId == null) return Guid.Empty;
|
|
|
|
var selectedEmailListItem = EmailList.FirstOrDefault(e => e.Id == SelectedEmailId);
|
|
return selectedEmailListItem?.CredentialId ?? Guid.Empty;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the credential name for the currently selected email.
|
|
/// </summary>
|
|
private string GetSelectedEmailCredentialName()
|
|
{
|
|
if (SelectedEmailId == null) return string.Empty;
|
|
|
|
var selectedEmailListItem = EmailList.FirstOrDefault(e => e.Id == SelectedEmailId);
|
|
return selectedEmailListItem?.CredentialName ?? string.Empty;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Load more emails for the desktop view.
|
|
/// </summary>
|
|
private async Task LoadMoreEmails()
|
|
{
|
|
if (IsLoadingMore || !HasMoreEmails) return;
|
|
|
|
IsLoadingMore = true;
|
|
StateHasChanged();
|
|
|
|
try
|
|
{
|
|
var emailClaimList = await DbService.GetEmailClaimListAsync();
|
|
var result = await LoadEmailsFromServerAsync(CurrentPage + 1, PageSize, emailClaimList);
|
|
|
|
if (result.HasValue)
|
|
{
|
|
var (emails, totalRecords, currentPage, pageSize) = result.Value;
|
|
await UpdateMailboxEmails(emails, totalRecords, currentPage, pageSize, true); // Append to existing list
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
GlobalNotificationService.AddErrorMessage(string.Format(Localizer["LoadMoreEmailsFailedMessage"], ex.Message), true);
|
|
Logger.LogError(ex, "An error occurred while loading more emails");
|
|
}
|
|
finally
|
|
{
|
|
IsLoadingMore = false;
|
|
StateHasChanged();
|
|
}
|
|
}
|
|
}
|