panel 2 courier api restore

This commit is contained in:
alma 2025-04-25 20:20:24 +02:00
parent 1e4f07058b
commit ec725ae8c1
4 changed files with 324 additions and 217 deletions

View File

@ -35,12 +35,14 @@ export async function POST(
{ params }: { params: { id: string } } { params }: { params: { id: string } }
) { ) {
try { try {
// Get session
const session = await getServerSession(authOptions); const session = await getServerSession(authOptions);
if (!session?.user?.id) { if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
} }
const emailId = params.id; // Properly await the params object before accessing its properties
const { id: emailId } = await Promise.resolve(params);
if (!emailId) { if (!emailId) {
return NextResponse.json({ error: 'Email ID is required' }, { status: 400 }); return NextResponse.json({ error: 'Email ID is required' }, { status: 400 });
} }

View File

@ -1,32 +1,39 @@
import { NextResponse } from 'next/server'; import { NextResponse } from 'next/server';
import { getServerSession } from 'next-auth'; import { getServerSession } from 'next-auth/next';
import { authOptions } from '@/app/api/auth/[...nextauth]/route'; import { authOptions } from '@/app/api/auth/[...nextauth]/route';
import { prisma } from '@/lib/prisma'; import { prisma } from '@/lib/prisma';
export async function GET(request: Request) { export async function GET() {
try { try {
// Verify user is authenticated
const session = await getServerSession(authOptions); const session = await getServerSession(authOptions);
if (!session?.user?.id) { if (!session || !session.user?.id) {
console.log("No authenticated session found");
return NextResponse.json( return NextResponse.json(
{ error: 'Unauthorized' }, { error: "Not authenticated" },
{ status: 401 } { status: 401 }
); );
} }
// Get credentials from database console.log(`Attempting to fetch mail credentials for user ${session.user.id}`);
// Get mail credentials
const credentials = await prisma.mailCredentials.findUnique({ const credentials = await prisma.mailCredentials.findUnique({
where: { userId: session.user.id } where: {
userId: session.user.id,
},
}); });
if (!credentials) { if (!credentials) {
console.log(`No mail credentials found for user ${session.user.id}`);
return NextResponse.json( return NextResponse.json(
{ error: 'No mail credentials found', credentials: null }, { error: "No mail credentials found" },
{ status: 404 } { status: 404 }
); );
} }
// Return only what's needed (especially the email) console.log(`Found credentials for email: ${credentials.email}`);
// Return only necessary credential details
return NextResponse.json({ return NextResponse.json({
credentials: { credentials: {
email: credentials.email, email: credentials.email,
@ -35,9 +42,17 @@ export async function GET(request: Request) {
} }
}); });
} catch (error) { } catch (error) {
console.error('Error fetching credentials:', error); console.error("Error fetching mail credentials:", error);
// Log more detailed error information
if (error instanceof Error) {
console.error("Error name:", error.name);
console.error("Error message:", error.message);
console.error("Error stack:", error.stack);
}
return NextResponse.json( return NextResponse.json(
{ error: 'Failed to fetch credentials' }, { error: "Failed to fetch mail credentials" },
{ status: 500 } { status: 500 }
); );
} }

View File

@ -3,33 +3,25 @@ import { ImapFlow } from 'imapflow';
import { getServerSession } from 'next-auth'; import { getServerSession } from 'next-auth';
import { authOptions } from '@/app/api/auth/[...nextauth]/route'; import { authOptions } from '@/app/api/auth/[...nextauth]/route';
import { prisma } from '@/lib/prisma'; import { prisma } from '@/lib/prisma';
import { simpleParser } from 'mailparser';
// Type definitions
interface EmailCacheEntry {
data: any;
timestamp: number;
}
interface CredentialsCacheEntry {
client: any; // Use any for ImapFlow to avoid type issues
timestamp: number;
}
// Email cache structure // Email cache structure
interface EmailCache { const emailListCache: Record<string, EmailCacheEntry> = {};
[key: string]: { const credentialsCache: Record<string, CredentialsCacheEntry> = {};
data: any;
timestamp: number;
};
}
// Credentials cache to reduce database queries // Cache TTL in milliseconds
interface CredentialsCache { const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
[userId: string]: {
credentials: any;
timestamp: number;
}
}
// In-memory caches with expiration
// 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)
const CACHE_TTL = 5 * 60 * 1000;
// Helper function to get credentials with caching // Helper function to get credentials with caching
async function getCredentialsWithCache(userId: string) { async function getCredentialsWithCache(userId: string) {
@ -38,7 +30,7 @@ async function getCredentialsWithCache(userId: string) {
const now = Date.now(); const now = Date.now();
if (cachedCreds && now - cachedCreds.timestamp < CACHE_TTL) { if (cachedCreds && now - cachedCreds.timestamp < CACHE_TTL) {
return cachedCreds.credentials; return cachedCreds.client;
} }
// Otherwise fetch from database // Otherwise fetch from database
@ -49,12 +41,21 @@ async function getCredentialsWithCache(userId: string) {
// Cache the result // Cache the result
if (credentials) { if (credentials) {
credentialsCache[userId] = { credentialsCache[userId] = {
credentials, client: credentialsCache[userId]?.client || new ImapFlow({
host: credentials.host,
port: credentials.port,
secure: true,
auth: {
user: credentials.email,
pass: credentials.password,
},
logger: false,
}),
timestamp: now timestamp: now
}; };
} }
return credentials; return credentialsCache[userId]?.client || null;
} }
// Retry logic for IMAP operations // Retry logic for IMAP operations
@ -82,203 +83,289 @@ async function retryOperation<T>(operation: () => Promise<T>, maxAttempts = 3, d
export async function GET(request: Request) { export async function GET(request: Request) {
try { try {
console.log('Courrier API call received');
const session = await getServerSession(authOptions); const session = await getServerSession(authOptions);
if (!session?.user?.id) { if (!session || !session.user?.id) {
console.log('No authenticated session found');
return NextResponse.json( return NextResponse.json(
{ error: 'Unauthorized' }, { error: "Not authenticated" },
{ status: 401 }
);
}
console.log('User authenticated:', session.user.id);
// Get URL parameters
const url = new URL(request.url);
const folder = url.searchParams.get('folder') || 'INBOX';
const page = parseInt(url.searchParams.get('page') || '1');
const limit = parseInt(url.searchParams.get('limit') || '20');
const skipCache = url.searchParams.get('skipCache') === 'true';
console.log('Request parameters:', { folder, page, limit, skipCache });
// Generate cache key based on request parameters
const cacheKey = `${session.user.id}:${folder}:${page}:${limit}:full`;
// Check cache first if not explicitly skipped
if (!skipCache && emailListCache[cacheKey]) {
const { data, timestamp } = emailListCache[cacheKey];
// Return cached data if it's fresh (less than 1 minute old)
if (Date.now() - timestamp < 60000) {
console.log('Returning cached email data');
return NextResponse.json(data);
}
}
// Get credentials from cache or database
const credentials = await getCredentialsWithCache(session.user.id);
console.log('Credentials retrieved:', credentials ? 'yes' : 'no');
if (!credentials) {
console.log('No mail credentials found for user');
return NextResponse.json(
{ error: 'No mail credentials found. Please configure your email account.' },
{ status: 401 } { status: 401 }
); );
} }
// Calculate start and end sequence numbers // Get mail credentials
const start = (page - 1) * limit + 1; const credentials = await prisma.mailCredentials.findUnique({
const end = start + limit - 1; where: {
console.log('Fetching emails from range:', { start, end }); userId: session.user.id,
// Connect to IMAP server
console.log('Connecting to IMAP server:', credentials.host, credentials.port);
const client = new ImapFlow({
host: credentials.host,
port: credentials.port,
secure: true,
auth: {
user: credentials.email,
pass: credentials.password,
}, },
logger: false,
emitLogs: false,
tls: {
rejectUnauthorized: false
}
}); });
try { if (!credentials) {
await client.connect(); return NextResponse.json(
console.log('Connected to IMAP server'); { error: "No mail credentials found" },
{ status: 404 }
// Get list of all mailboxes first );
const mailboxes = await client.list(); }
const availableFolders = mailboxes.map(box => box.path);
console.log('Available folders:', availableFolders); const { searchParams } = new URL(request.url);
const page = parseInt(searchParams.get("page") || "1");
// Open the requested mailbox const perPage = parseInt(searchParams.get("perPage") || "20");
console.log('Opening mailbox:', folder); const folder = searchParams.get("folder") || "INBOX";
const mailbox = await client.mailboxOpen(folder); const searchQuery = searchParams.get("search") || "";
console.log('Mailbox stats:', {
exists: mailbox.exists, // Check for entry in emailCache
name: mailbox.path, const cacheKey = `${session.user.id}:${folder}:${page}:${perPage}:${searchQuery}`;
flags: mailbox.flags const cachedEmails = emailListCache[cacheKey];
});
if (cachedEmails) {
const result = []; console.log(`Using cached emails for ${cacheKey}`);
return NextResponse.json(cachedEmails.data);
// Only try to fetch if the mailbox has messages }
if (mailbox.exists > 0) {
// Adjust start and end to be within bounds console.log(`Cache miss for ${cacheKey}, fetching from IMAP`);
const adjustedStart = Math.min(start, mailbox.exists);
const adjustedEnd = Math.min(end, mailbox.exists); // Fetch from IMAP
console.log('Adjusted fetch range:', { adjustedStart, adjustedEnd }); const cacheCredKey = `credentials:${session.user.id}`;
let imapClient: any = credentialsCache[cacheCredKey]?.client || null;
// Fetch both metadata AND full content
const fetchOptions: any = { if (!imapClient) {
envelope: true, // Create IMAP client
flags: true, const connectWithRetry = async (retries = 3, delay = 1000): Promise<any> => {
bodyStructure: true,
source: true // Include full email source
};
console.log('Fetching messages with options:', fetchOptions);
const fetchPromises = [];
for (let i = adjustedStart; i <= adjustedEnd; i++) {
// Convert to string sequence number as required by ImapFlow
fetchPromises.push(client.fetchOne(`${i}`, fetchOptions));
}
try { try {
const results = await Promise.all(fetchPromises); console.log(`Attempting to connect to IMAP server (${credentials.host}:${credentials.port})...`);
const client = new ImapFlow({
for (const message of results) { host: credentials.host,
if (!message) continue; // Skip undefined messages port: credentials.port,
secure: true,
console.log('Processing message ID:', message.uid); auth: {
const emailData: any = { user: credentials.email,
id: message.uid, pass: credentials.password,
from: message.envelope.from?.[0]?.address || '', },
fromName: message.envelope.from?.[0]?.name || message.envelope.from?.[0]?.address?.split('@')[0] || '', logger: false,
to: message.envelope.to?.map(addr => addr.address).join(', ') || '', });
subject: message.envelope.subject || '(No subject)',
date: message.envelope.date?.toISOString() || new Date().toISOString(), await client.connect();
read: message.flags.has('\\Seen'), console.log("Successfully connected to IMAP server");
starred: message.flags.has('\\Flagged'), return client;
folder: mailbox.path, } catch (error) {
hasAttachments: message.bodyStructure?.type === 'multipart', if (retries > 0) {
flags: Array.from(message.flags), console.log(`Connection failed, retrying... (${retries} attempts left)`);
content: message.source?.toString() || '' // Include full email content await new Promise((resolve) => setTimeout(resolve, delay));
}; return connectWithRetry(retries - 1, delay * 1.5);
result.push(emailData);
} }
} catch (fetchError) { throw error;
console.error('Error fetching emails:', fetchError);
// Continue with any successfully fetched messages
} }
} else { };
console.log('No messages in mailbox');
try {
imapClient = await connectWithRetry();
// Cache for future use
credentialsCache[cacheCredKey] = {
client: imapClient,
timestamp: Date.now()
};
} catch (error: any) {
console.error("Failed to connect to IMAP server after retries:", error);
return NextResponse.json(
{ error: "Failed to connect to IMAP server", message: error.message },
{ status: 500 }
);
}
} else {
console.log("Using cached IMAP client connection");
}
// Function to get mailboxes
const getMailboxes = async () => {
const mailboxes = [];
for await (const mailbox of imapClient.listMailboxes()) {
mailboxes.push(mailbox);
}
return mailboxes;
};
// Setup paging
const startIdx = (page - 1) * perPage + 1;
const endIdx = page * perPage;
let emails: any[] = [];
let mailboxData = null;
try {
// Select and lock mailbox
mailboxData = await imapClient.mailboxOpen(folder);
console.log(`Opened mailbox ${folder}, ${mailboxData.exists} messages total`);
// Calculate range based on total messages
const totalMessages = mailboxData.exists;
const from = Math.max(totalMessages - endIdx + 1, 1);
const to = Math.max(totalMessages - startIdx + 1, 1);
// Skip if no messages or invalid range
if (totalMessages === 0 || from > to) {
console.log("No messages in range, returning empty array");
const result = {
emails: [],
totalEmails: 0,
page,
perPage,
totalPages: 0,
folder,
mailboxes: await getMailboxes(),
};
emailListCache[cacheKey] = {
data: result,
timestamp: Date.now()
};
return NextResponse.json(result);
} }
const responseData = { console.log(`Fetching messages ${from}:${to} (page ${page}, ${perPage} per page)`);
emails: result,
folders: availableFolders, // Search if needed
total: mailbox.exists, let messageIds: any[] = [];
hasMore: end < mailbox.exists if (searchQuery) {
console.log(`Searching for: "${searchQuery}"`);
messageIds = await imapClient.search({
body: searchQuery
});
// Filter to our page range
messageIds = messageIds.filter(id => id >= from && id <= to);
console.log(`Found ${messageIds.length} messages matching search`);
} else {
messageIds = Array.from(
{ length: to - from + 1 },
(_, i) => from + i
);
}
// Fetch messages with their content
for (const id of messageIds) {
try {
const message = await imapClient.fetchOne(id, {
envelope: true,
flags: true,
bodyStructure: true,
internalDate: true,
size: true,
source: true // Include full message source to get content
});
if (!message) continue;
const { envelope, flags, bodyStructure, internalDate, size, source } = message;
// Extract content from the message source
let content = '';
if (source) {
const parsedEmail = await simpleParser(source.toString());
// Get HTML or text content
content = parsedEmail.html || parsedEmail.text || '';
}
// Convert attachments to our format
const attachments: Array<{
contentId?: string;
filename: string;
contentType: string;
size: number;
path: string;
}> = [];
const processAttachments = (node: any, path: Array<string | number> = []) => {
if (!node) return;
if (node.type === 'attachment') {
attachments.push({
contentId: node.contentId,
filename: node.filename || 'attachment',
contentType: node.contentType,
size: node.size,
path: [...path, node.part].join('.')
});
}
if (node.childNodes) {
node.childNodes.forEach((child: any, index: number) => {
processAttachments(child, [...path, node.part || index + 1]);
});
}
};
if (bodyStructure) {
processAttachments(bodyStructure);
}
// Convert flags from Set to boolean checks
const flagsArray = Array.from(flags as Set<string>);
emails.push({
id,
messageId: envelope.messageId,
subject: envelope.subject || "(No Subject)",
from: envelope.from.map((f: any) => ({
name: f.name || f.address,
address: f.address,
})),
to: envelope.to.map((t: any) => ({
name: t.name || t.address,
address: t.address,
})),
cc: (envelope.cc || []).map((c: any) => ({
name: c.name || c.address,
address: c.address,
})),
bcc: (envelope.bcc || []).map((b: any) => ({
name: b.name || b.address,
address: b.address,
})),
date: internalDate || new Date(),
flags: {
seen: flagsArray.includes("\\Seen"),
flagged: flagsArray.includes("\\Flagged"),
answered: flagsArray.includes("\\Answered"),
deleted: flagsArray.includes("\\Deleted"),
draft: flagsArray.includes("\\Draft"),
},
hasAttachments: attachments.length > 0,
attachments,
size,
content // Include content directly in email object
});
} catch (messageError) {
console.error(`Error fetching message ${id}:`, messageError);
}
}
// Sort by date, newest first
emails.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
const result = {
emails,
totalEmails: totalMessages,
page,
perPage,
totalPages: Math.ceil(totalMessages / perPage),
folder,
mailboxes: await getMailboxes(),
}; };
console.log('Response summary:', {
emailCount: result.length,
folderCount: availableFolders.length,
total: mailbox.exists,
hasMore: end < mailbox.exists
});
// Cache the result // Cache the result
emailListCache[cacheKey] = { emailListCache[cacheKey] = {
data: responseData, data: result,
timestamp: Date.now() timestamp: Date.now()
}; };
return NextResponse.json(responseData); return NextResponse.json(result);
} catch (error) {
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 { } finally {
try { // If we opened a mailbox, close it
await client.logout(); if (mailboxData) {
console.log('IMAP client logged out'); await imapClient.mailboxClose();
} catch (e) {
console.error('Error during logout:', e);
} }
} }
} catch (error) { } catch (error: any) {
console.error('Error in courrier route:', error); console.error("Error in GET:", error);
return NextResponse.json( return NextResponse.json(
{ error: 'An unexpected error occurred' }, { error: "Internal server error", message: error.message },
{ status: 500 } { status: 500 }
); );
} }

View File

@ -117,17 +117,20 @@ function EmailContent({ email }: { email: Email }) {
console.log('Loading content for email:', email.id); console.log('Loading content for email:', email.id);
console.log('Email content length:', email.content?.length || 0); console.log('Email content length:', email.content?.length || 0);
if (!email.content) { // Check if content is available in either content property or body property (for backward compatibility)
const emailContent = email.content || email.body || '';
if (!emailContent) {
console.log('No content available for email:', email.id); console.log('No content available for email:', email.id);
if (mounted) { if (mounted) {
setContent(<div className="text-gray-500">No content available</div>); setContent(<div className="text-gray-500">No content available</div>);
setDebugInfo('Email has no content property'); setDebugInfo('No content available for this email');
setIsLoading(false); setIsLoading(false);
} }
return; return;
} }
const formattedEmail = email.content.trim(); const formattedEmail = emailContent.trim();
if (!formattedEmail) { if (!formattedEmail) {
console.log('Empty content for email:', email.id); console.log('Empty content for email:', email.id);
if (mounted) { if (mounted) {
@ -187,7 +190,7 @@ function EmailContent({ email }: { email: Email }) {
return () => { return () => {
mounted = false; mounted = false;
}; };
}, [email?.id, email?.content]); }, [email?.id, email?.content, email?.body]);
if (isLoading) { if (isLoading) {
return ( return (