@bobfrankston/rmfmail 1.1.105 → 1.1.106
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/client/app.bundle.js +26 -12
- package/client/app.bundle.js.map +2 -2
- package/client/app.js +38 -23
- package/client/app.js.map +1 -1
- package/client/app.ts +34 -21
- package/client/styles/components.css +18 -0
- package/package.json +1 -1
- package/packages/mailx-store/charset.d.ts.map +1 -1
- package/packages/mailx-store/charset.js +20 -0
- package/packages/mailx-store/charset.js.map +1 -1
- package/packages/mailx-store/charset.ts +19 -0
- /package/packages/mailx-imap/{node_modules.npmglobalize-stash-18636 → node_modules.npmglobalize-stash-77444}/.package-lock.json +0 -0
package/client/app.ts
CHANGED
|
@@ -4194,28 +4194,41 @@ optEditorTiptap?.addEventListener("change", () => {
|
|
|
4194
4194
|
if (optEditorTiptap.checked) saveEditorSetting("tiptap");
|
|
4195
4195
|
});
|
|
4196
4196
|
optEditorTinymce?.addEventListener("change", () => {
|
|
4197
|
-
if (optEditorTinymce.checked)
|
|
4198
|
-
|
|
4199
|
-
|
|
4200
|
-
|
|
4201
|
-
|
|
4202
|
-
|
|
4203
|
-
|
|
4204
|
-
|
|
4205
|
-
|
|
4206
|
-
|
|
4207
|
-
|
|
4208
|
-
|
|
4209
|
-
|
|
4210
|
-
|
|
4211
|
-
|
|
4212
|
-
|
|
4213
|
-
link.rel = "prefetch";
|
|
4214
|
-
link.as = "script";
|
|
4215
|
-
link.href = cdnUrl;
|
|
4216
|
-
document.head.appendChild(link);
|
|
4217
|
-
} catch { /* prefetch is best-effort */ }
|
|
4197
|
+
if (!optEditorTinymce.checked) return;
|
|
4198
|
+
saveEditorSetting("tinymce");
|
|
4199
|
+
// Q133 + 2026-05-21 — warm the TinyMCE bundle into the HTTP cache so the
|
|
4200
|
+
// first compose-open doesn't stall downloading it. The old code used a
|
|
4201
|
+
// silent <link rel=prefetch> — no completion signal, so picking TinyMCE
|
|
4202
|
+
// looked like a frozen UI ("hang while fetching"). Now we do an explicit
|
|
4203
|
+
// fetch() (I/O, already off the UI thread — a Worker buys nothing for a
|
|
4204
|
+
// download) and surface a pending → ready indicator on the menu item.
|
|
4205
|
+
const cdnUrl = localStorage.getItem("mailx-tinymce-cdn") || "lib/tinymce/tinymce.min.js";
|
|
4206
|
+
const label = optEditorTinymce.closest("label");
|
|
4207
|
+
let status = document.getElementById("opt-editor-tinymce-status");
|
|
4208
|
+
if (label && !status) {
|
|
4209
|
+
status = document.createElement("span");
|
|
4210
|
+
status.id = "opt-editor-tinymce-status";
|
|
4211
|
+
status.className = "tb-menu-status";
|
|
4212
|
+
label.appendChild(status);
|
|
4218
4213
|
}
|
|
4214
|
+
const setStatus = (text: string, state: string): void => {
|
|
4215
|
+
if (!status) return;
|
|
4216
|
+
status.textContent = text;
|
|
4217
|
+
status.dataset.state = state;
|
|
4218
|
+
};
|
|
4219
|
+
setStatus(" loading…", "pending"); // CSS ::before draws the spinner
|
|
4220
|
+
fetch(cdnUrl, { cache: "force-cache" })
|
|
4221
|
+
.then(r => (r.ok ? r.arrayBuffer() : Promise.reject(new Error(`HTTP ${r.status}`))))
|
|
4222
|
+
.then(() => {
|
|
4223
|
+
setStatus(" ✓ ready", "ready");
|
|
4224
|
+
// Clear the badge after a few seconds — but only if still "ready"
|
|
4225
|
+
// (don't wipe a later pending/error state from a re-pick).
|
|
4226
|
+
setTimeout(() => { if (status?.dataset.state === "ready") setStatus("", "idle"); }, 4000);
|
|
4227
|
+
})
|
|
4228
|
+
.catch(e => {
|
|
4229
|
+
setStatus(" ⚠ load failed", "error");
|
|
4230
|
+
console.error("[tinymce] pre-warm fetch failed:", e?.message || e);
|
|
4231
|
+
});
|
|
4219
4232
|
});
|
|
4220
4233
|
|
|
4221
4234
|
// External editor preference (Edit-in-Word handoff target). Stored under
|
|
@@ -125,6 +125,24 @@ button.tb-menu-item { background: none; border: none; color: inherit; width: 100
|
|
|
125
125
|
.tb-menu-item input[type="radio"] { accent-color: var(--color-accent); }
|
|
126
126
|
.tb-sep { width: 1px; height: 1.2rem; background: var(--color-border); margin: 0 var(--gap-xs); }
|
|
127
127
|
|
|
128
|
+
/* Inline load-status badge on the TinyMCE menu item (pending → ready). */
|
|
129
|
+
.tb-menu-status { font-size: 0.8em; color: var(--color-text-muted); }
|
|
130
|
+
.tb-menu-status[data-state="pending"] { color: var(--color-accent); }
|
|
131
|
+
.tb-menu-status[data-state="ready"] { color: oklch(0.6 0.13 150); }
|
|
132
|
+
.tb-menu-status[data-state="error"] { color: oklch(0.55 0.18 25); }
|
|
133
|
+
.tb-menu-status[data-state="pending"]::before {
|
|
134
|
+
content: "";
|
|
135
|
+
display: inline-block;
|
|
136
|
+
width: 0.7em; height: 0.7em;
|
|
137
|
+
margin-right: 0.3em;
|
|
138
|
+
border: 2px solid currentColor;
|
|
139
|
+
border-right-color: transparent;
|
|
140
|
+
border-radius: 50%;
|
|
141
|
+
animation: tb-menu-spin 0.7s linear infinite;
|
|
142
|
+
vertical-align: -0.05em;
|
|
143
|
+
}
|
|
144
|
+
@keyframes tb-menu-spin { to { transform: rotate(360deg); } }
|
|
145
|
+
|
|
128
146
|
/* Submenu — single-line label that expands a nested popout. Used for the
|
|
129
147
|
Theme picker so the parent dropdown isn't cluttered with three radios. */
|
|
130
148
|
.tb-menu-submenu { position: relative; }
|
package/package.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"charset.d.ts","sourceRoot":"","sources":["charset.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH;0DAC0D;AAC1D,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,
|
|
1
|
+
{"version":3,"file":"charset.d.ts","sourceRoot":"","sources":["charset.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH;0DAC0D;AAC1D,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAiBtD"}
|
|
@@ -17,11 +17,31 @@ export function sniffAndFixCharset(raw) {
|
|
|
17
17
|
const re = /charset\s*=\s*"?(iso-8859-1|us-ascii|windows-1252|latin1)"?/gi;
|
|
18
18
|
if (!re.test(head))
|
|
19
19
|
return raw;
|
|
20
|
+
// The rewrite is only sound when the raw bytes ARE the body bytes — i.e.
|
|
21
|
+
// an 8bit / binary part. For a quoted-printable or base64 part the raw
|
|
22
|
+
// .eml is pure ASCII (the high bytes live inside `=XX` / base64 chars),
|
|
23
|
+
// so isValidUtf8(raw) passes vacuously and we would relabel a genuine
|
|
24
|
+
// Windows-1252 part as utf-8 — every smart-quote / em-dash then decodes
|
|
25
|
+
// as mojibake (Bob's 2026-05-21 report: a QP Windows-1252 Outlook mail).
|
|
26
|
+
// Requiring an actual non-ASCII byte in the raw gates the heuristic to
|
|
27
|
+
// the only case where the UTF-8 sniff is meaningful.
|
|
28
|
+
if (!hasNonAscii(raw))
|
|
29
|
+
return raw;
|
|
20
30
|
if (!isValidUtf8(raw))
|
|
21
31
|
return raw;
|
|
22
32
|
const fixed = head.replace(/charset\s*=\s*"?(iso-8859-1|us-ascii|windows-1252|latin1)"?/gi, "charset=utf-8");
|
|
23
33
|
return Buffer.concat([Buffer.from(fixed, "latin1"), raw.subarray(head.length)]);
|
|
24
34
|
}
|
|
35
|
+
/** True if the buffer contains at least one byte >= 0x80. A pure-ASCII
|
|
36
|
+
* buffer is trivially valid UTF-8, so the isValidUtf8 sniff tells us
|
|
37
|
+
* nothing about a quoted-printable / base64 body — gate on this first. */
|
|
38
|
+
function hasNonAscii(buf) {
|
|
39
|
+
for (let i = 0; i < buf.length; i++) {
|
|
40
|
+
if (buf[i] >= 0x80)
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
25
45
|
/** Strict UTF-8 validity check: rejects overlong forms, invalid start
|
|
26
46
|
* bytes, and dangling continuations. Used to confirm the body is really
|
|
27
47
|
* UTF-8 before overriding a Latin-1 declaration. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"charset.js","sourceRoot":"","sources":["charset.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH;0DAC0D;AAC1D,MAAM,UAAU,kBAAkB,CAAC,GAAW;IAC1C,MAAM,UAAU,GAAG,KAAK,CAAC;IACzB,MAAM,IAAI,GAAG,GAAG,CAAC,QAAQ,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,UAAU,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;IAClF,MAAM,EAAE,GAAG,+DAA+D,CAAC;IAC3E,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC;QAAE,OAAO,GAAG,CAAC;IAC/B,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC;QAAE,OAAO,GAAG,CAAC;IAClC,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,+DAA+D,EAAE,eAAe,CAAC,CAAC;IAC7G,OAAO,MAAM,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,EAAE,QAAQ,CAAC,EAAE,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;AACpF,CAAC;AAED;;qDAEqD;AACrD,SAAS,WAAW,CAAC,GAAW;IAC5B,IAAI,CAAC,GAAG,CAAC,CAAC;IACV,OAAO,CAAC,GAAG,GAAG,CAAC,MAAM,EAAE,CAAC;QACpB,MAAM,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC;QACjB,IAAI,CAAC,GAAG,IAAI,EAAE,CAAC;YAAC,CAAC,EAAE,CAAC;YAAC,SAAS;QAAC,CAAC;QAChC,IAAI,IAAY,CAAC;QACjB,IAAI,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC;YAAC,IAAI,CAAC,GAAG,IAAI;gBAAE,OAAO,KAAK,CAAC;YAAC,IAAI,GAAG,CAAC,CAAC;QAAC,CAAC;aAC7D,IAAI,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,IAAI;YAAE,IAAI,GAAG,CAAC,CAAC;aAClC,IAAI,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC;YAAC,IAAI,CAAC,GAAG,IAAI;gBAAE,OAAO,KAAK,CAAC;YAAC,IAAI,GAAG,CAAC,CAAC;QAAC,CAAC;;YAClE,OAAO,KAAK,CAAC;QAClB,IAAI,CAAC,GAAG,IAAI,IAAI,GAAG,CAAC,MAAM;YAAE,OAAO,KAAK,CAAC;QACzC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,IAAI,EAAE,CAAC,EAAE,EAAE,CAAC;YAC7B,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,IAAI;gBAAE,OAAO,KAAK,CAAC;QACnD,CAAC;QACD,CAAC,IAAI,IAAI,GAAG,CAAC,CAAC;IAClB,CAAC;IACD,OAAO,IAAI,CAAC;AAChB,CAAC"}
|
|
1
|
+
{"version":3,"file":"charset.js","sourceRoot":"","sources":["charset.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH;0DAC0D;AAC1D,MAAM,UAAU,kBAAkB,CAAC,GAAW;IAC1C,MAAM,UAAU,GAAG,KAAK,CAAC;IACzB,MAAM,IAAI,GAAG,GAAG,CAAC,QAAQ,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,UAAU,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;IAClF,MAAM,EAAE,GAAG,+DAA+D,CAAC;IAC3E,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC;QAAE,OAAO,GAAG,CAAC;IAC/B,yEAAyE;IACzE,uEAAuE;IACvE,wEAAwE;IACxE,sEAAsE;IACtE,wEAAwE;IACxE,yEAAyE;IACzE,uEAAuE;IACvE,qDAAqD;IACrD,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC;QAAE,OAAO,GAAG,CAAC;IAClC,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC;QAAE,OAAO,GAAG,CAAC;IAClC,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,+DAA+D,EAAE,eAAe,CAAC,CAAC;IAC7G,OAAO,MAAM,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,EAAE,QAAQ,CAAC,EAAE,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;AACpF,CAAC;AAED;;2EAE2E;AAC3E,SAAS,WAAW,CAAC,GAAW;IAC5B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,GAAG,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QAClC,IAAI,GAAG,CAAC,CAAC,CAAC,IAAI,IAAI;YAAE,OAAO,IAAI,CAAC;IACpC,CAAC;IACD,OAAO,KAAK,CAAC;AACjB,CAAC;AAED;;qDAEqD;AACrD,SAAS,WAAW,CAAC,GAAW;IAC5B,IAAI,CAAC,GAAG,CAAC,CAAC;IACV,OAAO,CAAC,GAAG,GAAG,CAAC,MAAM,EAAE,CAAC;QACpB,MAAM,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC;QACjB,IAAI,CAAC,GAAG,IAAI,EAAE,CAAC;YAAC,CAAC,EAAE,CAAC;YAAC,SAAS;QAAC,CAAC;QAChC,IAAI,IAAY,CAAC;QACjB,IAAI,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC;YAAC,IAAI,CAAC,GAAG,IAAI;gBAAE,OAAO,KAAK,CAAC;YAAC,IAAI,GAAG,CAAC,CAAC;QAAC,CAAC;aAC7D,IAAI,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,IAAI;YAAE,IAAI,GAAG,CAAC,CAAC;aAClC,IAAI,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC;YAAC,IAAI,CAAC,GAAG,IAAI;gBAAE,OAAO,KAAK,CAAC;YAAC,IAAI,GAAG,CAAC,CAAC;QAAC,CAAC;;YAClE,OAAO,KAAK,CAAC;QAClB,IAAI,CAAC,GAAG,IAAI,IAAI,GAAG,CAAC,MAAM;YAAE,OAAO,KAAK,CAAC;QACzC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,IAAI,EAAE,CAAC,EAAE,EAAE,CAAC;YAC7B,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,IAAI;gBAAE,OAAO,KAAK,CAAC;QACnD,CAAC;QACD,CAAC,IAAI,IAAI,GAAG,CAAC,CAAC;IAClB,CAAC;IACD,OAAO,IAAI,CAAC;AAChB,CAAC"}
|
|
@@ -17,11 +17,30 @@ export function sniffAndFixCharset(raw: Buffer): Buffer {
|
|
|
17
17
|
const head = raw.subarray(0, Math.min(HEAD_LIMIT, raw.length)).toString("latin1");
|
|
18
18
|
const re = /charset\s*=\s*"?(iso-8859-1|us-ascii|windows-1252|latin1)"?/gi;
|
|
19
19
|
if (!re.test(head)) return raw;
|
|
20
|
+
// The rewrite is only sound when the raw bytes ARE the body bytes — i.e.
|
|
21
|
+
// an 8bit / binary part. For a quoted-printable or base64 part the raw
|
|
22
|
+
// .eml is pure ASCII (the high bytes live inside `=XX` / base64 chars),
|
|
23
|
+
// so isValidUtf8(raw) passes vacuously and we would relabel a genuine
|
|
24
|
+
// Windows-1252 part as utf-8 — every smart-quote / em-dash then decodes
|
|
25
|
+
// as mojibake (Bob's 2026-05-21 report: a QP Windows-1252 Outlook mail).
|
|
26
|
+
// Requiring an actual non-ASCII byte in the raw gates the heuristic to
|
|
27
|
+
// the only case where the UTF-8 sniff is meaningful.
|
|
28
|
+
if (!hasNonAscii(raw)) return raw;
|
|
20
29
|
if (!isValidUtf8(raw)) return raw;
|
|
21
30
|
const fixed = head.replace(/charset\s*=\s*"?(iso-8859-1|us-ascii|windows-1252|latin1)"?/gi, "charset=utf-8");
|
|
22
31
|
return Buffer.concat([Buffer.from(fixed, "latin1"), raw.subarray(head.length)]);
|
|
23
32
|
}
|
|
24
33
|
|
|
34
|
+
/** True if the buffer contains at least one byte >= 0x80. A pure-ASCII
|
|
35
|
+
* buffer is trivially valid UTF-8, so the isValidUtf8 sniff tells us
|
|
36
|
+
* nothing about a quoted-printable / base64 body — gate on this first. */
|
|
37
|
+
function hasNonAscii(buf: Buffer): boolean {
|
|
38
|
+
for (let i = 0; i < buf.length; i++) {
|
|
39
|
+
if (buf[i] >= 0x80) return true;
|
|
40
|
+
}
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
|
|
25
44
|
/** Strict UTF-8 validity check: rejects overlong forms, invalid start
|
|
26
45
|
* bytes, and dangling continuations. Used to confirm the body is really
|
|
27
46
|
* UTF-8 before overriding a Latin-1 declaration. */
|