@bobfrankston/rmfmail 1.2.106 → 1.2.110

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 (42) hide show
  1. package/bin/mailx.js +8 -3
  2. package/bin/mailx.js.map +1 -1
  3. package/bin/mailx.ts +8 -3
  4. package/client/android-bootstrap.bundle.js +93 -4
  5. package/client/android-bootstrap.bundle.js.map +3 -3
  6. package/client/compose/compose.bundle.js +30 -3
  7. package/client/compose/compose.bundle.js.map +2 -2
  8. package/client/lib/rmf-tiny.js +41 -3
  9. package/debug.md +33 -0
  10. package/package.json +3 -3
  11. package/packages/mailx-imap/index.d.ts.map +1 -1
  12. package/packages/mailx-imap/index.js +17 -3
  13. package/packages/mailx-imap/index.js.map +1 -1
  14. package/packages/mailx-imap/index.ts +16 -3
  15. package/packages/mailx-imap/package-lock.json +2 -2
  16. package/packages/mailx-imap/package.json +1 -1
  17. package/packages/mailx-service/index.d.ts +9 -0
  18. package/packages/mailx-service/index.d.ts.map +1 -1
  19. package/packages/mailx-service/index.js +133 -77
  20. package/packages/mailx-service/index.js.map +1 -1
  21. package/packages/mailx-service/index.ts +107 -55
  22. package/packages/mailx-settings/package.json +1 -1
  23. package/packages/mailx-store-web/android-bootstrap.d.ts.map +1 -1
  24. package/packages/mailx-store-web/android-bootstrap.js +25 -0
  25. package/packages/mailx-store-web/android-bootstrap.js.map +1 -1
  26. package/packages/mailx-store-web/android-bootstrap.ts +25 -0
  27. package/packages/mailx-store-web/db.d.ts +7 -0
  28. package/packages/mailx-store-web/db.d.ts.map +1 -1
  29. package/packages/mailx-store-web/db.js +24 -1
  30. package/packages/mailx-store-web/db.js.map +1 -1
  31. package/packages/mailx-store-web/db.ts +20 -1
  32. package/packages/mailx-store-web/package.json +1 -1
  33. package/packages/mailx-store-web/web-service.d.ts +15 -2
  34. package/packages/mailx-store-web/web-service.d.ts.map +1 -1
  35. package/packages/mailx-store-web/web-service.js +35 -2
  36. package/packages/mailx-store-web/web-service.js.map +1 -1
  37. package/packages/mailx-store-web/web-service.ts +33 -2
  38. package/packages/mailx-store-web/web-settings.d.ts +6 -0
  39. package/packages/mailx-store-web/web-settings.d.ts.map +1 -1
  40. package/packages/mailx-store-web/web-settings.js +20 -0
  41. package/packages/mailx-store-web/web-settings.js.map +1 -1
  42. package/packages/mailx-store-web/web-settings.ts +20 -0
@@ -2568,6 +2568,7 @@ var web_settings_exports = {};
2568
2568
  __export(web_settings_exports, {
2569
2569
  clearSettings: () => clearSettings,
2570
2570
  cloudRead: () => cloudRead,
2571
+ cloudStat: () => cloudStat,
2571
2572
  cloudWrite: () => cloudWrite,
2572
2573
  getDeviceId: () => getDeviceId,
2573
2574
  getHistoryDays: () => getHistoryDays,
@@ -2647,6 +2648,23 @@ async function cloudRead(filename) {
2647
2648
  async function cloudWrite(filename, content) {
2648
2649
  return gDriveWrite(filename, content);
2649
2650
  }
2651
+ async function cloudStat(filename) {
2652
+ if (!tokenProvider || !gDriveFolderId)
2653
+ return null;
2654
+ try {
2655
+ const token = await tokenProvider();
2656
+ const q = encodeURIComponent(`name='${filename}' and '${gDriveFolderId}' in parents and trashed=false`);
2657
+ const res = await globalThis.fetch(`https://www.googleapis.com/drive/v3/files?q=${q}&fields=files(id,modifiedTime)`, { headers: { "Authorization": `Bearer ${token}` } });
2658
+ if (!res.ok)
2659
+ return null;
2660
+ const data = await res.json();
2661
+ const f = data.files?.[0];
2662
+ return f?.id ? { id: f.id, modifiedTime: f.modifiedTime || "" } : null;
2663
+ } catch (e) {
2664
+ console.error(`[settings] GDrive stat ${filename}: ${e.message}`);
2665
+ return null;
2666
+ }
2667
+ }
2650
2668
  async function gDriveRead(filename) {
2651
2669
  if (!tokenProvider || !gDriveFolderId)
2652
2670
  return null;
@@ -5329,6 +5347,28 @@ var WebMailxDB = class {
5329
5347
  setContactsDenylist(emails) {
5330
5348
  this._denylist = new Set((emails || []).map((e) => (e || "").trim().toLowerCase()).filter(Boolean));
5331
5349
  }
5350
+ /** Regex patterns from contacts.jsonc#denylistPatterns — same semantics
5351
+ * as desktop (mailx-store db.ts setContactsDenyPatterns): compiled
5352
+ * case-insensitive, tested against the lowercased address, invalid
5353
+ * patterns skipped with a warning. */
5354
+ _denyPatterns = [];
5355
+ setContactsDenyPatterns(patterns) {
5356
+ this._denyPatterns = [];
5357
+ for (const p of patterns || []) {
5358
+ if (!p)
5359
+ continue;
5360
+ try {
5361
+ this._denyPatterns.push(new RegExp(p, "i"));
5362
+ } catch (e) {
5363
+ console.warn(`[contacts] invalid denylistPattern "${p}": ${e.message}`);
5364
+ }
5365
+ }
5366
+ }
5367
+ isDenylisted(emailLower) {
5368
+ if (this._denylist.has(emailLower))
5369
+ return true;
5370
+ return this._denyPatterns.some((re) => re.test(emailLower));
5371
+ }
5332
5372
  searchContacts(query, limit = 10) {
5333
5373
  query = (query || "").trim();
5334
5374
  if (!query)
@@ -5337,7 +5377,7 @@ var WebMailxDB = class {
5337
5377
  const rows = this.all(`SELECT name, email, source, use_count as useCount FROM contacts
5338
5378
  WHERE email LIKE ? OR name LIKE ?
5339
5379
  ORDER BY use_count DESC, last_used DESC LIMIT ?`, [q, q, limit + 25]);
5340
- return rows.filter((r) => !this._denylist.has((r.email || "").toLowerCase())).slice(0, limit);
5380
+ return rows.filter((r) => !this.isDenylisted((r.email || "").toLowerCase())).slice(0, limit);
5341
5381
  }
5342
5382
  /** Address-book listing. Same shape as mailx-store/db.ts:listContacts so
5343
5383
  * the address-book modal renders identically on desktop and Android. */
@@ -5732,7 +5772,7 @@ function decodeBody(body, encoding, charset = "utf-8") {
5732
5772
  return new TextDecoder("utf-8").decode(bytes);
5733
5773
  }
5734
5774
  }
5735
- var WebMailxService = class {
5775
+ var WebMailxService = class _WebMailxService {
5736
5776
  db;
5737
5777
  bodyStore;
5738
5778
  syncManager;
@@ -6130,8 +6170,30 @@ ${bodyEncoded}`;
6130
6170
  query = (query || "").trim();
6131
6171
  if (query.length < 1)
6132
6172
  return [];
6173
+ void this.maybeRefreshContactsConfig().catch(() => {
6174
+ });
6133
6175
  return this.db.searchContacts(query);
6134
6176
  }
6177
+ /** Re-pull contacts.jsonc only when the Drive copy actually changed.
6178
+ * One metadata request (cloudStat) throttled to a few minutes; the
6179
+ * full download + reload runs only on a new modifiedTime. Kicked from
6180
+ * autocomplete, the periodic sync tick, and app-resume. */
6181
+ _contactsStatAt = 0;
6182
+ _contactsModifiedTime = null;
6183
+ static CONTACTS_STAT_MS = 3 * 60 * 1e3;
6184
+ async maybeRefreshContactsConfig() {
6185
+ const now = Date.now();
6186
+ if (now - this._contactsStatAt < _WebMailxService.CONTACTS_STAT_MS)
6187
+ return;
6188
+ this._contactsStatAt = now;
6189
+ const { cloudStat: cloudStat2 } = await Promise.resolve().then(() => (init_web_settings(), web_settings_exports));
6190
+ const stat = await cloudStat2("contacts.jsonc");
6191
+ if (!stat || stat.modifiedTime === this._contactsModifiedTime)
6192
+ return;
6193
+ this._contactsModifiedTime = stat.modifiedTime;
6194
+ console.log(`[contacts] contacts.jsonc changed on Drive (${stat.modifiedTime}) \u2014 reloading`);
6195
+ await this.loadContactsConfig();
6196
+ }
6135
6197
  /** Address-book listing — paginated, filterable. Mirrors mailx-service's
6136
6198
  * signature so the same client-side address-book modal works on Android
6137
6199
  * without an "ipc(...).listContacts is not a function" crash. */
@@ -6394,8 +6456,13 @@ ${bodyEncoded}`;
6394
6456
  * local contacts table. Desktop flushes the full union there (everything
6395
6457
  * it discovered from its mailbox corpus + Google contacts + preferred);
6396
6458
  * this is how an Android device gets contacts it never saw in its own
6397
- * (partial) on-device corpus. Called once at startup re-importing on a
6398
- * loop would bump `use_count` every pass and distort autocomplete rank. */
6459
+ * (partial) on-device corpus. Called at startup AND on a periodic tick /
6460
+ * app-resume / after a so a denylist edit made on another device
6461
+ * reaches a running phone (Bob 2026-07-07: GDrive edit was still being
6462
+ * offered). The denylist refresh runs every call; the contact IMPORT
6463
+ * runs once per session — recordSentAddress bumps `use_count`, so
6464
+ * re-importing on every reload would inflate autocomplete rank. */
6465
+ _contactsImported = false;
6399
6466
  async loadContactsConfig() {
6400
6467
  try {
6401
6468
  const { cloudRead: cloudRead2 } = await Promise.resolve().then(() => (init_web_settings(), web_settings_exports));
@@ -6406,6 +6473,10 @@ ${bodyEncoded}`;
6406
6473
  if (!cfg)
6407
6474
  return null;
6408
6475
  this.db.setContactsDenylist(Array.isArray(cfg.denylist) ? cfg.denylist : []);
6476
+ this.db.setContactsDenyPatterns(Array.isArray(cfg.denylistPatterns) ? cfg.denylistPatterns : []);
6477
+ if (this._contactsImported)
6478
+ return { imported: 0 };
6479
+ this._contactsImported = true;
6409
6480
  let imported = 0;
6410
6481
  for (const list of [cfg.preferred, cfg.discovered]) {
6411
6482
  if (!Array.isArray(list))
@@ -10995,6 +11066,7 @@ async function initAndroid() {
10995
11066
  syncManager.processSyncActions(account.id).catch((e) => console.error(`[android] processSyncActions ${account.id}: ${e.message}`));
10996
11067
  }
10997
11068
  syncManager.syncAll().catch((e) => console.error(`[android] Periodic sync error: ${e.message}`));
11069
+ service.maybeRefreshContactsConfig().catch((e) => console.error(`[android] contacts.jsonc freshness check: ${e?.message || e}`));
10998
11070
  if (++contactsSyncTickCounter % 8 === 0) {
10999
11071
  for (const account of db.getAccounts()) {
11000
11072
  if (!account.email || !isGoogleAccount(account)) continue;
@@ -11010,6 +11082,7 @@ async function initAndroid() {
11010
11082
  syncManager.processSendQueue(account.id).catch((e) => console.error(`[android] resume send-drain ${account.id}: ${e.message}`));
11011
11083
  }
11012
11084
  syncManager.syncAll().catch((e) => console.error(`[android] Resume sync error: ${e.message}`));
11085
+ service.maybeRefreshContactsConfig().catch((e) => console.error(`[android] resume contacts.jsonc check: ${e?.message || e}`));
11013
11086
  }
11014
11087
  });
11015
11088
  console.log("[android] Initialization complete");
@@ -11103,6 +11176,22 @@ function installBridge() {
11103
11176
  deleteContact: (email) => service.deleteContact(email),
11104
11177
  addContact: (name, email) => service.addContact(name || "", email),
11105
11178
  hasCcHistoryTo: (email) => ({ hasCc: service.hasCcHistoryTo?.(email) ?? false }),
11179
+ hasBccHistoryTo: (email) => ({ hasBcc: service.hasBccHistoryTo?.(email) ?? false }),
11180
+ // ⊘ / ★ in compose autocomplete. The service has implemented these
11181
+ // since the contacts-config share (web-service.ts), but they were
11182
+ // never exposed here, so the compose iframe's IPC relay failed with
11183
+ // `parent bridge has no method "addToDenylist"` (Bob 2026-07-07
11184
+ // screenshot). Signatures mirror client/lib/mailxapi.js — positional
11185
+ // args in, object built here.
11186
+ addPreferredContact: async (name, email, source, organization) => {
11187
+ await service.addPreferredContact({ name, email, source, organization });
11188
+ return { ok: true };
11189
+ },
11190
+ addToDenylist: async (email) => {
11191
+ await service.addToDenylist(email);
11192
+ return { ok: true };
11193
+ },
11194
+ loadContactsConfig: () => service.loadContactsConfig(),
11106
11195
  syncAll: async () => {
11107
11196
  await service.syncAll();
11108
11197
  return { ok: true };