@bobfrankston/rmfmail 1.1.44 → 1.1.49

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.
Files changed (46) hide show
  1. package/TODO.md +9 -0
  2. package/client/app.bundle.js +403 -83
  3. package/client/app.bundle.js.map +4 -4
  4. package/client/app.js +190 -35
  5. package/client/app.js.map +1 -1
  6. package/client/app.ts +175 -34
  7. package/client/components/calendar-sidebar.js +221 -88
  8. package/client/components/calendar-sidebar.js.map +1 -1
  9. package/client/components/calendar-sidebar.ts +224 -83
  10. package/client/components/message-viewer.js +24 -1
  11. package/client/components/message-viewer.js.map +1 -1
  12. package/client/components/message-viewer.ts +25 -1
  13. package/client/compose/compose.bundle.js +14 -0
  14. package/client/compose/compose.bundle.js.map +2 -2
  15. package/client/compose/spellcheck.js +15 -0
  16. package/client/compose/spellcheck.js.map +1 -1
  17. package/client/compose/spellcheck.ts +14 -0
  18. package/client/help/search-help.js +75 -0
  19. package/client/help/search-help.js.map +1 -0
  20. package/client/help/search-help.ts +75 -0
  21. package/client/index.html +7 -7
  22. package/client/lib/api-client.js +5 -0
  23. package/client/lib/api-client.js.map +1 -1
  24. package/client/lib/api-client.ts +5 -0
  25. package/client/lib/mailxapi.js +1 -0
  26. package/client/styles/components.css +204 -6
  27. package/docs/search.md +5 -1
  28. package/package.json +1 -1
  29. package/packages/mailx-service/google-sync.d.ts +3 -0
  30. package/packages/mailx-service/google-sync.d.ts.map +1 -1
  31. package/packages/mailx-service/google-sync.js +1 -0
  32. package/packages/mailx-service/google-sync.js.map +1 -1
  33. package/packages/mailx-service/google-sync.ts +4 -0
  34. package/packages/mailx-service/index.d.ts +11 -0
  35. package/packages/mailx-service/index.d.ts.map +1 -1
  36. package/packages/mailx-service/index.js +99 -127
  37. package/packages/mailx-service/index.js.map +1 -1
  38. package/packages/mailx-service/index.ts +91 -122
  39. package/packages/mailx-service/jsonrpc.js +2 -0
  40. package/packages/mailx-service/jsonrpc.js.map +1 -1
  41. package/packages/mailx-service/jsonrpc.ts +2 -0
  42. package/packages/mailx-settings/index.d.ts.map +1 -1
  43. package/packages/mailx-settings/index.js +4 -1
  44. package/packages/mailx-settings/index.js.map +1 -1
  45. package/packages/mailx-settings/index.ts +4 -1
  46. /package/packages/mailx-imap/{node_modules.npmglobalize-stash-45524 → node_modules.npmglobalize-stash-43988}/.package-lock.json +0 -0
@@ -42,6 +42,7 @@ __export(api_client_exports, {
42
42
  getAttachment: () => getAttachment,
43
43
  getAutocompleteSettings: () => getAutocompleteSettings,
44
44
  getCalendarEvents: () => getCalendarEvents,
45
+ getCalendars: () => getCalendars,
45
46
  getDeviceAccounts: () => getDeviceAccounts,
46
47
  getDiagnostics: () => getDiagnostics,
47
48
  getFolders: () => getFolders,
@@ -221,6 +222,9 @@ function getPrimaryAccount(feature) {
221
222
  function getCalendarEvents(fromMs, toMs) {
222
223
  return ipc().getCalendarEvents?.(fromMs, toMs) ?? Promise.resolve([]);
223
224
  }
225
+ function getCalendars() {
226
+ return ipc().getCalendars?.() ?? Promise.resolve([]);
227
+ }
224
228
  function createCalendarEvent(ev) {
225
229
  return ipc().createCalendarEvent?.(ev);
226
230
  }
@@ -2151,9 +2155,29 @@ function spawnDesktopPopout(msg, accountId) {
2151
2155
  wrapper.style.top = `${60 + existing * 28}px`;
2152
2156
  wrapper.style.right = `${20 + existing * 28}px`;
2153
2157
  }
2154
- void accountId;
2158
+ const toolbar = document.createElement("div");
2159
+ toolbar.style.cssText = "display:flex;gap:6px;padding:6px 12px;border-bottom:1px solid var(--color-border, #ddd);flex-shrink:0;";
2160
+ const mkPopoutBtn = (label, title, onClick) => {
2161
+ const b = document.createElement("button");
2162
+ b.textContent = label;
2163
+ b.title = title;
2164
+ b.style.cssText = "padding:4px 10px;font-size:13px;cursor:pointer;border:1px solid var(--color-border, #ccc);border-radius:4px;background:var(--color-bg, #fff);color:var(--color-text, #000);";
2165
+ b.addEventListener("click", onClick);
2166
+ return b;
2167
+ };
2168
+ const firePopoutAction = (action) => {
2169
+ document.dispatchEvent(new CustomEvent("mailx-popout-action", { detail: { action, msg, accountId } }));
2170
+ };
2171
+ toolbar.appendChild(mkPopoutBtn("Reply", "Reply", () => firePopoutAction("reply")));
2172
+ toolbar.appendChild(mkPopoutBtn("Reply All", "Reply to all", () => firePopoutAction("replyAll")));
2173
+ toolbar.appendChild(mkPopoutBtn("Forward", "Forward", () => firePopoutAction("forward")));
2174
+ toolbar.appendChild(mkPopoutBtn("Delete", "Delete this message", () => {
2175
+ firePopoutAction("delete");
2176
+ wrapper.remove();
2177
+ }));
2155
2178
  wrapper.appendChild(titleBar);
2156
2179
  wrapper.appendChild(headerInfo);
2180
+ wrapper.appendChild(toolbar);
2157
2181
  wrapper.appendChild(bodyContainer);
2158
2182
  document.body.appendChild(wrapper);
2159
2183
  }
@@ -3773,6 +3797,106 @@ __export(calendar_sidebar_exports, {
3773
3797
  isCalendarSidebarOn: () => isCalendarSidebarOn,
3774
3798
  showCalendarSidebar: () => showCalendarSidebar
3775
3799
  });
3800
+ function calendarKind(id, primary) {
3801
+ if (primary)
3802
+ return "personal";
3803
+ const c = (id || "").toLowerCase();
3804
+ if (c.includes("addressbook#contacts"))
3805
+ return "birthday";
3806
+ if (c.includes("#holiday@") || c.includes("holiday@group")) {
3807
+ if (c.includes("usa"))
3808
+ return "usHoliday";
3809
+ if (c.includes("judaism") || c.includes("jewish"))
3810
+ return "jewishHoliday";
3811
+ return "otherHoliday";
3812
+ }
3813
+ return "other";
3814
+ }
3815
+ function calIconHtml(info) {
3816
+ const kind = calendarKind(info.id, info.primary);
3817
+ const t = escapeHtml4(info.name);
3818
+ if (kind === "personal")
3819
+ return `<span class="cal-ico cal-ico-dot" title="${t}"></span>`;
3820
+ if (kind === "usHoliday")
3821
+ return `<span class="cal-ico cal-ico-emoji" title="${t}">\u{1F1FA}\u{1F1F8}</span>`;
3822
+ if (kind === "jewishHoliday")
3823
+ return `<span class="cal-ico cal-ico-emoji" title="${t}">\u2721\uFE0F</span>`;
3824
+ if (kind === "birthday")
3825
+ return `<span class="cal-ico cal-ico-emoji" title="${t}">\u{1F382}</span>`;
3826
+ if (kind === "otherHoliday")
3827
+ return `<span class="cal-ico cal-ico-emoji" title="${t}">\u2726</span>`;
3828
+ const letter = escapeHtml4((info.name.trim()[0] || "?").toUpperCase());
3829
+ const color = info.color || "#7a7a7a";
3830
+ return `<span class="cal-ico cal-ico-mono" style="background:${escapeHtml4(color)}" title="${t}">${letter}</span>`;
3831
+ }
3832
+ function calInfoFor(calendarId) {
3833
+ const id = calendarId || "primary";
3834
+ const found = calById.get(id);
3835
+ if (found)
3836
+ return found;
3837
+ return { id, name: id.split("@")[0] || id, color: "", primary: id === "primary" };
3838
+ }
3839
+ function isCalHidden(calendarId) {
3840
+ return hiddenCalendars.has(calInfoFor(calendarId).id);
3841
+ }
3842
+ async function loadHiddenCalendars() {
3843
+ try {
3844
+ const s = await getSettings();
3845
+ const arr = s?.calendar?.hiddenCalendars;
3846
+ hiddenCalendars = new Set(Array.isArray(arr) ? arr : []);
3847
+ } catch {
3848
+ hiddenCalendars = /* @__PURE__ */ new Set();
3849
+ }
3850
+ }
3851
+ async function saveHiddenCalendars() {
3852
+ try {
3853
+ const s = await getSettings();
3854
+ s.calendar = { ...s.calendar || {}, hiddenCalendars: [...hiddenCalendars] };
3855
+ await saveSettings(s);
3856
+ } catch (e) {
3857
+ console.error("[cal] save hiddenCalendars failed:", e);
3858
+ }
3859
+ }
3860
+ async function renderCalendarList() {
3861
+ const host = document.getElementById("cal-side-calendars");
3862
+ let list = [];
3863
+ try {
3864
+ list = await getCalendars();
3865
+ } catch {
3866
+ }
3867
+ calendarList = list;
3868
+ calById.clear();
3869
+ for (const c of list) {
3870
+ calById.set(c.id, c);
3871
+ if (c.primary)
3872
+ calById.set("primary", c);
3873
+ }
3874
+ if (host) {
3875
+ if (list.length === 0) {
3876
+ host.innerHTML = "";
3877
+ } else {
3878
+ const sorted = [...list].sort((a, b) => (a.primary ? 0 : 1) - (b.primary ? 0 : 1) || a.name.localeCompare(b.name));
3879
+ host.innerHTML = sorted.map((c) => `<label class="cal-side-cal-row" title="${escapeHtml4(c.name)}">
3880
+ <input type="checkbox" class="cal-side-cal-check" data-cal-id="${escapeHtml4(c.id)}" ${hiddenCalendars.has(c.id) ? "" : "checked"}>
3881
+ ${calIconHtml(c)}
3882
+ <span class="cal-side-cal-name">${escapeHtml4(c.name)}</span>
3883
+ </label>`).join("");
3884
+ host.querySelectorAll(".cal-side-cal-check").forEach((cb) => {
3885
+ cb.addEventListener("change", async () => {
3886
+ const id = cb.dataset.calId || "";
3887
+ if (cb.checked)
3888
+ hiddenCalendars.delete(id);
3889
+ else
3890
+ hiddenCalendars.add(id);
3891
+ await saveHiddenCalendars();
3892
+ renderEvents(lastEvents);
3893
+ });
3894
+ });
3895
+ }
3896
+ }
3897
+ if (lastEvents.length > 0)
3898
+ renderEvents(lastEvents);
3899
+ }
3776
3900
  function getSelectedTaskUuids() {
3777
3901
  return [...selectedTaskUuids];
3778
3902
  }
@@ -3849,11 +3973,26 @@ function renderHead() {
3849
3973
  const d = new Date(viewYear, viewMonth, viewDay);
3850
3974
  dateEl.innerHTML = `<strong>${d.getDate()}</strong> ${d.toLocaleDateString(void 0, { weekday: "short" })} <span class="cal-side-date-month">${d.toLocaleDateString(void 0, { month: "short", year: "numeric" })}</span>`;
3851
3975
  }
3976
+ function overdueTaskRowHtml(t) {
3977
+ let dueLabel = "";
3978
+ if (t.dueMs) {
3979
+ const d = new Date(t.dueMs);
3980
+ const sameYear = d.getFullYear() === (/* @__PURE__ */ new Date()).getFullYear();
3981
+ dueLabel = sameYear ? `${d.getMonth() + 1}/${d.getDate()}` : d.toISOString().slice(0, 10);
3982
+ }
3983
+ return `<div class="cal-side-task cal-side-task-overdue" data-uuid="${escapeHtml4(t.uuid)}">
3984
+ <input type="checkbox" class="cal-side-overdue-check" title="Mark done">
3985
+ <span class="cal-side-task-title">${escapeHtml4(t.title)}</span>
3986
+ <span class="cal-side-task-due overdue">${escapeHtml4(dueLabel)}</span>
3987
+ </div>`;
3988
+ }
3852
3989
  function renderEvents(events) {
3853
3990
  const body = document.getElementById("cal-side-body");
3854
3991
  if (!body)
3855
3992
  return;
3856
- if (events.length === 0) {
3993
+ events = events.filter((e) => !isCalHidden(e.calendarId));
3994
+ const overdue = lastOverdueTasks;
3995
+ if (events.length === 0 && overdue.length === 0) {
3857
3996
  body.innerHTML = `<div class="cal-side-empty">No upcoming events. Click + New event to add one.</div>`;
3858
3997
  return;
3859
3998
  }
@@ -3892,12 +4031,17 @@ function renderEvents(events) {
3892
4031
  dailyHeads.push(instances.reduce((a, b) => a.start < b.start ? a : b));
3893
4032
  }
3894
4033
  let html = "";
4034
+ if (overdue.length > 0) {
4035
+ html += `<div class="cal-side-day cal-side-day-overdue">Overdue tasks</div>`;
4036
+ for (const t of overdue)
4037
+ html += overdueTaskRowHtml(t);
4038
+ }
3895
4039
  if (dailyHeads.length > 0) {
3896
4040
  html += `<div class="cal-side-day cal-side-day-daily">Daily</div>`;
3897
4041
  for (const e of dailyHeads) {
3898
4042
  const link = e.htmlLink || "";
3899
4043
  html += `<div class="cal-side-event" data-id="${e.id}" data-link="${escapeHtml4(link)}" ${link ? 'title="Click to open in Google Calendar"' : ""}>
3900
- <span class="cal-side-event-dot ${e.source === "google" ? "g" : "l"}"></span>
4044
+ ${calIconHtml(calInfoFor(e.calendarId))}
3901
4045
  <span class="cal-side-event-time">${escapeHtml4(formatTime(e))}</span>
3902
4046
  <span class="cal-side-event-title">${escapeHtml4(e.title)}<span class="cal-side-event-recur" title="Daily">\u21BB</span></span>
3903
4047
  </div>`;
@@ -3909,7 +4053,10 @@ function renderEvents(events) {
3909
4053
  for (const e of events) {
3910
4054
  if (e.recurringEventId && dailyKeys.has(e.recurringEventId))
3911
4055
  continue;
3912
- if (e.isHoliday && e.start > holidayCutoff)
4056
+ const info = calInfoFor(e.calendarId);
4057
+ const kind = calendarKind(info.id, info.primary);
4058
+ const isHolidayKind = kind === "usHoliday" || kind === "jewishHoliday" || kind === "otherHoliday";
4059
+ if (isHolidayKind && e.start > holidayCutoff)
3913
4060
  continue;
3914
4061
  const d = new Date(e.start);
3915
4062
  const dayKey = `${d.getFullYear()}-${d.getMonth()}-${d.getDate()}`;
@@ -3919,35 +4066,30 @@ function renderEvents(events) {
3919
4066
  }
3920
4067
  const recurMark = e.recurringEventId ? `<span class="cal-side-event-recur" title="Recurring event">\u21BB</span>` : "";
3921
4068
  const link = e.htmlLink || "";
3922
- let holidayKind;
3923
- if (e.isHoliday) {
3924
- const cid = (e.calendarId || "").toLowerCase();
3925
- if (cid.includes("usa"))
3926
- holidayKind = "us";
3927
- else if (cid.includes("judaism") || cid.includes("jewish"))
3928
- holidayKind = "jewish";
3929
- else
3930
- holidayKind = "other";
3931
- }
3932
- const holidayAttr = e.isHoliday ? ` data-holiday="1" data-holiday-kind="${holidayKind}"` : "";
3933
4069
  const recurAttr = e.recurringEventId ? ' data-recurring="1"' : "";
3934
- const clickable = !e.isHoliday;
3935
- const titleAttr = clickable && link ? 'title="Click to open in Google Calendar"' : "";
3936
- const dataLink = clickable ? `data-link="${escapeHtml4(link)}"` : "";
3937
- if (e.isHoliday) {
3938
- const symbol = holidayKind === "us" ? "\u{1F1FA}\u{1F1F8}" : holidayKind === "jewish" ? "\u2721\uFE0F" : "\u2726";
3939
- html += `<div class="cal-side-event"${holidayAttr} data-id="${e.id}">
3940
- <span class="cal-side-event-title cal-side-event-holiday-title"><span class="cal-side-event-holiday-symbol">${symbol}</span> ${escapeHtml4(e.title)}</span>
4070
+ if (isHolidayKind || kind === "birthday") {
4071
+ html += `<div class="cal-side-event" data-holiday="1" data-holiday-kind="${kind}" data-id="${e.id}">
4072
+ <span class="cal-side-event-title cal-side-event-holiday-title">${calIconHtml(info)} ${escapeHtml4(e.title)}</span>
3941
4073
  </div>`;
3942
4074
  } else {
3943
- html += `<div class="cal-side-event" data-id="${e.id}"${recurAttr} ${dataLink} ${titleAttr}>
3944
- <span class="cal-side-event-dot ${e.source === "google" ? "g" : "l"}"></span>
4075
+ const titleAttr = link ? 'title="Click to open in Google Calendar"' : "";
4076
+ html += `<div class="cal-side-event" data-id="${e.id}"${recurAttr} data-link="${escapeHtml4(link)}" ${titleAttr}>
4077
+ ${calIconHtml(info)}
3945
4078
  <span class="cal-side-event-time">${escapeHtml4(formatTime(e))}</span>
3946
4079
  <span class="cal-side-event-title">${escapeHtml4(e.title)}${recurMark}</span>
3947
4080
  </div>`;
3948
4081
  }
3949
4082
  }
3950
4083
  body.innerHTML = html;
4084
+ body.querySelectorAll(".cal-side-task-overdue").forEach((row) => {
4085
+ const uuid = row.dataset.uuid;
4086
+ row.querySelector(".cal-side-overdue-check")?.addEventListener("change", async () => {
4087
+ await updateTask(uuid, { completedMs: Date.now() });
4088
+ lastOverdueTasks = lastOverdueTasks.filter((t) => t.uuid !== uuid);
4089
+ renderEvents(lastEvents);
4090
+ renderTasks();
4091
+ });
4092
+ });
3951
4093
  const openInCalendar = (url) => {
3952
4094
  const api = window.mailxapi;
3953
4095
  if (api?.openCalendarEvent) {
@@ -3990,10 +4132,10 @@ function renderEvents(events) {
3990
4132
  });
3991
4133
  });
3992
4134
  }
3993
- async function renderTasks() {
4135
+ async function renderTasks(prefetched) {
3994
4136
  const cb = document.getElementById("cal-side-show-done");
3995
4137
  const showDone = cb?.checked ?? false;
3996
- const tasks = await getTasks(showDone);
4138
+ const tasks = prefetched ?? await getTasks(showDone);
3997
4139
  const host = document.getElementById("cal-side-tasks");
3998
4140
  if (!host)
3999
4141
  return;
@@ -4082,15 +4224,25 @@ async function renderTasks() {
4082
4224
  async function refresh() {
4083
4225
  renderHead();
4084
4226
  const from = new Date(viewYear, viewMonth, viewDay);
4227
+ let prefetchedTasks;
4085
4228
  try {
4086
- lastEvents = await fetchUpcoming(from);
4229
+ const startOfToday = /* @__PURE__ */ new Date();
4230
+ startOfToday.setHours(0, 0, 0, 0);
4231
+ const showDone = document.getElementById("cal-side-show-done")?.checked ?? false;
4232
+ const [events, tasks] = await Promise.all([
4233
+ fetchUpcoming(from),
4234
+ getTasks(showDone).catch(() => [])
4235
+ ]);
4236
+ lastEvents = events;
4237
+ prefetchedTasks = tasks;
4238
+ lastOverdueTasks = prefetchedTasks.filter((t) => t.dueMs && !t.completedMs && t.dueMs < startOfToday.getTime()).map((t) => ({ uuid: t.uuid, title: t.title, dueMs: t.dueMs }));
4087
4239
  renderEvents(lastEvents);
4088
4240
  } catch (e) {
4089
4241
  const body = document.getElementById("cal-side-body");
4090
4242
  if (body)
4091
4243
  body.innerHTML = `<div class="cal-side-empty cal-side-quota-error">Couldn't load calendar: ${escapeHtml4(e?.message || String(e))}</div>`;
4092
4244
  }
4093
- renderTasks();
4245
+ renderTasks(prefetchedTasks);
4094
4246
  }
4095
4247
  function openNewEventDialog() {
4096
4248
  const backdrop = document.createElement("div");
@@ -4217,6 +4369,8 @@ async function showCalendarSidebar() {
4217
4369
  localStorage.setItem(SIDEBAR_PREF, "true");
4218
4370
  } catch {
4219
4371
  }
4372
+ await loadHiddenCalendars();
4373
+ void renderCalendarList();
4220
4374
  await refresh();
4221
4375
  }
4222
4376
  function hideCalendarSidebar() {
@@ -4301,7 +4455,7 @@ function initCalendarSidebar() {
4301
4455
  if (btn)
4302
4456
  btn.disabled = true;
4303
4457
  try {
4304
- await refresh();
4458
+ await Promise.all([renderCalendarList(), refresh()]);
4305
4459
  } finally {
4306
4460
  setTimeout(() => {
4307
4461
  btn?.classList.remove("cal-side-refreshing");
@@ -4337,32 +4491,6 @@ function initCalendarSidebar() {
4337
4491
  refresh();
4338
4492
  });
4339
4493
  }
4340
- const wireHolidayCheckbox = (cbId, settingsKey) => {
4341
- const cb = document.getElementById(cbId);
4342
- if (!cb || cb.__wired)
4343
- return;
4344
- cb.__wired = true;
4345
- let userTouched = false;
4346
- getSettings().then((s) => {
4347
- if (userTouched)
4348
- return;
4349
- cb.checked = !!s?.calendar?.[settingsKey];
4350
- }).catch(() => {
4351
- });
4352
- cb.addEventListener("change", async () => {
4353
- userTouched = true;
4354
- try {
4355
- const s = await getSettings();
4356
- s.calendar = { ...s.calendar || {}, [settingsKey]: cb.checked };
4357
- await saveSettings(s);
4358
- } catch (e) {
4359
- console.error(`[cal] ${settingsKey} save failed:`, e);
4360
- }
4361
- refresh();
4362
- });
4363
- };
4364
- wireHolidayCheckbox("cal-side-show-holidays", "showHolidays");
4365
- wireHolidayCheckbox("cal-side-show-jewish-holidays", "showJewishHolidays");
4366
4494
  const horizonInput = document.getElementById("cal-side-horizon");
4367
4495
  if (horizonInput && !horizonInput.__wired) {
4368
4496
  horizonInput.__wired = true;
@@ -4421,7 +4549,7 @@ function initCalendarSidebar() {
4421
4549
  }
4422
4550
  }
4423
4551
  }
4424
- var SIDEBAR_PREF, SHOW_RECURRING_PREF, SHOW_DONE_PREF, HORIZON_DAYS_PREF, HORIZON_DEFAULT_DAYS, HOLIDAY_HORIZON_MS, viewYear, viewMonth, viewDay, lastEvents, selectedTaskUuids;
4552
+ var SIDEBAR_PREF, SHOW_RECURRING_PREF, SHOW_DONE_PREF, HORIZON_DAYS_PREF, HORIZON_DEFAULT_DAYS, HOLIDAY_HORIZON_MS, viewYear, viewMonth, viewDay, lastEvents, lastOverdueTasks, calById, calendarList, hiddenCalendars, selectedTaskUuids;
4425
4553
  var init_calendar_sidebar = __esm({
4426
4554
  "client/components/calendar-sidebar.js"() {
4427
4555
  "use strict";
@@ -4437,6 +4565,10 @@ var init_calendar_sidebar = __esm({
4437
4565
  viewMonth = (/* @__PURE__ */ new Date()).getMonth();
4438
4566
  viewDay = (/* @__PURE__ */ new Date()).getDate();
4439
4567
  lastEvents = [];
4568
+ lastOverdueTasks = [];
4569
+ calById = /* @__PURE__ */ new Map();
4570
+ calendarList = [];
4571
+ hiddenCalendars = /* @__PURE__ */ new Set();
4440
4572
  selectedTaskUuids = /* @__PURE__ */ new Set();
4441
4573
  }
4442
4574
  });
@@ -5073,6 +5205,81 @@ var init_alarms = __esm({
5073
5205
  }
5074
5206
  });
5075
5207
 
5208
+ // client/help/search-help.js
5209
+ var search_help_exports = {};
5210
+ __export(search_help_exports, {
5211
+ SEARCH_HELP_HTML: () => SEARCH_HELP_HTML
5212
+ });
5213
+ var SEARCH_HELP_HTML;
5214
+ var init_search_help = __esm({
5215
+ "client/help/search-help.js"() {
5216
+ "use strict";
5217
+ SEARCH_HELP_HTML = `
5218
+ <p>Search runs in one of three modes depending on the scope you pick. The mode
5219
+ decides which operators work.</p>
5220
+ <ul>
5221
+ <li><strong>All folders</strong> (default) \u2014 searches the <em>local cache</em> with SQLite FTS5.</li>
5222
+ <li><strong>This folder</strong> \u2014 instant client-side filter on the visible rows; full FTS5 search on Enter.</li>
5223
+ <li><strong>Server</strong> (checkbox) \u2014 sends the query to the mail server (IMAP SEARCH; not yet wired for Gmail accounts).</li>
5224
+ </ul>
5225
+
5226
+ <h3>Qualifiers \u2014 work in every mode</h3>
5227
+ <table>
5228
+ <tr><th>Form</th><th>Effect</th></tr>
5229
+ <tr><td><code>from:bob</code></td><td>Sender contains</td></tr>
5230
+ <tr><td><code>to:eleanor</code></td><td>Recipient contains</td></tr>
5231
+ <tr><td><code>subject:lunch</code></td><td>Subject contains</td></tr>
5232
+ <tr><td><code>date:2026-05-01</code></td><td>On that date \u2014 also <code>date:&gt;1w</code>, <code>date:&lt;=2026-01-15</code></td></tr>
5233
+ <tr><td><code>after:1w</code> / <code>before:1m</code></td><td>Newer / older than \u2014 <code>d</code>, <code>w</code>, <code>m</code>, <code>y</code>, a date, <code>today</code>, <code>yesterday</code></td></tr>
5234
+ <tr><td><code>has:attachment</code></td><td>Has an attachment</td></tr>
5235
+ <tr><td><code>is:unread</code></td><td>Also <code>is:flagged</code>, <code>is:read</code>, <code>is:answered</code>, <code>is:draft</code></td></tr>
5236
+ <tr><td><code>folder:sent</code></td><td>Restrict to folders whose name contains the term</td></tr>
5237
+ <tr><td><code>/regex/</code></td><td>Client-side regex over the currently-visible rows. Local only \u2014 never sent to the server.</td></tr>
5238
+ </table>
5239
+ <p>Any remaining unqualified text is the free-text term.</p>
5240
+
5241
+ <h3>Local search (FTS5) \u2014 default</h3>
5242
+ <p>Full-text index over envelopes plus locally-cached bodies. Fast and indexed.</p>
5243
+ <table>
5244
+ <tr><th>Operator</th><th>Example</th><th>Meaning</th></tr>
5245
+ <tr><td>implicit AND</td><td><code>bob lunch</code></td><td>Both terms must appear</td></tr>
5246
+ <tr><td><code>OR</code></td><td><code>bob OR eleanor</code></td><td>Either term</td></tr>
5247
+ <tr><td><code>NOT</code></td><td><code>bob NOT spam</code></td><td>First without the second</td></tr>
5248
+ <tr><td><code>"phrase"</code></td><td><code>"happy birthday"</code></td><td>Exact phrase</td></tr>
5249
+ <tr><td><code>term*</code></td><td><code>lunch*</code></td><td>Prefix</td></tr>
5250
+ <tr><td><code>NEAR(a b, 5)</code></td><td><code>NEAR(bob lunch, 5)</code></td><td>Both within 5 tokens</td></tr>
5251
+ </table>
5252
+ <p>Indexed: subject, from, to, cc, a body snippet, and the full body <em>if</em> it
5253
+ has been downloaded locally (the blue dot in the list). Bodies not yet
5254
+ prefetched aren't searchable until they are.</p>
5255
+
5256
+ <h3>This-folder filter</h3>
5257
+ <p>With scope set to <strong>This folder</strong>, typing filters the rendered rows
5258
+ instantly \u2014 no server hit, no FTS5, just substring match. Press <kbd>Enter</kbd>
5259
+ to escalate to a full FTS5 search of the folder.</p>
5260
+
5261
+ <h3>Server search</h3>
5262
+ <p>Tick <strong>Server</strong> and the query goes to the mail server. On Dovecot
5263
+ (IMAP SEARCH) your <code>from:</code> / <code>to:</code> / <code>subject:</code>
5264
+ qualifiers map to IMAP keys; remaining text becomes a body search. IMAP has no
5265
+ literal <code>AND</code> keyword \u2014 keys are implicitly ANDed. Server search on
5266
+ Gmail accounts is not yet wired and currently returns local results only.</p>
5267
+
5268
+ <h3>Case sensitivity</h3>
5269
+ <p>All three modes \u2014 local FTS5, IMAP server SEARCH, and <code>/regex/</code> \u2014
5270
+ are <strong>case-insensitive</strong>. <code>Lunch</code>, <code>lunch</code>, and
5271
+ <code>LUNCH</code> match the same messages. There is no case-sensitive mode.</p>
5272
+
5273
+ <h3>Limitations</h3>
5274
+ <ul>
5275
+ <li>No regex on the server side, any provider \u2014 <code>/pattern/</code> only filters visible local rows.</li>
5276
+ <li>Server search results are stored locally (envelope-only) on first hit, so a server search also backfills those envelopes.</li>
5277
+ <li>Body search on bodies you haven't prefetched only works server-side; the local index can't search what isn't downloaded.</li>
5278
+ </ul>
5279
+ `;
5280
+ }
5281
+ });
5282
+
5076
5283
  // client/components/folder-tree.js
5077
5284
  init_api_client();
5078
5285
  init_context_menu();
@@ -6519,7 +6726,10 @@ initFolderTree(folderTree, (accountId, folderId, folderName, specialUse) => {
6519
6726
  currentFolderSpecialUse = specialUse;
6520
6727
  currentAccountId3 = accountId;
6521
6728
  currentFolderId2 = folderId;
6522
- if (searchInput) searchInput.value = "";
6729
+ if (searchInput) {
6730
+ searchInput.value = "";
6731
+ updateSearchHighlight();
6732
+ }
6523
6733
  clearSearchMode();
6524
6734
  markAsSeen();
6525
6735
  releaseFocus();
@@ -6530,7 +6740,10 @@ initFolderTree(folderTree, (accountId, folderId, folderName, specialUse) => {
6530
6740
  document.dispatchEvent(new CustomEvent("mailx-folder-changed", { detail: { accountId, folderId } }));
6531
6741
  }, () => {
6532
6742
  currentFolderSpecialUse = "inbox";
6533
- if (searchInput) searchInput.value = "";
6743
+ if (searchInput) {
6744
+ searchInput.value = "";
6745
+ updateSearchHighlight();
6746
+ }
6534
6747
  clearSearchMode();
6535
6748
  releaseFocus();
6536
6749
  loadUnifiedInbox();
@@ -6539,7 +6752,10 @@ initFolderTree(folderTree, (accountId, folderId, folderName, specialUse) => {
6539
6752
  setActiveView({ kind: "unified" }, "All Inboxes");
6540
6753
  });
6541
6754
  function applyTabView(tab) {
6542
- if (searchInput) searchInput.value = "";
6755
+ if (searchInput) {
6756
+ searchInput.value = "";
6757
+ updateSearchHighlight();
6758
+ }
6543
6759
  clearSearchMode();
6544
6760
  releaseFocus();
6545
6761
  const v = tab.view;
@@ -6556,7 +6772,10 @@ function applyTabView(tab) {
6556
6772
  setTitle(`${APP_NAME} - ${tab.title}`);
6557
6773
  setNarrowFolderTitle(tab.title);
6558
6774
  } else {
6559
- if (searchInput) searchInput.value = v.query;
6775
+ if (searchInput) {
6776
+ searchInput.value = v.query;
6777
+ updateSearchHighlight();
6778
+ }
6560
6779
  loadSearchResults(v.query, v.scope, v.accountId, v.folderId, v.includeTrash);
6561
6780
  setTitle(`${APP_NAME} - Search`);
6562
6781
  setNarrowFolderTitle(`Search: ${v.query}`);
@@ -6768,9 +6987,9 @@ document.getElementById("btn-factory-reset")?.addEventListener("click", async ()
6768
6987
  location.reload();
6769
6988
  }
6770
6989
  });
6771
- async function openCompose(mode) {
6990
+ async function openCompose(mode, overrideMsg, overrideAccountId) {
6772
6991
  logClientEvent("openCompose-entry", { mode });
6773
- const current = getCurrentMessage();
6992
+ const current = overrideMsg ? { message: overrideMsg, accountId: overrideAccountId || currentAccountId3 } : getCurrentMessage();
6774
6993
  if ((mode === "reply" || mode === "replyAll" || mode === "forward") && !current) {
6775
6994
  console.warn(`[compose] ${mode} \u2014 no message selected`);
6776
6995
  return;
@@ -7546,6 +7765,64 @@ function recordSearchHistory(query) {
7546
7765
  refreshSearchHistoryDatalist();
7547
7766
  }
7548
7767
  refreshSearchHistoryDatalist();
7768
+ var searchHl = document.getElementById("search-hl");
7769
+ var SEARCH_QUALIFIERS = /* @__PURE__ */ new Set(["from", "to", "subject", "date", "after", "before", "has", "is", "folder"]);
7770
+ var SEARCH_REGEX_SETTLE_MS = 500;
7771
+ var searchRegexSettleTimer = null;
7772
+ function escapeHl(s) {
7773
+ return s.replace(/[&<>]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;" })[c] || c);
7774
+ }
7775
+ function renderSearchHighlight(settled) {
7776
+ if (!searchHl || !searchInput) return;
7777
+ const text = searchInput.value;
7778
+ const tokens = text.match(/\s+|"[^"]*"?|\S+/g) || [];
7779
+ let html = "";
7780
+ for (const tok of tokens) {
7781
+ if (/^\s+$/.test(tok)) {
7782
+ html += escapeHl(tok);
7783
+ continue;
7784
+ }
7785
+ if (tok.startsWith("/") && tok.length > 1) {
7786
+ let bad = false;
7787
+ if (tok.endsWith("/")) {
7788
+ try {
7789
+ new RegExp(tok.slice(1, -1), "i");
7790
+ } catch {
7791
+ bad = true;
7792
+ }
7793
+ }
7794
+ const cls = bad && settled ? "sh-regex-bad" : "sh-regex";
7795
+ html += `<span class="${cls}">${escapeHl(tok)}</span>`;
7796
+ continue;
7797
+ }
7798
+ const qm = tok.match(/^([a-z]+):(.*)$/i);
7799
+ if (qm && SEARCH_QUALIFIERS.has(qm[1].toLowerCase())) {
7800
+ html += `<span class="sh-key">${escapeHl(qm[1] + ":")}</span>${escapeHl(qm[2])}`;
7801
+ continue;
7802
+ }
7803
+ if (/^(OR|NOT|AND)$/.test(tok)) {
7804
+ html += `<span class="sh-op">${escapeHl(tok)}</span>`;
7805
+ continue;
7806
+ }
7807
+ if (tok.startsWith('"')) {
7808
+ html += `<span class="sh-phrase">${escapeHl(tok)}</span>`;
7809
+ continue;
7810
+ }
7811
+ html += escapeHl(tok);
7812
+ }
7813
+ searchHl.innerHTML = html;
7814
+ searchHl.scrollLeft = searchInput.scrollLeft;
7815
+ }
7816
+ function updateSearchHighlight() {
7817
+ renderSearchHighlight(false);
7818
+ if (searchRegexSettleTimer) clearTimeout(searchRegexSettleTimer);
7819
+ searchRegexSettleTimer = setTimeout(() => renderSearchHighlight(true), SEARCH_REGEX_SETTLE_MS);
7820
+ }
7821
+ searchInput?.addEventListener("scroll", () => {
7822
+ if (searchHl) searchHl.scrollLeft = searchInput.scrollLeft;
7823
+ });
7824
+ searchInput?.addEventListener("change", () => updateSearchHighlight());
7825
+ updateSearchHighlight();
7549
7826
  function doSearch(immediate = false) {
7550
7827
  const query = searchInput.value.trim();
7551
7828
  if (query.length === 0) {
@@ -7582,6 +7859,7 @@ var currentFolderId2 = 0;
7582
7859
  var reloadDebounceTimer = null;
7583
7860
  searchInput?.addEventListener("input", () => {
7584
7861
  clearTimeout(searchTimeout);
7862
+ updateSearchHighlight();
7585
7863
  if (searchInput.value.trim() === "") {
7586
7864
  clearSearchMode();
7587
7865
  const body = document.getElementById("ml-body");
@@ -7599,6 +7877,7 @@ searchInput?.addEventListener("keydown", (e) => {
7599
7877
  }
7600
7878
  if (e.key === "Escape") {
7601
7879
  searchInput.value = "";
7880
+ updateSearchHighlight();
7602
7881
  clearSearchMode();
7603
7882
  const body = document.getElementById("ml-body");
7604
7883
  if (body) body.querySelectorAll(".filter-hidden").forEach((r) => r.classList.remove("filter-hidden"));
@@ -8757,34 +9036,66 @@ optSnippet?.addEventListener("change", () => {
8757
9036
  localStorage.setItem("mailx-snippet", String(optSnippet.checked));
8758
9037
  });
8759
9038
  document.getElementById("search-help")?.addEventListener("click", async () => {
8760
- const { readConfigHelp: readConfigHelp2 } = await Promise.resolve().then(() => (init_api_client(), api_client_exports));
8761
- const r = await readConfigHelp2("search").catch(() => ({ content: "" }));
8762
- const md = r?.content || "";
8763
- const backdrop = document.createElement("div");
8764
- backdrop.className = "mailx-modal-backdrop";
8765
- backdrop.style.cssText = "position:fixed;inset:0;z-index:2000;background:rgba(0,0,0,0.4);display:flex;align-items:center;justify-content:center;";
9039
+ const btn = document.getElementById("search-help");
9040
+ if (!btn) return;
9041
+ const existing = document.getElementById("search-help-panel");
9042
+ if (existing) {
9043
+ existing.remove();
9044
+ btn.setAttribute("aria-expanded", "false");
9045
+ return;
9046
+ }
9047
+ const { SEARCH_HELP_HTML: SEARCH_HELP_HTML2 } = await Promise.resolve().then(() => (init_search_help(), search_help_exports));
9048
+ const searchBar = document.querySelector("search.ml-search");
8766
9049
  const panel = document.createElement("div");
8767
- panel.style.cssText = "background:var(--color-bg,#fff);color:var(--color-text,#000);border-radius:8px;box-shadow:0 8px 32px rgba(0,0,0,0.3);width:min(820px,92vw);max-height:85vh;display:flex;flex-direction:column;";
9050
+ panel.id = "search-help-panel";
9051
+ panel.className = "search-help-panel";
8768
9052
  panel.innerHTML = `
8769
- <div style="padding:14px 18px;border-bottom:1px solid var(--color-border,#ddd);display:flex;justify-content:space-between;align-items:center">
8770
- <span style="font-weight:600">Search syntax</span>
8771
- <button type="button" id="search-help-close" style="background:none;border:0;font-size:18px;cursor:pointer">&times;</button>
9053
+ <div class="search-help-head">
9054
+ <span>Search syntax</span>
9055
+ <button type="button" id="search-help-close" title="Close (Esc)" aria-label="Close">&times;</button>
8772
9056
  </div>
8773
- <pre style="margin:0;padding:14px 18px;overflow:auto;font:13px/1.5 var(--font-ui);white-space:pre-wrap;word-wrap:break-word">${(md || "Help not available \u2014 check that docs/search.md is deployed.").replace(/[&<>]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;" })[c] || c)}</pre>
9057
+ <div class="search-help-body">${SEARCH_HELP_HTML2}</div>
8774
9058
  `;
8775
- backdrop.appendChild(panel);
8776
- document.body.appendChild(backdrop);
8777
- const close = () => backdrop.remove();
8778
- panel.querySelector("#search-help-close")?.addEventListener("click", close);
8779
- backdrop.addEventListener("click", (e) => {
8780
- if (e.target === backdrop) close();
8781
- });
8782
- document.addEventListener("keydown", function escClose(e) {
9059
+ document.body.appendChild(panel);
9060
+ btn.setAttribute("aria-expanded", "true");
9061
+ const position = () => {
9062
+ const anchor = searchBar || btn;
9063
+ const rect = anchor.getBoundingClientRect();
9064
+ const margin = 8;
9065
+ const top = rect.bottom + 2;
9066
+ const width = Math.min(640, Math.max(320, rect.width));
9067
+ let left = rect.left;
9068
+ if (left + width > window.innerWidth - margin) left = window.innerWidth - margin - width;
9069
+ if (left < margin) left = margin;
9070
+ panel.style.top = `${top}px`;
9071
+ panel.style.left = `${left}px`;
9072
+ panel.style.width = `${width}px`;
9073
+ panel.style.maxHeight = `${Math.max(160, window.innerHeight - top - margin)}px`;
9074
+ };
9075
+ position();
9076
+ const close = () => {
9077
+ panel.remove();
9078
+ btn.setAttribute("aria-expanded", "false");
9079
+ window.removeEventListener("resize", position);
9080
+ document.removeEventListener("keydown", onKey, true);
9081
+ document.removeEventListener("mousedown", onOutside);
9082
+ };
9083
+ const onKey = (e) => {
8783
9084
  if (e.key === "Escape") {
9085
+ e.preventDefault();
9086
+ e.stopPropagation();
8784
9087
  close();
8785
- document.removeEventListener("keydown", escClose);
8786
9088
  }
8787
- });
9089
+ };
9090
+ const onOutside = (e) => {
9091
+ const t = e.target;
9092
+ if (panel.contains(t) || btn.contains(t) || searchBar?.contains(t)) return;
9093
+ close();
9094
+ };
9095
+ panel.querySelector("#search-help-close")?.addEventListener("click", close);
9096
+ window.addEventListener("resize", position);
9097
+ document.addEventListener("keydown", onKey, true);
9098
+ document.addEventListener("mousedown", onOutside);
8788
9099
  });
8789
9100
  document.getElementById("btn-edit-jsonc")?.addEventListener("click", async () => {
8790
9101
  const settingsDropdown2 = document.getElementById("settings-dropdown");
@@ -9661,6 +9972,15 @@ function renderDiagnosticsBadge(snapshot) {
9661
9972
 
9662
9973
  ${detail}`;
9663
9974
  }
9975
+ document.addEventListener("mailx-popout-action", ((e) => {
9976
+ const { action, msg, accountId } = e.detail || {};
9977
+ if (!msg) return;
9978
+ if (action === "reply" || action === "replyAll" || action === "forward") {
9979
+ openCompose(action, msg, accountId);
9980
+ } else if (action === "delete") {
9981
+ deleteMessage(accountId, msg.uid).catch((err) => console.error(`[popout] delete failed: ${err?.message || err}`));
9982
+ }
9983
+ }));
9664
9984
  document.addEventListener("mailx-popout-message", (async (e) => {
9665
9985
  const { accountId, uid, folderId, subject } = e.detail || {};
9666
9986
  if (!accountId || !uid) return;