@bobfrankston/rmfmail 1.2.294 → 1.2.296

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 (37) hide show
  1. package/.commitmsg +25 -19
  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 +10 -0
  5. package/client/app.bundle.js.map +3 -3
  6. package/npmchanges.md +47 -0
  7. package/package.json +8 -8
  8. package/packages/mailx-imap/index.d.ts.map +1 -1
  9. package/packages/mailx-imap/index.js +41 -7
  10. package/packages/mailx-imap/index.js.map +1 -1
  11. package/packages/mailx-imap/index.ts +44 -7
  12. package/packages/mailx-imap/package-lock.json +2 -2
  13. package/packages/mailx-imap/package.json +1 -1
  14. package/packages/mailx-settings/index.d.ts +0 -1
  15. package/packages/mailx-settings/index.d.ts.map +1 -1
  16. package/packages/mailx-settings/index.js +29 -3
  17. package/packages/mailx-settings/index.js.map +1 -1
  18. package/packages/mailx-settings/index.ts +29 -3
  19. package/packages/mailx-settings/package.json +1 -1
  20. package/packages/mailx-store/package.json +1 -1
  21. package/packages/mailx-store-web/android-bootstrap.d.ts.map +1 -1
  22. package/packages/mailx-store-web/android-bootstrap.js +6 -2
  23. package/packages/mailx-store-web/android-bootstrap.js.map +1 -1
  24. package/packages/mailx-store-web/android-bootstrap.ts +8 -2
  25. package/packages/mailx-store-web/package.json +1 -1
  26. package/packages/mailx-types/index.d.ts +1 -0
  27. package/packages/mailx-types/index.d.ts.map +1 -1
  28. package/packages/mailx-types/index.js +1 -0
  29. package/packages/mailx-types/index.js.map +1 -1
  30. package/packages/mailx-types/index.ts +1 -0
  31. package/packages/mailx-types/log-changes.d.ts +29 -0
  32. package/packages/mailx-types/log-changes.d.ts.map +1 -0
  33. package/packages/mailx-types/log-changes.js +42 -0
  34. package/packages/mailx-types/log-changes.js.map +1 -0
  35. package/packages/mailx-types/log-changes.ts +44 -0
  36. package/packages/mailx-types/package.json +1 -1
  37. /package/packages/mailx-imap/{node_modules.npmglobalize-stash-38024 → node_modules.npmglobalize-stash-40440}/.package-lock.json +0 -0
package/.commitmsg CHANGED
@@ -1,23 +1,29 @@
1
- A union merge cannot express a removal so removals are now stated
1
+ A star set on the phone now reaches the desktop in seconds, not minutes
2
2
 
3
- Toggling the new trust entry back OFF exposed the other half of yesterday's
4
- merge bug: the IPC returned `{trusted: false}`, the entry left the local list,
5
- and the union with the cloud copy handed it straight back. The same has been
6
- true of `flagSenderOrDomain` since it was written — "unflag this sender" has
7
- always been a no-op whenever a cloud copy existed.
3
+ Bob 2026-08-29: "how long should it take for a * on the phone to get to my
4
+ desktop?" Measured, on his own log: a median of 5 minutes and up to 20.
8
5
 
9
- `mergeAllowlists(local, cloud, removals?)` now takes the values this write is
10
- deleting on purpose, per key, and subtracts them after the union. Everything
11
- not named stays additive, so a second machine's entries are exactly as safe as
12
- before the property that saved this file in the 2026-07-23 wipe is
13
- untouched. `saveAllowlist(list, removals?)` passes it through, and both
14
- toggles name what they removed.
6
+ Why: gal.iecc.com does not advertise NOTIFY, so the desktop watches INBOX with
7
+ plain IDLE. The server DOES push a flag change there as an unsolicited
8
+ `* 1234 FETCH (FLAGS (\Seen \Flagged))` iflow-direct dropped it (fixed in
9
+ 0.1.67), and mailx had nothing to receive it with. The 5-minute quick check
10
+ could not help either: it is a STATUS poll, and MESSAGES / UNSEEN / UIDNEXT do
11
+ not move when a message is starred. So the only path was the periodic full
12
+ sync's QRESYNC pass — which is why the numbers above are the sync cadence
13
+ rather than any property of flags.
15
14
 
16
- Inference was never going to work here: a value missing from the local copy is
17
- indistinguishable from a value another machine just added, which is precisely
18
- why the union was written that way. Stating the removal is the only thing that
19
- tells the two apart without a tombstone log.
15
+ The IDLE watcher now takes `onFlagChange` and, debounced 1.5 s, runs the
16
+ folder's normal QRESYNC pass the same one that already reconciles flags and
17
+ took 53 ms in that log. Marking ten messages read on the phone arrives as ten
18
+ pushes and costs one pass. Our own STOREs bounce back as flag pushes too, so
19
+ the handler skips while this account has sync actions draining; the local store
20
+ already has those.
20
21
 
21
- Two more tests in tests/allowlist-merge.test.ts: a stated removal sticks while
22
- its neighbours stay, and removals match case-insensitively like every other
23
- address comparison in this codebase.
22
+ INBOX only, because IDLE watches one mailbox per connection. Flags changed in
23
+ other folders still wait for that folder's turn in the sync cycle.
24
+
25
+ Also in this build: the log noise behind it. `[search]` / `[fetch]` per-command
26
+ lines moved behind iflow's existing `verbose` flag, and the repeated
27
+ per-folder reconcile lines now go through `logIfChanged` — first occurrence,
28
+ every change, and an hourly restatement — instead of once per folder per cycle
29
+ forever. On Android each of those lines was an HTTP request to the log server.
@@ -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);