281 lines
8.8 KiB
TypeScript
281 lines
8.8 KiB
TypeScript
/**
|
|
* CENTRAL EMAIL FORMATTER
|
|
*
|
|
* This is the primary and only email formatting utility to be used.
|
|
* All email formatting should go through here to ensure consistent
|
|
* handling of text direction and HTML sanitization.
|
|
*/
|
|
|
|
import DOMPurify from 'isomorphic-dompurify';
|
|
|
|
// Configure DOMPurify to preserve direction attributes
|
|
DOMPurify.addHook('afterSanitizeAttributes', function(node) {
|
|
// Preserve direction attributes
|
|
if (node.hasAttribute('dir')) {
|
|
node.setAttribute('dir', node.getAttribute('dir') || 'ltr');
|
|
}
|
|
// Preserve text-align in styles
|
|
if (node.hasAttribute('style')) {
|
|
const style = node.getAttribute('style') || '';
|
|
if (style.includes('text-align')) {
|
|
// Keep existing alignment
|
|
} else if (node.hasAttribute('dir') && node.getAttribute('dir') === 'rtl') {
|
|
node.setAttribute('style', style + '; text-align: right;');
|
|
}
|
|
}
|
|
});
|
|
|
|
// 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
|
|
* This is the ONLY function that should be used for cleaning HTML content
|
|
*/
|
|
export function cleanHtmlContent(content: string): string {
|
|
if (!content) return '';
|
|
|
|
// First sanitize the HTML with our configured DOMPurify
|
|
const sanitized = DOMPurify.sanitize(content);
|
|
|
|
// Process content to ensure consistent direction
|
|
let processed = sanitized;
|
|
|
|
// Replace RTL attributes with LTR if needed
|
|
// We're now more careful to only modify direction attributes if needed
|
|
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 - CENTRAL IMPLEMENTATION
|
|
* All other formatting functions should be deprecated in favor of this one
|
|
*/
|
|
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 || '');
|
|
|
|
// Check if the content already has a forwarded message header
|
|
const hasExistingHeader = originalContent.includes('---------- Forwarded message ---------');
|
|
|
|
// If there's already a forwarded message header, don't add another one
|
|
if (hasExistingHeader) {
|
|
// Just wrap the content in appropriate styling without adding another header
|
|
const content = `
|
|
<div style="min-height: 20px;"></div>
|
|
<div style="direction: ltr; text-align: left;" dir="ltr" class="email-original-content">
|
|
${originalContent}
|
|
</div>
|
|
`;
|
|
return { subject, content };
|
|
}
|
|
|
|
// 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 - CENTRAL IMPLEMENTATION
|
|
* All other formatting functions should be deprecated in favor of this one
|
|
*/
|
|
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
|
|
};
|
|
}
|
|
|
|
/**
|
|
* COMPATIBILITY LAYER: For backward compatibility with the old email-formatter.ts
|
|
* These functions map to our new implementation but preserve the old interface
|
|
*/
|
|
|
|
// For compatibility with old code that might be using the other email-formatter.ts
|
|
export function formatEmailForReplyOrForward(
|
|
email: EmailMessage,
|
|
type: 'reply' | 'reply-all' | 'forward'
|
|
): {
|
|
to: string;
|
|
cc?: string;
|
|
subject: string;
|
|
body: string;
|
|
} {
|
|
if (type === 'forward') {
|
|
const { subject, content } = formatForwardedEmail(email);
|
|
return {
|
|
to: '',
|
|
subject,
|
|
body: content
|
|
};
|
|
} else {
|
|
const { to, cc, subject, content } = formatReplyEmail(email, type as 'reply' | 'reply-all');
|
|
return {
|
|
to,
|
|
cc,
|
|
subject,
|
|
body: content
|
|
};
|
|
}
|
|
}
|