diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 00000000..074d9209
--- /dev/null
+++ b/CHANGELOG.md
@@ -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
diff --git a/VERSION b/VERSION
index 9084fa2f..524cb552 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-1.1.0
+1.1.1
diff --git a/components/email/email-viewer.tsx b/components/email/email-viewer.tsx
index c13014e3..0c8846fd 100644
--- a/components/email/email-viewer.tsx
+++ b/components/email/email-viewer.tsx
@@ -972,9 +972,27 @@ export function EmailViewer({
)}
- {/* Modern Expandable Details */}
+ {/* Details toggle - stays in place when expanded */}
+
+
+ {/* Expandable Details */}
{showFullHeaders && (
-
+
{/* Security & Authentication Section */}
{(email.authenticationResults || email.spamScore !== undefined) && (
@@ -1254,22 +1272,6 @@ export function EmailViewer({
)}
-
diff --git a/instrumentation.node.ts b/instrumentation.node.ts
new file mode 100644
index 00000000..f2985a30
--- /dev/null
+++ b/instrumentation.node.ts
@@ -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(() => {});
+}
diff --git a/instrumentation.ts b/instrumentation.ts
index 92fa0ce4..41630756 100644
--- a/instrumentation.ts
+++ b/instrumentation.ts
@@ -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");
}
}
diff --git a/package-lock.json b/package-lock.json
index 3b521771..bf6a4ccb 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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"
diff --git a/package.json b/package.json
index 732fa945..3655e633 100644
--- a/package.json
+++ b/package.json
@@ -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 ",
"license": "MIT",