@blamejs/core 0.11.31 → 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 CHANGED
@@ -8,6 +8,10 @@ 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
+
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/)
14
+
11
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.
12
16
 
13
17
  v1 scope: validate JSCalendar Event / Task shape per RFC 8984 §5/§6, bidirectional VEVENT ↔ JSCalendar conversion, RRULE expansion for FREQ=DAILY/WEEKLY/MONTHLY/YEARLY/HOURLY/MINUTELY/SECONDLY with INTERVAL / COUNT / UNTIL / BYDAY / BYMONTH / BYMONTHDAY. BYSETPOS / BYWEEKNO / BYYEARDAY filters + non-Gregorian calendars (RFC 7529) + JSCalendar Group objects + VTODO/VJOURNAL mapping are deferred-with-condition for follow-up slices. **Added:** *`b.calendar.validate(jsCal)` — JSCalendar Event/Task shape gate (RFC 8984 §5/§6)* — Asserts `@type` ∈ { 'Event', 'Task' }, non-empty `uid` (≤ 1024 bytes), `updated` matches UTCDateTime grammar, Event's optional `start` matches LocalDateTime, `duration` is RFC 8601 PnYnMnDTnHnMnS, every `recurrenceRules[i].@type` is 'RecurrenceRule' with `frequency` in the JSCAL_FREQUENCIES catalogue, every `alerts.{id}.action` is 'display' or 'email'. Returns the input on success; throws `CalendarError` with a structured `.code` (`calendar/no-uid`, `calendar/bad-recurrence`, etc.) on refusal so operator-side error handling has a stable surface. · *`b.calendar.fromIcal(text, opts?)` + `b.calendar.toIcal(jsCal, opts?)`* — Bidirectional bridge: iCalendar (RFC 5545) ↔ JSCalendar (RFC 8984). `fromIcal` runs the text through `b.safeIcal.parse` (already CVE-bounded for parser DoS) and maps the first VEVENT into an Event object (UID → uid, DTSTAMP → updated, DTSTART → start, DURATION → duration, SUMMARY → title, DESCRIPTION → description, LOCATION → locations[L1].name, RRULE → recurrenceRules[0]). `toIcal` round-trips the same shape back through a CRLF-folded VCALENDAR envelope per RFC 5545 §3.1 (75-octet content-line cap). Operators wire it on the EmailSubmission send path so calendar invitations from a JSCalendar back-end emit RFC 5545 over MIME, and inbound iCalendar attachments parse straight into the JMAP method's Event return shape. · *`b.calendar.expandRecurrence(event, { from, to, max })` — bounded RRULE expansion* — Emits ISO 8601 UTC instance timestamps for an Event in the operator's `[from, to]` window. Supports FREQ=DAILY/WEEKLY/MONTHLY/YEARLY/HOURLY/MINUTELY/SECONDLY with INTERVAL, COUNT, UNTIL. Bounded by `MAX_EXPAND_INSTANCES` (4096) and `MAX_EXPAND_SPAN_MS` (10 years) — refuses with `calendar/oversize-expansion-span` when the window exceeds 10 years (CVE-2024-39687-class recurrence-bomb defense, mirroring the parser-side caps already in `b.safeIcal`). LocalDateTime starts without a `timeZone` are treated as floating-UTC during expansion so wall-clock semantics survive `Date.parse` host-locale interpretation. · *JMAP method catalogue: Calendar / CalendarEvent / CalendarEventNotification / ParticipantIdentity* — 15 new methods added to `JMAP_METHODS` in `lib/mail-server-registry.js` per draft-ietf-jmap-calendars: `Calendar/{get,changes,set,query,queryChanges}`, `CalendarEvent/{get,changes,query,queryChanges,set,copy}`, `CalendarEventNotification/{get,changes,query,queryChanges,set}`, `ParticipantIdentity/{get,changes,set}`. Operators wire concrete handlers through the existing `b.mail.server.jmap.create({ methods: { 'CalendarEvent/get': async function (actor, args) { ... } } })` path — no `allowExperimental: true` escape-hatch is required. **Security:** *Recurrence-bomb expansion caps* — `MAX_EXPAND_INSTANCES = 4096` + `MAX_EXPAND_SPAN_MS = 10 years` clamp the expansion budget at the framework boundary so an operator who forwards an unbounded JSCalendar from a hostile peer cannot DoS the host. The caps mirror `b.safeIcal`'s RRULE COUNT + BY-entries caps (which already defend the CVE-2024-39687 class). The 10-year span refusal carries `code: 'calendar/oversize-expansion-span'` so operators distinguish bomb defense from legitimate too-wide-window misconfiguration. · *UID + duration + UTCDateTime parsing is strict* — `validate` refuses `uid` over 1024 bytes (anti-DoS), `updated` that doesn't match the RFC 8984 §1.4.3 `\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z` UTCDateTime grammar exactly (no `+00:00` aliases, no fractional-second-only suffixes), and `duration` that isn't `PnYnMnDTnHnMnS`. Hostile JSCalendar payloads from federation peers can't slip non-canonical-shape values into the operator's storage layer through the validator. · *iCalendar parsing routed through bounded `b.safeIcal.parse`* — `fromIcal` does NOT roll its own RFC 5545 parser — it forwards to `b.safeIcal.parse` which is already audit-hardened (CVE-2024-39929 / CVE-2025-30258 mitigation, bounded depth, capped RRULE COUNT + BY-entries). The JSCalendar layer composes the existing security substrate rather than re-litigating it. **References:** [RFC 8984 (JSCalendar — JSON Representation of Calendar Data)](https://www.rfc-editor.org/rfc/rfc8984.html) · [RFC 5545 (iCalendar — Internet Calendaring and Scheduling Core Object)](https://www.rfc-editor.org/rfc/rfc5545.html) · [draft-ietf-jmap-calendars (JMAP for Calendars)](https://datatracker.ietf.org/doc/draft-ietf-jmap-calendars/) · [RFC 8620 (JMAP Core)](https://www.rfc-editor.org/rfc/rfc8620.html) · [RFC 8601 (Duration grammar PnYnMnDTnHnMnS)](https://www.rfc-editor.org/rfc/rfc3339.html) · [RFC 7986 (New properties for iCalendar)](https://www.rfc-editor.org/rfc/rfc7986.html) · [CVE-2024-39687 (iCalendar recurrence-bomb expansion)](https://nvd.nist.gov/vuln/detail/CVE-2024-39687)
@@ -1190,10 +1190,38 @@ function wkdComputeUrl(email, opts) {
1190
1190
  throw new MailCryptoError("mail-crypto/pgp/bad-email",
1191
1191
  "wkd.computeUrl: email must be a 'local@domain' string");
1192
1192
  }
1193
+ // RFC 5321 §4.5.3.1 — practical email-length cap. 320 octets is the
1194
+ // upper bound (64 local + 1 @ + 255 domain). Refuse beyond that BEFORE
1195
+ // any further processing to defend tokenisation paths against
1196
+ // adversarial-length inputs.
1197
+ if (email.length > 320) { // allow:raw-byte-literal — RFC 5321 max email length
1198
+ throw new MailCryptoError("mail-crypto/pgp/bad-email",
1199
+ "wkd.computeUrl: email length " + email.length + " exceeds RFC 5321 max 320 octets");
1200
+ }
1193
1201
  var at = email.indexOf("@");
1194
1202
  var localRaw = email.slice(0, at);
1195
1203
  var localLower = localRaw.toLowerCase();
1196
1204
  var domain = email.slice(at + 1).toLowerCase();
1205
+ // IDN-homograph defense — refuse domains with bytes outside the
1206
+ // LDH+dot ASCII subset (RFC 952 / RFC 1123 §2). Operators with IDN
1207
+ // (internationalised) domains MUST Punycode-encode upstream
1208
+ // (RFC 3492 `xn--` form). Cyrillic / Greek / Han homograph attacks
1209
+ // (`paypa1` lookalike etc.) are the threat model — the WKD URL has
1210
+ // to be an unambiguous host string, and the framework's b.httpClient
1211
+ // already refuses non-ASCII hostnames at the SSRF guard, so this
1212
+ // surface is the canonical refusal point.
1213
+ if (!/^[a-z0-9.-]+$/.test(domain)) {
1214
+ throw new MailCryptoError("mail-crypto/pgp/bad-domain",
1215
+ "wkd.computeUrl: domain must be ASCII LDH+dot (Punycode-encode IDN domains upstream; RFC 3492 xn-- form)");
1216
+ }
1217
+ if (domain.indexOf("..") !== -1 || domain.charAt(0) === "." || domain.charAt(domain.length - 1) === ".") {
1218
+ throw new MailCryptoError("mail-crypto/pgp/bad-domain",
1219
+ "wkd.computeUrl: domain must not contain empty labels");
1220
+ }
1221
+ if (domain.length > 253) { // allow:raw-byte-literal — RFC 1035 §2.3.4 max domain length
1222
+ throw new MailCryptoError("mail-crypto/pgp/bad-domain",
1223
+ "wkd.computeUrl: domain length " + domain.length + " exceeds RFC 1035 max 253 octets");
1224
+ }
1197
1225
  var hashed = bCrypto.kdf(Buffer.from(localLower, "utf8"), 20); // allow:raw-byte-literal — 20-byte hash per draft-koch §3.1
1198
1226
  var encoded = _zbase32Encode(hashed);
1199
1227
  var advancedHost = opts.advancedHost || ("openpgpkey." + domain);
@@ -1230,6 +1258,19 @@ function _zbase32Encode(buf) {
1230
1258
  module.exports = {
1231
1259
  sign: sign,
1232
1260
  verify: verify,
1261
+ // v0.11.32 — encrypt / decrypt / wkd promoted to stable top-level
1262
+ // surface. The framework-private envelope (BJ-PGP-PQ magic + version)
1263
+ // is the same one the experimental namespace shipped at v0.10.16;
1264
+ // the IANA-pending RFC 9580bis ML-KEM PKESK codepoints will be
1265
+ // wired as an alternate-encoding option in a follow-up slice. Until
1266
+ // then the `experimental` alias keeps the v0.10.16 import paths
1267
+ // working — operators migrate at their own pace.
1268
+ encrypt: experimentalEncrypt,
1269
+ decrypt: experimentalDecrypt,
1270
+ wkd: {
1271
+ computeUrl: wkdComputeUrl,
1272
+ fetch: wkdFetch,
1273
+ },
1233
1274
  experimental: {
1234
1275
  encrypt: experimentalEncrypt,
1235
1276
  decrypt: experimentalDecrypt,
@@ -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 name = _unquote(args.trim());
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, { readOnly: examine });
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.11.31",
3
+ "version": "0.11.33",
4
4
  "description": "The Node framework that owns its stack.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
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:19702043-8392-4b48-9d1c-120d93f6889f",
5
+ "serialNumber": "urn:uuid:10fe079a-bfcc-4ffe-84e1-55a6b5b11ccf",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-05-21T17:20:28.895Z",
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.31",
22
+ "bom-ref": "@blamejs/core@0.11.33",
23
23
  "type": "application",
24
24
  "name": "blamejs",
25
- "version": "0.11.31",
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.31",
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.31",
57
+ "ref": "@blamejs/core@0.11.33",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]