@bobfrankston/rmfmail 1.2.6 → 1.2.8
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/client/app.bundle.js +70 -1
- package/client/app.bundle.js.map +2 -2
- package/client/components/alarms.js +75 -1
- package/client/components/alarms.js.map +1 -1
- package/client/components/alarms.ts +63 -1
- package/client/compose/compose.bundle.js +4 -0
- package/client/compose/compose.bundle.js.map +2 -2
- package/client/lib/api-client.js +3 -0
- package/client/lib/api-client.js.map +1 -1
- package/client/lib/api-client.ts +3 -0
- package/client/lib/mailxapi.js +3 -0
- package/package.json +5 -5
- package/packages/mailx-imap/index.d.ts.map +1 -1
- package/packages/mailx-imap/index.js +47 -15
- package/packages/mailx-imap/index.js.map +1 -1
- package/packages/mailx-imap/index.ts +42 -15
- package/packages/mailx-imap/package-lock.json +2 -2
- package/packages/mailx-imap/package.json +1 -1
- package/packages/mailx-service/index.d.ts +13 -0
- package/packages/mailx-service/index.d.ts.map +1 -1
- package/packages/mailx-service/index.js +42 -1
- package/packages/mailx-service/index.js.map +1 -1
- package/packages/mailx-service/index.ts +39 -1
- package/packages/mailx-service/jsonrpc.js +2 -0
- package/packages/mailx-service/jsonrpc.js.map +1 -1
- package/packages/mailx-service/jsonrpc.ts +2 -0
- package/packages/mailx-settings/cloud.d.ts +2 -0
- package/packages/mailx-settings/cloud.d.ts.map +1 -1
- package/packages/mailx-settings/cloud.js +45 -0
- package/packages/mailx-settings/cloud.js.map +1 -1
- package/packages/mailx-settings/cloud.ts +32 -0
- package/packages/mailx-settings/docs/architecture-review.md +182 -0
- package/packages/mailx-settings/docs/clean-architecture.md +161 -0
- package/packages/mailx-settings/docs/outlook.md +215 -0
- package/packages/mailx-settings/index.d.ts +9 -0
- package/packages/mailx-settings/index.d.ts.map +1 -1
- package/packages/mailx-settings/index.js +39 -0
- package/packages/mailx-settings/index.js.map +1 -1
- package/packages/mailx-settings/index.ts +35 -0
- package/packages/mailx-settings/package.json +1 -1
- package/packages/mailx-store/db.d.ts +9 -0
- package/packages/mailx-store/db.d.ts.map +1 -1
- package/packages/mailx-store/db.js +32 -1
- package/packages/mailx-store/db.js.map +1 -1
- package/packages/mailx-store/db.ts +28 -1
- package/packages/mailx-store/package.json +1 -1
- package/test/two-writer-lock.mjs +55 -0
- package/test/two-writer-thread.mjs +51 -0
- package/test/two-writer-worker.mjs +14 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# rmfmail / mailx — Target Architecture & Migration Plan
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-06-12
|
|
4
|
+
**Goal:** Thunderbird/Outlook-grade reliability without a green-field rewrite. Re-architect the **core data + sync layer**; keep the existing UI and feature surface and migrate it onto the new core in shippable, reversible phases.
|
|
5
|
+
**Companion:** `architecture-review.md` (the diagnosis this design answers).
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 1. The two principles that make mail clients reliable
|
|
10
|
+
|
|
11
|
+
Every reliable mail client (Thunderbird's global DB / "Panorama", Outlook's OST, Apple Mail's Envelope Index) obeys these:
|
|
12
|
+
|
|
13
|
+
1. **The display layer never blocks on the network.** A local store (a database) is the single source of truth for *everything the user sees and does*. Opening a folder, reading a message, searching, deleting — all are local operations that complete instantly. The network is a **separate, fully asynchronous background engine** that reconciles the store with the server. A dead/slow server makes mail *stale*, never makes the UI *freeze*.
|
|
14
|
+
|
|
15
|
+
2. **One canonical store, one stable identity, one path per operation.** A message has one durable local identity that survives folder moves and server UID renumbering. Each operation (delete, move, flag) has exactly one implementation that mutates the store and queues a server action. There are no parallel code paths to drift.
|
|
16
|
+
|
|
17
|
+
rmfmail violates both today (see review §3, §6). This design restores both.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## 2. Three layers, strictly separated
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
25
|
+
│ UI (client/) │
|
|
26
|
+
│ • reads: pure store queries (instant, never await network) │
|
|
27
|
+
│ • writes: call Store mutation → optimistic local change │
|
|
28
|
+
│ + enqueue server action; return immediately │
|
|
29
|
+
│ • re-renders from Store change events │
|
|
30
|
+
└───────────────▲───────────────────────────┬─────────────────┘
|
|
31
|
+
│ store events │ read / mutate (local only)
|
|
32
|
+
┌───────────────┴───────────────────────────▼─────────────────┐
|
|
33
|
+
│ STORE (the source of truth for display) │
|
|
34
|
+
│ • SQLite. Stable message identity = local UUID. │
|
|
35
|
+
│ • Pure, synchronous, network-free read API. │
|
|
36
|
+
│ • Mutation funnel: trash/move/flag/copy/deletePermanent — │
|
|
37
|
+
│ one function each, keyed by UUID. Each = local change + │
|
|
38
|
+
│ durable queued action + change event. │
|
|
39
|
+
│ • Durable action queue (idempotent, UUID-keyed). │
|
|
40
|
+
└───────────────▲───────────────────────────┬─────────────────┘
|
|
41
|
+
│ "here is new/changed data" │ "drain these actions"
|
|
42
|
+
┌───────────────┴───────────────────────────▼─────────────────┐
|
|
43
|
+
│ SYNC ENGINE (the ONLY thing that touches the network) │
|
|
44
|
+
│ • Owns all IMAP/Gmail connections, fully isolated. │
|
|
45
|
+
│ • PULL: incremental fetch (QRESYNC/CONDSTORE → set-diff). │
|
|
46
|
+
│ • PUSH: drain the action queue to the server, idempotently. │
|
|
47
|
+
│ • RECONCILE: re-bind identity across UID changes by │
|
|
48
|
+
│ Message-ID / UUID. Maintain (folder,uid,uidvalidity) map. │
|
|
49
|
+
│ • Bounded connections, per-op timeout + cancellation, │
|
|
50
|
+
│ backoff, poison-action cap. A hung op affects ONLY here. │
|
|
51
|
+
└──────────────────────────────────────────────────────────────┘
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The hard rule: **an arrow never points "up-and-blocking."** The UI calls into the Store and returns; it never awaits the Sync Engine. The Sync Engine pushes results down into the Store and the Store emits events up to the UI. There is no path where a network stall can sit inside a UI read.
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## 3. Identity (fixes review §5 — the reappear/ghost/wipe class)
|
|
59
|
+
|
|
60
|
+
- Every message gets a **stable local `id` (UUID)** at first sight.
|
|
61
|
+
- The identity key is **Message-ID** when present; otherwise a synthesized stable hash of `(account, internaldate, from, subject, size)`. This guarantees an identity even for malformed / Message-ID-less mail (the gap that breaks re-binding today).
|
|
62
|
+
- `(folder, server_uid, uidvalidity)` is **transient routing metadata**, owned and maintained only by the Sync Engine, in a `message_locations` table. **Nothing in the UI or Store mutations is ever keyed on a bare `uid`.**
|
|
63
|
+
- Multi-folder membership (Gmail labels, or a message genuinely in two folders) is N rows in `message_locations` pointing at one message UUID.
|
|
64
|
+
- Server-side UID renumber (move, UIDVALIDITY bump) re-binds the location row to the same UUID by Message-ID match — never creates a new identity, never orphans the body.
|
|
65
|
+
|
|
66
|
+
**Consequence:** the 70k-row wipe (a `WHERE uid=?` that hit every folder) becomes structurally impossible — there is no UID-only delete. Cross-folder collisions, stars-on-wrong-rows, ghosts, wrong-row-on-auto-advance all dissolve because UID is no longer identity.
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## 4. Mutation funnel (fixes review §6 — "every fix breaks another path")
|
|
71
|
+
|
|
72
|
+
The Store exposes exactly these mutators, each taking message UUIDs:
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
store.trash(uuids) // move to Trash, or permanent-expunge if already in Trash
|
|
76
|
+
store.move(uuids, toFolder)
|
|
77
|
+
store.copy(uuids, toFolder)
|
|
78
|
+
store.flag(uuids, flag, on)
|
|
79
|
+
store.deletePermanent(uuids)
|
|
80
|
+
store.markRead(uuids, on)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Each does, atomically: **(1)** update local rows + folder counts, **(2)** record an undo entry, **(3)** enqueue a durable, idempotent server action keyed by UUID, **(4)** emit a change event.
|
|
84
|
+
|
|
85
|
+
- **Every** UI trigger (Delete key, toolbar, drag, right-drag menu, context menu, pop-out, undo) calls these and nothing else. No optimistic-removal logic scattered in the UI; the UI just re-renders from the change event.
|
|
86
|
+
- `emptyFolder` becomes `store.deletePermanent(all-uuids-in-folder)` — same funnel, not a separate IMAP loop.
|
|
87
|
+
- **Delete `packages/mailx-core`** (dead duplicate).
|
|
88
|
+
- **Android shares this exact Store + queue.** The only platform difference is the `Transport` the Sync Engine injects (Node TCP vs bridge). No second `trashMessage`.
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## 5. Read path & dispatch isolation (fixes review §1/§2 — the timeouts you're hitting now)
|
|
93
|
+
|
|
94
|
+
- All reads — `listFolder`, `unifiedInbox`, `getMessage`, `search` — are **pure synchronous SQLite** behind a single read facade. They **cannot** call the network. `getMessage` returns the cached body if present, else `{cached:false}` and the UI shows a placeholder while the Sync Engine fetches and emits `bodyReady`. A read **can never time out.**
|
|
95
|
+
- The IPC dispatch (`bin/mailx.ts`) must not serialize a read behind a network write. Two concrete options (pick after measuring):
|
|
96
|
+
- **(A)** Run the Sync Engine's network work off the dispatch thread (worker), so `dispatch()` only ever does fast local work.
|
|
97
|
+
- **(B)** Split dispatch into a read lane (own SQLite read handle, always-fast) and a write/command lane; reads are answered immediately regardless of what the write lane is doing.
|
|
98
|
+
- Network handlers get a hard wall-clock timeout + cancellation; a hung op is abandoned and its connection discarded, never occupying a shared slot. (The `withTimeout`/dual-lane machinery already exists in `mailx-imap` — it just needs to be *above* nothing that a read depends on.)
|
|
99
|
+
|
|
100
|
+
**Consequence:** `getUnifiedInbox` (and every list/read) returns instantly even while the server is hung. The "nothing in the summary" / `mailxapi timeout` failure class is gone by construction.
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## 6. Sync Engine robustness (the part that earns "Thunderbird-grade")
|
|
105
|
+
|
|
106
|
+
- **Connection discipline:** per-account budget with a **dedicated interactive connection** that background sync/prefetch can never starve. Per-host semaphore stays as the server-cap guard.
|
|
107
|
+
- **Every op:** wall-clock timeout + cancellation; on timeout, discard the connection and back off. A single un-fetchable message (today's 4966060) is recorded with backoff and never re-attempted in a tight loop.
|
|
108
|
+
- **Push queue:** durable, idempotent, UUID-keyed actions; retry with backoff; a **poison cap** (give up after N, surface to the UI) so one bad action can't loop forever.
|
|
109
|
+
- **Pull:** capability-gated incremental sync — QRESYNC/CONDSTORE when the server advertises them, UID set-diff fallback otherwise; persist per-folder `UIDVALIDITY` + `HIGHESTMODSEQ`. (Memory note: capability-gate, don't hostname-branch.)
|
|
110
|
+
- **Reconcile:** the only place that re-binds UID↔UUID; tombstones with retained UUID suppress re-fetch of locally-deleted mail; UIDVALIDITY bump triggers a clean re-map, not a wipe.
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## 7. Migration plan — phased, shippable, reversible (NOT big-bang)
|
|
115
|
+
|
|
116
|
+
Each phase ships on its own, is independently verifiable, and can be reverted. We do **not** rewrite the UI or the features.
|
|
117
|
+
|
|
118
|
+
**Phase 0 — Read isolation (do first; ends the timeouts).**
|
|
119
|
+
- Guarantee every read RPC is network-free and cannot be head-of-line-blocked. Implement §5 option A or B in `bin/mailx.ts` + the service read methods. Add hard timeouts/cancellation to network handlers.
|
|
120
|
+
- *Exit test:* with the IMAP server artificially hung, the list, folder switch, search, and cached-message open all still respond instantly. No `mailxapi timeout` on any read.
|
|
121
|
+
|
|
122
|
+
**Phase 1 — Identity + mutation funnel.**
|
|
123
|
+
- Introduce/standardize the UUID identity and `message_locations`; backfill UUIDs (rebuild-from-server is acceptable per the "no migrations, redownload" rule).
|
|
124
|
+
- Route all deletes/moves/flags through the §4 funnel; delete `mailx-core`; converge `emptyFolder`.
|
|
125
|
+
- *Exit test:* a scripted scenario with the same numeric UID in INBOX + Trash + Sent — delete/flag/move one and confirm the others are untouched. No ghost rows after a move+delete cycle.
|
|
126
|
+
|
|
127
|
+
**Phase 2 — Sync Engine extraction.**
|
|
128
|
+
- Make the Sync Engine the sole network owner with §6 discipline; move cross-account move and outbox under it. Dedicated interactive lane.
|
|
129
|
+
- *Exit test:* hung-folder fault injection — background sync stalls, interactive open/delete stay responsive; poison action gives up instead of looping.
|
|
130
|
+
|
|
131
|
+
**Phase 3 — Search unification + Android convergence.**
|
|
132
|
+
- One query parser feeding the (local-FTS / server-IMAP) executors. Android uses the shared Store + queue + a `Transport`.
|
|
133
|
+
- *Exit test:* identical query semantics across scopes; Android delete sets the same pending/tombstone state as desktop.
|
|
134
|
+
|
|
135
|
+
Order rationale: Phase 0 stops the bleeding you're seeing *right now*; Phase 1 removes the data-integrity class (the scary one — the wipe); Phases 2–3 are robustness + cleanup.
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## 8. What we explicitly do NOT do
|
|
140
|
+
|
|
141
|
+
- No green-field rewrite. The UI, compose/editor, providers, OAuth, calendar/tasks, Android shell, and the feature set stay.
|
|
142
|
+
- No big-bang cutover. Each phase coexists with the rest.
|
|
143
|
+
- No hot-patching the live daemon during this work — changes land, get built, and are verified deliberately (the firefighting pattern is over).
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## 9. How we'll verify reliability (the Thunderbird bar)
|
|
148
|
+
|
|
149
|
+
A short fault-injection harness, run after each phase:
|
|
150
|
+
1. **Server hung** (stall `BODY[]` / SELECT): UI reads stay instant; no timeouts.
|
|
151
|
+
2. **Server slow** (5s/op): actions queue and drain; UI never blocks.
|
|
152
|
+
3. **Offline → online:** queued actions drain in order, idempotently; no dupes, no loss.
|
|
153
|
+
4. **UID collision** across folders: per-message ops never hit the wrong message.
|
|
154
|
+
5. **UIDVALIDITY bump:** re-map, no wipe.
|
|
155
|
+
6. **Crash mid-action:** durable queue resumes; nothing lost, nothing double-applied.
|
|
156
|
+
|
|
157
|
+
When all six pass under injection, we're at the reliability bar you're asking for.
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
*This is the target. Phase 0 is the next concrete step and is self-contained.*
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# Outlook.com / Microsoft 365 Support
|
|
2
|
+
|
|
3
|
+
Status (2026-06-05): **scaffolding present, not usable.** An account auto-detects
|
|
4
|
+
the right hosts but cannot authenticate — there is no Microsoft OAuth wired, and
|
|
5
|
+
Microsoft has disabled basic-auth (password) for outlook.com / office365, so the
|
|
6
|
+
IMAP path can't fall back to a password either. Gmail is the only fully-wired
|
|
7
|
+
OAuth provider today.
|
|
8
|
+
|
|
9
|
+
This doc scopes what it takes to make Outlook a first-class account.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## What already exists
|
|
14
|
+
|
|
15
|
+
| Piece | Where | State |
|
|
16
|
+
|---|---|---|
|
|
17
|
+
| Graph API provider | `@bobfrankston/mailx-sync/outlook.ts` (re-exported `packages/mailx-imap/providers/outlook-api.ts`) | **Read-only**: `listFolders`, `fetchSince/ByDate/ByUids/One`, `getUids`. No send, no write-back, no drafts/attachments. |
|
|
18
|
+
| Host auto-config | `packages/mailx-settings/index.ts:503` (`outlook.com`, `hotmail.com` → `outlook.office365.com:993` / `smtp.office365.com:587`, `auth: "oauth2"`) | Done |
|
|
19
|
+
| MX-based detection | `bin/mailx.ts:1307` (`*.outlook.com` / `*.protection.outlook.com` MX → Microsoft 365) | Done |
|
|
20
|
+
| Generic OAuth flow | `@bobfrankston/oauthsupport` `OAuthTokenManager.ts:369` — validates `client_id/client_secret/auth_uri/token_uri` from the creds file; localhost loopback auth-code flow | **Provider-agnostic** — not Google-specific |
|
|
21
|
+
| OAuth SMTP send | `packages/mailx-imap/index.ts:1533` builds `{type:"OAuth2", user, accessToken}` from `config.tokenProvider()` | Works for any provider with a tokenProvider |
|
|
22
|
+
|
|
23
|
+
The single thing that makes Gmail work and Outlook not: the **tokenProvider** at
|
|
24
|
+
`packages/mailx-imap/index.ts:798-848` is built only for Google. It hardcodes the
|
|
25
|
+
Google credentials file (`~/.mailx/google-credentials.json`) and the all-Google
|
|
26
|
+
scope string. Nothing constructs a Microsoft tokenProvider, so
|
|
27
|
+
`OutlookApiProvider` is never instantiated and the office365 IMAP host has no
|
|
28
|
+
token to present.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Decision: Graph API vs IMAP/XOAUTH2
|
|
33
|
+
|
|
34
|
+
Both need the same Azure app registration + Microsoft OAuth. The difference is
|
|
35
|
+
the sync transport once you have a token.
|
|
36
|
+
|
|
37
|
+
**Recommended: Graph API.** It mirrors the Gmail-API model the codebase already
|
|
38
|
+
follows (`isGmailAccount()` branch → REST provider, no IMAP client). Wins: no
|
|
39
|
+
connection-limit/timeout class of bugs (the entire `inactivityTimeout` /
|
|
40
|
+
`greetingTimeout` / chunk-size tuning at `index.ts:862` is IMAP-only pain);
|
|
41
|
+
provider is already half-written; same `MailProvider` interface so Android reuses
|
|
42
|
+
it verbatim. Cost: the provider is read-only — send + flag/move/delete + drafts
|
|
43
|
+
must be added (Graph endpoints, ~a day). Sharp edge: `idToUid()`
|
|
44
|
+
(`outlook.ts` ~135) hashes Graph's long string IDs to 48-bit ints, and
|
|
45
|
+
`fetchByUids`/`fetchOne` re-list the **whole** folder then filter — O(folder) per
|
|
46
|
+
fetch with collision risk. Real sync needs a UID↔providerId map (we already keep
|
|
47
|
+
`provider_id` in the DB) and direct `GET /messages/{id}` instead of list-and-scan.
|
|
48
|
+
|
|
49
|
+
**Alternative: IMAP + XOAUTH2.** Reuses the existing iflow-direct sync path
|
|
50
|
+
unchanged — just feed it a Microsoft token instead of a Google one. Wins: zero
|
|
51
|
+
new sync/send code; flag/move/delete/append already work over IMAP. Cost:
|
|
52
|
+
inherits every IMAP fragility we've spent months hardening for Dovecot/Gmail, now
|
|
53
|
+
against Exchange Online's quirks. Sharp edge: Microsoft is steering third-party
|
|
54
|
+
clients toward Graph and away from consumer IMAP; betting the integration on
|
|
55
|
+
office365 IMAP is the less future-proof path.
|
|
56
|
+
|
|
57
|
+
Pragmatic call: **IMAP/XOAUTH2 first** as the fast path to a working account (it's
|
|
58
|
+
mostly OAuth plumbing), then migrate sync to Graph for parity with Gmail. The
|
|
59
|
+
OAuth work below is shared, so it's not wasted either way.
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Work breakdown
|
|
64
|
+
|
|
65
|
+
### 1. Azure app registration [S, one-time, external]
|
|
66
|
+
Register an app in Entra/Azure AD:
|
|
67
|
+
- Account type: "personal Microsoft accounts" (consumer outlook.com) and/or
|
|
68
|
+
"any org directory" (Microsoft 365). For both, use the `/common` authority.
|
|
69
|
+
- Redirect URI: `http://localhost` (loopback) — matches oauthsupport's flow
|
|
70
|
+
(`OAuthTokenManager.ts:463`, parses the loopback callback).
|
|
71
|
+
- API permissions (delegated):
|
|
72
|
+
- Graph path: `Mail.ReadWrite`, `Mail.Send`, `offline_access`, `openid`,
|
|
73
|
+
`profile`.
|
|
74
|
+
- IMAP path: `https://outlook.office.com/IMAP.AccessAsUser.All`,
|
|
75
|
+
`https://outlook.office.com/SMTP.Send`, `offline_access`.
|
|
76
|
+
- Produce a `microsoft-credentials.json` shaped like the Google one, with
|
|
77
|
+
`auth_uri: https://login.microsoftonline.com/common/oauth2/v2.0/authorize`,
|
|
78
|
+
`token_uri: https://login.microsoftonline.com/common/oauth2/v2.0/token`,
|
|
79
|
+
`redirect_uris: ["http://localhost"]`, plus `client_id` / `client_secret`.
|
|
80
|
+
|
|
81
|
+
Store at `~/.mailx/microsoft-credentials.json` (parallel to
|
|
82
|
+
`google-credentials.json`).
|
|
83
|
+
|
|
84
|
+
### 2. Microsoft tokenProvider [M]
|
|
85
|
+
Generalize `index.ts:798-848` so the provider isn't Google-only:
|
|
86
|
+
- Pick the creds file + scope by account kind (gmail vs outlook), keyed off the
|
|
87
|
+
same signal `account.imap.auth === "oauth2"` plus host/email domain.
|
|
88
|
+
- For Outlook, point `authenticateOAuth` at `microsoft-credentials.json` with the
|
|
89
|
+
Microsoft scope set. The flow itself is unchanged — oauthsupport already reads
|
|
90
|
+
the endpoints from the creds file, so no oauthsupport changes are required
|
|
91
|
+
(confirm consumer-account PKCE: Microsoft may require `code_challenge` for the
|
|
92
|
+
loopback client; if so, add PKCE support in `OAuthTokenManager.getToken`).
|
|
93
|
+
- Token cache dir reuses the existing `tokens/<user>/` convention (`index.ts:814`).
|
|
94
|
+
|
|
95
|
+
This single change makes the **IMAP/XOAUTH2 path work end-to-end** — the office365
|
|
96
|
+
host config already exists, and SMTP OAuth (`index.ts:1533`) consumes the same
|
|
97
|
+
tokenProvider. Verify the token is minted with Outlook IMAP/SMTP scopes, not Graph
|
|
98
|
+
scopes (they're different audiences).
|
|
99
|
+
|
|
100
|
+
### 3. Provider selection [S]
|
|
101
|
+
Add an `isOutlookAccount()` sibling to `isGmailAccount()` (`index.ts:898`) and an
|
|
102
|
+
`getOutlookProvider()` mirroring `getGmailProvider()` (`index.ts:906`) that does
|
|
103
|
+
`new OutlookApiProvider(config.tokenProvider)`. Then extend each
|
|
104
|
+
`isGmailAccount()` branch (sync, periodic STATUS, IDLE-skip, etc. — ~10 sites) to
|
|
105
|
+
a three-way provider switch. *(Only needed for the Graph path; the IMAP path
|
|
106
|
+
needs none of this — it flows through the normal IMAP code.)*
|
|
107
|
+
|
|
108
|
+
### 4. Graph write-back + send [M] *(Graph path only)*
|
|
109
|
+
Add to `OutlookApiProvider`:
|
|
110
|
+
- `sendMail`: `POST /sendMail` with the MIME or the Graph message object.
|
|
111
|
+
- mark read/flag: `PATCH /messages/{id}` (`isRead`, `flag`).
|
|
112
|
+
- move: `POST /messages/{id}/move`.
|
|
113
|
+
- delete: `DELETE /messages/{id}` (or move to deleteditems).
|
|
114
|
+
- draft create/update: `POST /messages` / `PATCH`.
|
|
115
|
+
- attachments: `GET /messages/{id}/attachments`.
|
|
116
|
+
Replace list-and-scan in `fetchByUids`/`fetchOne` with direct
|
|
117
|
+
`GET /messages/{providerId}` using the DB's `provider_id` mapping.
|
|
118
|
+
|
|
119
|
+
### 5. Setup UX [S]
|
|
120
|
+
`bin/mailx.ts` `knownOAuth` list (~line 1320) already includes `outlook.com` /
|
|
121
|
+
`hotmail.com`. Once the tokenProvider exists, first-run setup triggers the
|
|
122
|
+
Microsoft consent screen the same way Gmail does — no extra UI. Add an error
|
|
123
|
+
banner case for "Microsoft credentials missing" pointing at
|
|
124
|
+
`microsoft-credentials.json`.
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## Step-by-step (with URLs)
|
|
129
|
+
|
|
130
|
+
### A. Register the Azure app — the part only you can do
|
|
131
|
+
1. Sign in to the Entra admin center → **App registrations**:
|
|
132
|
+
https://entra.microsoft.com/#view/Microsoft_AAD_RegisteredApps/ApplicationsListBlade
|
|
133
|
+
(equivalently Azure portal → "Microsoft Entra ID" → "App registrations":
|
|
134
|
+
https://portal.azure.com)
|
|
135
|
+
2. **New registration** (guide:
|
|
136
|
+
https://learn.microsoft.com/en-us/entra/identity-platform/quickstart-register-app):
|
|
137
|
+
- Name: `rmfmail`.
|
|
138
|
+
- Supported account types: **"Accounts in any organizational directory and
|
|
139
|
+
personal Microsoft accounts"** (covers both outlook.com consumer and M365).
|
|
140
|
+
- Redirect URI: platform **"Mobile and desktop applications"**, value
|
|
141
|
+
**`http://localhost`** (loopback — what oauthsupport's flow listens on).
|
|
142
|
+
Desktop/loopback registration:
|
|
143
|
+
https://learn.microsoft.com/en-us/entra/identity-platform/scenario-desktop-app-registration
|
|
144
|
+
3. Copy the **Application (client) ID** from the app's Overview page.
|
|
145
|
+
4. **Certificates & secrets** → **New client secret** → copy the secret *value*
|
|
146
|
+
(not the ID). (Public-client/PKCE is the alternative if you'd rather not ship a
|
|
147
|
+
secret — see Sharp edges.)
|
|
148
|
+
5. **API permissions** → **Add a permission** → **Microsoft Graph** → **Delegated**
|
|
149
|
+
(permissions reference:
|
|
150
|
+
https://learn.microsoft.com/en-us/graph/permissions-reference):
|
|
151
|
+
- Graph path: `Mail.ReadWrite`, `Mail.Send`, `offline_access`, `openid`,
|
|
152
|
+
`profile`.
|
|
153
|
+
- IMAP path instead/also: `IMAP.AccessAsUser.All`, `SMTP.Send`,
|
|
154
|
+
`offline_access` (these live under the *Office 365 Exchange Online* API, not
|
|
155
|
+
Graph). Microsoft's IMAP/SMTP-OAuth guide:
|
|
156
|
+
https://learn.microsoft.com/en-us/exchange/client-developer/legacy-protocols/how-to-authenticate-an-imap-pop-smtp-application-by-using-oauth
|
|
157
|
+
- Click **Grant admin consent** if it's a work tenant (personal accounts
|
|
158
|
+
consent at first sign-in).
|
|
159
|
+
|
|
160
|
+
Reference for the endpoints used below:
|
|
161
|
+
- Authorize: `https://login.microsoftonline.com/common/oauth2/v2.0/authorize`
|
|
162
|
+
- Token: `https://login.microsoftonline.com/common/oauth2/v2.0/token`
|
|
163
|
+
- Auth-code flow spec:
|
|
164
|
+
https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-auth-code-flow
|
|
165
|
+
- Graph mail API overview:
|
|
166
|
+
https://learn.microsoft.com/en-us/graph/api/resources/mail-api-overview
|
|
167
|
+
|
|
168
|
+
### B. Drop in the credentials file
|
|
169
|
+
Create `~/.mailx/microsoft-credentials.json` mirroring the Google one's shape so
|
|
170
|
+
oauthsupport's `OAuthTokenManager` (reads `client_id`/`client_secret`/`auth_uri`/
|
|
171
|
+
`token_uri`, `OAuthTokenManager.ts:369`) consumes it unchanged:
|
|
172
|
+
|
|
173
|
+
{
|
|
174
|
+
"installed": {
|
|
175
|
+
"client_id": "<Application (client) ID>",
|
|
176
|
+
"client_secret": "<secret value>",
|
|
177
|
+
"auth_uri": "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
|
|
178
|
+
"token_uri": "https://login.microsoftonline.com/common/oauth2/v2.0/token",
|
|
179
|
+
"redirect_uris": ["http://localhost"]
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
### C. Wire the code (steps 2-3 from the work breakdown)
|
|
184
|
+
1. In `packages/mailx-imap/index.ts` (~798), branch the tokenProvider on account
|
|
185
|
+
kind: Outlook accounts use `microsoft-credentials.json` + the Microsoft scope
|
|
186
|
+
string (IMAP scopes for the IMAP path, Graph scopes for the Graph path).
|
|
187
|
+
2. For the **IMAP path** that's the whole change — the office365 host config
|
|
188
|
+
(`mailx-settings/index.ts:503`) and OAuth-SMTP (`index.ts:1533`) already
|
|
189
|
+
consume the tokenProvider. Test sign-in: first send triggers the loopback
|
|
190
|
+
consent at `http://localhost`.
|
|
191
|
+
3. For the **Graph path**, add `isOutlookAccount()`/`getOutlookProvider()` next to
|
|
192
|
+
the Gmail equivalents (`index.ts:898/906`) and fill the provider's write-back
|
|
193
|
+
gaps (`sendMail`, flag/move/delete, drafts).
|
|
194
|
+
|
|
195
|
+
## Sharp edges / open questions
|
|
196
|
+
|
|
197
|
+
- **Consumer vs M365 differ.** Personal outlook.com and Microsoft 365 work
|
|
198
|
+
accounts use different scope audiences and consent behavior. `/common` covers
|
|
199
|
+
both at the authority level, but test both — a token good for Graph isn't valid
|
|
200
|
+
for IMAP and vice-versa.
|
|
201
|
+
- **PKCE.** Microsoft increasingly requires PKCE for loopback clients. If consent
|
|
202
|
+
fails with `invalid_request`, add `code_challenge`/`code_verifier` to
|
|
203
|
+
oauthsupport's auth-code flow (it's the one oauthsupport change that might be
|
|
204
|
+
needed).
|
|
205
|
+
- **Basic auth is gone.** Don't add a password fallback for office365 — it will
|
|
206
|
+
always fail. The error path should say "Outlook requires sign-in", not "wrong
|
|
207
|
+
password".
|
|
208
|
+
- **Drive/OneDrive is unrelated.** `cloud.ts` already sketches MS Graph
|
|
209
|
+
`Files.ReadWrite` scopes for OneDrive storage — that's a separate app/scope from
|
|
210
|
+
mail and shares none of this wiring (and OneDrive cloud storage was removed;
|
|
211
|
+
GDrive only).
|
|
212
|
+
- **`idToUid` collisions.** A 32-bit hash of Graph IDs across a 10-year mailbox
|
|
213
|
+
will eventually collide. The Graph path must key on `provider_id`
|
|
214
|
+
(string) as identity, with the integer UID as a display/sort convenience only —
|
|
215
|
+
same lesson as `imap_uid_not_identity`.
|
|
@@ -27,6 +27,12 @@ export declare function getLastCloudError(): string | null;
|
|
|
27
27
|
declare function getSharedDir(): string;
|
|
28
28
|
/** Read a file via cloud API (when filesystem mount not available) */
|
|
29
29
|
export declare function cloudRead(filename: string): Promise<string | null>;
|
|
30
|
+
/** Read a file from the shared cloud config folder as raw bytes, base64-encoded.
|
|
31
|
+
* Binary-safe sibling of cloudRead — used for the optional custom reminder
|
|
32
|
+
* sound that lives next to the JSONC config on Drive. Returns null if cloud
|
|
33
|
+
* isn't configured, the file is missing, or the read errors (caller falls back
|
|
34
|
+
* to the built-in chime). */
|
|
35
|
+
export declare function cloudReadBinary(filename: string): Promise<string | null>;
|
|
30
36
|
/** Write a file via cloud API. Throws on failure with a descriptive error,
|
|
31
37
|
* and updates lastCloudError so UI banners pick it up via getStorageInfo()
|
|
32
38
|
* and the onCloudError listener. */
|
|
@@ -79,6 +85,9 @@ declare const DEFAULT_PREFERENCES: {
|
|
|
79
85
|
historyDays: number;
|
|
80
86
|
prefetch: boolean;
|
|
81
87
|
};
|
|
88
|
+
reminders: {
|
|
89
|
+
sound: string;
|
|
90
|
+
};
|
|
82
91
|
autocomplete: {
|
|
83
92
|
enabled: boolean;
|
|
84
93
|
provider: "ollama";
|
|
@@ -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,CAgDxE;AAED;;qCAEqC;AACrC,wBAAsB,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAsCjF;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;;;;;;;;;;;;4EAY4E;AAC5E,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAcpD;AAED;;;0EAG0E;AAC1E,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAElD;AAED;;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
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":"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"}
|
|
@@ -288,6 +288,35 @@ export async function cloudRead(filename) {
|
|
|
288
288
|
// Don't set error for missing files — they may not exist yet (e.g., clients.jsonc on first run)
|
|
289
289
|
return content;
|
|
290
290
|
}
|
|
291
|
+
/** Read a file from the shared cloud config folder as raw bytes, base64-encoded.
|
|
292
|
+
* Binary-safe sibling of cloudRead — used for the optional custom reminder
|
|
293
|
+
* sound that lives next to the JSONC config on Drive. Returns null if cloud
|
|
294
|
+
* isn't configured, the file is missing, or the read errors (caller falls back
|
|
295
|
+
* to the built-in chime). */
|
|
296
|
+
export async function cloudReadBinary(filename) {
|
|
297
|
+
if (!pendingCloudConfig)
|
|
298
|
+
return null;
|
|
299
|
+
if (!pendingCloudConfig.folderId) {
|
|
300
|
+
pendingCloudConfig.folderId = await gDriveFindOrCreateFolder() || undefined;
|
|
301
|
+
if (pendingCloudConfig.folderId)
|
|
302
|
+
saveFolderIdToConfig(pendingCloudConfig.folderId);
|
|
303
|
+
if (!pendingCloudConfig.folderId)
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
const provider = getCloudProvider(pendingCloudConfig.provider, pendingCloudConfig.folderId);
|
|
307
|
+
if (!provider?.readBinary)
|
|
308
|
+
return null;
|
|
309
|
+
try {
|
|
310
|
+
return await Promise.race([
|
|
311
|
+
provider.readBinary(filename),
|
|
312
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error(`cloudReadBinary ${filename}: 15s timeout`)), 15_000)),
|
|
313
|
+
]);
|
|
314
|
+
}
|
|
315
|
+
catch (e) {
|
|
316
|
+
console.error(` [cloud] readBinary ${filename}: ${e?.message || e}`);
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
291
320
|
/** Write a file via cloud API. Throws on failure with a descriptive error,
|
|
292
321
|
* and updates lastCloudError so UI banners pick it up via getStorageInfo()
|
|
293
322
|
* and the onCloudError listener. */
|
|
@@ -622,6 +651,16 @@ const DEFAULT_PREFERENCES = {
|
|
|
622
651
|
historyDays: 30,
|
|
623
652
|
prefetch: true,
|
|
624
653
|
},
|
|
654
|
+
reminders: {
|
|
655
|
+
// Sound played when a calendar/task reminder fires.
|
|
656
|
+
// "" → built-in chime (a soft two-note tone, no file needed)
|
|
657
|
+
// "none" → silent
|
|
658
|
+
// <name> → a custom sound FILE, resolved relative to the config:
|
|
659
|
+
// tried in the LOCAL config dir (~/.rmfmail/<name>) first,
|
|
660
|
+
// then in the shared cloud config folder on Drive. If it
|
|
661
|
+
// can't be read or played, falls back to the built-in chime.
|
|
662
|
+
sound: "",
|
|
663
|
+
},
|
|
625
664
|
autocomplete: {
|
|
626
665
|
enabled: false,
|
|
627
666
|
provider: "ollama",
|