@bobfrankston/rmfmail 1.2.136 → 1.2.139

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.ts CHANGED
@@ -1768,10 +1768,28 @@ async function main(): Promise<void> {
1768
1768
  const d = new Date();
1769
1769
  return `${pad2(d.getHours())}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}.${pad3(d.getMilliseconds())}`;
1770
1770
  };
1771
- const logPath = path.join(logDir, `rmfmail-${localDate()}.log`);
1772
- const logStream = fs.createWriteStream(logPath, { flags: "a" });
1773
- console.log = (...a: any[]) => { logStream.write(`${ts()} ${a.join(" ")}\n`); };
1774
- console.error = (...a: any[]) => { logStream.write(`${ts()} ERROR ${a.join(" ")}\n`); };
1771
+ // Day-aware stream: recompute the filename on every write and swap
1772
+ // the stream at midnight. The old code resolved the path ONCE at
1773
+ // boot, so a daemon that ran past midnight kept appending to the
1774
+ // boot-day file forever while the sync worker's lines (written
1775
+ // through its own channel) landed in the new day's file. Result: the
1776
+ // morning of 2026-07-14 had ALL main-thread diagnostics ([ipc],
1777
+ // [client], [popout]) hiding in rmfmail-2026-07-13.log and the
1778
+ // current file showing only [w] worker lines — which made the
1779
+ // popout-send data-loss incident look untraceable (S68).
1780
+ let logDate = localDate();
1781
+ let logStream = fs.createWriteStream(path.join(logDir, `rmfmail-${logDate}.log`), { flags: "a" });
1782
+ const logWrite = (line: string): void => {
1783
+ const d = localDate();
1784
+ if (d !== logDate) {
1785
+ try { logStream.end(); } catch { /* old handle already dead */ }
1786
+ logDate = d;
1787
+ logStream = fs.createWriteStream(path.join(logDir, `rmfmail-${d}.log`), { flags: "a" });
1788
+ }
1789
+ logStream.write(line);
1790
+ };
1791
+ console.log = (...a: any[]) => { logWrite(`${ts()} ${a.join(" ")}\n`); };
1792
+ console.error = (...a: any[]) => { logWrite(`${ts()} ERROR ${a.join(" ")}\n`); };
1775
1793
  // Redirect daemon's process.stderr to the log too. msger forwards its
1776
1794
  // Rust child's stderr to our process.stderr; in daemon mode that's
1777
1795
  // stdio:"ignore" → /dev/null, which buries diagnostics like
@@ -1781,7 +1799,7 @@ async function main(): Promise<void> {
1781
1799
  process.stderr.write = ((chunk: any, ...rest: any[]): boolean => {
1782
1800
  try {
1783
1801
  const s = typeof chunk === "string" ? chunk : chunk.toString();
1784
- logStream.write(`${ts()} STDERR ${s}${s.endsWith("\n") ? "" : "\n"}`);
1802
+ logWrite(`${ts()} STDERR ${s}${s.endsWith("\n") ? "" : "\n"}`);
1785
1803
  } catch { /* ignore — best-effort logging */ }
1786
1804
  return origStderrWrite(chunk, ...rest);
1787
1805
  }) as typeof process.stderr.write;
@@ -1800,7 +1818,7 @@ async function main(): Promise<void> {
1800
1818
  { MailxDB, prewarmParseWorker, storeBus, Store },
1801
1819
  { ImapManager },
1802
1820
  { MailxService, spawnSyncWorker },
1803
- { dispatch },
1821
+ { dispatch, setDebugEvalSink },
1804
1822
  { loadSettings, loadAccountsAsync, loadAllowlistAsync, getConfigDir, getStorageInfo, getStorePath },
1805
1823
  { NodeTcpTransport },
1806
1824
  { FileMessageStore },
@@ -2026,7 +2044,10 @@ RFC 5322 with CRLF line endings. Bodies are quoted-printable encoded (readable i
2026
2044
  // see memory: feedback_cross_platform_strong_rule). Remove on
2027
2045
  // resolution so the file doesn't accumulate stale PIDs.
2028
2046
  svc.setPopupFn(async (opts: any) => {
2029
- const h = showMessageBoxEx(opts);
2047
+ // Own WebView2 profile so a reminder popup can never share (and
2048
+ // crash) the main window's browser process — see S67 note on the
2049
+ // popout windows below. Service-supplied opts may override.
2050
+ const h = showMessageBoxEx({ profile: "popout", ...opts });
2030
2051
  if (typeof h.pid === "number") addChildPid(h.pid);
2031
2052
  try {
2032
2053
  const r = await h.result;
@@ -2051,6 +2072,40 @@ RFC 5322 with CRLF line endings. Bodies are quoted-printable encoded (readable i
2051
2072
  const { ports } = await import("@bobfrankston/miscinfo");
2052
2073
  const dbgApp = express();
2053
2074
  dbgApp.use(express.json({ limit: "25mb" }));
2075
+ // Live-page eval: POST /api/eval {code} → daemon pushes a
2076
+ // `debugEval` event to the main window → app.ts runs the code and
2077
+ // answers via the `debugEvalResult` action → we reply here. The
2078
+ // expression may be async (it's wrapped in `(async () => (…))()`),
2079
+ // must evaluate as an EXPRESSION, and should return JSON-able
2080
+ // data. Lets an agent inspect the real DOM (elementFromPoint,
2081
+ // computed styles, scroll geometry) without devtools. Registered
2082
+ // BEFORE the generic /api router so it isn't shadowed.
2083
+ const pendingEvals = new Map<string, { finish: (v: any) => void; timer: NodeJS.Timeout }>();
2084
+ setDebugEvalSink((id, result, error) => {
2085
+ const p = pendingEvals.get(id);
2086
+ if (!p) return;
2087
+ pendingEvals.delete(id);
2088
+ clearTimeout(p.timer);
2089
+ p.finish(error ? { error } : { result });
2090
+ });
2091
+ dbgApp.post("/api/eval", (req, res) => {
2092
+ const code = req.body?.code;
2093
+ if (typeof code !== "string" || !code.trim()) {
2094
+ res.status(400).json({ error: "body must be { code: string } — a JS expression" });
2095
+ return;
2096
+ }
2097
+ if (!sendToClient) {
2098
+ res.status(503).json({ error: "main window not connected yet" });
2099
+ return;
2100
+ }
2101
+ const id = `ev-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
2102
+ const timer = setTimeout(() => {
2103
+ pendingEvals.delete(id);
2104
+ res.status(504).json({ error: "eval timeout (10s) — window busy, or client bundle predates debugEval" });
2105
+ }, 10_000);
2106
+ pendingEvals.set(id, { finish: (v) => res.json(v), timer });
2107
+ sendToClient({ _event: "debugEval", type: "debugEval", id, code });
2108
+ });
2054
2109
  dbgApp.use("/api", createApiRouter(store, imapManager));
2055
2110
  // Port: `--debug-server=<n>` / `--debug-server <n>` if given,
2056
2111
  // else the reserved `ports.rmfmaildbg` from miscinfo (the named
@@ -2141,6 +2196,13 @@ RFC 5322 with CRLF line endings. Bodies are quoted-printable encoded (readable i
2141
2196
  // window shows unactivated BEHIND the focused main window
2142
2197
  // and reads as "nothing opened" (Bob 2026-07-09).
2143
2198
  focusOnCreate: true,
2199
+ // Own WebView2 profile: a window sharing the main window's
2200
+ // user-data dir shares its browser process, and a crash
2201
+ // there blacks out the MAIN window permanently (S67,
2202
+ // 2026-07-14 + 2026-07-15: BROWSER_PROCESS_EXITED during
2203
+ // popout spawn). One shared "popout" profile for all
2204
+ // secondary windows — decoupled from main, bounded disk.
2205
+ profile: "popout",
2144
2206
  });
2145
2207
  popoutWindows.set(key, h);
2146
2208
  if (typeof h.pid === "number") addChildPid(h.pid);
@@ -2173,6 +2235,8 @@ RFC 5322 with CRLF line endings. Bodies are quoted-printable encoded (readable i
2173
2235
  // Same as the message popout: the daemon is background,
2174
2236
  // so an unactivated window lands behind the main window.
2175
2237
  focusOnCreate: true,
2238
+ // Own profile — same S67 rationale as the message popout.
2239
+ profile: "popout",
2176
2240
  // App entries on the NATIVE right-click menu (native stays
2177
2241
  // so spellcheck suggestions survive). compose.ts maps ids
2178
2242
  // to editor commands via window.__msgerContextCommand.
@@ -2221,6 +2285,10 @@ RFC 5322 with CRLF line endings. Bodies are quoted-printable encoded (readable i
2221
2285
  aumid: "com.frankston.rmfmail",
2222
2286
  escapeCloses: false,
2223
2287
  focusOnCreate: true,
2288
+ // Own profile — same S67 rationale as the message popout.
2289
+ // Note: localStorage view prefs won't be shared with the
2290
+ // main window; preferences.jsonc (the real store) is.
2291
+ profile: "popout",
2224
2292
  });
2225
2293
  popoutWindows.set(key, h);
2226
2294
  if (typeof h.pid === "number") addChildPid(h.pid);
@@ -37,6 +37,7 @@ __export(api_client_exports, {
37
37
  createCalendarEvent: () => createCalendarEvent,
38
38
  createFolder: () => createFolder,
39
39
  createTask: () => createTask,
40
+ debugEvalResult: () => debugEvalResult,
40
41
  deleteCalendarEvent: () => deleteCalendarEvent,
41
42
  deleteContact: () => deleteContact,
42
43
  deleteDraft: () => deleteDraft,
@@ -496,6 +497,9 @@ function logClientEvent(tag, data) {
496
497
  function sendMessage(body) {
497
498
  return ipc().sendMessage?.(body);
498
499
  }
500
+ function debugEvalResult(id, result, error) {
501
+ return ipc().debugEvalResult?.(id, result, error);
502
+ }
499
503
  function saveDraft(body) {
500
504
  return ipc().saveDraft?.(body);
501
505
  }
@@ -3849,11 +3853,16 @@ function clearSearchMode() {
3849
3853
  }
3850
3854
  async function loadUnifiedInbox(autoSelect = true) {
3851
3855
  const myGen = ++loadGen;
3856
+ const wasUnified = unifiedMode;
3852
3857
  unifiedMode = true;
3853
3858
  searchMode = false;
3854
3859
  currentSpecialUse = "";
3855
3860
  showToInsteadOfFrom = false;
3856
- currentPage = 1;
3861
+ const loadedPages = !autoSelect && wasUnified ? Math.min(Math.max(1, currentPage), MAX_REFRESH_PAGES) : 1;
3862
+ if (!autoSelect && wasUnified && currentPage > MAX_REFRESH_PAGES) {
3863
+ console.log(` [ml-refresh] depth capped: ${currentPage} loaded pages, refreshing only ${MAX_REFRESH_PAGES}`);
3864
+ }
3865
+ currentPage = loadedPages;
3857
3866
  totalMessages = 0;
3858
3867
  const body = document.getElementById("ml-body");
3859
3868
  if (!body)
@@ -3886,7 +3895,7 @@ async function loadUnifiedInbox(autoSelect = true) {
3886
3895
  body.innerHTML = `<div class="ml-empty">Loading...</div>`;
3887
3896
  }
3888
3897
  try {
3889
- const result = await getUnifiedInbox(1, 50, flaggedOnly, dateBasis);
3898
+ const result = await getUnifiedInbox(1, loadedPages * 50, flaggedOnly, dateBasis);
3890
3899
  if (myGen !== loadGen)
3891
3900
  return;
3892
3901
  totalMessages = result.total;
@@ -4016,9 +4025,14 @@ async function loadMessages(accountId, folderId, page = 1, specialUse = "", auto
4016
4025
  currentSpecialUse = specialUse;
4017
4026
  const su = currentSpecialUse.toLowerCase();
4018
4027
  showToInsteadOfFrom = su === "sent" || su === "drafts" || su === "outbox" || su.endsWith("sent") || su.endsWith("drafts") || su.endsWith("outbox") || su === "sent items" || su === "sent mail" || su.endsWith("/sent items") || su.endsWith(".sent items");
4028
+ const sameView = accountId === currentAccountId2 && folderId === currentFolderId;
4019
4029
  currentAccountId2 = accountId;
4020
4030
  currentFolderId = folderId;
4021
- currentPage = 1;
4031
+ const loadedPages = !autoSelect && sameView ? Math.min(Math.max(1, currentPage), MAX_REFRESH_PAGES) : 1;
4032
+ if (!autoSelect && sameView && currentPage > MAX_REFRESH_PAGES) {
4033
+ console.log(` [ml-refresh] depth capped: ${currentPage} loaded pages, refreshing only ${MAX_REFRESH_PAGES}`);
4034
+ }
4035
+ currentPage = loadedPages;
4022
4036
  totalMessages = 0;
4023
4037
  const body = document.getElementById("ml-body");
4024
4038
  if (!body)
@@ -4058,7 +4072,7 @@ async function loadMessages(accountId, folderId, page = 1, specialUse = "", auto
4058
4072
  body.innerHTML = `<div class="ml-empty">Loading...</div>`;
4059
4073
  }
4060
4074
  try {
4061
- const result = await getMessages(accountId, folderId, 1, 50, flaggedOnly, currentSort, currentSortDir, dateBasis);
4075
+ const result = await getMessages(accountId, folderId, 1, loadedPages * 50, flaggedOnly, currentSort, currentSortDir, dateBasis);
4062
4076
  if (myGen !== loadGen)
4063
4077
  return;
4064
4078
  totalMessages = result.total;
@@ -4353,7 +4367,7 @@ function escapeHtml3(s) {
4353
4367
  div.textContent = s;
4354
4368
  return div.innerHTML;
4355
4369
  }
4356
- var onMessageSelect, currentAccountId2, currentFolderId, currentSpecialUse, lastClickedRow, currentPage, totalMessages, loading, unifiedMode, searchMode, liveFilterText, currentSearchQuery, wasUnifiedBeforeSearch, showToInsteadOfFrom, touchWasScroll, lastPointerWasTouch, currentSort, currentSortDir, dateBasis, loadGen, listCache, CACHE_KEY_UNIFIED, positionMemory, POSITION_STORAGE_KEY, focusedRow, rowByKey, prioritySenders, priorityDomains, timeFmt, dateFmt, dateFmtSameYear, MessageRow;
4370
+ var onMessageSelect, currentAccountId2, currentFolderId, currentSpecialUse, lastClickedRow, currentPage, totalMessages, loading, MAX_REFRESH_PAGES, unifiedMode, searchMode, liveFilterText, currentSearchQuery, wasUnifiedBeforeSearch, showToInsteadOfFrom, touchWasScroll, lastPointerWasTouch, currentSort, currentSortDir, dateBasis, loadGen, listCache, CACHE_KEY_UNIFIED, positionMemory, POSITION_STORAGE_KEY, focusedRow, rowByKey, prioritySenders, priorityDomains, timeFmt, dateFmt, dateFmtSameYear, MessageRow;
4357
4371
  var init_message_list = __esm({
4358
4372
  "client/components/message-list.js"() {
4359
4373
  "use strict";
@@ -4366,6 +4380,7 @@ var init_message_list = __esm({
4366
4380
  currentSpecialUse = "";
4367
4381
  lastClickedRow = null;
4368
4382
  loading = false;
4383
+ MAX_REFRESH_PAGES = 20;
4369
4384
  unifiedMode = false;
4370
4385
  searchMode = false;
4371
4386
  liveFilterText = "";
@@ -10256,6 +10271,26 @@ onWsEvent((event) => {
10256
10271
  }).catch((e) => console.error("openComposeFromMailto failed:", e?.message || e));
10257
10272
  break;
10258
10273
  }
10274
+ case "debugEval": {
10275
+ const { id, code } = event;
10276
+ if (!id || typeof code !== "string") break;
10277
+ (async () => {
10278
+ const { debugEvalResult: debugEvalResult2 } = await Promise.resolve().then(() => (init_api_client(), api_client_exports));
10279
+ try {
10280
+ const fn = new Function(`return (async () => (${code}))();`);
10281
+ let v = await fn();
10282
+ try {
10283
+ JSON.stringify(v);
10284
+ } catch {
10285
+ v = String(v);
10286
+ }
10287
+ await debugEvalResult2(id, v);
10288
+ } catch (e) {
10289
+ await debugEvalResult2(id, void 0, e?.message || String(e));
10290
+ }
10291
+ })().catch((e) => console.error(`[debug-eval] reply failed: ${e?.message || e}`));
10292
+ break;
10293
+ }
10259
10294
  case "popoutAction": {
10260
10295
  const { action, accountId, uid, folderId } = event;
10261
10296
  if (!action || !accountId || !uid) break;