feat: refactor MailViewer and add utility functions for email handling and attachment processing
This commit is contained in:
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