From 5a3f9faafebfa09d0898918788d6e8d7e1d335bd Mon Sep 17 00:00:00 2001 From: Linus Rath Date: Wed, 18 Mar 2026 16:10:47 +0100 Subject: [PATCH 1/3] fix: update dompurify to version 3.3.3 and elliptic to version 6.6.1, add undici override --- package-lock.json | 31 ++++++++++++++++--------------- package.json | 6 +++++- 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7e0e19d1..e319266a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "asn1js": "^3.0.7", "clsx": "^2.1.1", "date-fns": "^4.1.0", - "dompurify": "^3.3.1", + "dompurify": "^3.3.3", "lucide-react": "^0.575.0", "next": "^16.1.5", "next-intl": "^4.5.8", @@ -4731,9 +4731,9 @@ "license": "MIT" }, "node_modules/dompurify": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.1.tgz", - "integrity": "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.3.tgz", + "integrity": "sha512-Oj6pzI2+RqBfFG+qOaOLbFXLQ90ARpcGG6UePL82bJLtdsa6CYJD7nmiU8MW9nQNOtCHV3lZ/Bzq1X0QYbBZCA==", "license": "(MPL-2.0 OR Apache-2.0)", "optionalDependencies": { "@types/trusted-types": "^2.0.7" @@ -4762,17 +4762,18 @@ "license": "ISC" }, "node_modules/elliptic": { - "version": "6.5.0", - "resolved": "git+ssh://git@github.com/mahrud/elliptic.git#75637c76678e83c31682fd967c2fa9ff4761b3fc", + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.1.tgz", + "integrity": "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==", "license": "MIT", "dependencies": { - "bn.js": "^4.4.0", - "brorand": "^1.0.1", + "bn.js": "^4.11.9", + "brorand": "^1.1.0", "hash.js": "^1.0.0", - "hmac-drbg": "^1.0.0", - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0", - "minimalistic-crypto-utils": "^1.0.0" + "hmac-drbg": "^1.0.1", + "inherits": "^2.0.4", + "minimalistic-assert": "^1.0.1", + "minimalistic-crypto-utils": "^1.0.1" } }, "node_modules/enhanced-resolve": { @@ -8656,9 +8657,9 @@ } }, "node_modules/undici": { - "version": "7.22.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.22.0.tgz", - "integrity": "sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg==", + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.4.tgz", + "integrity": "sha512-BM/JzwwaRXxrLdElV2Uo6cTLEjhSb3WXboncJamZ15NgUURmvlXvxa6xkwIOILIjPNo9i8ku136ZvWV0Uly8+w==", "dev": true, "license": "MIT", "engines": { diff --git a/package.json b/package.json index b0c1efd1..92e7aecd 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "asn1js": "^3.0.7", "clsx": "^2.1.1", "date-fns": "^4.1.0", - "dompurify": "^3.3.1", + "dompurify": "^3.3.3", "lucide-react": "^0.575.0", "next": "^16.1.5", "next-intl": "^4.5.8", @@ -74,5 +74,9 @@ "tailwindcss": "^4.1.17", "typescript": "^5.9.3", "vitest": "^4.0.16" + }, + "overrides": { + "elliptic": "^6.6.1", + "undici": "^7.24.0" } } From 2b4ff2f3de28ab697db180e978bce5aab0f09e76 Mon Sep 17 00:00:00 2001 From: Linus Rath Date: Wed, 18 Mar 2026 16:33:55 +0100 Subject: [PATCH 2/3] fix: remove debugMode usage from EmailViewer component --- components/email/email-viewer.tsx | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/components/email/email-viewer.tsx b/components/email/email-viewer.tsx index 89ed732a..657d206e 100644 --- a/components/email/email-viewer.tsx +++ b/components/email/email-viewer.tsx @@ -817,7 +817,6 @@ export function EmailViewer({ const addTrustedSender = useSettingsStore((state) => state.addTrustedSender); const isSenderTrusted = useSettingsStore((state) => state.isSenderTrusted); const emailKeywords = useSettingsStore((state) => state.emailKeywords); - const debugMode = useSettingsStore((state) => state.debugMode); const toolbarPosition = useSettingsStore((state) => state.toolbarPosition); const showToolbarLabels = useSettingsStore((state) => state.showToolbarLabels); const calendarInvitationParsingEnabled = useSettingsStore((state) => state.calendarInvitationParsingEnabled); @@ -1081,21 +1080,19 @@ export function EmailViewer({ if (!email || !client) return; const smimeDebug = (...args: unknown[]) => { - if (debugMode) { + if (useSettingsStore.getState().debugMode) { console.debug(...args); } }; const smimeWarn = (...args: unknown[]) => { - if (debugMode) { + if (useSettingsStore.getState().debugMode) { console.warn(...args); } }; const smimeError = (...args: unknown[]) => { - if (debugMode) { - console.error(...args); - } + console.error(...args); }; const rawContentType = email.headers?.['content-type'] || email.headers?.['Content-Type']; @@ -1336,7 +1333,7 @@ export function EmailViewer({ candidates: candidateSummaries, }); - if (debugMode && typeof window !== 'undefined') { + if (useSettingsStore.getState().debugMode && typeof window !== 'undefined') { const debugPayload = { emailId: email!.id, detection, @@ -1616,7 +1613,6 @@ export function EmailViewer({ }, [ email, client, - debugMode, prepareSmimeUnlock, smimeStore.autoImportSignerCerts, smimeStore.keyRecords, From a57492d7c0ad3af854c62764ebf66bb038a0e964 Mon Sep 17 00:00:00 2001 From: Linus Rath Date: Wed, 18 Mar 2026 16:56:24 +0100 Subject: [PATCH 3/3] fix: refactor overflow handling in EmailViewer component to use hidden priorities and layout effects --- components/email/email-viewer.tsx | 105 +++++++++++++++++++++--------- 1 file changed, 73 insertions(+), 32 deletions(-) diff --git a/components/email/email-viewer.tsx b/components/email/email-viewer.tsx index 657d206e..632df8bb 100644 --- a/components/email/email-viewer.tsx +++ b/components/email/email-viewer.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useEffect, useMemo, useRef, useCallback } from "react"; +import { useState, useEffect, useLayoutEffect, useMemo, useRef, useCallback } from "react"; import ReactDOM from "react-dom"; import DOMPurify from "dompurify"; import { Email, ContactCard, Mailbox } from "@/lib/jmap/types"; @@ -853,7 +853,7 @@ export function EmailViewer({ const tagMenuRef = useRef(null); const moveMenuRef = useRef(null); const toolbarRef = useRef(null); - const [overflowCount, setOverflowCount] = useState(0); + const [hiddenPriorities, setHiddenPriorities] = useState>(new Set()); const currentColor = getCurrentColor(email?.keywords); // S/MIME state @@ -872,7 +872,7 @@ export function EmailViewer({ const [tnefAttachments, setTnefAttachments] = useState([]); // Ensure S/MIME key records are loaded from IndexedDB - useEffect(() => { + useLayoutEffect(() => { smimeStore.load(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -946,10 +946,17 @@ export function EmailViewer({ useEffect(() => { const el = toolbarRef.current; if (!el) return; + + let rafId: number | null = null; + const calculate = () => { + rafId = null; const items = Array.from(el.querySelectorAll('[data-overflow-item]')); - if (items.length === 0) return; - // Sort descending by priority so highest number (least important) is first + if (items.length === 0) { + setHiddenPriorities(prev => prev.size === 0 ? prev : new Set()); + return; + } + // Sort descending by priority so highest number (least important) is hidden first items.sort((a, b) => Number(b.dataset.overflowPriority || 0) - Number(a.dataset.overflowPriority || 0) ); @@ -964,7 +971,7 @@ export function EmailViewer({ rightGroup.style.flexShrink = '0'; el.style.overflow = 'hidden'; // Iteratively hide items until content fits - let count = 0; + const hidden = new Set(); const isOverflowing = () => leftGroup.scrollWidth + rightGroup.scrollWidth + mainGap > containerWidth + 1; for (const item of items) { @@ -972,18 +979,54 @@ export function EmailViewer({ // Skip items already hidden by CSS (e.g., on mobile) if (item.offsetWidth === 0) continue; item.style.display = 'none'; - count++; + hidden.add(Number(item.dataset.overflowPriority)); } // Restore layout leftGroup.style.flexShrink = ''; rightGroup.style.flexShrink = ''; el.style.overflow = ''; - setOverflowCount(prev => prev === count ? prev : count); + setHiddenPriorities(prev => { + if (prev.size === hidden.size && [...hidden].every(p => prev.has(p))) return prev; + return hidden; + }); }; - const observer = new ResizeObserver(calculate); - observer.observe(el); - return () => observer.disconnect(); - }, [toolbarPosition]); + + const scheduleCalculate = () => { + if (rafId !== null) cancelAnimationFrame(rafId); + rafId = requestAnimationFrame(calculate); + }; + + // Recalculate on container resize + const resizeObserver = new ResizeObserver(scheduleCalculate); + resizeObserver.observe(el); + + // Recalculate when children change (conditional items, label visibility) + const mutationObserver = new MutationObserver(scheduleCalculate); + mutationObserver.observe(el, { childList: true, subtree: true }); + + // Initial synchronous calculation to avoid flash + calculate(); + + return () => { + if (rafId !== null) cancelAnimationFrame(rafId); + resizeObserver.disconnect(); + mutationObserver.disconnect(); + }; + }, [ + toolbarPosition, + email?.id, + showToolbarLabels, + isLoading, + moveTree.length, + colorOptions.length, + currentColor, + isInJunkFolder, + isTablet, + tabletListVisible, + onBack, + onMarkAsSpam, + onUndoSpam, + ]); // Contact sidebar state const [contactSidebarEmail, setContactSidebarEmail] = useState(null); @@ -1632,18 +1675,16 @@ export function EmailViewer({ debug.group('TNEF Processing'); debug.log('Found TNEF attachment:', tnefAtt.name, 'type:', tnefAtt.type, 'blobId:', tnefAtt.blobId, 'size:', tnefAtt.size); - // Only process if the email has no usable HTML body + // Check if the email already has a usable HTML body const hasHtmlBody = !!( email.htmlBody?.[0]?.partId && email.bodyValues?.[email.htmlBody[0].partId]?.value?.trim() ); if (hasHtmlBody) { - debug.log('TNEF: Email already has HTML body, skipping TNEF extraction'); - debug.log(' HTML partId:', email.htmlBody?.[0]?.partId, 'body length:', email.bodyValues?.[email.htmlBody![0].partId]?.value?.length); - debug.groupEnd(); - return; + debug.log('TNEF: Email already has HTML body, will extract attachments only'); + } else { + debug.log('TNEF: Email has no HTML body, proceeding with full TNEF extraction'); } - debug.log('TNEF: Email has no HTML body, proceeding with TNEF extraction'); let cancelled = false; @@ -1678,10 +1719,10 @@ export function EmailViewer({ debug.log('TNEF parse result — htmlBody:', !!parsed.htmlBody, '(' + (parsed.htmlBody?.length ?? 0) + ' chars)', ', body:', !!parsed.body, '(' + (parsed.body?.length ?? 0) + ' chars)', ', attachments:', parsed.attachments.length); - if (parsed.htmlBody) { + if (parsed.htmlBody && !hasHtmlBody) { setTnefHtml(parsed.htmlBody); } - if (parsed.body) { + if (parsed.body && !hasHtmlBody) { setTnefText(parsed.body); } if (parsed.attachments.length > 0) { @@ -1786,8 +1827,8 @@ export function EmailViewer({ } const jmapAttachments = (email?.attachments ?? []) - // Hide winmail.dat when we have successfully extracted TNEF content - .filter(att => !(tnefHtml || tnefText) || !isTnefAttachment(att.name, att.type)) + // Hide winmail.dat when we have successfully extracted TNEF content or attachments + .filter(att => !(tnefHtml || tnefText || tnefAttachments.length > 0) || !isTnefAttachment(att.name, att.type)) .map((attachment, index) => ({ id: attachment.blobId || `${attachment.name || 'attachment'}-${index}`, name: attachment.name || null, @@ -2733,7 +2774,7 @@ export function EmailViewer({ {/* Overflow: reply */} {/* Overflow: move to folder — submenu */} {moveTree.length > 0 && onMoveToMailbox && ( -
= 6 ? "" : "sm:hidden")} +
setMoreMenuSub('move')} onMouseLeave={() => setMoreMenuSub(null)} > @@ -2816,7 +2857,7 @@ export function EmailViewer({ )} {/* Overflow: tag — submenu */} {colorOptions.length > 0 && ( -
= 5 ? "" : "sm:hidden")} +
setMoreMenuSub('tag')} onMouseLeave={() => setMoreMenuSub(null)} > @@ -2864,7 +2905,7 @@ export function EmailViewer({ {(onMarkAsSpam || onUndoSpam) && (