courrier preview
This commit is contained in:
parent
2eaca502b2
commit
3734a976d2
70
app/api/courrier/[id]/flag/route.ts
Normal file
70
app/api/courrier/[id]/flag/route.ts
Normal file
@ -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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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 }
|
||||
);
|
||||
}
|
||||
|
||||
88
app/api/courrier/emails/route.ts
Normal file
88
app/api/courrier/emails/route.ts
Normal file
@ -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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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<boolean> {
|
||||
// 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
|
||||
*/
|
||||
|
||||
Loading…
Reference in New Issue
Block a user