@bobfrankston/mailx 1.0.99 → 1.0.101
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
|
@@ -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
|
-
|
|
387
|
-
const serverUids = new Set(await
|
|
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
|
-
|
|
537
|
-
|
|
538
|
-
const
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
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
|
-
|
|
564
|
-
|
|
565
|
-
|
|
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
|
-
|
|
568
|
-
|
|
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);
|