From 3734a976d2b53cdd2e857d9cca4f5da33c0bab00 Mon Sep 17 00:00:00 2001 From: alma Date: Thu, 1 May 2025 21:42:50 +0200 Subject: [PATCH] courrier preview --- app/api/courrier/[id]/flag/route.ts | 70 +++++++++++++++++++ app/api/courrier/[id]/mark-read/route.ts | 60 +++++++++------- app/api/courrier/emails/route.ts | 88 ++++++++++++++++++++++++ hooks/use-email-state.ts | 4 +- lib/services/email-service.ts | 52 +++++++++++++- 5 files changed, 248 insertions(+), 26 deletions(-) create mode 100644 app/api/courrier/[id]/flag/route.ts create mode 100644 app/api/courrier/emails/route.ts diff --git a/app/api/courrier/[id]/flag/route.ts b/app/api/courrier/[id]/flag/route.ts new file mode 100644 index 00000000..1b38c675 --- /dev/null +++ b/app/api/courrier/[id]/flag/route.ts @@ -0,0 +1,70 @@ +import { NextResponse } from 'next/server'; +import { getServerSession } from 'next-auth'; +import { authOptions } from '@/app/api/auth/[...nextauth]/route'; +import { toggleEmailFlag } from '@/lib/services/email-service'; +import { invalidateEmailContentCache, invalidateFolderCache } from '@/lib/redis'; + +export async function POST( + request: Request, + context: { params: { id: string } } +) { + try { + const session = await getServerSession(authOptions); + if (!session || !session.user?.id) { + return NextResponse.json( + { error: "Not authenticated" }, + { status: 401 } + ); + } + + // Await params as per Next.js requirements + const params = await context.params; + const id = params?.id; + if (!id) { + return NextResponse.json( + { error: "Missing email ID" }, + { status: 400 } + ); + } + + const { flagged, folder, accountId } = await request.json(); + + if (typeof flagged !== 'boolean') { + return NextResponse.json( + { error: "Invalid 'flagged' parameter. Must be a boolean." }, + { status: 400 } + ); + } + + const normalizedFolder = folder || "INBOX"; + const effectiveAccountId = accountId || 'default'; + + // Use the email service to toggle the flag + // Note: You'll need to implement this function in email-service.ts + const success = await toggleEmailFlag( + session.user.id, + id, + flagged, + normalizedFolder, + effectiveAccountId + ); + + if (!success) { + return NextResponse.json( + { error: `Failed to ${flagged ? 'star' : 'unstar'} email` }, + { status: 500 } + ); + } + + // Invalidate cache for this email + await invalidateEmailContentCache(session.user.id, effectiveAccountId, id); + + return NextResponse.json({ success: true }); + } catch (error: any) { + console.error("Error in flag API:", error); + return NextResponse.json( + { error: "Internal server error", message: error.message }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/app/api/courrier/[id]/mark-read/route.ts b/app/api/courrier/[id]/mark-read/route.ts index 87cf95e8..a6bb7b1f 100644 --- a/app/api/courrier/[id]/mark-read/route.ts +++ b/app/api/courrier/[id]/mark-read/route.ts @@ -1,7 +1,8 @@ import { NextResponse } from 'next/server'; import { getServerSession } from 'next-auth'; -import { authOptions } from '@/lib/auth'; +import { authOptions } from '@/app/api/auth/[...nextauth]/route'; import { markEmailReadStatus } from '@/lib/services/email-service'; +import { invalidateEmailContentCache, invalidateFolderCache } from '@/lib/redis'; // Global cache reference (will be moved to a proper cache solution in the future) declare global { @@ -30,36 +31,43 @@ const invalidateCache = (userId: string, folder?: string) => { // Mark email as read export async function POST( request: Request, - { params }: { params: { id: string } } + context: { params: { id: string } } ) { try { const session = await getServerSession(authOptions); - if (!session?.user?.id) { - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + if (!session || !session.user?.id) { + return NextResponse.json( + { error: "Not authenticated" }, + { status: 401 } + ); } - const { id: emailId } = params; - if (!emailId) { - return NextResponse.json({ error: 'Email ID is required' }, { status: 400 }); + // Await params as per Next.js requirements + const params = await context.params; + const id = params?.id; + if (!id) { + return NextResponse.json( + { error: "Missing email ID" }, + { status: 400 } + ); } - const { folder = 'INBOX', accountId, isRead = true } = await request.json(); - - // Extract account ID from folder name if present and none was explicitly provided - const folderAccountId = folder.includes(':') ? folder.split(':')[0] : accountId; + const { isRead, folder, accountId } = await request.json(); - // Use the most specific account ID available - const effectiveAccountId = folderAccountId || accountId || 'default'; - - // Normalize folder name by removing account prefix if present - const normalizedFolder = folder.includes(':') ? folder.split(':')[1] : folder; + if (typeof isRead !== 'boolean') { + return NextResponse.json( + { error: "Invalid 'isRead' parameter. Must be a boolean." }, + { status: 400 } + ); + } - // Log operation details for debugging - console.log(`Marking email ${emailId} as ${isRead ? 'read' : 'unread'} in folder ${normalizedFolder} for account ${effectiveAccountId}`); + const normalizedFolder = folder || "INBOX"; + const effectiveAccountId = accountId || 'default'; + // Use the email service to mark the email const success = await markEmailReadStatus( session.user.id, - emailId, + id, isRead, normalizedFolder, effectiveAccountId @@ -67,16 +75,22 @@ export async function POST( if (!success) { return NextResponse.json( - { error: 'Failed to mark email as read' }, + { error: `Failed to ${isRead ? 'mark email as read' : 'mark email as unread'}` }, { status: 500 } ); } + // Invalidate cache for this email + await invalidateEmailContentCache(session.user.id, effectiveAccountId, id); + + // Also invalidate folder cache to update unread counts + await invalidateFolderCache(session.user.id, effectiveAccountId, normalizedFolder); + return NextResponse.json({ success: true }); - } catch (error) { - console.error('Error marking email as read:', error); + } catch (error: any) { + console.error("Error in mark-read API:", error); return NextResponse.json( - { error: 'Internal server error' }, + { error: "Internal server error", message: error.message }, { status: 500 } ); } diff --git a/app/api/courrier/emails/route.ts b/app/api/courrier/emails/route.ts new file mode 100644 index 00000000..6de19871 --- /dev/null +++ b/app/api/courrier/emails/route.ts @@ -0,0 +1,88 @@ +import { NextResponse } from 'next/server'; +import { getServerSession } from 'next-auth'; +import { authOptions } from '@/app/api/auth/[...nextauth]/route'; +import { getEmails } from '@/lib/services/email-service'; +import { + getCachedEmailList, + cacheEmailList, + invalidateFolderCache +} from '@/lib/redis'; + +export async function GET(request: Request) { + try { + // Authenticate user + const session = await getServerSession(authOptions); + if (!session || !session.user?.id) { + return NextResponse.json( + { error: "Not authenticated" }, + { status: 401 } + ); + } + + // Extract query parameters + 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") || ""; + const accountId = searchParams.get("accountId") || ""; + const checkOnly = searchParams.get("checkOnly") === "true"; + + // Log exact parameters received by the API + console.log(`[API/emails] Received request with: folder=${folder}, accountId=${accountId}, page=${page}, checkOnly=${checkOnly}`); + + // Parameter normalization + // If folder contains an account prefix, extract it but DO NOT use it + // Always prioritize the explicit accountId parameter + let normalizedFolder = folder; + let effectiveAccountId = accountId || 'default'; + + if (folder.includes(':')) { + const parts = folder.split(':'); + normalizedFolder = parts[1]; + + console.log(`[API/emails] Folder has prefix, normalized to ${normalizedFolder}`); + } + + console.log(`[API/emails] Using normalized parameters: folder=${normalizedFolder}, accountId=${effectiveAccountId}`); + + // Try to get from Redis cache first, but only if it's not a search query and not checkOnly + if (!searchQuery && !checkOnly) { + console.log(`[API/emails] Checking Redis cache for ${session.user.id}:${effectiveAccountId}:${normalizedFolder}:${page}:${perPage}`); + const cachedEmails = await getCachedEmailList( + session.user.id, + effectiveAccountId, + normalizedFolder, + page, + perPage + ); + if (cachedEmails) { + console.log(`[API/emails] Using Redis cached emails for ${session.user.id}:${effectiveAccountId}:${normalizedFolder}:${page}:${perPage}`); + return NextResponse.json(cachedEmails); + } + } + + console.log(`[API/emails] Redis cache miss for ${session.user.id}:${effectiveAccountId}:${normalizedFolder}:${page}:${perPage}, fetching emails from IMAP`); + + // Use the email service to fetch emails + const emailsResult = await getEmails( + session.user.id, + normalizedFolder, + page, + perPage, + effectiveAccountId, + checkOnly + ); + + console.log(`[API/emails] Successfully fetched ${emailsResult.emails.length} emails from IMAP for account ${effectiveAccountId}`); + + // Return result + return NextResponse.json(emailsResult); + } catch (error: any) { + console.error("[API/emails] Error fetching emails:", error); + return NextResponse.json( + { error: "Failed to fetch emails", message: error.message }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/hooks/use-email-state.ts b/hooks/use-email-state.ts index a3cd438b..f22b07af 100644 --- a/hooks/use-email-state.ts +++ b/hooks/use-email-state.ts @@ -149,8 +149,8 @@ export const useEmailState = () => { // Fetch emails from API if no cache hit logEmailOp('API_FETCH', `Fetching emails from API: ${queryParams.toString()}, isLoadMore: ${isLoadMore}`); - console.log(`[DEBUG-API_FETCH] Fetching from /api/courrier?${queryParams.toString()}`); - const response = await fetch(`/api/courrier?${queryParams.toString()}`); + console.log(`[DEBUG-API_FETCH] Fetching from /api/courrier/emails?${queryParams.toString()}`); + const response = await fetch(`/api/courrier/emails?${queryParams.toString()}`); if (!response.ok) { // CRITICAL FIX: Try to recover from fetch errors by retrying with different pagination diff --git a/lib/services/email-service.ts b/lib/services/email-service.ts index 8ce2fc66..159ec43e 100644 --- a/lib/services/email-service.ts +++ b/lib/services/email-service.ts @@ -618,7 +618,9 @@ export async function getEmails( accountId: resolvedAccountId, content: { text: '', - html: '' + html: '', + isHtml: false, + direction: 'ltr' } }; emails.push(email); @@ -885,6 +887,54 @@ export async function markEmailReadStatus( } } +/** + * Toggle an email's flagged (starred) status + */ +export async function toggleEmailFlag( + userId: string, + emailId: string, + flagged: boolean, + folder: string = 'INBOX', + accountId?: string +): Promise { + // Extract account ID from folder name if present and none was explicitly provided + const folderAccountId = folder.includes(':') ? folder.split(':')[0] : accountId; + + // Use the most specific account ID available + const effectiveAccountId = folderAccountId || accountId || 'default'; + + // Normalize folder name by removing account prefix if present + const normalizedFolder = folder.includes(':') ? folder.split(':')[1] : folder; + + console.log(`[toggleEmailFlag] Marking email ${emailId} as ${flagged ? 'flagged' : 'unflagged'} in folder ${normalizedFolder}, account ${effectiveAccountId}`); + + const client = await getImapConnection(userId, effectiveAccountId); + + try { + await client.mailboxOpen(normalizedFolder); + + if (flagged) { + await client.messageFlagsAdd(emailId, ['\\Flagged']); + } else { + await client.messageFlagsRemove(emailId, ['\\Flagged']); + } + + // Invalidate content cache since the flags changed + await invalidateEmailContentCache(userId, effectiveAccountId, emailId); + + return true; + } catch (error) { + console.error(`Error toggling flag for email ${emailId} in folder ${normalizedFolder}, account ${effectiveAccountId}:`, error); + return false; + } finally { + try { + await client.mailboxClose(); + } catch (error) { + console.error('Error closing mailbox:', error); + } + } +} + /** * Send an email */