@blamejs/core 0.11.32 → 0.11.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2 -0
- package/lib/mail-server-imap.js +66 -6
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,8 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.11.x
|
|
10
10
|
|
|
11
|
+
- v0.11.33 (2026-05-21) — **IMAP QRESYNC (RFC 7162 §3.2) — VANISHED responses + SELECT delta on `b.mail.server.imap`.** Closes the v0.11.27 deferral. The IMAP listener now advertises QRESYNC in CAPABILITY, accepts `ENABLE QRESYNC` (which implicitly engages CONDSTORE per §3.2.5), and parses the `(QRESYNC (<uidvalidity> <modseq> [<knownUids>] [<knownSequenceMatchData>]))` parameter list on SELECT / EXAMINE. When the client's UIDVALIDITY matches the backend's, the listener emits a single `* VANISHED (EARLIER) <uid-set>` listing UIDs the server expunged since the client's snapshot — operators implement the actual delta computation via `mailStore.selectFolder(actor, mailbox, { qresync }) → { ..., vanishedEarlier }`. Stale-UIDVALIDITY clients fall through to a full re-SELECT. **Added:** *CAPABILITY advertises `QRESYNC`* — Sits next to the existing `CONDSTORE` advertisement. Both extensions are server-advertised; clients ENABLE before relying on the responses. RFC 7162 §3.2.5 — QRESYNC implies CONDSTORE. · *`ENABLE QRESYNC` engages both flags* — The handler flips `state.enabledQResync = true` AND `state.enabledCondStore = true` and emits `* ENABLED QRESYNC` + `OK ENABLE completed`. Already-engaged QRESYNC re-issues skip the advertisement line (only newly-engaged extensions appear). · *`SELECT mailbox (QRESYNC (<uidvalidity> <modseq> [<knownUids>] [<knownSeq>]))`* — The QRESYNC parameter list is stripped from the SELECT args before the mailbox-name validator runs and forwarded to `mailStore.selectFolder` as `opts.qresync = { uidvalidity, modseq, knownUids, knownSeq }`. Backends compute the delta and return `vanishedEarlier` (sequence-set string) in the existing select-info object. Non-finite uidvalidity / modseq values refuse with `BAD SELECT QRESYNC params must be (<uidvalidity> <modseq> ...) numerics` before the backend call. · *Implicit CONDSTORE+QRESYNC engagement on parameterised SELECT* — Per RFC 7162 §3.2.4, a client that issues `SELECT INBOX (QRESYNC (...))` without a prior `ENABLE QRESYNC` flips both flags implicitly for the session. Subsequent FETCH responses include `MODSEQ (<n>)` just as they would after explicit ENABLE. · *`* VANISHED (EARLIER) <uid-set>` emission* — The listener emits a single VANISHED untagged response between `HIGHESTMODSEQ` and the tagged OK when (a) the client supplied a QRESYNC parameter, (b) the client's UIDVALIDITY matches the backend's, AND (c) the backend supplied a non-empty `vanishedEarlier`. Mismatched UIDVALIDITY suppresses the VANISHED line so the client correctly falls through to a full re-sync (RFC 7162 §3.2.5). **Security:** *QRESYNC parameter parse is bounded* — The regex anchors on `\(\s*QRESYNC\s*\(\s*([^)]+)\)` — `[^)]*` not `.*` — so a malformed parameter list cannot consume the entire SELECT args. Both `uidvalidity` and `modseq` parse strictly as `\d+` via `parseInt(...)` + `isFinite` check; non-numeric values refuse before the backend dispatch. · *VANISHED suppressed on UIDVALIDITY mismatch* — RFC 7162 §3.2.5 — when the client's `uidvalidity` does NOT match the server's current value, the mailbox has been reset (e.g., recreated under the same name) and the client's UID cache is invalid. The listener intentionally drops the VANISHED line so the client forces a fresh full-sync instead of acting on a delta that references a different message set. **References:** [RFC 7162 §3.2 (QRESYNC — Quick Mailbox Resynchronization)](https://www.rfc-editor.org/rfc/rfc7162.html) · [RFC 9051 (IMAP4rev2)](https://www.rfc-editor.org/rfc/rfc9051.html) · [RFC 5161 (IMAP ENABLE Extension)](https://www.rfc-editor.org/rfc/rfc5161.html)
|
|
12
|
+
|
|
11
13
|
- v0.11.32 (2026-05-21) — **`b.mail.crypto.pgp.encrypt` / `.decrypt` / `.wkd` promoted to stable — WKD IDN-homograph defense.** The PGP encrypt / decrypt / Web Key Directory (WKD) primitives that shipped under `b.mail.crypto.pgp.experimental.*` at v0.10.16 are promoted to top-level stable. `b.mail.crypto.pgp.encrypt` / `b.mail.crypto.pgp.decrypt` / `b.mail.crypto.pgp.wkd.fetch` / `b.mail.crypto.pgp.wkd.computeUrl` are now the canonical paths; the v0.10.16 `experimental` aliases continue to work so existing operator code keeps importing through the old path until they migrate at their own pace. WKD picks up a hard IDN-homograph refusal at `wkd.computeUrl` — Cyrillic / Greek / full-width / Han confusable domain characters refuse with `mail-crypto/pgp/bad-domain` before any HTTP fetch runs. Operators with internationalised domains MUST Punycode-encode upstream (RFC 3492 `xn--` form). **Added:** *Top-level `b.mail.crypto.pgp.encrypt` / `.decrypt` / `.wkd.{fetch,computeUrl}`* — Same function bodies that shipped under `b.mail.crypto.pgp.experimental.*` at v0.10.16 are now exported at the top of the namespace. The framework-private envelope (BJ-PGP-PQ magic + version) is unchanged; the IANA-pending RFC 9580bis ML-KEM PKESK codepoints will land as an alternate-encoding option in a follow-up slice. The `experimental` alias keeps the v0.10.16 import paths working — operator migration is opt-in. `b.mail.crypto.pgp.encrypt === b.mail.crypto.pgp.experimental.encrypt` (same reference) so a feature-detection test like `typeof b.mail.crypto.pgp.encrypt === 'function'` is the new operator-facing pattern. **Security:** *WKD IDN-homograph refusal at `wkd.computeUrl`* — `wkd.computeUrl(email)` now refuses any domain containing characters outside the LDH+dot ASCII subset (RFC 952 / RFC 1123 §2). The threat model: Cyrillic `а` (U+0430), Greek `ο` (U+03BF), full-width `A` (U+FF21), and Han homographs visually impersonate Latin letters in `paypa1.com` / `gοogle.com` style phishing host strings; a naive `toLowerCase` + concat into the WKD URL would route the key fetch to the attacker's domain. Operators with legitimate internationalised domains MUST Punycode-encode upstream (the `xn--` form is plain ASCII LDH and passes). The framework's `b.httpClient` already refuses non-ASCII hostnames at the SSRF guard layer; this primitive surfaces the same refusal at the WKD entry point so the error surface is consistent. · *RFC 5321 + RFC 1035 length caps* — Email length capped at 320 octets (RFC 5321 §4.5.3.1 maximum). Domain length capped at 253 octets (RFC 1035 §2.3.4). Empty labels (`bad..example.com`), leading-dot, and trailing-dot domains refuse before any URL construction. Adversarial-length inputs cannot reach the tokenizer / hasher path. **References:** [draft-koch-openpgp-webkey-service (Web Key Directory)](https://datatracker.ietf.org/doc/draft-koch-openpgp-webkey-service/) · [RFC 9580 (OpenPGP)](https://www.rfc-editor.org/rfc/rfc9580.html) · [RFC 3492 (Punycode — IDNA `xn--` form)](https://www.rfc-editor.org/rfc/rfc3492.html) · [RFC 5891 (IDNA 2008)](https://www.rfc-editor.org/rfc/rfc5891.html) · [RFC 5321 §4.5.3.1 (SMTP — local-part + domain octet maximums)](https://www.rfc-editor.org/rfc/rfc5321.html) · [RFC 1035 §2.3.4 (domain label length)](https://www.rfc-editor.org/rfc/rfc1035.html) · [Unicode TR 39 (Security Mechanisms — confusable identifiers)](https://www.unicode.org/reports/tr39/)
|
|
12
14
|
|
|
13
15
|
- v0.11.31 (2026-05-21) — **`b.calendar` — JSCalendar (RFC 8984) primitive + JMAP Calendars method catalogue.** JSCalendar is the JSON-native calendar grammar JMAP Calendars rides on. The framework now ships `b.calendar` — a thin layer over the existing `b.safeIcal.parse` (RFC 5545 bounded parser shipped earlier) that exposes validate / fromIcal / toIcal / expandRecurrence. The JMAP method catalogue at `b.mail.serverRegistry` picks up the 15 Calendar / CalendarEvent / CalendarEventNotification / ParticipantIdentity methods per the draft-ietf-jmap-calendars spec, so operators wire them through the existing `b.mail.server.jmap.create({ methods: { ... } })` dispatch path without `allowExperimental: true` escape-hatches.
|
package/lib/mail-server-imap.js
CHANGED
|
@@ -646,8 +646,11 @@ function create(opts) {
|
|
|
646
646
|
var caps = ["IMAP4rev2"];
|
|
647
647
|
if (!state.tls) caps.push("STARTTLS");
|
|
648
648
|
// RFC 7162 §3 — CONDSTORE is server-advertised; clients ENABLE
|
|
649
|
-
// before relying on MODSEQ in untagged FETCH responses.
|
|
649
|
+
// before relying on MODSEQ in untagged FETCH responses. QRESYNC
|
|
650
|
+
// (§3.2) adds the VANISHED responses on SELECT + post-EXPUNGE
|
|
651
|
+
// and implicitly engages CONDSTORE per §3.2.5.
|
|
650
652
|
caps.push("CONDSTORE");
|
|
653
|
+
caps.push("QRESYNC");
|
|
651
654
|
// v0.11.28 — opt-in extensions (advertised so capable clients can
|
|
652
655
|
// exercise them; each handler refuses gracefully when the operator
|
|
653
656
|
// backend doesn't supply the corresponding hook).
|
|
@@ -690,10 +693,18 @@ function create(opts) {
|
|
|
690
693
|
state.enabledCondStore = true;
|
|
691
694
|
enabled.push("CONDSTORE");
|
|
692
695
|
}
|
|
696
|
+
} else if (name === "QRESYNC") {
|
|
697
|
+
// RFC 7162 §3.2.5 — QRESYNC implicitly engages CONDSTORE.
|
|
698
|
+
// The client signals it can consume `* VANISHED (EARLIER)`
|
|
699
|
+
// responses on SELECT / EXAMINE + post-EXPUNGE; the listener
|
|
700
|
+
// flips both flags and the SELECT handler honours the
|
|
701
|
+
// QRESYNC parameter list when present.
|
|
702
|
+
if (!state.enabledQResync) {
|
|
703
|
+
state.enabledQResync = true;
|
|
704
|
+
state.enabledCondStore = true;
|
|
705
|
+
enabled.push("QRESYNC");
|
|
706
|
+
}
|
|
693
707
|
}
|
|
694
|
-
// QRESYNC (RFC 7162 §3.2.5) implies CONDSTORE — accepted only
|
|
695
|
-
// when the operator backend supplies the QRESYNC vanished /
|
|
696
|
-
// expunged-set surface; v1 of the listener stops at CONDSTORE.
|
|
697
708
|
}
|
|
698
709
|
_writeUntagged(socket, "ENABLED" + (enabled.length ? " " + enabled.join(" ") : ""));
|
|
699
710
|
_writeTagged(socket, tag, "OK ENABLE completed");
|
|
@@ -1148,15 +1159,47 @@ function create(opts) {
|
|
|
1148
1159
|
|
|
1149
1160
|
function _handleSelect(state, socket, tag, args, examine) {
|
|
1150
1161
|
if (!_requireAuth(state, socket, tag)) return;
|
|
1151
|
-
var
|
|
1162
|
+
var trimmed = (args || "").trim();
|
|
1163
|
+
// RFC 7162 §3.2.4 — `SELECT mailbox (QRESYNC (<uidvalidity>
|
|
1164
|
+
// <modseq> [<knownUids>] [<knownSequenceMatchData>]))`. The
|
|
1165
|
+
// QRESYNC parameter is wrapped in an outer parenthesis pair after
|
|
1166
|
+
// the mailbox name. Extract it before parsing the mailbox so the
|
|
1167
|
+
// mailbox-name validator sees just the name.
|
|
1168
|
+
var qresyncParam = null;
|
|
1169
|
+
var qresyncMatch = trimmed.match(/^(\S+|"[^"]+")\s+\(\s*QRESYNC\s*\(\s*([^)]+)\)\s*(?:\(\s*([^)]+)\)\s*)?\)\s*$/i); // allow:regex-no-length-cap — args length already capped upstream
|
|
1170
|
+
if (qresyncMatch) {
|
|
1171
|
+
var inner = qresyncMatch[2].trim().split(/\s+/);
|
|
1172
|
+
qresyncParam = {
|
|
1173
|
+
uidvalidity: parseInt(inner[0], 10),
|
|
1174
|
+
modseq: parseInt(inner[1], 10),
|
|
1175
|
+
knownUids: inner[2] || null,
|
|
1176
|
+
knownSeq: qresyncMatch[3] || null,
|
|
1177
|
+
};
|
|
1178
|
+
if (!isFinite(qresyncParam.uidvalidity) || !isFinite(qresyncParam.modseq)) {
|
|
1179
|
+
_writeTagged(socket, tag, "BAD SELECT QRESYNC params must be (<uidvalidity> <modseq> ...) numerics");
|
|
1180
|
+
return;
|
|
1181
|
+
}
|
|
1182
|
+
trimmed = qresyncMatch[1];
|
|
1183
|
+
}
|
|
1184
|
+
var name = _unquote(trimmed);
|
|
1152
1185
|
if (!_validateMailboxName(name, { allowLegacyMUtf7: allowLegacyMUtf7 })) {
|
|
1153
1186
|
_writeTagged(socket, tag, "BAD Mailbox name refused");
|
|
1154
1187
|
return;
|
|
1155
1188
|
}
|
|
1189
|
+
// QRESYNC requires CONDSTORE to be engaged; if the client sent
|
|
1190
|
+
// the parameter without having issued ENABLE first, RFC 7162
|
|
1191
|
+
// §3.2.4 lets the server flip the flags implicitly.
|
|
1192
|
+
if (qresyncParam && !state.enabledQResync) {
|
|
1193
|
+
state.enabledQResync = true;
|
|
1194
|
+
state.enabledCondStore = true;
|
|
1195
|
+
}
|
|
1156
1196
|
Promise.resolve()
|
|
1157
1197
|
.then(function () {
|
|
1158
1198
|
if (typeof mailStore.selectFolder === "function") {
|
|
1159
|
-
return mailStore.selectFolder(state.actor, name, {
|
|
1199
|
+
return mailStore.selectFolder(state.actor, name, {
|
|
1200
|
+
readOnly: examine,
|
|
1201
|
+
qresync: qresyncParam,
|
|
1202
|
+
});
|
|
1160
1203
|
}
|
|
1161
1204
|
// RFC 9051 §2.3.1.1 — UIDVALIDITY MUST be strictly increasing
|
|
1162
1205
|
// and 32-bit unique across the mailbox lifetime. The earlier
|
|
@@ -1183,9 +1226,26 @@ function create(opts) {
|
|
|
1183
1226
|
if (info.modseq !== undefined) {
|
|
1184
1227
|
_writeUntagged(socket, "OK [HIGHESTMODSEQ " + info.modseq + "]");
|
|
1185
1228
|
}
|
|
1229
|
+
// RFC 7162 §3.2.5 — when SELECT carried a QRESYNC parameter
|
|
1230
|
+
// AND the client's UIDVALIDITY matches, emit a single
|
|
1231
|
+
// `* VANISHED (EARLIER) <uid-set>` listing UIDs the server
|
|
1232
|
+
// expunged since the client's snapshot. The backend supplies
|
|
1233
|
+
// this via `info.vanishedEarlier` (sequence-set string) — the
|
|
1234
|
+
// listener does the wire emission. Mismatched UIDVALIDITY
|
|
1235
|
+
// means the client's cache is stale and MUST re-SELECT; we
|
|
1236
|
+
// skip the VANISHED line in that case so the client falls
|
|
1237
|
+
// through to a full re-sync. RFC 7162 §3.2.5.2 says the
|
|
1238
|
+
// server MAY also include changed-since-modseq FETCH lines
|
|
1239
|
+
// — those flow through the normal FETCH path with
|
|
1240
|
+
// CHANGEDSINCE so we leave them to the operator.
|
|
1241
|
+
if (qresyncParam && info.vanishedEarlier &&
|
|
1242
|
+
info.uidvalidity === qresyncParam.uidvalidity) {
|
|
1243
|
+
_writeUntagged(socket, "VANISHED (EARLIER) " + info.vanishedEarlier);
|
|
1244
|
+
}
|
|
1186
1245
|
_emit("mail.server.imap.select", {
|
|
1187
1246
|
connectionId: state.id, mailbox: name,
|
|
1188
1247
|
modseq: info.modseq || 0, exists: info.exists,
|
|
1248
|
+
qresync: qresyncParam !== null,
|
|
1189
1249
|
});
|
|
1190
1250
|
_writeTagged(socket, tag, "OK [" + (examine ? "READ-ONLY" : "READ-WRITE") + "] " +
|
|
1191
1251
|
(examine ? "EXAMINE" : "SELECT") + " completed");
|
package/package.json
CHANGED
package/sbom.cdx.json
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
|
|
3
3
|
"bomFormat": "CycloneDX",
|
|
4
4
|
"specVersion": "1.5",
|
|
5
|
-
"serialNumber": "urn:uuid:
|
|
5
|
+
"serialNumber": "urn:uuid:10fe079a-bfcc-4ffe-84e1-55a6b5b11ccf",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-05-
|
|
8
|
+
"timestamp": "2026-05-21T18:47:05.672Z",
|
|
9
9
|
"lifecycles": [
|
|
10
10
|
{
|
|
11
11
|
"phase": "build"
|
|
@@ -19,14 +19,14 @@
|
|
|
19
19
|
}
|
|
20
20
|
],
|
|
21
21
|
"component": {
|
|
22
|
-
"bom-ref": "@blamejs/core@0.11.
|
|
22
|
+
"bom-ref": "@blamejs/core@0.11.33",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.11.
|
|
25
|
+
"version": "0.11.33",
|
|
26
26
|
"scope": "required",
|
|
27
27
|
"author": "blamejs contributors",
|
|
28
28
|
"description": "The Node framework that owns its stack.",
|
|
29
|
-
"purl": "pkg:npm/%40blamejs/core@0.11.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/core@0.11.33",
|
|
30
30
|
"properties": [],
|
|
31
31
|
"externalReferences": [
|
|
32
32
|
{
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"components": [],
|
|
55
55
|
"dependencies": [
|
|
56
56
|
{
|
|
57
|
-
"ref": "@blamejs/core@0.11.
|
|
57
|
+
"ref": "@blamejs/core@0.11.33",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|