mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-03-06 07:48:12 -05:00
467 lines
16 KiB
Plaintext
467 lines
16 KiB
Plaintext
@using System.Net
|
|
@using System.Text.Json
|
|
@using AliasVault.Shared.Models.Spamok
|
|
@using AliasVault.Shared.Models.WebApi
|
|
@inherits ComponentBase
|
|
@inject IHttpClientFactory HttpClientFactory
|
|
@inject HttpClient HttpClient
|
|
@inject JsInteropService JsInteropService
|
|
@inject DbService DbService
|
|
@inject Config Config
|
|
@inject EmailService EmailService
|
|
@using AliasVault.Shared.Core
|
|
@inject ILogger<RecentEmails> Logger
|
|
@implements IAsyncDisposable
|
|
|
|
@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">Email</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="Auto-refresh enabled"></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.5 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">
|
|
Refresh
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
@if (IsLoading)
|
|
{
|
|
<LoadingIndicator/>
|
|
}
|
|
else if (!string.IsNullOrEmpty(Error))
|
|
{
|
|
<AlertMessageError Message="@Error" />
|
|
}
|
|
else if (MailboxEmails.Count == 0)
|
|
{
|
|
<div class="text-gray-500 dark:text-gray-400">No emails received (yet).</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">
|
|
Subject
|
|
</th>
|
|
<th scope="col" class="p-4 text-xs font-medium tracking-wider text-left text-gray-500 uppercase dark:text-white">
|
|
Date
|
|
</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 { get; set; } = true;
|
|
|
|
private bool IsSpamOk { get; set; } = false;
|
|
|
|
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()
|
|
{
|
|
IsLoading = true;
|
|
StateHasChanged();
|
|
CloseEmailModal();
|
|
await LoadRecentEmailsAsync();
|
|
IsLoading = false;
|
|
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 = "The current chosen email address is already in use. Please change the email address by editing this credential.";
|
|
break;
|
|
case "CLAIM_DOES_NOT_EXIST":
|
|
Error = "An error occurred while trying to load the emails. Please try to edit and " +
|
|
"save the credential entry to synchronize the database, then try again.";
|
|
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();
|
|
}
|
|
}
|