Neah/lib/mail-parser-wrapper.ts
2025-04-26 11:27:01 +02:00

207 lines
6.3 KiB
TypeScript

'use client';
import DOMPurify from 'dompurify';
export interface ParsedEmail {
subject: string | null;
from: string | null;
to: string | null;
cc: string | null;
bcc: string | null;
date: Date | null;
html: string | null;
text: string | null;
attachments: Array<{
filename: string;
contentType: string;
size: number;
}>;
headers: Record<string, any>;
}
export async function decodeEmail(emailContent: string): Promise<ParsedEmail> {
try {
// Ensure the email content is properly formatted
const formattedContent = emailContent?.trim();
if (!formattedContent) {
return {
subject: null,
from: null,
to: null,
cc: null,
bcc: null,
date: null,
html: null,
text: 'No content available',
attachments: [],
headers: {}
};
}
const response = await fetch('/api/parse-email', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ email: formattedContent }),
});
const data = await response.json();
if (!response.ok) {
console.error('API Error:', data);
return {
subject: null,
from: null,
to: null,
cc: null,
bcc: null,
date: null,
html: null,
text: data.error || 'Failed to parse email',
attachments: [],
headers: {}
};
}
// If we have a successful response but no content
if (!data.html && !data.text) {
return {
...data,
date: data.date ? new Date(data.date) : null,
html: null,
text: 'No content available',
attachments: data.attachments || [],
headers: data.headers || {}
};
}
return {
...data,
date: data.date ? new Date(data.date) : null,
text: data.text || null,
html: data.html || null,
attachments: data.attachments || [],
headers: data.headers || {}
};
} catch (error) {
console.error('Error parsing email:', error);
return {
subject: null,
from: null,
to: null,
cc: null,
bcc: null,
date: null,
html: null,
text: 'Error parsing email content',
attachments: [],
headers: {}
};
}
}
/**
* Cleans HTML content by removing potentially harmful elements while preserving styling.
* This is the centralized HTML sanitization function to be used across the application.
* @param html HTML content to sanitize
* @param options Optional configuration
* @returns Sanitized HTML
*/
export function cleanHtml(html: string, options: {
preserveStyles?: boolean;
scopeStyles?: boolean;
addWrapper?: boolean;
} = {}): string {
if (!html) return '';
try {
const defaultOptions = {
preserveStyles: true,
scopeStyles: true,
addWrapper: true,
...options
};
// Extract style tags if we're preserving them
const styleTagsContent: string[] = [];
let processedHtml = html;
if (defaultOptions.preserveStyles) {
processedHtml = html.replace(/<style[^>]*>([\s\S]*?)<\/style>/gi, (match, styleContent) => {
styleTagsContent.push(styleContent);
return ''; // Remove style tags temporarily
});
}
// Process the HTML content
processedHtml = processedHtml
// Remove potentially harmful elements and attributes
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
.replace(/<iframe\b[^<]*(?:(?!<\/iframe>)<[^<]*)*<\/iframe>/gi, '')
.replace(/<object\b[^<]*(?:(?!<\/object>)<[^<]*)*<\/object>/gi, '')
.replace(/<embed\b[^<]*(?:(?!<\/embed>)<[^<]*)*<\/embed>/gi, '')
.replace(/<form\b[^<]*(?:(?!<\/form>)<[^<]*)*<\/form>/gi, '')
.replace(/on\w+="[^"]*"/gi, '') // Remove inline event handlers (onclick, onload, etc.)
.replace(/on\w+='[^']*'/gi, '')
// Fix self-closing tags that might break React
.replace(/<(br|hr|img|input|link|meta|area|base|col|embed|keygen|param|source|track|wbr)([^>]*)>/gi, '<$1$2 />');
// If we're scoping styles, prefix classes
if (defaultOptions.scopeStyles) {
processedHtml = processedHtml.replace(/class=["']([^"']*)["']/gi, (match, classContent) => {
const classes = classContent.split(/\s+/).map((cls: string) => `email-forwarded-${cls}`).join(' ');
return `class="${classes}"`;
});
}
// Add scoped styles if needed
if (defaultOptions.preserveStyles && styleTagsContent.length > 0 && defaultOptions.scopeStyles) {
// Create a modified version of the styles that scope them to our container
const scopedStyles = styleTagsContent.map(style => {
// Replace CSS selectors to be scoped to our container
return style
// Add scope to class selectors
.replace(/(\.[a-zA-Z0-9_-]+)/g, '.email-forwarded-$1')
// Add scope to ID selectors
.replace(/(#[a-zA-Z0-9_-]+)/g, '#email-forwarded-$1')
// Fix any CSS that might break out
.replace(/@import/g, '/* @import */')
.replace(/@media/g, '/* @media */');
}).join('\n');
// Add the styles back in a scoped way
if (defaultOptions.addWrapper) {
return `
<div class="email-forwarded-content" style="position: relative; overflow: auto; max-width: 100%;">
<style type="text/css">
/* Scoped styles for forwarded email */
.email-forwarded-content {
/* Base containment */
font-family: Arial, sans-serif;
color: #333;
line-height: 1.5;
}
/* Original email styles (scoped) */
${scopedStyles}
</style>
${processedHtml}
</div>
`;
} else {
return `<style type="text/css">${scopedStyles}</style>${processedHtml}`;
}
}
// Just wrap the content if needed
if (defaultOptions.addWrapper) {
return `<div class="email-forwarded-content" style="position: relative; overflow: auto; max-width: 100%;">${processedHtml}</div>`;
}
return processedHtml;
} catch (error) {
console.error('Error cleaning HTML:', error);
// Return something safe in case of error
return `<div style="color: #666; font-style: italic;">Error processing HTML content</div>`;
}
}