@bobfrankston/mailx 1.0.158 → 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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/mailx",
3
- "version": "1.0.158",
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",
@@ -23,7 +23,7 @@
23
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.208",
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)
@@ -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;