@bobfrankston/rmfmail 1.2.170 → 1.2.172

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.
@@ -2588,7 +2588,8 @@ __export(web_settings_exports, {
2588
2588
  savePreferences: () => savePreferences,
2589
2589
  saveSettings: () => saveSettings,
2590
2590
  setGDriveFolderId: () => setGDriveFolderId,
2591
- setGDriveTokenProvider: () => setGDriveTokenProvider
2591
+ setGDriveTokenProvider: () => setGDriveTokenProvider,
2592
+ updateAllowlist: () => updateAllowlist
2592
2593
  });
2593
2594
  function openSettingsDb() {
2594
2595
  return new Promise((resolve, reject) => {
@@ -2630,6 +2631,15 @@ async function idbDelete(key) {
2630
2631
  tx.onerror = () => reject(tx.error);
2631
2632
  });
2632
2633
  }
2634
+ async function idbListKeys(prefix) {
2635
+ const db2 = await openSettingsDb();
2636
+ return new Promise((resolve, reject) => {
2637
+ const tx = db2.transaction(STORE_NAME2, "readonly");
2638
+ const req = tx.objectStore(STORE_NAME2).getAllKeys(IDBKeyRange.bound(prefix, prefix + "\uFFFF"));
2639
+ req.onsuccess = () => resolve(req.result.map(String));
2640
+ req.onerror = () => reject(req.error);
2641
+ });
2642
+ }
2633
2643
  function setGDriveTokenProvider(provider) {
2634
2644
  tokenProvider = provider;
2635
2645
  }
@@ -2665,17 +2675,28 @@ async function cloudStat(filename) {
2665
2675
  return null;
2666
2676
  }
2667
2677
  }
2678
+ async function gDriveFindFile(filename, token) {
2679
+ const q = encodeURIComponent(`name='${filename}' and '${gDriveFolderId}' in parents and trashed=false`);
2680
+ const res = await globalThis.fetch(`https://www.googleapis.com/drive/v3/files?q=${q}&fields=files(id,modifiedTime,ownedByMe,capabilities(canEdit))`, { headers: { "Authorization": `Bearer ${token}` } });
2681
+ if (!res.ok)
2682
+ return null;
2683
+ const files = (await res.json()).files || [];
2684
+ if (files.length === 0)
2685
+ return null;
2686
+ if (files.length > 1) {
2687
+ console.warn(`[settings] ${filename}: ${files.length} same-named files in the shared folder \u2014 using own/newest. Clean the duplicates (owners differ?).`);
2688
+ files.sort((a, b) => (b.ownedByMe === true ? 1 : 0) - (a.ownedByMe === true ? 1 : 0) || String(b.modifiedTime).localeCompare(String(a.modifiedTime)));
2689
+ }
2690
+ const f = files[0];
2691
+ return { id: f.id, modifiedTime: f.modifiedTime || "", ownedByMe: f.ownedByMe === true, capabilitiesCanEdit: f.capabilities?.canEdit !== false };
2692
+ }
2668
2693
  async function gDriveRead(filename) {
2669
2694
  if (!tokenProvider || !gDriveFolderId)
2670
2695
  return null;
2671
2696
  try {
2672
2697
  const token = await tokenProvider();
2673
- const q = encodeURIComponent(`name='${filename}' and '${gDriveFolderId}' in parents and trashed=false`);
2674
- const listRes = await globalThis.fetch(`https://www.googleapis.com/drive/v3/files?q=${q}&fields=files(id)`, { headers: { "Authorization": `Bearer ${token}` } });
2675
- if (!listRes.ok)
2676
- return null;
2677
- const listData = await listRes.json();
2678
- const fileId = listData.files?.[0]?.id;
2698
+ const found = await gDriveFindFile(filename, token);
2699
+ const fileId = found?.id;
2679
2700
  if (!fileId)
2680
2701
  return null;
2681
2702
  const res = await globalThis.fetch(`https://www.googleapis.com/drive/v3/files/${fileId}?alt=media`, { headers: { "Authorization": `Bearer ${token}` } });
@@ -2692,13 +2713,13 @@ async function gDriveWrite(filename, content) {
2692
2713
  return false;
2693
2714
  try {
2694
2715
  const token = await tokenProvider();
2695
- const q = encodeURIComponent(`name='${filename}' and '${gDriveFolderId}' in parents and trashed=false`);
2696
- const listRes = await globalThis.fetch(`https://www.googleapis.com/drive/v3/files?q=${q}&fields=files(id)`, { headers: { "Authorization": `Bearer ${token}` } });
2697
- if (!listRes.ok)
2698
- return false;
2699
- const listData = await listRes.json();
2700
- const fileId = listData.files?.[0]?.id;
2716
+ const found = await gDriveFindFile(filename, token);
2717
+ const fileId = found?.id;
2701
2718
  if (fileId) {
2719
+ if (!found.capabilitiesCanEdit) {
2720
+ console.error(`[settings] ${filename}: existing shared file is not editable by this account \u2014 NOT creating a duplicate. Change kept locally.`);
2721
+ return false;
2722
+ }
2702
2723
  const res = await globalThis.fetch(`https://www.googleapis.com/upload/drive/v3/files/${fileId}?uploadType=media`, {
2703
2724
  method: "PATCH",
2704
2725
  headers: {
@@ -2707,6 +2728,8 @@ async function gDriveWrite(filename, content) {
2707
2728
  },
2708
2729
  body: content
2709
2730
  });
2731
+ if (!res.ok)
2732
+ console.error(`[settings] ${filename}: cloud update failed (${res.status})`);
2710
2733
  return res.ok;
2711
2734
  } else {
2712
2735
  const metadata = JSON.stringify({
@@ -2816,6 +2839,20 @@ function parseJsonc(text) {
2816
2839
  stripped = stripped.replace(/,(\s*[}\]])/g, "$1");
2817
2840
  return JSON.parse(stripped);
2818
2841
  }
2842
+ async function stashPrevIdb(filename, aboutToBecome) {
2843
+ try {
2844
+ const old = await idbRead(filename);
2845
+ if (!old || old === aboutToBecome)
2846
+ return;
2847
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
2848
+ await idbWrite(`prev:${filename}:${stamp}`, old);
2849
+ const keys = await idbListKeys(`prev:${filename}:`);
2850
+ for (const k of keys.sort().slice(0, Math.max(0, keys.length - PREV_KEEP))) {
2851
+ await idbDelete(k);
2852
+ }
2853
+ } catch {
2854
+ }
2855
+ }
2819
2856
  function refreshSharedFile(filename, cached) {
2820
2857
  const now = Date.now();
2821
2858
  if (now - (__lastSharedRefresh.get(filename) || 0) < SHARED_REFRESH_MS)
@@ -2908,6 +2945,7 @@ async function loadPreferences() {
2908
2945
  }
2909
2946
  async function savePreferences(prefs) {
2910
2947
  const content = JSON.stringify(prefs, null, 2);
2948
+ await stashPrevIdb("preferences.jsonc", content);
2911
2949
  await idbWrite("preferences.jsonc", content);
2912
2950
  await gDriveWrite("preferences.jsonc", content);
2913
2951
  }
@@ -2948,9 +2986,44 @@ async function loadAllowlist() {
2948
2986
  }
2949
2987
  return { ...DEFAULT_ALLOWLIST };
2950
2988
  }
2989
+ function allowlistEntryCount(l) {
2990
+ return ALLOWLIST_KEYS.reduce((n, k) => n + (Array.isArray(l?.[k]) ? l[k].length : 0), 0);
2991
+ }
2992
+ async function updateAllowlist(mutate) {
2993
+ let base = null;
2994
+ try {
2995
+ const fresh = await gDriveRead("allowlist.jsonc");
2996
+ if (fresh) {
2997
+ await idbWrite("allowlist.jsonc", fresh);
2998
+ try {
2999
+ base = parseJsonc(fresh);
3000
+ } catch {
3001
+ }
3002
+ }
3003
+ } catch {
3004
+ }
3005
+ if (!base)
3006
+ base = await loadAllowlist();
3007
+ const out = mutate(base) || base;
3008
+ await saveAllowlist(out);
3009
+ return out;
3010
+ }
2951
3011
  async function saveAllowlist(list) {
2952
3012
  const content = JSON.stringify(list, null, 2);
3013
+ await stashPrevIdb("allowlist.jsonc", content);
2953
3014
  await idbWrite("allowlist.jsonc", content);
3015
+ try {
3016
+ const cloudRaw = await gDriveRead("allowlist.jsonc");
3017
+ if (cloudRaw) {
3018
+ const cloudCount = allowlistEntryCount(parseJsonc(cloudRaw));
3019
+ const newCount = allowlistEntryCount(list);
3020
+ if (cloudCount > 10 && newCount < cloudCount / 2) {
3021
+ console.error(`[settings] REFUSING allowlist cloud write: ${cloudCount} \u2192 ${newCount} entries (stale/default base \u2014 see 2026-07-23 wipe). Local cache updated only.`);
3022
+ return;
3023
+ }
3024
+ }
3025
+ } catch {
3026
+ }
2954
3027
  await gDriveWrite("allowlist.jsonc", content);
2955
3028
  }
2956
3029
  async function loadAutocomplete() {
@@ -3020,7 +3093,7 @@ async function loadDeviceState() {
3020
3093
  }
3021
3094
  return {};
3022
3095
  }
3023
- var IDB_NAME2, IDB_VERSION, STORE_NAME2, gDriveFolderId, gDriveFolderName, gDriveFolderPath, gDriveFolderOwner, tokenProvider, PROVIDERS, DEFAULT_PREFERENCES, DEFAULT_ALLOWLIST, SHARED_REFRESH_MS, __lastSharedRefresh, DEVICE_ID_KEY;
3096
+ var IDB_NAME2, IDB_VERSION, STORE_NAME2, gDriveFolderId, gDriveFolderName, gDriveFolderPath, gDriveFolderOwner, tokenProvider, PROVIDERS, DEFAULT_PREFERENCES, DEFAULT_ALLOWLIST, PREV_KEEP, SHARED_REFRESH_MS, __lastSharedRefresh, ALLOWLIST_KEYS, DEVICE_ID_KEY;
3024
3097
  var init_web_settings = __esm({
3025
3098
  "packages/mailx-store-web/web-settings.js"() {
3026
3099
  "use strict";
@@ -3095,8 +3168,10 @@ var init_web_settings = __esm({
3095
3168
  flaggedSenders: [],
3096
3169
  flaggedDomains: []
3097
3170
  };
3171
+ PREV_KEEP = 5;
3098
3172
  SHARED_REFRESH_MS = 5 * 6e4;
3099
3173
  __lastSharedRefresh = /* @__PURE__ */ new Map();
3174
+ ALLOWLIST_KEYS = ["senders", "domains", "recipients", "flaggedSenders", "flaggedDomains"];
3100
3175
  DEVICE_ID_KEY = "mailx-device-id";
3101
3176
  }
3102
3177
  });
@@ -5939,23 +6014,24 @@ var WebMailxService = class _WebMailxService {
5939
6014
  * flaggedSenders / flaggedDomains in allowlist.jsonc, which syncs
5940
6015
  * back to the cloud copy. */
5941
6016
  async flagSenderOrDomain(type, value) {
5942
- const list = await loadAllowlist();
5943
- const key = type === "sender" ? "flaggedSenders" : "flaggedDomains";
5944
- const arr = Array.isArray(list[key]) ? list[key] : [];
5945
6017
  const v = (value || "").trim().toLowerCase();
5946
6018
  if (!v)
5947
6019
  return { flagged: false };
5948
- const idx = arr.findIndex((x) => (x || "").toLowerCase() === v);
5949
- let flagged;
5950
- if (idx >= 0) {
5951
- arr.splice(idx, 1);
5952
- flagged = false;
5953
- } else {
5954
- arr.push(v);
5955
- flagged = true;
5956
- }
5957
- list[key] = arr;
5958
- await saveAllowlist(list);
6020
+ let flagged = false;
6021
+ await updateAllowlist((list) => {
6022
+ const key = type === "sender" ? "flaggedSenders" : "flaggedDomains";
6023
+ const arr = Array.isArray(list[key]) ? list[key] : [];
6024
+ const idx = arr.findIndex((x) => (x || "").toLowerCase() === v);
6025
+ if (idx >= 0) {
6026
+ arr.splice(idx, 1);
6027
+ flagged = false;
6028
+ } else {
6029
+ arr.push(v);
6030
+ flagged = true;
6031
+ }
6032
+ list[key] = arr;
6033
+ return list;
6034
+ });
5959
6035
  return { flagged };
5960
6036
  }
5961
6037
  async updateFlags(accountId, uid, flags) {
@@ -5964,18 +6040,19 @@ var WebMailxService = class _WebMailxService {
5964
6040
  }
5965
6041
  // ── Remote content allow-list ──
5966
6042
  async allowRemoteContent(type, value) {
5967
- const list = await loadAllowlist();
5968
- if (type === "sender" && !list.senders.includes(value))
5969
- list.senders.push(value);
5970
- else if (type === "domain" && !list.domains.includes(value))
5971
- list.domains.push(value);
5972
- else if (type === "recipient") {
5973
- if (!list.recipients)
5974
- list.recipients = [];
5975
- if (!list.recipients.includes(value))
5976
- list.recipients.push(value);
5977
- }
5978
- await saveAllowlist(list);
6043
+ await updateAllowlist((list) => {
6044
+ if (type === "sender" && !list.senders.includes(value))
6045
+ list.senders.push(value);
6046
+ else if (type === "domain" && !list.domains.includes(value))
6047
+ list.domains.push(value);
6048
+ else if (type === "recipient") {
6049
+ if (!list.recipients)
6050
+ list.recipients = [];
6051
+ if (!list.recipients.includes(value))
6052
+ list.recipients.push(value);
6053
+ }
6054
+ return list;
6055
+ });
5979
6056
  }
5980
6057
  // ── Search ──
5981
6058
  async search(q, page = 1, pageSize = 50, scope = "all", accountId, folderId) {