compose mime

This commit is contained in:
alma 2025-04-24 19:38:44 +02:00
parent e7a627a322
commit 9a46e8839c
2 changed files with 93 additions and 127 deletions

View File

@ -94,59 +94,19 @@ export default function ComposeEmail({
try { try {
const originalContent = replyTo?.body || forwardFrom?.body || ''; const originalContent = replyTo?.body || forwardFrom?.body || '';
const response = await fetch('/api/parse-email', { // Create initial content without waiting for parsing
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ emailContent: originalContent }),
});
if (!response.ok) {
throw new Error('Failed to parse email');
}
const parsed = await response.json();
content = ` content = `
<div class="compose-area" contenteditable="true" style="min-height: 100px; padding: 10px; color: #000000;"> <div class="compose-area" contenteditable="true" style="min-height: 100px; padding: 10px; color: #000000;">
<br/> <br/>
${forwardFrom ? ` <div id="reply-placeholder">Loading original message...</div>
<div style="border-top: 1px solid #e5e7eb; padding-top: 20px; margin-top: 20px; color: #6b7280; font-size: 0.875rem;">
---------- Forwarded message ---------<br/>
From: ${forwardFrom.from}<br/>
Date: ${new Date(forwardFrom.date).toLocaleString()}<br/>
Subject: ${forwardFrom.subject}<br/>
To: ${forwardFrom.to}<br/>
${forwardFrom.cc ? `Cc: ${forwardFrom.cc}<br/>` : ''}
<br/>
${parsed.html || parsed.text}
</div>
` : `
<div style="border-top: 1px solid #e5e7eb; padding-top: 20px; margin-top: 20px; color: #6b7280; font-size: 0.875rem;">
On ${new Date(replyTo?.date || '').toLocaleString()}, ${replyTo?.from} wrote:
</div>
<blockquote style="margin: 0; padding-left: 1em; border-left: 2px solid #e5e7eb; color: #6b7280;">
${parsed.html || parsed.text}
</blockquote>
`}
</div> </div>
`; `;
} catch (error) {
console.error('Error parsing email:', error);
content = `<div class="compose-area" contenteditable="true" style="min-height: 100px; padding: 10px; color: #000000;"></div>`;
} finally {
setIsLoading(false);
}
} else {
content = `<div class="compose-area" contenteditable="true" style="min-height: 100px; padding: 10px; color: #000000;"></div>`;
}
if (composeBodyRef.current) { // Set initial content immediately
composeBodyRef.current.innerHTML = content; composeBodyRef.current.innerHTML = content;
setLocalContent(content); setLocalContent(content);
// Place cursor at the beginning of the compose area // Place cursor at the beginning
const composeArea = composeBodyRef.current.querySelector('.compose-area'); const composeArea = composeBodyRef.current.querySelector('.compose-area');
if (composeArea) { if (composeArea) {
const range = document.createRange(); const range = document.createRange();
@ -157,6 +117,53 @@ export default function ComposeEmail({
sel?.addRange(range); sel?.addRange(range);
(composeArea as HTMLElement).focus(); (composeArea as HTMLElement).focus();
} }
// Now parse the email content
if (originalContent.trim()) {
const decodedContent = await decodeComposeContent(originalContent);
const quotedContent = `
${forwardFrom ? `
<div style="border-top: 1px solid #e5e7eb; padding-top: 20px; margin-top: 20px; color: #6b7280; font-size: 0.875rem;">
---------- Forwarded message ---------<br/>
From: ${forwardFrom.from}<br/>
Date: ${new Date(forwardFrom.date).toLocaleString()}<br/>
Subject: ${forwardFrom.subject}<br/>
To: ${forwardFrom.to}<br/>
${forwardFrom.cc ? `Cc: ${forwardFrom.cc}<br/>` : ''}
<br/>
${decodedContent.html || decodedContent.text || 'No content available'}
</div>
` : `
<div style="border-top: 1px solid #e5e7eb; padding-top: 20px; margin-top: 20px; color: #6b7280; font-size: 0.875rem;">
On ${new Date(replyTo?.date || '').toLocaleString()}, ${replyTo?.from} wrote:
</div>
<blockquote style="margin: 0; padding-left: 1em; border-left: 2px solid #e5e7eb; color: #6b7280;">
${decodedContent.html || decodedContent.text || 'No content available'}
</blockquote>
`}
`;
// Replace placeholder with actual content
const placeholder = composeBodyRef.current.querySelector('#reply-placeholder');
if (placeholder) {
placeholder.insertAdjacentHTML('beforebegin', quotedContent);
placeholder.remove();
}
}
} catch (error) {
console.error('Error parsing email:', error);
const placeholder = composeBodyRef.current.querySelector('#reply-placeholder');
if (placeholder) {
placeholder.textContent = 'Error loading original message.';
}
} finally {
setIsLoading(false);
}
} else {
content = `<div class="compose-area" contenteditable="true" style="min-height: 100px; padding: 10px; color: #000000;"></div>`;
composeBodyRef.current.innerHTML = content;
setLocalContent(content);
} }
}; };
@ -170,8 +177,13 @@ export default function ComposeEmail({
if (!composeArea) return; if (!composeArea) return;
const content = composeArea.innerHTML; const content = composeArea.innerHTML;
if (!content.trim()) {
setLocalContent('');
setComposeBody('');
} else {
setLocalContent(content); setLocalContent(content);
setComposeBody(content); setComposeBody(content);
}
if (onBodyChange) { if (onBodyChange) {
onBodyChange(content); onBodyChange(content);
@ -179,40 +191,20 @@ export default function ComposeEmail({
}; };
const handleSendEmail = async () => { const handleSendEmail = async () => {
// Ensure we have content before sending if (!composeBodyRef.current) return;
if (!composeBodyRef.current) {
console.error('Compose body ref is not available');
return;
}
const composeArea = composeBodyRef.current.querySelector('.compose-area'); const composeArea = composeBodyRef.current.querySelector('.compose-area');
if (!composeArea) { if (!composeArea) return;
console.error('Compose area not found');
return;
}
// Get the current content
const content = composeArea.innerHTML; const content = composeArea.innerHTML;
if (!content.trim()) { if (!content.trim()) {
console.error('Email content is empty'); console.error('Email content is empty');
return; return;
} }
// Create MIME headers
const mimeHeaders = {
'MIME-Version': '1.0',
'Content-Type': 'text/html; charset="utf-8"',
'Content-Transfer-Encoding': 'quoted-printable'
};
// Combine headers and content
const mimeContent = Object.entries(mimeHeaders)
.map(([key, value]) => `${key}: ${value}`)
.join('\n') + '\n\n' + content;
setComposeBody(mimeContent);
try { try {
const encodedContent = await encodeComposeContent(content);
setComposeBody(encodedContent);
await handleSend(); await handleSend();
setShowCompose(false); setShowCompose(false);
} catch (error) { } catch (error) {

View File

@ -3,73 +3,47 @@
* Handles basic email content without creating nested structures * Handles basic email content without creating nested structures
*/ */
export function decodeComposeContent(content: string): string { import { simpleParser } from 'mailparser';
if (!content) return '';
// Basic HTML cleaning without creating nested structures interface ParsedContent {
let cleaned = content html: string | null;
// Remove script and style tags text: string | null;
.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
// Remove meta tags
.replace(/<meta[^>]*>/gi, '')
// Remove head and title
.replace(/<head[^>]*>[\s\S]*?<\/head>/gi, '')
.replace(/<title[^>]*>[\s\S]*?<\/title>/gi, '')
// Remove body tags
.replace(/<body[^>]*>/gi, '')
.replace(/<\/body>/gi, '')
// Remove html tags
.replace(/<html[^>]*>/gi, '')
.replace(/<\/html>/gi, '')
// Handle basic formatting
.replace(/<br\s*\/?>/gi, '\n')
.replace(/<p[^>]*>/gi, '\n')
.replace(/<\/p>/gi, '\n')
// Handle lists
.replace(/<ul[^>]*>/gi, '\n')
.replace(/<\/ul>/gi, '\n')
.replace(/<ol[^>]*>/gi, '\n')
.replace(/<\/ol>/gi, '\n')
.replace(/<li[^>]*>/gi, '• ')
.replace(/<\/li>/gi, '\n')
// Handle basic text formatting
.replace(/<strong[^>]*>/gi, '**')
.replace(/<\/strong>/gi, '**')
.replace(/<b[^>]*>/gi, '**')
.replace(/<\/b>/gi, '**')
.replace(/<em[^>]*>/gi, '*')
.replace(/<\/em>/gi, '*')
.replace(/<i[^>]*>/gi, '*')
.replace(/<\/i>/gi, '*')
// Handle links
.replace(/<a[^>]*href="([^"]*)"[^>]*>(.*?)<\/a>/gi, '$2 ($1)')
// Handle basic entities
.replace(/&nbsp;/g, ' ')
.replace(/&amp;/g, '&')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&quot;/g, '"')
.replace(/&#39;/g, "'")
// Clean up whitespace
.replace(/\s+/g, ' ')
.trim();
// Do NOT wrap in additional divs
return cleaned;
} }
export function encodeComposeContent(content: string): string { export async function decodeComposeContent(content: string): Promise<ParsedContent> {
if (!content) return ''; if (!content.trim()) {
return { html: null, text: null };
}
// Basic HTML encoding without adding structure try {
const encoded = content const parsed = await simpleParser(content);
.replace(/&/g, '&amp;') return {
.replace(/</g, '&lt;') html: parsed.html || null,
.replace(/>/g, '&gt;') text: parsed.text || null
.replace(/"/g, '&quot;') };
.replace(/'/g, '&#39;') } catch (error) {
.replace(/\n/g, '<br>'); console.error('Error parsing email content:', error);
return {
return encoded; html: content,
text: content
};
}
}
export async function encodeComposeContent(content: string): Promise<string> {
if (!content.trim()) {
throw new Error('Email content is empty');
}
// Create MIME headers
const mimeHeaders = {
'MIME-Version': '1.0',
'Content-Type': 'text/html; charset="utf-8"',
'Content-Transfer-Encoding': 'quoted-printable'
};
// Combine headers and content
return Object.entries(mimeHeaders)
.map(([key, value]) => `${key}: ${value}`)
.join('\n') + '\n\n' + content;
} }