@bobfrankston/mailx 1.0.157 → 1.0.159

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 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"];
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,10 @@ 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 flag skips the interactive prompt
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);
605
+ await runSetup(emailArg);
601
606
  }
602
607
  // Redirect console to log file — keep terminal clean
603
608
  if (!verbose) {
@@ -42,6 +42,10 @@
42
42
  function flushPending() {
43
43
  _ready = true;
44
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
+ }
45
49
  for (var i = 0; i < pending.length; i++) {
46
50
  if (window.ipc && window.ipc.postMessage) {
47
51
  window.ipc.postMessage(JSON.stringify(pending[i]));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/mailx",
3
- "version": "1.0.157",
3
+ "version": "1.0.159",
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.53",
23
+ "@bobfrankston/iflow": "^1.0.54",
24
24
  "@bobfrankston/miscinfo": "^1.0.7",
25
25
  "@bobfrankston/oauthsupport": "^1.0.20",
26
- "@bobfrankston/msger": "^0.1.207",
26
+ "@bobfrankston/msger": "^0.1.209",
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)
@@ -394,6 +434,7 @@ export class ImapManager extends EventEmitter {
394
434
  this.emit("syncProgress", accountId, `sync:${folder.path}`, 0);
395
435
  // Get the highest UID we already have for this folder
396
436
  const highestUid = this.db.getHighestUid(accountId, folderId);
437
+ console.log(` [sync] ${accountId}/${folder.path}: highestUid=${highestUid}, fetching...`);
397
438
  let messages;
398
439
  const firstSync = highestUid === 0;
399
440
  const historyDays = getHistoryDays(accountId);
@@ -450,15 +491,25 @@ export class ImapManager extends EventEmitter {
450
491
  }
451
492
  }
452
493
  else {
453
- // First sync: metadata only bodies fetched on demand when user clicks a message
454
- messages = await client.fetchMessageByDate(folder.path, startDate, new Date(), { source: false });
455
- }
456
- // Sort newest first so most recent messages appear in the UI immediately
457
- messages.sort((a, b) => {
458
- const da = a.date instanceof Date ? a.date.getTime() : (typeof a.date === "number" ? a.date : 0);
459
- const db = b.date instanceof Date ? b.date.getTime() : (typeof b.date === "number" ? b.date : 0);
460
- return db - da;
461
- });
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
+ }
462
513
  if (messages.length > 0)
463
514
  console.log(` ${folder.path}: ${messages.length} new messages`);
464
515
  let newCount = 0;
@@ -613,15 +664,21 @@ export class ImapManager extends EventEmitter {
613
664
  // Step 2: Sync INBOX first
614
665
  const inbox = folders.find(f => f.specialUse === "inbox");
615
666
  if (inbox) {
667
+ console.log(` [sync] ${accountId}: starting INBOX sync (folder ${inbox.id})`);
616
668
  try {
617
669
  client = await this.getOpsClient(accountId);
670
+ console.log(` [sync] ${accountId}: got client, calling syncFolder for INBOX`);
618
671
  await this.syncFolder(accountId, inbox.id, client);
672
+ console.log(` [sync] ${accountId}: INBOX sync complete`);
619
673
  }
620
674
  catch (e) {
621
675
  console.error(` Inbox sync error for ${accountId}: ${e.message}`);
622
676
  await this.reconnectOps(accountId);
623
677
  }
624
678
  }
679
+ else {
680
+ console.log(` [sync] ${accountId}: no INBOX folder found`);
681
+ }
625
682
  // Step 3: Sync remaining folders
626
683
  const remaining = folders.filter(f => f.specialUse !== "inbox");
627
684
  remaining.sort((a, b) => {