import sanitizeHtml from 'sanitize-html'; import * as cheerio from 'cheerio'; interface ProcessEmailOptions { html: string; shouldLoadImages: boolean; theme: 'light' | 'dark'; } // Server-side: Heavy lifting, preference-independent processing export function preprocessEmailHtml(html: string): string { const sanitizeConfig: sanitizeHtml.IOptions = { allowedTags: sanitizeHtml.defaults.allowedTags.concat(['img', 'title', 'details', 'summary']), allowedAttributes: { '*': [ 'class', 'style', 'align', 'valign', 'width', 'height', 'cellpadding', 'cellspacing', 'border', 'bgcolor', 'colspan', 'rowspan', ], a: ['href', 'name', 'target', 'rel', 'class', 'style'], img: ['src', 'alt', 'width', 'height', 'class', 'style'], }, // Allow only safe schemes - no blob for security allowedSchemes: ['http', 'https', 'mailto', 'tel', 'data', 'cid'], allowedSchemesByTag: { img: ['http', 'https', 'data', 'cid'], }, transformTags: { a: (tagName, attribs) => { return { tagName, attribs: { ...attribs, target: attribs.target || '_blank', rel: 'noopener noreferrer', }, }; }, }, }; const sanitized = sanitizeHtml(html, sanitizeConfig); const $ = cheerio.load(sanitized); // Collapse quoted text (structure only, no theme colors) const collapseQuoted = (selector: string) => { $(selector).each((_, el) => { const $el = $(el); if ($el.parents('details.quoted-toggle').length) return; const innerHtml = $el.html(); if (typeof innerHtml !== 'string') return; const detailsHtml = `
Show quoted text ${innerHtml}
`; $el.replaceWith(detailsHtml); }); }; collapseQuoted('blockquote'); collapseQuoted('.gmail_quote'); // Remove unwanted elements $('title').remove(); $('img[width="1"][height="1"]').remove(); $('img[width="0"][height="0"]').remove(); // Remove preheader content $('.preheader, .preheaderText, [class*="preheader"]').each((_, el) => { const $el = $(el); const style = $el.attr('style') || ''; if ( style.includes('display:none') || style.includes('display: none') || style.includes('font-size:0') || style.includes('font-size: 0') || style.includes('line-height:0') || style.includes('line-height: 0') || style.includes('max-height:0') || style.includes('max-height: 0') || style.includes('mso-hide:all') || style.includes('opacity:0') || style.includes('opacity: 0') ) { $el.remove(); } }); return $.html(); } // Client-side: Light styling + image preferences export function applyEmailPreferences( preprocessedHtml: string, theme: 'light' | 'dark', shouldLoadImages: boolean ): { processedHtml: string; hasBlockedImages: boolean } { let hasBlockedImages = false; const isDarkTheme = theme === 'dark'; const $ = cheerio.load(preprocessedHtml); // Handle image blocking if needed if (!shouldLoadImages) { $('img').each((_, el) => { const $img = $(el); const src = $img.attr('src'); // Allow CID images (inline attachments) if (src && !src.startsWith('cid:')) { hasBlockedImages = true; $img.replaceWith(``); } }); } const html = $.html(); // Apply theme-specific styles const themeStyles = ` `; const finalHtml = `${themeStyles}${html}`; return { processedHtml: finalHtml, hasBlockedImages, }; } // Original function for backward compatibility export function processEmailHtml({ html, shouldLoadImages, theme }: ProcessEmailOptions): { processedHtml: string; hasBlockedImages: boolean; } { const preprocessed = preprocessEmailHtml(html); return applyEmailPreferences(preprocessed, theme, shouldLoadImages); }