@bobfrankston/rmfmail 1.2.175 → 1.2.178
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 +2 -2
- package/client/android-bootstrap.bundle.js +112 -0
- package/client/android-bootstrap.bundle.js.map +3 -3
- package/client/app.bundle.js +79 -16
- package/client/app.bundle.js.map +3 -3
- package/client/components/alarms.js +107 -20
- package/client/components/alarms.js.map +1 -1
- package/client/components/alarms.ts +104 -20
- package/client/compose/compose.bundle.js +4 -0
- package/client/compose/compose.bundle.js.map +2 -2
- package/client/lib/api-client.js +6 -0
- package/client/lib/api-client.js.map +1 -1
- package/client/lib/api-client.ts +9 -0
- package/client/lib/mailxapi.js +27 -5
- package/package.json +7 -7
- package/packages/mailx-imap/package-lock.json +2 -2
- package/packages/mailx-imap/package.json +1 -1
- package/packages/mailx-service/index.d.ts +5 -0
- package/packages/mailx-service/index.d.ts.map +1 -1
- package/packages/mailx-service/index.js +22 -1
- package/packages/mailx-service/index.js.map +1 -1
- package/packages/mailx-service/index.ts +25 -1
- package/packages/mailx-service/jsonrpc.js +4 -0
- package/packages/mailx-service/jsonrpc.js.map +1 -1
- package/packages/mailx-service/jsonrpc.ts +4 -0
- package/packages/mailx-service/package.json +1 -1
- package/packages/mailx-settings/index.d.ts +8 -1
- package/packages/mailx-settings/index.d.ts.map +1 -1
- package/packages/mailx-settings/index.js +71 -0
- package/packages/mailx-settings/index.js.map +1 -1
- package/packages/mailx-settings/index.ts +61 -1
- package/packages/mailx-settings/package.json +1 -1
- package/packages/mailx-store/package.json +1 -1
- package/packages/mailx-store-web/android-bootstrap.js +4 -0
- package/packages/mailx-store-web/android-bootstrap.js.map +1 -1
- package/packages/mailx-store-web/android-bootstrap.ts +4 -0
- package/packages/mailx-store-web/package.json +1 -1
- package/packages/mailx-store-web/web-service.d.ts +4 -0
- package/packages/mailx-store-web/web-service.d.ts.map +1 -1
- package/packages/mailx-store-web/web-service.js +48 -2
- package/packages/mailx-store-web/web-service.js.map +1 -1
- package/packages/mailx-store-web/web-service.ts +41 -2
- package/packages/mailx-types/index.d.ts +2 -0
- package/packages/mailx-types/index.d.ts.map +1 -1
- package/packages/mailx-types/index.js +3 -0
- package/packages/mailx-types/index.js.map +1 -1
- package/packages/mailx-types/index.ts +7 -0
- package/packages/mailx-types/mailx-api.d.ts +14 -0
- package/packages/mailx-types/mailx-api.d.ts.map +1 -1
- package/packages/mailx-types/mailx-api.ts +5 -0
- package/packages/mailx-types/package.json +1 -1
- package/packages/mailx-types/reminder-state.d.ts +42 -0
- package/packages/mailx-types/reminder-state.d.ts.map +1 -0
- package/packages/mailx-types/reminder-state.js +85 -0
- package/packages/mailx-types/reminder-state.js.map +1 -0
- package/packages/mailx-types/reminder-state.ts +88 -0
- /package/packages/mailx-imap/{node_modules.npmglobalize-stash-33656 → node_modules.npmglobalize-stash-7544}/.package-lock.json +0 -0
package/TODO.md
CHANGED
|
@@ -219,7 +219,7 @@ Items blocked on a design decision. Short-form question here; full context in th
|
|
|
219
219
|
| **C41** | PARTIAL | Reply quoting — already wraps in `<blockquote>`; read-only + non-editable-above-the-line still owed (needs Quill module work). |
|
|
220
220
|
| **C43** | OPEN | Message list column controls + threading flags. ✓ (sort-by-column-click is small) |
|
|
221
221
|
| **C44** | DONE (v1.2.105) | Real OS popup windows / pop-out — floating-overlay popout landed (Q64 v1.0.344); v1.2.105 unblocked the real OS window: 🗗 asks the daemon, which spawns a native msger window (`showMessageBoxEx({url})`) pointed at the loopback popout server. Toolbar actions POST back → `popoutAction` event → main window opens editable compose / flags / deletes (delete also closes the window). Browser-window option deferred (same URL, just `openExternal` it). |
|
|
222
|
-
| [**C46**](#ext46) | PARTIAL | Mailto handler split out as [P115](#ext115) (registry-only, not blocked). Share target SHIPPED v1.2.
|
|
222
|
+
| [**C46**](#ext46) | PARTIAL | Mailto handler split out as [P115](#ext115) (registry-only, not blocked). Share target SHIPPED v1.2.175 (`-register-share`, loose AppX — no MSIX needed). Remaining (tray, notifications) still blocked on MSIX packaging. |
|
|
223
223
|
| **C127** | OPTIONAL | **Push relay for calendar / mail / tasks** — design at [`docs/push-relay.md`](push-relay.md). New `MailApps/relayer/` package, alerter-shape (single Node Express+ws process, deploy alongside other small services). Generic API: client POSTs `/subscribe` with upstream + params, relay registers the upstream `events.watch` and returns an SSE/WS endpoint the client holds open. Per-upstream adapter files (`google-calendar.ts`, `google-gmail.ts`, `ms-graph-events.ts`); fan-out + replay buffer + subscription store generic. Client side: `pushRelayUrl` + credentials in settings, falls back to syncToken polling when relay unreachable. **Build only when polling latency demonstrably matters** — for typical use, 5–10 s syncToken cadence is functionally instant. Push is a freshness boost, not a correctness fix. |
|
|
224
224
|
| **C128** | OPEN | **Body pre-parse on prefetch** — first click on any unviewed message pays `simpleParser` + `sanitizeHtml` cost (~100–500 ms on rich HTML). The parsedCache (added 2026-05-09) eliminates 2nd-and-later view costs but not the first. Fix: extend prefetch to ALSO parse + sanitize the body during background fetch and store the result alongside the .eml (e.g., `<uuid>.parsed.json` next to `<uuid>.eml`, or a `body_html_cached` column). Click-time path reads pre-parsed JSON, no parser invocation. Touches `mailx-imap/index.ts` prefetch path + `mailx-store/file-store.ts` for the parsed-blob storage. |
|
|
225
225
|
| **C129** | OPEN | **Android prod/dev split via filename/URL channel** — User 2026-05-09 (revised): no separate MAUI project. Same source tree builds; output filenames distinguish channels: Phase 1 (start now) writes `rmfmaildev.apk` alongside today's `mailx-maui.apk`. Phase 2 (when a release is declared) switches to `rmfmail-<version>.apk` versioned + `rmfmaildev.apk` for latest, with promotion = re-pointing `rmfmail.apk` at a chosen version. Single APK contents per build; channel is runtime setting in AppUpdater (Settings toggle for which URL to poll). Side-by-side install on one device NOT supported by this design (single Android package id) — that's a separate need. See `docs/prod-android.md` for full plan. Phase 1 ~30 min; full flow ~2.5 hours when activated. |
|
|
@@ -488,7 +488,7 @@ Most likely cause based on the bobma folder layout: Dovecot designates "Deleted
|
|
|
488
488
|
### C46 — Windows OS integration (PARTIAL) [↑ summary](#sum)
|
|
489
489
|
|
|
490
490
|
- [ ] **mailto: handler** → see [P115](#ext115) — split out as standalone priority item, NOT blocked on MSIX (registry-only).
|
|
491
|
-
- [x] **Windows Share target** (v1.2.
|
|
491
|
+
- [x] **Windows Share target** (v1.2.175) — no MSIX signing needed after all: loose AppX deployment (`Add-AppxPackage -Register`, unsigned, needs Developer Mode). `rmfmail -register-share` stages `%LOCALAPPDATA%\rmfmail\share-target\` (rmfshare.exe + AppxManifest with `windows.shareTarget` + logos scaled from icon.png). `bin/rmfshare-src/` is a C# NativeAOT helper (3.8 MB, checked-in like rmfmailto.exe): ShareTarget activation → WinRT DataPackageView → files copied to `~/.rmfmail/share-staging/` → `pending-share.json` → daemon fs.watch base64s files and pushes `openShare` → compose opens with attachments/link/text. Startup-race twin `consumePendingShare` IPC, same shape as P115 mailto. Machines without Developer Mode get a clear message; Settings-UI toggle still owed (lands with P116 Settings rework).
|
|
492
492
|
- [ ] Native right-click context menus, system tray, file associations, notifications (tray + notifications still MSIX-blocked)
|
|
493
493
|
|
|
494
494
|
<a id="ext115"></a>
|
|
@@ -2328,10 +2328,69 @@ var init_groups = __esm({
|
|
|
2328
2328
|
}
|
|
2329
2329
|
});
|
|
2330
2330
|
|
|
2331
|
+
// packages/mailx-types/reminder-state.js
|
|
2332
|
+
function normalizeReminderState(raw, now) {
|
|
2333
|
+
const out = { dismissed: {}, snoozed: {} };
|
|
2334
|
+
if (raw && typeof raw === "object") {
|
|
2335
|
+
for (const [k, v] of Object.entries(raw.dismissed || {})) {
|
|
2336
|
+
if (v === true)
|
|
2337
|
+
out.dismissed[k] = now;
|
|
2338
|
+
else if (Number.isFinite(v) && v > 0)
|
|
2339
|
+
out.dismissed[k] = v;
|
|
2340
|
+
}
|
|
2341
|
+
for (const [k, v] of Object.entries(raw.snoozed || {})) {
|
|
2342
|
+
if (Number.isFinite(v) && v > 0)
|
|
2343
|
+
out.snoozed[k] = v;
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
2346
|
+
return out;
|
|
2347
|
+
}
|
|
2348
|
+
function mergeReminderStates(a, b, now) {
|
|
2349
|
+
const out = { dismissed: {}, snoozed: {} };
|
|
2350
|
+
const dismissedCutoff = now - DISMISSED_RETENTION_MS;
|
|
2351
|
+
const snoozedCutoff = now - SNOOZED_RETENTION_MS;
|
|
2352
|
+
for (const src of [a.dismissed, b.dismissed]) {
|
|
2353
|
+
for (const [k, v] of Object.entries(src)) {
|
|
2354
|
+
if (v < dismissedCutoff)
|
|
2355
|
+
continue;
|
|
2356
|
+
if (!(k in out.dismissed) || v > out.dismissed[k])
|
|
2357
|
+
out.dismissed[k] = v;
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2360
|
+
for (const src of [a.snoozed, b.snoozed]) {
|
|
2361
|
+
for (const [k, v] of Object.entries(src)) {
|
|
2362
|
+
if (v < snoozedCutoff)
|
|
2363
|
+
continue;
|
|
2364
|
+
if (!(k in out.snoozed) || v > out.snoozed[k])
|
|
2365
|
+
out.snoozed[k] = v;
|
|
2366
|
+
}
|
|
2367
|
+
}
|
|
2368
|
+
return out;
|
|
2369
|
+
}
|
|
2370
|
+
function reminderStatesEqual(a, b) {
|
|
2371
|
+
const mapsEqual = (x, y) => {
|
|
2372
|
+
const xk = Object.keys(x);
|
|
2373
|
+
if (xk.length !== Object.keys(y).length)
|
|
2374
|
+
return false;
|
|
2375
|
+
return xk.every((k) => y[k] === x[k]);
|
|
2376
|
+
};
|
|
2377
|
+
return mapsEqual(a.dismissed, b.dismissed) && mapsEqual(a.snoozed, b.snoozed);
|
|
2378
|
+
}
|
|
2379
|
+
var REMINDER_STATE_FILE, DISMISSED_RETENTION_MS, SNOOZED_RETENTION_MS;
|
|
2380
|
+
var init_reminder_state = __esm({
|
|
2381
|
+
"packages/mailx-types/reminder-state.js"() {
|
|
2382
|
+
"use strict";
|
|
2383
|
+
REMINDER_STATE_FILE = "reminders.jsonc";
|
|
2384
|
+
DISMISSED_RETENTION_MS = 30 * 864e5;
|
|
2385
|
+
SNOOZED_RETENTION_MS = 7 * 864e5;
|
|
2386
|
+
}
|
|
2387
|
+
});
|
|
2388
|
+
|
|
2331
2389
|
// packages/mailx-types/index.js
|
|
2332
2390
|
var mailx_types_exports = {};
|
|
2333
2391
|
__export(mailx_types_exports, {
|
|
2334
2392
|
CONTACT_RULES: () => CONTACT_RULES,
|
|
2393
|
+
REMINDER_STATE_FILE: () => REMINDER_STATE_FILE,
|
|
2335
2394
|
addContactsDenylistEntry: () => addContactsDenylistEntry,
|
|
2336
2395
|
addContactsPreferredEntry: () => addContactsPreferredEntry,
|
|
2337
2396
|
answeredOf: () => answeredOf,
|
|
@@ -2343,7 +2402,10 @@ __export(mailx_types_exports, {
|
|
|
2343
2402
|
flaggedOf: () => flaggedOf,
|
|
2344
2403
|
htmlToPlainText: () => htmlToPlainText,
|
|
2345
2404
|
isAddressToken: () => isAddressToken,
|
|
2405
|
+
mergeReminderStates: () => mergeReminderStates,
|
|
2406
|
+
normalizeReminderState: () => normalizeReminderState,
|
|
2346
2407
|
parseSearchQuery: () => parseSearchQuery,
|
|
2408
|
+
reminderStatesEqual: () => reminderStatesEqual,
|
|
2347
2409
|
sanitizeHtml: () => sanitizeHtml,
|
|
2348
2410
|
seenOf: () => seenOf,
|
|
2349
2411
|
setAnswered: () => setAnswered,
|
|
@@ -2544,6 +2606,7 @@ var init_mailx_types = __esm({
|
|
|
2544
2606
|
init_contact_rules();
|
|
2545
2607
|
init_contacts_config();
|
|
2546
2608
|
init_groups();
|
|
2609
|
+
init_reminder_state();
|
|
2547
2610
|
_SEEN = "\\Seen";
|
|
2548
2611
|
_FLAGGED = "\\Flagged";
|
|
2549
2612
|
_ANSWERED = "\\Answered";
|
|
@@ -6640,6 +6703,51 @@ ${bodyEncoded}`;
|
|
|
6640
6703
|
async deleteTask(_uuid) {
|
|
6641
6704
|
return this.notImpl("deleteTask");
|
|
6642
6705
|
}
|
|
6706
|
+
// Reminder dismissed/snoozed state — same reminders.jsonc on the shared
|
|
6707
|
+
// Drive folder the desktop uses, same mailx-types merge. Alarms don't
|
|
6708
|
+
// fire on Android today (getCalendarEvents/getTasks return []), but the
|
|
6709
|
+
// API is live so this device still contributes its state and adopts the
|
|
6710
|
+
// desktops' — the moment calendar lands here, sync just works.
|
|
6711
|
+
reminderStateCache = null;
|
|
6712
|
+
async getReminderState() {
|
|
6713
|
+
if (this.reminderStateCache && Date.now() - this.reminderStateCache.at < 6e4) {
|
|
6714
|
+
return this.reminderStateCache.state;
|
|
6715
|
+
}
|
|
6716
|
+
const now = Date.now();
|
|
6717
|
+
let state = { dismissed: {}, snoozed: {} };
|
|
6718
|
+
try {
|
|
6719
|
+
const json = await cloudRead(REMINDER_STATE_FILE);
|
|
6720
|
+
if (json != null)
|
|
6721
|
+
state = normalizeReminderState(JSON.parse(json), now);
|
|
6722
|
+
} catch {
|
|
6723
|
+
}
|
|
6724
|
+
this.reminderStateCache = { state, at: Date.now() };
|
|
6725
|
+
return state;
|
|
6726
|
+
}
|
|
6727
|
+
async mergeReminderState(patch) {
|
|
6728
|
+
const now = Date.now();
|
|
6729
|
+
const local = normalizeReminderState(patch, now);
|
|
6730
|
+
let cloud = { dismissed: {}, snoozed: {} };
|
|
6731
|
+
let hadCloud = false;
|
|
6732
|
+
try {
|
|
6733
|
+
const json = await cloudRead(REMINDER_STATE_FILE);
|
|
6734
|
+
if (json != null) {
|
|
6735
|
+
cloud = normalizeReminderState(JSON.parse(json), now);
|
|
6736
|
+
hadCloud = true;
|
|
6737
|
+
}
|
|
6738
|
+
} catch {
|
|
6739
|
+
}
|
|
6740
|
+
const merged = mergeReminderStates(cloud, local, now);
|
|
6741
|
+
if (!(hadCloud && reminderStatesEqual(merged, cloud))) {
|
|
6742
|
+
try {
|
|
6743
|
+
await cloudWrite(REMINDER_STATE_FILE, JSON.stringify(merged, null, 2) + "\n");
|
|
6744
|
+
} catch (e) {
|
|
6745
|
+
console.error(`[reminders] cloud write failed: ${e?.message || e}`);
|
|
6746
|
+
}
|
|
6747
|
+
}
|
|
6748
|
+
this.reminderStateCache = { state: merged, at: Date.now() };
|
|
6749
|
+
return merged;
|
|
6750
|
+
}
|
|
6643
6751
|
// User dictionary (cloud-mirrored) — Android can implement these via
|
|
6644
6752
|
// the same userdict.csv cloud round-trip the desktop uses; until that
|
|
6645
6753
|
// wire-up lands, return safe defaults so spellcheck.ts catch() paths
|
|
@@ -11346,6 +11454,10 @@ function installBridge() {
|
|
|
11346
11454
|
return { ok: true };
|
|
11347
11455
|
},
|
|
11348
11456
|
loadContactsConfig: () => service.loadContactsConfig(),
|
|
11457
|
+
// Cross-device reminder dismissed/snoozed state (reminders.jsonc on
|
|
11458
|
+
// Drive) — alarm poller pushes/pulls through these.
|
|
11459
|
+
getReminderState: () => service.getReminderState(),
|
|
11460
|
+
mergeReminderState: (patch) => service.mergeReminderState(patch),
|
|
11349
11461
|
syncAll: async () => {
|
|
11350
11462
|
await service.syncAll();
|
|
11351
11463
|
return { ok: true };
|