From b1b09d54c8c365b28a3dac90069314bc66823953 Mon Sep 17 00:00:00 2001 From: Steven RYDELL Date: Sun, 5 Jul 2026 19:18:54 +0200 Subject: [PATCH 1/8] feat(pwa): add Apple Touch icons for iOS home screen - Add apple-touch-icon.png (180x180) - default iOS icon - Add apple-touch-icon-120x120.png - iPhone retina - Add apple-touch-icon-152x152.png - iPad retina - Add apple-touch-icon-167x167.png - iPad Pro - Add apple-touch-icon-180x180.png - iPhone Plus/Pro iOS automatically detects these icons when users add the PWA to their home screen. No code changes required - icons are discovered by convention. Fixes: PWA icon not appearing on iOS home screen Co-authored-by: Cursor --- public/apple-touch-icon-120x120.png | Bin 0 -> 4397 bytes public/apple-touch-icon-152x152.png | Bin 0 -> 6260 bytes public/apple-touch-icon-167x167.png | Bin 0 -> 7222 bytes public/apple-touch-icon-180x180.png | Bin 0 -> 6587 bytes public/apple-touch-icon.png | Bin 0 -> 6587 bytes 5 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 public/apple-touch-icon-120x120.png create mode 100644 public/apple-touch-icon-152x152.png create mode 100644 public/apple-touch-icon-167x167.png create mode 100644 public/apple-touch-icon-180x180.png create mode 100644 public/apple-touch-icon.png diff --git a/public/apple-touch-icon-120x120.png b/public/apple-touch-icon-120x120.png new file mode 100644 index 0000000000000000000000000000000000000000..ff841ef4bd74e0d7bf81b9681e5305529d7f1c2f GIT binary patch literal 4397 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 IQn3yB9}@I=CjbBd literal 0 HcmV?d00001 diff --git a/public/apple-touch-icon-152x152.png b/public/apple-touch-icon-152x152.png new file mode 100644 index 0000000000000000000000000000000000000000..e7732130cfdd2e8f6c250c1d5199fdd9a86771c6 GIT binary patch literal 6260 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 literal 0 HcmV?d00001 diff --git a/public/apple-touch-icon-180x180.png b/public/apple-touch-icon-180x180.png new file mode 100644 index 0000000000000000000000000000000000000000..6603576b3852d0e644b07c4a01a6c6df44f8b61f GIT binary patch literal 6587 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 literal 0 HcmV?d00001 diff --git a/public/apple-touch-icon.png b/public/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..6603576b3852d0e644b07c4a01a6c6df44f8b61f GIT binary patch literal 6587 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 literal 0 HcmV?d00001 From 0d73cb5dfb3ab37e993b51d49d8b5d14cd479ef5 Mon Sep 17 00:00:00 2001 From: Shuki Vaknin Date: Tue, 30 Jun 2026 18:41:34 +0300 Subject: [PATCH 2/8] fix(csp): allow external/data fonts so email webfonts render The page CSP set font-src 'self', which blocked fonts referenced by rendered email CSS (brand webfonts loaded over https). Allow https:/data: for font-src. Email bodies render inside the sandboxed iframe, which keeps its own stricter blocking-mode font-src for privacy. --- proxy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proxy.ts b/proxy.ts index c108e2f9..f0905645 100644 --- a/proxy.ts +++ b/proxy.ts @@ -112,7 +112,7 @@ export async function proxy(request: NextRequest) { `script-src ${scriptSrc}`, `style-src 'self' 'unsafe-inline'`, `img-src 'self' data: blob: https:`, - `font-src 'self'`, + `font-src 'self' https: data:`, `connect-src ${connectSrc}`, frameSrc, `object-src 'self' blob:`, From 6b74615969fa6f4352f0325c07fa46d5e42b38bc Mon Sep 17 00:00:00 2001 From: Shuki Vaknin Date: Thu, 25 Jun 2026 22:18:47 +0300 Subject: [PATCH 3/8] fix(email-viewer): hide images that fail to load MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Instead of leaving the browser's broken-image placeholder + alt text (which reads as stray label text — e.g. a 'logo' alt — in an otherwise image-only email), hide any image that fails to load. Sanitizer-blocked images already use a 1x1 transparent pixel with display:none, so they're unaffected. --- components/email/email-viewer.tsx | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/components/email/email-viewer.tsx b/components/email/email-viewer.tsx index ccc7b778..31f5d982 100644 --- a/components/email/email-viewer.tsx +++ b/components/email/email-viewer.tsx @@ -2172,6 +2172,26 @@ export function EmailViewer({ lastBodyHeightRef.current = initialHeight; setIframeReady(true); + // Hide images that fail to load (dead/mixed-content/unreachable external + // URLs) rather than leaving the browser's broken-image placeholder and + // alt text, which read as stray label text in an otherwise image-only + // email (e.g. a blocked "logo" alt). Blocked images already carry a 1x1 + // transparent pixel (naturalWidth 1) and display:none, so they're skipped. + const hideIfBroken = (img: HTMLImageElement) => { + if (img.complete && img.naturalWidth === 0 && img.getAttribute('src')) { + img.style.display = 'none'; + } + }; + doc.querySelectorAll('img').forEach((el) => { + const img = el as HTMLImageElement; + if (img.complete) { + hideIfBroken(img); + } else { + img.addEventListener('error', () => { img.style.display = 'none'; }, { once: true }); + img.addEventListener('load', () => hideIfBroken(img), { once: true }); + } + }); + // Make links open in new tab doc.querySelectorAll('a').forEach(a => { a.setAttribute('target', '_blank'); From 06ddda688d2aabe7d7f129879ce45507dc8cf594 Mon Sep 17 00:00:00 2001 From: dealerweb Date: Mon, 6 Jul 2026 10:43:16 +0200 Subject: [PATCH 4/8] Fix: spam actions left folder counters and the open message stale Marking mail as spam (or not spam) updated the email list but nothing else, unlike delete/move which patch the folder counters optimistically. Without a connected JMAP push the sidebar badges simply never moved, and "not spam" additionally left the reading pane stuck on the message that had just left the folder. markAsSpam now mirrors moveToMailbox: source folder counts down, junk counts up (honoring that trash-and-read delivers the mail to junk already read). batchMarkAsSpam and batchUndoSpam mirror the batch move pattern the same way. undoSpam advances the selection to the next message like markAsSpam already did, and refreshes the mailbox list instead of patching counts: in the undo-toast path the email is no longer in the list, so its unread state is unknown and an optimistic patch is not possible. --- stores/email-store.ts | 147 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 133 insertions(+), 14 deletions(-) diff --git a/stores/email-store.ts b/stores/email-store.ts index 7e3b23ab..e5c4da4e 100644 --- a/stores/email-store.ts +++ b/stores/email-store.ts @@ -2278,10 +2278,44 @@ export const useEmailStore = create((set, get) => ({ const alsoMarkRead = useSettingsStore.getState().deleteAction === 'trash-and-read' && isUnread; await actionClient.markAsSpam(emailId, accountId, alsoMarkRead); - set(state => ({ - emails: state.emails.filter(e => e.id !== emailId), - selectedEmail: getNextSelectedEmail(state, emailId), - })); + // Same junk resolution the client used for the move itself. + const junkMailbox = mailboxes.find(m => + accountId ? (m.role === 'junk' && m.accountId === accountId) : (m.role === 'junk' && !m.isShared) + ); + // After marking read in the same request, the email arrives in junk as read. + const arrivesUnread = isUnread && !alsoMarkRead; + + set(state => { + // Counter changes go to the email's own account list; source and junk + // live in the same account. (#281) + const mailboxPatch = applyMailboxCounterUpdate(state, email, (mailbox) => { + if (emailInMailbox(email, mailbox)) { + return { + ...mailbox, + totalEmails: Math.max(0, mailbox.totalEmails - 1), + unreadEmails: isUnread ? Math.max(0, mailbox.unreadEmails - 1) : mailbox.unreadEmails, + totalThreads: Math.max(0, mailbox.totalThreads - 1), + unreadThreads: isUnread ? Math.max(0, mailbox.unreadThreads - 1) : mailbox.unreadThreads, + }; + } + if (junkMailbox && mailbox.id === junkMailbox.id) { + return { + ...mailbox, + totalEmails: mailbox.totalEmails + 1, + unreadEmails: arrivesUnread ? mailbox.unreadEmails + 1 : mailbox.unreadEmails, + totalThreads: mailbox.totalThreads + 1, + unreadThreads: arrivesUnread ? mailbox.unreadThreads + 1 : mailbox.unreadThreads, + }; + } + return mailbox; + }); + + return { + emails: state.emails.filter(e => e.id !== emailId), + selectedEmail: getNextSelectedEmail(state, emailId), + ...mailboxPatch, + }; + }); } catch (error) { console.error('Failed to mark as spam:', error); throw error; @@ -2340,6 +2374,15 @@ export const useEmailStore = create((set, get) => ({ try { await undoClient.undoSpam(emailId, targetMailboxId, accountId); + + // Drop the restored mail from the current junk view and advance the open + // message to the next one, mirroring markAsSpam. Without this the viewer + // stays stuck on the mail that just left the folder. + set(state => ({ + emails: state.emails.filter(e => e.id !== emailId), + selectedEmail: getNextSelectedEmail(state, emailId), + })); + // Refresh the view the user is actually looking at. if (get().isUnifiedView && get().crossView) { const includeGroup = useSettingsStore.getState().includeGroupInUnified; @@ -2352,6 +2395,11 @@ export const useEmailStore = create((set, get) => ({ } else { await get().fetchEmails(client, selectedMailbox); } + + // Folder counts don't move on their own without a reliable JMAP push + // (often absent on subpath/cross-origin deploys), so refresh them here so + // the junk folder's badge drops right away. + void get().fetchMailboxes(client); } catch (error) { console.error('Failed to restore email:', error); throw error; @@ -2375,11 +2423,52 @@ export const useEmailStore = create((set, get) => ({ await effectiveClient.markAsSpam(emailId, currentMailbox.accountId, markRead); } - set(state => ({ - emails: state.emails.filter(e => !emailIds.includes(e.id)), - selectedEmail: emailIds.includes(state.selectedEmail?.id || '') ? null : state.selectedEmail, - selectedEmailIds: new Set(), - })); + // Adjust counters and drop the emails from the current view. Junk is + // resolved the same way the client did for the move itself. + const affected = emails.filter(e => emailIds.includes(e.id)); + const junkMailbox = mailboxes.find(m => + currentMailbox.accountId + ? (m.role === 'junk' && m.accountId === currentMailbox.accountId) + : (m.role === 'junk' && !m.isShared) + ); + let junkTotal = 0; + let junkUnread = 0; + for (const e of affected) { + junkTotal += 1; + // After marking read in the same request, the email arrives in junk as read. + if (!e.keywords?.$seen && !alsoMarkRead) junkUnread += 1; + } + + set(state => { + // Decrement source folders per the email's own account list (#281). + const patch = applyBatchMailboxCounterUpdate(state, affected, (mb, group) => { + let dTotal = 0; + let dUnread = 0; + for (const e of group as Email[]) { + if (emailInMailbox(e, mb)) { + dTotal--; + if (!e.keywords?.$seen) dUnread--; + } + } + return dTotal === 0 && dUnread === 0 ? mb : { + ...mb, + totalEmails: Math.max(0, mb.totalEmails + dTotal), + unreadEmails: Math.max(0, mb.unreadEmails + dUnread), + }; + }); + // The junk folder (picked from the active/viewing list) gains them. + if (junkMailbox) { + patch.mailboxes = patch.mailboxes.map(mb => mb.id === junkMailbox.id + ? { ...mb, totalEmails: mb.totalEmails + junkTotal, unreadEmails: mb.unreadEmails + junkUnread } + : mb); + } + return { + emails: state.emails.filter(e => !emailIds.includes(e.id)), + selectedEmail: emailIds.includes(state.selectedEmail?.id || '') ? null : state.selectedEmail, + selectedEmailIds: new Set(), + ...patch, + }; + }); } catch (error) { console.error('Failed to batch mark as spam:', error); throw error; @@ -2409,11 +2498,41 @@ export const useEmailStore = create((set, get) => ({ await effectiveClient.undoSpam(emailId, inboxMailbox.originalId || inboxMailbox.id, accountId); } - set(state => ({ - emails: state.emails.filter(e => !emailIds.includes(e.id)), - selectedEmail: emailIds.includes(state.selectedEmail?.id || '') ? null : state.selectedEmail, - selectedEmailIds: new Set(), - })); + // Adjust counters and drop the restored emails from the junk view. + const affected = get().emails.filter(e => emailIds.includes(e.id)); + let unreadDelta = 0; + for (const e of affected) { + if (!e.keywords?.$seen) unreadDelta += 1; + } + + set(state => { + // Decrement source folders per the email's own account list (#281). + const patch = applyBatchMailboxCounterUpdate(state, affected, (mb, group) => { + let dTotal = 0; + let dUnread = 0; + for (const e of group as Email[]) { + if (emailInMailbox(e, mb)) { + dTotal--; + if (!e.keywords?.$seen) dUnread--; + } + } + return dTotal === 0 && dUnread === 0 ? mb : { + ...mb, + totalEmails: Math.max(0, mb.totalEmails + dTotal), + unreadEmails: Math.max(0, mb.unreadEmails + dUnread), + }; + }); + // The inbox (picked from the active/viewing list) gains them. + patch.mailboxes = patch.mailboxes.map(mb => mb.id === inboxMailbox.id + ? { ...mb, totalEmails: mb.totalEmails + affected.length, unreadEmails: mb.unreadEmails + unreadDelta } + : mb); + return { + emails: state.emails.filter(e => !emailIds.includes(e.id)), + selectedEmail: emailIds.includes(state.selectedEmail?.id || '') ? null : state.selectedEmail, + selectedEmailIds: new Set(), + ...patch, + }; + }); } catch (error) { console.error('Failed to batch restore emails:', error); throw error; From d7a64fd9d6dbd6ee8fb5b9a04b0fde1534925c40 Mon Sep 17 00:00:00 2001 From: dealerweb Date: Mon, 6 Jul 2026 10:43:16 +0200 Subject: [PATCH 5/8] Fix: hide the spam action in Sent, Drafts and Scheduled Marking your own outgoing mail as spam makes no sense, but the action was offered in every non-junk folder: context menu, hover quick-actions, viewer toolbar and its overflow menu, plus the "!" shortcut. All surfaces now skip the action when the folder role is sent, drafts or scheduled, and the shortcut is a no-op there. Scheduled messages were already covered per-email via isScheduled; the role check additionally covers the server-side Scheduled folder before that annotation loads. The hover quick-actions bar gets a spamApplicable prop for this, since it renders its buttons without knowing the folder. --- app/(main)/[locale]/page.tsx | 4 +++ components/email/email-context-menu.tsx | 37 ++++++++++++++---------- components/email/email-hover-actions.tsx | 4 +++ components/email/email-list-item.tsx | 1 + components/email/email-viewer.tsx | 7 +++-- components/email/thread-list-item.tsx | 2 ++ 6 files changed, 38 insertions(+), 17 deletions(-) diff --git a/app/(main)/[locale]/page.tsx b/app/(main)/[locale]/page.tsx index 67699187..04d78eba 100644 --- a/app/(main)/[locale]/page.tsx +++ b/app/(main)/[locale]/page.tsx @@ -621,6 +621,10 @@ export default function Home() { onToggleSpam: async () => { if (isScheduledView) return; const currentMailbox = mailboxes.find(m => m.id === selectedMailbox); + // Marking your own outgoing mail as spam makes no sense - the toolbar + // and menus hide the action in Sent/Drafts/Scheduled, so the shortcut + // is a no-op there too. + if (['sent', 'drafts', 'scheduled'].includes(currentMailbox?.role || '')) return; const isInJunk = currentMailbox?.role === 'junk'; if (selectedEmailIds.size > 0 && client) { const ids = Array.from(selectedEmailIds); diff --git a/components/email/email-context-menu.tsx b/components/email/email-context-menu.tsx index 83a4b4de..53ae2eac 100644 --- a/components/email/email-context-menu.tsx +++ b/components/email/email-context-menu.tsx @@ -153,6 +153,9 @@ export function EmailContextMenu({ const currentColors = getCurrentColors(email.keywords); const showBatchActions = isMultiSelect && selectedCount > 1; const isInJunkFolder = currentMailboxRole === 'junk'; + // Marking your own outgoing mail as spam makes no sense - hide the action + // in Sent, Drafts and Scheduled. + const spamApplicable = !['sent', 'drafts', 'scheduled'].includes(currentMailboxRole || ''); const isScheduled = email.isScheduled === true; const canCancelScheduled = isScheduled && email.scheduledUndoStatus === 'pending'; @@ -385,22 +388,26 @@ export function EmailContextMenu({ )} - + {/* Spam - contextual based on folder; pointless on own outgoing mail */} + {spamApplicable && ( + <> + - {/* Spam - contextual based on folder */} - - handleAction( - showBatchActions - ? (isInJunkFolder ? onBatchUndoSpam! : onBatchMarkAsSpam!) - : (isInJunkFolder ? onUndoSpam! : onMarkAsSpam!) - ) - } - disabled={showBatchActions ? (isInJunkFolder ? !onBatchUndoSpam : !onBatchMarkAsSpam) : (isInJunkFolder ? !onUndoSpam : !onMarkAsSpam)} - destructive={!isInJunkFolder} - /> + + handleAction( + showBatchActions + ? (isInJunkFolder ? onBatchUndoSpam! : onBatchMarkAsSpam!) + : (isInJunkFolder ? onUndoSpam! : onMarkAsSpam!) + ) + } + disabled={showBatchActions ? (isInJunkFolder ? !onBatchUndoSpam : !onBatchMarkAsSpam) : (isInJunkFolder ? !onUndoSpam : !onMarkAsSpam)} + destructive={!isInJunkFolder} + /> + + )} diff --git a/components/email/email-hover-actions.tsx b/components/email/email-hover-actions.tsx index 4e465fe7..7bb4a734 100644 --- a/components/email/email-hover-actions.tsx +++ b/components/email/email-hover-actions.tsx @@ -21,6 +21,8 @@ interface EmailHoverActionsProps { // the spam quick-action flips to "not spam". isInJunk?: boolean; onUndoSpam?: () => void; + // Hidden where marking spam is meaningless for self-authored mail (Drafts, Sent). + spamApplicable?: boolean; } const ACTION_CONFIG: Record state.hoverActions); const hoverActionsMode = useSettingsStore((state) => state.hoverActionsMode); @@ -121,6 +124,7 @@ export function EmailHoverActions({ const actionButtons = hoverActions.map((actionId) => { const config = ACTION_CONFIG[actionId]; if (!config) return null; + if (actionId === "spam" && !spamApplicable) return null; const Icon = config.icon; // In a junk context the spam action becomes "not spam". diff --git a/components/email/email-list-item.tsx b/components/email/email-list-item.tsx index 27fb5c64..faabb6a2 100644 --- a/components/email/email-list-item.tsx +++ b/components/email/email-list-item.tsx @@ -338,6 +338,7 @@ export function EmailListItem({ email, selected, onClick, onDoubleClick, onConte onMarkAsSpam={onMarkAsSpam} onUndoSpam={onUndoSpam} isInJunk={currentMailboxRole === 'junk'} + spamApplicable={!['sent', 'drafts', 'scheduled'].includes(currentMailboxRole || '')} /> ); diff --git a/components/email/email-viewer.tsx b/components/email/email-viewer.tsx index 31f5d982..76ec3c50 100644 --- a/components/email/email-viewer.tsx +++ b/components/email/email-viewer.tsx @@ -693,6 +693,9 @@ export function EmailViewer({ // Detect if current mailbox is Junk folder const isInJunkFolder = currentMailboxRole === 'junk'; + // Marking your own outgoing mail as spam makes no sense - hide the action + // in Sent, Drafts and Scheduled. + const spamApplicable = !['sent', 'drafts', 'scheduled'].includes(currentMailboxRole || ''); // Detect if the email is a draft const isDraft = email?.keywords?.['$draft'] === true; @@ -2922,7 +2925,7 @@ export function EmailViewer({ {/* Spam */} - {(onMarkAsSpam || onUndoSpam) && ( + {spamApplicable && (onMarkAsSpam || onUndoSpam) && (