From 7fa9f1ae76aa9471f8d11b15f74004a0f0c83f1a Mon Sep 17 00:00:00 2001 From: alma Date: Sun, 27 Apr 2025 17:01:48 +0200 Subject: [PATCH] courrier multi account --- app/api/courrier/account-folders/route.ts | 160 ++++++++++++++++++++++ app/api/courrier/debug-account/route.ts | 17 ++- app/api/courrier/fix-folders/route.ts | 116 ++++++++++++++++ app/api/redis/status/route.ts | 19 ++- app/components/debug/EmailDebug.tsx | 114 +++++++++++++++ app/components/debug/RedisCacheStatus.tsx | 56 ++++++++ app/courrier/debug-tool.tsx | 10 ++ app/courrier/layout.tsx | 14 ++ 8 files changed, 494 insertions(+), 12 deletions(-) create mode 100644 app/api/courrier/account-folders/route.ts create mode 100644 app/api/courrier/fix-folders/route.ts create mode 100644 app/components/debug/EmailDebug.tsx create mode 100644 app/components/debug/RedisCacheStatus.tsx create mode 100644 app/courrier/debug-tool.tsx create mode 100644 app/courrier/layout.tsx diff --git a/app/api/courrier/account-folders/route.ts b/app/api/courrier/account-folders/route.ts new file mode 100644 index 00000000..564c5a4d --- /dev/null +++ b/app/api/courrier/account-folders/route.ts @@ -0,0 +1,160 @@ +import { NextResponse } from 'next/server'; +import { getServerSession } from 'next-auth'; +import { authOptions } from '@/app/api/auth/[...nextauth]/route'; +import { getMailboxes } from '@/lib/services/email-service'; +import { ImapFlow } from 'imapflow'; +import { prisma } from '@/lib/prisma'; +import { getCachedEmailCredentials } from '@/lib/redis'; + +export async function GET(request: Request) { + // Verify auth + const session = await getServerSession(authOptions); + if (!session?.user?.id) { + return NextResponse.json( + { error: 'Unauthorized' }, + { status: 401 } + ); + } + + const { searchParams } = new URL(request.url); + const accountId = searchParams.get('accountId'); + const userId = session.user.id; + + try { + // If specific accountId is provided, get folders for that account + if (accountId) { + // Get account from database + const account = await prisma.mailCredentials.findFirst({ + where: { + id: accountId, + userId + }, + select: { + email: true, + password: true, + host: true, + port: true + } + }); + + if (!account) { + return NextResponse.json( + { error: 'Account not found' }, + { status: 404 } + ); + } + + // Connect to IMAP server for this account + const client = new ImapFlow({ + host: account.host, + port: account.port, + secure: true, + auth: { + user: account.email, + pass: account.password, + }, + logger: false, + tls: { + rejectUnauthorized: false + } + }); + + try { + await client.connect(); + + // Get folders for this account + const folders = await getMailboxes(client); + + // Close connection + await client.logout(); + + return NextResponse.json({ + accountId, + email: account.email, + folders + }); + } catch (error) { + return NextResponse.json( + { + error: 'Failed to connect to IMAP server', + details: error instanceof Error ? error.message : 'Unknown error' + }, + { status: 500 } + ); + } + } else { + // Get folders for all accounts + const accounts = await prisma.mailCredentials.findMany({ + where: { userId }, + select: { + id: true, + email: true + } + }); + + // For demo purposes, rather than connecting to all accounts, + // get the default set of folders from the first cached account + const credentials = await getCachedEmailCredentials(userId); + + if (!credentials) { + return NextResponse.json({ + accounts: accounts.map(account => ({ + id: account.id, + email: account.email, + folders: ['INBOX', 'Sent', 'Drafts', 'Trash'] // Fallback folders + })) + }); + } + + // Connect to IMAP server + const client = new ImapFlow({ + host: credentials.host, + port: credentials.port, + secure: true, + auth: { + user: credentials.email, + pass: credentials.password, + }, + logger: false, + tls: { + rejectUnauthorized: false + } + }); + + try { + await client.connect(); + + // Get folders + const folders = await getMailboxes(client); + + // Close connection + await client.logout(); + + return NextResponse.json({ + accounts: accounts.map(account => ({ + id: account.id, + email: account.email, + folders + })) + }); + } catch (error) { + return NextResponse.json({ + accounts: accounts.map(account => ({ + id: account.id, + email: account.email, + folders: ['INBOX', 'Sent', 'Drafts', 'Trash'] // Fallback folders on error + })), + error: error instanceof Error ? error.message : 'Unknown error' + }); + } + } + } catch (error) { + return NextResponse.json( + { + error: 'Failed to get account folders', + details: error instanceof Error ? error.message : 'Unknown error' + }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/app/api/courrier/debug-account/route.ts b/app/api/courrier/debug-account/route.ts index 65d17261..2826444f 100644 --- a/app/api/courrier/debug-account/route.ts +++ b/app/api/courrier/debug-account/route.ts @@ -25,7 +25,8 @@ export async function GET() { session: null }, database: { - accounts: [] + accounts: [], + schema: null }, imap: { connectionAttempt: false, @@ -75,6 +76,20 @@ export async function GET() { }; } + // Try to get database schema information to help diagnose issues + try { + const schemaInfo = await prisma.$queryRaw` + SELECT column_name, data_type, is_nullable + FROM information_schema.columns + WHERE table_name = 'MailCredentials' + AND table_schema = 'public' + ORDER BY ordinal_position + `; + debugData.database.schema = schemaInfo; + } catch (e) { + debugData.database.schemaError = e instanceof Error ? e.message : 'Unknown error'; + } + // Check database for accounts try { const accounts = await prisma.mailCredentials.findMany({ diff --git a/app/api/courrier/fix-folders/route.ts b/app/api/courrier/fix-folders/route.ts new file mode 100644 index 00000000..915e0b56 --- /dev/null +++ b/app/api/courrier/fix-folders/route.ts @@ -0,0 +1,116 @@ +import { NextResponse } from 'next/server'; +import { getServerSession } from 'next-auth'; +import { authOptions } from '@/app/api/auth/[...nextauth]/route'; +import { prisma } from '@/lib/prisma'; +import { getMailboxes } from '@/lib/services/email-service'; +import { ImapFlow } from 'imapflow'; +import { cacheImapSession, getCachedImapSession } from '@/lib/redis'; + +export async function POST() { + // Verify auth + const session = await getServerSession(authOptions); + if (!session?.user?.id) { + return NextResponse.json( + { error: 'Unauthorized' }, + { status: 401 } + ); + } + + const userId = session.user.id; + const results = { + success: false, + userId, + accountsProcessed: 0, + foldersFound: 0, + accounts: [] as any[] + }; + + try { + // Get all accounts for this user + const accounts = await prisma.mailCredentials.findMany({ + where: { userId }, + select: { + id: true, + email: true, + password: true, + host: true, + port: true + } + }); + + if (accounts.length === 0) { + return NextResponse.json({ + success: false, + error: 'No email accounts found' + }); + } + + // Process each account + for (const account of accounts) { + try { + // Connect to IMAP server for this account + const client = new ImapFlow({ + host: account.host, + port: account.port, + secure: true, + auth: { + user: account.email, + pass: account.password, + }, + logger: false, + tls: { + rejectUnauthorized: false + } + }); + + await client.connect(); + + // Get folders for this account + const folders = await getMailboxes(client); + + // Store the results + results.accounts.push({ + id: account.id, + email: account.email, + folderCount: folders.length, + folders + }); + + results.foldersFound += folders.length; + results.accountsProcessed++; + + // Get existing session data + const existingSession = await getCachedImapSession(userId); + + // Update the Redis cache with the folders + await cacheImapSession(userId, { + ...(existingSession || { lastActive: Date.now() }), + mailboxes: folders, + lastVisit: Date.now() + }); + + // Close connection + await client.logout(); + } catch (error) { + results.accounts.push({ + id: account.id, + email: account.email, + error: error instanceof Error ? error.message : 'Unknown error' + }); + } + } + + results.success = results.accountsProcessed > 0; + + return NextResponse.json(results); + } catch (error) { + return NextResponse.json( + { + success: false, + error: 'Failed to fix folders', + details: error instanceof Error ? error.message : 'Unknown error' + }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/app/api/redis/status/route.ts b/app/api/redis/status/route.ts index 8818203b..250e4354 100644 --- a/app/api/redis/status/route.ts +++ b/app/api/redis/status/route.ts @@ -1,5 +1,5 @@ import { NextResponse } from 'next/server'; -import { getRedisClient } from '@/lib/redis'; +import { getRedisStatus } from '@/lib/redis'; /** * API route to check Redis connection status @@ -7,21 +7,18 @@ import { getRedisClient } from '@/lib/redis'; */ export async function GET() { try { - const redis = getRedisClient(); - const pong = await redis.ping(); + const status = await getRedisStatus(); return NextResponse.json({ - status: 'connected', - ping: pong, - timestamp: new Date().toISOString() + ready: status.status === 'connected', + status: status.status, + ping: status.ping, + error: status.error }); } catch (error) { - console.error('Redis status check failed:', error); - return NextResponse.json({ - status: 'error', - error: error instanceof Error ? error.message : String(error), - timestamp: new Date().toISOString() + ready: false, + error: error instanceof Error ? error.message : 'Unknown error' }, { status: 500 }); } } \ No newline at end of file diff --git a/app/components/debug/EmailDebug.tsx b/app/components/debug/EmailDebug.tsx new file mode 100644 index 00000000..bd58f0c5 --- /dev/null +++ b/app/components/debug/EmailDebug.tsx @@ -0,0 +1,114 @@ +'use client'; + +import { useState } from 'react'; +import { Button } from '@/components/ui/button'; +import { toast } from '@/components/ui/use-toast'; +import { AlertCircle, Bug, RefreshCw } from 'lucide-react'; + +export function EmailDebug() { + const [loading, setLoading] = useState(false); + + const handleDebug = async () => { + try { + setLoading(true); + const response = await fetch('/api/courrier/debug-account'); + const data = await response.json(); + console.log('Account Debug Data:', data); + + // Show toast with basic info + toast({ + title: "Debug Information", + description: `Found ${data.database.accountCount || 0} accounts and ${data.redis.session?.folderCount || 0} folders`, + duration: 5000 + }); + } catch (error) { + console.error('Debug error:', error); + toast({ + title: "Debug Error", + description: error instanceof Error ? error.message : 'Unknown error', + variant: "destructive", + duration: 5000 + }); + } finally { + setLoading(false); + } + }; + + const handleFixFolders = async () => { + try { + setLoading(true); + toast({ + title: "Fixing Folders", + description: "Connecting to IMAP server to refresh folders...", + duration: 5000 + }); + + const response = await fetch('/api/courrier/fix-folders', { + method: 'POST' + }); + + const data = await response.json(); + console.log('Fix Folders Result:', data); + + if (data.success) { + toast({ + title: "Folders Updated", + description: `Processed ${data.accountsProcessed} accounts and found ${data.foldersFound} folders`, + duration: 5000 + }); + + // Refresh the page to see changes + setTimeout(() => { + window.location.reload(); + }, 2000); + } else { + toast({ + title: "Failed to Update Folders", + description: data.error || 'Unknown error', + variant: "destructive", + duration: 5000 + }); + } + } catch (error) { + console.error('Fix folders error:', error); + toast({ + title: "Folder Update Error", + description: error instanceof Error ? error.message : 'Unknown error', + variant: "destructive", + duration: 5000 + }); + } finally { + setLoading(false); + } + }; + + return ( +
+ + + +
+ ); +} \ No newline at end of file diff --git a/app/components/debug/RedisCacheStatus.tsx b/app/components/debug/RedisCacheStatus.tsx new file mode 100644 index 00000000..98dcf4fe --- /dev/null +++ b/app/components/debug/RedisCacheStatus.tsx @@ -0,0 +1,56 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { toast } from '@/components/ui/use-toast'; + +export function RedisCacheStatus() { + const [status, setStatus] = useState<'loading' | 'connected' | 'error'>('loading'); + + useEffect(() => { + // Don't run in production + if (process.env.NODE_ENV === 'production') { + return; + } + + const checkRedisStatus = async () => { + try { + const response = await fetch('/api/redis/status'); + const data = await response.json(); + + if (data.ready) { + setStatus('connected'); + // Include EmailDebug component dynamically if it exists + const { EmailDebug } = await import('./EmailDebug'); + if (EmailDebug) { + // It's available + } + } else { + setStatus('error'); + toast({ + title: "Redis Connection Issue", + description: "Redis cache is not responding. Email data may be slow to load.", + variant: "destructive", + duration: 5000 + }); + } + } catch (error) { + setStatus('error'); + } + }; + + checkRedisStatus(); + }, []); + + // In development, render a minimal indicator + if (process.env.NODE_ENV !== 'production' && status !== 'loading') { + return ( +
+
+
+ ); + } + + return null; +} \ No newline at end of file diff --git a/app/courrier/debug-tool.tsx b/app/courrier/debug-tool.tsx new file mode 100644 index 00000000..723f5ba2 --- /dev/null +++ b/app/courrier/debug-tool.tsx @@ -0,0 +1,10 @@ +'use client'; + +import { EmailDebug } from '@/components/debug/EmailDebug'; + +export default function EmailDebugTool() { + if (process.env.NODE_ENV === 'production') { + return null; + } + return ; +} \ No newline at end of file diff --git a/app/courrier/layout.tsx b/app/courrier/layout.tsx new file mode 100644 index 00000000..6e6ca215 --- /dev/null +++ b/app/courrier/layout.tsx @@ -0,0 +1,14 @@ +import EmailDebugTool from './debug-tool'; + +export default function CourrierLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + <> + {children} + + + ); +} \ No newline at end of file