feat: refactor MailViewer and add utility functions for email handling and attachment processing
This commit is contained in:
@@ -1,218 +1,89 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { X, MailOpen, Image, FileText, File, ShieldCheck, Shield, Signature, FileCode, Loader2 } from "@lucide/svelte";
|
import {
|
||||||
import { ShowOpenFileDialog, ReadEML, OpenPDF, OpenImageWindow, OpenPDFWindow, OpenImage, ReadMSG, ReadPEC, OpenEMLWindow, ConvertToUTF8, SetCurrentMailFilePath } from "$lib/wailsjs/go/main/App";
|
X,
|
||||||
import type { internal } from "$lib/wailsjs/go/models";
|
MailOpen,
|
||||||
import { sidebarOpen } from "$lib/stores/app";
|
Image,
|
||||||
import { onDestroy, onMount } from "svelte";
|
FileText,
|
||||||
import { toast } from "svelte-sonner";
|
File,
|
||||||
import { EventsOn, WindowShow, WindowUnminimise } from "$lib/wailsjs/runtime/runtime";
|
ShieldCheck,
|
||||||
import { mailState } from "$lib/stores/mail-state.svelte";
|
Signature,
|
||||||
import { settingsStore } from "$lib/stores/settings.svelte";
|
FileCode,
|
||||||
import * as m from "$lib/paraglide/messages";
|
Loader2,
|
||||||
import { dev } from "$app/environment";
|
} from '@lucide/svelte';
|
||||||
import { isBase64, isHtml } from "$lib/utils";
|
import { sidebarOpen } from '$lib/stores/app';
|
||||||
|
import { onDestroy, onMount } from 'svelte';
|
||||||
|
import { toast } from 'svelte-sonner';
|
||||||
|
import { EventsOn, WindowShow, WindowUnminimise } from '$lib/wailsjs/runtime/runtime';
|
||||||
|
import { mailState } from '$lib/stores/mail-state.svelte';
|
||||||
|
import * as m from '$lib/paraglide/messages';
|
||||||
|
import { dev } from '$app/environment';
|
||||||
|
|
||||||
|
// Import refactored utilities
|
||||||
|
import {
|
||||||
|
IFRAME_UTIL_HTML,
|
||||||
|
CONTENT_TYPES,
|
||||||
|
PEC_FILES,
|
||||||
|
arrayBufferToBase64,
|
||||||
|
createDataUrl,
|
||||||
|
openPDFAttachment,
|
||||||
|
openImageAttachment,
|
||||||
|
openEMLAttachment,
|
||||||
|
openAndLoadEmail,
|
||||||
|
loadEmailFromPath,
|
||||||
|
processEmailBody,
|
||||||
|
isEmailFile,
|
||||||
|
} from '$lib/utils/mail';
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// State
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
let unregisterEvents = () => {};
|
let unregisterEvents = () => {};
|
||||||
let isLoading = $state(false);
|
let isLoading = $state(false);
|
||||||
let loadingText = $state("");
|
let loadingText = $state('');
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
let iFrameUtilHTML = "<style>body{margin:0;padding:20px;font-family:sans-serif;} a{pointer-events:none!important;cursor:default!important;}</style><script>function handleWheel(event){if(event.ctrlKey){event.preventDefault();}}document.addEventListener('wheel',handleWheel,{passive:false});<\/script>";
|
// Event Handlers
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
function onClear() {
|
function onClear() {
|
||||||
mailState.clear();
|
mailState.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
$effect(() => {
|
|
||||||
const process = async () => {
|
|
||||||
if (mailState.currentEmail?.body) {
|
|
||||||
let content = mailState.currentEmail.body;
|
|
||||||
// 1. Try to decode if not HTML
|
|
||||||
if (!isHtml(content)) {
|
|
||||||
const clean = content.replace(/[\s\r\n]+/g, '');
|
|
||||||
if (isBase64(clean)) {
|
|
||||||
try {
|
|
||||||
const decoded = window.atob(clean);
|
|
||||||
content = decoded;
|
|
||||||
} catch (e) {
|
|
||||||
console.warn("Failed to decode base64 body:", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. If it is HTML (original or decoded), try to fix encoding
|
|
||||||
if (isHtml(content)) {
|
|
||||||
try {
|
|
||||||
const fixed = await ConvertToUTF8(content);
|
|
||||||
if (fixed) {
|
|
||||||
content = fixed;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.warn("Failed to fix encoding:", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Update if changed
|
|
||||||
if (content !== mailState.currentEmail.body) {
|
|
||||||
mailState.currentEmail.body = content;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(dev) {
|
|
||||||
console.debug("emailObj:", mailState.currentEmail)
|
|
||||||
}
|
|
||||||
console.info("Current email changed:", mailState.currentEmail?.subject);
|
|
||||||
if(mailState.currentEmail !== null) {
|
|
||||||
sidebarOpen.set(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
process();
|
|
||||||
})
|
|
||||||
|
|
||||||
onDestroy(() => {
|
|
||||||
if (unregisterEvents) unregisterEvents();
|
|
||||||
});
|
|
||||||
|
|
||||||
onMount(async () => {
|
|
||||||
// Listen for second instance args
|
|
||||||
unregisterEvents = EventsOn("launchArgs", async (args: string[]) => {
|
|
||||||
console.log("got event launchArgs:", args);
|
|
||||||
if (args && args.length > 0) {
|
|
||||||
for (const arg of args) {
|
|
||||||
const lowerArg = arg.toLowerCase();
|
|
||||||
if (lowerArg.endsWith(".eml") || lowerArg.endsWith(".msg")) {
|
|
||||||
console.log("Loading file from second instance:", arg);
|
|
||||||
isLoading = true;
|
|
||||||
loadingText = m.layout_loading_text();
|
|
||||||
|
|
||||||
try {
|
|
||||||
let emlContent;
|
|
||||||
|
|
||||||
if (lowerArg.endsWith(".msg")) {
|
|
||||||
loadingText = m.mail_loading_msg_conversion();
|
|
||||||
emlContent = await ReadMSG(arg, true);
|
|
||||||
} else {
|
|
||||||
// EML handling
|
|
||||||
try {
|
|
||||||
emlContent = await ReadPEC(arg);
|
|
||||||
} catch (e) {
|
|
||||||
console.warn("ReadPEC failed, trying ReadEML:", e);
|
|
||||||
emlContent = await ReadEML(arg);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (emlContent && emlContent.body) {
|
|
||||||
const trimmed = emlContent.body.trim();
|
|
||||||
const clean = trimmed.replace(/[\s\r\n]+/g, '');
|
|
||||||
if (clean.length > 0 && clean.length % 4 === 0 && /^[A-Za-z0-9+/]+=*$/.test(clean)) {
|
|
||||||
try {
|
|
||||||
emlContent.body = window.atob(clean);
|
|
||||||
} catch (e) { }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mailState.setParams(emlContent);
|
|
||||||
sidebarOpen.set(false);
|
|
||||||
WindowUnminimise();
|
|
||||||
WindowShow();
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to load email:", error);
|
|
||||||
toast.error("Failed to load email file");
|
|
||||||
} finally {
|
|
||||||
isLoading = false;
|
|
||||||
loadingText = "";
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
async function openPDFHandler(base64Data: string, filename: string) {
|
|
||||||
try {
|
|
||||||
if (settingsStore.settings.useBuiltinPDFViewer) {
|
|
||||||
await OpenPDFWindow(base64Data, filename);
|
|
||||||
} else {
|
|
||||||
await OpenPDF(base64Data, filename);
|
|
||||||
}
|
|
||||||
} catch (error: string | any) {
|
|
||||||
if(error.includes(filename) && error.includes("already open")) {
|
|
||||||
toast.error(m.mail_pdf_already_open());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
console.error("Failed to open PDF:", error);
|
|
||||||
toast.error(m.mail_error_pdf());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function openImageHandler(base64Data: string, filename: string) {
|
|
||||||
try {
|
|
||||||
if (settingsStore.settings.useBuiltinPreview) {
|
|
||||||
await OpenImageWindow(base64Data, filename);
|
|
||||||
} else {
|
|
||||||
await OpenImage(base64Data, filename);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to open image:", error);
|
|
||||||
toast.error(m.mail_error_image());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function openEMLHandler(base64Data: string, filename: string) {
|
|
||||||
try {
|
|
||||||
await OpenEMLWindow(base64Data, filename);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to open EML:", error);
|
|
||||||
toast.error("Failed to open EML attachment");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function onOpenMail() {
|
async function onOpenMail() {
|
||||||
isLoading = true;
|
isLoading = true;
|
||||||
loadingText = m.layout_loading_text();
|
loadingText = m.layout_loading_text();
|
||||||
const result = await ShowOpenFileDialog();
|
|
||||||
if (result && result.length > 0) {
|
|
||||||
// Handle opening the mail file
|
|
||||||
try {
|
|
||||||
// If the file is .eml, otherwise if is .msg, read accordingly
|
|
||||||
let email: internal.EmailData;
|
|
||||||
if(result.toLowerCase().endsWith(".msg")) {
|
|
||||||
loadingText = m.mail_loading_msg_conversion();
|
|
||||||
email = await ReadMSG(result, true);
|
|
||||||
} else {
|
|
||||||
email = await ReadEML(result);
|
|
||||||
}
|
|
||||||
// Track the current mail file path for bug reports
|
|
||||||
await SetCurrentMailFilePath(result);
|
|
||||||
mailState.setParams(email);
|
|
||||||
sidebarOpen.set(false);
|
|
||||||
|
|
||||||
} catch (error) {
|
const result = await openAndLoadEmail();
|
||||||
console.error("Failed to read EML file:", error);
|
|
||||||
toast.error(m.mail_error_opening());
|
if (result.cancelled) {
|
||||||
} finally {
|
isLoading = false;
|
||||||
isLoading = false;
|
loadingText = '';
|
||||||
loadingText = "";
|
return;
|
||||||
}
|
|
||||||
} else {
|
|
||||||
isLoading = false;
|
|
||||||
loadingText = "";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (result.success && result.email) {
|
||||||
|
mailState.setParams(result.email);
|
||||||
|
sidebarOpen.set(false);
|
||||||
|
} else if (result.error) {
|
||||||
|
console.error('Failed to read email file:', result.error);
|
||||||
|
toast.error(m.mail_error_opening());
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoading = false;
|
||||||
|
loadingText = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
function arrayBufferToBase64(buffer: any): string {
|
async function handleOpenPDF(base64Data: string, filename: string) {
|
||||||
if (typeof buffer === "string") return buffer; // Already base64 string
|
await openPDFAttachment(base64Data, filename);
|
||||||
if (Array.isArray(buffer)) {
|
}
|
||||||
let binary = "";
|
|
||||||
const bytes = new Uint8Array(buffer);
|
async function handleOpenImage(base64Data: string, filename: string) {
|
||||||
const len = bytes.byteLength;
|
await openImageAttachment(base64Data, filename);
|
||||||
for (let i = 0; i < len; i++) {
|
}
|
||||||
binary += String.fromCharCode(bytes[i]);
|
|
||||||
}
|
async function handleOpenEML(base64Data: string, filename: string) {
|
||||||
return window.btoa(binary);
|
await openEMLAttachment(base64Data, filename);
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleWheel(event: WheelEvent) {
|
function handleWheel(event: WheelEvent) {
|
||||||
@@ -220,6 +91,102 @@
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Effects
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// Process email body when current email changes
|
||||||
|
$effect(() => {
|
||||||
|
const processCurrentEmail = async () => {
|
||||||
|
if (mailState.currentEmail?.body) {
|
||||||
|
const processedBody = await processEmailBody(mailState.currentEmail.body);
|
||||||
|
|
||||||
|
if (processedBody !== mailState.currentEmail.body) {
|
||||||
|
mailState.currentEmail.body = processedBody;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dev) {
|
||||||
|
console.debug('emailObj:', mailState.currentEmail);
|
||||||
|
}
|
||||||
|
console.info('Current email changed:', mailState.currentEmail?.subject);
|
||||||
|
|
||||||
|
if (mailState.currentEmail !== null) {
|
||||||
|
sidebarOpen.set(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
processCurrentEmail();
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Lifecycle
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
// Listen for second instance args (when another file is opened while app is running)
|
||||||
|
unregisterEvents = EventsOn('launchArgs', async (args: string[]) => {
|
||||||
|
console.log('got event launchArgs:', args);
|
||||||
|
|
||||||
|
if (!args || args.length === 0) return;
|
||||||
|
|
||||||
|
for (const arg of args) {
|
||||||
|
if (isEmailFile(arg)) {
|
||||||
|
console.log('Loading file from second instance:', arg);
|
||||||
|
isLoading = true;
|
||||||
|
loadingText = m.layout_loading_text();
|
||||||
|
|
||||||
|
// Check if MSG file for special loading text
|
||||||
|
if (arg.toLowerCase().endsWith('.msg')) {
|
||||||
|
loadingText = m.mail_loading_msg_conversion();
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await loadEmailFromPath(arg);
|
||||||
|
|
||||||
|
if (result.success && result.email) {
|
||||||
|
mailState.setParams(result.email);
|
||||||
|
sidebarOpen.set(false);
|
||||||
|
WindowUnminimise();
|
||||||
|
WindowShow();
|
||||||
|
} else if (result.error) {
|
||||||
|
console.error('Failed to load email:', result.error);
|
||||||
|
toast.error('Failed to load email file');
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoading = false;
|
||||||
|
loadingText = '';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
if (unregisterEvents) {
|
||||||
|
unregisterEvents();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Helpers
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
function getAttachmentClass(att: { contentType: string; filename: string }): string {
|
||||||
|
if (att.contentType.startsWith(CONTENT_TYPES.IMAGE)) return 'image';
|
||||||
|
if (att.contentType === CONTENT_TYPES.PDF || att.filename.toLowerCase().endsWith('.pdf'))
|
||||||
|
return 'pdf';
|
||||||
|
if (att.filename.toLowerCase().endsWith('.eml')) return 'eml';
|
||||||
|
return 'file';
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPecSignature(filename: string, isPec: boolean): boolean {
|
||||||
|
return isPec && filename.toLowerCase().endsWith(PEC_FILES.SIGNATURE);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPecCertificate(filename: string, isPec: boolean): boolean {
|
||||||
|
return isPec && filename.toLowerCase() === PEC_FILES.CERTIFICATE;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="panel fill" aria-label="Events">
|
<div class="panel fill" aria-label="Events">
|
||||||
@@ -229,8 +196,10 @@
|
|||||||
<div class="loading-text">{loadingText}</div>
|
<div class="loading-text">{loadingText}</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="events" role="log" aria-live="polite">
|
<div class="events" role="log" aria-live="polite">
|
||||||
{#if mailState.currentEmail === null}
|
{#if mailState.currentEmail === null}
|
||||||
|
<!-- Empty State -->
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
<div class="empty-icon">
|
<div class="empty-icon">
|
||||||
<MailOpen size="48" strokeWidth={1} />
|
<MailOpen size="48" strokeWidth={1} />
|
||||||
@@ -241,7 +210,9 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
|
<!-- Email View -->
|
||||||
<div class="email-view">
|
<div class="email-view">
|
||||||
|
<!-- Header -->
|
||||||
<div class="email-header-content">
|
<div class="email-header-content">
|
||||||
<div class="subject-row">
|
<div class="subject-row">
|
||||||
<div class="email-subject">
|
<div class="email-subject">
|
||||||
@@ -255,7 +226,7 @@
|
|||||||
title={m.mail_open_btn_title()}
|
title={m.mail_open_btn_title()}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
>
|
>
|
||||||
<MailOpen size="15" ></MailOpen>
|
<MailOpen size="15" />
|
||||||
{m.mail_open_btn_text()}
|
{m.mail_open_btn_text()}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@@ -271,77 +242,84 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Meta Grid -->
|
||||||
<div class="email-meta-grid">
|
<div class="email-meta-grid">
|
||||||
<span class="label">{m.mail_from()}</span>
|
<span class="label">{m.mail_from()}</span>
|
||||||
<span class="value">{mailState.currentEmail.from}</span>
|
<span class="value">{mailState.currentEmail.from}</span>
|
||||||
|
|
||||||
{#if mailState.currentEmail.to && mailState.currentEmail.to.length > 0}
|
{#if mailState.currentEmail.to && mailState.currentEmail.to.length > 0}
|
||||||
<span class="label">{m.mail_to()}</span>
|
<span class="label">{m.mail_to()}</span>
|
||||||
<span class="value">{mailState.currentEmail.to.join(", ")}</span>
|
<span class="value">{mailState.currentEmail.to.join(', ')}</span>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if mailState.currentEmail.cc && mailState.currentEmail.cc.length > 0}
|
{#if mailState.currentEmail.cc && mailState.currentEmail.cc.length > 0}
|
||||||
<span class="label">{m.mail_cc()}</span>
|
<span class="label">{m.mail_cc()}</span>
|
||||||
<span class="value">{mailState.currentEmail.cc.join(", ")}</span>
|
<span class="value">{mailState.currentEmail.cc.join(', ')}</span>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if mailState.currentEmail.bcc && mailState.currentEmail.bcc.length > 0}
|
{#if mailState.currentEmail.bcc && mailState.currentEmail.bcc.length > 0}
|
||||||
<span class="label">{m.mail_bcc()}</span>
|
<span class="label">{m.mail_bcc()}</span>
|
||||||
<span class="value">{mailState.currentEmail.bcc.join(", ")}</span>
|
<span class="value">{mailState.currentEmail.bcc.join(', ')}</span>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if mailState.currentEmail.isPec}
|
{#if mailState.currentEmail.isPec}
|
||||||
<span class="label">{m.mail_sign_label()}</span>
|
<span class="label">{m.mail_sign_label()}</span>
|
||||||
<span class="value"><span class="pec-badge" title="Posta Elettronica Certificata">
|
<span class="value">
|
||||||
<ShieldCheck size="14" />
|
<span class="pec-badge" title="Posta Elettronica Certificata">
|
||||||
PEC
|
<ShieldCheck size="14" />
|
||||||
</span></span>
|
PEC
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Attachments -->
|
||||||
<div class="email-attachments">
|
<div class="email-attachments">
|
||||||
<span class="att-section-label">{m.mail_attachments()}</span>
|
<span class="att-section-label">{m.mail_attachments()}</span>
|
||||||
<div class="att-list">
|
<div class="att-list">
|
||||||
{#if mailState.currentEmail.attachments && mailState.currentEmail.attachments.length > 0}
|
{#if mailState.currentEmail.attachments && mailState.currentEmail.attachments.length > 0}
|
||||||
{#each mailState.currentEmail.attachments as att}
|
{#each mailState.currentEmail.attachments as att}
|
||||||
{#if att.contentType.startsWith("image/")}
|
{@const base64 = arrayBufferToBase64(att.data)}
|
||||||
|
{@const isImage = att.contentType.startsWith(CONTENT_TYPES.IMAGE)}
|
||||||
|
{@const isPdf =
|
||||||
|
att.contentType === CONTENT_TYPES.PDF ||
|
||||||
|
att.filename.toLowerCase().endsWith('.pdf')}
|
||||||
|
{@const isEml = att.filename.toLowerCase().endsWith('.eml')}
|
||||||
|
{@const isPecSig = isPecSignature(att.filename, mailState.currentEmail.isPec)}
|
||||||
|
{@const isPecCert = isPecCertificate(att.filename, mailState.currentEmail.isPec)}
|
||||||
|
|
||||||
|
{#if isImage}
|
||||||
<button
|
<button
|
||||||
class="att-btn image"
|
class="att-btn image"
|
||||||
onclick={() => openImageHandler(arrayBufferToBase64(att.data), att.filename)}
|
onclick={() => handleOpenImage(base64, att.filename)}
|
||||||
>
|
>
|
||||||
<Image size="14" />
|
<Image size="14" />
|
||||||
<span class="att-name">{att.filename}</span>
|
<span class="att-name">{att.filename}</span>
|
||||||
</button>
|
</button>
|
||||||
{:else if att.contentType === "application/pdf" || att.filename.toLowerCase().endsWith(".pdf")}
|
{:else if isPdf}
|
||||||
<button
|
<button class="att-btn pdf" onclick={() => handleOpenPDF(base64, att.filename)}>
|
||||||
class="att-btn pdf"
|
<FileText size="14" />
|
||||||
onclick={() => openPDFHandler(arrayBufferToBase64(att.data), att.filename)}
|
|
||||||
>
|
|
||||||
<FileText />
|
|
||||||
<span class="att-name">{att.filename}</span>
|
<span class="att-name">{att.filename}</span>
|
||||||
</button>
|
</button>
|
||||||
{:else if att.filename.toLowerCase().endsWith(".eml")}
|
{:else if isEml}
|
||||||
<button
|
<button class="att-btn eml" onclick={() => handleOpenEML(base64, att.filename)}>
|
||||||
class="att-btn eml"
|
|
||||||
onclick={() => openEMLHandler(arrayBufferToBase64(att.data), att.filename)}
|
|
||||||
>
|
|
||||||
<MailOpen size="14" />
|
<MailOpen size="14" />
|
||||||
<span class="att-name">{att.filename}</span>
|
<span class="att-name">{att.filename}</span>
|
||||||
</button>
|
</button>
|
||||||
{:else if mailState.currentEmail.isPec && att.filename.toLowerCase().endsWith(".p7s")}
|
{:else if isPecSig}
|
||||||
<a
|
<a
|
||||||
class="att-btn file"
|
class="att-btn file"
|
||||||
href={`data:${att.contentType};base64,${arrayBufferToBase64(att.data)}`}
|
href={createDataUrl(att.contentType, base64)}
|
||||||
download={att.filename}
|
download={att.filename}
|
||||||
>
|
>
|
||||||
<Signature size="14" />
|
<Signature size="14" />
|
||||||
<span class="att-name">{att.filename}</span>
|
<span class="att-name">{att.filename}</span>
|
||||||
</a>
|
</a>
|
||||||
{:else if mailState.currentEmail.isPec && att.filename.toLowerCase() === "daticert.xml"}
|
{:else if isPecCert}
|
||||||
<a
|
<a
|
||||||
class="att-btn file"
|
class="att-btn file"
|
||||||
href={`data:${att.contentType};base64,${arrayBufferToBase64(att.data)}`}
|
href={createDataUrl(att.contentType, base64)}
|
||||||
download={att.filename}
|
download={att.filename}
|
||||||
>
|
>
|
||||||
<FileCode size="14" />
|
<FileCode size="14" />
|
||||||
@@ -350,14 +328,14 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<a
|
<a
|
||||||
class="att-btn file"
|
class="att-btn file"
|
||||||
href={`data:${att.contentType};base64,${arrayBufferToBase64(att.data)}`}
|
href={createDataUrl(att.contentType, base64)}
|
||||||
download={att.filename}
|
download={att.filename}
|
||||||
>
|
>
|
||||||
{#if att.contentType.startsWith("image/")}
|
{#if isImage}
|
||||||
<Image size="14" />
|
<Image size="14" />
|
||||||
{:else}
|
{:else}
|
||||||
<File size="14" />
|
<File size="14" />
|
||||||
{/if}
|
{/if}
|
||||||
<span class="att-name">{att.filename}</span>
|
<span class="att-name">{att.filename}</span>
|
||||||
</a>
|
</a>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -365,12 +343,13 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<span class="att-empty">{m.mail_no_attachments()}</span>
|
<span class="att-empty">{m.mail_no_attachments()}</span>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Email Body -->
|
||||||
<div class="email-body-wrapper">
|
<div class="email-body-wrapper">
|
||||||
<iframe
|
<iframe
|
||||||
srcdoc={mailState.currentEmail.body + iFrameUtilHTML}
|
srcdoc={mailState.currentEmail.body + IFRAME_UTIL_HTML}
|
||||||
title="Email Body"
|
title="Email Body"
|
||||||
class="email-iframe"
|
class="email-iframe"
|
||||||
sandbox="allow-same-origin allow-scripts"
|
sandbox="allow-same-origin allow-scripts"
|
||||||
@@ -379,7 +358,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@@ -399,14 +378,17 @@
|
|||||||
gap: 16px;
|
gap: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Make sure internal loader spins if not using class-based animation library like Tailwind */
|
|
||||||
:global(.spinner) {
|
:global(.spinner) {
|
||||||
animation: spin 1s linear infinite;
|
animation: spin 1s linear infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
from { transform: rotate(0deg); }
|
from {
|
||||||
to { transform: rotate(360deg); }
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading-text {
|
.loading-text {
|
||||||
@@ -494,10 +476,10 @@
|
|||||||
gap: 6px;
|
gap: 6px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.subject-row .btn {
|
.subject-row .btn {
|
||||||
height: 28px;
|
height: 28px;
|
||||||
padding: 0 8px;
|
padding: 0 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.email-meta-grid {
|
.email-meta-grid {
|
||||||
@@ -564,15 +546,30 @@
|
|||||||
background: rgba(255, 255, 255, 0.05);
|
background: rgba(255, 255, 255, 0.05);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.att-btn.image { color: #60a5fa; border-color: rgba(96, 165, 250, 0.3); }
|
|
||||||
.att-btn.image:hover { color: #93c5fd; }
|
|
||||||
|
|
||||||
.att-btn.pdf { color: #f87171; border-color: rgba(248, 113, 113, 0.3); }
|
.att-btn.image {
|
||||||
.att-btn.pdf:hover { color: #fca5a5; }
|
color: #60a5fa;
|
||||||
|
border-color: rgba(96, 165, 250, 0.3);
|
||||||
|
}
|
||||||
|
.att-btn.image:hover {
|
||||||
|
color: #93c5fd;
|
||||||
|
}
|
||||||
|
|
||||||
.att-btn.eml { color: hsl(49, 80%, 49%); border-color: rgba(224, 206, 39, 0.3); }
|
.att-btn.pdf {
|
||||||
.att-btn.eml:hover { color: hsl(49, 80%, 65%); }
|
color: #f87171;
|
||||||
|
border-color: rgba(248, 113, 113, 0.3);
|
||||||
|
}
|
||||||
|
.att-btn.pdf:hover {
|
||||||
|
color: #fca5a5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.att-btn.eml {
|
||||||
|
color: hsl(49, 80%, 49%);
|
||||||
|
border-color: rgba(224, 206, 39, 0.3);
|
||||||
|
}
|
||||||
|
.att-btn.eml:hover {
|
||||||
|
color: hsl(49, 80%, 65%);
|
||||||
|
}
|
||||||
|
|
||||||
.att-name {
|
.att-name {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@@ -635,7 +632,8 @@
|
|||||||
border-color: rgba(255, 255, 255, 0.25);
|
border-color: rgba(255, 255, 255, 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
.browse-btn:disabled, .btn:disabled {
|
.browse-btn:disabled,
|
||||||
|
.btn:disabled {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
|||||||
94
frontend/src/lib/utils/mail/attachment-handlers.ts
Normal file
94
frontend/src/lib/utils/mail/attachment-handlers.ts
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
/**
|
||||||
|
* Handlers for opening different attachment types
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
OpenPDF,
|
||||||
|
OpenPDFWindow,
|
||||||
|
OpenImage,
|
||||||
|
OpenImageWindow,
|
||||||
|
OpenEMLWindow,
|
||||||
|
} from '$lib/wailsjs/go/main/App';
|
||||||
|
import { settingsStore } from '$lib/stores/settings.svelte';
|
||||||
|
import { toast } from 'svelte-sonner';
|
||||||
|
import * as m from '$lib/paraglide/messages';
|
||||||
|
|
||||||
|
export interface AttachmentHandlerResult {
|
||||||
|
success: boolean;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens a PDF attachment using either built-in or external viewer
|
||||||
|
* @param base64Data - Base64 encoded PDF data
|
||||||
|
* @param filename - Name of the PDF file
|
||||||
|
*/
|
||||||
|
export async function openPDFAttachment(
|
||||||
|
base64Data: string,
|
||||||
|
filename: string
|
||||||
|
): Promise<AttachmentHandlerResult> {
|
||||||
|
try {
|
||||||
|
if (settingsStore.settings.useBuiltinPDFViewer) {
|
||||||
|
await OpenPDFWindow(base64Data, filename);
|
||||||
|
} else {
|
||||||
|
await OpenPDF(base64Data, filename);
|
||||||
|
}
|
||||||
|
return { success: true };
|
||||||
|
} catch (error: unknown) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
|
||||||
|
// Check if PDF is already open
|
||||||
|
if (errorMessage.includes(filename) && errorMessage.includes('already open')) {
|
||||||
|
toast.error(m.mail_pdf_already_open());
|
||||||
|
return { success: false, error: 'already_open' };
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error('Failed to open PDF:', error);
|
||||||
|
toast.error(m.mail_error_pdf());
|
||||||
|
return { success: false, error: errorMessage };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens an image attachment using either built-in or external viewer
|
||||||
|
* @param base64Data - Base64 encoded image data
|
||||||
|
* @param filename - Name of the image file
|
||||||
|
*/
|
||||||
|
export async function openImageAttachment(
|
||||||
|
base64Data: string,
|
||||||
|
filename: string
|
||||||
|
): Promise<AttachmentHandlerResult> {
|
||||||
|
try {
|
||||||
|
if (settingsStore.settings.useBuiltinPreview) {
|
||||||
|
await OpenImageWindow(base64Data, filename);
|
||||||
|
} else {
|
||||||
|
await OpenImage(base64Data, filename);
|
||||||
|
}
|
||||||
|
return { success: true };
|
||||||
|
} catch (error: unknown) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
console.error('Failed to open image:', error);
|
||||||
|
toast.error(m.mail_error_image());
|
||||||
|
return { success: false, error: errorMessage };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens an EML attachment in a new EMLy window
|
||||||
|
* @param base64Data - Base64 encoded EML data
|
||||||
|
* @param filename - Name of the EML file
|
||||||
|
*/
|
||||||
|
export async function openEMLAttachment(
|
||||||
|
base64Data: string,
|
||||||
|
filename: string
|
||||||
|
): Promise<AttachmentHandlerResult> {
|
||||||
|
try {
|
||||||
|
await OpenEMLWindow(base64Data, filename);
|
||||||
|
return { success: true };
|
||||||
|
} catch (error: unknown) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
console.error('Failed to open EML:', error);
|
||||||
|
toast.error('Failed to open EML attachment');
|
||||||
|
return { success: false, error: errorMessage };
|
||||||
|
}
|
||||||
|
}
|
||||||
31
frontend/src/lib/utils/mail/constants.ts
Normal file
31
frontend/src/lib/utils/mail/constants.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
/**
|
||||||
|
* HTML/CSS injected into the email body iframe for styling and security
|
||||||
|
* - Removes default body margins
|
||||||
|
* - Disables link clicking for security
|
||||||
|
* - Prevents Ctrl+Wheel zoom in iframe
|
||||||
|
*/
|
||||||
|
export const IFRAME_UTIL_HTML = `<style>body{margin:0;padding:20px;font-family:sans-serif;} a{pointer-events:none!important;cursor:default!important;}</style><script>function handleWheel(event){if(event.ctrlKey){event.preventDefault();}}document.addEventListener('wheel',handleWheel,{passive:false});<\/script>`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Supported email file extensions
|
||||||
|
*/
|
||||||
|
export const EMAIL_EXTENSIONS = {
|
||||||
|
EML: '.eml',
|
||||||
|
MSG: '.msg',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attachment content type prefixes/patterns
|
||||||
|
*/
|
||||||
|
export const CONTENT_TYPES = {
|
||||||
|
IMAGE: 'image/',
|
||||||
|
PDF: 'application/pdf',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Special PEC (Italian Certified Email) file names
|
||||||
|
*/
|
||||||
|
export const PEC_FILES = {
|
||||||
|
SIGNATURE: '.p7s',
|
||||||
|
CERTIFICATE: 'daticert.xml',
|
||||||
|
} as const;
|
||||||
77
frontend/src/lib/utils/mail/data-utils.ts
Normal file
77
frontend/src/lib/utils/mail/data-utils.ts
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
/**
|
||||||
|
* Utility functions for mail data conversion and processing
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts an ArrayBuffer or byte array to a base64 string
|
||||||
|
* @param buffer - The buffer to convert (can be string, array, or ArrayBuffer)
|
||||||
|
* @returns Base64 encoded string
|
||||||
|
*/
|
||||||
|
export function arrayBufferToBase64(buffer: unknown): string {
|
||||||
|
// Already a base64 string
|
||||||
|
if (typeof buffer === 'string') {
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle array of bytes
|
||||||
|
if (Array.isArray(buffer)) {
|
||||||
|
let binary = '';
|
||||||
|
const bytes = new Uint8Array(buffer);
|
||||||
|
const len = bytes.byteLength;
|
||||||
|
for (let i = 0; i < len; i++) {
|
||||||
|
binary += String.fromCharCode(bytes[i]);
|
||||||
|
}
|
||||||
|
return window.btoa(binary);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle ArrayBuffer
|
||||||
|
if (buffer instanceof ArrayBuffer) {
|
||||||
|
let binary = '';
|
||||||
|
const bytes = new Uint8Array(buffer);
|
||||||
|
const len = bytes.byteLength;
|
||||||
|
for (let i = 0; i < len; i++) {
|
||||||
|
binary += String.fromCharCode(bytes[i]);
|
||||||
|
}
|
||||||
|
return window.btoa(binary);
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a data URL for downloading attachments
|
||||||
|
* @param contentType - MIME type of the attachment
|
||||||
|
* @param base64Data - Base64 encoded data
|
||||||
|
* @returns Data URL string
|
||||||
|
*/
|
||||||
|
export function createDataUrl(contentType: string, base64Data: string): string {
|
||||||
|
return `data:${contentType};base64,${base64Data}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a string looks like valid base64 content
|
||||||
|
* @param content - String to check
|
||||||
|
* @returns True if the content appears to be base64 encoded
|
||||||
|
*/
|
||||||
|
export function looksLikeBase64(content: string): boolean {
|
||||||
|
const clean = content.replace(/[\s\r\n]+/g, '');
|
||||||
|
return (
|
||||||
|
clean.length > 0 &&
|
||||||
|
clean.length % 4 === 0 &&
|
||||||
|
/^[A-Za-z0-9+/]+=*$/.test(clean)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to decode base64 content
|
||||||
|
* @param content - Base64 string to decode
|
||||||
|
* @returns Decoded string or null if decoding fails
|
||||||
|
*/
|
||||||
|
export function tryDecodeBase64(content: string): string | null {
|
||||||
|
try {
|
||||||
|
const clean = content.replace(/[\s\r\n]+/g, '');
|
||||||
|
return window.atob(clean);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
163
frontend/src/lib/utils/mail/email-loader.ts
Normal file
163
frontend/src/lib/utils/mail/email-loader.ts
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
/**
|
||||||
|
* Email loading and processing utilities
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
ReadEML,
|
||||||
|
ReadMSG,
|
||||||
|
ReadPEC,
|
||||||
|
ShowOpenFileDialog,
|
||||||
|
SetCurrentMailFilePath,
|
||||||
|
ConvertToUTF8,
|
||||||
|
} from '$lib/wailsjs/go/main/App';
|
||||||
|
import type { internal } from '$lib/wailsjs/go/models';
|
||||||
|
import { isBase64, isHtml } from '$lib/utils';
|
||||||
|
import { looksLikeBase64, tryDecodeBase64 } from './data-utils';
|
||||||
|
|
||||||
|
export interface LoadEmailResult {
|
||||||
|
success: boolean;
|
||||||
|
email?: internal.EmailData;
|
||||||
|
filePath?: string;
|
||||||
|
error?: string;
|
||||||
|
cancelled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines the email file type from the path
|
||||||
|
*/
|
||||||
|
export function getEmailFileType(filePath: string): 'eml' | 'msg' | null {
|
||||||
|
const lowerPath = filePath.toLowerCase();
|
||||||
|
if (lowerPath.endsWith('.eml')) return 'eml';
|
||||||
|
if (lowerPath.endsWith('.msg')) return 'msg';
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a file path is a valid email file
|
||||||
|
*/
|
||||||
|
export function isEmailFile(filePath: string): boolean {
|
||||||
|
return getEmailFileType(filePath) !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads an email from a file path
|
||||||
|
* @param filePath - Path to the email file
|
||||||
|
* @returns LoadEmailResult with the email data or error
|
||||||
|
*/
|
||||||
|
export async function loadEmailFromPath(filePath: string): Promise<LoadEmailResult> {
|
||||||
|
const fileType = getEmailFileType(filePath);
|
||||||
|
|
||||||
|
if (!fileType) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: 'Invalid file type. Only .eml and .msg files are supported.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let email: internal.EmailData;
|
||||||
|
|
||||||
|
if (fileType === 'msg') {
|
||||||
|
email = await ReadMSG(filePath, true);
|
||||||
|
} else {
|
||||||
|
// Try PEC first, fall back to regular EML
|
||||||
|
try {
|
||||||
|
email = await ReadPEC(filePath);
|
||||||
|
} catch {
|
||||||
|
email = await ReadEML(filePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process body if needed (decode base64)
|
||||||
|
if (email?.body) {
|
||||||
|
const trimmed = email.body.trim();
|
||||||
|
if (looksLikeBase64(trimmed)) {
|
||||||
|
const decoded = tryDecodeBase64(trimmed);
|
||||||
|
if (decoded) {
|
||||||
|
email.body = decoded;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
email,
|
||||||
|
filePath,
|
||||||
|
};
|
||||||
|
} catch (error: unknown) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
console.error('Failed to load email:', error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: errorMessage,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens a file dialog and loads the selected email
|
||||||
|
* @returns LoadEmailResult with the email data or error
|
||||||
|
*/
|
||||||
|
export async function openAndLoadEmail(): Promise<LoadEmailResult> {
|
||||||
|
try {
|
||||||
|
const filePath = await ShowOpenFileDialog();
|
||||||
|
|
||||||
|
if (!filePath || filePath.length === 0) {
|
||||||
|
return { success: false, cancelled: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await loadEmailFromPath(filePath);
|
||||||
|
|
||||||
|
if (result.success && result.email) {
|
||||||
|
// Track the current mail file path for bug reports
|
||||||
|
await SetCurrentMailFilePath(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (error: unknown) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
console.error('Failed to open email file:', error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: errorMessage,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Processes and fixes the email body content
|
||||||
|
* - Decodes base64 content if needed
|
||||||
|
* - Fixes character encoding issues
|
||||||
|
* @param body - The raw email body
|
||||||
|
* @returns Processed body content
|
||||||
|
*/
|
||||||
|
export async function processEmailBody(body: string): Promise<string> {
|
||||||
|
if (!body) return body;
|
||||||
|
|
||||||
|
let content = body;
|
||||||
|
|
||||||
|
// 1. Try to decode if not HTML
|
||||||
|
if (!isHtml(content)) {
|
||||||
|
const clean = content.replace(/[\s\r\n]+/g, '');
|
||||||
|
if (isBase64(clean)) {
|
||||||
|
const decoded = tryDecodeBase64(clean);
|
||||||
|
if (decoded) {
|
||||||
|
content = decoded;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. If it is HTML (original or decoded), try to fix encoding
|
||||||
|
if (isHtml(content)) {
|
||||||
|
try {
|
||||||
|
const fixed = await ConvertToUTF8(content);
|
||||||
|
if (fixed) {
|
||||||
|
content = fixed;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to fix encoding:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return content;
|
||||||
|
}
|
||||||
37
frontend/src/lib/utils/mail/index.ts
Normal file
37
frontend/src/lib/utils/mail/index.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
/**
|
||||||
|
* Mail utilities barrel export
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Constants
|
||||||
|
export {
|
||||||
|
IFRAME_UTIL_HTML,
|
||||||
|
EMAIL_EXTENSIONS,
|
||||||
|
CONTENT_TYPES,
|
||||||
|
PEC_FILES,
|
||||||
|
} from './constants';
|
||||||
|
|
||||||
|
// Data utilities
|
||||||
|
export {
|
||||||
|
arrayBufferToBase64,
|
||||||
|
createDataUrl,
|
||||||
|
looksLikeBase64,
|
||||||
|
tryDecodeBase64,
|
||||||
|
} from './data-utils';
|
||||||
|
|
||||||
|
// Attachment handlers
|
||||||
|
export {
|
||||||
|
openPDFAttachment,
|
||||||
|
openImageAttachment,
|
||||||
|
openEMLAttachment,
|
||||||
|
type AttachmentHandlerResult,
|
||||||
|
} from './attachment-handlers';
|
||||||
|
|
||||||
|
// Email loader
|
||||||
|
export {
|
||||||
|
getEmailFileType,
|
||||||
|
isEmailFile,
|
||||||
|
loadEmailFromPath,
|
||||||
|
openAndLoadEmail,
|
||||||
|
processEmailBody,
|
||||||
|
type LoadEmailResult,
|
||||||
|
} from './email-loader';
|
||||||
Reference in New Issue
Block a user