@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 +2 -2
- package/bin/mailx.js +15 -9
- package/client/lib/mailxapi.js +35 -28
- package/package.json +3 -3
- package/packages/mailx-imap/index.d.ts +2 -0
- package/packages/mailx-imap/index.js +59 -9
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 --
|
|
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
|
-
//
|
|
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,12 @@ async function main() {
|
|
|
597
599
|
if (setupMode || !hasConfig()) {
|
|
598
600
|
if (!setupMode)
|
|
599
601
|
console.log("No mailx configuration found.");
|
|
600
|
-
|
|
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
|
-
//
|
|
707
|
-
await new Promise(r => setTimeout(r,
|
|
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)
|
package/client/lib/mailxapi.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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:
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
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;
|