From f7cf40b0e7748f134c024734a3b959cbb4a4549f Mon Sep 17 00:00:00 2001 From: alma Date: Fri, 25 Apr 2025 18:44:28 +0200 Subject: [PATCH] panel 2 courier api restore --- app/api/courrier/[id]/mark-read/route.ts | 94 ++++++++++--- app/api/courrier/route.ts | 170 +++++++++++++++++------ app/courrier/page.tsx | 109 ++++++++++++--- 3 files changed, 288 insertions(+), 85 deletions(-) diff --git a/app/api/courrier/[id]/mark-read/route.ts b/app/api/courrier/[id]/mark-read/route.ts index 51f3c45d..f1d88c6c 100644 --- a/app/api/courrier/[id]/mark-read/route.ts +++ b/app/api/courrier/[id]/mark-read/route.ts @@ -4,37 +4,57 @@ import { getServerSession } from 'next-auth'; import { authOptions } from '@/app/api/auth/[...nextauth]/route'; import { prisma } from '@/lib/prisma'; +// Get the email list cache from main API route +// This is a hack - ideally we'd use a shared module or Redis for caching +declare global { + var emailListCache: { [key: string]: { data: any, timestamp: number } }; +} + +// Helper function to invalidate cache for a specific folder +const invalidateCache = (userId: string, folder?: string) => { + if (!global.emailListCache) return; + + Object.keys(global.emailListCache).forEach(key => { + // If folder is provided, only invalidate that folder's cache + if (folder) { + if (key.includes(`${userId}:${folder}`)) { + delete global.emailListCache[key]; + } + } else { + // Otherwise invalidate all user's caches + if (key.startsWith(`${userId}:`)) { + delete global.emailListCache[key]; + } + } + }); +}; + +// Mark email as read export async function POST( request: Request, { params }: { params: { id: string } } ) { try { - const { id } = await Promise.resolve(params); - - // Authentication check const session = await getServerSession(authOptions); if (!session?.user?.id) { - return NextResponse.json( - { error: 'Unauthorized' }, - { status: 401 } - ); + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const emailId = params.id; + if (!emailId) { + return NextResponse.json({ error: 'Email ID is required' }, { status: 400 }); } // Get credentials from database const credentials = await prisma.mailCredentials.findUnique({ - where: { - userId: session.user.id - } + where: { userId: session.user.id }, }); if (!credentials) { - return NextResponse.json( - { error: 'No mail credentials found. Please configure your email account.' }, - { status: 401 } - ); + return NextResponse.json({ error: 'No mail credentials found' }, { status: 401 }); } - // Create IMAP client + // Connect to IMAP server const client = new ImapFlow({ host: credentials.host, port: credentials.port, @@ -49,22 +69,54 @@ export async function POST( rejectUnauthorized: false } }); - + try { await client.connect(); - // Open INBOX - await client.mailboxOpen('INBOX'); + // Find which folder contains this email + const mailboxes = await client.list(); + let emailFolder = 'INBOX'; // Default to INBOX + let foundEmail = false; - // Mark the email as read - await client.messageFlagsAdd(id, ['\\Seen'], { uid: true }); + // Search through folders to find the email + for (const box of mailboxes) { + try { + await client.mailboxOpen(box.path); + + // Search for the email by UID + const message = await client.fetchOne(emailId, { flags: true }); + if (message) { + emailFolder = box.path; + foundEmail = true; + + // Mark as read if not already + if (!message.flags.has('\\Seen')) { + await client.messageFlagsAdd(emailId, ['\\Seen']); + } + break; + } + } catch (error) { + console.log(`Error searching in folder ${box.path}:`, error); + // Continue with next folder + } + } + + if (!foundEmail) { + return NextResponse.json( + { error: 'Email not found' }, + { status: 404 } + ); + } + // Invalidate the cache for this folder + invalidateCache(session.user.id, emailFolder); + return NextResponse.json({ success: true }); } finally { try { await client.logout(); } catch (e) { - console.error('Error during IMAP logout:', e); + console.error('Error during logout:', e); } } } catch (error) { diff --git a/app/api/courrier/route.ts b/app/api/courrier/route.ts index e0fe14a0..08ad5eff 100644 --- a/app/api/courrier/route.ts +++ b/app/api/courrier/route.ts @@ -6,7 +6,10 @@ import { prisma } from '@/lib/prisma'; // Email cache structure interface EmailCache { - [key: string]: any; + [key: string]: { + data: any; + timestamp: number; + }; } // Credentials cache to reduce database queries @@ -18,7 +21,11 @@ interface CredentialsCache { } // In-memory caches with expiration -const emailListCache: EmailCache = {}; +// 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) @@ -50,6 +57,29 @@ async function getCredentialsWithCache(userId: string) { return credentials; } +// Retry logic for IMAP operations +async function retryOperation(operation: () => Promise, maxAttempts = 3, delay = 1000): Promise { + let lastError: Error; + + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + try { + return await operation(); + } catch (error) { + lastError = error as Error; + console.warn(`Operation failed (attempt ${attempt}/${maxAttempts}):`, error); + + if (attempt < maxAttempts) { + // Exponential backoff + const backoffDelay = delay * Math.pow(2, attempt - 1); + console.log(`Retrying in ${backoffDelay}ms...`); + await new Promise(resolve => setTimeout(resolve, backoffDelay)); + } + } + } + + throw lastError!; +} + export async function GET(request: Request) { try { console.log('Courrier API call received'); @@ -161,32 +191,41 @@ export async function GET(request: Request) { console.log('Fetching messages with options:', fetchOptions); const fetchPromises = []; for (let i = adjustedStart; i <= adjustedEnd; i++) { - fetchPromises.push(client.fetchOne(i, fetchOptions)); + // Convert to string sequence number as required by ImapFlow + fetchPromises.push(client.fetchOne(`${i}`, fetchOptions)); } - const results = await Promise.all(fetchPromises); - for await (const message of results) { - 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) - }; + try { + const results = await Promise.all(fetchPromises); - // Include preview content if available - if (preview && message.bodyParts && message.bodyParts.has('TEXT')) { - emailData.preview = message.bodyParts.get('TEXT')?.toString() || null; + 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) + }; + + // Include preview content if available + if (preview && message.bodyParts && message.bodyParts.has('TEXT')) { + emailData.preview = message.bodyParts.get('TEXT')?.toString() || null; + } + + result.push(emailData); } - - result.push(emailData); + } catch (fetchError) { + console.error('Error fetching emails:', fetchError); + // Continue with any successfully fetched messages } } else { console.log('No messages in mailbox'); @@ -214,16 +253,35 @@ export async function GET(request: Request) { return NextResponse.json(responseData); } catch (error) { - if (error.source === 'timeout') { - // Retry with exponential backoff - return retryOperation(operation, attempt + 1); - } else if (error.source === 'auth') { - // Prompt for credentials refresh - return NextResponse.json({ error: 'Authentication failed', code: 'AUTH_ERROR' }); - } else { - // General error handling - console.error('Operation failed:', error); - return NextResponse.json({ error: 'Operation failed', details: error.message }); + 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 } + ); + } finally { + try { + await client.logout(); + console.log('IMAP client logged out'); + } catch (e) { + console.error('Error during logout:', e); } } } catch (error) { @@ -235,13 +293,41 @@ export async function GET(request: Request) { } } -export async function POST(request: Request, { params }: { params: { id: string } }) { - // Mark as read logic... - - // Invalidate cache entries for this folder - Object.keys(emailListCache).forEach(key => { - if (key.includes(`${userId}:${folderName}`)) { - delete emailListCache[key]; +export async function POST(request: Request) { + try { + const session = await getServerSession(authOptions); + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } - }); + + const { emailId, folderName, action } = await request.json(); + + if (!emailId) { + return NextResponse.json({ error: 'Missing emailId parameter' }, { status: 400 }); + } + + // Invalidate cache entries for this folder + const userId = session.user.id; + + // If folder is specified, only invalidate that folder's cache + if (folderName) { + Object.keys(emailListCache).forEach(key => { + if (key.includes(`${userId}:${folderName}`)) { + delete emailListCache[key]; + } + }); + } else { + // Otherwise invalidate all cache entries for this user + Object.keys(emailListCache).forEach(key => { + if (key.startsWith(`${userId}:`)) { + delete emailListCache[key]; + } + }); + } + + return NextResponse.json({ success: true }); + } catch (error) { + console.error('Error in POST handler:', error); + return NextResponse.json({ error: 'An unexpected error occurred' }, { status: 500 }); + } } \ No newline at end of file diff --git a/app/courrier/page.tsx b/app/courrier/page.tsx index cd5bf718..72804aac 100644 --- a/app/courrier/page.tsx +++ b/app/courrier/page.tsx @@ -585,7 +585,7 @@ export default function CourrierPage() { checkMailCredentials(); }, []); - // Update the loadEmails function with better debugging + // Update your loadEmails function to properly handle folders const loadEmails = async (isLoadMore = false) => { try { // Don't reload if we're already loading @@ -620,8 +620,23 @@ export default function CourrierPage() { const data = await response.json(); // Get available folders from the API response - if (data.folders) { + if (data.folders && data.folders.length > 0) { + console.log('Setting available folders:', data.folders); setAvailableFolders(data.folders); + + // Generate sidebar items based on folders + const folderSidebarItems = data.folders.map((folderName: string) => ({ + label: folderName, + view: folderName, + icon: getFolderIcon(folderName) + })); + + // Update the sidebar items + setSidebarItems(prevItems => { + // Keep standard items (first 5 items) and replace folders + const standardItems = initialSidebarItems.slice(0, 5); + return [...standardItems, ...folderSidebarItems]; + }); } // Process emails keeping exact folder names and sort by date @@ -1151,24 +1166,6 @@ export default function CourrierPage() { ); - // Update sidebar items when available folders change - useEffect(() => { - if (availableFolders.length > 0) { - const newItems = [ - ...initialSidebarItems, - ...availableFolders - .filter(folder => !['INBOX'].includes(folder)) // Exclude folders already in initial items - .map(folder => ({ - view: folder as MailFolder, - label: folder.charAt(0).toUpperCase() + folder.slice(1).toLowerCase(), - icon: getFolderIcon(folder), - folder: folder - })) - ]; - setSidebarItems(newItems); - } - }, [availableFolders]); - // Update the email list item to match header checkbox alignment const renderEmailListItem = (email: Email) => (
( );