courrier clean 2$

This commit is contained in:
alma 2025-04-26 20:38:34 +02:00
parent 5686b9fb7d
commit 26e72f4f73
4 changed files with 52 additions and 189 deletions

View File

@ -1,7 +1,6 @@
import { NextRequest, NextResponse } from 'next/server';
import { simpleParser } from 'mailparser';
import * as DOMPurify from 'isomorphic-dompurify';
import { cleanHtml as cleanHtmlCentralized } from '@/lib/mail-parser-wrapper';
import { parseEmail } from '@/lib/server/email-parser';
import { sanitizeHtml } from '@/lib/utils/email-formatter';
interface EmailAddress {
name?: string;
@ -45,68 +44,29 @@ function getEmailAddresses(addresses: any): EmailAddress[] {
return [];
}
/**
* @deprecated This function is deprecated and will be removed in future versions.
* Use the cleanHtml function from '@/lib/mail-parser-wrapper' instead.
* Maintained for backward compatibility only.
*/
function processHtml(html: string): string {
if (!html) return '';
// Delegate to the centralized implementation
return cleanHtmlCentralized(html, {
preserveStyles: true,
scopeStyles: true,
addWrapper: true
});
}
export async function POST(req: NextRequest) {
export async function POST(request: NextRequest) {
try {
const { email } = await req.json();
const body = await request.json();
if (!email || typeof email !== 'string') {
if (!body.email) {
return NextResponse.json(
{ error: 'Invalid email content' },
{ error: 'Missing email content' },
{ status: 400 }
);
}
const parsedEmail = await parseEmail(body.email);
const parsed = await simpleParser(email);
// Process HTML content if available
if (parsedEmail.html) {
parsedEmail.html = sanitizeHtml(parsedEmail.html);
}
// Process the HTML content to make it safe and displayable
const html = parsed.html
? processHtml(parsed.html.toString())
: undefined;
const text = parsed.text
? parsed.text.toString()
: undefined;
// Extract attachments info if available
const attachments = parsed.attachments?.map(attachment => ({
filename: attachment.filename,
contentType: attachment.contentType,
contentDisposition: attachment.contentDisposition,
size: attachment.size
})) || [];
// Return all parsed email details
return NextResponse.json({
subject: parsed.subject,
from: getEmailAddresses(parsed.from),
to: getEmailAddresses(parsed.to),
cc: getEmailAddresses(parsed.cc),
bcc: getEmailAddresses(parsed.bcc),
date: parsed.date,
html,
text,
attachments
});
return NextResponse.json(parsedEmail);
} catch (error) {
console.error('Error parsing email:', error);
return NextResponse.json(
{ error: 'Failed to parse email content' },
{ error: 'Failed to parse email', details: error instanceof Error ? error.message : String(error) },
{ status: 500 }
);
}

View File

@ -1,3 +1,10 @@
/**
* @deprecated This entire file is deprecated and will be removed in future versions.
* Use the parseEmail function from lib/server/email-parser.ts and
* sanitizeHtml from lib/utils/email-formatter.ts instead.
* This file is maintained only for backward compatibility.
*/
interface EmailHeaders {
from: string;
subject: string;
@ -5,7 +12,12 @@ interface EmailHeaders {
to?: string;
}
/**
* @deprecated Use parseEmail from lib/server/email-parser.ts instead.
*/
export function parseEmailHeaders(headerContent: string): EmailHeaders {
console.warn('parseEmailHeaders is deprecated. Use parseEmail from lib/server/email-parser.ts instead.');
const headers: { [key: string]: string } = {};
let currentHeader = '';
let currentValue = '';
@ -48,7 +60,12 @@ export function parseEmailHeaders(headerContent: string): EmailHeaders {
};
}
/**
* @deprecated Use sanitizeHtml from lib/utils/email-formatter.ts instead.
*/
export function decodeEmailBody(content: string, contentType: string): string {
console.warn('decodeEmailBody is deprecated. Use sanitizeHtml from lib/utils/email-formatter.ts instead.');
try {
// Remove email client-specific markers
content = content.replace(/\r\n/g, '\n')
@ -68,7 +85,12 @@ export function decodeEmailBody(content: string, contentType: string): string {
}
}
/**
* @deprecated Use sanitizeHtml from lib/utils/email-formatter.ts instead.
*/
function extractTextFromHtml(html: string): string {
console.warn('extractTextFromHtml is deprecated. Use sanitizeHtml from lib/utils/email-formatter.ts instead.');
// Remove scripts and style tags
html = html.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '');

View File

@ -1,6 +1,7 @@
'use client';
import DOMPurify from 'dompurify';
import { sanitizeHtml } from '@/lib/utils/email-formatter';
export interface ParsedEmail {
subject: string | null;
@ -99,119 +100,4 @@ export async function decodeEmail(emailContent: string): Promise<ParsedEmail> {
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.
*
* Key features:
* - Safely removes scripts, iframes, and other potentially harmful elements
* - Optionally preserves and scopes CSS styles to prevent them from affecting the rest of the page
* - Fixes self-closing tags that might break in React or contentEditable contexts
* - Can add a wrapper div with isolation for additional safety
*
* @param html HTML content to sanitize
* @param options Configuration options:
* - preserveStyles: Whether to keep <style> tags (default: true)
* - scopeStyles: Whether to scope CSS to prevent leakage into the rest of the page (default: true)
* - addWrapper: Whether to add a container div with CSS isolation (default: true)
* @returns Sanitized HTML that can be safely inserted into the document
*/
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>`;
}
}

View File

@ -1,22 +1,20 @@
import { sanitizeHtml } from '@/lib/utils/email-formatter';
import { simpleParser } from 'mailparser';
import { cleanHtml as cleanHtmlCentralized } from '@/lib/mail-parser-wrapper';
/**
* @deprecated This function is deprecated and will be removed in future versions.
* Use the centralized cleanHtml function from '@/lib/mail-parser-wrapper.ts instead.
* This is maintained only for backward compatibility.
*/
export function cleanHtml(html: string): string {
console.warn('The cleanHtml function in lib/server/email-parser.ts is deprecated. Use the one in lib/mail-parser-wrapper.ts');
return cleanHtmlCentralized(html, { preserveStyles: true, scopeStyles: false });
}
function getAddressText(address: any): string | null {
if (!address) return null;
if (Array.isArray(address)) {
return address.map(addr => addr.value?.[0]?.address || '').filter(Boolean).join(', ');
function getAddressText(addresses: any): string | null {
if (!addresses) return null;
try {
if (Array.isArray(addresses)) {
return addresses.map(a => a.address || '').join(', ');
} else if (typeof addresses === 'object') {
return addresses.address || null;
}
return null;
} catch (error) {
console.error('Error formatting addresses:', error);
return null;
}
return address.value?.[0]?.address || null;
}
export async function parseEmail(emailContent: string) {
@ -30,10 +28,7 @@ export async function parseEmail(emailContent: string) {
cc: getAddressText(parsed.cc),
bcc: getAddressText(parsed.bcc),
date: parsed.date || null,
html: parsed.html ? cleanHtmlCentralized(parsed.html as string, {
preserveStyles: true,
scopeStyles: false
}) : null,
html: parsed.html ? sanitizeHtml(parsed.html as string) : null,
text: parsed.text || null,
attachments: parsed.attachments || [],
headers: Object.fromEntries(parsed.headers)