@bobfrankston/mailx-store 0.1.74 → 0.1.76

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 (3) hide show
  1. package/db.d.ts +6 -2
  2. package/db.js +46 -16
  3. package/package.json +1 -1
package/db.d.ts CHANGED
@@ -274,8 +274,12 @@ export declare class MailxDB {
274
274
  * During the additive migration we ALSO have to handle the legacy
275
275
  * case where the row's folder_id matches but no message_folders row
276
276
  * was migrated for it (race during initial population, etc.) — we
277
- * fall back to the messages.folder_id check to clean up consistently. */
278
- dropFolderMembership(accountId: string, folderId: number, uid: number, folderPath?: string): boolean;
277
+ * fall back to the messages.folder_id check to clean up consistently.
278
+ *
279
+ * `reason`/`source` override the audit text — deleteMessage routes user
280
+ * deletes through here, and "reconcile: server missing this UID" would
281
+ * be a lie in that audit trail. */
282
+ dropFolderMembership(accountId: string, folderId: number, uid: number, folderPath?: string, reason?: string, source?: string): boolean;
279
283
  upsertMessage(msg: {
280
284
  accountId: string;
281
285
  folderId: number;
package/db.js CHANGED
@@ -1675,8 +1675,12 @@ export class MailxDB {
1675
1675
  * During the additive migration we ALSO have to handle the legacy
1676
1676
  * case where the row's folder_id matches but no message_folders row
1677
1677
  * was migrated for it (race during initial population, etc.) — we
1678
- * fall back to the messages.folder_id check to clean up consistently. */
1679
- dropFolderMembership(accountId, folderId, uid, folderPath) {
1678
+ * fall back to the messages.folder_id check to clean up consistently.
1679
+ *
1680
+ * `reason`/`source` override the audit text — deleteMessage routes user
1681
+ * deletes through here, and "reconcile: server missing this UID" would
1682
+ * be a lie in that audit trail. */
1683
+ dropFolderMembership(accountId, folderId, uid, folderPath, reason, source) {
1680
1684
  // First find the messages row this membership pointed at.
1681
1685
  const mf = this.db.prepare("SELECT message_row_id FROM message_folders WHERE folder_id = ? AND uid = ?").get(folderId, uid);
1682
1686
  let rowId = null;
@@ -1711,8 +1715,8 @@ export class MailxDB {
1711
1715
  this.audit({
1712
1716
  kind: "delete-membership",
1713
1717
  accountId, folderId, uid,
1714
- reason: `reconcile dropped folder membership; ${remaining.cnt} other location(s) remain`,
1715
- source: `dropFolderMembership (${folderPath || ""})`,
1718
+ reason: reason || `reconcile dropped folder membership; ${remaining.cnt} other location(s) remain`,
1719
+ source: source || `dropFolderMembership (${folderPath || ""})`,
1716
1720
  });
1717
1721
  return true;
1718
1722
  }
@@ -1726,8 +1730,8 @@ export class MailxDB {
1726
1730
  accountId, folderId, uid,
1727
1731
  messageId: env.message_id || undefined,
1728
1732
  subject: env.subject || undefined,
1729
- reason: "reconcile: server missing this UID after grace, no other folder memberships",
1730
- source: `dropFolderMembership (${folderPath || ""})`,
1733
+ reason: reason || "reconcile: server missing this UID after grace, no other folder memberships",
1734
+ source: source || `dropFolderMembership (${folderPath || ""})`,
1731
1735
  });
1732
1736
  console.log(` [reconcile-delete] ${accountId} ${folderPath || folderId}/${uid} msgid=${env.message_id || "?"} (no other memberships) — body ${env.body_path || "(none)"}`);
1733
1737
  }
@@ -2310,13 +2314,19 @@ export class MailxDB {
2310
2314
  * hitting the message in `_Spam` (correct) and hitting nothing
2311
2315
  * because the legacy messages.folder_id still points at INBOX. */
2312
2316
  getMessageByUid(accountId, uid, folderId) {
2317
+ // Alias mf.folder_id over m.folder_id too — the envelope must carry
2318
+ // the identity of the membership that MATCHED, not the legacy primary
2319
+ // location, or a caller doing delete/move/flag with the returned
2320
+ // folderId operates on a different folder than the one it looked up
2321
+ // (the two drift: local moves keep the original uid, re-binds update
2322
+ // one table but not the other).
2313
2323
  const sql = folderId != null
2314
- ? `SELECT m.*, mf.uid AS uid
2324
+ ? `SELECT m.*, mf.uid AS uid, mf.folder_id AS folder_id
2315
2325
  FROM messages m
2316
2326
  JOIN message_folders mf ON mf.message_row_id = m.id
2317
2327
  WHERE m.account_id = ? AND mf.uid = ? AND mf.folder_id = ?
2318
2328
  LIMIT 1`
2319
- : `SELECT m.*, mf.uid AS uid
2329
+ : `SELECT m.*, mf.uid AS uid, mf.folder_id AS folder_id
2320
2330
  FROM messages m
2321
2331
  JOIN message_folders mf ON mf.message_row_id = m.id
2322
2332
  WHERE m.account_id = ? AND mf.uid = ?
@@ -2884,14 +2894,31 @@ export class MailxDB {
2884
2894
  * Reason is propagated into the audit_log so the DB carries an
2885
2895
  * authoritative trail of every removal. */
2886
2896
  deleteMessage(accountId, folderId, uid, reason, source) {
2887
- // Get message_id + subject before deleting so the audit row carries
2888
- // enough context to identify what was removed.
2889
- const msg = folderId != null
2890
- ? this.db.prepare("SELECT folder_id, message_id, subject FROM messages WHERE account_id = ? AND folder_id = ? AND uid = ?").get(accountId, folderId, uid)
2891
- : this.db.prepare("SELECT folder_id, message_id, subject FROM messages WHERE account_id = ? AND uid = ?").get(accountId, uid);
2892
- const r = folderId != null
2893
- ? this.db.prepare("DELETE FROM messages WHERE account_id = ? AND folder_id = ? AND uid = ?").run(accountId, folderId, uid)
2894
- : this.db.prepare("DELETE FROM messages WHERE account_id = ? AND uid = ?").run(accountId, uid);
2897
+ if (folderId != null) {
2898
+ // Resolve through message_folders the SAME identity that
2899
+ // getMessageByUid and every list query use. The legacy
2900
+ // messages.folder_id/uid columns can drift from the membership
2901
+ // rows (local moves keep the original uid, server re-binds update
2902
+ // one table but not the other), and a delete keyed only on the
2903
+ // legacy columns silently matches ZERO rows on a drifted message:
2904
+ // the membership keeps it visible in the UI and it becomes
2905
+ // undeletable forever (Eleanor 2026-07-29: ElkinWs uid 125482 in
2906
+ // Trash "refuses to delete" — every attempt no-op'd here while
2907
+ // the sync action reported success). dropFolderMembership drops
2908
+ // the membership, orphan-cleans the messages row, and falls back
2909
+ // to the legacy columns for pre-migration rows.
2910
+ const cleaned = this.dropFolderMembership(accountId, folderId, uid, undefined, reason || "deleteMessage (no reason)", source || "db.deleteMessage");
2911
+ if (!cleaned) {
2912
+ // A delete that deletes nothing is the "refuses to delete"
2913
+ // symptom — never let it pass silently.
2914
+ console.error(` [delete-miss] ${accountId} folder=${folderId} uid=${uid}: no membership or legacy row matched — nothing deleted (source=${source || "db.deleteMessage"})`);
2915
+ }
2916
+ this.recalcFolderCounts(folderId);
2917
+ return;
2918
+ }
2919
+ // No folderId (legacy callers): fall back to the messages-table key.
2920
+ const msg = this.db.prepare("SELECT id, folder_id, message_id, subject FROM messages WHERE account_id = ? AND uid = ?").get(accountId, uid);
2921
+ const r = this.db.prepare("DELETE FROM messages WHERE account_id = ? AND uid = ?").run(accountId, uid);
2895
2922
  if (r.changes && msg) {
2896
2923
  this.audit({
2897
2924
  kind: "delete-msg",
@@ -2904,6 +2931,9 @@ export class MailxDB {
2904
2931
  source: source || "db.deleteMessage",
2905
2932
  });
2906
2933
  }
2934
+ else if (!r.changes) {
2935
+ console.error(` [delete-miss] ${accountId} uid=${uid} (no folderId): no messages row matched — nothing deleted (source=${source || "db.deleteMessage"})`);
2936
+ }
2907
2937
  // Refresh folder counts
2908
2938
  if (msg)
2909
2939
  this.recalcFolderCounts(msg.folder_id);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/mailx-store",
3
- "version": "0.1.74",
3
+ "version": "0.1.76",
4
4
  "type": "module",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",