@bobfrankston/rmfmail 1.1.198 → 1.1.200

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
@@ -236,10 +236,12 @@ Previously shown as showstoppers; moved here because they haven't recurred on re
236
236
 
237
237
  Small, self-contained items. Pick them up between higher-priority blocks without asking. Bump version per fix.
238
238
 
239
+ - ~~**Q152 — "Edit as new message" / resend on a sent (or any) message.**~~ **DONE 2026-05-31.** New `editAsNew` compose mode in `client/app.ts` (`openCompose` branch + `editAsNewBody` helper) clones the selected message into a fresh compose: original To/Cc/Subject verbatim, body via `sanitizeQuotedBody` with no quote/forward wrapper, no In-Reply-To/References (new Message-ID at send). Right-click menu entry "Edit as new message" added after Forward in `message-list.ts`. Replaces the move-to-Drafts kludge. *(Original entry below.)* Bob 2026-05-29: no clean way to take an already-sent message, tweak it, and send it again — today's only path is move-to-Drafts-then-edit (a kludge) or Forward (adds `Fwd:` + quote wrapper, wrong recipients). Add an **"Edit as new message"** affordance — in the viewer toolbar (next to Forward) and/or the message-list right-click menu — that loads the selected message's `.eml` **straight into a fresh compose** as the author: original To/Cc/Subject/body editable, **no `Fwd:`/`Re:` prefix, no quote indent, no In-Reply-To/References threading headers**, a new Message-ID. Effectively "duplicate into compose." Distinct from the existing Drafts `Edit & Send` (that edits the draft in place); this clones any message into a new outgoing draft. Reuse the compose-init plumbing the draft-edit path already uses (`showComposeOverlay` + the init payload built in `app.ts`); the only difference is which headers get stripped vs. carried. Most natural as the message-viewer From/To right-click "duplicate"-style action plus a toolbar button gated on non-draft messages. [S]
240
+ - ~~**Preview CSS leak — `<style>`/`<head>` contents bled into the message-list one-liner.**~~ DONE 2026-05-31. `extractPreview` (`mailx-imap/index.ts`) flattened HTML by stripping tags only, leaving the CSS *between* `<style>…</style>` (and `<head>`/`<script>`) in the preview as `*{box-sizing…}` / `@media…{…}` garbage (Bob's marketing-mail shot). Now strips those block contents + HTML comments before the tag-strip; `[image]` marker preserved. IMAP-path only (Gmail uses Google's clean snippet). Existing rows refresh via `rmfmail -repair`.
239
241
  - ~~**Q138 — Quoted-reply image sizes lost.**~~ DONE 2026-05-11 in `client/app.ts:1000` — strip skips `<img>`. Two-pass loop handles multiple stripped attrs per tag.
240
- - **Q140 — User-configurable holiday calendars (generalization).** Today the holiday-pull list is hardcoded to two Google calendars: `en.usa.official#holiday` (federal-secular) and `en.judaism#holiday` (Jewish), each with its own sidebar checkbox + leading symbol (🇺🇸 / ✡). Bob 2026-05-11: "Static calendars are OK. You can add generalization to the maybe-future list. Unlikely to do unless I want a more general audience." When that audience expands, generalize to `settings.calendar.holidayCalendars: Array<{ id: string; label: string; symbol: string; enabled: boolean }>`. The refresh loop iterates the array; the sidebar renders one checkbox per entry plus its symbol on each row; a small editor in Settings calendar lets the user add/remove entries by calendar ID. Schema-side `calendar_id` column already partitions rows by source, so toggling one off doesn't affect the others the existing reconcile logic carries straight over.
242
+ - ~~**Q140 — User-configurable holiday calendars (generalization).**~~ **RETIRED 2026-05-29 already dynamic, superseded by Q143.** Bob: the holiday list is pulled automatically from whatever calendars the user has selected in Google Calendar, so it's dynamic already. Confirmed in code: `refreshCalendarEvents` enumerates `listCalendars().filter(c => c.selected)` and tags each row with its source; the old hardcoded `HOLIDAY_SOURCES` + `showHolidays`/`showJewishHolidays` toggles are retired (`mailx-service/index.ts:1485`). The user curates the set in Google's web UI no in-app holiday-calendar editor needed. The curation-gotcha + Hebcal notes below are kept only as reference IF a free-form calendar-ID *picker* is ever added (not currently plannedselection happens in Google, not mailx).
241
243
 
242
- **Curation gotcha worth carrying into the implementation**: Google's `<locale>.<tradition>#holiday@group.v.calendar.google.com` IDs are NOT all clean per-tradition feeds. The "obvious" IDs are mis-curated catch-alls; the `.official` / proper-noun variant is the actually-clean source for each tradition. Documented examples from 2026-05-11 testing:
244
+ **Curation gotcha (reference only)**: Google's `<locale>.<tradition>#holiday@group.v.calendar.google.com` IDs are NOT all clean per-tradition feeds. The "obvious" IDs are mis-curated catch-alls; the `.official` / proper-noun variant is the actually-clean source for each tradition. Documented examples from 2026-05-11 testing:
243
245
  - `en.usa#holiday` — mixed (federal + Christian + Jewish + Orthodox + Armenian + Ethiopian Jewish + …)
244
246
  - `en.usa.official#holiday` — clean federal-secular
245
247
  - `en.jewish#holiday` — mixed (Jewish + Christian + Orthodox neighbors)
@@ -3983,6 +3983,7 @@ var init_message_list = __esm({
3983
3983
  { label: "Reply", action: () => document.dispatchEvent(new CustomEvent("mailx-compose", { detail: { mode: "reply" } })) },
3984
3984
  { label: "Reply All", action: () => document.dispatchEvent(new CustomEvent("mailx-compose", { detail: { mode: "replyAll" } })) },
3985
3985
  { label: "Forward", action: () => document.dispatchEvent(new CustomEvent("mailx-compose", { detail: { mode: "forward" } })) },
3986
+ { label: "Edit as new message", action: () => document.dispatchEvent(new CustomEvent("mailx-compose", { detail: { mode: "editAsNew" } })) },
3986
3987
  { label: "", action: () => {
3987
3988
  }, separator: true },
3988
3989
  {
@@ -7434,13 +7435,13 @@ document.getElementById("btn-factory-reset")?.addEventListener("click", async ()
7434
7435
  async function openCompose(mode, overrideMsg, overrideAccountId) {
7435
7436
  logClientEvent("openCompose-entry", { mode });
7436
7437
  const current = overrideMsg ? { message: overrideMsg, accountId: overrideAccountId || currentAccountId3 } : getCurrentMessage();
7437
- if ((mode === "reply" || mode === "replyAll" || mode === "forward") && !current) {
7438
+ if ((mode === "reply" || mode === "replyAll" || mode === "forward" || mode === "editAsNew") && !current) {
7438
7439
  console.warn(`[compose] ${mode} \u2014 no message selected`);
7439
7440
  return;
7440
7441
  }
7441
7442
  const accountsP = getAccounts();
7442
7443
  const msg = current?.message;
7443
- const titlePrefix = mode === "reply" ? "Reply" : mode === "replyAll" ? "Reply All" : mode === "forward" ? "Forward" : "Compose";
7444
+ const titlePrefix = mode === "reply" ? "Reply" : mode === "replyAll" ? "Reply All" : mode === "forward" ? "Forward" : mode === "editAsNew" ? "Edit as new" : "Compose";
7444
7445
  const titleSubject = mode === "new" ? "" : msg?.subject || "";
7445
7446
  const frame = showComposeOverlay(titleSubject ? `${titlePrefix}: ${titleSubject}` : titlePrefix);
7446
7447
  const accounts = await accountsP;
@@ -7548,6 +7549,11 @@ async function openCompose(mode, overrideMsg, overrideAccountId) {
7548
7549
  init.subject = `Fwd: ${cleanSubject}`;
7549
7550
  init.bodyHtml = forwardBody(msg);
7550
7551
  init.fromAddress = detectReplyFrom();
7552
+ } else if (msg && mode === "editAsNew") {
7553
+ init.to = Array.isArray(msg.to) ? msg.to : [];
7554
+ init.cc = Array.isArray(msg.cc) ? msg.cc : [];
7555
+ init.subject = msg.subject || "";
7556
+ init.bodyHtml = editAsNewBody(msg);
7551
7557
  }
7552
7558
  const initJson = JSON.stringify(init);
7553
7559
  try {
@@ -7795,6 +7801,9 @@ function quoteBody(msg) {
7795
7801
  const body = sanitizeQuotedBody(msg);
7796
7802
  return `<p></p><br><br><div class="reply"><p>On ${date}, ${from} wrote:</p><blockquote>${body}</blockquote></div>`;
7797
7803
  }
7804
+ function editAsNewBody(msg) {
7805
+ return `<p></p>${sanitizeQuotedBody(msg)}`;
7806
+ }
7798
7807
  function forwardBody(msg) {
7799
7808
  const date = new Date(msg.date).toLocaleString();
7800
7809
  const from = msg.from.name ? `${msg.from.name} &lt;${msg.from.address}&gt;` : msg.from.address;