@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.
- package/bin/mailx.js +8 -3
- package/bin/mailx.js.map +1 -1
- package/bin/mailx.ts +8 -3
- package/client/android-bootstrap.bundle.js +93 -4
- package/client/android-bootstrap.bundle.js.map +3 -3
- package/client/compose/compose.bundle.js +30 -3
- package/client/compose/compose.bundle.js.map +2 -2
- package/client/lib/rmf-tiny.js +41 -3
- package/debug.md +33 -0
- package/package.json +3 -3
- package/packages/mailx-imap/index.d.ts.map +1 -1
- package/packages/mailx-imap/index.js +17 -3
- package/packages/mailx-imap/index.js.map +1 -1
- package/packages/mailx-imap/index.ts +16 -3
- package/packages/mailx-imap/package-lock.json +2 -2
- package/packages/mailx-imap/package.json +1 -1
- package/packages/mailx-service/index.d.ts +9 -0
- package/packages/mailx-service/index.d.ts.map +1 -1
- package/packages/mailx-service/index.js +133 -77
- package/packages/mailx-service/index.js.map +1 -1
- package/packages/mailx-service/index.ts +107 -55
- package/packages/mailx-settings/package.json +1 -1
- package/packages/mailx-store-web/android-bootstrap.d.ts.map +1 -1
- package/packages/mailx-store-web/android-bootstrap.js +25 -0
- package/packages/mailx-store-web/android-bootstrap.js.map +1 -1
- package/packages/mailx-store-web/android-bootstrap.ts +25 -0
- package/packages/mailx-store-web/db.d.ts +7 -0
- package/packages/mailx-store-web/db.d.ts.map +1 -1
- package/packages/mailx-store-web/db.js +24 -1
- package/packages/mailx-store-web/db.js.map +1 -1
- package/packages/mailx-store-web/db.ts +20 -1
- package/packages/mailx-store-web/package.json +1 -1
- package/packages/mailx-store-web/web-service.d.ts +15 -2
- package/packages/mailx-store-web/web-service.d.ts.map +1 -1
- package/packages/mailx-store-web/web-service.js +35 -2
- package/packages/mailx-store-web/web-service.js.map +1 -1
- package/packages/mailx-store-web/web-service.ts +33 -2
- package/packages/mailx-store-web/web-settings.d.ts +6 -0
- package/packages/mailx-store-web/web-settings.d.ts.map +1 -1
- package/packages/mailx-store-web/web-settings.js +20 -0
- package/packages/mailx-store-web/web-settings.js.map +1 -1
- 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.
|
|
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
|
|
6398
|
-
*
|
|
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 };
|