@bobfrankston/mailx 1.0.91 → 1.0.93
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 +26 -176
- package/client/lib/api-client.js +153 -230
- package/client/package.json +1 -1
- package/package.json +1 -1
package/client/app.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { initFolderTree, refreshFolderTree } from "./components/folder-tree.js";
|
|
6
6
|
import { initMessageList, loadMessages, loadUnifiedInbox, loadSearchResults, reloadCurrentFolder, getSelectedMessages } from "./components/message-list.js";
|
|
7
7
|
import { showMessage, getCurrentMessage } from "./components/message-viewer.js";
|
|
8
|
-
import { connectWebSocket, onWsEvent, triggerSync, getAccounts, getFolders, deleteMessage, undeleteMessage, restartServer, rebuildServer
|
|
8
|
+
import { connectWebSocket, onWsEvent, triggerSync, getAccounts, getFolders, deleteMessage, undeleteMessage, restartServer, rebuildServer } from "./lib/api-client.js";
|
|
9
9
|
// ── New message badge (favicon + title) ──
|
|
10
10
|
let baseTitle = "mailx";
|
|
11
11
|
let lastSeenCount = 0;
|
|
@@ -781,186 +781,37 @@ optEditorTiptap?.addEventListener("change", () => {
|
|
|
781
781
|
saveEditorSetting("tiptap");
|
|
782
782
|
});
|
|
783
783
|
const isApp = typeof mailxapi !== "undefined" && mailxapi?.isApp;
|
|
784
|
-
|
|
785
|
-
const
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
784
|
+
fetch("/api/version").then(r => r.json()).then(d => {
|
|
785
|
+
const el = document.getElementById("app-version");
|
|
786
|
+
const storage = d.storage || {};
|
|
787
|
+
const storageLabel = storage.provider && storage.provider !== "local" ? ` [${storage.provider}]` : "";
|
|
788
|
+
if (el)
|
|
789
|
+
el.textContent = `mailx v${d.version}${storageLabel}${isApp ? "" : " [browser]"}`;
|
|
790
|
+
}).catch(async () => {
|
|
791
|
+
// Server not running — try to start it if we're in the app
|
|
792
|
+
const startupStatus = document.getElementById("startup-status");
|
|
793
|
+
if (isApp) {
|
|
794
|
+
if (startupStatus)
|
|
795
|
+
startupStatus.textContent = "Starting server...";
|
|
796
|
+
await mailxapi.ensureServer();
|
|
797
|
+
location.reload();
|
|
798
|
+
}
|
|
799
|
+
else {
|
|
792
800
|
const el = document.getElementById("app-version");
|
|
793
|
-
const storage = d.storage || { provider: "local", mode: "local" };
|
|
794
|
-
const storageLabel = storage.provider === "local" ? "" : ` · ${storage.provider}${storage.mode === "api" ? " (API)" : ""}`;
|
|
795
801
|
if (el)
|
|
796
|
-
el.textContent =
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
if (overlay)
|
|
800
|
-
overlay.hidden = true;
|
|
802
|
+
el.textContent = "mailx [server offline]";
|
|
803
|
+
if (startupStatus)
|
|
804
|
+
startupStatus.textContent = "Server offline — start with: node packages/mailx-server/index.js";
|
|
801
805
|
}
|
|
802
|
-
|
|
803
|
-
const startupStatus = document.getElementById("startup-status");
|
|
804
|
-
if (isApp && mailxapi?.ensureServer) {
|
|
805
|
-
// Desktop native app — start the embedded server
|
|
806
|
-
if (startupStatus)
|
|
807
|
-
startupStatus.textContent = "Starting server...";
|
|
808
|
-
await mailxapi.ensureServer();
|
|
809
|
-
location.reload();
|
|
810
|
-
}
|
|
811
|
-
else if (isApp) {
|
|
812
|
-
// MAUI Android app — runs via bridge, no HTTP server
|
|
813
|
-
const overlay = document.getElementById("startup-overlay");
|
|
814
|
-
const content = overlay?.querySelector(".startup-content");
|
|
815
|
-
if (content) {
|
|
816
|
-
content.innerHTML = `
|
|
817
|
-
<div style="text-align:center;max-width:400px">
|
|
818
|
-
<h2 style="margin-bottom:1rem;color:var(--color-text)">mailx</h2>
|
|
819
|
-
<p style="color:var(--color-text-muted);font-size:0.9rem" id="bridge-status">
|
|
820
|
-
Initializing...
|
|
821
|
-
</p>
|
|
822
|
-
</div>`;
|
|
823
|
-
}
|
|
824
|
-
if (overlay)
|
|
825
|
-
overlay.hidden = false;
|
|
826
|
-
// Initialize local service
|
|
827
|
-
try {
|
|
828
|
-
const localService = await import("./lib/local-service.js");
|
|
829
|
-
const statusEl = document.getElementById("bridge-status");
|
|
830
|
-
// Check for saved settings
|
|
831
|
-
let savedSettings = localService.getSettings();
|
|
832
|
-
if (!savedSettings || !savedSettings.accounts?.length) {
|
|
833
|
-
// No settings — prompt for account
|
|
834
|
-
if (statusEl)
|
|
835
|
-
statusEl.textContent = "No accounts configured";
|
|
836
|
-
if (content) {
|
|
837
|
-
content.innerHTML = `
|
|
838
|
-
<div style="text-align:center;max-width:400px">
|
|
839
|
-
<h2 style="margin-bottom:1rem;color:var(--color-text)">mailx — Setup</h2>
|
|
840
|
-
<p style="color:var(--color-text-muted);font-size:0.9rem;margin-bottom:1rem">
|
|
841
|
-
Enter your email account to get started.
|
|
842
|
-
</p>
|
|
843
|
-
<input type="email" id="setup-email" placeholder="you@example.com" style="width:100%;padding:0.5rem;border:1px solid var(--color-border);border-radius:4px;background:var(--color-bg);color:var(--color-text);font-size:1rem;margin-bottom:0.5rem">
|
|
844
|
-
<input type="text" id="setup-imap" placeholder="IMAP server (e.g. imap.example.com)" style="width:100%;padding:0.5rem;border:1px solid var(--color-border);border-radius:4px;background:var(--color-bg);color:var(--color-text);font-size:1rem;margin-bottom:0.5rem">
|
|
845
|
-
<input type="password" id="setup-pass" placeholder="Password" style="width:100%;padding:0.5rem;border:1px solid var(--color-border);border-radius:4px;background:var(--color-bg);color:var(--color-text);font-size:1rem;margin-bottom:0.75rem">
|
|
846
|
-
<button id="setup-go" style="padding:0.5rem 1.5rem;background:var(--color-accent);color:#fff;border:none;border-radius:4px;font-size:1rem;cursor:pointer">Connect</button>
|
|
847
|
-
<p id="setup-error" style="margin-top:0.75rem;color:oklch(0.65 0.2 25);font-size:0.85rem" hidden></p>
|
|
848
|
-
</div>`;
|
|
849
|
-
}
|
|
850
|
-
document.getElementById("setup-go")?.addEventListener("click", async () => {
|
|
851
|
-
const email = document.getElementById("setup-email").value.trim();
|
|
852
|
-
const imap = document.getElementById("setup-imap").value.trim();
|
|
853
|
-
const pass = document.getElementById("setup-pass").value;
|
|
854
|
-
const errEl = document.getElementById("setup-error");
|
|
855
|
-
if (!email || !pass) {
|
|
856
|
-
errEl.textContent = "Email and password required";
|
|
857
|
-
errEl.hidden = false;
|
|
858
|
-
return;
|
|
859
|
-
}
|
|
860
|
-
const domain = email.split("@")[1] || "";
|
|
861
|
-
const host = imap || `imap.${domain}`;
|
|
862
|
-
const id = domain.replace(/\./g, "");
|
|
863
|
-
const newSettings = {
|
|
864
|
-
accounts: [{
|
|
865
|
-
id, name: email.split("@")[0], email,
|
|
866
|
-
imap: { host, port: 993, user: email, password: pass, auth: "password" },
|
|
867
|
-
smtp: { host: `smtp.${domain}`, port: 587, user: email, password: pass, auth: "password" },
|
|
868
|
-
enabled: true,
|
|
869
|
-
}],
|
|
870
|
-
ui: { theme: "system" },
|
|
871
|
-
sync: { intervalMinutes: 5, historyDays: 100 },
|
|
872
|
-
};
|
|
873
|
-
errEl.hidden = true;
|
|
874
|
-
document.getElementById("setup-go").textContent = "Connecting...";
|
|
875
|
-
await localService.initialize(newSettings);
|
|
876
|
-
if (overlay)
|
|
877
|
-
overlay.hidden = true;
|
|
878
|
-
});
|
|
879
|
-
}
|
|
880
|
-
else {
|
|
881
|
-
// Settings exist — initialize and sync
|
|
882
|
-
if (statusEl)
|
|
883
|
-
statusEl.textContent = "Loading accounts...";
|
|
884
|
-
await localService.initialize();
|
|
885
|
-
if (overlay)
|
|
886
|
-
overlay.hidden = true;
|
|
887
|
-
}
|
|
888
|
-
}
|
|
889
|
-
catch (e) {
|
|
890
|
-
const statusEl = document.getElementById("bridge-status");
|
|
891
|
-
if (statusEl)
|
|
892
|
-
statusEl.textContent = `Error: ${e.message}`;
|
|
893
|
-
}
|
|
894
|
-
}
|
|
895
|
-
else {
|
|
896
|
-
// Browser mode — prompt for server URL
|
|
897
|
-
promptForServer();
|
|
898
|
-
}
|
|
899
|
-
}
|
|
900
|
-
}
|
|
901
|
-
function promptForServer() {
|
|
902
|
-
const overlay = document.getElementById("startup-overlay");
|
|
903
|
-
const content = overlay?.querySelector(".startup-content");
|
|
904
|
-
if (!content)
|
|
905
|
-
return;
|
|
906
|
-
const current = getServerUrl();
|
|
907
|
-
content.innerHTML = `
|
|
908
|
-
<div style="text-align:center;max-width:400px">
|
|
909
|
-
<h2 style="margin-bottom:1rem;color:var(--color-text)">Connect to mailx server</h2>
|
|
910
|
-
<p style="margin-bottom:1rem;color:var(--color-text-muted);font-size:0.9rem">
|
|
911
|
-
Enter the address of your mailx server (running on your desktop or network).
|
|
912
|
-
</p>
|
|
913
|
-
<input type="text" id="server-url-input" placeholder="http://192.168.1.x:9333"
|
|
914
|
-
value="${current}" autocomplete="off"
|
|
915
|
-
style="width:100%;padding:0.5rem;border:1px solid var(--color-border);border-radius:var(--radius-sm);
|
|
916
|
-
background:var(--color-bg);color:var(--color-text);font-size:1rem;margin-bottom:0.75rem">
|
|
917
|
-
<button id="server-url-btn" style="padding:0.5rem 1.5rem;background:var(--color-accent);color:#fff;
|
|
918
|
-
border:none;border-radius:var(--radius-sm);font-size:1rem;cursor:pointer">Connect</button>
|
|
919
|
-
<p id="server-url-error" style="margin-top:0.75rem;color:oklch(0.65 0.2 25);font-size:0.85rem" hidden></p>
|
|
920
|
-
</div>
|
|
921
|
-
`;
|
|
922
|
-
if (overlay)
|
|
923
|
-
overlay.hidden = false;
|
|
924
|
-
const input = document.getElementById("server-url-input");
|
|
925
|
-
const btn = document.getElementById("server-url-btn");
|
|
926
|
-
const err = document.getElementById("server-url-error");
|
|
927
|
-
async function tryConnect() {
|
|
928
|
-
const url = input.value.trim().replace(/\/$/, "");
|
|
929
|
-
if (!url) {
|
|
930
|
-
err.textContent = "Enter a server URL";
|
|
931
|
-
err.hidden = false;
|
|
932
|
-
return;
|
|
933
|
-
}
|
|
934
|
-
btn.disabled = true;
|
|
935
|
-
btn.textContent = "Connecting...";
|
|
936
|
-
err.hidden = true;
|
|
937
|
-
try {
|
|
938
|
-
const res = await fetch(`${url}/api/version`, { signal: AbortSignal.timeout(5000) });
|
|
939
|
-
const ct = res.headers.get("content-type") || "";
|
|
940
|
-
if (ct.includes("text/html") || !res.ok)
|
|
941
|
-
throw new Error("Server returned an error page");
|
|
942
|
-
await res.json();
|
|
943
|
-
setServerUrl(url);
|
|
944
|
-
location.reload();
|
|
945
|
-
}
|
|
946
|
-
catch (e) {
|
|
947
|
-
btn.disabled = false;
|
|
948
|
-
btn.textContent = "Connect";
|
|
949
|
-
err.textContent = e.message || "Cannot reach server";
|
|
950
|
-
err.hidden = false;
|
|
951
|
-
}
|
|
952
|
-
}
|
|
953
|
-
btn.addEventListener("click", tryConnect);
|
|
954
|
-
input.addEventListener("keydown", (e) => { if (e.key === "Enter")
|
|
955
|
-
tryConnect(); });
|
|
956
|
-
input.focus();
|
|
957
|
-
}
|
|
958
|
-
checkServer();
|
|
806
|
+
});
|
|
959
807
|
// ── Sync pending indicator + server health check ──
|
|
960
808
|
let serverDown = false;
|
|
961
809
|
setInterval(async () => {
|
|
962
810
|
try {
|
|
963
|
-
const
|
|
811
|
+
const res = await fetch("/api/sync/pending");
|
|
812
|
+
if (!res.ok)
|
|
813
|
+
return;
|
|
814
|
+
const data = await res.json();
|
|
964
815
|
const el = document.getElementById("status-pending");
|
|
965
816
|
if (el) {
|
|
966
817
|
el.textContent = data.pending > 0 ? `↻ ${data.pending} pending` : "";
|
|
@@ -1000,8 +851,7 @@ function scheduleMiddnightRefresh() {
|
|
|
1000
851
|
}
|
|
1001
852
|
scheduleMiddnightRefresh();
|
|
1002
853
|
// ── Apply theme from settings ──
|
|
1003
|
-
|
|
1004
|
-
fetch(`${svrBase}/api/version`).then(r => r.json()).then(d => {
|
|
854
|
+
fetch("/api/version").then(r => r.json()).then(d => {
|
|
1005
855
|
if (d.theme === "dark")
|
|
1006
856
|
document.documentElement.classList.add("theme-dark");
|
|
1007
857
|
else if (d.theme === "light")
|
package/client/lib/api-client.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* API client —
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* - IPC: injected by native launcher (mailxapi global)
|
|
6
|
-
* - BridgeTransport: Android (capacitor-nodejs bridge) — future
|
|
2
|
+
* API client — auto-detects IPC (WebView) vs HTTP (browser).
|
|
3
|
+
* When mailxapi is available (injected by launcher), calls go directly via IPC.
|
|
4
|
+
* Otherwise falls back to REST/WebSocket.
|
|
7
5
|
*
|
|
8
6
|
* All server operations MUST go through these centralized methods.
|
|
9
7
|
* Never use fetch("/api/...") directly in components.
|
|
10
8
|
*/
|
|
11
|
-
|
|
9
|
+
const hasIPC = typeof mailxapi !== "undefined" && mailxapi?.isApp;
|
|
10
|
+
// ── HTTP fallback ──
|
|
11
|
+
// Abort controller for message-list requests — cancel stale fetches when folder changes
|
|
12
12
|
let messageListAbort = null;
|
|
13
13
|
export function abortMessageListRequests() {
|
|
14
14
|
if (messageListAbort) {
|
|
@@ -21,283 +21,206 @@ function newMessageListSignal() {
|
|
|
21
21
|
messageListAbort = new AbortController();
|
|
22
22
|
return messageListAbort.signal;
|
|
23
23
|
}
|
|
24
|
-
|
|
25
|
-
let
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
31
|
-
/** Get the configured server URL */
|
|
32
|
-
export function getServerUrl() { return serverUrl; }
|
|
33
|
-
class HttpTransport {
|
|
34
|
-
handlers = [];
|
|
35
|
-
async call(method, params) {
|
|
36
|
-
const route = httpRoutes[method];
|
|
37
|
-
if (!route)
|
|
38
|
-
throw new Error(`Unknown method: ${method}`);
|
|
39
|
-
const { path, options } = route(params);
|
|
40
|
-
const base = serverUrl ? serverUrl : "";
|
|
41
|
-
let res;
|
|
42
|
-
try {
|
|
43
|
-
res = await fetch(`${base}/api${path}`, {
|
|
44
|
-
headers: { "Content-Type": "application/json" },
|
|
45
|
-
...options,
|
|
46
|
-
});
|
|
47
|
-
}
|
|
48
|
-
catch (e) {
|
|
49
|
-
if (e.name === "AbortError")
|
|
50
|
-
throw e;
|
|
51
|
-
throw new Error("Server offline — check connection or run: mailx -server");
|
|
52
|
-
}
|
|
53
|
-
// Detect HTML error responses (server not reachable, returns error page)
|
|
54
|
-
const ct = res.headers.get("content-type") || "";
|
|
55
|
-
if (ct.includes("text/html")) {
|
|
56
|
-
throw new Error("Server returned HTML instead of JSON — check server URL");
|
|
57
|
-
}
|
|
58
|
-
if (!res.ok) {
|
|
59
|
-
const err = await res.json().catch(() => ({ error: res.statusText }));
|
|
60
|
-
throw new Error(err.error || res.statusText);
|
|
61
|
-
}
|
|
62
|
-
return res.json();
|
|
63
|
-
}
|
|
64
|
-
onEvent(handler) {
|
|
65
|
-
this.handlers.push(handler);
|
|
66
|
-
}
|
|
67
|
-
connect() {
|
|
68
|
-
let wsUrl;
|
|
69
|
-
if (serverUrl) {
|
|
70
|
-
// Remote server — derive WS URL from HTTP URL
|
|
71
|
-
const u = new URL(serverUrl);
|
|
72
|
-
wsUrl = `${u.protocol === "https:" ? "wss:" : "ws:"}//${u.host}`;
|
|
73
|
-
}
|
|
74
|
-
else {
|
|
75
|
-
wsUrl = `${location.protocol === "https:" ? "wss:" : "ws:"}//${location.host}`;
|
|
76
|
-
}
|
|
77
|
-
const ws = new WebSocket(wsUrl);
|
|
78
|
-
ws.onmessage = (ev) => {
|
|
79
|
-
try {
|
|
80
|
-
const event = JSON.parse(ev.data);
|
|
81
|
-
for (const h of this.handlers)
|
|
82
|
-
h(event);
|
|
83
|
-
}
|
|
84
|
-
catch { /* ignore */ }
|
|
85
|
-
};
|
|
86
|
-
ws.onclose = () => { setTimeout(() => this.connect(), 3000); };
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
class IpcTransport {
|
|
90
|
-
async call(method, params) {
|
|
91
|
-
// IPC methods are direct function calls on the injected mailxapi object
|
|
92
|
-
const p = params || {};
|
|
93
|
-
switch (method) {
|
|
94
|
-
case "getAccounts": return mailxapi.getAccounts();
|
|
95
|
-
case "getFolders": return mailxapi.getFolders(p.accountId);
|
|
96
|
-
case "getMessages": return mailxapi.getMessages(p.accountId, p.folderId, p.page, p.pageSize);
|
|
97
|
-
case "getUnifiedInbox": return mailxapi.getUnifiedInbox(p.page, p.pageSize);
|
|
98
|
-
case "searchMessages": return mailxapi.searchMessages(p.query, p.page, p.pageSize);
|
|
99
|
-
case "getMessage": return mailxapi.getMessage(p.accountId, p.uid, p.allowRemote, p.folderId);
|
|
100
|
-
case "updateFlags": return mailxapi.updateFlags(p.accountId, p.uid, p.flags);
|
|
101
|
-
case "triggerSync": return mailxapi.syncAll();
|
|
102
|
-
case "getSyncPending": return mailxapi.getSyncPending();
|
|
103
|
-
case "searchContacts": return mailxapi.searchContacts(p.query);
|
|
104
|
-
case "allowRemoteContent": return mailxapi.allowRemoteContent(p.type, p.value);
|
|
105
|
-
case "deleteMessage": return mailxapi.deleteMessage(p.accountId, p.uid);
|
|
106
|
-
case "undeleteMessage": return mailxapi.undeleteMessage(p.accountId, p.uid, p.folderId);
|
|
107
|
-
case "moveMessage": return mailxapi.moveMessage(p.accountId, p.uid, p.targetFolderId, p.targetAccountId);
|
|
108
|
-
case "restartServer": return mailxapi.restart?.();
|
|
109
|
-
case "markFolderRead": return mailxapi.markFolderRead?.(p.accountId, p.folderId);
|
|
110
|
-
case "createFolder": return mailxapi.createFolder?.(p.accountId, p.parentPath, p.name);
|
|
111
|
-
case "renameFolder": return mailxapi.renameFolder?.(p.accountId, p.folderId, p.newName);
|
|
112
|
-
case "deleteFolder": return mailxapi.deleteFolder?.(p.accountId, p.folderId);
|
|
113
|
-
case "emptyFolder": return mailxapi.emptyFolder?.(p.accountId, p.folderId);
|
|
114
|
-
case "sendMessage": return mailxapi.sendMessage?.(p.body);
|
|
115
|
-
case "saveDraft": return mailxapi.saveDraft?.(p.body);
|
|
116
|
-
default: throw new Error(`Unknown IPC method: ${method}`);
|
|
117
|
-
}
|
|
24
|
+
async function api(path, options) {
|
|
25
|
+
let res;
|
|
26
|
+
try {
|
|
27
|
+
res = await fetch(`/api${path}`, {
|
|
28
|
+
headers: { "Content-Type": "application/json" },
|
|
29
|
+
...options
|
|
30
|
+
});
|
|
118
31
|
}
|
|
119
|
-
|
|
120
|
-
|
|
32
|
+
catch (e) {
|
|
33
|
+
// Network error — server is down
|
|
34
|
+
if (e.name === "AbortError")
|
|
35
|
+
throw e;
|
|
36
|
+
throw new Error("Server offline — run: mailx -server");
|
|
121
37
|
}
|
|
122
|
-
|
|
123
|
-
|
|
38
|
+
if (!res.ok) {
|
|
39
|
+
const err = await res.json().catch(() => ({ error: res.statusText }));
|
|
40
|
+
throw new Error(err.error || res.statusText);
|
|
124
41
|
}
|
|
42
|
+
return res.json();
|
|
125
43
|
}
|
|
126
|
-
|
|
127
|
-
getAccounts: () => ({ path: "/accounts" }),
|
|
128
|
-
getFolders: (p) => ({ path: `/folders/${p.accountId}` }),
|
|
129
|
-
getMessages: (p) => ({
|
|
130
|
-
path: `/messages/${p.accountId}/${p.folderId}?page=${p.page || 1}&pageSize=${p.pageSize || 50}`,
|
|
131
|
-
options: { signal: newMessageListSignal() }
|
|
132
|
-
}),
|
|
133
|
-
getUnifiedInbox: (p) => ({
|
|
134
|
-
path: `/messages/unified/inbox?page=${p.page || 1}&pageSize=${p.pageSize || 50}`,
|
|
135
|
-
options: { signal: newMessageListSignal() }
|
|
136
|
-
}),
|
|
137
|
-
searchMessages: (p) => {
|
|
138
|
-
const params = new URLSearchParams({ q: p.query, page: String(p.page || 1), pageSize: String(p.pageSize || 50), scope: p.scope || "all" });
|
|
139
|
-
if ((p.scope === "current" || p.scope === "server") && p.accountId) {
|
|
140
|
-
params.set("accountId", p.accountId);
|
|
141
|
-
params.set("folderId", String(p.folderId));
|
|
142
|
-
}
|
|
143
|
-
return { path: `/search?${params}` };
|
|
144
|
-
},
|
|
145
|
-
getMessage: (p) => {
|
|
146
|
-
const params = new URLSearchParams();
|
|
147
|
-
if (p.allowRemote)
|
|
148
|
-
params.set("allowRemote", "true");
|
|
149
|
-
if (p.folderId != null)
|
|
150
|
-
params.set("folderId", String(p.folderId));
|
|
151
|
-
const q = params.toString() ? `?${params}` : "";
|
|
152
|
-
return { path: `/message/${p.accountId}/${p.uid}${q}` };
|
|
153
|
-
},
|
|
154
|
-
updateFlags: (p) => ({
|
|
155
|
-
path: `/message/${p.accountId}/${p.uid}/flags`,
|
|
156
|
-
options: { method: "PATCH", body: JSON.stringify({ flags: p.flags }) }
|
|
157
|
-
}),
|
|
158
|
-
triggerSync: () => ({ path: "/sync", options: { method: "POST" } }),
|
|
159
|
-
getSyncPending: () => ({ path: "/sync/pending" }),
|
|
160
|
-
searchContacts: (p) => ({ path: `/contacts?q=${encodeURIComponent(p.query)}` }),
|
|
161
|
-
allowRemoteContent: (p) => ({
|
|
162
|
-
path: "/settings/allow-remote",
|
|
163
|
-
options: { method: "POST", body: JSON.stringify({ type: p.type, value: p.value }) }
|
|
164
|
-
}),
|
|
165
|
-
deleteMessage: (p) => ({ path: `/message/${p.accountId}/${p.uid}`, options: { method: "DELETE" } }),
|
|
166
|
-
undeleteMessage: (p) => ({
|
|
167
|
-
path: `/message/${p.accountId}/${p.uid}/undelete`,
|
|
168
|
-
options: { method: "POST", body: JSON.stringify({ folderId: p.folderId }) }
|
|
169
|
-
}),
|
|
170
|
-
moveMessage: (p) => {
|
|
171
|
-
const body = { targetFolderId: p.targetFolderId };
|
|
172
|
-
if (p.targetAccountId)
|
|
173
|
-
body.targetAccountId = p.targetAccountId;
|
|
174
|
-
return { path: `/message/${p.accountId}/${p.uid}/move`, options: { method: "POST", body: JSON.stringify(body) } };
|
|
175
|
-
},
|
|
176
|
-
restartServer: () => ({ path: "/restart", options: { method: "POST" } }),
|
|
177
|
-
rebuildServer: () => ({ path: "/rebuild", options: { method: "POST" } }),
|
|
178
|
-
markFolderRead: (p) => ({ path: `/folder/${p.accountId}/${p.folderId}/mark-read`, options: { method: "POST" } }),
|
|
179
|
-
createFolder: (p) => ({
|
|
180
|
-
path: `/folder/${p.accountId}`,
|
|
181
|
-
options: { method: "POST", body: JSON.stringify({ parentPath: p.parentPath, name: p.name }) }
|
|
182
|
-
}),
|
|
183
|
-
renameFolder: (p) => ({
|
|
184
|
-
path: `/folder/${p.accountId}/${p.folderId}/rename`,
|
|
185
|
-
options: { method: "POST", body: JSON.stringify({ newName: p.newName }) }
|
|
186
|
-
}),
|
|
187
|
-
deleteFolder: (p) => ({ path: `/folder/${p.accountId}/${p.folderId}`, options: { method: "DELETE" } }),
|
|
188
|
-
emptyFolder: (p) => ({ path: `/folder/${p.accountId}/${p.folderId}/empty`, options: { method: "POST" } }),
|
|
189
|
-
sendMessage: (p) => ({ path: "/send", options: { method: "POST", body: JSON.stringify(p.body) } }),
|
|
190
|
-
saveDraft: (p) => ({ path: "/draft", options: { method: "POST", body: JSON.stringify(p.body) } }),
|
|
191
|
-
};
|
|
192
|
-
// ── Local Transport (MAUI Android — runs IMAP in WebView via bridge) ──
|
|
193
|
-
class LocalTransport {
|
|
194
|
-
handlers = [];
|
|
195
|
-
localService = null;
|
|
196
|
-
async call(method, params) {
|
|
197
|
-
if (!this.localService) {
|
|
198
|
-
this.localService = await import("./local-service.js");
|
|
199
|
-
}
|
|
200
|
-
return this.localService.handleCall(method, params || {});
|
|
201
|
-
}
|
|
202
|
-
onEvent(handler) {
|
|
203
|
-
this.handlers.push(handler);
|
|
204
|
-
}
|
|
205
|
-
async connect() {
|
|
206
|
-
if (!this.localService) {
|
|
207
|
-
this.localService = await import("./local-service.js");
|
|
208
|
-
}
|
|
209
|
-
// Forward events from local service to transport handlers
|
|
210
|
-
this.localService.onEvent((event) => {
|
|
211
|
-
for (const h of this.handlers)
|
|
212
|
-
h(event);
|
|
213
|
-
});
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
// ── Select transport ──
|
|
217
|
-
const hasIPC = typeof mailxapi !== "undefined" && mailxapi?.isApp;
|
|
218
|
-
const hasBridge = typeof mailxapi !== "undefined" && mailxapi?.tcp !== undefined;
|
|
219
|
-
const hasEnsureServer = typeof mailxapi !== "undefined" && mailxapi?.ensureServer !== undefined;
|
|
220
|
-
// MAUI bridge (has tcp but no ensureServer) → LocalTransport
|
|
221
|
-
// Desktop native (has ensureServer) → IpcTransport
|
|
222
|
-
// Browser → HttpTransport
|
|
223
|
-
const transport = hasBridge && !hasEnsureServer
|
|
224
|
-
? new LocalTransport()
|
|
225
|
-
: hasIPC ? new IpcTransport() : new HttpTransport();
|
|
226
|
-
// ── Public API (unchanged signatures for all callers) ──
|
|
44
|
+
// ── API Methods (IPC or HTTP) ──
|
|
227
45
|
export function getAccounts() {
|
|
228
|
-
|
|
46
|
+
if (hasIPC)
|
|
47
|
+
return mailxapi.getAccounts();
|
|
48
|
+
return api("/accounts");
|
|
229
49
|
}
|
|
230
50
|
export function getFolders(accountId) {
|
|
231
|
-
|
|
51
|
+
if (hasIPC)
|
|
52
|
+
return mailxapi.getFolders(accountId);
|
|
53
|
+
return api(`/folders/${accountId}`);
|
|
232
54
|
}
|
|
233
55
|
export function getMessages(accountId, folderId, page = 1, pageSize = 50) {
|
|
234
|
-
|
|
56
|
+
if (hasIPC)
|
|
57
|
+
return mailxapi.getMessages(accountId, folderId, page, pageSize);
|
|
58
|
+
const signal = newMessageListSignal();
|
|
59
|
+
return api(`/messages/${accountId}/${folderId}?page=${page}&pageSize=${pageSize}`, { signal });
|
|
235
60
|
}
|
|
236
61
|
export function getUnifiedInbox(page = 1, pageSize = 50) {
|
|
237
|
-
|
|
62
|
+
if (hasIPC)
|
|
63
|
+
return mailxapi.getUnifiedInbox(page, pageSize);
|
|
64
|
+
const signal = newMessageListSignal();
|
|
65
|
+
return api(`/messages/unified/inbox?page=${page}&pageSize=${pageSize}`, { signal });
|
|
238
66
|
}
|
|
239
67
|
export function searchMessages(query, page = 1, pageSize = 50, scope = "all", accountId = "", folderId = 0) {
|
|
240
|
-
|
|
68
|
+
if (hasIPC)
|
|
69
|
+
return mailxapi.searchMessages(query, page, pageSize);
|
|
70
|
+
const params = new URLSearchParams({ q: query, page: String(page), pageSize: String(pageSize), scope });
|
|
71
|
+
if (scope === "current" && accountId) {
|
|
72
|
+
params.set("accountId", accountId);
|
|
73
|
+
params.set("folderId", String(folderId));
|
|
74
|
+
}
|
|
75
|
+
if (scope === "server" && accountId) {
|
|
76
|
+
params.set("accountId", accountId);
|
|
77
|
+
params.set("folderId", String(folderId));
|
|
78
|
+
}
|
|
79
|
+
return api(`/search?${params}`);
|
|
241
80
|
}
|
|
242
81
|
export function getMessage(accountId, uid, allowRemote = false, folderId) {
|
|
243
|
-
|
|
82
|
+
if (hasIPC)
|
|
83
|
+
return mailxapi.getMessage(accountId, uid, allowRemote, folderId);
|
|
84
|
+
const params = new URLSearchParams();
|
|
85
|
+
if (allowRemote)
|
|
86
|
+
params.set("allowRemote", "true");
|
|
87
|
+
if (folderId != null)
|
|
88
|
+
params.set("folderId", String(folderId));
|
|
89
|
+
const q = params.toString() ? `?${params}` : "";
|
|
90
|
+
return api(`/message/${accountId}/${uid}${q}`);
|
|
244
91
|
}
|
|
245
92
|
export function updateFlags(accountId, uid, flags) {
|
|
246
|
-
|
|
93
|
+
if (hasIPC)
|
|
94
|
+
return mailxapi.updateFlags(accountId, uid, flags);
|
|
95
|
+
return api(`/message/${accountId}/${uid}/flags`, {
|
|
96
|
+
method: "PATCH",
|
|
97
|
+
body: JSON.stringify({ flags })
|
|
98
|
+
});
|
|
247
99
|
}
|
|
248
100
|
export function triggerSync() {
|
|
249
|
-
|
|
101
|
+
if (hasIPC)
|
|
102
|
+
return mailxapi.syncAll();
|
|
103
|
+
return api("/sync", { method: "POST" });
|
|
250
104
|
}
|
|
251
105
|
export function getSyncPending() {
|
|
252
|
-
|
|
106
|
+
if (hasIPC)
|
|
107
|
+
return mailxapi.getSyncPending();
|
|
108
|
+
return api("/sync/pending");
|
|
253
109
|
}
|
|
254
110
|
export function searchContacts(query) {
|
|
255
|
-
|
|
111
|
+
if (hasIPC)
|
|
112
|
+
return mailxapi.searchContacts(query);
|
|
113
|
+
return api(`/contacts?q=${encodeURIComponent(query)}`);
|
|
256
114
|
}
|
|
257
115
|
export function allowRemoteContent(type, value) {
|
|
258
|
-
|
|
116
|
+
if (hasIPC)
|
|
117
|
+
return mailxapi.allowRemoteContent(type, value);
|
|
118
|
+
return api("/settings/allow-remote", {
|
|
119
|
+
method: "POST",
|
|
120
|
+
body: JSON.stringify({ type, value })
|
|
121
|
+
});
|
|
259
122
|
}
|
|
260
123
|
export function deleteMessage(accountId, uid) {
|
|
261
|
-
|
|
124
|
+
if (hasIPC)
|
|
125
|
+
return mailxapi.deleteMessage?.(accountId, uid);
|
|
126
|
+
return api(`/message/${accountId}/${uid}`, { method: "DELETE" });
|
|
262
127
|
}
|
|
263
128
|
export function undeleteMessage(accountId, uid, folderId) {
|
|
264
|
-
|
|
129
|
+
if (hasIPC)
|
|
130
|
+
return mailxapi.undeleteMessage?.(accountId, uid, folderId);
|
|
131
|
+
return api(`/message/${accountId}/${uid}/undelete`, {
|
|
132
|
+
method: "POST",
|
|
133
|
+
body: JSON.stringify({ folderId })
|
|
134
|
+
});
|
|
265
135
|
}
|
|
266
136
|
export function moveMessage(accountId, uid, targetFolderId, targetAccountId) {
|
|
267
|
-
|
|
137
|
+
if (hasIPC)
|
|
138
|
+
return mailxapi.moveMessage?.(accountId, uid, targetFolderId, targetAccountId);
|
|
139
|
+
const body = { targetFolderId };
|
|
140
|
+
if (targetAccountId)
|
|
141
|
+
body.targetAccountId = targetAccountId;
|
|
142
|
+
return api(`/message/${accountId}/${uid}/move`, {
|
|
143
|
+
method: "POST",
|
|
144
|
+
body: JSON.stringify(body)
|
|
145
|
+
});
|
|
268
146
|
}
|
|
269
147
|
export function restartServer() {
|
|
270
|
-
|
|
148
|
+
if (hasIPC)
|
|
149
|
+
return mailxapi.restart?.();
|
|
150
|
+
return api("/restart", { method: "POST" }).catch(() => { });
|
|
271
151
|
}
|
|
272
152
|
export function rebuildServer() {
|
|
273
|
-
return
|
|
153
|
+
return api("/rebuild", { method: "POST" }).catch(() => { });
|
|
274
154
|
}
|
|
275
155
|
export function markFolderRead(accountId, folderId) {
|
|
276
|
-
|
|
156
|
+
if (hasIPC)
|
|
157
|
+
return mailxapi.markFolderRead?.(accountId, folderId);
|
|
158
|
+
return api(`/folder/${accountId}/${folderId}/mark-read`, { method: "POST" });
|
|
277
159
|
}
|
|
278
160
|
export function createFolder(accountId, parentPath, name) {
|
|
279
|
-
|
|
161
|
+
if (hasIPC)
|
|
162
|
+
return mailxapi.createFolder?.(accountId, parentPath, name);
|
|
163
|
+
return api(`/folder/${accountId}`, {
|
|
164
|
+
method: "POST",
|
|
165
|
+
body: JSON.stringify({ parentPath, name })
|
|
166
|
+
});
|
|
280
167
|
}
|
|
281
168
|
export function renameFolder(accountId, folderId, newName) {
|
|
282
|
-
|
|
169
|
+
if (hasIPC)
|
|
170
|
+
return mailxapi.renameFolder?.(accountId, folderId, newName);
|
|
171
|
+
return api(`/folder/${accountId}/${folderId}/rename`, {
|
|
172
|
+
method: "POST",
|
|
173
|
+
body: JSON.stringify({ newName })
|
|
174
|
+
});
|
|
283
175
|
}
|
|
284
176
|
export function deleteFolder(accountId, folderId) {
|
|
285
|
-
|
|
177
|
+
if (hasIPC)
|
|
178
|
+
return mailxapi.deleteFolder?.(accountId, folderId);
|
|
179
|
+
return api(`/folder/${accountId}/${folderId}`, { method: "DELETE" });
|
|
286
180
|
}
|
|
287
181
|
export function emptyFolder(accountId, folderId) {
|
|
288
|
-
|
|
182
|
+
if (hasIPC)
|
|
183
|
+
return mailxapi.emptyFolder?.(accountId, folderId);
|
|
184
|
+
return api(`/folder/${accountId}/${folderId}/empty`, { method: "POST" });
|
|
289
185
|
}
|
|
290
186
|
export function sendMessage(body) {
|
|
291
|
-
|
|
187
|
+
if (hasIPC)
|
|
188
|
+
return mailxapi.sendMessage?.(body);
|
|
189
|
+
return api("/send", { method: "POST", body: JSON.stringify(body) });
|
|
292
190
|
}
|
|
293
191
|
export function saveDraft(body) {
|
|
294
|
-
|
|
192
|
+
if (hasIPC)
|
|
193
|
+
return mailxapi.saveDraft?.(body);
|
|
194
|
+
return api("/draft", { method: "POST", body: JSON.stringify(body) });
|
|
295
195
|
}
|
|
196
|
+
const eventHandlers = [];
|
|
296
197
|
export function onEvent(handler) {
|
|
297
|
-
|
|
198
|
+
eventHandlers.push(handler);
|
|
298
199
|
}
|
|
299
200
|
export function connectEvents() {
|
|
300
|
-
|
|
201
|
+
if (hasIPC) {
|
|
202
|
+
// IPC events come via mailxapi.onEvent
|
|
203
|
+
mailxapi.onEvent((event) => {
|
|
204
|
+
for (const h of eventHandlers)
|
|
205
|
+
h(event);
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
// WebSocket for HTTP mode
|
|
210
|
+
const protocol = location.protocol === "https:" ? "wss:" : "ws:";
|
|
211
|
+
const ws = new WebSocket(`${protocol}//${location.host}`);
|
|
212
|
+
ws.onmessage = (ev) => {
|
|
213
|
+
try {
|
|
214
|
+
const event = JSON.parse(ev.data);
|
|
215
|
+
for (const h of eventHandlers)
|
|
216
|
+
h(event);
|
|
217
|
+
}
|
|
218
|
+
catch { /* ignore */ }
|
|
219
|
+
};
|
|
220
|
+
ws.onclose = () => {
|
|
221
|
+
setTimeout(connectEvents, 3000);
|
|
222
|
+
};
|
|
223
|
+
}
|
|
301
224
|
}
|
|
302
225
|
// Legacy exports for backward compatibility
|
|
303
226
|
export const connectWebSocket = connectEvents;
|
package/client/package.json
CHANGED