panel 2 courier api restore
This commit is contained in:
parent
3aae79e76b
commit
522683b599
@ -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 }
|
||||
);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user