From 0251b0811306554ce3b3c55a5ade05526ca56750 Mon Sep 17 00:00:00 2001 From: alma Date: Thu, 1 May 2025 11:44:58 +0200 Subject: [PATCH] courrier preview --- components/email/ComposeEmail.tsx | 17 +- lib/utils/email-utils.ts | 322 +++++++++++++++++++++++------- 2 files changed, 263 insertions(+), 76 deletions(-) diff --git a/components/email/ComposeEmail.tsx b/components/email/ComposeEmail.tsx index a080df68..8d4a4583 100644 --- a/components/email/ComposeEmail.tsx +++ b/components/email/ComposeEmail.tsx @@ -172,6 +172,16 @@ export default function ComposeEmail(props: ComposeEmailProps) { }); setEmailContent(content); } + + // Handle any attachments from reply (e.g., inline images extracted as attachments) + if (formatted.attachments && formatted.attachments.length > 0) { + const formattedAttachments = formatted.attachments.map(att => ({ + name: att.filename || 'attachment', + type: att.contentType || 'application/octet-stream', + content: att.content || '' + })); + setAttachments(formattedAttachments); + } } else if (type === 'forward') { // Get formatted data for forward @@ -209,9 +219,10 @@ export default function ComposeEmail(props: ComposeEmailProps) { setEmailContent(content); } - // If the original email has attachments, include them - if (initialEmail.attachments && initialEmail.attachments.length > 0) { - const formattedAttachments = initialEmail.attachments.map(att => ({ + // Handle attachments for forward (original attachments + extracted inline images) + if (formatted.attachments && formatted.attachments.length > 0) { + console.log(`Processing ${formatted.attachments.length} attachments for forwarded email`); + const formattedAttachments = formatted.attachments.map(att => ({ name: att.filename || 'attachment', type: att.contentType || 'application/octet-stream', content: att.content || '' diff --git a/lib/utils/email-utils.ts b/lib/utils/email-utils.ts index 18d2aa51..e0b2999f 100644 --- a/lib/utils/email-utils.ts +++ b/lib/utils/email-utils.ts @@ -36,6 +36,11 @@ export interface FormattedEmail { cc?: string; subject: string; content: EmailContent; + attachments?: Array<{ + filename: string; + contentType: string; + content?: string; + }>; } /** @@ -252,6 +257,71 @@ function getFormattedHeaderInfo(email: any): { return { fromStr, toStr, ccStr, dateStr, subject }; } +/** + * Extract image attachments from HTML content + */ +function extractInlineImages(htmlContent: string): Array<{ + filename: string; + contentType: string; + content?: string; +}> { + const images: Array<{ + filename: string; + contentType: string; + content?: string; + }> = []; + + try { + if (!htmlContent || typeof window === 'undefined') return images; + + // Create a temporary DOM element to parse the HTML + const tempDiv = document.createElement('div'); + tempDiv.innerHTML = htmlContent; + + // Find all image elements + const imgElements = tempDiv.querySelectorAll('img'); + + // Process each image + imgElements.forEach((img, index) => { + const src = img.getAttribute('src'); + if (!src) return; + + // Only process data URLs and non-tracking pixels + if (src.startsWith('data:image')) { + const contentType = src.split(',')[0].split(':')[1].split(';')[0]; + const imageData = src.split(',')[1]; + + // Skip tiny images (likely tracking pixels) + if (imageData.length < 100) return; + + images.push({ + filename: `inline-image-${index + 1}.${contentType.split('/')[1] || 'png'}`, + contentType, + content: imageData + }); + + // Replace the image source with a placeholder + img.setAttribute('src', `cid:inline-image-${index + 1}`); + } + else if (src.startsWith('cid:')) { + // Already a CID reference, just add a placeholder + const cid = src.substring(4); + images.push({ + filename: `${cid}.png`, + contentType: 'image/png' + }); + } + }); + + // Update the HTML content to use the placeholders + htmlContent = tempDiv.innerHTML; + } catch (error) { + console.error('Error extracting inline images:', error); + } + + return images; +} + /** * Format email for reply */ @@ -276,45 +346,79 @@ export function formatReplyEmail(originalEmail: EmailMessage | LegacyEmailMessag // Get header information const { fromStr, dateStr, subject } = getFormattedHeaderInfo(originalEmail); - // Extract content using centralized utility + // Extract content using centralized utility - get simpler text version when possible const { text: originalTextContent, html: originalHtmlContent } = extractEmailContent(originalEmail); - // Prefer HTML content when available, but simplify it for Quill compatibility + // Simpler approach - prefer text content when available for clean replies let replyBody = ''; + let textReply = ''; // Create a header that works in both HTML and plain text const headerHtml = `
On ${dateStr}, ${fromStr} wrote:
`; - if (originalHtmlContent) { - try { - // Sanitize the original HTML to remove problematic elements - const sanitizedHtml = sanitizeHtml(originalHtmlContent); + // Use extracted text content when available for cleaner replies + if (originalTextContent) { + // Use text content with proper line breaks - limit to a reasonable size + const maxChars = 1500; + const truncatedText = originalTextContent.length > maxChars + ? originalTextContent.slice(0, maxChars) + '... [message truncated]' + : originalTextContent; - // Wrap the content in a blockquote with styling - replyBody = ` - ${headerHtml} -
- ${sanitizedHtml} -
- `; - } catch (error) { - console.error('Error processing HTML for reply:', error); - // Fallback to text if HTML processing fails - replyBody = ` - ${headerHtml} -
- ${originalTextContent.replace(/\n/g, '
')} -
- `; - } - } else if (originalTextContent) { - // Use text content with proper line breaks replyBody = ` ${headerHtml}
- ${originalTextContent.replace(/\n/g, '
')} + ${truncatedText.replace(/\n/g, '
')}
`; + + textReply = ` +On ${dateStr}, ${fromStr} wrote: +> ${truncatedText.split('\n').join('\n> ')} + `; + } + // If no text, try to sanitize and simplify HTML + else if (originalHtmlContent) { + try { + // Sanitize the original HTML to remove problematic elements and simplify + const sanitizedHtml = sanitizeHtml(originalHtmlContent); + + // Extract the text content from the sanitized HTML for the plaintext version + const tempDiv = document.createElement('div'); + tempDiv.innerHTML = sanitizedHtml; + const extractedText = tempDiv.textContent || tempDiv.innerText || ''; + + // Limit to a reasonable size + const maxChars = 1500; + const truncatedText = extractedText.length > maxChars + ? extractedText.slice(0, maxChars) + '... [message truncated]' + : extractedText; + + replyBody = ` + ${headerHtml} +
+ ${truncatedText.replace(/\n/g, '
')} +
+ `; + + textReply = ` +On ${dateStr}, ${fromStr} wrote: +> ${truncatedText.split('\n').join('\n> ')} + `; + } catch (error) { + console.error('Error processing HTML for reply:', error); + // Fallback to a basic template if everything fails + replyBody = ` + ${headerHtml} +
+ [Original message content could not be processed] +
+ `; + + textReply = ` +On ${dateStr}, ${fromStr} wrote: +> [Original message content could not be processed] + `; + } } else { // Empty or unrecognized content replyBody = ` @@ -323,27 +427,31 @@ export function formatReplyEmail(originalEmail: EmailMessage | LegacyEmailMessag [Original message content not available] `; + + textReply = ` +On ${dateStr}, ${fromStr} wrote: +> [Original message content not available] + `; } // Process the content with proper direction const processed = processContentWithDirection(replyBody); - - // Create plain text content - const textContent = ` -On ${dateStr}, ${fromStr} wrote: -> ${originalTextContent.split('\n').join('\n> ')} - `; + + // Extract any inline images as attachments + const inlineImages = extractInlineImages(originalHtmlContent); return { to, cc, subject: subject.startsWith('Re:') ? subject : `Re: ${subject}`, content: { - text: textContent, + text: textReply.trim(), html: processed.html, isHtml: true, direction: processed.direction - } + }, + // Include inline images as attachments if any were found + attachments: inlineImages.length > 0 ? inlineImages : undefined }; } @@ -367,11 +475,12 @@ export function formatForwardedEmail(originalEmail: EmailMessage | LegacyEmailMe // Get header information const { fromStr, toStr, ccStr, dateStr, subject } = getFormattedHeaderInfo(originalEmail); - // Extract content using centralized utility + // Extract content using centralized utility - get simpler text version when possible const { text: originalTextContent, html: originalHtmlContent } = extractEmailContent(originalEmail); - // Prefer HTML content when available, but simplify it for Quill compatibility + // Simpler approach - prefer text content when available for clean forwards let forwardBody = ''; + let textForward = ''; // Create metadata header that works in both HTML and plain text const headerHtml = ` @@ -385,36 +494,87 @@ export function formatForwardedEmail(originalEmail: EmailMessage | LegacyEmailMe `; - if (originalHtmlContent) { - try { - // Sanitize the original HTML to remove problematic elements - const sanitizedHtml = sanitizeHtml(originalHtmlContent); + // Use extracted text content when available for cleaner forwards + if (originalTextContent) { + // Use text content with proper line breaks - limit to a reasonable size + const maxChars = 2000; + const truncatedText = originalTextContent.length > maxChars + ? originalTextContent.slice(0, maxChars) + '... [message truncated]' + : originalTextContent; - // Wrap the content in a blockquote with styling - forwardBody = ` - ${headerHtml} -
- ${sanitizedHtml} -
- `; - } catch (error) { - console.error('Error processing HTML for forward:', error); - // Fallback to text if HTML processing fails - forwardBody = ` - ${headerHtml} -
- ${originalTextContent.replace(/\n/g, '
')} -
- `; - } - } else if (originalTextContent) { - // Use text content with proper line breaks forwardBody = ` ${headerHtml}
- ${originalTextContent.replace(/\n/g, '
')} + ${truncatedText.replace(/\n/g, '
')}
`; + + textForward = ` +---------- Forwarded message --------- +From: ${fromStr} +Date: ${dateStr} +Subject: ${subject || ''} +To: ${toStr} +${ccStr ? `Cc: ${ccStr}\n` : ''} + +${truncatedText} + `; + } + // If no text, try to sanitize and simplify HTML + else if (originalHtmlContent) { + try { + // Sanitize the original HTML to remove problematic elements and simplify + const sanitizedHtml = sanitizeHtml(originalHtmlContent); + + // Extract the text content from the sanitized HTML for the plaintext version + const tempDiv = document.createElement('div'); + tempDiv.innerHTML = sanitizedHtml; + const extractedText = tempDiv.textContent || tempDiv.innerText || ''; + + // Limit to a reasonable size + const maxChars = 2000; + const truncatedText = extractedText.length > maxChars + ? extractedText.slice(0, maxChars) + '... [message truncated]' + : extractedText; + + forwardBody = ` + ${headerHtml} +
+ ${truncatedText.replace(/\n/g, '
')} +
+ `; + + textForward = ` +---------- Forwarded message --------- +From: ${fromStr} +Date: ${dateStr} +Subject: ${subject || ''} +To: ${toStr} +${ccStr ? `Cc: ${ccStr}\n` : ''} + +${truncatedText} + `; + } catch (error) { + console.error('Error processing HTML for forward:', error); + // Fallback to a basic template if everything fails + forwardBody = ` + ${headerHtml} +
+ [Original message content could not be processed] +
+ `; + + textForward = ` +---------- Forwarded message --------- +From: ${fromStr} +Date: ${dateStr} +Subject: ${subject || ''} +To: ${toStr} +${ccStr ? `Cc: ${ccStr}\n` : ''} + +[Original message content could not be processed] + `; + } } else { // Empty or unrecognized content forwardBody = ` @@ -423,14 +583,8 @@ export function formatForwardedEmail(originalEmail: EmailMessage | LegacyEmailMe [Original message content not available] `; - } - - // Process the content with proper direction - const processed = processContentWithDirection(forwardBody); - - // Create plain text content - const textContent = ` - + + textForward = ` ---------- Forwarded message --------- From: ${fromStr} Date: ${dateStr} @@ -438,18 +592,40 @@ Subject: ${subject || ''} To: ${toStr} ${ccStr ? `Cc: ${ccStr}\n` : ''} -${originalTextContent} - `; +[Original message content not available] + `; + } + + // Process the content with proper direction + const processed = processContentWithDirection(forwardBody); + + // Check if the original email has attachments + const originalAttachments = originalEmail.attachments || []; + + // Extract any inline images and add to attachments + const inlineImages = extractInlineImages(originalHtmlContent); + + // Combine original attachments and inline images + const combinedAttachments = [ + ...originalAttachments.map(att => ({ + filename: att.filename || 'attachment', + contentType: att.contentType || 'application/octet-stream', + content: att.content + })), + ...inlineImages + ]; return { to: '', subject: subject.startsWith('Fwd:') ? subject : `Fwd: ${subject}`, content: { - text: textContent, + text: textForward.trim(), html: processed.html, isHtml: true, - direction: 'ltr' as const - } + direction: 'ltr' + }, + // Include attachments if any were found + attachments: combinedAttachments.length > 0 ? combinedAttachments : undefined }; }