@bobfrankston/mailx 1.0.159 → 1.0.161

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/README.md CHANGED
@@ -200,10 +200,10 @@ Under **Settings** in the toolbar:
200
200
 
201
201
  ```
202
202
  mailx Start the app (native window via IPC)
203
- mailx --server Start HTTP server mode (http://localhost:9333)
204
- mailx --no-browser Server mode without opening browser
203
+ mailx --email <addr> First-time setup with this email (skips prompt)
205
204
  mailx --verbose Show log output in terminal (default: log file only)
206
205
  mailx --import <file> Import accounts.jsonc into Google Drive
206
+ mailx --server Start HTTP server for dev/remote (http://localhost:9333)
207
207
 
208
208
  mailx -kill Kill running mailx processes + clean up WAL files
209
209
  mailx -repair Re-sync message metadata (fix garbled subjects)
package/again.cmd ADDED
@@ -0,0 +1,3 @@
1
+ call rebuild
2
+ ager.ts
3
+ mailx -email BobFrankston@Gmail.com
package/bin/ager.js ADDED
@@ -0,0 +1,57 @@
1
+ /**
2
+ * ager — archive .mailx to c:\temp\.mailx## and preserve OAuth tokens
3
+ *
4
+ * Moves ~/.mailx to c:\temp\.mailx01 (or next available number),
5
+ * then copies the tokens directory back so OAuth doesn't need re-consent.
6
+ *
7
+ * Usage: node ager.js
8
+ */
9
+ import fs from "node:fs";
10
+ import path from "node:path";
11
+ const home = process.env.USERPROFILE || process.env.HOME || ".";
12
+ const source = path.join(home, ".mailx");
13
+ const tempDir = "C:\\temp";
14
+ if (!fs.existsSync(source)) {
15
+ console.error(`No .mailx directory at ${source}`);
16
+ process.exit(1);
17
+ }
18
+ // Find next available number
19
+ fs.mkdirSync(tempDir, { recursive: true });
20
+ let num = 1;
21
+ while (fs.existsSync(path.join(tempDir, `.mailx${String(num).padStart(2, "0")}`))) {
22
+ num++;
23
+ }
24
+ const dest = path.join(tempDir, `.mailx${String(num).padStart(2, "0")}`);
25
+ // Save tokens before moving
26
+ const tokensDir = path.join(source, "tokens");
27
+ const hasTokens = fs.existsSync(tokensDir);
28
+ const tokenFiles = [];
29
+ if (hasTokens) {
30
+ const walk = (dir, base) => {
31
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
32
+ const full = path.join(dir, entry.name);
33
+ const rel = path.join(base, entry.name);
34
+ if (entry.isDirectory())
35
+ walk(full, rel);
36
+ else
37
+ tokenFiles.push({ rel, data: fs.readFileSync(full) });
38
+ }
39
+ };
40
+ walk(tokensDir, "");
41
+ console.log(` Saved ${tokenFiles.length} token file(s)`);
42
+ }
43
+ // Move .mailx to temp
44
+ fs.renameSync(source, dest);
45
+ console.log(`Moved ${source} → ${dest}`);
46
+ // Restore tokens to fresh .mailx
47
+ if (tokenFiles.length > 0) {
48
+ const newTokens = path.join(source, "tokens");
49
+ for (const { rel, data } of tokenFiles) {
50
+ const target = path.join(newTokens, rel);
51
+ fs.mkdirSync(path.dirname(target), { recursive: true });
52
+ fs.writeFileSync(target, data);
53
+ }
54
+ console.log(` Restored tokens to ${newTokens}`);
55
+ }
56
+ console.log("Ready for fresh start.");
57
+ //# sourceMappingURL=ager.js.map
package/bin/mailx.js CHANGED
@@ -35,7 +35,7 @@ const rebuildMode = hasFlag("rebuild");
35
35
  const repairMode = hasFlag("repair");
36
36
  const importMode = hasFlag("import");
37
37
  // Validate arguments
38
- const knownFlags = ["server", "no-browser", "verbose", "external", "kill", "v", "version", "setup", "add", "test", "rebuild", "repair", "native-imap", "log", "import", "email"];
38
+ const knownFlags = ["server", "no-browser", "verbose", "external", "kill", "v", "version", "setup", "add", "test", "rebuild", "repair", "native-imap", "log", "import", "email", "mail"];
39
39
  for (const arg of args) {
40
40
  const flag = arg.replace(/^--?/, "");
41
41
  if (arg.startsWith("-") && !knownFlags.includes(flag)) {
@@ -599,9 +599,11 @@ async function main() {
599
599
  if (setupMode || !hasConfig()) {
600
600
  if (!setupMode)
601
601
  console.log("No mailx configuration found.");
602
- // --email flag skips the interactive prompt
602
+ // --email (or -mail) flag skips the interactive prompt
603
603
  const emailArg = args.find(a => a.startsWith("--email="))?.split("=")[1]
604
- || (hasFlag("email") ? args[args.indexOf("--email") + 1] || args[args.indexOf("-email") + 1] : undefined);
604
+ || args.find(a => a.startsWith("-mail="))?.split("=")[1]
605
+ || (hasFlag("email") ? args[args.indexOf("--email") + 1] || args[args.indexOf("-email") + 1] : undefined)
606
+ || (hasFlag("mail") ? args[args.indexOf("--mail") + 1] || args[args.indexOf("-mail") + 1] : undefined);
605
607
  await runSetup(emailArg);
606
608
  }
607
609
  // Redirect console to log file — keep terminal clean
@@ -708,9 +710,8 @@ async function main() {
708
710
  imapManager.on("accountError", (accountId, error, hint, isOAuth) => {
709
711
  handle.send({ _event: "accountError", type: "accountError", accountId, error, hint, isOAuth });
710
712
  });
711
- // Wait for WebView2 initialization, then signal readiness
712
- await new Promise(r => setTimeout(r, 2000));
713
- handle.send({ _event: "ready", type: "ready" });
713
+ // Brief pause for WebView2 to initialize before starting IMAP (avoids stdin writes during init)
714
+ await new Promise(r => setTimeout(r, 500));
714
715
  // Register all accounts (OAuth may open browser for Gmail — event loop stays free for IPC)
715
716
  for (const account of settings.accounts) {
716
717
  if (!account.enabled)
@@ -747,10 +748,16 @@ async function main() {
747
748
  }
748
749
  process.on("SIGINT", () => gracefulShutdown("SIGINT"));
749
750
  process.on("SIGTERM", () => gracefulShutdown("SIGTERM"));
750
- process.on("exit", () => {
751
+ process.on("uncaughtException", (err) => {
752
+ console.error(`UNCAUGHT EXCEPTION: ${err.stack || err.message}`);
753
+ gracefulShutdown("uncaughtException");
754
+ });
755
+ process.on("unhandledRejection", (reason) => {
756
+ console.error(`UNHANDLED REJECTION: ${reason?.stack || reason?.message || reason}`);
757
+ });
758
+ process.on("exit", (code) => {
759
+ console.log(`Process exit (code ${code})`);
751
760
  if (!shuttingDown) {
752
- // Sync fallback — shutdown() is async so can't fully run here,
753
- // but at least stop timers and release semaphores
754
761
  imapManager.stopPeriodicSync();
755
762
  imapManager.stopOutboxWorker();
756
763
  db.close();
@@ -12,9 +12,6 @@
12
12
  var _callbacks = {};
13
13
  var _callbackId = 0;
14
14
  var _eventHandlers = [];
15
- var _ready = false;
16
- var _pendingCalls = []; // buffered until server sends "ready"
17
-
18
15
  function callNode(action, params) {
19
16
  var id = String(++_callbackId);
20
17
  return new Promise(function(resolve, reject) {
@@ -24,11 +21,6 @@
24
21
  }, 120000);
25
22
  _callbacks[id] = { resolve: resolve, reject: reject, timer: timer };
26
23
  var msg = Object.assign({ _action: action, _cbid: id }, params || {});
27
- if (!_ready) {
28
- // Buffer until server is ready (early calls are lost in the pipe)
29
- _pendingCalls.push(msg);
30
- return;
31
- }
32
24
  if (window.ipc && window.ipc.postMessage) {
33
25
  window.ipc.postMessage(JSON.stringify(msg));
34
26
  } else {
@@ -39,20 +31,6 @@
39
31
  });
40
32
  }
41
33
 
42
- function flushPending() {
43
- _ready = true;
44
- var pending = _pendingCalls.splice(0);
45
- // Send diagnostic so it shows in Node.js IPC log
46
- if (window.ipc && window.ipc.postMessage) {
47
- window.ipc.postMessage(JSON.stringify({ _action: "_debug", _cbid: "0", info: "flush " + pending.length + " calls: " + pending.map(function(m) { return m._action; }).join(", ") }));
48
- }
49
- for (var i = 0; i < pending.length; i++) {
50
- if (window.ipc && window.ipc.postMessage) {
51
- window.ipc.postMessage(JSON.stringify(pending[i]));
52
- }
53
- }
54
- }
55
-
56
34
  // Called by Rust to resolve promises
57
35
  window._mailxapiResolve = function(id, value) {
58
36
  var cb = _callbacks[id];
@@ -73,11 +51,6 @@
73
51
 
74
52
  // Called by Rust to push events (new mail, sync progress, etc.)
75
53
  window._mailxapiEvent = function(event) {
76
- // "ready" signal from server — flush buffered IPC calls
77
- if (event && event.type === "ready") {
78
- flushPending();
79
- return;
80
- }
81
54
  for (var i = 0; i < _eventHandlers.length; i++) {
82
55
  try { _eventHandlers[i](event); } catch(e) { /* ignore */ }
83
56
  }
@@ -132,13 +105,47 @@
132
105
  syncAll: function() { return callNode("syncAll"); },
133
106
  getSyncPending: function() { return callNode("getSyncPending"); },
134
107
 
108
+ // Bulk operations
109
+ deleteMessages: function(accountId, uids) {
110
+ return callNode("deleteMessages", { accountId: accountId, uids: uids });
111
+ },
112
+ moveMessage: function(accountId, uid, targetFolderId, targetAccountId) {
113
+ return callNode("moveMessage", { accountId: accountId, uid: uid, targetFolderId: targetFolderId, targetAccountId: targetAccountId });
114
+ },
115
+ moveMessages: function(accountId, uids, targetFolderId) {
116
+ return callNode("moveMessages", { accountId: accountId, uids: uids, targetFolderId: targetFolderId });
117
+ },
118
+ markFolderRead: function(accountId, folderId) {
119
+ return callNode("markFolderRead", { folderId: folderId });
120
+ },
121
+ createFolder: function(accountId, parentPath, name) {
122
+ return callNode("createFolder", { accountId: accountId, parentPath: parentPath, name: name });
123
+ },
124
+ renameFolder: function(accountId, folderId, newName) {
125
+ return callNode("renameFolder", { accountId: accountId, folderId: folderId, newName: newName });
126
+ },
127
+ deleteFolder: function(accountId, folderId) {
128
+ return callNode("deleteFolder", { accountId: accountId, folderId: folderId });
129
+ },
130
+ emptyFolder: function(accountId, folderId) {
131
+ return callNode("emptyFolder", { accountId: accountId, folderId: folderId });
132
+ },
133
+
135
134
  // Settings
136
135
  allowRemoteContent: function(type, value) {
137
136
  return callNode("allowRemoteContent", { type: type, value: value });
138
137
  },
139
138
  getSettings: function() { return callNode("getSettings"); },
140
- saveSettings: function(data) { return callNode("saveSettingsData", data); },
139
+ saveSettingsData: function(data) { return callNode("saveSettingsData", data); },
141
140
  getVersion: function() { return callNode("getVersion"); },
141
+ getAutocompleteSettings: function() { return callNode("getAutocompleteSettings"); },
142
+ saveAutocompleteSettings: function(settings) { return callNode("saveAutocompleteSettings", settings); },
143
+
144
+ // Setup & Repair
145
+ setupAccount: function(name, email, password) {
146
+ return callNode("setupAccount", { name: name, email: email, password: password });
147
+ },
148
+ repairAccounts: function() { return callNode("repairAccounts"); },
142
149
 
143
150
  // Events
144
151
  onEvent: function(handler) { _eventHandlers.push(handler); },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/mailx",
3
- "version": "1.0.159",
3
+ "version": "1.0.161",
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,10 +20,10 @@
20
20
  "postinstall": "node bin/postinstall.js"
21
21
  },
22
22
  "dependencies": {
23
- "@bobfrankston/iflow": "^1.0.54",
23
+ "@bobfrankston/iflow": "^1.0.55",
24
24
  "@bobfrankston/miscinfo": "^1.0.7",
25
25
  "@bobfrankston/oauthsupport": "^1.0.20",
26
- "@bobfrankston/msger": "^0.1.209",
26
+ "@bobfrankston/msger": "^0.1.211",
27
27
  "@capacitor/android": "^8.3.0",
28
28
  "@capacitor/cli": "^8.3.0",
29
29
  "@capacitor/core": "^8.3.0",