@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.
Files changed (34) hide show
  1. package/bin/mailx.js +22 -2
  2. package/bin/mailx.js.map +1 -1
  3. package/bin/mailx.ts +21 -2
  4. package/package.json +1 -1
  5. package/packages/mailx-imap/index.d.ts +15 -0
  6. package/packages/mailx-imap/index.d.ts.map +1 -1
  7. package/packages/mailx-imap/index.js +146 -0
  8. package/packages/mailx-imap/index.js.map +1 -1
  9. package/packages/mailx-imap/index.ts +107 -0
  10. package/packages/mailx-imap/package-lock.json +2 -2
  11. package/packages/mailx-imap/package.json +1 -1
  12. package/packages/mailx-service/index.d.ts +1 -5
  13. package/packages/mailx-service/index.d.ts.map +1 -1
  14. package/packages/mailx-service/index.js +18 -171
  15. package/packages/mailx-service/index.js.map +1 -1
  16. package/packages/mailx-service/index.ts +19 -128
  17. package/packages/mailx-service/package.json +1 -0
  18. package/packages/mailx-service/sync-worker-client.d.ts +33 -0
  19. package/packages/mailx-service/sync-worker-client.d.ts.map +1 -0
  20. package/packages/mailx-service/sync-worker-client.js +89 -0
  21. package/packages/mailx-service/sync-worker-client.js.map +1 -0
  22. package/packages/mailx-service/sync-worker-client.ts +92 -0
  23. package/packages/mailx-service/sync-worker.d.ts +33 -0
  24. package/packages/mailx-service/sync-worker.d.ts.map +1 -0
  25. package/packages/mailx-service/sync-worker.js +92 -0
  26. package/packages/mailx-service/sync-worker.js.map +1 -0
  27. package/packages/mailx-service/sync-worker.ts +101 -0
  28. package/packages/mailx-store/db.d.ts +1 -0
  29. package/packages/mailx-store/db.d.ts.map +1 -1
  30. package/packages/mailx-store/db.js +19 -0
  31. package/packages/mailx-store/db.js.map +1 -1
  32. package/packages/mailx-store/db.ts +20 -1
  33. package/packages/mailx-store/package.json +1 -1
  34. 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>();