courrier clean 2

This commit is contained in:
alma 2025-04-26 18:24:28 +02:00
parent ddf72cc4f0
commit e3b946f7e9
2 changed files with 273 additions and 242 deletions

View File

@ -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();

View 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
};
}