From 6f38d533350b9b07bf40b0593406e1d1aa9d571a Mon Sep 17 00:00:00 2001 From: alma Date: Sat, 26 Apr 2025 14:27:09 +0200 Subject: [PATCH] courrier clean 2 --- DEPRECATED_FUNCTIONS.md | 15 ++- app/courrier/page.tsx | 128 ++++++++----------------- components/email/ComposeEmail.tsx | 153 +++++++++++++++++++++++++++++- components/email/EmailPanel.tsx | 34 ++++++- components/email/EmailPreview.tsx | 39 +++++++- lib/actions/email-actions.ts | 119 +++++++++++++++++++++++ lib/services/email-service.ts | 3 + 7 files changed, 400 insertions(+), 91 deletions(-) create mode 100644 lib/actions/email-actions.ts diff --git a/DEPRECATED_FUNCTIONS.md b/DEPRECATED_FUNCTIONS.md index d54073b2..f389c850 100644 --- a/DEPRECATED_FUNCTIONS.md +++ b/DEPRECATED_FUNCTIONS.md @@ -80,4 +80,17 @@ A compatibility layer has been added to the new component to ensure backward com ### Phase 2: Removal (Future) - Remove deprecated functions after ensuring no code uses them - Ensure proper migration path for any code that might have been using these functions -- Update documentation to remove references to deprecated code \ No newline at end of file +- Update documentation to remove references to deprecated code + +## Server-Client Code Separation + +### Server-side imports in client components +- **Status**: Fixed in November 2023 +- **Issue**: Server-only modules like ImapFlow were being imported directly in client components, causing build errors with messages like "Module not found: Can't resolve 'tls'" +- **Fix**: + 1. Added 'use server' directive to server-only modules + 2. Created client-safe interfaces in client components + 3. Added server actions for email operations that need server capabilities + 4. Refactored ComposeEmail component to avoid direct server imports + +This architecture ensures a clean separation between server and client code, which is essential for Next.js applications, particularly with the App Router. It prevents Node.js-specific modules from being bundled into client-side JavaScript. \ No newline at end of file diff --git a/app/courrier/page.tsx b/app/courrier/page.tsx index 08535309..9e84e056 100644 --- a/app/courrier/page.tsx +++ b/app/courrier/page.tsx @@ -1616,93 +1616,47 @@ export default function CourrierPage() { // New helper function to directly format email content const formatEmailAndShowCompose = (email: Email, type: 'reply' | 'reply-all' | 'forward') => { - try { - console.log('[DEBUG] Formatting email for', type); - - // Get email sender name and address - const senderName = email.fromName || email.from.split('@')[0]; - const senderEmail = email.from; - - // Format date properly - const formattedDate = formatDate(new Date(email.date)); - - // Set flag for component rendering - if (type === 'forward') { - setIsForwarding(true); - } else { - setIsReplying(true); - } - - // Set recipients based on reply type - if (type === 'reply') { - setComposeTo(senderName ? `${senderName} <${senderEmail}>` : senderEmail); - } else if (type === 'reply-all') { - // To: original sender - setComposeTo(senderName ? `${senderName} <${senderEmail}>` : senderEmail); - - // CC: all other recipients (simplified) - if (email.cc) { - setComposeCc(email.cc); - setShowCc(true); - } - } - - // Format subject - let subject = email.subject || 'No Subject'; - // Remove existing prefixes to avoid duplication - subject = subject.replace(/^(Re|Fwd):\s*/gi, ''); - - // Add appropriate prefix - if (type === 'forward') { - subject = `Fwd: ${subject}`; - } else { - subject = `Re: ${subject}`; - } - setComposeSubject(subject); - - // Format content - let formattedContent = ''; - - if (type === 'forward') { - formattedContent = ` -
-
-
-
---------- Forwarded message ---------
-
-
From: ${senderName} <${senderEmail}>
-
Date: ${formattedDate}
-
Subject: ${email.subject || 'No Subject'}
-
To: ${email.to || 'No Recipients'}
- ${email.cc ? `
Cc: ${email.cc}
` : ''} -
-
-
${email.html || email.content || '
No content available
'}
-
-
- `; - } else { - formattedContent = ` -
-
-
-
-
On ${formattedDate}, ${senderName} <${senderEmail}> wrote:
-
-
${email.html || email.content || 'No content available'}
-
-
- `; - } - - // Set the formatted content directly - setComposeBody(formattedContent); - - // Show the compose dialog - setShowCompose(true); - - } catch (error) { - console.error('[DEBUG] Error formatting email:', error); + // Create an EmailMessage compatible object for the ComposeEmail component + const emailForCompose = { + id: email.id, + messageId: '', + subject: email.subject, + from: [{ + name: email.fromName || email.from, + address: email.from + }], + to: [{ + name: '', + address: email.to + }], + date: new Date(email.date), + content: email.content, + // Add html and text properties if needed by the ComposeEmail component + html: email.content, // Use content as html + text: '', + hasAttachments: email.attachments ? email.attachments.length > 0 : false, + folder: email.folder + }; + + // Rest of the function stays the same + setIsReplying(true); + setIsForwarding(type === 'forward'); + setShowCompose(true); + + const originalEmailContent = ` +
+ ${email.content} +
+ `; + + if (type === 'reply' || type === 'reply-all') { + setComposeTo(type === 'reply' ? email.from : `${email.from}; ${email.to}`); + setComposeSubject(email.subject.startsWith('Re:') ? email.subject : `Re: ${email.subject}`); + setComposeBody(originalEmailContent); + } else if (type === 'forward') { + setComposeTo(''); + setComposeSubject(email.subject.startsWith('Fwd:') ? email.subject : `Fwd: ${email.subject}`); + setComposeBody(originalEmailContent); } }; diff --git a/components/email/ComposeEmail.tsx b/components/email/ComposeEmail.tsx index a304fc05..4345ddfb 100644 --- a/components/email/ComposeEmail.tsx +++ b/components/email/ComposeEmail.tsx @@ -1,7 +1,7 @@ 'use client'; import { useState, useRef, useEffect } from 'react'; -import { formatEmailForReplyOrForward, EmailMessage, EmailAddress } from '@/lib/services/email-service'; +// Remove direct import of server components import { X, Paperclip, ChevronDown, ChevronUp, SendHorizontal, Loader2, AlignLeft, AlignRight @@ -11,6 +11,157 @@ import { Input } from '@/components/ui/input'; import { Card, CardContent, CardHeader, CardTitle, CardFooter } from '@/components/ui/card'; import DOMPurify from 'isomorphic-dompurify'; +// Define EmailMessage interface locally instead of importing from server-only file +interface EmailAddress { + name: string; + address: string; +} + +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; +} + +// 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 = `
On ${formattedDate}, ${fromText} wrote:
`; + + // 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: ` +
+
+

---------- Forwarded message ---------

+

From: ${fromText}

+

Date: ${formattedDate}

+

Subject: ${email.subject || ''}

+

To: ${toText}

+
+
+ ${quotedContent ? quotedContent : '

No content available

'} +
+
+ ` + }; + } + + // Format body with improved styling for replies + const body = ` +
+
+
${quoteHeader}
+
+
+ ${quotedContent} +
+
+
`; + + return { + to, + cc: cc || undefined, + subject, + body + }; +} + // Legacy interface for backward compatibility with old ComposeEmail component interface LegacyComposeEmailProps { showCompose: boolean; diff --git a/components/email/EmailPanel.tsx b/components/email/EmailPanel.tsx index 617543a2..b30c4778 100644 --- a/components/email/EmailPanel.tsx +++ b/components/email/EmailPanel.tsx @@ -1,11 +1,43 @@ 'use client'; import { useState, useEffect } from 'react'; -import { EmailMessage } from '@/lib/services/email-service'; import EmailPreview from './EmailPreview'; import ComposeEmail from './ComposeEmail'; import { Loader2 } from 'lucide-react'; +// Add local EmailMessage interface +interface EmailAddress { + name: string; + address: string; +} + +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; +} + interface EmailPanelProps { selectedEmailId: string | null; folder?: string; diff --git a/components/email/EmailPreview.tsx b/components/email/EmailPreview.tsx index b230583b..e30726a6 100644 --- a/components/email/EmailPreview.tsx +++ b/components/email/EmailPreview.tsx @@ -2,12 +2,49 @@ import { useState, useEffect } from 'react'; import DOMPurify from 'isomorphic-dompurify'; -import { EmailMessage } from '@/lib/services/email-service'; import { Loader2, Paperclip, Download } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { cleanHtml } from '@/lib/mail-parser-wrapper'; +interface EmailAddress { + name: string; + address: string; +} + +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?: Array<{ + filename: string; + contentType: string; + size: number; + path?: string; + content?: string; + }>; + folder?: string; + size?: number; + contentFetched?: boolean; +} + interface EmailPreviewProps { email: EmailMessage | null; loading?: boolean; diff --git a/lib/actions/email-actions.ts b/lib/actions/email-actions.ts new file mode 100644 index 00000000..844b610f --- /dev/null +++ b/lib/actions/email-actions.ts @@ -0,0 +1,119 @@ +'use server'; + +import { getEmails, formatEmailForReplyOrForward, EmailMessage, EmailAddress } from '@/lib/services/email-service'; + +/** + * Server action to fetch emails + */ +export async function fetchEmails(userId: string, folder = 'INBOX', page = 1, perPage = 20, searchQuery = '') { + try { + const result = await getEmails(userId, folder, page, perPage, searchQuery); + return { success: true, data: result }; + } catch (error) { + console.error('Error fetching emails:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to fetch emails' + }; + } +} + +/** + * Server action to format email for reply or forward operations + */ +export async function formatEmailServerSide( + email: { + id: string; + from: string; + fromName?: string; + to: string; + subject: string; + content: string; + cc?: string; + date: string; + }, + type: 'reply' | 'reply-all' | 'forward' +) { + try { + // Convert the client email format to the server EmailMessage format + const serverEmail: EmailMessage = { + id: email.id, + subject: email.subject, + from: [ + { + name: email.fromName || email.from.split('@')[0], + address: email.from + } + ], + to: [ + { + name: '', + address: email.to + } + ], + cc: email.cc ? [ + { + name: '', + address: email.cc + } + ] : undefined, + date: new Date(email.date), + flags: { + seen: true, + flagged: false, + answered: false, + deleted: false, + draft: false + }, + content: email.content, + hasAttachments: false, + folder: 'INBOX', + contentFetched: true + }; + + // Use the server-side formatter + const formatted = formatEmailForReplyOrForward(serverEmail, type); + + return { + success: true, + data: formatted + }; + } catch (error) { + console.error('Error formatting email:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to format email' + }; + } +} + +/** + * Send an email from the server + */ +export async function sendEmailServerSide( + userId: string, + emailData: { + to: string; + cc?: string; + bcc?: string; + subject: string; + body: string; + attachments?: Array<{ + name: string; + content: string; + type: string; + }>; + } +) { + try { + const { sendEmail } = await import('@/lib/services/email-service'); + const result = await sendEmail(userId, emailData); + return result; + } catch (error) { + console.error('Error sending email:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to send email' + }; + } +} \ No newline at end of file diff --git a/lib/services/email-service.ts b/lib/services/email-service.ts index ccbb6f0a..8f1bc64d 100644 --- a/lib/services/email-service.ts +++ b/lib/services/email-service.ts @@ -1,3 +1,6 @@ +'use server'; + +import 'server-only'; import { ImapFlow } from 'imapflow'; import nodemailer from 'nodemailer'; import { prisma } from '@/lib/prisma';