@bobfrankston/mailx 1.0.351 → 1.0.353

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/client/app.js CHANGED
@@ -5,7 +5,7 @@
5
5
  import { initFolderTree, refreshFolderTree, updateFolderCounts, setFolderSynced, getFolderSynced } from "./components/folder-tree.js";
6
6
  import { initMessageList, loadMessages, loadUnifiedInbox, loadSearchResults, reloadCurrentFolder, getSelectedMessages, markBodiesCached } from "./components/message-list.js";
7
7
  import { showMessage, getCurrentMessage, initViewer } from "./components/message-viewer.js";
8
- import { connectWebSocket, onWsEvent, triggerSync, syncAccount, reauthenticate, getAccounts, getFolders, deleteMessages, undeleteMessage, restartServer, getSyncPending, getVersion, getSettings, saveSettings, getAutocompleteSettings, saveAutocompleteSettings, repairAccounts, updateFlags, markAsSpamMessages } from "./lib/api-client.js";
8
+ import { connectWebSocket, onWsEvent, triggerSync, syncAccount, reauthenticate, getAccounts, getFolders, deleteMessages, undeleteMessage, restartServer, getSyncPending, getVersion, getSettings, saveSettings, getAutocompleteSettings, saveAutocompleteSettings, repairAccounts, updateFlags, markAsSpamMessages, logClientEvent } from "./lib/api-client.js";
9
9
  import * as messageState from "./lib/message-state.js";
10
10
  // ── New message badge (favicon + title) ──
11
11
  let baseTitle = "mailx";
@@ -524,6 +524,7 @@ document.getElementById("btn-factory-reset")?.addEventListener("click", async ()
524
524
  }
525
525
  });
526
526
  async function openCompose(mode) {
527
+ logClientEvent("openCompose-entry", { mode });
527
528
  const current = getCurrentMessage();
528
529
  // Local-first: if the row is selected we already have its headers in the
529
530
  // local DB. Populate the compose form unconditionally; the user can edit
@@ -4,9 +4,13 @@
4
4
  * Receives init data via window.opener.postMessage or URL params.
5
5
  */
6
6
  import { createEditor } from "./editor.js";
7
- import { getSettings, getAccounts, searchContacts, sendMessage, saveDraft as apiSaveDraft, deleteDraft } from "../lib/api-client.js";
7
+ import { getSettings, getAccounts, searchContacts, sendMessage, saveDraft as apiSaveDraft, deleteDraft, logClientEvent } from "../lib/api-client.js";
8
+ // Very first line the iframe runs — if this doesn't reach Node, the iframe
9
+ // itself isn't loading or the bridge is completely broken.
10
+ logClientEvent("compose-module-loaded", { href: location.href, version: window.mailxVersion || "?" });
8
11
  /** Close compose window */
9
12
  function closeCompose() {
13
+ logClientEvent("compose-close");
10
14
  window.close();
11
15
  }
12
16
  // ── Load editor scripts dynamically ──
@@ -576,6 +580,11 @@ window.addEventListener("blur", () => {
576
580
  catch { /* */ }
577
581
  });
578
582
  document.getElementById("btn-send")?.addEventListener("click", () => {
583
+ // Loud tracing through the whole send pipeline. Every step ships a
584
+ // `[client] compose-send-*` event to the Node log so a "vanished message"
585
+ // report can be traced end-to-end without devtools. If the log stops
586
+ // at any step, that's where the pipeline broke.
587
+ logClientEvent("compose-send-click");
579
588
  const body = {
580
589
  from: getFromAccountId(),
581
590
  fromAddress: getFromAddress(),
@@ -587,10 +596,12 @@ document.getElementById("btn-send")?.addEventListener("click", () => {
587
596
  bodyText: editor.getText(),
588
597
  attachments: attachments.map(a => ({ filename: a.filename, mimeType: a.mimeType, dataBase64: a.dataBase64 })),
589
598
  };
599
+ logClientEvent("compose-send-body-built", { from: body.from, toCount: body.to.length, subjectLen: (body.subject || "").length, bodyHtmlLen: (body.bodyHtml || "").length, atts: body.attachments.length });
590
600
  // Local validity (one missing-To check) — must run before close so the
591
601
  // user gets an inline error instead of silent loss. Anything else (real
592
602
  // address validation, MIME assembly, disk write) happens server-side.
593
603
  if (!body.to.length) {
604
+ logClientEvent("compose-send-rejected-no-to");
594
605
  alert("Please add at least one To recipient.");
595
606
  return;
596
607
  }
@@ -611,11 +622,14 @@ document.getElementById("btn-send")?.addEventListener("click", () => {
611
622
  statusEl.textContent = "";
612
623
  const sendStart = Date.now();
613
624
  let ipcPromise;
625
+ logClientEvent("compose-send-pre-ipc");
614
626
  try {
615
627
  ipcPromise = sendMessage(body);
628
+ logClientEvent("compose-send-ipc-invoked", { promiseType: typeof ipcPromise, isThenable: !!(ipcPromise && typeof ipcPromise.then === "function") });
616
629
  }
617
630
  catch (e) {
618
631
  const msg = e?.message || String(e);
632
+ logClientEvent("compose-send-sync-throw", { error: msg });
619
633
  console.error(`[compose] Send threw synchronously: ${msg}`);
620
634
  if (sendBtn) {
621
635
  sendBtn.disabled = false;
@@ -629,6 +643,7 @@ document.getElementById("btn-send")?.addEventListener("click", () => {
629
643
  }
630
644
  Promise.resolve(ipcPromise)
631
645
  .then(() => {
646
+ logClientEvent("compose-send-ipc-resolved", { ms: Date.now() - sendStart });
632
647
  console.log(`[compose] Send IPC returned OK in ${Date.now() - sendStart}ms`);
633
648
  // Stop autosave only after ACK — if send threw we want the draft
634
649
  // autosave to keep the message safe.
@@ -643,6 +658,7 @@ document.getElementById("btn-send")?.addEventListener("click", () => {
643
658
  })
644
659
  .catch((e) => {
645
660
  const msg = e?.message || String(e);
661
+ logClientEvent("compose-send-ipc-rejected", { error: msg, ms: Date.now() - sendStart });
646
662
  console.error(`[compose] Send IPC failed after ${Date.now() - sendStart}ms: ${msg}`);
647
663
  if (sendBtn) {
648
664
  sendBtn.disabled = false;
@@ -129,6 +129,20 @@ export function deleteFolder(accountId, folderId) {
129
129
  export function emptyFolder(accountId, folderId) {
130
130
  return ipc().emptyFolder?.(accountId, folderId);
131
131
  }
132
+ /** Ship a named event to the Node log as `[client] <tag> <data>`. Fire and
133
+ * forget — never awaits, never throws, never blocks the caller. If the IPC
134
+ * bridge is unavailable, the call is a no-op so tracing calls scattered
135
+ * through the UI don't become failure points themselves. */
136
+ export function logClientEvent(tag, data) {
137
+ try {
138
+ const bridge = typeof globalThis.mailxapi !== "undefined" && globalThis.mailxapi?.isApp ? globalThis.mailxapi
139
+ : window.opener?.mailxapi?.isApp ? window.opener.mailxapi
140
+ : window.parent?.mailxapi?.isApp ? window.parent.mailxapi
141
+ : null;
142
+ bridge?.logClientEvent?.(tag, data);
143
+ }
144
+ catch { /* never throw from tracing */ }
145
+ }
132
146
  export function sendMessage(body) {
133
147
  return ipc().sendMessage?.(body);
134
148
  }
@@ -86,6 +86,12 @@
86
86
  return callNode("undeleteMessage", { accountId: accountId, uid: uid, folderId: folderId });
87
87
  },
88
88
 
89
+ // Diagnostic tracing — callers (compose iframe, app, components) can
90
+ // ship arbitrary named events to the Node log. Surfaces as
91
+ // `[client] <tag> <data>` so a failing pipeline can be traced end
92
+ // to end in a single log file.
93
+ logClientEvent: function(tag, data) { return callNode("logClientEvent", { tag: tag, data: data }); },
94
+
89
95
  // Compose
90
96
  sendMessage: function(msg) { return callNode("sendMessage", msg); },
91
97
  saveDraft: function(params) { return callNode("saveDraft", params); },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/mailx",
3
- "version": "1.0.351",
3
+ "version": "1.0.353",
4
4
  "description": "Local-first email client with IMAP sync and standalone native app",
5
5
  "type": "module",
6
6
  "main": "bin/mailx.js",
@@ -20,7 +20,7 @@
20
20
  "postinstall": "node bin/postinstall.js"
21
21
  },
22
22
  "dependencies": {
23
- "@bobfrankston/iflow-direct": "^0.1.25",
23
+ "@bobfrankston/iflow-direct": "^0.1.26",
24
24
  "@bobfrankston/iflow-node": "^0.1.7",
25
25
  "@bobfrankston/miscinfo": "^1.0.9",
26
26
  "@bobfrankston/oauthsupport": "^1.0.24",
@@ -84,7 +84,7 @@
84
84
  },
85
85
  ".transformedSnapshot": {
86
86
  "dependencies": {
87
- "@bobfrankston/iflow-direct": "^0.1.25",
87
+ "@bobfrankston/iflow-direct": "^0.1.26",
88
88
  "@bobfrankston/iflow-node": "^0.1.7",
89
89
  "@bobfrankston/miscinfo": "^1.0.9",
90
90
  "@bobfrankston/oauthsupport": "^1.0.24",
@@ -128,6 +128,14 @@ async function dispatchAction(svc, action, p) {
128
128
  return { content: await svc.readConfigHelp(p.name) };
129
129
  case "unsubscribeOneClick":
130
130
  return await svc.unsubscribeOneClick(p.url);
131
+ // Client-side tracing — lets webview / iframe code ship events to the
132
+ // Node log so a "compose→send→vanished" report can be diagnosed without
133
+ // opening devtools. Every call shows up as `[client] <tag> <data>` in
134
+ // the main log. Keep it on the top of the switch so it's cheap + first
135
+ // to dispatch.
136
+ case "logClientEvent":
137
+ console.log(` [client] ${p.tag || "?"}${p.data ? " " + JSON.stringify(p.data).slice(0, 400) : ""}`);
138
+ return { ok: true };
131
139
  // Settings
132
140
  case "getSettings":
133
141
  return svc.getSettings();