@bobfrankston/rmfmail 1.2.87 → 1.2.95

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 (81) hide show
  1. package/chk-search-tmp.mjs +14 -0
  2. package/cleanup-sent-tmp.mjs +43 -0
  3. package/client/android-bootstrap.bundle.js +65 -15
  4. package/client/android-bootstrap.bundle.js.map +2 -2
  5. package/client/app.bundle.js +361 -282
  6. package/client/app.bundle.js.map +4 -4
  7. package/client/app.js +23 -1
  8. package/client/app.js.map +1 -1
  9. package/client/app.ts +20 -1
  10. package/client/components/address-book.js +5 -1
  11. package/client/components/address-book.js.map +1 -1
  12. package/client/components/address-book.ts +5 -1
  13. package/client/components/message-list.js +164 -54
  14. package/client/components/message-list.js.map +1 -1
  15. package/client/components/message-list.ts +159 -51
  16. package/client/components/message-viewer.js +7 -0
  17. package/client/components/message-viewer.js.map +1 -1
  18. package/client/components/message-viewer.ts +7 -0
  19. package/client/compose/compose.bundle.js +4 -4
  20. package/client/compose/compose.bundle.js.map +2 -2
  21. package/client/index.html +4 -0
  22. package/client/lib/api-client.js +4 -4
  23. package/client/lib/api-client.js.map +1 -1
  24. package/client/lib/api-client.ts +4 -4
  25. package/client/lib/mailxapi.js +4 -4
  26. package/client/styles/components.css +20 -0
  27. package/package.json +1 -1
  28. package/packages/mailx-imap/index.d.ts +17 -0
  29. package/packages/mailx-imap/index.d.ts.map +1 -1
  30. package/packages/mailx-imap/index.js +86 -4
  31. package/packages/mailx-imap/index.js.map +1 -1
  32. package/packages/mailx-imap/index.ts +85 -4
  33. package/packages/mailx-imap/package-lock.json +2 -2
  34. package/packages/mailx-imap/package.json +1 -1
  35. package/packages/mailx-service/index.d.ts +2 -2
  36. package/packages/mailx-service/index.d.ts.map +1 -1
  37. package/packages/mailx-service/index.js +6 -4
  38. package/packages/mailx-service/index.js.map +1 -1
  39. package/packages/mailx-service/index.ts +7 -5
  40. package/packages/mailx-service/jsonrpc.js +2 -2
  41. package/packages/mailx-service/jsonrpc.js.map +1 -1
  42. package/packages/mailx-service/jsonrpc.ts +2 -2
  43. package/packages/mailx-service/sync-queue.d.ts +3 -0
  44. package/packages/mailx-service/sync-queue.d.ts.map +1 -1
  45. package/packages/mailx-service/sync-queue.js +30 -11
  46. package/packages/mailx-service/sync-queue.js.map +1 -1
  47. package/packages/mailx-service/sync-queue.ts +34 -9
  48. package/packages/mailx-service/sync-worker.js +51 -0
  49. package/packages/mailx-service/sync-worker.js.map +1 -1
  50. package/packages/mailx-service/sync-worker.ts +45 -0
  51. package/packages/mailx-settings/package.json +1 -1
  52. package/packages/mailx-store/db.d.ts +10 -1
  53. package/packages/mailx-store/db.d.ts.map +1 -1
  54. package/packages/mailx-store/db.js +68 -7
  55. package/packages/mailx-store/db.js.map +1 -1
  56. package/packages/mailx-store/db.ts +70 -8
  57. package/packages/mailx-store/package.json +1 -1
  58. package/packages/mailx-store/store.d.ts +1 -1
  59. package/packages/mailx-store/store.d.ts.map +1 -1
  60. package/packages/mailx-store/store.js +2 -2
  61. package/packages/mailx-store/store.js.map +1 -1
  62. package/packages/mailx-store/store.ts +2 -2
  63. package/packages/mailx-store-web/imap-web-provider.d.ts +7 -0
  64. package/packages/mailx-store-web/imap-web-provider.d.ts.map +1 -1
  65. package/packages/mailx-store-web/imap-web-provider.js +12 -0
  66. package/packages/mailx-store-web/imap-web-provider.js.map +1 -1
  67. package/packages/mailx-store-web/imap-web-provider.ts +15 -0
  68. package/packages/mailx-store-web/package.json +1 -1
  69. package/packages/mailx-store-web/sync-manager.d.ts.map +1 -1
  70. package/packages/mailx-store-web/sync-manager.js +12 -0
  71. package/packages/mailx-store-web/sync-manager.js.map +1 -1
  72. package/packages/mailx-store-web/sync-manager.ts +12 -0
  73. package/packages/mailx-store-web/web-service.d.ts.map +1 -1
  74. package/packages/mailx-store-web/web-service.js +53 -19
  75. package/packages/mailx-store-web/web-service.js.map +1 -1
  76. package/packages/mailx-store-web/web-service.ts +60 -20
  77. package/packages/mailx-types/index.d.ts +5 -1
  78. package/packages/mailx-types/index.d.ts.map +1 -1
  79. package/packages/mailx-types/index.js.map +1 -1
  80. package/packages/mailx-types/index.ts +5 -1
  81. package/packages/mailx-types/package.json +1 -1
@@ -556,6 +556,44 @@ export class MailxDB {
556
556
  // parsed (existing IMAP-synced rows that predate the prefetch-reparse
557
557
  // fix, where has_attachments stays 0 forever). NULL = never parsed.
558
558
  this.addColumnIfMissing("messages", "body_parsed_at", "INTEGER");
559
+ // sent_date: the message's own Date: header (when it was SENT), distinct
560
+ // from `date` which is the per-account arrival time (IMAP INTERNALDATE /
561
+ // Gmail internalDate). Kept as its own column so the list can offer a
562
+ // Sent/Received date basis (Bob 2026-07-01) AND so identical messages
563
+ // delivered to multiple accounts cluster on send time. Backfilled to
564
+ // `date` for existing rows so it is ALWAYS non-null → a plain index sorts
565
+ // it as fast as the received-date path (no COALESCE, no hot-path
566
+ // regression of the getUnifiedInbox streaming walk). Real send times fill
567
+ // in as folders re-sync (or via `-repair`).
568
+ this.addColumnIfMissing("messages", "sent_date", "INTEGER");
569
+ // One-shot heal: strip header-fold whitespace from stored Message-IDs /
570
+ // In-Reply-To. A folded "Message-ID:\r\n\t<...>" header reached the DB
571
+ // as "\t<...>" (pre-trim iflow-direct envelope parser); the tab made
572
+ // every HEADER search for that id return 0 → the sent-sweep re-APPENDed
573
+ // "missing" messages forever (3,707 duplicates of one reply,
574
+ // 2026-07-02). Idempotent — after the first pass the WHERE matches
575
+ // nothing. char(9/10/13)+space covers all RFC 5322 fold whitespace.
576
+ try {
577
+ const ws = "char(9)||char(10)||char(13)||' '";
578
+ this.db.exec(`UPDATE messages SET message_id = TRIM(message_id, ${ws}) WHERE message_id <> TRIM(message_id, ${ws})`);
579
+ this.db.exec(`UPDATE messages SET in_reply_to = TRIM(in_reply_to, ${ws}) WHERE in_reply_to IS NOT NULL AND in_reply_to <> TRIM(in_reply_to, ${ws})`);
580
+ }
581
+ catch { /* non-fatal */ }
582
+ try {
583
+ this.db.exec("CREATE INDEX IF NOT EXISTS idx_messages_folder_sentdate ON messages(account_id, folder_id, sent_date DESC)");
584
+ }
585
+ catch { /* already exists */ }
586
+ try {
587
+ this.db.exec("CREATE INDEX IF NOT EXISTS idx_messages_sentdate ON messages(folder_id, sent_date DESC)");
588
+ }
589
+ catch { /* already exists */ }
590
+ try {
591
+ // One-shot backfill: seed sent_date from date wherever it's still
592
+ // null. Indexed IS NULL check keeps this near-instant after the first
593
+ // pass; new writes set sent_date directly so it converges to a no-op.
594
+ this.db.exec("UPDATE messages SET sent_date = date WHERE sent_date IS NULL");
595
+ }
596
+ catch { /* non-fatal */ }
559
597
  // One-shot is_replied backfill on every boot. SQL-only, indexed
560
598
  // both sides via idx_messages_message_id and idx_messages_in_reply_to,
561
599
  // so a 100k-row mailbox finishes in milliseconds. Idempotent — after
@@ -1757,7 +1795,7 @@ export class MailxDB {
1757
1795
  // subject HERE, but only when the existing mid is empty AND the
1758
1796
  // incoming msg actually has one — never clobber a good value.
1759
1797
  if (msg.messageId && (!existing.message_id || existing.message_id === "")) {
1760
- this.db.prepare("UPDATE messages SET message_id = ?, date = ?, subject = ? WHERE id = ?").run(msg.messageId, msg.date || Date.now(), msg.subject || "", existing.id);
1798
+ this.db.prepare("UPDATE messages SET message_id = ?, date = ?, sent_date = ?, subject = ? WHERE id = ?").run(msg.messageId, msg.date || Date.now(), msg.sentDate ?? msg.date ?? Date.now(), msg.subject || "", existing.id);
1761
1799
  // The row was indexed with an empty subject (it had no envelope),
1762
1800
  // so search missed it. Re-index now that we have the real subject
1763
1801
  // + from/to. Standalone FTS5 → delete + insert. body_text
@@ -1832,10 +1870,10 @@ export class MailxDB {
1832
1870
  const result = this.db.prepare(`
1833
1871
  INSERT INTO messages (
1834
1872
  account_id, folder_id, uid, uuid, message_id, in_reply_to, refs, thread_id,
1835
- date, subject, from_address, from_name, to_json, cc_json,
1873
+ date, sent_date, subject, from_address, from_name, to_json, cc_json,
1836
1874
  flags_json, size, has_attachments, preview, body_path, body_parsed_at, cached_at, provider_id, is_replied
1837
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1838
- `).run(msg.accountId, msg.folderId, msg.uid, uuid, msg.messageId, msg.inReplyTo, JSON.stringify(msg.references), threadId, msg.date, msg.subject, msg.from.address, msg.from.name, JSON.stringify(msg.to), JSON.stringify(msg.cc), JSON.stringify(msg.flags), msg.size, msg.hasAttachments ? 1 : 0, msg.preview, msg.bodyPath, msg.bodyPath ? Date.now() : null, Date.now(), msg.providerId || null, alreadyReplied ? 1 : 0);
1875
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1876
+ `).run(msg.accountId, msg.folderId, msg.uid, uuid, msg.messageId, msg.inReplyTo, JSON.stringify(msg.references), threadId, msg.date, msg.sentDate ?? msg.date, msg.subject, msg.from.address, msg.from.name, JSON.stringify(msg.to), JSON.stringify(msg.cc), JSON.stringify(msg.flags), msg.size, msg.hasAttachments ? 1 : 0, msg.preview, msg.bodyPath, msg.bodyPath ? Date.now() : null, Date.now(), msg.providerId || null, alreadyReplied ? 1 : 0);
1839
1877
  const rowId = Number(result.lastInsertRowid);
1840
1878
  // Forward direction for ↩: if this message is a reply, mark the parent
1841
1879
  // as replied-to. Cheap indexed UPDATE; no-op when the parent isn't in
@@ -1893,7 +1931,10 @@ export class MailxDB {
1893
1931
  const offset = (page - 1) * pageSize;
1894
1932
  const sort = query.sort || "date";
1895
1933
  const sortDir = query.sortDir || "desc";
1896
- const sortCol = sort === "from" ? "m.from_name" : sort === "subject" ? "m.subject" : "m.date";
1934
+ // Date basis: "sent" (Date: header, default clusters identical mail
1935
+ // and matches other clients) or "received" (per-account arrival time).
1936
+ const dateCol = query.dateBasis === "received" ? "m.date" : "m.sent_date";
1937
+ const sortCol = sort === "from" ? "m.from_name" : sort === "subject" ? "m.subject" : dateCol;
1897
1938
  let where = "m.account_id = ? AND mf.folder_id = ?";
1898
1939
  const params = [query.accountId, query.folderId];
1899
1940
  if (query.search) {
@@ -1958,6 +1999,7 @@ export class MailxDB {
1958
1999
  references: JSON.parse(r.refs || "[]"),
1959
2000
  threadId: r.thread_id || undefined,
1960
2001
  date: r.date,
2002
+ sentDate: r.sent_date ?? r.date,
1961
2003
  subject: r.subject,
1962
2004
  from: { name: r.from_name, address: r.from_address },
1963
2005
  to: JSON.parse(r.to_json),
@@ -1976,8 +2018,13 @@ export class MailxDB {
1976
2018
  /** Short-TTL cache for the unified-inbox survivor COUNT (the GROUP BY that
1977
2019
  * scans the whole inbox). Keyed by flaggedOnly+folder-set. */
1978
2020
  _unifiedTotalCache = new Map();
1979
- getUnifiedInbox(page = 1, pageSize = 50, flaggedOnly = false) {
2021
+ getUnifiedInbox(page = 1, pageSize = 50, flaggedOnly = false, dateBasis = "sent") {
1980
2022
  const offset = (page - 1) * pageSize;
2023
+ // Date basis: sort/stream by the Date: header (sent, default) or the
2024
+ // per-account arrival time (received). sent_date is always non-null
2025
+ // (backfilled to date), and idx_messages_sentdate mirrors
2026
+ // idx_messages_folder_date, so the streaming walk stays O(page).
2027
+ const dateCol = dateBasis === "received" ? "m.date" : "m.sent_date";
1981
2028
  // Find all inbox folder IDs
1982
2029
  const inboxRows = this.db.prepare("SELECT id FROM folders WHERE special_use = 'inbox'").all();
1983
2030
  if (inboxRows.length === 0)
@@ -2042,7 +2089,7 @@ export class MailxDB {
2042
2089
  FROM messages m
2043
2090
  JOIN message_folders mf ON mf.message_row_id = m.id
2044
2091
  WHERE mf.folder_id IN (${placeholders})${flagFilter}
2045
- ORDER BY m.date DESC, m.id DESC`);
2092
+ ORDER BY ${dateCol} DESC, m.id DESC`);
2046
2093
  // Per-survivor enrichment (runs only for the ≤pageSize kept rows, not the
2047
2094
  // whole inbox). Same semantics as the old inline correlated subqueries.
2048
2095
  const pendingStmt = this.db.prepare("SELECT 1 FROM sync_actions WHERE account_id = ? AND uid = ? LIMIT 1");
@@ -2083,6 +2130,7 @@ export class MailxDB {
2083
2130
  references: JSON.parse(r.refs || "[]"),
2084
2131
  threadId: r.thread_id || undefined,
2085
2132
  date: r.date,
2133
+ sentDate: r.sent_date ?? r.date,
2086
2134
  subject: r.subject,
2087
2135
  from: { name: r.from_name, address: r.from_address },
2088
2136
  to: JSON.parse(r.to_json),
@@ -2516,6 +2564,19 @@ export class MailxDB {
2516
2564
  WHERE account_id = ? AND folder_id = ? AND cached_at >= ?
2517
2565
  AND message_id IS NOT NULL AND message_id <> ''`).all(accountId, folderId, sinceMs);
2518
2566
  }
2567
+ /** Count local rows in a folder sharing one Message-ID. The Sent-sweep
2568
+ * uses this as a sanity cross-check before re-APPENDing a "missing"
2569
+ * message: >1 local copies mean earlier duplicates synced IN from the
2570
+ * server, so the server-side header search that just said "missing"
2571
+ * was lying (the 3,704-duplicate loop, 2026-07-02). */
2572
+ countMessagesByMessageId(accountId, folderId, messageId) {
2573
+ if (!messageId)
2574
+ return 0;
2575
+ const r = this.db.prepare(`SELECT COUNT(*) AS c FROM messages m
2576
+ JOIN message_folders mf ON mf.message_row_id = m.id
2577
+ WHERE m.account_id = ? AND mf.folder_id = ? AND m.message_id = ?`).get(accountId, folderId, messageId);
2578
+ return r?.c | 0;
2579
+ }
2519
2580
  /** Rebind a local row to a different server UID without re-upserting.
2520
2581
  * Used by the Sent-sweep to repair optimistic-insert mispredictions
2521
2582
  * (local row was inserted at the predicted UIDNEXT but the server