207 lines
6.3 KiB
TypeScript
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>`;
|
|
}
|
|
}
|