@bobfrankston/mailx 1.0.99 → 1.0.100

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/mailx",
3
- "version": "1.0.99",
3
+ "version": "1.0.100",
4
4
  "description": "Local-first email client with IMAP sync and standalone native app",
5
5
  "type": "module",
6
6
  "main": "bin/mailx.js",
@@ -47,9 +47,13 @@ export declare class ImapManager extends EventEmitter {
47
47
  searchOnServer(accountId: string, mailboxPath: string, criteria: any): Promise<number[]>;
48
48
  /** Create a fresh IMAP client for an account (public access for API endpoints) */
49
49
  createPublicClient(accountId: string): any;
50
+ /** Track active IMAP connections for diagnostics */
51
+ private activeConnections;
50
52
  /** Create a fresh IMAP client for an account (disposable, single-use).
51
53
  * Returns CompatImapClient (native) or ImapClient (imapflow) based on useNativeClient flag. */
52
54
  private createClient;
55
+ /** Track client logout for connection counting */
56
+ private trackLogout;
53
57
  /** Register an account */
54
58
  addAccount(account: AccountConfig): Promise<void>;
55
59
  /** Sync folder list for an account */
@@ -179,6 +179,11 @@ export class ImapManager extends EventEmitter {
179
179
  createPublicClient(accountId) {
180
180
  return this.createClient(accountId);
181
181
  }
182
+ // Legacy fallback disabled — was doubling connections without helping.
183
+ // To re-enable: uncomment legacyFallbacks logic in createClient and _syncAll.
184
+ // private legacyFallbacks = new Set<string>();
185
+ /** Track active IMAP connections for diagnostics */
186
+ activeConnections = new Map(); // accountId → count
182
187
  /** Create a fresh IMAP client for an account (disposable, single-use).
183
188
  * Returns CompatImapClient (native) or ImapClient (imapflow) based on useNativeClient flag. */
184
189
  createClient(accountId) {
@@ -192,11 +197,21 @@ export class ImapManager extends EventEmitter {
192
197
  const config = this.configs.get(accountId);
193
198
  if (!config)
194
199
  throw new Error(`No config for account ${accountId}`);
200
+ const count = (this.activeConnections.get(accountId) || 0) + 1;
201
+ this.activeConnections.set(accountId, count);
202
+ const clientType = this.useNativeClient ? "native" : "imapflow";
203
+ console.log(` [conn] ${accountId}: +1 ${clientType} (${count} active)`);
195
204
  if (this.useNativeClient) {
196
205
  return new CompatImapClient(config, () => new NodeTransport({ rejectUnauthorized: config.rejectUnauthorized !== false }));
197
206
  }
198
207
  return new ImapClient(config);
199
208
  }
209
+ /** Track client logout for connection counting */
210
+ trackLogout(accountId) {
211
+ const count = Math.max(0, (this.activeConnections.get(accountId) || 1) - 1);
212
+ this.activeConnections.set(accountId, count);
213
+ console.log(` [conn] ${accountId}: -1 (${count} active)`);
214
+ }
200
215
  /** Register an account */
201
216
  async addAccount(account) {
202
217
  if (this.configs.has(account.id))
@@ -233,7 +248,10 @@ export class ImapManager extends EventEmitter {
233
248
  if (!client)
234
249
  client = this.createClient(accountId);
235
250
  this.emit("syncProgress", accountId, "folders", 0);
251
+ const t0 = Date.now();
252
+ console.log(` [diag] ${accountId}: getFolderList starting...`);
236
253
  const folders = await client.getFolderList();
254
+ console.log(` [diag] ${accountId}: getFolderList done in ${Date.now() - t0}ms (${folders.length} folders)`);
237
255
  const specialFolders = client.getSpecialFolders(folders);
238
256
  for (const folder of folders) {
239
257
  // Skip non-selectable folders (virtual parents like "Added", "Added2")
@@ -381,10 +399,9 @@ export class ImapManager extends EventEmitter {
381
399
  // Remove messages deleted on the server (skip on first sync — nothing to reconcile)
382
400
  let deletedCount = 0;
383
401
  if (!firstSync) {
384
- let delClient = null;
385
402
  try {
386
- delClient = this.createClient(accountId);
387
- const serverUids = new Set(await delClient.getUids(folder.path));
403
+ // Reuse the passed-in client instead of opening a new connection
404
+ const serverUids = new Set(await client.getUids(folder.path));
388
405
  const localUids = this.db.getUidsForFolder(accountId, folderId);
389
406
  for (const uid of localUids) {
390
407
  if (!serverUids.has(uid)) {
@@ -395,18 +412,10 @@ export class ImapManager extends EventEmitter {
395
412
  }
396
413
  if (deletedCount > 0)
397
414
  console.log(` removed ${deletedCount} deleted messages`);
398
- await delClient.logout();
399
415
  }
400
416
  catch (e) {
401
417
  console.error(` deletion sync error: ${e.message}`);
402
418
  }
403
- finally {
404
- if (delClient)
405
- try {
406
- await delClient.logout();
407
- }
408
- catch { /* ignore */ }
409
- }
410
419
  }
411
420
  // Update folder counts from local DB (after deletions + additions)
412
421
  const result = this.db.getMessages({ accountId, folderId, page: 1, pageSize: 1 });
@@ -443,12 +452,17 @@ export class ImapManager extends EventEmitter {
443
452
  for (const [accountId] of this.configs) {
444
453
  let client = null;
445
454
  try {
455
+ const t0 = Date.now();
446
456
  client = this.createClient(accountId);
447
457
  const folders = await Promise.race([
448
458
  this.syncFolders(accountId, client),
449
459
  new Promise((_, reject) => setTimeout(() => reject(new Error("Folder list timeout (30s)")), 30000))
450
460
  ]);
461
+ console.log(` [timing] ${accountId}: folder list ${Date.now() - t0}ms (${folders.length} folders)`);
462
+ // Legacy fallback removed — was doubling connections.
463
+ // If native client has issues, set useNativeClient=false or use --legacy-imap flag.
451
464
  await client.logout();
465
+ this.trackLogout(accountId);
452
466
  client = null;
453
467
  accountFolders.set(accountId, folders);
454
468
  // Sync inbox immediately
@@ -461,6 +475,7 @@ export class ImapManager extends EventEmitter {
461
475
  new Promise((_, reject) => setTimeout(() => reject(new Error("Sync timeout (60s)")), 60000))
462
476
  ]);
463
477
  await client.logout();
478
+ this.trackLogout(accountId);
464
479
  client = null;
465
480
  }
466
481
  catch (e) {
@@ -469,6 +484,7 @@ export class ImapManager extends EventEmitter {
469
484
  await client.logout();
470
485
  }
471
486
  catch { /* ignore */ }
487
+ this.trackLogout(accountId);
472
488
  client = null;
473
489
  }
474
490
  console.error(` Inbox sync error for ${accountId}: ${e.message}`);
@@ -515,11 +531,13 @@ export class ImapManager extends EventEmitter {
515
531
  }
516
532
  }
517
533
  finally {
518
- if (client)
534
+ if (client) {
519
535
  try {
520
536
  await client.logout();
521
537
  }
522
538
  catch { /* ignore */ }
539
+ this.trackLogout(accountId);
540
+ }
523
541
  }
524
542
  }
525
543
  // Phase 2: Sync remaining folders — priority order, skip Trash subfolders on first sync
@@ -532,41 +550,51 @@ export class ImapManager extends EventEmitter {
532
550
  const pb = priorityOrder.indexOf(b.specialUse || "") >= 0 ? priorityOrder.indexOf(b.specialUse || "") : 5;
533
551
  return pa - pb;
534
552
  });
553
+ // Reuse one IMAP connection per account for all folders (avoid 87+ TLS handshakes)
535
554
  let client = null;
536
- for (const folder of remaining) {
537
- // Skip Trash subfolders on first sync — they're large and low priority
538
- const isTrashChild = folder.path.includes("/") && folder.path.toLowerCase().startsWith("trash");
539
- const highestUid = this.db.getHighestUid(accountId, folder.id);
540
- if (isTrashChild && highestUid === 0) {
541
- console.log(` Deferring first sync of ${folder.path} (Trash subfolder)`);
542
- continue;
543
- }
544
- // Longer timeout for folders we know are large (Trash, first sync)
545
- const timeout = highestUid === 0 ? 180000 : 60000;
546
- try {
547
- client = this.createClient(accountId);
548
- await Promise.race([
549
- this.syncFolder(accountId, folder.id, client),
550
- new Promise((_, reject) => setTimeout(() => reject(new Error(`Sync timeout (${timeout / 1000}s)`)), timeout))
551
- ]);
552
- await client.logout();
553
- client = null;
554
- }
555
- catch (e) {
556
- if (client) {
557
- try {
558
- await client.logout();
559
- }
560
- catch { /* ignore */ }
561
- client = null;
555
+ try {
556
+ client = this.createClient(accountId);
557
+ for (const folder of remaining) {
558
+ // Skip Trash subfolders on first sync — they're large and low priority
559
+ const isTrashChild = folder.path.includes("/") && folder.path.toLowerCase().startsWith("trash");
560
+ const highestUid = this.db.getHighestUid(accountId, folder.id);
561
+ if (isTrashChild && highestUid === 0) {
562
+ console.log(` Deferring first sync of ${folder.path} (Trash subfolder)`);
563
+ continue;
562
564
  }
563
- if (e.responseText?.includes("doesn't exist")) {
564
- console.log(` Removing non-existent folder: ${folder.path}`);
565
- this.db.deleteFolder(folder.id);
565
+ // Longer timeout for folders we know are large (Trash, first sync)
566
+ const timeout = highestUid === 0 ? 180000 : 60000;
567
+ try {
568
+ await Promise.race([
569
+ this.syncFolder(accountId, folder.id, client),
570
+ new Promise((_, reject) => setTimeout(() => reject(new Error(`Sync timeout (${timeout / 1000}s) on ${folder.path}`)), timeout))
571
+ ]);
566
572
  }
567
- else {
568
- console.error(` Skipping folder ${folder.path}: ${e.message}`);
573
+ catch (e) {
574
+ if (e.responseText?.includes("doesn't exist")) {
575
+ console.log(` Removing non-existent folder: ${folder.path}`);
576
+ this.db.deleteFolder(folder.id);
577
+ }
578
+ else {
579
+ console.error(` Skipping folder ${folder.path}: ${e.message}`);
580
+ // Connection may be broken — reconnect
581
+ try {
582
+ await client.logout();
583
+ }
584
+ catch { /* */ }
585
+ this.trackLogout(accountId);
586
+ client = this.createClient(accountId);
587
+ }
588
+ }
589
+ }
590
+ }
591
+ finally {
592
+ if (client) {
593
+ try {
594
+ await client.logout();
569
595
  }
596
+ catch { /* */ }
597
+ this.trackLogout(accountId);
570
598
  }
571
599
  }
572
600
  this.accountErrorShown.delete(accountId);