proton-mail-bridge-client 2.2.4 → 2.3.1

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.
@@ -34,6 +34,10 @@ export declare class LocalIndexService {
34
34
  syncedAt: string;
35
35
  folderStats: Array<MailboxSyncCheckpoint>;
36
36
  }): Promise<LocalIndexStatus>;
37
+ getFreshness(): Promise<{
38
+ storedMessageCount: number;
39
+ isStale: boolean;
40
+ }>;
37
41
  getStatus(): Promise<LocalIndexStatus>;
38
42
  private realMessageCounts;
39
43
  search(filters: SearchEmailsInput): Promise<{
@@ -119,6 +123,7 @@ export declare class LocalIndexService {
119
123
  private ensureDb;
120
124
  private chmodDbFiles;
121
125
  private applySnapshot;
126
+ private rebuildFtsIfNeeded;
122
127
  private runMigrations;
123
128
  private ensureMessagesColumns;
124
129
  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,10 +3,20 @@ 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;
14
+ function indexAgeMinutes(updatedAt) {
15
+ return updatedAt ? Math.max(0, Math.round((Date.now() - new Date(updatedAt).getTime()) / 60_000)) : undefined;
16
+ }
17
+ function isIndexStale(ageMinutes) {
18
+ return typeof ageMinutes === "number" ? ageMinutes > STALE_THRESHOLD_MINUTES : true;
19
+ }
10
20
  const DEFAULT_SNAPSHOT_LIMIT = 5000;
11
21
  // Results older than this carry `stale:true` plus a warning. Deliberately much lower than
12
22
  // STALE_THRESHOLD_MINUTES (which gates a full auto-refresh): the background sync runs every
@@ -48,6 +58,19 @@ export function shapeThreadForList(thread) {
48
58
  function escapeLike(value) {
49
59
  return value.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
50
60
  }
61
+ // WHERE fragment: any of `columns` contains `needle`, ignoring case, accents and spelling variants
62
+ // (Müller/Mueller, see searchKey). FOLD() is the SQL function registered in ensureDb.
63
+ function foldedLike(columns, needle) {
64
+ const parts = [];
65
+ const params = [];
66
+ for (const form of searchNeedles(needle)) {
67
+ for (const column of columns) {
68
+ parts.push(`FOLD(${column}) LIKE ? ESCAPE '\\'`);
69
+ params.push(`%${escapeLike(form)}%`);
70
+ }
71
+ }
72
+ return { sql: `(${parts.join(" OR ")})`, params };
73
+ }
51
74
  function parseSearchQuery(query) {
52
75
  const parsed = { residualTerms: [] };
53
76
  for (const rawToken of query?.trim().split(/\s+/) ?? []) {
@@ -100,9 +123,9 @@ function matchesIndexedSearch(email, filters) {
100
123
  return false;
101
124
  }
102
125
  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);
126
+ const wanted = normalizedFilters.label;
127
+ const folderMatch = searchEquals(normalizeMailboxLabel(email.folder) ?? "", wanted);
128
+ const labelMatch = email.labels.some((label) => searchEquals(normalizeMailboxLabel(label) ?? "", wanted));
106
129
  if (!folderMatch && !labelMatch) {
107
130
  return false;
108
131
  }
@@ -131,8 +154,8 @@ function matchesIndexedSearch(email, filters) {
131
154
  return false;
132
155
  }
133
156
  if (normalizedFilters.attachmentName) {
134
- const attachmentNeedle = foldSearchText(normalizedFilters.attachmentName);
135
- const match = email.attachments.some((attachment) => foldSearchText(attachment.filename || "").includes(attachmentNeedle));
157
+ const attachmentNeedle = normalizedFilters.attachmentName;
158
+ const match = email.attachments.some((attachment) => searchIncludes(attachment.filename || "", attachmentNeedle));
136
159
  if (!match) {
137
160
  return false;
138
161
  }
@@ -153,33 +176,33 @@ function matchesIndexedSearch(email, filters) {
153
176
  ...email.to.map((value) => `${value.name ?? ""} ${value.address ?? ""}`),
154
177
  ...email.cc.map((value) => `${value.name ?? ""} ${value.address ?? ""}`),
155
178
  ].join("\n");
156
- const foldedHaystacks = foldSearchText(haystacks);
179
+ const haystackKey = searchKey(haystacks);
157
180
  // searchFtsIds() ANDs every whitespace-separated query token as its own quoted FTS5 term
158
181
  // (so "invoice payment" matches a preview like "payment for the invoice is overdue"), but
159
182
  // this post-filter used to require the ENTIRE joined query as one literal substring —
160
183
  // rejecting anything FTS already matched whose words were merely out of order or separated
161
184
  // by other words. Match FTS's own AND-of-terms semantics instead of a single-phrase substring.
162
185
  if (normalizedFilters.query) {
163
- const terms = foldSearchText(normalizedFilters.query).split(/\s+/).filter(Boolean);
164
- if (terms.length > 0 && !terms.every((term) => foldedHaystacks.includes(term))) {
186
+ const terms = normalizedFilters.query.split(/\s+/).filter(Boolean);
187
+ if (terms.length > 0 && !terms.every((term) => searchNeedles(term).some((form) => haystackKey.includes(form)))) {
165
188
  return false;
166
189
  }
167
190
  }
168
191
  if (normalizedFilters.subject &&
169
- !foldSearchText(email.subject).includes(foldSearchText(normalizedFilters.subject))) {
192
+ !searchIncludes(email.subject, normalizedFilters.subject)) {
170
193
  return false;
171
194
  }
172
195
  if (normalizedFilters.from) {
173
- const fromNeedle = foldSearchText(normalizedFilters.from);
174
- const match = email.from.some((value) => foldSearchText(`${value.name ?? ""} ${value.address ?? ""}`).includes(fromNeedle));
196
+ const fromNeedle = normalizedFilters.from;
197
+ const match = email.from.some((value) => searchIncludes(`${value.name ?? ""} ${value.address ?? ""}`, fromNeedle));
175
198
  if (!match) {
176
199
  return false;
177
200
  }
178
201
  }
179
202
  if (normalizedFilters.to) {
180
- const toNeedle = foldSearchText(normalizedFilters.to);
203
+ const toNeedle = normalizedFilters.to;
181
204
  const recipients = [...email.to, ...email.cc, ...email.bcc];
182
- const match = recipients.some((value) => foldSearchText(`${value.name ?? ""} ${value.address ?? ""}`).includes(toNeedle));
205
+ const match = recipients.some((value) => searchIncludes(`${value.name ?? ""} ${value.address ?? ""}`, toNeedle));
183
206
  if (!match) {
184
207
  return false;
185
208
  }
@@ -381,6 +404,11 @@ function safeJsonParse(value, fallback) {
381
404
  return fallback;
382
405
  }
383
406
  }
407
+ // The six searchable columns of messages_fts, in table order after email_id, as their searchKey.
408
+ function ftsColumns(email, preview, attachmentText) {
409
+ const parts = emailToSearchParts({ ...email, attachmentText });
410
+ return [email.subject, preview, email.folder, parts.labels, parts.participants, parts.attachmentNames].map(searchKey);
411
+ }
384
412
  function emailToSearchParts(email) {
385
413
  return {
386
414
  labels: [normalizeMailboxLabel(email.folder) || email.folder, ...email.labels.map((label) => normalizeMailboxLabel(label) || label)].join(" "),
@@ -482,6 +510,15 @@ export class LocalIndexService {
482
510
  this.snapshotQueue = run.catch(() => undefined);
483
511
  return run;
484
512
  }
513
+ // Just what the refresh-before-search check needs: whether the index has any messages and whether it
514
+ // is older than the stale threshold. getStatus() also builds every thread and label from up to 5000
515
+ // messages (about 170 ms on a 57,000-message index), which this check ran before every search.
516
+ async getFreshness() {
517
+ const db = await this.ensureDb();
518
+ const { updatedAt } = this.loadFoldersAndMetadata(db);
519
+ const storedMessageCount = Number(db.prepare(`SELECT COUNT(*) AS count FROM messages`).get().count);
520
+ return { storedMessageCount, isStale: isIndexStale(indexAgeMinutes(updatedAt)) };
521
+ }
485
522
  async getStatus() {
486
523
  const db = await this.ensureDb();
487
524
  const { ownerEmail, updatedAt, folders, indexedFolders } = this.loadFoldersAndMetadata(db);
@@ -762,19 +799,18 @@ export class LocalIndexService {
762
799
  // get plain ThreadSummary shapes, not the full per-message detail.
763
800
  const threads = this.buildThreads(snapshot, true).filter((thread) => {
764
801
  if (input.label) {
765
- const labelNeedle = foldSearchText(input.label);
766
- if (!thread.normalizedLabels.some((label) => foldSearchText(label) === labelNeedle)) {
802
+ const wanted = input.label;
803
+ if (!thread.normalizedLabels.some((label) => searchEquals(label, wanted))) {
767
804
  return false;
768
805
  }
769
806
  }
770
807
  if (input.query) {
771
- const queryNeedle = foldSearchText(input.query);
772
- const threadLevelHaystack = foldSearchText([
808
+ const threadLevelHaystack = [
773
809
  ...thread.participants.map((participant) => `${participant.name ?? ""} ${participant.address ?? ""}`),
774
810
  ...thread.normalizedLabels,
775
- ].join("\n"));
776
- const matchesAnyMessage = thread.messages.some((message) => foldSearchText(message.subject).includes(queryNeedle));
777
- if (!matchesAnyMessage && !threadLevelHaystack.includes(queryNeedle)) {
811
+ ].join("\n");
812
+ const matchesAnyMessage = thread.messages.some((message) => searchIncludes(message.subject, input.query));
813
+ if (!matchesAnyMessage && !searchIncludes(threadLevelHaystack, input.query)) {
778
814
  return false;
779
815
  }
780
816
  }
@@ -865,14 +901,14 @@ export class LocalIndexService {
865
901
  conditions.push(`is_read = 0`);
866
902
  }
867
903
  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}%`);
904
+ const like = foldedLike(["folder", "labels_json"], input.label);
905
+ conditions.push(like.sql);
906
+ params.push(...like.params);
871
907
  }
872
908
  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);
909
+ const like = foldedLike(["subject", "preview", "from_json", "labels_json"], input.query);
910
+ conditions.push(like.sql);
911
+ params.push(...like.params);
876
912
  }
877
913
  const sql = `SELECT * FROM messages${conditions.length > 0 ? ` WHERE ${conditions.join(" AND ")}` : ""}`;
878
914
  const candidateMessages = db
@@ -913,19 +949,19 @@ export class LocalIndexService {
913
949
  return false;
914
950
  }
915
951
  if (input.label) {
916
- const labelNeedle = foldSearchText(input.label);
917
- if (!thread.normalizedLabels.some((label) => foldSearchText(label) === labelNeedle)) {
952
+ const wanted = input.label;
953
+ if (!thread.normalizedLabels.some((label) => searchEquals(label, wanted))) {
918
954
  return false;
919
955
  }
920
956
  }
921
957
  if (input.query) {
922
- const haystack = foldSearchText([
958
+ const haystack = [
923
959
  thread.subject,
924
960
  thread.latestPreview || "",
925
961
  ...thread.latestFrom.map((value) => `${value.name ?? ""} ${value.address ?? ""}`),
926
962
  ...thread.normalizedLabels,
927
- ].join("\n"));
928
- if (!haystack.includes(foldSearchText(input.query))) {
963
+ ].join("\n");
964
+ if (!searchIncludes(haystack, input.query)) {
929
965
  return false;
930
966
  }
931
967
  }
@@ -1220,15 +1256,15 @@ export class LocalIndexService {
1220
1256
  const db = await this.ensureDb();
1221
1257
  const { ownerEmail, updatedAt, folders, indexedFolders } = this.loadFoldersAndMetadata(db);
1222
1258
  const syncCheckpoints = this.loadCheckpointsSync(db);
1223
- const personNeedle = input.person ? foldSearchText(input.person) : undefined;
1259
+ const personNeedle = input.person || undefined;
1224
1260
  const domainNeedle = input.domain?.toLowerCase();
1225
1261
  const limit = input.limit ?? 10;
1226
1262
  const conditions = [];
1227
1263
  const params = [];
1228
1264
  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);
1265
+ const like = foldedLike(["from_json", "to_json", "cc_json"], personNeedle);
1266
+ conditions.push(like.sql);
1267
+ params.push(...like.params);
1232
1268
  }
1233
1269
  if (domainNeedle) {
1234
1270
  const needle = `%@${escapeLike(domainNeedle)}%`;
@@ -1258,9 +1294,9 @@ export class LocalIndexService {
1258
1294
  const snapshot = { ownerEmail, updatedAt, folders, indexedFolders, syncCheckpoints, messages };
1259
1295
  const threads = this.buildThreads(snapshot, true)
1260
1296
  .filter((thread) => thread.participants.some((participant) => {
1261
- const participantText = foldSearchText(`${participant.name ?? ""} ${participant.address ?? ""}`);
1297
+ const participantText = `${participant.name ?? ""} ${participant.address ?? ""}`;
1262
1298
  const participantDomain = extractDomain(participant.address || "");
1263
- if (personNeedle && participantText.includes(personNeedle)) {
1299
+ if (personNeedle && searchIncludes(participantText, personNeedle)) {
1264
1300
  return true;
1265
1301
  }
1266
1302
  if (domainNeedle && participantDomain === domainNeedle) {
@@ -1324,7 +1360,7 @@ export class LocalIndexService {
1324
1360
  this.chmodDbFiles();
1325
1361
  // SQLite's LOWER() only folds ASCII and LIKE does not ignore accents, so "łódź" could not find
1326
1362
  // "Łó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))));
1363
+ db.function("FOLD", { deterministic: true }, (value) => (value == null ? null : searchKey(String(value))));
1328
1364
  }
1329
1365
  // auto_vacuum only takes effect on a brand-new/empty database (page_count 0) —
1330
1366
  // setting the pragma alone does NOT retroactively enable incremental vacuuming
@@ -1352,6 +1388,7 @@ export class LocalIndexService {
1352
1388
  db.exec("VACUUM");
1353
1389
  }
1354
1390
  this.runMigrations(db);
1391
+ this.rebuildFtsIfNeeded(db);
1355
1392
  this.db = db;
1356
1393
  await this.maybeImportLegacyIndex(db);
1357
1394
  this.initialized = true;
@@ -1707,10 +1744,8 @@ export class LocalIndexService {
1707
1744
  is_automated: email.isAutomated === undefined ? null : email.isAutomated ? 1 : 0,
1708
1745
  delivered_to: email.deliveredTo ?? null,
1709
1746
  });
1710
- const mergedPreview = persisted.preview ?? "";
1711
- const search = emailToSearchParts({ ...email, attachmentText: persisted.attachment_text ?? undefined });
1712
1747
  deleteFts.run(email.id);
1713
- insertFts.run(email.id, email.subject, mergedPreview, email.folder, search.labels, search.participants, search.attachmentNames);
1748
+ insertFts.run(email.id, ...ftsColumns(email, persisted.preview ?? "", persisted.attachment_text ?? undefined));
1714
1749
  }
1715
1750
  if (cleanupExpunged) {
1716
1751
  createSnapshotUidTable.run();
@@ -1777,6 +1812,37 @@ export class LocalIndexService {
1777
1812
  db.pragma("incremental_vacuum");
1778
1813
  }
1779
1814
  }
1815
+ // Rebuilds messages_fts from the messages table when it was written by an older version (text stored
1816
+ // as-is, so "lodzi" could not find "Łodzi"). One transaction: either every row is converted or none.
1817
+ rebuildFtsIfNeeded(db) {
1818
+ const stored = db.prepare(`SELECT value FROM metadata WHERE key = 'ftsKeyVersion'`).get();
1819
+ if (stored?.value === FTS_KEY_VERSION)
1820
+ return;
1821
+ const total = Number(db.prepare(`SELECT COUNT(*) AS count FROM messages`).get().count);
1822
+ if (total > 0) {
1823
+ this.log.info(`Rebuilding the full-text index for ${total} messages (one-time)`, "LocalIndexService");
1824
+ }
1825
+ const insertFts = db.prepare(`
1826
+ INSERT INTO messages_fts (email_id, subject, preview, folder, labels, participants, attachment_names)
1827
+ VALUES (?, ?, ?, ?, ?, ?, ?)
1828
+ `);
1829
+ const nextBatch = db.prepare(`SELECT rowid AS _rowid, * FROM messages WHERE rowid > ? ORDER BY rowid LIMIT 500`);
1830
+ db.transaction(() => {
1831
+ db.exec(`DELETE FROM messages_fts`);
1832
+ let after = 0;
1833
+ for (;;) {
1834
+ const rows = nextBatch.all(after);
1835
+ if (rows.length === 0)
1836
+ break;
1837
+ for (const row of rows) {
1838
+ const email = this.rowToEmailSummary(row);
1839
+ insertFts.run(email.id, ...ftsColumns(email, row.preview ?? "", row.attachment_text ?? undefined));
1840
+ }
1841
+ after = rows[rows.length - 1]._rowid;
1842
+ }
1843
+ db.prepare(`INSERT INTO metadata (key, value) VALUES ('ftsKeyVersion', ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value`).run(FTS_KEY_VERSION);
1844
+ })();
1845
+ }
1780
1846
  runMigrations(db) {
1781
1847
  db.exec(`
1782
1848
  CREATE TABLE IF NOT EXISTS metadata (
@@ -1961,8 +2027,9 @@ export class LocalIndexService {
1961
2027
  params.push(filters.hasAttachment ? 1 : 0);
1962
2028
  }
1963
2029
  if (filters.subject) {
1964
- conditions.push(`FOLD(subject) LIKE ? ESCAPE '\\'`);
1965
- params.push(`%${escapeLike(foldSearchText(filters.subject))}%`);
2030
+ const like = foldedLike(["subject"], filters.subject);
2031
+ conditions.push(like.sql);
2032
+ params.push(...like.params);
1966
2033
  }
1967
2034
  if (filters.senderDomain) {
1968
2035
  conditions.push(`FOLD(from_json) LIKE ? ESCAPE '\\'`);
@@ -1976,13 +2043,14 @@ export class LocalIndexService {
1976
2043
  // genuine match older than the window instead of finding it. Mirror the
1977
2044
  // existing senderDomain LIKE pattern (a safe superset pre-filter).
1978
2045
  if (filters.from) {
1979
- conditions.push(`FOLD(from_json) LIKE ? ESCAPE '\\'`);
1980
- params.push(`%${escapeLike(foldSearchText(filters.from))}%`);
2046
+ const like = foldedLike(["from_json"], filters.from);
2047
+ conditions.push(like.sql);
2048
+ params.push(...like.params);
1981
2049
  }
1982
2050
  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);
2051
+ const like = foldedLike(["to_json", "cc_json", "bcc_json"], filters.to);
2052
+ conditions.push(like.sql);
2053
+ params.push(...like.params);
1986
2054
  }
1987
2055
  if (filters.messageId) {
1988
2056
  conditions.push(`message_id = ?`);
@@ -2033,8 +2101,13 @@ export class LocalIndexService {
2033
2101
  // NOT-prefix, so this searches for the term itself instead of silently dropping
2034
2102
  // it (e.g. `AND gate schematics` now matches "AND", "gate" and "schematics" all
2035
2103
  // required, rather than dropping "AND" and searching only the other two).
2104
+ // Each term is looked for in its folded form(s): the stored text is a searchKey (see
2105
+ // rebuildFtsIfNeeded), so Müller is stored as "muller" and "mueller" and either finds it.
2106
+ const quote = (form) => `"${form.replace(/"/g, '""')}"`;
2036
2107
  const match = parsed.residualTerms
2037
- .map((token) => `"${token.replace(/"/g, '""')}"`)
2108
+ .map((token) => searchNeedles(token).filter(Boolean).map(quote))
2109
+ .filter((forms) => forms.length > 0)
2110
+ .map((forms) => (forms.length > 1 ? `(${forms.join(" OR ")})` : forms[0]))
2038
2111
  .join(" AND ");
2039
2112
  if (!match) {
2040
2113
  // No terms survived parsing at all (e.g. an empty/whitespace-only query), so
@@ -2146,9 +2219,9 @@ export class LocalIndexService {
2146
2219
  messageParams.push(options.folder);
2147
2220
  }
2148
2221
  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}%`);
2222
+ const like = foldedLike(["folder", "labels_json"], options.label);
2223
+ messageConditions.push(like.sql);
2224
+ messageParams.push(...like.params);
2152
2225
  }
2153
2226
  if (typeof options.isRead === "boolean") {
2154
2227
  messageConditions.push(`is_read = ?`);
@@ -2188,14 +2261,14 @@ export class LocalIndexService {
2188
2261
  params.push(input.folder);
2189
2262
  }
2190
2263
  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}%`);
2264
+ const like = foldedLike(["folder", "labels_json"], input.label);
2265
+ conditions.push(like.sql);
2266
+ params.push(...like.params);
2194
2267
  }
2195
2268
  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);
2269
+ const like = foldedLike(["subject", "from_json", "to_json", "cc_json", "bcc_json", "labels_json"], input.query);
2270
+ conditions.push(like.sql);
2271
+ params.push(...like.params);
2199
2272
  }
2200
2273
  const sql = `SELECT * FROM messages${conditions.length > 0 ? ` WHERE ${conditions.join(" AND ")}` : ""}`;
2201
2274
  const candidates = db
@@ -2371,16 +2444,14 @@ export class LocalIndexService {
2371
2444
  const mailboxMessages = this.buildMailboxMessages(snapshot);
2372
2445
  const threadCount = this.buildThreads(snapshot).length;
2373
2446
  const labelCount = new Set(mailboxMessages.flatMap((message) => message.normalizedLabels.map((label) => label.toLowerCase()))).size;
2374
- const ageMinutes = snapshot.updatedAt
2375
- ? Math.max(0, Math.round((Date.now() - new Date(snapshot.updatedAt).getTime()) / 60_000))
2376
- : undefined;
2447
+ const ageMinutes = indexAgeMinutes(snapshot.updatedAt);
2377
2448
  return {
2378
2449
  path: this.dbPath,
2379
2450
  ownerEmail: snapshot.ownerEmail,
2380
2451
  updatedAt: snapshot.updatedAt,
2381
2452
  ageMinutes,
2382
2453
  staleThresholdMinutes: STALE_THRESHOLD_MINUTES,
2383
- isStale: typeof ageMinutes === "number" ? ageMinutes > STALE_THRESHOLD_MINUTES : true,
2454
+ isStale: isIndexStale(ageMinutes),
2384
2455
  lastSyncAt: snapshot.updatedAt,
2385
2456
  indexFreshnessMinutes: ageMinutes,
2386
2457
  // Selectable folders the index has never synced: mail arriving there is invisible to