@bobfrankston/mailx 1.0.180 → 1.0.182
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
|
@@ -28,6 +28,20 @@ function hasFlag(name) { return args.includes(`-${name}`) || args.includes(`--${
|
|
|
28
28
|
const serverMode = hasFlag("server");
|
|
29
29
|
const noBrowser = hasFlag("no-browser");
|
|
30
30
|
const verbose = hasFlag("verbose");
|
|
31
|
+
const isDaemon = hasFlag("daemon"); // internal: re-spawned detached process
|
|
32
|
+
// Auto-detach: re-spawn as background process so terminal returns immediately
|
|
33
|
+
// Skip for: --verbose (want console), --server (needs terminal), --daemon (already detached),
|
|
34
|
+
// and any command flags (setup, kill, test, etc.)
|
|
35
|
+
if (!verbose && !serverMode && !isDaemon && !process.argv.slice(2).some(a => /^-/.test(a) && !["--no-browser"].includes(a))) {
|
|
36
|
+
const { spawn } = await import("node:child_process");
|
|
37
|
+
const child = spawn(process.execPath, [...process.argv.slice(1), "--daemon"], {
|
|
38
|
+
detached: true,
|
|
39
|
+
stdio: "ignore",
|
|
40
|
+
windowsHide: true,
|
|
41
|
+
});
|
|
42
|
+
child.unref();
|
|
43
|
+
process.exit(0);
|
|
44
|
+
}
|
|
31
45
|
const setupMode = hasFlag("setup");
|
|
32
46
|
const addMode = hasFlag("add");
|
|
33
47
|
const testMode = hasFlag("test");
|
|
@@ -35,7 +49,7 @@ const rebuildMode = hasFlag("rebuild");
|
|
|
35
49
|
const repairMode = hasFlag("repair");
|
|
36
50
|
const importMode = hasFlag("import");
|
|
37
51
|
// Validate arguments
|
|
38
|
-
const knownFlags = ["server", "no-browser", "verbose", "external", "kill", "v", "version", "setup", "add", "test", "rebuild", "repair", "native-imap", "log", "import", "email", "mail"];
|
|
52
|
+
const knownFlags = ["server", "no-browser", "verbose", "external", "kill", "v", "version", "setup", "add", "test", "rebuild", "repair", "native-imap", "log", "import", "email", "mail", "daemon"];
|
|
39
53
|
for (const arg of args) {
|
|
40
54
|
const flag = arg.replace(/^--?/, "");
|
|
41
55
|
if (arg.startsWith("-") && !knownFlags.includes(flag)) {
|
|
@@ -665,6 +679,10 @@ async function main() {
|
|
|
665
679
|
// Pass server version to dispatch so getVersion returns it
|
|
666
680
|
const rootPkg = JSON.parse(fs.readFileSync(path.join(import.meta.dirname, "..", "package.json"), "utf-8"));
|
|
667
681
|
handle.onRequest(async (req) => {
|
|
682
|
+
if (!req._action) {
|
|
683
|
+
console.log(`[ipc] ← ignored (no _action): ${JSON.stringify(req).substring(0, 100)}`);
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
668
686
|
console.log(`[ipc] ← ${req._action} (${req._cbid})`);
|
|
669
687
|
try {
|
|
670
688
|
const response = await dispatch(svc, req);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"height":
|
|
1
|
+
{"height":1344,"width":1438,"x":216,"y":107}
|
package/client/app.js
CHANGED
|
@@ -265,12 +265,31 @@ async function openCompose(mode) {
|
|
|
265
265
|
references: [],
|
|
266
266
|
accounts: accounts.map((a) => ({ id: a.id, name: a.name, email: a.email })),
|
|
267
267
|
};
|
|
268
|
+
// Auto-detect reply From: if the message was delivered to an identity domain,
|
|
269
|
+
// reply from that address instead of the default account address.
|
|
270
|
+
// Identity domains configured per-account in accounts.jsonc (identityDomains array).
|
|
271
|
+
// Default identity domains for bob.ma account:
|
|
272
|
+
const identityDomains = ["bob.ma", "frankston.com"];
|
|
273
|
+
function detectReplyFrom() {
|
|
274
|
+
if (!msg)
|
|
275
|
+
return undefined;
|
|
276
|
+
// Check deliveredTo first (most reliable), then To addresses
|
|
277
|
+
const candidates = [msg.deliveredTo, ...(msg.to || []).map((a) => a.address)].filter(Boolean);
|
|
278
|
+
for (const addr of candidates) {
|
|
279
|
+
const domain = addr.split("@")[1]?.toLowerCase();
|
|
280
|
+
if (domain && identityDomains.some(d => domain === d || domain.endsWith(`.${d}`))) {
|
|
281
|
+
return addr;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return undefined;
|
|
285
|
+
}
|
|
268
286
|
if (msg && mode === "reply") {
|
|
269
287
|
init.to = [msg.from];
|
|
270
288
|
init.subject = `Re: ${cleanSubject}`;
|
|
271
289
|
init.bodyHtml = quoteBody(msg);
|
|
272
290
|
init.inReplyTo = msg.messageId;
|
|
273
291
|
init.references = [...(msg.references || []), msg.messageId];
|
|
292
|
+
init.fromAddress = detectReplyFrom();
|
|
274
293
|
}
|
|
275
294
|
else if (msg && mode === "replyAll") {
|
|
276
295
|
init.to = [msg.from, ...msg.to.filter((a) => a.address !== msg.from.address)];
|
|
@@ -279,6 +298,7 @@ async function openCompose(mode) {
|
|
|
279
298
|
init.bodyHtml = quoteBody(msg);
|
|
280
299
|
init.inReplyTo = msg.messageId;
|
|
281
300
|
init.references = [...(msg.references || []), msg.messageId];
|
|
301
|
+
init.fromAddress = detectReplyFrom();
|
|
282
302
|
}
|
|
283
303
|
else if (msg && mode === "forward") {
|
|
284
304
|
init.subject = `Fwd: ${cleanSubject}`;
|
|
@@ -565,22 +585,10 @@ onWsEvent((event) => {
|
|
|
565
585
|
break;
|
|
566
586
|
}
|
|
567
587
|
case "folderCountsChanged": {
|
|
568
|
-
//
|
|
588
|
+
// Update folder badges only — never reload the message list or touch the viewer.
|
|
589
|
+
// The list refreshes when the user clicks a folder or presses Sync.
|
|
569
590
|
updateFolderCounts();
|
|
570
591
|
updateNewMessageCount();
|
|
571
|
-
// Only reload message list if the synced account is the one we're viewing
|
|
572
|
-
// (or unified inbox which shows all accounts). Debounce to avoid rapid reloads
|
|
573
|
-
// during first sync which emits per-batch.
|
|
574
|
-
const syncedAccount = event.accountId;
|
|
575
|
-
const viewingThis = !currentAccountId || currentAccountId === syncedAccount;
|
|
576
|
-
if (viewingThis) {
|
|
577
|
-
if (reloadDebounceTimer)
|
|
578
|
-
clearTimeout(reloadDebounceTimer);
|
|
579
|
-
reloadDebounceTimer = setTimeout(() => {
|
|
580
|
-
reloadDebounceTimer = null;
|
|
581
|
-
reloadCurrentFolder();
|
|
582
|
-
}, 500);
|
|
583
|
-
}
|
|
584
592
|
// Sync finished — re-enable sync button
|
|
585
593
|
const syncBtn = document.getElementById("btn-sync");
|
|
586
594
|
if (syncBtn) {
|
|
@@ -17,16 +17,23 @@ export function getCurrentMessage() {
|
|
|
17
17
|
/** Initialize viewer — subscribe to state changes */
|
|
18
18
|
export function initViewer() {
|
|
19
19
|
state.subscribe((change) => {
|
|
20
|
-
if (change === "removed"
|
|
20
|
+
if (change === "removed") {
|
|
21
|
+
// Message was deleted/moved — show auto-selected next, or clear
|
|
21
22
|
const sel = state.getSelected();
|
|
22
23
|
if (!sel) {
|
|
23
24
|
clearViewer();
|
|
24
25
|
}
|
|
25
26
|
else if (sel.uid !== currentMessage?.uid || sel.accountId !== currentAccountId) {
|
|
26
|
-
// State auto-selected a new message after removal — show it
|
|
27
27
|
showMessage(sel.accountId, sel.uid, sel.folderId);
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
|
+
else if (change === "selected") {
|
|
31
|
+
// Explicit deselect (folder switch, clearViewer)
|
|
32
|
+
if (!state.getSelected()) {
|
|
33
|
+
clearViewer();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
// "messages" change (sync reload) — don't touch the viewer
|
|
30
37
|
});
|
|
31
38
|
}
|
|
32
39
|
function clearViewer() {
|
|
@@ -260,6 +260,13 @@ function parseAddrs(s) {
|
|
|
260
260
|
function applyInit(init) {
|
|
261
261
|
// Populate From dropdown
|
|
262
262
|
populateFromSelect(init.accounts, init.accountId);
|
|
263
|
+
// Auto-detect reply From: if fromAddress is set (identity domain match),
|
|
264
|
+
// use it as the From address via the "Other..." custom field
|
|
265
|
+
if (init.fromAddress) {
|
|
266
|
+
fromSelect.value = "__custom__";
|
|
267
|
+
fromCustom.hidden = false;
|
|
268
|
+
fromCustom.value = init.fromAddress;
|
|
269
|
+
}
|
|
263
270
|
toInput.value = formatAddrs(init.to);
|
|
264
271
|
ccInput.value = formatAddrs(init.cc);
|
|
265
272
|
subjectInput.value = init.subject;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bobfrankston/mailx",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.182",
|
|
4
4
|
"description": "Local-first email client with IMAP sync and standalone native app",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "bin/mailx.js",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"@bobfrankston/iflow-node": "^0.1.2",
|
|
25
25
|
"@bobfrankston/miscinfo": "^1.0.8",
|
|
26
26
|
"@bobfrankston/oauthsupport": "^1.0.21",
|
|
27
|
-
"@bobfrankston/msger": "^0.1.
|
|
27
|
+
"@bobfrankston/msger": "^0.1.232",
|
|
28
28
|
"@capacitor/android": "^8.3.0",
|
|
29
29
|
"@capacitor/cli": "^8.3.0",
|
|
30
30
|
"@capacitor/core": "^8.3.0",
|
|
@@ -106,7 +106,7 @@ export class MailxService {
|
|
|
106
106
|
for (const cfg of settings.accounts) {
|
|
107
107
|
const a = dbAccounts.find(d => d.id === cfg.id);
|
|
108
108
|
if (a)
|
|
109
|
-
ordered.push({ ...a, label: cfg.label, defaultSend: cfg.defaultSend || false });
|
|
109
|
+
ordered.push({ ...a, label: cfg.label, defaultSend: cfg.defaultSend || false, identityDomains: cfg.identityDomains || [] });
|
|
110
110
|
}
|
|
111
111
|
// Append any DB accounts not in settings
|
|
112
112
|
for (const a of dbAccounts) {
|
|
@@ -339,7 +339,9 @@ export class MailxService {
|
|
|
339
339
|
const account = settings.accounts.find(a => a.id === msg.from);
|
|
340
340
|
if (!account)
|
|
341
341
|
throw new Error(`Unknown account: ${msg.from}`);
|
|
342
|
-
|
|
342
|
+
// Use custom From address if set (identity domain reply), but always wrap with account display name
|
|
343
|
+
const fromAddr = msg.fromAddress || account.email;
|
|
344
|
+
const fromHeader = `${account.name} <${fromAddr}>`;
|
|
343
345
|
const to = msg.to.map((a) => a.name ? `${a.name} <${a.address}>` : a.address).join(", ");
|
|
344
346
|
const cc = msg.cc?.map((a) => a.name ? `${a.name} <${a.address}>` : a.address).join(", ");
|
|
345
347
|
const bcc = msg.bcc?.map((a) => a.name ? `${a.name} <${a.address}>` : a.address).join(", ");
|