proton-mail-bridge-client 2.0.6 → 2.0.7
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/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +13 -1
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +35 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +27 -2
- package/dist/index.js.map +1 -1
- package/dist/services/local-index-service.d.ts +1 -0
- package/dist/services/local-index-service.d.ts.map +1 -1
- package/dist/services/local-index-service.js +101 -22
- package/dist/services/local-index-service.js.map +1 -1
- package/dist/services/simple-imap-service.d.ts +2 -0
- package/dist/services/simple-imap-service.d.ts.map +1 -1
- package/dist/services/simple-imap-service.js +79 -2
- package/dist/services/simple-imap-service.js.map +1 -1
- package/dist/services/smtp-service.d.ts.map +1 -1
- package/dist/services/smtp-service.js +12 -1
- package/dist/services/smtp-service.js.map +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/helpers.d.ts.map +1 -1
- package/dist/utils/helpers.js +14 -1
- package/dist/utils/helpers.js.map +1 -1
- package/package.json +1 -1
|
@@ -95,6 +95,7 @@ export declare class LocalIndexService {
|
|
|
95
95
|
private loadCheckpointsSync;
|
|
96
96
|
private loadMessages;
|
|
97
97
|
private loadThreadCandidateMessages;
|
|
98
|
+
private hasUnresolvedReferenceCandidate;
|
|
98
99
|
private loadAllMessages;
|
|
99
100
|
private expandCandidatesToFullThreads;
|
|
100
101
|
private loadSnapshot;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"local-index-service.d.ts","sourceRoot":"","sources":["../../src/services/local-index-service.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,uBAAuB,EACvB,YAAY,EACZ,UAAU,EACV,YAAY,EACZ,cAAc,EAEd,gBAAgB,EAChB,qBAAqB,EACrB,gBAAgB,EAChB,iBAAiB,EACjB,YAAY,EACZ,aAAa,EACd,MAAM,mBAAmB,CAAC;AAc3B,OAAO,EAAU,KAAK,MAAM,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"local-index-service.d.ts","sourceRoot":"","sources":["../../src/services/local-index-service.ts"],"names":[],"mappings":"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"}
|
|
@@ -118,8 +118,16 @@ function matchesIndexedSearch(email, filters) {
|
|
|
118
118
|
]
|
|
119
119
|
.join("\n")
|
|
120
120
|
.toLowerCase();
|
|
121
|
-
|
|
122
|
-
|
|
121
|
+
// searchFtsIds() ANDs every whitespace-separated query token as its own quoted FTS5 term
|
|
122
|
+
// (so "invoice payment" matches a preview like "payment for the invoice is overdue"), but
|
|
123
|
+
// this post-filter used to require the ENTIRE joined query as one literal substring —
|
|
124
|
+
// rejecting anything FTS already matched whose words were merely out of order or separated
|
|
125
|
+
// by other words. Match FTS's own AND-of-terms semantics instead of a single-phrase substring.
|
|
126
|
+
if (normalizedFilters.query) {
|
|
127
|
+
const terms = normalizedFilters.query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
128
|
+
if (terms.length > 0 && !terms.every((term) => haystacks.includes(term))) {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
123
131
|
}
|
|
124
132
|
if (normalizedFilters.subject &&
|
|
125
133
|
!email.subject.toLowerCase().includes(normalizedFilters.subject.toLowerCase())) {
|
|
@@ -279,13 +287,18 @@ function isOutgoingMessage(message, ownerEmail) {
|
|
|
279
287
|
}
|
|
280
288
|
return message.from.some((address) => lowerCaseAddress(address.address) === owner);
|
|
281
289
|
}
|
|
282
|
-
// ponytail:
|
|
283
|
-
//
|
|
284
|
-
//
|
|
285
|
-
//
|
|
286
|
-
//
|
|
290
|
+
// ponytail: the upgrade path has been taken — messages.is_automated now stores the real
|
|
291
|
+
// List-Unsubscribe/List-Id/Precedence/Auto-Submitted verdict captured at index time
|
|
292
|
+
// (EmailSummary.isAutomated), and it wins whenever present. This local-part heuristic is kept
|
|
293
|
+
// only as the fallback for rows indexed before that column existed (is_automated NULL), which
|
|
294
|
+
// stay on the heuristic until a full re-sync backfills them. It proved too coarse live: it cut
|
|
295
|
+
// "pending on you" from 49,026 to 38,532 of ~57k threads, but transactional senders like an
|
|
296
|
+
// order-confirmation address with no no-reply marker are invisible to it.
|
|
287
297
|
const AUTOMATED_SENDER_PATTERN = /(^|[._-])(no-?reply|donotreply|do-not-reply|notification|powiadomien|mailer-daemon|postmaster|bounce)/i;
|
|
288
298
|
function isLikelyAutomatedSender(message) {
|
|
299
|
+
if (message.isAutomated !== undefined) {
|
|
300
|
+
return message.isAutomated;
|
|
301
|
+
}
|
|
289
302
|
return message.from.some((address) => AUTOMATED_SENDER_PATTERN.test(address.address ?? ""));
|
|
290
303
|
}
|
|
291
304
|
function actionableThreadScore(thread, ownerEmail) {
|
|
@@ -673,7 +686,17 @@ export class LocalIndexService {
|
|
|
673
686
|
messagesCapped = messages.length >= DEFAULT_SNAPSHOT_LIMIT;
|
|
674
687
|
}
|
|
675
688
|
const snapshot = { ownerEmail, updatedAt, folders, indexedFolders, syncCheckpoints, messages };
|
|
676
|
-
|
|
689
|
+
// includeMessages:true so the query check below can look at EVERY message's subject, not
|
|
690
|
+
// just thread.subject (which buildThreads derives from the single LATEST message). Found
|
|
691
|
+
// live via the fix above: once loadThreadCandidateMessages() correctly pulls in a
|
|
692
|
+
// reference-chain thread's full membership even when the query only matched its root, this
|
|
693
|
+
// filter's old `thread.subject` check started wrongly excluding that thread entirely — the
|
|
694
|
+
// root's subject may share nothing with the thread's most recent reply (e.g. root "Invoice
|
|
695
|
+
// 03/2026" vs. latest reply "Question about VAT"). Checking every message means a thread
|
|
696
|
+
// the SQL prefilter already proved relevant is never re-excluded by which message happened
|
|
697
|
+
// to be newest. `messages` is stripped back off in the final return below — callers still
|
|
698
|
+
// get plain ThreadSummary shapes, not the full per-message detail.
|
|
699
|
+
const threads = this.buildThreads(snapshot, true).filter((thread) => {
|
|
677
700
|
if (input.label) {
|
|
678
701
|
const labelNeedle = input.label.toLowerCase();
|
|
679
702
|
if (!thread.normalizedLabels.some((label) => label.toLowerCase() === labelNeedle)) {
|
|
@@ -681,14 +704,15 @@ export class LocalIndexService {
|
|
|
681
704
|
}
|
|
682
705
|
}
|
|
683
706
|
if (input.query) {
|
|
684
|
-
const
|
|
685
|
-
|
|
707
|
+
const queryNeedle = input.query.toLowerCase();
|
|
708
|
+
const threadLevelHaystack = [
|
|
686
709
|
...thread.participants.map((participant) => `${participant.name ?? ""} ${participant.address ?? ""}`),
|
|
687
710
|
...thread.normalizedLabels,
|
|
688
711
|
]
|
|
689
712
|
.join("\n")
|
|
690
713
|
.toLowerCase();
|
|
691
|
-
|
|
714
|
+
const matchesAnyMessage = thread.messages.some((message) => message.subject.toLowerCase().includes(queryNeedle));
|
|
715
|
+
if (!matchesAnyMessage && !threadLevelHaystack.includes(queryNeedle)) {
|
|
692
716
|
return false;
|
|
693
717
|
}
|
|
694
718
|
}
|
|
@@ -697,10 +721,11 @@ export class LocalIndexService {
|
|
|
697
721
|
const limit = input.limit ?? 100;
|
|
698
722
|
const offset = 0;
|
|
699
723
|
const totalCount = threads.length;
|
|
724
|
+
const summaries = threads.map(({ messages: _messages, ...summary }) => summary);
|
|
700
725
|
return {
|
|
701
726
|
total: totalCount,
|
|
702
727
|
hasMore: totalCount > offset + limit,
|
|
703
|
-
threads:
|
|
728
|
+
threads: summaries.slice(offset, offset + limit),
|
|
704
729
|
...this.indexFreshnessFields(snapshot.updatedAt),
|
|
705
730
|
...(messagesCapped ? { messagesCapped: true } : {}),
|
|
706
731
|
};
|
|
@@ -1302,11 +1327,13 @@ export class LocalIndexService {
|
|
|
1302
1327
|
INSERT INTO messages (
|
|
1303
1328
|
email_id, folder, uid, seq, message_id, in_reply_to, references_json, thread_id, subject,
|
|
1304
1329
|
from_json, to_json, cc_json, bcc_json, reply_to_json, date, internal_date,
|
|
1305
|
-
is_read, is_starred, flags_json, size, preview, has_attachments, attachments_json, attachment_text, labels_json
|
|
1330
|
+
is_read, is_starred, flags_json, size, preview, has_attachments, attachments_json, attachment_text, labels_json,
|
|
1331
|
+
is_automated
|
|
1306
1332
|
) VALUES (
|
|
1307
1333
|
@email_id, @folder, @uid, @seq, @message_id, @in_reply_to, @references_json, @thread_id, @subject,
|
|
1308
1334
|
@from_json, @to_json, @cc_json, @bcc_json, @reply_to_json, @date, @internal_date,
|
|
1309
|
-
@is_read, @is_starred, @flags_json, @size, @preview, @has_attachments, @attachments_json, @attachment_text, @labels_json
|
|
1335
|
+
@is_read, @is_starred, @flags_json, @size, @preview, @has_attachments, @attachments_json, @attachment_text, @labels_json,
|
|
1336
|
+
@is_automated
|
|
1310
1337
|
)
|
|
1311
1338
|
ON CONFLICT(email_id) DO UPDATE SET
|
|
1312
1339
|
folder = excluded.folder,
|
|
@@ -1336,7 +1363,10 @@ export class LocalIndexService {
|
|
|
1336
1363
|
has_attachments = excluded.has_attachments,
|
|
1337
1364
|
attachments_json = excluded.attachments_json,
|
|
1338
1365
|
attachment_text = COALESCE(excluded.attachment_text, messages.attachment_text),
|
|
1339
|
-
labels_json = excluded.labels_json
|
|
1366
|
+
labels_json = excluded.labels_json,
|
|
1367
|
+
-- Same reasoning: a flags-only refresh fetches no headers, so is_automated arrives
|
|
1368
|
+
-- NULL; the headers of a fixed UID never change, so keeping the stored verdict is safe.
|
|
1369
|
+
is_automated = COALESCE(excluded.is_automated, messages.is_automated)
|
|
1340
1370
|
RETURNING preview, attachment_text
|
|
1341
1371
|
`);
|
|
1342
1372
|
const upsertSyncState = db.prepare(`
|
|
@@ -1552,6 +1582,7 @@ export class LocalIndexService {
|
|
|
1552
1582
|
attachments_json: JSON.stringify(email.attachments),
|
|
1553
1583
|
attachment_text: email.attachmentText ?? legacyAttachmentText,
|
|
1554
1584
|
labels_json: JSON.stringify(email.labels),
|
|
1585
|
+
is_automated: email.isAutomated === undefined ? null : email.isAutomated ? 1 : 0,
|
|
1555
1586
|
});
|
|
1556
1587
|
const mergedPreview = persisted.preview ?? "";
|
|
1557
1588
|
const search = emailToSearchParts({ ...email, attachmentText: persisted.attachment_text ?? undefined });
|
|
@@ -1670,7 +1701,8 @@ export class LocalIndexService {
|
|
|
1670
1701
|
has_attachments INTEGER NOT NULL,
|
|
1671
1702
|
attachments_json TEXT NOT NULL,
|
|
1672
1703
|
attachment_text TEXT,
|
|
1673
|
-
labels_json TEXT NOT NULL
|
|
1704
|
+
labels_json TEXT NOT NULL,
|
|
1705
|
+
is_automated INTEGER
|
|
1674
1706
|
);
|
|
1675
1707
|
|
|
1676
1708
|
CREATE TABLE IF NOT EXISTS sync_state (
|
|
@@ -1716,6 +1748,12 @@ export class LocalIndexService {
|
|
|
1716
1748
|
if (!columns.has("attachment_text")) {
|
|
1717
1749
|
db.exec(`ALTER TABLE messages ADD COLUMN attachment_text TEXT`);
|
|
1718
1750
|
}
|
|
1751
|
+
// Nullable on purpose: existing rows read back as NULL (= isAutomated undefined) so the
|
|
1752
|
+
// actionable-thread scorer falls back to the sender regex for them instead of treating
|
|
1753
|
+
// every pre-migration message as human. A full re-sync backfills the real value.
|
|
1754
|
+
if (!columns.has("is_automated")) {
|
|
1755
|
+
db.exec(`ALTER TABLE messages ADD COLUMN is_automated INTEGER`);
|
|
1756
|
+
}
|
|
1719
1757
|
const syncStateColumns = new Set(db.prepare(`PRAGMA table_info(sync_state)`).all().map((row) => row.name));
|
|
1720
1758
|
if (!syncStateColumns.has("backfilled_to_uid")) {
|
|
1721
1759
|
db.exec(`ALTER TABLE sync_state ADD COLUMN backfilled_to_uid INTEGER`);
|
|
@@ -1826,8 +1864,14 @@ export class LocalIndexService {
|
|
|
1826
1864
|
params.push(filters.threadId);
|
|
1827
1865
|
}
|
|
1828
1866
|
if (filters.dateFrom) {
|
|
1867
|
+
// Same bug class as dateTo below: dateFrom used to be pushed raw and compared as a
|
|
1868
|
+
// string against full ISO timestamps ("internal_date" is always a full ISO string).
|
|
1869
|
+
// A caller-supplied value that isn't already exactly that shape (a bare date, a value
|
|
1870
|
+
// with a timezone offset, an English date like "March 24, 2026") does a wrong
|
|
1871
|
+
// lexicographic comparison instead of a real date comparison, silently dropping
|
|
1872
|
+
// matching messages with no error. Normalize the same way dateTo already does.
|
|
1829
1873
|
conditions.push(`COALESCE(internal_date, date) >= ?`);
|
|
1830
|
-
params.push(filters.dateFrom);
|
|
1874
|
+
params.push(new Date(filters.dateFrom).toISOString());
|
|
1831
1875
|
}
|
|
1832
1876
|
if (filters.dateTo) {
|
|
1833
1877
|
// dateTo is commonly a bare date ("2026-09-02") without a time
|
|
@@ -1982,8 +2026,11 @@ export class LocalIndexService {
|
|
|
1982
2026
|
messageParams.push(options.isRead ? 1 : 0);
|
|
1983
2027
|
}
|
|
1984
2028
|
if (options.since) {
|
|
2029
|
+
// Same normalization as the dateFrom fix in loadCandidateEmails() above — options.since
|
|
2030
|
+
// is fed straight from a caller's dateFrom (see the threadId search path), so it needs
|
|
2031
|
+
// the identical ISO normalization to avoid a wrong string comparison.
|
|
1985
2032
|
messageConditions.push(`COALESCE(internal_date, date) >= ?`);
|
|
1986
|
-
messageParams.push(options.since);
|
|
2033
|
+
messageParams.push(new Date(options.since).toISOString());
|
|
1987
2034
|
}
|
|
1988
2035
|
if (messageConditions.length > 0) {
|
|
1989
2036
|
messageSqlParts.push(`WHERE ${messageConditions.join(" AND ")}`);
|
|
@@ -2049,14 +2096,43 @@ export class LocalIndexService {
|
|
|
2049
2096
|
// is present, pull in the full, uncapped message set — the same source
|
|
2050
2097
|
// getThreadById() now uses for these threads — so the thread is always built
|
|
2051
2098
|
// from its complete membership, filtered query or not.
|
|
2052
|
-
|
|
2053
|
-
if (hasUnresolvedReferenceCandidate) {
|
|
2099
|
+
if (this.hasUnresolvedReferenceCandidate(db, candidates)) {
|
|
2054
2100
|
for (const email of this.loadAllMessages(db)) {
|
|
2055
2101
|
byId.set(email.id, email);
|
|
2056
2102
|
}
|
|
2057
2103
|
}
|
|
2058
2104
|
return [...byId.values()];
|
|
2059
2105
|
}
|
|
2106
|
+
// The check above only looked at the candidate's OWN inReplyTo/references — but a
|
|
2107
|
+
// candidate that IS the root of a reference chain has neither (it started the thread),
|
|
2108
|
+
// while a reply elsewhere in the mailbox points AT it via In-Reply-To. Found live: a
|
|
2109
|
+
// getThreads({query}) match on the root message alone built a 1-message thread instead of
|
|
2110
|
+
// the real 3-message thread, because the root has no headers to trip the original check and
|
|
2111
|
+
// no persisted thread_id to expand through. A candidate lacking a persisted thread_id is
|
|
2112
|
+
// genuinely resolved by full-membership-via-references in exactly two cases: it references
|
|
2113
|
+
// something else (the original check), or something else references IT. The in_reply_to
|
|
2114
|
+
// column is indexed (idx_messages_in_reply_to), so this stays a targeted lookup per
|
|
2115
|
+
// reference-less candidate rather than paying for the full scan on the common case where
|
|
2116
|
+
// every candidate already has a native thread_id.
|
|
2117
|
+
hasUnresolvedReferenceCandidate(db, candidates) {
|
|
2118
|
+
return candidates.some((email) => {
|
|
2119
|
+
if (email.threadId?.trim()) {
|
|
2120
|
+
return false;
|
|
2121
|
+
}
|
|
2122
|
+
if (email.inReplyTo || (email.references?.length ?? 0) > 0) {
|
|
2123
|
+
return true;
|
|
2124
|
+
}
|
|
2125
|
+
if (email.messageId) {
|
|
2126
|
+
const referencedByOther = db
|
|
2127
|
+
.prepare(`SELECT 1 FROM messages WHERE in_reply_to = ? LIMIT 1`)
|
|
2128
|
+
.get(email.messageId);
|
|
2129
|
+
if (referencedByOther) {
|
|
2130
|
+
return true;
|
|
2131
|
+
}
|
|
2132
|
+
}
|
|
2133
|
+
return false;
|
|
2134
|
+
});
|
|
2135
|
+
}
|
|
2060
2136
|
// Uncapped message read, unlike loadMessages() (deliberately capped at
|
|
2061
2137
|
// DEFAULT_SNAPSHOT_LIMIT). Used only where correctness requires seeing every
|
|
2062
2138
|
// message regardless of index size — resolving a fallback/reference-chain
|
|
@@ -2093,8 +2169,10 @@ export class LocalIndexService {
|
|
|
2093
2169
|
byId.set(email.id, email);
|
|
2094
2170
|
}
|
|
2095
2171
|
}
|
|
2096
|
-
|
|
2097
|
-
|
|
2172
|
+
// See hasUnresolvedReferenceCandidate()'s comment above (loadThreadCandidateMessages) for
|
|
2173
|
+
// why this must also check whether some OTHER message's In-Reply-To points at a
|
|
2174
|
+
// header-less candidate, not just the candidate's own inReplyTo/references.
|
|
2175
|
+
if (this.hasUnresolvedReferenceCandidate(db, candidates)) {
|
|
2098
2176
|
for (const email of this.loadAllMessages(db)) {
|
|
2099
2177
|
byId.set(email.id, email);
|
|
2100
2178
|
}
|
|
@@ -2156,6 +2234,7 @@ export class LocalIndexService {
|
|
|
2156
2234
|
attachments: safeJsonParse(row.attachments_json, []),
|
|
2157
2235
|
attachmentText: row.attachment_text ?? undefined,
|
|
2158
2236
|
labels: safeJsonParse(row.labels_json, []),
|
|
2237
|
+
isAutomated: row.is_automated === null || row.is_automated === undefined ? undefined : Boolean(row.is_automated),
|
|
2159
2238
|
};
|
|
2160
2239
|
}
|
|
2161
2240
|
toStatus(snapshot, counts) {
|