@bobfrankston/rmfmail 1.0.681 → 1.0.688

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 (77) hide show
  1. package/bin/lean-accounts.js +0 -1
  2. package/client/app.bundle.js +138 -38
  3. package/client/app.bundle.js.map +2 -2
  4. package/client/app.js +12 -1
  5. package/client/app.js.map +1 -1
  6. package/client/app.ts +12 -1
  7. package/client/components/context-menu.js +2 -0
  8. package/client/components/context-menu.js.map +1 -1
  9. package/client/components/context-menu.ts +6 -0
  10. package/client/components/folder-tree.js +26 -4
  11. package/client/components/folder-tree.js.map +1 -1
  12. package/client/components/folder-tree.ts +21 -4
  13. package/client/components/message-list.js +108 -40
  14. package/client/components/message-list.js.map +1 -1
  15. package/client/components/message-list.ts +103 -38
  16. package/client/compose/compose.bundle.js +148 -15
  17. package/client/compose/compose.bundle.js.map +3 -3
  18. package/client/compose/spellcheck.js +178 -12
  19. package/client/compose/spellcheck.js.map +1 -1
  20. package/client/compose/spellcheck.ts +168 -8
  21. package/client/lib/api-client.js +3 -0
  22. package/client/lib/api-client.js.map +1 -1
  23. package/client/lib/api-client.ts +4 -0
  24. package/client/lib/mailxapi.js +3 -0
  25. package/client/lib/rmf-tiny.js +25 -6
  26. package/package.json +7 -7
  27. package/packages/mailx-core/index.d.ts.map +1 -1
  28. package/packages/mailx-core/index.js +2 -12
  29. package/packages/mailx-core/index.js.map +1 -1
  30. package/packages/mailx-core/index.ts +2 -12
  31. package/packages/mailx-imap/index.d.ts.map +1 -1
  32. package/packages/mailx-imap/index.js +51 -6
  33. package/packages/mailx-imap/index.js.map +1 -1
  34. package/packages/mailx-imap/index.ts +51 -6
  35. package/packages/mailx-imap/node_modules.npmglobalize-stash-78076/.package-lock.json +116 -0
  36. package/packages/mailx-imap/package-lock.json +2 -2
  37. package/packages/mailx-imap/package.json +1 -1
  38. package/packages/mailx-service/index.d.ts +22 -0
  39. package/packages/mailx-service/index.d.ts.map +1 -1
  40. package/packages/mailx-service/index.js +123 -0
  41. package/packages/mailx-service/index.js.map +1 -1
  42. package/packages/mailx-service/index.ts +109 -0
  43. package/packages/mailx-service/jsonrpc.js +3 -0
  44. package/packages/mailx-service/jsonrpc.js.map +1 -1
  45. package/packages/mailx-service/jsonrpc.ts +3 -0
  46. package/packages/mailx-service/local-store.d.ts.map +1 -1
  47. package/packages/mailx-service/local-store.js +17 -12
  48. package/packages/mailx-service/local-store.js.map +1 -1
  49. package/packages/mailx-service/local-store.ts +15 -12
  50. package/packages/mailx-settings/docs/accounts.md +14 -1
  51. package/packages/mailx-settings/docs/npmglobalize-disttag.md +90 -0
  52. package/packages/mailx-settings/docs/prod-android.md +88 -0
  53. package/packages/mailx-settings/docs/prod.md +224 -0
  54. package/packages/mailx-settings/docs/push-relay.md +141 -0
  55. package/packages/mailx-settings/docs/rmf-tiny.md +156 -0
  56. package/packages/mailx-settings/index.d.ts +1 -1
  57. package/packages/mailx-settings/index.d.ts.map +1 -1
  58. package/packages/mailx-settings/index.js +1 -4
  59. package/packages/mailx-settings/index.js.map +1 -1
  60. package/packages/mailx-settings/index.ts +1 -3
  61. package/packages/mailx-settings/package.json +1 -1
  62. package/packages/mailx-store/db.d.ts +6 -0
  63. package/packages/mailx-store/db.d.ts.map +1 -1
  64. package/packages/mailx-store/db.js +67 -7
  65. package/packages/mailx-store/db.js.map +1 -1
  66. package/packages/mailx-store/db.ts +73 -7
  67. package/packages/mailx-store/package.json +1 -1
  68. package/packages/mailx-store-web/package.json +4 -1
  69. package/packages/mailx-store-web/web-settings.d.ts.map +1 -1
  70. package/packages/mailx-store-web/web-settings.js +0 -1
  71. package/packages/mailx-store-web/web-settings.js.map +1 -1
  72. package/packages/mailx-store-web/web-settings.ts +0 -1
  73. package/packages/mailx-types/index.d.ts +1 -2
  74. package/packages/mailx-types/index.d.ts.map +1 -1
  75. package/packages/mailx-types/index.js.map +1 -1
  76. package/packages/mailx-types/index.ts +1 -2
  77. package/packages/mailx-types/package.json +1 -1
@@ -98,7 +98,6 @@ function denormalizeAccount(acct, globalName) {
98
98
  if (acct.defaultSend) out.defaultSend = true;
99
99
  if (acct.enabled === false) out.enabled = false;
100
100
  if (acct.relayDomains?.length > 0) out.relayDomains = acct.relayDomains;
101
- if (acct.deliveredToPrefix?.length > 0) out.deliveredToPrefix = acct.deliveredToPrefix;
102
101
  if (acct.identityDomains?.length > 0) out.identityDomains = acct.identityDomains;
103
102
 
104
103
  const syncContactsDefault = provider?.imap.auth === "oauth2";
@@ -62,6 +62,7 @@ __export(api_client_exports, {
62
62
  logClientEvent: () => logClientEvent,
63
63
  markAsSpamMessages: () => markAsSpamMessages,
64
64
  markFolderRead: () => markFolderRead,
65
+ moveFolderToTrash: () => moveFolderToTrash,
65
66
  moveMessage: () => moveMessage,
66
67
  moveMessages: () => moveMessages,
67
68
  onEvent: () => onEvent,
@@ -334,6 +335,9 @@ function renameFolder(accountId, folderId, newName) {
334
335
  function deleteFolder(accountId, folderId) {
335
336
  return ipc().deleteFolder?.(accountId, folderId);
336
337
  }
338
+ function moveFolderToTrash(accountId, folderId) {
339
+ return ipc().moveFolderToTrash?.(accountId, folderId);
340
+ }
337
341
  function emptyFolder(accountId, folderId) {
338
342
  return ipc().emptyFolder?.(accountId, folderId);
339
343
  }
@@ -489,6 +493,8 @@ function showContextMenu(x, y, items) {
489
493
  const el = document.createElement("div");
490
494
  el.className = "ctx-item" + (item.disabled ? " ctx-disabled" : "");
491
495
  el.textContent = item.label;
496
+ if (item.tooltip)
497
+ el.title = item.tooltip;
492
498
  if (!item.disabled) {
493
499
  el.addEventListener("click", () => {
494
500
  closeContextMenu();
@@ -2164,6 +2170,57 @@ function cacheKey(mode, a, f, flagged, q) {
2164
2170
  return `folder:${a}:${f}:${flagged ? "flag" : ""}`;
2165
2171
  return `search:${q}`;
2166
2172
  }
2173
+ function persistPositions() {
2174
+ try {
2175
+ const obj = {};
2176
+ for (const [k, v] of positionMemory)
2177
+ obj[k] = v;
2178
+ sessionStorage.setItem(POSITION_STORAGE_KEY, JSON.stringify(obj));
2179
+ } catch {
2180
+ }
2181
+ }
2182
+ function currentViewKey() {
2183
+ if (searchMode)
2184
+ return cacheKey("search", void 0, void 0, void 0, currentSearchQuery);
2185
+ if (unifiedMode)
2186
+ return CACHE_KEY_UNIFIED;
2187
+ if (!currentAccountId2 || currentFolderId == null)
2188
+ return null;
2189
+ const flaggedOnly = document.getElementById("ml-body")?.classList.contains("flagged-only") || false;
2190
+ return cacheKey("folder", currentAccountId2, currentFolderId, flaggedOnly);
2191
+ }
2192
+ function rememberPosition() {
2193
+ const key = currentViewKey();
2194
+ if (!key)
2195
+ return;
2196
+ const body = document.getElementById("ml-body");
2197
+ if (!body)
2198
+ return;
2199
+ const sel = body.querySelector(".ml-row.selected");
2200
+ if (!sel)
2201
+ return;
2202
+ const uid = Number(sel.dataset.uid);
2203
+ if (!Number.isFinite(uid))
2204
+ return;
2205
+ positionMemory.set(key, { uid, scroll: body.scrollTop });
2206
+ persistPositions();
2207
+ }
2208
+ function pickRestoreUid(items, saved) {
2209
+ if (!items.length)
2210
+ return null;
2211
+ if (items.some((m) => m.uid === saved))
2212
+ return saved;
2213
+ let best = -1;
2214
+ for (const m of items) {
2215
+ if (typeof m.uid !== "number")
2216
+ continue;
2217
+ if (m.uid < saved && m.uid > best)
2218
+ best = m.uid;
2219
+ }
2220
+ if (best >= 0)
2221
+ return best;
2222
+ return typeof items[0]?.uid === "number" ? items[0].uid : null;
2223
+ }
2167
2224
  function listResultsEqual(a, b) {
2168
2225
  if (!a || a.length !== b.length)
2169
2226
  return false;
@@ -2210,6 +2267,7 @@ function focusRow(row) {
2210
2267
  showMessage(row.accountId, row.msg.uid, row.msg.folderId, void 0, false, row.msg);
2211
2268
  onMessageSelect(row.accountId, row.msg.uid, row.msg.folderId);
2212
2269
  document.dispatchEvent(new CustomEvent("mailx-focus-changed", { detail: row.msg }));
2270
+ rememberPosition();
2213
2271
  }
2214
2272
  function getCurrentFocused() {
2215
2273
  return focusedRow ? focusedRow.msg : null;
@@ -2467,19 +2525,19 @@ async function loadUnifiedInbox(autoSelect = true) {
2467
2525
  const fromHeader = document.querySelector(".ml-col-from");
2468
2526
  if (fromHeader)
2469
2527
  fromHeader.textContent = "From";
2470
- const savedScroll = !autoSelect ? body.scrollTop : 0;
2471
- const currentSelectedUid = () => !autoSelect ? body.querySelector(".ml-row.selected")?.getAttribute("data-uid") || null : null;
2528
+ const remembered = positionMemory.get(CACHE_KEY_UNIFIED);
2529
+ const savedScroll = remembered?.scroll ?? (!autoSelect ? body.scrollTop : 0);
2472
2530
  const cached = listCache.get(CACHE_KEY_UNIFIED);
2473
2531
  if (cached) {
2474
- const preCacheUid = currentSelectedUid();
2475
2532
  totalMessages = cached.total;
2476
2533
  setMessages(cached.items);
2477
2534
  renderMessages(body, "", cached.items);
2478
- if (autoSelect)
2479
- selectFirst(body);
2480
- else {
2535
+ const targetUid = remembered ? pickRestoreUid(cached.items, remembered.uid) : null;
2536
+ if (targetUid != null) {
2481
2537
  body.scrollTop = savedScroll;
2482
- restoreSelection(body, preCacheUid);
2538
+ restoreSelection(body, String(targetUid));
2539
+ } else if (autoSelect) {
2540
+ selectFirst(body);
2483
2541
  }
2484
2542
  } else if (autoSelect) {
2485
2543
  body.innerHTML = `<div class="ml-empty">Loading...</div>`;
@@ -2497,14 +2555,14 @@ async function loadUnifiedInbox(autoSelect = true) {
2497
2555
  body.innerHTML = `<div class="ml-empty">${result.total > 0 ? `${result.total} messages syncing...` : "Syncing \u2014 messages will appear shortly"}</div>`;
2498
2556
  return;
2499
2557
  }
2500
- const preRenderUid = currentSelectedUid();
2501
2558
  setMessages(result.items);
2502
2559
  renderMessages(body, "", result.items);
2503
- if (autoSelect) {
2504
- selectFirst(body);
2505
- } else {
2560
+ const targetUid = remembered ? pickRestoreUid(result.items, remembered.uid) : null;
2561
+ if (targetUid != null) {
2506
2562
  body.scrollTop = savedScroll;
2507
- restoreSelection(body, preRenderUid);
2563
+ restoreSelection(body, String(targetUid));
2564
+ } else if (autoSelect) {
2565
+ selectFirst(body);
2508
2566
  }
2509
2567
  } catch (e) {
2510
2568
  if (e.name === "AbortError")
@@ -2604,23 +2662,23 @@ async function loadMessages(accountId, folderId, page = 1, specialUse = "", auto
2604
2662
  const fromHeader = document.querySelector(".ml-col-from");
2605
2663
  if (fromHeader)
2606
2664
  fromHeader.textContent = showToInsteadOfFrom ? "To" : "From";
2607
- const savedScroll = !autoSelect ? body.scrollTop : 0;
2608
- const currentSelectedUid = () => !autoSelect ? body.querySelector(".ml-row.selected")?.getAttribute("data-uid") || null : null;
2609
2665
  const flaggedOnly = document.getElementById("ml-body")?.classList.contains("flagged-only") || false;
2610
2666
  const cKey = cacheKey("folder", accountId, folderId, flaggedOnly);
2667
+ const remembered = positionMemory.get(cKey);
2668
+ const savedScroll = remembered?.scroll ?? (!autoSelect ? body.scrollTop : 0);
2611
2669
  const cached = listCache.get(cKey);
2612
2670
  if (cached) {
2613
- const preCacheUid = currentSelectedUid();
2614
2671
  totalMessages = cached.total;
2615
2672
  setMessages(cached.items);
2616
2673
  renderMessages(body, accountId, cached.items);
2617
- if (autoSelect)
2618
- selectFirst(body);
2619
- else {
2674
+ const targetUid = remembered ? pickRestoreUid(cached.items, remembered.uid) : null;
2675
+ if (targetUid != null) {
2620
2676
  requestAnimationFrame(() => {
2621
2677
  body.scrollTop = savedScroll;
2622
- restoreSelection(body, preCacheUid);
2678
+ restoreSelection(body, String(targetUid));
2623
2679
  });
2680
+ } else if (autoSelect) {
2681
+ selectFirst(body);
2624
2682
  }
2625
2683
  } else if (autoSelect) {
2626
2684
  body.innerHTML = `<div class="ml-empty">Loading...</div>`;
@@ -2639,18 +2697,18 @@ async function loadMessages(accountId, folderId, page = 1, specialUse = "", auto
2639
2697
  body.innerHTML = `<div class="ml-empty">${flaggedOnly ? "No flagged messages" : "No messages"}</div>`;
2640
2698
  return;
2641
2699
  }
2642
- const preRenderUid = currentSelectedUid();
2643
2700
  setMessages(result.items);
2644
2701
  renderMessages(body, accountId, result.items);
2645
- if (autoSelect) {
2646
- selectFirst(body);
2647
- } else {
2702
+ const targetUid = remembered ? pickRestoreUid(result.items, remembered.uid) : null;
2703
+ if (targetUid != null) {
2648
2704
  requestAnimationFrame(() => {
2649
2705
  if (myGen !== loadGen)
2650
2706
  return;
2651
2707
  body.scrollTop = savedScroll;
2652
- restoreSelection(body, preRenderUid);
2708
+ restoreSelection(body, String(targetUid));
2653
2709
  });
2710
+ } else if (autoSelect) {
2711
+ selectFirst(body);
2654
2712
  }
2655
2713
  } catch (e) {
2656
2714
  if (e.name === "AbortError")
@@ -2812,7 +2870,7 @@ function escapeHtml2(s) {
2812
2870
  div.textContent = s;
2813
2871
  return div.innerHTML;
2814
2872
  }
2815
- var onMessageSelect, currentAccountId2, currentFolderId, currentSpecialUse, lastClickedRow, currentPage, totalMessages, loading, unifiedMode, searchMode, currentSearchQuery, wasUnifiedBeforeSearch, showToInsteadOfFrom, touchWasScroll, currentSort, currentSortDir, loadGen, listCache, CACHE_KEY_UNIFIED, focusedRow, rowByKey, prioritySenders, priorityDomains, timeFmt, dateFmt, dateFmtSameYear, MessageRow;
2873
+ var onMessageSelect, currentAccountId2, currentFolderId, currentSpecialUse, lastClickedRow, currentPage, totalMessages, loading, unifiedMode, searchMode, currentSearchQuery, wasUnifiedBeforeSearch, showToInsteadOfFrom, touchWasScroll, currentSort, currentSortDir, loadGen, listCache, CACHE_KEY_UNIFIED, positionMemory, POSITION_STORAGE_KEY, focusedRow, rowByKey, prioritySenders, priorityDomains, timeFmt, dateFmt, dateFmtSameYear, MessageRow;
2816
2874
  var init_message_list = __esm({
2817
2875
  "client/components/message-list.js"() {
2818
2876
  "use strict";
@@ -2835,6 +2893,19 @@ var init_message_list = __esm({
2835
2893
  loadGen = 0;
2836
2894
  listCache = /* @__PURE__ */ new Map();
2837
2895
  CACHE_KEY_UNIFIED = "unified";
2896
+ positionMemory = /* @__PURE__ */ new Map();
2897
+ POSITION_STORAGE_KEY = "mailx-list-positions";
2898
+ try {
2899
+ const raw = sessionStorage.getItem(POSITION_STORAGE_KEY);
2900
+ if (raw) {
2901
+ const parsed = JSON.parse(raw);
2902
+ for (const [k, v] of Object.entries(parsed || {})) {
2903
+ if (typeof v?.uid === "number")
2904
+ positionMemory.set(k, v);
2905
+ }
2906
+ }
2907
+ } catch {
2908
+ }
2838
2909
  focusedRow = null;
2839
2910
  rowByKey = /* @__PURE__ */ new Map();
2840
2911
  prioritySenders = /* @__PURE__ */ new Set();
@@ -4918,18 +4989,47 @@ function renderNode(node, container, depth) {
4918
4989
  alert(`Failed: ${err.message}`);
4919
4990
  }
4920
4991
  }, disabled: !!node.specialUse },
4921
- { label: "Delete folder", action: async () => {
4922
- if (!confirm(`Delete folder "${node.name}"? Messages will be moved to Trash.`))
4923
- return;
4924
- try {
4925
- await deleteFolder(node.accountId, node.id);
4926
- const treeContainer = document.getElementById("folder-tree");
4927
- if (treeContainer)
4928
- loadFolderTree(treeContainer);
4929
- } catch (err) {
4930
- alert(`Failed: ${err.message}`);
4931
- }
4932
- }, disabled: !!node.specialUse },
4992
+ // Two delete entries. Move-to-Trash is the default; permanent
4993
+ // delete is a second, separated item with a tooltip so it's
4994
+ // visible from the menu rather than requiring a discovery
4995
+ // shortcut. The IMAP RENAME under Trash brings messages +
4996
+ // subfolders along; the server-side fallback (when Trash is
4997
+ // \Noinferiors) moves messages to Trash root and then deletes
4998
+ // the empty folder. Permanent skips Trash entirely.
4999
+ {
5000
+ label: "Move folder to Trash",
5001
+ action: async () => {
5002
+ if (!confirm(`Move folder "${node.name}" to Trash? It can be restored by dragging back out of Trash.`))
5003
+ return;
5004
+ try {
5005
+ await moveFolderToTrash(node.accountId, node.id);
5006
+ const treeContainer = document.getElementById("folder-tree");
5007
+ if (treeContainer)
5008
+ loadFolderTree(treeContainer);
5009
+ } catch (err) {
5010
+ alert(`Failed: ${err.message}`);
5011
+ }
5012
+ },
5013
+ disabled: !!node.specialUse,
5014
+ tooltip: "Renames the folder into Trash (date-suffixed if needed); use Delete permanently below to skip Trash."
5015
+ },
5016
+ {
5017
+ label: "Delete folder permanently",
5018
+ action: async () => {
5019
+ if (!confirm(`Permanently delete folder "${node.name}" and ALL its messages? This cannot be undone.`))
5020
+ return;
5021
+ try {
5022
+ await deleteFolder(node.accountId, node.id);
5023
+ const treeContainer = document.getElementById("folder-tree");
5024
+ if (treeContainer)
5025
+ loadFolderTree(treeContainer);
5026
+ } catch (err) {
5027
+ alert(`Failed: ${err.message}`);
5028
+ }
5029
+ },
5030
+ disabled: !!node.specialUse,
5031
+ tooltip: "Skips Trash. Same as Shift+Delete on a regular file. No undo."
5032
+ },
4933
5033
  { label: "", action: () => {
4934
5034
  }, separator: true },
4935
5035
  // Q57: copy IMAP path so user can paste into accounts.jsonc as
@@ -6435,7 +6535,7 @@ function addComposeResizeHandles(wrapper, frame) {
6435
6535
  function sanitizeQuotedBody(msg) {
6436
6536
  const isPlainText = !msg.bodyHtml;
6437
6537
  if (isPlainText) {
6438
- const escaped = String(msg.bodyText || "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
6538
+ const escaped = String(msg.bodyText || "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/\r\n?/g, "\n").replace(/\n/g, "<br>");
6439
6539
  return `<div style="white-space:pre-wrap;font-family:inherit;margin:0">${escaped}</div>`;
6440
6540
  }
6441
6541
  let body = msg.bodyHtml;