courrier preview

This commit is contained in:
alma 2025-05-01 17:01:26 +02:00
parent 5160967c75
commit ddcb428233
4 changed files with 160 additions and 87 deletions

View File

@ -35,16 +35,38 @@ function cleanupTableStructures(htmlContent: string): string {
htmlContent.includes('forwarded message') ||
(htmlContent.includes('From:') && htmlContent.includes('Date:') && htmlContent.includes('Subject:'));
const isReplyEmail =
htmlContent.includes('wrote:') ||
htmlContent.includes('<blockquote') ||
htmlContent.includes('gmail_quote');
// Check if content has complex tables that might cause issues
const hasComplexTables = tables.length > 0 &&
(isForwardedEmail || htmlContent.includes('gmail_quote') ||
htmlContent.includes('blockquote') || htmlContent.includes('wrote:'));
(isForwardedEmail || isReplyEmail);
if (hasComplexTables) {
console.log(`Found ${tables.length} tables in complex email content`);
let convertedCount = 0;
tables.forEach(table => {
// Special handling for tables inside blockquotes (quoted content)
if (table.closest('blockquote') ||
(isReplyEmail && table.innerHTML.includes('wrote:'))) {
console.log('Preserving table inside quoted content');
// Apply minimal styling to ensure it renders correctly
table.setAttribute('style', 'border-collapse: collapse; width: 100%; max-width: 100%; margin: 8px 0;');
// Make sure all cells have some basic styling
const cells = table.querySelectorAll('td, th');
cells.forEach(cell => {
if (cell instanceof HTMLTableCellElement) {
cell.style.padding = '4px';
cell.style.textAlign = 'left';
cell.style.verticalAlign = 'top';
}
});
return;
}
// Preserve the main forwarded email header table
if (isForwardedEmail &&
table.innerHTML.includes('From:') &&
@ -180,8 +202,18 @@ const RichEmailEditor: React.FC<RichEmailEditorProps> = ({
hasBlockquote: initialContent.includes('<blockquote')
});
// Process HTML content using centralized utility
const processed = processHtmlContent(initialContent);
// Check if content is reply or forward to use special handling
const isReplyOrForward =
initialContent.includes('wrote:') ||
initialContent.includes('<blockquote') ||
initialContent.includes('Forwarded message') ||
initialContent.includes('---------- Forwarded message ----------');
// Process HTML content using centralized utility with special settings for replies/forwards
const processed = processHtmlContent(initialContent, {
sanitize: true,
preserveReplyFormat: isReplyOrForward
});
const sanitizedContent = processed.sanitizedContent;
const direction = processed.direction; // Use direction from processed result
@ -194,6 +226,7 @@ const RichEmailEditor: React.FC<RichEmailEditorProps> = ({
containsQuoteHeader: sanitizedContent.includes('wrote:'),
hasTable: sanitizedContent.includes('<table'),
hasBlockquote: sanitizedContent.includes('<blockquote'),
isReplyOrForward: isReplyOrForward,
firstNChars: sanitizedContent.substring(0, 100).replace(/\n/g, '\\n')
});
@ -213,8 +246,19 @@ const RichEmailEditor: React.FC<RichEmailEditorProps> = ({
quillRef.current.setText('Error loading content');
}
} else {
// Use direct innerHTML setting for the initial content
quillRef.current.root.innerHTML = sanitizedContent;
// Special handling for reply or forwarded content
if (isReplyOrForward) {
console.log('Using special handling for reply/forward content');
// Clean up any problematic table structures, with special care for quoted content
const cleanedContent = cleanupTableStructures(sanitizedContent);
// Use direct innerHTML setting with minimal processing for reply/forward content
quillRef.current.root.innerHTML = cleanedContent;
} else {
// Use direct innerHTML setting for regular content
quillRef.current.root.innerHTML = sanitizedContent;
}
// Set the direction for the content
if (quillRef.current && quillRef.current.format) {
@ -319,8 +363,18 @@ const RichEmailEditor: React.FC<RichEmailEditorProps> = ({
firstNChars: initialContent.substring(0, 100).replace(/\n/g, '\\n')
});
// Check if content is reply or forward to use special handling
const isReplyOrForward =
initialContent.includes('wrote:') ||
initialContent.includes('<blockquote') ||
initialContent.includes('Forwarded message') ||
initialContent.includes('---------- Forwarded message ----------');
// Process HTML content using centralized utility
const processed = processHtmlContent(initialContent);
const processed = processHtmlContent(initialContent, {
sanitize: true,
preserveReplyFormat: isReplyOrForward
});
const sanitizedContent = processed.sanitizedContent;
const direction = processed.direction; // Use direction from processed result
@ -333,6 +387,7 @@ const RichEmailEditor: React.FC<RichEmailEditorProps> = ({
containsQuoteHeader: sanitizedContent.includes('wrote:'),
hasTable: sanitizedContent.includes('<table'),
hasBlockquote: sanitizedContent.includes('<blockquote'),
isReplyOrForward: isReplyOrForward,
firstNChars: sanitizedContent.substring(0, 100).replace(/\n/g, '\\n')
});
@ -351,11 +406,17 @@ const RichEmailEditor: React.FC<RichEmailEditorProps> = ({
} else {
// SIMPLIFIED: Set content directly to the root element rather than using clipboard
if (quillRef.current && quillRef.current.root) {
// Clean up any problematic table structures first
const cleanedContent = cleanupTableStructures(sanitizedContent);
// Special handling for reply or forward content
let contentToSet = sanitizedContent;
if (isReplyOrForward) {
console.log('Using special handling for reply/forward content update');
// Clean up tables with special care for quoted content
contentToSet = cleanupTableStructures(sanitizedContent);
}
// First set the content
quillRef.current.root.innerHTML = cleanedContent;
quillRef.current.root.innerHTML = contentToSet;
// Then safely apply formatting only if quillRef is valid
try {

View File

@ -84,14 +84,23 @@ export const purify = configureDOMPurify();
/**
* Sanitize HTML content using our email-specific configuration
*/
export function sanitizeHtml(content: string): string {
export function sanitizeHtml(content: string, options?: { preserveReplyFormat?: boolean }): string {
if (!content) return '';
try {
// Sanitize with our configured instance
// Special handling for reply/forward emails to be less aggressive with sanitization
const extraTags = options?.preserveReplyFormat
? ['style', 'blockquote', 'table', 'thead', 'tbody', 'tr', 'td', 'th']
: ['style'];
const extraAttrs = options?.preserveReplyFormat
? ['style', 'class', 'align', 'valign', 'bgcolor', 'colspan', 'rowspan', 'width', 'height', 'border']
: ['style', 'class'];
// Sanitize with our configured instance and options
return purify.sanitize(content, {
ADD_TAGS: ['style'], // Allow internal styles temporarily for cleaning
ADD_ATTR: ['style', 'class'] // Allow style and class attributes
ADD_TAGS: extraTags,
ADD_ATTR: extraAttrs
});
} catch (error) {
console.error('Failed to sanitize HTML content:', error);

View File

@ -226,6 +226,7 @@ export function processHtmlContent(
options?: {
sanitize?: boolean;
blockExternalContent?: boolean;
preserveReplyFormat?: boolean;
attachments?: Array<{
filename?: string;
name?: string;
@ -252,6 +253,7 @@ export function processHtmlContent(
containsForwardedMessage: htmlContent?.includes('---------- Forwarded message ----------'),
containsQuoteHeader: htmlContent?.includes('<div class="gmail_quote"'),
sanitize: options?.sanitize,
preserveReplyFormat: options?.preserveReplyFormat,
blockExternalContent: options?.blockExternalContent,
hasAttachments: options?.attachments?.length || 0
});
@ -275,8 +277,13 @@ export function processHtmlContent(
}
try {
// Special handling for reply/forwarded content with less aggressive sanitization
const isReplyOrForward = options?.preserveReplyFormat === true;
// Apply sanitization by default unless explicitly turned off
let sanitizedContent = (options?.sanitize !== false) ? sanitizeHtml(htmlContent) : htmlContent;
let sanitizedContent = (options?.sanitize !== false)
? sanitizeHtml(htmlContent, { preserveReplyFormat: isReplyOrForward })
: htmlContent;
// Log content changes from sanitization
console.log('HTML sanitization results:', {
@ -284,10 +291,11 @@ export function processHtmlContent(
sanitizedLength: sanitizedContent.length,
difference: originalContent.length - sanitizedContent.length,
percentRemoved: ((originalContent.length - sanitizedContent.length) / originalContent.length * 100).toFixed(2) + '%',
isEmpty: !sanitizedContent || sanitizedContent.trim().length === 0
isEmpty: !sanitizedContent || sanitizedContent.trim().length === 0,
isReplyOrForward: isReplyOrForward
});
// Check if content is a forwarded message to ensure special handling for tables
// Detect if content is a forwarded message to ensure special handling for tables
const isForwardedEmail =
sanitizedContent.includes('---------- Forwarded message ----------') ||
sanitizedContent.includes('Forwarded message') ||
@ -295,8 +303,8 @@ export function processHtmlContent(
sanitizedContent.includes('Subject:') && sanitizedContent.includes('To:'));
// Special processing for forwarded email styling
if (isForwardedEmail) {
console.log('Detected forwarded email content, preserving table structure');
if (isForwardedEmail || isReplyOrForward) {
console.log('Detected forwarded email or reply content, enhancing structure');
// Make sure we're not removing important table structures
sanitizedContent = sanitizedContent
// Preserve table styling for email headers

View File

@ -324,14 +324,26 @@ export function formatReplyEmail(originalEmail: EmailMessage | LegacyEmailMessag
// Create the quoted reply content
if (htmlContent) {
// Format HTML reply
// Format HTML reply with better styling for quoted content
console.log('Formatting HTML reply, quoted content length:', htmlContent.length);
// Apply minimal sanitization to the original content - preserve more structure
// We'll do a more comprehensive sanitization later in the flow
const sanitizedOriginal = sanitizeHtml(htmlContent, { preserveReplyFormat: true });
// Check if original content already contains blockquotes or is reply/forward
const containsExistingQuote =
sanitizedOriginal.includes('<blockquote') ||
sanitizedOriginal.includes('wrote:') ||
sanitizedOriginal.includes('---------- Forwarded message ----------');
// Preserve existing quotes and add outer quote
htmlContent = `
<div style="margin: 20px 0 10px 0; color: #666; border-bottom: 1px solid #ddd; padding-bottom: 5px;">
On ${date}, ${sender} wrote:
</div>
<blockquote style="margin: 0; padding-left: 10px; border-left: 3px solid #ddd; color: #505050; background-color: #f9f9f9; padding: 10px;">
${sanitizeHtml(htmlContent)}
${sanitizedOriginal}
</blockquote>
`;
}
@ -477,7 +489,7 @@ export function processCidReferences(htmlContent: string, attachments?: Array<{
* Format email for forwarding
*/
export function formatForwardedEmail(originalEmail: EmailMessage | LegacyEmailMessage | null): FormattedEmail {
console.log('formatForwardedEmail called:', { emailId: originalEmail?.id });
console.log('formatForwardedEmail called, emailId:', originalEmail?.id);
if (!originalEmail) {
console.warn('formatForwardedEmail: No original email provided');
@ -496,14 +508,6 @@ export function formatForwardedEmail(originalEmail: EmailMessage | LegacyEmailMe
(email.subject.toLowerCase().startsWith('fwd:') ? email.subject : `Fwd: ${email.subject}`) :
'Fwd: ';
// Get original email info for headers
const { fromStr, toStr, ccStr, dateStr } = getFormattedHeaderInfo(email);
console.log('Forward header info:', { fromStr, toStr, dateStr, subject });
// Original sent date
const date = dateStr;
// Get email content
const originalContent = email.content;
@ -544,75 +548,66 @@ export function formatForwardedEmail(originalEmail: EmailMessage | LegacyEmailMe
htmlContent = formatPlainTextToHtml(textContent);
}
}
// Process embedded images with CID references
if (htmlContent && email.attachments && email.attachments.length > 0) {
console.log('Processing CID references before sanitization');
htmlContent = processCidReferences(htmlContent, email.attachments);
}
// Create the forwarded email HTML content
// Get header info for the forwarded message
const headerInfo = getFormattedHeaderInfo(email);
// Create the forwarded content
if (htmlContent) {
console.log('Formatting HTML forward, original content length:', htmlContent.length);
console.log('Formatting HTML forward, content length:', htmlContent.length);
// Important: First sanitize the content portion only
const sanitizedOriginalContent = sanitizeHtml(htmlContent);
console.log('Sanitized original content length:', sanitizedOriginalContent.length);
// Apply minimal sanitization to the original content - preserve more structure
// We'll do a more comprehensive sanitization later in the flow
const sanitizedOriginal = sanitizeHtml(htmlContent, { preserveReplyFormat: true });
// Create the complete forwarded email with header info
const fullForwardedEmail = `
// Create forwarded message with header info
htmlContent = `
<div style="margin: 20px 0 10px 0; color: #666; font-family: Arial, sans-serif;">
---------- Forwarded message ----------<br>
<table style="margin: 10px 0 15px 0; border-collapse: collapse; font-size: 13px; color: #333;">
<tbody>
<tr>
<td style="padding: 3px 10px 3px 0; font-weight: bold; text-align: right; vertical-align: top;">From:</td>
<td style="padding: 3px 0;">${fromStr}</td>
</tr>
<tr>
<td style="padding: 3px 10px 3px 0; font-weight: bold; text-align: right; vertical-align: top;">Date:</td>
<td style="padding: 3px 0;">${date}</td>
</tr>
<tr>
<td style="padding: 3px 10px 3px 0; font-weight: bold; text-align: right; vertical-align: top;">Subject:</td>
<td style="padding: 3px 0;">${email.subject || ''}</td>
</tr>
<tr>
<td style="padding: 3px 10px 3px 0; font-weight: bold; text-align: right; vertical-align: top;">To:</td>
<td style="padding: 3px 0;">${toStr}</td>
</tr>
${ccStr ? `
<tr>
<td style="padding: 3px 10px 3px 0; font-weight: bold; text-align: right; vertical-align: top;">Cc:</td>
<td style="padding: 3px 0;">${ccStr}</td>
</tr>
` : ''}
</tbody>
<div style="border-bottom: 1px solid #ccc; margin-bottom: 10px; padding-bottom: 5px;">
<div>---------------------------- Forwarded Message ----------------------------</div>
</div>
<table style="margin-bottom: 10px; font-size: 14px; border-collapse: collapse;">
<tr>
<td style="padding: 3px 10px 3px 0; font-weight: bold; text-align: right; vertical-align: top;">From:</td>
<td style="padding: 3px 0;">${headerInfo.fromStr}</td>
</tr>
<tr>
<td style="padding: 3px 10px 3px 0; font-weight: bold; text-align: right; vertical-align: top;">Date:</td>
<td style="padding: 3px 0;">${headerInfo.dateStr}</td>
</tr>
<tr>
<td style="padding: 3px 10px 3px 0; font-weight: bold; text-align: right; vertical-align: top;">Subject:</td>
<td style="padding: 3px 0;">${headerInfo.subject}</td>
</tr>
<tr>
<td style="padding: 3px 10px 3px 0; font-weight: bold; text-align: right; vertical-align: top;">To:</td>
<td style="padding: 3px 0;">${headerInfo.toStr}</td>
</tr>
${headerInfo.ccStr ? `
<tr>
<td style="padding: 3px 10px 3px 0; font-weight: bold; text-align: right; vertical-align: top;">Cc:</td>
<td style="padding: 3px 0;">${headerInfo.ccStr}</td>
</tr>` : ''}
</table>
<div style="border-bottom: 1px solid #ccc; margin-top: 5px; margin-bottom: 15px; padding-bottom: 5px;">
<div>----------------------------------------------------------------------</div>
</div>
</div>
<div class="forwarded-content" style="margin: 0; color: #333;">
${sanitizedOriginal}
</div>
<blockquote style="margin: 0; padding-left: 10px; border-left: 3px solid #ddd; color: #505050; background-color: #f9f9f9; padding: 10px;">
${sanitizedOriginalContent}
</blockquote>
`;
// Now we have the full forwarded email structure without sanitizing it again
htmlContent = fullForwardedEmail;
console.log('Final forward HTML content length:', htmlContent.length,
'contains table:', htmlContent.includes('<table'),
'contains forwarded message:', htmlContent.includes('---------- Forwarded message ----------'),
'contains sanitized content:', htmlContent.includes(sanitizedOriginalContent.substring(0, 30)));
}
// Format the plain text version
if (textContent) {
// Format plain text forward
textContent = `
---------- Forwarded message ----------
From: ${fromStr}
Date: ${date}
Subject: ${email.subject || ''}
To: ${toStr}
${ccStr ? `Cc: ${ccStr}\n` : ''}
From: ${headerInfo.fromStr}
Date: ${headerInfo.dateStr}
Subject: ${headerInfo.subject}
To: ${headerInfo.toStr}
${headerInfo.ccStr ? `Cc: ${headerInfo.ccStr}` : ''}
${textContent}
`.trim();