From b78590727b91c2154e9e482319b48ec5183c8f46 Mon Sep 17 00:00:00 2001 From: alma Date: Mon, 28 Apr 2025 13:01:01 +0200 Subject: [PATCH] courrier multi account restore compose --- app/api/courrier/session/route.ts | 251 +++++++++++------------------- lib/services/email-service.ts | 41 +++++ 2 files changed, 128 insertions(+), 164 deletions(-) diff --git a/app/api/courrier/session/route.ts b/app/api/courrier/session/route.ts index 9171a932..a7d1686c 100644 --- a/app/api/courrier/session/route.ts +++ b/app/api/courrier/session/route.ts @@ -1,19 +1,28 @@ import { NextResponse } from 'next/server'; import { getServerSession } from 'next-auth'; -import { authOptions } from '@/app/api/auth/[...nextauth]/route'; +import { authOptions } from '@/lib/auth'; import { getUserEmailCredentials, getMailboxes } from '@/lib/services/email-service'; import { prefetchUserEmailData } from '@/lib/services/prefetch-service'; import { getCachedEmailCredentials, getRedisStatus, warmupRedisCache, getCachedImapSession, cacheImapSession } from '@/lib/redis'; import { prisma } from '@/lib/prisma'; import { ImapFlow } from 'imapflow'; +import { getRedisClient } from '@/lib/redis'; +import { getImapConnection } from '@/lib/services/email-service'; +import { MailCredentials } from '@prisma/client'; +import { redis } from '@/lib/redis'; // Keep track of last prefetch time for each user const lastPrefetchMap = new Map(); const PREFETCH_COOLDOWN_MS = 30000; // 30 seconds cooldown between prefetches +// Cache TTL for folders in Redis (5 minutes) +const FOLDERS_CACHE_TTL = 3600; // 1 hour + // Cache to store account folders to avoid repeated calls to the IMAP server -const accountFoldersCache = new Map(); -const FOLDERS_CACHE_TTL = 5 * 60 * 1000; // 5 minute cache +// const accountFoldersCache = new Map(); + +// Redis key for folders cache +const FOLDERS_CACHE_KEY = (userId: string, accountId: string) => `email:folders:${userId}:${accountId}`; /** * Get folders for a specific account @@ -72,172 +81,86 @@ async function getAccountFolders(accountId: string, account: any): Promise PREFETCH_COOLDOWN_MS; - - // Check if we have a cached session - const cachedSession = await getCachedImapSession(userId); - console.log(`[DEBUG] Cached session for user ${userId}:`, - cachedSession ? { - hasMailboxes: Array.isArray(cachedSession.mailboxes), - mailboxCount: cachedSession.mailboxes?.length || 0 - } : 'null'); - - // First, check Redis cache for credentials - this is for backward compatibility - let credentials = await getCachedEmailCredentials(userId); - console.log(`[DEBUG] Cached credentials for user ${userId}:`, - credentials ? { email: credentials.email, hasPassword: !!credentials.password } : 'null'); - let credentialsSource = 'cache'; - - // Now fetch all email accounts for this user - // Query the database directly using Prisma - try { - const allAccounts = await prisma.mailCredentials.findMany({ - where: { userId }, - select: { - id: true, - email: true, - password: true, - host: true, - port: true - } - }); - console.log(`[DEBUG] Found ${allAccounts.length} accounts in database for user ${userId}:`); - allAccounts.forEach((account, idx) => { - console.log(`[DEBUG] Account ${idx + 1}: ID=${account.id}, Email=${account.email}`); - }); - - // Get additional fields that might be in the database but not in the type - const accountsWithMetadata = await Promise.all(allAccounts.map(async (account) => { - // Get the raw account data to access fields not in the type - const rawAccount = await prisma.$queryRaw` - SELECT display_name, color - FROM "MailCredentials" - WHERE id = ${account.id} - `; - - // Cast the raw result to an array and get the first item - const metadata = Array.isArray(rawAccount) ? rawAccount[0] : rawAccount; - - // Get folders for this specific account - const accountFolders = await getAccountFolders(account.id, { - ...account, - ...metadata - }); - - return { - ...account, - display_name: metadata?.display_name || account.email, - color: metadata?.color || "#0082c9", - folders: accountFolders - }; - })); - - console.log(`Found ${allAccounts.length} email accounts for user ${userId}`); - - // If not in cache and no accounts found, check database for single account (backward compatibility) - if (!credentials && allAccounts.length === 0) { - credentials = await getUserEmailCredentials(userId); - credentialsSource = 'database'; - } - - // If no credentials found - if (!credentials && allAccounts.length === 0) { - return NextResponse.json({ - authenticated: true, - hasEmailCredentials: false, - redisStatus, - message: "No email credentials found" - }); - } - - let prefetchStarted = false; - - // Only prefetch if the cooldown period has elapsed - if (shouldPrefetch) { - // Update the last prefetch time - lastPrefetchMap.set(userId, now); - - // Start prefetching email data in the background - // We don't await this to avoid blocking the response - prefetchUserEmailData(userId).catch(err => { - console.error('Background prefetch error:', err); - }); - - prefetchStarted = true; - } else { - console.log(`Skipping prefetch for ${userId}, last prefetch was ${Math.round((now - lastPrefetchTime)/1000)}s ago`); - } - - // Store last visit time in session data - if (cachedSession) { - await cacheImapSession(userId, { - ...cachedSession, - lastActive: cachedSession.lastActive || Date.now(), // Ensure lastActive is set - lastVisit: now - }); - } else { - await cacheImapSession(userId, { - lastActive: Date.now(), - lastVisit: now - }); - } - - // Return all accounts information with their specific folders - return NextResponse.json({ - authenticated: true, - hasEmailCredentials: true, - email: credentials?.email || (allAccounts.length > 0 ? allAccounts[0].email : ''), - redisStatus, - prefetchStarted, - credentialsSource, - lastVisit: cachedSession?.lastVisit, - mailboxes: cachedSession?.mailboxes || [], // For backward compatibility - allAccounts: accountsWithMetadata.map(account => ({ - id: account.id, - email: account.email, - display_name: account.display_name || account.email, - color: account.color || "#0082c9", - folders: account.folders || [] // Use account-specific folders - })) - }); - } catch (dbError) { - console.error(`[ERROR] Database query failed:`, dbError); - return NextResponse.json({ - authenticated: true, - hasEmailCredentials: false, - error: "Database query failed", - details: dbError instanceof Error ? dbError.message : "Unknown database error", - redisStatus - }, { status: 500 }); + // Get Redis connection + const redis = getRedisClient(); + if (!redis) { + return NextResponse.json({ error: 'Redis connection failed' }, { status: 500 }); } + + // Get user with their accounts + const user = await prisma.user.findUnique({ + where: { id: session.user.id }, + include: { mailCredentials: true } + }); + + if (!user) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }); + } + + // Get all accounts for the user + const accounts: MailCredentials[] = user.mailCredentials || []; + if (accounts.length === 0) { + return NextResponse.json({ error: 'No email accounts found' }, { status: 404 }); + } + + // Fetch folders for each account + const accountsWithFolders = await Promise.all( + accounts.map(async (account) => { + const cacheKey = FOLDERS_CACHE_KEY(user.id, account.id); + // Try to get folders from Redis cache first + const cachedFolders = await redis.get(cacheKey); + if (cachedFolders) { + return { + ...account, + folders: JSON.parse(cachedFolders) + }; + } + + // If not in cache, fetch from IMAP + const client = await getImapConnection(account); + if (!client) { + return { + ...account, + folders: ['INBOX', 'Sent', 'Drafts', 'Trash', 'Junk'] + }; + } + + try { + const folders = await getMailboxes(client); + // Cache the folders in Redis + await redis.set( + cacheKey, + JSON.stringify(folders), + 'EX', + FOLDERS_CACHE_TTL + ); + return { + ...account, + folders + }; + } catch (error) { + console.error(`Error fetching folders for account ${account.id}:`, error); + return { + ...account, + folders: ['INBOX', 'Sent', 'Drafts', 'Trash', 'Junk'] + }; + } + }) + ); + + return NextResponse.json({ + accounts: accountsWithFolders + }); } catch (error) { - console.error("Error checking session:", error); - return NextResponse.json({ - authenticated: false, - error: "Internal Server Error" - }, { status: 500 }); + console.error('Error in session route:', error); + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 } + ); } } \ No newline at end of file diff --git a/lib/services/email-service.ts b/lib/services/email-service.ts index 35d95d80..34394e1c 100644 --- a/lib/services/email-service.ts +++ b/lib/services/email-service.ts @@ -700,4 +700,45 @@ export async function sendEmail( error: error instanceof Error ? error.message : 'Unknown error' }; } +} + +/** + * Get list of mailboxes from an IMAP connection + */ +export async function getMailboxes(client: ImapFlow): Promise { + try { + const mailboxes = await client.list(); + + // Map special folders to standard names + const specialFolders = new Map([ + ['Sent Messages', 'Sent'], + ['Sent Items', 'Sent'], + ['Drafts', 'Drafts'], + ['Deleted Items', 'Trash'], + ['Junk Email', 'Junk'], + ['Spam', 'Junk'] + ]); + + // Process mailboxes and map special folders + const processedMailboxes = mailboxes.map(mailbox => { + const path = mailbox.path; + // Check if this is a special folder + for (const [special, standard] of specialFolders) { + if (path.includes(special)) { + return standard; + } + } + return path; + }); + + // Ensure we have all standard folders + const standardFolders = ['INBOX', 'Sent', 'Drafts', 'Trash', 'Junk']; + const uniqueFolders = new Set([...standardFolders, ...processedMailboxes]); + + return Array.from(uniqueFolders); + } catch (error) { + console.error('Error fetching mailboxes:', error); + // Return default folders on error + return ['INBOX', 'Sent', 'Drafts', 'Trash', 'Junk']; + } } \ No newline at end of file