@bobfrankston/rmfmail 1.2.282 → 1.2.284
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/.commitmsg +37 -62
- package/bin/mailx.js +122 -5
- package/bin/mailx.js.map +1 -1
- package/bin/mailx.ts +110 -5
- package/client/app.bundle.js +3 -3
- package/client/app.bundle.js.map +2 -2
- package/client/components/message-viewer.js +15 -14
- package/client/components/message-viewer.js.map +1 -1
- package/client/components/message-viewer.ts +15 -14
- package/client/styles/components.css +6 -5
- package/npmchanges.md +85 -0
- package/package.json +5 -5
- /package/packages/mailx-imap/{node_modules.npmglobalize-stash-56556 → node_modules.npmglobalize-stash-83688}/.package-lock.json +0 -0
package/bin/mailx.ts
CHANGED
|
@@ -474,7 +474,17 @@ function readInstanceFile(): InstanceFile | null {
|
|
|
474
474
|
function writeInstanceFile(pid: number, childPids: number[] = []): void {
|
|
475
475
|
try {
|
|
476
476
|
fs.mkdirSync(path.dirname(__instanceFile), { recursive: true });
|
|
477
|
-
|
|
477
|
+
// startedAt must survive a childPids update. It was recomputed on every
|
|
478
|
+
// write, and since every popup and popout window rewrites this file,
|
|
479
|
+
// the field reported the last window event rather than when the daemon
|
|
480
|
+
// started — actively misleading when diagnosing "how long has this been
|
|
481
|
+
// up" or "which of these daemons is the old one" (it cost a wrong turn
|
|
482
|
+
// on 2026-08-27). Keep the existing value whenever the PID is unchanged.
|
|
483
|
+
const prev = readInstanceFile();
|
|
484
|
+
const startedAt = prev && prev.pid === pid && typeof prev.startedAt === "number"
|
|
485
|
+
? prev.startedAt
|
|
486
|
+
: Date.now();
|
|
487
|
+
const payload: InstanceFile = { pid, version: __selfVersion, startedAt, childPids };
|
|
478
488
|
fs.writeFileSync(__instanceFile, JSON.stringify(payload, null, 2));
|
|
479
489
|
} catch { /* non-fatal */ }
|
|
480
490
|
}
|
|
@@ -543,6 +553,17 @@ const __handoffStamp = path.join(path.dirname(__instanceFile), "last-handoff.sta
|
|
|
543
553
|
function touchHandoffStamp(): void {
|
|
544
554
|
try { fs.writeFileSync(__handoffStamp, String(Date.now())); } catch { /* */ }
|
|
545
555
|
}
|
|
556
|
+
|
|
557
|
+
/** "Someone launched me while you are already running — show yourself."
|
|
558
|
+
*
|
|
559
|
+
* A file rather than a socket, for the same reason the mailto and share
|
|
560
|
+
* handoffs use one: the launching process is a fresh node that will exit in
|
|
561
|
+
* milliseconds, and the daemon is already watching this directory. The daemon
|
|
562
|
+
* side deletes it and sends the native focus command. */
|
|
563
|
+
const __activateFile = path.join(path.dirname(__instanceFile), "pending-activate.json");
|
|
564
|
+
function requestActivate(): void {
|
|
565
|
+
try { fs.writeFileSync(__activateFile, JSON.stringify({ at: Date.now(), fromPid: process.pid })); } catch { /* */ }
|
|
566
|
+
}
|
|
546
567
|
if (!isDaemon && !__isCommandInvocation && !__keepOthers) {
|
|
547
568
|
// Handoff launches must NOT replace a live daemon. rmfshare.exe (share
|
|
548
569
|
// sheet) and rmfmailto.exe (mailto links) write a pending file and spawn
|
|
@@ -567,12 +588,36 @@ if (!isDaemon && !__isCommandInvocation && !__keepOthers) {
|
|
|
567
588
|
}
|
|
568
589
|
}
|
|
569
590
|
}
|
|
570
|
-
//
|
|
591
|
+
// Activate-on-launch: launching the SAME version that is already running
|
|
592
|
+
// raises the window that exists instead of replacing it.
|
|
593
|
+
//
|
|
594
|
+
// Replace-on-launch (below) was written for the upgrade case — an older or
|
|
595
|
+
// orphaned daemon surviving a partial upgrade and leaving two windows on
|
|
596
|
+
// two versions. But it fired on EVERY launch, including the ordinary one:
|
|
597
|
+
// press the taskbar icon, the new process SIGTERMs the running daemon, its
|
|
598
|
+
// window vanishes, and seconds later a fresh one appears. From the outside
|
|
599
|
+
// that reads as the app flickering out of existence for no reason (Bob
|
|
600
|
+
// 2026-08-26: "why does it show and disappear... you should still put me in
|
|
601
|
+
// the running version when I press the icon on the task bar").
|
|
602
|
+
//
|
|
603
|
+
// instance.json already records the running version, so the two cases are
|
|
604
|
+
// distinguishable without guessing: same version means the user wants the
|
|
605
|
+
// app, so surface it; a different version means an upgrade landed and the
|
|
606
|
+
// old daemon must go. `-another` still opts out entirely, and every handoff
|
|
607
|
+
// path above still returns before reaching here.
|
|
608
|
+
{
|
|
609
|
+
const inst0 = readInstanceFile();
|
|
610
|
+
if (inst0 && pidIsMailx(inst0.pid) && inst0.pid !== process.pid && inst0.version === __selfVersion) {
|
|
611
|
+
requestActivate();
|
|
612
|
+
console.log(`rmfmail: already running (PID ${inst0.pid}, v${inst0.version}) — raising its window`);
|
|
613
|
+
process.exit(0);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// Replace-on-launch: a daemon running a DIFFERENT version gets killed and
|
|
571
618
|
// the new one takes over. instance.json only tracks ONE PID — older or
|
|
572
619
|
// orphaned daemons can survive a partial upgrade and leave the user with
|
|
573
|
-
// multiple windows on different versions.
|
|
574
|
-
// rmfmail-looking node process and kill it (unconditionally — version
|
|
575
|
-
// match alone isn't enough; we always want exactly one daemon).
|
|
620
|
+
// multiple windows on different versions.
|
|
576
621
|
const myPid = process.pid;
|
|
577
622
|
const killedPids: number[] = [];
|
|
578
623
|
const inst = readInstanceFile();
|
|
@@ -633,6 +678,29 @@ if (!verbose && !isDaemon && !process.argv.slice(2).some(a => /^-/.test(a))) {
|
|
|
633
678
|
windowsHide: true,
|
|
634
679
|
});
|
|
635
680
|
child.unref();
|
|
681
|
+
// Claim the instance slot for the child NOW, before we exit.
|
|
682
|
+
//
|
|
683
|
+
// Without this there is a multi-second hole in which no instance exists on
|
|
684
|
+
// paper: we spawn a detached daemon and exit immediately, but the daemon
|
|
685
|
+
// does not register itself until it has built the store, started the
|
|
686
|
+
// popout server and opened its window — several seconds later. Any launch
|
|
687
|
+
// arriving inside that hole reads instance.json, sees nothing alive, and
|
|
688
|
+
// starts ANOTHER daemon. That is how three of them ended up running at
|
|
689
|
+
// once on 2026-08-26, spawned 4 and 5 seconds apart (PIDs 1452, 34012,
|
|
690
|
+
// 94964), each with its own window, only one of them in instance.json —
|
|
691
|
+
// which is also why nothing came to the front and why replace-on-launch
|
|
692
|
+
// could never clean them up: it only ever knew about the last one to
|
|
693
|
+
// register (Bob: "this does not look like a single instance ... but not
|
|
694
|
+
// one of the windows came to the front").
|
|
695
|
+
//
|
|
696
|
+
// The child is alive from the moment spawn returns, and pidIsMailx is a
|
|
697
|
+
// liveness check, so the claim is valid immediately. If the child dies
|
|
698
|
+
// during boot the existing stale-PID check clears it on the next launch,
|
|
699
|
+
// and when the daemon does reach its own registration it writes the same
|
|
700
|
+
// PID again — no conflict. A launch that lands during boot now takes the
|
|
701
|
+
// activate path and drops pending-activate.json, which the daemon picks up
|
|
702
|
+
// when it starts watching. (Claude Code 2026-08-27)
|
|
703
|
+
if (typeof child.pid === "number") writeInstanceFile(child.pid, []);
|
|
636
704
|
process.exit(0);
|
|
637
705
|
}
|
|
638
706
|
|
|
@@ -2984,6 +3052,43 @@ RFC 5322 with CRLF line endings. Bodies are quoted-printable encoded (readable i
|
|
|
2984
3052
|
}
|
|
2985
3053
|
}
|
|
2986
3054
|
|
|
3055
|
+
// Activation requests. A second launch of the SAME version (taskbar icon,
|
|
3056
|
+
// Start menu, a double-clicked shortcut) drops pending-activate.json and
|
|
3057
|
+
// exits instead of replacing us; this is the half that answers it by
|
|
3058
|
+
// bringing the existing window to the front. Same fs.watch + native
|
|
3059
|
+
// control-message shape as the mailto and share handoffs below.
|
|
3060
|
+
//
|
|
3061
|
+
// Without this the launcher would have nothing to hand off TO, and
|
|
3062
|
+
// "already running — raising its window" would be a lie: the process would
|
|
3063
|
+
// exit and the user would be left staring at whatever they were looking at
|
|
3064
|
+
// before, which is worse than the restart it replaced.
|
|
3065
|
+
{
|
|
3066
|
+
try {
|
|
3067
|
+
const dir = path.dirname(__activateFile);
|
|
3068
|
+
const baseName = path.basename(__activateFile);
|
|
3069
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
3070
|
+
// A request that landed while we were still booting — the watch
|
|
3071
|
+
// below can only see files that arrive after it is installed.
|
|
3072
|
+
if (fs.existsSync(__activateFile)) {
|
|
3073
|
+
try { fs.unlinkSync(__activateFile); } catch { /* */ }
|
|
3074
|
+
handle.send({ _msgerWindow: "focus" });
|
|
3075
|
+
}
|
|
3076
|
+
fs.watch(dir, (_event, filename) => {
|
|
3077
|
+
if (filename !== baseName) return;
|
|
3078
|
+
if (!fs.existsSync(__activateFile)) return;
|
|
3079
|
+
// Delete FIRST. fs.watch fires twice for one write on Windows
|
|
3080
|
+
// (rename + change), and a second focus command is harmless but
|
|
3081
|
+
// the unlink is what makes the second pass a no-op.
|
|
3082
|
+
try { fs.unlinkSync(__activateFile); } catch { return; }
|
|
3083
|
+
touchHandoffStamp();
|
|
3084
|
+
console.log(" [activate] second launch asked for the window — raising it");
|
|
3085
|
+
handle.send({ _msgerWindow: "focus" });
|
|
3086
|
+
});
|
|
3087
|
+
} catch (e: any) {
|
|
3088
|
+
console.error(` [activate] watch setup failed: ${e.message}`);
|
|
3089
|
+
}
|
|
3090
|
+
}
|
|
3091
|
+
|
|
2987
3092
|
// Pending Windows-share handler (C46). Same two-path pickup as mailto
|
|
2988
3093
|
// above: the client's startup consumePendingShare poll covers the
|
|
2989
3094
|
// "rmfshare.exe spawned us" race; this fs.watch covers shares that land
|
package/client/app.bundle.js
CHANGED
|
@@ -3108,11 +3108,11 @@ async function showMessage(accountId, uid, folderId, specialUse, isRetry = false
|
|
|
3108
3108
|
}
|
|
3109
3109
|
bodyEl.innerHTML = "";
|
|
3110
3110
|
const spam = msg.spamScore;
|
|
3111
|
-
if (spam && Number.isFinite(spam.score)) {
|
|
3112
|
-
const level = spam.flagged ? "high" :
|
|
3111
|
+
if (spam && Number.isFinite(spam.score) && (spam.flagged || spam.score >= spam.threshold / 2)) {
|
|
3112
|
+
const level = spam.flagged ? "high" : "mid";
|
|
3113
3113
|
const chip = document.createElement("div");
|
|
3114
3114
|
chip.className = `mv-spamscore mv-spamscore-${level}`;
|
|
3115
|
-
chip.title = level === "high" ? "Your mail server scored this at or above its spam threshold and flagged it." :
|
|
3115
|
+
chip.title = level === "high" ? "Your mail server scored this at or above its spam threshold and flagged it." : "Below your server's spam threshold, but more than halfway to it.";
|
|
3116
3116
|
chip.textContent = `spam score ${spam.score} of ${spam.threshold}`;
|
|
3117
3117
|
bodyEl.appendChild(chip);
|
|
3118
3118
|
}
|