@bobfrankston/mailx 1.0.339 → 1.0.348

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
@@ -130,6 +130,8 @@ if (!isDaemon && !__isCommandInvocation) {
130
130
  if (!verbose && !isDaemon && !process.argv.slice(2).some(a => /^-/.test(a))) {
131
131
  const { spawn } = await import("node:child_process");
132
132
  const child = spawn(process.execPath, [...process.argv.slice(1), "--daemon"], {
133
+ // windowsHide on the spawn options below — prevents the brief
134
+ // console-window flash when the daemon launches.
133
135
  detached: true,
134
136
  stdio: "ignore",
135
137
  windowsHide: true,
@@ -909,6 +911,53 @@ async function main() {
909
911
  }
910
912
  }
911
913
  const db = new MailxDB(getConfigDir());
914
+ // Auto-create the sending/ recovery README on every startup. Stays in
915
+ // sync with the running version of mailx; user can ignore once the
916
+ // disk-staging fallback is no longer needed.
917
+ try {
918
+ const sendingDir = path.join(getConfigDir(), "sending");
919
+ fs.mkdirSync(sendingDir, { recursive: true });
920
+ const readmePath = path.join(sendingDir, "README.md");
921
+ const readmeBody = `# \`~/.mailx/sending/\` and \`~/.mailx/outbox/\` — outgoing-mail staging
922
+
923
+ Auto-generated by mailx on startup. Manual recovery reference for when mailx is broken or you need to feed an outgoing message into another mail program.
924
+
925
+ ## Layout
926
+
927
+ \`\`\`
928
+ ~/.mailx/
929
+ ├── outbox/<account>/
930
+ │ └── *.ltr ← THE QUEUE. Worker scans every 10s, sends, deletes on success.
931
+ └── sending/<account>/
932
+ ├── editing/ ← Last 3 draft autosaves while composing.
933
+ ├── queued/ ← Manual drop-in / crash-recovery copies.
934
+ └── sent/ ← Audit trail of successfully sent messages.
935
+ \`\`\`
936
+
937
+ In-flight files are atomically renamed to \`<file>.sending-<host>-<pid>\` while the worker is processing them — same-machine claim so two mailx instances don't double-send. Stale claims (dead PIDs on this host) are recovered on the next tick.
938
+
939
+ ## Manual fallback
940
+
941
+ - **mailx is dead, need to send a draft** — most recent file in \`sending/<account>/editing/\` is a complete RFC 822 message; copy the body into another mail client and resend.
942
+ - **Feed a raw .eml to mailx** — drop into \`sending/<account>/queued/\`. Picked up within 10s.
943
+ - **mailx says queued but server doesn't have it** — look in \`outbox/<account>/\`. \`.ltr\` still there → worker hasn't sent yet (check \`~/.mailx/logs/\`). \`.sending-<host>-<pid>\` → in flight. Gone → success.
944
+
945
+ ## Format
946
+
947
+ RFC 5322 with CRLF line endings. Bodies are quoted-printable encoded (readable in a text editor). Every message carries \`Message-ID:\` for cross-device dedup; \`X-Mailx-Retry\` marks retry attempts.
948
+ `;
949
+ // Only rewrite if content drifted (avoids gratuitous mtime updates).
950
+ let existing = "";
951
+ try {
952
+ existing = fs.readFileSync(readmePath, "utf-8");
953
+ }
954
+ catch { /* missing */ }
955
+ if (existing !== readmeBody)
956
+ fs.writeFileSync(readmePath, readmeBody);
957
+ }
958
+ catch (e) {
959
+ console.error(` [readme] Could not write sending README: ${e?.message || e}`);
960
+ }
912
961
  const { NodeTcpTransport } = await import("@bobfrankston/node-tcp-transport");
913
962
  const imapManager = new ImapManager(db, () => new NodeTcpTransport());
914
963
  // Native client is the only option (iflow-direct)
@@ -1023,7 +1072,7 @@ async function main() {
1023
1072
  try {
1024
1073
  clearInstanceFile();
1025
1074
  const { spawn: spawnChild } = await import("child_process");
1026
- const child = spawnChild("mailx", [], { detached: true, stdio: "ignore", shell: true });
1075
+ const child = spawnChild("mailx", [], { detached: true, stdio: "ignore", shell: true, windowsHide: true });
1027
1076
  child.unref();
1028
1077
  console.log(" [restart] Spawned fresh daemon; shutting down current");
1029
1078
  // Give the spawn a moment to take hold before we start
@@ -1043,10 +1092,10 @@ async function main() {
1043
1092
  try {
1044
1093
  const { execSync, spawn: spawnChild } = await import("child_process");
1045
1094
  console.log(" [update] Installing latest version...");
1046
- execSync("npm install -g @bobfrankston/mailx", { encoding: "utf-8", timeout: 120_000, stdio: "inherit" });
1095
+ execSync("npm install -g @bobfrankston/mailx", { encoding: "utf-8", timeout: 120_000, stdio: "inherit", windowsHide: true });
1047
1096
  console.log(" [update] Install complete — relaunching");
1048
1097
  // Spawn the new version detached so it outlives this process
1049
- const child = spawnChild("mailx", [], { detached: true, stdio: "ignore", shell: true });
1098
+ const child = spawnChild("mailx", [], { detached: true, stdio: "ignore", shell: true, windowsHide: true });
1050
1099
  child.unref();
1051
1100
  }
1052
1101
  catch (e) {
@@ -1055,13 +1104,18 @@ async function main() {
1055
1104
  gracefulShutdown("Update applied");
1056
1105
  return;
1057
1106
  }
1107
+ // Per-action wall-clock timing so a "took N seconds" report tells us
1108
+ // where between Rust→stdin→dispatch→service the time actually went.
1109
+ const ipcT0 = Date.now();
1058
1110
  try {
1059
1111
  const response = await dispatch(svc, req);
1060
- console.log(`[ipc] ${req._action} (${req._cbid}) ok`);
1112
+ const elapsed = Date.now() - ipcT0;
1113
+ console.log(`[ipc] → ${req._action} (${req._cbid}) ok in ${elapsed}ms`);
1061
1114
  handle.send(response);
1062
1115
  }
1063
1116
  catch (e) {
1064
- console.error(`[ipc] ${req._action} (${req._cbid}) error: ${e.message}`);
1117
+ const elapsed = Date.now() - ipcT0;
1118
+ console.error(`[ipc] → ${req._action} (${req._cbid}) error in ${elapsed}ms: ${e.message}`);
1065
1119
  handle.send({ _cbid: req._cbid, error: e.message });
1066
1120
  }
1067
1121
  });
@@ -1119,6 +1173,9 @@ async function main() {
1119
1173
  imapManager.on("configChanged", (filename) => {
1120
1174
  handle.send({ _event: "configChanged", type: "configChanged", filename });
1121
1175
  });
1176
+ imapManager.on("outboxStatus", (status) => {
1177
+ handle.send({ _event: "outboxStatus", type: "outboxStatus", ...status });
1178
+ });
1122
1179
  // syncComplete drives the folder-tree refresh that picks up newly-discovered
1123
1180
  // folders on first run (Gmail accounts have no folders in the DB until the
1124
1181
  // first sync fetches the labels). Without this forward, the UI shows the
@@ -1237,8 +1294,31 @@ async function main() {
1237
1294
  const UPDATE_CHECK_MS = 30 * 60_000; // 30 minutes
1238
1295
  async function checkForUpdate() {
1239
1296
  try {
1240
- const { execSync } = await import("child_process");
1241
- const latest = execSync("npm view @bobfrankston/mailx version", { encoding: "utf-8", timeout: 15_000 }).trim();
1297
+ // spawn with windowsHide:true — execSync briefly flashes a cmd
1298
+ // window on Windows every time the periodic check fires.
1299
+ const { spawn } = await import("child_process");
1300
+ const latest = await new Promise((resolve, reject) => {
1301
+ const child = spawn("npm", ["view", "@bobfrankston/mailx", "version"], {
1302
+ windowsHide: true,
1303
+ shell: true,
1304
+ });
1305
+ let out = "";
1306
+ let err = "";
1307
+ child.stdout.on("data", (d) => { out += d.toString(); });
1308
+ child.stderr.on("data", (d) => { err += d.toString(); });
1309
+ const killer = setTimeout(() => { try {
1310
+ child.kill();
1311
+ }
1312
+ catch { /* */ } reject(new Error("npm view timed out")); }, 15_000);
1313
+ child.on("error", (e) => { clearTimeout(killer); reject(e); });
1314
+ child.on("exit", (code) => {
1315
+ clearTimeout(killer);
1316
+ if (code === 0)
1317
+ resolve(out.trim());
1318
+ else
1319
+ reject(new Error(err.trim() || `npm view exit ${code}`));
1320
+ });
1321
+ });
1242
1322
  const current = rootPkgVersion;
1243
1323
  if (latest && latest !== current) {
1244
1324
  console.log(` [update] New version available: ${current} → ${latest}`);