panel 2 courier api restore
This commit is contained in:
parent
f805463949
commit
5d058428fd
@ -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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.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
273
app/courrier/[id]/route.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user