diff --git a/app/api/courrier/debug-account/route.ts b/app/api/courrier/debug-account/route.ts new file mode 100644 index 00000000..65d17261 --- /dev/null +++ b/app/api/courrier/debug-account/route.ts @@ -0,0 +1,167 @@ +import { NextResponse } from 'next/server'; +import { getServerSession } from 'next-auth'; +import { authOptions } from '@/app/api/auth/[...nextauth]/route'; +import { getCachedEmailCredentials, getCachedImapSession } from '@/lib/redis'; +import { prisma } from '@/lib/prisma'; +import { getMailboxes } from '@/lib/services/email-service'; +import { ImapFlow } from 'imapflow'; + +export async function GET() { + // Verify auth + const session = await getServerSession(authOptions); + if (!session?.user?.id) { + return NextResponse.json( + { error: 'Unauthorized' }, + { status: 401 } + ); + } + + const userId = session.user.id; + const debugData: any = { + userId, + timestamp: new Date().toISOString(), + redis: { + emailCredentials: null, + session: null + }, + database: { + accounts: [] + }, + imap: { + connectionAttempt: false, + connected: false, + folders: [] + } + }; + + // Check Redis cache for credentials + try { + const credentials = await getCachedEmailCredentials(userId); + if (credentials) { + debugData.redis.emailCredentials = { + found: true, + email: credentials.email, + host: credentials.host, + port: credentials.port, + hasPassword: !!credentials.password, + hasSmtp: !!credentials.smtp_host + }; + } else { + debugData.redis.emailCredentials = { found: false }; + } + } catch (e) { + debugData.redis.emailCredentials = { + error: e instanceof Error ? e.message : 'Unknown error' + }; + } + + // Check Redis for session data (which contains folders) + try { + const sessionData = await getCachedImapSession(userId); + if (sessionData) { + debugData.redis.session = { + found: true, + lastActive: new Date(sessionData.lastActive).toISOString(), + hasFolders: !!sessionData.mailboxes, + folderCount: sessionData.mailboxes?.length || 0, + folders: sessionData.mailboxes || [] + }; + } else { + debugData.redis.session = { found: false }; + } + } catch (e) { + debugData.redis.session = { + error: e instanceof Error ? e.message : 'Unknown error' + }; + } + + // Check database for accounts + try { + const accounts = await prisma.mailCredentials.findMany({ + where: { userId }, + select: { + id: true, + email: true, + host: true, + port: true + } + }); + + // Also try to get additional fields from raw query + const accountsWithMetadata = await Promise.all(accounts.map(async (account) => { + try { + const rawAccount = await prisma.$queryRaw` + SELECT display_name, color, smtp_host, smtp_port, smtp_secure, secure + FROM "MailCredentials" + WHERE id = ${account.id} + `; + + const metadata = Array.isArray(rawAccount) && rawAccount.length > 0 + ? rawAccount[0] + : {}; + + return { + ...account, + display_name: metadata.display_name, + color: metadata.color, + smtp_host: metadata.smtp_host, + smtp_port: metadata.smtp_port, + smtp_secure: metadata.smtp_secure, + secure: metadata.secure + }; + } catch (e) { + return { + ...account, + _queryError: e instanceof Error ? e.message : 'Unknown error' + }; + } + })); + + debugData.database.accounts = accountsWithMetadata; + debugData.database.accountCount = accounts.length; + } catch (e) { + debugData.database.error = e instanceof Error ? e.message : 'Unknown error'; + } + + // Try to get IMAP folders for the main account + if (debugData.redis.emailCredentials?.found || debugData.database.accountCount > 0) { + try { + debugData.imap.connectionAttempt = true; + + // Use cached credentials + const credentials = await getCachedEmailCredentials(userId); + + if (credentials && credentials.email && credentials.password) { + const client = new ImapFlow({ + host: credentials.host, + port: credentials.port, + secure: true, + auth: { + user: credentials.email, + pass: credentials.password, + }, + logger: false, + tls: { + rejectUnauthorized: false + } + }); + + await client.connect(); + debugData.imap.connected = true; + + // Get folders + const folders = await getMailboxes(client); + debugData.imap.folders = folders; + + // Close connection + await client.logout(); + } else { + debugData.imap.error = "No valid credentials found"; + } + } catch (e) { + debugData.imap.error = e instanceof Error ? e.message : 'Unknown error'; + } + } + + return NextResponse.json(debugData); +} \ No newline at end of file diff --git a/app/api/courrier/session/route.ts b/app/api/courrier/session/route.ts index 25209014..73227d91 100644 --- a/app/api/courrier/session/route.ts +++ b/app/api/courrier/session/route.ts @@ -4,6 +4,7 @@ import { authOptions } from '@/app/api/auth/[...nextauth]/route'; import { getUserEmailCredentials } 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'; // Keep track of last prefetch time for each user const lastPrefetchMap = new Map(); @@ -42,66 +43,131 @@ export async function GET() { // 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 + // 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'; - // If not in cache, check database - if (!credentials) { - credentials = await getUserEmailCredentials(userId); - credentialsSource = 'database'; - } + // 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, + 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}`); + }); - // If no credentials found - if (!credentials) { + // 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; + + return { + ...account, + display_name: metadata?.display_name || account.email, + color: metadata?.color || "#0082c9" + }; + })); + + 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 + 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 || [], + allAccounts: accountsWithMetadata.map(account => ({ + id: account.id, + email: account.email, + display_name: account.display_name || account.email, + color: account.color || "#0082c9" + })) + }); + } catch (dbError) { + console.error(`[ERROR] Database query failed:`, dbError); return NextResponse.json({ authenticated: true, hasEmailCredentials: false, - redisStatus, - message: "No email credentials found" - }); + error: "Database query failed", + details: dbError instanceof Error ? dbError.message : "Unknown database error", + redisStatus + }, { status: 500 }); } - - 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, - lastVisit: now - }); - } else { - await cacheImapSession(userId, { lastVisit: now }); - } - - // Return session info without sensitive data - return NextResponse.json({ - authenticated: true, - hasEmailCredentials: true, - email: credentials.email, - redisStatus, - prefetchStarted, - credentialsSource, - lastVisit: cachedSession?.lastVisit, - mailboxes: cachedSession?.mailboxes || [] - }); } catch (error) { console.error("Error checking session:", error); return NextResponse.json({ diff --git a/app/courrier/page.tsx b/app/courrier/page.tsx index 6af03e2c..89531ae5 100644 --- a/app/courrier/page.tsx +++ b/app/courrier/page.tsx @@ -127,22 +127,46 @@ export default function CourrierPage() { // Update account folders when mailboxes change useEffect(() => { + console.log('Mailboxes updated:', mailboxes); setAccounts(prev => { const updated = [...prev]; if (updated[1]) { updated[1].folders = mailboxes; } + console.log('Updated accounts with new mailboxes:', updated); return updated; }); }, [mailboxes]); + // Debug accounts state + useEffect(() => { + console.log('Current accounts state:', accounts); + }, [accounts]); + // Calculate unread count (this would be replaced with actual data in production) useEffect(() => { // Example: counting unread emails in the inbox - const unreadInInbox = (emails || []).filter(email => !email.read && currentFolder === 'INBOX').length; + const unreadInInbox = (emails || []).filter(email => { + // Access the 'read' property safely, handling both old and new email formats + return (!email.read && email.read !== undefined) || + (email.flags && !email.flags.seen) || + false; + }).filter(email => currentFolder === 'INBOX').length; setUnreadCount(unreadInInbox); }, [emails, currentFolder]); + // Ensure accounts section is never empty + useEffect(() => { + // If accounts array becomes empty (bug), restore default accounts + if (!accounts || accounts.length === 0) { + console.warn('Accounts array is empty, restoring defaults'); + setAccounts([ + { id: 0, name: 'All', email: '', color: 'bg-gray-500' }, + { id: 1, name: 'Loading...', email: '', color: 'bg-blue-500', folders: mailboxes } + ]); + } + }, [accounts, mailboxes]); + // Initialize session and start prefetching useEffect(() => { // Flag to prevent multiple initialization attempts @@ -167,6 +191,18 @@ export default function CourrierPage() { const response = await fetch('/api/courrier/session'); const data = await response.json(); + console.log('[DEBUG] Session API response:', { + authenticated: data.authenticated, + hasEmailCredentials: data.hasEmailCredentials, + email: data.email, + allAccountsExists: !!data.allAccounts, + allAccountsIsArray: Array.isArray(data.allAccounts), + allAccountsLength: data.allAccounts?.length || 0, + mailboxesExists: !!data.mailboxes, + mailboxesIsArray: Array.isArray(data.mailboxes), + mailboxesLength: data.mailboxes?.length || 0 + }); + if (!isMounted) return; if (data.authenticated) { @@ -174,20 +210,45 @@ export default function CourrierPage() { console.log('Session initialized, prefetch status:', data.prefetchStarted ? 'running' : 'not started'); setPrefetchStarted(Boolean(data.prefetchStarted)); - // Update the accounts with the actual email address - if (data.email) { - setAccounts(prev => { - const updated = [...prev]; - updated[1] = { - ...updated[1], - name: data.email, - email: data.email, - folders: data.mailboxes || mailboxes + // Update accounts with the default email as fallback + const updatedAccounts = [ + { id: 0, name: 'All', email: '', color: 'bg-gray-500' } + ]; + + // Check if we have multiple accounts returned + if (data.allAccounts && Array.isArray(data.allAccounts) && data.allAccounts.length > 0) { + console.log('Multiple accounts found:', data.allAccounts.length); + + // Add each account from the server + data.allAccounts.forEach((account, index) => { + console.log(`[DEBUG] Processing account: ${account.email}, display_name: ${account.display_name}, has folders: ${!!data.mailboxes}`); + + const accountWithFolders = { + id: account.id || index + 1, + name: account.display_name || account.email, + email: account.email, + color: account.color || 'bg-blue-500', + folders: data.mailboxes || [] }; - return updated; + console.log(`[DEBUG] Adding account with ${accountWithFolders.folders.length} folders:`, accountWithFolders.folders); + updatedAccounts.push(accountWithFolders); + }); + } else if (data.email) { + // Fallback to single account if allAccounts is not available + console.log(`[DEBUG] Fallback to single account: ${data.email}`); + + updatedAccounts.push({ + id: 1, + name: data.email, + email: data.email, + color: 'bg-blue-500', + folders: data.mailboxes || [] }); } + console.log('Setting accounts:', updatedAccounts); + setAccounts(updatedAccounts); + // Preload first page of emails for faster initial rendering if (session?.user?.id) { await loadEmails(); diff --git a/db_query.sql b/db_query.sql new file mode 100644 index 00000000..7455b4e8 --- /dev/null +++ b/db_query.sql @@ -0,0 +1 @@ +SELECT * FROM "MailCredentials" LIMIT 5; diff --git a/lib/services/email-service.ts b/lib/services/email-service.ts index 3228a4c8..01ab7bdd 100644 --- a/lib/services/email-service.ts +++ b/lib/services/email-service.ts @@ -248,8 +248,26 @@ export async function getEmails( console.log(`Cache miss for emails ${userId}:${folder}:${page}:${perPage}, fetching from IMAP`); const client = await getImapConnection(userId); + let mailboxes: string[] = []; try { + console.log(`[DEBUG] Fetching mailboxes for user ${userId}`); + // Get list of mailboxes first + try { + mailboxes = await getMailboxes(client); + console.log(`[DEBUG] Found ${mailboxes.length} mailboxes:`, mailboxes); + + // Save mailboxes in session data + const cachedSession = await getCachedImapSession(userId); + await cacheImapSession(userId, { + ...(cachedSession || { lastActive: Date.now() }), + mailboxes + }); + console.log(`[DEBUG] Updated cached session with mailboxes for user ${userId}`); + } catch (mailboxError) { + console.error(`[ERROR] Failed to fetch mailboxes:`, mailboxError); + } + // Open mailbox const mailboxData = await client.mailboxOpen(folder); const totalMessages = mailboxData.exists; @@ -262,14 +280,6 @@ export async function getEmails( // Empty result if no messages if (totalMessages === 0 || from > to) { - const mailboxes = await getMailboxes(client); - - // Cache mailbox list in session data - await cacheImapSession(userId, { - lastActive: Date.now(), - mailboxes - }); - const result = { emails: [], totalEmails: 0, @@ -414,14 +424,6 @@ export async function getEmails( // Sort by date, newest first emails.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()); - const mailboxes = await getMailboxes(client); - - // Cache mailbox list in session data - await cacheImapSession(userId, { - lastActive: Date.now(), - mailboxes - }); - const result = { emails, totalEmails: totalMessages, diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 71de85f9..e918a760 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -19,7 +19,7 @@ model User { updatedAt DateTime @updatedAt calendars Calendar[] events Event[] - mailCredentials MailCredentials? + mailCredentials MailCredentials[] webdavCredentials WebDAVCredentials? } @@ -58,7 +58,7 @@ model Event { model MailCredentials { id String @id @default(uuid()) - userId String @unique + userId String email String password String host String