mime change
This commit is contained in:
parent
d62b6d2a34
commit
f4990623da
@ -40,6 +40,8 @@ import {
|
|||||||
} from '@/lib/infomaniak-mime-decoder';
|
} from '@/lib/infomaniak-mime-decoder';
|
||||||
import DOMPurify from 'isomorphic-dompurify';
|
import DOMPurify from 'isomorphic-dompurify';
|
||||||
import ComposeEmail from '@/components/ComposeEmail';
|
import ComposeEmail from '@/components/ComposeEmail';
|
||||||
|
import { decodeEmail } from '@/lib/mail-parser-wrapper';
|
||||||
|
import { Attachment as MailParserAttachment } from 'mailparser';
|
||||||
|
|
||||||
export interface Account {
|
export interface Account {
|
||||||
id: number;
|
id: number;
|
||||||
@ -108,188 +110,67 @@ function splitEmailHeadersAndBody(emailBody: string): { headers: string; body: s
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderEmailContent(email: Email) {
|
async function renderEmailContent(email: Email) {
|
||||||
if (!email.body) {
|
if (!email.body) {
|
||||||
console.warn('No email body provided');
|
console.warn('No email body provided');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Split email into headers and body
|
const decoded = await decodeEmail(email.body);
|
||||||
const [headersPart, ...bodyParts] = email.body.split('\r\n\r\n');
|
|
||||||
if (!headersPart || bodyParts.length === 0) {
|
// Prefer HTML content if available
|
||||||
throw new Error('Invalid email format: missing headers or body');
|
if (decoded.html) {
|
||||||
|
return (
|
||||||
|
<div className="email-content" dir="ltr">
|
||||||
|
<div className="prose max-w-none" dir="ltr" dangerouslySetInnerHTML={{ __html: cleanHtml(decoded.html) }} />
|
||||||
|
{decoded.attachments.length > 0 && renderAttachments(decoded.attachments)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const body = bodyParts.join('\r\n\r\n');
|
// Fall back to text content
|
||||||
|
if (decoded.text) {
|
||||||
// Parse headers using Infomaniak MIME decoder
|
return (
|
||||||
const headerInfo = parseEmailHeaders(headersPart);
|
<div className="email-content" dir="ltr">
|
||||||
const boundary = extractBoundary(headersPart);
|
|
||||||
|
|
||||||
// If it's a multipart email
|
|
||||||
if (boundary) {
|
|
||||||
try {
|
|
||||||
const parts = body.split(`--${boundary}`);
|
|
||||||
let htmlContent = '';
|
|
||||||
let textContent = '';
|
|
||||||
let attachments: { filename: string; content: string }[] = [];
|
|
||||||
|
|
||||||
for (const part of parts) {
|
|
||||||
if (!part.trim()) continue;
|
|
||||||
|
|
||||||
const [partHeaders, ...partBodyParts] = part.split('\r\n\r\n');
|
|
||||||
if (!partHeaders || partBodyParts.length === 0) continue;
|
|
||||||
|
|
||||||
const partBody = partBodyParts.join('\r\n\r\n');
|
|
||||||
const contentType = extractHeader(partHeaders, 'Content-Type').toLowerCase();
|
|
||||||
const encoding = extractHeader(partHeaders, 'Content-Transfer-Encoding').toLowerCase();
|
|
||||||
const charset = extractHeader(partHeaders, 'charset') || 'utf-8';
|
|
||||||
|
|
||||||
try {
|
|
||||||
let decodedContent = '';
|
|
||||||
if (encoding === 'base64') {
|
|
||||||
decodedContent = decodeBase64(partBody, charset);
|
|
||||||
} else if (encoding === 'quoted-printable') {
|
|
||||||
decodedContent = decodeQuotedPrintable(partBody, charset);
|
|
||||||
} else {
|
|
||||||
decodedContent = convertCharset(partBody, charset);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (contentType.includes('text/html')) {
|
|
||||||
// For HTML content, we want to preserve the HTML structure
|
|
||||||
// Only clean up problematic elements while keeping the formatting
|
|
||||||
htmlContent = decodedContent
|
|
||||||
.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
|
|
||||||
.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
|
|
||||||
.replace(/<meta[^>]*>/gi, '')
|
|
||||||
.replace(/<link[^>]*>/gi, '')
|
|
||||||
.replace(/<base[^>]*>/gi, '')
|
|
||||||
.replace(/<title[^>]*>[\s\S]*?<\/title>/gi, '')
|
|
||||||
.replace(/<head[^>]*>[\s\S]*?<\/head>/gi, '')
|
|
||||||
.replace(/<body[^>]*>/gi, '')
|
|
||||||
.replace(/<\/body>/gi, '')
|
|
||||||
.replace(/<html[^>]*>/gi, '')
|
|
||||||
.replace(/<\/html>/gi, '');
|
|
||||||
} else if (contentType.includes('text/plain')) {
|
|
||||||
textContent = decodedContent;
|
|
||||||
} else if (contentType.includes('attachment') || extractHeader(partHeaders, 'Content-Disposition').includes('attachment')) {
|
|
||||||
attachments.push({
|
|
||||||
filename: extractFilename(partHeaders) || 'unnamed_attachment',
|
|
||||||
content: decodedContent
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (partError) {
|
|
||||||
console.error('Error processing email part:', partError);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prefer HTML content if available
|
|
||||||
if (htmlContent) {
|
|
||||||
return (
|
|
||||||
<div className="email-content" dir="ltr">
|
|
||||||
<div className="prose max-w-none" dir="ltr" dangerouslySetInnerHTML={{ __html: htmlContent }} />
|
|
||||||
{attachments.length > 0 && renderAttachments(attachments)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fall back to text content
|
|
||||||
if (textContent) {
|
|
||||||
return (
|
|
||||||
<div className="email-content" dir="ltr">
|
|
||||||
<div className="whitespace-pre-wrap font-sans text-base leading-relaxed" dir="ltr">
|
|
||||||
{textContent.split('\n').map((line: string, i: number) => (
|
|
||||||
<p key={i} className="mb-2">{line}</p>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
{attachments.length > 0 && renderAttachments(attachments)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (multipartError) {
|
|
||||||
console.error('Error processing multipart email:', multipartError);
|
|
||||||
throw new Error('Failed to process multipart email');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If it's a simple email, try to detect content type and decode
|
|
||||||
const contentType = extractHeader(headersPart, 'Content-Type').toLowerCase();
|
|
||||||
const encoding = extractHeader(headersPart, 'Content-Transfer-Encoding').toLowerCase();
|
|
||||||
const charset = extractHeader(headersPart, 'charset') || 'utf-8';
|
|
||||||
|
|
||||||
try {
|
|
||||||
let decodedBody = '';
|
|
||||||
if (encoding === 'base64') {
|
|
||||||
decodedBody = decodeBase64(body, charset);
|
|
||||||
} else if (encoding === 'quoted-printable') {
|
|
||||||
decodedBody = decodeQuotedPrintable(body, charset);
|
|
||||||
} else {
|
|
||||||
decodedBody = convertCharset(body, charset);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (contentType.includes('text/html')) {
|
|
||||||
// For HTML content, preserve the HTML structure
|
|
||||||
const cleanedHtml = decodedBody
|
|
||||||
.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
|
|
||||||
.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
|
|
||||||
.replace(/<meta[^>]*>/gi, '')
|
|
||||||
.replace(/<link[^>]*>/gi, '')
|
|
||||||
.replace(/<base[^>]*>/gi, '')
|
|
||||||
.replace(/<title[^>]*>[\s\S]*?<\/title>/gi, '')
|
|
||||||
.replace(/<head[^>]*>[\s\S]*?<\/head>/gi, '')
|
|
||||||
.replace(/<body[^>]*>/gi, '')
|
|
||||||
.replace(/<\/body>/gi, '')
|
|
||||||
.replace(/<html[^>]*>/gi, '')
|
|
||||||
.replace(/<\/html>/gi, '');
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="email-content" dir="ltr">
|
|
||||||
<div className="prose max-w-none" dir="ltr" dangerouslySetInnerHTML={{ __html: cleanedHtml }} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
<div className="email-content" dir="ltr">
|
|
||||||
<div className="whitespace-pre-wrap font-sans text-base leading-relaxed" dir="ltr">
|
<div className="whitespace-pre-wrap font-sans text-base leading-relaxed" dir="ltr">
|
||||||
{decodedBody.split('\n').map((line: string, i: number) => (
|
{decoded.text.split('\n').map((line: string, i: number) => (
|
||||||
<p key={i} className="mb-2">{line}</p>
|
<p key={i} className="mb-2">{line}</p>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{decoded.attachments.length > 0 && renderAttachments(decoded.attachments)}
|
||||||
);
|
</div>
|
||||||
}
|
);
|
||||||
} catch (decodeError) {
|
|
||||||
console.error('Error decoding email body:', decodeError);
|
|
||||||
throw new Error('Failed to decode email body');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error rendering email content:', error);
|
console.error('Error rendering email content:', error);
|
||||||
return (
|
return (
|
||||||
<div className="email-content">
|
<div className="email-content text-red-500">
|
||||||
<div className="text-red-500 mb-4">Error displaying email content: {error instanceof Error ? error.message : 'Unknown error'}</div>
|
Error rendering email content
|
||||||
<pre className="whitespace-pre-wrap text-sm bg-gray-100 p-4 rounded">
|
|
||||||
{email.body}
|
|
||||||
</pre>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to render attachments
|
function renderAttachments(attachments: MailParserAttachment[]) {
|
||||||
function renderAttachments(attachments: { filename: string; content: string }[]) {
|
if (!attachments.length) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-4">
|
<div className="mt-4 border-t border-gray-200 pt-4">
|
||||||
<h3 className="text-sm font-medium mb-2">Attachments:</h3>
|
<h3 className="text-sm font-medium text-gray-900 mb-2">Attachments</h3>
|
||||||
<ul className="space-y-2">
|
<div className="space-y-2">
|
||||||
{attachments.map((attachment, index) => (
|
{attachments.map((attachment, index) => (
|
||||||
<li key={index} className="flex items-center gap-2">
|
<div key={index} className="flex items-center gap-2 p-2 bg-gray-50 rounded">
|
||||||
<Paperclip className="h-4 w-4 text-muted-foreground" />
|
<Paperclip className="h-4 w-4 text-gray-400" />
|
||||||
<span className="text-sm">{attachment.filename}</span>
|
<span className="text-sm text-gray-600">{attachment.filename || 'unnamed_attachment'}</span>
|
||||||
</li>
|
<span className="text-xs text-gray-400">
|
||||||
|
{attachment.size ? `(${Math.round(attachment.size / 1024)} KB)` : ''}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -328,124 +209,43 @@ const initialSidebarItems = [
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
function getReplyBody(email: Email, type: 'reply' | 'reply-all' | 'forward' = 'reply') {
|
async function getReplyBody(email: Email, type: 'reply' | 'reply-all' | 'forward' = 'reply') {
|
||||||
if (!email.body) return '';
|
if (!email.body) return '';
|
||||||
|
|
||||||
let content = '';
|
try {
|
||||||
let headers = '';
|
const decoded = await decodeEmail(email.body);
|
||||||
let body = '';
|
|
||||||
|
// Format the reply/forward content with proper structure and direction
|
||||||
// Split headers and body
|
let formattedContent = '';
|
||||||
const parts = email.body.split('\r\n\r\n');
|
|
||||||
if (parts.length > 1) {
|
if (type === 'forward') {
|
||||||
headers = parts[0];
|
formattedContent = `
|
||||||
body = parts.slice(1).join('\r\n\r\n');
|
<div class="forwarded-message">
|
||||||
} else {
|
<p>---------- Forwarded message ---------</p>
|
||||||
body = email.body;
|
<p>From: ${decoded.from}</p>
|
||||||
}
|
<p>Date: ${decoded.date.toLocaleString()}</p>
|
||||||
|
<p>Subject: ${decoded.subject}</p>
|
||||||
// Parse headers
|
<p>To: ${decoded.to}</p>
|
||||||
const headerInfo = parseEmailHeaders(headers);
|
<br>
|
||||||
const boundary = extractBoundary(headers);
|
${decoded.html || `<pre>${decoded.text}</pre>`}
|
||||||
|
</div>
|
||||||
// Handle multipart emails
|
`;
|
||||||
if (boundary) {
|
} else {
|
||||||
const parts = body.split(`--${boundary}`);
|
formattedContent = `
|
||||||
for (const part of parts) {
|
<div class="quoted-message">
|
||||||
if (!part.trim()) continue;
|
<p>On ${decoded.date.toLocaleString()}, ${decoded.from} wrote:</p>
|
||||||
|
<blockquote>
|
||||||
const [partHeaders, ...partBodyParts] = part.split('\r\n\r\n');
|
${decoded.html || `<pre>${decoded.text}</pre>`}
|
||||||
if (!partHeaders || partBodyParts.length === 0) continue;
|
</blockquote>
|
||||||
|
|
||||||
const partBody = partBodyParts.join('\r\n\r\n');
|
|
||||||
const partHeaderInfo = parseEmailHeaders(partHeaders);
|
|
||||||
|
|
||||||
try {
|
|
||||||
let decodedContent = '';
|
|
||||||
if (partHeaderInfo.encoding === 'base64') {
|
|
||||||
decodedContent = decodeBase64(partBody, partHeaderInfo.charset);
|
|
||||||
} else if (partHeaderInfo.encoding === 'quoted-printable') {
|
|
||||||
decodedContent = decodeQuotedPrintable(partBody, partHeaderInfo.charset);
|
|
||||||
} else {
|
|
||||||
decodedContent = convertCharset(partBody, partHeaderInfo.charset);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Preserve the original MIME structure
|
|
||||||
if (partHeaderInfo.contentType.includes('text/html')) {
|
|
||||||
content = `
|
|
||||||
<div class="email-part" data-mime-type="text/html" data-charset="${partHeaderInfo.charset}">
|
|
||||||
${decodedContent}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
break;
|
|
||||||
} else if (partHeaderInfo.contentType.includes('text/plain') && !content) {
|
|
||||||
content = `
|
|
||||||
<div class="email-part" data-mime-type="text/plain" data-charset="${partHeaderInfo.charset}">
|
|
||||||
<pre style="white-space: pre-wrap; font-family: inherit;">${decodedContent}</pre>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error decoding email part:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Handle simple email
|
|
||||||
try {
|
|
||||||
let decodedBody = '';
|
|
||||||
if (headerInfo.encoding === 'base64') {
|
|
||||||
decodedBody = decodeBase64(body, headerInfo.charset);
|
|
||||||
} else if (headerInfo.encoding === 'quoted-printable') {
|
|
||||||
decodedBody = decodeQuotedPrintable(body, headerInfo.charset);
|
|
||||||
} else {
|
|
||||||
decodedBody = convertCharset(body, headerInfo.charset);
|
|
||||||
}
|
|
||||||
|
|
||||||
content = `
|
|
||||||
<div class="email-part" data-mime-type="${headerInfo.contentType}" data-charset="${headerInfo.charset}">
|
|
||||||
${headerInfo.contentType.includes('text/html') ? decodedBody : `<pre style="white-space: pre-wrap; font-family: inherit;">${decodedBody}</pre>`}
|
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
} catch (error) {
|
|
||||||
console.error('Error decoding email body:', error);
|
|
||||||
content = body;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return cleanHtml(formattedContent);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error generating reply body:', error);
|
||||||
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean and sanitize HTML content while preserving structure
|
|
||||||
content = cleanHtml(content);
|
|
||||||
|
|
||||||
// Format the reply/forward content with proper structure and direction
|
|
||||||
let formattedContent = '';
|
|
||||||
if (type === 'forward') {
|
|
||||||
formattedContent = `
|
|
||||||
<div class="email-container" dir="ltr">
|
|
||||||
<div class="email-header" dir="ltr">
|
|
||||||
<p class="text-sm text-gray-600 mb-2">Forwarded message from ${email.from}</p>
|
|
||||||
<p class="text-sm text-gray-600 mb-2">Date: ${new Date(email.date).toLocaleString()}</p>
|
|
||||||
<p class="text-sm text-gray-600 mb-2">Subject: ${email.subject}</p>
|
|
||||||
<p class="text-sm text-gray-600 mb-2">To: ${email.to}</p>
|
|
||||||
${email.cc ? `<p class="text-sm text-gray-600 mb-2">Cc: ${email.cc}</p>` : ''}
|
|
||||||
</div>
|
|
||||||
<div class="email-content" dir="auto">
|
|
||||||
${content}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
} else {
|
|
||||||
formattedContent = `
|
|
||||||
<div class="email-container" dir="ltr">
|
|
||||||
<div class="email-header" dir="ltr">
|
|
||||||
<p class="text-sm text-gray-600 mb-2">On ${new Date(email.date).toLocaleString()}, ${email.from} wrote:</p>
|
|
||||||
</div>
|
|
||||||
<div class="email-content" dir="auto">
|
|
||||||
${content}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return formattedContent;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CourrierPage() {
|
export default function CourrierPage() {
|
||||||
@ -1170,84 +970,13 @@ export default function CourrierPage() {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const generateEmailPreview = (email: Email): string => {
|
const generateEmailPreview = async (email: Email): Promise<string> => {
|
||||||
console.log('=== generateEmailPreview Debug ===');
|
|
||||||
console.log('Email ID:', email.id);
|
|
||||||
console.log('Subject:', email.subject);
|
|
||||||
console.log('Body length:', email.body.length);
|
|
||||||
console.log('First 200 chars of body:', email.body.substring(0, 200));
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Split email into headers and body
|
const decoded = await decodeEmail(email.body);
|
||||||
const [headersPart, ...bodyParts] = email.body.split('\r\n\r\n');
|
return decoded.text || cleanHtml(decoded.html || '');
|
||||||
const body = bodyParts.join('\r\n\r\n');
|
|
||||||
|
|
||||||
// Parse headers using our MIME decoder
|
|
||||||
const headerInfo = parseEmailHeaders(headersPart);
|
|
||||||
const boundary = extractBoundary(headersPart);
|
|
||||||
|
|
||||||
let preview = '';
|
|
||||||
|
|
||||||
// If it's a multipart email
|
|
||||||
if (boundary) {
|
|
||||||
const parts = body.split(`--${boundary}`);
|
|
||||||
|
|
||||||
for (const part of parts) {
|
|
||||||
if (!part.trim()) continue;
|
|
||||||
|
|
||||||
const [partHeaders, ...partBodyParts] = part.split('\r\n\r\n');
|
|
||||||
const partBody = partBodyParts.join('\r\n\r\n');
|
|
||||||
const partHeaderInfo = parseEmailHeaders(partHeaders);
|
|
||||||
|
|
||||||
if (partHeaderInfo.contentType.includes('text/plain')) {
|
|
||||||
preview = decodeQuotedPrintable(partBody, partHeaderInfo.charset);
|
|
||||||
break;
|
|
||||||
} else if (partHeaderInfo.contentType.includes('text/html') && !preview) {
|
|
||||||
preview = cleanHtml(decodeQuotedPrintable(partBody, partHeaderInfo.charset));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If no preview from multipart, try to decode the whole body
|
|
||||||
if (!preview) {
|
|
||||||
preview = decodeQuotedPrintable(body, headerInfo.charset);
|
|
||||||
if (headerInfo.contentType.includes('text/html')) {
|
|
||||||
preview = cleanHtml(preview);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clean up the preview
|
|
||||||
preview = preview
|
|
||||||
.replace(/^>+/gm, '')
|
|
||||||
.replace(/Content-Type:[^\n]+/g, '')
|
|
||||||
.replace(/Content-Transfer-Encoding:[^\n]+/g, '')
|
|
||||||
.replace(/--[a-zA-Z0-9]+(-[a-zA-Z0-9]+)?/g, '')
|
|
||||||
.replace(/boundary=[^\n]+/g, '')
|
|
||||||
.replace(/charset=[^\n]+/g, '')
|
|
||||||
.replace(/[\r\n]+/g, ' ')
|
|
||||||
.trim();
|
|
||||||
|
|
||||||
// Take first 100 characters
|
|
||||||
preview = preview.substring(0, 100);
|
|
||||||
|
|
||||||
// Try to end at a complete word
|
|
||||||
if (preview.length === 100) {
|
|
||||||
const lastSpace = preview.lastIndexOf(' ');
|
|
||||||
if (lastSpace > 80) {
|
|
||||||
preview = preview.substring(0, lastSpace);
|
|
||||||
}
|
|
||||||
preview += '...';
|
|
||||||
}
|
|
||||||
|
|
||||||
return preview;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error generating email preview:', error);
|
console.error('Error generating email preview:', error);
|
||||||
return email.body
|
return '';
|
||||||
.replace(/<[^>]+>/g, ' ')
|
|
||||||
.replace(/ |‌|»|«|>/g, ' ')
|
|
||||||
.replace(/\s+/g, ' ')
|
|
||||||
.substring(0, 100)
|
|
||||||
.trim() + '...';
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
71
lib/mail-parser-wrapper.ts
Normal file
71
lib/mail-parser-wrapper.ts
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import { simpleParser, ParsedMail, Attachment, HeaderValue, AddressObject } from 'mailparser';
|
||||||
|
|
||||||
|
export interface DecodedEmail {
|
||||||
|
html: string | false;
|
||||||
|
text: string | false;
|
||||||
|
attachments: Attachment[];
|
||||||
|
headers: Map<string, HeaderValue>;
|
||||||
|
subject: string;
|
||||||
|
from: string;
|
||||||
|
to: string;
|
||||||
|
date: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAddressText(address: AddressObject | AddressObject[] | undefined): string {
|
||||||
|
if (!address) return '';
|
||||||
|
if (Array.isArray(address)) {
|
||||||
|
return address.map(addr => addr.value?.[0]?.address || '').filter(Boolean).join(', ');
|
||||||
|
}
|
||||||
|
return address.value?.[0]?.address || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function decodeEmail(rawEmail: string): Promise<DecodedEmail> {
|
||||||
|
try {
|
||||||
|
const parsed = await simpleParser(rawEmail);
|
||||||
|
|
||||||
|
return {
|
||||||
|
html: parsed.html || false,
|
||||||
|
text: parsed.text || false,
|
||||||
|
attachments: parsed.attachments || [],
|
||||||
|
headers: parsed.headers,
|
||||||
|
subject: parsed.subject || '',
|
||||||
|
from: getAddressText(parsed.from),
|
||||||
|
to: getAddressText(parsed.to),
|
||||||
|
date: parsed.date || new Date()
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error decoding email:', error);
|
||||||
|
throw new Error('Failed to decode email');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function cleanHtml(html: string): string {
|
||||||
|
if (!html) return '';
|
||||||
|
|
||||||
|
// Detect text direction from the content
|
||||||
|
const hasRtlChars = /[\u0591-\u07FF\u200F\u202B\u202E\uFB1D-\uFDFD\uFE70-\uFEFC]/.test(html);
|
||||||
|
const defaultDir = hasRtlChars ? 'rtl' : 'ltr';
|
||||||
|
|
||||||
|
// Basic HTML cleaning while preserving structure
|
||||||
|
const cleaned = html
|
||||||
|
// 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, '')
|
||||||
|
// Clean up whitespace
|
||||||
|
.replace(/\s+/g, ' ')
|
||||||
|
.trim();
|
||||||
|
|
||||||
|
// Wrap in a div with the detected direction
|
||||||
|
return `<div dir="${defaultDir}">${cleaned}</div>`;
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user