panel 2 courier api restore

This commit is contained in:
alma 2025-04-25 18:44:28 +02:00
parent 6d319af061
commit f7cf40b0e7
3 changed files with 288 additions and 85 deletions

View File

@ -4,37 +4,57 @@ import { getServerSession } from 'next-auth';
import { authOptions } from '@/app/api/auth/[...nextauth]/route';
import { prisma } from '@/lib/prisma';
// Get the email list cache from main API route
// This is a hack - ideally we'd use a shared module or Redis for caching
declare global {
var emailListCache: { [key: string]: { data: any, timestamp: number } };
}
// Helper function to invalidate cache for a specific folder
const invalidateCache = (userId: string, folder?: string) => {
if (!global.emailListCache) return;
Object.keys(global.emailListCache).forEach(key => {
// If folder is provided, only invalidate that folder's cache
if (folder) {
if (key.includes(`${userId}:${folder}`)) {
delete global.emailListCache[key];
}
} else {
// Otherwise invalidate all user's caches
if (key.startsWith(`${userId}:`)) {
delete global.emailListCache[key];
}
}
});
};
// Mark email as read
export async function POST(
request: Request,
{ params }: { params: { id: string } }
) {
try {
const { id } = await Promise.resolve(params);
// Authentication check
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 }
);
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const emailId = params.id;
if (!emailId) {
return NextResponse.json({ error: 'Email ID is required' }, { status: 400 });
}
// Get credentials from database
const credentials = await prisma.mailCredentials.findUnique({
where: {
userId: session.user.id
}
where: { userId: session.user.id },
});
if (!credentials) {
return NextResponse.json(
{ error: 'No mail credentials found. Please configure your email account.' },
{ status: 401 }
);
return NextResponse.json({ error: 'No mail credentials found' }, { status: 401 });
}
// Create IMAP client
// Connect to IMAP server
const client = new ImapFlow({
host: credentials.host,
port: credentials.port,
@ -49,22 +69,54 @@ export async function POST(
rejectUnauthorized: false
}
});
try {
await client.connect();
// Open INBOX
await client.mailboxOpen('INBOX');
// Find which folder contains this email
const mailboxes = await client.list();
let emailFolder = 'INBOX'; // Default to INBOX
let foundEmail = false;
// Mark the email as read
await client.messageFlagsAdd(id, ['\\Seen'], { uid: true });
// Search through folders to find the email
for (const box of mailboxes) {
try {
await client.mailboxOpen(box.path);
// Search for the email by UID
const message = await client.fetchOne(emailId, { flags: true });
if (message) {
emailFolder = box.path;
foundEmail = true;
// Mark as read if not already
if (!message.flags.has('\\Seen')) {
await client.messageFlagsAdd(emailId, ['\\Seen']);
}
break;
}
} catch (error) {
console.log(`Error searching in folder ${box.path}:`, error);
// Continue with next folder
}
}
if (!foundEmail) {
return NextResponse.json(
{ error: 'Email not found' },
{ status: 404 }
);
}
// Invalidate the cache for this folder
invalidateCache(session.user.id, emailFolder);
return NextResponse.json({ success: true });
} finally {
try {
await client.logout();
} catch (e) {
console.error('Error during IMAP logout:', e);
console.error('Error during logout:', e);
}
}
} catch (error) {

View File

@ -6,7 +6,10 @@ import { prisma } from '@/lib/prisma';
// Email cache structure
interface EmailCache {
[key: string]: any;
[key: string]: {
data: any;
timestamp: number;
};
}
// Credentials cache to reduce database queries
@ -18,7 +21,11 @@ interface CredentialsCache {
}
// In-memory caches with expiration
const emailListCache: EmailCache = {};
// Make emailListCache available globally for other routes
if (!global.emailListCache) {
global.emailListCache = {};
}
const emailListCache: EmailCache = global.emailListCache;
const credentialsCache: CredentialsCache = {};
// Cache TTL in milliseconds (5 minutes)
@ -50,6 +57,29 @@ async function getCredentialsWithCache(userId: string) {
return credentials;
}
// Retry logic for IMAP operations
async function retryOperation<T>(operation: () => Promise<T>, maxAttempts = 3, delay = 1000): Promise<T> {
let lastError: Error;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await operation();
} catch (error) {
lastError = error as Error;
console.warn(`Operation failed (attempt ${attempt}/${maxAttempts}):`, error);
if (attempt < maxAttempts) {
// Exponential backoff
const backoffDelay = delay * Math.pow(2, attempt - 1);
console.log(`Retrying in ${backoffDelay}ms...`);
await new Promise(resolve => setTimeout(resolve, backoffDelay));
}
}
}
throw lastError!;
}
export async function GET(request: Request) {
try {
console.log('Courrier API call received');
@ -161,32 +191,41 @@ export async function GET(request: Request) {
console.log('Fetching messages with options:', fetchOptions);
const fetchPromises = [];
for (let i = adjustedStart; i <= adjustedEnd; i++) {
fetchPromises.push(client.fetchOne(i, fetchOptions));
// Convert to string sequence number as required by ImapFlow
fetchPromises.push(client.fetchOne(`${i}`, fetchOptions));
}
const results = await Promise.all(fetchPromises);
for await (const message of results) {
console.log('Processing message ID:', message.uid);
const emailData: any = {
id: message.uid,
from: message.envelope.from?.[0]?.address || '',
fromName: message.envelope.from?.[0]?.name || message.envelope.from?.[0]?.address?.split('@')[0] || '',
to: message.envelope.to?.map(addr => addr.address).join(', ') || '',
subject: message.envelope.subject || '(No subject)',
date: message.envelope.date?.toISOString() || new Date().toISOString(),
read: message.flags.has('\\Seen'),
starred: message.flags.has('\\Flagged'),
folder: mailbox.path,
hasAttachments: message.bodyStructure?.type === 'multipart',
flags: Array.from(message.flags)
};
try {
const results = await Promise.all(fetchPromises);
// Include preview content if available
if (preview && message.bodyParts && message.bodyParts.has('TEXT')) {
emailData.preview = message.bodyParts.get('TEXT')?.toString() || null;
for (const message of results) {
if (!message) continue; // Skip undefined messages
console.log('Processing message ID:', message.uid);
const emailData: any = {
id: message.uid,
from: message.envelope.from?.[0]?.address || '',
fromName: message.envelope.from?.[0]?.name || message.envelope.from?.[0]?.address?.split('@')[0] || '',
to: message.envelope.to?.map(addr => addr.address).join(', ') || '',
subject: message.envelope.subject || '(No subject)',
date: message.envelope.date?.toISOString() || new Date().toISOString(),
read: message.flags.has('\\Seen'),
starred: message.flags.has('\\Flagged'),
folder: mailbox.path,
hasAttachments: message.bodyStructure?.type === 'multipart',
flags: Array.from(message.flags)
};
// Include preview content if available
if (preview && message.bodyParts && message.bodyParts.has('TEXT')) {
emailData.preview = message.bodyParts.get('TEXT')?.toString() || null;
}
result.push(emailData);
}
result.push(emailData);
} catch (fetchError) {
console.error('Error fetching emails:', fetchError);
// Continue with any successfully fetched messages
}
} else {
console.log('No messages in mailbox');
@ -214,16 +253,35 @@ export async function GET(request: Request) {
return NextResponse.json(responseData);
} catch (error) {
if (error.source === 'timeout') {
// Retry with exponential backoff
return retryOperation(operation, attempt + 1);
} else if (error.source === 'auth') {
// Prompt for credentials refresh
return NextResponse.json({ error: 'Authentication failed', code: 'AUTH_ERROR' });
} else {
// General error handling
console.error('Operation failed:', error);
return NextResponse.json({ error: 'Operation failed', details: error.message });
console.error('Error in IMAP operations:', error);
let errorMessage = 'Failed to fetch emails';
let statusCode = 500;
// Type guard for Error objects
if (error instanceof Error) {
errorMessage = error.message;
// Handle specific error cases
if (errorMessage.includes('authentication') || errorMessage.includes('login')) {
statusCode = 401;
errorMessage = 'Authentication failed. Please check your email credentials.';
} else if (errorMessage.includes('connect')) {
errorMessage = 'Failed to connect to email server. Please check your settings.';
} else if (errorMessage.includes('timeout')) {
errorMessage = 'Connection timed out. Please try again later.';
}
}
return NextResponse.json(
{ error: errorMessage },
{ status: statusCode }
);
} finally {
try {
await client.logout();
console.log('IMAP client logged out');
} catch (e) {
console.error('Error during logout:', e);
}
}
} catch (error) {
@ -235,13 +293,41 @@ export async function GET(request: Request) {
}
}
export async function POST(request: Request, { params }: { params: { id: string } }) {
// Mark as read logic...
// Invalidate cache entries for this folder
Object.keys(emailListCache).forEach(key => {
if (key.includes(`${userId}:${folderName}`)) {
delete emailListCache[key];
export async function POST(request: Request) {
try {
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
});
const { emailId, folderName, action } = await request.json();
if (!emailId) {
return NextResponse.json({ error: 'Missing emailId parameter' }, { status: 400 });
}
// Invalidate cache entries for this folder
const userId = session.user.id;
// If folder is specified, only invalidate that folder's cache
if (folderName) {
Object.keys(emailListCache).forEach(key => {
if (key.includes(`${userId}:${folderName}`)) {
delete emailListCache[key];
}
});
} else {
// Otherwise invalidate all cache entries for this user
Object.keys(emailListCache).forEach(key => {
if (key.startsWith(`${userId}:`)) {
delete emailListCache[key];
}
});
}
return NextResponse.json({ success: true });
} catch (error) {
console.error('Error in POST handler:', error);
return NextResponse.json({ error: 'An unexpected error occurred' }, { status: 500 });
}
}

View File

@ -585,7 +585,7 @@ export default function CourrierPage() {
checkMailCredentials();
}, []);
// Update the loadEmails function with better debugging
// Update your loadEmails function to properly handle folders
const loadEmails = async (isLoadMore = false) => {
try {
// Don't reload if we're already loading
@ -620,8 +620,23 @@ export default function CourrierPage() {
const data = await response.json();
// Get available folders from the API response
if (data.folders) {
if (data.folders && data.folders.length > 0) {
console.log('Setting available folders:', data.folders);
setAvailableFolders(data.folders);
// Generate sidebar items based on folders
const folderSidebarItems = data.folders.map((folderName: string) => ({
label: folderName,
view: folderName,
icon: getFolderIcon(folderName)
}));
// Update the sidebar items
setSidebarItems(prevItems => {
// Keep standard items (first 5 items) and replace folders
const standardItems = initialSidebarItems.slice(0, 5);
return [...standardItems, ...folderSidebarItems];
});
}
// Process emails keeping exact folder names and sort by date
@ -1151,24 +1166,6 @@ export default function CourrierPage() {
</div>
);
// Update sidebar items when available folders change
useEffect(() => {
if (availableFolders.length > 0) {
const newItems = [
...initialSidebarItems,
...availableFolders
.filter(folder => !['INBOX'].includes(folder)) // Exclude folders already in initial items
.map(folder => ({
view: folder as MailFolder,
label: folder.charAt(0).toUpperCase() + folder.slice(1).toLowerCase(),
icon: getFolderIcon(folder),
folder: folder
}))
];
setSidebarItems(newItems);
}
}, [availableFolders]);
// Update the email list item to match header checkbox alignment
const renderEmailListItem = (email: Email) => (
<div
@ -1280,11 +1277,47 @@ export default function CourrierPage() {
}
};
// Add back the renderSidebarNav function
// Render the sidebar navigation
const renderSidebarNav = () => (
<nav className="p-3">
<div className="flex justify-between items-center mb-3 px-2">
<h2 className="text-sm font-semibold">Mail</h2>
<Button
variant="ghost"
size="sm"
onClick={() => {
// Force reload with skipCache=true
fetch(`/api/courrier?folder=${encodeURIComponent(currentView)}&skipCache=true`)
.then(response => response.json())
.then(data => {
if (data.folders && data.folders.length > 0) {
setAvailableFolders(data.folders);
const folderSidebarItems = data.folders.map((folderName: string) => ({
label: folderName,
view: folderName,
icon: getFolderIcon(folderName)
}));
setSidebarItems(prevItems => {
const standardItems = initialSidebarItems.slice(0, 5);
return [...standardItems, ...folderSidebarItems];
});
}
loadEmails();
})
.catch(error => {
console.error('Error refreshing folders:', error);
});
}}
className="h-8 w-8 p-0"
title="Refresh folders"
>
<RefreshCw className="h-4 w-4" />
</Button>
</div>
<ul className="space-y-0.5 px-2">
{sidebarItems.map((item) => (
{/* Standard folder items */}
{initialSidebarItems.slice(0, 5).map((item) => (
<li key={item.view}>
<Button
variant={currentView === item.view ? 'secondary' : 'ghost'}
@ -1310,6 +1343,38 @@ export default function CourrierPage() {
</Button>
</li>
))}
{/* Divider */}
<li className="py-2">
<div className="flex items-center">
<div className="h-px flex-1 bg-gray-200"></div>
<div className="text-xs text-gray-500 px-2">Folders</div>
<div className="h-px flex-1 bg-gray-200"></div>
</div>
</li>
{/* Folder items from server */}
{availableFolders
.filter(folder => !['INBOX', 'Sent', 'Drafts', 'Trash', 'Spam'].includes(folder)) // Filter out standard folders
.map((folder) => (
<li key={folder}>
<Button
variant={currentView === folder ? 'secondary' : 'ghost'}
className={`w-full justify-start py-2 ${
currentView === folder ? 'bg-gray-100 text-gray-900' : 'text-gray-600 hover:text-gray-900'
}`}
onClick={() => {
setCurrentView(folder);
setSelectedEmail(null);
}}
>
<div className="flex items-center">
<Folder className="h-4 w-4 mr-2" />
<span>{folder}</span>
</div>
</Button>
</li>
))}
</ul>
</nav>
);