courrier preview

This commit is contained in:
alma 2025-05-01 11:37:05 +02:00
parent 7f87e4f00a
commit 1ced248940
5 changed files with 482 additions and 77 deletions

View File

@ -56,50 +56,122 @@ interface ComposeEmailProps {
export default function ComposeEmail(props: ComposeEmailProps) {
const { initialEmail, type = 'new', onClose, onSend, accounts = [] } = props;
// Email form state
const [to, setTo] = useState<string>('');
const [cc, setCc] = useState<string>('');
const [bcc, setBcc] = useState<string>('');
const [subject, setSubject] = useState<string>('');
const [emailContent, setEmailContent] = useState<string>('');
const [showCc, setShowCc] = useState<boolean>(false);
const [showBcc, setShowBcc] = useState<boolean>(false);
const [sending, setSending] = useState<boolean>(false);
const [selectedAccount, setSelectedAccount] = useState<{
id: string;
email: string;
display_name?: string;
} | null>(accounts.length > 0 ? accounts[0] : null);
const [attachments, setAttachments] = useState<Array<{
name: string;
content: string;
type: string;
}>>([]);
// State for email form
const [selectedAccount, setSelectedAccount] = useState<any>(accounts[0]);
const [to, setTo] = useState('');
const [cc, setCc] = useState('');
const [bcc, setBcc] = useState('');
const [subject, setSubject] = useState('');
const [emailContent, setEmailContent] = useState('');
const [showCc, setShowCc] = useState(false);
const [showBcc, setShowBcc] = useState(false);
const [sending, setSending] = useState(false);
const [attachments, setAttachments] = useState<Array<{name: string; content: string; type: string;}>>([]);
// Reference to editor
const editorRef = useRef<HTMLDivElement>(null);
// Helper function to get formatted info from email
function getFormattedInfoForEmail(email: any) {
// Format the subject
const subject = email.subject || '';
// Format the date
const dateStr = email.date ? new Date(email.date).toLocaleString() : 'Unknown Date';
// Format sender
const fromStr = Array.isArray(email.from)
? email.from.map((addr: any) => {
if (typeof addr === 'string') return addr;
return addr.name ? `${addr.name} <${addr.address}>` : addr.address;
}).join(', ')
: typeof email.from === 'string'
? email.from
: email.from?.address
? email.from.name
? `${email.from.name} <${email.from.address}>`
: email.from.address
: 'Unknown Sender';
// Format recipients
const toStr = Array.isArray(email.to)
? email.to.map((addr: any) => {
if (typeof addr === 'string') return addr;
return addr.name ? `${addr.name} <${addr.address}>` : addr.address;
}).join(', ')
: typeof email.to === 'string'
? email.to
: '';
// Format CC
const ccStr = Array.isArray(email.cc)
? email.cc.map((addr: any) => {
if (typeof addr === 'string') return addr;
return addr.name ? `${addr.name} <${addr.address}>` : addr.address;
}).join(', ')
: typeof email.cc === 'string'
? email.cc
: '';
return { fromStr, toStr, ccStr, dateStr, subject };
}
// Initialize the form when replying to or forwarding an email
// Initialize email form based on initial email and type
useEffect(() => {
if (initialEmail && type !== 'new') {
if (initialEmail) {
try {
// Set recipients based on type
console.log('Initializing compose with email:', {
id: initialEmail.id,
subject: initialEmail.subject,
hasContent: !!initialEmail.content,
contentType: initialEmail.content ? typeof initialEmail.content : 'none'
});
// Set default account from original email - use type assertion since accountId might be custom property
const emailAny = initialEmail as any;
if (emailAny.accountId && accounts?.length) {
const account = accounts.find(a => a.id === emailAny.accountId);
if (account) {
setSelectedAccount(account);
}
}
// Get recipients based on type
if (type === 'reply' || type === 'reply-all') {
// Get formatted data for reply
const formatted = formatReplyEmail(initialEmail, type);
// Set the recipients
// Set reply addresses
setTo(formatted.to);
if (formatted.cc) {
setCc(formatted.cc);
setShowCc(true);
setCc(formatted.cc);
}
// Set subject
setSubject(formatted.subject);
// Set content with original email
const content = formatted.content.html || formatted.content.text;
setEmailContent(content);
// Set content with original email - ensure we have content
const content = formatted.content.html || formatted.content.text || '';
if (!content) {
console.warn('Reply content is empty, falling back to a basic template');
// Provide a basic template if the content is empty
const { fromStr, dateStr } = getFormattedInfoForEmail(initialEmail);
const fallbackContent = `
<div style="margin-top: 20px; margin-bottom: 10px; color: #666;">On ${dateStr}, ${fromStr} wrote:</div>
<blockquote style="margin: 10px 0; padding-left: 10px; border-left: 2px solid #ddd; color: #505050;">
[Original message content could not be loaded]
</blockquote>
`;
setEmailContent(fallbackContent);
} else {
console.log('Setting reply content:', {
length: content.length,
isHtml: formatted.content.isHtml
});
setEmailContent(content);
}
}
else if (type === 'forward') {
// Get formatted data for forward
@ -108,9 +180,34 @@ export default function ComposeEmail(props: ComposeEmailProps) {
// Set subject
setSubject(formatted.subject);
// Set content with original email
const content = formatted.content.html || formatted.content.text;
setEmailContent(content);
// Set content with original email - ensure we have content
const content = formatted.content.html || formatted.content.text || '';
if (!content) {
console.warn('Forward content is empty, falling back to a basic template');
// Provide a basic template if the content is empty
const { fromStr, toStr, ccStr, dateStr, subject } = getFormattedInfoForEmail(initialEmail);
const fallbackContent = `
<div style="margin-top: 20px; color: #666;">
<div>---------- Forwarded message ---------</div>
<div><strong>From:</strong> ${fromStr}</div>
<div><strong>Date:</strong> ${dateStr}</div>
<div><strong>Subject:</strong> ${subject || ''}</div>
<div><strong>To:</strong> ${toStr}</div>
${ccStr ? `<div><strong>Cc:</strong> ${ccStr}</div>` : ''}
</div>
<blockquote style="margin-top: 10px; padding-left: 10px; border-left: 2px solid #ddd; color: #505050;">
[Original message content could not be loaded]
</blockquote>
`;
setEmailContent(fallbackContent);
} else {
console.log('Setting forward content:', {
length: content.length,
isHtml: formatted.content.isHtml
});
setEmailContent(content);
}
// If the original email has attachments, include them
if (initialEmail.attachments && initialEmail.attachments.length > 0) {
@ -124,9 +221,11 @@ export default function ComposeEmail(props: ComposeEmailProps) {
}
} catch (error) {
console.error('Error initializing compose form:', error);
// Provide a fallback in case of error
setEmailContent('<p>Error loading email content</p>');
}
}
}, [initialEmail, type]);
}, [initialEmail, type, accounts]);
// Place cursor at beginning and ensure content is scrolled to top
useEffect(() => {

View File

@ -99,13 +99,30 @@ const RichEmailEditor: React.FC<RichEmailEditorProps> = ({
// Simplify complex email content to something Quill can handle better
const sanitizedContent = sanitizeHtml(processedContent || initialContent);
// Use direct innerHTML setting for the initial content
quillRef.current.root.innerHTML = sanitizedContent;
// Set the direction for the content
quillRef.current.format('direction', direction);
if (direction === 'rtl') {
quillRef.current.format('align', 'right');
// Check if sanitized content is valid
if (sanitizedContent.trim().length === 0) {
console.warn('Sanitized content is empty after processing, using fallback approach');
// Try to extract text content if HTML processing failed
try {
const tempDiv = document.createElement('div');
tempDiv.innerHTML = initialContent;
const textContent = tempDiv.textContent || tempDiv.innerText || 'Empty content';
// Set text directly to ensure something displays
quillRef.current.setText(textContent);
} catch (e) {
console.error('Text extraction fallback failed:', e);
quillRef.current.setText('Error loading content');
}
} else {
// Use direct innerHTML setting for the initial content
quillRef.current.root.innerHTML = sanitizedContent;
// Set the direction for the content
quillRef.current.format('direction', direction);
if (direction === 'rtl') {
quillRef.current.format('align', 'right');
}
}
// Set cursor at the beginning
@ -131,19 +148,25 @@ const RichEmailEditor: React.FC<RichEmailEditorProps> = ({
}
} catch (err) {
console.error('Error setting initial content:', err);
// Fallback: just set text
quillRef.current.setText('');
// Extract text as a last resort
// Enhanced fallback mechanism for complex content
try {
// Create a temporary div to extract text from HTML
// First try to extract text from HTML
const tempDiv = document.createElement('div');
tempDiv.innerHTML = initialContent;
const textContent = tempDiv.textContent || tempDiv.innerText || '';
quillRef.current.setText(textContent);
if (textContent.trim()) {
console.log('Using extracted text fallback, length:', textContent.length);
quillRef.current.setText(textContent);
} else {
// If text extraction fails or returns empty, provide a message
console.log('Using empty content fallback');
quillRef.current.setText('Unable to load original content');
}
} catch (e) {
console.error('Fallback failed too:', e);
quillRef.current.setText('');
console.error('All fallbacks failed:', e);
quillRef.current.setText('Error loading content');
}
}
}
@ -184,19 +207,36 @@ const RichEmailEditor: React.FC<RichEmailEditorProps> = ({
// Only update if content changed to avoid editor position reset
if (initialContent !== currentContent) {
try {
console.log('Updating content in editor:', {
contentLength: initialContent.length,
startsWithHtml: initialContent.trim().startsWith('<')
});
// Process content to ensure correct direction
const { direction, html: processedContent } = processContentWithDirection(initialContent);
// Sanitize the HTML
const sanitizedContent = sanitizeHtml(processedContent || initialContent);
// SIMPLIFIED: Set content directly to the root element rather than using clipboard
quillRef.current.root.innerHTML = sanitizedContent;
// Set the direction for the content
quillRef.current.format('direction', direction);
if (direction === 'rtl') {
quillRef.current.format('align', 'right');
// Check if content is valid HTML
if (sanitizedContent.trim().length === 0) {
console.warn('Sanitized content is empty, using original content');
// If sanitized content is empty, try to extract text from original
const tempDiv = document.createElement('div');
tempDiv.innerHTML = initialContent;
const textContent = tempDiv.textContent || tempDiv.innerText || '';
// Create simple HTML with text content
quillRef.current.setText(textContent);
} else {
// SIMPLIFIED: Set content directly to the root element rather than using clipboard
quillRef.current.root.innerHTML = sanitizedContent;
// Set the direction for the content
quillRef.current.format('direction', direction);
if (direction === 'rtl') {
quillRef.current.format('align', 'right');
}
}
// Force update
@ -215,6 +255,8 @@ const RichEmailEditor: React.FC<RichEmailEditorProps> = ({
quillRef.current.setText(textContent);
} catch (e) {
console.error('All fallbacks failed:', e);
// Last resort
quillRef.current.setText('Error loading content');
}
}
}

View File

@ -75,7 +75,35 @@ export function formatEmailContent(email: any): string {
}
// Use the centralized sanitizeHtml function
const sanitizedContent = sanitizeHtml(content);
let sanitizedContent = sanitizeHtml(content);
// Fix URL encoding issues that might occur during sanitization
try {
// Temporary element to manipulate the HTML
const tempDiv = document.createElement('div');
tempDiv.innerHTML = sanitizedContent;
// Fix all links
const links = tempDiv.querySelectorAll('a');
links.forEach(link => {
const href = link.getAttribute('href');
if (href && href.includes('%')) {
try {
// Try to decode URLs that might have been double-encoded
const decodedHref = decodeURIComponent(href);
link.setAttribute('href', decodedHref);
} catch (e) {
// If decoding fails, keep the original
console.warn('Failed to decode href:', href);
}
}
});
// Get the fixed HTML
sanitizedContent = tempDiv.innerHTML;
} catch (e) {
console.error('Error fixing URLs in content:', e);
}
// Fix common email client quirks
let fixedContent = sanitizedContent

View File

@ -279,16 +279,51 @@ export function formatReplyEmail(originalEmail: EmailMessage | LegacyEmailMessag
// Extract content using centralized utility
const { text: originalTextContent, html: originalHtmlContent } = extractEmailContent(originalEmail);
// Create a simpler HTML structure that's easier for Quill to handle
const replyBody = `
<br/>
<div class="email-original-content">
<div>On ${dateStr}, ${fromStr} wrote:</div>
// Prefer HTML content when available, but simplify it for Quill compatibility
let replyBody = '';
// 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>`;
if (originalHtmlContent) {
try {
// Sanitize the original HTML to remove problematic elements
const sanitizedHtml = sanitizeHtml(originalHtmlContent);
// 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 = `
${headerHtml}
<blockquote style="margin: 10px 0; padding-left: 10px; border-left: 2px solid #ddd; color: #505050;">
${originalTextContent.replace(/\n/g, '<br>')}
</blockquote>
</div>
`;
`;
} else {
// Empty or unrecognized content
replyBody = `
${headerHtml}
<blockquote style="margin: 10px 0; padding-left: 10px; border-left: 2px solid #ddd; color: #505050;">
[Original message content not available]
</blockquote>
`;
}
// Process the content with proper direction
const processed = processContentWithDirection(replyBody);
@ -335,21 +370,60 @@ export function formatForwardedEmail(originalEmail: EmailMessage | LegacyEmailMe
// Extract content using centralized utility
const { text: originalTextContent, html: originalHtmlContent } = extractEmailContent(originalEmail);
// Create a simpler forwarded content structure for better Quill compatibility
const forwardBody = `
<br/>
<div class="email-original-content">
// Prefer HTML content when available, but simplify it for Quill compatibility
let forwardBody = '';
// Create metadata header that works in both HTML and plain text
const headerHtml = `
<div style="margin-top: 20px; color: #666;">
<div>---------- Forwarded message ---------</div>
<div><strong>From:</strong> ${fromStr}</div>
<div><strong>Date:</strong> ${dateStr}</div>
<div><strong>Subject:</strong> ${subject || ''}</div>
<div><strong>To:</strong> ${toStr}</div>
${ccStr ? `<div><strong>Cc:</strong> ${ccStr}</div>` : ''}
</div>
`;
if (originalHtmlContent) {
try {
// Sanitize the original HTML to remove problematic elements
const sanitizedHtml = sanitizeHtml(originalHtmlContent);
// 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 = `
${headerHtml}
<blockquote style="margin-top: 10px; padding-left: 10px; border-left: 2px solid #ddd; color: #505050;">
${originalTextContent.replace(/\n/g, '<br>')}
</blockquote>
</div>
`;
`;
} else {
// Empty or unrecognized content
forwardBody = `
${headerHtml}
<blockquote style="margin-top: 10px; padding-left: 10px; border-left: 2px solid #ddd; color: #505050;">
[Original message content not available]
</blockquote>
`;
}
// Process the content with proper direction
const processed = processContentWithDirection(forwardBody);

View File

@ -78,8 +78,61 @@ export function extractEmailContent(email: any): { text: string; html: string }
// Extract based on common formats
if (email) {
if (typeof email.content === 'object' && email.content) {
// Standard format with content object
textContent = email.content.text || '';
htmlContent = email.content.html || '';
// Handle complex email formats where content might be nested
if (!textContent && !htmlContent) {
// Try to find content in deeper nested structure
if (email.content.body) {
if (typeof email.content.body === 'string') {
// Determine if body is HTML or text
if (email.content.body.includes('<') && (
email.content.body.includes('<html') ||
email.content.body.includes('<body') ||
email.content.body.includes('<div')
)) {
htmlContent = email.content.body;
} else {
textContent = email.content.body;
}
} else if (typeof email.content.body === 'object' && email.content.body) {
// Some email formats nest content inside body
htmlContent = email.content.body.html || '';
textContent = email.content.body.text || '';
}
}
// Check for data property which some email services use
if (!textContent && !htmlContent && email.content.data) {
if (typeof email.content.data === 'string') {
// Check if data looks like HTML
if (email.content.data.includes('<') && (
email.content.data.includes('<html') ||
email.content.data.includes('<body') ||
email.content.data.includes('<div')
)) {
htmlContent = email.content.data;
} else {
textContent = email.content.data;
}
}
}
// Last resort: try to convert the entire content object to string
if (!textContent && !htmlContent) {
try {
// Some email servers encode content as JSON string
const contentStr = JSON.stringify(email.content);
if (contentStr && contentStr !== '{}') {
textContent = `[Complex email content - please view in original format]`;
}
} catch (e) {
console.error('Error extracting content from complex object:', e);
}
}
}
} else if (typeof email.content === 'string') {
// Check if content is likely HTML
if (email.content.includes('<') && (
@ -95,9 +148,39 @@ export function extractEmailContent(email: any): { text: string; html: string }
// Check other common properties
htmlContent = email.html || '';
textContent = email.text || '';
// If still no content, check for less common properties
if (!htmlContent && !textContent) {
// Try additional properties that some email clients use
htmlContent = email.body?.html || email.bodyHtml || email.htmlBody || '';
textContent = email.body?.text || email.bodyText || email.plainText || '';
}
}
}
// Ensure we always have at least some text content
if (!textContent && htmlContent) {
try {
// Create a helper function to extract text from HTML
const tempDiv = document.createElement('div');
tempDiv.innerHTML = htmlContent;
textContent = tempDiv.textContent || tempDiv.innerText || '';
} catch (e) {
// Fallback for non-browser environments or if extraction fails
textContent = htmlContent.replace(/<[^>]*>/g, ' ')
.replace(/\s+/g, ' ')
.trim() || '[Email content]';
}
}
// Add debug logging to help troubleshoot content extraction
console.log('Extracted email content:', {
hasHtml: !!htmlContent,
htmlLength: htmlContent.length,
hasText: !!textContent,
textLength: textContent.length
});
return { text: textContent, html: htmlContent };
}
@ -133,7 +216,8 @@ export function processContentWithDirection(content: string | EmailContent | nul
if (content.includes('<') && (
content.includes('<html') ||
content.includes('<body') ||
content.includes('<div')
content.includes('<div') ||
content.includes('<p>')
)) {
htmlContent = content;
} else {
@ -145,14 +229,73 @@ export function processContentWithDirection(content: string | EmailContent | nul
htmlContent = content.html || '';
}
// Handle complex email content that might not be properly detected
if (!textContent && !htmlContent && typeof content === 'object') {
console.log('Processing complex content object:', content);
// Try to extract content from complex object structure
try {
// Check for common email content formats
// Type assertion to 'any' since we need to handle various email formats
const contentAny = content as any;
if (contentAny.body) {
if (typeof contentAny.body === 'string') {
// Detect if body is HTML or text
if (contentAny.body.includes('<') && (
contentAny.body.includes('<html') ||
contentAny.body.includes('<body') ||
contentAny.body.includes('<div')
)) {
htmlContent = contentAny.body;
} else {
textContent = contentAny.body;
}
} else if (typeof contentAny.body === 'object' && contentAny.body) {
// Extract from nested body object
htmlContent = contentAny.body.html || '';
textContent = contentAny.body.text || '';
}
}
// Try to convert complex content to string for debugging
if (!textContent && !htmlContent) {
try {
const contentStr = JSON.stringify(content);
console.log('Complex content structure:', contentStr.slice(0, 300) + '...');
textContent = '[Complex email content]';
} catch (e) {
console.error('Failed to stringify complex content:', e);
}
}
} catch (error) {
console.error('Error processing complex content:', error);
}
}
// Always ensure we have text for direction detection
if (!textContent && htmlContent) {
// Extract text from HTML for direction detection
textContent = htmlContent.replace(/<[^>]*>/g, '')
.replace(/&nbsp;/g, ' ')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&amp;/g, '&');
try {
// Use DOM API if available
if (typeof document !== 'undefined') {
const tempDiv = document.createElement('div');
tempDiv.innerHTML = htmlContent;
textContent = tempDiv.textContent || tempDiv.innerText || '';
} else {
// Simple regex fallback for non-browser environments
textContent = htmlContent.replace(/<[^>]*>/g, ' ')
.replace(/&nbsp;/g, ' ')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&amp;/g, '&')
.replace(/\s+/g, ' ')
.trim();
}
} catch (e) {
console.error('Error extracting text from HTML:', e);
textContent = 'Failed to extract text content';
}
}
// Detect direction from text
@ -160,16 +303,35 @@ export function processContentWithDirection(content: string | EmailContent | nul
// Sanitize HTML if present
if (htmlContent) {
// Sanitize HTML first
htmlContent = sanitizeHtml(htmlContent);
// Then apply direction
htmlContent = applyTextDirection(htmlContent, textContent);
try {
// Sanitize HTML first using the centralized function
htmlContent = sanitizeHtml(htmlContent);
// Then apply direction
htmlContent = applyTextDirection(htmlContent, textContent);
} catch (error) {
console.error('Error sanitizing HTML content:', error);
// Create fallback content if sanitization fails
htmlContent = `<div dir="${direction}">${
textContent ?
textContent.replace(/\n/g, '<br>') :
'Could not process HTML content'
}</div>`;
}
} else if (textContent) {
// Convert plain text to HTML with proper direction
htmlContent = `<div dir="${direction}">${textContent.replace(/\n/g, '<br>')}</div>`;
}
// Add debug logging for troubleshooting
console.log('Processed content:', {
direction,
htmlLength: htmlContent.length,
textLength: textContent.length,
hasHtml: !!htmlContent,
hasText: !!textContent
});
// Return processed content
return {
text: textContent,