@bobfrankston/mailx 1.0.158 → 1.0.160

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/bin/mailx.js CHANGED
@@ -7,6 +7,7 @@
7
7
  * mailx --server Start Express HTTP server (dev/remote)
8
8
  * mailx --no-browser Start server only (headless)
9
9
  * mailx --verbose Show console output (default: log file only)
10
+ * mailx --email <addr> First-time setup with email (skips prompt)
10
11
  * mailx --import <file> Import accounts.jsonc into GDrive and merge
11
12
  * mailx -v / --version Show version and exit
12
13
  * mailx -kill Kill running mailx processes
@@ -34,7 +35,7 @@ const rebuildMode = hasFlag("rebuild");
34
35
  const repairMode = hasFlag("repair");
35
36
  const importMode = hasFlag("import");
36
37
  // Validate arguments
37
- const knownFlags = ["server", "no-browser", "verbose", "external", "kill", "v", "version", "setup", "add", "test", "rebuild", "repair", "native-imap", "log", "import"];
38
+ const knownFlags = ["server", "no-browser", "verbose", "external", "kill", "v", "version", "setup", "add", "test", "rebuild", "repair", "native-imap", "log", "import", "email", "mail"];
38
39
  for (const arg of args) {
39
40
  const flag = arg.replace(/^--?/, "");
40
41
  if (arg.startsWith("-") && !knownFlags.includes(flag)) {
@@ -364,18 +365,19 @@ async function promptForAccount(intro) {
364
365
  };
365
366
  }
366
367
  /** Interactive first-time setup — GDrive API for cloud storage */
367
- async function runSetup() {
368
+ async function runSetup(providedEmail) {
368
369
  console.log("\nmailx — first-time setup\n");
369
370
  const home = process.env.USERPROFILE || process.env.HOME || "";
370
371
  const mailxDir = path.join(home, ".mailx");
371
- // Ask for email first if it's Gmail, check Drive for existing accounts before prompting further
372
- console.log("Enter your email address to get started (Gmail recommended for cloud sync).\n");
373
- const email = await prompt("Email address: ");
372
+ // Use --email flag or prompt interactively
373
+ const email = providedEmail || await prompt("Email address (Gmail recommended): ");
374
374
  if (!email || !email.includes("@")) {
375
375
  console.log(`\nNo account added. The UI will show a setup form.`);
376
376
  fs.mkdirSync(mailxDir, { recursive: true });
377
377
  return false;
378
378
  }
379
+ if (providedEmail)
380
+ console.log(`Using email: ${email}`);
379
381
  const domain = email.split("@")[1]?.toLowerCase() || "";
380
382
  let isGoogle = ["gmail.com", "googlemail.com"].includes(domain);
381
383
  if (!isGoogle) {
@@ -597,7 +599,12 @@ async function main() {
597
599
  if (setupMode || !hasConfig()) {
598
600
  if (!setupMode)
599
601
  console.log("No mailx configuration found.");
600
- await runSetup();
602
+ // --email (or -mail) flag skips the interactive prompt
603
+ const emailArg = args.find(a => a.startsWith("--email="))?.split("=")[1]
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);
607
+ await runSetup(emailArg);
601
608
  }
602
609
  // Redirect console to log file — keep terminal clean
603
610
  if (!verbose) {
@@ -703,9 +710,8 @@ async function main() {
703
710
  imapManager.on("accountError", (accountId, error, hint, isOAuth) => {
704
711
  handle.send({ _event: "accountError", type: "accountError", accountId, error, hint, isOAuth });
705
712
  });
706
- // Wait for WebView2 initialization, then signal readiness
707
- await new Promise(r => setTimeout(r, 2000));
708
- 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));
709
715
  // Register all accounts (OAuth may open browser for Gmail — event loop stays free for IPC)
710
716
  for (const account of settings.accounts) {
711
717
  if (!account.enabled)
@@ -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.158",
3
+ "version": "1.0.160",
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.208",
26
+ "@bobfrankston/msger": "^0.1.210",
27
27
  "@capacitor/android": "^8.3.0",
28
28
  "@capacitor/cli": "^8.3.0",
29
29
  "@capacitor/core": "^8.3.0",
@@ -74,6 +74,8 @@ export declare class ImapManager extends EventEmitter {
74
74
  addAccount(account: AccountConfig): Promise<void>;
75
75
  /** Sync folder list for an account */
76
76
  syncFolders(accountId: string, client?: ImapClient): Promise<Folder[]>;
77
+ /** Store a batch of messages to DB immediately — used by onChunk for incremental sync */
78
+ private storeMessages;
77
79
  /** Sync messages for a specific folder */
78
80
  syncFolder(accountId: string, folderId: number, client?: ImapClient): Promise<number>;
79
81
  /** Sync all folders for all accounts */
@@ -383,6 +383,46 @@ export class ImapManager extends EventEmitter {
383
383
  this.emit("folderCountsChanged", accountId, {});
384
384
  return dbFolders;
385
385
  }
386
+ /** Store a batch of messages to DB immediately — used by onChunk for incremental sync */
387
+ storeMessages(accountId, folderId, folder, msgs, highestUid) {
388
+ let stored = 0;
389
+ this.db.beginTransaction();
390
+ try {
391
+ for (const msg of msgs) {
392
+ if (msg.uid <= highestUid)
393
+ continue; // already have it
394
+ const source = msg.source || "";
395
+ let bodyPath = "";
396
+ // Skip body storage during sync — bodies fetched on demand
397
+ const flags = [];
398
+ if (msg.seen)
399
+ flags.push("\\Seen");
400
+ if (msg.flagged)
401
+ flags.push("\\Flagged");
402
+ if (msg.answered)
403
+ flags.push("\\Answered");
404
+ if (msg.draft)
405
+ flags.push("\\Draft");
406
+ this.db.upsertMessage({
407
+ accountId, folderId, uid: msg.uid,
408
+ messageId: msg.messageId || "", inReplyTo: "", references: [],
409
+ date: msg.date instanceof Date ? msg.date.getTime() : (typeof msg.date === "number" ? msg.date : Date.now()),
410
+ subject: msg.subject || "",
411
+ from: toEmailAddress(msg.from?.[0] || {}),
412
+ to: toEmailAddresses(msg.to || []),
413
+ cc: toEmailAddresses(msg.cc || []),
414
+ flags, size: msg.size || 0, hasAttachments: false, preview: "", bodyPath
415
+ });
416
+ stored++;
417
+ }
418
+ this.db.commitTransaction();
419
+ }
420
+ catch (e) {
421
+ this.db.rollbackTransaction();
422
+ console.error(` storeMessages error: ${e.message}`);
423
+ }
424
+ return stored;
425
+ }
386
426
  /** Sync messages for a specific folder */
387
427
  async syncFolder(accountId, folderId, client) {
388
428
  if (!client)
@@ -451,15 +491,25 @@ export class ImapManager extends EventEmitter {
451
491
  }
452
492
  }
453
493
  else {
454
- // First sync: metadata only bodies fetched on demand when user clicks a message
455
- messages = await client.fetchMessageByDate(folder.path, startDate, new Date(), { source: false });
456
- }
457
- // Sort newest first so most recent messages appear in the UI immediately
458
- messages.sort((a, b) => {
459
- const da = a.date instanceof Date ? a.date.getTime() : (typeof a.date === "number" ? a.date : 0);
460
- const db = b.date instanceof Date ? b.date.getTime() : (typeof b.date === "number" ? b.date : 0);
461
- return db - da;
462
- });
494
+ // First sync: fetch in chunks, store each chunk immediately for instant UI
495
+ let totalStored = 0;
496
+ const onChunk = (chunk) => {
497
+ const stored = this.storeMessages(accountId, folderId, folder, chunk, highestUid);
498
+ totalStored += stored;
499
+ if (stored > 0) {
500
+ this.db.recalcFolderCounts(folderId);
501
+ this.emit("folderCountsChanged", accountId, {});
502
+ }
503
+ };
504
+ messages = await client.fetchMessageByDate(folder.path, startDate, new Date(), { source: false }, onChunk);
505
+ if (totalStored > 0) {
506
+ console.log(` ${folder.path}: ${totalStored} messages (streamed)`);
507
+ this.db.recalcFolderCounts(folderId);
508
+ this.emit("folderCountsChanged", accountId, {});
509
+ this.emit("syncProgress", accountId, `sync:${folder.path}`, 100);
510
+ return totalStored;
511
+ }
512
+ }
463
513
  if (messages.length > 0)
464
514
  console.log(` ${folder.path}: ${messages.length} new messages`);
465
515
  let newCount = 0;