diff --git a/app/api/courrier/[id]/mark-read/route.ts b/app/api/courrier/[id]/mark-read/route.ts index f1d88c6c..0b6258bd 100644 --- a/app/api/courrier/[id]/mark-read/route.ts +++ b/app/api/courrier/[id]/mark-read/route.ts @@ -35,12 +35,14 @@ export async function POST( { params }: { params: { id: string } } ) { try { + // Get session const session = await getServerSession(authOptions); if (!session?.user?.id) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } - const emailId = params.id; + // Properly await the params object before accessing its properties + const { id: emailId } = await Promise.resolve(params); if (!emailId) { return NextResponse.json({ error: 'Email ID is required' }, { status: 400 }); } diff --git a/app/api/courrier/credentials/route.ts b/app/api/courrier/credentials/route.ts index 9affd60d..7652b635 100644 --- a/app/api/courrier/credentials/route.ts +++ b/app/api/courrier/credentials/route.ts @@ -1,32 +1,39 @@ import { NextResponse } from 'next/server'; -import { getServerSession } from 'next-auth'; +import { getServerSession } from 'next-auth/next'; import { authOptions } from '@/app/api/auth/[...nextauth]/route'; import { prisma } from '@/lib/prisma'; -export async function GET(request: Request) { +export async function GET() { try { - // Verify user is authenticated const session = await getServerSession(authOptions); - if (!session?.user?.id) { + if (!session || !session.user?.id) { + console.log("No authenticated session found"); return NextResponse.json( - { error: 'Unauthorized' }, + { error: "Not authenticated" }, { status: 401 } ); } - // Get credentials from database + console.log(`Attempting to fetch mail credentials for user ${session.user.id}`); + + // Get mail credentials const credentials = await prisma.mailCredentials.findUnique({ - where: { userId: session.user.id } + where: { + userId: session.user.id, + }, }); - + if (!credentials) { + console.log(`No mail credentials found for user ${session.user.id}`); return NextResponse.json( - { error: 'No mail credentials found', credentials: null }, + { error: "No mail credentials found" }, { status: 404 } ); } - // Return only what's needed (especially the email) + console.log(`Found credentials for email: ${credentials.email}`); + + // Return only necessary credential details return NextResponse.json({ credentials: { email: credentials.email, @@ -35,9 +42,17 @@ export async function GET(request: Request) { } }); } catch (error) { - console.error('Error fetching credentials:', error); + console.error("Error fetching mail credentials:", error); + + // Log more detailed error information + if (error instanceof Error) { + console.error("Error name:", error.name); + console.error("Error message:", error.message); + console.error("Error stack:", error.stack); + } + return NextResponse.json( - { error: 'Failed to fetch credentials' }, + { error: "Failed to fetch mail credentials" }, { status: 500 } ); } diff --git a/app/api/courrier/route.ts b/app/api/courrier/route.ts index 103881ea..2ef52391 100644 --- a/app/api/courrier/route.ts +++ b/app/api/courrier/route.ts @@ -3,33 +3,25 @@ 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'; + +// Type definitions +interface EmailCacheEntry { + data: any; + timestamp: number; +} + +interface CredentialsCacheEntry { + client: any; // Use any for ImapFlow to avoid type issues + timestamp: number; +} // Email cache structure -interface EmailCache { - [key: string]: { - data: any; - timestamp: number; - }; -} +const emailListCache: Record = {}; +const credentialsCache: Record = {}; -// Credentials cache to reduce database queries -interface CredentialsCache { - [userId: string]: { - credentials: any; - timestamp: number; - } -} - -// In-memory caches with expiration -// Make emailListCache available globally for other routes -if (!global.emailListCache) { - global.emailListCache = {}; -} -const emailListCache: EmailCache = global.emailListCache; -const credentialsCache: CredentialsCache = {}; - -// Cache TTL in milliseconds (5 minutes) -const CACHE_TTL = 5 * 60 * 1000; +// Cache TTL in milliseconds +const CACHE_TTL = 5 * 60 * 1000; // 5 minutes // Helper function to get credentials with caching async function getCredentialsWithCache(userId: string) { @@ -38,7 +30,7 @@ async function getCredentialsWithCache(userId: string) { const now = Date.now(); if (cachedCreds && now - cachedCreds.timestamp < CACHE_TTL) { - return cachedCreds.credentials; + return cachedCreds.client; } // Otherwise fetch from database @@ -49,12 +41,21 @@ async function getCredentialsWithCache(userId: string) { // Cache the result if (credentials) { credentialsCache[userId] = { - credentials, + client: credentialsCache[userId]?.client || new ImapFlow({ + host: credentials.host, + port: credentials.port, + secure: true, + auth: { + user: credentials.email, + pass: credentials.password, + }, + logger: false, + }), timestamp: now }; } - return credentials; + return credentialsCache[userId]?.client || null; } // Retry logic for IMAP operations @@ -82,203 +83,289 @@ async function retryOperation(operation: () => Promise, maxAttempts = 3, d export async function GET(request: Request) { try { - console.log('Courrier API call received'); const session = await getServerSession(authOptions); - if (!session?.user?.id) { - console.log('No authenticated session found'); + if (!session || !session.user?.id) { return NextResponse.json( - { error: 'Unauthorized' }, - { status: 401 } - ); - } - console.log('User authenticated:', session.user.id); - - // Get URL 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 skipCache = url.searchParams.get('skipCache') === 'true'; - console.log('Request parameters:', { folder, page, limit, skipCache }); - - // Generate cache key based on request parameters - const cacheKey = `${session.user.id}:${folder}:${page}:${limit}:full`; - - // Check cache first if not explicitly skipped - if (!skipCache && emailListCache[cacheKey]) { - const { data, timestamp } = emailListCache[cacheKey]; - // Return cached data if it's fresh (less than 1 minute old) - if (Date.now() - timestamp < 60000) { - console.log('Returning cached email data'); - return NextResponse.json(data); - } - } - - // Get credentials from cache or database - const credentials = await getCredentialsWithCache(session.user.id); - console.log('Credentials retrieved:', credentials ? 'yes' : 'no'); - - if (!credentials) { - console.log('No mail credentials found for user'); - return NextResponse.json( - { error: 'No mail credentials found. Please configure your email account.' }, + { error: "Not authenticated" }, { status: 401 } ); } - // Calculate start and end sequence numbers - const start = (page - 1) * limit + 1; - const end = start + limit - 1; - console.log('Fetching emails from range:', { start, end }); - - // Connect to IMAP server - console.log('Connecting to IMAP server:', credentials.host, credentials.port); - const client = new ImapFlow({ - host: credentials.host, - port: credentials.port, - secure: true, - auth: { - user: credentials.email, - pass: credentials.password, + // Get mail credentials + const credentials = await prisma.mailCredentials.findUnique({ + where: { + userId: session.user.id, }, - logger: false, - emitLogs: false, - tls: { - rejectUnauthorized: false - } }); - try { - await client.connect(); - console.log('Connected to IMAP server'); - - // Get list of all mailboxes first - const mailboxes = await client.list(); - const availableFolders = mailboxes.map(box => box.path); - console.log('Available folders:', availableFolders); - - // Open the requested mailbox - console.log('Opening mailbox:', folder); - const mailbox = await client.mailboxOpen(folder); - console.log('Mailbox stats:', { - exists: mailbox.exists, - name: mailbox.path, - flags: mailbox.flags - }); - - const result = []; - - // Only try to fetch if the mailbox has messages - if (mailbox.exists > 0) { - // Adjust start and end to be within bounds - const adjustedStart = Math.min(start, mailbox.exists); - const adjustedEnd = Math.min(end, mailbox.exists); - console.log('Adjusted fetch range:', { adjustedStart, adjustedEnd }); - - // Fetch both metadata AND full content - const fetchOptions: any = { - envelope: true, - flags: true, - bodyStructure: true, - source: true // Include full email source - }; - - console.log('Fetching messages with options:', fetchOptions); - const fetchPromises = []; - for (let i = adjustedStart; i <= adjustedEnd; i++) { - // Convert to string sequence number as required by ImapFlow - fetchPromises.push(client.fetchOne(`${i}`, fetchOptions)); - } - + if (!credentials) { + return NextResponse.json( + { error: "No mail credentials found" }, + { status: 404 } + ); + } + + const { searchParams } = new URL(request.url); + const page = parseInt(searchParams.get("page") || "1"); + const perPage = parseInt(searchParams.get("perPage") || "20"); + const folder = searchParams.get("folder") || "INBOX"; + const searchQuery = searchParams.get("search") || ""; + + // Check for entry in emailCache + const cacheKey = `${session.user.id}:${folder}:${page}:${perPage}:${searchQuery}`; + const cachedEmails = emailListCache[cacheKey]; + + if (cachedEmails) { + console.log(`Using cached emails for ${cacheKey}`); + return NextResponse.json(cachedEmails.data); + } + + console.log(`Cache miss for ${cacheKey}, fetching from IMAP`); + + // Fetch from IMAP + const cacheCredKey = `credentials:${session.user.id}`; + let imapClient: any = credentialsCache[cacheCredKey]?.client || null; + + if (!imapClient) { + // Create IMAP client + const connectWithRetry = async (retries = 3, delay = 1000): Promise => { try { - const results = await Promise.all(fetchPromises); - - for (const message of results) { - if (!message) continue; // Skip undefined messages - - console.log('Processing message ID:', message.uid); - const emailData: any = { - 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 => addr.address).join(', ') || '', - subject: message.envelope.subject || '(No subject)', - date: message.envelope.date?.toISOString() || new Date().toISOString(), - read: message.flags.has('\\Seen'), - starred: message.flags.has('\\Flagged'), - folder: mailbox.path, - hasAttachments: message.bodyStructure?.type === 'multipart', - flags: Array.from(message.flags), - content: message.source?.toString() || '' // Include full email content - }; - - result.push(emailData); + console.log(`Attempting to connect to IMAP server (${credentials.host}:${credentials.port})...`); + const client = new ImapFlow({ + host: credentials.host, + port: credentials.port, + secure: true, + auth: { + user: credentials.email, + pass: credentials.password, + }, + logger: false, + }); + + await client.connect(); + console.log("Successfully connected to IMAP server"); + return client; + } catch (error) { + if (retries > 0) { + console.log(`Connection failed, retrying... (${retries} attempts left)`); + await new Promise((resolve) => setTimeout(resolve, delay)); + return connectWithRetry(retries - 1, delay * 1.5); } - } catch (fetchError) { - console.error('Error fetching emails:', fetchError); - // Continue with any successfully fetched messages + throw error; } - } else { - console.log('No messages in mailbox'); + }; + + try { + imapClient = await connectWithRetry(); + // Cache for future use + credentialsCache[cacheCredKey] = { + client: imapClient, + timestamp: Date.now() + }; + } catch (error: any) { + console.error("Failed to connect to IMAP server after retries:", error); + return NextResponse.json( + { error: "Failed to connect to IMAP server", message: error.message }, + { status: 500 } + ); + } + } else { + console.log("Using cached IMAP client connection"); + } + + // Function to get mailboxes + const getMailboxes = async () => { + const mailboxes = []; + for await (const mailbox of imapClient.listMailboxes()) { + mailboxes.push(mailbox); + } + return mailboxes; + }; + + // Setup paging + const startIdx = (page - 1) * perPage + 1; + const endIdx = page * perPage; + + let emails: any[] = []; + let mailboxData = null; + + try { + // Select and lock mailbox + mailboxData = await imapClient.mailboxOpen(folder); + console.log(`Opened mailbox ${folder}, ${mailboxData.exists} messages total`); + + // Calculate range based on total messages + const totalMessages = mailboxData.exists; + const from = Math.max(totalMessages - endIdx + 1, 1); + const to = Math.max(totalMessages - startIdx + 1, 1); + + // Skip if no messages or invalid range + if (totalMessages === 0 || from > to) { + console.log("No messages in range, returning empty array"); + const result = { + emails: [], + totalEmails: 0, + page, + perPage, + totalPages: 0, + folder, + mailboxes: await getMailboxes(), + }; + emailListCache[cacheKey] = { + data: result, + timestamp: Date.now() + }; + return NextResponse.json(result); } - const responseData = { - emails: result, - folders: availableFolders, - total: mailbox.exists, - hasMore: end < mailbox.exists + console.log(`Fetching messages ${from}:${to} (page ${page}, ${perPage} per page)`); + + // Search if needed + let messageIds: any[] = []; + if (searchQuery) { + console.log(`Searching for: "${searchQuery}"`); + messageIds = await imapClient.search({ + body: searchQuery + }); + + // Filter to our page range + messageIds = messageIds.filter(id => id >= from && id <= to); + console.log(`Found ${messageIds.length} messages matching search`); + } else { + messageIds = Array.from( + { length: to - from + 1 }, + (_, i) => from + i + ); + } + + // Fetch messages with their content + for (const id of messageIds) { + try { + const message = await imapClient.fetchOne(id, { + envelope: true, + flags: true, + bodyStructure: true, + internalDate: true, + size: true, + source: true // Include full message source to get content + }); + + if (!message) continue; + + const { envelope, flags, bodyStructure, internalDate, size, source } = message; + + // Extract content from the message source + let content = ''; + if (source) { + const parsedEmail = await simpleParser(source.toString()); + // Get HTML or text content + content = parsedEmail.html || parsedEmail.text || ''; + } + + // Convert attachments to our format + const attachments: Array<{ + contentId?: string; + filename: string; + contentType: string; + size: number; + path: string; + }> = []; + + const processAttachments = (node: any, path: Array = []) => { + if (!node) return; + + if (node.type === 'attachment') { + attachments.push({ + contentId: node.contentId, + filename: node.filename || 'attachment', + contentType: node.contentType, + size: node.size, + path: [...path, node.part].join('.') + }); + } + + if (node.childNodes) { + node.childNodes.forEach((child: any, index: number) => { + processAttachments(child, [...path, node.part || index + 1]); + }); + } + }; + + if (bodyStructure) { + processAttachments(bodyStructure); + } + + // Convert flags from Set to boolean checks + const flagsArray = Array.from(flags as Set); + + emails.push({ + id, + messageId: envelope.messageId, + subject: envelope.subject || "(No Subject)", + from: envelope.from.map((f: any) => ({ + name: f.name || f.address, + address: f.address, + })), + to: envelope.to.map((t: any) => ({ + name: t.name || t.address, + address: t.address, + })), + cc: (envelope.cc || []).map((c: any) => ({ + name: c.name || c.address, + address: c.address, + })), + bcc: (envelope.bcc || []).map((b: any) => ({ + name: b.name || b.address, + address: b.address, + })), + date: internalDate || new Date(), + flags: { + seen: flagsArray.includes("\\Seen"), + flagged: flagsArray.includes("\\Flagged"), + answered: flagsArray.includes("\\Answered"), + deleted: flagsArray.includes("\\Deleted"), + draft: flagsArray.includes("\\Draft"), + }, + hasAttachments: attachments.length > 0, + attachments, + size, + content // Include content directly in email object + }); + } catch (messageError) { + console.error(`Error fetching message ${id}:`, messageError); + } + } + + // Sort by date, newest first + emails.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()); + + const result = { + emails, + totalEmails: totalMessages, + page, + perPage, + totalPages: Math.ceil(totalMessages / perPage), + folder, + mailboxes: await getMailboxes(), }; - - console.log('Response summary:', { - emailCount: result.length, - folderCount: availableFolders.length, - total: mailbox.exists, - hasMore: end < mailbox.exists - }); - + // Cache the result emailListCache[cacheKey] = { - data: responseData, + data: result, timestamp: Date.now() }; - return NextResponse.json(responseData); - } catch (error) { - console.error('Error in IMAP operations:', error); - let errorMessage = 'Failed to fetch emails'; - let statusCode = 500; - - // Type guard for Error objects - if (error instanceof Error) { - errorMessage = error.message; - - // Handle specific error cases - if (errorMessage.includes('authentication') || errorMessage.includes('login')) { - statusCode = 401; - errorMessage = 'Authentication failed. Please check your email credentials.'; - } else if (errorMessage.includes('connect')) { - errorMessage = 'Failed to connect to email server. Please check your settings.'; - } else if (errorMessage.includes('timeout')) { - errorMessage = 'Connection timed out. Please try again later.'; - } - } - - return NextResponse.json( - { error: errorMessage }, - { status: statusCode } - ); + return NextResponse.json(result); } finally { - try { - await client.logout(); - console.log('IMAP client logged out'); - } catch (e) { - console.error('Error during logout:', e); + // If we opened a mailbox, close it + if (mailboxData) { + await imapClient.mailboxClose(); } } - } catch (error) { - console.error('Error in courrier route:', error); + } catch (error: any) { + console.error("Error in GET:", error); return NextResponse.json( - { error: 'An unexpected error occurred' }, + { error: "Internal server error", message: error.message }, { status: 500 } ); } diff --git a/app/courrier/page.tsx b/app/courrier/page.tsx index 7ab20117..dacac128 100644 --- a/app/courrier/page.tsx +++ b/app/courrier/page.tsx @@ -117,17 +117,20 @@ function EmailContent({ email }: { email: Email }) { console.log('Loading content for email:', email.id); console.log('Email content length:', email.content?.length || 0); - if (!email.content) { + // Check if content is available in either content property or body property (for backward compatibility) + const emailContent = email.content || email.body || ''; + + if (!emailContent) { console.log('No content available for email:', email.id); if (mounted) { setContent(
No content available
); - setDebugInfo('Email has no content property'); + setDebugInfo('No content available for this email'); setIsLoading(false); } return; } - const formattedEmail = email.content.trim(); + const formattedEmail = emailContent.trim(); if (!formattedEmail) { console.log('Empty content for email:', email.id); if (mounted) { @@ -187,7 +190,7 @@ function EmailContent({ email }: { email: Email }) { return () => { mounted = false; }; - }, [email?.id, email?.content]); + }, [email?.id, email?.content, email?.body]); if (isLoading) { return (