fix: details toggle UX, Edge Runtime warnings, minimatch CVE
- Keep show/hide details button in place when expanded (#18) - Use i18n translations for details toggle text - Split instrumentation for Edge Runtime compatibility - Patch minimatch ReDoS (CVE-2026-27903)
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
# Changelog
|
||||
|
||||
## 1.1.1 (2026-02-28)
|
||||
|
||||
### Fixes
|
||||
|
||||
- **Email viewer**: Show/hide details toggle now stays in place when expanded instead of jumping to the bottom of the details section (#18)
|
||||
- **Email viewer**: Details toggle text is now properly translated (was hardcoded in English)
|
||||
- **Instrumentation**: Resolve Edge Runtime warnings by splitting Node.js-only code into a separate module
|
||||
- **Security**: Patch minimatch ReDoS vulnerability (CVE-2026-27903) — upgrade 9.0.6→9.0.9 and 3.1.3→3.1.5
|
||||
|
||||
## 1.1.0 (2026-02-28)
|
||||
|
||||
- Server-side version update check on startup (logs when a newer release is available)
|
||||
|
||||
## 1.0.2 (2026-02-27)
|
||||
|
||||
- Fix 4 CVEs in production Docker image (removed npm, upgraded Alpine packages)
|
||||
|
||||
## 1.0.1 (2026-02-26)
|
||||
|
||||
- Remove stale references, clean up README
|
||||
|
||||
## 1.0.0 (2026-02-25)
|
||||
|
||||
- Initial public release
|
||||
@@ -972,9 +972,27 @@ export function EmailViewer({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Modern Expandable Details */}
|
||||
{/* Details toggle - stays in place when expanded */}
|
||||
<button
|
||||
onClick={() => setShowFullHeaders(!showFullHeaders)}
|
||||
className="mt-3 text-xs text-muted-foreground hover:text-foreground flex items-center gap-1 transition-colors"
|
||||
>
|
||||
{showFullHeaders ? (
|
||||
<>
|
||||
<ChevronUp className="w-3 h-3" />
|
||||
{t('hide_details')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ChevronDown className="w-3 h-3" />
|
||||
{t('show_details')}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* Expandable Details */}
|
||||
{showFullHeaders && (
|
||||
<div className="mt-4 space-y-3">
|
||||
<div className="mt-3 space-y-3">
|
||||
{/* Security & Authentication Section */}
|
||||
{(email.authenticationResults || email.spamScore !== undefined) && (
|
||||
<div className="border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
|
||||
@@ -1254,22 +1272,6 @@ export function EmailViewer({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={() => setShowFullHeaders(!showFullHeaders)}
|
||||
className="mt-3 text-xs text-muted-foreground hover:text-foreground flex items-center gap-1 transition-colors"
|
||||
>
|
||||
{showFullHeaders ? (
|
||||
<>
|
||||
<ChevronUp className="w-3 h-3" />
|
||||
Hide details
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ChevronDown className="w-3 h-3" />
|
||||
Show details
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { readFileSync } from "fs";
|
||||
|
||||
const VERSION_CHECK_URL =
|
||||
"https://raw.githubusercontent.com/root-fr/jmap-webmail/main/VERSION";
|
||||
|
||||
const SEMVER_RE = /^\d+\.\d+\.\d+$/;
|
||||
|
||||
function compareVersions(current: string, remote: string): number {
|
||||
const a = current.split(".").map(Number);
|
||||
const b = remote.split(".").map(Number);
|
||||
for (let i = 0; i < 3; i++) {
|
||||
if ((b[i] ?? 0) > (a[i] ?? 0)) return 1;
|
||||
if ((b[i] ?? 0) < (a[i] ?? 0)) return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
const pkg = JSON.parse(
|
||||
readFileSync(`${process.cwd()}/package.json`, "utf-8")
|
||||
);
|
||||
const current: string = pkg.version ?? "0.0.0";
|
||||
console.info(`JMAP Webmail v${current}`);
|
||||
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
fetch(VERSION_CHECK_URL, {
|
||||
cache: "no-store",
|
||||
signal: AbortSignal.timeout(5000),
|
||||
})
|
||||
.then((res) => {
|
||||
if (!res.ok) return;
|
||||
return res.text();
|
||||
})
|
||||
.then((text) => {
|
||||
if (!text) return;
|
||||
const remote = text.trim();
|
||||
if (!SEMVER_RE.test(remote)) return;
|
||||
if (compareVersions(current, remote) > 0) {
|
||||
console.info(
|
||||
`Update available: v${remote} — https://github.com/root-fr/jmap-webmail`
|
||||
);
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
+2
-40
@@ -1,43 +1,5 @@
|
||||
import { readFileSync } from "fs";
|
||||
|
||||
const VERSION_CHECK_URL =
|
||||
"https://raw.githubusercontent.com/root-fr/jmap-webmail/main/VERSION";
|
||||
|
||||
const SEMVER_RE = /^\d+\.\d+\.\d+$/;
|
||||
|
||||
function compareVersions(current: string, remote: string): number {
|
||||
const a = current.split(".").map(Number);
|
||||
const b = remote.split(".").map(Number);
|
||||
for (let i = 0; i < 3; i++) {
|
||||
if ((b[i] ?? 0) > (a[i] ?? 0)) return 1;
|
||||
if ((b[i] ?? 0) < (a[i] ?? 0)) return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
export async function register() {
|
||||
const pkg = JSON.parse(
|
||||
readFileSync(`${process.cwd()}/package.json`, "utf-8")
|
||||
);
|
||||
const current: string = pkg.version ?? "0.0.0";
|
||||
console.info(`JMAP Webmail v${current}`);
|
||||
|
||||
if (process.env.NODE_ENV !== "production") return;
|
||||
|
||||
try {
|
||||
const res = await fetch(VERSION_CHECK_URL, {
|
||||
cache: "no-store",
|
||||
signal: AbortSignal.timeout(5000),
|
||||
});
|
||||
if (!res.ok) return;
|
||||
const remote = (await res.text()).trim();
|
||||
if (!SEMVER_RE.test(remote)) return;
|
||||
if (compareVersions(current, remote) > 0) {
|
||||
console.info(
|
||||
`Update available: v${remote} — https://github.com/root-fr/jmap-webmail`
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
// Network unavailable — not critical
|
||||
if (process.env.NEXT_RUNTIME === "nodejs") {
|
||||
await import("./instrumentation.node");
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+26
-60
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "jmap-webmail",
|
||||
"version": "0.1.0",
|
||||
"version": "1.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "jmap-webmail",
|
||||
"version": "0.1.0",
|
||||
"version": "1.1.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/react-virtual": "^3.13.18",
|
||||
@@ -1108,13 +1108,6 @@
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/config-array/node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@eslint/config-array/node_modules/brace-expansion": {
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
@@ -1127,9 +1120,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/config-array/node_modules/minimatch": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.3.tgz",
|
||||
"integrity": "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==",
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
|
||||
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
@@ -1189,13 +1182,6 @@
|
||||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/eslintrc/node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@eslint/eslintrc/node_modules/brace-expansion": {
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
@@ -1231,9 +1217,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/eslintrc/node_modules/minimatch": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.3.tgz",
|
||||
"integrity": "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==",
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
|
||||
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
@@ -4117,14 +4103,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
|
||||
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
}
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/baseline-browser-mapping": {
|
||||
"version": "2.10.0",
|
||||
@@ -4149,16 +4132,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz",
|
||||
"integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==",
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^4.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/browserslist": {
|
||||
@@ -5026,13 +5006,6 @@
|
||||
"eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-react/node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/eslint-plugin-react/node_modules/brace-expansion": {
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
@@ -5045,9 +5018,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-react/node_modules/minimatch": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.3.tgz",
|
||||
"integrity": "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==",
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
|
||||
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
@@ -5121,13 +5094,6 @@
|
||||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint/node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/eslint/node_modules/brace-expansion": {
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
@@ -5163,9 +5129,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint/node_modules/minimatch": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.3.tgz",
|
||||
"integrity": "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==",
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
|
||||
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
@@ -6762,13 +6728,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "9.0.6",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.6.tgz",
|
||||
"integrity": "sha512-kQAVowdR33euIqeA0+VZTDqU+qo1IeVY+hrKYtZMio3Pg0P0vuh/kwRylLUddJhB6pf3q/botcOvRtx4IN1wqQ==",
|
||||
"version": "9.0.9",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
|
||||
"integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^5.0.2"
|
||||
"brace-expansion": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "jmap-webmail",
|
||||
"version": "1.1.0",
|
||||
"version": "1.1.1",
|
||||
"description": "A modern JMAP webmail client built for Stalwart Mail Server",
|
||||
"author": "Matthieu MALVACHE <matthieu@root.cloud>",
|
||||
"license": "MIT",
|
||||
|
||||
Reference in New Issue
Block a user