@bobfrankston/rmfmail 1.2.147 → 1.2.148

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.
@@ -990,14 +990,38 @@ export class MailxService implements MailxApi {
990
990
  `;
991
991
  try {
992
992
  const htmlToDocx = (await import("html-to-docx")).default;
993
- const buf = await htmlToDocx(wrapped) as Buffer | ArrayBuffer;
994
- const out = Buffer.isBuffer(buf) ? buf : Buffer.from(buf as ArrayBuffer);
993
+ let out: Buffer;
994
+ try {
995
+ const buf = await htmlToDocx(wrapped) as Buffer | ArrayBuffer;
996
+ out = Buffer.isBuffer(buf) ? buf : Buffer.from(buf as ArrayBuffer);
997
+ } catch (first: any) {
998
+ // Two known html-to-docx crashes on real mail bodies
999
+ // (Bob 2026-07-18, reply with a deep quoted chain):
1000
+ // - "Cannot read properties of undefined (reading
1001
+ // 'createFont')" — font-family styles in some nestings
1002
+ // hit an undefined docxDocumentInstance inside the lib;
1003
+ // - "unsupported file type: undefined" — a remote http(s)
1004
+ // <img> the lib tries (and fails) to download.
1005
+ // Retry once with font-family styles stripped and remote
1006
+ // imgs swapped for their alt text — slightly lower fidelity
1007
+ // beats losing the .docx round-trip (the .html fallback
1008
+ // needs Save-As-Web-Page gymnastics in Word).
1009
+ const sanitized = wrapped
1010
+ .replace(/font-family\s*:[^;"']*;?/gi, "")
1011
+ .replace(/<img\b[^>]*\bsrc\s*=\s*["']https?:[^>]*>/gi, (tag) => {
1012
+ const alt = /\balt\s*=\s*["']([^"']*)["']/i.exec(tag)?.[1] || "";
1013
+ return alt ? `[image: ${alt}]` : "[image]";
1014
+ });
1015
+ console.warn(` [word-edit] html-to-docx failed (${first?.message || first}) — retrying with fonts/remote-imgs sanitized`);
1016
+ const buf = await htmlToDocx(sanitized) as Buffer | ArrayBuffer;
1017
+ out = Buffer.isBuffer(buf) ? buf : Buffer.from(buf as ArrayBuffer);
1018
+ }
995
1019
  fs.writeFileSync(filePath, out);
996
1020
  } catch (e: any) {
997
- // html-to-docx couldn't produce a docx (rare; usually CJK font
998
- // fallback issues). Fall back to plain .html so the user still
999
- // gets *something* — they'll need Save-As-Web-Page in Word but
1000
- // that's better than no edit path at all.
1021
+ // html-to-docx couldn't produce a docx even sanitized. Fall back
1022
+ // to plain .html so the user still gets *something* — they'll
1023
+ // need Save-As-Web-Page in Word but that's better than no edit
1024
+ // path at all.
1001
1025
  console.warn(` [word-edit] html-to-docx failed: ${e?.message || e} — falling back to .html`);
1002
1026
  const htmlPath = path.join(dir, `${fileBase}.html`);
1003
1027
  fs.writeFileSync(htmlPath, wrapped, "utf-8");
@@ -1082,20 +1106,33 @@ export class MailxService implements MailxApi {
1082
1106
  // save so the cold-start cost is paid only when the user actually
1083
1107
  // edits in Word — most sessions never hit it.
1084
1108
  let debounce: ReturnType<typeof setTimeout> | null = null;
1085
- let lastSize = -1;
1109
+ let lastSig = "";
1110
+ // Word saves the .docx in place on Ctrl+S, but Save-As-Web-Page
1111
+ // writes a SIBLING .htm/.html (same base name) — catch those too so
1112
+ // a web-page save doesn't vanish into an unwatched file (Bob
1113
+ // 2026-07-18, .html-fallback variant of the same report).
1114
+ const watchNames = new Set([`${fileBase}.docx`, `${fileBase}.htm`, `${fileBase}.html`]);
1086
1115
  const watcher = fs.watch(dir, (eventType, name) => {
1087
- if (name !== `${fileBase}.docx`) return;
1116
+ if (!name || !watchNames.has(name)) return;
1117
+ const savedPath = path.join(dir, name);
1118
+ const isDocx = name.endsWith(".docx");
1088
1119
  if (debounce) clearTimeout(debounce);
1089
1120
  debounce = setTimeout(async () => {
1090
1121
  let stat: fs.Stats;
1091
- try { stat = fs.statSync(filePath); } catch { return; }
1092
- if (stat.size === lastSize) return;
1093
- lastSize = stat.size;
1122
+ try { stat = fs.statSync(savedPath); } catch { return; }
1123
+ const sig = `${savedPath}:${stat.size}:${stat.mtimeMs}`;
1124
+ if (sig === lastSig) return;
1125
+ lastSig = sig;
1094
1126
  try {
1095
- const buf = fs.readFileSync(filePath);
1096
- const mammoth = (await import("mammoth")).default;
1097
- const result = await mammoth.convertToHtml({ buffer: buf });
1098
- let inner = result.value || "";
1127
+ let inner: string;
1128
+ if (isDocx) {
1129
+ const buf = fs.readFileSync(savedPath);
1130
+ const mammoth = (await import("mammoth")).default;
1131
+ const result = await mammoth.convertToHtml({ buffer: buf });
1132
+ inner = result.value || "";
1133
+ } else {
1134
+ inner = fs.readFileSync(savedPath, "utf-8");
1135
+ }
1099
1136
  // mammoth returns body-content HTML (no <html><body> wrapper).
1100
1137
  // Defensive: if a wrapper sneaks in, strip it.
1101
1138
  const bodyMatch = inner.match(/<body[^>]*>([\s\S]*?)<\/body>/i);
@@ -1148,35 +1185,41 @@ export class MailxService implements MailxApi {
1148
1185
  if (tryLaunch("xdg-open", [filePath])) opener = "default";
1149
1186
  }
1150
1187
  let debounce: ReturnType<typeof setTimeout> | null = null;
1151
- let lastSize = -1;
1188
+ let lastSig = "";
1152
1189
  const htmlName = path.basename(filePath); // <editId>.html
1153
1190
  const docxName = htmlName.replace(/\.html$/i, ".docx"); // what Word writes on Ctrl+S
1191
+ const htmName = htmlName.replace(/\.html$/i, ".htm"); // what Word's Save-As-Web-Page writes
1154
1192
  const docxPath = path.join(dir, docxName);
1193
+ const htmPath = path.join(dir, htmName);
1155
1194
  // CRITICAL: Word, given an .html, converts it to a SIBLING .docx on save
1156
1195
  // (Bob 2026-06-06: "Word turns it into a docx you don't see when I do
1157
- // ^s"). The old watcher only watched the .html, so those edits never came
1158
- // back. Watch BOTH; prefer the .docx (run it through mammoth) since
1159
- // that's the authoritative save, falling back to the .html.
1196
+ // ^s") and Save-As-Web-Page writes a SIBLING .htm (Bob 2026-07-18:
1197
+ // "not seeing the updated .htm when I save it in Word"). The watcher
1198
+ // must cover all three spellings; prefer the .docx (mammoth) as the
1199
+ // authoritative save, then .htm, then the original .html.
1160
1200
  const watcher = fs.watch(dir, (_evt, name) => {
1161
- if (name !== htmlName && name !== docxName) return;
1201
+ if (name !== htmlName && name !== docxName && name !== htmName) return;
1162
1202
  if (debounce) clearTimeout(debounce);
1163
1203
  debounce = setTimeout(async () => {
1164
1204
  try {
1165
1205
  let inner: string;
1166
1206
  if (fs.existsSync(docxPath)) {
1167
1207
  const st = fs.statSync(docxPath);
1168
- if (st.size === lastSize) return;
1169
- lastSize = st.size;
1208
+ const sig = `docx:${st.size}:${st.mtimeMs}`;
1209
+ if (sig === lastSig) return;
1210
+ lastSig = sig;
1170
1211
  const mammoth = (await import("mammoth")).default;
1171
1212
  const result = await mammoth.convertToHtml({ buffer: fs.readFileSync(docxPath) });
1172
1213
  inner = result.value || "";
1173
1214
  const bm = inner.match(/<body[^>]*>([\s\S]*?)<\/body>/i);
1174
1215
  if (bm) inner = bm[1];
1175
1216
  } else {
1176
- const st = fs.statSync(filePath);
1177
- if (st.size === lastSize) return;
1178
- lastSize = st.size;
1179
- const updatedHtml = fs.readFileSync(filePath, "utf-8");
1217
+ const srcPath = fs.existsSync(htmPath) ? htmPath : filePath;
1218
+ const st = fs.statSync(srcPath);
1219
+ const sig = `${srcPath}:${st.size}:${st.mtimeMs}`;
1220
+ if (sig === lastSig) return;
1221
+ lastSig = sig;
1222
+ const updatedHtml = fs.readFileSync(srcPath, "utf-8");
1180
1223
  const bm = updatedHtml.match(/<body[^>]*>([\s\S]*?)<\/body>/i);
1181
1224
  inner = bm ? bm[1] : updatedHtml;
1182
1225
  }