@bobfrankston/rmfmail 1.2.295 → 1.2.297

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 (38) hide show
  1. package/.commitmsg +29 -15
  2. package/client/android-bootstrap.bundle.js +69 -11
  3. package/client/android-bootstrap.bundle.js.map +3 -3
  4. package/client/app.bundle.js +30 -0
  5. package/client/app.bundle.js.map +3 -3
  6. package/client/app.js +27 -1
  7. package/client/app.js.map +1 -1
  8. package/client/app.ts +25 -1
  9. package/client/index.html +5 -0
  10. package/client/styles/components.css +22 -5
  11. package/npmchanges.md +53 -0
  12. package/package.json +8 -8
  13. package/packages/mailx-imap/index.d.ts +1 -0
  14. package/packages/mailx-imap/index.d.ts.map +1 -1
  15. package/packages/mailx-imap/index.js +75 -11
  16. package/packages/mailx-imap/index.js.map +1 -1
  17. package/packages/mailx-imap/index.ts +76 -12
  18. package/packages/mailx-imap/package-lock.json +2 -2
  19. package/packages/mailx-imap/package.json +1 -1
  20. package/packages/mailx-settings/package.json +1 -1
  21. package/packages/mailx-store/package.json +1 -1
  22. package/packages/mailx-store-web/android-bootstrap.d.ts.map +1 -1
  23. package/packages/mailx-store-web/android-bootstrap.js +6 -2
  24. package/packages/mailx-store-web/android-bootstrap.js.map +1 -1
  25. package/packages/mailx-store-web/android-bootstrap.ts +8 -2
  26. package/packages/mailx-store-web/package.json +1 -1
  27. package/packages/mailx-types/index.d.ts +1 -0
  28. package/packages/mailx-types/index.d.ts.map +1 -1
  29. package/packages/mailx-types/index.js +1 -0
  30. package/packages/mailx-types/index.js.map +1 -1
  31. package/packages/mailx-types/index.ts +1 -0
  32. package/packages/mailx-types/log-changes.d.ts +29 -0
  33. package/packages/mailx-types/log-changes.d.ts.map +1 -0
  34. package/packages/mailx-types/log-changes.js +42 -0
  35. package/packages/mailx-types/log-changes.js.map +1 -0
  36. package/packages/mailx-types/log-changes.ts +44 -0
  37. package/packages/mailx-types/package.json +1 -1
  38. /package/packages/mailx-imap/{node_modules.npmglobalize-stash-52364 → node_modules.npmglobalize-stash-77248}/.package-lock.json +0 -0
package/.commitmsg CHANGED
@@ -1,18 +1,32 @@
1
- A cloud read must not cache over a local write that beat it home
1
+ Reading a message outranks syncing it; a filter is not a selection
2
2
 
3
- Untrusting a sender wrote correctly to Drive — a clean restart pulled the right
4
- list — while the LOCAL copy still had the entry, mtime stamped at the moment of
5
- the write. A cloud read that started before the write finished after it, and
6
- its caching step put the pre-change content back on disk. Everything read
7
- through the sync loadAllowlist() then saw the value the user had just removed,
8
- until the next poll happened to overwrite it with the truth.
3
+ Two things Bob hit on 2026-08-29.
9
4
 
10
- cloudRead now stamps each shared file's last local write and skips its cache
11
- step when the file changed while the read was in flight the content it holds
12
- is older than what is on disk, and the local write has already gone to the
13
- cloud. The stamp is taken BEFORE the write, so a race resolves in favour of the
14
- local copy.
5
+ **"If I look at a letter while sync, things get confused. Eventually it reports
6
+ an error rather than retrying. Viewing the body should get priority instead of
7
+ waiting for sync."** Both halves were real:
15
8
 
16
- Same shape as the contacts.jsonc flush clobber, and the same lesson: the local
17
- file is not a cache to be overwritten, it is a copy someone may have just
18
- edited.
9
+ - Lanes are strictly sequential, and a click-time body fetch went on the tail
10
+ of the fast lane behind whatever background work was already queued.
11
+ `withConnection` now takes `priority`, which puts the task at the FRONT of
12
+ its lane; the interactive fetch uses it and a 30 s cap instead of 90 s. It
13
+ still cannot preempt the command in flight — an IMAP command cannot be
14
+ cancelled without dropping the connection — but it no longer waits out the
15
+ backlog.
16
+ - `fetchMessageBody` swallowed every error into `null`. To the reconciler that
17
+ means "the server has no body for this message", which is classified
18
+ non-transient, skips the 3-attempt retry budget, and paints a banner. A lane
19
+ timeout says nothing about whether the message has a body: transient errors
20
+ (timeout, dropped socket, discarded client) now propagate, so the existing
21
+ retry-with-backoff applies and the reader gets the message instead of an
22
+ error. Only a genuine "no body here" still arms the prefetch backoff — a
23
+ timeout used to arm it too, poisoning the next view for up to 12 h.
24
+
25
+ **"If I press * on search it shouldn't automatically select those letters, just
26
+ show them."** Nothing was selected — the flagged-only filter washed the whole
27
+ list in the flag colour (added 2026-06-26 to make the filter unmistakable), and
28
+ a coloured background over rows is exactly how this list says "chosen". The
29
+ wash is gone; the filter now announces itself above the list, by name:
30
+ "Showing flagged (★) messages only — click to show everything", and clicking it
31
+ clears the filter. Same chip covers the priority-senders filter, which had the
32
+ same shape.
@@ -2640,6 +2640,28 @@ var init_trust = __esm({
2640
2640
  }
2641
2641
  });
2642
2642
 
2643
+ // packages/mailx-types/log-changes.js
2644
+ function logIfChanged(key, line, emit = console.log) {
2645
+ const now = Date.now();
2646
+ const prev = lastSeen.get(key);
2647
+ if (prev && prev.line === line && now - prev.at < RESTATE_AFTER_MS)
2648
+ return false;
2649
+ lastSeen.set(key, { line, at: now });
2650
+ emit(prev && prev.line === line ? `${line} (unchanged for an hour)` : line);
2651
+ return true;
2652
+ }
2653
+ function clearLogState(key) {
2654
+ lastSeen.delete(key);
2655
+ }
2656
+ var RESTATE_AFTER_MS, lastSeen;
2657
+ var init_log_changes = __esm({
2658
+ "packages/mailx-types/log-changes.js"() {
2659
+ "use strict";
2660
+ RESTATE_AFTER_MS = 60 * 60 * 1e3;
2661
+ lastSeen = /* @__PURE__ */ new Map();
2662
+ }
2663
+ });
2664
+
2643
2665
  // packages/mailx-types/contacts-config.js
2644
2666
  function parseContactsConfig(raw) {
2645
2667
  if (!raw)
@@ -3013,6 +3035,7 @@ __export(mailx_types_exports, {
3013
3035
  answeredOf: () => answeredOf,
3014
3036
  assessMessageTrust: () => assessMessageTrust,
3015
3037
  buildMimeMessage: () => buildMimeMessage,
3038
+ clearLogState: () => clearLogState,
3016
3039
  deletedOf: () => deletedOf,
3017
3040
  displayNameClaimsOtherAddress: () => displayNameClaimsOtherAddress,
3018
3041
  draftOf: () => draftOf,
@@ -3028,6 +3051,7 @@ __export(mailx_types_exports, {
3028
3051
  inviteHasGoogleRsvp: () => inviteHasGoogleRsvp,
3029
3052
  isAddressToken: () => isAddressToken,
3030
3053
  isJunkPreviewText: () => isJunkPreviewText,
3054
+ logIfChanged: () => logIfChanged,
3031
3055
  mergeReminderStates: () => mergeReminderStates,
3032
3056
  normalizeReminderState: () => normalizeReminderState,
3033
3057
  parseInvite: () => parseInvite,
@@ -3507,6 +3531,7 @@ var init_mailx_types = __esm({
3507
3531
  "use strict";
3508
3532
  init_contact_rules();
3509
3533
  init_trust();
3534
+ init_log_changes();
3510
3535
  init_contacts_config();
3511
3536
  init_groups();
3512
3537
  init_reminder_state();
@@ -8855,6 +8880,10 @@ var NativeImapClient = class {
8855
8880
  * a server-side deletion in real time instead of waiting for the next
8856
8881
  * periodic poll. Distinct from `idleCallback` (new mail only). */
8857
8882
  idleExpungeCallback = null;
8883
+ /** Fired for an unsolicited FETCH carrying FLAGS while parked in IDLE —
8884
+ * another client (a phone, Thunderbird, webmail) changed a flag on a
8885
+ * message in the selected mailbox. */
8886
+ idleFlagCallback = null;
8858
8887
  idleRefreshTimer = null;
8859
8888
  /** RFC 5465 NOTIFY: fires on unsolicited STATUS responses for non-selected
8860
8889
  * mailboxes (the server pushes these when the client has issued NOTIFY
@@ -9302,7 +9331,8 @@ var NativeImapClient = class {
9302
9331
  if (uids.length === 0)
9303
9332
  return [];
9304
9333
  uids.reverse();
9305
- console.log(` [fetch] ${uids.length} UIDs since ${sinceUid} (newest first)`);
9334
+ if (this.verbose)
9335
+ console.log(` [fetch] ${uids.length} UIDs since ${sinceUid} (newest first)`);
9306
9336
  if (uids.length <= this.fetchChunkSize) {
9307
9337
  const msgs = await this.fetchMessages(uids.join(","), options);
9308
9338
  if (onChunk)
@@ -9330,9 +9360,11 @@ var NativeImapClient = class {
9330
9360
  before: before || void 0
9331
9361
  });
9332
9362
  const t0 = Date.now();
9333
- console.log(` [search] SINCE ${since.toISOString().slice(0, 10)}${before ? ` BEFORE ${before.toISOString().slice(0, 10)}` : ""} \u2014 running...`);
9363
+ if (this.verbose)
9364
+ console.log(` [search] SINCE ${since.toISOString().slice(0, 10)}${before ? ` BEFORE ${before.toISOString().slice(0, 10)}` : ""} \u2014 running...`);
9334
9365
  const uids = await this.search(criteria);
9335
- console.log(` [search] returned ${uids.length} UIDs in ${Date.now() - t0}ms`);
9366
+ if (this.verbose)
9367
+ console.log(` [search] returned ${uids.length} UIDs in ${Date.now() - t0}ms`);
9336
9368
  if (uids.length === 0)
9337
9369
  return [];
9338
9370
  uids.reverse();
@@ -9345,7 +9377,8 @@ var NativeImapClient = class {
9345
9377
  allMessages.push(...msgs);
9346
9378
  chunkIndex++;
9347
9379
  if (chunkIndex === 1 || chunkIndex % 5 === 0) {
9348
- console.log(` [fetch] ${allMessages.length}/${uids.length} messages (chunk ${chunkIndex})`);
9380
+ if (this.verbose)
9381
+ console.log(` [fetch] ${allMessages.length}/${uids.length} messages (chunk ${chunkIndex})`);
9349
9382
  }
9350
9383
  if (onChunk)
9351
9384
  onChunk(msgs);
@@ -9353,7 +9386,8 @@ var NativeImapClient = class {
9353
9386
  if (chunkSize < this.fetchChunkSizeMax)
9354
9387
  chunkSize = Math.min(chunkSize * 4, this.fetchChunkSizeMax);
9355
9388
  }
9356
- console.log(` [fetch] done \u2014 ${allMessages.length} messages in ${Date.now() - t0}ms`);
9389
+ if (this.verbose)
9390
+ console.log(` [fetch] done \u2014 ${allMessages.length} messages in ${Date.now() - t0}ms`);
9357
9391
  return allMessages;
9358
9392
  }
9359
9393
  /** Fetch the most recent N messages by sequence number — avoids
@@ -9369,7 +9403,8 @@ var NativeImapClient = class {
9369
9403
  const start = Math.max(1, exists - n + 1);
9370
9404
  const range = `${start}:${exists}`;
9371
9405
  const t0 = Date.now();
9372
- console.log(` [fetch-latest] ${this.selectedMailbox || "?"}: sequence ${range} (${Math.min(n, exists)} most recent of ${exists})`);
9406
+ if (this.verbose)
9407
+ console.log(` [fetch-latest] ${this.selectedMailbox || "?"}: sequence ${range} (${Math.min(n, exists)} most recent of ${exists})`);
9373
9408
  const items = ["UID", "FLAGS", "ENVELOPE", "RFC822.SIZE", "INTERNALDATE", "BODY.PEEK[HEADER]"];
9374
9409
  if (options.source)
9375
9410
  items.push("BODY.PEEK[]");
@@ -9395,7 +9430,8 @@ var NativeImapClient = class {
9395
9430
  onChunk([]);
9396
9431
  }
9397
9432
  streamed.reverse();
9398
- console.log(` [fetch-latest] done \u2014 ${streamed.length} messages in ${Date.now() - t0}ms`);
9433
+ if (this.verbose)
9434
+ console.log(` [fetch-latest] done \u2014 ${streamed.length} messages in ${Date.now() - t0}ms`);
9399
9435
  return streamed;
9400
9436
  }
9401
9437
  /** Fetch a single message by UID */
@@ -9505,9 +9541,10 @@ var NativeImapClient = class {
9505
9541
  }
9506
9542
  }
9507
9543
  // ── IDLE ──
9508
- async startIdle(onNewMail, onExpunge) {
9544
+ async startIdle(onNewMail, onExpunge, onFlagChange) {
9509
9545
  this.idleCallback = onNewMail;
9510
9546
  this.idleExpungeCallback = onExpunge ?? null;
9547
+ this.idleFlagCallback = onFlagChange ?? null;
9511
9548
  this.idleStopped = false;
9512
9549
  const beginIdleCycle = async () => {
9513
9550
  const tag = nextTag();
@@ -10002,6 +10039,20 @@ var NativeImapClient = class {
10002
10039
  this.idleExpungeCallback();
10003
10040
  continue;
10004
10041
  }
10042
+ if (this.idleTag && resp.tag === "*" && resp.type === "FETCH" && /FLAGS\s*\(/i.test(resp.text)) {
10043
+ if (this.idleFlagCallback) {
10044
+ const seq = parseInt(resp.text, 10);
10045
+ const inner = resp.text.match(/FLAGS\s*\(([^)]*)\)/i)?.[1] || "";
10046
+ const flags = inner.split(/\s+/).filter(Boolean);
10047
+ try {
10048
+ this.idleFlagCallback(Number.isFinite(seq) ? seq : 0, flags);
10049
+ } catch (err) {
10050
+ if (this.verbose)
10051
+ console.error(` [imap] onFlagChange threw: ${err.message}`);
10052
+ }
10053
+ }
10054
+ continue;
10055
+ }
10005
10056
  if (this.idleTag && resp.tag === "*" && resp.type === "STATUS" && this.onMailboxStatus) {
10006
10057
  const parsed = parseStatusResponseFull(resp.text);
10007
10058
  if (parsed) {
@@ -10515,7 +10566,7 @@ var CompatImapClient = class {
10515
10566
  this.native.onMailboxStatus = opts.onMailboxStatus;
10516
10567
  if (opts?.notifySpec)
10517
10568
  await this.native.notify(opts.notifySpec);
10518
- return this.native.startIdle(onNew, opts?.onExpunge);
10569
+ return this.native.startIdle(onNew, opts?.onExpunge, opts?.onFlagChange);
10519
10570
  }
10520
10571
  /** Copy a message to another server (cross-account) */
10521
10572
  async moveMessageToServer(msg, fromMailbox, targetClient, toMailbox) {
@@ -11075,6 +11126,7 @@ function b64utf8(s) {
11075
11126
  }
11076
11127
 
11077
11128
  // packages/mailx-store-web/android-bootstrap.ts
11129
+ init_mailx_types();
11078
11130
  var db;
11079
11131
  var bodyStore;
11080
11132
  var service;
@@ -11487,12 +11539,18 @@ var AndroidSyncManager = class {
11487
11539
  const serverUids = new Set(serverUidsArr);
11488
11540
  const localUids = this.db.getUidsForFolder(accountId, folderId);
11489
11541
  if (serverUidsArr.length === 0 && localUids.length > 0) {
11490
- console.log(`[sync] ${folder.path}: reconcile skipped \u2014 server returned empty but local has ${localUids.length}`);
11542
+ logIfChanged(
11543
+ `${accountId}/${folder.path}:reconcile`,
11544
+ `[sync] ${folder.path}: reconcile skipped \u2014 server returned empty but local has ${localUids.length}`
11545
+ );
11491
11546
  } else {
11492
11547
  const toDelete = localUids.filter((uid) => !serverUids.has(uid));
11493
11548
  const RECONCILE_DELETE_THRESHOLD = 0.5;
11494
11549
  if (localUids.length > 0 && toDelete.length / localUids.length > RECONCILE_DELETE_THRESHOLD) {
11495
- console.log(`[sync] ${folder.path}: reconcile refused \u2014 would delete ${toDelete.length}/${localUids.length} (${Math.round(toDelete.length / localUids.length * 100)}%)`);
11550
+ logIfChanged(
11551
+ `${accountId}/${folder.path}:reconcile`,
11552
+ `[sync] ${folder.path}: reconcile refused \u2014 would delete ${toDelete.length}/${localUids.length} (${Math.round(toDelete.length / localUids.length * 100)}%)`
11553
+ );
11496
11554
  } else {
11497
11555
  for (const uid of toDelete) {
11498
11556
  this.db.deleteMessage(accountId, folderId, uid);