@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":1047,"width":1844,"x":531,"y":264}
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
- // Incremental count updateno DOM rebuild, no jitter
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" || change === "messages") {
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.180",
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.230",
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
- const fromHeader = msg.fromAddress || `${account.name} <${account.email}>`;
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(", ");