panel 2 courier api restore

This commit is contained in:
alma 2025-04-25 21:17:33 +02:00
parent f805463949
commit 5d058428fd
4 changed files with 444 additions and 21 deletions

View File

@ -11,38 +11,81 @@ function getEmailAddress(address: AddressObject | AddressObject[] | undefined):
export async function POST(request: Request) {
try {
console.log('[DEBUG] Parse-email API called');
const body = await request.json();
const emailContent = body.email || body.emailContent;
if (!emailContent || typeof emailContent !== 'string') {
console.error('[DEBUG] Parse-email API error: Invalid email content');
return NextResponse.json(
{ error: 'Invalid email content' },
{ status: 400 }
);
}
const parsed = await simpleParser(emailContent);
console.log('[DEBUG] Parse-email API processing email content, length:', emailContent.length);
console.log('[DEBUG] Content sample:', emailContent.substring(0, 100) + '...');
return NextResponse.json({
subject: parsed.subject || null,
from: getEmailAddress(parsed.from),
to: getEmailAddress(parsed.to),
cc: getEmailAddress(parsed.cc),
bcc: getEmailAddress(parsed.bcc),
date: parsed.date || null,
html: parsed.html || null,
text: parsed.textAsHtml || parsed.text || null,
attachments: parsed.attachments?.map(att => ({
filename: att.filename,
contentType: att.contentType,
size: att.size
})) || [],
headers: parsed.headers || {}
});
try {
const parsed = await simpleParser(emailContent);
console.log('[DEBUG] Parse-email API successfully parsed email:', {
hasSubject: !!parsed.subject,
hasHtml: !!parsed.html,
hasText: !!parsed.text,
hasTextAsHtml: !!parsed.textAsHtml,
fromCount: parsed.from ? (Array.isArray(parsed.from) ? parsed.from.length : 1) : 0,
attachmentCount: parsed.attachments?.length || 0
});
return NextResponse.json({
subject: parsed.subject || null,
from: getEmailAddress(parsed.from),
to: getEmailAddress(parsed.to),
cc: getEmailAddress(parsed.cc),
bcc: getEmailAddress(parsed.bcc),
date: parsed.date || null,
html: parsed.html || parsed.textAsHtml || null,
text: parsed.text || null,
attachments: parsed.attachments?.map(att => ({
filename: att.filename,
contentType: att.contentType,
size: att.size
})) || [],
headers: parsed.headers || {}
});
} catch (parseError) {
console.error('[DEBUG] Parse-email API error parsing email:', parseError);
// Try simpler parsing method for more resilience
try {
console.log('[DEBUG] Attempting fallback parsing method');
const resultObj: any = { text: emailContent, html: null };
// Simple check if it might be HTML
if (emailContent.includes('<html') || emailContent.includes('<body')) {
resultObj.html = emailContent;
} else {
// Convert plain text to HTML
resultObj.html = emailContent
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/\n/g, '<br>');
}
console.log('[DEBUG] Fallback parsing generated simple result');
return NextResponse.json(resultObj);
} catch (fallbackError) {
console.error('[DEBUG] Even fallback parsing failed:', fallbackError);
throw parseError; // Throw the original error
}
}
} catch (error) {
console.error('Error parsing email:', error);
console.error('[DEBUG] Parse-email API unhandled error:', error);
return NextResponse.json(
{ error: 'Failed to parse email' },
{ error: 'Failed to parse email', details: error instanceof Error ? error.message : 'Unknown error' },
{ status: 500 }
);
}

273
app/courrier/[id]/route.ts Normal file
View File

@ -0,0 +1,273 @@
import { NextResponse } from 'next/server';
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, { content: string, timestamp: number }>();
const CACHE_TTL = 30 * 60 * 1000; // 30 minutes in milliseconds
export async function GET(
request: Request,
{ params }: { params: { id: string } }
) {
try {
console.log(`[DEBUG] GET request for single email ID: ${params.id}`);
const session = await getServerSession(authOptions);
if (!session || !session.user?.id) {
console.log('[DEBUG] Not authenticated');
return NextResponse.json(
{ error: "Not authenticated" },
{ status: 401 }
);
}
const { id } = params;
if (!id) {
console.log('[DEBUG] Missing email ID');
return NextResponse.json(
{ error: "Missing email ID" },
{ status: 400 }
);
}
// Check cache first
const cacheKey = `${session.user.id}:${id}`;
const cachedContent = emailContentCache.get(cacheKey);
const now = Date.now();
if (cachedContent && now - cachedContent.timestamp < CACHE_TTL) {
console.log(`[DEBUG] Using cached content for email ${id}`);
return NextResponse.json({
id,
content: cachedContent.content,
contentFetched: true,
fromCache: true
});
}
// Get mail credentials
const credentials = await prisma.mailCredentials.findUnique({
where: {
userId: session.user.id,
},
});
if (!credentials) {
console.log('[DEBUG] No mail credentials found');
return NextResponse.json(
{ error: "No mail credentials found" },
{ status: 404 }
);
}
const { searchParams } = new URL(request.url);
const folder = searchParams.get("folder") || "INBOX";
console.log(`[DEBUG] Fetching email ${id} from folder ${folder}`);
// 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();
console.log(`[DEBUG] Connected to IMAP server to fetch email ${id}`);
// Select mailbox
const mailboxData = await imapClient.mailboxOpen(folder);
console.log(`[DEBUG] Opened mailbox ${folder}, total messages: ${mailboxData.exists}`);
// Fetch the complete email with its source
const message = await imapClient.fetchOne(Number(id), {
source: true,
envelope: true
});
if (!message) {
console.log(`[DEBUG] Email ${id} not found`);
return NextResponse.json(
{ error: "Email not found" },
{ status: 404 }
);
}
const { source, envelope } = message;
console.log(`[DEBUG] Successfully fetched email source, length: ${source.length}`);
// Parse the full email content
const parsedEmail = await simpleParser(source.toString());
console.log(`[DEBUG] Parsed email source: has HTML: ${!!parsedEmail.html}, has text: ${!!parsedEmail.text}`);
// Prioritize HTML content with fallbacks
const content = parsedEmail.html || parsedEmail.textAsHtml || parsedEmail.text || '';
// Cache the content
emailContentCache.set(cacheKey, {
content,
timestamp: now
});
// Return the content
return NextResponse.json({
id,
subject: envelope.subject,
content,
contentFetched: true
});
} catch (error: any) {
console.error("[DEBUG] Error fetching email content:", error);
return NextResponse.json(
{
error: "Failed to fetch email content",
message: error.message,
details: error.stack
},
{ status: 500 }
);
} finally {
// Close the mailbox and connection
if (imapClient) {
try {
await imapClient.mailboxClose();
await imapClient.logout();
console.log(`[DEBUG] Closed IMAP connection for email ${id}`);
} catch (e) {
console.error("[DEBUG] Error closing IMAP connection:", e);
}
}
}
} catch (error: any) {
console.error("[DEBUG] Unhandled error in GET:", error);
return NextResponse.json(
{
error: "Internal server error",
message: error.message,
details: error.stack
},
{ status: 500 }
);
}
}
// Handle marking emails as read
export async function POST(
request: Request,
{ params }: { params: { id: string } }
) {
try {
console.log(`[DEBUG] POST request for email ID: ${params.id}`);
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 body = await request.json();
const { action } = body;
if (action !== 'mark-read' && action !== 'mark-unread') {
return NextResponse.json(
{ error: "Invalid action. Supported actions: mark-read, mark-unread" },
{ status: 400 }
);
}
console.log(`[DEBUG] Processing action ${action} for email ${id}`);
// 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();
console.log(`[DEBUG] Connected to IMAP server for ${action}`);
// Select mailbox
await imapClient.mailboxOpen(folder);
console.log(`[DEBUG] Opened mailbox ${folder} for ${action}`);
// Set flag based on action
if (action === 'mark-read') {
await imapClient.messageFlagsAdd(Number(id), ['\\Seen']);
console.log(`[DEBUG] Marked email ${id} as read`);
} else {
await imapClient.messageFlagsRemove(Number(id), ['\\Seen']);
console.log(`[DEBUG] Marked email ${id} as unread`);
}
return NextResponse.json({ success: true, action });
} catch (error: any) {
console.error(`[DEBUG] Error ${action}:`, error);
return NextResponse.json(
{ error: `Failed to ${action}`, message: error.message },
{ status: 500 }
);
} finally {
// Close the mailbox and connection
if (imapClient) {
try {
await imapClient.mailboxClose();
await imapClient.logout();
console.log(`[DEBUG] Closed IMAP connection after ${action}`);
} catch (e) {
console.error("[DEBUG] Error closing IMAP connection:", e);
}
}
}
} catch (error: any) {
console.error("[DEBUG] Unhandled error in POST:", error);
return NextResponse.json(
{ error: "Internal server error", message: error.message },
{ status: 500 }
);
}
}

View File

@ -888,6 +888,38 @@ export default function CourrierPage() {
throw new Error('Email not found in list');
}
// Check if we need to fetch full content
if (!selectedEmail.content || selectedEmail.content.length === 0) {
console.log('[DEBUG] Fetching full content for email:', emailId);
try {
const response = await fetch(`/api/courrier/${emailId}?folder=${encodeURIComponent(selectedEmail.folder || 'INBOX')}`);
if (!response.ok) {
throw new Error(`Failed to fetch email content: ${response.status}`);
}
const fullContent = await response.json();
// Update the email content with the fetched full content
selectedEmail.content = fullContent.content;
selectedEmail.contentFetched = true;
// Update the email in the list too so we don't refetch
setEmails(prevEmails =>
prevEmails.map(email =>
email.id === emailId
? { ...email, content: fullContent.content, contentFetched: true }
: email
)
);
console.log('[DEBUG] Successfully fetched full content for email:', emailId);
} catch (error) {
console.error('[DEBUG] Error fetching full content:', error);
}
}
// Set selected email from our existing data (which now includes full content)
setSelectedEmail(selectedEmail);
@ -1507,6 +1539,44 @@ export default function CourrierPage() {
if (!selectedEmail) return;
try {
// Ensure we have full content before proceeding
if (!selectedEmail.content || selectedEmail.content.length === 0) {
console.log('[DEBUG] Need to fetch content before reply/forward');
setContentLoading(true);
try {
const response = await fetch(`/api/courrier/${selectedEmail.id}?folder=${encodeURIComponent(selectedEmail.folder || 'INBOX')}`);
if (!response.ok) {
throw new Error(`Failed to fetch email content: ${response.status}`);
}
const fullContent = await response.json();
// Update the email content with the fetched full content
selectedEmail.content = fullContent.content;
selectedEmail.contentFetched = true;
// Update the email in the list too so we don't refetch
setEmails(prevEmails =>
prevEmails.map(email =>
email.id === selectedEmail.id
? { ...email, content: fullContent.content, contentFetched: true }
: email
)
);
console.log('[DEBUG] Successfully fetched content for reply/forward');
} catch (error) {
console.error('[DEBUG] Error fetching content for reply:', error);
alert('Failed to load email content for reply. Please try again.');
setContentLoading(false);
return; // Exit if we couldn't get the content
}
setContentLoading(false);
}
const getReplyTo = () => {
if (type === 'forward') return '';
return selectedEmail.from;

View File

@ -90,8 +90,27 @@ export default function ComposeEmail({
try {
const emailToProcess = replyTo || forwardFrom;
console.log('[DEBUG] Initializing compose content with email:',
emailToProcess ? {
id: emailToProcess.id,
subject: emailToProcess.subject,
hasContent: !!emailToProcess.content,
contentLength: emailToProcess.content ? emailToProcess.content.length : 0,
preview: emailToProcess.preview
} : 'null'
);
if (!emailToProcess?.content) {
console.error('No email content found to process');
console.error('[DEBUG] No email content found to process');
composeBodyRef.current.innerHTML = `
<div class="compose-area" contenteditable="true">
<br/>
<div style="color: #ef4444;">Error: No original message content available.</div>
<div style="color: #64748b; font-size: 0.875rem; margin-top: 0.5rem;">
Please select the email again or try refreshing the page.
</div>
</div>
`;
return;
}
@ -103,6 +122,8 @@ export default function ComposeEmail({
</div>
`;
console.log('[DEBUG] Sending content to parse-email API, length:', emailToProcess.content.length);
// Parse the original email using the API
const response = await fetch('/api/parse-email', {
method: 'POST',
@ -112,12 +133,24 @@ export default function ComposeEmail({
body: JSON.stringify({ email: emailToProcess.content }),
});
console.log('[DEBUG] Parse-email API response status:', response.status);
const data = await response.json();
console.log('[DEBUG] Parse-email API response:', {
hasHtml: !!data.html,
hasText: !!data.text,
subject: data.subject,
error: data.error
});
if (!response.ok) {
throw new Error(data.error || 'Failed to parse email');
}
const emailContent = data.html || data.text || '';
if (!emailContent) {
console.warn('[DEBUG] No HTML or text content returned from parser');
}
// Format the reply/forward content
const quotedContent = forwardFrom ? `
@ -167,14 +200,18 @@ export default function ComposeEmail({
// Update compose state
setComposeBody(formattedContent);
setLocalContent(formattedContent);
console.log('[DEBUG] Successfully set compose content');
}
} catch (error) {
console.error('Error initializing compose content:', error);
console.error('[DEBUG] Error initializing compose content:', error);
if (composeBodyRef.current) {
const errorContent = `
<div class="compose-area" contenteditable="true">
<br/>
<div style="color: #ef4444;">Error loading original message.</div>
<div style="color: #64748b; font-size: 0.875rem; margin-top: 0.5rem;">
Technical details: ${error instanceof Error ? error.message : 'Unknown error'}
</div>
</div>
`;
composeBodyRef.current.innerHTML = errorContent;