@bobfrankston/rmfmail 1.1.105 → 1.1.107
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 +54 -19
- package/client/app.bundle.js.map +2 -2
- package/client/app.js +96 -37
- package/client/app.js.map +1 -1
- package/client/app.ts +86 -35
- package/client/compose/compose.bundle.js +25 -9
- package/client/compose/compose.bundle.js.map +2 -2
- package/client/lib/rmf-tiny.js +40 -11
- 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-61168}/.package-lock.json +0 -0
package/client/lib/rmf-tiny.js
CHANGED
|
@@ -322,19 +322,48 @@ export async function createTinyMceEditor(container, opts = {}) {
|
|
|
322
322
|
// (reply / forward — user types above the quoted block);
|
|
323
323
|
// anything else → cursor at END (legacy "put cursor at end"
|
|
324
324
|
// semantics).
|
|
325
|
-
|
|
326
|
-
editor.
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
325
|
+
const place = () => {
|
|
326
|
+
const body = editor.getBody();
|
|
327
|
+
if (pos === 0) {
|
|
328
|
+
// Put the caret INSIDE the first block element. Collapsing
|
|
329
|
+
// to raw body-start lands it outside any block (before /
|
|
330
|
+
// between bare nodes) where contentEditable insertion is
|
|
331
|
+
// unpredictable — Bob 2026-05-21: "typing goes in the
|
|
332
|
+
// wrong place until you try again". rmfmail's reply body
|
|
333
|
+
// now leads with a real <p>; drop the caret into it.
|
|
334
|
+
const first = body.firstChild;
|
|
335
|
+
if (first && first.nodeType === 1 /* element */) {
|
|
336
|
+
editor.selection.setCursorLocation(first, 0);
|
|
337
|
+
}
|
|
338
|
+
else {
|
|
339
|
+
editor.selection.select(body, true);
|
|
340
|
+
editor.selection.collapse(true);
|
|
341
|
+
}
|
|
342
|
+
editor.focus();
|
|
343
|
+
// Viewport to the top so the user looks at the empty
|
|
344
|
+
// reply line, not scrolled down into the quote.
|
|
335
345
|
editor.getWin()?.scrollTo(0, 0);
|
|
336
|
-
|
|
346
|
+
}
|
|
347
|
+
else {
|
|
348
|
+
editor.selection.select(body, true);
|
|
349
|
+
editor.selection.collapse(false);
|
|
350
|
+
editor.focus();
|
|
337
351
|
editor.selection.scrollIntoView();
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
try {
|
|
355
|
+
place();
|
|
356
|
+
// Re-apply on the next frame. Cross-iframe focus (compose is
|
|
357
|
+
// an iframe; TinyMCE's edit area is a nested iframe) lets the
|
|
358
|
+
// first selection set get clobbered by a late layout / focus
|
|
359
|
+
// event — the "have to click in and try again" symptom. A
|
|
360
|
+
// second pass after the frame settles makes it stick.
|
|
361
|
+
editor.getWin()?.requestAnimationFrame?.(() => {
|
|
362
|
+
try {
|
|
363
|
+
place();
|
|
364
|
+
}
|
|
365
|
+
catch { /* */ }
|
|
366
|
+
});
|
|
338
367
|
}
|
|
339
368
|
catch { /* */ }
|
|
340
369
|
},
|
|
@@ -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. */
|