@bobfrankston/rmfmail 1.2.143 → 1.2.145

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.
@@ -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.143",
3
+ "version": "1.2.145",
4
4
  "description": "Local-first email client with IMAP sync and standalone native app",
5
5
  "type": "module",
6
6
  "main": "bin/mailx.js",
@@ -34,13 +34,13 @@
34
34
  "postinstall": "node bin/postinstall.js"
35
35
  },
36
36
  "dependencies": {
37
- "@bobfrankston/iflow-direct": "^0.1.57",
37
+ "@bobfrankston/iflow-direct": "^0.1.58",
38
38
  "@bobfrankston/mailx-host": "^0.1.13",
39
39
  "@bobfrankston/mailx-imap": "^0.1.117",
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.396",
44
44
  "@bobfrankston/node-tcp-transport": "^0.1.10",
45
45
  "@bobfrankston/oauthsupport": "^1.0.34",
46
46
  "@bobfrankston/rmf-tiny": "^0.1.38",
@@ -112,13 +112,13 @@
112
112
  },
113
113
  ".transformedSnapshot": {
114
114
  "dependencies": {
115
- "@bobfrankston/iflow-direct": "^0.1.57",
115
+ "@bobfrankston/iflow-direct": "^0.1.58",
116
116
  "@bobfrankston/mailx-host": "^0.1.13",
117
117
  "@bobfrankston/mailx-imap": "^0.1.117",
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.396",
122
122
  "@bobfrankston/node-tcp-transport": "^0.1.10",
123
123
  "@bobfrankston/oauthsupport": "^1.0.34",
124
124
  "@bobfrankston/rmf-tiny": "^0.1.38",