@bobfrankston/rmfmail 1.1.53 → 1.1.58
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 +4 -0
- package/client/compose/compose.bundle.js +43 -8
- package/client/compose/compose.bundle.js.map +2 -2
- package/client/compose/compose.css +41 -1
- package/client/compose/compose.js +64 -17
- package/client/compose/compose.js.map +1 -1
- package/client/compose/compose.ts +60 -16
- package/client/styles/components.css +11 -1
- package/package.json +1 -1
- package/packages/mailx-settings/cloud.d.ts.map +1 -1
- package/packages/mailx-settings/cloud.js +22 -4
- package/packages/mailx-settings/cloud.js.map +1 -1
- package/packages/mailx-settings/cloud.ts +21 -4
- /package/packages/mailx-imap/{node_modules.npmglobalize-stash-40524 → node_modules.npmglobalize-stash-20936}/.package-lock.json +0 -0
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.
|
|
@@ -3613,29 +3613,64 @@ function setupAutocomplete(input) {
|
|
|
3613
3613
|
dropdown.className = "ac-dropdown";
|
|
3614
3614
|
activeIndex = 0;
|
|
3615
3615
|
for (let i = 0; i < results.length; i++) {
|
|
3616
|
+
const r = results[i];
|
|
3616
3617
|
const item = document.createElement("div");
|
|
3617
3618
|
item.className = `ac-item${i === 0 ? " ac-active" : ""}`;
|
|
3619
|
+
const main = document.createElement("div");
|
|
3620
|
+
main.className = "ac-item-main";
|
|
3618
3621
|
const nameEl = document.createElement("span");
|
|
3619
3622
|
nameEl.className = "ac-item-name";
|
|
3620
|
-
nameEl.textContent =
|
|
3623
|
+
nameEl.textContent = r.name || r.email;
|
|
3621
3624
|
const emailEl = document.createElement("span");
|
|
3622
3625
|
emailEl.className = "ac-item-email";
|
|
3623
|
-
emailEl.textContent =
|
|
3626
|
+
emailEl.textContent = r.email;
|
|
3624
3627
|
const sourceEl = document.createElement("span");
|
|
3625
3628
|
sourceEl.className = "ac-item-source";
|
|
3626
|
-
sourceEl.textContent =
|
|
3627
|
-
|
|
3628
|
-
if (
|
|
3629
|
-
if (
|
|
3629
|
+
sourceEl.textContent = r.source || "";
|
|
3630
|
+
main.appendChild(nameEl);
|
|
3631
|
+
if (r.name) main.appendChild(emailEl);
|
|
3632
|
+
if (r.source) main.appendChild(sourceEl);
|
|
3633
|
+
const actions = document.createElement("div");
|
|
3634
|
+
actions.className = "ac-item-actions";
|
|
3635
|
+
const mkBtn = (glyph, title, run) => {
|
|
3636
|
+
const b = document.createElement("button");
|
|
3637
|
+
b.type = "button";
|
|
3638
|
+
b.className = "ac-item-btn";
|
|
3639
|
+
b.textContent = glyph;
|
|
3640
|
+
b.title = title;
|
|
3641
|
+
b.addEventListener("mousedown", (e) => {
|
|
3642
|
+
e.preventDefault();
|
|
3643
|
+
e.stopPropagation();
|
|
3644
|
+
});
|
|
3645
|
+
b.addEventListener("click", async (e) => {
|
|
3646
|
+
e.preventDefault();
|
|
3647
|
+
e.stopPropagation();
|
|
3648
|
+
try {
|
|
3649
|
+
await run();
|
|
3650
|
+
} catch (err) {
|
|
3651
|
+
alert(`Failed: ${err?.message || err}`);
|
|
3652
|
+
}
|
|
3653
|
+
closeDropdown();
|
|
3654
|
+
});
|
|
3655
|
+
return b;
|
|
3656
|
+
};
|
|
3657
|
+
actions.appendChild(mkBtn("\u2605", "Prefer this contact", () => addPreferredContact({ name: r.name, email: r.email, source: "", organization: "" })));
|
|
3658
|
+
actions.appendChild(mkBtn("\u2298", "Never suggest this address", () => addToDenylist(r.email)));
|
|
3659
|
+
actions.appendChild(mkBtn("\u2197", "Open in Google Contacts (add, edit, merge aliases)", async () => {
|
|
3660
|
+
window.open(`https://contacts.google.com/search/${encodeURIComponent(r.name || r.email)}`, "_blank");
|
|
3661
|
+
}));
|
|
3662
|
+
item.appendChild(main);
|
|
3663
|
+
item.appendChild(actions);
|
|
3630
3664
|
item.addEventListener("mousedown", (e) => {
|
|
3665
|
+
if (e.button !== 0) return;
|
|
3631
3666
|
e.preventDefault();
|
|
3632
|
-
const display =
|
|
3667
|
+
const display = r.name ? `${r.name} <${r.email}>` : r.email;
|
|
3633
3668
|
replaceLastToken(display);
|
|
3634
3669
|
});
|
|
3635
3670
|
item.addEventListener("contextmenu", (e) => {
|
|
3636
3671
|
e.preventDefault();
|
|
3637
3672
|
e.stopPropagation();
|
|
3638
|
-
showAutocompleteContextMenu(e,
|
|
3673
|
+
showAutocompleteContextMenu(e, r);
|
|
3639
3674
|
});
|
|
3640
3675
|
dropdown.appendChild(item);
|
|
3641
3676
|
}
|