mirror of
https://github.com/Mail-0/Zero.git
synced 2026-07-01 08:16:28 +00:00
# Email Content Prefetching and Processing Optimization ## Description This PR improves email loading performance by implementing prefetching and caching of processed email HTML content. It splits the email processing logic into two parts: 1. Server-side preprocessing that handles sanitization and structure 2. Client-side processing that applies theme-specific styling and image loading preferences The changes also add prefetching of the latest message in a thread to improve perceived loading speed when users open emails. --- ## Type of Change - [x] ⚡ Performance improvement - [x] 🎨 UI/UX improvement ## Areas Affected - [x] Email Integration (Gmail, IMAP, etc.) - [x] User Interface/Experience - [x] Performance Optimization ## Testing Done - [x] Manual testing performed - [x] Cross-browser testing (if UI changes) ## Checklist - [x] I have performed a self-review of my code - [x] My changes generate no new warnings - [x] My code follows the project's style guidelines ## Additional Notes The email processing logic has been refactored to: 1. Separate heavy sanitization work (which can be done once) from theme/preference application 2. Cache processed content with a 30-minute stale time and 1-hour garbage collection time 3. Respect user preferences for external image loading and trusted senders 4. Apply theme-specific styling based on user settings or system preference This should significantly improve the perceived performance when opening emails, especially for threads with complex HTML content. --- _By submitting this pull request, I confirm that my contribution is made under the terms of the project's license._ <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Improved email thread view by displaying the latest non-draft message. * Enhanced email content processing to apply user settings and theme preferences, including external image loading and dark/light mode support. * **Bug Fixes** * More accurate handling of external images and theme styling in emails based on user preferences. * **Chores** * Updated internal configuration for local development environment. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
209 lines
5.3 KiB
TypeScript
209 lines
5.3 KiB
TypeScript
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 = `<details class="quoted-toggle" style="margin-top:1em;">
|
|
<summary style="cursor:pointer;" data-theme-color="muted">
|
|
Show quoted text
|
|
</summary>
|
|
${innerHtml}
|
|
</details>`;
|
|
|
|
$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(`<span style="display:none;"><!-- blocked image: ${src} --></span>`);
|
|
}
|
|
});
|
|
}
|
|
|
|
const html = $.html();
|
|
|
|
// Apply theme-specific styles
|
|
const themeStyles = `
|
|
<style type="text/css">
|
|
:host {
|
|
display: block;
|
|
line-height: 1.5;
|
|
background-color: ${isDarkTheme ? '#1A1A1A' : '#ffffff'};
|
|
color: ${isDarkTheme ? '#ffffff' : '#000000'};
|
|
}
|
|
|
|
*, *::before, *::after {
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
body {
|
|
margin: 0;
|
|
padding: 0;
|
|
}
|
|
|
|
a {
|
|
cursor: pointer;
|
|
color: ${isDarkTheme ? '#60a5fa' : '#2563eb'};
|
|
text-decoration: underline;
|
|
}
|
|
|
|
table {
|
|
border-collapse: collapse;
|
|
}
|
|
|
|
::selection {
|
|
background: #b3d4fc;
|
|
text-shadow: none;
|
|
}
|
|
|
|
/* Styling for collapsed quoted text */
|
|
details.quoted-toggle {
|
|
border-left: 2px solid ${isDarkTheme ? '#374151' : '#d1d5db'};
|
|
padding-left: 8px;
|
|
margin-top: 0.75rem;
|
|
}
|
|
|
|
details.quoted-toggle summary {
|
|
cursor: pointer;
|
|
color: ${isDarkTheme ? '#9CA3AF' : '#6B7280'};
|
|
list-style: none;
|
|
user-select: none;
|
|
}
|
|
|
|
details.quoted-toggle summary::-webkit-details-marker {
|
|
display: none;
|
|
}
|
|
|
|
[data-theme-color="muted"] {
|
|
color: ${isDarkTheme ? '#9CA3AF' : '#6B7280'};
|
|
}
|
|
</style>
|
|
`;
|
|
|
|
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);
|
|
}
|