@bobfrankston/mailx-store 0.1.16 → 0.1.18
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/db.js +49 -4
- package/package.json +1 -1
package/db.js
CHANGED
|
@@ -7,7 +7,29 @@ import { DatabaseSync } from "node:sqlite";
|
|
|
7
7
|
import { randomUUID } from "node:crypto";
|
|
8
8
|
import * as path from "node:path";
|
|
9
9
|
import * as fs from "node:fs";
|
|
10
|
+
// libmime has no @types; declare module for the one symbol we use.
|
|
11
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
12
|
+
const _libmimeMod = await import("libmime");
|
|
10
13
|
import { CONTACT_RULES } from "@bobfrankston/mailx-types";
|
|
14
|
+
/** RFC 2047 encoded-word decode. mailparser handles the standard cases
|
|
15
|
+
* (Subject, unquoted display names) but leaves encoded-words inside
|
|
16
|
+
* quoted-strings raw because they're RFC-strictly forbidden there —
|
|
17
|
+
* in practice senders do it anyway. Running libmime.decodeWords as a
|
|
18
|
+
* uniform pass over every header string at the storage seam means
|
|
19
|
+
* every code path (IMAP fetch, Gmail API, manual insert, eml replay)
|
|
20
|
+
* gets the same treatment. Safe on already-decoded strings (no `=?`
|
|
21
|
+
* markers → no-op). */
|
|
22
|
+
const _libmime = _libmimeMod.default || _libmimeMod;
|
|
23
|
+
function decodeHeaderWords(s) {
|
|
24
|
+
if (!s || s.indexOf("=?") < 0)
|
|
25
|
+
return s || "";
|
|
26
|
+
try {
|
|
27
|
+
return _libmime.decodeWords(s);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return s;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
11
33
|
/** Addresses that have no business in autocomplete. Patterns load from
|
|
12
34
|
* contact-rules.jsonc in mailx-types — a single source of truth shipped
|
|
13
35
|
* with every release. To change the rules edit the JSONC and rebuild. */
|
|
@@ -1191,6 +1213,20 @@ export class MailxDB {
|
|
|
1191
1213
|
return true;
|
|
1192
1214
|
}
|
|
1193
1215
|
upsertMessage(msg) {
|
|
1216
|
+
// Uniform RFC 2047 encoded-word decode at the storage seam. Every
|
|
1217
|
+
// path that lands a message here (IMAP fetch, Gmail API, manual
|
|
1218
|
+
// insert) gets the same treatment — no per-callsite patches.
|
|
1219
|
+
// mailparser handles many cases but not encoded-words inside
|
|
1220
|
+
// quoted-strings (RFC-strictly forbidden, common in the wild). Run
|
|
1221
|
+
// libmime over Subject and every address display name once; safe
|
|
1222
|
+
// to call on already-decoded text (no `=?...?=` markers → no-op).
|
|
1223
|
+
msg.subject = decodeHeaderWords(msg.subject);
|
|
1224
|
+
if (msg.from)
|
|
1225
|
+
msg.from = { name: decodeHeaderWords(msg.from.name || ""), address: msg.from.address || "" };
|
|
1226
|
+
if (msg.to)
|
|
1227
|
+
msg.to = msg.to.map(a => ({ name: decodeHeaderWords(a.name || ""), address: a.address || "" }));
|
|
1228
|
+
if (msg.cc)
|
|
1229
|
+
msg.cc = msg.cc.map(a => ({ name: decodeHeaderWords(a.name || ""), address: a.address || "" }));
|
|
1194
1230
|
const existing = this.db.prepare("SELECT id, provider_id FROM messages WHERE account_id = ? AND folder_id = ? AND uid = ?").get(msg.accountId, msg.folderId, msg.uid);
|
|
1195
1231
|
if (existing) {
|
|
1196
1232
|
// Backfill provider_id on existing rows that predate this column —
|
|
@@ -2301,10 +2337,19 @@ export class MailxDB {
|
|
|
2301
2337
|
scopeWhere += " AND " + extraWhere.join(" AND ");
|
|
2302
2338
|
scopeParams.push(...extraParams);
|
|
2303
2339
|
}
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2340
|
+
// Cap COUNT at 1001 — a bare `SELECT COUNT(*)` on FTS5 has to
|
|
2341
|
+
// enumerate every match, which for a common term ("ieee", "the")
|
|
2342
|
+
// can be tens of thousands of rows and several seconds. We never
|
|
2343
|
+
// paginate past a few hundred in the UI anyway, so capping the
|
|
2344
|
+
// count is invisible to the user and turns search from
|
|
2345
|
+
// multi-second to sub-second. UI shows "1000+" when cnt === 1001.
|
|
2346
|
+
const countRow = this.db.prepare(`SELECT COUNT(*) as cnt FROM (
|
|
2347
|
+
SELECT 1 FROM messages m
|
|
2348
|
+
JOIN messages_fts fts ON m.id = fts.rowid
|
|
2349
|
+
LEFT JOIN folders f ON f.id = m.folder_id AND f.account_id = m.account_id
|
|
2350
|
+
WHERE messages_fts MATCH ?${scopeWhere}
|
|
2351
|
+
LIMIT 1001
|
|
2352
|
+
)`).get(ftsQuery, ...scopeParams);
|
|
2308
2353
|
const total = countRow?.cnt || 0;
|
|
2309
2354
|
const rows = this.db.prepare(`SELECT m.*, f.name AS folder_name FROM messages m
|
|
2310
2355
|
JOIN messages_fts fts ON m.id = fts.rowid
|