@bobfrankston/rmfmail 1.2.4 → 1.2.5
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/bin/mailx.js +22 -2
- package/bin/mailx.js.map +1 -1
- package/bin/mailx.ts +21 -2
- package/package.json +1 -1
- package/packages/mailx-imap/index.d.ts +15 -0
- package/packages/mailx-imap/index.d.ts.map +1 -1
- package/packages/mailx-imap/index.js +146 -0
- package/packages/mailx-imap/index.js.map +1 -1
- package/packages/mailx-imap/index.ts +107 -0
- package/packages/mailx-imap/package-lock.json +2 -2
- package/packages/mailx-imap/package.json +1 -1
- package/packages/mailx-service/index.d.ts +1 -5
- package/packages/mailx-service/index.d.ts.map +1 -1
- package/packages/mailx-service/index.js +18 -171
- package/packages/mailx-service/index.js.map +1 -1
- package/packages/mailx-service/index.ts +19 -128
- package/packages/mailx-service/package.json +1 -0
- package/packages/mailx-service/sync-worker-client.d.ts +33 -0
- package/packages/mailx-service/sync-worker-client.d.ts.map +1 -0
- package/packages/mailx-service/sync-worker-client.js +89 -0
- package/packages/mailx-service/sync-worker-client.js.map +1 -0
- package/packages/mailx-service/sync-worker-client.ts +92 -0
- package/packages/mailx-service/sync-worker.d.ts +33 -0
- package/packages/mailx-service/sync-worker.d.ts.map +1 -0
- package/packages/mailx-service/sync-worker.js +92 -0
- package/packages/mailx-service/sync-worker.js.map +1 -0
- package/packages/mailx-service/sync-worker.ts +101 -0
- package/packages/mailx-store/db.d.ts +1 -0
- package/packages/mailx-store/db.d.ts.map +1 -1
- package/packages/mailx-store/db.js +19 -0
- package/packages/mailx-store/db.js.map +1 -1
- package/packages/mailx-store/db.ts +20 -1
- package/packages/mailx-store/package.json +1 -1
- package/test/sync-worker-smoke.mjs +60 -0
|
@@ -408,6 +408,152 @@ export class ImapManager extends EventEmitter {
|
|
|
408
408
|
async createPublicClient(accountId) {
|
|
409
409
|
return this.createClient(accountId);
|
|
410
410
|
}
|
|
411
|
+
// ── Server-side folder operations ──────────────────────────────────────
|
|
412
|
+
// These run the IMAP folder mutation AND the local folder-table write here
|
|
413
|
+
// (on whatever thread ImapManager lives — the sync worker), so the service
|
|
414
|
+
// never needs a live IMAP client object across the worker boundary. The
|
|
415
|
+
// service keeps the surrounding UX (path computation, error messaging); it
|
|
416
|
+
// calls these for the actual server+folder-row work. Returns are primitive
|
|
417
|
+
// so they cross the bus cleanly.
|
|
418
|
+
/** Create `fullPath` on the server, upsert the local folder row, re-sync
|
|
419
|
+
* the folder list, and report whether the folder actually exists after. */
|
|
420
|
+
async createFolderViaServer(accountId, fullPath, name, delimiter) {
|
|
421
|
+
const client = await this.createPublicClient(accountId);
|
|
422
|
+
try {
|
|
423
|
+
await client.createmailbox(fullPath);
|
|
424
|
+
this.db.upsertFolder(accountId, fullPath, name, null, delimiter);
|
|
425
|
+
await this.syncFolders(accountId, client);
|
|
426
|
+
return this.db.getFolders(accountId).some(f => f.path === fullPath);
|
|
427
|
+
}
|
|
428
|
+
finally {
|
|
429
|
+
try {
|
|
430
|
+
await client.logout();
|
|
431
|
+
}
|
|
432
|
+
catch { /* */ }
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
/** Rename `oldPath` → `newPath` on the server and re-sync folders. */
|
|
436
|
+
async renameFolderViaServer(accountId, oldPath, newPath) {
|
|
437
|
+
const client = await this.createPublicClient(accountId);
|
|
438
|
+
try {
|
|
439
|
+
if (client.renameMailbox)
|
|
440
|
+
await client.renameMailbox(oldPath, newPath);
|
|
441
|
+
else
|
|
442
|
+
await client.withConnection(async () => { await client.client.mailboxRename(oldPath, newPath); });
|
|
443
|
+
await this.syncFolders(accountId, client);
|
|
444
|
+
}
|
|
445
|
+
finally {
|
|
446
|
+
try {
|
|
447
|
+
await client.logout();
|
|
448
|
+
}
|
|
449
|
+
catch { /* */ }
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
/** Delete `path` on the server (tolerating already-gone). Local folder-row
|
|
453
|
+
* removal stays with the caller (it knows the folderId). */
|
|
454
|
+
async deleteFolderViaServer(accountId, path) {
|
|
455
|
+
const client = await this.createPublicClient(accountId);
|
|
456
|
+
try {
|
|
457
|
+
try {
|
|
458
|
+
if (client.deleteMailbox)
|
|
459
|
+
await client.deleteMailbox(path);
|
|
460
|
+
else
|
|
461
|
+
await client.withConnection(async () => { await client.client.mailboxDelete(path); });
|
|
462
|
+
}
|
|
463
|
+
catch (e) {
|
|
464
|
+
const msg = String(e?.message || e || "").toLowerCase();
|
|
465
|
+
if (!/nonexistent|does not exist|no such|not found|404/i.test(msg))
|
|
466
|
+
throw e;
|
|
467
|
+
console.log(` [folder] ${accountId} delete "${path}": server says already gone`);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
finally {
|
|
471
|
+
try {
|
|
472
|
+
await client.logout();
|
|
473
|
+
}
|
|
474
|
+
catch { /* */ }
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
/** Expunge every message in `path` on the server. */
|
|
478
|
+
async emptyFolderViaServer(accountId, path) {
|
|
479
|
+
const client = await this.createPublicClient(accountId);
|
|
480
|
+
try {
|
|
481
|
+
const uids = await client.getUids(path);
|
|
482
|
+
for (const uid of uids)
|
|
483
|
+
await client.deleteMessageByUid(path, uid);
|
|
484
|
+
}
|
|
485
|
+
finally {
|
|
486
|
+
try {
|
|
487
|
+
await client.logout();
|
|
488
|
+
}
|
|
489
|
+
catch { /* */ }
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
/** Move a folder into Trash by IMAP RENAME, falling back to spilling its
|
|
493
|
+
* messages into Trash root + deleting the empty folder when the server
|
|
494
|
+
* forbids nesting under Trash. Self-contained on the worker. */
|
|
495
|
+
async moveFolderToTrashViaServer(accountId, folderId, folderPath, targetPath, trashId, trashPath, delim) {
|
|
496
|
+
const client = await this.createPublicClient(accountId);
|
|
497
|
+
try {
|
|
498
|
+
try {
|
|
499
|
+
if (client.renameMailbox)
|
|
500
|
+
await client.renameMailbox(folderPath, targetPath);
|
|
501
|
+
else
|
|
502
|
+
await client.withConnection(async () => { await client.client.mailboxRename(folderPath, targetPath); });
|
|
503
|
+
console.log(` [folder] ${accountId} moved "${folderPath}" → "${targetPath}"`);
|
|
504
|
+
}
|
|
505
|
+
catch (e) {
|
|
506
|
+
const msg = String(e?.message || e || "").toLowerCase();
|
|
507
|
+
if (!/noinferiors|hierarchy|invalid (mailbox )?name|cannot rename|inferiors/i.test(msg))
|
|
508
|
+
throw e;
|
|
509
|
+
console.log(` [folder] ${accountId} cannot RENAME under Trash; spilling messages`);
|
|
510
|
+
await this.spillFolderToTrashServer(accountId, folderId, folderPath, trashId, trashPath, delim, client);
|
|
511
|
+
}
|
|
512
|
+
await this.syncFolders(accountId, client);
|
|
513
|
+
}
|
|
514
|
+
finally {
|
|
515
|
+
try {
|
|
516
|
+
await client.logout();
|
|
517
|
+
}
|
|
518
|
+
catch { /* */ }
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
async spillFolderToTrashServer(accountId, folderId, folderPath, trashId, trashPath, delim, client) {
|
|
522
|
+
// Children first so the parent ends up empty.
|
|
523
|
+
const childPrefix = folderPath + delim;
|
|
524
|
+
const children = this.db.getFolders(accountId).filter(f => f.path.startsWith(childPrefix));
|
|
525
|
+
for (const child of children) {
|
|
526
|
+
await this.spillFolderToTrashServer(accountId, child.id, child.path, trashId, trashPath, delim, client);
|
|
527
|
+
}
|
|
528
|
+
// Move each message to Trash on the server, then mirror locally.
|
|
529
|
+
const uids = await client.getUids(folderPath).catch(() => []);
|
|
530
|
+
for (const uid of uids) {
|
|
531
|
+
try {
|
|
532
|
+
if (client.moveMessage)
|
|
533
|
+
await client.moveMessage(folderPath, trashPath, uid);
|
|
534
|
+
else {
|
|
535
|
+
await client.copyMessage(folderPath, trashPath, uid);
|
|
536
|
+
await client.deleteMessageByUid(folderPath, uid);
|
|
537
|
+
}
|
|
538
|
+
this.store.trashMessage(accountId, uid, folderId, trashId);
|
|
539
|
+
}
|
|
540
|
+
catch (e) {
|
|
541
|
+
console.error(` [folder] spill uid ${uid}: ${e?.message || e}`);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
try {
|
|
545
|
+
if (client.deleteMailbox)
|
|
546
|
+
await client.deleteMailbox(folderPath);
|
|
547
|
+
else
|
|
548
|
+
await client.withConnection(async () => { await client.client.mailboxDelete(folderPath); });
|
|
549
|
+
}
|
|
550
|
+
catch (e) {
|
|
551
|
+
const m = String(e?.message || e || "").toLowerCase();
|
|
552
|
+
if (!/nonexistent|does not exist|no such/.test(m))
|
|
553
|
+
throw e;
|
|
554
|
+
}
|
|
555
|
+
this.db.deleteFolder(folderId);
|
|
556
|
+
}
|
|
411
557
|
// Legacy fallback disabled — was doubling connections without helping.
|
|
412
558
|
// To re-enable: uncomment legacyFallbacks logic in createClient and _syncAll.
|
|
413
559
|
// private legacyFallbacks = new Set<string>();
|