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 } }
) {
try {
// Get session
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
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) {
return NextResponse.json({ error: 'Email ID is required' }, { status: 400 });
}

View File

@ -1,32 +1,39 @@
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 { prisma } from '@/lib/prisma';
export async function GET(request: Request) {
export async function GET() {
try {
// Verify user is authenticated
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
if (!session || !session.user?.id) {
console.log("No authenticated session found");
return NextResponse.json(
{ error: 'Unauthorized' },
{ error: "Not authenticated" },
{ 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({
where: { userId: session.user.id }
where: {
userId: session.user.id,
},
});
if (!credentials) {
console.log(`No mail credentials found for user ${session.user.id}`);
return NextResponse.json(
{ error: 'No mail credentials found', credentials: null },
{ error: "No mail credentials found" },
{ 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({
credentials: {
email: credentials.email,
@ -35,9 +42,17 @@ export async function GET(request: Request) {
}
});
} 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(
{ error: 'Failed to fetch credentials' },
{ error: "Failed to fetch mail credentials" },
{ status: 500 }
);
}

View File

@ -3,33 +3,25 @@ import { ImapFlow } from 'imapflow';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/app/api/auth/[...nextauth]/route';
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
interface EmailCache {
[key: string]: {
data: any;
timestamp: number;
};
}
const emailListCache: Record<string, EmailCacheEntry> = {};
const credentialsCache: Record<string, CredentialsCacheEntry> = {};
// Credentials cache to reduce database queries
interface CredentialsCache {
[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;
// Cache TTL in milliseconds
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
// Helper function to get credentials with caching
async function getCredentialsWithCache(userId: string) {
@ -38,7 +30,7 @@ async function getCredentialsWithCache(userId: string) {
const now = Date.now();
if (cachedCreds && now - cachedCreds.timestamp < CACHE_TTL) {
return cachedCreds.credentials;
return cachedCreds.client;
}
// Otherwise fetch from database
@ -49,12 +41,21 @@ async function getCredentialsWithCache(userId: string) {
// Cache the result
if (credentials) {
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
};
}
return credentials;
return credentialsCache[userId]?.client || null;
}
// 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) {
try {
console.log('Courrier API call received');
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
console.log('No authenticated session found');
if (!session || !session.user?.id) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ 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.' },
{ error: "Not authenticated" },
{ status: 401 }
);
}
// Calculate start and end sequence numbers
const start = (page - 1) * limit + 1;
const end = start + limit - 1;
console.log('Fetching emails from range:', { start, end });
// 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,
// Get mail credentials
const credentials = await prisma.mailCredentials.findUnique({
where: {
userId: session.user.id,
},
logger: false,
emitLogs: false,
tls: {
rejectUnauthorized: false
}
});
try {
await client.connect();
console.log('Connected to IMAP server');
// Get list of all mailboxes first
const mailboxes = await client.list();
const availableFolders = mailboxes.map(box => box.path);
console.log('Available folders:', availableFolders);
// Open the requested mailbox
console.log('Opening mailbox:', folder);
const mailbox = await client.mailboxOpen(folder);
console.log('Mailbox stats:', {
exists: mailbox.exists,
name: mailbox.path,
flags: mailbox.flags
});
const result = [];
// Only try to fetch if the mailbox has messages
if (mailbox.exists > 0) {
// Adjust start and end to be within bounds
const adjustedStart = Math.min(start, mailbox.exists);
const adjustedEnd = Math.min(end, mailbox.exists);
console.log('Adjusted fetch range:', { adjustedStart, adjustedEnd });
// Fetch both metadata AND full content
const fetchOptions: any = {
envelope: true,
flags: true,
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));
}
if (!credentials) {
return NextResponse.json(
{ error: "No mail credentials found" },
{ status: 404 }
);
}
const { searchParams } = new URL(request.url);
const page = parseInt(searchParams.get("page") || "1");
const perPage = parseInt(searchParams.get("perPage") || "20");
const folder = searchParams.get("folder") || "INBOX";
const searchQuery = searchParams.get("search") || "";
// Check for entry in emailCache
const cacheKey = `${session.user.id}:${folder}:${page}:${perPage}:${searchQuery}`;
const cachedEmails = emailListCache[cacheKey];
if (cachedEmails) {
console.log(`Using cached emails for ${cacheKey}`);
return NextResponse.json(cachedEmails.data);
}
console.log(`Cache miss for ${cacheKey}, fetching from IMAP`);
// Fetch from IMAP
const cacheCredKey = `credentials:${session.user.id}`;
let imapClient: any = credentialsCache[cacheCredKey]?.client || null;
if (!imapClient) {
// Create IMAP client
const connectWithRetry = async (retries = 3, delay = 1000): Promise<any> => {
try {
const results = await Promise.all(fetchPromises);
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),
content: message.source?.toString() || '' // Include full email content
};
result.push(emailData);
console.log(`Attempting to connect 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,
});
await client.connect();
console.log("Successfully connected to IMAP server");
return client;
} catch (error) {
if (retries > 0) {
console.log(`Connection failed, retrying... (${retries} attempts left)`);
await new Promise((resolve) => setTimeout(resolve, delay));
return connectWithRetry(retries - 1, delay * 1.5);
}
} catch (fetchError) {
console.error('Error fetching emails:', fetchError);
// Continue with any successfully fetched messages
throw error;
}
} 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 = {
emails: result,
folders: availableFolders,
total: mailbox.exists,
hasMore: end < mailbox.exists
console.log(`Fetching messages ${from}:${to} (page ${page}, ${perPage} per page)`);
// Search if needed
let messageIds: any[] = [];
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
emailListCache[cacheKey] = {
data: responseData,
data: result,
timestamp: Date.now()
};
return NextResponse.json(responseData);
} 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 }
);
return NextResponse.json(result);
} finally {
try {
await client.logout();
console.log('IMAP client logged out');
} catch (e) {
console.error('Error during logout:', e);
// If we opened a mailbox, close it
if (mailboxData) {
await imapClient.mailboxClose();
}
}
} catch (error) {
console.error('Error in courrier route:', error);
} catch (error: any) {
console.error("Error in GET:", error);
return NextResponse.json(
{ error: 'An unexpected error occurred' },
{ error: "Internal server error", message: error.message },
{ status: 500 }
);
}

View File

@ -117,17 +117,20 @@ function EmailContent({ email }: { email: Email }) {
console.log('Loading content for email:', email.id);
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);
if (mounted) {
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);
}
return;
}
const formattedEmail = email.content.trim();
const formattedEmail = emailContent.trim();
if (!formattedEmail) {
console.log('Empty content for email:', email.id);
if (mounted) {
@ -187,7 +190,7 @@ function EmailContent({ email }: { email: Email }) {
return () => {
mounted = false;
};
}, [email?.id, email?.content]);
}, [email?.id, email?.content, email?.body]);
if (isLoading) {
return (