proton-mail-bridge-client 2.2.3 → 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, 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 = normalizedFilters.label.toLowerCase();
104
- const folderMatch = normalizeMailboxLabel(email.folder)?.toLowerCase() === labelNeedle;
105
- const labelMatch = email.labels.some((label) => normalizeMailboxLabel(label)?.toLowerCase() === 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 = normalizedFilters.attachmentName.toLowerCase();
135
- const match = email.attachments.some((attachment) => (attachment.filename || "").toLowerCase().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
  }
@@ -152,35 +169,34 @@ function matchesIndexedSearch(email, filters) {
152
169
  ...email.from.map((value) => `${value.name ?? ""} ${value.address ?? ""}`),
153
170
  ...email.to.map((value) => `${value.name ?? ""} ${value.address ?? ""}`),
154
171
  ...email.cc.map((value) => `${value.name ?? ""} ${value.address ?? ""}`),
155
- ]
156
- .join("\n")
157
- .toLowerCase();
172
+ ].join("\n");
173
+ const haystackKey = searchKey(haystacks);
158
174
  // searchFtsIds() ANDs every whitespace-separated query token as its own quoted FTS5 term
159
175
  // (so "invoice payment" matches a preview like "payment for the invoice is overdue"), but
160
176
  // this post-filter used to require the ENTIRE joined query as one literal substring —
161
177
  // rejecting anything FTS already matched whose words were merely out of order or separated
162
178
  // by other words. Match FTS's own AND-of-terms semantics instead of a single-phrase substring.
163
179
  if (normalizedFilters.query) {
164
- const terms = normalizedFilters.query.toLowerCase().split(/\s+/).filter(Boolean);
165
- if (terms.length > 0 && !terms.every((term) => haystacks.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)))) {
166
182
  return false;
167
183
  }
168
184
  }
169
185
  if (normalizedFilters.subject &&
170
- !email.subject.toLowerCase().includes(normalizedFilters.subject.toLowerCase())) {
186
+ !searchIncludes(email.subject, normalizedFilters.subject)) {
171
187
  return false;
172
188
  }
173
189
  if (normalizedFilters.from) {
174
- const fromNeedle = normalizedFilters.from.toLowerCase();
175
- const match = email.from.some((value) => `${value.name ?? ""} ${value.address ?? ""}`.toLowerCase().includes(fromNeedle));
190
+ const fromNeedle = normalizedFilters.from;
191
+ const match = email.from.some((value) => searchIncludes(`${value.name ?? ""} ${value.address ?? ""}`, fromNeedle));
176
192
  if (!match) {
177
193
  return false;
178
194
  }
179
195
  }
180
196
  if (normalizedFilters.to) {
181
- const toNeedle = normalizedFilters.to.toLowerCase();
197
+ const toNeedle = normalizedFilters.to;
182
198
  const recipients = [...email.to, ...email.cc, ...email.bcc];
183
- const match = recipients.some((value) => `${value.name ?? ""} ${value.address ?? ""}`.toLowerCase().includes(toNeedle));
199
+ const match = recipients.some((value) => searchIncludes(`${value.name ?? ""} ${value.address ?? ""}`, toNeedle));
184
200
  if (!match) {
185
201
  return false;
186
202
  }
@@ -382,6 +398,11 @@ function safeJsonParse(value, fallback) {
382
398
  return fallback;
383
399
  }
384
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
+ }
385
406
  function emailToSearchParts(email) {
386
407
  return {
387
408
  labels: [normalizeMailboxLabel(email.folder) || email.folder, ...email.labels.map((label) => normalizeMailboxLabel(label) || label)].join(" "),
@@ -763,21 +784,18 @@ export class LocalIndexService {
763
784
  // get plain ThreadSummary shapes, not the full per-message detail.
764
785
  const threads = this.buildThreads(snapshot, true).filter((thread) => {
765
786
  if (input.label) {
766
- const labelNeedle = input.label.toLowerCase();
767
- if (!thread.normalizedLabels.some((label) => label.toLowerCase() === labelNeedle)) {
787
+ const wanted = input.label;
788
+ if (!thread.normalizedLabels.some((label) => searchEquals(label, wanted))) {
768
789
  return false;
769
790
  }
770
791
  }
771
792
  if (input.query) {
772
- const queryNeedle = input.query.toLowerCase();
773
793
  const threadLevelHaystack = [
774
794
  ...thread.participants.map((participant) => `${participant.name ?? ""} ${participant.address ?? ""}`),
775
795
  ...thread.normalizedLabels,
776
- ]
777
- .join("\n")
778
- .toLowerCase();
779
- const matchesAnyMessage = thread.messages.some((message) => message.subject.toLowerCase().includes(queryNeedle));
780
- 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)) {
781
799
  return false;
782
800
  }
783
801
  }
@@ -868,14 +886,14 @@ export class LocalIndexService {
868
886
  conditions.push(`is_read = 0`);
869
887
  }
870
888
  if (input.label) {
871
- const labelNeedle = escapeLike(input.label.toLowerCase());
872
- conditions.push(`(LOWER(folder) LIKE ? ESCAPE '\\' OR LOWER(labels_json) LIKE ? ESCAPE '\\')`);
873
- params.push(`%${labelNeedle}%`, `%${labelNeedle}%`);
889
+ const like = foldedLike(["folder", "labels_json"], input.label);
890
+ conditions.push(like.sql);
891
+ params.push(...like.params);
874
892
  }
875
893
  if (input.query) {
876
- const needle = `%${escapeLike(input.query.toLowerCase())}%`;
877
- conditions.push(`(LOWER(subject) LIKE ? ESCAPE '\\' OR LOWER(preview) LIKE ? ESCAPE '\\' OR LOWER(from_json) LIKE ? ESCAPE '\\' OR LOWER(labels_json) LIKE ? ESCAPE '\\')`);
878
- 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);
879
897
  }
880
898
  const sql = `SELECT * FROM messages${conditions.length > 0 ? ` WHERE ${conditions.join(" AND ")}` : ""}`;
881
899
  const candidateMessages = db
@@ -916,8 +934,8 @@ export class LocalIndexService {
916
934
  return false;
917
935
  }
918
936
  if (input.label) {
919
- const labelNeedle = input.label.toLowerCase();
920
- if (!thread.normalizedLabels.some((label) => label.toLowerCase() === labelNeedle)) {
937
+ const wanted = input.label;
938
+ if (!thread.normalizedLabels.some((label) => searchEquals(label, wanted))) {
921
939
  return false;
922
940
  }
923
941
  }
@@ -927,10 +945,8 @@ export class LocalIndexService {
927
945
  thread.latestPreview || "",
928
946
  ...thread.latestFrom.map((value) => `${value.name ?? ""} ${value.address ?? ""}`),
929
947
  ...thread.normalizedLabels,
930
- ]
931
- .join("\n")
932
- .toLowerCase();
933
- if (!haystack.includes(input.query.toLowerCase())) {
948
+ ].join("\n");
949
+ if (!searchIncludes(haystack, input.query)) {
934
950
  return false;
935
951
  }
936
952
  }
@@ -1151,7 +1167,7 @@ export class LocalIndexService {
1151
1167
  // membership so a matching document thread outside DEFAULT_SNAPSHOT_LIMIT is never
1152
1168
  // silently missed. A message with no attachments can never contribute a document
1153
1169
  // match regardless of its text, so has_attachments = 1 narrows this further.
1154
- const keywordConditions = keywords.map(() => `(LOWER(attachments_json) LIKE ? ESCAPE '\\' OR LOWER(subject) LIKE ? ESCAPE '\\' OR LOWER(preview) LIKE ? ESCAPE '\\' OR LOWER(attachment_text) LIKE ? ESCAPE '\\')`);
1170
+ const keywordConditions = keywords.map(() => `(FOLD(attachments_json) LIKE ? ESCAPE '\\' OR FOLD(subject) LIKE ? ESCAPE '\\' OR FOLD(preview) LIKE ? ESCAPE '\\' OR FOLD(attachment_text) LIKE ? ESCAPE '\\')`);
1155
1171
  const keywordParams = [];
1156
1172
  for (const keyword of keywords) {
1157
1173
  const needle = `%${escapeLike(keyword)}%`;
@@ -1225,19 +1241,19 @@ export class LocalIndexService {
1225
1241
  const db = await this.ensureDb();
1226
1242
  const { ownerEmail, updatedAt, folders, indexedFolders } = this.loadFoldersAndMetadata(db);
1227
1243
  const syncCheckpoints = this.loadCheckpointsSync(db);
1228
- const personNeedle = input.person?.toLowerCase();
1244
+ const personNeedle = input.person || undefined;
1229
1245
  const domainNeedle = input.domain?.toLowerCase();
1230
1246
  const limit = input.limit ?? 10;
1231
1247
  const conditions = [];
1232
1248
  const params = [];
1233
1249
  if (personNeedle) {
1234
- const needle = `%${escapeLike(personNeedle)}%`;
1235
- conditions.push(`(LOWER(from_json) LIKE ? ESCAPE '\\' OR LOWER(to_json) LIKE ? ESCAPE '\\' OR LOWER(cc_json) LIKE ? ESCAPE '\\')`);
1236
- 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);
1237
1253
  }
1238
1254
  if (domainNeedle) {
1239
1255
  const needle = `%@${escapeLike(domainNeedle)}%`;
1240
- conditions.push(`(LOWER(from_json) LIKE ? ESCAPE '\\' OR LOWER(to_json) LIKE ? ESCAPE '\\' OR LOWER(cc_json) LIKE ? ESCAPE '\\')`);
1256
+ conditions.push(`(FOLD(from_json) LIKE ? ESCAPE '\\' OR FOLD(to_json) LIKE ? ESCAPE '\\' OR FOLD(cc_json) LIKE ? ESCAPE '\\')`);
1241
1257
  params.push(needle, needle, needle);
1242
1258
  }
1243
1259
  let messages;
@@ -1263,9 +1279,9 @@ export class LocalIndexService {
1263
1279
  const snapshot = { ownerEmail, updatedAt, folders, indexedFolders, syncCheckpoints, messages };
1264
1280
  const threads = this.buildThreads(snapshot, true)
1265
1281
  .filter((thread) => thread.participants.some((participant) => {
1266
- const participantText = `${participant.name ?? ""} ${participant.address ?? ""}`.toLowerCase();
1282
+ const participantText = `${participant.name ?? ""} ${participant.address ?? ""}`;
1267
1283
  const participantDomain = extractDomain(participant.address || "");
1268
- if (personNeedle && participantText.includes(personNeedle)) {
1284
+ if (personNeedle && searchIncludes(participantText, personNeedle)) {
1269
1285
  return true;
1270
1286
  }
1271
1287
  if (domainNeedle && participantDomain === domainNeedle) {
@@ -1327,6 +1343,9 @@ export class LocalIndexService {
1327
1343
  const db = this.db ?? new Database(this.dbPath);
1328
1344
  if (isFirstOpen) {
1329
1345
  this.chmodDbFiles();
1346
+ // SQLite's LOWER() only folds ASCII and LIKE does not ignore accents, so "łódź" could not find
1347
+ // "Łódź" and "pelcova" could not find "Pelcová". Compare folded text on both sides instead.
1348
+ db.function("FOLD", { deterministic: true }, (value) => (value == null ? null : searchKey(String(value))));
1330
1349
  }
1331
1350
  // auto_vacuum only takes effect on a brand-new/empty database (page_count 0) —
1332
1351
  // setting the pragma alone does NOT retroactively enable incremental vacuuming
@@ -1354,6 +1373,7 @@ export class LocalIndexService {
1354
1373
  db.exec("VACUUM");
1355
1374
  }
1356
1375
  this.runMigrations(db);
1376
+ this.rebuildFtsIfNeeded(db);
1357
1377
  this.db = db;
1358
1378
  await this.maybeImportLegacyIndex(db);
1359
1379
  this.initialized = true;
@@ -1709,10 +1729,8 @@ export class LocalIndexService {
1709
1729
  is_automated: email.isAutomated === undefined ? null : email.isAutomated ? 1 : 0,
1710
1730
  delivered_to: email.deliveredTo ?? null,
1711
1731
  });
1712
- const mergedPreview = persisted.preview ?? "";
1713
- const search = emailToSearchParts({ ...email, attachmentText: persisted.attachment_text ?? undefined });
1714
1732
  deleteFts.run(email.id);
1715
- 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));
1716
1734
  }
1717
1735
  if (cleanupExpunged) {
1718
1736
  createSnapshotUidTable.run();
@@ -1779,6 +1797,37 @@ export class LocalIndexService {
1779
1797
  db.pragma("incremental_vacuum");
1780
1798
  }
1781
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
+ }
1782
1831
  runMigrations(db) {
1783
1832
  db.exec(`
1784
1833
  CREATE TABLE IF NOT EXISTS metadata (
@@ -1963,12 +2012,13 @@ export class LocalIndexService {
1963
2012
  params.push(filters.hasAttachment ? 1 : 0);
1964
2013
  }
1965
2014
  if (filters.subject) {
1966
- conditions.push(`LOWER(subject) LIKE ? ESCAPE '\\'`);
1967
- params.push(`%${escapeLike(filters.subject.toLowerCase())}%`);
2015
+ const like = foldedLike(["subject"], filters.subject);
2016
+ conditions.push(like.sql);
2017
+ params.push(...like.params);
1968
2018
  }
1969
2019
  if (filters.senderDomain) {
1970
- conditions.push(`LOWER(from_json) LIKE ? ESCAPE '\\'`);
1971
- params.push(`%${escapeLike(filters.senderDomain.toLowerCase())}%`);
2020
+ conditions.push(`FOLD(from_json) LIKE ? ESCAPE '\\'`);
2021
+ params.push(`%${escapeLike(foldSearchText(filters.senderDomain))}%`);
1972
2022
  }
1973
2023
  // from/to/messageId were accepted by search_indexed_emails but never
1974
2024
  // narrowed the SQL candidate scan — only applied afterward by
@@ -1978,13 +2028,14 @@ export class LocalIndexService {
1978
2028
  // genuine match older than the window instead of finding it. Mirror the
1979
2029
  // existing senderDomain LIKE pattern (a safe superset pre-filter).
1980
2030
  if (filters.from) {
1981
- conditions.push(`LOWER(from_json) LIKE ? ESCAPE '\\'`);
1982
- params.push(`%${escapeLike(filters.from.toLowerCase())}%`);
2031
+ const like = foldedLike(["from_json"], filters.from);
2032
+ conditions.push(like.sql);
2033
+ params.push(...like.params);
1983
2034
  }
1984
2035
  if (filters.to) {
1985
- conditions.push(`(LOWER(to_json) LIKE ? ESCAPE '\\' OR LOWER(cc_json) LIKE ? ESCAPE '\\' OR LOWER(bcc_json) LIKE ? ESCAPE '\\')`);
1986
- const toNeedle = `%${escapeLike(filters.to.toLowerCase())}%`;
1987
- 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);
1988
2039
  }
1989
2040
  if (filters.messageId) {
1990
2041
  conditions.push(`message_id = ?`);
@@ -2035,8 +2086,13 @@ export class LocalIndexService {
2035
2086
  // NOT-prefix, so this searches for the term itself instead of silently dropping
2036
2087
  // it (e.g. `AND gate schematics` now matches "AND", "gate" and "schematics" all
2037
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, '""')}"`;
2038
2092
  const match = parsed.residualTerms
2039
- .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]))
2040
2096
  .join(" AND ");
2041
2097
  if (!match) {
2042
2098
  // No terms survived parsing at all (e.g. an empty/whitespace-only query), so
@@ -2148,9 +2204,9 @@ export class LocalIndexService {
2148
2204
  messageParams.push(options.folder);
2149
2205
  }
2150
2206
  if (options.label) {
2151
- const labelNeedle = escapeLike(options.label.toLowerCase());
2152
- messageConditions.push(`(LOWER(folder) LIKE ? ESCAPE '\\' OR LOWER(labels_json) LIKE ? ESCAPE '\\')`);
2153
- messageParams.push(`%${labelNeedle}%`, `%${labelNeedle}%`);
2207
+ const like = foldedLike(["folder", "labels_json"], options.label);
2208
+ messageConditions.push(like.sql);
2209
+ messageParams.push(...like.params);
2154
2210
  }
2155
2211
  if (typeof options.isRead === "boolean") {
2156
2212
  messageConditions.push(`is_read = ?`);
@@ -2190,14 +2246,14 @@ export class LocalIndexService {
2190
2246
  params.push(input.folder);
2191
2247
  }
2192
2248
  if (input.label) {
2193
- const labelNeedle = escapeLike(input.label.toLowerCase());
2194
- conditions.push(`(LOWER(folder) LIKE ? ESCAPE '\\' OR LOWER(labels_json) LIKE ? ESCAPE '\\')`);
2195
- params.push(`%${labelNeedle}%`, `%${labelNeedle}%`);
2249
+ const like = foldedLike(["folder", "labels_json"], input.label);
2250
+ conditions.push(like.sql);
2251
+ params.push(...like.params);
2196
2252
  }
2197
2253
  if (input.query) {
2198
- const needle = `%${escapeLike(input.query.toLowerCase())}%`;
2199
- conditions.push(`(LOWER(subject) LIKE ? ESCAPE '\\' OR LOWER(from_json) LIKE ? ESCAPE '\\' OR LOWER(to_json) LIKE ? ESCAPE '\\' OR LOWER(cc_json) LIKE ? ESCAPE '\\' OR LOWER(bcc_json) LIKE ? ESCAPE '\\' OR LOWER(labels_json) LIKE ? ESCAPE '\\')`);
2200
- 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);
2201
2257
  }
2202
2258
  const sql = `SELECT * FROM messages${conditions.length > 0 ? ` WHERE ${conditions.join(" AND ")}` : ""}`;
2203
2259
  const candidates = db