Merge dev into main - version 1.4.1

This commit is contained in:
Linus Rath
2026-03-18 15:51:22 +01:00
15 changed files with 986 additions and 841 deletions
+1 -1
View File
@@ -72,7 +72,7 @@ JMAP_SERVER_URL=https://your-jmap-server.com
# ============================================================================= # =============================================================================
# Hostname the server binds to (default: 0.0.0.0) # Hostname the server binds to (default: 0.0.0.0)
# Set to "::" for IPv6 or "[::]" for dual-stack support. # Set to "::" for dual-stack
# HOSTNAME=0.0.0.0 # HOSTNAME=0.0.0.0
# Port the server listens on (default: 3000) # Port the server listens on (default: 3000)
+1 -1
View File
@@ -1,6 +1,6 @@
# Changelog # Changelog
## 1.4.0 (2026-03-17) ## 1.4.1 (2026-03-18)
### Features ### Features
+1 -1
View File
@@ -12,7 +12,7 @@ A modern, self-hosted webmail client for [Stalwart Mail Server](https://stalw.ar
Built with Next.js and the JMAP protocol. Built with Next.js and the JMAP protocol.
[![License: AGPL v3](https://img.shields.io/badge/license-AGPL%20v3-blue.svg)](LICENSE) [![License: AGPL v3](https://img.shields.io/badge/license-AGPL%20v3-blue.svg)](LICENSE)
[![Version](https://img.shields.io/badge/version-1.4.0-green.svg)](CHANGELOG.md) [![Version](https://img.shields.io/badge/version-1.4.1-green.svg)](CHANGELOG.md)
[![Docker](https://img.shields.io/badge/docker-ghcr.io%2Fbulwarkmail%2Fwebmail-blue)](https://ghcr.io/bulwarkmail/webmail) [![Docker](https://img.shields.io/badge/docker-ghcr.io%2Fbulwarkmail%2Fwebmail-blue)](https://ghcr.io/bulwarkmail/webmail)
</div> </div>
+1 -1
View File
@@ -1 +1 @@
1.4.0 1.4.1
+1 -1
View File
@@ -16,7 +16,7 @@ import { discoverOAuth, type OAuthMetadata } from "@/lib/oauth/discovery";
import { generateCodeVerifier, generateCodeChallenge, generateState } from "@/lib/oauth/pkce"; import { generateCodeVerifier, generateCodeChallenge, generateState } from "@/lib/oauth/pkce";
import { OAUTH_SCOPES } from "@/lib/oauth/tokens"; import { OAUTH_SCOPES } from "@/lib/oauth/tokens";
const APP_VERSION = "1.4.0"; const APP_VERSION = "1.4.1";
const THEME_OPTIONS = [ const THEME_OPTIONS = [
{ value: "light" as const, icon: Sun, label: "Light" }, { value: "light" as const, icon: Sun, label: "Light" },
File diff suppressed because it is too large Load Diff
+10 -1
View File
@@ -316,7 +316,16 @@ function EmailCard({
if (email.htmlBody?.[0]?.partId && email.bodyValues[email.htmlBody[0].partId]) { if (email.htmlBody?.[0]?.partId && email.bodyValues[email.htmlBody[0].partId]) {
htmlContent = email.bodyValues[email.htmlBody[0].partId].value; htmlContent = email.bodyValues[email.htmlBody[0].partId].value;
useHtmlVersion = !!htmlContent; // Prefer textBody when HTML is auto-generated minimal wrapper (no rich formatting).
// Server-generated HTML from text/plain emails often lacks <br> tags, collapsing newlines.
const hasTextBody = email.textBody?.[0]?.partId && email.bodyValues[email.textBody[0].partId];
if (hasTextBody && htmlContent) {
const stripped = htmlContent.replace(/<\/?(html|head|body|meta|!doctype|!DOCTYPE|br\s*\/?)[^>]*>/gi, '').trim();
const hasRichContent = /<(table|tr|td|th|img|style|link|div\s+[^>]*class|span\s+[^>]*class|font|center|blockquote|ul|ol|li|h[1-6])\b/i.test(stripped);
useHtmlVersion = hasRichContent;
} else {
useHtmlVersion = !!htmlContent;
}
} }
if (useHtmlVersion && htmlContent) { if (useHtmlVersion && htmlContent) {
+10 -1
View File
@@ -1088,7 +1088,16 @@ export class JMAPClient {
["Email/get", { ["Email/get", {
accountId: targetAccountId, accountId: targetAccountId,
ids: thread.emailIds, ids: thread.emailIds,
properties: [...EMAIL_LIST_PROPERTIES], properties: [
...EMAIL_LIST_PROPERTIES,
"textBody", "htmlBody", "bodyValues",
"attachments", "blobId", "sentAt", "bcc", "replyTo",
"messageId", "inReplyTo", "references", "headers", "bodyStructure",
],
fetchTextBodyValues: true,
fetchHTMLBodyValues: true,
fetchAllBodyValues: true,
maxBodyValueBytes: 256000,
}, "0"], }, "0"],
]); ]);
+76
View File
@@ -0,0 +1,76 @@
/**
* Crypto engine backed by webcrypto-liner for legacy algorithm support.
*
* webcrypto-liner extends the native Web Crypto API with algorithms
* like DES-EDE3-CBC (3DES) that are commonly found in S/MIME messages
* and PKCS#12 files produced by legacy clients (Outlook, Thunderbird, etc.).
*
* Native Web Crypto calls are passed through to the real implementation;
* liner only intercepts algorithms that the browser doesn't natively support.
*/
import * as pkijs from 'pkijs';
// webcrypto-liner exports a Crypto constructor at runtime that extends native
// Web Crypto with legacy algorithms (3DES, etc.). Its type declarations only
// expose the type alias, so we import the module dynamically and cast.
// eslint-disable-next-line @typescript-eslint/no-require-imports
const liner = require('webcrypto-liner') as {
Crypto: { new (): Crypto };
setCrypto: (subtle: SubtleCrypto) => void;
nativeCrypto: Crypto | Record<string, never>;
};
let linerEngine: pkijs.CryptoEngine | null = null;
let linerCryptoInstance: Crypto | null = null;
function ensureLiner() {
if (!linerCryptoInstance) {
// In Node.js, webcrypto-liner can't auto-detect the native crypto
// (it looks for self.crypto which doesn't exist). Feed it manually
// so that native algorithms (RSA, AES, etc.) stay hardware-accelerated
// and only truly missing algorithms (3DES) use the software fallback.
if (
typeof liner.nativeCrypto?.getRandomValues !== 'function' &&
typeof globalThis.crypto?.subtle !== 'undefined'
) {
liner.setCrypto(globalThis.crypto.subtle);
}
linerCryptoInstance = new liner.Crypto();
}
if (!linerEngine) {
linerEngine = new pkijs.CryptoEngine({
crypto: linerCryptoInstance,
subtle: linerCryptoInstance.subtle,
name: 'webcrypto-liner',
});
}
}
/** Get a PKI.js CryptoEngine with 3DES (and other legacy algorithm) support. */
export function getLinerCryptoEngine(): pkijs.CryptoEngine {
ensureLiner();
return linerEngine!;
}
/**
* Run an async operation with the global PKI.js engine set to webcrypto-liner,
* then restore the previous engine afterwards.
*
* Required for operations that use the global engine internally
* (e.g. PFX.parseInternalValues for PKCS#12 import).
*/
export async function withLinerEngine<T>(fn: () => Promise<T>): Promise<T> {
ensureLiner();
// Save the current global engine so we can restore it
const prev = pkijs.getEngine();
pkijs.setEngine('webcrypto-liner', linerCryptoInstance!, linerEngine!);
try {
return await fn();
} finally {
// Restore the previous engine
pkijs.setEngine(prev.name, prev.crypto as unknown as pkijs.CryptoEngine);
}
}
+14 -6
View File
@@ -6,6 +6,7 @@ import {
classifyCapabilities, classifyCapabilities,
} from './certificate-utils'; } from './certificate-utils';
import type { SmimeKeyRecord, Pkcs12ImportResult } from './types'; import type { SmimeKeyRecord, Pkcs12ImportResult } from './types';
import { withLinerEngine } from './crypto-engine';
const KDF_ITERATIONS = 600_000; const KDF_ITERATIONS = 600_000;
const AES_KEY_LENGTH = 256; const AES_KEY_LENGTH = 256;
@@ -39,9 +40,12 @@ export async function importPkcs12(
// PKIjs handles MAC verification internally during parseInternalValues // PKIjs handles MAC verification internally during parseInternalValues
} }
// Parse internal values // Use webcrypto-liner as the global engine for 3DES support.
await pfx.parseInternalValues({ // Many PKCS#12 files use pbeWithSHAAnd3-KeyTripleDES-CBC internally.
password: stringToAB(p12Passphrase), await withLinerEngine(async () => {
await pfx.parseInternalValues({
password: stringToAB(p12Passphrase),
});
}); });
// Extract certificates and private key from parsed PKCS#12 // Extract certificates and private key from parsed PKCS#12
@@ -63,7 +67,9 @@ export async function importPkcs12(
} }
return {}; return {};
}); });
await authSafe.parseInternalValues({ safeContents: safeContentsParams }); await withLinerEngine(async () => {
await authSafe.parseInternalValues({ safeContents: safeContentsParams });
});
for (const safeContent of authSafe.parsedValue.safeContents) { for (const safeContent of authSafe.parsedValue.safeContents) {
const sc = safeContent.value ?? safeContent.parsedValue; const sc = safeContent.value ?? safeContent.parsedValue;
@@ -115,8 +121,10 @@ export async function importPkcs12(
privateKeyInfo = shroudedBag.parsedValue; privateKeyInfo = shroudedBag.parsedValue;
} else { } else {
// Decrypt shrouded key bag to get private key info // Decrypt shrouded key bag to get private key info
await (shroudedBag as unknown as { parseInternalValues(params: { password: ArrayBuffer }): Promise<void> }).parseInternalValues({ await withLinerEngine(async () => {
password: stringToAB(p12Passphrase), await (shroudedBag as unknown as { parseInternalValues(params: { password: ArrayBuffer }): Promise<void> }).parseInternalValues({
password: stringToAB(p12Passphrase),
});
}); });
if (shroudedBag.parsedValue) { if (shroudedBag.parsedValue) {
privateKeyInfo = shroudedBag.parsedValue; privateKeyInfo = shroudedBag.parsedValue;
+3 -5
View File
@@ -8,6 +8,7 @@
import * as pkijs from 'pkijs'; import * as pkijs from 'pkijs';
import * as asn1js from 'asn1js'; import * as asn1js from 'asn1js';
import type { SmimeKeyRecord } from './types'; import type { SmimeKeyRecord } from './types';
import { getLinerCryptoEngine } from './crypto-engine';
export interface DecryptionInput { export interface DecryptionInput {
/** Raw CMS EnvelopedData bytes (DER) */ /** Raw CMS EnvelopedData bytes (DER) */
@@ -359,11 +360,8 @@ async function decryptWithKey(
const certAsn1 = asn1js.fromBER(keyRecord.certificate); const certAsn1 = asn1js.fromBER(keyRecord.certificate);
const cert = new pkijs.Certificate({ schema: certAsn1.result }); const cert = new pkijs.Certificate({ schema: certAsn1.result });
const cryptoEngine = new pkijs.CryptoEngine({ // Use webcrypto-liner engine for legacy algorithm support (e.g. 3DES)
crypto: crypto, const cryptoEngine = getLinerCryptoEngine();
subtle: crypto.subtle,
name: 'webcrypto',
});
const result = await envelopedData.decrypt( const result = await envelopedData.decrypt(
recipientIndex, recipientIndex,
+53 -2
View File
@@ -7,6 +7,8 @@
* Reference: MS-OXTNEF / MS-TNEF specification. * Reference: MS-OXTNEF / MS-TNEF specification.
*/ */
import { debug } from '@/lib/debug';
// TNEF signature // TNEF signature
const TNEF_SIGNATURE = 0x223E9F78; const TNEF_SIGNATURE = 0x223E9F78;
@@ -234,35 +236,58 @@ export function parseTnef(data: Uint8Array): TnefResult {
attachments: [], attachments: [],
}; };
if (data.byteLength < 6) return result; debug.group('TNEF Parser');
debug.log('Input data size:', data.byteLength, 'bytes');
if (data.byteLength < 6) {
debug.warn('TNEF data too small (< 6 bytes), skipping');
debug.groupEnd();
return result;
}
const r = new BinaryReader(data); const r = new BinaryReader(data);
const signature = r.readUint32LE(); const signature = r.readUint32LE();
if (signature !== TNEF_SIGNATURE) { if (signature !== TNEF_SIGNATURE) {
debug.warn('Invalid TNEF signature:', '0x' + signature.toString(16).toUpperCase(), '(expected 0x223E9F78)');
debug.groupEnd();
return result; return result;
} }
debug.log('TNEF signature valid');
r.skip(2); // legacy key r.skip(2); // legacy key
// Current attachment being assembled // Current attachment being assembled
let curAttach: { name: string; mimeType: string; data: Uint8Array | null } | null = null; let curAttach: { name: string; mimeType: string; data: Uint8Array | null } | null = null;
let attrCount = 0;
while (r.remaining >= 11) { while (r.remaining >= 11) {
const level = r.readUint8(); const level = r.readUint8();
const attrID = r.readUint32LE(); const attrID = r.readUint32LE();
const attrLen = r.readUint32LE(); const attrLen = r.readUint32LE();
attrCount++;
if (attrLen > r.remaining - 2) break; // not enough data for payload + checksum if (attrLen > r.remaining - 2) {
debug.warn('Attribute #' + attrCount + ': truncated data — need', attrLen, 'bytes but only', r.remaining - 2, 'available');
break;
}
const attrData = r.readBytes(attrLen); const attrData = r.readBytes(attrLen);
r.skip(2); // checksum r.skip(2); // checksum
const levelName = level === LVL_MESSAGE ? 'MESSAGE' : level === LVL_ATTACHMENT ? 'ATTACHMENT' : 'UNKNOWN(' + level + ')';
debug.log('Attribute #' + attrCount + ':', levelName, 'id=0x' + attrID.toString(16).toUpperCase(), 'len=' + attrLen);
if (level === LVL_MESSAGE) { if (level === LVL_MESSAGE) {
if (attrID === attBody) { if (attrID === attBody) {
result.body = new TextDecoder('utf-8').decode(attrData); result.body = new TextDecoder('utf-8').decode(attrData);
debug.log(' → Extracted plain text body (' + result.body.length + ' chars)');
} else if (attrID === attMAPIProps) { } else if (attrID === attMAPIProps) {
const props = parseMAPIProps(attrData); const props = parseMAPIProps(attrData);
debug.log(' → Parsed', props.size, 'MAPI properties from message');
props.forEach((val, propID) => {
debug.log(' MAPI prop 0x' + propID.toString(16).toUpperCase(), 'type=0x' + val.type.toString(16), 'value=' + (val.value instanceof Uint8Array ? val.value.byteLength + ' bytes' : val.value));
});
// HTML body // HTML body
const htmlProp = props.get(PR_BODY_HTML); const htmlProp = props.get(PR_BODY_HTML);
@@ -273,6 +298,9 @@ export function parseTnef(data: Uint8Array): TnefResult {
} else { } else {
result.htmlBody = new TextDecoder('utf-8').decode(htmlProp.value); result.htmlBody = new TextDecoder('utf-8').decode(htmlProp.value);
} }
debug.log(' → Extracted HTML body (' + result.htmlBody.length + ' chars)');
} else {
debug.log(' → No HTML body property (PR_BODY_HTML 0x1013) found in MAPI props');
} }
// Plain text body from MAPI props (fallback) // Plain text body from MAPI props (fallback)
@@ -280,6 +308,9 @@ export function parseTnef(data: Uint8Array): TnefResult {
const bodyProp = props.get(PR_BODY); const bodyProp = props.get(PR_BODY);
if (bodyProp?.value instanceof Uint8Array) { if (bodyProp?.value instanceof Uint8Array) {
result.body = decodeMAPIString(bodyProp.value, bodyProp.type & 0x0FFF); result.body = decodeMAPIString(bodyProp.value, bodyProp.type & 0x0FFF);
debug.log(' → Extracted plain text body from MAPI props (' + result.body.length + ' chars)');
} else {
debug.log(' → No plain text body property (PR_BODY 0x1000) found in MAPI props');
} }
} }
} }
@@ -287,6 +318,7 @@ export function parseTnef(data: Uint8Array): TnefResult {
if (attrID === attAttachRenddata) { if (attrID === attAttachRenddata) {
// Start of a new attachment — flush previous // Start of a new attachment — flush previous
if (curAttach?.data) { if (curAttach?.data) {
debug.log(' → Flushing previous attachment:', curAttach.name, '(' + curAttach.mimeType + ',', curAttach.data.byteLength, 'bytes)');
result.attachments.push({ result.attachments.push({
name: curAttach.name, name: curAttach.name,
mimeType: curAttach.mimeType, mimeType: curAttach.mimeType,
@@ -294,28 +326,40 @@ export function parseTnef(data: Uint8Array): TnefResult {
}); });
} }
curAttach = { name: 'attachment', mimeType: 'application/octet-stream', data: null }; curAttach = { name: 'attachment', mimeType: 'application/octet-stream', data: null };
debug.log(' → New attachment started');
} else if (attrID === attAttachTitle && curAttach) { } else if (attrID === attAttachTitle && curAttach) {
let len = attrData.byteLength; let len = attrData.byteLength;
if (len > 0 && attrData[len - 1] === 0) len--; if (len > 0 && attrData[len - 1] === 0) len--;
curAttach.name = new TextDecoder('utf-8').decode(attrData.subarray(0, len)); curAttach.name = new TextDecoder('utf-8').decode(attrData.subarray(0, len));
debug.log(' → Attachment short name:', curAttach.name);
} else if (attrID === attAttachData && curAttach) { } else if (attrID === attAttachData && curAttach) {
curAttach.data = attrData; curAttach.data = attrData;
debug.log(' → Attachment data (attAttachData):', attrData.byteLength, 'bytes');
} else if (attrID === attAttachment && curAttach) { } else if (attrID === attAttachment && curAttach) {
const props = parseMAPIProps(attrData); const props = parseMAPIProps(attrData);
debug.log(' → Parsed', props.size, 'MAPI properties from attachment');
props.forEach((val, propID) => {
debug.log(' MAPI prop 0x' + propID.toString(16).toUpperCase(), 'type=0x' + val.type.toString(16), 'value=' + (val.value instanceof Uint8Array ? val.value.byteLength + ' bytes' : val.value));
});
const longName = props.get(PR_ATTACH_LONG_FILENAME); const longName = props.get(PR_ATTACH_LONG_FILENAME);
if (longName?.value instanceof Uint8Array) { if (longName?.value instanceof Uint8Array) {
curAttach.name = decodeMAPIString(longName.value, longName.type & 0x0FFF); curAttach.name = decodeMAPIString(longName.value, longName.type & 0x0FFF);
debug.log(' → Attachment long filename:', curAttach.name);
} }
const mimeTag = props.get(PR_ATTACH_MIME_TAG); const mimeTag = props.get(PR_ATTACH_MIME_TAG);
if (mimeTag?.value instanceof Uint8Array) { if (mimeTag?.value instanceof Uint8Array) {
curAttach.mimeType = decodeMAPIString(mimeTag.value, mimeTag.type & 0x0FFF); curAttach.mimeType = decodeMAPIString(mimeTag.value, mimeTag.type & 0x0FFF);
debug.log(' → Attachment MIME type:', curAttach.mimeType);
} }
const attachData = props.get(PR_ATTACH_DATA_BIN); const attachData = props.get(PR_ATTACH_DATA_BIN);
if (attachData?.value instanceof Uint8Array) { if (attachData?.value instanceof Uint8Array) {
curAttach.data = attachData.value; curAttach.data = attachData.value;
debug.log(' → Attachment data (PR_ATTACH_DATA_BIN):', attachData.value.byteLength, 'bytes');
} else {
debug.log(' → No PR_ATTACH_DATA_BIN found in attachment MAPI props');
} }
} }
} }
@@ -323,6 +367,7 @@ export function parseTnef(data: Uint8Array): TnefResult {
// Flush last attachment // Flush last attachment
if (curAttach?.data) { if (curAttach?.data) {
debug.log('Flushing final attachment:', curAttach.name, '(' + curAttach.mimeType + ',', curAttach.data.byteLength, 'bytes)');
result.attachments.push({ result.attachments.push({
name: curAttach.name, name: curAttach.name,
mimeType: curAttach.mimeType, mimeType: curAttach.mimeType,
@@ -330,6 +375,12 @@ export function parseTnef(data: Uint8Array): TnefResult {
}); });
} }
debug.log('TNEF parsing complete — body:', !!result.body, ', htmlBody:', !!result.htmlBody, ', attachments:', result.attachments.length);
if (result.attachments.length > 0) {
debug.table(result.attachments.map(a => ({ name: a.name, mimeType: a.mimeType, size: a.data.byteLength })));
}
debug.groupEnd();
return result; return result;
} }
+6 -1
View File
@@ -191,6 +191,8 @@
"mark_read": "Mark as read", "mark_read": "Mark as read",
"print": "Print", "print": "Print",
"view_source": "View source", "view_source": "View source",
"export_email": "Export as .eml",
"import_email": "Import .eml",
"keyboard_shortcuts": "Keyboard shortcuts (?)", "keyboard_shortcuts": "Keyboard shortcuts (?)",
"email_source": "Email Source", "email_source": "Email Source",
"copy_source": "Copy to clipboard", "copy_source": "Copy to clipboard",
@@ -527,7 +529,10 @@
"templates_exported": "Templates exported successfully", "templates_exported": "Templates exported successfully",
"templates_imported": "{count, plural, one {# template} other {# templates}} imported", "templates_imported": "{count, plural, one {# template} other {# templates}} imported",
"templates_import_errors": "Some templates could not be imported", "templates_import_errors": "Some templates could not be imported",
"templates_import_empty": "No templates found in the file" "templates_import_empty": "No templates found in the file",
"export_email_error": "Failed to export email",
"import_email_success": "Email imported successfully",
"import_email_error": "Failed to import email"
}, },
"date": { "date": {
"today": "Today", "today": "Today",
+188 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "bulwark-webmail", "name": "bulwark-webmail",
"version": "1.4.0", "version": "1.4.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "bulwark-webmail", "name": "bulwark-webmail",
"version": "1.4.0", "version": "1.4.1",
"license": "AGPL-3.0-only", "license": "AGPL-3.0-only",
"dependencies": { "dependencies": {
"@tanstack/react-virtual": "^3.13.18", "@tanstack/react-virtual": "^3.13.18",
@@ -24,6 +24,7 @@
"react-dom": "^19.2.1", "react-dom": "^19.2.1",
"sonner": "^2.0.7", "sonner": "^2.0.7",
"tailwind-merge": "^3.4.0", "tailwind-merge": "^3.4.0",
"webcrypto-liner": "^1.4.3",
"zustand": "^5.0.9" "zustand": "^5.0.9"
}, },
"devDependencies": { "devDependencies": {
@@ -2375,6 +2376,29 @@
"url": "https://github.com/sponsors/jonschlinkert" "url": "https://github.com/sponsors/jonschlinkert"
} }
}, },
"node_modules/@peculiar/asn1-schema": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.6.0.tgz",
"integrity": "sha512-xNLYLBFTBKkCzEZIw842BxytQQATQv+lDTCEMZ8C196iJcJJMBUZxrhSTxLaohMyKK8QlzRNTRkUmanucnDSqg==",
"license": "MIT",
"dependencies": {
"asn1js": "^3.0.6",
"pvtsutils": "^1.3.6",
"tslib": "^2.8.1"
}
},
"node_modules/@peculiar/json-schema": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/@peculiar/json-schema/-/json-schema-1.1.12.tgz",
"integrity": "sha512-coUfuoMeIB7B8/NMekxaDzLhaYmp0HZNPEjYRm9goRou8UZIC3z21s0sL9AWoCw4EG876QyO3kYrc61WNF9B/w==",
"license": "MIT",
"dependencies": {
"tslib": "^2.0.0"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/@playwright/test": { "node_modules/@playwright/test": {
"version": "1.58.2", "version": "1.58.2",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz",
@@ -2761,6 +2785,44 @@
"integrity": "sha512-bXHSaW5jRTmke9Vd0h5P7BtWZG9Znqb8gSDxZnxaGSJnGwPLDPfS+3g0BKzeWqzgZPsIVZkM7m2tbo18cm5HBw==", "integrity": "sha512-bXHSaW5jRTmke9Vd0h5P7BtWZG9Znqb8gSDxZnxaGSJnGwPLDPfS+3g0BKzeWqzgZPsIVZkM7m2tbo18cm5HBw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@stablelib/binary": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@stablelib/binary/-/binary-1.0.1.tgz",
"integrity": "sha512-ClJWvmL6UBM/wjkvv/7m5VP3GMr9t0osr4yVgLZsLCOz4hGN9gIAFEqnJ0TsSMAN+n840nf2cHZnA5/KFqHC7Q==",
"license": "MIT",
"dependencies": {
"@stablelib/int": "^1.0.1"
}
},
"node_modules/@stablelib/hash": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@stablelib/hash/-/hash-1.0.1.tgz",
"integrity": "sha512-eTPJc/stDkdtOcrNMZ6mcMK1e6yBbqRBaNW55XA1jU8w/7QdnCF0CmMmOD1m7VSkBR44PWrMHU2l6r8YEQHMgg==",
"license": "MIT"
},
"node_modules/@stablelib/int": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@stablelib/int/-/int-1.0.1.tgz",
"integrity": "sha512-byr69X/sDtDiIjIV6m4roLVWnNNlRGzsvxw+agj8CIEazqWGOQp2dTYgQhtyVXV9wpO6WyXRQUzLV/JRNumT2w==",
"license": "MIT"
},
"node_modules/@stablelib/sha3": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@stablelib/sha3/-/sha3-1.0.1.tgz",
"integrity": "sha512-82OHZcxWsJAS34L64VItIbqZdcdYgBJmeToYaou9lUA+iMjajdfOVZDDrditfV8C8yXUDrlS3BuMRWmKf9NQhQ==",
"license": "MIT",
"dependencies": {
"@stablelib/binary": "^1.0.1",
"@stablelib/hash": "^1.0.1",
"@stablelib/wipe": "^1.0.1"
}
},
"node_modules/@stablelib/wipe": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@stablelib/wipe/-/wipe-1.0.1.tgz",
"integrity": "sha512-WfqfX/eXGiAd3RJe4VU2snh/ZPwtSjLG4ynQ/vYzvghTh7dHFcI1wl+nrkWG6lGhukOxOsUHfv8dUXr58D0ayg==",
"license": "MIT"
},
"node_modules/@standard-schema/spec": { "node_modules/@standard-schema/spec": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
@@ -4075,6 +4137,12 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/asmcrypto.js": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/asmcrypto.js/-/asmcrypto.js-2.3.2.tgz",
"integrity": "sha512-3FgFARf7RupsZETQ1nHnhLUUvpcttcCq1iZCaVAbJZbCZ5VNRrNyvpDyHTOb0KC3llFcsyOT/a99NZcCbeiEsA==",
"license": "MIT"
},
"node_modules/asn1js": { "node_modules/asn1js": {
"version": "3.0.7", "version": "3.0.7",
"resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.7.tgz", "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.7.tgz",
@@ -4154,6 +4222,12 @@
"require-from-string": "^2.0.2" "require-from-string": "^2.0.2"
} }
}, },
"node_modules/bn.js": {
"version": "4.12.3",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz",
"integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==",
"license": "MIT"
},
"node_modules/brace-expansion": { "node_modules/brace-expansion": {
"version": "5.0.4", "version": "5.0.4",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz",
@@ -4177,6 +4251,12 @@
"node": "18 || 20 || >=22" "node": "18 || 20 || >=22"
} }
}, },
"node_modules/brorand": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz",
"integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==",
"license": "MIT"
},
"node_modules/browserslist": { "node_modules/browserslist": {
"version": "4.28.1", "version": "4.28.1",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
@@ -4376,6 +4456,17 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/core-js": {
"version": "3.49.0",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.49.0.tgz",
"integrity": "sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==",
"hasInstallScript": true,
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/core-js"
}
},
"node_modules/cross-spawn": { "node_modules/cross-spawn": {
"version": "7.0.6", "version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -4600,6 +4691,16 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/des.js": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/des.js/-/des.js-1.1.0.tgz",
"integrity": "sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg==",
"license": "MIT",
"dependencies": {
"inherits": "^2.0.1",
"minimalistic-assert": "^1.0.0"
}
},
"node_modules/detect-libc": { "node_modules/detect-libc": {
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
@@ -4660,6 +4761,20 @@
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/elliptic": {
"version": "6.5.0",
"resolved": "git+ssh://git@github.com/mahrud/elliptic.git#75637c76678e83c31682fd967c2fa9ff4761b3fc",
"license": "MIT",
"dependencies": {
"bn.js": "^4.4.0",
"brorand": "^1.0.1",
"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"
}
},
"node_modules/enhanced-resolve": { "node_modules/enhanced-resolve": {
"version": "5.20.0", "version": "5.20.0",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.0.tgz", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.0.tgz",
@@ -5663,6 +5778,16 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/hash.js": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz",
"integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==",
"license": "MIT",
"dependencies": {
"inherits": "^2.0.3",
"minimalistic-assert": "^1.0.1"
}
},
"node_modules/hasown": { "node_modules/hasown": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
@@ -5693,6 +5818,17 @@
"hermes-estree": "0.25.1" "hermes-estree": "0.25.1"
} }
}, },
"node_modules/hmac-drbg": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz",
"integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==",
"license": "MIT",
"dependencies": {
"hash.js": "^1.0.3",
"minimalistic-assert": "^1.0.0",
"minimalistic-crypto-utils": "^1.0.1"
}
},
"node_modules/html-encoding-sniffer": { "node_modules/html-encoding-sniffer": {
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz",
@@ -5812,6 +5948,12 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC"
},
"node_modules/internal-slot": { "node_modules/internal-slot": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz",
@@ -6779,6 +6921,18 @@
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/minimalistic-assert": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
"integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==",
"license": "ISC"
},
"node_modules/minimalistic-crypto-utils": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz",
"integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==",
"license": "MIT"
},
"node_modules/minimatch": { "node_modules/minimatch": {
"version": "10.2.4", "version": "10.2.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz",
@@ -8805,6 +8959,38 @@
"node": ">=18" "node": ">=18"
} }
}, },
"node_modules/webcrypto-core": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/webcrypto-core/-/webcrypto-core-1.8.1.tgz",
"integrity": "sha512-P+x1MvlNCXlKbLSOY4cYrdreqPG5hbzkmawbcXLKN/mf6DZW0SdNNkZ+sjwsqVkI4A4Ko2sPZmkZtCKY58w83A==",
"license": "MIT",
"dependencies": {
"@peculiar/asn1-schema": "^2.3.13",
"@peculiar/json-schema": "^1.1.12",
"asn1js": "^3.0.5",
"pvtsutils": "^1.3.5",
"tslib": "^2.7.0"
}
},
"node_modules/webcrypto-liner": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/webcrypto-liner/-/webcrypto-liner-1.4.3.tgz",
"integrity": "sha512-gzlk7ciS5zqc8QZMwpzpRxxwkcQKDJDndhr/hHWQe18Rzafhji3a7CaSxIeA2jcL0bLcAK+P77K3lWS1QXMMYA==",
"license": "MIT",
"dependencies": {
"@peculiar/asn1-schema": "^2.3.8",
"@peculiar/json-schema": "^1.1.12",
"@stablelib/sha3": "^1.0.1",
"asmcrypto.js": "^2.3.2",
"asn1js": "^3.0.5",
"core-js": "^3.35.1",
"des.js": "^1.1.0",
"elliptic": "git+https://github.com/mahrud/elliptic.git",
"pvtsutils": "^1.3.5",
"tslib": "^2.6.2",
"webcrypto-core": "^1.7.8"
}
},
"node_modules/webidl-conversions": { "node_modules/webidl-conversions": {
"version": "8.0.1", "version": "8.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz",
+2 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "bulwark-webmail", "name": "bulwark-webmail",
"version": "1.4.0", "version": "1.4.1",
"description": "Bulwark Webmail — a modern webmail client built for Stalwart Mail Server", "description": "Bulwark Webmail — a modern webmail client built for Stalwart Mail Server",
"author": "Bulwark Webmail <bulwark@rbm.systems>", "author": "Bulwark Webmail <bulwark@rbm.systems>",
"license": "AGPL-3.0-only", "license": "AGPL-3.0-only",
@@ -47,6 +47,7 @@
"react-dom": "^19.2.1", "react-dom": "^19.2.1",
"sonner": "^2.0.7", "sonner": "^2.0.7",
"tailwind-merge": "^3.4.0", "tailwind-merge": "^3.4.0",
"webcrypto-liner": "^1.4.3",
"zustand": "^5.0.9" "zustand": "^5.0.9"
}, },
"devDependencies": { "devDependencies": {