panel 2 courier api restore

This commit is contained in:
alma 2025-04-26 08:27:31 +02:00
parent f56304775a
commit c1b1e74519
3 changed files with 92 additions and 201 deletions

View File

@ -11,81 +11,38 @@ function getEmailAddress(address: AddressObject | AddressObject[] | undefined):
export async function POST(request: Request) { export async function POST(request: Request) {
try { try {
console.log('[DEBUG] Parse-email API called');
const body = await request.json(); const body = await request.json();
const emailContent = body.email || body.emailContent; const { email } = body;
if (!emailContent || typeof emailContent !== 'string') { if (!email || typeof email !== 'string') {
console.error('[DEBUG] Parse-email API error: Invalid email content');
return NextResponse.json( return NextResponse.json(
{ error: 'Invalid email content' }, { error: 'Invalid email content' },
{ status: 400 } { status: 400 }
); );
} }
console.log('[DEBUG] Parse-email API processing email content, length:', emailContent.length); const parsed = await simpleParser(email);
console.log('[DEBUG] Content sample:', emailContent.substring(0, 100) + '...');
try { return NextResponse.json({
const parsed = await simpleParser(emailContent); subject: parsed.subject || null,
from: getEmailAddress(parsed.from),
console.log('[DEBUG] Parse-email API successfully parsed email:', { to: getEmailAddress(parsed.to),
hasSubject: !!parsed.subject, cc: getEmailAddress(parsed.cc),
hasHtml: !!parsed.html, bcc: getEmailAddress(parsed.bcc),
hasText: !!parsed.text, date: parsed.date || null,
hasTextAsHtml: !!parsed.textAsHtml, html: parsed.html || null,
fromCount: parsed.from ? (Array.isArray(parsed.from) ? parsed.from.length : 1) : 0, text: parsed.textAsHtml || parsed.text || null,
attachmentCount: parsed.attachments?.length || 0 attachments: parsed.attachments?.map(att => ({
}); filename: att.filename,
contentType: att.contentType,
return NextResponse.json({ size: att.size
subject: parsed.subject || null, })) || [],
from: getEmailAddress(parsed.from), headers: parsed.headers || {}
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) { } catch (error) {
console.error('[DEBUG] Parse-email API unhandled error:', error); console.error('Error parsing email:', error);
return NextResponse.json( return NextResponse.json(
{ error: 'Failed to parse email', details: error instanceof Error ? error.message : 'Unknown error' }, { error: 'Failed to parse email' },
{ status: 500 } { status: 500 }
); );
} }

View File

@ -125,7 +125,7 @@ export default function ComposeEmail({
return; return;
} }
// Check if we need to fetch full content first - same as panel 3 // Check if we need to fetch full content first
if (!emailToProcess.content || emailToProcess.content.length === 0) { if (!emailToProcess.content || emailToProcess.content.length === 0) {
console.log('[DEBUG] Need to fetch content before composing reply/forward'); console.log('[DEBUG] Need to fetch content before composing reply/forward');
@ -156,144 +156,78 @@ export default function ComposeEmail({
} }
} }
// Now proceed with the usual decoding // Parse the original email using the API
const type = replyTo ? 'reply' : 'forward'; const response = await fetch('/api/parse-email', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ email: emailToProcess.content }),
});
if (!response.ok) {
throw new Error(`Failed to parse email: ${response.status}`);
}
// DIRECTLY MATCH PANEL 3 IMPLEMENTATION const data = await response.json();
try { console.log('[DEBUG] Parsed email data:', {
const decoded = await decodeEmail(emailToProcess.content); hasHtml: !!data.html,
console.log('[DEBUG] Decoded email for compose:', { hasText: !!data.text,
hasHtml: !!decoded.html, from: data.from,
hasText: !!decoded.text, subject: data.subject
from: decoded.from, });
subject: decoded.subject
}); const emailContent = data.html || data.text || '';
// Create the email container with header information // Format the reply/forward content
let emailHeader = ''; const quotedContent = forwardFrom ? `
if (type === 'forward') { <div style="border-top: 1px solid #e5e7eb; padding-top: 20px; margin-top: 20px; color: #6b7280; font-size: 0.875rem;">
emailHeader = ` ---------- Forwarded message ---------<br/>
<div style="border-top: 1px solid #e5e7eb; margin-top: 20px; padding-top: 20px; color: #6b7280; font-size: 14px;"> From: ${emailToProcess.from}<br/>
<p>---------- Forwarded message ---------</p> Date: ${new Date(emailToProcess.date).toLocaleString()}<br/>
<p>From: ${decoded.from || ''}</p> Subject: ${emailToProcess.subject}<br/>
<p>Date: ${formatDate(decoded.date ? new Date(decoded.date) : null)}</p> To: ${emailToProcess.to}<br/>
<p>Subject: ${decoded.subject || ''}</p> ${emailToProcess.cc ? `Cc: ${emailToProcess.cc}<br/>` : ''}
<p>To: ${decoded.to || ''}</p> </div>
</div> <div style="margin-top: 10px; color: #374151;">
`; ${emailContent}
} else { </div>
emailHeader = ` ` : `
<div style="border-top: 1px solid #e5e7eb; margin-top: 20px; padding-top: 20px; color: #6b7280; font-size: 14px;"> <div style="border-top: 1px solid #e5e7eb; padding-top: 20px; margin-top: 20px; color: #6b7280; font-size: 0.875rem;">
<p>On ${formatDate(decoded.date ? new Date(decoded.date) : null)}, ${decoded.from || ''} wrote:</p> On ${new Date(emailToProcess.date).toLocaleString()}, ${emailToProcess.from} wrote:
</div> </div>
`; <blockquote style="margin: 10px 0 0 10px; padding-left: 1em; border-left: 2px solid #e5e7eb; color: #374151;">
${emailContent}
</blockquote>
`;
// Set the content in the compose area with proper structure
const formattedContent = `
<div class="compose-area" contenteditable="true" style="min-height: 100px; padding: 10px;">
<div style="min-height: 20px;"><br/></div>
${quotedContent}
</div>
`;
if (composeBodyRef.current) {
composeBodyRef.current.innerHTML = formattedContent;
// Place cursor at the beginning before the quoted content
const selection = window.getSelection();
const range = document.createRange();
const firstDiv = composeBodyRef.current.querySelector('div[style*="min-height: 20px;"]');
if (firstDiv) {
range.setStart(firstDiv, 0);
range.collapse(true);
selection?.removeAllRanges();
selection?.addRange(range);
(firstDiv as HTMLElement).focus();
} }
// Get the proper content with minimal sanitization to preserve structure
let emailContent = '';
if (decoded.html) {
// Allow ALL HTML elements and attributes to fully preserve formatting
emailContent = DOMPurify.sanitize(decoded.html, {
ADD_TAGS: ['style', 'meta', 'link', 'script'],
ADD_ATTR: ['*', 'style', 'class', 'id', 'src', 'href', 'target', 'rel', 'align', 'valign', 'border', 'cellpadding', 'cellspacing', 'bgcolor', 'width', 'height'],
ALLOW_UNKNOWN_PROTOCOLS: true,
WHOLE_DOCUMENT: true,
RETURN_DOM: false,
KEEP_CONTENT: true
});
} else if (decoded.text) {
emailContent = `<pre style="white-space: pre-wrap; font-family: inherit;">${decoded.text}</pre>`;
} else {
emailContent = '<div>No content available</div>';
}
// Set the content in the compose area with proper structure
const wrappedContent = `
<div class="compose-area" contenteditable="true" style="min-height: 100px; padding: 10px;">
<div class="cursor-position" style="min-height: 20px; cursor: text;"><br/></div>
${emailHeader}
<div class="email-content" style="margin-top: 10px; border: 1px solid #e5e7eb; padding: 15px; border-radius: 4px; max-height: 500px; overflow-y: auto;">
${emailContent}
</div>
</div>
`;
if (composeBodyRef.current) { // Update compose state
composeBodyRef.current.innerHTML = wrappedContent; setComposeBody(formattedContent);
setLocalContent(formattedContent);
// Place cursor at the beginning before the quoted content console.log('[DEBUG] Successfully set compose content');
const selection = window.getSelection();
const range = document.createRange();
const firstDiv = composeBodyRef.current.querySelector('.cursor-position');
if (firstDiv) {
range.setStart(firstDiv, 0);
range.collapse(true);
selection?.removeAllRanges();
selection?.addRange(range);
(firstDiv as HTMLElement).focus();
}
// After setting the HTML content, add event listeners for scrolling
const messageContents = composeBodyRef.current.querySelectorAll('.email-content');
messageContents.forEach(container => {
// Make sure the container is properly styled for scrolling
(container as HTMLElement).style.maxHeight = '400px';
(container as HTMLElement).style.overflowY = 'auto';
(container as HTMLElement).style.border = '1px solid #e5e7eb';
(container as HTMLElement).style.borderRadius = '4px';
(container as HTMLElement).style.padding = '15px';
// Ensure wheel events are properly handled
if (!(container as HTMLElement).hasAttribute('data-scroll-handler-attached')) {
container.addEventListener('wheel', (e: Event) => {
const wheelEvent = e as WheelEvent;
const target = e.currentTarget as HTMLElement;
// Check if we're at the boundary of the scrollable area
const isAtBottom = target.scrollHeight - target.scrollTop <= target.clientHeight + 1;
const isAtTop = target.scrollTop <= 0;
// Only prevent default if we're not at the boundaries in the direction of scrolling
if ((wheelEvent.deltaY > 0 && !isAtBottom) || (wheelEvent.deltaY < 0 && !isAtTop)) {
e.stopPropagation();
e.preventDefault(); // Prevent the parent container from scrolling
}
}, { passive: false });
// Mark this element as having a scroll handler attached
(container as HTMLElement).setAttribute('data-scroll-handler-attached', 'true');
}
});
// Update compose state
setComposeBody(wrappedContent);
setLocalContent(wrappedContent);
console.log('[DEBUG] Successfully set compose content with scrollable message area');
}
} catch (error) {
console.error('[DEBUG] Error parsing email for compose:', error);
// Fallback to basic content display
const errorContent = `
<div class="compose-area" contenteditable="true">
<br/>
<div style="color: #64748b;">
---------- Original Message ---------<br/>
${emailToProcess.subject ? `Subject: ${emailToProcess.subject}<br/>` : ''}
${emailToProcess.from ? `From: ${emailToProcess.from}<br/>` : ''}
${emailToProcess.date ? `Date: ${new Date(emailToProcess.date).toLocaleString()}<br/>` : ''}
</div>
<div style="color: #64748b; border-left: 2px solid #e5e7eb; padding-left: 10px; margin: 10px 0;">
${emailToProcess.preview || 'No content available'}
</div>
</div>
`;
if (composeBodyRef.current) {
composeBodyRef.current.innerHTML = errorContent;
setComposeBody(errorContent);
setLocalContent(errorContent);
}
} }
} catch (error) { } catch (error) {
console.error('[DEBUG] Error initializing compose content:', error); console.error('[DEBUG] Error initializing compose content:', error);

View File

@ -19,7 +19,7 @@ export async function decodeComposeContent(content: string): Promise<ParsedConte
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify({ emailContent: content }), body: JSON.stringify({ email: content }),
}); });
if (!response.ok) { if (!response.ok) {