@bobfrankston/mailx 1.0.145 → 1.0.147
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.js +23 -8
- package/client/app.js +40 -41
- package/package.json +2 -2
- package/packages/mailx-imap/index.d.ts +1 -0
- package/packages/mailx-imap/index.js +9 -5
- package/packages/mailx-service/jsonrpc.js +5 -2
- package/rebuild.cmd +2 -2
package/bin/mailx.js
CHANGED
|
@@ -529,21 +529,34 @@ async function main() {
|
|
|
529
529
|
const imapManager = new ImapManager(db);
|
|
530
530
|
imapManager.useNativeClient = true;
|
|
531
531
|
const svc = new MailxService(db, imapManager);
|
|
532
|
-
//
|
|
533
|
-
const mailxapiPath = path.join(import.meta.dirname, "..", "client", "lib", "mailxapi.js");
|
|
534
|
-
const mailxapiScript = fs.readFileSync(mailxapiPath, "utf-8");
|
|
535
|
-
// Open msger in service mode — file:// URL, no HTTP
|
|
532
|
+
// Open msger in service mode — custom protocol serves files from client dir
|
|
536
533
|
const clientDir = path.join(import.meta.dirname, "..", "client");
|
|
537
|
-
const
|
|
534
|
+
const mailxapiPath = path.join(clientDir, "lib", "mailxapi.js");
|
|
535
|
+
const mailxapiScript = fs.readFileSync(mailxapiPath, "utf-8");
|
|
538
536
|
const handle = showService({
|
|
539
|
-
url:
|
|
537
|
+
url: "index.html",
|
|
538
|
+
contentDir: clientDir,
|
|
540
539
|
initScript: mailxapiScript,
|
|
541
540
|
size: { width: 1400, height: 900 },
|
|
541
|
+
escapeCloses: false,
|
|
542
542
|
});
|
|
543
543
|
// Handle requests from WebView → dispatch to MailxService
|
|
544
|
+
// Pass server version to dispatch so getVersion returns it
|
|
545
|
+
const rootPkg = JSON.parse(fs.readFileSync(path.join(import.meta.dirname, "..", "package.json"), "utf-8"));
|
|
544
546
|
handle.onRequest(async (req) => {
|
|
545
|
-
|
|
546
|
-
|
|
547
|
+
if (verbose)
|
|
548
|
+
console.error(`[ipc] ← ${req._action} (${req._cbid})`);
|
|
549
|
+
req._version = rootPkg.version;
|
|
550
|
+
try {
|
|
551
|
+
const response = await dispatch(svc, req);
|
|
552
|
+
if (verbose)
|
|
553
|
+
console.error(`[ipc] → ${req._action} (${req._cbid}) ok`);
|
|
554
|
+
handle.send(response);
|
|
555
|
+
}
|
|
556
|
+
catch (e) {
|
|
557
|
+
console.error(`[ipc] → ${req._action} (${req._cbid}) error: ${e.message}`);
|
|
558
|
+
handle.send({ _cbid: req._cbid, error: e.message });
|
|
559
|
+
}
|
|
547
560
|
});
|
|
548
561
|
// Wire IMAP events → push to WebView
|
|
549
562
|
imapManager.on("syncProgress", (accountId, phase, progress) => {
|
|
@@ -558,6 +571,8 @@ async function main() {
|
|
|
558
571
|
imapManager.on("accountError", (accountId, error, hint, isOAuth) => {
|
|
559
572
|
handle.send({ _event: "accountError", type: "accountError", accountId, error, hint, isOAuth });
|
|
560
573
|
});
|
|
574
|
+
// Wait for WebView2 initialization before starting IMAP (stdin writes during init crash wry)
|
|
575
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
561
576
|
// Add accounts and start sync
|
|
562
577
|
for (const account of settings.accounts) {
|
|
563
578
|
if (!account.enabled)
|
package/client/app.js
CHANGED
|
@@ -900,7 +900,8 @@ optAutocomplete?.addEventListener("change", () => {
|
|
|
900
900
|
}).catch(() => { });
|
|
901
901
|
});
|
|
902
902
|
const isApp = typeof mailxapi !== "undefined" && mailxapi?.isApp;
|
|
903
|
-
fetch("/api/version").then(r => r.json())
|
|
903
|
+
const versionPromise = isApp ? mailxapi.getVersion() : fetch("/api/version").then(r => r.json());
|
|
904
|
+
versionPromise.then((d) => {
|
|
904
905
|
const el = document.getElementById("app-version");
|
|
905
906
|
const storage = d.storage || {};
|
|
906
907
|
const storageLabel = storage.provider && storage.provider !== "local"
|
|
@@ -947,56 +948,54 @@ fetch("/api/version").then(r => r.json()).then(d => {
|
|
|
947
948
|
else if (storage.cloudError) {
|
|
948
949
|
showAlert(`Cloud storage error: ${storage.cloudError}`, "cloud-error");
|
|
949
950
|
}
|
|
950
|
-
}).catch(
|
|
951
|
-
//
|
|
952
|
-
|
|
953
|
-
if (isApp) {
|
|
954
|
-
if (startupStatus)
|
|
955
|
-
startupStatus.textContent = "Starting server...";
|
|
956
|
-
await mailxapi.ensureServer();
|
|
957
|
-
location.reload();
|
|
958
|
-
}
|
|
959
|
-
else {
|
|
951
|
+
}).catch(() => {
|
|
952
|
+
// Version fetch failed — in IPC mode this means service isn't responding yet
|
|
953
|
+
if (!isApp) {
|
|
960
954
|
const el = document.getElementById("app-version");
|
|
961
955
|
if (el)
|
|
962
956
|
el.textContent = "mailx [server offline]";
|
|
957
|
+
const startupStatus = document.getElementById("startup-status");
|
|
963
958
|
if (startupStatus)
|
|
964
959
|
startupStatus.textContent = "Server offline — start with: node packages/mailx-server/index.js";
|
|
965
960
|
}
|
|
966
961
|
});
|
|
967
|
-
// ── Sync pending indicator + server health check ──
|
|
962
|
+
// ── Sync pending indicator + server health check (HTTP mode only) ──
|
|
968
963
|
let serverDown = false;
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
if
|
|
985
|
-
|
|
986
|
-
|
|
964
|
+
if (isApp) {
|
|
965
|
+
// IPC mode: events come via push, no polling needed
|
|
966
|
+
}
|
|
967
|
+
else
|
|
968
|
+
setInterval(async () => {
|
|
969
|
+
try {
|
|
970
|
+
const res = await fetch("/api/sync/pending");
|
|
971
|
+
if (!res.ok)
|
|
972
|
+
return;
|
|
973
|
+
const data = await res.json();
|
|
974
|
+
const el = document.getElementById("status-pending");
|
|
975
|
+
if (el) {
|
|
976
|
+
el.textContent = data.pending > 0 ? `↻ ${data.pending} pending` : "";
|
|
977
|
+
el.style.color = data.pending > 0 ? "oklch(0.75 0.15 60)" : "";
|
|
978
|
+
}
|
|
979
|
+
// Server is back — reload if it was down
|
|
980
|
+
if (serverDown) {
|
|
981
|
+
serverDown = false;
|
|
982
|
+
const statusEl = document.getElementById("status-sync");
|
|
983
|
+
if (statusEl)
|
|
984
|
+
statusEl.textContent = "Server reconnected";
|
|
985
|
+
location.reload();
|
|
986
|
+
}
|
|
987
987
|
}
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
988
|
+
catch {
|
|
989
|
+
if (!serverDown) {
|
|
990
|
+
serverDown = true;
|
|
991
|
+
const statusEl = document.getElementById("status-sync");
|
|
992
|
+
if (statusEl) {
|
|
993
|
+
statusEl.textContent = "SERVER OFFLINE";
|
|
994
|
+
statusEl.style.color = "oklch(0.65 0.2 25)";
|
|
995
|
+
}
|
|
996
996
|
}
|
|
997
997
|
}
|
|
998
|
-
}
|
|
999
|
-
}, 5000);
|
|
998
|
+
}, 5000);
|
|
1000
999
|
console.log("mailx client initialized, location:", location.href);
|
|
1001
1000
|
updateNewMessageCount();
|
|
1002
1001
|
// ── Midnight refresh — update date display when day changes ──
|
|
@@ -1011,7 +1010,7 @@ function scheduleMiddnightRefresh() {
|
|
|
1011
1010
|
}
|
|
1012
1011
|
scheduleMiddnightRefresh();
|
|
1013
1012
|
// ── Apply theme from settings ──
|
|
1014
|
-
fetch("/api/version").then(r => r.json()).then(d => {
|
|
1013
|
+
(isApp ? mailxapi.getVersion() : fetch("/api/version").then(r => r.json())).then((d) => {
|
|
1015
1014
|
if (d.theme === "dark")
|
|
1016
1015
|
document.documentElement.classList.add("theme-dark");
|
|
1017
1016
|
else if (d.theme === "light")
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bobfrankston/mailx",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.147",
|
|
4
4
|
"description": "Local-first email client with IMAP sync and standalone native app",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "bin/mailx.js",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"@bobfrankston/iflow": "^1.0.52",
|
|
24
24
|
"@bobfrankston/miscinfo": "^1.0.7",
|
|
25
25
|
"@bobfrankston/oauthsupport": "^1.0.20",
|
|
26
|
-
"@bobfrankston/msger": "^0.1.
|
|
26
|
+
"@bobfrankston/msger": "^0.1.199",
|
|
27
27
|
"@capacitor/android": "^8.3.0",
|
|
28
28
|
"@capacitor/cli": "^8.3.0",
|
|
29
29
|
"@capacitor/core": "^8.3.0",
|
|
@@ -164,6 +164,7 @@ export declare class ImapManager extends EventEmitter {
|
|
|
164
164
|
processOutbox(accountId: string): Promise<void>;
|
|
165
165
|
/** Start background Outbox worker — runs immediately then every 10 seconds */
|
|
166
166
|
private outboxBackoff;
|
|
167
|
+
private outboxBackoffDelay;
|
|
167
168
|
startOutboxWorker(): void;
|
|
168
169
|
/** Stop Outbox worker */
|
|
169
170
|
stopOutboxWorker(): void;
|
|
@@ -1592,7 +1592,8 @@ export class ImapManager extends EventEmitter {
|
|
|
1592
1592
|
}
|
|
1593
1593
|
}
|
|
1594
1594
|
/** Start background Outbox worker — runs immediately then every 10 seconds */
|
|
1595
|
-
outboxBackoff = new Map(); // accountId → next retry
|
|
1595
|
+
outboxBackoff = new Map(); // accountId → next retry timestamp
|
|
1596
|
+
outboxBackoffDelay = new Map(); // accountId → current delay ms
|
|
1596
1597
|
startOutboxWorker() {
|
|
1597
1598
|
if (this.outboxInterval)
|
|
1598
1599
|
return;
|
|
@@ -1606,12 +1607,15 @@ export class ImapManager extends EventEmitter {
|
|
|
1606
1607
|
try {
|
|
1607
1608
|
await this.processLocalQueue(accountId);
|
|
1608
1609
|
await this.processOutbox(accountId);
|
|
1609
|
-
|
|
1610
|
+
// Success — clear backoff
|
|
1611
|
+
this.outboxBackoff.delete(accountId);
|
|
1612
|
+
this.outboxBackoffDelay.delete(accountId);
|
|
1610
1613
|
}
|
|
1611
1614
|
catch (e) {
|
|
1612
|
-
// Exponential backoff: 30s
|
|
1613
|
-
const
|
|
1614
|
-
const delay =
|
|
1615
|
+
// Exponential backoff: 30s → 60s → 120s → 300s (max 5min)
|
|
1616
|
+
const prevDelay = this.outboxBackoffDelay.get(accountId) || 0;
|
|
1617
|
+
const delay = prevDelay ? Math.min(prevDelay * 2, 300000) : 30000;
|
|
1618
|
+
this.outboxBackoffDelay.set(accountId, delay);
|
|
1615
1619
|
this.outboxBackoff.set(accountId, now + delay);
|
|
1616
1620
|
console.error(` [outbox] Error for ${accountId}: ${imapError(e)} (retry in ${Math.round(delay / 1000)}s)`);
|
|
1617
1621
|
}
|
|
@@ -93,8 +93,11 @@ async function dispatchAction(svc, action, p) {
|
|
|
93
93
|
case "allowRemoteContent":
|
|
94
94
|
svc.allowRemoteContent(p.type, p.value);
|
|
95
95
|
return { ok: true };
|
|
96
|
-
case "getVersion":
|
|
97
|
-
|
|
96
|
+
case "getVersion": {
|
|
97
|
+
const settings = svc.getSettings();
|
|
98
|
+
const storage = svc.getStorageInfo();
|
|
99
|
+
return { version: p._version || "dev", theme: settings.ui?.theme || "system", storage };
|
|
100
|
+
}
|
|
98
101
|
// Autocomplete
|
|
99
102
|
case "autocomplete":
|
|
100
103
|
return svc.autocomplete(p);
|
package/rebuild.cmd
CHANGED