@bobfrankston/rmfmail 1.0.502 → 1.0.504
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/bin/mailx.js +9 -1
- package/bin/mailx.js.map +1 -1
- package/bin/mailx.ts +9 -1
- package/client/app.js +82 -32
- package/client/app.js.map +1 -1
- package/client/app.ts +88 -32
- package/client/components/message-viewer.js +51 -21
- package/client/components/message-viewer.js.map +1 -1
- package/client/components/message-viewer.ts +38 -19
- package/client/compose/compose.css +14 -0
- package/client/compose/compose.html +4 -3
- package/client/compose/compose.js +288 -36
- package/client/compose/compose.js.map +1 -1
- package/client/compose/compose.ts +270 -40
- package/client/compose/editor-help.js +107 -0
- package/client/compose/editor-help.js.map +1 -0
- package/client/compose/editor-help.ts +106 -0
- package/client/compose/editor.js +43 -0
- package/client/compose/editor.js.map +1 -1
- package/client/compose/editor.ts +22 -0
- package/client/index.html +1 -4
- package/docs/editor.md +92 -0
- package/package.json +10 -4
- package/packages/mailx-service/html-to-docx.d.ts +1 -0
- package/packages/mailx-service/index.d.ts +10 -2
- package/packages/mailx-service/index.d.ts.map +1 -1
- package/packages/mailx-service/index.js +143 -21
- package/packages/mailx-service/index.js.map +1 -1
- package/packages/mailx-service/index.ts +114 -21
- package/packages/mailx-service/package.json +5 -3
- package/packages/mailx-settings/docs/accounts.md +41 -0
- package/packages/mailx-settings/docs/allowlist.md +27 -0
- package/packages/mailx-settings/docs/clients.md +24 -0
- package/packages/mailx-settings/docs/config.md +32 -0
- package/packages/mailx-settings/docs/contact-rules.md +40 -0
- package/packages/mailx-settings/docs/contacts.md +48 -0
- package/packages/mailx-settings/docs/editor.md +92 -0
- package/packages/mailx-settings/docs/preferences.md +43 -0
- package/packages/mailx-settings/index.d.ts +2 -2
- package/packages/mailx-settings/index.d.ts.map +1 -1
- package/packages/mailx-settings/index.js +120 -23
- package/packages/mailx-settings/index.js.map +1 -1
- package/packages/mailx-settings/index.ts +98 -23
- package/packages/mailx-store/db.d.ts +8 -0
- package/packages/mailx-store/db.d.ts.map +1 -1
- package/packages/mailx-store/db.js +22 -0
- package/packages/mailx-store/db.js.map +1 -1
- package/packages/mailx-store/db.ts +22 -0
- package/packages/mailx-store/file-store.d.ts +31 -13
- package/packages/mailx-store/file-store.d.ts.map +1 -1
- package/packages/mailx-store/file-store.js +75 -31
- package/packages/mailx-store/file-store.js.map +1 -1
- package/packages/mailx-store/file-store.ts +72 -27
- package/packages/mailx-types/groups.d.ts +38 -0
- package/packages/mailx-types/groups.d.ts.map +1 -0
- package/packages/mailx-types/groups.js +99 -0
- package/packages/mailx-types/groups.js.map +1 -0
- package/packages/mailx-types/groups.ts +112 -0
- package/packages/mailx-types/index.d.ts +2 -0
- package/packages/mailx-types/index.d.ts.map +1 -1
- package/packages/mailx-types/index.js +5 -0
- package/packages/mailx-types/index.js.map +1 -1
- package/packages/mailx-types/index.ts +9 -0
|
@@ -678,16 +678,32 @@ export class MailxService {
|
|
|
678
678
|
async openInWord(editId, html) {
|
|
679
679
|
const dir = path.join(getConfigDir(), "external-edit");
|
|
680
680
|
fs.mkdirSync(dir, { recursive: true });
|
|
681
|
-
|
|
682
|
-
//
|
|
683
|
-
//
|
|
684
|
-
//
|
|
685
|
-
|
|
681
|
+
// Pre-convert to .docx (Word's native format) so Save in Word writes
|
|
682
|
+
// back to the same file naturally — no Save-As-Web-Page gymnastics.
|
|
683
|
+
// The .html-as-input flow had a 50% rate of "user saved but rmfmail
|
|
684
|
+
// didn't reload" because Word silently switched format to .docx.
|
|
685
|
+
const filePath = path.join(dir, `${editId}.docx`);
|
|
686
686
|
const wrapped = `<!doctype html>
|
|
687
687
|
<html><head><meta charset="utf-8"><title>mailx draft</title></head>
|
|
688
688
|
<body>${html || "<p></p>"}</body></html>
|
|
689
689
|
`;
|
|
690
|
-
|
|
690
|
+
try {
|
|
691
|
+
const htmlToDocx = (await import("html-to-docx")).default;
|
|
692
|
+
const buf = await htmlToDocx(wrapped);
|
|
693
|
+
const out = Buffer.isBuffer(buf) ? buf : Buffer.from(buf);
|
|
694
|
+
fs.writeFileSync(filePath, out);
|
|
695
|
+
}
|
|
696
|
+
catch (e) {
|
|
697
|
+
// html-to-docx couldn't produce a docx (rare; usually CJK font
|
|
698
|
+
// fallback issues). Fall back to plain .html so the user still
|
|
699
|
+
// gets *something* — they'll need Save-As-Web-Page in Word but
|
|
700
|
+
// that's better than no edit path at all.
|
|
701
|
+
console.warn(` [word-edit] html-to-docx failed: ${e?.message || e} — falling back to .html`);
|
|
702
|
+
const htmlPath = path.join(dir, `${editId}.html`);
|
|
703
|
+
fs.writeFileSync(htmlPath, wrapped, "utf-8");
|
|
704
|
+
// Use the .html path instead of .docx for both launch and watch.
|
|
705
|
+
return this.openExternalHtmlFallback(editId, htmlPath);
|
|
706
|
+
}
|
|
691
707
|
// Stop any existing watcher for this edit (re-open re-arms cleanly).
|
|
692
708
|
this.wordEdits.get(editId)?.stop();
|
|
693
709
|
// Try Word explicitly first; on failure (Word not installed, exec not
|
|
@@ -769,14 +785,19 @@ export class MailxService {
|
|
|
769
785
|
// UI only reloads once per save. Watch the directory rather than the
|
|
770
786
|
// file directly because Word writes via temp-file rename, which can
|
|
771
787
|
// invalidate a file-level watch.
|
|
788
|
+
//
|
|
789
|
+
// On save: read the .docx, run mammoth to convert to HTML, emit the
|
|
790
|
+
// wordEditUpdated event. Mammoth is dynamically imported on first
|
|
791
|
+
// save so the cold-start cost is paid only when the user actually
|
|
792
|
+
// edits in Word — most sessions never hit it.
|
|
772
793
|
let debounce = null;
|
|
773
794
|
let lastSize = -1;
|
|
774
795
|
const watcher = fs.watch(dir, (eventType, name) => {
|
|
775
|
-
if (name !== `${editId}.
|
|
796
|
+
if (name !== `${editId}.docx`)
|
|
776
797
|
return;
|
|
777
798
|
if (debounce)
|
|
778
799
|
clearTimeout(debounce);
|
|
779
|
-
debounce = setTimeout(() => {
|
|
800
|
+
debounce = setTimeout(async () => {
|
|
780
801
|
let stat;
|
|
781
802
|
try {
|
|
782
803
|
stat = fs.statSync(filePath);
|
|
@@ -784,17 +805,28 @@ export class MailxService {
|
|
|
784
805
|
catch {
|
|
785
806
|
return;
|
|
786
807
|
}
|
|
787
|
-
// Skip duplicate events with the same size — Word fires several
|
|
788
|
-
// change notifications per save and we only want one reload.
|
|
789
808
|
if (stat.size === lastSize)
|
|
790
809
|
return;
|
|
791
810
|
lastSize = stat.size;
|
|
792
811
|
try {
|
|
793
|
-
const
|
|
794
|
-
|
|
795
|
-
const
|
|
796
|
-
|
|
812
|
+
const buf = fs.readFileSync(filePath);
|
|
813
|
+
const mammoth = (await import("mammoth")).default;
|
|
814
|
+
const result = await mammoth.convertToHtml({ buffer: buf });
|
|
815
|
+
let inner = result.value || "";
|
|
816
|
+
// mammoth returns body-content HTML (no <html><body> wrapper).
|
|
817
|
+
// Defensive: if a wrapper sneaks in, strip it.
|
|
818
|
+
const bodyMatch = inner.match(/<body[^>]*>([\s\S]*?)<\/body>/i);
|
|
819
|
+
if (bodyMatch)
|
|
820
|
+
inner = bodyMatch[1];
|
|
797
821
|
this.imapManager.emit("wordEditUpdated", { editId, html: inner });
|
|
822
|
+
// Bring the rmfmail compose window to front so the user
|
|
823
|
+
// doesn't need to Alt+Tab — saves a step in the round-trip.
|
|
824
|
+
try {
|
|
825
|
+
const host = await import("@bobfrankston/mailx-host");
|
|
826
|
+
if (host.focusMainWindow)
|
|
827
|
+
host.focusMainWindow();
|
|
828
|
+
}
|
|
829
|
+
catch { /* host doesn't expose focus yet — non-fatal */ }
|
|
798
830
|
}
|
|
799
831
|
catch (e) {
|
|
800
832
|
console.error(` [word-edit] read after save failed: ${e.message}`);
|
|
@@ -812,6 +844,80 @@ export class MailxService {
|
|
|
812
844
|
this.wordEdits.set(editId, { path: filePath, stop });
|
|
813
845
|
return { ok: opener !== "none", path: filePath, opener };
|
|
814
846
|
}
|
|
847
|
+
/** Fallback path used when html-to-docx fails. Reproduces the old
|
|
848
|
+
* .html-based flow: writes <editId>.html, watches it, emits the body
|
|
849
|
+
* content on save. User has to Save-As-Web-Page in Word for the file
|
|
850
|
+
* to update — but at least the edit path works. */
|
|
851
|
+
async openExternalHtmlFallback(editId, filePath) {
|
|
852
|
+
const dir = path.dirname(filePath);
|
|
853
|
+
this.wordEdits.get(editId)?.stop();
|
|
854
|
+
const { spawn } = await import("node:child_process");
|
|
855
|
+
const tryLaunch = (cmd, args) => {
|
|
856
|
+
try {
|
|
857
|
+
const child = spawn(cmd, args, { detached: true, stdio: "ignore", windowsHide: true });
|
|
858
|
+
child.on("error", () => { });
|
|
859
|
+
child.unref();
|
|
860
|
+
return true;
|
|
861
|
+
}
|
|
862
|
+
catch {
|
|
863
|
+
return false;
|
|
864
|
+
}
|
|
865
|
+
};
|
|
866
|
+
let opener = "none";
|
|
867
|
+
if (process.platform === "win32") {
|
|
868
|
+
if (tryLaunch("cmd", ["/c", "start", "", "/B", "winword.exe", filePath]))
|
|
869
|
+
opener = "word";
|
|
870
|
+
else if (tryLaunch("cmd", ["/c", "start", "", filePath]))
|
|
871
|
+
opener = "default";
|
|
872
|
+
}
|
|
873
|
+
else if (process.platform === "darwin") {
|
|
874
|
+
if (tryLaunch("open", ["-a", "Microsoft Word", filePath]))
|
|
875
|
+
opener = "word";
|
|
876
|
+
else if (tryLaunch("open", [filePath]))
|
|
877
|
+
opener = "default";
|
|
878
|
+
}
|
|
879
|
+
else {
|
|
880
|
+
if (tryLaunch("xdg-open", [filePath]))
|
|
881
|
+
opener = "default";
|
|
882
|
+
}
|
|
883
|
+
let debounce = null;
|
|
884
|
+
let lastSize = -1;
|
|
885
|
+
const fileName = path.basename(filePath);
|
|
886
|
+
const watcher = fs.watch(dir, (_evt, name) => {
|
|
887
|
+
if (name !== fileName)
|
|
888
|
+
return;
|
|
889
|
+
if (debounce)
|
|
890
|
+
clearTimeout(debounce);
|
|
891
|
+
debounce = setTimeout(() => {
|
|
892
|
+
let stat;
|
|
893
|
+
try {
|
|
894
|
+
stat = fs.statSync(filePath);
|
|
895
|
+
}
|
|
896
|
+
catch {
|
|
897
|
+
return;
|
|
898
|
+
}
|
|
899
|
+
if (stat.size === lastSize)
|
|
900
|
+
return;
|
|
901
|
+
lastSize = stat.size;
|
|
902
|
+
try {
|
|
903
|
+
const updatedHtml = fs.readFileSync(filePath, "utf-8");
|
|
904
|
+
const bodyMatch = updatedHtml.match(/<body[^>]*>([\s\S]*?)<\/body>/i);
|
|
905
|
+
const inner = bodyMatch ? bodyMatch[1] : updatedHtml;
|
|
906
|
+
this.imapManager.emit("wordEditUpdated", { editId, html: inner });
|
|
907
|
+
}
|
|
908
|
+
catch (e) {
|
|
909
|
+
console.error(` [word-edit fallback] read failed: ${e.message}`);
|
|
910
|
+
}
|
|
911
|
+
}, 300);
|
|
912
|
+
});
|
|
913
|
+
const stop = () => { try {
|
|
914
|
+
watcher.close();
|
|
915
|
+
}
|
|
916
|
+
catch { /* */ } if (debounce)
|
|
917
|
+
clearTimeout(debounce); };
|
|
918
|
+
this.wordEdits.set(editId, { path: filePath, stop });
|
|
919
|
+
return { ok: opener !== "none", path: filePath, opener };
|
|
920
|
+
}
|
|
815
921
|
/** End external editing. Stops the watcher, removes the temp file.
|
|
816
922
|
* Caller is the compose UI when the user closes the window or sends. */
|
|
817
923
|
async closeWordEdit(editId) {
|
|
@@ -2188,19 +2294,35 @@ export class MailxService {
|
|
|
2188
2294
|
});
|
|
2189
2295
|
return applyEdits(content, edits);
|
|
2190
2296
|
}
|
|
2191
|
-
/** Return the help
|
|
2192
|
-
*
|
|
2297
|
+
/** Return the help markdown for a named config file. Prefers a per-file
|
|
2298
|
+
* doc (`<name>.md` minus the `.jsonc` suffix — e.g. `contacts.md` for
|
|
2299
|
+
* `contacts.jsonc`) shipped in the package's `docs/` dir; falls back to
|
|
2300
|
+
* the legacy `config-help.md`-with-sections format if the per-file
|
|
2301
|
+
* isn't available. */
|
|
2193
2302
|
async readConfigHelp(name) {
|
|
2194
|
-
const WHITELIST = ["accounts.jsonc", "allowlist.jsonc", "clients.jsonc", "config.jsonc", "contacts.jsonc"];
|
|
2303
|
+
const WHITELIST = ["accounts.jsonc", "allowlist.jsonc", "clients.jsonc", "config.jsonc", "contacts.jsonc", "preferences.jsonc"];
|
|
2195
2304
|
if (!WHITELIST.includes(name))
|
|
2196
2305
|
return "";
|
|
2197
|
-
|
|
2198
|
-
|
|
2306
|
+
const stem = name.replace(/\.jsonc$/, "");
|
|
2307
|
+
// Per-file: `accounts.md`, `contacts.md`, etc. — preferred.
|
|
2308
|
+
const perFileCandidates = [
|
|
2309
|
+
path.join(__dirname, "..", "..", "docs", `${stem}.md`), // workspace dev
|
|
2310
|
+
path.join(__dirname, "docs", `${stem}.md`), // installed pkg (prepack copies here)
|
|
2311
|
+
path.join(__dirname, "..", "docs", `${stem}.md`),
|
|
2312
|
+
];
|
|
2313
|
+
for (const p of perFileCandidates) {
|
|
2314
|
+
try {
|
|
2315
|
+
return fs.readFileSync(p, "utf-8").trim();
|
|
2316
|
+
}
|
|
2317
|
+
catch { /* try next */ }
|
|
2318
|
+
}
|
|
2319
|
+
// Legacy fallback: single config-help.md with `## <filename>` sections.
|
|
2320
|
+
const legacyCandidates = [
|
|
2199
2321
|
path.join(__dirname, "..", "..", "docs", "config-help.md"),
|
|
2200
2322
|
path.join(__dirname, "config-help.md"),
|
|
2201
2323
|
];
|
|
2202
2324
|
let md = "";
|
|
2203
|
-
for (const p of
|
|
2325
|
+
for (const p of legacyCandidates) {
|
|
2204
2326
|
try {
|
|
2205
2327
|
md = fs.readFileSync(p, "utf-8");
|
|
2206
2328
|
break;
|
|
@@ -2216,7 +2338,7 @@ export class MailxService {
|
|
|
2216
2338
|
const h2 = /^##\s+(.+?)\s*$/.exec(line);
|
|
2217
2339
|
if (h2) {
|
|
2218
2340
|
if (inSection)
|
|
2219
|
-
break;
|
|
2341
|
+
break;
|
|
2220
2342
|
if (h2[1].trim() === name) {
|
|
2221
2343
|
inSection = true;
|
|
2222
2344
|
continue;
|