diff --git a/app/api/courrier/[id]/route.ts b/app/api/courrier/[id]/route.ts deleted file mode 100644 index a9eb7759..00000000 --- a/app/api/courrier/[id]/route.ts +++ /dev/null @@ -1,233 +0,0 @@ -import { NextResponse } from 'next/server'; -import { ImapFlow } from 'imapflow'; -import { getServerSession } from 'next-auth'; -import { authOptions } from '@/app/api/auth/[...nextauth]/route'; -import { prisma } from '@/lib/prisma'; -import { parseEmail } from '@/lib/server/email-parser'; -import { LRUCache } from 'lru-cache'; - -// Simple in-memory cache for email content -const emailContentCache = new LRUCache({ - max: 100, - ttl: 1000 * 60 * 15, // 15 minutes -}); - -export async function GET( - request: Request, - { params }: { params: { id: string } } -) { - try { - // 1. Get email ID from params (properly awaited) - const { id } = await Promise.resolve(params); - - // 2. Authentication check - const session = await getServerSession(authOptions); - if (!session?.user?.id) { - return NextResponse.json( - { error: 'Unauthorized' }, - { status: 401 } - ); - } - - // 3. Check cache first - const cacheKey = `email:${session.user.id}:${id}`; - const cachedEmail = emailContentCache.get(cacheKey); - if (cachedEmail) { - return NextResponse.json(cachedEmail); - } - - // 4. Get credentials from database - const credentials = await prisma.mailCredentials.findUnique({ - where: { - userId: session.user.id - } - }); - - if (!credentials) { - return NextResponse.json( - { error: 'No mail credentials found. Please configure your email account.' }, - { status: 401 } - ); - } - - // 5. Get the current folder from the request URL - const url = new URL(request.url); - const folder = url.searchParams.get('folder') || 'INBOX'; - - // 6. Create IMAP client - const client = new ImapFlow({ - host: credentials.host, - port: credentials.port, - secure: true, - auth: { - user: credentials.email, - pass: credentials.password, - }, - logger: false, - emitLogs: false, - tls: { - rejectUnauthorized: false - }, - disableAutoIdle: true - }); - - try { - await client.connect(); - - // 7. Open the folder - const mailbox = await client.mailboxOpen(folder); - console.log(`Mailbox opened: ${folder}, total messages: ${mailbox.exists}`); - - // 8. Download the raw message data using a dynamic fetch approach - console.log(`Attempting to fetch message with UID: ${id}`); - - // Create a loop to process all messages until we find the right one - let foundMessage = null; - const chunkSize = 10; - - for (let i = 1; i <= mailbox.exists; i += chunkSize) { - const endIdx = Math.min(i + chunkSize - 1, mailbox.exists); - const range = `${i}:${endIdx}`; - - console.log(`Scanning messages ${range}`); - - // Fetch messages in chunks with UID - const messages = client.fetch(range, { - uid: true, - envelope: true, - flags: true, - bodyStructure: true, - source: true - }); - - for await (const message of messages) { - if (message.uid.toString() === id) { - console.log(`Found matching message with UID ${id}`); - foundMessage = message; - break; - } - } - - if (foundMessage) { - break; - } - } - - if (!foundMessage) { - console.log(`No message found with UID ${id}`); - return NextResponse.json( - { error: `Email not found with UID ${id}` }, - { status: 404 } - ); - } - - console.log(`Successfully fetched message, parsing content...`); - - // 9. Parse the email content - const parsedEmail = await parseEmail(foundMessage.source.toString()); - - // Debug the parsed email structure - console.log('Parsed email data structure:', { - hasHtml: !!parsedEmail.html, - hasText: !!parsedEmail.text, - htmlLength: parsedEmail.html ? parsedEmail.html.length : 0, - textLength: parsedEmail.text ? parsedEmail.text.length : 0, - attachmentsCount: parsedEmail.attachments ? parsedEmail.attachments.length : 0 - }); - - // 10. Prepare the full email object with all needed data - const rawEmail = { - id, - from: foundMessage.envelope.from?.[0]?.address || '', - fromName: foundMessage.envelope.from?.[0]?.name || - foundMessage.envelope.from?.[0]?.address?.split('@')[0] || '', - to: foundMessage.envelope.to?.map((addr: any) => addr.address).join(', ') || '', - subject: foundMessage.envelope.subject || '(No subject)', - date: foundMessage.envelope.date?.toISOString() || new Date().toISOString(), - html: parsedEmail.html || '', - text: parsedEmail.text || '', - rawSource: foundMessage.source?.toString() || '', - read: foundMessage.flags.has('\\Seen'), - starred: foundMessage.flags.has('\\Flagged'), - folder: folder - }; - - // 10. Prepare the full email object with all needed data - const fullEmail = { - id, - from: foundMessage.envelope.from?.[0]?.address || '', - fromName: foundMessage.envelope.from?.[0]?.name || - foundMessage.envelope.from?.[0]?.address?.split('@')[0] || '', - to: foundMessage.envelope.to?.map((addr: any) => addr.address).join(', ') || '', - subject: foundMessage.envelope.subject || '(No subject)', - date: foundMessage.envelope.date?.toISOString() || new Date().toISOString(), - // Use the raw fields directly to avoid any complex processing - content: parsedEmail.html || parsedEmail.text || '', - textContent: parsedEmail.text || '', - rawContent: foundMessage.source?.toString() || '', - read: foundMessage.flags.has('\\Seen'), - starred: foundMessage.flags.has('\\Flagged'), - folder: folder, - hasAttachments: foundMessage.bodyStructure?.type === 'multipart', - attachments: parsedEmail.attachments || [], - flags: Array.from(foundMessage.flags), - headers: parsedEmail.headers || {}, - // Include the raw email for debugging - _raw: rawEmail - }; - - // Log the structure of the email being returned - console.log('Returning email object with content structure:', { - id: fullEmail.id, - hasContent: !!fullEmail.content, - contentLength: fullEmail.content ? fullEmail.content.length : 0, - hasTextContent: !!fullEmail.textContent, - textContentLength: fullEmail.textContent ? fullEmail.textContent.length : 0 - }); - - // 11. Mark as read if not already - if (!foundMessage.flags.has('\\Seen')) { - try { - // Use the same sequence range to mark message as read - for (let i = 1; i <= mailbox.exists; i += chunkSize) { - const endIdx = Math.min(i + chunkSize - 1, mailbox.exists); - const range = `${i}:${endIdx}`; - - // Find message and mark as read - const messages = client.fetch(range, { uid: true }); - for await (const message of messages) { - if (message.uid.toString() === id) { - await client.messageFlagsAdd(i.toString(), ['\\Seen']); - break; - } - } - } - } catch (error) { - console.error('Error marking message as read:', error); - } - } - - // Deep clone and stringify the email object to ensure no circular references - const sanitizedEmail = JSON.parse(JSON.stringify(fullEmail)); - - // 12. Cache the email content - emailContentCache.set(cacheKey, sanitizedEmail); - - // 13. Return the full email - return NextResponse.json(sanitizedEmail); - } finally { - // 14. Close the connection - try { - await client.logout(); - } catch (e) { - console.error('Error during IMAP logout:', e); - } - } - } catch (error) { - console.error('Error fetching email:', error); - return NextResponse.json( - { error: 'Failed to fetch email' }, - { status: 500 } - ); - } -} \ No newline at end of file diff --git a/app/api/courrier/route.ts b/app/api/courrier/route.ts index bd0f93f5..5ae450fb 100644 --- a/app/api/courrier/route.ts +++ b/app/api/courrier/route.ts @@ -3,106 +3,28 @@ import { ImapFlow } from 'imapflow'; import { getServerSession } from 'next-auth'; import { authOptions } from '@/app/api/auth/[...nextauth]/route'; import { prisma } from '@/lib/prisma'; -import { LRUCache } from 'lru-cache'; -// Define types -interface Email { - id: string; - from: string; - fromName?: string; - to: string; - subject: string; - date: string; - read: boolean; - starred: boolean; - folder: string; - hasAttachments: boolean; - flags: string[]; - preview?: string | null; -} +// Add caching for better performance +const cache = new Map(); -interface CachedData { - emails: Email[]; - folders: string[]; - total: number; - hasMore: boolean; - page: number; - limit: number; -} +const getCachedEmail = (id: string) => cache.get(id); +const setCachedEmail = (id: string, email: Email) => cache.set(id, email); -// Configure efficient caching with TTL - fix the type to allow any data -const emailCache = new LRUCache({ - max: 500, // Store up to 500 emails - ttl: 1000 * 60 * 5, // Cache for 5 minutes -}); +// Add debouncing for folder changes +const debouncedLoadEmails = debounce((folder: string) => { + loadEmails(folder); +}, 300); -// Simple in-memory cache for email content -const emailContentCache = new LRUCache({ - max: 100, - ttl: 1000 * 60 * 15, // 15 minutes -}); - -// Keep IMAP connections per user with timeouts -const connectionPool = new Map(); - -// Clean up idle connections periodically -setInterval(() => { - const now = Date.now(); - connectionPool.forEach((connection, userId) => { - if (now - connection.lastUsed > 1000 * 60 * 2) { // 2 minutes idle - connection.client.logout().catch(console.error); - connectionPool.delete(userId); - } - }); -}, 60000); // Check every minute - -// Get or create IMAP client for user -async function getImapClient(userId: string, credentials: any): Promise { - const existing = connectionPool.get(userId); - - if (existing) { - existing.lastUsed = Date.now(); - return existing.client; +// Add infinite scroll +const handleScroll = (e: React.UIEvent) => { + const { scrollTop, scrollHeight, clientHeight } = e.currentTarget; + if (scrollHeight - scrollTop === clientHeight) { + // Load more emails } - - // Remove invalid options - const client = new ImapFlow({ - host: credentials.host, - port: credentials.port, - secure: true, - auth: { - user: credentials.email, - pass: credentials.password, - }, - logger: false, - emitLogs: false, - tls: { - rejectUnauthorized: false - }, - disableAutoIdle: true - }); - - await client.connect(); - - connectionPool.set(userId, { - client, - lastUsed: Date.now() - }); - - return client; -} - -// Generate cache key -function getCacheKey(userId: string, folder: string, page: number, limit: number): string { - return `${userId}:${folder}:${page}:${limit}`; -} +}; export async function GET(request: Request) { try { - // 1. Authentication const session = await getServerSession(authOptions); if (!session?.user?.id) { return NextResponse.json( @@ -111,24 +33,7 @@ export async function GET(request: Request) { ); } - // 2. Parse request parameters - const url = new URL(request.url); - const folder = url.searchParams.get('folder') || 'INBOX'; - const page = parseInt(url.searchParams.get('page') || '1'); - const limit = parseInt(url.searchParams.get('limit') || '20'); - const preview = url.searchParams.get('preview') === 'true'; - const forceRefresh = url.searchParams.get('refresh') === 'true'; - - // 3. Check cache first (unless refresh requested) - const cacheKey = getCacheKey(session.user.id, folder, page, limit); - if (!forceRefresh) { - const cachedData = emailCache.get(cacheKey); - if (cachedData) { - return NextResponse.json(cachedData); - } - } - - // 4. Get credentials + // Get credentials from database const credentials = await prisma.mailCredentials.findUnique({ where: { userId: session.user.id @@ -142,72 +47,73 @@ export async function GET(request: Request) { ); } - // 5. Get IMAP client from pool (or create new) - const client = await getImapClient(session.user.id, credentials); - - try { - // 6. Get mailboxes (with caching) - let availableFolders: string[]; - const foldersCacheKey = `folders:${session.user.id}`; - const cachedFolders = emailCache.get(foldersCacheKey); - - if (cachedFolders) { - availableFolders = cachedFolders; - } else { - const mailboxes = await client.list(); - availableFolders = mailboxes.map(box => box.path); - emailCache.set(foldersCacheKey, availableFolders); + // Get query parameters + const url = new URL(request.url); + const folder = url.searchParams.get('folder') || 'INBOX'; + const page = parseInt(url.searchParams.get('page') || '1'); + const limit = parseInt(url.searchParams.get('limit') || '20'); + const preview = url.searchParams.get('preview') === 'true'; + + // Calculate start and end sequence numbers + const start = (page - 1) * limit + 1; + const end = start + limit - 1; + + // Connect to IMAP server + const client = new ImapFlow({ + host: credentials.host, + port: credentials.port, + secure: true, + auth: { + user: credentials.email, + pass: credentials.password, + }, + logger: false, + emitLogs: false, + tls: { + rejectUnauthorized: false } + }); + + try { + await client.connect(); - // 7. Open mailbox + // Get list of all mailboxes first + const mailboxes = await client.list(); + const availableFolders = mailboxes.map(box => box.path); + + // Open the requested mailbox const mailbox = await client.mailboxOpen(folder); - const result: Email[] = []; - - // 8. Fetch emails (if any exist) - // Define start and end variables HERE - let start = 1; - let end = 0; + const result = []; + // Only try to fetch if the mailbox has messages if (mailbox.exists > 0) { - // Calculate range with boundaries - start = Math.min((page - 1) * limit + 1, mailbox.exists); - end = Math.min(start + limit - 1, mailbox.exists); + // Adjust start and end to be within bounds + const adjustedStart = Math.min(start, mailbox.exists); + const adjustedEnd = Math.min(end, mailbox.exists); - // Use sequence numbers in descending order for newest first - const range = `${mailbox.exists - end + 1}:${mailbox.exists - start + 1}`; - - // Fetch messages with optimized options - const options: any = { + // Fetch both metadata and preview content + const messages = await client.fetch(`${adjustedStart}:${adjustedEnd}`, { envelope: true, flags: true, - bodyStructure: true - }; + bodyStructure: true, + // Only fetch preview content if requested + ...(preview ? { + bodyParts: ['TEXT'], + bodyPartsOptions: { + TEXT: { + maxLength: 1000 // Limit preview length + } + } + } : {}) + }); - // Only fetch preview if requested - if (preview) { - options.bodyParts = ['TEXT', 'HTML']; - } - - const messages = await client.fetch(range, options); - - // Process messages for await (const message of messages) { - // Extract preview content correctly - let previewContent = null; - if (preview && message.bodyParts) { - // Try HTML first, then TEXT - const htmlPart = message.bodyParts.get('HTML'); - const textPart = message.bodyParts.get('TEXT'); - previewContent = htmlPart?.toString() || textPart?.toString() || null; - } - - const email: Email = { - id: message.uid.toString(), + result.push({ + id: message.uid, from: message.envelope.from?.[0]?.address || '', - fromName: message.envelope.from?.[0]?.name || - message.envelope.from?.[0]?.address?.split('@')[0] || '', - to: message.envelope.to?.map((addr: any) => addr.address).join(', ') || '', + fromName: message.envelope.from?.[0]?.name || message.envelope.from?.[0]?.address?.split('@')[0] || '', + to: message.envelope.to?.map(addr => addr.address).join(', ') || '', subject: message.envelope.subject || '(No subject)', date: message.envelope.date?.toISOString() || new Date().toISOString(), read: message.flags.has('\\Seen'), @@ -215,31 +121,24 @@ export async function GET(request: Request) { folder: mailbox.path, hasAttachments: message.bodyStructure?.type === 'multipart', flags: Array.from(message.flags), - preview: previewContent - }; - - result.push(email); + // Include preview content if available + preview: preview ? message.bodyParts?.TEXT?.toString() : null + }); } } - // 9. Prepare response data - const responseData = { + return NextResponse.json({ emails: result, folders: availableFolders, total: mailbox.exists, - hasMore: end < mailbox.exists, - page, - limit - }; - - // 10. Cache the results - emailCache.set(cacheKey, responseData); - - return NextResponse.json(responseData); - } catch (error) { - // Connection error - remove from pool - connectionPool.delete(session.user.id); - throw error; + hasMore: end < mailbox.exists + }); + } finally { + try { + await client.logout(); + } catch (e) { + console.error('Error during logout:', e); + } } } catch (error) { console.error('Error in courrier route:', error); @@ -248,12 +147,4 @@ export async function GET(request: Request) { { status: 500 } ); } -} - -// Helper method to release connection when app shutting down -export async function cleanup() { - for (const [userId, connection] of connectionPool.entries()) { - await connection.client.logout().catch(console.error); - connectionPool.delete(userId); - } -} \ No newline at end of file +} \ No newline at end of file diff --git a/app/api/mail/mark-read/route.ts b/app/api/mail/mark-read/route.ts index 8185d39b..d306b896 100644 --- a/app/api/mail/mark-read/route.ts +++ b/app/api/mail/mark-read/route.ts @@ -1,7 +1,7 @@ import { NextResponse } from 'next/server'; import { getServerSession } from 'next-auth'; import { authOptions } from '@/app/api/auth/[...nextauth]/route'; -import { prisma } from '@/lib/prisma'; +import { getImapClient } from '@/lib/imap'; import { ImapFlow } from 'imapflow'; export async function POST(request: Request) { @@ -9,7 +9,7 @@ export async function POST(request: Request) { try { // Get the session and validate it const session = await getServerSession(authOptions); - console.log('Session:', session); + console.log('Session:', session); // Debug log if (!session?.user?.id) { console.error('No session or user ID found'); @@ -21,7 +21,7 @@ export async function POST(request: Request) { // Get the request body const { emailId, isRead } = await request.json(); - console.log('Request body:', { emailId, isRead }); + console.log('Request body:', { emailId, isRead }); // Debug log if (!emailId || typeof isRead !== 'boolean') { console.error('Invalid request parameters:', { emailId, isRead }); @@ -34,99 +34,43 @@ export async function POST(request: Request) { // Get the current folder from the request URL const url = new URL(request.url); const folder = url.searchParams.get('folder') || 'INBOX'; - console.log('Folder:', folder); - - // Get credentials directly from database - const credentials = await prisma.mailCredentials.findUnique({ - where: { - userId: session.user.id - } - }); - - if (!credentials) { - return NextResponse.json( - { error: 'No mail credentials found. Please configure your email account.' }, - { status: 401 } - ); - } - - // Create IMAP client - client = new ImapFlow({ - host: credentials.host, - port: credentials.port, - secure: true, - auth: { - user: credentials.email, - pass: credentials.password, - }, - logger: false, - emitLogs: false, - tls: { - rejectUnauthorized: false - }, - disableAutoIdle: true - }); + console.log('Folder:', folder); // Debug log try { - await client.connect(); - - // Open the mailbox - const mailbox = await client.mailboxOpen(folder); - console.log(`Mailbox opened: ${folder}, total messages: ${mailbox.exists}`); - - // Scan through messages in chunks to find the one with the right UID - let foundMessage = false; - const chunkSize = 10; - - for (let i = 1; i <= mailbox.exists; i += chunkSize) { - const endIdx = Math.min(i + chunkSize - 1, mailbox.exists); - const range = `${i}:${endIdx}`; - - console.log(`Scanning messages ${range} to find UID ${emailId}`); - - // Fetch messages in chunks with UID - const messages = client.fetch(range, { - uid: true, - flags: true - }); - - for await (const message of messages) { - if (message.uid.toString() === emailId) { - console.log(`Found matching message with UID ${emailId}`); - - // Get the sequence number (i is the start of the range) - const seqNum = i + message.seq - 1; - console.log(`Message sequence number: ${seqNum}`); - - // Update the flags based on the isRead parameter - if (isRead && !message.flags.has('\\Seen')) { - console.log(`Marking message ${emailId} as read`); - await client.messageFlagsAdd(seqNum.toString(), ['\\Seen']); - } else if (!isRead && message.flags.has('\\Seen')) { - console.log(`Marking message ${emailId} as unread`); - await client.messageFlagsRemove(seqNum.toString(), ['\\Seen']); - } else { - console.log(`Message ${emailId} already has the correct read status`); - } - - foundMessage = true; - break; - } - } - - if (foundMessage) { - break; - } + // Initialize IMAP client with user credentials + client = await getImapClient(); + if (!client) { + console.error('Failed to initialize IMAP client'); + return NextResponse.json( + { error: 'Failed to initialize email client' }, + { status: 500 } + ); } - if (!foundMessage) { - console.error(`Email with UID ${emailId} not found`); + await client.connect(); + await client.mailboxOpen(folder); + + // Fetch the email to get its UID + const message = await client.fetchOne(emailId.toString(), { + uid: true, + flags: true + }); + + if (!message) { + console.error('Email not found:', emailId); return NextResponse.json( { error: 'Email not found' }, { status: 404 } ); } + // Update the flags + if (isRead) { + await client.messageFlagsAdd(message.uid.toString(), ['\\Seen'], { uid: true }); + } else { + await client.messageFlagsRemove(message.uid.toString(), ['\\Seen'], { uid: true }); + } + return NextResponse.json({ success: true }); } finally { if (client) { diff --git a/app/courrier/page.tsx b/app/courrier/page.tsx index c6706174..42e06a8f 100644 --- a/app/courrier/page.tsx +++ b/app/courrier/page.tsx @@ -49,8 +49,6 @@ export interface Email { subject: string; content: string; body?: string; // For backward compatibility - textContent?: string; - rawContent?: string; // Raw email content for fallback display date: string; read: boolean; starred: boolean; @@ -101,147 +99,90 @@ function splitEmailHeadersAndBody(emailBody: string): { headers: string; body: s } function EmailContent({ email }: { email: Email }) { - // Improved debugging with more details - console.log("EmailContent rendering with EMAIL OBJECT:", email); - - // Check if we have a raw object with content - const rawEmail = (email as any)._raw; - if (rawEmail) { - console.log("Found _raw email field:", rawEmail); - } - - console.log("Content details:", { - hasContent: Boolean(email.content), - contentLength: email.content?.length || 0, - contentType: typeof email.content, - hasTextContent: Boolean(email.textContent), - textContentLength: email.textContent?.length || 0, - textContentType: typeof email.textContent, - hasRawContent: Boolean(email.rawContent), - rawContentLength: email.rawContent?.length || 0, - hasBody: Boolean(email.body), - bodyLength: email.body?.length || 0, - hasRawHtml: Boolean(rawEmail?.html), - rawHtmlLength: rawEmail?.html?.length || 0, - hasRawText: Boolean(rawEmail?.text), - rawTextLength: rawEmail?.text?.length || 0 - }); + const [content, setContent] = useState(null); + const [error, setError] = useState(null); + const [isLoading, setIsLoading] = useState(false); - // Use state to track if we've rendered content - const [renderedContent, setRenderedContent] = useState(false); - - // Special handling for potential nested content structure - const content = typeof email.content === 'object' - ? JSON.stringify(email.content) - : email.content; - - const textContent = typeof email.textContent === 'object' - ? JSON.stringify(email.textContent) - : email.textContent; - - const rawContent = typeof email.rawContent === 'object' - ? JSON.stringify(email.rawContent) - : email.rawContent; - - const body = typeof email.body === 'object' - ? JSON.stringify(email.body) - : email.body; - - // Also try to get content from the _raw field if it exists - const rawHtml = rawEmail?.html || ''; - const rawText = rawEmail?.text || ''; - - // Use a more defensive approach with content - const hasContent = content !== undefined && content !== null && content.trim() !== ''; - const hasTextContent = textContent !== undefined && textContent !== null && textContent.trim() !== ''; - const hasRawContent = rawContent !== undefined && rawContent !== null && rawContent.trim() !== ''; - const hasBody = body !== undefined && body !== null && body.trim() !== ''; - const hasRawHtml = rawHtml !== undefined && rawHtml !== null && rawHtml.trim() !== ''; - const hasRawText = rawText !== undefined && rawText !== null && rawText.trim() !== ''; - - // Try raw text first (most reliable format) - if (hasRawText && !renderedContent) { - setRenderedContent(true); + useEffect(() => { + let mounted = true; + + async function loadContent() { + if (!email) return; + + setIsLoading(true); + try { + if (!email.content) { + if (mounted) { + setContent(
No content available
); + setIsLoading(false); + } + return; + } + + const formattedEmail = email.content.trim(); + if (!formattedEmail) { + if (mounted) { + setContent(
No content available
); + setIsLoading(false); + } + return; + } + + const parsedEmail = await decodeEmail(formattedEmail); + + if (mounted) { + if (parsedEmail.html) { + setContent( +
+ ); + } else if (parsedEmail.text) { + setContent( +
+ {parsedEmail.text} +
+ ); + } else { + setContent(
No content available
); + } + setError(null); + setIsLoading(false); + } + } catch (err) { + console.error('Error rendering email content:', err); + if (mounted) { + setError('Error rendering email content. Please try again.'); + setContent(null); + setIsLoading(false); + } + } + } + + loadContent(); + + return () => { + mounted = false; + }; + }, [email?.content]); + + if (isLoading) { return ( -
- {rawText} +
+
); } - - // Try raw HTML - if (hasRawHtml && !renderedContent) { - setRenderedContent(true); - return ( -
- ); + + if (error) { + return
{error}
; } - - // If textContent is available, render it directly - if (hasTextContent && !renderedContent) { - setRenderedContent(true); - return ( -
- {textContent} -
- ); - } - - // If html content is available, render it directly - if (hasContent && !renderedContent) { - setRenderedContent(true); - return ( -
- ); - } - - // Try body field (backward compatibility) - if (hasBody && !renderedContent) { - setRenderedContent(true); - return ( -
- ); - } - - // Fallback to raw content if available - if (hasRawContent && !renderedContent) { - setRenderedContent(true); - return ( -
- {rawContent} -
- ); - } - - // Last resort - show a better error message with debugging info - return ( -
-

No content available

-
-

Content flags:

-

- Has content: {hasContent ? 'Yes' : 'No'} (type: {typeof email.content})

-

- Has text content: {hasTextContent ? 'Yes' : 'No'} (type: {typeof email.textContent})

-

- Has raw content: {hasRawContent ? 'Yes' : 'No'} (type: {typeof email.rawContent})

-

- Has body: {hasBody ? 'Yes' : 'No'} (type: {typeof email.body})

-

- Has raw HTML: {hasRawHtml ? 'Yes' : 'No'}

-

- Has raw text: {hasRawText ? 'Yes' : 'No'}

-
-
- ); + + return content ||
No content available
; } function renderEmailContent(email: Email) { - if (!email) return
No email selected
; - return ; } @@ -536,7 +477,6 @@ export default function CourrierPage() { content: string; type: 'reply' | 'reply-all' | 'forward'; } | null>(null); - const [selectEmailLoading, setSelectEmailLoading] = useState(false); // Debug logging for email distribution useEffect(() => { @@ -680,89 +620,60 @@ export default function CourrierPage() { return account ? account.color : 'bg-gray-500'; }; - // Update handleEmailSelect to set selectedEmail correctly and improve error handling + // Update handleEmailSelect to set selectedEmail correctly const handleEmailSelect = async (emailId: string) => { try { - // Set a provisional selected email immediately for better UX - const initialEmail = emails.find((e) => e.id === emailId) || null; - setSelectedEmail(initialEmail); - setSelectEmailLoading(true); - - console.log(`Fetching email ${emailId} from folder ${currentView}`); - const response = await fetch(`/api/courrier/${emailId}?folder=${currentView}`); + // Make sure it's using this: + const response = await fetch(`/api/courrier/${emailId}`); if (!response.ok) { - console.error(`Error fetching email: ${response.status} ${response.statusText}`); - setSelectEmailLoading(false); - return; + throw new Error('Failed to fetch full email content'); } - // Get the raw JSON string first to log it exactly as received - const rawJsonText = await response.text(); - console.log("Raw API response text:", rawJsonText); + const fullEmail = await response.json(); - // Then parse it - let fullEmail; + // Update the email in the list and selected email with full content + setEmails(prevEmails => prevEmails.map(email => + email.id === emailId + ? { ...email, content: fullEmail.content || fullEmail.body || email.content } + : email + )); + + setSelectedEmail(prev => prev ? { + ...prev, + content: fullEmail.content || fullEmail.body || prev.content + } : prev); + + // Try to mark as read in the background try { - fullEmail = JSON.parse(rawJsonText); - console.log("Parsed JSON API response:", fullEmail); - } catch (jsonError) { - console.error("Error parsing JSON response:", jsonError); - setSelectEmailLoading(false); - return; - } - - // Create a clean, processed version of the email ensuring all necessary fields exist - const processedEmail = { - ...fullEmail, - id: fullEmail.id || emailId, - content: typeof fullEmail.content === 'string' ? fullEmail.content : '', - textContent: typeof fullEmail.textContent === 'string' ? fullEmail.textContent : '', - rawContent: typeof fullEmail.rawContent === 'string' ? fullEmail.rawContent : '', - body: typeof fullEmail.body === 'string' ? fullEmail.body : '', - from: fullEmail.from || initialEmail?.from || '', - fromName: fullEmail.fromName || initialEmail?.fromName || '', - to: fullEmail.to || initialEmail?.to || '', - subject: fullEmail.subject || initialEmail?.subject || '(No subject)', - date: fullEmail.date || initialEmail?.date || new Date().toISOString(), - read: fullEmail.read !== undefined ? fullEmail.read : true, - folder: fullEmail.folder || currentView - }; - - console.log("Processed email for UI:", { - ...processedEmail, - contentLength: processedEmail.content.length, - textContentLength: processedEmail.textContent.length, - rawContentLength: processedEmail.rawContent.length - }); - - // Set the selected email with complete information - setSelectedEmail(processedEmail); - - // Update the email in the list to mark it as read - setEmails((prevEmails) => - prevEmails.map((email) => - email.id === emailId ? { ...email, read: true } : email - ) - ); - - // Try to mark the email as read in the background - try { - const markReadResponse = await fetch(`/api/mail/mark-read?id=${emailId}&folder=${currentView}`, { - method: "POST", + const markReadResponse = await fetch(`/api/mail/mark-read`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + emailId, + isRead: true, + }), }); - - if (!markReadResponse.ok) { - console.error(`Error marking email as read: ${markReadResponse.status} ${markReadResponse.statusText}`); + + if (markReadResponse.ok) { + // Only update the emails list if the API call was successful + setEmails((prevEmails: Email[]) => + prevEmails.map((email: Email): Email => + email.id === emailId + ? { ...email, read: true } + : email + ) + ); + } else { + console.error('Failed to mark email as read:', await markReadResponse.text()); } - } catch (markReadError) { - console.error("Error calling mark-read API:", markReadError); + } catch (error) { + console.error('Error marking email as read:', error); } - - setSelectEmailLoading(false); } catch (error) { - console.error("Error in handleEmailSelect:", error); - setSelectEmailLoading(false); + console.error('Error fetching email:', error); } }; @@ -1124,60 +1035,7 @@ export default function CourrierPage() {
- {/* Email status message */} -

- Viewing message from: {selectedEmail.from} • {new Date(selectedEmail.date).toLocaleString()} -

- - {/* Direct content display for debugging */} -
-
-

Direct Content Preview

-
-
- Content: {selectedEmail.content - ?
-

Type: {typeof selectedEmail.content}

-

Value: {typeof selectedEmail.content === 'object' - ? JSON.stringify(selectedEmail.content) - : selectedEmail.content.substring(0, 500)}...

-
- : 'Empty'}
- Text Content: {selectedEmail.textContent - ?
-

Type: {typeof selectedEmail.textContent}

-

Value: {typeof selectedEmail.textContent === 'object' - ? JSON.stringify(selectedEmail.textContent) - : selectedEmail.textContent.substring(0, 500)}...

-
- : 'Empty'}
- Raw Content: {selectedEmail.rawContent - ?
-

Type: {typeof selectedEmail.rawContent}

-

Value: {typeof selectedEmail.rawContent === 'object' - ? JSON.stringify(selectedEmail.rawContent) - : selectedEmail.rawContent.substring(0, 500)}...

-
- : 'Empty'}
- - {(selectedEmail as any)._raw && ( - <> - Raw Email Fields: -
-

HTML: {(selectedEmail as any)._raw.html - ? `${(selectedEmail as any)._raw.html.substring(0, 200)}...` - : 'Empty'}

-

Text: {(selectedEmail as any)._raw.text - ? `${(selectedEmail as any)._raw.text.substring(0, 200)}...` - : 'Empty'}

-
- - )} -
-
-
- {/* Go back to using the original renderEmailContent function */} {renderEmailContent(selectedEmail)}
diff --git a/lib/server/email-parser.ts b/lib/server/email-parser.ts index dc9a581e..587644da 100644 --- a/lib/server/email-parser.ts +++ b/lib/server/email-parser.ts @@ -1,8 +1,6 @@ import { simpleParser } from 'mailparser'; -function cleanHtml(html: string | null | undefined): string { - if (!html) return ''; - +function cleanHtml(html: string): string { try { // Basic HTML cleaning without DOMPurify return html @@ -19,7 +17,7 @@ function cleanHtml(html: string | null | undefined): string { .trim(); } catch (error) { console.error('Error cleaning HTML:', error); - return html || ''; + return html; } } @@ -33,25 +31,8 @@ function getAddressText(address: any): string | null { export async function parseEmail(emailContent: string) { try { - // Add debug logging for the raw content length - console.log(`Starting to parse email content (length: ${emailContent ? emailContent.length : 0})`); - const parsed = await simpleParser(emailContent); - // Add debug logging for the parsed content - console.log('Parsed email fields:', { - hasSubject: !!parsed.subject, - hasHtml: !!parsed.html, - htmlLength: parsed.html?.length || 0, - hasText: !!parsed.text, - textLength: parsed.text?.length || 0, - attachmentsCount: parsed.attachments?.length || 0, - }); - - // Clean the HTML content if it exists - const cleanedHtml = parsed.html ? cleanHtml(parsed.html) : null; - - // Return a properly structured object with all fields explicitly specified return { subject: parsed.subject || null, from: getAddressText(parsed.from), @@ -59,7 +40,7 @@ export async function parseEmail(emailContent: string) { cc: getAddressText(parsed.cc), bcc: getAddressText(parsed.bcc), date: parsed.date || null, - html: cleanedHtml, + html: parsed.html ? cleanHtml(parsed.html) : null, text: parsed.text || null, attachments: parsed.attachments || [], headers: Object.fromEntries(parsed.headers)