@bobfrankston/iflow-direct 0.1.62 → 0.1.64

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.
Files changed (2) hide show
  1. package/imap-protocol.js +41 -4
  2. package/package.json +1 -1
package/imap-protocol.js CHANGED
@@ -344,6 +344,40 @@ function unquote(s) {
344
344
  return s.slice(1, -1).replace(/\\(.)/g, "$1");
345
345
  return s;
346
346
  }
347
+ /** Build a TextDecoder for an encoded-word charset label, trying progressively
348
+ * looser readings of the label before giving up on UTF-8.
349
+ *
350
+ * TextDecoder throws RangeError on a label it doesn't know, and the caller's
351
+ * catch then returned the RAW encoded text — so ONE unrecognized label put
352
+ * `London_Startup_Is_Selling_Permanent=2C_Painless=2C_=22Stick-On=22` in the
353
+ * user's subject line (Bob 2026-08-02, NNSquad via mailman). Labels seen in
354
+ * the wild that TextDecoder rejects:
355
+ * `en_US.UTF-8` — a POSIX LOCALE (language_TERRITORY.CODESET), not a
356
+ * charset; the real charset is the codeset after the dot.
357
+ * `utf-8*en` — RFC 2231 language suffix appended to the charset.
358
+ * `utf8` — no hyphen.
359
+ * The raw label is tried FIRST so legitimate dotted names (`ansi_x3.4-1968`)
360
+ * are never mangled by the locale rule. Last resort is UTF-8, which decodes
361
+ * leniently (U+FFFD for bad bytes) rather than throwing — a couple of
362
+ * replacement characters beat a subject full of quoted-printable. */
363
+ function decoderFor(label) {
364
+ const raw = label.trim().toLowerCase();
365
+ const candidates = [raw];
366
+ const noLang = raw.split("*")[0]; // RFC 2231 suffix
367
+ if (noLang !== raw)
368
+ candidates.push(noLang);
369
+ const locale = noLang.match(/^[a-z]{1,8}(?:_[a-z0-9]{1,8})?\.(.+)$/);
370
+ if (locale)
371
+ candidates.push(locale[1]); // POSIX codeset
372
+ candidates.push(noLang.replace(/^utf[_]?8$/, "utf-8"));
373
+ for (const cs of candidates) {
374
+ try {
375
+ return new TextDecoder(cs);
376
+ }
377
+ catch { /* unknown label — try the next reading */ }
378
+ }
379
+ return new TextDecoder("utf-8");
380
+ }
347
381
  /** Decode IMAP encoded-word (=?charset?encoding?text?=) */
348
382
  function decodeImapString(s) {
349
383
  if (!s)
@@ -352,13 +386,13 @@ function decodeImapString(s) {
352
386
  const unfolded = s.replace(/\?=\s+=\?/g, "?==?");
353
387
  return unfolded.replace(/=\?([^?]+)\?([BQ])\?([^?]+)\?=/gi, (_match, charset, encoding, text) => {
354
388
  try {
355
- const cs = charset.toLowerCase().replace(/^utf8$/, "utf-8");
389
+ const decoder = decoderFor(charset);
356
390
  if (encoding.toUpperCase() === "B") {
357
391
  const raw = atob(text);
358
392
  const bytes = new Uint8Array(raw.length);
359
393
  for (let j = 0; j < raw.length; j++)
360
394
  bytes[j] = raw.charCodeAt(j);
361
- return new TextDecoder(cs).decode(bytes);
395
+ return decoder.decode(bytes);
362
396
  }
363
397
  else {
364
398
  // Quoted-printable: collect bytes then decode with charset
@@ -375,11 +409,14 @@ function decodeImapString(s) {
375
409
  i++;
376
410
  }
377
411
  }
378
- return new TextDecoder(cs).decode(new Uint8Array(bytes));
412
+ return decoder.decode(new Uint8Array(bytes));
379
413
  }
380
414
  }
381
415
  catch {
382
- return text;
416
+ // Only reachable now for malformed base64 (atob throws). Strip the
417
+ // Q-encoding's underscore-for-space at least, so the fallback isn't
418
+ // more wrong than it has to be.
419
+ return encoding.toUpperCase() === "Q" ? text.replace(/_/g, " ") : text;
383
420
  }
384
421
  });
385
422
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/iflow-direct",
3
- "version": "0.1.62",
3
+ "version": "0.1.64",
4
4
  "description": "Direct IMAP client — transport-agnostic, no Node.js dependencies, browser-ready",
5
5
  "main": "index.js",
6
6
  "types": "index.ts",