proton-mail-bridge-client 2.2.4 → 2.3.0

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.
@@ -119,6 +119,7 @@ export declare class LocalIndexService {
119
119
  private ensureDb;
120
120
  private chmodDbFiles;
121
121
  private applySnapshot;
122
+ private rebuildFtsIfNeeded;
122
123
  private runMigrations;
123
124
  private ensureMessagesColumns;
124
125
  private maybeImportLegacyIndex;
@@ -1 +1 @@
1
- {"version":3,"file":"local-index-service.d.ts","sourceRoot":"","sources":["../../src/services/local-index-service.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"local-index-service.d.ts","sourceRoot":"","sources":["../../src/services/local-index-service.ts"],"names":[],"mappings":"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"}
@@ -3,9 +3,13 @@ import { chmodSync } from "node:fs";
3
3
  import { mkdir, readFile, rm } from "node:fs/promises";
4
4
  import { dirname, join } from "node:path";
5
5
  import { ensureAccountIdentityMatches } from "../utils/account-identity.js";
6
- import { createEmailId, dedupeEmails, extractDomain, extractMessageIdList, foldSearchText, lowerCaseAddress, nextDay, normalizeMailboxLabel, normalizeMessageId, normalizeSubjectForThread, sortEmailsByNewest, } from "../utils/helpers.js";
6
+ import { createEmailId, dedupeEmails, extractDomain, extractMessageIdList, foldSearchText, lowerCaseAddress, nextDay, normalizeMailboxLabel, normalizeMessageId, normalizeSubjectForThread, searchEquals, searchIncludes, searchKey, searchNeedles, sortEmailsByNewest, } from "../utils/helpers.js";
7
7
  import { logger } from "../utils/logger.js";
8
8
  const DB_SCHEMA_VERSION = 3;
9
+ // Version of what the full-text index stores. 2 = every column holds the text's searchKey (folded,
10
+ // with Müller also as "mueller"), so ł, ß, ø and the Mueller spelling are found. Bumping it makes the
11
+ // next open rebuild the index from the messages table, once.
12
+ const FTS_KEY_VERSION = "2";
9
13
  const STALE_THRESHOLD_MINUTES = 60;
10
14
  const DEFAULT_SNAPSHOT_LIMIT = 5000;
11
15
  // Results older than this carry `stale:true` plus a warning. Deliberately much lower than
@@ -48,6 +52,19 @@ export function shapeThreadForList(thread) {
48
52
  function escapeLike(value) {
49
53
  return value.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
50
54
  }
55
+ // WHERE fragment: any of `columns` contains `needle`, ignoring case, accents and spelling variants
56
+ // (Müller/Mueller, see searchKey). FOLD() is the SQL function registered in ensureDb.
57
+ function foldedLike(columns, needle) {
58
+ const parts = [];
59
+ const params = [];
60
+ for (const form of searchNeedles(needle)) {
61
+ for (const column of columns) {
62
+ parts.push(`FOLD(${column}) LIKE ? ESCAPE '\\'`);
63
+ params.push(`%${escapeLike(form)}%`);
64
+ }
65
+ }
66
+ return { sql: `(${parts.join(" OR ")})`, params };
67
+ }
51
68
  function parseSearchQuery(query) {
52
69
  const parsed = { residualTerms: [] };
53
70
  for (const rawToken of query?.trim().split(/\s+/) ?? []) {
@@ -100,9 +117,9 @@ function matchesIndexedSearch(email, filters) {
100
117
  return false;
101
118
  }
102
119
  if (normalizedFilters.label) {
103
- const labelNeedle = foldSearchText(normalizedFilters.label);
104
- const folderMatch = foldSearchText(normalizeMailboxLabel(email.folder) ?? "") === labelNeedle;
105
- const labelMatch = email.labels.some((label) => foldSearchText(normalizeMailboxLabel(label) ?? "") === labelNeedle);
120
+ const wanted = normalizedFilters.label;
121
+ const folderMatch = searchEquals(normalizeMailboxLabel(email.folder) ?? "", wanted);
122
+ const labelMatch = email.labels.some((label) => searchEquals(normalizeMailboxLabel(label) ?? "", wanted));
106
123
  if (!folderMatch && !labelMatch) {
107
124
  return false;
108
125
  }
@@ -131,8 +148,8 @@ function matchesIndexedSearch(email, filters) {
131
148
  return false;
132
149
  }
133
150
  if (normalizedFilters.attachmentName) {
134
- const attachmentNeedle = foldSearchText(normalizedFilters.attachmentName);
135
- const match = email.attachments.some((attachment) => foldSearchText(attachment.filename || "").includes(attachmentNeedle));
151
+ const attachmentNeedle = normalizedFilters.attachmentName;
152
+ const match = email.attachments.some((attachment) => searchIncludes(attachment.filename || "", attachmentNeedle));
136
153
  if (!match) {
137
154
  return false;
138
155
  }
@@ -153,33 +170,33 @@ function matchesIndexedSearch(email, filters) {
153
170
  ...email.to.map((value) => `${value.name ?? ""} ${value.address ?? ""}`),
154
171
  ...email.cc.map((value) => `${value.name ?? ""} ${value.address ?? ""}`),
155
172
  ].join("\n");
156
- const foldedHaystacks = foldSearchText(haystacks);
173
+ const haystackKey = searchKey(haystacks);
157
174
  // searchFtsIds() ANDs every whitespace-separated query token as its own quoted FTS5 term
158
175
  // (so "invoice payment" matches a preview like "payment for the invoice is overdue"), but
159
176
  // this post-filter used to require the ENTIRE joined query as one literal substring —
160
177
  // rejecting anything FTS already matched whose words were merely out of order or separated
161
178
  // by other words. Match FTS's own AND-of-terms semantics instead of a single-phrase substring.
162
179
  if (normalizedFilters.query) {
163
- const terms = foldSearchText(normalizedFilters.query).split(/\s+/).filter(Boolean);
164
- if (terms.length > 0 && !terms.every((term) => foldedHaystacks.includes(term))) {
180
+ const terms = normalizedFilters.query.split(/\s+/).filter(Boolean);
181
+ if (terms.length > 0 && !terms.every((term) => searchNeedles(term).some((form) => haystackKey.includes(form)))) {
165
182
  return false;
166
183
  }
167
184
  }
168
185
  if (normalizedFilters.subject &&
169
- !foldSearchText(email.subject).includes(foldSearchText(normalizedFilters.subject))) {
186
+ !searchIncludes(email.subject, normalizedFilters.subject)) {
170
187
  return false;
171
188
  }
172
189
  if (normalizedFilters.from) {
173
- const fromNeedle = foldSearchText(normalizedFilters.from);
174
- const match = email.from.some((value) => foldSearchText(`${value.name ?? ""} ${value.address ?? ""}`).includes(fromNeedle));
190
+ const fromNeedle = normalizedFilters.from;
191
+ const match = email.from.some((value) => searchIncludes(`${value.name ?? ""} ${value.address ?? ""}`, fromNeedle));
175
192
  if (!match) {
176
193
  return false;
177
194
  }
178
195
  }
179
196
  if (normalizedFilters.to) {
180
- const toNeedle = foldSearchText(normalizedFilters.to);
197
+ const toNeedle = normalizedFilters.to;
181
198
  const recipients = [...email.to, ...email.cc, ...email.bcc];
182
- const match = recipients.some((value) => foldSearchText(`${value.name ?? ""} ${value.address ?? ""}`).includes(toNeedle));
199
+ const match = recipients.some((value) => searchIncludes(`${value.name ?? ""} ${value.address ?? ""}`, toNeedle));
183
200
  if (!match) {
184
201
  return false;
185
202
  }
@@ -381,6 +398,11 @@ function safeJsonParse(value, fallback) {
381
398
  return fallback;
382
399
  }
383
400
  }
401
+ // The six searchable columns of messages_fts, in table order after email_id, as their searchKey.
402
+ function ftsColumns(email, preview, attachmentText) {
403
+ const parts = emailToSearchParts({ ...email, attachmentText });
404
+ return [email.subject, preview, email.folder, parts.labels, parts.participants, parts.attachmentNames].map(searchKey);
405
+ }
384
406
  function emailToSearchParts(email) {
385
407
  return {
386
408
  labels: [normalizeMailboxLabel(email.folder) || email.folder, ...email.labels.map((label) => normalizeMailboxLabel(label) || label)].join(" "),
@@ -762,19 +784,18 @@ export class LocalIndexService {
762
784
  // get plain ThreadSummary shapes, not the full per-message detail.
763
785
  const threads = this.buildThreads(snapshot, true).filter((thread) => {
764
786
  if (input.label) {
765
- const labelNeedle = foldSearchText(input.label);
766
- if (!thread.normalizedLabels.some((label) => foldSearchText(label) === labelNeedle)) {
787
+ const wanted = input.label;
788
+ if (!thread.normalizedLabels.some((label) => searchEquals(label, wanted))) {
767
789
  return false;
768
790
  }
769
791
  }
770
792
  if (input.query) {
771
- const queryNeedle = foldSearchText(input.query);
772
- const threadLevelHaystack = foldSearchText([
793
+ const threadLevelHaystack = [
773
794
  ...thread.participants.map((participant) => `${participant.name ?? ""} ${participant.address ?? ""}`),
774
795
  ...thread.normalizedLabels,
775
- ].join("\n"));
776
- const matchesAnyMessage = thread.messages.some((message) => foldSearchText(message.subject).includes(queryNeedle));
777
- if (!matchesAnyMessage && !threadLevelHaystack.includes(queryNeedle)) {
796
+ ].join("\n");
797
+ const matchesAnyMessage = thread.messages.some((message) => searchIncludes(message.subject, input.query));
798
+ if (!matchesAnyMessage && !searchIncludes(threadLevelHaystack, input.query)) {
778
799
  return false;
779
800
  }
780
801
  }
@@ -865,14 +886,14 @@ export class LocalIndexService {
865
886
  conditions.push(`is_read = 0`);
866
887
  }
867
888
  if (input.label) {
868
- const labelNeedle = escapeLike(foldSearchText(input.label));
869
- conditions.push(`(FOLD(folder) LIKE ? ESCAPE '\\' OR FOLD(labels_json) LIKE ? ESCAPE '\\')`);
870
- params.push(`%${labelNeedle}%`, `%${labelNeedle}%`);
889
+ const like = foldedLike(["folder", "labels_json"], input.label);
890
+ conditions.push(like.sql);
891
+ params.push(...like.params);
871
892
  }
872
893
  if (input.query) {
873
- const needle = `%${escapeLike(foldSearchText(input.query))}%`;
874
- conditions.push(`(FOLD(subject) LIKE ? ESCAPE '\\' OR FOLD(preview) LIKE ? ESCAPE '\\' OR FOLD(from_json) LIKE ? ESCAPE '\\' OR FOLD(labels_json) LIKE ? ESCAPE '\\')`);
875
- params.push(needle, needle, needle, needle);
894
+ const like = foldedLike(["subject", "preview", "from_json", "labels_json"], input.query);
895
+ conditions.push(like.sql);
896
+ params.push(...like.params);
876
897
  }
877
898
  const sql = `SELECT * FROM messages${conditions.length > 0 ? ` WHERE ${conditions.join(" AND ")}` : ""}`;
878
899
  const candidateMessages = db
@@ -913,19 +934,19 @@ export class LocalIndexService {
913
934
  return false;
914
935
  }
915
936
  if (input.label) {
916
- const labelNeedle = foldSearchText(input.label);
917
- if (!thread.normalizedLabels.some((label) => foldSearchText(label) === labelNeedle)) {
937
+ const wanted = input.label;
938
+ if (!thread.normalizedLabels.some((label) => searchEquals(label, wanted))) {
918
939
  return false;
919
940
  }
920
941
  }
921
942
  if (input.query) {
922
- const haystack = foldSearchText([
943
+ const haystack = [
923
944
  thread.subject,
924
945
  thread.latestPreview || "",
925
946
  ...thread.latestFrom.map((value) => `${value.name ?? ""} ${value.address ?? ""}`),
926
947
  ...thread.normalizedLabels,
927
- ].join("\n"));
928
- if (!haystack.includes(foldSearchText(input.query))) {
948
+ ].join("\n");
949
+ if (!searchIncludes(haystack, input.query)) {
929
950
  return false;
930
951
  }
931
952
  }
@@ -1220,15 +1241,15 @@ export class LocalIndexService {
1220
1241
  const db = await this.ensureDb();
1221
1242
  const { ownerEmail, updatedAt, folders, indexedFolders } = this.loadFoldersAndMetadata(db);
1222
1243
  const syncCheckpoints = this.loadCheckpointsSync(db);
1223
- const personNeedle = input.person ? foldSearchText(input.person) : undefined;
1244
+ const personNeedle = input.person || undefined;
1224
1245
  const domainNeedle = input.domain?.toLowerCase();
1225
1246
  const limit = input.limit ?? 10;
1226
1247
  const conditions = [];
1227
1248
  const params = [];
1228
1249
  if (personNeedle) {
1229
- const needle = `%${escapeLike(personNeedle)}%`;
1230
- conditions.push(`(FOLD(from_json) LIKE ? ESCAPE '\\' OR FOLD(to_json) LIKE ? ESCAPE '\\' OR FOLD(cc_json) LIKE ? ESCAPE '\\')`);
1231
- params.push(needle, needle, needle);
1250
+ const like = foldedLike(["from_json", "to_json", "cc_json"], personNeedle);
1251
+ conditions.push(like.sql);
1252
+ params.push(...like.params);
1232
1253
  }
1233
1254
  if (domainNeedle) {
1234
1255
  const needle = `%@${escapeLike(domainNeedle)}%`;
@@ -1258,9 +1279,9 @@ export class LocalIndexService {
1258
1279
  const snapshot = { ownerEmail, updatedAt, folders, indexedFolders, syncCheckpoints, messages };
1259
1280
  const threads = this.buildThreads(snapshot, true)
1260
1281
  .filter((thread) => thread.participants.some((participant) => {
1261
- const participantText = foldSearchText(`${participant.name ?? ""} ${participant.address ?? ""}`);
1282
+ const participantText = `${participant.name ?? ""} ${participant.address ?? ""}`;
1262
1283
  const participantDomain = extractDomain(participant.address || "");
1263
- if (personNeedle && participantText.includes(personNeedle)) {
1284
+ if (personNeedle && searchIncludes(participantText, personNeedle)) {
1264
1285
  return true;
1265
1286
  }
1266
1287
  if (domainNeedle && participantDomain === domainNeedle) {
@@ -1324,7 +1345,7 @@ export class LocalIndexService {
1324
1345
  this.chmodDbFiles();
1325
1346
  // SQLite's LOWER() only folds ASCII and LIKE does not ignore accents, so "łódź" could not find
1326
1347
  // "Łódź" and "pelcova" could not find "Pelcová". Compare folded text on both sides instead.
1327
- db.function("FOLD", { deterministic: true }, (value) => (value == null ? null : foldSearchText(String(value))));
1348
+ db.function("FOLD", { deterministic: true }, (value) => (value == null ? null : searchKey(String(value))));
1328
1349
  }
1329
1350
  // auto_vacuum only takes effect on a brand-new/empty database (page_count 0) —
1330
1351
  // setting the pragma alone does NOT retroactively enable incremental vacuuming
@@ -1352,6 +1373,7 @@ export class LocalIndexService {
1352
1373
  db.exec("VACUUM");
1353
1374
  }
1354
1375
  this.runMigrations(db);
1376
+ this.rebuildFtsIfNeeded(db);
1355
1377
  this.db = db;
1356
1378
  await this.maybeImportLegacyIndex(db);
1357
1379
  this.initialized = true;
@@ -1707,10 +1729,8 @@ export class LocalIndexService {
1707
1729
  is_automated: email.isAutomated === undefined ? null : email.isAutomated ? 1 : 0,
1708
1730
  delivered_to: email.deliveredTo ?? null,
1709
1731
  });
1710
- const mergedPreview = persisted.preview ?? "";
1711
- const search = emailToSearchParts({ ...email, attachmentText: persisted.attachment_text ?? undefined });
1712
1732
  deleteFts.run(email.id);
1713
- insertFts.run(email.id, email.subject, mergedPreview, email.folder, search.labels, search.participants, search.attachmentNames);
1733
+ insertFts.run(email.id, ...ftsColumns(email, persisted.preview ?? "", persisted.attachment_text ?? undefined));
1714
1734
  }
1715
1735
  if (cleanupExpunged) {
1716
1736
  createSnapshotUidTable.run();
@@ -1777,6 +1797,37 @@ export class LocalIndexService {
1777
1797
  db.pragma("incremental_vacuum");
1778
1798
  }
1779
1799
  }
1800
+ // Rebuilds messages_fts from the messages table when it was written by an older version (text stored
1801
+ // as-is, so "lodzi" could not find "Łodzi"). One transaction: either every row is converted or none.
1802
+ rebuildFtsIfNeeded(db) {
1803
+ const stored = db.prepare(`SELECT value FROM metadata WHERE key = 'ftsKeyVersion'`).get();
1804
+ if (stored?.value === FTS_KEY_VERSION)
1805
+ return;
1806
+ const total = Number(db.prepare(`SELECT COUNT(*) AS count FROM messages`).get().count);
1807
+ if (total > 0) {
1808
+ this.log.info(`Rebuilding the full-text index for ${total} messages (one-time)`, "LocalIndexService");
1809
+ }
1810
+ const insertFts = db.prepare(`
1811
+ INSERT INTO messages_fts (email_id, subject, preview, folder, labels, participants, attachment_names)
1812
+ VALUES (?, ?, ?, ?, ?, ?, ?)
1813
+ `);
1814
+ const nextBatch = db.prepare(`SELECT rowid AS _rowid, * FROM messages WHERE rowid > ? ORDER BY rowid LIMIT 500`);
1815
+ db.transaction(() => {
1816
+ db.exec(`DELETE FROM messages_fts`);
1817
+ let after = 0;
1818
+ for (;;) {
1819
+ const rows = nextBatch.all(after);
1820
+ if (rows.length === 0)
1821
+ break;
1822
+ for (const row of rows) {
1823
+ const email = this.rowToEmailSummary(row);
1824
+ insertFts.run(email.id, ...ftsColumns(email, row.preview ?? "", row.attachment_text ?? undefined));
1825
+ }
1826
+ after = rows[rows.length - 1]._rowid;
1827
+ }
1828
+ db.prepare(`INSERT INTO metadata (key, value) VALUES ('ftsKeyVersion', ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value`).run(FTS_KEY_VERSION);
1829
+ })();
1830
+ }
1780
1831
  runMigrations(db) {
1781
1832
  db.exec(`
1782
1833
  CREATE TABLE IF NOT EXISTS metadata (
@@ -1961,8 +2012,9 @@ export class LocalIndexService {
1961
2012
  params.push(filters.hasAttachment ? 1 : 0);
1962
2013
  }
1963
2014
  if (filters.subject) {
1964
- conditions.push(`FOLD(subject) LIKE ? ESCAPE '\\'`);
1965
- params.push(`%${escapeLike(foldSearchText(filters.subject))}%`);
2015
+ const like = foldedLike(["subject"], filters.subject);
2016
+ conditions.push(like.sql);
2017
+ params.push(...like.params);
1966
2018
  }
1967
2019
  if (filters.senderDomain) {
1968
2020
  conditions.push(`FOLD(from_json) LIKE ? ESCAPE '\\'`);
@@ -1976,13 +2028,14 @@ export class LocalIndexService {
1976
2028
  // genuine match older than the window instead of finding it. Mirror the
1977
2029
  // existing senderDomain LIKE pattern (a safe superset pre-filter).
1978
2030
  if (filters.from) {
1979
- conditions.push(`FOLD(from_json) LIKE ? ESCAPE '\\'`);
1980
- params.push(`%${escapeLike(foldSearchText(filters.from))}%`);
2031
+ const like = foldedLike(["from_json"], filters.from);
2032
+ conditions.push(like.sql);
2033
+ params.push(...like.params);
1981
2034
  }
1982
2035
  if (filters.to) {
1983
- conditions.push(`(FOLD(to_json) LIKE ? ESCAPE '\\' OR FOLD(cc_json) LIKE ? ESCAPE '\\' OR FOLD(bcc_json) LIKE ? ESCAPE '\\')`);
1984
- const toNeedle = `%${escapeLike(foldSearchText(filters.to))}%`;
1985
- params.push(toNeedle, toNeedle, toNeedle);
2036
+ const like = foldedLike(["to_json", "cc_json", "bcc_json"], filters.to);
2037
+ conditions.push(like.sql);
2038
+ params.push(...like.params);
1986
2039
  }
1987
2040
  if (filters.messageId) {
1988
2041
  conditions.push(`message_id = ?`);
@@ -2033,8 +2086,13 @@ export class LocalIndexService {
2033
2086
  // NOT-prefix, so this searches for the term itself instead of silently dropping
2034
2087
  // it (e.g. `AND gate schematics` now matches "AND", "gate" and "schematics" all
2035
2088
  // required, rather than dropping "AND" and searching only the other two).
2089
+ // Each term is looked for in its folded form(s): the stored text is a searchKey (see
2090
+ // rebuildFtsIfNeeded), so Müller is stored as "muller" and "mueller" and either finds it.
2091
+ const quote = (form) => `"${form.replace(/"/g, '""')}"`;
2036
2092
  const match = parsed.residualTerms
2037
- .map((token) => `"${token.replace(/"/g, '""')}"`)
2093
+ .map((token) => searchNeedles(token).filter(Boolean).map(quote))
2094
+ .filter((forms) => forms.length > 0)
2095
+ .map((forms) => (forms.length > 1 ? `(${forms.join(" OR ")})` : forms[0]))
2038
2096
  .join(" AND ");
2039
2097
  if (!match) {
2040
2098
  // No terms survived parsing at all (e.g. an empty/whitespace-only query), so
@@ -2146,9 +2204,9 @@ export class LocalIndexService {
2146
2204
  messageParams.push(options.folder);
2147
2205
  }
2148
2206
  if (options.label) {
2149
- const labelNeedle = escapeLike(foldSearchText(options.label));
2150
- messageConditions.push(`(FOLD(folder) LIKE ? ESCAPE '\\' OR FOLD(labels_json) LIKE ? ESCAPE '\\')`);
2151
- messageParams.push(`%${labelNeedle}%`, `%${labelNeedle}%`);
2207
+ const like = foldedLike(["folder", "labels_json"], options.label);
2208
+ messageConditions.push(like.sql);
2209
+ messageParams.push(...like.params);
2152
2210
  }
2153
2211
  if (typeof options.isRead === "boolean") {
2154
2212
  messageConditions.push(`is_read = ?`);
@@ -2188,14 +2246,14 @@ export class LocalIndexService {
2188
2246
  params.push(input.folder);
2189
2247
  }
2190
2248
  if (input.label) {
2191
- const labelNeedle = escapeLike(foldSearchText(input.label));
2192
- conditions.push(`(FOLD(folder) LIKE ? ESCAPE '\\' OR FOLD(labels_json) LIKE ? ESCAPE '\\')`);
2193
- params.push(`%${labelNeedle}%`, `%${labelNeedle}%`);
2249
+ const like = foldedLike(["folder", "labels_json"], input.label);
2250
+ conditions.push(like.sql);
2251
+ params.push(...like.params);
2194
2252
  }
2195
2253
  if (input.query) {
2196
- const needle = `%${escapeLike(foldSearchText(input.query))}%`;
2197
- conditions.push(`(FOLD(subject) LIKE ? ESCAPE '\\' OR FOLD(from_json) LIKE ? ESCAPE '\\' OR FOLD(to_json) LIKE ? ESCAPE '\\' OR FOLD(cc_json) LIKE ? ESCAPE '\\' OR FOLD(bcc_json) LIKE ? ESCAPE '\\' OR FOLD(labels_json) LIKE ? ESCAPE '\\')`);
2198
- params.push(needle, needle, needle, needle, needle, needle);
2254
+ const like = foldedLike(["subject", "from_json", "to_json", "cc_json", "bcc_json", "labels_json"], input.query);
2255
+ conditions.push(like.sql);
2256
+ params.push(...like.params);
2199
2257
  }
2200
2258
  const sql = `SELECT * FROM messages${conditions.length > 0 ? ` WHERE ${conditions.join(" AND ")}` : ""}`;
2201
2259
  const candidates = db