@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 +4 -0
- package/lib/mail-crypto-pgp.js +41 -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,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)
|
package/lib/mail-crypto-pgp.js
CHANGED
|
@@ -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,
|
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
|
]
|