courrier clean 2
This commit is contained in:
parent
ddf72cc4f0
commit
e3b946f7e9
@ -11,6 +11,13 @@ import { Input } from '@/components/ui/input';
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardFooter } from '@/components/ui/card';
|
||||
import DOMPurify from 'isomorphic-dompurify';
|
||||
|
||||
// Import the new email formatter utilities
|
||||
import {
|
||||
formatForwardedEmail,
|
||||
formatReplyEmail,
|
||||
EmailMessage as FormatterEmailMessage
|
||||
} from '@/lib/utils/email-formatter';
|
||||
|
||||
// Define EmailMessage interface locally instead of importing from server-only file
|
||||
interface EmailAddress {
|
||||
name: string;
|
||||
@ -44,124 +51,6 @@ interface EmailMessage {
|
||||
contentFetched?: boolean;
|
||||
}
|
||||
|
||||
// Simplified formatEmailForReplyOrForward that doesn't rely on server code
|
||||
function formatEmailForReplyOrForward(
|
||||
email: EmailMessage,
|
||||
type: 'reply' | 'reply-all' | 'forward'
|
||||
): {
|
||||
to: string;
|
||||
cc?: string;
|
||||
subject: string;
|
||||
body: string;
|
||||
} {
|
||||
// Format subject
|
||||
let subject = email.subject || '';
|
||||
if (type === 'reply' || type === 'reply-all') {
|
||||
if (!subject.startsWith('Re:')) {
|
||||
subject = `Re: ${subject}`;
|
||||
}
|
||||
} else if (type === 'forward') {
|
||||
if (!subject.startsWith('Fwd:')) {
|
||||
subject = `Fwd: ${subject}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Create 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'
|
||||
});
|
||||
|
||||
const sender = email.from[0];
|
||||
const fromText = sender?.name
|
||||
? `${sender.name} <${sender.address}>`
|
||||
: sender?.address || 'Unknown sender';
|
||||
|
||||
const quoteHeader = `<div style="font-weight: 500;">On ${formattedDate}, ${fromText} wrote:</div>`;
|
||||
|
||||
// Format content
|
||||
const quotedContent = email.html || email.content || email.text || '';
|
||||
|
||||
// Format recipients
|
||||
let to = '';
|
||||
let cc = '';
|
||||
|
||||
if (type === 'reply') {
|
||||
// Reply to sender only
|
||||
to = email.from.map(addr => `${addr.name} <${addr.address}>`).join(', ');
|
||||
} else if (type === 'reply-all') {
|
||||
// Reply to sender and all recipients
|
||||
to = email.from.map(addr => `${addr.name} <${addr.address}>`).join(', ');
|
||||
|
||||
// Add all original recipients to CC
|
||||
const allRecipients = [
|
||||
...(email.to || []),
|
||||
...(email.cc || [])
|
||||
];
|
||||
|
||||
cc = allRecipients
|
||||
.map(addr => `${addr.name} <${addr.address}>`)
|
||||
.join(', ');
|
||||
} else if (type === 'forward') {
|
||||
// Forward doesn't set recipients
|
||||
to = '';
|
||||
|
||||
// Format forward differently
|
||||
const formattedDate = typeof email.date === 'string'
|
||||
? new Date(email.date).toLocaleString()
|
||||
: email.date.toLocaleString();
|
||||
|
||||
const fromText = email.from.map(f => f.name ? `${f.name} <${f.address}>` : f.address).join(', ');
|
||||
const toText = email.to.map(t => t.name ? `${t.name} <${t.address}>` : t.address).join(', ');
|
||||
|
||||
return {
|
||||
to: '',
|
||||
subject,
|
||||
body: `
|
||||
<div class="forwarded-message" style="margin-top: 20px;">
|
||||
<div style="background-color: #f5f5f5; border-left: 3px solid #ddd; padding: 12px; margin-bottom: 15px; border-radius: 4px;">
|
||||
<p style="margin: 0 0 8px 0; font-weight: 500; color: #555; font-size: 14px;">---------- Forwarded message ---------</p>
|
||||
<p style="margin: 0 0 6px 0; font-size: 13px;"><span style="font-weight: 600; color: #444;">From:</span> ${fromText}</p>
|
||||
<p style="margin: 0 0 6px 0; font-size: 13px;"><span style="font-weight: 600; color: #444;">Date:</span> ${formattedDate}</p>
|
||||
<p style="margin: 0 0 6px 0; font-size: 13px;"><span style="font-weight: 600; color: #444;">Subject:</span> ${email.subject || ''}</p>
|
||||
<p style="margin: 0 0 6px 0; font-size: 13px;"><span style="font-weight: 600; color: #444;">To:</span> ${toText}</p>
|
||||
</div>
|
||||
<div class="forwarded-content" style="border-left: 2px solid #ddd; padding-left: 15px; color: #333;">
|
||||
${quotedContent ? quotedContent : '<p>No content available</p>'}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
};
|
||||
}
|
||||
|
||||
// Format body with improved styling for replies
|
||||
const body = `
|
||||
<div class="reply-body">
|
||||
<br/>
|
||||
<div class="quote-header" style="color: #555; font-size: 13px; margin: 20px 0 10px 0;">${quoteHeader}</div>
|
||||
<blockquote style="margin: 0; padding: 10px 0 10px 15px; border-left: 3px solid #ddd; color: #555; background-color: #f8f8f8; border-radius: 4px;">
|
||||
<div class="quoted-content" style="font-size: 13px;">
|
||||
${quotedContent}
|
||||
</div>
|
||||
</blockquote>
|
||||
</div>`;
|
||||
|
||||
return {
|
||||
to,
|
||||
cc: cc || undefined,
|
||||
subject,
|
||||
body
|
||||
};
|
||||
}
|
||||
|
||||
// Legacy interface for backward compatibility with old ComposeEmail component
|
||||
interface LegacyComposeEmailProps {
|
||||
showCompose: boolean;
|
||||
@ -270,99 +159,70 @@ export default function ComposeEmail(props: ComposeEmailAllProps) {
|
||||
// Initialize the form when replying to or forwarding an email
|
||||
useEffect(() => {
|
||||
if (initialEmail && type !== 'new') {
|
||||
// For all types of emails, format with a consistent approach
|
||||
let formattedContent = '';
|
||||
|
||||
if (type === 'forward') {
|
||||
// Format subject with Fwd: prefix if needed
|
||||
const subjectBase = initialEmail.subject || '(No subject)';
|
||||
const subjectRegex = /^(Fwd|FW|Forward):\s*/i;
|
||||
const subject = subjectRegex.test(subjectBase)
|
||||
? subjectBase
|
||||
: `Fwd: ${subjectBase}`;
|
||||
|
||||
setSubject(subject);
|
||||
|
||||
// Format forwarded content
|
||||
const fromString = initialEmail.from?.map(addr =>
|
||||
addr.name ? `${addr.name} <${addr.address}>` : addr.address
|
||||
).join(', ') || '';
|
||||
|
||||
const toString = initialEmail.to?.map(addr =>
|
||||
addr.name ? `${addr.name} <${addr.address}>` : addr.address
|
||||
).join(', ') || '';
|
||||
|
||||
const dateString = typeof initialEmail.date === 'string'
|
||||
? new Date(initialEmail.date).toLocaleString()
|
||||
: initialEmail.date.toLocaleString();
|
||||
|
||||
// Preprocess original content to avoid direction issues
|
||||
const originalContent = preprocessEmailContent(
|
||||
initialEmail.content || initialEmail.html || initialEmail.text || ''
|
||||
);
|
||||
|
||||
formattedContent = `
|
||||
<br><br>
|
||||
<div style="border-top: 1px solid #ccc; margin-top: 20px; padding-top: 10px; direction: ltr; text-align: left;">
|
||||
<div style="font-family: Arial, sans-serif; color: #333;">
|
||||
<div style="margin-bottom: 15px;">
|
||||
<div style="direction: ltr; text-align: left;">---------- Forwarded message ---------</div>
|
||||
<div style="direction: ltr; text-align: left;"><b>From:</b> ${fromString}</div>
|
||||
<div style="direction: ltr; text-align: left;"><b>Date:</b> ${dateString}</div>
|
||||
<div style="direction: ltr; text-align: left;"><b>Subject:</b> ${initialEmail.subject || ''}</div>
|
||||
<div style="direction: ltr; text-align: left;"><b>To:</b> ${toString}</div>
|
||||
</div>
|
||||
<div style="direction: ltr; text-align: left;">${originalContent}</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
// For reply/reply-all
|
||||
const formattedEmail = formatEmailForReplyOrForward(initialEmail, type as 'reply' | 'reply-all');
|
||||
|
||||
setTo(formattedEmail.to);
|
||||
|
||||
if (formattedEmail.cc) {
|
||||
setCc(formattedEmail.cc);
|
||||
setShowCc(true);
|
||||
try {
|
||||
const formatterEmail: FormatterEmailMessage = {
|
||||
id: initialEmail.id,
|
||||
messageId: initialEmail.messageId,
|
||||
subject: initialEmail.subject,
|
||||
from: initialEmail.from || [],
|
||||
to: initialEmail.to || [],
|
||||
cc: initialEmail.cc || [],
|
||||
bcc: initialEmail.bcc || [],
|
||||
date: initialEmail.date,
|
||||
content: initialEmail.content,
|
||||
html: initialEmail.html,
|
||||
text: initialEmail.text,
|
||||
hasAttachments: initialEmail.hasAttachments || false
|
||||
};
|
||||
|
||||
if (type === 'forward') {
|
||||
// For forwarding, use the dedicated formatter
|
||||
const { subject, content } = formatForwardedEmail(formatterEmail);
|
||||
setSubject(subject);
|
||||
setBody(content);
|
||||
setUserMessage(content);
|
||||
} else {
|
||||
// For reply/reply-all, use the reply formatter
|
||||
const { to, cc, subject, content } = formatReplyEmail(formatterEmail, type as 'reply' | 'reply-all');
|
||||
setTo(to);
|
||||
if (cc) {
|
||||
setCc(cc);
|
||||
setShowCc(true);
|
||||
}
|
||||
setSubject(subject);
|
||||
setBody(content);
|
||||
setUserMessage(content);
|
||||
}
|
||||
|
||||
setSubject(formattedEmail.subject);
|
||||
|
||||
// Process content to fix direction issues
|
||||
formattedContent = preprocessEmailContent(formattedEmail.body);
|
||||
}
|
||||
|
||||
// Set the entire content as one editable area
|
||||
// Force LTR direction for quoted content
|
||||
setBody(formattedContent);
|
||||
setUserMessage(formattedContent);
|
||||
|
||||
// Focus editor after initializing
|
||||
setTimeout(() => {
|
||||
if (editorRef.current) {
|
||||
editorRef.current.focus();
|
||||
editorRef.current.dir = isRTL ? 'rtl' : 'ltr';
|
||||
|
||||
// Place cursor at the beginning of the content
|
||||
const selection = window.getSelection();
|
||||
if (selection) {
|
||||
const range = document.createRange();
|
||||
// Focus editor after initializing
|
||||
setTimeout(() => {
|
||||
if (editorRef.current) {
|
||||
editorRef.current.focus();
|
||||
|
||||
// Find the first text node or element node
|
||||
let firstNode = editorRef.current.firstChild;
|
||||
if (firstNode) {
|
||||
range.setStart(firstNode, 0);
|
||||
range.collapse(true);
|
||||
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
// Place cursor at the beginning
|
||||
const selection = window.getSelection();
|
||||
if (selection) {
|
||||
try {
|
||||
const range = document.createRange();
|
||||
|
||||
if (editorRef.current.firstChild) {
|
||||
range.setStart(editorRef.current.firstChild, 0);
|
||||
range.collapse(true);
|
||||
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error positioning cursor:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 100);
|
||||
}, 100);
|
||||
} catch (error) {
|
||||
console.error('Error formatting email:', error);
|
||||
}
|
||||
}
|
||||
}, [initialEmail, type, isRTL]);
|
||||
}, [initialEmail, type]);
|
||||
|
||||
// Format date for the forwarded message header
|
||||
const formatDate = (date: Date | null): string => {
|
||||
@ -401,43 +261,6 @@ export default function ComposeEmail(props: ComposeEmailAllProps) {
|
||||
).join(', ');
|
||||
};
|
||||
|
||||
// Initialize forwarded email with clear structure and style preservation
|
||||
const initializeForwardedEmail = async () => {
|
||||
console.log('Starting initializeForwardedEmail');
|
||||
if (!initialEmail) {
|
||||
console.error('No email available for forwarding');
|
||||
setBody('<div style="color: #666; font-style: italic;">No email available for forwarding</div>');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Format subject with Fwd: prefix if needed
|
||||
const subjectBase = initialEmail.subject || '(No subject)';
|
||||
const subjectRegex = /^(Fwd|FW|Forward):\s*/i;
|
||||
const subject = subjectRegex.test(subjectBase)
|
||||
? subjectBase
|
||||
: `Fwd: ${subjectBase}`;
|
||||
|
||||
setSubject(subject);
|
||||
|
||||
// For forwarded emails, we'll use a completely different approach
|
||||
// Just save the original HTML content and use it directly
|
||||
// This preserves all formatting without trying to parse it
|
||||
|
||||
// Set message parts for the editor
|
||||
const content = initialEmail.content || initialEmail.html || initialEmail.text || '';
|
||||
setOriginalContent(content);
|
||||
setUserMessage(''); // Start with empty user message
|
||||
setBody(''); // Will be constructed when sending
|
||||
|
||||
// Log for debugging
|
||||
console.log('Set originalContent:', content.substring(0, 100) + '...');
|
||||
} catch (error) {
|
||||
console.error('Error formatting forwarded email:', error);
|
||||
setBody('<div style="color: #666; font-style: italic;">Error formatting forwarded email content</div>');
|
||||
}
|
||||
};
|
||||
|
||||
// Handle attachment selection
|
||||
const handleAttachmentClick = () => {
|
||||
attachmentInputRef.current?.click();
|
||||
|
||||
208
lib/utils/email-formatter.ts
Normal file
208
lib/utils/email-formatter.ts
Normal file
@ -0,0 +1,208 @@
|
||||
/**
|
||||
* 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
|
||||
};
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user