panel 2 courier api restore

This commit is contained in:
alma 2025-04-25 20:32:29 +02:00
parent 3aae79e76b
commit 522683b599
3 changed files with 310 additions and 134 deletions

View File

@ -11,6 +11,7 @@ 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';
// Simple in-memory cache for email content
const emailContentCache = new Map<string, any>();
@ -20,108 +21,207 @@ export async function GET(
{ params }: { params: { id: string } }
) {
try {
const { id } = await Promise.resolve(params);
// Authentication check
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
if (!session || !session.user?.id) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ error: "Not authenticated" },
{ status: 401 }
);
}
// Check cache first
const cacheKey = `email:${session.user.id}:${id}`;
if (emailContentCache.has(cacheKey)) {
return NextResponse.json(emailContentCache.get(cacheKey));
const { id } = params;
if (!id) {
return NextResponse.json(
{ error: "Missing email ID" },
{ status: 400 }
);
}
// Get credentials from database
// Get mail credentials
const credentials = await prisma.mailCredentials.findUnique({
where: {
userId: session.user.id
}
userId: session.user.id,
},
});
if (!credentials) {
return NextResponse.json(
{ error: 'No mail credentials found. Please configure your email account.' },
{ status: 401 }
{ error: "No mail credentials found" },
{ status: 404 }
);
}
const { searchParams } = new URL(request.url);
const folder = searchParams.get("folder") || "INBOX";
// Create IMAP client
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
}
});
let imapClient: any = null;
try {
await client.connect();
// Open INBOX
await client.mailboxOpen('INBOX');
// Fetch the email with UID search
const message = await client.fetchOne(id, {
uid: true,
source: true,
envelope: true,
bodyStructure: true,
flags: true
imapClient = new ImapFlow({
host: credentials.host,
port: credentials.port,
secure: true,
auth: {
user: credentials.email,
pass: credentials.password,
},
logger: false,
});
await imapClient.connect();
console.log(`Connected to IMAP server to fetch full email ${id}`);
// Select mailbox
const mailboxData = await imapClient.mailboxOpen(folder);
console.log(`Opened mailbox ${folder} to fetch email ${id}`);
// Fetch the complete email with its source
const message = await imapClient.fetchOne(Number(id), {
source: true,
envelope: true
});
if (!message) {
return NextResponse.json(
{ error: 'Email not found' },
{ error: "Email not found" },
{ status: 404 }
);
}
// Parse the email content
const emailContent = {
id: message.uid.toString(),
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: any) => addr.address).join(', ') || '',
subject: message.envelope.subject || '(No subject)',
date: message.envelope.date?.toISOString() || new Date().toISOString(),
content: message.source?.toString() || '',
read: message.flags.has('\\Seen'),
starred: message.flags.has('\\Flagged'),
flags: Array.from(message.flags),
hasAttachments: message.bodyStructure?.type === 'multipart'
};
// Cache the email content (with a 15-minute expiry)
emailContentCache.set(cacheKey, emailContent);
setTimeout(() => emailContentCache.delete(cacheKey), 15 * 60 * 1000);
// Return the email content
return NextResponse.json(emailContent);
const { source, envelope } = message;
// Parse the full email content
const parsedEmail = await simpleParser(source.toString());
// Return only the content
return NextResponse.json({
id,
subject: envelope.subject,
content: parsedEmail.html || parsedEmail.textAsHtml || parsedEmail.text || '',
contentFetched: true
});
} catch (error: any) {
console.error("Error fetching email content:", error);
return NextResponse.json(
{ error: "Failed to fetch email content", message: error.message },
{ status: 500 }
);
} finally {
try {
await client.logout();
} catch (e) {
console.error('Error during IMAP logout:', e);
// Close the mailbox and connection
if (imapClient) {
try {
await imapClient.mailboxClose();
await imapClient.logout();
} catch (e) {
console.error("Error closing IMAP connection:", e);
}
}
}
} catch (error) {
console.error('Error fetching email:', error);
} catch (error: any) {
console.error("Error in GET:", error);
return NextResponse.json(
{ error: 'Failed to fetch email content' },
{ error: "Internal server error", message: error.message },
{ status: 500 }
);
}
}
// Add a route to mark email as read
export async function POST(
request: Request,
{ params }: { params: { id: string } }
) {
try {
const session = await getServerSession(authOptions);
if (!session || !session.user?.id) {
return NextResponse.json(
{ error: "Not authenticated" },
{ status: 401 }
);
}
const { id } = params;
if (!id) {
return NextResponse.json(
{ error: "Missing email ID" },
{ status: 400 }
);
}
const { action } = await request.json();
if (action !== 'mark-read' && action !== 'mark-unread') {
return NextResponse.json(
{ error: "Invalid action. Supported actions: mark-read, mark-unread" },
{ status: 400 }
);
}
// Get mail credentials
const credentials = await prisma.mailCredentials.findUnique({
where: {
userId: session.user.id,
},
});
if (!credentials) {
return NextResponse.json(
{ error: "No mail credentials found" },
{ status: 404 }
);
}
const { searchParams } = new URL(request.url);
const folder = searchParams.get("folder") || "INBOX";
// Create IMAP client
let imapClient: any = null;
try {
imapClient = new ImapFlow({
host: credentials.host,
port: credentials.port,
secure: true,
auth: {
user: credentials.email,
pass: credentials.password,
},
logger: false,
});
await imapClient.connect();
// Select mailbox
await imapClient.mailboxOpen(folder);
// Set flag based on action
if (action === 'mark-read') {
await imapClient.messageFlagsAdd(Number(id), ['\\Seen']);
} else {
await imapClient.messageFlagsRemove(Number(id), ['\\Seen']);
}
return NextResponse.json({ success: true });
} catch (error: any) {
console.error(`Error ${action === 'mark-read' ? 'marking email as read' : 'marking email as unread'}:`, error);
return NextResponse.json(
{ error: `Failed to ${action === 'mark-read' ? 'mark email as read' : 'mark email as unread'}`, message: error.message },
{ status: 500 }
);
} finally {
// Close the mailbox and connection
if (imapClient) {
try {
await imapClient.mailboxClose();
await imapClient.logout();
} catch (e) {
console.error("Error closing IMAP connection:", e);
}
}
}
} catch (error: any) {
console.error("Error in POST:", error);
return NextResponse.json(
{ error: "Internal server error", message: error.message },
{ status: 500 }
);
}

View File

@ -253,19 +253,34 @@ export async function GET(request: Request) {
bodyStructure: true,
internalDate: true,
size: true,
source: true // Include full message source to get content
// Only fetch a preview of the body initially for faster loading
bodyParts: [
{
query: {
type: "text",
},
limit: 5000, // Limit to first 5KB of text
}
]
});
if (!message) continue;
const { envelope, flags, bodyStructure, internalDate, size, source } = message;
const { envelope, flags, bodyStructure, internalDate, size, bodyParts } = 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 || '';
// Extract content from the body parts for a preview
let preview = '';
if (bodyParts && bodyParts.length > 0) {
const textPart = bodyParts.find((part: any) => part.type === 'text/plain');
const htmlPart = bodyParts.find((part: any) => part.type === 'text/html');
// Prefer text for preview as it's smaller and faster to process
const content = textPart?.content || htmlPart?.content || '';
if (typeof content === 'string') {
preview = content.substring(0, 150) + '...';
} else if (Buffer.isBuffer(content)) {
preview = content.toString('utf-8', 0, 150) + '...';
}
}
// Convert attachments to our format
@ -335,7 +350,10 @@ export async function GET(request: Request) {
hasAttachments: attachments.length > 0,
attachments,
size,
content // Include content directly in email object
// Just include the preview instead of the full content initially
preview,
// Store the fetched state to know we only have preview
contentFetched: false
});
} catch (messageError) {
console.error(`Error fetching message ${id}:`, messageError);

View File

@ -49,13 +49,16 @@ export interface Email {
to: string;
subject: string;
content: string;
body?: string; // For backward compatibility
preview?: string; // Preview content for list view
body?: string; // For backward compatibility
date: string;
read: boolean;
starred: boolean;
attachments?: { name: string; url: string }[];
folder: string;
cc?: string;
bcc?: string;
contentFetched?: boolean; // Track if full content has been fetched
}
interface Attachment {
@ -115,22 +118,42 @@ function EmailContent({ email }: { email: Email }) {
setDebugInfo(null);
try {
console.log('Loading content for email:', email.id);
console.log('Email content length:', email.content?.length || 0);
// 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);
// Check if we need to fetch full content
if (!email.content || email.content.length === 0) {
console.log('Fetching full content for email:', email.id);
const response = await fetch(`/api/courrier/${email.id}?folder=${encodeURIComponent(email.folder || 'INBOX')}`);
if (!response.ok) {
throw new Error(`Failed to fetch email content: ${response.status}`);
}
const fullContent = await response.json();
if (mounted) {
setContent(<div className="text-gray-500">No content available</div>);
setDebugInfo('No content available for this email');
// Update the email content with the fetched full content
email.content = fullContent.content;
// Render the content
const sanitizedHtml = DOMPurify.sanitize(fullContent.content);
setContent(
<div
className="email-content prose prose-sm max-w-none dark:prose-invert"
dangerouslySetInnerHTML={{ __html: sanitizedHtml }}
/>
);
setDebugInfo('Rendered fetched HTML content');
setError(null);
setIsLoading(false);
}
return;
}
const formattedEmail = emailContent.trim();
// Use existing content if available
console.log('Using existing content for email');
const formattedEmail = email.content.trim();
if (!formattedEmail) {
console.log('Empty content for email:', email.id);
if (mounted) {
@ -141,16 +164,22 @@ function EmailContent({ email }: { email: Email }) {
return;
}
console.log('Parsing email content:', formattedEmail.substring(0, 100) + '...');
const parsedEmail = await decodeEmail(formattedEmail);
console.log('Parsed email result:', {
hasHtml: !!parsedEmail.html,
hasText: !!parsedEmail.text,
htmlLength: parsedEmail.html?.length || 0,
textLength: parsedEmail.text?.length || 0
});
if (mounted) {
// Check if content is already HTML
if (formattedEmail.startsWith('<') && formattedEmail.endsWith('>')) {
// Content is likely HTML, sanitize and display directly
const sanitizedHtml = DOMPurify.sanitize(formattedEmail);
setContent(
<div
className="email-content prose prose-sm max-w-none dark:prose-invert"
dangerouslySetInnerHTML={{ __html: sanitizedHtml }}
/>
);
setDebugInfo('Rendered existing HTML content');
} else {
// Use mailparser for more complex formats
console.log('Parsing email content');
const parsedEmail = await decodeEmail(formattedEmail);
if (parsedEmail.html) {
const sanitizedHtml = DOMPurify.sanitize(parsedEmail.html);
setContent(
@ -159,27 +188,30 @@ function EmailContent({ email }: { email: Email }) {
dangerouslySetInnerHTML={{ __html: sanitizedHtml }}
/>
);
setDebugInfo('Rendered HTML content');
setDebugInfo('Rendered HTML content from parser');
} else if (parsedEmail.text) {
setContent(
<div className="email-content whitespace-pre-wrap">
{parsedEmail.text}
</div>
);
setDebugInfo('Rendered text content');
setDebugInfo('Rendered text content from parser');
} else {
setContent(<div className="text-gray-500">No displayable content available</div>);
setDebugInfo('No HTML or text content in parsed email');
}
setError(null);
setIsLoading(false);
}
setError(null);
} catch (err) {
console.error('Error rendering email content:', err);
if (mounted) {
setError('Error rendering email content. Please try again.');
setDebugInfo(err instanceof Error ? err.message : 'Unknown error');
setContent(null);
}
} finally {
if (mounted) {
setIsLoading(false);
}
}
@ -190,12 +222,13 @@ function EmailContent({ email }: { email: Email }) {
return () => {
mounted = false;
};
}, [email?.id, email?.content, email?.body]);
}, [email?.id, email?.content, email?.folder]);
if (isLoading) {
return (
<div className="flex items-center justify-center py-8">
<div className="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-blue-500"></div>
<span className="ml-3 text-gray-500">Loading email content...</span>
</div>
);
}
@ -370,23 +403,41 @@ function EmailPreview({ email }: { email: Email }) {
let mounted = true;
async function loadPreview() {
if (!email?.content) {
if (!email) {
if (mounted) setPreview('No content available');
return;
}
// If email already has a preview, use it directly
if (email.preview) {
if (mounted) setPreview(email.preview);
return;
}
setIsLoading(true);
try {
const decoded = await decodeEmail(email.content);
if (mounted) {
if (decoded.text) {
setPreview(decoded.text.substring(0, 150) + '...');
} else if (decoded.html) {
const cleanText = decoded.html.replace(/<[^>]*>/g, ' ').trim();
setPreview(cleanText.substring(0, 150) + '...');
} else {
setPreview('No preview available');
// If we have the content already, extract preview from it
if (email.content) {
const plainText = email.content.replace(/<[^>]*>/g, ' ').trim();
if (mounted) {
setPreview(plainText.substring(0, 150) + '...');
}
} else {
// Fallback to using parser for older emails
const decoded = await decodeEmail(email.content || '');
if (mounted) {
if (decoded.text) {
setPreview(decoded.text.substring(0, 150) + '...');
} else if (decoded.html) {
const cleanText = decoded.html.replace(/<[^>]*>/g, ' ').trim();
setPreview(cleanText.substring(0, 150) + '...');
} else {
setPreview('No preview available');
}
}
}
if (mounted) {
setError(null);
}
} catch (err) {
@ -405,7 +456,7 @@ function EmailPreview({ email }: { email: Email }) {
return () => {
mounted = false;
};
}, [email?.content]);
}, [email]);
if (isLoading) {
return <span className="text-gray-400">Loading preview...</span>;
@ -795,22 +846,29 @@ export default function CourrierPage() {
// Set selected email from our existing data (which now includes full content)
setSelectedEmail(selectedEmail);
// Try to mark as read in the background
try {
await fetch(`/api/courrier/${emailId}/mark-read`, {
method: 'POST',
});
// Update read status in the list
setEmails(prevEmails =>
prevEmails.map(email =>
email.id === emailId
? { ...email, read: true }
: email
)
);
} catch (error) {
console.error('Error marking email as read:', error);
// Try to mark as read in the background if not already read
if (!selectedEmail.read) {
try {
// Use the new API endpoint
await fetch(`/api/courrier/${emailId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ action: 'mark-read' }),
});
// Update read status in the list
setEmails(prevEmails =>
prevEmails.map(email =>
email.id === emailId
? { ...email, read: true }
: email
)
);
} catch (error) {
console.error('Error marking email as read:', error);
}
}
} catch (error) {
console.error('Error selecting email:', error);