From 3ea7441f9374ba486aade380bd3cb81bcb59d2dc Mon Sep 17 00:00:00 2001 From: Matthieu MALVACHE Date: Thu, 14 May 2026 22:32:20 +0200 Subject: [PATCH] fix: v1.5.2 - patch Next.js WebSocket SSRF (CVE-2026-44578) Security: upgrade Next.js 16.2.4 -> 16.2.6 to patch CVE-2026-44578 (GHSA-c4j6-fc7j-m34r, CVSS 8.6, unauthenticated WebSocket-upgrade SSRF on the built-in Node server) and eleven other May 2026 advisories bundled in the same release. next-intl patched for GHSA-4c35-wcg5-mm9h prototype pollution in the experimental precompile path. React/react-dom rolled forward to 19.2.6. Fixes: - bulk delete now honours the "delete to trash" setting instead of always hard-deleting - favicon repaints the base icon on zero unread so the badge clears --- CHANGELOG.md | 36 ++++++++ ROADMAP.md | 5 ++ VERSION | 2 +- hooks/use-favicon-badge.ts | 37 ++++++--- lib/jmap/client.ts | 5 +- package-lock.json | 166 +++++++++++++++++++------------------ package.json | 8 +- stores/email-store.ts | 32 +++++-- 8 files changed, 189 insertions(+), 102 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 93c69ad..c436f95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,41 @@ # Changelog +## 1.5.2 (2026-05-14) + +### Security + +- **Next.js upgraded to 16.2.6 to patch CVE-2026-44578** + (GHSA-c4j6-fc7j-m34r, CVSS 8.6). The framework's WebSocket upgrade + handler did not apply the safe-rewrite checks used for normal HTTP + requests, so a single unauthenticated HTTP upgrade could cause the + Node server to issue an internal request to any host reachable on + port 80 and return the response to the attacker. Self-hosted + deployments (the only mode this project supports) were exposed to + cloud metadata endpoints, internal APIs, and admin panels. The bump + to 16.2.6 also pulls in eleven other May 2026 advisories covering + middleware/proxy bypass, denial of service, and a React patch. +- **`next-intl` upgraded to patch GHSA-4c35-wcg5-mm9h**, a prototype + pollution issue in the experimental precompile path. This project + doesn't enable that path, but the dependency is patched anyway. +- **Hardening note for self-hosters**: a public PoC for CVE-2026-44578 + exists. Redeploy on `rootfr/jmap-webmail:1.5.2` (or `latest`) and, + where you can, keep the container off untrusted networks and block + egress to cloud metadata endpoints (AWS IMDS, GCP metadata). + +### Fixes + +- **Bulk delete now honours the "delete to trash" setting**: emptying + a selection from the toolbar always performed a hard delete, even + when the user had configured deletes to move to Trash first. The + store path used by the toolbar now routes through the same trash + helper as the single-message delete action, so the behaviour matches + Settings. +- **Favicon clears the unread badge as soon as the inbox empties**: + on `unreadCount === 0` the favicon kept the last-painted badge until + the next refresh because the redraw skipped the zero case. The hook + now repaints the base icon on the zero transition so the badge + disappears immediately. + ## 1.5.1 (2026-04-17) ### Fixes diff --git a/ROADMAP.md b/ROADMAP.md index cb26f83..109f0fa 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -276,6 +276,11 @@ This document tracks the development status and planned features for JMAP Webmai - [x] Attachments render for mail originating from providers that stamp a Content-ID on every part (#58) - [x] Email-to-self delivered instead of being dropped by the MTA's duplicate-message check (#60) +### Release 1.5.2 (2026-05-14) +- [x] Patch CVE-2026-44578 (Next.js WebSocket SSRF, CVSS 8.6) by upgrading to 16.2.6, plus eleven other May 2026 advisories +- [x] Bulk delete honours the "delete to trash" setting instead of always hard-deleting +- [x] Favicon unread badge clears on the zero transition instead of sticking until the next refresh + ## Planned Features ### Real-time and PWA (1.6.0) diff --git a/VERSION b/VERSION index 26ca594..4cda8f1 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.5.1 +1.5.2 diff --git a/hooks/use-favicon-badge.ts b/hooks/use-favicon-badge.ts index 1b49bbe..ecf86e6 100644 --- a/hooks/use-favicon-badge.ts +++ b/hooks/use-favicon-badge.ts @@ -34,9 +34,7 @@ export function useFaviconBadge(count: number) { const img = new Image(); img.onload = () => { imageRef.current = img; - if (countRef.current > 0) { - applyBadge(canvas, img, countRef.current); - } + renderFavicon(canvas, img, countRef.current); }; img.src = reader.result as string; }; @@ -52,17 +50,36 @@ export function useFaviconBadge(count: number) { }, []); useEffect(() => { - if (count <= 0) { - clearDynamicFavicon(); - return; - } - - if (canvasRef.current) { - applyBadge(canvasRef.current, imageRef.current, count); + if (canvasRef.current && imageRef.current) { + renderFavicon(canvasRef.current, imageRef.current, count); } }, [count]); } +function renderFavicon( + canvas: HTMLCanvasElement, + image: HTMLImageElement, + count: number, +) { + if (count > 0) { + applyBadge(canvas, image, count); + } else { + // Paint the plain base icon into the dynamic link instead of removing + // it. Chromium caches the previously-painted badged favicon until a + // new href replaces it; simply removing the link leaves the stale + // badge visible until the tab is reloaded. + drawBase(canvas, image); + } +} + +function drawBase(canvas: HTMLCanvasElement, image: HTMLImageElement) { + const ctx = canvas.getContext("2d"); + if (!ctx) return; + ctx.clearRect(0, 0, SIZE, SIZE); + ctx.drawImage(image, 0, 0, SIZE, SIZE); + setDynamicFavicon(canvas.toDataURL("image/png")); +} + function applyBadge( canvas: HTMLCanvasElement, image: HTMLImageElement | null, diff --git a/lib/jmap/client.ts b/lib/jmap/client.ts index 311f23b..c5dd35e 100644 --- a/lib/jmap/client.ts +++ b/lib/jmap/client.ts @@ -764,12 +764,13 @@ export class JMAPClient { }; } - async batchMoveEmails(emailIds: string[], toMailboxId: string): Promise { + async batchMoveEmails(emailIds: string[], toMailboxId: string, accountId?: string): Promise { if (emailIds.length === 0) return; + const targetAccountId = accountId || this.accountId; const updates = Object.fromEntries(emailIds.map(id => [id, { mailboxIds: { [toMailboxId]: true } }])); await this.request([ - ["Email/set", { accountId: this.accountId, update: updates }, "0"], + ["Email/set", { accountId: targetAccountId, update: updates }, "0"], ]); } diff --git a/package-lock.json b/package-lock.json index b29c8e2..ce52fb6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,10 +14,10 @@ "date-fns": "^4.1.0", "dompurify": "^3.4.0", "lucide-react": "^0.575.0", - "next": "^16.2.4", + "next": "^16.2.6", "next-intl": "^4.9.1", - "react": "^19.2.1", - "react-dom": "^19.2.1", + "react": "^19.2.6", + "react-dom": "^19.2.6", "sonner": "^2.0.7", "tailwind-merge": "^3.4.0", "zustand": "^5.0.9" @@ -1317,18 +1317,18 @@ "license": "MIT" }, "node_modules/@formatjs/icu-messageformat-parser": { - "version": "3.5.4", - "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-3.5.4.tgz", - "integrity": "sha512-JVY39ROgLt+pIYngo6piyj4OVfZmXs/2FkC4wLS+ql1Eig/sGJKB7YwDO/5bkJFkfwaFAeIpgEiJc8hiYxNalw==", + "version": "3.5.8", + "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-3.5.8.tgz", + "integrity": "sha512-uZLvzLFN7iV2l8cbDdROwgKGtdELeLI4bpnsuz1DnyscHDxn8TdDE0anHzcfjtWK66XYCllGLV3Mi3CYcEPg/g==", "license": "MIT", "dependencies": { - "@formatjs/icu-skeleton-parser": "2.1.4" + "@formatjs/icu-skeleton-parser": "2.1.8" } }, "node_modules/@formatjs/icu-skeleton-parser": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-2.1.4.tgz", - "integrity": "sha512-8bSFZbrlvGX11ywMZxtgkPBt5Q8/etyts7j7j+GWpOVK1g43zwMIH3LZxk43HAtEP7L/jtZ+OZaMiFTOiBj9CA==", + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-2.1.8.tgz", + "integrity": "sha512-iX5i0O15gPf69l1WqmLFYwn7wq53lauvytvWFnHamIfX/5Ta56gpFj6fdeHRcKTV58IhrKv8QOvWfTYZYm7f+g==", "license": "MIT" }, "node_modules/@formatjs/intl-localematcher": { @@ -1926,15 +1926,15 @@ } }, "node_modules/@next/env": { - "version": "16.2.4", - "resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.4.tgz", - "integrity": "sha512-dKkkOzOSwFYe5RX6y26fZgkSpVAlIOJKQHIiydQcrWH6y/97+RceSOAdjZ14Qa3zLduVUy0TXcn+EiM6t4rPgw==", + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.6.tgz", + "integrity": "sha512-gd8HoHN4ufj73WmR3JmVolrpJR47ILK6LouP5xElPglaVxir6e1a7VzvTvDWkOoPXT9rkkTzyCxBu4yeZfZwcw==", "license": "MIT" }, "node_modules/@next/swc-darwin-arm64": { - "version": "16.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.4.tgz", - "integrity": "sha512-OXTFFox5EKN1Ym08vfrz+OXxmCcEjT4SFMbNRsWZE99dMqt2Kcusl5MqPXcW232RYkMLQTy0hqgAMEsfEd/l2A==", + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.6.tgz", + "integrity": "sha512-ZJGkkcNfYgrrMkqOdZ7zoLa1TOy0qpcMfk/z4Mh/FKUz40gVO+HNQWqmLxf67Z5WB64DRp0dhEbyHfel+6sJUg==", "cpu": [ "arm64" ], @@ -1948,9 +1948,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "16.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.4.tgz", - "integrity": "sha512-XhpVnUfmYWvD3YrXu55XdcAkQtOnvaI6wtQa8fuF5fGoKoxIUZ0kWPtcOfqJEWngFF/lOS9l3+O9CcownhiQxQ==", + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.6.tgz", + "integrity": "sha512-v/YLBHIY132Ced3puBJ7YJKw1lqsCrgcNo2aRJlCEyQrrCeRJlvGlnmxhPxNQI3KE3N1DN5r9TPNPvka3nq5RQ==", "cpu": [ "x64" ], @@ -1964,9 +1964,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "16.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.4.tgz", - "integrity": "sha512-Mx/tjlNA3G8kg14QvuGAJ4xBwPk1tUHq56JxZ8CXnZwz1Etz714soCEzGQQzVMz4bEnGPowzkV6Xrp6wAkEWOQ==", + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.6.tgz", + "integrity": "sha512-RPOvqlYBbcQjkz9VQQDZ2T2bARIjXZV1KFlt+V2Mr6SW/e4I9fcKsaA0hdyf2FHoTlsV2xnBd5Y912rP/1Ce6w==", "cpu": [ "arm64" ], @@ -1980,9 +1980,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "16.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.4.tgz", - "integrity": "sha512-iVMMp14514u7Nup2umQS03nT/bN9HurK8ufylC3FZNykrwjtx7V1A7+4kvhbDSCeonTVqV3Txnv0Lu+m2oDXNg==", + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.6.tgz", + "integrity": "sha512-URUTu1+dMkxJsPFgm+OeEvq9wf5sujw0EvgYy80TDGHTSLTnIHeqb0Eu8A3sC95IRgjejQL+kC4mw+4yPxiAXA==", "cpu": [ "arm64" ], @@ -1996,9 +1996,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "16.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.4.tgz", - "integrity": "sha512-EZOvm1aQWgnI/N/xcWOlnS3RQBk0VtVav5Zo7n4p0A7UKyTDx047k8opDbXgBpHl4CulRqRfbw3QrX2w5UOXMQ==", + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.6.tgz", + "integrity": "sha512-DOj182mPV8G3UkrayLoREM5YEYI+Dk5wv7Ox9xl1fFibAELEsFD0lDPfHIeILlutMMfdyhlzYPELG3peuKaurw==", "cpu": [ "x64" ], @@ -2012,9 +2012,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "16.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.4.tgz", - "integrity": "sha512-h9FxsngCm9cTBf71AR4fGznDEDx1hS7+kSEiIRjq5kO1oXWm07DxVGZjCvk0SGx7TSjlUqhI8oOyz7NfwAdPoA==", + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.6.tgz", + "integrity": "sha512-HKQ5SP/V/ub73UvF7n/zeJlxk2kLmtL7Wzrg4WfmkjmNos5onJ2tKu7yZOPdL18A6Svfn3max29ym+ry7NkK4g==", "cpu": [ "x64" ], @@ -2028,9 +2028,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "16.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.4.tgz", - "integrity": "sha512-3NdJV5OXMSOeJYijX+bjaLge3mJBlh4ybydbT4GFoB/2hAojWHtMhl3CYlYoMrjPuodp0nzFVi4Tj2+WaMg+Ow==", + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.6.tgz", + "integrity": "sha512-LZXpTlPyS5v7HhSmnvsLGP3iIYgYOBnc8r8ArlT55sGHV89bR2HlDdBjWQ+PY6SJMmk8TuVGFuxalnP3k/0Dwg==", "cpu": [ "arm64" ], @@ -2044,9 +2044,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "16.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.4.tgz", - "integrity": "sha512-kMVGgsqhO5YTYODD9IPGGhA6iprWidQckK3LmPeW08PIFENRmgfb4MjXHO+p//d+ts2rpjvK5gXWzXSMrPl9cw==", + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.6.tgz", + "integrity": "sha512-F0+4i0h9J6C4eE3EAPWsoCk7UW/dbzOjyzxY0qnDUOYFu6FFmdZ6l97/XdV3/Nz3VYyO7UWjyEJUXkGqcoXfMA==", "cpu": [ "x64" ], @@ -5742,9 +5742,9 @@ } }, "node_modules/icu-minify": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/icu-minify/-/icu-minify-4.9.1.tgz", - "integrity": "sha512-6NkfF9GHHFouqnz+wuiLjCWQiyxoEyJ5liUv4Jxxo/8wyhV7MY0L0iTEGDAVEa4aAD58WqTxFMa20S5nyMjwNw==", + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/icu-minify/-/icu-minify-4.12.0.tgz", + "integrity": "sha512-zDmM05uav3t3+kxSfRrNlmyXOdj2b+uHA+p04CG32eJabtaHbugXujuL+YfRkwP9joAnf0Uh+RMGCKD5NLa5rQ==", "funding": [ { "type": "individual", @@ -5819,15 +5819,21 @@ } }, "node_modules/intl-messageformat": { - "version": "11.2.1", - "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-11.2.1.tgz", - "integrity": "sha512-1gAVEUt3wEPvTqML4Fsw9klZV5j0vszQxayP/fi6gUroAc8AUHiNaisBKLWxybL1AdWq1mP07YV1q8v4N92ilQ==", + "version": "11.2.5", + "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-11.2.5.tgz", + "integrity": "sha512-zaROHiUsnlSFXVypU54AsQuAm3DLmmSH8KfDhiUuG1XZ9NTQ4o3xlxIJYVNmeWAklyp3CWg0lhexNUnee8PsYQ==", "license": "BSD-3-Clause", "dependencies": { - "@formatjs/fast-memoize": "3.1.2", - "@formatjs/icu-messageformat-parser": "3.5.4" + "@formatjs/fast-memoize": "3.1.5", + "@formatjs/icu-messageformat-parser": "3.5.8" } }, + "node_modules/intl-messageformat/node_modules/@formatjs/fast-memoize": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-3.1.5.tgz", + "integrity": "sha512-KLi3fan6WnCHmigd9pmEEN8Hid0v4wiFBW576M/d07KMWYecf1CvyMI3n34vCmHT4AoVqG2n702kiHbXjzZX2A==", + "license": "MIT" + }, "node_modules/is-array-buffer": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", @@ -6837,12 +6843,12 @@ } }, "node_modules/next": { - "version": "16.2.4", - "resolved": "https://registry.npmjs.org/next/-/next-16.2.4.tgz", - "integrity": "sha512-kPvz56wF5frc+FxlHI5qnklCzbq53HTwORaWBGdT0vNoKh1Aya9XC8aPauH4NJxqtzbWsS5mAbctm4cr+EkQ2Q==", + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/next/-/next-16.2.6.tgz", + "integrity": "sha512-qOVgKJg1+At15NpeUP+eJgCHvTCgXsogweq87Ri/Ix7PkqQHg4sdaXmSFqKlgaIXE4kW0g25LE68W87UANlHtw==", "license": "MIT", "dependencies": { - "@next/env": "16.2.4", + "@next/env": "16.2.6", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", @@ -6856,14 +6862,14 @@ "node": ">=20.9.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "16.2.4", - "@next/swc-darwin-x64": "16.2.4", - "@next/swc-linux-arm64-gnu": "16.2.4", - "@next/swc-linux-arm64-musl": "16.2.4", - "@next/swc-linux-x64-gnu": "16.2.4", - "@next/swc-linux-x64-musl": "16.2.4", - "@next/swc-win32-arm64-msvc": "16.2.4", - "@next/swc-win32-x64-msvc": "16.2.4", + "@next/swc-darwin-arm64": "16.2.6", + "@next/swc-darwin-x64": "16.2.6", + "@next/swc-linux-arm64-gnu": "16.2.6", + "@next/swc-linux-arm64-musl": "16.2.6", + "@next/swc-linux-x64-gnu": "16.2.6", + "@next/swc-linux-x64-musl": "16.2.6", + "@next/swc-win32-arm64-msvc": "16.2.6", + "@next/swc-win32-x64-msvc": "16.2.6", "sharp": "^0.34.5" }, "peerDependencies": { @@ -6890,9 +6896,9 @@ } }, "node_modules/next-intl": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/next-intl/-/next-intl-4.9.1.tgz", - "integrity": "sha512-N7ga0CjtYcdxNvaKNIi6eJ2mmatlHK5hp8rt0YO2Omoc1m0gean242/Ukdj6+gJNiReBVcYIjK0HZeNx7CV1ug==", + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/next-intl/-/next-intl-4.12.0.tgz", + "integrity": "sha512-v8KpppWG0yLLlChJ3Of6uoPew9LeRDBAtY6vpJmF7YJmBZlHEzzoEL4w1g1dAU+VleEPNoXNm9hg1eEsKWV5hw==", "funding": [ { "type": "individual", @@ -6904,11 +6910,11 @@ "@formatjs/intl-localematcher": "^0.8.1", "@parcel/watcher": "^2.4.1", "@swc/core": "^1.15.2", - "icu-minify": "^4.9.1", + "icu-minify": "^4.12.0", "negotiator": "^1.0.0", - "next-intl-swc-plugin-extractor": "^4.9.1", + "next-intl-swc-plugin-extractor": "^4.12.0", "po-parser": "^2.1.1", - "use-intl": "^4.9.1" + "use-intl": "^4.12.0" }, "peerDependencies": { "next": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0", @@ -6921,9 +6927,9 @@ } }, "node_modules/next-intl-swc-plugin-extractor": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/next-intl-swc-plugin-extractor/-/next-intl-swc-plugin-extractor-4.9.1.tgz", - "integrity": "sha512-8whJJ6oxJz8JqkHarggmmuEDyXgC7nEnaPhZD91CJwEWW4xp0AST3Mw17YxvHyP2vAF3taWfFbs1maD+WWtz3w==", + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/next-intl-swc-plugin-extractor/-/next-intl-swc-plugin-extractor-4.12.0.tgz", + "integrity": "sha512-jUxVEu1Nryjt4YgaDktSys7ioOgQfcNPF/SF2dbPNxbVb6U+P1INRgHeCVN+EC59H2rnTFIQwbddmOCrUWFr3g==", "license": "MIT" }, "node_modules/next-intl/node_modules/@swc/core": { @@ -7350,9 +7356,9 @@ } }, "node_modules/postcss": { - "version": "8.5.8", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", - "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", + "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", "dev": true, "funding": [ { @@ -7446,9 +7452,9 @@ } }, "node_modules/react": { - "version": "19.2.4", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", - "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz", + "integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==", "license": "MIT", "peer": true, "engines": { @@ -7456,16 +7462,16 @@ } }, "node_modules/react-dom": { - "version": "19.2.4", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", - "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.6.tgz", + "integrity": "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==", "license": "MIT", "peer": true, "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { - "react": "^19.2.4" + "react": "^19.2.6" } }, "node_modules/react-is": { @@ -8495,9 +8501,9 @@ } }, "node_modules/use-intl": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/use-intl/-/use-intl-4.9.1.tgz", - "integrity": "sha512-iGVV/xFYlhe3btafRlL8RPLD2Jsuet4yqn9DR6LWWbMhULsJnXgLonDkzDmsAIBIwFtk02oJuX/Ox2vwHKF+UQ==", + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/use-intl/-/use-intl-4.12.0.tgz", + "integrity": "sha512-r+qVb7UI1+kiOhjYsmsNUCY+jrnjVopwGeFlmMyQj4YInlwZzgMeMSv9n8MqnWWy77HL5BVM8K2WgX50SbtcpA==", "funding": [ { "type": "individual", @@ -8508,7 +8514,7 @@ "dependencies": { "@formatjs/fast-memoize": "^3.1.0", "@schummar/icu-type-parser": "1.21.5", - "icu-minify": "^4.9.1", + "icu-minify": "^4.12.0", "intl-messageformat": "^11.1.0" }, "peerDependencies": { diff --git a/package.json b/package.json index bdd037d..68aa7ff 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jmap-webmail", - "version": "1.5.1", + "version": "1.5.2", "description": "A modern JMAP webmail client built for Stalwart Mail Server", "author": "Matthieu MALVACHE ", "license": "MIT", @@ -36,10 +36,10 @@ "date-fns": "^4.1.0", "dompurify": "^3.4.0", "lucide-react": "^0.575.0", - "next": "^16.2.4", + "next": "^16.2.6", "next-intl": "^4.9.1", - "react": "^19.2.1", - "react-dom": "^19.2.1", + "react": "^19.2.6", + "react-dom": "^19.2.6", "sonner": "^2.0.7", "tailwind-merge": "^3.4.0", "zustand": "^5.0.9" diff --git a/stores/email-store.ts b/stores/email-store.ts index 828c1d2..ea8089b 100644 --- a/stores/email-store.ts +++ b/stores/email-store.ts @@ -897,19 +897,35 @@ export const useEmailStore = create((set, get) => ({ }, batchDelete: async (client) => { - const { selectedEmailIds, emails, mailboxes } = get(); + const { selectedEmailIds, emails, mailboxes, selectedMailbox } = get(); if (selectedEmailIds.size === 0) return; set({ error: null }); try { const emailIdsArray = Array.from(selectedEmailIds); - await client.batchDeleteEmails(emailIdsArray); + const deletedEmails = emails.filter(e => selectedEmailIds.has(e.id)); + + const deleteAction = useSettingsStore.getState().deleteAction; + const currentMailbox = mailboxes.find(mb => mb.id === selectedMailbox); + const accountId = currentMailbox?.isShared ? currentMailbox.accountId : undefined; + + let trashMailbox: typeof mailboxes[number] | undefined; + if (deleteAction === 'trash') { + trashMailbox = mailboxes.find(mb => { + if (accountId) return mb.role === 'trash' && mb.accountId === accountId; + return mb.role === 'trash' && !mb.isShared; + }); + } + + if (trashMailbox) { + const trashId = trashMailbox.originalId || trashMailbox.id; + await client.batchMoveEmails(emailIdsArray, trashId, accountId); + } else { + await client.batchDeleteEmails(emailIdsArray); + } - // Remove deleted emails from local state const remainingEmails = emails.filter(e => !selectedEmailIds.has(e.id)); - // Update mailbox counters - const deletedEmails = emails.filter(e => selectedEmailIds.has(e.id)); const updatedMailboxes = mailboxes.map(mailbox => { let deltaTotalEmails = 0; let deltaUnreadEmails = 0; @@ -921,6 +937,12 @@ export const useEmailStore = create((set, get) => ({ deltaUnreadEmails--; } } + if (trashMailbox && mailbox.id === trashMailbox.id && !email.mailboxIds?.[mailbox.id]) { + deltaTotalEmails++; + if (!email.keywords?.$seen) { + deltaUnreadEmails++; + } + } }); return {