@bobfrankston/rmfmail 1.0.681 → 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.
- package/bin/lean-accounts.js +0 -1
- package/client/app.bundle.js +138 -38
- package/client/app.bundle.js.map +2 -2
- package/client/app.js +12 -1
- package/client/app.js.map +1 -1
- package/client/app.ts +12 -1
- package/client/components/context-menu.js +2 -0
- package/client/components/context-menu.js.map +1 -1
- package/client/components/context-menu.ts +6 -0
- package/client/components/folder-tree.js +26 -4
- package/client/components/folder-tree.js.map +1 -1
- package/client/components/folder-tree.ts +21 -4
- package/client/components/message-list.js +108 -40
- package/client/components/message-list.js.map +1 -1
- package/client/components/message-list.ts +103 -38
- package/client/compose/compose.bundle.js +148 -15
- package/client/compose/compose.bundle.js.map +3 -3
- package/client/compose/spellcheck.js +178 -12
- package/client/compose/spellcheck.js.map +1 -1
- package/client/compose/spellcheck.ts +168 -8
- package/client/lib/api-client.js +3 -0
- package/client/lib/api-client.js.map +1 -1
- package/client/lib/api-client.ts +4 -0
- package/client/lib/mailxapi.js +3 -0
- package/client/lib/rmf-tiny.js +25 -6
- package/package.json +7 -7
- package/packages/mailx-core/index.d.ts.map +1 -1
- package/packages/mailx-core/index.js +2 -12
- package/packages/mailx-core/index.js.map +1 -1
- package/packages/mailx-core/index.ts +2 -12
- package/packages/mailx-imap/index.d.ts.map +1 -1
- package/packages/mailx-imap/index.js +31 -6
- package/packages/mailx-imap/index.js.map +1 -1
- package/packages/mailx-imap/index.ts +32 -6
- package/packages/mailx-imap/node_modules.npmglobalize-stash-11884/.package-lock.json +116 -0
- package/packages/mailx-imap/package-lock.json +2 -2
- package/packages/mailx-imap/package.json +1 -1
- package/packages/mailx-service/index.d.ts +22 -0
- package/packages/mailx-service/index.d.ts.map +1 -1
- package/packages/mailx-service/index.js +123 -0
- package/packages/mailx-service/index.js.map +1 -1
- package/packages/mailx-service/index.ts +109 -0
- package/packages/mailx-service/jsonrpc.js +3 -0
- package/packages/mailx-service/jsonrpc.js.map +1 -1
- package/packages/mailx-service/jsonrpc.ts +3 -0
- package/packages/mailx-service/local-store.d.ts.map +1 -1
- package/packages/mailx-service/local-store.js +6 -12
- package/packages/mailx-service/local-store.js.map +1 -1
- package/packages/mailx-service/local-store.ts +6 -12
- package/packages/mailx-settings/docs/accounts.md +14 -1
- package/packages/mailx-settings/docs/npmglobalize-disttag.md +90 -0
- package/packages/mailx-settings/docs/prod-android.md +88 -0
- package/packages/mailx-settings/docs/prod.md +224 -0
- package/packages/mailx-settings/docs/push-relay.md +141 -0
- package/packages/mailx-settings/docs/rmf-tiny.md +156 -0
- package/packages/mailx-settings/index.d.ts +1 -1
- package/packages/mailx-settings/index.d.ts.map +1 -1
- package/packages/mailx-settings/index.js +1 -4
- package/packages/mailx-settings/index.js.map +1 -1
- package/packages/mailx-settings/index.ts +1 -3
- package/packages/mailx-settings/package.json +1 -1
- package/packages/mailx-store/db.d.ts.map +1 -1
- package/packages/mailx-store/db.js +44 -6
- package/packages/mailx-store/db.js.map +1 -1
- package/packages/mailx-store/db.ts +47 -6
- package/packages/mailx-store/package.json +1 -1
- package/packages/mailx-store-web/package.json +4 -1
- package/packages/mailx-store-web/web-settings.d.ts.map +1 -1
- package/packages/mailx-store-web/web-settings.js +0 -1
- package/packages/mailx-store-web/web-settings.js.map +1 -1
- package/packages/mailx-store-web/web-settings.ts +0 -1
- package/packages/mailx-types/index.d.ts +1 -2
- package/packages/mailx-types/index.d.ts.map +1 -1
- package/packages/mailx-types/index.js.map +1 -1
- package/packages/mailx-types/index.ts +1 -2
- 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.
|
|
@@ -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 →
|
|
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":"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,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":"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"}
|
|
@@ -535,7 +535,6 @@ export function normalizeAccount(acct, globalName, globalSig, globalSignature) {
|
|
|
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
|
|
@@ -727,7 +726,7 @@ export async function loadAccountsAsync() {
|
|
|
727
726
|
* - Drop `imap.user` / `smtp.user` if they equal the email.
|
|
728
727
|
* - Drop `sig.html: false` (default; only keep when user enabled HTML sigs).
|
|
729
728
|
* - Keep field order: id → label → email → primary* → imap → smtp → defaultSend
|
|
730
|
-
* → relayDomains →
|
|
729
|
+
* → relayDomains → identityDomains → syncContacts → sig.
|
|
731
730
|
* Curated for readability, not alphabetic. */
|
|
732
731
|
export function denormalizeAccount(acct, globalName) {
|
|
733
732
|
const domain = (acct.email || "").split("@")[1]?.toLowerCase() || "";
|
|
@@ -785,8 +784,6 @@ export function denormalizeAccount(acct, globalName) {
|
|
|
785
784
|
out.enabled = false; // default true → omit
|
|
786
785
|
if (acct.relayDomains && acct.relayDomains.length > 0)
|
|
787
786
|
out.relayDomains = acct.relayDomains;
|
|
788
|
-
if (acct.deliveredToPrefix && acct.deliveredToPrefix.length > 0)
|
|
789
|
-
out.deliveredToPrefix = acct.deliveredToPrefix;
|
|
790
787
|
if (acct.identityDomains && acct.identityDomains.length > 0)
|
|
791
788
|
out.identityDomains = acct.identityDomains;
|
|
792
789
|
// syncContacts default: true for OAuth, false otherwise. Only emit when
|