Neah/lib/utils/email-formatter.ts
2025-04-26 18:24:28 +02:00

208 lines
6.3 KiB
TypeScript

/**
* Utilities for email formatting with proper text direction handling
*/
import DOMPurify from 'isomorphic-dompurify';
// Interface definitions
export interface EmailAddress {
name: string;
address: string;
}
export interface EmailMessage {
id: string;
messageId?: string;
subject: string;
from: EmailAddress[];
to: EmailAddress[];
cc?: EmailAddress[];
bcc?: EmailAddress[];
date: Date | string;
flags?: {
seen: boolean;
flagged: boolean;
answered: boolean;
deleted: boolean;
draft: boolean;
};
preview?: string;
content?: string;
html?: string;
text?: string;
hasAttachments?: boolean;
attachments?: any[];
folder?: string;
size?: number;
contentFetched?: boolean;
}
/**
* Format email addresses for display
*/
export function formatEmailAddresses(addresses: EmailAddress[]): string {
if (!addresses || addresses.length === 0) return '';
return addresses.map(addr =>
addr.name && addr.name !== addr.address
? `${addr.name} <${addr.address}>`
: addr.address
).join(', ');
}
/**
* Format date for display
*/
export function formatEmailDate(date: Date | string | undefined): string {
if (!date) return '';
try {
const dateObj = typeof date === 'string' ? new Date(date) : date;
return dateObj.toLocaleString('en-US', {
weekday: 'short',
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
} catch (e) {
return typeof date === 'string' ? date : date.toString();
}
}
/**
* Clean HTML content to prevent RTL/LTR issues
*/
export function cleanHtmlContent(content: string): string {
if (!content) return '';
// First sanitize the HTML
const sanitized = DOMPurify.sanitize(content);
// Process content to ensure consistent direction
let processed = sanitized;
// Replace RTL attributes with LTR
processed = processed.replace(/dir\s*=\s*["']rtl["']/gi, 'dir="ltr"');
processed = processed.replace(/style\s*=\s*["']([^"']*)direction\s*:\s*rtl;?([^"']*)["']/gi,
(match, before, after) => `style="${before}direction: ltr;${after}"`);
return processed;
}
/**
* Format an email for forwarding
*/
export function formatForwardedEmail(email: EmailMessage): {
subject: string;
content: string;
} {
// Format subject with Fwd: prefix if needed
const subjectBase = email.subject || '(No subject)';
const subject = subjectBase.match(/^(Fwd|FW|Forward):/i)
? subjectBase
: `Fwd: ${subjectBase}`;
// Get sender and recipient information
const fromString = formatEmailAddresses(email.from || []);
const toString = formatEmailAddresses(email.to || []);
const dateString = formatEmailDate(email.date);
// Get and clean original content
const originalContent = cleanHtmlContent(email.content || email.html || email.text || '');
// Create formatted content with explicit LTR formatting
const content = `
<div style="min-height: 20px;"></div>
<div style="border-top: 1px solid #ccc; margin-top: 10px; padding-top: 10px; direction: ltr; text-align: left;" dir="ltr">
<div style="font-family: Arial, sans-serif; color: #333; direction: ltr; text-align: left;" dir="ltr">
<div style="margin-bottom: 15px; direction: ltr; text-align: left;" dir="ltr">
<div style="direction: ltr; text-align: left;" dir="ltr">---------- Forwarded message ---------</div>
<div style="direction: ltr; text-align: left;" dir="ltr"><b>From:</b> ${fromString}</div>
<div style="direction: ltr; text-align: left;" dir="ltr"><b>Date:</b> ${dateString}</div>
<div style="direction: ltr; text-align: left;" dir="ltr"><b>Subject:</b> ${email.subject || ''}</div>
<div style="direction: ltr; text-align: left;" dir="ltr"><b>To:</b> ${toString}</div>
</div>
<div style="direction: ltr; text-align: left;" dir="ltr" class="email-original-content">
${originalContent}
</div>
</div>
</div>
`;
return { subject, content };
}
/**
* Format an email for reply or reply-all
*/
export function formatReplyEmail(email: EmailMessage, type: 'reply' | 'reply-all'): {
to: string;
cc?: string;
subject: string;
content: string;
} {
// Format subject with Re: prefix if needed
const subjectBase = email.subject || '(No subject)';
const subject = subjectBase.match(/^Re:/i)
? subjectBase
: `Re: ${subjectBase}`;
// Get sender information for quote header
const sender = email.from[0];
const fromText = sender?.name
? `${sender.name} <${sender.address}>`
: sender?.address || 'Unknown sender';
// Format date for quote header
const date = typeof email.date === 'string' ? new Date(email.date) : email.date;
const formattedDate = date.toLocaleString('en-US', {
weekday: 'short',
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
// Create quote header
const quoteHeader = `<div style="font-weight: 500; direction: ltr; text-align: left;" dir="ltr">On ${formattedDate}, ${fromText} wrote:</div>`;
// Get and clean original content
const quotedContent = cleanHtmlContent(email.html || email.content || email.text || '');
// Format recipients
let to = formatEmailAddresses(email.from || []);
let cc = '';
if (type === 'reply-all') {
// For reply-all, add all original recipients to CC
const allRecipients = [
...(email.to || []),
...(email.cc || [])
];
cc = formatEmailAddresses(allRecipients);
}
// Format content with explicit LTR for quoted parts
const content = `
<div style="min-height: 20px;"></div>
<div class="reply-body" style="direction: ltr; text-align: left;" dir="ltr">
<div class="quote-header" style="color: #555; font-size: 13px; margin: 20px 0 10px 0; direction: ltr; text-align: left;" dir="ltr">${quoteHeader}</div>
<blockquote style="margin: 0; padding: 10px 0 10px 15px; border-left: 3px solid #ddd; color: #555; background-color: #f8f8f8; border-radius: 4px; direction: ltr; text-align: left;" dir="ltr">
<div class="quoted-content" style="font-size: 13px; direction: ltr; text-align: left;" dir="ltr">
${quotedContent}
</div>
</blockquote>
</div>
`;
return {
to,
cc: cc || undefined,
subject,
content
};
}