@bobfrankston/rmfmail 1.1.45 → 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.
- package/TODO.md +9 -0
- package/client/app.bundle.js +371 -80
- package/client/app.bundle.js.map +4 -4
- package/client/app.js +167 -32
- package/client/app.js.map +1 -1
- package/client/app.ts +153 -32
- package/client/components/calendar-sidebar.js +221 -88
- package/client/components/calendar-sidebar.js.map +1 -1
- package/client/components/calendar-sidebar.ts +224 -83
- package/client/compose/compose.bundle.js +14 -0
- package/client/compose/compose.bundle.js.map +2 -2
- package/client/compose/spellcheck.js +15 -0
- package/client/compose/spellcheck.js.map +1 -1
- package/client/compose/spellcheck.ts +14 -0
- package/client/help/search-help.js +75 -0
- package/client/help/search-help.js.map +1 -0
- package/client/help/search-help.ts +75 -0
- package/client/index.html +7 -7
- package/client/lib/api-client.js +5 -0
- package/client/lib/api-client.js.map +1 -1
- package/client/lib/api-client.ts +5 -0
- package/client/lib/mailxapi.js +1 -0
- package/client/styles/components.css +204 -6
- package/docs/search.md +5 -1
- package/package.json +1 -1
- package/packages/mailx-service/google-sync.d.ts +3 -0
- package/packages/mailx-service/google-sync.d.ts.map +1 -1
- package/packages/mailx-service/google-sync.js +1 -0
- package/packages/mailx-service/google-sync.js.map +1 -1
- package/packages/mailx-service/google-sync.ts +4 -0
- package/packages/mailx-service/index.d.ts +11 -0
- package/packages/mailx-service/index.d.ts.map +1 -1
- package/packages/mailx-service/index.js +99 -127
- package/packages/mailx-service/index.js.map +1 -1
- package/packages/mailx-service/index.ts +91 -122
- package/packages/mailx-service/jsonrpc.js +2 -0
- package/packages/mailx-service/jsonrpc.js.map +1 -1
- package/packages/mailx-service/jsonrpc.ts +2 -0
- package/packages/mailx-settings/index.d.ts.map +1 -1
- package/packages/mailx-settings/index.js +4 -1
- package/packages/mailx-settings/index.js.map +1 -1
- package/packages/mailx-settings/index.ts +4 -1
- /package/packages/mailx-imap/{node_modules.npmglobalize-stash-28848 → node_modules.npmglobalize-stash-43988}/.package-lock.json +0 -0
|
@@ -80,7 +80,8 @@ function calendarRowEquals(prior, fresh) {
|
|
|
80
80
|
&& (prior.notes || "") === (fresh.notes || "")
|
|
81
81
|
&& (prior.etag || "") === (fresh.etag || "")
|
|
82
82
|
&& (prior.recurringEventId || null) === (fresh.recurringEventId || null)
|
|
83
|
-
&& (prior.htmlLink || null) === (fresh.htmlLink || null)
|
|
83
|
+
&& (prior.htmlLink || null) === (fresh.htmlLink || null)
|
|
84
|
+
&& (prior.calendarId || "") === (fresh.calendarId || "");
|
|
84
85
|
}
|
|
85
86
|
export class MailxService {
|
|
86
87
|
store;
|
|
@@ -1249,6 +1250,29 @@ export class MailxService {
|
|
|
1249
1250
|
console.log(` [calendar] getCalendarEvents → ${rows.length} rows (${recurring} recurring, ${holidays} holiday) for window ${new Date(fromMs).toISOString().slice(0, 10)}..${new Date(toMs).toISOString().slice(0, 10)}`);
|
|
1250
1251
|
return rows;
|
|
1251
1252
|
}
|
|
1253
|
+
/** List the user's *selected* Google calendars (id, display name, color,
|
|
1254
|
+
* primary flag) so the sidebar can render one checkbox + icon per
|
|
1255
|
+
* calendar. The user curates the set by selecting calendars in Google;
|
|
1256
|
+
* mailx only reflects it. Returns [] on quota cooldown / auth failure
|
|
1257
|
+
* — the sidebar then just shows no per-calendar controls. */
|
|
1258
|
+
async getCalendars() {
|
|
1259
|
+
const acct = this.getPrimaryAccount("calendar");
|
|
1260
|
+
if (!acct)
|
|
1261
|
+
return [];
|
|
1262
|
+
if (this.inQuotaCooldown("calendar"))
|
|
1263
|
+
return [];
|
|
1264
|
+
try {
|
|
1265
|
+
const tp = await this.primaryTokenProvider("calendar");
|
|
1266
|
+
const all = await gsync.listCalendars(tp);
|
|
1267
|
+
return all
|
|
1268
|
+
.filter(c => c.selected)
|
|
1269
|
+
.map(c => ({ id: c.id, name: c.summary, color: c.backgroundColor || "", primary: c.primary }));
|
|
1270
|
+
}
|
|
1271
|
+
catch (e) {
|
|
1272
|
+
this.handleGoogleRefreshError("calendar", e);
|
|
1273
|
+
return [];
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1252
1276
|
/** Returns true if the feature is currently in a quota-exceeded cooldown. */
|
|
1253
1277
|
inQuotaCooldown(feature) {
|
|
1254
1278
|
const until = this.quotaCooldown.get(feature);
|
|
@@ -1315,6 +1339,11 @@ export class MailxService {
|
|
|
1315
1339
|
// calendarList; mailx now does too. Failing to list calendars
|
|
1316
1340
|
// falls back to primary so single-calendar users still work.
|
|
1317
1341
|
let calendarsToFetch = [{ id: "primary", label: "primary", defaultReminderMinutes: [] }];
|
|
1342
|
+
// A failed enumeration / fetch returns fewer events than really
|
|
1343
|
+
// exist; running the delete-reconciliation then purges live rows
|
|
1344
|
+
// (the same data-loss shape as the Gmail partial-list bug). Track
|
|
1345
|
+
// failures and skip the purge when any occurred.
|
|
1346
|
+
let fetchFailed = false;
|
|
1318
1347
|
try {
|
|
1319
1348
|
const all = await gsync.listCalendars(tp);
|
|
1320
1349
|
const selected = all.filter(c => c.selected);
|
|
@@ -1330,6 +1359,9 @@ export class MailxService {
|
|
|
1330
1359
|
}
|
|
1331
1360
|
}
|
|
1332
1361
|
catch (e) {
|
|
1362
|
+
// listCalendars failed — we'd fetch only `primary` and miss every
|
|
1363
|
+
// secondary calendar's events. Treat as a failed pass.
|
|
1364
|
+
fetchFailed = true;
|
|
1333
1365
|
console.warn(` [calendar] listCalendars failed (${e?.message || e}) — falling back to primary`);
|
|
1334
1366
|
}
|
|
1335
1367
|
const fetchResults = await Promise.all(calendarsToFetch.map(async (c) => {
|
|
@@ -1339,144 +1371,64 @@ export class MailxService {
|
|
|
1339
1371
|
return { events: ev, calendarId: c.id, defaultReminderMinutes: c.defaultReminderMinutes };
|
|
1340
1372
|
}
|
|
1341
1373
|
catch (e) {
|
|
1374
|
+
fetchFailed = true;
|
|
1342
1375
|
console.warn(` [calendar] fetch from "${c.label}" failed: ${e?.message || e}`);
|
|
1343
1376
|
return { events: [], calendarId: c.id, defaultReminderMinutes: c.defaultReminderMinutes };
|
|
1344
1377
|
}
|
|
1345
1378
|
}));
|
|
1346
|
-
// Flatten with each event remembering its source calendar
|
|
1347
|
-
// reminder list
|
|
1348
|
-
//
|
|
1379
|
+
// Flatten with each event remembering its source calendar id + that
|
|
1380
|
+
// calendar's default reminder list. The `calendarId` rides onto every
|
|
1381
|
+
// row so the sidebar can show a per-calendar icon and the user can
|
|
1382
|
+
// hide a calendar without touching Google.
|
|
1349
1383
|
const eventsWithDefaults = [];
|
|
1350
1384
|
for (const fr of fetchResults) {
|
|
1351
1385
|
for (const ev of fr.events) {
|
|
1352
|
-
eventsWithDefaults.push({ ev, defaultReminderMinutes: fr.defaultReminderMinutes });
|
|
1353
|
-
}
|
|
1354
|
-
}
|
|
1355
|
-
const events = eventsWithDefaults.map(x => x.ev);
|
|
1356
|
-
// Holiday calendars (Q131 + Bob 2026-05-11 second-toggle request).
|
|
1357
|
-
// Each enabled toggle pulls a separate Google public calendar; rows
|
|
1358
|
-
// are tagged with the source `calendar_id` so toggling one off only
|
|
1359
|
-
// purges that source's rows, not the others. Future generalization
|
|
1360
|
-
// to arbitrary user-supplied calendars is filed as Q140.
|
|
1361
|
-
// Cached — refreshCalendarEvents runs on a poll AND on every
|
|
1362
|
-
// getCalendarEvents call (envelope-first pattern). Synchronous
|
|
1363
|
-
// GDrive read of accounts.jsonc here was a major contributor to
|
|
1364
|
-
// the daemon-event-loop wedge that broke preview rendering.
|
|
1365
|
-
const settings = this.getCachedSettings();
|
|
1366
|
-
// `en.usa#holiday` is Google's "Holidays in United States" calendar
|
|
1367
|
-
// which is *mixed*: federal (MLK, Memorial Day, July 4, Thanksgiving,
|
|
1368
|
-
// …) PLUS Christian (Ascension Day, Pentecost, Catholic/Protestant
|
|
1369
|
-
// /Orthodox/Armenian variants), Jewish observances, Ethiopian Jewish,
|
|
1370
|
-
// etc. `en.usa.official#holiday` is the federal-secular-only variant.
|
|
1371
|
-
// Per Bob 2026-05-11: "is there one for just secular holidays?" —
|
|
1372
|
-
// yes, this is it.
|
|
1373
|
-
const HOLIDAY_SOURCES = [
|
|
1374
|
-
{
|
|
1375
|
-
enabled: !!settings?.calendar?.showHolidays,
|
|
1376
|
-
calId: "en.usa.official#holiday@group.v.calendar.google.com",
|
|
1377
|
-
label: "US-official",
|
|
1378
|
-
},
|
|
1379
|
-
{
|
|
1380
|
-
enabled: !!settings?.calendar?.showJewishHolidays,
|
|
1381
|
-
// `en.judaism#holiday` is Google's Jewish-only feed.
|
|
1382
|
-
// `en.jewish#holiday` (the obvious guess) is actually
|
|
1383
|
-
// mis-curated and includes Christian observances
|
|
1384
|
-
// (Ascension Eve / Day, Pentecost Eve / Day, Catholic /
|
|
1385
|
-
// Protestant / Armenian / Orthodox variants) — Google
|
|
1386
|
-
// packs cross-tradition events into the "regional"
|
|
1387
|
-
// calendars. Bob 2026-05-11 confirmed `en.judaism` is
|
|
1388
|
-
// the clean source via Google's embed URL.
|
|
1389
|
-
calId: "en.judaism#holiday@group.v.calendar.google.com",
|
|
1390
|
-
label: "Jewish",
|
|
1391
|
-
},
|
|
1392
|
-
];
|
|
1393
|
-
// Fan out enabled holiday-source fetches in parallel — Google's API
|
|
1394
|
-
// handles each independently and the results merge by calId after.
|
|
1395
|
-
// Earlier code awaited each in series, doubling latency when both
|
|
1396
|
-
// US and Jewish sources were enabled (the common case for users who
|
|
1397
|
-
// turn both on).
|
|
1398
|
-
const enabledSources = HOLIDAY_SOURCES.filter(s => s.enabled);
|
|
1399
|
-
const bundleResults = await Promise.all(enabledSources.map(async (src) => {
|
|
1400
|
-
try {
|
|
1401
|
-
const ev = await gsync.listCalendarEvents(tp, fromMs, toMs, src.calId);
|
|
1402
|
-
console.log(` [calendar] pulled ${ev.length} ${src.label} holiday events`);
|
|
1403
|
-
return { calId: src.calId, events: ev };
|
|
1386
|
+
eventsWithDefaults.push({ ev, calendarId: fr.calendarId, defaultReminderMinutes: fr.defaultReminderMinutes });
|
|
1404
1387
|
}
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1388
|
+
}
|
|
1389
|
+
// Holiday calendars are no longer hard-coded (the old HOLIDAY_SOURCES
|
|
1390
|
+
// pull + `showHolidays`/`showJewishHolidays` toggles are retired). The
|
|
1391
|
+
// user curates which calendars exist by selecting them in Google
|
|
1392
|
+
// Calendar; mailx enumerates them above and tags every row with its
|
|
1393
|
+
// source `calendarId`. The sidebar derives holiday/birthday/personal
|
|
1394
|
+
// kind from that id and lets the user hide calendars per-mailx.
|
|
1412
1395
|
let changed = false;
|
|
1413
1396
|
// Upsert by provider_id — dedup globally, not just within the window,
|
|
1414
1397
|
// so an event whose start moves outside the prior query range doesn't
|
|
1415
|
-
// get a second row on the next pull.
|
|
1398
|
+
// get a second row on the next pull. If the same event id arrives
|
|
1399
|
+
// from two calendars, last-write-wins on `calendarId` (one row, one
|
|
1400
|
+
// source icon — multi-source badges are a future refinement, Q143).
|
|
1416
1401
|
const seenProviderIds = new Set();
|
|
1417
|
-
for (const { ev, defaultReminderMinutes } of eventsWithDefaults) {
|
|
1418
|
-
const local = gsync.calendarEventToLocal(ev, accountId, defaultReminderMinutes);
|
|
1402
|
+
for (const { ev, calendarId, defaultReminderMinutes } of eventsWithDefaults) {
|
|
1403
|
+
const local = { ...gsync.calendarEventToLocal(ev, accountId, defaultReminderMinutes), calendarId };
|
|
1419
1404
|
seenProviderIds.add(ev.id);
|
|
1420
1405
|
const existing = this.db.getCalendarEventByProviderId(accountId, ev.id);
|
|
1421
|
-
if (existing && calendarRowEquals(existing, local)
|
|
1406
|
+
if (existing && calendarRowEquals(existing, local))
|
|
1422
1407
|
continue;
|
|
1423
|
-
this.db.upsertCalendarEvent({ uuid: existing?.uuid, ...local
|
|
1408
|
+
this.db.upsertCalendarEvent({ uuid: existing?.uuid, ...local });
|
|
1424
1409
|
changed = true;
|
|
1425
1410
|
}
|
|
1426
|
-
//
|
|
1427
|
-
//
|
|
1428
|
-
//
|
|
1429
|
-
//
|
|
1430
|
-
//
|
|
1431
|
-
//
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
seenInHolidayPass.add(ev.id);
|
|
1441
|
-
const baseLocal = gsync.calendarEventToLocal(ev, accountId);
|
|
1442
|
-
// Override calendarId so reconcile (below) can tell which
|
|
1443
|
-
// holiday source this row belongs to — toggling one source
|
|
1444
|
-
// off must not purge rows from another active source.
|
|
1445
|
-
const local = { ...baseLocal, calendarId: bundle.calId };
|
|
1446
|
-
seenProviderIds.add(ev.id);
|
|
1447
|
-
const existing = this.db.getCalendarEventByProviderId(accountId, ev.id);
|
|
1448
|
-
if (existing && calendarRowEquals(existing, local) && existing.isHoliday)
|
|
1411
|
+
// Server-side delete reconciliation: any local non-dirty row whose
|
|
1412
|
+
// start falls in the queried window and whose provider_id wasn't
|
|
1413
|
+
// returned was deleted on Google — or its calendar was deselected in
|
|
1414
|
+
// Google (no longer enumerated, so its events stop arriving). Both
|
|
1415
|
+
// mean the row should go. SKIPPED when any fetch failed this pass —
|
|
1416
|
+
// a partial result would otherwise purge live events.
|
|
1417
|
+
if (!fetchFailed) {
|
|
1418
|
+
const localWindow = this.db.getCalendarEvents(accountId, fromMs, toMs);
|
|
1419
|
+
for (const row of localWindow) {
|
|
1420
|
+
if (!row.providerId)
|
|
1421
|
+
continue; // local-only, never pushed
|
|
1422
|
+
if (row.dirty)
|
|
1423
|
+
continue; // locally edited, pending push
|
|
1424
|
+
if (seenProviderIds.has(row.providerId))
|
|
1449
1425
|
continue;
|
|
1450
|
-
this.db.
|
|
1426
|
+
this.db.purgeCalendarEvent(row.uuid);
|
|
1451
1427
|
changed = true;
|
|
1452
1428
|
}
|
|
1453
1429
|
}
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
// returned must have been deleted on Google. Holiday rows are
|
|
1457
|
-
// partitioned by `calendarId`: only purge a holiday row when its
|
|
1458
|
-
// source calendar is currently disabled. Sources that are enabled
|
|
1459
|
-
// but returned nothing for this row keep the row (transient fail).
|
|
1460
|
-
const localWindow = this.db.getCalendarEvents(accountId, fromMs, toMs);
|
|
1461
|
-
for (const row of localWindow) {
|
|
1462
|
-
if (!row.providerId)
|
|
1463
|
-
continue; // local-only, never pushed
|
|
1464
|
-
if (row.dirty)
|
|
1465
|
-
continue; // locally edited, pending push
|
|
1466
|
-
if (seenProviderIds.has(row.providerId))
|
|
1467
|
-
continue;
|
|
1468
|
-
if (row.isHoliday) {
|
|
1469
|
-
const fromActiveSource = enabledHolidayCalIds.has(row.calendarId || "");
|
|
1470
|
-
if (!fromActiveSource) {
|
|
1471
|
-
// Source toggled off — purge.
|
|
1472
|
-
this.db.purgeCalendarEvent(row.uuid);
|
|
1473
|
-
changed = true;
|
|
1474
|
-
}
|
|
1475
|
-
// Source enabled but row missing → transient; keep.
|
|
1476
|
-
continue;
|
|
1477
|
-
}
|
|
1478
|
-
this.db.purgeCalendarEvent(row.uuid);
|
|
1479
|
-
changed = true;
|
|
1430
|
+
else {
|
|
1431
|
+
console.warn(` [calendar] fetch incomplete — skipping delete reconciliation to protect the local cache`);
|
|
1480
1432
|
}
|
|
1481
1433
|
return changed;
|
|
1482
1434
|
}
|
|
@@ -1555,6 +1507,12 @@ export class MailxService {
|
|
|
1555
1507
|
// a single `@default` query keeps single-list users working when
|
|
1556
1508
|
// the listTaskLists call fails.
|
|
1557
1509
|
let lists = [{ id: "@default", title: "@default" }];
|
|
1510
|
+
// Track whether ANY fetch failed. A failed fetch returns an empty
|
|
1511
|
+
// list indistinguishable from "all tasks deleted on the server" —
|
|
1512
|
+
// running the delete-reconciliation then wipes the whole local task
|
|
1513
|
+
// cache. `firstError` is surfaced so the quota cooldown gets armed.
|
|
1514
|
+
let fetchFailed = false;
|
|
1515
|
+
let firstError = null;
|
|
1558
1516
|
try {
|
|
1559
1517
|
const all = await gsync.listTaskLists(tp);
|
|
1560
1518
|
if (all.length > 0) {
|
|
@@ -1563,6 +1521,8 @@ export class MailxService {
|
|
|
1563
1521
|
}
|
|
1564
1522
|
}
|
|
1565
1523
|
catch (e) {
|
|
1524
|
+
fetchFailed = true;
|
|
1525
|
+
firstError = e;
|
|
1566
1526
|
console.warn(` [tasks] listTaskLists failed (${e?.message || e}) — falling back to @default`);
|
|
1567
1527
|
}
|
|
1568
1528
|
const fetchResults = await Promise.all(lists.map(async (l) => {
|
|
@@ -1572,6 +1532,8 @@ export class MailxService {
|
|
|
1572
1532
|
return t;
|
|
1573
1533
|
}
|
|
1574
1534
|
catch (e) {
|
|
1535
|
+
fetchFailed = true;
|
|
1536
|
+
firstError = firstError || e;
|
|
1575
1537
|
console.warn(` [tasks] fetch from "${l.title}" failed: ${e?.message || e}`);
|
|
1576
1538
|
return [];
|
|
1577
1539
|
}
|
|
@@ -1592,15 +1554,25 @@ export class MailxService {
|
|
|
1592
1554
|
this.db.upsertTask({ uuid: prior?.uuid, ...local });
|
|
1593
1555
|
changed = true;
|
|
1594
1556
|
}
|
|
1595
|
-
// Server-side delete reconciliation
|
|
1596
|
-
//
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1557
|
+
// Server-side delete reconciliation — ONLY when every list fetched
|
|
1558
|
+
// cleanly. On a failed fetch (429, network) the server result is
|
|
1559
|
+
// an empty list, which the purge below would read as "everything
|
|
1560
|
+
// was deleted" and wipe the entire local task cache. Guard it.
|
|
1561
|
+
if (!fetchFailed) {
|
|
1562
|
+
for (const row of existing) {
|
|
1563
|
+
if (!row.providerId || row.dirty)
|
|
1564
|
+
continue;
|
|
1565
|
+
if (seen.has(row.providerId))
|
|
1566
|
+
continue;
|
|
1567
|
+
this.db.purgeTask(row.uuid);
|
|
1568
|
+
changed = true;
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
else {
|
|
1572
|
+
console.warn(` [tasks] fetch incomplete — skipping delete reconciliation to protect the local cache`);
|
|
1573
|
+
// Arm the quota cooldown. Without this the 429 is swallowed here
|
|
1574
|
+
// and the 30 s poll hammers Google's Tasks API forever.
|
|
1575
|
+
this.handleGoogleRefreshError("tasks", firstError);
|
|
1604
1576
|
}
|
|
1605
1577
|
return changed;
|
|
1606
1578
|
}
|