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,23 +94,35 @@ export default function ComposeEmail({
try {
const originalContent = replyTo?.body || forwardFrom?.body || '';
const response = await fetch('/api/parse-email', {
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();
// Create initial content without waiting for parsing
content = `
<div class="compose-area" contenteditable="true" style="min-height: 100px; padding: 10px; color: #000000;">
<br/>
<div id="reply-placeholder">Loading original message...</div>
</div>
`;
// Set initial content immediately
composeBodyRef.current.innerHTML = content;
setLocalContent(content);
// Place cursor at the beginning
const composeArea = composeBodyRef.current.querySelector('.compose-area');
if (composeArea) {
const range = document.createRange();
const sel = window.getSelection();
range.setStart(composeArea, 0);
range.collapse(true);
sel?.removeAllRanges();
sel?.addRange(range);
(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/>
@ -120,43 +132,38 @@ export default function ComposeEmail({
To: ${forwardFrom.to}<br/>
${forwardFrom.cc ? `Cc: ${forwardFrom.cc}<br/>` : ''}
<br/>
${parsed.html || parsed.text}
${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;">
${parsed.html || parsed.text}
${decodedContent.html || decodedContent.text || 'No content available'}
</blockquote>
`}
</div>
`;
`;
// 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);
content = `<div class="compose-area" contenteditable="true" style="min-height: 100px; padding: 10px; color: #000000;"></div>`;
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>`;
}
if (composeBodyRef.current) {
composeBodyRef.current.innerHTML = content;
setLocalContent(content);
// Place cursor at the beginning of the compose area
const composeArea = composeBodyRef.current.querySelector('.compose-area');
if (composeArea) {
const range = document.createRange();
const sel = window.getSelection();
range.setStart(composeArea, 0);
range.collapse(true);
sel?.removeAllRanges();
sel?.addRange(range);
(composeArea as HTMLElement).focus();
}
}
};
@ -170,8 +177,13 @@ export default function ComposeEmail({
if (!composeArea) return;
const content = composeArea.innerHTML;
setLocalContent(content);
setComposeBody(content);
if (!content.trim()) {
setLocalContent('');
setComposeBody('');
} else {
setLocalContent(content);
setComposeBody(content);
}
if (onBodyChange) {
onBodyChange(content);
@ -179,40 +191,20 @@ export default function ComposeEmail({
};
const handleSendEmail = async () => {
// Ensure we have content before sending
if (!composeBodyRef.current) {
console.error('Compose body ref is not available');
return;
}
if (!composeBodyRef.current) return;
const composeArea = composeBodyRef.current.querySelector('.compose-area');
if (!composeArea) {
console.error('Compose area not found');
return;
}
if (!composeArea) return;
// Get the current content
const content = composeArea.innerHTML;
if (!content.trim()) {
console.error('Email content is empty');
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 {
const encodedContent = await encodeComposeContent(content);
setComposeBody(encodedContent);
await handleSend();
setShowCompose(false);
} catch (error) {

View File

@ -3,73 +3,47 @@
* Handles basic email content without creating nested structures
*/
export function decodeComposeContent(content: string): string {
if (!content) return '';
// Basic HTML cleaning without creating nested structures
let cleaned = content
// Remove script and style tags
.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;
import { simpleParser } from 'mailparser';
interface ParsedContent {
html: string | null;
text: string | null;
}
export function encodeComposeContent(content: string): string {
if (!content) return '';
// Basic HTML encoding without adding structure
const encoded = content
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;')
.replace(/\n/g, '<br>');
return encoded;
export async function decodeComposeContent(content: string): Promise<ParsedContent> {
if (!content.trim()) {
return { html: null, text: null };
}
try {
const parsed = await simpleParser(content);
return {
html: parsed.html || null,
text: parsed.text || null
};
} catch (error) {
console.error('Error parsing email content:', error);
return {
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;
}