@bobfrankston/rmfmail 1.2.253 → 1.2.256

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.
@@ -173,7 +173,11 @@ export async function createTinyMceEditor(container, opts = {}) {
173
173
  // left. Replaced the generic alignleft/center/right icons, which
174
174
  // don't communicate wrapping (Bob 2026-07-18: "the align is
175
175
  // confusing, better to use the Word icons for the three cases").
176
- quickbars_image_toolbar: "rmfwrapinline rmfwrapleft rmfwrapright",
176
+ // …plus a border control (Bob 2026-08-13: "image editing should have
177
+ // an option to put a border around the image. It could be a check box
178
+ // or a width size"). Split button: click = 1px border on/off, ▾ = pick
179
+ // the width. Registered in setup below.
180
+ quickbars_image_toolbar: "rmfwrapinline rmfwrapleft rmfwrapright | rmfborder",
177
181
  // Code-sample dropdown languages. TinyMCE's default list omits
178
182
  // "Text" / plain — every option triggers syntax highlighting
179
183
  // which mangles unrelated paste content (Bob 2026-05-24).
@@ -192,6 +196,11 @@ export async function createTinyMceEditor(container, opts = {}) {
192
196
  // OS file-picker the compose Attach button uses (<input type=file>,
193
197
  // which works in the msger WebView), then inlines the chosen image as
194
198
  // a base64 data URL — matching the drag-drop-an-image behavior.
199
+ // Insert/Edit Image → Advanced tab: border width + style, and the
200
+ // vertical/horizontal spacing that goes with a framed image. This is
201
+ // the precise-control half of the border feature; the quickbar split
202
+ // button below is the one-click half.
203
+ image_advtab: true,
195
204
  file_picker_types: "image",
196
205
  file_picker_callback: (cb, _value, meta) => {
197
206
  if (meta && meta.filetype && meta.filetype !== "image")
@@ -353,6 +362,74 @@ export async function createTinyMceEditor(container, opts = {}) {
353
362
  wrapButton("rmfwrapinline", "rmf-wrap-inline", "In line with text", "inline");
354
363
  wrapButton("rmfwrapleft", "rmf-wrap-left", "Image left — text wraps around the right", "left");
355
364
  wrapButton("rmfwrapright", "rmf-wrap-right", "Image right — text wraps around the left", "right");
365
+ // ── Image border ──
366
+ // Gray rather than black: a border's job here is to separate the
367
+ // image from the text around it, and #888 stays visible whether
368
+ // the recipient reads on a white or a dark background — a black
369
+ // frame disappears into a dark-mode message body.
370
+ const BORDER_COLOR = "#888888";
371
+ const BORDER_WIDTHS = [1, 2, 3, 5];
372
+ ed.ui.registry.addIcon("rmf-border", `<svg width="24" height="24" viewBox="0 0 24 24">`
373
+ + `<rect x="3.5" y="4.5" width="17" height="15" rx="1" fill="none" stroke="currentColor" stroke-width="2"/>`
374
+ + arch(12, 16.5)
375
+ + `</svg>`);
376
+ /** Current visible border width in px — 0 when there is none.
377
+ * A width with `border-style: none` draws nothing, so it counts
378
+ * as no border. */
379
+ const borderWidthOf = (node) => {
380
+ if (!node || node.nodeName !== "IMG")
381
+ return 0;
382
+ const style = node.style?.borderStyle || "";
383
+ if (!style || style === "none")
384
+ return 0;
385
+ const w = parseFloat(node.style?.borderWidth || "");
386
+ return Number.isFinite(w) ? w : 0;
387
+ };
388
+ const applyBorder = (px) => {
389
+ const node = ed.selection.getNode();
390
+ if (!node || node.nodeName !== "IMG")
391
+ return;
392
+ ed.undoManager.transact(() => {
393
+ ed.dom.setStyles(node, px > 0
394
+ ? { border: `${px}px solid ${BORDER_COLOR}` }
395
+ : { border: "" });
396
+ });
397
+ ed.nodeChanged();
398
+ };
399
+ ed.ui.registry.addSplitButton("rmfborder", {
400
+ icon: "rmf-border",
401
+ tooltip: "Border around the image — click to toggle, ▾ to pick the width",
402
+ // Click toggles: bordered → none, unbordered → 1px hairline.
403
+ onAction: () => applyBorder(borderWidthOf(ed.selection.getNode()) > 0 ? 0 : 1),
404
+ onItemAction: (_api, value) => applyBorder(Number(value)),
405
+ select: (value) => Number(value) === borderWidthOf(ed.selection.getNode()),
406
+ fetch: (cb) => cb([
407
+ { type: "choiceitem", text: "No border", value: "0" },
408
+ ...BORDER_WIDTHS.map((w) => ({ type: "choiceitem", text: `${w} px`, value: String(w) })),
409
+ ]),
410
+ onSetup: (api) => {
411
+ const update = () => api.setActive(borderWidthOf(ed.selection.getNode()) > 0);
412
+ ed.on("NodeChange", update);
413
+ return () => ed.off("NodeChange", update);
414
+ },
415
+ });
416
+ // The image dialog's Advanced tab takes a border WIDTH and a border
417
+ // STYLE as separate fields, and CSS defaults the style to `none` —
418
+ // so typing "2" and leaving the style dropdown alone produced an
419
+ // image with an invisible border and no hint why. A width the user
420
+ // asked for means a border they can see: fill in `solid`.
421
+ ed.on("NodeChange", () => {
422
+ const n = ed.selection.getNode();
423
+ if (!n || n.nodeName !== "IMG")
424
+ return;
425
+ const w = parseFloat(n.style?.borderWidth || "");
426
+ const s = n.style?.borderStyle || "";
427
+ if (Number.isFinite(w) && w > 0 && (!s || s === "none")) {
428
+ ed.dom.setStyle(n, "border-style", "solid");
429
+ if (!n.style.borderColor)
430
+ ed.dom.setStyle(n, "border-color", BORDER_COLOR);
431
+ }
432
+ });
356
433
  // Bitmap-first paste. An image copied from a web app (Gemini,
357
434
  // browser "Copy image") puts BOTH a bitmap file item and an
358
435
  // html flavor on the clipboard — and the html flavor is a
@@ -920,7 +997,26 @@ export async function createTinyMceEditor(container, opts = {}) {
920
997
  function openSourceDialog(editor) {
921
998
  const SENTINEL = ""; // private-use area — never in real mail text
922
999
  let offset = -1;
1000
+ let savedRng = null;
923
1001
  try {
1002
+ // The sentinel must be INSERTED, never SUBSTITUTED. selection.setContent()
1003
+ // replaces whatever is selected — and right-clicking an image selects the
1004
+ // <img>, so "view source" swallowed the image (Bob 2026-08-13: "if I right
1005
+ // mouse view source on an image the image disappears"). Same loss for any
1006
+ // non-collapsed text selection, and undoManager.ignore kept it out of the
1007
+ // undo stack, so Ctrl+Z couldn't bring it back either. Collapse to the
1008
+ // range START first (that's the caret the source view should land on), then
1009
+ // restore the user's original selection once the sentinel is out again.
1010
+ const sel = editor.selection;
1011
+ const rng = sel.getRng();
1012
+ if (rng) {
1013
+ savedRng = rng.cloneRange(); // live range — tracks the insert/strip
1014
+ if (!rng.collapsed) {
1015
+ const collapsed = rng.cloneRange();
1016
+ collapsed.collapse(true);
1017
+ sel.setRng(collapsed);
1018
+ }
1019
+ }
924
1020
  // Insert the sentinel at the caret without polluting undo history.
925
1021
  const insert = () => editor.selection.setContent(SENTINEL);
926
1022
  if (typeof editor.undoManager?.ignore === "function")
@@ -945,6 +1041,14 @@ function openSourceDialog(editor) {
945
1041
  }
946
1042
  }
947
1043
  catch { /* sentinel cleanup is best-effort */ }
1044
+ // Put the selection back where the user left it (the selected image
1045
+ // stays selected, so closing the dialog leaves the quickbar up).
1046
+ if (savedRng) {
1047
+ try {
1048
+ editor.selection.setRng(savedRng);
1049
+ }
1050
+ catch { /* range invalidated by the strip — caret stays put */ }
1051
+ }
948
1052
  }
949
1053
  catch { /* fall through to a plain source view at offset 0 */ }
950
1054
  // Display the pristine source (sentinel already stripped from the DOM);
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/mailx-client",
3
- "version": "1.0.31",
3
+ "version": "1.0.32",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "scripts": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/rmfmail",
3
- "version": "1.2.253",
3
+ "version": "1.2.256",
4
4
  "description": "Local-first email client with IMAP sync and standalone native app",
5
5
  "type": "module",
6
6
  "main": "bin/mailx.js",
@@ -34,14 +34,14 @@
34
34
  "dependencies": {
35
35
  "@bobfrankston/iflow-direct": "^0.1.65",
36
36
  "@bobfrankston/mailx-host": "^0.1.15",
37
- "@bobfrankston/mailx-imap": "^0.1.151",
38
- "@bobfrankston/mailx-store-web": "^0.1.78",
37
+ "@bobfrankston/mailx-imap": "^0.1.153",
38
+ "@bobfrankston/mailx-store-web": "^0.1.80",
39
39
  "@bobfrankston/mailx-sync": "^0.1.29",
40
40
  "@bobfrankston/miscinfo": "^1.0.17",
41
- "@bobfrankston/msger": "^0.1.424",
41
+ "@bobfrankston/msger": "^0.1.425",
42
42
  "@bobfrankston/node-tcp-transport": "^0.1.10",
43
43
  "@bobfrankston/oauthsupport": "^1.0.34",
44
- "@bobfrankston/rmf-tiny": "^0.1.46",
44
+ "@bobfrankston/rmf-tiny": "^0.1.47",
45
45
  "@bobfrankston/smtp-direct": "^0.1.9",
46
46
  "@bobfrankston/tcp-transport": "^0.1.8",
47
47
  "@capacitor/android": "^8.3.0",
@@ -114,14 +114,14 @@
114
114
  "dependencies": {
115
115
  "@bobfrankston/iflow-direct": "^0.1.65",
116
116
  "@bobfrankston/mailx-host": "^0.1.15",
117
- "@bobfrankston/mailx-imap": "^0.1.151",
118
- "@bobfrankston/mailx-store-web": "^0.1.78",
117
+ "@bobfrankston/mailx-imap": "^0.1.153",
118
+ "@bobfrankston/mailx-store-web": "^0.1.80",
119
119
  "@bobfrankston/mailx-sync": "^0.1.29",
120
120
  "@bobfrankston/miscinfo": "^1.0.17",
121
- "@bobfrankston/msger": "^0.1.424",
121
+ "@bobfrankston/msger": "^0.1.425",
122
122
  "@bobfrankston/node-tcp-transport": "^0.1.10",
123
123
  "@bobfrankston/oauthsupport": "^1.0.34",
124
- "@bobfrankston/rmf-tiny": "^0.1.46",
124
+ "@bobfrankston/rmf-tiny": "^0.1.47",
125
125
  "@bobfrankston/smtp-direct": "^0.1.9",
126
126
  "@bobfrankston/tcp-transport": "^0.1.8",
127
127
  "@capacitor/android": "^8.3.0",
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bobfrankston/mailx-imap",
3
- "version": "0.1.152",
3
+ "version": "0.1.153",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bobfrankston/mailx-imap",
9
- "version": "0.1.152",
9
+ "version": "0.1.153",
10
10
  "license": "ISC",
11
11
  "dependencies": {
12
12
  "@bobfrankston/iflow-direct": "^0.1.27",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/mailx-imap",
3
- "version": "0.1.152",
3
+ "version": "0.1.153",
4
4
  "type": "module",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/mailx-settings",
3
- "version": "0.1.53",
3
+ "version": "0.1.54",
4
4
  "type": "module",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/mailx-store",
3
- "version": "0.1.86",
3
+ "version": "0.1.87",
4
4
  "type": "module",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/mailx-store-web",
3
- "version": "0.1.79",
3
+ "version": "0.1.80",
4
4
  "type": "module",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -146,8 +146,15 @@ function wrapWithAttachments(commonHeaders, inner, attachments, newBoundary) {
146
146
  return [...commonHeaders, ...inner.headers].join("\r\n") + `\r\n\r\n` + inner.body;
147
147
  }
148
148
  const mixedBoundary = newBoundary();
149
+ // RFC 2046: a boundary delimiter is "CRLF--boundary" — it MUST start its
150
+ // own line. The multipart/alternative inner body already ends with CRLF,
151
+ // but a bare quoted-printable body (draft, or a text-only send) does not,
152
+ // which glued `</p>--mailx_abc` together and made mailparser report ZERO
153
+ // attachments even though the bytes were all there. Normalize instead of
154
+ // assuming, so no future inner shape can reintroduce it.
155
+ const endCRLF = (s) => (s.endsWith("\r\n") ? s : s + "\r\n");
149
156
  const parts = [
150
- `--${mixedBoundary}\r\n` + inner.headers.join("\r\n") + `\r\n\r\n` + inner.body,
157
+ `--${mixedBoundary}\r\n` + inner.headers.join("\r\n") + `\r\n\r\n` + endCRLF(inner.body),
151
158
  ];
152
159
  for (const att of attachments) {
153
160
  // A quote or newline in a filename would break out of the header.
@@ -1 +1 @@
1
- {"version":3,"file":"mime-build.js","sourceRoot":"","sources":["mime-build.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"mime-build.js","sourceRoot":"","sources":["mime-build.ts"],"names":[],"mappings":"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"}
@@ -209,8 +209,15 @@ function wrapWithAttachments(
209
209
  return [...commonHeaders, ...inner.headers].join("\r\n") + `\r\n\r\n` + inner.body;
210
210
  }
211
211
  const mixedBoundary = newBoundary();
212
+ // RFC 2046: a boundary delimiter is "CRLF--boundary" — it MUST start its
213
+ // own line. The multipart/alternative inner body already ends with CRLF,
214
+ // but a bare quoted-printable body (draft, or a text-only send) does not,
215
+ // which glued `</p>--mailx_abc` together and made mailparser report ZERO
216
+ // attachments even though the bytes were all there. Normalize instead of
217
+ // assuming, so no future inner shape can reintroduce it.
218
+ const endCRLF = (s: string): string => (s.endsWith("\r\n") ? s : s + "\r\n");
212
219
  const parts: string[] = [
213
- `--${mixedBoundary}\r\n` + inner.headers.join("\r\n") + `\r\n\r\n` + inner.body,
220
+ `--${mixedBoundary}\r\n` + inner.headers.join("\r\n") + `\r\n\r\n` + endCRLF(inner.body),
214
221
  ];
215
222
  for (const att of attachments) {
216
223
  // A quote or newline in a filename would break out of the header.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/mailx-types",
3
- "version": "0.1.47",
3
+ "version": "0.1.49",
4
4
  "type": "module",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",