@bobfrankston/rmfmail 1.2.142 → 1.2.144

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/TODO.md CHANGED
@@ -99,13 +99,14 @@ Everything AI-shaped collected here so Bob can think about it on its own terms,
99
99
  |---|---|
100
100
  | **Q103-AI** ❓ | **Rules / extensions engine shape (C39).** Parked 2026-04-23 while Bob experiments with imail. Revisit once imail findings settle. Candidate shapes when it's time: declarative JSONC rules (`if from: foo then move to bar`), TypeScript plugins under `~/.mailx/extensions/`, AI-classified categories (newsletter / priority / action-required), or a hybrid. |
101
101
  | **Q111-AI** ❓ | **Writing assistance.** LanguageTool (API or self-hosted), custom AI via the existing ghost-text path (Ollama / Claude / OpenAI), in-WebView2 native spellcheck only (current), or something else? |
102
- | **Q113-AI** ❓ | **AI-flavored per-message metadata.** The schema shape in Q113 above directly affects where AI-assigned priority / categories / summaries live. If Q113 lands on a separate `message_meta` table the AI tags have a natural home; if it's a JSON column on `messages`, the AI writer mutates the same row. |
102
+ | **Q113-AI** ❓ | **AI-flavored per-message metadata.** The schema shape in Q113 above directly affects where AI-assigned priority / categories / summaries live. If Q113 lands on a separate `message_meta` table the AI tags have a natural home; if it's a JSON column on `messages`, the AI writer mutates the same row. *Update 2026-07-16: C155 answers the cross-app half — categories/signals live ON the message as IMAP keywords with a shared registry; Q113's table-vs-column question now only covers rmfmail-internal rich residue.* |
103
103
 
104
104
  ### AI — Backlog
105
105
 
106
106
  | # | Status | Item |
107
107
  |---|---|---|
108
- | [**C39**](#ext39) ❓ | PARKED | Rules / extensions engine + AI classification. See Q103-AI. |
108
+ | **C155** ❓ | DRAFT | **Annotation registry + imail companion annotator** [M]. Companion app annotates each letter *before* sorting. Two-layer design (2026-07-16 discussion): data plane = IMAP keywords on the message itself (core RFC 3501, portable — Gmail labels / Graph categories are the same concept per provider); vocabulary plane = one shared `annotations.jsonc` registry with MCP-tool-style entries (`description`/`whenToApply` prompt the AI annotator, `sortAction` drives the sorter, `label` feeds UIs). No shared DB needed — IMAP STORE is the concurrency layer; rich residue (dates, summaries) stays out or comes back later as a service-owned sidecar. Design note: `docs/annotation-registry.md`; starter vocabulary: `docs/annotations.jsonc`. Awaiting Bob's think-through — open questions at the end of the design note. Subsumes the cross-app half of Q113/Q113-AI; feeds Q103-AI/C39/P19 (rules engine shape). |
109
+ | [**C39**](#ext39) ❓ | PARKED | Rules / extensions engine + AI classification. See Q103-AI, C155. |
109
110
  | [**P19**](#priority-daily) ❓ | OPEN | Screener — imail rules + AI classifier. Elevated under daily-driver; same classifier question as C39. |
110
111
  | **AI-writing** ❓ | OPEN | Proofread / rewrite / tone-adjust in compose. Currently a Settings toggle that's off; needs the shape from Q111-AI before wiring. |
111
112
  | **AI-extract-contact** | OPEN | Right-click an email → Add contact auto-fills Name / Organization / Phone from the letter body. "In the future" per user 2026-04-23. Depends on a working AI back-end choice from Q111-AI. |
@@ -588,12 +588,12 @@
588
588
  </div>
589
589
  </div>
590
590
  <div class="compose-toolbar">
591
- <button class="tb-btn" id="btn-send">Send</button>
592
- <button class="tb-btn" id="btn-attach">Attach</button>
591
+ <button class="tb-btn" id="btn-send" title="Send (Ctrl+Enter)">Send</button>
592
+ <button class="tb-btn" id="btn-attach" title="Attach files">Attach</button>
593
593
  <input type="file" id="compose-file" multiple hidden>
594
594
  <button class="tb-btn" id="btn-edit-in-word" title="Open the body in Microsoft Word — saves in Word reload back into the editor">Edit in Word</button>
595
595
  <button class="tb-btn" id="btn-compose-help" title="Editor shortcuts and formatting reference (Quill vs tiptap differences)">?</button>
596
- <button class="tb-btn" id="btn-discard">Discard</button>
596
+ <button class="tb-btn" id="btn-discard" title="Discard this message without saving a draft">Discard</button>
597
597
  <span id="compose-editor-badge" class="compose-editor-badge" title="Active editor — change in Settings → Compose"></span>
598
598
  <span id="compose-status" class="compose-status"></span>
599
599
  </div>
package/client/index.html CHANGED
@@ -714,7 +714,7 @@
714
714
  </div>
715
715
  <div class="toolbar-center">
716
716
  <div class="tb-menu" id="view-menu">
717
- <button class="tb-btn" id="btn-view">View</button>
717
+ <button class="tb-btn" id="btn-view" title="View menu — sort order, date basis, thread grouping, layout">View</button>
718
718
  <div class="tb-menu-dropdown" id="view-dropdown" hidden>
719
719
  <button class="tb-menu-item" id="opt-new-window" title="Open another full mailx window (own OS window — e.g. Drafts here, Sent there, side by side). Secondary windows don't live-update on new mail; switch folders or sync to refresh.">🗗 New window</button>
720
720
  <hr class="tb-menu-sep">
@@ -738,7 +738,7 @@
738
738
  </div>
739
739
  </div>
740
740
  <div class="tb-menu" id="settings-menu">
741
- <button class="tb-btn" id="btn-settings">Settings</button>
741
+ <button class="tb-btn" id="btn-settings" title="Settings menu — accounts, compose, AI, config files">Settings</button>
742
742
  <div class="tb-menu-dropdown" id="settings-dropdown" hidden>
743
743
  <div class="tb-menu-submenu" id="theme-submenu-wrap">
744
744
  <button type="button" class="tb-menu-item tb-menu-item-submenu" id="theme-submenu-toggle" aria-haspopup="true" aria-expanded="false" title="Theme: System / Light / Dark">Theme <span class="tb-menu-submenu-current" id="theme-submenu-current">System</span> ▸</button>
@@ -883,7 +883,7 @@
883
883
  <button class="tb-btn" id="btn-print" title="Print (Ctrl+P)">🖨</button>
884
884
  <button class="tb-btn" id="mv-view-thread" title="View thread (conversation)" hidden>💬</button>
885
885
  <span style="flex:1"></span>
886
- <button class="mv-action mv-action-primary" id="mv-edit-draft" hidden>Edit & Send</button>
886
+ <button class="mv-action mv-action-primary" id="mv-edit-draft" title="Open this draft in the compose editor" hidden>Edit & Send</button>
887
887
  <a class="mv-unsubscribe" id="mv-unsubscribe" hidden>Unsubscribe</a>
888
888
  <button class="mv-action" id="mv-view-source" title="View source (.eml)" hidden>Source</button>
889
889
  <button class="mv-action" id="mv-popout" title="Pop out (desktop) / Full-screen (mobile)">⤢</button>
@@ -0,0 +1,74 @@
1
+ # Annotation registry — companion annotator for imail (design sketch)
2
+
3
+ *Status: DRAFT 2026-07-16 — captured from discussion; Bob thinking it over. TODO item C155.*
4
+
5
+ ## Goal
6
+
7
+ A companion app annotates each letter **before** sorting. imail's sorter (and any
8
+ other consumer — rmfmail UI, agents, future tools) reads the annotations and acts.
9
+ The mechanism should be general — not private to imail the way its `ai/` cache is.
10
+
11
+ ## Decision so far: keywords on the message + a shared JSON registry
12
+
13
+ Two-layer design, no shared database required:
14
+
15
+ 1. **Data plane — tags on the message itself.** IMAP keywords (core RFC 3501,
16
+ not Dovecot-specific: any server advertising `\*` in PERMANENTFLAGS). The
17
+ message carries its own annotations; they survive folder moves; IMAP STORE is
18
+ atomic per message so concurrency is the server's problem, already solved.
19
+ Provider mapping for the same concept:
20
+ | Provider | Mechanism |
21
+ |---|---|
22
+ | Dovecot / Cyrus / Fastmail / iCloud / Yahoo IMAP | user keywords (atoms) |
23
+ | Gmail | API labels (Gmail IMAP ignores custom keywords) |
24
+ | Outlook / Graph | categories |
25
+ | JMAP | keywords (unified with IMAP's by design) |
26
+
27
+ 2. **Vocabulary plane — one shared JSON registry** (`annotations.jsonc`, draft
28
+ next to this doc) with MCP-tool-style entries per keyword: `label`,
29
+ `description`, `whenToApply`, `exclusiveGroup`, `setBy`, `sortAction`.
30
+ The registry does triple duty:
31
+ - the AI annotator's prompt is generated from `description` / `whenToApply`
32
+ (same way MCP tool descriptions steer an agent);
33
+ - the sorter maps keyword → action from `sortAction`;
34
+ - UIs (rmfmail chips, Thunderbird tags) get display names from `label`.
35
+ Adding a category = editing one file. No schema migration, no API change.
36
+
37
+ ## Constraints / cautions
38
+
39
+ - **Keyword budget**: Dovecot on maildir caps keywords ~26/mailbox (filename
40
+ letters). Registry stays small and curated; `exclusiveGroup` keeps categories
41
+ mutually exclusive instead of accreting.
42
+ - **Atoms only**: no spaces, ASCII, prefix everything `im_` so the vocabulary
43
+ can't collide with `$Junk`, `$Forwarded`, etc.
44
+ - **Tags, not key-value**: scores get bucketed (`im_spam_high`); anything
45
+ non-enumerable (extracted dates, reasoning text, summaries) does NOT fit.
46
+ If rich values are needed later, a sidecar store comes back *only for that
47
+ residue* — the keyword layer stays as the interoperable spine. (imail's
48
+ `.ics` writer is precedent: rich results can also just be files.)
49
+ - **Registry distribution**: share it the way accounts.jsonc is shared — GDrive
50
+ via API with a local cache, never a file on a network path. Slots into
51
+ rmfmail's existing config-editor/watch machinery; imail (self-contained by
52
+ policy) keeps its own cached copy.
53
+ - **Shared SQLite over SMB is ruled out** (WAL needs shared memory; SMB locking
54
+ corrupts). If a shared rich store is ever needed: a small annotation service
55
+ owning WAL SQLite behind HTTP/MCP (the rmfmail-daemon pattern), or libSQL
56
+ embedded replicas for multi-machine.
57
+
58
+ ## Fit with existing TODO items
59
+
60
+ - **Q113 / Q113-AI** (per-message metadata shape): this design answers the
61
+ cross-app half — message-attached keywords + registry. A local
62
+ `message_meta` table remains a possible rmfmail-internal cache/residue store.
63
+ - **Q103-AI / C39 / P19** (rules engine + screener): `sortAction` in the
64
+ registry is a candidate rule shape; imail experiments feed this directly.
65
+
66
+ ## Open questions for Bob
67
+
68
+ 1. Starter vocabulary — which categories actually earn one of the ~26 slots?
69
+ 2. Does the annotator mirror keywords → Gmail labels / Graph categories from
70
+ day one, or Dovecot-only first (bobma is the hot path)?
71
+ 3. Where does the annotator run — IDLE-driven daemon on new mail (before
72
+ imail's scheduled pass), or a stage inside imail's own loop?
73
+ 4. Should rmfmail render `im_*` keywords as chips in the message list (it
74
+ already syncs them in `flags_json` — display-only change)?
@@ -0,0 +1,78 @@
1
+ // Annotation registry — DRAFT starter vocabulary (see annotation-registry.md).
2
+ // One entry per IMAP keyword the companion annotator may set. MCP-tool-style
3
+ // descriptions: `description` + `whenToApply` are written for an AI annotator
4
+ // the same way a tool description is written for an agent; `sortAction` is
5
+ // read by the sorter; `label` by UIs. Keywords are IMAP atoms: ASCII, no
6
+ // spaces, always prefixed `im_`. Budget: Dovecot/maildir allows ~26 keywords
7
+ // per mailbox — keep this list SMALL and curated.
8
+ {
9
+ "version": 1,
10
+ "keywords": {
11
+ // ── Categories (mutually exclusive — at most one per message) ──
12
+ "im_bills": {
13
+ "label": "Bills & orders",
14
+ "description": "Invoices, receipts, order/shipping confirmations from a vendor the user has transacted with.",
15
+ "whenToApply": "Sender is a merchant/utility/bank AND the body references a specific transaction, amount, order number, or account.",
16
+ "exclusiveGroup": "category",
17
+ "setBy": ["annotator"],
18
+ "sortAction": { "moveTo": "Added/bills and orders" }
19
+ },
20
+ "im_newsletter": {
21
+ "label": "Newsletter",
22
+ "description": "Bulk editorial content the user subscribed to — digests, columns, mailing-list posts that are broadcast rather than personal.",
23
+ "whenToApply": "List-Unsubscribe header present AND content is editorial/broadcast, not transactional and not addressed to the user personally.",
24
+ "exclusiveGroup": "category",
25
+ "setBy": ["annotator"],
26
+ "sortAction": { "moveTo": "Added/newsletters" }
27
+ },
28
+ "im_political": {
29
+ "label": "Political",
30
+ "description": "Campaign fundraising, advocacy, and political-action mail.",
31
+ "whenToApply": "Sender is a campaign/PAC/advocacy org, or body is dominated by donation asks or political calls to action.",
32
+ "exclusiveGroup": "category",
33
+ "setBy": ["annotator"],
34
+ "sortAction": { "moveTo": "Added/political" }
35
+ },
36
+ "im_event": {
37
+ "label": "Event / invite",
38
+ "description": "Invitations and event announcements with a concrete date/time the user could attend.",
39
+ "whenToApply": "Body or attachment carries a specific event with date/time/place (calendar invite, webinar, meetup, talk).",
40
+ "exclusiveGroup": "category",
41
+ "setBy": ["annotator"],
42
+ // Rich residue (the extracted datetime) doesn't fit a keyword —
43
+ // the annotator writes the .ics file, as imail already does.
44
+ "sortAction": { "keep": true }
45
+ },
46
+ "im_personal": {
47
+ "label": "Personal",
48
+ "description": "A human wrote this specifically to the user — correspondence, not broadcast.",
49
+ "whenToApply": "Individual sender, no unsubscribe machinery, content addresses the user or an ongoing thread.",
50
+ "exclusiveGroup": "category",
51
+ "setBy": ["annotator"],
52
+ "sortAction": { "keep": true }
53
+ },
54
+
55
+ // ── Independent signals (combinable) ──
56
+ "im_spam_high": {
57
+ "label": "Likely spam",
58
+ "description": "High-confidence spam beyond what SpamAssassin already headers — bucketed score, since keywords cannot carry values.",
59
+ "whenToApply": "Model confidence ≥ 0.9 that the message is unsolicited bulk/scam. Never apply on mere newsletter-ness.",
60
+ "setBy": ["annotator"],
61
+ "sortAction": { "moveTo": "Spam" }
62
+ },
63
+ "im_action": {
64
+ "label": "Action required",
65
+ "description": "The user is being asked to do something with a real deadline or dependency (reply, pay, RSVP, sign, review).",
66
+ "whenToApply": "Explicit request directed at the user with a deadline or a blocked party waiting.",
67
+ "setBy": ["annotator"],
68
+ "sortAction": { "keep": true }
69
+ },
70
+ "im_screened": {
71
+ "label": "Screened",
72
+ "description": "Bookkeeping: the annotator has processed this message. Lets the sorter and re-runs skip already-annotated mail cheaply.",
73
+ "whenToApply": "Set on every message the annotator completes, regardless of verdict.",
74
+ "setBy": ["annotator"],
75
+ "sortAction": { "keep": true }
76
+ }
77
+ }
78
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/rmfmail",
3
- "version": "1.2.142",
3
+ "version": "1.2.144",
4
4
  "description": "Local-first email client with IMAP sync and standalone native app",
5
5
  "type": "module",
6
6
  "main": "bin/mailx.js",
@@ -40,7 +40,7 @@
40
40
  "@bobfrankston/mailx-store-web": "^0.1.38",
41
41
  "@bobfrankston/mailx-sync": "^0.1.27",
42
42
  "@bobfrankston/miscinfo": "^1.0.13",
43
- "@bobfrankston/msger": "^0.1.394",
43
+ "@bobfrankston/msger": "^0.1.395",
44
44
  "@bobfrankston/node-tcp-transport": "^0.1.10",
45
45
  "@bobfrankston/oauthsupport": "^1.0.34",
46
46
  "@bobfrankston/rmf-tiny": "^0.1.38",
@@ -118,7 +118,7 @@
118
118
  "@bobfrankston/mailx-store-web": "^0.1.38",
119
119
  "@bobfrankston/mailx-sync": "^0.1.27",
120
120
  "@bobfrankston/miscinfo": "^1.0.13",
121
- "@bobfrankston/msger": "^0.1.394",
121
+ "@bobfrankston/msger": "^0.1.395",
122
122
  "@bobfrankston/node-tcp-transport": "^0.1.10",
123
123
  "@bobfrankston/oauthsupport": "^1.0.34",
124
124
  "@bobfrankston/rmf-tiny": "^0.1.38",