Merge dev into main - version 1.4.1
This commit is contained in:
+1
-1
@@ -72,7 +72,7 @@ JMAP_SERVER_URL=https://your-jmap-server.com
|
||||
# =============================================================================
|
||||
|
||||
# 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
|
||||
|
||||
# Port the server listens on (default: 3000)
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
# Changelog
|
||||
|
||||
## 1.4.0 (2026-03-17)
|
||||
## 1.4.1 (2026-03-18)
|
||||
|
||||
### Features
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
[](LICENSE)
|
||||
[](CHANGELOG.md)
|
||||
[](CHANGELOG.md)
|
||||
[](https://ghcr.io/bulwarkmail/webmail)
|
||||
|
||||
</div>
|
||||
|
||||
@@ -16,7 +16,7 @@ import { discoverOAuth, type OAuthMetadata } from "@/lib/oauth/discovery";
|
||||
import { generateCodeVerifier, generateCodeChallenge, generateState } from "@/lib/oauth/pkce";
|
||||
import { OAUTH_SCOPES } from "@/lib/oauth/tokens";
|
||||
|
||||
const APP_VERSION = "1.4.0";
|
||||
const APP_VERSION = "1.4.1";
|
||||
|
||||
const THEME_OPTIONS = [
|
||||
{ value: "light" as const, icon: Sun, label: "Light" },
|
||||
|
||||
+619
-817
File diff suppressed because it is too large
Load Diff
@@ -316,7 +316,16 @@ function EmailCard({
|
||||
|
||||
if (email.htmlBody?.[0]?.partId && email.bodyValues[email.htmlBody[0].partId]) {
|
||||
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) {
|
||||
|
||||
+10
-1
@@ -1088,7 +1088,16 @@ export class JMAPClient {
|
||||
["Email/get", {
|
||||
accountId: targetAccountId,
|
||||
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,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);
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
classifyCapabilities,
|
||||
} from './certificate-utils';
|
||||
import type { SmimeKeyRecord, Pkcs12ImportResult } from './types';
|
||||
import { withLinerEngine } from './crypto-engine';
|
||||
|
||||
const KDF_ITERATIONS = 600_000;
|
||||
const AES_KEY_LENGTH = 256;
|
||||
@@ -39,9 +40,12 @@ export async function importPkcs12(
|
||||
// PKIjs handles MAC verification internally during parseInternalValues
|
||||
}
|
||||
|
||||
// Parse internal values
|
||||
await pfx.parseInternalValues({
|
||||
password: stringToAB(p12Passphrase),
|
||||
// Use webcrypto-liner as the global engine for 3DES support.
|
||||
// Many PKCS#12 files use pbeWithSHAAnd3-KeyTripleDES-CBC internally.
|
||||
await withLinerEngine(async () => {
|
||||
await pfx.parseInternalValues({
|
||||
password: stringToAB(p12Passphrase),
|
||||
});
|
||||
});
|
||||
|
||||
// Extract certificates and private key from parsed PKCS#12
|
||||
@@ -63,7 +67,9 @@ export async function importPkcs12(
|
||||
}
|
||||
return {};
|
||||
});
|
||||
await authSafe.parseInternalValues({ safeContents: safeContentsParams });
|
||||
await withLinerEngine(async () => {
|
||||
await authSafe.parseInternalValues({ safeContents: safeContentsParams });
|
||||
});
|
||||
|
||||
for (const safeContent of authSafe.parsedValue.safeContents) {
|
||||
const sc = safeContent.value ?? safeContent.parsedValue;
|
||||
@@ -115,8 +121,10 @@ export async function importPkcs12(
|
||||
privateKeyInfo = shroudedBag.parsedValue;
|
||||
} else {
|
||||
// Decrypt shrouded key bag to get private key info
|
||||
await (shroudedBag as unknown as { parseInternalValues(params: { password: ArrayBuffer }): Promise<void> }).parseInternalValues({
|
||||
password: stringToAB(p12Passphrase),
|
||||
await withLinerEngine(async () => {
|
||||
await (shroudedBag as unknown as { parseInternalValues(params: { password: ArrayBuffer }): Promise<void> }).parseInternalValues({
|
||||
password: stringToAB(p12Passphrase),
|
||||
});
|
||||
});
|
||||
if (shroudedBag.parsedValue) {
|
||||
privateKeyInfo = shroudedBag.parsedValue;
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import * as pkijs from 'pkijs';
|
||||
import * as asn1js from 'asn1js';
|
||||
import type { SmimeKeyRecord } from './types';
|
||||
import { getLinerCryptoEngine } from './crypto-engine';
|
||||
|
||||
export interface DecryptionInput {
|
||||
/** Raw CMS EnvelopedData bytes (DER) */
|
||||
@@ -359,11 +360,8 @@ async function decryptWithKey(
|
||||
const certAsn1 = asn1js.fromBER(keyRecord.certificate);
|
||||
const cert = new pkijs.Certificate({ schema: certAsn1.result });
|
||||
|
||||
const cryptoEngine = new pkijs.CryptoEngine({
|
||||
crypto: crypto,
|
||||
subtle: crypto.subtle,
|
||||
name: 'webcrypto',
|
||||
});
|
||||
// Use webcrypto-liner engine for legacy algorithm support (e.g. 3DES)
|
||||
const cryptoEngine = getLinerCryptoEngine();
|
||||
|
||||
const result = await envelopedData.decrypt(
|
||||
recipientIndex,
|
||||
|
||||
+53
-2
@@ -7,6 +7,8 @@
|
||||
* Reference: MS-OXTNEF / MS-TNEF specification.
|
||||
*/
|
||||
|
||||
import { debug } from '@/lib/debug';
|
||||
|
||||
// TNEF signature
|
||||
const TNEF_SIGNATURE = 0x223E9F78;
|
||||
|
||||
@@ -234,35 +236,58 @@ export function parseTnef(data: Uint8Array): TnefResult {
|
||||
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 signature = r.readUint32LE();
|
||||
if (signature !== TNEF_SIGNATURE) {
|
||||
debug.warn('Invalid TNEF signature:', '0x' + signature.toString(16).toUpperCase(), '(expected 0x223E9F78)');
|
||||
debug.groupEnd();
|
||||
return result;
|
||||
}
|
||||
debug.log('TNEF signature valid');
|
||||
|
||||
r.skip(2); // legacy key
|
||||
|
||||
// Current attachment being assembled
|
||||
let curAttach: { name: string; mimeType: string; data: Uint8Array | null } | null = null;
|
||||
let attrCount = 0;
|
||||
|
||||
while (r.remaining >= 11) {
|
||||
const level = r.readUint8();
|
||||
const attrID = 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);
|
||||
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 (attrID === attBody) {
|
||||
result.body = new TextDecoder('utf-8').decode(attrData);
|
||||
debug.log(' → Extracted plain text body (' + result.body.length + ' chars)');
|
||||
} else if (attrID === attMAPIProps) {
|
||||
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
|
||||
const htmlProp = props.get(PR_BODY_HTML);
|
||||
@@ -273,6 +298,9 @@ export function parseTnef(data: Uint8Array): TnefResult {
|
||||
} else {
|
||||
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)
|
||||
@@ -280,6 +308,9 @@ export function parseTnef(data: Uint8Array): TnefResult {
|
||||
const bodyProp = props.get(PR_BODY);
|
||||
if (bodyProp?.value instanceof Uint8Array) {
|
||||
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) {
|
||||
// Start of a new attachment — flush previous
|
||||
if (curAttach?.data) {
|
||||
debug.log(' → Flushing previous attachment:', curAttach.name, '(' + curAttach.mimeType + ',', curAttach.data.byteLength, 'bytes)');
|
||||
result.attachments.push({
|
||||
name: curAttach.name,
|
||||
mimeType: curAttach.mimeType,
|
||||
@@ -294,28 +326,40 @@ export function parseTnef(data: Uint8Array): TnefResult {
|
||||
});
|
||||
}
|
||||
curAttach = { name: 'attachment', mimeType: 'application/octet-stream', data: null };
|
||||
debug.log(' → New attachment started');
|
||||
} else if (attrID === attAttachTitle && curAttach) {
|
||||
let len = attrData.byteLength;
|
||||
if (len > 0 && attrData[len - 1] === 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) {
|
||||
curAttach.data = attrData;
|
||||
debug.log(' → Attachment data (attAttachData):', attrData.byteLength, 'bytes');
|
||||
} else if (attrID === attAttachment && curAttach) {
|
||||
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);
|
||||
if (longName?.value instanceof Uint8Array) {
|
||||
curAttach.name = decodeMAPIString(longName.value, longName.type & 0x0FFF);
|
||||
debug.log(' → Attachment long filename:', curAttach.name);
|
||||
}
|
||||
|
||||
const mimeTag = props.get(PR_ATTACH_MIME_TAG);
|
||||
if (mimeTag?.value instanceof Uint8Array) {
|
||||
curAttach.mimeType = decodeMAPIString(mimeTag.value, mimeTag.type & 0x0FFF);
|
||||
debug.log(' → Attachment MIME type:', curAttach.mimeType);
|
||||
}
|
||||
|
||||
const attachData = props.get(PR_ATTACH_DATA_BIN);
|
||||
if (attachData?.value instanceof Uint8Array) {
|
||||
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
|
||||
if (curAttach?.data) {
|
||||
debug.log('Flushing final attachment:', curAttach.name, '(' + curAttach.mimeType + ',', curAttach.data.byteLength, 'bytes)');
|
||||
result.attachments.push({
|
||||
name: curAttach.name,
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -191,6 +191,8 @@
|
||||
"mark_read": "Mark as read",
|
||||
"print": "Print",
|
||||
"view_source": "View source",
|
||||
"export_email": "Export as .eml",
|
||||
"import_email": "Import .eml",
|
||||
"keyboard_shortcuts": "Keyboard shortcuts (?)",
|
||||
"email_source": "Email Source",
|
||||
"copy_source": "Copy to clipboard",
|
||||
@@ -527,7 +529,10 @@
|
||||
"templates_exported": "Templates exported successfully",
|
||||
"templates_imported": "{count, plural, one {# template} other {# templates}} 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": {
|
||||
"today": "Today",
|
||||
|
||||
Generated
+188
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "bulwark-webmail",
|
||||
"version": "1.4.0",
|
||||
"version": "1.4.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "bulwark-webmail",
|
||||
"version": "1.4.0",
|
||||
"version": "1.4.1",
|
||||
"license": "AGPL-3.0-only",
|
||||
"dependencies": {
|
||||
"@tanstack/react-virtual": "^3.13.18",
|
||||
@@ -24,6 +24,7 @@
|
||||
"react-dom": "^19.2.1",
|
||||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"webcrypto-liner": "^1.4.3",
|
||||
"zustand": "^5.0.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -2375,6 +2376,29 @@
|
||||
"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": {
|
||||
"version": "1.58.2",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz",
|
||||
@@ -2761,6 +2785,44 @@
|
||||
"integrity": "sha512-bXHSaW5jRTmke9Vd0h5P7BtWZG9Znqb8gSDxZnxaGSJnGwPLDPfS+3g0BKzeWqzgZPsIVZkM7m2tbo18cm5HBw==",
|
||||
"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": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
|
||||
@@ -4075,6 +4137,12 @@
|
||||
"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": {
|
||||
"version": "3.0.7",
|
||||
"resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.7.tgz",
|
||||
@@ -4154,6 +4222,12 @@
|
||||
"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": {
|
||||
"version": "5.0.4",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz",
|
||||
@@ -4177,6 +4251,12 @@
|
||||
"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": {
|
||||
"version": "4.28.1",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
|
||||
@@ -4376,6 +4456,17 @@
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
@@ -4600,6 +4691,16 @@
|
||||
"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": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||
@@ -4660,6 +4761,20 @@
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "5.20.0",
|
||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.0.tgz",
|
||||
@@ -5663,6 +5778,16 @@
|
||||
"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": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||
@@ -5693,6 +5818,17 @@
|
||||
"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": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz",
|
||||
@@ -5812,6 +5948,12 @@
|
||||
"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": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz",
|
||||
@@ -6779,6 +6921,18 @@
|
||||
"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": {
|
||||
"version": "10.2.4",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz",
|
||||
@@ -8805,6 +8959,38 @@
|
||||
"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": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz",
|
||||
|
||||
+2
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bulwark-webmail",
|
||||
"version": "1.4.0",
|
||||
"version": "1.4.1",
|
||||
"description": "Bulwark Webmail — a modern webmail client built for Stalwart Mail Server",
|
||||
"author": "Bulwark Webmail <bulwark@rbm.systems>",
|
||||
"license": "AGPL-3.0-only",
|
||||
@@ -47,6 +47,7 @@
|
||||
"react-dom": "^19.2.1",
|
||||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"webcrypto-liner": "^1.4.3",
|
||||
"zustand": "^5.0.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
Reference in New Issue
Block a user