diff --git a/app/api/parse-email/route.ts b/app/api/parse-email/route.ts index 80246be7..c23e3059 100644 --- a/app/api/parse-email/route.ts +++ b/app/api/parse-email/route.ts @@ -11,38 +11,81 @@ function getEmailAddress(address: AddressObject | AddressObject[] | undefined): export async function POST(request: Request) { try { + console.log('[DEBUG] Parse-email API called'); + const body = await request.json(); const emailContent = body.email || body.emailContent; if (!emailContent || typeof emailContent !== 'string') { + console.error('[DEBUG] Parse-email API error: Invalid email content'); return NextResponse.json( { error: 'Invalid email content' }, { status: 400 } ); } - const parsed = await simpleParser(emailContent); + console.log('[DEBUG] Parse-email API processing email content, length:', emailContent.length); + console.log('[DEBUG] Content sample:', emailContent.substring(0, 100) + '...'); - return NextResponse.json({ - subject: parsed.subject || null, - from: getEmailAddress(parsed.from), - to: getEmailAddress(parsed.to), - cc: getEmailAddress(parsed.cc), - bcc: getEmailAddress(parsed.bcc), - date: parsed.date || null, - html: parsed.html || null, - text: parsed.textAsHtml || parsed.text || null, - attachments: parsed.attachments?.map(att => ({ - filename: att.filename, - contentType: att.contentType, - size: att.size - })) || [], - headers: parsed.headers || {} - }); + try { + const parsed = await simpleParser(emailContent); + + console.log('[DEBUG] Parse-email API successfully parsed email:', { + hasSubject: !!parsed.subject, + hasHtml: !!parsed.html, + hasText: !!parsed.text, + hasTextAsHtml: !!parsed.textAsHtml, + fromCount: parsed.from ? (Array.isArray(parsed.from) ? parsed.from.length : 1) : 0, + attachmentCount: parsed.attachments?.length || 0 + }); + + return NextResponse.json({ + subject: parsed.subject || null, + from: getEmailAddress(parsed.from), + to: getEmailAddress(parsed.to), + cc: getEmailAddress(parsed.cc), + bcc: getEmailAddress(parsed.bcc), + date: parsed.date || null, + html: parsed.html || parsed.textAsHtml || null, + text: parsed.text || null, + attachments: parsed.attachments?.map(att => ({ + filename: att.filename, + contentType: att.contentType, + size: att.size + })) || [], + headers: parsed.headers || {} + }); + } catch (parseError) { + console.error('[DEBUG] Parse-email API error parsing email:', parseError); + + // Try simpler parsing method for more resilience + try { + console.log('[DEBUG] Attempting fallback parsing method'); + const resultObj: any = { text: emailContent, html: null }; + + // Simple check if it might be HTML + if (emailContent.includes('/g, '>') + .replace(/\n/g, '
'); + } + + console.log('[DEBUG] Fallback parsing generated simple result'); + return NextResponse.json(resultObj); + } catch (fallbackError) { + console.error('[DEBUG] Even fallback parsing failed:', fallbackError); + throw parseError; // Throw the original error + } + } } catch (error) { - console.error('Error parsing email:', error); + console.error('[DEBUG] Parse-email API unhandled error:', error); return NextResponse.json( - { error: 'Failed to parse email' }, + { error: 'Failed to parse email', details: error instanceof Error ? error.message : 'Unknown error' }, { status: 500 } ); } diff --git a/app/courrier/[id]/route.ts b/app/courrier/[id]/route.ts new file mode 100644 index 00000000..bdd0084d --- /dev/null +++ b/app/courrier/[id]/route.ts @@ -0,0 +1,273 @@ +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 { simpleParser } from 'mailparser'; + +// Simple in-memory cache for email content +const emailContentCache = new Map(); +const CACHE_TTL = 30 * 60 * 1000; // 30 minutes in milliseconds + +export async function GET( + request: Request, + { params }: { params: { id: string } } +) { + try { + console.log(`[DEBUG] GET request for single email ID: ${params.id}`); + + const session = await getServerSession(authOptions); + if (!session || !session.user?.id) { + console.log('[DEBUG] Not authenticated'); + return NextResponse.json( + { error: "Not authenticated" }, + { status: 401 } + ); + } + + const { id } = params; + if (!id) { + console.log('[DEBUG] Missing email ID'); + return NextResponse.json( + { error: "Missing email ID" }, + { status: 400 } + ); + } + + // Check cache first + const cacheKey = `${session.user.id}:${id}`; + const cachedContent = emailContentCache.get(cacheKey); + const now = Date.now(); + + if (cachedContent && now - cachedContent.timestamp < CACHE_TTL) { + console.log(`[DEBUG] Using cached content for email ${id}`); + return NextResponse.json({ + id, + content: cachedContent.content, + contentFetched: true, + fromCache: true + }); + } + + // Get mail credentials + const credentials = await prisma.mailCredentials.findUnique({ + where: { + userId: session.user.id, + }, + }); + + if (!credentials) { + console.log('[DEBUG] No mail credentials found'); + return NextResponse.json( + { error: "No mail credentials found" }, + { status: 404 } + ); + } + + const { searchParams } = new URL(request.url); + const folder = searchParams.get("folder") || "INBOX"; + console.log(`[DEBUG] Fetching email ${id} from folder ${folder}`); + + // Create IMAP client + let imapClient: any = null; + try { + imapClient = new ImapFlow({ + host: credentials.host, + port: credentials.port, + secure: true, + auth: { + user: credentials.email, + pass: credentials.password, + }, + logger: false, + }); + + await imapClient.connect(); + console.log(`[DEBUG] Connected to IMAP server to fetch email ${id}`); + + // Select mailbox + const mailboxData = await imapClient.mailboxOpen(folder); + console.log(`[DEBUG] Opened mailbox ${folder}, total messages: ${mailboxData.exists}`); + + // Fetch the complete email with its source + const message = await imapClient.fetchOne(Number(id), { + source: true, + envelope: true + }); + + if (!message) { + console.log(`[DEBUG] Email ${id} not found`); + return NextResponse.json( + { error: "Email not found" }, + { status: 404 } + ); + } + + const { source, envelope } = message; + console.log(`[DEBUG] Successfully fetched email source, length: ${source.length}`); + + // Parse the full email content + const parsedEmail = await simpleParser(source.toString()); + console.log(`[DEBUG] Parsed email source: has HTML: ${!!parsedEmail.html}, has text: ${!!parsedEmail.text}`); + + // Prioritize HTML content with fallbacks + const content = parsedEmail.html || parsedEmail.textAsHtml || parsedEmail.text || ''; + + // Cache the content + emailContentCache.set(cacheKey, { + content, + timestamp: now + }); + + // Return the content + return NextResponse.json({ + id, + subject: envelope.subject, + content, + contentFetched: true + }); + } catch (error: any) { + console.error("[DEBUG] Error fetching email content:", error); + return NextResponse.json( + { + error: "Failed to fetch email content", + message: error.message, + details: error.stack + }, + { status: 500 } + ); + } finally { + // Close the mailbox and connection + if (imapClient) { + try { + await imapClient.mailboxClose(); + await imapClient.logout(); + console.log(`[DEBUG] Closed IMAP connection for email ${id}`); + } catch (e) { + console.error("[DEBUG] Error closing IMAP connection:", e); + } + } + } + } catch (error: any) { + console.error("[DEBUG] Unhandled error in GET:", error); + return NextResponse.json( + { + error: "Internal server error", + message: error.message, + details: error.stack + }, + { status: 500 } + ); + } +} + +// Handle marking emails as read +export async function POST( + request: Request, + { params }: { params: { id: string } } +) { + try { + console.log(`[DEBUG] POST request for email ID: ${params.id}`); + + const session = await getServerSession(authOptions); + if (!session || !session.user?.id) { + return NextResponse.json( + { error: "Not authenticated" }, + { status: 401 } + ); + } + + const { id } = params; + if (!id) { + return NextResponse.json( + { error: "Missing email ID" }, + { status: 400 } + ); + } + + const body = await request.json(); + const { action } = body; + + if (action !== 'mark-read' && action !== 'mark-unread') { + return NextResponse.json( + { error: "Invalid action. Supported actions: mark-read, mark-unread" }, + { status: 400 } + ); + } + + console.log(`[DEBUG] Processing action ${action} for email ${id}`); + + // Get mail credentials + const credentials = await prisma.mailCredentials.findUnique({ + where: { + userId: session.user.id, + }, + }); + + if (!credentials) { + return NextResponse.json( + { error: "No mail credentials found" }, + { status: 404 } + ); + } + + const { searchParams } = new URL(request.url); + const folder = searchParams.get("folder") || "INBOX"; + + // Create IMAP client + let imapClient: any = null; + try { + imapClient = new ImapFlow({ + host: credentials.host, + port: credentials.port, + secure: true, + auth: { + user: credentials.email, + pass: credentials.password, + }, + logger: false, + }); + + await imapClient.connect(); + console.log(`[DEBUG] Connected to IMAP server for ${action}`); + + // Select mailbox + await imapClient.mailboxOpen(folder); + console.log(`[DEBUG] Opened mailbox ${folder} for ${action}`); + + // Set flag based on action + if (action === 'mark-read') { + await imapClient.messageFlagsAdd(Number(id), ['\\Seen']); + console.log(`[DEBUG] Marked email ${id} as read`); + } else { + await imapClient.messageFlagsRemove(Number(id), ['\\Seen']); + console.log(`[DEBUG] Marked email ${id} as unread`); + } + + return NextResponse.json({ success: true, action }); + } catch (error: any) { + console.error(`[DEBUG] Error ${action}:`, error); + return NextResponse.json( + { error: `Failed to ${action}`, message: error.message }, + { status: 500 } + ); + } finally { + // Close the mailbox and connection + if (imapClient) { + try { + await imapClient.mailboxClose(); + await imapClient.logout(); + console.log(`[DEBUG] Closed IMAP connection after ${action}`); + } catch (e) { + console.error("[DEBUG] Error closing IMAP connection:", e); + } + } + } + } catch (error: any) { + console.error("[DEBUG] Unhandled error in POST:", error); + return NextResponse.json( + { error: "Internal server error", message: error.message }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/app/courrier/page.tsx b/app/courrier/page.tsx index 164719c1..c45bacb1 100644 --- a/app/courrier/page.tsx +++ b/app/courrier/page.tsx @@ -888,6 +888,38 @@ export default function CourrierPage() { throw new Error('Email not found in list'); } + // Check if we need to fetch full content + if (!selectedEmail.content || selectedEmail.content.length === 0) { + console.log('[DEBUG] Fetching full content for email:', emailId); + + try { + const response = await fetch(`/api/courrier/${emailId}?folder=${encodeURIComponent(selectedEmail.folder || 'INBOX')}`); + + if (!response.ok) { + throw new Error(`Failed to fetch email content: ${response.status}`); + } + + 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 === emailId + ? { ...email, content: fullContent.content, contentFetched: true } + : email + ) + ); + + console.log('[DEBUG] Successfully fetched full content for email:', emailId); + } catch (error) { + console.error('[DEBUG] Error fetching full content:', error); + } + } + // Set selected email from our existing data (which now includes full content) setSelectedEmail(selectedEmail); @@ -1507,6 +1539,44 @@ 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 (!response.ok) { + throw new Error(`Failed to fetch email content: ${response.status}`); + } + + 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 + } + + setContentLoading(false); + } + const getReplyTo = () => { if (type === 'forward') return ''; return selectedEmail.from; diff --git a/components/ComposeEmail.tsx b/components/ComposeEmail.tsx index 65f69164..2ac5c515 100644 --- a/components/ComposeEmail.tsx +++ b/components/ComposeEmail.tsx @@ -90,8 +90,27 @@ export default function ComposeEmail({ try { const emailToProcess = replyTo || forwardFrom; + console.log('[DEBUG] Initializing compose content with email:', + emailToProcess ? { + id: emailToProcess.id, + subject: emailToProcess.subject, + hasContent: !!emailToProcess.content, + contentLength: emailToProcess.content ? emailToProcess.content.length : 0, + preview: emailToProcess.preview + } : 'null' + ); + if (!emailToProcess?.content) { - console.error('No email content found to process'); + console.error('[DEBUG] No email content found to process'); + composeBodyRef.current.innerHTML = ` +
+
+
Error: No original message content available.
+
+ Please select the email again or try refreshing the page. +
+
+ `; return; } @@ -103,6 +122,8 @@ export default function ComposeEmail({ `; + console.log('[DEBUG] Sending content to parse-email API, length:', emailToProcess.content.length); + // Parse the original email using the API const response = await fetch('/api/parse-email', { method: 'POST', @@ -112,12 +133,24 @@ export default function ComposeEmail({ body: JSON.stringify({ email: emailToProcess.content }), }); + console.log('[DEBUG] Parse-email API response status:', response.status); + const data = await response.json(); + console.log('[DEBUG] Parse-email API response:', { + hasHtml: !!data.html, + hasText: !!data.text, + subject: data.subject, + error: data.error + }); + if (!response.ok) { throw new Error(data.error || 'Failed to parse email'); } const emailContent = data.html || data.text || ''; + if (!emailContent) { + console.warn('[DEBUG] No HTML or text content returned from parser'); + } // Format the reply/forward content const quotedContent = forwardFrom ? ` @@ -167,14 +200,18 @@ export default function ComposeEmail({ // Update compose state setComposeBody(formattedContent); setLocalContent(formattedContent); + console.log('[DEBUG] Successfully set compose content'); } } catch (error) { - console.error('Error initializing compose content:', error); + console.error('[DEBUG] Error initializing compose content:', error); if (composeBodyRef.current) { const errorContent = `

Error loading original message.
+
+ Technical details: ${error instanceof Error ? error.message : 'Unknown error'} +
`; composeBodyRef.current.innerHTML = errorContent;