@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.
- package/chk-search-tmp.mjs +14 -0
- package/cleanup-sent-tmp.mjs +43 -0
- package/client/android-bootstrap.bundle.js +65 -15
- package/client/android-bootstrap.bundle.js.map +2 -2
- package/client/app.bundle.js +361 -282
- package/client/app.bundle.js.map +4 -4
- package/client/app.js +23 -1
- package/client/app.js.map +1 -1
- package/client/app.ts +20 -1
- package/client/components/address-book.js +5 -1
- package/client/components/address-book.js.map +1 -1
- package/client/components/address-book.ts +5 -1
- package/client/components/message-list.js +164 -54
- package/client/components/message-list.js.map +1 -1
- package/client/components/message-list.ts +159 -51
- package/client/components/message-viewer.js +7 -0
- package/client/components/message-viewer.js.map +1 -1
- package/client/components/message-viewer.ts +7 -0
- package/client/compose/compose.bundle.js +4 -4
- package/client/compose/compose.bundle.js.map +2 -2
- package/client/index.html +4 -0
- package/client/lib/api-client.js +4 -4
- package/client/lib/api-client.js.map +1 -1
- package/client/lib/api-client.ts +4 -4
- package/client/lib/mailxapi.js +4 -4
- package/client/styles/components.css +20 -0
- package/package.json +1 -1
- package/packages/mailx-imap/index.d.ts +17 -0
- package/packages/mailx-imap/index.d.ts.map +1 -1
- package/packages/mailx-imap/index.js +86 -4
- package/packages/mailx-imap/index.js.map +1 -1
- package/packages/mailx-imap/index.ts +85 -4
- package/packages/mailx-imap/package-lock.json +2 -2
- package/packages/mailx-imap/package.json +1 -1
- package/packages/mailx-service/index.d.ts +2 -2
- package/packages/mailx-service/index.d.ts.map +1 -1
- package/packages/mailx-service/index.js +6 -4
- package/packages/mailx-service/index.js.map +1 -1
- package/packages/mailx-service/index.ts +7 -5
- package/packages/mailx-service/jsonrpc.js +2 -2
- package/packages/mailx-service/jsonrpc.js.map +1 -1
- package/packages/mailx-service/jsonrpc.ts +2 -2
- package/packages/mailx-service/sync-queue.d.ts +3 -0
- package/packages/mailx-service/sync-queue.d.ts.map +1 -1
- package/packages/mailx-service/sync-queue.js +30 -11
- package/packages/mailx-service/sync-queue.js.map +1 -1
- package/packages/mailx-service/sync-queue.ts +34 -9
- package/packages/mailx-service/sync-worker.js +51 -0
- package/packages/mailx-service/sync-worker.js.map +1 -1
- package/packages/mailx-service/sync-worker.ts +45 -0
- package/packages/mailx-settings/package.json +1 -1
- package/packages/mailx-store/db.d.ts +10 -1
- package/packages/mailx-store/db.d.ts.map +1 -1
- package/packages/mailx-store/db.js +68 -7
- package/packages/mailx-store/db.js.map +1 -1
- package/packages/mailx-store/db.ts +70 -8
- package/packages/mailx-store/package.json +1 -1
- package/packages/mailx-store/store.d.ts +1 -1
- package/packages/mailx-store/store.d.ts.map +1 -1
- package/packages/mailx-store/store.js +2 -2
- package/packages/mailx-store/store.js.map +1 -1
- package/packages/mailx-store/store.ts +2 -2
- package/packages/mailx-store-web/imap-web-provider.d.ts +7 -0
- package/packages/mailx-store-web/imap-web-provider.d.ts.map +1 -1
- package/packages/mailx-store-web/imap-web-provider.js +12 -0
- package/packages/mailx-store-web/imap-web-provider.js.map +1 -1
- package/packages/mailx-store-web/imap-web-provider.ts +15 -0
- package/packages/mailx-store-web/package.json +1 -1
- package/packages/mailx-store-web/sync-manager.d.ts.map +1 -1
- package/packages/mailx-store-web/sync-manager.js +12 -0
- package/packages/mailx-store-web/sync-manager.js.map +1 -1
- package/packages/mailx-store-web/sync-manager.ts +12 -0
- package/packages/mailx-store-web/web-service.d.ts.map +1 -1
- package/packages/mailx-store-web/web-service.js +53 -19
- package/packages/mailx-store-web/web-service.js.map +1 -1
- package/packages/mailx-store-web/web-service.ts +60 -20
- package/packages/mailx-types/index.d.ts +5 -1
- package/packages/mailx-types/index.d.ts.map +1 -1
- package/packages/mailx-types/index.js.map +1 -1
- package/packages/mailx-types/index.ts +5 -1
- 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
|
-
|
|
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
|
|
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
|