@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 Logger @inject MinDurationLoadingService LoadingService @implements IAsyncDisposable @if (EmailModalVisible) { } @if (ShowComponent) {

Email

@if (DbService.Settings.AutoEmailRefresh) {
}
@if (IsLoading) {
@for (int i = 0; i < 2; i++) { }
Subject Date
} else if (!string.IsNullOrEmpty(Error)) { } else if (MailboxEmails.Count == 0) {
No emails received (yet).
} else {
@foreach (var mail in MailboxEmails) { }
Subject Date
@if (mail.Subject.Length > 30) { @(mail.Subject.Substring(0, 30))... } else { @mail.Subject } @mail.DateSystem.ToString("yyyy-MM-dd")
}
} @code { /// /// The email address to show recent emails for. /// [Parameter] public string? EmailAddress { get; set; } = string.Empty; private List 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 const int ACTIVE_TAB_REFRESH_INTERVAL = 2000; // 2 seconds private CancellationTokenSource? _pollingCts; private DotNetObjectReference? _dotNetRef; private bool _isPageVisible = true; /// /// Callback invoked by JavaScript when the page visibility changes. This is used to start/stop the polling for new emails. /// /// Indicates whether the page is visible or not. [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"); } } /// 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(); } } /// public async ValueTask DisposeAsync() { // Stop polling StopPolling(); // Unregister the visibility callback using the same reference if (_dotNetRef != null) { await JsInteropService.UnregisterVisibilityCallback(_dotNetRef); _dotNetRef.Dispose(); } } /// protected override async Task OnAfterRenderAsync(bool firstRender) { await base.OnAfterRenderAsync(firstRender); if (!ShowComponent) { return; } if (firstRender) { await ManualRefresh(); } } /// protected override void OnParametersSet() { base.OnParametersSet(); if (EmailAddress is null) { return; } IsSpamOk = EmailService.IsSpamOkDomain(EmailAddress); } /// /// Manually refresh the emails. /// /// private async Task ManualRefresh() { LoadingService.StartLoading("recentemails", 300, StateHasChanged); StateHasChanged(); CloseEmailModal(); await LoadRecentEmailsAsync(); LoadingService.FinishLoading("recentemails", StateHasChanged); StateHasChanged(); } /// /// (Re)load recent emails by making an API call to the server. /// /// Task. 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(); } /// /// Open the email modal. /// 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); } } /// /// Load recent emails from SpamOK. /// 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(); if (mailbox != null) { // Show maximum of 10 recent emails. MailboxEmails = mailbox.Mails.Take(10).ToList(); } } } /// /// Load recent emails from SpamOK. /// 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(); if (mail != null) { Email = mail; EmailModalVisible = true; StateHasChanged(); } } } /// /// Load recent emails from AliasVault. /// 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(); await UpdateMailboxEmails(mailbox); } else { var errorContent = await response.Content.ReadAsStringAsync(); var errorResponse = JsonSerializer.Deserialize(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."); } } /// /// Update the mailbox emails and decrypt the subject locally. /// 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; } /// /// Load recent emails from AliasVault. /// private async Task ShowAliasVaultEmailInModal(int emailId) { EmailApiModel? mail = await HttpClient.GetFromJsonAsync($"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(); } } /// /// Close the email modal. /// private void CloseEmailModal() { EmailModalVisible = false; StateHasChanged(); } }