@bobfrankston/rmfmail 1.1.53 → 1.1.62

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
@@ -248,6 +248,10 @@ Small, self-contained items. Pick them up between higher-priority blocks without
248
248
  When exposing the calendar picker to users, ship a curated allowlist of vetted IDs rather than a free-form text input, OR show a warning when a user enters a non-`.official` / non-proper-noun ID. The naming asymmetry is on Google.
249
249
 
250
250
  Alternative source worth evaluating before generalizing: **Hebcal** (`https://www.hebcal.com/`) publishes a more rigorously curated Jewish-only feed (iCal + JSON API), and similar projects exist for other traditions. A `JsonHolidayProvider` interface that abstracts Google vs Hebcal vs ICS-by-URL behind a common shape would let users plug in whatever feed they trust.
251
+ - **Q150 — `discovered` contacts don't belong in the cloud file — split it.** Bob 2026-05-16 hit a 1.5 MB `contacts.jsonc` (11,034 `discovered` rows) and asked about CSV. CSV is the wrong fix (saves ~half the bytes, loses the preferred/denylist/groups structure). Real issue: `discovered` is a *derived cache* — name/email/useCount/lastUsed rebuildable per-device from the message corpus (`seedContactsFromMessages`) — so it shouldn't be in the cloud-synced JSONC at all. Split: cloud `contacts.jsonc` keeps only user-curated data (`preferred`, `denylist`, `groups`) ≈ a few KB; `discovered` lives in the local DB only, rebuilt from each device's mail. Shrinks the cloud file ~700×, kills the 1.5 MB-blob fragility, and matches the "local store is a cache" rule.
252
+ - **Q151 — AI config window (natural-language config edits).** Bob 2026-05-16: an AI window that understands the config files so he can say "don't show outlook_…@outlook.com". Feasible — mailx already has AI plumbing (aiTransform, provider+key in autocomplete settings) so it reuses the existing key. Design constraint: the AI must NOT free-form-rewrite a JSONC file (a malformed contacts.jsonc wipes everything — just saw it). It proposes a structured operation (`denylist add <email>`, `preferred add …`), shows a diff, the user confirms, and mailx applies it via the existing typed methods (`addToDenylist`, `addPreferredContact`, …). Worth it for open-ended requests ("stop showing anything from that marketing company", "merge these contacts") — for a single denylist it's overkill vs. a direct affordance (Q149).
253
+ - **Q149 — Visible prefer/ignore affordances.** (1) ✅ **Done v1.1.55** — compose autocomplete rows now have visible per-row ★ (prefer → `contacts.jsonc#preferred[]`) and ⊘ (ignore → `denylist[]`) buttons, shown on hover; plain click, no dependence on the flaky right-click. (2) STILL OPEN — "Never use this address" in the message-viewer From/To/Cc right-click menu (currently Copy / Add to contacts / Reply only). (3) Related: route ★ to a Google-Contacts star instead of `preferred[]` once that's decided (see Q151 discussion).
254
+ - **Q148 — "Open in editor" + live file-watch for the JSONC config editor.** Bob 2026-05-16: the Edit-config-file modal's textarea is cramped; he wants a button revealing the source path so he can edit in a full editor (VS Code), with the modal watching the file and reflecting external changes. Local/cloud split: `config.jsonc` is a real local file (`~/.rmfmail/config.jsonc`) — trivial: a "Reveal source" button + `fs.watch` → re-read the textarea on change. The other files (`accounts.jsonc`, `allowlist.jsonc`, `clients.jsonc`, `contacts.jsonc`) live on Google Drive via the Drive API — **no local path exists**. To edit those externally, "check out" a local working copy (e.g. `~/.rmfmail/config-edit/<name>`), reveal that path, `fs.watch` it, and `cloudWrite` back on change. Modal gains: a "Reveal/Open source" button per file + a watcher that updates the textarea (and warns on a dirty-buffer conflict). ImapManager already runs `fs.watch` on the local config files (`watchConfigFiles()` → `configChanged`) — reuse that channel for the local case.
251
255
  - **Q147 — Message list stalls mid-scroll during sync churn — incremental updates + virtualization.** Bob 2026-05-16: "scrolling the list of messages, it seems stuck and finally gets loose." Design defect, not a symptom to patch. Root cause: `renderMessages` rebuilds the whole list (`new MessageRow` per row, ~10 DOM nodes each) and there is **no virtualization** — infinite scroll appends pages so a large folder holds thousands of live nodes. Sync events (and the every-30s calendar/task refresh storm makes them constant) trigger synchronous list re-render and/or `scrollIntoView` re-anchoring on the **main thread the user is scrolling** → scroll input queues until the burst clears. Existing `{scroll:false}` guards (added 2026-05-14 for the same "won't let me scroll" report) patched the symptom; the defect remains. Fix, two parts: (1) **sync events update rows in place** — a changed flag/date/folder mutates that one `MessageRow`'s DOM; never rebuild the list and never touch scroll/focus on a background reconcile; (2) **virtualize** large folders so only viewport-adjacent rows are in the DOM. Continuation of the row-objects-own-behavior direction (related: S56). Touches `message-list.ts` render + the store-subscription/event handlers.
252
256
  - **Q146 — All-day events are floating calendar dates, not instants — model fix.** Bob 2026-05-16. Interim fix shipped v1.1.50: `calendarEventToLocal` parses an all-day `start.date` as *local* midnight instead of UTC (`Date.parse` treats date-only as UTC → a June 14 birthday rendered June 13 in US Eastern). That makes it correct *on the current machine in the current timezone* but the underlying model is still wrong: an all-day event — a birthday especially — is a **floating date** ("June 14", true everywhere), not a timestamp. Storing it as `start_ms` epoch ties a date to a timezone; it'll drift again on travel / timezone change / cross-device view. Proper fix: distinguish **timed events** (an instant — `start_ms`, fine) from **date events** (store the `YYYY-MM-DD` string, no timezone). Day-grouping, the day header, and rendering key off the date directly for date-events; only timed events do epoch math. Reminders on all-day events (Google's "1 day before at 9am") still need a timezone anchor — resolve that against the *local* day. Touches `GCalEvent`/`calendarEventToLocal`, `calendar_events` schema (a date column or an `all_day` + date pair), `calendarRowToObject`, the sidebar `CalEvent` + `fetchUpcoming` + `renderEvents` grouping, and the alarm scheduler. Medium model change — the v1.1.50 parse is a stopgap until it lands.
253
257
  - **Q145 — From header: RFC 2047 encoded-word not decoded when it spans the whole `name <addr>`.** ✅ **Fixed v1.1.49** — `db.upsertMessage` now runs `decodeHeaderWords` over the `address` of from/to/cc, not just the display `name`. A legit address never carries an encoded-word so it's a no-op there; for the spam case the `=?utf-8?q?…=3C…=40…?=` mailbox decodes to readable text. Bob 2026-05-16, `screenshots/Screenshot 2026-05-16 141730.png`, sample `.eml` at `~/.rmfmail/mailxstore/bobma/1f/1fc0be6eb64d43e88503afaa8e424009.eml`. The From renders raw as `=?utf-8?q?PayPal_Nouveaut=C3=A9s_=3CParts=40samisec=2Ecom=3E?=@exch-smtp-out-01.wtr.livemail.co.uk`. The sender packed the entire display-name-and-address — including the `<`, `@`, `.`, `>` as `=3C =40 =2E =3E` — into ONE encoded-word, then concatenated `@domain` with no whitespace. Decoded it is `PayPal Nouveautés <Parts@samisec.com>`. mailx fails to decode it: the encoded-word isn't whitespace-delimited and sits where an addr-spec is expected, so the address parser (envelope `tokenizeParenList` / header decode) skips RFC 2047 decoding. Technically malformed mail, but Thunderbird decodes it. Fix: run encoded-word decoding on From/To/Cc display strings before address splitting, and tolerate a `=?...?=` token adjacent to non-whitespace. The broken string also propagates into the remote-content "Always: …" allowlist suggestions.
@@ -4857,6 +4857,7 @@ async function collectDueAlarms(now) {
4857
4857
  }
4858
4858
  }
4859
4859
  if (pick) {
4860
+ console.log(`[alarm] fire decision key=${pick.key} title="${ev.title || ""}" startMs=${startMs}`);
4860
4861
  items.push({
4861
4862
  uuid: pick.key,
4862
4863
  kind: "calendar",
@@ -5129,6 +5130,7 @@ async function firePopupForItem(item) {
5129
5130
  const m = loadDismissed();
5130
5131
  m[item.uuid] = true;
5131
5132
  saveDismissed(m);
5133
+ console.log(`[alarm] dismiss saved key=${item.uuid}`);
5132
5134
  }
5133
5135
  break;
5134
5136
  case "Delete":