courrier multi account

This commit is contained in:
alma 2025-04-27 17:01:48 +02:00
parent 7276ca8861
commit 7fa9f1ae76
8 changed files with 494 additions and 12 deletions

View File

@ -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 }
);
}
}

View File

@ -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({

View File

@ -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 }
);
}
}

View File

@ -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 });
}
}

View File

@ -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 (
<div className="fixed bottom-4 right-4 z-50 flex gap-2">
<Button
size="sm"
variant="outline"
className="bg-white text-gray-700 hover:text-gray-900 shadow-md"
onClick={handleDebug}
disabled={loading}
>
<Bug className="h-3.5 w-3.5 mr-1" />
Debug
</Button>
<Button
size="sm"
variant="outline"
className="bg-white text-gray-700 hover:text-gray-900 shadow-md"
onClick={handleFixFolders}
disabled={loading}
>
{loading ? (
<RefreshCw className="h-3.5 w-3.5 mr-1 animate-spin" />
) : (
<AlertCircle className="h-3.5 w-3.5 mr-1" />
)}
Fix Folders
</Button>
</div>
);
}

View File

@ -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 (
<div className="fixed top-0 right-0 m-2 z-50">
<div className={`h-2 w-2 rounded-full ${
status === 'connected' ? 'bg-green-500' : 'bg-red-500'
}`}></div>
</div>
);
}
return null;
}

View File

@ -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 <EmailDebug />;
}

14
app/courrier/layout.tsx Normal file
View File

@ -0,0 +1,14 @@
import EmailDebugTool from './debug-tool';
export default function CourrierLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<>
{children}
<EmailDebugTool />
</>
);
}