@bobfrankston/mailx-store 0.1.112 → 0.1.116

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 (4) hide show
  1. package/db.d.ts +23 -0
  2. package/db.js +47 -0
  3. package/package.json +5 -5
  4. package/store.js +3 -0
package/db.d.ts CHANGED
@@ -654,6 +654,29 @@ export declare class MailxDB {
654
654
  runInTxn<T>(fn: () => T): T;
655
655
  /** Record an address used in sent mail */
656
656
  recordSentAddress(name: string, email: string): void;
657
+ /** Apply one email entry of a Google People contact.
658
+ * (Claude Code 2026-09-06, at Bob's direction)
659
+ *
660
+ * The `google` row for this address is the one Google owns: its name and
661
+ * card follow Google on every sync, so a contact renamed in Google
662
+ * Contacts is renamed here. Discovered rows for the same address are left
663
+ * alone — they hold the name the correspondent's own mail carried, and
664
+ * searchContacts folds the two into one line with the google row on top.
665
+ *
666
+ * Replaces the pre-2026-09-06 sequence recordSentAddress + a bare
667
+ * `UPDATE ... WHERE email = ?` in the People sync, which had two defects,
668
+ * both visible on one contact in Bob's DB: (1) it never wrote `name`, so
669
+ * a google row kept whatever name it was first created with — Tyler's
670
+ * Gmail address stayed "T Nichols" after the contact became "tyler K
671
+ * Nichols" in Google, and typing "Tyler Nichols" in compose found only
672
+ * his protonmail row; (2) the WHERE-by-email update also tried to turn
673
+ * the discovered duplicate into a second google row with the same
674
+ * (source, email, name) and hit the UNIQUE constraint — 4,505
675
+ * "[contacts] field update failed" lines in one day's log, and none of
676
+ * those contacts ever received a card update either.
677
+ *
678
+ * Returns true when the google row was created. */
679
+ upsertGoogleContact(googleId: string, name: string, email: string, card: Partial<ContactFields>): boolean;
657
680
  /** True if `email` (lowercased) appears in the active denylist. Cached
658
681
  * in-memory; refreshed on contacts.jsonc reload via setContactsDenylist. */
659
682
  private _denylist;
package/db.js CHANGED
@@ -3201,6 +3201,53 @@ export class MailxDB {
3201
3201
  }
3202
3202
  this.notifyContactsChanged();
3203
3203
  }
3204
+ /** Apply one email entry of a Google People contact.
3205
+ * (Claude Code 2026-09-06, at Bob's direction)
3206
+ *
3207
+ * The `google` row for this address is the one Google owns: its name and
3208
+ * card follow Google on every sync, so a contact renamed in Google
3209
+ * Contacts is renamed here. Discovered rows for the same address are left
3210
+ * alone — they hold the name the correspondent's own mail carried, and
3211
+ * searchContacts folds the two into one line with the google row on top.
3212
+ *
3213
+ * Replaces the pre-2026-09-06 sequence recordSentAddress + a bare
3214
+ * `UPDATE ... WHERE email = ?` in the People sync, which had two defects,
3215
+ * both visible on one contact in Bob's DB: (1) it never wrote `name`, so
3216
+ * a google row kept whatever name it was first created with — Tyler's
3217
+ * Gmail address stayed "T Nichols" after the contact became "tyler K
3218
+ * Nichols" in Google, and typing "Tyler Nichols" in compose found only
3219
+ * his protonmail row; (2) the WHERE-by-email update also tried to turn
3220
+ * the discovered duplicate into a second google row with the same
3221
+ * (source, email, name) and hit the UNIQUE constraint — 4,505
3222
+ * "[contacts] field update failed" lines in one day's log, and none of
3223
+ * those contacts ever received a card update either.
3224
+ *
3225
+ * Returns true when the google row was created. */
3226
+ upsertGoogleContact(googleId, name, email, card) {
3227
+ const lower = (email || "").toLowerCase();
3228
+ if (!lower || !/^[^\s<>@]+@[^\s<>@]+\.[^\s<>@]+$/.test(lower))
3229
+ return false;
3230
+ name = cleanContactName(name);
3231
+ const now = Date.now();
3232
+ const rows = this.db.prepare("SELECT id FROM contacts WHERE source = 'google' AND lower(email) = ? ORDER BY use_count DESC, id ASC").all(lower);
3233
+ // Older syncs could leave two google rows for one address (the
3234
+ // WHERE-by-email update converted discovered rows). Keep the
3235
+ // most-used one; the rest would collide with the rename below.
3236
+ for (const extra of rows.slice(1))
3237
+ this.db.prepare("DELETE FROM contacts WHERE id = ?").run(extra.id);
3238
+ const cardVals = CONTACT_FIELD_COLUMNS.map(c => card?.[c] ?? "");
3239
+ if (rows.length) {
3240
+ this.db.prepare(`UPDATE contacts SET name = CASE WHEN ? != '' THEN ? ELSE name END, google_id = ?,
3241
+ ${CONTACT_FIELD_COLUMNS.map(c => `${c} = ?`).join(", ")}, updated_at = ?
3242
+ WHERE id = ?`).run(name, name, googleId, ...cardVals, now, rows[0].id);
3243
+ this.notifyContactsChanged();
3244
+ return false;
3245
+ }
3246
+ this.db.prepare(`INSERT INTO contacts (source, google_id, name, email, ${CONTACT_FIELD_COLUMNS.join(", ")}, last_used, use_count, updated_at)
3247
+ VALUES ('google', ?, ?, ?, ${CONTACT_FIELD_COLUMNS.map(() => "?").join(", ")}, 0, 0, ?)`).run(googleId, name, lower, ...cardVals, now);
3248
+ this.notifyContactsChanged();
3249
+ return true;
3250
+ }
3204
3251
  /** True if `email` (lowercased) appears in the active denylist. Cached
3205
3252
  * in-memory; refreshed on contacts.jsonc reload via setContactsDenylist. */
3206
3253
  _denylist = new Set();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/mailx-store",
3
- "version": "0.1.112",
3
+ "version": "0.1.116",
4
4
  "type": "module",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -9,8 +9,8 @@
9
9
  },
10
10
  "license": "ISC",
11
11
  "dependencies": {
12
- "@bobfrankston/mailx-types": "^0.1.82",
13
- "@bobfrankston/mailx-settings": "^0.1.76",
12
+ "@bobfrankston/mailx-types": "^0.1.84",
13
+ "@bobfrankston/mailx-settings": "^0.1.78",
14
14
  "@bobfrankston/mailx-bus": "^0.1.2",
15
15
  "mailparser": "^3.7.2"
16
16
  },
@@ -29,8 +29,8 @@
29
29
  },
30
30
  ".transformedSnapshot": {
31
31
  "dependencies": {
32
- "@bobfrankston/mailx-types": "^0.1.82",
33
- "@bobfrankston/mailx-settings": "^0.1.76",
32
+ "@bobfrankston/mailx-types": "^0.1.84",
33
+ "@bobfrankston/mailx-settings": "^0.1.78",
34
34
  "@bobfrankston/mailx-bus": "^0.1.2",
35
35
  "mailparser": "^3.7.2"
36
36
  }
package/store.js CHANGED
@@ -575,6 +575,9 @@ export class Store {
575
575
  senderTrusted,
576
576
  bodyHtml,
577
577
  bodyText,
578
+ // Services the reader trusts to relay on others' behalf; counts
579
+ // only when the service provably handled the message (2026-09-08).
580
+ trustedIntermediaries: (allowList.trustedIntermediaries || []).map((d) => (d || "").toLowerCase()),
578
581
  };
579
582
  const trust = assessMessageTrust(trustInput);
580
583
  // The server's score travels separately from the findings: a finding is