@bobfrankston/mailx-imap 0.1.167 → 0.1.169

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/index.js +41 -7
  2. package/package.json +9 -9
package/index.js CHANGED
@@ -7,7 +7,7 @@ import { createAutoImapConfig, CompatImapClient } from "@bobfrankston/iflow-dire
7
7
  import { authenticateOAuth, clearReauthBackoff } from "@bobfrankston/oauthsupport";
8
8
  import { parseSerial, fixCharsetDeclString, storeBus } from "@bobfrankston/mailx-store";
9
9
  import { loadSettings, getConfigDir, getHistoryDays, getPrefetch, tokenDirName } from "@bobfrankston/mailx-settings";
10
- import { isJunkPreviewText } from "@bobfrankston/mailx-types";
10
+ import { isJunkPreviewText, logIfChanged } from "@bobfrankston/mailx-types";
11
11
  import { EventEmitter } from "node:events";
12
12
  import * as fs from "node:fs";
13
13
  import * as path from "node:path";
@@ -2405,10 +2405,13 @@ export class ImapManager extends EventEmitter {
2405
2405
  && statusMessageCount !== null
2406
2406
  && serverUidsArr.length === statusMessageCount;
2407
2407
  if (serverUidsArr.length === 0 && localUidsAll.length > 0 && !authoritative) {
2408
- console.log(` [sync] ${accountId}/${folder.path}: reconcile skipped server UID list empty but local has ${localUidsAll.length} (treating as transient)`);
2408
+ // Same line every cycle for a folder stuck in this state
2409
+ // logIfChanged prints the first one, each change, and an
2410
+ // hourly restatement. (Claude Code 2026-08-29)
2411
+ logIfChanged(`${accountId}/${folder.path}:reconcile`, ` [sync] ${accountId}/${folder.path}: reconcile skipped — server UID list empty but local has ${localUidsAll.length} (treating as transient)`);
2409
2412
  }
2410
2413
  else if (!authoritative && localUids.length > 0 && toDelete.length / localUids.length > 0.5) {
2411
- console.log(` [sync] ${accountId}/${folder.path}: reconcile REFUSED — would delete ${toDelete.length}/${localUids.length} (${Math.round(toDelete.length / localUids.length * 100)}%) — probably a sync bug, skipping`);
2414
+ logIfChanged(`${accountId}/${folder.path}:reconcile`, ` [sync] ${accountId}/${folder.path}: reconcile REFUSED — would delete ${toDelete.length}/${localUids.length} (${Math.round(toDelete.length / localUids.length * 100)}%) — probably a sync bug, skipping`);
2412
2415
  }
2413
2416
  else if (toDelete.length > 0) {
2414
2417
  // DEFERRED DELETE — DO NOT delete immediately. Server-side
@@ -2888,10 +2891,10 @@ export class ImapManager extends EventEmitter {
2888
2891
  const serverUids = new Set(serverUidsArr);
2889
2892
  const localUids = this.db.getUidsForFolder(accountId, folder.id);
2890
2893
  if (serverUidsArr._truncated) {
2891
- console.log(` [api] ${accountId}/${folder.path}: reconcile skipped — server list truncated (${serverUidsArr.length} ids)`);
2894
+ logIfChanged(`${accountId}/${folder.path}:api-reconcile`, ` [api] ${accountId}/${folder.path}: reconcile skipped — server list truncated (${serverUidsArr.length} ids)`);
2892
2895
  }
2893
2896
  else if (serverUidsArr.length === 0 && localUids.length > 0) {
2894
- console.log(` [api] ${accountId}/${folder.path}: reconcile skipped — server list empty but local has ${localUids.length}`);
2897
+ logIfChanged(`${accountId}/${folder.path}:api-reconcile`, ` [api] ${accountId}/${folder.path}: reconcile skipped — server list empty but local has ${localUids.length}`);
2895
2898
  }
2896
2899
  else {
2897
2900
  const toDelete = localUids.filter(uid => !serverUids.has(uid));
@@ -2925,7 +2928,7 @@ export class ImapManager extends EventEmitter {
2925
2928
  }
2926
2929
  }
2927
2930
  if (!confirmed) {
2928
- console.log(` [api] ${accountId}/${folder.path}: reconcile refused — would delete ${toDelete.length}/${localUids.length} (${Math.round(toDelete.length / localUids.length * 100)}%) — probably a sync bug, skipping`);
2931
+ logIfChanged(`${accountId}/${folder.path}:api-reconcile`, ` [api] ${accountId}/${folder.path}: reconcile refused — would delete ${toDelete.length}/${localUids.length} (${Math.round(toDelete.length / localUids.length * 100)}%) — probably a sync bug, skipping`);
2929
2932
  }
2930
2933
  else {
2931
2934
  for (const uid of toDelete) {
@@ -3511,10 +3514,41 @@ export class ImapManager extends EventEmitter {
3511
3514
  this.syncFolder(accountId, inbox.id).catch(e => console.error(` [idle] expunge reconcile failed: ${e.message}`));
3512
3515
  }, 2000);
3513
3516
  };
3517
+ // A flag changed on a message we already have — someone starred
3518
+ // or read it on another client. The server pushes it during
3519
+ // IDLE as an unsolicited FETCH; before v1.2.296 that was
3520
+ // dropped, so a star set on the phone reached the desktop only
3521
+ // on the next periodic sync of INBOX. Measured on Bob's own
3522
+ // log that was a median of 5 minutes and up to 20 (Bob
3523
+ // 2026-08-29: "how long should it take for a * on the phone to
3524
+ // get to my desktop?").
3525
+ //
3526
+ // The push says WHICH flags but by sequence number, which this
3527
+ // connection cannot map to a UID reliably — so it triggers the
3528
+ // folder's normal QRESYNC pass, which asks the server for
3529
+ // exactly what changed since our modseq and took 53 ms in the
3530
+ // same log. Debounced: marking ten messages read on the phone
3531
+ // arrives as ten pushes.
3532
+ let flagTimer = null;
3533
+ const onFlagChange = () => {
3534
+ if (flagTimer)
3535
+ clearTimeout(flagTimer);
3536
+ flagTimer = setTimeout(() => {
3537
+ flagTimer = null;
3538
+ // Our own STOREs bounce straight back as flag pushes;
3539
+ // the local store already has those.
3540
+ if (this.db.getPendingSyncActions(accountId).length > 0)
3541
+ return;
3542
+ const inbox = this.db.getFolders(accountId).find(f => f.specialUse === "inbox");
3543
+ if (!inbox)
3544
+ return;
3545
+ this.syncFolder(accountId, inbox.id).catch(e => console.error(` [idle] flag-change sync failed: ${e.message}`));
3546
+ }, 1500);
3547
+ };
3514
3548
  const stop = await watchClient.watchMailbox("INBOX", (newCount) => {
3515
3549
  console.log(` [idle] ${accountId}: ${newCount} new message(s)`);
3516
3550
  this.syncInboxNewOnly(accountId).catch(e => console.error(` [idle] sync error: ${e.message}`));
3517
- }, { notifySpec, onMailboxStatus, onExpunge });
3551
+ }, { notifySpec, onMailboxStatus, onExpunge, onFlagChange });
3518
3552
  this.watchers.set(accountId, async () => {
3519
3553
  await stop();
3520
3554
  await watchClient.logout();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/mailx-imap",
3
- "version": "0.1.167",
3
+ "version": "0.1.169",
4
4
  "type": "module",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -9,10 +9,10 @@
9
9
  },
10
10
  "license": "ISC",
11
11
  "dependencies": {
12
- "@bobfrankston/mailx-types": "^0.1.73",
13
- "@bobfrankston/mailx-settings": "^0.1.70",
14
- "@bobfrankston/mailx-store": "^0.1.107",
15
- "@bobfrankston/iflow-direct": "^0.1.65",
12
+ "@bobfrankston/mailx-types": "^0.1.75",
13
+ "@bobfrankston/mailx-settings": "^0.1.72",
14
+ "@bobfrankston/mailx-store": "^0.1.108",
15
+ "@bobfrankston/iflow-direct": "^0.1.68",
16
16
  "@bobfrankston/tcp-transport": "^0.1.8",
17
17
  "@bobfrankston/smtp-direct": "^0.1.9",
18
18
  "@bobfrankston/mailx-sync": "^0.1.29",
@@ -37,10 +37,10 @@
37
37
  },
38
38
  ".transformedSnapshot": {
39
39
  "dependencies": {
40
- "@bobfrankston/mailx-types": "^0.1.73",
41
- "@bobfrankston/mailx-settings": "^0.1.70",
42
- "@bobfrankston/mailx-store": "^0.1.107",
43
- "@bobfrankston/iflow-direct": "^0.1.65",
40
+ "@bobfrankston/mailx-types": "^0.1.75",
41
+ "@bobfrankston/mailx-settings": "^0.1.72",
42
+ "@bobfrankston/mailx-store": "^0.1.108",
43
+ "@bobfrankston/iflow-direct": "^0.1.68",
44
44
  "@bobfrankston/tcp-transport": "^0.1.8",
45
45
  "@bobfrankston/smtp-direct": "^0.1.9",
46
46
  "@bobfrankston/mailx-sync": "^0.1.29",