courrier preview
This commit is contained in:
parent
1ced248940
commit
0251b08113
@ -172,6 +172,16 @@ export default function ComposeEmail(props: ComposeEmailProps) {
|
|||||||
});
|
});
|
||||||
setEmailContent(content);
|
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') {
|
else if (type === 'forward') {
|
||||||
// Get formatted data for forward
|
// Get formatted data for forward
|
||||||
@ -209,9 +219,10 @@ export default function ComposeEmail(props: ComposeEmailProps) {
|
|||||||
setEmailContent(content);
|
setEmailContent(content);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the original email has attachments, include them
|
// Handle attachments for forward (original attachments + extracted inline images)
|
||||||
if (initialEmail.attachments && initialEmail.attachments.length > 0) {
|
if (formatted.attachments && formatted.attachments.length > 0) {
|
||||||
const formattedAttachments = initialEmail.attachments.map(att => ({
|
console.log(`Processing ${formatted.attachments.length} attachments for forwarded email`);
|
||||||
|
const formattedAttachments = formatted.attachments.map(att => ({
|
||||||
name: att.filename || 'attachment',
|
name: att.filename || 'attachment',
|
||||||
type: att.contentType || 'application/octet-stream',
|
type: att.contentType || 'application/octet-stream',
|
||||||
content: att.content || ''
|
content: att.content || ''
|
||||||
|
|||||||
@ -36,6 +36,11 @@ export interface FormattedEmail {
|
|||||||
cc?: string;
|
cc?: string;
|
||||||
subject: string;
|
subject: string;
|
||||||
content: EmailContent;
|
content: EmailContent;
|
||||||
|
attachments?: Array<{
|
||||||
|
filename: string;
|
||||||
|
contentType: string;
|
||||||
|
content?: string;
|
||||||
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -252,6 +257,71 @@ function getFormattedHeaderInfo(email: any): {
|
|||||||
return { fromStr, toStr, ccStr, dateStr, subject };
|
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
|
* Format email for reply
|
||||||
*/
|
*/
|
||||||
@ -276,45 +346,79 @@ export function formatReplyEmail(originalEmail: EmailMessage | LegacyEmailMessag
|
|||||||
// Get header information
|
// Get header information
|
||||||
const { fromStr, dateStr, subject } = getFormattedHeaderInfo(originalEmail);
|
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);
|
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 replyBody = '';
|
||||||
|
let textReply = '';
|
||||||
|
|
||||||
// Create a header that works in both HTML and plain text
|
// Create a header that works in both HTML and plain text
|
||||||
const headerHtml = `<div style="margin-top: 20px; margin-bottom: 10px; color: #666;">On ${dateStr}, ${fromStr} wrote:</div>`;
|
const headerHtml = `<div style="margin-top: 20px; margin-bottom: 10px; color: #666;">On ${dateStr}, ${fromStr} wrote:</div>`;
|
||||||
|
|
||||||
if (originalHtmlContent) {
|
// Use extracted text content when available for cleaner replies
|
||||||
try {
|
if (originalTextContent) {
|
||||||
// Sanitize the original HTML to remove problematic elements
|
// Use text content with proper line breaks - limit to a reasonable size
|
||||||
const sanitizedHtml = sanitizeHtml(originalHtmlContent);
|
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}
|
|
||||||
<blockquote style="margin: 10px 0; padding-left: 10px; border-left: 2px solid #ddd; color: #505050;">
|
|
||||||
${sanitizedHtml}
|
|
||||||
</blockquote>
|
|
||||||
`;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error processing HTML for reply:', error);
|
|
||||||
// Fallback to text if HTML processing fails
|
|
||||||
replyBody = `
|
|
||||||
${headerHtml}
|
|
||||||
<blockquote style="margin: 10px 0; padding-left: 10px; border-left: 2px solid #ddd; color: #505050;">
|
|
||||||
${originalTextContent.replace(/\n/g, '<br>')}
|
|
||||||
</blockquote>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
} else if (originalTextContent) {
|
|
||||||
// Use text content with proper line breaks
|
|
||||||
replyBody = `
|
replyBody = `
|
||||||
${headerHtml}
|
${headerHtml}
|
||||||
<blockquote style="margin: 10px 0; padding-left: 10px; border-left: 2px solid #ddd; color: #505050;">
|
<blockquote style="margin: 10px 0; padding-left: 10px; border-left: 2px solid #ddd; color: #505050;">
|
||||||
${originalTextContent.replace(/\n/g, '<br>')}
|
${truncatedText.replace(/\n/g, '<br>')}
|
||||||
</blockquote>
|
</blockquote>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
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}
|
||||||
|
<blockquote style="margin: 10px 0; padding-left: 10px; border-left: 2px solid #ddd; color: #505050;">
|
||||||
|
${truncatedText.replace(/\n/g, '<br>')}
|
||||||
|
</blockquote>
|
||||||
|
`;
|
||||||
|
|
||||||
|
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}
|
||||||
|
<blockquote style="margin: 10px 0; padding-left: 10px; border-left: 2px solid #ddd; color: #505050;">
|
||||||
|
[Original message content could not be processed]
|
||||||
|
</blockquote>
|
||||||
|
`;
|
||||||
|
|
||||||
|
textReply = `
|
||||||
|
On ${dateStr}, ${fromStr} wrote:
|
||||||
|
> [Original message content could not be processed]
|
||||||
|
`;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Empty or unrecognized content
|
// Empty or unrecognized content
|
||||||
replyBody = `
|
replyBody = `
|
||||||
@ -323,27 +427,31 @@ export function formatReplyEmail(originalEmail: EmailMessage | LegacyEmailMessag
|
|||||||
[Original message content not available]
|
[Original message content not available]
|
||||||
</blockquote>
|
</blockquote>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
textReply = `
|
||||||
|
On ${dateStr}, ${fromStr} wrote:
|
||||||
|
> [Original message content not available]
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process the content with proper direction
|
// Process the content with proper direction
|
||||||
const processed = processContentWithDirection(replyBody);
|
const processed = processContentWithDirection(replyBody);
|
||||||
|
|
||||||
// Create plain text content
|
// Extract any inline images as attachments
|
||||||
const textContent = `
|
const inlineImages = extractInlineImages(originalHtmlContent);
|
||||||
On ${dateStr}, ${fromStr} wrote:
|
|
||||||
> ${originalTextContent.split('\n').join('\n> ')}
|
|
||||||
`;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
to,
|
to,
|
||||||
cc,
|
cc,
|
||||||
subject: subject.startsWith('Re:') ? subject : `Re: ${subject}`,
|
subject: subject.startsWith('Re:') ? subject : `Re: ${subject}`,
|
||||||
content: {
|
content: {
|
||||||
text: textContent,
|
text: textReply.trim(),
|
||||||
html: processed.html,
|
html: processed.html,
|
||||||
isHtml: true,
|
isHtml: true,
|
||||||
direction: processed.direction
|
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
|
// Get header information
|
||||||
const { fromStr, toStr, ccStr, dateStr, subject } = getFormattedHeaderInfo(originalEmail);
|
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);
|
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 forwardBody = '';
|
||||||
|
let textForward = '';
|
||||||
|
|
||||||
// Create metadata header that works in both HTML and plain text
|
// Create metadata header that works in both HTML and plain text
|
||||||
const headerHtml = `
|
const headerHtml = `
|
||||||
@ -385,36 +494,87 @@ export function formatForwardedEmail(originalEmail: EmailMessage | LegacyEmailMe
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
if (originalHtmlContent) {
|
// Use extracted text content when available for cleaner forwards
|
||||||
try {
|
if (originalTextContent) {
|
||||||
// Sanitize the original HTML to remove problematic elements
|
// Use text content with proper line breaks - limit to a reasonable size
|
||||||
const sanitizedHtml = sanitizeHtml(originalHtmlContent);
|
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}
|
|
||||||
<blockquote style="margin-top: 10px; padding-left: 10px; border-left: 2px solid #ddd; color: #505050;">
|
|
||||||
${sanitizedHtml}
|
|
||||||
</blockquote>
|
|
||||||
`;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error processing HTML for forward:', error);
|
|
||||||
// Fallback to text if HTML processing fails
|
|
||||||
forwardBody = `
|
|
||||||
${headerHtml}
|
|
||||||
<blockquote style="margin-top: 10px; padding-left: 10px; border-left: 2px solid #ddd; color: #505050;">
|
|
||||||
${originalTextContent.replace(/\n/g, '<br>')}
|
|
||||||
</blockquote>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
} else if (originalTextContent) {
|
|
||||||
// Use text content with proper line breaks
|
|
||||||
forwardBody = `
|
forwardBody = `
|
||||||
${headerHtml}
|
${headerHtml}
|
||||||
<blockquote style="margin-top: 10px; padding-left: 10px; border-left: 2px solid #ddd; color: #505050;">
|
<blockquote style="margin-top: 10px; padding-left: 10px; border-left: 2px solid #ddd; color: #505050;">
|
||||||
${originalTextContent.replace(/\n/g, '<br>')}
|
${truncatedText.replace(/\n/g, '<br>')}
|
||||||
</blockquote>
|
</blockquote>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
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}
|
||||||
|
<blockquote style="margin-top: 10px; padding-left: 10px; border-left: 2px solid #ddd; color: #505050;">
|
||||||
|
${truncatedText.replace(/\n/g, '<br>')}
|
||||||
|
</blockquote>
|
||||||
|
`;
|
||||||
|
|
||||||
|
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}
|
||||||
|
<blockquote style="margin-top: 10px; padding-left: 10px; border-left: 2px solid #ddd; color: #505050;">
|
||||||
|
[Original message content could not be processed]
|
||||||
|
</blockquote>
|
||||||
|
`;
|
||||||
|
|
||||||
|
textForward = `
|
||||||
|
---------- Forwarded message ---------
|
||||||
|
From: ${fromStr}
|
||||||
|
Date: ${dateStr}
|
||||||
|
Subject: ${subject || ''}
|
||||||
|
To: ${toStr}
|
||||||
|
${ccStr ? `Cc: ${ccStr}\n` : ''}
|
||||||
|
|
||||||
|
[Original message content could not be processed]
|
||||||
|
`;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Empty or unrecognized content
|
// Empty or unrecognized content
|
||||||
forwardBody = `
|
forwardBody = `
|
||||||
@ -423,14 +583,8 @@ export function formatForwardedEmail(originalEmail: EmailMessage | LegacyEmailMe
|
|||||||
[Original message content not available]
|
[Original message content not available]
|
||||||
</blockquote>
|
</blockquote>
|
||||||
`;
|
`;
|
||||||
}
|
|
||||||
|
textForward = `
|
||||||
// Process the content with proper direction
|
|
||||||
const processed = processContentWithDirection(forwardBody);
|
|
||||||
|
|
||||||
// Create plain text content
|
|
||||||
const textContent = `
|
|
||||||
|
|
||||||
---------- Forwarded message ---------
|
---------- Forwarded message ---------
|
||||||
From: ${fromStr}
|
From: ${fromStr}
|
||||||
Date: ${dateStr}
|
Date: ${dateStr}
|
||||||
@ -438,18 +592,40 @@ Subject: ${subject || ''}
|
|||||||
To: ${toStr}
|
To: ${toStr}
|
||||||
${ccStr ? `Cc: ${ccStr}\n` : ''}
|
${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 {
|
return {
|
||||||
to: '',
|
to: '',
|
||||||
subject: subject.startsWith('Fwd:') ? subject : `Fwd: ${subject}`,
|
subject: subject.startsWith('Fwd:') ? subject : `Fwd: ${subject}`,
|
||||||
content: {
|
content: {
|
||||||
text: textContent,
|
text: textForward.trim(),
|
||||||
html: processed.html,
|
html: processed.html,
|
||||||
isHtml: true,
|
isHtml: true,
|
||||||
direction: 'ltr' as const
|
direction: 'ltr'
|
||||||
}
|
},
|
||||||
|
// Include attachments if any were found
|
||||||
|
attachments: combinedAttachments.length > 0 ? combinedAttachments : undefined
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user