diff --git a/app/courrier/page.tsx b/app/courrier/page.tsx index 7305eb9b..70bab153 100644 --- a/app/courrier/page.tsx +++ b/app/courrier/page.tsx @@ -1570,88 +1570,71 @@ export default function CourrierPage() { if (!selectedEmail) return; try { - // Ensure we have full content before proceeding - if (!selectedEmail.content || selectedEmail.content.length === 0) { - console.log('[DEBUG] Need to fetch content before reply/forward'); - setContentLoading(true); - - try { - const response = await fetch(`/api/courrier/${selectedEmail.id}?folder=${encodeURIComponent(selectedEmail.folder || 'INBOX')}`); + // If content hasn't been loaded yet, fetch it + if (!selectedEmail.contentFetched) { + console.log('[DEBUG] Fetching email content for reply:', selectedEmail.id); + const content = await getEmailContent(selectedEmail.id, selectedEmail.folder); + if (content) { + // Update the selected email with content + const updatedEmail = { + ...selectedEmail, + content: content.html || content.text || '', + html: content.html || '', + text: content.text || '', + contentFetched: true, + // Add proper from/to/cc format for client-side formatters + from: content.from, + to: content.to, + cc: content.cc, + bcc: content.bcc, + date: content.date + }; - if (!response.ok) { - throw new Error(`Failed to fetch email content: ${response.status}`); + setSelectedEmail(updatedEmail); + + // For forwarding, we need to set the forwardFrom prop with the updated content + if (type === 'forward') { + console.log('[DEBUG] Setting forwardFrom with content for forwarding'); + setForwardFrom(updatedEmail); + } else { + // For replying, we need to set the replyTo prop with the correct reply type + console.log('[DEBUG] Setting replyTo with content for replying'); + setReplyTo({ + ...updatedEmail, + replyType: type === 'reply-all' ? 'replyAll' : 'reply' // Convert to format expected by formatter + }); } - - const fullContent = await response.json(); - - // Update the email content with the fetched full content - selectedEmail.content = fullContent.content; - selectedEmail.contentFetched = true; - - // Update the email in the list too so we don't refetch - setEmails(prevEmails => - prevEmails.map(email => - email.id === selectedEmail.id - ? { ...email, content: fullContent.content, contentFetched: true } - : email - ) - ); - - console.log('[DEBUG] Successfully fetched content for reply/forward'); - } catch (error) { - console.error('[DEBUG] Error fetching content for reply:', error); - alert('Failed to load email content for reply. Please try again.'); - setContentLoading(false); - return; // Exit if we couldn't get the content } + } else { + // Content already loaded, just set the props + // Make sure the email has the correct format for client-side formatters + const formattedEmail = { + ...selectedEmail, + // Ensure we have proper address objects for the formatters + from: selectedEmail.from ? + (Array.isArray(selectedEmail.from) ? selectedEmail.from : [{ address: selectedEmail.from, name: selectedEmail.fromName }]) : + [{ address: selectedEmail.from || '', name: selectedEmail.fromName }], + to: selectedEmail.to ? + (Array.isArray(selectedEmail.to) ? selectedEmail.to : [{ address: selectedEmail.to }]) : + [{ address: selectedEmail.to || '' }] + }; - setContentLoading(false); + if (type === 'forward') { + console.log('[DEBUG] Setting forwardFrom for forwarding (content already loaded)'); + setForwardFrom(formattedEmail); + } else { + console.log('[DEBUG] Setting replyTo for replying (content already loaded)'); + setReplyTo({ + ...formattedEmail, + replyType: type === 'reply-all' ? 'replyAll' : 'reply' // Convert to format expected by formatter + }); + } } - const getReplyTo = () => { - if (type === 'forward') return ''; - return selectedEmail.from; - }; - - const getReplyCc = () => { - if (type !== 'reply-all') return ''; - return selectedEmail.cc || ''; - }; - - const getReplySubject = () => { - const subject = selectedEmail.subject || ''; - if (type === 'forward') { - return subject.startsWith('Fwd:') ? subject : `Fwd: ${subject}`; - } - return subject.startsWith('Re:') ? subject : `Re: ${subject}`; - }; - - // Add body property for backward compatibility - const emailWithBody = { - ...selectedEmail, - body: selectedEmail.content // Add body property that maps to content - }; - - // Set the appropriate flags - setIsReplying(type === 'reply' || type === 'reply-all'); - setIsForwarding(type === 'forward'); - - // Update the compose form - setComposeTo(getReplyTo()); - setComposeCc(getReplyCc()); - setComposeSubject(getReplySubject()); - setComposeBcc(''); + // Show the compose dialog setShowCompose(true); - setShowCc(type === 'reply-all'); - setShowBcc(false); - setAttachments([]); - - // Pass the email with both content and body properties - setReplyToEmail(emailWithBody); - setForwardEmail(type === 'forward' ? emailWithBody : null); - } catch (error) { - console.error('Error preparing reply:', error); + console.error('[DEBUG] Error preparing email for reply/forward:', error); } }; diff --git a/components/ComposeEmail.tsx b/components/ComposeEmail.tsx index cc8b89a2..22b9fbe5 100644 --- a/components/ComposeEmail.tsx +++ b/components/ComposeEmail.tsx @@ -1,13 +1,14 @@ 'use client'; -import { useState, useRef, useEffect } from 'react'; +import React, { useEffect, useState, useRef } from 'react'; import { X, Paperclip, ChevronDown, ChevronUp, SendHorizontal, Loader2 } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Textarea } from '@/components/ui/textarea'; import { Label } from '@/components/ui/label'; import DOMPurify from 'isomorphic-dompurify'; -import { formatEmailForReply, formatEmailForForward } from '@/lib/email-formatter'; +import { formatEmailForReply, formatEmailForForward, EmailMessageForFormatting } from '@/lib/email-formatter'; +import { getEmailContent, getUserEmailCredentials } from '@/lib/services/email-service'; interface EmailObject { id?: string; @@ -99,22 +100,7 @@ export default function ComposeEmail({ useEffect(() => { // Initialize reply if replyTo is provided if (replyTo) { - // For reply/reply-all - const formattedEmail = formatEmailForReply(replyTo as any, 'reply'); - setComposeTo(formattedEmail.to); - setComposeSubject(formattedEmail.subject); - - // Store the quoted content separately - const bodyContent = DOMPurify.sanitize(formattedEmail.body, { - ADD_TAGS: ['style'], - FORBID_TAGS: ['script', 'iframe'] - }); - - setQuotedContent(bodyContent); - setUserMessage(''); // Clear user input - - // Keep composeBody for backwards compatibility - setComposeBody(bodyContent); + initializeReplyEmail(replyTo); } }, [replyTo, setComposeTo, setComposeSubject, setComposeBody]); @@ -127,59 +113,83 @@ export default function ComposeEmail({ // Initialize forwarded email content const initializeForwardedEmail = async (email: any) => { - if (!email) return; + console.log("Initializing forwarded email", email); + try { + // Convert the email to the format expected by our formatter + const emailForFormatting: EmailMessageForFormatting = { + subject: email.subject, + from: email.from, + to: email.to, + date: email.date, + html: email.html || email.content, + text: email.text, + cc: email.cc, + bcc: email.bcc + }; - console.log('Initializing forwarded email:', email); - - // Use our client-side formatter - const formattedEmail = formatEmailForForward(email); - - // Set the formatted subject with Fwd: prefix - setComposeSubject(formattedEmail.subject); - - // Create header for forwarded email - use the original styling - const headerHtml = formattedEmail.headerHtml; - - // Prepare content - let contentHtml = '
No content available
'; - - if (email.content) { - // Sanitize the content - contentHtml = DOMPurify.sanitize(email.content, { - ADD_TAGS: ['style'], - FORBID_TAGS: ['script', 'iframe'] - }); - } else if (email.html) { - contentHtml = DOMPurify.sanitize(email.html, { - ADD_TAGS: ['style'], - FORBID_TAGS: ['script', 'iframe'] - }); - } else if (email.text) { - contentHtml = `
${email.text}
`; - } else if (email.body) { - contentHtml = DOMPurify.sanitize(email.body, { - ADD_TAGS: ['style'], - FORBID_TAGS: ['script', 'iframe'] - }); - } - - // Store the quoted content - const formattedQuote = ` -
- ${headerHtml} -
-
- ${contentHtml} -
+ // Use the client-side formatter + const formattedEmail = formatEmailForForward(emailForFormatting); + + setComposeSubject(formattedEmail.subject); + setQuotedContent(formattedEmail.body); + setComposeBody(''); // Clear user message when forwarding + } catch (error) { + console.error("Error initializing forwarded email:", error); + setQuotedContent(` +
+ Error loading original message content
-
- `; - - setQuotedContent(formattedQuote); - setUserMessage(''); // Clear user input - - // Keep composeBody for backwards compatibility - setComposeBody(formattedQuote); + `); + } + }; + + // Initialize reply email content + const initializeReplyEmail = async (email: any, replyType: 'reply' | 'replyAll' = 'reply') => { + console.log("Initializing reply email", email, replyType); + try { + // Convert the email to the format expected by our formatter + const emailForFormatting: EmailMessageForFormatting = { + subject: email.subject, + from: email.from, + to: email.to, + date: email.date, + html: email.html || email.content, + text: email.text, + cc: email.cc, + bcc: email.bcc + }; + + // Use the client-side formatter + const formattedEmail = formatEmailForReply(emailForFormatting, replyType); + + setComposeSubject(formattedEmail.subject); + + // Set recipients + if (formattedEmail.to) { + const toAddresses = formattedEmail.to.map(recipient => + recipient.name ? `${recipient.name} <${recipient.address}>` : recipient.address + ).join(', '); + setComposeTo(toAddresses); + } + + if (formattedEmail.cc && formattedEmail.cc.length > 0) { + const ccAddresses = formattedEmail.cc.map(recipient => + recipient.name ? `${recipient.name} <${recipient.address}>` : recipient.address + ).join(', '); + setComposeCc(ccAddresses); + setShowCc(true); + } + + setQuotedContent(formattedEmail.body); + setComposeBody(''); // Clear user message when replying + } catch (error) { + console.error("Error initializing reply email:", error); + setQuotedContent(` +
+ Error loading original message content +
+ `); + } }; // Handle file attachment selection @@ -196,39 +206,54 @@ export default function ComposeEmail({ setUserMessage(contentEditableRef.current.innerHTML); // Combine user message with quoted content for the full email body - const combined = ` -
- ${contentEditableRef.current.innerHTML} -
- ${quotedContent} - `; - + const combined = `${contentEditableRef.current.innerHTML}${quotedContent ? quotedContent : ''}`; setComposeBody(combined); } }; - // Handle sending with combined content + // Handle sending email with combined content const handleSendWithCombinedContent = async () => { - // For rich editor mode, ensure we combine user message with quoted content - if (useRichEditor) { - // Create the final combined email body - const finalBody = ` -
- ${userMessage || ''} -
- ${quotedContent || ''} - `; + if (isSending) return; + + try { + setIsSending(true); - // Set the complete body and send after a brief delay to ensure state is updated - setComposeBody(finalBody); + // For rich editor, combine user message with quoted content + if (useRichEditor) { + const combinedContent = `${userMessage || ''}${quotedContent ? quotedContent : ''}`; + setComposeBody(combinedContent); + + // Wait for state update to complete + await new Promise(resolve => setTimeout(resolve, 0)); + } - // Small delay to ensure state update completes - setTimeout(() => { - handleSend(); - }, 50); - } else { - // For normal textarea mode, just use the existing handler - handleSend(); + // Call the provided onSend function + await onSend({ + to: composeTo, + cc: composeCc, + bcc: composeBcc, + subject: composeSubject, + body: composeBody, + attachments: attachments + }); + + // Reset the compose state + setShowCompose(false); + setComposeTo(''); + setComposeCc(''); + setComposeBcc(''); + setComposeSubject(''); + setComposeBody(''); + setShowCc(false); + setShowBcc(false); + setAttachments([]); + setUserMessage(''); + setQuotedContent(''); + + } catch (error) { + console.error('Failed to send email:', error); + } finally { + setIsSending(false); } }; @@ -347,34 +372,18 @@ export default function ComposeEmail({ ref={contentEditableRef} contentEditable="true" className="w-full p-3 bg-white min-h-[100px] text-gray-900 email-editor" - style={{ - direction: 'ltr', - unicodeBidi: 'isolate', - textAlign: 'left' - }} onInput={handleUserMessageChange} - dir="ltr" - dangerouslySetInnerHTML={userMessage ? { __html: userMessage } : { __html: '

Write your message here...

' }} + dangerouslySetInnerHTML={userMessage ? { __html: userMessage } : { __html: '

Write your message here...

' }} /> {/* Original email content - also editable */} {quotedContent && (
{ const target = e.currentTarget; @@ -391,8 +400,6 @@ export default function ComposeEmail({ onChange={(e) => setComposeBody(e.target.value)} placeholder="Write your message..." className="w-full mt-1 min-h-[200px] bg-white border-gray-300 text-gray-900 resize-none email-editor" - dir="ltr" - style={{ direction: 'ltr', unicodeBidi: 'isolate', textAlign: 'left' }} /> )}
diff --git a/components/email/ComposeEmail.tsx b/components/email/ComposeEmail.tsx index 5bfb89fb..5f049d11 100644 --- a/components/email/ComposeEmail.tsx +++ b/components/email/ComposeEmail.tsx @@ -2,7 +2,7 @@ import { useState, useRef, useEffect } from 'react'; import { formatEmailForReplyOrForward, EmailMessage, EmailAddress } from '@/lib/services/email-service'; -import { X, Paperclip, ChevronDown, ChevronUp, SendHorizontal, Loader2 } from 'lucide-react'; +import { X, Paperclip, ChevronDown, ChevronUp, SendHorizontal, Loader2, TextAlignLeft, TextAlignRight } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Card, CardContent, CardHeader, CardTitle, CardFooter } from '@/components/ui/card'; @@ -41,6 +41,7 @@ export default function ComposeEmail({ const [showCc, setShowCc] = useState(false); const [showBcc, setShowBcc] = useState(false); const [sending, setSending] = useState(false); + const [isRTL, setIsRTL] = useState(false); const [attachments, setAttachments] = useState { + setIsRTL(!isRTL); + if (editorRef.current) { + editorRef.current.dir = !isRTL ? 'rtl' : 'ltr'; + } + }; + return ( @@ -468,6 +477,19 @@ export default function ComposeEmail({
+ {/* Editor toolbar */} +
+ +
+ {/* Email body editor */}
diff --git a/lib/email-formatter.ts b/lib/email-formatter.ts index 9cc17d1f..140d5adf 100644 --- a/lib/email-formatter.ts +++ b/lib/email-formatter.ts @@ -1,209 +1,225 @@ 'use client'; +import DOMPurify from 'dompurify'; + /** * Client-side utilities for formatting email content * This file contains functions for formatting email content in the browser * without any server dependencies. */ -interface EmailAddress { - name?: string; +export interface EmailAddress { address: string; + name?: string; } -interface FormattedEmail { - to: string; - cc?: string; +export interface FormattedEmail { subject: string; + to?: EmailAddress[]; + cc?: EmailAddress[]; + bcc?: EmailAddress[]; body: string; } +export interface EmailMessageForFormatting { + subject?: string; + from?: EmailAddress | EmailAddress[]; + to?: EmailAddress | EmailAddress[]; + date?: Date | string; + html?: string; + text?: string; + cc?: EmailAddress | EmailAddress[]; + bcc?: EmailAddress | EmailAddress[]; +} + /** * Format an email for replying or forwarding * Client-side friendly version that doesn't depend on server modules */ export function formatEmailForReply( - email: any, - type: 'reply' | 'reply-all' = 'reply' + originalEmail: EmailMessageForFormatting, + type: 'reply' | 'replyAll' | 'forward' = 'reply' ): FormattedEmail { - // Format the subject with Re: prefix - const subject = formatSubject(email.subject || '(No subject)', type); + // Format the subject with Re: or Fwd: prefix + const subject = formatSubject(originalEmail.subject || '', type); - // Format recipients - let to = ''; - let cc = ''; + // Initialize recipients based on reply type + let to: EmailAddress[] = []; + let cc: EmailAddress[] = []; - // Process 'to' field for reply - if (typeof email.from === 'string') { - to = email.from; - } else if (Array.isArray(email.from)) { - to = email.from.map((addr: EmailAddress) => - addr.name && addr.name !== addr.address - ? `${addr.name} <${addr.address}>` - : addr.address - ).join(', '); - } else if (email.fromName || email.from) { - // Handle cases where from is an object with name and address - if (email.fromName && email.from && email.fromName !== email.from) { - to = `${email.fromName} <${email.from}>`; - } else { - to = email.from; - } - } - - // For reply-all, include other recipients in cc - if (type === 'reply-all' && email.to) { - if (typeof email.to === 'string') { - cc = email.to; - } else if (Array.isArray(email.to)) { - cc = email.to.map((addr: EmailAddress) => - addr.name && addr.name !== addr.address - ? `${addr.name} <${addr.address}>` - : addr.address - ).join(', '); + if (type === 'reply' && originalEmail.from) { + to = Array.isArray(originalEmail.from) ? originalEmail.from : [originalEmail.from]; + } else if (type === 'replyAll') { + // To: original sender + if (originalEmail.from) { + to = Array.isArray(originalEmail.from) ? originalEmail.from : [originalEmail.from]; } - // Include cc recipients from original email too if available - if (email.cc) { - const ccList = typeof email.cc === 'string' - ? email.cc - : Array.isArray(email.cc) - ? email.cc.map((addr: EmailAddress) => addr.address).join(', ') - : ''; - - if (ccList) { - cc = cc ? `${cc}, ${ccList}` : ccList; - } + // CC: all other recipients + if (originalEmail.to) { + cc = Array.isArray(originalEmail.to) ? originalEmail.to : [originalEmail.to]; } + + if (originalEmail.cc) { + const existingCc = Array.isArray(originalEmail.cc) ? originalEmail.cc : [originalEmail.cc]; + cc = [...cc, ...existingCc]; + } + + // Remove duplicates and self from CC (would need user's email here) + // This is simplified - in a real app you'd filter out the current user + cc = cc.filter((value, index, self) => + index === self.findIndex((t) => t.address === value.address) + ); } - // Create quote header - const quoteHeader = createQuoteHeader(email); + // Create the quoted content with header + const quoteHeader = createQuoteHeader(originalEmail); - // Format body with quote - let body = `

${quoteHeader}
`; - - // Add quoted content - if (email.content) { - body += email.content; - } else if (email.html) { - body += email.html; - } else if (email.text) { - body += `
${email.text}
`; - } else if (email.body) { - body += email.body; - } else { - body += '
No content available
'; + // Get the original content, preferring HTML over plain text + let originalContent = ''; + if (originalEmail.html) { + // Sanitize any potentially unsafe HTML + originalContent = DOMPurify.sanitize(originalEmail.html); + } else if (originalEmail.text) { + // Convert text to HTML by replacing newlines with br tags + originalContent = originalEmail.text.replace(/\n/g, '
'); } - body += '
'; + // Combine the header with the original content + const body = ` +
+
+
+ ${quoteHeader} +
${originalContent || 'No content available'}
+
+
+ `; return { + subject, to, cc, - subject, body }; } -function formatSubject(subject: string, type: 'reply' | 'reply-all' | 'forward'): string { - // Clean up existing prefixes first - let cleanSubject = subject.replace(/^(Re|Fwd|FW|Forward):\s*/gi, ''); - cleanSubject = cleanSubject.trim() || '(No subject)'; +/** + * Format email subject with appropriate prefix + */ +export function formatSubject( + originalSubject: string, + type: 'reply' | 'replyAll' | 'forward' +): string { + // Trim whitespace + let subject = originalSubject.trim(); - // Add appropriate prefix + // Remove existing prefixes to avoid duplication + subject = subject.replace(/^(Re|Fwd):\s*/gi, ''); + + // Add appropriate prefix based on action type if (type === 'forward') { - return `Fwd: ${cleanSubject}`; + return `Fwd: ${subject}`; } else { - // For reply and reply-all - return `Re: ${cleanSubject}`; + return `Re: ${subject}`; } } -function createQuoteHeader(email: any): string { - let from = 'Unknown Sender'; - let date = email.date ? new Date(email.date).toLocaleString() : 'Unknown Date'; - let subject = email.subject || '(No subject)'; - let to = ''; +/** + * Create a formatted quote header with sender and date information + */ +export function createQuoteHeader(email: EmailMessageForFormatting): string { + let fromName = 'Unknown Sender'; + let fromEmail = ''; - // Extract from - if (typeof email.from === 'string') { - from = email.from; - } else if (Array.isArray(email.from)) { - from = email.from.map((addr: EmailAddress) => - addr.name ? `${addr.name} <${addr.address}>` : addr.address - ).join(', '); - } else if (email.fromName || email.from) { - from = email.fromName && email.fromName !== email.from - ? `${email.fromName} <${email.from}>` - : email.from; + // Extract sender information + if (email.from) { + if (Array.isArray(email.from)) { + fromName = email.from[0].name || email.from[0].address; + fromEmail = email.from[0].address; + } else { + fromName = email.from.name || email.from.address; + fromEmail = email.from.address; + } } - // Extract to - if (typeof email.to === 'string') { - to = email.to; - } else if (Array.isArray(email.to)) { - to = email.to.map((addr: EmailAddress) => - addr.name ? `${addr.name} <${addr.address}>` : addr.address - ).join(', '); + // Format the date + let dateFormatted = ''; + if (email.date) { + const date = typeof email.date === 'string' ? new Date(email.date) : email.date; + + // Check if the date is valid + if (!isNaN(date.getTime())) { + dateFormatted = date.toLocaleString('en-US', { + weekday: 'short', + year: 'numeric', + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit' + }); + } } + // Generate recipients string + let recipients = ''; + if (email.to) { + if (Array.isArray(email.to)) { + recipients = email.to.map(r => r.name || r.address).join(', '); + } else { + recipients = email.to.name || email.to.address; + } + } + + // Create the header HTML return ` -
-
On ${date}, ${from} wrote:
+
+
From: ${fromName} <${fromEmail}>
+ ${dateFormatted ? `
Date: ${dateFormatted}
` : ''} +
Subject: ${email.subject || 'No Subject'}
+
To: ${recipients || 'No Recipients'}
+ ${email.cc ? `
Cc: ${Array.isArray(email.cc) ? + email.cc.map(r => r.name || r.address).join(', ') : + (email.cc.name || email.cc.address)}
` : ''}
+
`; } /** * Format an email for forwarding */ -export function formatEmailForForward(email: any): { - subject: string; - headerHtml: string; -} { - // Format subject with Fwd: prefix - const subject = formatSubject(email.subject || '(No subject)', 'forward'); +export function formatEmailForForward(email: EmailMessageForFormatting): FormattedEmail { + // Format the subject with Fwd: prefix + const subject = formatSubject(email.subject || '', 'forward'); - // Get sender information - let fromString = 'Unknown Sender'; - if (typeof email.from === 'string') { - fromString = email.from; - } else if (Array.isArray(email.from)) { - fromString = email.from.map((addr: EmailAddress) => - addr.name ? `${addr.name} <${addr.address}>` : addr.address - ).join(', '); - } else if (email.fromName && email.from) { - fromString = email.fromName !== email.from - ? `${email.fromName} <${email.from}>` - : email.from; + // Create the forward header + const forwardHeader = createQuoteHeader(email); + + // Get the original content, preferring HTML over plain text + let originalContent = ''; + if (email.html) { + // Sanitize any potentially unsafe HTML + originalContent = DOMPurify.sanitize(email.html); + } else if (email.text) { + // Convert text to HTML by replacing newlines with br tags + originalContent = email.text.replace(/\n/g, '
'); } - // Get recipient information - let toString = ''; - if (typeof email.to === 'string') { - toString = email.to; - } else if (Array.isArray(email.to)) { - toString = email.to.map((addr: EmailAddress) => - addr.name ? `${addr.name} <${addr.address}>` : addr.address - ).join(', '); - } - - // Create header for forwarded email - const headerHtml = ` -
-
-
---------- Forwarded message ---------
-
From: ${fromString}
-
Date: ${email.date ? new Date(email.date).toLocaleString() : 'Unknown Date'}
-
Subject: ${email.subject || '(No subject)'}
-
To: ${toString || 'Unknown Recipient'}
+ // Combine the header with the original content + const body = ` +
+
+
+
---------- Forwarded message ---------
+ ${forwardHeader} +
${originalContent || '
No content available
'}
`; return { subject, - headerHtml + body }; } \ No newline at end of file