@bobfrankston/mailx 1.0.352 → 1.0.355
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, sendMessage as apiSendMessage } 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
|
|
@@ -1143,6 +1144,40 @@ if (ftFilterInput) {
|
|
|
1143
1144
|
}
|
|
1144
1145
|
// ── Open links from email body in system browser ──
|
|
1145
1146
|
window.addEventListener("message", (e) => {
|
|
1147
|
+
// Relay traces from iframes (compose) to Node via our working bridge.
|
|
1148
|
+
// The iframe calls logClientEvent which tries its own bridge first; if
|
|
1149
|
+
// that path is broken it also posts here as backup. Tag gets a `via-relay`
|
|
1150
|
+
// suffix when the iframe couldn't reach its own bridge — that alone
|
|
1151
|
+
// diagnoses whether the iframe bridge works.
|
|
1152
|
+
if (e.data?.type === "mailx-trace" && typeof e.data.tag === "string") {
|
|
1153
|
+
const relayTag = e.data.bridged ? e.data.tag : `${e.data.tag} (via-relay)`;
|
|
1154
|
+
logClientEvent(relayTag, e.data.data);
|
|
1155
|
+
return;
|
|
1156
|
+
}
|
|
1157
|
+
// Compose-send relay: iframe posts the send request here because its own
|
|
1158
|
+
// bridge call to sendMessage was failing to reach Node. This window's
|
|
1159
|
+
// bridge is proven (getAccounts / getOutboxStatus run every few seconds
|
|
1160
|
+
// with no failures), so we do the IPC from here and post the result back
|
|
1161
|
+
// to the iframe via its source. `e.source` is the iframe's window; use it
|
|
1162
|
+
// so targeting works even if the iframe moves in the DOM.
|
|
1163
|
+
if (e.data?.type === "mailx-compose-send" && e.data.id && e.data.body) {
|
|
1164
|
+
const src = e.source;
|
|
1165
|
+
const id = e.data.id;
|
|
1166
|
+
logClientEvent("relay-compose-send-received", { id });
|
|
1167
|
+
(async () => {
|
|
1168
|
+
try {
|
|
1169
|
+
await apiSendMessage(e.data.body);
|
|
1170
|
+
logClientEvent("relay-compose-send-ok", { id });
|
|
1171
|
+
src?.postMessage({ type: "mailx-compose-send-result", id, ok: true }, "*");
|
|
1172
|
+
}
|
|
1173
|
+
catch (err) {
|
|
1174
|
+
const msg = err?.message || String(err);
|
|
1175
|
+
logClientEvent("relay-compose-send-error", { id, error: msg });
|
|
1176
|
+
src?.postMessage({ type: "mailx-compose-send-result", id, ok: false, error: msg }, "*");
|
|
1177
|
+
}
|
|
1178
|
+
})();
|
|
1179
|
+
return;
|
|
1180
|
+
}
|
|
1146
1181
|
if (e.data?.type === "openLink" && e.data.url) {
|
|
1147
1182
|
window.open(e.data.url, "_blank", "noopener,noreferrer");
|
|
1148
1183
|
}
|
|
@@ -75,6 +75,14 @@ body {
|
|
|
75
75
|
padding: var(--gap-xs) 0;
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
/* HTML `hidden` attribute is display:none by UA default, but the `display:
|
|
79
|
+
flex` above wins specificity-wise and the row stays visible. Restore the
|
|
80
|
+
expected hide behavior — the Cc/Bcc toggle buttons flip `hidden` to
|
|
81
|
+
reveal/conceal the rows, which is pointless if CSS forces them on. */
|
|
82
|
+
.compose-field[hidden] {
|
|
83
|
+
display: none;
|
|
84
|
+
}
|
|
85
|
+
|
|
78
86
|
.compose-field label {
|
|
79
87
|
flex: 0 0 60px;
|
|
80
88
|
font-size: var(--font-size-sm);
|
|
@@ -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,
|
|
7
|
+
import { getSettings, getAccounts, searchContacts, 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
|
}
|
|
@@ -610,25 +621,49 @@ document.getElementById("btn-send")?.addEventListener("click", () => {
|
|
|
610
621
|
if (statusEl)
|
|
611
622
|
statusEl.textContent = "";
|
|
612
623
|
const sendStart = Date.now();
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
624
|
+
logClientEvent("compose-send-pre-ipc");
|
|
625
|
+
// Parent-window relay for send. Empirical observation: Android (direct
|
|
626
|
+
// in-process SMTP) sends reliably; desktop (iframe → parent.mailxapi →
|
|
627
|
+
// msger → Node → service) has sendMessage IPCs failing to reach Node
|
|
628
|
+
// for reasons still unknown (iframe bridge behaves differently from the
|
|
629
|
+
// top frame in msger's WebView2 for this specific call). Meanwhile the
|
|
630
|
+
// parent window's bridge is proven — getAccounts / getOutboxStatus run
|
|
631
|
+
// through it every few seconds with no failures.
|
|
632
|
+
//
|
|
633
|
+
// Fix: the iframe doesn't touch the bridge at all for send. It posts
|
|
634
|
+
// a request to the parent, and the parent calls the real sendMessage
|
|
635
|
+
// from its own frame. Parent posts the result back. This bypasses
|
|
636
|
+
// whatever is wrong with iframe-scoped IPC.
|
|
637
|
+
const ipcPromise = new Promise((resolve, reject) => {
|
|
638
|
+
const reqId = `send-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
639
|
+
const timer = setTimeout(() => {
|
|
640
|
+
window.removeEventListener("message", onMsg);
|
|
641
|
+
reject(new Error("parent-relay send timeout (120s)"));
|
|
642
|
+
}, 120000);
|
|
643
|
+
const onMsg = (ev) => {
|
|
644
|
+
if (!ev.data || ev.data.type !== "mailx-compose-send-result" || ev.data.id !== reqId)
|
|
645
|
+
return;
|
|
646
|
+
clearTimeout(timer);
|
|
647
|
+
window.removeEventListener("message", onMsg);
|
|
648
|
+
if (ev.data.ok)
|
|
649
|
+
resolve();
|
|
650
|
+
else
|
|
651
|
+
reject(new Error(ev.data.error || "unknown"));
|
|
652
|
+
};
|
|
653
|
+
window.addEventListener("message", onMsg);
|
|
654
|
+
try {
|
|
655
|
+
parent.postMessage({ type: "mailx-compose-send", id: reqId, body }, "*");
|
|
656
|
+
logClientEvent("compose-send-ipc-invoked", { via: "parent-relay", reqId });
|
|
623
657
|
}
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
}
|
|
658
|
+
catch (e) {
|
|
659
|
+
clearTimeout(timer);
|
|
660
|
+
window.removeEventListener("message", onMsg);
|
|
661
|
+
reject(e);
|
|
662
|
+
}
|
|
663
|
+
});
|
|
630
664
|
Promise.resolve(ipcPromise)
|
|
631
665
|
.then(() => {
|
|
666
|
+
logClientEvent("compose-send-ipc-resolved", { ms: Date.now() - sendStart });
|
|
632
667
|
console.log(`[compose] Send IPC returned OK in ${Date.now() - sendStart}ms`);
|
|
633
668
|
// Stop autosave only after ACK — if send threw we want the draft
|
|
634
669
|
// autosave to keep the message safe.
|
|
@@ -643,6 +678,7 @@ document.getElementById("btn-send")?.addEventListener("click", () => {
|
|
|
643
678
|
})
|
|
644
679
|
.catch((e) => {
|
|
645
680
|
const msg = e?.message || String(e);
|
|
681
|
+
logClientEvent("compose-send-ipc-rejected", { error: msg, ms: Date.now() - sendStart });
|
|
646
682
|
console.error(`[compose] Send IPC failed after ${Date.now() - sendStart}ms: ${msg}`);
|
|
647
683
|
if (sendBtn) {
|
|
648
684
|
sendBtn.disabled = false;
|
package/client/lib/api-client.js
CHANGED
|
@@ -129,6 +129,34 @@ 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. Tries two
|
|
134
|
+
* paths so a broken primary channel can't swallow the trace:
|
|
135
|
+
* 1. Direct bridge call (self / opener / parent mailxapi).
|
|
136
|
+
* 2. parent.postMessage fallback — the main window listens and relays.
|
|
137
|
+
* The fallback matters because the whole point of tracing is to diagnose
|
|
138
|
+
* a broken iframe bridge; a single-path tracer that goes through that same
|
|
139
|
+
* bridge is useless in exactly the case we need it. */
|
|
140
|
+
export function logClientEvent(tag, data) {
|
|
141
|
+
let delivered = false;
|
|
142
|
+
try {
|
|
143
|
+
const bridge = typeof globalThis.mailxapi !== "undefined" && globalThis.mailxapi?.isApp ? globalThis.mailxapi
|
|
144
|
+
: window.opener?.mailxapi?.isApp ? window.opener.mailxapi
|
|
145
|
+
: window.parent?.mailxapi?.isApp ? window.parent.mailxapi
|
|
146
|
+
: null;
|
|
147
|
+
if (bridge?.logClientEvent) {
|
|
148
|
+
bridge.logClientEvent(tag, data);
|
|
149
|
+
delivered = true;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
catch { /* never throw from tracing */ }
|
|
153
|
+
try {
|
|
154
|
+
if (window.parent && window.parent !== window) {
|
|
155
|
+
window.parent.postMessage({ type: "mailx-trace", tag, data, bridged: delivered }, "*");
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
catch { /* */ }
|
|
159
|
+
}
|
|
132
160
|
export function sendMessage(body) {
|
|
133
161
|
return ipc().sendMessage?.(body);
|
|
134
162
|
}
|
package/client/lib/mailxapi.js
CHANGED
|
@@ -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
|
@@ -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();
|