@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/.commitmsg
CHANGED
|
@@ -1,62 +1,37 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
the registrable-domain level, since a host legitimately sending for a domain
|
|
39
|
-
authenticates as some account AT that domain and the local part varies for
|
|
40
|
-
ordinary reasons. Covers the cPanel/Exim, Postfix and Communigate spellings.
|
|
41
|
-
|
|
42
|
-
zero-width-obfuscation — the body had ~1380 zero-width characters stuffed
|
|
43
|
-
between letters, so "i regret to inform you" reached every word-matching filter
|
|
44
|
-
as gibberish while rendering as clean prose. SpamAssassin saw it
|
|
45
|
-
(UNICODE_OBFU_ZW_MANY) and still totalled only 2.7. Counted only BETWEEN TWO
|
|
46
|
-
LATIN LETTERS, which is what obfuscation looks like and what the legitimate
|
|
47
|
-
uses are not: an emoji ZWJ joins pictographs, Arabic and Indic ZWNJ sit between
|
|
48
|
-
their own scripts' letters. The occurrence count guards against a stray
|
|
49
|
-
character surviving a copy-paste; it is a sanity bound, not a sensitivity dial.
|
|
50
|
-
|
|
51
|
-
The score itself is now always visible — a chip above the body reading "spam
|
|
52
|
-
score 2.7 of 4.5" ("should we have a flag ... with the spam assassin number and
|
|
53
|
-
a color code?"). Separate from the findings on purpose: a finding is an
|
|
54
|
-
accusation and fires only on evidence, this is a measurement. Colour is a
|
|
55
|
-
PROPORTION of the server's own threshold — red once flagged, amber past halfway
|
|
56
|
-
to its line, quiet below — so it follows if the server's threshold changes
|
|
57
|
-
rather than hardcoding a number. Viewer only for now; the message-list row
|
|
58
|
-
would need the score stored at sync time.
|
|
59
|
-
|
|
60
|
-
tests/trust.test.ts covers all three, and the negative cases with them: spf=pass
|
|
61
|
-
and no-header both silent, a relay identity at the same organisation, an emoji
|
|
62
|
-
ZWJ family sequence, and a stray zero-width character from a paste.
|
|
1
|
+
Pressing the icon while it is running raises the window instead of restarting
|
|
2
|
+
|
|
3
|
+
"Why does it show and disappear? Are you assuring only a single instance? ...
|
|
4
|
+
you should still put me in the running version when I press the icon on the
|
|
5
|
+
task bar."
|
|
6
|
+
|
|
7
|
+
Single instance was assured — by REPLACEMENT. Every launch that wasn't a
|
|
8
|
+
share/mailto handoff ran replace-on-launch: SIGTERM the running daemon, wait,
|
|
9
|
+
SIGKILL, then boot fresh. Press the taskbar icon and the window you already had
|
|
10
|
+
is killed, disappears, and a new one fades up seconds later. From outside it
|
|
11
|
+
reads as the app flickering out of existence for no reason.
|
|
12
|
+
|
|
13
|
+
Replace-on-launch was written for the upgrade case — an older or orphaned daemon
|
|
14
|
+
surviving a partial upgrade and leaving two windows on two versions — but it
|
|
15
|
+
fired on every launch, ordinary ones included.
|
|
16
|
+
|
|
17
|
+
instance.json already records the running version, so the two cases separate
|
|
18
|
+
without guessing:
|
|
19
|
+
|
|
20
|
+
same version the user wants the app, and it is already there — write
|
|
21
|
+
pending-activate.json, let the running daemon raise its
|
|
22
|
+
window, exit 0. Nothing is killed.
|
|
23
|
+
different an upgrade landed; replace as before.
|
|
24
|
+
|
|
25
|
+
The daemon answers with the same fs.watch + { _msgerWindow: "focus" } shape the
|
|
26
|
+
mailto and share handoffs already use — that native control message exists
|
|
27
|
+
precisely so a background daemon can surface its window, which is exactly this
|
|
28
|
+
situation. It also consumes a request that arrived while it was still booting,
|
|
29
|
+
since fs.watch only sees files that land after it is installed.
|
|
30
|
+
|
|
31
|
+
`-another` still opts out entirely, and every handoff path still returns before
|
|
32
|
+
reaching any of this.
|
|
33
|
+
|
|
34
|
+
Verified against the live daemon before publishing: launching a second copy
|
|
35
|
+
printed "already running (PID 34012, v1.2.283) — raising its window", exited 0,
|
|
36
|
+
wrote the request, and left the running daemon alive — where the old code would
|
|
37
|
+
have killed it.
|
package/bin/mailx.js
CHANGED
|
@@ -492,7 +492,17 @@ function readInstanceFile() {
|
|
|
492
492
|
function writeInstanceFile(pid, childPids = []) {
|
|
493
493
|
try {
|
|
494
494
|
fs.mkdirSync(path.dirname(__instanceFile), { recursive: true });
|
|
495
|
-
|
|
495
|
+
// startedAt must survive a childPids update. It was recomputed on every
|
|
496
|
+
// write, and since every popup and popout window rewrites this file,
|
|
497
|
+
// the field reported the last window event rather than when the daemon
|
|
498
|
+
// started — actively misleading when diagnosing "how long has this been
|
|
499
|
+
// up" or "which of these daemons is the old one" (it cost a wrong turn
|
|
500
|
+
// on 2026-08-27). Keep the existing value whenever the PID is unchanged.
|
|
501
|
+
const prev = readInstanceFile();
|
|
502
|
+
const startedAt = prev && prev.pid === pid && typeof prev.startedAt === "number"
|
|
503
|
+
? prev.startedAt
|
|
504
|
+
: Date.now();
|
|
505
|
+
const payload = { pid, version: __selfVersion, startedAt, childPids };
|
|
496
506
|
fs.writeFileSync(__instanceFile, JSON.stringify(payload, null, 2));
|
|
497
507
|
}
|
|
498
508
|
catch { /* non-fatal */ }
|
|
@@ -575,6 +585,19 @@ function touchHandoffStamp() {
|
|
|
575
585
|
}
|
|
576
586
|
catch { /* */ }
|
|
577
587
|
}
|
|
588
|
+
/** "Someone launched me while you are already running — show yourself."
|
|
589
|
+
*
|
|
590
|
+
* A file rather than a socket, for the same reason the mailto and share
|
|
591
|
+
* handoffs use one: the launching process is a fresh node that will exit in
|
|
592
|
+
* milliseconds, and the daemon is already watching this directory. The daemon
|
|
593
|
+
* side deletes it and sends the native focus command. */
|
|
594
|
+
const __activateFile = path.join(path.dirname(__instanceFile), "pending-activate.json");
|
|
595
|
+
function requestActivate() {
|
|
596
|
+
try {
|
|
597
|
+
fs.writeFileSync(__activateFile, JSON.stringify({ at: Date.now(), fromPid: process.pid }));
|
|
598
|
+
}
|
|
599
|
+
catch { /* */ }
|
|
600
|
+
}
|
|
578
601
|
if (!isDaemon && !__isCommandInvocation && !__keepOthers) {
|
|
579
602
|
// Handoff launches must NOT replace a live daemon. rmfshare.exe (share
|
|
580
603
|
// sheet) and rmfmailto.exe (mailto links) write a pending file and spawn
|
|
@@ -604,12 +627,35 @@ if (!isDaemon && !__isCommandInvocation && !__keepOthers) {
|
|
|
604
627
|
}
|
|
605
628
|
}
|
|
606
629
|
}
|
|
607
|
-
//
|
|
630
|
+
// Activate-on-launch: launching the SAME version that is already running
|
|
631
|
+
// raises the window that exists instead of replacing it.
|
|
632
|
+
//
|
|
633
|
+
// Replace-on-launch (below) was written for the upgrade case — an older or
|
|
634
|
+
// orphaned daemon surviving a partial upgrade and leaving two windows on
|
|
635
|
+
// two versions. But it fired on EVERY launch, including the ordinary one:
|
|
636
|
+
// press the taskbar icon, the new process SIGTERMs the running daemon, its
|
|
637
|
+
// window vanishes, and seconds later a fresh one appears. From the outside
|
|
638
|
+
// that reads as the app flickering out of existence for no reason (Bob
|
|
639
|
+
// 2026-08-26: "why does it show and disappear... you should still put me in
|
|
640
|
+
// the running version when I press the icon on the task bar").
|
|
641
|
+
//
|
|
642
|
+
// instance.json already records the running version, so the two cases are
|
|
643
|
+
// distinguishable without guessing: same version means the user wants the
|
|
644
|
+
// app, so surface it; a different version means an upgrade landed and the
|
|
645
|
+
// old daemon must go. `-another` still opts out entirely, and every handoff
|
|
646
|
+
// path above still returns before reaching here.
|
|
647
|
+
{
|
|
648
|
+
const inst0 = readInstanceFile();
|
|
649
|
+
if (inst0 && pidIsMailx(inst0.pid) && inst0.pid !== process.pid && inst0.version === __selfVersion) {
|
|
650
|
+
requestActivate();
|
|
651
|
+
console.log(`rmfmail: already running (PID ${inst0.pid}, v${inst0.version}) — raising its window`);
|
|
652
|
+
process.exit(0);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
// Replace-on-launch: a daemon running a DIFFERENT version gets killed and
|
|
608
656
|
// the new one takes over. instance.json only tracks ONE PID — older or
|
|
609
657
|
// orphaned daemons can survive a partial upgrade and leave the user with
|
|
610
|
-
// multiple windows on different versions.
|
|
611
|
-
// rmfmail-looking node process and kill it (unconditionally — version
|
|
612
|
-
// match alone isn't enough; we always want exactly one daemon).
|
|
658
|
+
// multiple windows on different versions.
|
|
613
659
|
const myPid = process.pid;
|
|
614
660
|
const killedPids = [];
|
|
615
661
|
const inst = readInstanceFile();
|
|
@@ -681,6 +727,30 @@ if (!verbose && !isDaemon && !process.argv.slice(2).some(a => /^-/.test(a))) {
|
|
|
681
727
|
windowsHide: true,
|
|
682
728
|
});
|
|
683
729
|
child.unref();
|
|
730
|
+
// Claim the instance slot for the child NOW, before we exit.
|
|
731
|
+
//
|
|
732
|
+
// Without this there is a multi-second hole in which no instance exists on
|
|
733
|
+
// paper: we spawn a detached daemon and exit immediately, but the daemon
|
|
734
|
+
// does not register itself until it has built the store, started the
|
|
735
|
+
// popout server and opened its window — several seconds later. Any launch
|
|
736
|
+
// arriving inside that hole reads instance.json, sees nothing alive, and
|
|
737
|
+
// starts ANOTHER daemon. That is how three of them ended up running at
|
|
738
|
+
// once on 2026-08-26, spawned 4 and 5 seconds apart (PIDs 1452, 34012,
|
|
739
|
+
// 94964), each with its own window, only one of them in instance.json —
|
|
740
|
+
// which is also why nothing came to the front and why replace-on-launch
|
|
741
|
+
// could never clean them up: it only ever knew about the last one to
|
|
742
|
+
// register (Bob: "this does not look like a single instance ... but not
|
|
743
|
+
// one of the windows came to the front").
|
|
744
|
+
//
|
|
745
|
+
// The child is alive from the moment spawn returns, and pidIsMailx is a
|
|
746
|
+
// liveness check, so the claim is valid immediately. If the child dies
|
|
747
|
+
// during boot the existing stale-PID check clears it on the next launch,
|
|
748
|
+
// and when the daemon does reach its own registration it writes the same
|
|
749
|
+
// PID again — no conflict. A launch that lands during boot now takes the
|
|
750
|
+
// activate path and drops pending-activate.json, which the daemon picks up
|
|
751
|
+
// when it starts watching. (Claude Code 2026-08-27)
|
|
752
|
+
if (typeof child.pid === "number")
|
|
753
|
+
writeInstanceFile(child.pid, []);
|
|
684
754
|
process.exit(0);
|
|
685
755
|
}
|
|
686
756
|
const setupMode = hasFlag("setup");
|
|
@@ -3104,6 +3174,53 @@ RFC 5322 with CRLF line endings. Bodies are quoted-printable encoded (readable i
|
|
|
3104
3174
|
console.error(` [mailto] watch setup failed: ${e.message}`);
|
|
3105
3175
|
}
|
|
3106
3176
|
}
|
|
3177
|
+
// Activation requests. A second launch of the SAME version (taskbar icon,
|
|
3178
|
+
// Start menu, a double-clicked shortcut) drops pending-activate.json and
|
|
3179
|
+
// exits instead of replacing us; this is the half that answers it by
|
|
3180
|
+
// bringing the existing window to the front. Same fs.watch + native
|
|
3181
|
+
// control-message shape as the mailto and share handoffs below.
|
|
3182
|
+
//
|
|
3183
|
+
// Without this the launcher would have nothing to hand off TO, and
|
|
3184
|
+
// "already running — raising its window" would be a lie: the process would
|
|
3185
|
+
// exit and the user would be left staring at whatever they were looking at
|
|
3186
|
+
// before, which is worse than the restart it replaced.
|
|
3187
|
+
{
|
|
3188
|
+
try {
|
|
3189
|
+
const dir = path.dirname(__activateFile);
|
|
3190
|
+
const baseName = path.basename(__activateFile);
|
|
3191
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
3192
|
+
// A request that landed while we were still booting — the watch
|
|
3193
|
+
// below can only see files that arrive after it is installed.
|
|
3194
|
+
if (fs.existsSync(__activateFile)) {
|
|
3195
|
+
try {
|
|
3196
|
+
fs.unlinkSync(__activateFile);
|
|
3197
|
+
}
|
|
3198
|
+
catch { /* */ }
|
|
3199
|
+
handle.send({ _msgerWindow: "focus" });
|
|
3200
|
+
}
|
|
3201
|
+
fs.watch(dir, (_event, filename) => {
|
|
3202
|
+
if (filename !== baseName)
|
|
3203
|
+
return;
|
|
3204
|
+
if (!fs.existsSync(__activateFile))
|
|
3205
|
+
return;
|
|
3206
|
+
// Delete FIRST. fs.watch fires twice for one write on Windows
|
|
3207
|
+
// (rename + change), and a second focus command is harmless but
|
|
3208
|
+
// the unlink is what makes the second pass a no-op.
|
|
3209
|
+
try {
|
|
3210
|
+
fs.unlinkSync(__activateFile);
|
|
3211
|
+
}
|
|
3212
|
+
catch {
|
|
3213
|
+
return;
|
|
3214
|
+
}
|
|
3215
|
+
touchHandoffStamp();
|
|
3216
|
+
console.log(" [activate] second launch asked for the window — raising it");
|
|
3217
|
+
handle.send({ _msgerWindow: "focus" });
|
|
3218
|
+
});
|
|
3219
|
+
}
|
|
3220
|
+
catch (e) {
|
|
3221
|
+
console.error(` [activate] watch setup failed: ${e.message}`);
|
|
3222
|
+
}
|
|
3223
|
+
}
|
|
3107
3224
|
// Pending Windows-share handler (C46). Same two-path pickup as mailto
|
|
3108
3225
|
// above: the client's startup consumePendingShare poll covers the
|
|
3109
3226
|
// "rmfshare.exe spawned us" race; this fs.watch covers shares that land
|