@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 +11 -6
- package/client/lib/mailxapi.js +4 -0
- package/package.json +3 -3
- package/packages/mailx-imap/index.d.ts +2 -0
- package/packages/mailx-imap/index.js +66 -9
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
|
-
//
|
|
372
|
-
|
|
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
|
-
|
|
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/client/lib/mailxapi.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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:
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
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) => {
|