@bobfrankston/rmfmail 1.0.501 → 1.0.503

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.
Files changed (50) hide show
  1. package/client/app.js +82 -32
  2. package/client/app.js.map +1 -1
  3. package/client/app.ts +88 -32
  4. package/client/components/message-viewer.js +51 -21
  5. package/client/components/message-viewer.js.map +1 -1
  6. package/client/components/message-viewer.ts +38 -19
  7. package/client/compose/compose.css +14 -0
  8. package/client/compose/compose.html +4 -3
  9. package/client/compose/compose.js +288 -36
  10. package/client/compose/compose.js.map +1 -1
  11. package/client/compose/compose.ts +270 -40
  12. package/client/compose/editor-help.js +107 -0
  13. package/client/compose/editor-help.js.map +1 -0
  14. package/client/compose/editor-help.ts +106 -0
  15. package/client/compose/editor.js +43 -0
  16. package/client/compose/editor.js.map +1 -1
  17. package/client/compose/editor.ts +22 -0
  18. package/client/index.html +1 -4
  19. package/docs/editor.md +92 -0
  20. package/package.json +4 -2
  21. package/packages/mailx-service/html-to-docx.d.ts +1 -0
  22. package/packages/mailx-service/index.d.ts +10 -2
  23. package/packages/mailx-service/index.d.ts.map +1 -1
  24. package/packages/mailx-service/index.js +143 -21
  25. package/packages/mailx-service/index.js.map +1 -1
  26. package/packages/mailx-service/index.ts +114 -21
  27. package/packages/mailx-service/package.json +5 -3
  28. package/packages/mailx-settings/docs/accounts.md +41 -0
  29. package/packages/mailx-settings/docs/allowlist.md +27 -0
  30. package/packages/mailx-settings/docs/clients.md +24 -0
  31. package/packages/mailx-settings/docs/config.md +32 -0
  32. package/packages/mailx-settings/docs/contact-rules.md +40 -0
  33. package/packages/mailx-settings/docs/contacts.md +48 -0
  34. package/packages/mailx-settings/docs/editor.md +92 -0
  35. package/packages/mailx-settings/docs/preferences.md +43 -0
  36. package/packages/mailx-settings/index.d.ts +2 -2
  37. package/packages/mailx-settings/index.d.ts.map +1 -1
  38. package/packages/mailx-settings/index.js +7 -9
  39. package/packages/mailx-settings/index.js.map +1 -1
  40. package/packages/mailx-settings/index.ts +7 -9
  41. package/packages/mailx-types/groups.d.ts +38 -0
  42. package/packages/mailx-types/groups.d.ts.map +1 -0
  43. package/packages/mailx-types/groups.js +99 -0
  44. package/packages/mailx-types/groups.js.map +1 -0
  45. package/packages/mailx-types/groups.ts +112 -0
  46. package/packages/mailx-types/index.d.ts +2 -0
  47. package/packages/mailx-types/index.d.ts.map +1 -1
  48. package/packages/mailx-types/index.js +5 -0
  49. package/packages/mailx-types/index.js.map +1 -1
  50. package/packages/mailx-types/index.ts +9 -0
@@ -657,16 +657,31 @@ export class MailxService {
657
657
  async openInWord(editId: string, html: string): Promise<{ ok: boolean; path: string; opener: string }> {
658
658
  const dir = path.join(getConfigDir(), "external-edit");
659
659
  fs.mkdirSync(dir, { recursive: true });
660
- const filePath = path.join(dir, `${editId}.html`);
661
- // Wrap in a minimal HTML doc so Word picks up encoding + treats the
662
- // body content as the document. Word imports the <body> contents and
663
- // converts them to its own model; saving HTML preserves enough of
664
- // the structure for re-import (paragraphs, links, basic formatting).
660
+ // Pre-convert to .docx (Word's native format) so Save in Word writes
661
+ // back to the same file naturally no Save-As-Web-Page gymnastics.
662
+ // The .html-as-input flow had a 50% rate of "user saved but rmfmail
663
+ // didn't reload" because Word silently switched format to .docx.
664
+ const filePath = path.join(dir, `${editId}.docx`);
665
665
  const wrapped = `<!doctype html>
666
666
  <html><head><meta charset="utf-8"><title>mailx draft</title></head>
667
667
  <body>${html || "<p></p>"}</body></html>
668
668
  `;
669
- fs.writeFileSync(filePath, wrapped, "utf-8");
669
+ try {
670
+ const htmlToDocx = (await import("html-to-docx")).default;
671
+ const buf = await htmlToDocx(wrapped) as Buffer | ArrayBuffer;
672
+ const out = Buffer.isBuffer(buf) ? buf : Buffer.from(buf as ArrayBuffer);
673
+ fs.writeFileSync(filePath, out);
674
+ } catch (e: any) {
675
+ // html-to-docx couldn't produce a docx (rare; usually CJK font
676
+ // fallback issues). Fall back to plain .html so the user still
677
+ // gets *something* — they'll need Save-As-Web-Page in Word but
678
+ // that's better than no edit path at all.
679
+ console.warn(` [word-edit] html-to-docx failed: ${e?.message || e} — falling back to .html`);
680
+ const htmlPath = path.join(dir, `${editId}.html`);
681
+ fs.writeFileSync(htmlPath, wrapped, "utf-8");
682
+ // Use the .html path instead of .docx for both launch and watch.
683
+ return this.openExternalHtmlFallback(editId, htmlPath);
684
+ }
670
685
 
671
686
  // Stop any existing watcher for this edit (re-open re-arms cleanly).
672
687
  this.wordEdits.get(editId)?.stop();
@@ -739,24 +754,37 @@ export class MailxService {
739
754
  // UI only reloads once per save. Watch the directory rather than the
740
755
  // file directly because Word writes via temp-file rename, which can
741
756
  // invalidate a file-level watch.
757
+ //
758
+ // On save: read the .docx, run mammoth to convert to HTML, emit the
759
+ // wordEditUpdated event. Mammoth is dynamically imported on first
760
+ // save so the cold-start cost is paid only when the user actually
761
+ // edits in Word — most sessions never hit it.
742
762
  let debounce: ReturnType<typeof setTimeout> | null = null;
743
763
  let lastSize = -1;
744
764
  const watcher = fs.watch(dir, (eventType, name) => {
745
- if (name !== `${editId}.html`) return;
765
+ if (name !== `${editId}.docx`) return;
746
766
  if (debounce) clearTimeout(debounce);
747
- debounce = setTimeout(() => {
767
+ debounce = setTimeout(async () => {
748
768
  let stat: fs.Stats;
749
769
  try { stat = fs.statSync(filePath); } catch { return; }
750
- // Skip duplicate events with the same size — Word fires several
751
- // change notifications per save and we only want one reload.
752
770
  if (stat.size === lastSize) return;
753
771
  lastSize = stat.size;
754
772
  try {
755
- const updatedHtml = fs.readFileSync(filePath, "utf-8");
756
- // Strip the wrapper to extract just the body content.
757
- const bodyMatch = updatedHtml.match(/<body[^>]*>([\s\S]*?)<\/body>/i);
758
- const inner = bodyMatch ? bodyMatch[1] : updatedHtml;
773
+ const buf = fs.readFileSync(filePath);
774
+ const mammoth = (await import("mammoth")).default;
775
+ const result = await mammoth.convertToHtml({ buffer: buf });
776
+ let inner = result.value || "";
777
+ // mammoth returns body-content HTML (no <html><body> wrapper).
778
+ // Defensive: if a wrapper sneaks in, strip it.
779
+ const bodyMatch = inner.match(/<body[^>]*>([\s\S]*?)<\/body>/i);
780
+ if (bodyMatch) inner = bodyMatch[1];
759
781
  this.imapManager.emit("wordEditUpdated", { editId, html: inner });
782
+ // Bring the rmfmail compose window to front so the user
783
+ // doesn't need to Alt+Tab — saves a step in the round-trip.
784
+ try {
785
+ const host = await import("@bobfrankston/mailx-host");
786
+ if ((host as any).focusMainWindow) (host as any).focusMainWindow();
787
+ } catch { /* host doesn't expose focus yet — non-fatal */ }
760
788
  } catch (e: any) {
761
789
  console.error(` [word-edit] read after save failed: ${e.message}`);
762
790
  }
@@ -771,6 +799,58 @@ export class MailxService {
771
799
  return { ok: opener !== "none", path: filePath, opener };
772
800
  }
773
801
 
802
+ /** Fallback path used when html-to-docx fails. Reproduces the old
803
+ * .html-based flow: writes <editId>.html, watches it, emits the body
804
+ * content on save. User has to Save-As-Web-Page in Word for the file
805
+ * to update — but at least the edit path works. */
806
+ private async openExternalHtmlFallback(editId: string, filePath: string): Promise<{ ok: boolean; path: string; opener: string }> {
807
+ const dir = path.dirname(filePath);
808
+ this.wordEdits.get(editId)?.stop();
809
+ const { spawn } = await import("node:child_process");
810
+ const tryLaunch = (cmd: string, args: string[]): boolean => {
811
+ try {
812
+ const child = spawn(cmd, args, { detached: true, stdio: "ignore", windowsHide: true });
813
+ child.on("error", () => { /* */ });
814
+ child.unref();
815
+ return true;
816
+ } catch { return false; }
817
+ };
818
+ let opener = "none";
819
+ if (process.platform === "win32") {
820
+ if (tryLaunch("cmd", ["/c", "start", "", "/B", "winword.exe", filePath])) opener = "word";
821
+ else if (tryLaunch("cmd", ["/c", "start", "", filePath])) opener = "default";
822
+ } else if (process.platform === "darwin") {
823
+ if (tryLaunch("open", ["-a", "Microsoft Word", filePath])) opener = "word";
824
+ else if (tryLaunch("open", [filePath])) opener = "default";
825
+ } else {
826
+ if (tryLaunch("xdg-open", [filePath])) opener = "default";
827
+ }
828
+ let debounce: ReturnType<typeof setTimeout> | null = null;
829
+ let lastSize = -1;
830
+ const fileName = path.basename(filePath);
831
+ const watcher = fs.watch(dir, (_evt, name) => {
832
+ if (name !== fileName) return;
833
+ if (debounce) clearTimeout(debounce);
834
+ debounce = setTimeout(() => {
835
+ let stat: fs.Stats;
836
+ try { stat = fs.statSync(filePath); } catch { return; }
837
+ if (stat.size === lastSize) return;
838
+ lastSize = stat.size;
839
+ try {
840
+ const updatedHtml = fs.readFileSync(filePath, "utf-8");
841
+ const bodyMatch = updatedHtml.match(/<body[^>]*>([\s\S]*?)<\/body>/i);
842
+ const inner = bodyMatch ? bodyMatch[1] : updatedHtml;
843
+ this.imapManager.emit("wordEditUpdated", { editId, html: inner });
844
+ } catch (e: any) {
845
+ console.error(` [word-edit fallback] read failed: ${e.message}`);
846
+ }
847
+ }, 300);
848
+ });
849
+ const stop = () => { try { watcher.close(); } catch { /* */ } if (debounce) clearTimeout(debounce); };
850
+ this.wordEdits.set(editId, { path: filePath, stop });
851
+ return { ok: opener !== "none", path: filePath, opener };
852
+ }
853
+
774
854
  /** End external editing. Stops the watcher, removes the temp file.
775
855
  * Caller is the compose UI when the user closes the window or sends. */
776
856
  async closeWordEdit(editId: string): Promise<void> {
@@ -2123,18 +2203,31 @@ export class MailxService {
2123
2203
  return applyEdits(content, edits);
2124
2204
  }
2125
2205
 
2126
- /** Return the help section for a named config file, extracted from docs/config-help.md.
2127
- * Matches a level-2 heading whose text equals the filename. Returns markdown. */
2206
+ /** Return the help markdown for a named config file. Prefers a per-file
2207
+ * doc (`<name>.md` minus the `.jsonc` suffix e.g. `contacts.md` for
2208
+ * `contacts.jsonc`) shipped in the package's `docs/` dir; falls back to
2209
+ * the legacy `config-help.md`-with-sections format if the per-file
2210
+ * isn't available. */
2128
2211
  async readConfigHelp(name: string): Promise<string> {
2129
- const WHITELIST = ["accounts.jsonc", "allowlist.jsonc", "clients.jsonc", "config.jsonc", "contacts.jsonc"];
2212
+ const WHITELIST = ["accounts.jsonc", "allowlist.jsonc", "clients.jsonc", "config.jsonc", "contacts.jsonc", "preferences.jsonc"];
2130
2213
  if (!WHITELIST.includes(name)) return "";
2131
- // Look in the repo root (dev) and in the installed package dir (production).
2132
- const candidates = [
2214
+ const stem = name.replace(/\.jsonc$/, "");
2215
+ // Per-file: `accounts.md`, `contacts.md`, etc. — preferred.
2216
+ const perFileCandidates = [
2217
+ path.join(__dirname, "..", "..", "docs", `${stem}.md`), // workspace dev
2218
+ path.join(__dirname, "docs", `${stem}.md`), // installed pkg (prepack copies here)
2219
+ path.join(__dirname, "..", "docs", `${stem}.md`),
2220
+ ];
2221
+ for (const p of perFileCandidates) {
2222
+ try { return fs.readFileSync(p, "utf-8").trim(); } catch { /* try next */ }
2223
+ }
2224
+ // Legacy fallback: single config-help.md with `## <filename>` sections.
2225
+ const legacyCandidates = [
2133
2226
  path.join(__dirname, "..", "..", "docs", "config-help.md"),
2134
2227
  path.join(__dirname, "config-help.md"),
2135
2228
  ];
2136
2229
  let md = "";
2137
- for (const p of candidates) {
2230
+ for (const p of legacyCandidates) {
2138
2231
  try { md = fs.readFileSync(p, "utf-8"); break; } catch { /* try next */ }
2139
2232
  }
2140
2233
  if (!md) return "";
@@ -2144,7 +2237,7 @@ export class MailxService {
2144
2237
  for (const line of lines) {
2145
2238
  const h2 = /^##\s+(.+?)\s*$/.exec(line);
2146
2239
  if (h2) {
2147
- if (inSection) break; // next section — stop
2240
+ if (inSection) break;
2148
2241
  if (h2[1].trim() === name) { inSection = true; continue; }
2149
2242
  }
2150
2243
  if (inSection) out.push(line);
@@ -9,11 +9,13 @@
9
9
  },
10
10
  "license": "ISC",
11
11
  "dependencies": {
12
- "@bobfrankston/mailx-types": "file:../mailx-types",
13
- "@bobfrankston/mailx-store": "file:../mailx-store",
14
12
  "@bobfrankston/mailx-imap": "file:../mailx-imap",
15
13
  "@bobfrankston/mailx-settings": "file:../mailx-settings",
16
- "mailparser": "^3.7.2"
14
+ "@bobfrankston/mailx-store": "file:../mailx-store",
15
+ "@bobfrankston/mailx-types": "file:../mailx-types",
16
+ "html-to-docx": "^1.8.0",
17
+ "mailparser": "^3.7.2",
18
+ "mammoth": "^1.12.0"
17
19
  },
18
20
  "repository": {
19
21
  "type": "git",
@@ -0,0 +1,41 @@
1
+ # accounts.jsonc
2
+
3
+ > **Owned by rmfmail. Do not edit this file — your changes will be overwritten the next time the app starts.** The canonical copy ships with each release; this file is a deployed copy for reference. To change account *settings*, edit `accounts.jsonc` (which IS user-editable). To change *documentation*, file an issue.
4
+
5
+ ## What this file documents
6
+
7
+ `accounts.jsonc` lists every email account rmfmail manages. It lives in your shared GDrive folder (`My Drive/home/.rmfmail/`) so all your devices read the same list.
8
+
9
+ ## Shape
10
+
11
+ ```jsonc
12
+ {
13
+ "name": "Bob Frankston", // optional; default name applied to every account that doesn't override
14
+ "accounts": [
15
+ {
16
+ "id": "gmail", // short tag used in folder paths and the UI
17
+ "email": "you@example.com", // primary login address
18
+ "name": "Bob Frankston", // display name (optional if file-level name is set)
19
+ "imap": { "host": "imap.example.com", "port": 993, "tls": true, "auth": "password" },
20
+ "smtp": { "host": "smtp.example.com", "port": 465, "tls": true, "auth": "password" },
21
+ "enabled": true, // false skips this account at startup
22
+ "identityDomains": ["alias.com"] // extra domains for Reply-From auto-detect
23
+ }
24
+ ]
25
+ }
26
+ ```
27
+
28
+ ## Field rules
29
+
30
+ - **id** — short identifier. Used in folder paths (e.g. `bob.ma/INBOX`) and as a stable key when accounts.jsonc is edited.
31
+ - **email** — primary login. Determines provider auto-config when `imap`/`smtp` are omitted.
32
+ - **imap / smtp** — explicit server config. Omit for known providers (Gmail / Outlook / Yahoo / iCloud / Google Workspace are detected from the email domain via MX records).
33
+ - **auth** — `"password"` for traditional IMAP/SMTP, `"oauth2"` for Gmail/Google Workspace/Outlook.
34
+ - **enabled** — set `false` to keep the account record but skip sync at startup.
35
+ - **identityDomains** — addresses you receive at on alternative domains. When you Reply, rmfmail picks the matching identity address as From instead of the account's primary.
36
+
37
+ ## Notes
38
+
39
+ - JSONC: `// line comments` and trailing commas are allowed. The parser is lenient.
40
+ - Adding/removing accounts requires a daemon restart (`rmfmail -kill && rmfmail`). A status banner reminds you when the file changes.
41
+ - For OAuth accounts (Gmail, Google Workspace), the token cache lives in `~/.rmfmail/tokens/` and is per-machine.
@@ -0,0 +1,27 @@
1
+ # allowlist.jsonc
2
+
3
+ > **Owned by rmfmail. Do not edit this file — your changes will be overwritten the next time the app starts.** This is reference documentation. To change allow-list data, edit `allowlist.jsonc` itself.
4
+
5
+ ## What this file documents
6
+
7
+ `allowlist.jsonc` controls which senders' remote content (images, etc.) loads automatically and which senders/domains are flagged with a ⚠ banner. Lives in `My Drive/home/.rmfmail/`.
8
+
9
+ ## Shape
10
+
11
+ ```jsonc
12
+ {
13
+ "senders": ["alice@example.com"], // load remote content for these senders
14
+ "domains": ["example.com"], // load remote content for any sender at these domains
15
+ "recipients": ["you@example.com"], // addresses you receive at — treated as "your inbox identity"
16
+ "flaggedSenders": ["spam@bad.com"], // viewer shows ⚠ FLAGGED banner for these senders
17
+ "flaggedDomains": ["phishing.example"] // viewer shows ⚠ FLAGGED banner for any sender at these domains
18
+ }
19
+ ```
20
+
21
+ ## Notes
22
+
23
+ - All entries are case-insensitive plain email or domain strings.
24
+ - Multi-client safe: each device's save merges with the cloud copy (set-union), so adds from any device propagate.
25
+ - Use the message viewer's "allow remote content" button to add a sender/domain to `senders`/`domains` interactively.
26
+ - Use the right-click menu in the viewer to flag a sender/domain — adds to `flaggedSenders`/`flaggedDomains`.
27
+ - JSONC: `// line comments` and trailing commas are allowed.
@@ -0,0 +1,24 @@
1
+ # clients.jsonc
2
+
3
+ > **Owned by rmfmail. Do not edit this file — your changes will be overwritten the next time the app starts.** This is reference documentation.
4
+
5
+ ## What this file documents
6
+
7
+ `clients.jsonc` is the registry of devices running rmfmail under your account. It's auto-managed: each device adds itself on first launch and refreshes its `lastSeen` timestamp on every startup. You don't normally need to look at it.
8
+
9
+ ## Shape
10
+
11
+ ```jsonc
12
+ {
13
+ "devices": [
14
+ { "id": "abc-uuid", "name": "Pixel 9 Pro Fold", "lastSeen": 1746381600000, "accounts": ["gmail", "bobma"] },
15
+ { "id": "def-uuid", "name": "rmf39 desktop", "lastSeen": 1746399999000, "accounts": ["gmail", "bobma"] }
16
+ ]
17
+ }
18
+ ```
19
+
20
+ ## Notes
21
+
22
+ - `id` is a stable UUID generated on first launch and stored locally (`localStorage` on Android, config dir on desktop).
23
+ - `accounts` is the list of account ids this device is currently configured for.
24
+ - Removing a device from this file forces it to re-register on next launch — harmless but pointless.
@@ -0,0 +1,32 @@
1
+ # config.jsonc
2
+
3
+ > **Owned by rmfmail. Do not edit this DOCUMENTATION file — changes will be overwritten.** Note: the **actual** `config.jsonc` IS user-editable; this `.md` file is just the reference for it.
4
+
5
+ ## What this file documents
6
+
7
+ `config.jsonc` is your *local, per-machine* config. It is **NOT** synced to GDrive — every machine has its own. It points at the shared GDrive folder and overrides machine-specific paths.
8
+
9
+ Lives at `~/.rmfmail/config.jsonc` on the local filesystem.
10
+
11
+ ## Shape
12
+
13
+ ```jsonc
14
+ {
15
+ "sharedDir": {
16
+ "provider": "gdrive",
17
+ "path": ".rmfmail", // folder name in My Drive (currently My Drive/home/.rmfmail)
18
+ "folderId": "1AbCdEf...XYZ" // resolved Drive folderId, cached after first lookup
19
+ },
20
+ "storePath": "C:/Users/Bob/.rmfmail/mailxstore", // local directory for .eml message bodies
21
+ "historyDays": 30 // how far back to sync; overrides the shared default
22
+ }
23
+ ```
24
+
25
+ ## Notes
26
+
27
+ - `sharedDir.provider`: `"gdrive"` is the only currently-supported value (OneDrive/Dropbox were removed).
28
+ - `sharedDir.folderId` is resolved at startup if missing; caching it avoids a Drive query on every launch.
29
+ - `storePath` is where `.eml` bodies are stored locally. Defaults to `~/.rmfmail/mailxstore` on desktop. Android uses app-private sandbox storage and ignores this field.
30
+ - `historyDays` is a per-machine override for the cloud-synced default in `preferences.jsonc`.
31
+ - Because this file is local, edits don't propagate to other devices. Use `accounts.jsonc` / `preferences.jsonc` for shared settings.
32
+ - JSONC: `// line comments` and trailing commas are allowed.
@@ -0,0 +1,40 @@
1
+ # contact-rules.jsonc
2
+
3
+ > **Owned by rmfmail. Do not edit this file — your changes will be overwritten the next time the app starts.** This is reference documentation. The actual rules ship with the app and update on every release.
4
+
5
+ ## What this file documents
6
+
7
+ `contact-rules.jsonc` defines the **global** junk-contact filter patterns (noreply, mailer-daemon, gateway domains, one-off-shape locals, etc.). It is *NOT* user-editable — it's part of the app, deployed alongside your data so you can see what's being filtered.
8
+
9
+ User-specific filters live in `contacts.jsonc` (`denylist` exact match; `denylistPatterns` regex array — TODO).
10
+
11
+ ## Shape
12
+
13
+ ```jsonc
14
+ {
15
+ "rulesVersion": "v3-domain-oneoff", // bump when patterns tighten — triggers one-shot purge
16
+ "junk": {
17
+ "localExact": "^(no-?reply|do-?not-?reply|noreply|mailer-daemon|...)$",
18
+ "localSuffix": "(-bounces|\\+bounces|-noreply|-no-reply|...)$",
19
+ "localPrefix": "^(no-?reply|noreply|notifications?|alerts?|bounces?|mailer)[-_+]",
20
+ "localOneoff": "^[0-9a-f]{4,}\\.[0-9a-f]{4,}(\\.[0-9a-z]{6,})?$",
21
+ "domain": "^(txt\\.voice\\.google\\.com|reply\\.facebook\\.com|reply\\.linkedin\\.com)$"
22
+ }
23
+ }
24
+ ```
25
+
26
+ ## How rules apply
27
+
28
+ Each newly-harvested address is checked against ALL rule patterns; any match drops it. Each rule is a single regex, case-insensitive:
29
+
30
+ - **localExact** — whole-string match against the local part (before `@`).
31
+ - **localSuffix** — local part ends with the pattern.
32
+ - **localPrefix** — local part begins with the pattern + a `-` / `_` / `+` separator. Catches `noreply-<random>` style.
33
+ - **localOneoff** — full-shape match for per-message gateway addresses (Google Voice SMS uses three dot-separated alphanumeric segments).
34
+ - **domain** — whole-string match against the domain (after `@`).
35
+
36
+ ## When rules update
37
+
38
+ Each rmfmail release ships an updated `contact-rules.jsonc`. On startup, the app compares the bundled `rulesVersion` to the value stored locally. If they differ, the app sweeps the contacts table once and removes rows the new rules now reject — then records the new version so the sweep doesn't repeat.
39
+
40
+ To add a rule: file an issue, or fork+PR. (Eventually `rules.jsonc` per-user will allow personal additions without forking.)
@@ -0,0 +1,48 @@
1
+ # contacts.jsonc
2
+
3
+ > **Owned by rmfmail. Do not edit this file — your changes will be overwritten the next time the app starts.** This is reference documentation. To change contact data, edit `contacts.jsonc` itself.
4
+
5
+ ## What this file documents
6
+
7
+ `contacts.jsonc` is your address book. It's split into three tiers and lives in `My Drive/home/.rmfmail/`.
8
+
9
+ ## Shape
10
+
11
+ ```jsonc
12
+ {
13
+ "preferred": [
14
+ { "name": "David Reed", "email": "dpreed@example.com", "organization": "Deep Plum" }
15
+ ],
16
+ "denylist": [
17
+ "spammer@example.com"
18
+ ],
19
+ "discovered": [
20
+ { "name": "Bob", "email": "bob@example.com", "useCount": 715, "lastUsed": 1777921829000 }
21
+ ]
22
+ }
23
+ ```
24
+
25
+ ## Tiers
26
+
27
+ - **preferred** — manually-curated, top-ranked in autocomplete. Add people you actually want to reach quickly.
28
+ - **denylist** — addresses (lowercased) excluded from autocomplete entirely.
29
+ - **discovered** — auto-collected from sent/received mail. `useCount` × recency drives autocomplete ranking. Junk patterns (see below) are dropped at insertion.
30
+
31
+ ## Global junk filter
32
+
33
+ Addresses matching any of these are dropped at insertion AND swept on startup:
34
+
35
+ - Whole local part: `noreply`, `no-reply`, `do-not-reply`, `mailer-daemon`, `postmaster`, `bounces`, etc.
36
+ - Local-part prefix: `noreply-<random>`, `notifications-<id>`, `alerts-<x>`, `bounces-<x>`, `mailer-<x>`
37
+ - Local-part suffix: `*-bounces`, `*+bounces`, `*-noreply`, `*-notifications`
38
+ - Multi-segment one-off shape: `16179691997.15082868100.nfblcbll1x` (per-message gateway addresses)
39
+ - Domains: `txt.voice.google.com` (Google Voice SMS gateway), `reply.facebook.com`, `reply.linkedin.com`
40
+
41
+ The full pattern set lives in `contact-rules.jsonc` (shipped with the app). Bumping `rulesVersion` in that file triggers a one-shot purge of historical rows the new rules now reject.
42
+
43
+ ## Notes
44
+
45
+ - JSONC: `// line comments` and trailing commas are allowed.
46
+ - `useCount` and `lastUsed` are auto-managed; manually editing them only changes ranking briefly until the next sync.
47
+ - Removing a `preferred` entry doesn't add it to `denylist` — it just demotes back to `discovered` (or drops if not seen in mail).
48
+ - Each device contributes its observed addresses to `discovered`. The file accumulates the union across devices.
@@ -0,0 +1,92 @@
1
+ # Compose editor — formatting and shortcuts
2
+
3
+ mailx ships with two rich-text editors: **Quill** (default) and **tiptap**.
4
+ Most things work the same in both. Differences are called out below.
5
+
6
+ Switch editors via **Settings → Editor → Quill | tiptap**.
7
+
8
+ ## Universal — works in both editors
9
+
10
+ | Action | Shortcut | Where |
11
+ |---|---|---|
12
+ | **Send** | Ctrl+Enter | toolbar Send button |
13
+ | Bold / Italic / Underline | Ctrl+B / Ctrl+I / Ctrl+U | toolbar B / I / U |
14
+ | Strikethrough | Ctrl+Shift+X | toolbar S |
15
+ | Bulleted list | Ctrl+Shift+8 | toolbar • |
16
+ | Ordered list | Ctrl+Shift+7 | toolbar 1. |
17
+ | Insert / edit link | Ctrl+K | toolbar 🔗 |
18
+ | Remove link | Ctrl+Shift+K | (no toolbar button — use Ctrl+Shift+K) |
19
+ | Blockquote | (Quill: Ctrl+Shift+Q; tiptap: toolbar) | toolbar `“` |
20
+ | Clear formatting | Ctrl+\\ | toolbar ✖ |
21
+ | Heading (H1 / H2 / H3) | (Quill: format dropdown; tiptap: select dropdown) | "Normal / Heading 1 / 2 / 3" |
22
+ | Undo / Redo | Ctrl+Z / Ctrl+Y | (no toolbar button) |
23
+ | Spell-check | (browser native — red underlines) | right-click word |
24
+ | Paste plain text | Ctrl+Shift+V | (browser native) |
25
+
26
+ ## Quill-only
27
+
28
+ | Action | Shortcut |
29
+ |---|---|
30
+ | Indent / outdent | Ctrl+] / Ctrl+[ |
31
+ | Color text | Ctrl+Shift+C |
32
+ | Format dropdown | toolbar (left side) |
33
+ | Inline code | toolbar `<>` |
34
+ | Code block | toolbar |
35
+ | Image inserter | (no built-in; use drag-and-drop or paste) |
36
+
37
+ Quill has a more elaborate toolbar with format-specific dropdowns (font,
38
+ size, color). Internally Quill uses its own *Delta* document model — copy/
39
+ paste from Word/Outlook sometimes leaves extra `<p><br></p>` empty
40
+ paragraphs that you'll see in the sent message body.
41
+
42
+ ## tiptap-only
43
+
44
+ | Action | Where |
45
+ |---|---|
46
+ | Heading select | left of toolbar |
47
+ | Toggle bold / italic / underline / strike | toolbar B / I / U / S |
48
+ | Blockquote | toolbar `“` |
49
+ | Image (drag-and-drop) | drop a file into the body |
50
+
51
+ tiptap is built on ProseMirror. Output HTML is cleaner than Quill on
52
+ Word/Outlook paste roundtrips. Bundle is smaller. Some Quill toolbar
53
+ features (inline code, indent shortcuts, color picker) aren't wired —
54
+ use the heading select / format menu instead.
55
+
56
+ ## Common features (across both)
57
+
58
+ - **Drag-and-drop attachments** — drop files anywhere on the compose
59
+ window to attach. Overlay highlights the drop target while dragging.
60
+ - **Edit in Word / LibreOffice** — toolbar **Edit in Word** button opens
61
+ the body in your default external editor (configurable in
62
+ Settings → External editor). Save in the external editor and the body
63
+ reloads here. Behind the scenes mailx writes a temporary `.docx` file
64
+ (see `~/.rmfmail/external-edit/`) and watches it for changes.
65
+ - **Auto-save drafts** — every 5 seconds (and on input debounce + on
66
+ blur). Drafts land in the Drafts folder via IMAP append.
67
+ - **Address auto-completion** — type a partial name in To/Cc/Bcc; matches
68
+ rank by recency × use-count. Group names from `contacts.jsonc → groups`
69
+ also surface here.
70
+ - **Address-field expansion** — recipient fields are auto-growing
71
+ textareas; long lists wrap to multiple lines.
72
+ - **Group expansion on send** — type a group name (e.g. `family`) in
73
+ To/Cc/Bcc and it expands to the address list at send time.
74
+ - **Ghost-text autocomplete** (off by default) — Settings →
75
+ AI autocomplete → on. Predicts the next words while you type.
76
+
77
+ ## When the toolbar doesn't appear
78
+
79
+ The editor loads from a CDN (jsdelivr). If your network can't reach it,
80
+ the toolbar disappears and a plain `contenteditable` fallback takes over.
81
+ Status bar shows the failure. mailx now tries the *other* editor before
82
+ falling all the way back; if both fail you get a plain textarea with no
83
+ shortcuts and no toolbar — sending still works.
84
+
85
+ ## See also
86
+
87
+ - `accounts.md`, `contacts.md`, `allowlist.md`, `clients.md`, `config.md`,
88
+ `preferences.md` — config-file references (these live in your GDrive
89
+ `.rmfmail/` folder).
90
+ - This `editor.md` is **app-internal** — it ships with each release and
91
+ documents the editor as it currently exists in the version you're
92
+ running. It is not deployed to your user folder.
@@ -0,0 +1,43 @@
1
+ # preferences.jsonc
2
+
3
+ > **Owned by rmfmail. Do not edit this file — your changes will be overwritten the next time the app starts.** This is reference documentation.
4
+
5
+ ## What this file documents
6
+
7
+ `preferences.jsonc` holds your shared UI / sync / autocomplete preferences, synced via `My Drive/home/.rmfmail/`.
8
+
9
+ ## Shape
10
+
11
+ ```jsonc
12
+ {
13
+ "ui": {
14
+ "theme": "system", // "system" | "light" | "dark"
15
+ "twoLine": false, // two-line message-list rows
16
+ "previewPane": true,
17
+ "previewSnippets": true,
18
+ "threaded": false,
19
+ "flaggedOnly": false,
20
+ "folderCounts": false,
21
+ "calendarSidebar": false,
22
+ "editor": "quill" // "quill" | "tiptap"
23
+ },
24
+ "sync": {
25
+ "intervalMinutes": 5, // full-sync interval (per-folder); IDLE handles INBOX inline
26
+ "historyDays": 30, // how far back to sync (overridable per-machine in config.jsonc)
27
+ "prefetch": true // background-fetch message bodies after metadata sync
28
+ },
29
+ "autocomplete": {
30
+ "enabled": false, // ghost-text completions in compose
31
+ "provider": "ollama", // "ollama" | "claude" | "openai"
32
+ "translateEnabled": false, // AI-powered Translate menu item in viewer
33
+ "proofreadEnabled": false
34
+ }
35
+ }
36
+ ```
37
+
38
+ ## Notes
39
+
40
+ - The View menu and Settings menu in the toolbar both write here.
41
+ - `sync.intervalMinutes` controls the full-folder sync cadence. INBOX uses RFC 2177 IDLE (instant push) on Dovecot accounts; Gmail accounts poll every 30s.
42
+ - `sync.historyDays` is a default; override per-machine via `config.jsonc`.
43
+ - AI features are off by default. Enabling `autocomplete.enabled` requires a configured provider (Ollama local, Claude API key, or OpenAI API key).
@@ -130,8 +130,8 @@ export { getSharedDir };
130
130
  /** Initialize local config if it doesn't exist */
131
131
  export declare function initLocalConfig(sharedDir?: string, storePath?: string): void;
132
132
  /** Initialize config with Google Drive cloud storage.
133
- * Finds or creates the app-owned "mailx" folder via Drive API and stores its ID.
134
- * No mount scanning — API only. Existing settings at other paths (e.g., home/.mailx
133
+ * Finds or creates the app-owned ".rmfmail" folder via Drive API and stores its ID.
134
+ * No mount scanning — API only. Existing settings at other paths (e.g., home/.rmfmail
135
135
  * from Desktop sync) must be migrated manually or via config.jsonc importPath. */
136
136
  export declare function initCloudConfig(provider?: "gdrive"): Promise<void>;
137
137
  declare const DEFAULT_SETTINGS: MailxSettings;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":"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"}
@@ -828,21 +828,19 @@ export function initLocalConfig(sharedDir, storePath) {
828
828
  atomicWrite(LOCAL_CONFIG_PATH, config);
829
829
  }
830
830
  /** Initialize config with Google Drive cloud storage.
831
- * Finds or creates the app-owned "mailx" folder via Drive API and stores its ID.
832
- * No mount scanning — API only. Existing settings at other paths (e.g., home/.mailx
831
+ * Finds or creates the app-owned ".rmfmail" folder via Drive API and stores its ID.
832
+ * No mount scanning — API only. Existing settings at other paths (e.g., home/.rmfmail
833
833
  * from Desktop sync) must be migrated manually or via config.jsonc importPath. */
834
834
  export async function initCloudConfig(provider = "gdrive") {
835
835
  const existing = readLocalConfig();
836
836
  if (existing.sharedDir)
837
837
  return; // Already configured
838
- // Find or create the settings folder via Drive API tries "home/.mailx"
839
- // first (family convention), then "mailx" (default). The found path gets
840
- // saved so future lookups don't re-scan.
838
+ // Find or create the settings folder via Drive API. Stored path is the
839
+ // canonical post-rebrand location; the folderId is what actually drives
840
+ // subsequent reads/writes, so even legacy users with the old folder name
841
+ // work fine since gDriveFindOrCreateFolder resolves whichever exists.
841
842
  const folderId = await gDriveFindOrCreateFolder();
842
- // Detect which path was actually found by reading back from the API
843
- // (gDriveFindOrCreateFolder logs it). For now use "mailx" as default
844
- // label — the folderId is what matters for subsequent reads/writes.
845
- const sharedDir = { provider, path: "home/.mailx", folderId: folderId || undefined };
843
+ const sharedDir = { provider, path: "home/.rmfmail", folderId: folderId || undefined };
846
844
  const config = { ...existing, sharedDir, storePath: existing.storePath || DEFAULT_STORE_PATH };
847
845
  fs.mkdirSync(LOCAL_DIR, { recursive: true });
848
846
  atomicWrite(LOCAL_CONFIG_PATH, config);