@bobfrankston/rmfmail 1.0.680 → 1.0.686

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 (83) hide show
  1. package/bin/build-quill.js +35 -0
  2. package/bin/lean-accounts.js +0 -1
  3. package/client/app.bundle.js +172 -55
  4. package/client/app.bundle.js.map +2 -2
  5. package/client/app.js +73 -27
  6. package/client/app.js.map +1 -1
  7. package/client/app.ts +73 -29
  8. package/client/components/context-menu.js +2 -0
  9. package/client/components/context-menu.js.map +1 -1
  10. package/client/components/context-menu.ts +6 -0
  11. package/client/components/folder-tree.js +26 -4
  12. package/client/components/folder-tree.js.map +1 -1
  13. package/client/components/folder-tree.ts +21 -4
  14. package/client/components/message-list.js +108 -40
  15. package/client/components/message-list.js.map +1 -1
  16. package/client/components/message-list.ts +103 -38
  17. package/client/compose/compose.bundle.js +189 -17
  18. package/client/compose/compose.bundle.js.map +3 -3
  19. package/client/compose/compose.js +51 -3
  20. package/client/compose/compose.js.map +1 -1
  21. package/client/compose/compose.ts +47 -3
  22. package/client/compose/spellcheck.js +178 -12
  23. package/client/compose/spellcheck.js.map +1 -1
  24. package/client/compose/spellcheck.ts +168 -8
  25. package/client/lib/api-client.js +3 -0
  26. package/client/lib/api-client.js.map +1 -1
  27. package/client/lib/api-client.ts +4 -0
  28. package/client/lib/mailxapi.js +3 -0
  29. package/client/lib/quill/quill.js +3 -0
  30. package/client/lib/quill/quill.snow.css +10 -0
  31. package/client/lib/rmf-tiny.js +25 -6
  32. package/docs/accounts.md +7 -2
  33. package/package.json +8 -8
  34. package/packages/mailx-core/index.d.ts.map +1 -1
  35. package/packages/mailx-core/index.js +2 -12
  36. package/packages/mailx-core/index.js.map +1 -1
  37. package/packages/mailx-core/index.ts +2 -12
  38. package/packages/mailx-imap/index.d.ts.map +1 -1
  39. package/packages/mailx-imap/index.js +31 -6
  40. package/packages/mailx-imap/index.js.map +1 -1
  41. package/packages/mailx-imap/index.ts +32 -6
  42. package/packages/mailx-imap/node_modules.npmglobalize-stash-11884/.package-lock.json +116 -0
  43. package/packages/mailx-imap/package-lock.json +2 -2
  44. package/packages/mailx-imap/package.json +1 -1
  45. package/packages/mailx-service/index.d.ts +22 -0
  46. package/packages/mailx-service/index.d.ts.map +1 -1
  47. package/packages/mailx-service/index.js +134 -6
  48. package/packages/mailx-service/index.js.map +1 -1
  49. package/packages/mailx-service/index.ts +128 -11
  50. package/packages/mailx-service/jsonrpc.js +3 -0
  51. package/packages/mailx-service/jsonrpc.js.map +1 -1
  52. package/packages/mailx-service/jsonrpc.ts +3 -0
  53. package/packages/mailx-service/local-store.d.ts.map +1 -1
  54. package/packages/mailx-service/local-store.js +15 -12
  55. package/packages/mailx-service/local-store.js.map +1 -1
  56. package/packages/mailx-service/local-store.ts +15 -12
  57. package/packages/mailx-settings/docs/accounts.md +14 -1
  58. package/packages/mailx-settings/docs/npmglobalize-disttag.md +90 -0
  59. package/packages/mailx-settings/docs/prod-android.md +88 -0
  60. package/packages/mailx-settings/docs/prod.md +224 -0
  61. package/packages/mailx-settings/docs/push-relay.md +141 -0
  62. package/packages/mailx-settings/docs/rmf-tiny.md +156 -0
  63. package/packages/mailx-settings/index.d.ts +2 -2
  64. package/packages/mailx-settings/index.d.ts.map +1 -1
  65. package/packages/mailx-settings/index.js +13 -10
  66. package/packages/mailx-settings/index.js.map +1 -1
  67. package/packages/mailx-settings/index.ts +13 -9
  68. package/packages/mailx-settings/package.json +1 -1
  69. package/packages/mailx-store/db.d.ts.map +1 -1
  70. package/packages/mailx-store/db.js +44 -6
  71. package/packages/mailx-store/db.js.map +1 -1
  72. package/packages/mailx-store/db.ts +47 -6
  73. package/packages/mailx-store/package.json +1 -1
  74. package/packages/mailx-store-web/package.json +4 -1
  75. package/packages/mailx-store-web/web-settings.d.ts.map +1 -1
  76. package/packages/mailx-store-web/web-settings.js +0 -1
  77. package/packages/mailx-store-web/web-settings.js.map +1 -1
  78. package/packages/mailx-store-web/web-settings.ts +0 -1
  79. package/packages/mailx-types/index.d.ts +1 -2
  80. package/packages/mailx-types/index.d.ts.map +1 -1
  81. package/packages/mailx-types/index.js.map +1 -1
  82. package/packages/mailx-types/index.ts +1 -2
  83. package/packages/mailx-types/package.json +1 -1
@@ -0,0 +1,224 @@
1
+ # prod / dev release workflow
2
+
3
+ Status: design. Two viable approaches; pick one. Both deliver the same shape:
4
+
5
+ - `rebuild.cmd` publishes builds without affecting what casual users get.
6
+ - A separate `prod.cmd` step promotes a known-good build to be what casual users install.
7
+ - Casual users keep typing `npm install -g @bobfrankston/rmfmail` (no flags).
8
+ - Dev users explicitly opt in.
9
+
10
+ ## Key invariant (answers the obvious confusion)
11
+
12
+ `@latest` is **what casual `npm install` gets**. It only moves when prod.cmd promotes. So between promotions:
13
+
14
+ ```
15
+ @latest = @prod = 1.0.609 ← last promoted, what users get
16
+ @dev = 1.0.620 ← every rebuild bumps this
17
+ ```
18
+
19
+ Casual users keep installing 1.0.609 until you say so. Dev tip never accidentally becomes the casual install.
20
+
21
+ ---
22
+
23
+ ## Approach A — npm dist-tags (one package)
24
+
25
+ ### Mechanics
26
+
27
+ npm's built-in [dist-tag](https://docs.npmjs.com/cli/v10/commands/npm-dist-tag) feature: one package, multiple movable pointers.
28
+
29
+ - `npm publish --tag dev` sets `@dev` to the new version. `@latest` untouched.
30
+ - `npm dist-tag add @bobfrankston/rmfmail@<v> prod` moves the `prod` pointer; `npm dist-tag add ... latest` moves `latest` too. Two HTTP calls, no republish.
31
+
32
+ ### Required changes
33
+
34
+ **1. npmglobalize** — accepts a `--dist-tag` flag and forwards to `npm publish`. ~5 lines. See `docs/npmglobalize-disttag.md` for the patch.
35
+
36
+ **2. rebuild.cmd** — invoke `npmglobalize --dist-tag dev`. One-line edit.
37
+
38
+ **3. prod.cmd** — new file. Explicit arg wins; otherwise query npm for the
39
+ current `@dev` and promote that. (`@dev` is the right default because you've
40
+ just published it via `rebuild.cmd` and want it to become user-facing.)
41
+
42
+ ```cmd
43
+ @echo off
44
+ setlocal
45
+ set V=%1
46
+ if "%V%"=="" (
47
+ for /f "delims=" %%v in ('npm view @bobfrankston/rmfmail dist-tags.dev') do set V=%%v
48
+ )
49
+ if "%V%"=="" (
50
+ echo No version supplied and 'npm view ... dist-tags.dev' returned empty.
51
+ echo Usage: prod.cmd [version] e.g. prod.cmd 1.0.611
52
+ exit /b 1
53
+ )
54
+ echo Promoting @bobfrankston/rmfmail@%V% to prod and latest...
55
+ call npm dist-tag add @bobfrankston/rmfmail@%V% prod
56
+ call npm dist-tag add @bobfrankston/rmfmail@%V% latest
57
+ echo Done.
58
+ endlocal
59
+ ```
60
+
61
+ Roll back to an older known-good: `prod.cmd 1.0.598` — explicit arg path.
62
+
63
+ The "default = local package.json" alternative (if you never want to query
64
+ the registry):
65
+
66
+ ```cmd
67
+ for /f "tokens=2 delims=:," %%v in ('findstr /b " \"version\":" "%~dp0app\package.json"') do set V=%%~v
68
+ set V=%V: =%
69
+ set V=%V:"=%
70
+ ```
71
+
72
+ Use whichever default matches your workflow — the explicit-arg path works
73
+ either way.
74
+
75
+ ### Workflow
76
+
77
+ 1. Edit code, run `rebuild.cmd` — version bumps, publishes as `@dev`. `@latest`/`@prod` unaffected.
78
+ 2. On the dev machine: `npm install -g @bobfrankston/rmfmail@dev` to grab the freshest build.
79
+ 3. When something feels stable: run `prod.cmd`. Promotes the current `package.json` version to `@prod` and `@latest`.
80
+ 4. On the prod machine (and any default install): `npm install -g @bobfrankston/rmfmail` (no tag) gets the newly-promoted version.
81
+
82
+ Roll back: `npm dist-tag add @bobfrankston/rmfmail@<earlier> prod` + `... latest`. No republish, no version bump.
83
+
84
+ ### Effort
85
+
86
+ - npmglobalize change: ~5 lines, ~5 minutes.
87
+ - `rebuild.cmd`: one-line edit.
88
+ - `prod.cmd`: 15 lines, fresh file.
89
+ - Test cycle: ~10 minutes.
90
+
91
+ ---
92
+
93
+ ## Approach B — wrapper packages
94
+
95
+ ### Mechanics
96
+
97
+ Split the current single package into three:
98
+
99
+ | Name | Role | Versioning |
100
+ |---|---|---|
101
+ | `@bobfrankston/rmfmailapp` | Today's actual code. | Bumps freely on every build. |
102
+ | `@bobfrankston/rmfmail` | Wrapper. `dependencies: { rmfmailapp: "1.0.609" }` pinned. | Bumps only on promotion. |
103
+ | `@bobfrankston/rmfmaildev` | Wrapper. `dependencies: { rmfmailapp: "*" }`. | Tracks rmfmailapp tip. |
104
+
105
+ Casual user: `npm install -g @bobfrankston/rmfmail` — installs the wrapper, which pulls in the pinned rmfmailapp via npm's normal dependency resolution. Wrapper's `bin` shim invokes rmfmailapp's actual entry point, so the CLI is identical.
106
+
107
+ Promotion = git commit on the wrapper bumping `dependencies.rmfmailapp` from one frozen version to another, plus `npm publish` of the wrapper.
108
+
109
+ ### Layout
110
+
111
+ ```
112
+ mailx/
113
+ rmfmailapp/ # was app/
114
+ package.json "name": "@bobfrankston/rmfmailapp"
115
+ bin/, packages/, client/ unchanged
116
+ rmfmail/ # NEW
117
+ package.json pins rmfmailapp version
118
+ bin/rmfmail.js shim that imports rmfmailapp's entry
119
+ rmfmaildev/ # NEW
120
+ package.json "*" range or always-repinned
121
+ bin/rmfmaildev.js shim
122
+ ```
123
+
124
+ ### `rmfmail/package.json`
125
+
126
+ ```json
127
+ {
128
+ "name": "@bobfrankston/rmfmail",
129
+ "version": "1.0.0",
130
+ "type": "module",
131
+ "bin": { "rmfmail": "bin/rmfmail.js" },
132
+ "dependencies": { "@bobfrankston/rmfmailapp": "1.0.609" }
133
+ }
134
+ ```
135
+
136
+ ### `rmfmail/bin/rmfmail.js`
137
+
138
+ ```js
139
+ #!/usr/bin/env node
140
+ import { createRequire } from "node:module";
141
+ const require = createRequire(import.meta.url);
142
+ await import(`file://${require.resolve("@bobfrankston/rmfmailapp/bin/mailx.js")}`);
143
+ ```
144
+
145
+ ### `rmfmaildev/package.json`
146
+
147
+ Same shape but `"@bobfrankston/rmfmailapp": "*"` so npm always resolves to the freshest published rmfmailapp.
148
+
149
+ ### Required changes
150
+
151
+ **1. Rename** `@bobfrankston/rmfmail` → `@bobfrankston/rmfmailapp` in `app/package.json` and any workspace deps that reference it. One-time.
152
+
153
+ **2. Create** `rmfmail/` and `rmfmaildev/` directories with the package.json + shim listed above. ~30 lines total.
154
+
155
+ **3. prod.cmd** — bumps the pin and publishes the wrapper:
156
+
157
+ ```cmd
158
+ @echo off
159
+ setlocal
160
+ set TARGET=%1
161
+ if "%TARGET%"=="" (
162
+ for /f "tokens=2 delims=:," %%v in ('findstr /b " \"version\":" "%~dp0rmfmailapp\package.json"') do set TARGET=%%~v
163
+ set TARGET=%TARGET: =%
164
+ set TARGET=%TARGET:"=%
165
+ )
166
+ node -e "const p=require('./rmfmail/package.json'); p.dependencies['@bobfrankston/rmfmailapp']='%TARGET%'; require('fs').writeFileSync('./rmfmail/package.json', JSON.stringify(p,null,2)+'\n');"
167
+ cd rmfmail
168
+ call npm version patch
169
+ call npm publish --access public
170
+ cd ..
171
+ git add rmfmail/package.json
172
+ git commit -m "rmfmail: pin rmfmailapp@%TARGET%"
173
+ endlocal
174
+ ```
175
+
176
+ **4. npmglobalize**: no changes required.
177
+
178
+ ### Workflow
179
+
180
+ 1. Build, run `rebuild.cmd` — publishes rmfmailapp.
181
+ 2. Dev install: `npm install -g @bobfrankston/rmfmaildev`. Pulls latest rmfmailapp through the `*` range.
182
+ 3. Promotion: `prod.cmd 1.0.611`. Edits rmfmail's pin, bumps wrapper patch version, publishes wrapper, commits.
183
+ 4. Casual install: `npm install -g @bobfrankston/rmfmail` resolves the new wrapper, npm fetches the pinned rmfmailapp, both land under the wrapper.
184
+
185
+ Roll back: edit rmfmail/package.json to a prior pin, `git revert`, republish wrapper.
186
+
187
+ ### Effort
188
+
189
+ - One-time rename: ~30 minutes.
190
+ - Wrapper skeletons: ~15 minutes.
191
+ - prod.cmd: ~15 minutes.
192
+ - Test: ~30 minutes.
193
+ - Total: ~1.5 hours up-front, then near-zero per promotion.
194
+
195
+ ---
196
+
197
+ ## Comparison
198
+
199
+ | | A: dist-tags | B: wrappers |
200
+ |--|--|--|
201
+ | Number of npm packages | 1 | 3 (app + 2 wrappers) |
202
+ | What "promotion" is | `npm dist-tag add` (HTTP call) | git commit + `npm publish` of wrapper |
203
+ | Promotion in git history | no (registry-only) | yes |
204
+ | Roll-back | retag prior version | `git revert` then republish wrapper |
205
+ | npmglobalize change needed | yes (`--tag` flag, ~5 lines) | no |
206
+ | Casual install command | `npm install -g @bobfrankston/rmfmail` | (same) |
207
+ | Dev install command | `... rmfmail@dev` | `... rmfmaildev` |
208
+ | One-time setup work | ~15 minutes | ~1.5 hours |
209
+ | Per-promotion work | seconds (registry call) | minutes (commit + publish) |
210
+ | Multi-channel future (beta, rc) | more dist-tags | more wrappers |
211
+
212
+ ## Recommendation
213
+
214
+ If you want it working today with minimum disturbance: **A (dist-tags)**.
215
+
216
+ If auditability of "what version was prod when" matters and you're willing to spend the one-time rename: **B (wrappers)**.
217
+
218
+ A → B migration later is straightforward (the rename and wrapper creation is the same work whenever you do it; dist-tags become a non-issue once wrappers exist).
219
+
220
+ ## What neither approach does
221
+
222
+ - Does not enable side-by-side coexistence on a single machine — that's the orthogonal `RMFMAIL_PROFILE` env-var design (separate config dir, AUMID, mailto handler, etc.).
223
+ - Does not change the in-app updater. mailx's existing version-check flow keeps working with either.
224
+ - Does not affect Android. APKs aren't on npm — `prod-android.md` is the parallel document with the equivalent channel-manifest design.
@@ -0,0 +1,141 @@
1
+ # Push Relay (C127, optional)
2
+
3
+ Status: design only — not yet built. Relevant when polling latency feels too slow OR when battery / quota matters more than now.
4
+
5
+ ## Problem
6
+
7
+ mailx polls Google Calendar / Gmail / Outlook for changes. Polling is cheap, simple, and works on every network — the right default. But it has two costs:
8
+
9
+ 1. **Latency floor.** Even at a 5 s syncToken cadence, "the user just rescheduled this on their phone" takes ~5 s to surface in mailx. For most workflows that's invisible; for "alarm fires for an event that was moved an hour ago" it isn't.
10
+ 2. **Battery / quota on mobile.** The Android shell polls the same way the desktop does; on a phone that's measurable wakeups.
11
+
12
+ Push notifications (Google's `events.watch`, Microsoft Graph webhook subscriptions, Gmail's `users.watch`) fix both, but only deliver to a public HTTPS URL. The desktop client doesn't have one. So we need a relay.
13
+
14
+ ## Shape
15
+
16
+ A standalone Node service in `MailApps/relayer/`, deployed wherever the operator runs other small services (alerter-style: tiny Express app, one config file, one long-running process). One relay process serves multiple users; per-user state is keyed by user-issued tokens.
17
+
18
+ ```
19
+ ┌──────────────┐ watch ┌─────────┐ webhook ┌───────────┐ WS/SSE ┌────────┐
20
+ │ Google / MS │ ──register── │ relayer │ ◀──────── │ relayer │ ◀────────│ rmfmail│
21
+ │ Calendar API │ │ /sub │ │ /hook/:id │ │ client │
22
+ └──────────────┘ └─────────┘ └───────────┘ └────────┘
23
+ ▲ │ ▲
24
+ │ events.watch( │ per-subscription state: │
25
+ │ address: relayer/hook │ upstream channel id, expiry, owner-token, │
26
+ │ token: per-sub secret │ fan-out endpoints │
27
+ │ ) │ │
28
+ └──── done by relayer on │ │
29
+ client request, with │ │
30
+ client-supplied OAuth │ │
31
+ access token (kept │ │
32
+ short-lived; client │ │
33
+ re-presents on │ │
34
+ renewal) │ │
35
+
36
+ ```
37
+
38
+ Three roles, separable:
39
+ - **Subscriber API** — clients POST to register what they want watched.
40
+ - **Hook receiver** — public endpoint(s) the upstream services post to.
41
+ - **Fan-out** — per-subscription WS or SSE that holds open client connections and pushes payloads.
42
+
43
+ ## API (relayer side)
44
+
45
+ Generic relay shape — caller specifies upstream service + the params that service needs to register a watch. The relay knows enough about each supported upstream to issue the watch-registration call and decode the inbound webhook envelope; it doesn't introspect payload content.
46
+
47
+ ```
48
+ POST /subscribe
49
+ body: {
50
+ upstream: "google-calendar" | "google-gmail" | "ms-graph-events" | …
51
+ params: { …upstream-specific watch args, e.g. calendarId, accessToken, expiry preference }
52
+ deliver: "sse" | "ws"
53
+ }
54
+ → 200 {
55
+ subscriptionId: "<uuid>",
56
+ deliverUrl: "/sub/<uuid>", // open SSE / WS here
57
+ authToken: "<random-secret>", // present on connect
58
+ expiresAt: <ms> // when relay's upstream channel needs renewal
59
+ }
60
+
61
+ POST /subscribe/<id>/renew
62
+ body: { params: { accessToken: "<fresh OAuth>" } }
63
+ → 200 { expiresAt: <ms> }
64
+
65
+ DELETE /subscribe/<id>
66
+ → 204 (also calls upstream stop-watch)
67
+
68
+ GET /subscribe/<id>/status
69
+ → 200 { upstream, expiresAt, lastEventAt, queueDepth }
70
+
71
+ GET /sub/<id> (SSE) — emits `data: {payload}\n\n` per upstream event
72
+ (or WS upgrade) — same payload shape as text frames
73
+ ```
74
+
75
+ Auth: `Authorization: Bearer <authToken>` on every call after `/subscribe`. The token issued at subscribe time scopes the subscription; one client manages one or more subscriptions, each with its own token.
76
+
77
+ ## API (client side)
78
+
79
+ Two settings the user fills in:
80
+ - `pushRelayUrl` — e.g. `https://mail1.example.com:NNNN`
81
+ - `pushRelayCredentials` — issued by the operator at first contact (one-time, opaque blob; relay verifies)
82
+
83
+ When `pushRelayUrl` is non-empty, the client at startup:
84
+ 1. Reads its previous subscription IDs from local config.
85
+ 2. For each, opens the SSE / WS connection.
86
+ 3. If the connection refuses with "unknown subscription" (relay restart, expiry), re-subscribes from scratch with the current OAuth token and stores the new id.
87
+ 4. Hooks the inbound event stream into the same `mailxapi.onEvent` channel the daemon already uses, so the alarm subsystem / message-list / folder-tree all consume push events the same way they consume the daemon's IDLE / sync events.
88
+
89
+ The client owns OAuth tokens; the relay receives short-lived access tokens at register / renew time, doesn't store refresh tokens.
90
+
91
+ ## Reliability
92
+
93
+ - **Replay queue per subscription** — last N events (in-memory ring, e.g. N=100) so a brief disconnect doesn't lose events. Client passes `Last-Event-ID` (SSE) or a sequence cursor (WS) on reconnect.
94
+ - **Channel renewal** — relay tracks each upstream channel's expiry, schedules renewal at 80% of TTL, calls back to the client (via the WS/SSE itself) for a fresh OAuth token if needed.
95
+ - **Fail-open** — if the relay is unreachable, the client's existing syncToken polling continues. Push is "freshness boost," never load-bearing for correctness.
96
+
97
+ ## Generic relay vs per-upstream code
98
+
99
+ The relay needs per-upstream code for:
100
+ - How to register a watch (the API call shape varies).
101
+ - How to decode the inbound webhook envelope (Google sends headers in `X-Goog-*`, Graph sends a JSON body, etc.).
102
+ - How to renew or stop a watch.
103
+
104
+ Per-upstream is a small adapter (~50–100 lines each). Adding "Outlook Tasks" or "iCloud Calendar" later is a new file in `relayer/upstreams/`. The fan-out, subscription store, WS/SSE plumbing, replay buffer — all generic, written once.
105
+
106
+ ## Operator footprint
107
+
108
+ - One Node process, one port, one HTTPS cert (or behind a reverse proxy that handles TLS).
109
+ - Storage: SQLite or JSON for the subscription map (small — one row per active subscription).
110
+ - Logs to a file the operator already tails.
111
+ - No queue infrastructure, no message broker, no DB cluster.
112
+
113
+ This is the same operational shape as alerter — drop into `MailApps/`, deploy alongside other small services, treat as one more daemon.
114
+
115
+ ## When to build this
116
+
117
+ Only when polling demonstrably isn't fast enough, OR when mobile battery becomes a real concern (push lets the Android shell wake on event instead of polling on a timer). Polling at 5–10 s syncToken cadence is functionally instant for the "user rescheduled an event" case and quota-cheap. Push is a freshness optimization, not a correctness fix.
118
+
119
+ ## Alternatives considered
120
+
121
+ - **Cloudflare Workers + Durable Objects** — cleaner per-user-isolation story but requires each user to deploy their own Worker (or one shared operator-run Worker, with a different trust shape). Higher onboarding friction; users without a CF account can't use it. Pro: free tier, edge latency. Con: vendor lock-in, two-click onboarding minimum (account + token).
122
+ - **Cloudflare Tunnel / ngrok to the local daemon** — no relay needed, the daemon exposes its own webhook receiver. Catch: only works while the daemon is running; events while offline are lost (recovered by the next syncToken poll, which already exists). Per-instance tunnel.
123
+ - **Status quo (poll only)** — what mailx does today. Right default; this doc is for when "default isn't enough."
124
+
125
+ ## Files (when built)
126
+
127
+ ```
128
+ MailApps/relayer/
129
+ package.json
130
+ tsconfig.json
131
+ index.ts — Express server + WS upgrade + SSE
132
+ upstreams/
133
+ google-calendar.ts
134
+ google-gmail.ts
135
+ ms-graph-events.ts
136
+ subs.ts — subscription store (SQLite)
137
+ fanout.ts — per-sub connection pool + replay buffer
138
+ readme.md — operator doc (deploy + config)
139
+ ```
140
+
141
+ Client side: extend `mailx-service` with a `pushRelayClient.ts` that opens / re-opens / re-subscribes; surface its status in the existing sync-status banner.
@@ -0,0 +1,156 @@
1
+ # `rmf-tiny` — optional TinyMCE editor adapter for rmfmail
2
+
3
+ Status: design only. Not implemented.
4
+
5
+ ## Goal
6
+
7
+ Let users who want Thunderbird-class paste fidelity opt into TinyMCE without baking it into rmfmail itself. Bob 2026-05-09 verified: pasted a Word document into Thunderbird, tiptap, Quill, and TinyMCE — only TinyMCE preserved the formatting (boxed monospace block with borders, paragraph spacing, font preservation). Nothing else came close.
8
+
9
+ The license catch — TinyMCE is GPLv2 — means rmfmail (MIT) can't bundle it. The arms-length pattern is the resolution: rmfmail ships **no** TinyMCE bytes; a separate `rmf-tiny` package the user installs themselves provides the adapter + pulls in TinyMCE via npm.
10
+
11
+ ## Shape
12
+
13
+ ```
14
+ @bobfrankston/rmfmail ← MIT, ships no TinyMCE
15
+ client/compose/editor.ts ← MailxEditor interface + factory
16
+ gains a "tinymce" branch that
17
+ dynamically imports rmf-tiny
18
+
19
+ @bobfrankston/rmf-tiny ← MIT (the adapter glue is original code)
20
+ package.json ← peerDependency: tinymce
21
+ src/adapter.ts ← implements MailxEditor against TinyMCE
22
+ README.md ← install instructions
23
+ ```
24
+
25
+ User opts in:
26
+
27
+ ```
28
+ npm install -g @bobfrankston/rmf-tiny tinymce
29
+ ```
30
+
31
+ (Two packages explicit — keeps the licensing posture clean. rmf-tiny is just the adapter; tinymce comes from Tiny's own npm publication, not bundled with anything rmfmail-related.)
32
+
33
+ In rmfmail Settings → Compose → Editor: the dropdown grows a "TinyMCE (requires rmf-tiny)" option. If selected and the import fails, surface a status-bar error pointing at the install command.
34
+
35
+ ## Why a separate package, not a flag inside rmfmail
36
+
37
+ - rmfmail's git history and npm tarball never contain TinyMCE → distribution layer is clean.
38
+ - rmf-tiny is small (~200 lines of adapter glue + a thin install README) and can be MIT — no GPL infection because it imports TinyMCE at runtime via the same kind of dynamic boundary npm packages always use. Combination at user's machine is fully GPL-permitted.
39
+ - Other editors can follow the same pattern (`rmf-ckeditor`, `rmf-prosemirror-pro`) without polluting rmfmail.
40
+
41
+ ## Why call it `rmf-tiny`
42
+
43
+ Short, distinguishable from the upstream package, namespaces under your scope (`@bobfrankston/rmf-tiny`), and "tiny" is the upstream's own brand so the connection is obvious. Other naming options:
44
+ - `@bobfrankston/mailx-tinymce-adapter` (verbose, descriptive)
45
+ - `@bobfrankston/rmftiny` (no hyphen, matches rmfmail style)
46
+
47
+ `rmf-tiny` is fine; pick the form that scans cleanest in the install line.
48
+
49
+ ## Files
50
+
51
+ ### `rmf-tiny/package.json`
52
+
53
+ ```json
54
+ {
55
+ "name": "@bobfrankston/rmf-tiny",
56
+ "version": "0.1.0",
57
+ "description": "TinyMCE editor adapter for rmfmail. Bring-your-own TinyMCE.",
58
+ "type": "module",
59
+ "main": "src/adapter.js",
60
+ "license": "MIT",
61
+ "peerDependencies": {
62
+ "tinymce": ">=6"
63
+ },
64
+ "peerDependenciesMeta": {
65
+ "tinymce": { "optional": false }
66
+ }
67
+ }
68
+ ```
69
+
70
+ `peerDependencies` puts the install responsibility on the user — `npm install @bobfrankston/rmf-tiny` warns that `tinymce` must also be installed; rmf-tiny does not vendor or bundle TinyMCE.
71
+
72
+ ### `rmf-tiny/src/adapter.ts`
73
+
74
+ ```ts
75
+ // MIT-licensed adapter glue. Imports the user-installed TinyMCE at
76
+ // runtime; doesn't redistribute any TinyMCE bytes. Implements the same
77
+ // `MailxEditor` interface rmfmail's createEditor factory expects, so
78
+ // the host code path is identical.
79
+ import type { Editor as TinyEditor } from "tinymce";
80
+
81
+ export interface MailxEditor {
82
+ setHtml(html: string): void;
83
+ getHtml(): string;
84
+ getText(): string;
85
+ focus(): void;
86
+ setCursor(pos: number): void;
87
+ root: HTMLElement;
88
+ // …other methods rmfmail's interface requires
89
+ }
90
+
91
+ export async function createTinyMceEditor(container: HTMLElement): Promise<MailxEditor> {
92
+ // Dynamic import — fails clearly if the user hasn't installed tinymce.
93
+ const tinymce = (await import("tinymce")).default;
94
+ // Plus the plugins TinyMCE's paste-from-Word handler needs:
95
+ await Promise.all([
96
+ import("tinymce/themes/silver"),
97
+ import("tinymce/icons/default"),
98
+ import("tinymce/plugins/paste"),
99
+ import("tinymce/plugins/lists"),
100
+ import("tinymce/plugins/link"),
101
+ import("tinymce/plugins/table"),
102
+ import("tinymce/plugins/code"),
103
+ ]);
104
+ // ... initialize tinymce against `container`, return MailxEditor shim.
105
+ }
106
+ ```
107
+
108
+ The actual init/wire-up is the work — wrapping TinyMCE's API in our `MailxEditor` shape, hooking paste events, setHtml/getHtml round-tripping, etc. Maybe ~200 lines.
109
+
110
+ ### rmfmail-side change to `editor.ts`
111
+
112
+ ```ts
113
+ export async function createEditor(
114
+ container: HTMLElement,
115
+ type: "quill" | "tiptap" | "tinymce"
116
+ ): Promise<MailxEditor> {
117
+ if (type === "tinymce") {
118
+ try {
119
+ const m = await import("@bobfrankston/rmf-tiny");
120
+ return m.createTinyMceEditor(container);
121
+ } catch (e: any) {
122
+ const status = document.getElementById("status-sync");
123
+ if (status) status.textContent = `TinyMCE editor not available — install: npm install -g @bobfrankston/rmf-tiny tinymce`;
124
+ return createQuillEditor(container); // fall back so compose still works
125
+ }
126
+ }
127
+ if (type === "tiptap") return createTiptapEditor(container);
128
+ return createQuillEditor(container);
129
+ }
130
+ ```
131
+
132
+ Settings panel adds the "TinyMCE" option in the editor dropdown.
133
+
134
+ ## Effort
135
+
136
+ - `rmf-tiny` package skeleton (package.json, README, basic adapter shell): ~30 minutes.
137
+ - Adapter wiring (init, setHtml/getHtml, paste handling, toolbar config): ~3 hours including testing the Word-paste case.
138
+ - rmfmail-side factory branch + Settings dropdown option: ~30 minutes.
139
+ - Documentation for users (README in rmf-tiny + Settings tooltip): ~30 minutes.
140
+
141
+ About half a day total.
142
+
143
+ ## Order
144
+
145
+ 1. Build `rmf-tiny` against your local TinyMCE install. Verify the Word-paste case actually works through your adapter.
146
+ 2. Add the factory branch and Settings option to rmfmail.
147
+ 3. Publish `rmf-tiny` to npm.
148
+ 4. Document the opt-in install command in rmfmail's Settings tooltip and `README.md`.
149
+
150
+ Don't publish the adapter until the Word-paste case actually works end-to-end through the adapter — TinyMCE direct is one thing, our adapter possibly mangles it differently.
151
+
152
+ ## Caveats
153
+
154
+ - The adapter must not re-implement parts of TinyMCE — it's a thin wrapper. Re-implementing parts of GPL code in MIT is the legal trap; staying purely in adapter / interface territory is safe.
155
+ - TinyMCE's bundle is large (~1MB minified). Users opting in pay that cost on the first compose-window load. Fine for opt-in; would be unacceptable as a default.
156
+ - `rmf-tiny` major version should track TinyMCE's major version (rmf-tiny@7.x for tinymce@7.x) so peerDependency mismatches are obvious.
@@ -46,7 +46,7 @@ export declare function getStorageInfo(): {
46
46
  /** Fill in provider defaults for an account based on email domain.
47
47
  * Exported so mailx-service's leanAccountsJsonc helper can reuse the same
48
48
  * canonicalization rules that loadAccounts uses. */
49
- export declare function normalizeAccount(acct: any, globalName?: string): AccountConfig;
49
+ export declare function normalizeAccount(acct: any, globalName?: string, globalSig?: any, globalSignature?: string): AccountConfig;
50
50
  declare const DEFAULT_PREFERENCES: {
51
51
  ui: {
52
52
  theme: "system" | "dark" | "light";
@@ -101,7 +101,7 @@ export declare function loadAccountsAsync(): Promise<AccountConfig[]>;
101
101
  * - Drop `imap.user` / `smtp.user` if they equal the email.
102
102
  * - Drop `sig.html: false` (default; only keep when user enabled HTML sigs).
103
103
  * - Keep field order: id → label → email → primary* → imap → smtp → defaultSend
104
- * → relayDomains → deliveredToPrefix → identityDomains → syncContacts → sig.
104
+ * → relayDomains → identityDomains → syncContacts → sig.
105
105
  * Curated for readability, not alphabetic. */
106
106
  export declare function denormalizeAccount(acct: AccountConfig, globalName?: string): any;
107
107
  /** Save account configs */
@@ -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":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAKH,OAAO,KAAK,EAAE,aAAa,EAAE,aAAa,EAAE,oBAAoB,EAAE,MAAM,EAAE,MAAM,2BAA2B,CAAC;AAyG5G,QAAA,MAAM,SAAS,QAA4E,CAAC;AAiE5F,qFAAqF;AACrF,KAAK,kBAAkB,GAAG,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,EAAE,OAAO,CAAC,EAAE;IAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,KAAK,IAAI,CAAC;AAE/G,wBAAgB,YAAY,CAAC,EAAE,EAAE,kBAAkB,GAAG,MAAM,IAAI,CAM/D;AAOD,wBAAgB,iBAAiB,IAAI,MAAM,GAAG,IAAI,CAA2B;AAU7E,iBAAS,YAAY,IAAI,MAAM,CAgB9B;AAOD,sEAAsE;AACtE,wBAAsB,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAoCxE;AAED;;qCAEqC;AACrC,wBAAsB,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA8BjF;AAyBD,2CAA2C;AAC3C,wBAAgB,WAAW,IAAI,OAAO,CAErC;AAED,4CAA4C;AAC5C,wBAAgB,cAAc,IAAI;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,OAAO,GAAG,KAAK,GAAG,OAAO,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,CA+B3L;AAuID;;qDAEqD;AACrD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,GAAG,EAAE,UAAU,CAAC,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,GAAG,EAAE,eAAe,CAAC,EAAE,MAAM,GAAG,aAAa,CA6DzH;AAMD,QAAA,MAAM,mBAAmB;;eAEE,QAAQ,GAAG,MAAM,GAAG,OAAO;gBAC3B,OAAO,GAAG,QAAQ;;;;;;;;;;;;;;;;;;;;CAoB5C,CAAC;AAEF,QAAA,MAAM,oBAAoB,EAAE,oBAS3B,CAAC;AAEF,QAAA,MAAM,iBAAiB;aACJ,MAAM,EAAE;aACR,MAAM,EAAE;gBACL,MAAM,EAAE;oBAOJ,MAAM,EAAE;oBACR,MAAM,EAAE;CACjC,CAAC;AAIF,2BAA2B;AAC3B,wBAAgB,YAAY,IAAI,aAAa,EAAE,CA4C9C;AAoCD;;;;0CAI0C;AAC1C,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,aAAa,EAAE,CAAC,CAuBlE;AAED;;;;;;;;;;;;;;;iDAeiD;AACjD,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,aAAa,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,GAAG,CA8ChF;AAED,2BAA2B;AAC3B;;;oEAGoE;AACpE,wBAAsB,YAAY,CAAC,QAAQ,EAAE,aAAa,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAyC3E;AAED;;;wEAGwE;AACxE,wBAAgB,QAAQ,IAAI,MAAM,CAWjC;AAED;;4DAE4D;AAC5D,wBAAsB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAoB1D;AAED;;;;;uEAKuE;AACvE,wBAAsB,uBAAuB,IAAI,OAAO,CAAC,IAAI,CAAC,CAmB7D;AAED;;;;;;kCAMkC;AAClC,wBAAsB,wBAAwB,IAAI,OAAO,CAAC,IAAI,CAAC,CAa9D;AAED;;;;;0CAK0C;AAC1C,wBAAsB,4BAA4B,IAAI,OAAO,CAAC,IAAI,CAAC,CAYlE;AAED,wEAAwE;AACxE,wBAAgB,eAAe,IAAI,OAAO,mBAAmB,CAkC5D;AAED,uBAAuB;AACvB,wBAAgB,eAAe,CAAC,KAAK,EAAE,GAAG,GAAG,IAAI,CAEhD;AAED,iCAAiC;AACjC,wBAAgB,gBAAgB,IAAI,oBAAoB,CAGvD;AAED,iCAAiC;AACjC,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,oBAAoB,GAAG,IAAI,CAIrE;AAED,qCAAqC;AACrC,wBAAgB,aAAa,IAAI,OAAO,iBAAiB,CAExD;AAED,4EAA4E;AAC5E,wBAAsB,aAAa,CAAC,IAAI,EAAE,OAAO,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAqBjF;AAcD,6EAA6E;AAC7E,wBAAgB,YAAY,IAAI,aAAa,CA0B5C;AAyBD,wBAAsB,YAAY,CAAC,QAAQ,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAkBzE;AAED,oCAAoC;AACpC,wBAAgB,YAAY,IAAI,MAAM,CAGrC;AAED,qDAAqD;AACrD,wBAAgB,YAAY,IAAI,MAAM,CAErC;AAED,wCAAwC;AACxC,OAAO,EAAE,YAAY,EAAE,CAAC;AAKxB,kDAAkD;AAClD,wBAAgB,eAAe,CAAC,SAAS,CAAC,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAkB5E;AAED;;;mFAGmF;AACnF,wBAAsB,eAAe,CAAC,QAAQ,GAAE,QAAmB,GAAG,OAAO,CAAC,IAAI,CAAC,CAclF;AAED,QAAA,MAAM,gBAAgB,EAAE,aAMvB,CAAC;AAEF,8FAA8F;AAC9F,wBAAgB,cAAc,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,CAQzD;AAED,uEAAuE;AACvE,wBAAgB,WAAW,IAAI,OAAO,CAGrC;AAED,OAAO,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,SAAS,EAAE,CAAC;AAErG;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,UAAU,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA0ClE"}
@@ -492,7 +492,7 @@ const PROVIDERS = {
492
492
  /** Fill in provider defaults for an account based on email domain.
493
493
  * Exported so mailx-service's leanAccountsJsonc helper can reuse the same
494
494
  * canonicalization rules that loadAccounts uses. */
495
- export function normalizeAccount(acct, globalName) {
495
+ export function normalizeAccount(acct, globalName, globalSig, globalSignature) {
496
496
  const email = acct.email || "";
497
497
  const localPart = email.split("@")[0]?.toLowerCase() || "";
498
498
  const domain = email.split("@")[1]?.toLowerCase() || "";
@@ -535,7 +535,6 @@ export function normalizeAccount(acct, globalName) {
535
535
  defaultSend: acct.defaultSend,
536
536
  syncContacts: acct.syncContacts ?? (provider?.imap.auth === "oauth2"),
537
537
  relayDomains: acct.relayDomains,
538
- deliveredToPrefix: acct.deliveredToPrefix,
539
538
  identityDomains: acct.identityDomains,
540
539
  // `spam` passthrough retired 2026-04-22 — markAsSpamMessages now finds
541
540
  // the junk folder via `specialUse === "junk"` on the DB folder record
@@ -546,9 +545,11 @@ export function normalizeAccount(acct, globalName) {
546
545
  // `as any` is the minimum-blast-radius way to add the field without
547
546
  // blocking the build. Once mailx-types has rebuilt once (post-field)
548
547
  // this cast can be removed.
549
- ...(acct.signature ? { signature: acct.signature } : {}),
548
+ ...(acct.signature ? { signature: acct.signature } : (globalSignature ? { signature: globalSignature } : {})),
550
549
  ...(acct.sig && typeof acct.sig === "object" && typeof acct.sig.text === "string"
551
- ? { sig: { text: acct.sig.text, html: !!acct.sig.html } } : {}),
550
+ ? { sig: { text: acct.sig.text, html: !!acct.sig.html } }
551
+ : (globalSig && typeof globalSig === "object" && typeof globalSig.text === "string"
552
+ ? { sig: { text: globalSig.text, html: !!globalSig.html } } : {})),
552
553
  };
553
554
  }
554
555
  // ── Defaults ──
@@ -637,13 +638,15 @@ export function loadAccounts() {
637
638
  }
638
639
  const raw = accounts.accounts || accounts;
639
640
  const globalName = accounts.name || "";
640
- const result = deduplicateAccounts(raw.map((a) => normalizeAccount(a, globalName)));
641
+ const globalSig = accounts.sig;
642
+ const globalSignature = accounts.signature;
643
+ const result = deduplicateAccounts(raw.map((a) => normalizeAccount(a, globalName, globalSig, globalSignature)));
641
644
  return applyAccountOverrides(result);
642
645
  }
643
646
  // Legacy: read from settings.jsonc
644
647
  const legacy = loadLegacySettings();
645
648
  if (legacy?.accounts)
646
- return applyAccountOverrides(legacy.accounts.map((a) => normalizeAccount(a, legacy.name)));
649
+ return applyAccountOverrides(legacy.accounts.map((a) => normalizeAccount(a, legacy.name, legacy.sig, legacy.signature)));
647
650
  return DEFAULT_ACCOUNTS;
648
651
  }
649
652
  /** Normalize email for dedup — Gmail ignores dots before @ */
@@ -699,8 +702,10 @@ export async function loadAccountsAsync() {
699
702
  if (data?.accounts || Array.isArray(data)) {
700
703
  const raw = data.accounts || data;
701
704
  const globalName = data.name || "";
705
+ const globalSig = data.sig;
706
+ const globalSignature = data.signature;
702
707
  // cloudRead has already cached content to LOCAL_DIR.
703
- return applyAccountOverrides(raw.map((a) => normalizeAccount(a, globalName)));
708
+ return applyAccountOverrides(raw.map((a) => normalizeAccount(a, globalName, globalSig, globalSignature)));
704
709
  }
705
710
  }
706
711
  // Cloud unreachable / unparseable — fall through to local cache.
@@ -721,7 +726,7 @@ export async function loadAccountsAsync() {
721
726
  * - Drop `imap.user` / `smtp.user` if they equal the email.
722
727
  * - Drop `sig.html: false` (default; only keep when user enabled HTML sigs).
723
728
  * - Keep field order: id → label → email → primary* → imap → smtp → defaultSend
724
- * → relayDomains → deliveredToPrefix → identityDomains → syncContacts → sig.
729
+ * → relayDomains → identityDomains → syncContacts → sig.
725
730
  * Curated for readability, not alphabetic. */
726
731
  export function denormalizeAccount(acct, globalName) {
727
732
  const domain = (acct.email || "").split("@")[1]?.toLowerCase() || "";
@@ -779,8 +784,6 @@ export function denormalizeAccount(acct, globalName) {
779
784
  out.enabled = false; // default true → omit
780
785
  if (acct.relayDomains && acct.relayDomains.length > 0)
781
786
  out.relayDomains = acct.relayDomains;
782
- if (acct.deliveredToPrefix && acct.deliveredToPrefix.length > 0)
783
- out.deliveredToPrefix = acct.deliveredToPrefix;
784
787
  if (acct.identityDomains && acct.identityDomains.length > 0)
785
788
  out.identityDomains = acct.identityDomains;
786
789
  // syncContacts default: true for OAuth, false otherwise. Only emit when