@bobfrankston/mailx 1.0.349 → 1.0.351

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.
@@ -236,8 +236,13 @@ export async function openTasks() {
236
236
  document.removeEventListener("keydown", onKey, true);
237
237
  isOpen = false;
238
238
  };
239
+ // Esc always closes. Earlier guard blocked Esc when focus was in the
240
+ // quickadd input, which made the panel feel stuck — user hits Esc to
241
+ // dismiss and nothing happens. Blur-on-Esc before close is the right
242
+ // behavior: the close handler is idempotent and the input loses focus
243
+ // when the backdrop goes away anyway.
239
244
  const onKey = (e) => {
240
- if (e.key === "Escape" && document.activeElement?.tagName !== "INPUT") {
245
+ if (e.key === "Escape") {
241
246
  e.stopPropagation();
242
247
  e.preventDefault();
243
248
  close();
@@ -595,32 +595,66 @@ document.getElementById("btn-send")?.addEventListener("click", () => {
595
595
  return;
596
596
  }
597
597
  console.log(`[compose] Send clicked: from=${body.from} to=${JSON.stringify(body.to)} subject="${body.subject}" attachments=${body.attachments.length}`);
598
- // Stop autosave so it can't write a fresh draft after we close.
599
- if (draftTimer) {
600
- clearInterval(draftTimer);
601
- draftTimer = null;
602
- }
603
- // Fire-and-forget. The disk-write is the durable commit; queue depth +
604
- // retry status are visible in the parent status bar. No modal dialogs.
598
+ // Wait for the IPC round-trip before closing compose. The IPC is supposed
599
+ // to be <100ms (Node validates + disk-writes synchronously inside send()
600
+ // then returns). If it throws OR takes too long, the user keeps their
601
+ // typed message and sees the error inline instead of losing it to a
602
+ // fire-and-forget that never landed. Earlier fire-and-forget version lost
603
+ // messages silently when anything upstream of disk write broke.
604
+ const sendBtn = document.getElementById("btn-send");
605
+ if (sendBtn) {
606
+ sendBtn.disabled = true;
607
+ sendBtn.textContent = "Sending…";
608
+ }
609
+ const statusEl = document.getElementById("compose-status");
610
+ if (statusEl)
611
+ statusEl.textContent = "";
605
612
  const sendStart = Date.now();
606
- sendMessage(body)
613
+ let ipcPromise;
614
+ try {
615
+ ipcPromise = sendMessage(body);
616
+ }
617
+ catch (e) {
618
+ const msg = e?.message || String(e);
619
+ console.error(`[compose] Send threw synchronously: ${msg}`);
620
+ if (sendBtn) {
621
+ sendBtn.disabled = false;
622
+ sendBtn.textContent = "Send";
623
+ }
624
+ if (statusEl)
625
+ statusEl.textContent = `Send failed: ${msg}`;
626
+ else
627
+ alert(`Send failed: ${msg}`);
628
+ return;
629
+ }
630
+ Promise.resolve(ipcPromise)
607
631
  .then(() => {
608
632
  console.log(`[compose] Send IPC returned OK in ${Date.now() - sendStart}ms`);
633
+ // Stop autosave only after ACK — if send threw we want the draft
634
+ // autosave to keep the message safe.
635
+ if (draftTimer) {
636
+ clearInterval(draftTimer);
637
+ draftTimer = null;
638
+ }
609
639
  if (draftUid || draftId) {
610
640
  deleteDraft(getFromAccountId(), draftUid || 0, draftId || "").catch(() => { });
611
641
  }
642
+ closeCompose();
612
643
  })
613
644
  .catch((e) => {
614
645
  const msg = e?.message || String(e);
615
646
  console.error(`[compose] Send IPC failed after ${Date.now() - sendStart}ms: ${msg}`);
616
- // Forward to parent so it surfaces in the status bar / banner — no
617
- // alert here because the compose window is already closed.
647
+ if (sendBtn) {
648
+ sendBtn.disabled = false;
649
+ sendBtn.textContent = "Send";
650
+ }
651
+ if (statusEl)
652
+ statusEl.textContent = `Send failed: ${msg}`;
618
653
  try {
619
654
  parent.postMessage({ type: "mailx-send-error", message: msg, accountId: body.from }, "*");
620
655
  }
621
656
  catch { /* */ }
622
657
  });
623
- closeCompose();
624
658
  });
625
659
  // ── Close handling ──
626
660
  /** True if the compose has anything worth asking about. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/mailx",
3
- "version": "1.0.349",
3
+ "version": "1.0.351",
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,7 +20,7 @@
20
20
  "postinstall": "node bin/postinstall.js"
21
21
  },
22
22
  "dependencies": {
23
- "@bobfrankston/iflow-direct": "^0.1.24",
23
+ "@bobfrankston/iflow-direct": "^0.1.25",
24
24
  "@bobfrankston/iflow-node": "^0.1.7",
25
25
  "@bobfrankston/miscinfo": "^1.0.9",
26
26
  "@bobfrankston/oauthsupport": "^1.0.24",
@@ -84,7 +84,7 @@
84
84
  },
85
85
  ".transformedSnapshot": {
86
86
  "dependencies": {
87
- "@bobfrankston/iflow-direct": "^0.1.24",
87
+ "@bobfrankston/iflow-direct": "^0.1.25",
88
88
  "@bobfrankston/iflow-node": "^0.1.7",
89
89
  "@bobfrankston/miscinfo": "^1.0.9",
90
90
  "@bobfrankston/oauthsupport": "^1.0.24",
@@ -2471,6 +2471,12 @@ export class ImapManager extends EventEmitter {
2471
2471
  * work and risked double-send when both paths fired on the same message. */
2472
2472
  queueOutgoingLocal(accountId, rawMessage) {
2473
2473
  // Loud logging so a "vanished message" report is diagnosable from the log alone.
2474
+ // ALWAYS leave a backup copy in sending/<acct>/attempted/ first — unconditionally,
2475
+ // before the outbox write. The outbox .ltr may be claimed/consumed by the worker
2476
+ // within milliseconds; this copy survives regardless of SMTP success, IMAP
2477
+ // append, worker crash, or any other downstream failure. User asked for this
2478
+ // as a fallback because "there isn't even the backup copy in sent".
2479
+ this.saveSendingCopy(accountId, rawMessage, "attempted");
2474
2480
  const outboxDir = path.join(getConfigDir(), "outbox", accountId);
2475
2481
  try {
2476
2482
  fs.mkdirSync(outboxDir, { recursive: true });