mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-01-25 14:27:52 -05:00
504 lines
19 KiB
Plaintext
504 lines
19 KiB
Plaintext
@using System.Net
|
|
@using System.Text.Json
|
|
@using AliasVault.Shared.Models.Spamok
|
|
@using AliasVault.Shared.Models.WebApi
|
|
@using AliasVault.Client.Main.Services
|
|
@inherits ComponentBase
|
|
@inject IHttpClientFactory HttpClientFactory
|
|
@inject HttpClient HttpClient
|
|
@inject JsInteropService JsInteropService
|
|
@inject DbService DbService
|
|
@inject EmailService EmailService
|
|
@using AliasVault.Shared.Core
|
|
@inject ILogger<RecentEmails> Logger
|
|
@inject MinDurationLoadingService LoadingService
|
|
@inject IStringLocalizerFactory LocalizerFactory
|
|
@implements IAsyncDisposable
|
|
@using Microsoft.Extensions.Localization
|
|
|
|
@if (EmailModalVisible)
|
|
{
|
|
<EmailModal Email="@Email" IsSpamOk="@IsSpamOk" OnClose="CloseEmailModal" OnEmailDeleted="ManualRefresh" />
|
|
}
|
|
|
|
@if (ShowComponent)
|
|
{
|
|
<div class="p-4 mb-4 bg-white border border-gray-200 rounded-lg shadow-sm 2xl:col-span-2 dark:border-gray-700 sm:p-6 dark:bg-gray-800">
|
|
<div class="flex justify-between">
|
|
<div>
|
|
<h3 class="mb-4 text-xl font-semibold dark:text-white">@Localizer["EmailSectionTitle"]</h3>
|
|
</div>
|
|
<div class="flex justify-end items-center space-x-2">
|
|
@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>
|
|
}
|
|
<button id="recent-email-refresh" @onclick="ManualRefresh" type="button" class="text-blue-700 border border-blue-700 hover:bg-blue-700 hover:text-white focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-full text-sm p-2 text-center inline-flex items-center dark:border-blue-500 dark:text-blue-500 dark:hover:text-white dark:focus:ring-blue-800 dark:hover:bg-blue-500">
|
|
<svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2.5" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
@if (IsLoading)
|
|
{
|
|
<div class="flex flex-col mt-6">
|
|
<div class="overflow-x-auto rounded-lg">
|
|
<div class="inline-block min-w-full align-middle">
|
|
<div class="overflow-hidden shadow sm:rounded-lg">
|
|
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-600">
|
|
<thead class="bg-gray-50 dark:bg-gray-700">
|
|
<tr>
|
|
<th scope="col" class="p-4 text-xs font-medium tracking-wider text-left text-gray-500 uppercase dark:text-white">
|
|
@Localizer["SubjectColumn"]
|
|
</th>
|
|
<th scope="col" class="p-4 text-xs font-medium tracking-wider text-left text-gray-500 uppercase dark:text-white">
|
|
@Localizer["DateColumn"]
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="bg-white dark:bg-gray-800">
|
|
@for (int i = 0; i < 2; i++)
|
|
{
|
|
<tr class="hover:bg-gray-50 dark:hover:bg-gray-600">
|
|
<td class="p-4">
|
|
<SkeletonBase Height="20" AdditionalClasses="w-48">
|
|
<div class="w-full h-full bg-gray-300 dark:bg-gray-700 rounded"></div>
|
|
</SkeletonBase>
|
|
</td>
|
|
<td class="p-4">
|
|
<SkeletonBase Height="20" AdditionalClasses="w-24">
|
|
<div class="w-full h-full bg-gray-300 dark:bg-gray-700 rounded"></div>
|
|
</SkeletonBase>
|
|
</td>
|
|
</tr>
|
|
}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
else if (!string.IsNullOrEmpty(Error))
|
|
{
|
|
<AlertMessageError Message="@Error" />
|
|
}
|
|
else if (MailboxEmails.Count == 0)
|
|
{
|
|
<div class="text-gray-500 dark:text-gray-400">@Localizer["NoEmailsReceivedMessage"]</div>
|
|
}
|
|
else
|
|
{
|
|
<div class="flex flex-col mt-6">
|
|
<div class="overflow-x-auto rounded-lg">
|
|
<div class="inline-block min-w-full align-middle">
|
|
<div class="overflow-hidden shadow sm:rounded-lg">
|
|
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-600">
|
|
<thead class="bg-gray-50 dark:bg-gray-700">
|
|
<tr>
|
|
<th scope="col" class="p-4 text-xs font-medium tracking-wider text-left text-gray-500 uppercase dark:text-white">
|
|
@Localizer["SubjectColumn"]
|
|
</th>
|
|
<th scope="col" class="p-4 text-xs font-medium tracking-wider text-left text-gray-500 uppercase dark:text-white">
|
|
@Localizer["DateColumn"]
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="bg-white dark:bg-gray-800">
|
|
@foreach (var mail in MailboxEmails)
|
|
{
|
|
<tr class="hover:bg-gray-50 dark:hover:bg-gray-600">
|
|
<td class="p-4 text-sm font-normal text-gray-900 whitespace-nowrap dark:text-white">
|
|
@if (mail.Subject.Length > 30)
|
|
{
|
|
<span class="cursor-pointer" @onclick="() => OpenEmail(mail.Id)">@(mail.Subject.Substring(0, 30))...</span>
|
|
}
|
|
else
|
|
{
|
|
<span class="cursor-pointer" @onclick="() => OpenEmail(mail.Id)">@mail.Subject</span>
|
|
}
|
|
</td>
|
|
<td class="p-4 text-sm font-normal text-gray-500 whitespace-nowrap dark:text-gray-400">
|
|
<span class="cursor-pointer" @onclick="() => OpenEmail(mail.Id)">@mail.DateSystem.ToString("yyyy-MM-dd")</span>
|
|
</td>
|
|
</tr>
|
|
}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
</div>
|
|
}
|
|
|
|
@code {
|
|
/// <summary>
|
|
/// The email address to show recent emails for.
|
|
/// </summary>
|
|
[Parameter]
|
|
public string? EmailAddress { get; set; } = string.Empty;
|
|
|
|
private List<MailboxEmailApiModel> MailboxEmails { get; set; } = new();
|
|
private bool ShowComponent { get; set; } = false;
|
|
private EmailApiModel Email { get; set; } = new();
|
|
private bool EmailModalVisible { get; set; }
|
|
private string Error { get; set; } = string.Empty;
|
|
private bool IsLoading => LoadingService.IsLoading("recentemails");
|
|
private bool IsSpamOk { get; set; } = false;
|
|
private IStringLocalizer Localizer => LocalizerFactory.Create("Components.Main.Email.RecentEmails", "AliasVault.Client");
|
|
|
|
private const int ACTIVE_TAB_REFRESH_INTERVAL = 2000; // 2 seconds
|
|
private CancellationTokenSource? _pollingCts;
|
|
private DotNetObjectReference<RecentEmails>? _dotNetRef;
|
|
private bool _isPageVisible = true;
|
|
|
|
/// <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)
|
|
{
|
|
_ = ManualRefresh();
|
|
}
|
|
}
|
|
|
|
private void StartPolling()
|
|
{
|
|
// If already polling, no need to start again
|
|
if (_pollingCts != null) {
|
|
return;
|
|
}
|
|
|
|
_pollingCts = new CancellationTokenSource();
|
|
|
|
// Start polling task
|
|
_ = PollForEmails(_pollingCts.Token);
|
|
}
|
|
|
|
private void StopPolling()
|
|
{
|
|
if (_pollingCts != null)
|
|
{
|
|
_pollingCts.Cancel();
|
|
_pollingCts.Dispose();
|
|
_pollingCts = null;
|
|
}
|
|
}
|
|
|
|
private async Task PollForEmails(CancellationToken cancellationToken)
|
|
{
|
|
try
|
|
{
|
|
while (!cancellationToken.IsCancellationRequested)
|
|
{
|
|
await LoadRecentEmailsAsync();
|
|
await Task.Delay(ACTIVE_TAB_REFRESH_INTERVAL, cancellationToken);
|
|
}
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
// Normal cancellation, ignore.
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.LogError(ex, "Error in email refresh polling");
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
protected override async Task OnInitializedAsync()
|
|
{
|
|
await base.OnInitializedAsync();
|
|
|
|
if (EmailAddress is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Check if email has a known SpamOK domain, if not, don't show this component.
|
|
ShowComponent = EmailService.IsAliasVaultSupportedDomain(EmailAddress);
|
|
IsSpamOk = EmailService.IsSpamOkDomain(EmailAddress);
|
|
|
|
// 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 (!ShowComponent)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (firstRender)
|
|
{
|
|
await ManualRefresh();
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
protected override void OnParametersSet()
|
|
{
|
|
base.OnParametersSet();
|
|
|
|
if (EmailAddress is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
IsSpamOk = EmailService.IsSpamOkDomain(EmailAddress);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Manually refresh the emails.
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
private async Task ManualRefresh()
|
|
{
|
|
LoadingService.StartLoading("recentemails", 300, StateHasChanged);
|
|
StateHasChanged();
|
|
CloseEmailModal();
|
|
await LoadRecentEmailsAsync();
|
|
LoadingService.FinishLoading("recentemails", StateHasChanged);
|
|
StateHasChanged();
|
|
}
|
|
|
|
/// <summary>
|
|
/// (Re)load recent emails by making an API call to the server.
|
|
/// </summary>
|
|
/// <returns>Task.</returns>
|
|
private async Task LoadRecentEmailsAsync()
|
|
{
|
|
if (!ShowComponent || EmailAddress is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Get email prefix, which is the part before the @ symbol.
|
|
string emailPrefix = EmailAddress.Split('@')[0];
|
|
|
|
if (EmailService.IsSpamOkDomain(EmailAddress))
|
|
{
|
|
await LoadSpamOkEmails(emailPrefix);
|
|
}
|
|
else if (EmailService.IsAliasVaultDomain(EmailAddress))
|
|
{
|
|
await LoadAliasVaultEmails();
|
|
}
|
|
|
|
StateHasChanged();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Open the email modal.
|
|
/// </summary>
|
|
private async Task OpenEmail(int emailId)
|
|
{
|
|
if (EmailAddress is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Get email prefix, which is the part before the @ symbol.
|
|
string emailPrefix = EmailAddress.Split('@')[0];
|
|
|
|
if (EmailService.IsSpamOkDomain(EmailAddress))
|
|
{
|
|
await ShowSpamOkEmailInModal(emailPrefix, emailId);
|
|
}
|
|
else if (EmailService.IsAliasVaultDomain(EmailAddress))
|
|
{
|
|
await ShowAliasVaultEmailInModal(emailId);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Load recent emails from SpamOK.
|
|
/// </summary>
|
|
private async Task LoadSpamOkEmails(string emailPrefix)
|
|
{
|
|
// We construct a new HttpClient to avoid using the default one, which is used for the API and sends
|
|
// the Authorization header. We don't want to send the Authorization header to the external email API.
|
|
var client = HttpClientFactory.CreateClient("EmailClient");
|
|
var request = new HttpRequestMessage(HttpMethod.Get, $"https://api.spamok.com/v2/EmailBox/{emailPrefix}");
|
|
request.Headers.Add("X-Asdasd-Platform-Id", "av-web");
|
|
request.Headers.Add("X-Asdasd-Platform-Version", AppInfo.GetFullVersion());
|
|
|
|
var response = await client.SendAsync(request);
|
|
if (response.IsSuccessStatusCode)
|
|
{
|
|
var mailbox = await response.Content.ReadFromJsonAsync<MailboxApiModel>();
|
|
if (mailbox != null)
|
|
{
|
|
// Show maximum of 10 recent emails.
|
|
MailboxEmails = mailbox.Mails.Take(10).ToList();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Load recent emails from SpamOK.
|
|
/// </summary>
|
|
private async Task ShowSpamOkEmailInModal(string emailPrefix, int emailId)
|
|
{
|
|
var client = HttpClientFactory.CreateClient("EmailClient");
|
|
var request = new HttpRequestMessage(HttpMethod.Get, $"https://api.spamok.com/v2/Email/{emailPrefix}/{emailId}");
|
|
request.Headers.Add("X-Asdasd-Platform-Id", "av-web");
|
|
request.Headers.Add("X-Asdasd-Platform-Version", AppInfo.GetFullVersion());
|
|
|
|
var response = await client.SendAsync(request);
|
|
if (response.IsSuccessStatusCode)
|
|
{
|
|
var mail = await response.Content.ReadFromJsonAsync<EmailApiModel>();
|
|
if (mail != null)
|
|
{
|
|
Email = mail;
|
|
EmailModalVisible = true;
|
|
StateHasChanged();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Load recent emails from AliasVault.
|
|
/// </summary>
|
|
private async Task LoadAliasVaultEmails()
|
|
{
|
|
var request = new HttpRequestMessage(HttpMethod.Get, $"v1/EmailBox/{EmailAddress}");
|
|
try
|
|
{
|
|
var response = await HttpClient.SendAsync(request);
|
|
if (response.IsSuccessStatusCode)
|
|
{
|
|
var mailbox = await response.Content.ReadFromJsonAsync<MailboxApiModel>();
|
|
await UpdateMailboxEmails(mailbox);
|
|
}
|
|
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_MATCH_USER":
|
|
Error = Localizer["EmailAddressInUseError"];
|
|
break;
|
|
case "CLAIM_DOES_NOT_EXIST":
|
|
Error = Localizer["EmailLoadError"];
|
|
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)
|
|
{
|
|
Error = ex.Message;
|
|
Logger.LogError(ex, "An error occurred while loading AliasVault emails.");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Update the mailbox emails and decrypt the subject locally.
|
|
/// </summary>
|
|
private async Task UpdateMailboxEmails(MailboxApiModel? mailbox)
|
|
{
|
|
if (mailbox?.Mails != null)
|
|
{
|
|
// Show maximum of 10 recent emails.
|
|
MailboxEmails = mailbox.Mails.Take(10).ToList();
|
|
}
|
|
|
|
MailboxEmails = await EmailService.DecryptEmailList(MailboxEmails);
|
|
Error = string.Empty;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Load recent emails from AliasVault.
|
|
/// </summary>
|
|
private async Task ShowAliasVaultEmailInModal(int 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);
|
|
}
|
|
|
|
Email = mail;
|
|
EmailModalVisible = true;
|
|
StateHasChanged();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Close the email modal.
|
|
/// </summary>
|
|
private void CloseEmailModal()
|
|
{
|
|
EmailModalVisible = false;
|
|
StateHasChanged();
|
|
}
|
|
}
|