Files
aliasvault/apps/server/AliasVault.Client/Main/Components/Email/EmailPreview.razor
2025-07-16 11:28:28 +02:00

338 lines
13 KiB
Plaintext

@using AliasVault.Shared.Models.Spamok
@using AliasVault.Shared.Utilities
@using AliasVault.Shared.Core;
@using AliasVault.Client.Main.Components.Layout
@using AliasVault.RazorComponents.Services
@using Microsoft.Extensions.Localization
@inject JsInteropService JsInteropService
@inject GlobalNotificationService GlobalNotificationService
@inject IHttpClientFactory HttpClientFactory
@inject EmailService EmailService
@inject HttpClient HttpClient
@inject ConfirmModalService ConfirmModalService
@inject IStringLocalizerFactory LocalizerFactory
<div class="h-full flex flex-col bg-white border-l border-gray-200 dark:border-gray-700 rounded-l-lg">
@if (Email != null)
{
<!-- Header -->
<div class="p-4 border-b border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-700">
<div class="flex items-center justify-between mb-3">
<h2 class="text-lg font-semibold text-gray-900 dark:text-gray-100 truncate">
@if (IsSpamOk)
{
<a target="_blank" href="https://spamok.com/@(Email!.ToLocal)/@(Email!.Id)" class="hover:underline">@Email.Subject</a>
}
else
{
<span>@Email.Subject</span>
}
</h2>
<button @onclick="ShowDeleteConfirmation" id="delete-email" class="text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300">
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
</svg>
</button>
</div>
<!-- 2-column layout for email details -->
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4 text-sm text-gray-600 dark:text-gray-300">
<div class="space-y-1">
<p><span class="font-medium">@Localizer["FromLabel"]</span> @(Email.FromLocal)@@@(Email.FromDomain)</p>
<p><span class="font-medium">@Localizer["ToLabel"]</span> @(Email.ToLocal)@@@(Email.ToDomain)</p>
</div>
<div class="space-y-1">
<p><span class="font-medium">@Localizer["DateLabel"]</span> @Email.DateSystem</p>
@if (!string.IsNullOrEmpty(CredentialName) && CredentialId != Guid.Empty)
{
<p><span class="font-medium">@Localizer["CredentialLabel"]</span>
<button @onclick="@(() => OnCredentialClick.InvokeAsync(CredentialId))"
class="text-blue-600 hover:underline dark:text-blue-400 cursor-pointer">
@CredentialName
</button>
</p>
}
else
{
<p><span class="font-medium">@Localizer["CredentialLabel"]</span> <span class="text-gray-400 dark:text-gray-500">@Localizer["NoneValue"]</span></p>
}
</div>
</div>
</div>
<!-- Scrollable Content -->
<div class="flex-1 flex flex-col overflow-hidden">
<!-- Email Content - Takes remaining space -->
<div class="flex-1 overflow-y-auto p-4">
<div class="text-gray-700 dark:text-gray-300 h-full">
<iframe class="w-full h-full border-0" srcdoc="@EmailBody">
</iframe>
</div>
</div>
@if (Email.Attachments?.Any() == true)
{
<!-- Attachments Section - Fixed height -->
<div class="border-t border-gray-200 dark:border-gray-600 p-4 bg-gray-50 dark:bg-gray-800">
<h3 class="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">@Localizer["AttachmentsLabel"]</h3>
<div class="grid grid-cols-1 gap-2 max-h-32 overflow-y-auto">
@foreach (var attachment in Email.Attachments)
{
<div class="flex items-center space-x-2">
<svg class="w-4 h-4 text-gray-500 dark:text-gray-400 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.172 7l-6.586 6.586a2 2 0 102.828 2.828l6.414-6.586a4 4 0 00-5.656-5.656l-6.415 6.585a6 6 0 108.486 8.486L20.5 13"></path>
</svg>
<button @onclick="() => DownloadAttachment(attachment)"
class="text-blue-600 hover:underline text-sm truncate dark:text-blue-400 attachment-link">
(@(Math.Ceiling((double)attachment.Filesize / 1024)) KB) @attachment.Filename
</button>
</div>
}
</div>
</div>
}
</div>
}
else
{
<div class="flex bg-gray-50 dark:bg-gray-700 items-center justify-center h-full text-gray-500 dark:text-gray-400">
<div class="text-center">
<svg class="mx-auto h-12 w-12 text-gray-300 dark:text-gray-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 4.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
</svg>
<p class="mt-2 text-sm">@Localizer["SelectEmailMessage"]</p>
</div>
</div>
}
</div>
@code {
private IStringLocalizer Localizer => LocalizerFactory.Create("Components.Main.Email.EmailPreview", "AliasVault.Client");
/// <summary>
/// The email to show in the preview.
/// </summary>
[Parameter]
public EmailApiModel? Email { get; set; }
/// <summary>
/// Boolean that indicates if the email is from SpamOK public API.
/// </summary>
[Parameter]
public bool IsSpamOk { get; set; }
/// <summary>
/// Callback when an email is deleted.
/// </summary>
[Parameter]
public EventCallback<int> OnEmailDeleted { get; set; }
/// <summary>
/// The ID of the credential associated with this email.
/// </summary>
[Parameter]
public Guid CredentialId { get; set; }
/// <summary>
/// The name of the credential associated with this email.
/// </summary>
[Parameter]
public string CredentialName { get; set; } = string.Empty;
/// <summary>
/// Callback when credential is clicked.
/// </summary>
[Parameter]
public EventCallback<Guid> OnCredentialClick { get; set; }
/// <summary>
/// The message body to display
/// </summary>
private string EmailBody = string.Empty;
/// <summary>
/// Show confirmation modal before deleting email.
/// </summary>
private async Task ShowDeleteConfirmation()
{
if (Email == null)
{
return;
}
var result = await ConfirmModalService.ShowConfirmation(
Localizer["DeleteEmailTitle"],
Localizer["DeleteEmailConfirmation"]
);
if (result)
{
await DeleteEmail();
}
}
/// <summary>
/// Delete the current email.
/// </summary>
private async Task DeleteEmail()
{
if (Email == null)
{
return;
}
if (IsSpamOk)
{
await DeleteEmailSpamOk();
}
else
{
await DeleteEmailAliasVault();
}
}
/// <summary>
/// Delete the current email in SpamOk.
/// </summary>
private async Task DeleteEmailSpamOk()
{
if (Email == null)
{
return;
}
try
{
var client = HttpClientFactory.CreateClient("EmailClient");
var request = new HttpRequestMessage(HttpMethod.Delete, $"https://api.spamok.com/v2/Email/{Email.ToLocal}/{Email.Id}");
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)
{
await OnEmailDeleted.InvokeAsync(Email.Id);
GlobalNotificationService.AddSuccessMessage(Localizer["EmailDeletedSuccess"], true);
}
else
{
var errorMessage = await response.Content.ReadAsStringAsync();
GlobalNotificationService.AddErrorMessage($"{Localizer["EmailDeleteFailed"]}: {errorMessage}", true);
}
}
catch (Exception ex)
{
GlobalNotificationService.AddErrorMessage($"{Localizer["GenericError"]}: {ex.Message}", true);
}
}
/// <summary>
/// Delete the current email in AliasVault.
/// </summary>
private async Task DeleteEmailAliasVault()
{
if (Email == null)
{
return;
}
try
{
var response = await HttpClient.DeleteAsync($"v1/Email/{Email.Id}");
if (response.IsSuccessStatusCode)
{
await OnEmailDeleted.InvokeAsync(Email.Id);
GlobalNotificationService.AddSuccessMessage(Localizer["EmailDeletedSuccess"], true);
}
else
{
GlobalNotificationService.AddErrorMessage(Localizer["EmailDeleteFailed"], true);
}
}
catch (Exception ex)
{
GlobalNotificationService.AddErrorMessage($"{Localizer["EmailDeleteFailed"]}: {ex.Message}", true);
}
}
/// <inheritdoc />
protected override async Task OnParametersSetAsync()
{
await base.OnParametersSetAsync();
if (Email != null)
{
// Check if there is HTML content, if not, then set default viewtype to plain
if (Email.MessageHtml is not null && !string.IsNullOrWhiteSpace(Email.MessageHtml))
{
// HTML is available
EmailBody = ConversionUtility.ConvertAnchorTagsToOpenInNewTab(Email.MessageHtml);
}
else if (Email.MessagePlain is not null)
{
// No HTML but plain text is available
EmailBody = Email.MessagePlain;
}
else
{
// No HTML and no plain text available
EmailBody = Localizer["NoEmailBody"];
}
}
}
/// <summary>
/// Download an attachment.
/// </summary>
private async Task DownloadAttachment(AttachmentApiModel attachment)
{
try
{
if (IsSpamOk)
{
var client = HttpClientFactory.CreateClient("EmailClient");
var request = new HttpRequestMessage(HttpMethod.Get, $"https://api.spamok.com/v2/Attachment/{Email!.Id}/{attachment.Id}/download");
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 bytes = await response.Content.ReadAsByteArrayAsync();
await JsInteropService.DownloadFileFromStream(attachment.Filename, bytes);
}
else
{
GlobalNotificationService.AddErrorMessage(Localizer["AttachmentDownloadFailed"], true);
}
}
else
{
var response = await HttpClient.GetAsync($"v1/Email/{Email!.Id}/attachments/{attachment.Id}");
if (response.IsSuccessStatusCode)
{
// Get attachment bytes from API.
var bytes = await response.Content.ReadAsByteArrayAsync();
// Decrypt the attachment locally with email's encryption key.
var decryptedBytes = await EmailService.DecryptEmailAttachment(Email, bytes);
// Offer the decrypted attachment as download to the user's browser.
if (decryptedBytes != null)
{
await JsInteropService.DownloadFileFromStream(attachment.Filename, decryptedBytes);
}
}
else
{
GlobalNotificationService.AddErrorMessage(Localizer["AttachmentDownloadFailed"], true);
}
}
}
catch (Exception ex)
{
GlobalNotificationService.AddErrorMessage($"{Localizer["AttachmentDownloadError"]}: {ex.Message}", true);
}
}
}