@remit/mailbox-service 0.0.111 → 0.0.113
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/account-credentials.d.ts +83 -0
- package/dist/account-credentials.js +104 -0
- package/dist/attribute-mapper.d.ts +36 -0
- package/dist/attribute-mapper.js +119 -0
- package/dist/body-parse.d.ts +28 -0
- package/{src/body-parse.ts → dist/body-parse.js} +18 -26
- package/dist/body-part-mapper.d.ts +92 -0
- package/dist/body-part-mapper.js +301 -0
- package/dist/body-sync-queue.d.ts +59 -0
- package/dist/body-sync-queue.js +56 -0
- package/dist/body-sync-terminal.d.ts +58 -0
- package/dist/body-sync-terminal.js +95 -0
- package/dist/body-sync.d.ts +351 -0
- package/dist/body-sync.js +1204 -0
- package/dist/calendar-parts.d.ts +23 -0
- package/dist/calendar-parts.js +18 -0
- package/dist/classification-backfill.d.ts +91 -0
- package/dist/classification-backfill.js +180 -0
- package/dist/classify-message.d.ts +31 -0
- package/dist/classify-message.js +89 -0
- package/dist/connection-factory.d.ts +54 -0
- package/dist/connection-factory.js +54 -0
- package/dist/connection-test.d.ts +41 -0
- package/dist/connection-test.js +152 -0
- package/dist/external-move.d.ts +94 -0
- package/dist/external-move.js +173 -0
- package/dist/filter-operations.d.ts +42 -0
- package/dist/filter-operations.js +211 -0
- package/dist/filters/filter-config.d.ts +14 -0
- package/dist/filters/filter-config.js +10 -0
- package/{src/filters/list-id.ts → dist/filters/list-id.d.ts} +2 -16
- package/dist/filters/list-id.js +26 -0
- package/dist/filters/match.d.ts +69 -0
- package/dist/filters/match.js +165 -0
- package/dist/filters/pipeline.d.ts +64 -0
- package/dist/filters/pipeline.js +100 -0
- package/{src/flag-push-terminal.ts → dist/flag-push-terminal.d.ts} +19 -77
- package/dist/flag-push-terminal.js +63 -0
- package/dist/flag-push.d.ts +69 -0
- package/dist/flag-push.js +111 -0
- package/dist/flag-queue.d.ts +175 -0
- package/dist/flag-queue.js +291 -0
- package/dist/folder-operations.d.ts +39 -0
- package/dist/folder-operations.js +112 -0
- package/dist/folder-placement.d.ts +13 -0
- package/dist/folder-placement.js +23 -0
- package/dist/heuristics/authenticationResults.d.ts +10 -0
- package/dist/heuristics/authenticationResults.js +187 -0
- package/dist/heuristics/classifyByHeaders.d.ts +82 -0
- package/dist/heuristics/classifyByHeaders.js +365 -0
- package/dist/heuristics/classifyPlacement.d.ts +67 -0
- package/dist/heuristics/classifyPlacement.js +153 -0
- package/dist/heuristics/machineSenders.d.ts +21 -0
- package/{src/heuristics/machineSenders.ts → dist/heuristics/machineSenders.js} +34 -40
- package/dist/heuristics/senderMismatch.d.ts +81 -0
- package/dist/heuristics/senderMismatch.js +192 -0
- package/dist/heuristics/socialDomains.d.ts +14 -0
- package/{src/heuristics/socialDomains.ts → dist/heuristics/socialDomains.js} +9 -9
- package/dist/heuristics/transactionalDomains.d.ts +18 -0
- package/{src/heuristics/transactionalDomains.ts → dist/heuristics/transactionalDomains.js} +8 -8
- package/dist/imapflow-connection.d.ts +377 -0
- package/dist/imapflow-connection.js +1147 -0
- package/dist/index.d.ts +63 -0
- package/dist/index.js +65 -0
- package/dist/list-id-backfill.d.ts +83 -0
- package/dist/list-id-backfill.js +132 -0
- package/dist/mailbox-cursor-rebuild.d.ts +74 -0
- package/dist/mailbox-cursor-rebuild.js +71 -0
- package/{src/mailbox-cursor.ts → dist/mailbox-cursor.d.ts} +20 -98
- package/dist/mailbox-cursor.js +102 -0
- package/dist/mailbox-intent.d.ts +41 -0
- package/{src/mailbox-intent.ts → dist/mailbox-intent.js} +39 -63
- package/dist/mailbox-management.d.ts +240 -0
- package/dist/mailbox-management.js +576 -0
- package/dist/mailbox-presence.d.ts +52 -0
- package/{src/mailbox-presence.ts → dist/mailbox-presence.js} +19 -30
- package/dist/mailbox-queue.d.ts +126 -0
- package/dist/mailbox-queue.js +214 -0
- package/dist/mailbox-sync.d.ts +93 -0
- package/dist/mailbox-sync.js +490 -0
- package/dist/mailbox-upstream.d.ts +38 -0
- package/{src/mailbox-upstream.ts → dist/mailbox-upstream.js} +37 -47
- package/dist/message-category.d.ts +51 -0
- package/{src/message-category.ts → dist/message-category.js} +28 -71
- package/dist/message-move.d.ts +285 -0
- package/dist/message-move.js +858 -0
- package/dist/message-parser.d.ts +14 -0
- package/dist/message-parser.js +17 -0
- package/{src/message-presence.ts → dist/message-presence.d.ts} +1 -13
- package/dist/message-presence.js +29 -0
- package/dist/message-read.d.ts +51 -0
- package/dist/message-read.js +31 -0
- package/dist/message-sync.d.ts +345 -0
- package/dist/message-sync.js +1358 -0
- package/dist/mime-walker.d.ts +102 -0
- package/dist/mime-walker.js +198 -0
- package/dist/organize-job.d.ts +20 -0
- package/dist/organize-job.js +37 -0
- package/dist/outbox-attachment-content.d.ts +37 -0
- package/dist/outbox-attachment-content.js +50 -0
- package/dist/outbox-attachment-filename.d.ts +19 -0
- package/dist/outbox-attachment-filename.js +64 -0
- package/dist/outbox-attachment.d.ts +148 -0
- package/dist/outbox-attachment.js +273 -0
- package/dist/outbox-queue.d.ts +73 -0
- package/dist/outbox-queue.js +284 -0
- package/{src/outbox-status.ts → dist/outbox-status.d.ts} +1 -6
- package/dist/outbox-status.js +18 -0
- package/dist/ownership.d.ts +24 -0
- package/dist/ownership.js +83 -0
- package/dist/placement-move-terminal.d.ts +67 -0
- package/dist/placement-move-terminal.js +88 -0
- package/dist/placement-move.d.ts +83 -0
- package/dist/placement-move.js +216 -0
- package/{src/placement-settled.ts → dist/placement-settled.d.ts} +19 -50
- package/dist/placement-settled.js +75 -0
- package/dist/quarantine.d.ts +95 -0
- package/dist/quarantine.js +131 -0
- package/dist/refusal.d.ts +31 -0
- package/dist/refusal.js +3 -0
- package/{src/restore-source-placement.ts → dist/restore-source-placement.d.ts} +29 -101
- package/dist/restore-source-placement.js +79 -0
- package/dist/search-text-backfill.d.ts +15 -0
- package/dist/search-text-backfill.js +33 -0
- package/dist/snippet.d.ts +61 -0
- package/dist/snippet.js +129 -0
- package/dist/spam-report.d.ts +86 -0
- package/dist/spam-report.js +241 -0
- package/dist/sync-warning.d.ts +21 -0
- package/dist/sync-warning.js +26 -0
- package/dist/sync-watermarks.d.ts +138 -0
- package/dist/sync-watermarks.js +205 -0
- package/dist/test-helpers/fake-imap.d.ts +151 -0
- package/dist/test-helpers/fake-imap.js +508 -0
- package/dist/test-helpers/folder-roles.d.ts +34 -0
- package/dist/test-helpers/folder-roles.js +47 -0
- package/dist/test-helpers/mime-tree-generator.d.ts +82 -0
- package/dist/test-helpers/mime-tree-generator.js +329 -0
- package/dist/test-helpers/test-store.d.ts +69 -0
- package/dist/test-helpers/test-store.js +193 -0
- package/dist/text/normalizer.d.ts +20 -0
- package/dist/text/normalizer.js +102 -0
- package/dist/thread-message-rows.d.ts +69 -0
- package/dist/thread-message-rows.js +40 -0
- package/dist/thread-root.d.ts +7 -0
- package/dist/thread-root.js +25 -0
- package/dist/types.d.ts +343 -0
- package/dist/types.js +19 -0
- package/package.json +86 -73
- package/src/account-credentials.ts +0 -210
- package/src/adversarial-mime.e2e.test.ts +0 -307
- package/src/attribute-mapper.test.ts +0 -104
- package/src/attribute-mapper.ts +0 -148
- package/src/body-parse.test.ts +0 -55
- package/src/body-part-mapper.property.test.ts +0 -354
- package/src/body-part-mapper.ts +0 -360
- package/src/body-sync-auto-read-writeonce.test.ts +0 -219
- package/src/body-sync-calendar-suggestion.test.ts +0 -433
- package/src/body-sync-category-denormalize.test.ts +0 -352
- package/src/body-sync-category-override.test.ts +0 -501
- package/src/body-sync-category-writeonce.test.ts +0 -234
- package/src/body-sync-filter.test.ts +0 -237
- package/src/body-sync-message-gone.test.ts +0 -139
- package/src/body-sync-placement-flags.test.ts +0 -347
- package/src/body-sync-placement-writeonce.test.ts +0 -337
- package/src/body-sync-quarantine.test.ts +0 -276
- package/src/body-sync-queue.test.ts +0 -124
- package/src/body-sync-queue.ts +0 -131
- package/src/body-sync-resync.test.ts +0 -605
- package/src/body-sync-terminal.test.ts +0 -310
- package/src/body-sync-terminal.ts +0 -176
- package/src/body-sync-unclassifiable-once.test.ts +0 -260
- package/src/body-sync-unsubscribed-auto-read.test.ts +0 -176
- package/src/body-sync.ts +0 -1970
- package/src/calendar-parts.ts +0 -41
- package/src/classification-backfill.test.ts +0 -615
- package/src/classification-backfill.ts +0 -363
- package/src/classify-message.test.ts +0 -205
- package/src/classify-message.ts +0 -116
- package/src/connection-factory.ts +0 -102
- package/src/connection-test.ts +0 -208
- package/src/credentials.test.ts +0 -181
- package/src/external-move.test.ts +0 -369
- package/src/external-move.ts +0 -272
- package/src/filters/anchor-drift.ts +0 -55
- package/src/filters/filter-config.test.ts +0 -229
- package/src/filters/filter-config.ts +0 -69
- package/src/filters/list-id.test.ts +0 -50
- package/src/filters/match.test.ts +0 -429
- package/src/filters/match.ts +0 -274
- package/src/filters/message-embedder.test.ts +0 -39
- package/src/filters/message-embedder.ts +0 -33
- package/src/filters/pipeline.test.ts +0 -330
- package/src/filters/pipeline.ts +0 -284
- package/src/flag-push-terminal.test.ts +0 -209
- package/src/flag-push.test.ts +0 -408
- package/src/flag-push.ts +0 -198
- package/src/flag-queue-stars.test.ts +0 -171
- package/src/flag-queue.ts +0 -509
- package/src/folder-placement.ts +0 -56
- package/src/heuristics/authenticationResults.test.ts +0 -189
- package/src/heuristics/authenticationResults.ts +0 -212
- package/src/heuristics/classifyByHeaders.test.ts +0 -889
- package/src/heuristics/classifyByHeaders.ts +0 -467
- package/src/heuristics/classifyPlacement.test.ts +0 -366
- package/src/heuristics/classifyPlacement.ts +0 -211
- package/src/heuristics/classifyRealMail.test.ts +0 -242
- package/src/heuristics/machineSenders.test.ts +0 -101
- package/src/heuristics/senderMismatch.test.ts +0 -346
- package/src/heuristics/senderMismatch.ts +0 -259
- package/src/imapflow-connect-retry.test.ts +0 -134
- package/src/imapflow-connection.e2e.test.ts +0 -335
- package/src/imapflow-connection.integ.test.ts +0 -316
- package/src/imapflow-connection.test.ts +0 -469
- package/src/imapflow-connection.ts +0 -1475
- package/src/index.ts +0 -350
- package/src/list-id-backfill.test.ts +0 -573
- package/src/list-id-backfill.ts +0 -274
- package/src/mailbox-cursor-rebuild.test.ts +0 -171
- package/src/mailbox-cursor-rebuild.ts +0 -146
- package/src/mailbox-cursor.test.ts +0 -249
- package/src/mailbox-delete-intent.test.ts +0 -246
- package/src/mailbox-management.test.ts +0 -356
- package/src/mailbox-management.ts +0 -839
- package/src/mailbox-presence.test.ts +0 -72
- package/src/mailbox-queue.ts +0 -415
- package/src/mailbox-rename-intent.test.ts +0 -389
- package/src/mailbox-rename-subtree.test.ts +0 -664
- package/src/mailbox-sync.test.ts +0 -848
- package/src/mailbox-sync.ts +0 -724
- package/src/message-delete-junk-reconcile.test.ts +0 -117
- package/src/message-delete-no-trash.test.ts +0 -245
- package/src/message-delete-unsettled-placement.test.ts +0 -396
- package/src/message-move-copy.test.ts +0 -562
- package/src/message-move-empty-trash.test.ts +0 -326
- package/src/message-move-same-mailbox.test.ts +0 -139
- package/src/message-move-unsettled-placement.test.ts +0 -378
- package/src/message-move.ts +0 -1348
- package/src/message-parser.ts +0 -26
- package/src/message-presence.test.ts +0 -50
- package/src/message-sync-address-spoof.test.ts +0 -293
- package/src/message-sync-category.test.ts +0 -142
- package/src/message-sync-changedsince.test.ts +0 -840
- package/src/message-sync-flags.test.ts +0 -158
- package/src/message-sync-junk-harvest.test.ts +0 -244
- package/src/message-sync-quarantine.test.ts +0 -416
- package/src/message-sync-repoint.test.ts +0 -419
- package/src/message-sync.ts +0 -1963
- package/src/mime-walker.test.ts +0 -311
- package/src/mime-walker.ts +0 -305
- package/src/outbox-attachment-content.test.ts +0 -153
- package/src/outbox-attachment-content.ts +0 -104
- package/src/outbox-attachment-filename.test.ts +0 -132
- package/src/outbox-attachment-filename.ts +0 -76
- package/src/outbox-attachment.test.ts +0 -738
- package/src/outbox-attachment.ts +0 -536
- package/src/outbox-failed-recovery.test.ts +0 -370
- package/src/outbox-queue-settled-row.test.ts +0 -140
- package/src/outbox-queue.test.ts +0 -291
- package/src/outbox-queue.ts +0 -508
- package/src/pass-through-unit-of-work.ts +0 -17
- package/src/placement-move-terminal.test.ts +0 -300
- package/src/placement-move-terminal.ts +0 -148
- package/src/placement-move-unsettled.test.ts +0 -275
- package/src/placement-move.ts +0 -336
- package/src/placement-settled.test.ts +0 -147
- package/src/placement-transition-race.test.ts +0 -482
- package/src/quarantine.test.ts +0 -191
- package/src/quarantine.ts +0 -228
- package/src/snippet.test.ts +0 -213
- package/src/snippet.ts +0 -152
- package/src/spam-report.test.ts +0 -801
- package/src/spam-report.ts +0 -357
- package/src/stale-message-reconcile.test.ts +0 -82
- package/src/stale-message-reconcile.ts +0 -55
- package/src/sync-watermarks.test.ts +0 -409
- package/src/sync-watermarks.ts +0 -319
- package/src/system-flag-wire-format.test.ts +0 -77
- package/src/test-helpers/folder-roles.ts +0 -66
- package/src/test-helpers/isolated-mailbox.ts +0 -44
- package/src/test-helpers/mailfuzz-connection.ts +0 -27
- package/src/test-helpers/mime-tree-generator.ts +0 -461
- package/src/text/normalizer.test.ts +0 -223
- package/src/text/normalizer.ts +0 -140
- package/src/thread-message-rows.ts +0 -77
- package/src/types/mailparser-augment.d.ts +0 -15
- package/src/types/natural-porter-stemmer.d.ts +0 -50
- package/src/types/stopword.d.ts +0 -69
- package/src/types.ts +0 -371
- package/test/fixtures/mime-adversarial-e2e/01-octet-stream-pdf.eml +0 -21
- package/test/fixtures/mime-adversarial-e2e/02-nested-related-cid-image.eml +0 -31
- package/test/fixtures/mime-adversarial-e2e/03-calendar-invite.eml +0 -33
- package/test/fixtures/mime-adversarial-e2e/04-forwarded-rfc822-with-attachments.eml +0 -42
- package/test/fixtures/mime-adversarial-e2e/05-tnef-winmail.eml +0 -25
- package/test/fixtures/mime-adversarial-e2e/06-8bit-non-ascii.eml +0 -15
- package/test/fixtures/mime-adversarial-e2e/07-quoted-printable-soft-breaks.eml +0 -17
- package/test/fixtures/mime-adversarial-e2e/08-alternative-empty-text.eml +0 -18
- package/tsconfig.json +0 -8
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Message parsing utilities for extracting content from raw RFC822 messages.
|
|
3
|
+
*/
|
|
4
|
+
export interface ParsedMessageContent {
|
|
5
|
+
text: string | null;
|
|
6
|
+
html: string | null;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Parse a raw RFC822 message buffer and extract text/html content.
|
|
10
|
+
*
|
|
11
|
+
* @param buffer - The raw message content (RFC822 format)
|
|
12
|
+
* @returns Parsed content with text and html fields
|
|
13
|
+
*/
|
|
14
|
+
export declare const parseMessageContent: (buffer: Buffer) => Promise<ParsedMessageContent>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Message parsing utilities for extracting content from raw RFC822 messages.
|
|
3
|
+
*/
|
|
4
|
+
import { simpleParser } from "mailparser";
|
|
5
|
+
/**
|
|
6
|
+
* Parse a raw RFC822 message buffer and extract text/html content.
|
|
7
|
+
*
|
|
8
|
+
* @param buffer - The raw message content (RFC822 format)
|
|
9
|
+
* @returns Parsed content with text and html fields
|
|
10
|
+
*/
|
|
11
|
+
export const parseMessageContent = async (buffer) => {
|
|
12
|
+
const parsed = await simpleParser(buffer);
|
|
13
|
+
return {
|
|
14
|
+
text: parsed.text ?? null,
|
|
15
|
+
html: typeof parsed.html === "string" ? parsed.html : null,
|
|
16
|
+
};
|
|
17
|
+
};
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { IImapConnection } from "./types.js";
|
|
2
|
-
|
|
3
2
|
/**
|
|
4
3
|
* Whether a UID is confirmed gone from the currently open mailbox.
|
|
5
4
|
*
|
|
@@ -21,15 +20,4 @@ import type { IImapConnection } from "./types.js";
|
|
|
21
20
|
* hands this function a deletion authority it cannot tell apart from a genuine
|
|
22
21
|
* miss.
|
|
23
22
|
*/
|
|
24
|
-
export const isMessageGoneFromOpenMailbox
|
|
25
|
-
connection: Pick<IImapConnection, "fetchMessages" | "search">,
|
|
26
|
-
uid: number,
|
|
27
|
-
): Promise<boolean> => {
|
|
28
|
-
const found = await connection.fetchMessages([uid]);
|
|
29
|
-
if (found.length > 0) {
|
|
30
|
-
return false;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const matched = await connection.search([["UID", String(uid)]]);
|
|
34
|
-
return !matched.includes(uid);
|
|
35
|
-
};
|
|
23
|
+
export declare const isMessageGoneFromOpenMailbox: (connection: Pick<IImapConnection, "fetchMessages" | "search">, uid: number) => Promise<boolean>;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether a UID is confirmed gone from the currently open mailbox.
|
|
3
|
+
*
|
|
4
|
+
* A FETCH that yields no row is not proof of absence. imapflow drops rows on
|
|
5
|
+
* back-to-back FETCHes (#408) — the same client glitch #100 had to stop
|
|
6
|
+
* reading as authoritative on the sync path — so a transient blip returns an
|
|
7
|
+
* empty result for a message that is still on the server. Anything that
|
|
8
|
+
* deletes local rows on that reading destroys live mail.
|
|
9
|
+
*
|
|
10
|
+
* Absence is therefore confirmed with a UID SEARCH, which the server answers
|
|
11
|
+
* with a plain UID set rather than a stream of message rows, and which
|
|
12
|
+
* `placement-move-push.ts` already uses as its verification probe. Only a
|
|
13
|
+
* SEARCH that does not list the UID counts as gone; an empty FETCH the SEARCH
|
|
14
|
+
* contradicts leaves the message present, and the caller treats it as such.
|
|
15
|
+
*
|
|
16
|
+
* The absence verdict rests on an empty SEARCH meaning the server matched
|
|
17
|
+
* nothing, so `IImapConnection.search` must throw when a SEARCH fails rather
|
|
18
|
+
* than answer `[]` — an implementation that reports failure as an empty array
|
|
19
|
+
* hands this function a deletion authority it cannot tell apart from a genuine
|
|
20
|
+
* miss.
|
|
21
|
+
*/
|
|
22
|
+
export const isMessageGoneFromOpenMailbox = async (connection, uid) => {
|
|
23
|
+
const found = await connection.fetchMessages([uid]);
|
|
24
|
+
if (found.length > 0) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
const matched = await connection.search([["UID", String(uid)]]);
|
|
28
|
+
return !matched.includes(uid);
|
|
29
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The slice of `BodySyncService` the read path needs to lazily materialize
|
|
3
|
+
* deferred per-part objects.
|
|
4
|
+
*/
|
|
5
|
+
export interface BodyPartMaterializer {
|
|
6
|
+
ensureBodyPartsStored(accountConfigId: string, accountId: string, messageId: string, bodyStorageKey: string): Promise<unknown>;
|
|
7
|
+
}
|
|
8
|
+
export interface MaterializeLogger {
|
|
9
|
+
warn(obj: Record<string, unknown>, msg: string): void;
|
|
10
|
+
error(obj: Record<string, unknown>, msg: string): void;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Re-arms the body-sync cue for one message (the read-path slice of
|
|
14
|
+
* `BodySyncQueueService`).
|
|
15
|
+
*/
|
|
16
|
+
export interface BodySyncCue {
|
|
17
|
+
requestBodySync(input: {
|
|
18
|
+
accountId: string;
|
|
19
|
+
mailboxId: string;
|
|
20
|
+
messageId: string;
|
|
21
|
+
uid?: number;
|
|
22
|
+
}): Promise<void>;
|
|
23
|
+
}
|
|
24
|
+
export interface MaterializeResult {
|
|
25
|
+
/** `false` when the body object was missing — the caller must not serve the body. */
|
|
26
|
+
ready: boolean;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Materialize the deferred per-part storage objects for a message before any
|
|
30
|
+
* part is read. Distinguishes the two failure modes the body contract cares
|
|
31
|
+
* about:
|
|
32
|
+
*
|
|
33
|
+
* - Missing storage object (never synced or lost) — re-arm the SYNC_MESSAGE_BODY
|
|
34
|
+
* cue so the worker (re)stores the body, and report `ready: false`. Not a
|
|
35
|
+
* 500: a retry succeeds once the worker catches up.
|
|
36
|
+
* - Any other error — rethrow so the request 500s and the bug is observable.
|
|
37
|
+
* Never swallowed, never "best-effort".
|
|
38
|
+
*
|
|
39
|
+
* No-op (`ready: true`) when the body isn't stored yet (no `bodyStorageKey`) or
|
|
40
|
+
* when the parts were written eagerly.
|
|
41
|
+
*/
|
|
42
|
+
export declare const materializeBodyParts: (materializer: BodyPartMaterializer, args: {
|
|
43
|
+
accountConfigId: string;
|
|
44
|
+
accountId: string;
|
|
45
|
+
mailboxId: string;
|
|
46
|
+
messageId: string;
|
|
47
|
+
uid?: number;
|
|
48
|
+
bodyStorageKey: string | undefined;
|
|
49
|
+
cue: BodySyncCue;
|
|
50
|
+
logger: MaterializeLogger;
|
|
51
|
+
}) => Promise<MaterializeResult>;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { isStorageNotFoundError } from "@remit/storage-service";
|
|
2
|
+
/**
|
|
3
|
+
* Materialize the deferred per-part storage objects for a message before any
|
|
4
|
+
* part is read. Distinguishes the two failure modes the body contract cares
|
|
5
|
+
* about:
|
|
6
|
+
*
|
|
7
|
+
* - Missing storage object (never synced or lost) — re-arm the SYNC_MESSAGE_BODY
|
|
8
|
+
* cue so the worker (re)stores the body, and report `ready: false`. Not a
|
|
9
|
+
* 500: a retry succeeds once the worker catches up.
|
|
10
|
+
* - Any other error — rethrow so the request 500s and the bug is observable.
|
|
11
|
+
* Never swallowed, never "best-effort".
|
|
12
|
+
*
|
|
13
|
+
* No-op (`ready: true`) when the body isn't stored yet (no `bodyStorageKey`) or
|
|
14
|
+
* when the parts were written eagerly.
|
|
15
|
+
*/
|
|
16
|
+
export const materializeBodyParts = async (materializer, args) => {
|
|
17
|
+
const { accountConfigId, accountId, mailboxId, messageId, uid } = args;
|
|
18
|
+
if (!args.bodyStorageKey)
|
|
19
|
+
return { ready: true };
|
|
20
|
+
try {
|
|
21
|
+
await materializer.ensureBodyPartsStored(accountConfigId, accountId, messageId, args.bodyStorageKey);
|
|
22
|
+
return { ready: true };
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
if (!isStorageNotFoundError(error))
|
|
26
|
+
throw error;
|
|
27
|
+
args.logger.warn({ messageId, accountId, mailboxId }, "Body object missing on read; re-arming body-sync cue and signalling retry");
|
|
28
|
+
await args.cue.requestBodySync({ accountId, mailboxId, messageId, uid });
|
|
29
|
+
return { ready: false };
|
|
30
|
+
}
|
|
31
|
+
};
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
import type { IAddressRepository, IEnvelopeRepository, IMailboxRepository, IMailboxSpecialUseRepository, IMessageFlagPushRepository, IMessageFlagRepository, IMessageRepository, IThreadMessageRepository } from "@remit/data-ports";
|
|
2
|
+
import type { JunkRoleMailboxes } from "@remit/data-ports/folder-role";
|
|
3
|
+
import type { ManagedConnectionFactory } from "./connection-factory.js";
|
|
4
|
+
import type { QuarantineService } from "./quarantine.js";
|
|
5
|
+
import type { SyncWarningService } from "./sync-warning.js";
|
|
6
|
+
import type { ImapAddress } from "./types.js";
|
|
7
|
+
/**
|
|
8
|
+
* A From address is only usable when it looks like a real mailbox: both parts
|
|
9
|
+
* present, neither is a known "could not parse" sentinel, and the host carries
|
|
10
|
+
* an actual domain (at least one dot). Detect this structurally so a fabricated
|
|
11
|
+
* string like `missing_mailbox@missing_domain` is never persisted as a sender.
|
|
12
|
+
*/
|
|
13
|
+
export declare const isParseableEmailAddress: (address: ImapAddress | undefined) => boolean;
|
|
14
|
+
export type AddressSighting = "junk" | "discarded" | "correspondent";
|
|
15
|
+
/** The account's Junk and Trash mailboxes, resolved once per batch. */
|
|
16
|
+
export interface AccountFolderRoles {
|
|
17
|
+
readonly junkMailboxId: string | null;
|
|
18
|
+
readonly trashMailboxId: string | null;
|
|
19
|
+
/**
|
|
20
|
+
* The same two roles across every account under the config, for the
|
|
21
|
+
* reconcile that follows a save into Junk. The address book is keyed by
|
|
22
|
+
* config, so a predicate over an address weighs sightings from all of them —
|
|
23
|
+
* resolving only this account withholds a sender on one and restores it on
|
|
24
|
+
* the next.
|
|
25
|
+
*/
|
|
26
|
+
readonly configJunkRoles: JunkRoleMailboxes;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* What a sighting of an address in this mailbox is worth. Junk withholds the
|
|
30
|
+
* sender from autocomplete (#822), Trash decides nothing either way, and
|
|
31
|
+
* anywhere else is a correspondent.
|
|
32
|
+
*
|
|
33
|
+
* Decided by mailbox IDENTITY against the account's resolved roles, not by the
|
|
34
|
+
* folder's flag and name: a role belongs to exactly one folder (RFC 032), and
|
|
35
|
+
* the folder it belongs to is whichever one the user appointed. Reading the
|
|
36
|
+
* name here meant a second folder called `Spam` also withheld its senders,
|
|
37
|
+
* while the folder the user actually appointed harvested every spammer in it as
|
|
38
|
+
* a correspondent (#837).
|
|
39
|
+
*/
|
|
40
|
+
export declare const addressSightingIn: (mailboxId: string, roles: AccountFolderRoles) => AddressSighting;
|
|
41
|
+
/**
|
|
42
|
+
* The name an envelope carries for an address, as it should be stored: any
|
|
43
|
+
* claim to be some other address removed (issue #826), the rest of the name
|
|
44
|
+
* kept. Refused wherever a name lands — the address book, the envelope the
|
|
45
|
+
* message header renders, and the sender label on the thread row.
|
|
46
|
+
*/
|
|
47
|
+
export declare const harvestedDisplayName: (address: ImapAddress | undefined) => string;
|
|
48
|
+
/**
|
|
49
|
+
* Parse an external `Date:` header into an epoch-millisecond integer.
|
|
50
|
+
*
|
|
51
|
+
* The IMAP envelope `date` is a raw RFC 2822 header copied verbatim from the
|
|
52
|
+
* message. It can be missing, malformed, or in a format `Date` cannot parse —
|
|
53
|
+
* `new Date(raw).getTime()` then yields `NaN`. `NaN` is not a valid integer
|
|
54
|
+
* and ElectroDB rejects it, which previously threw on the envelope upsert and
|
|
55
|
+
* (because the batch aborted on the first rejection) stalled the whole mailbox.
|
|
56
|
+
*
|
|
57
|
+
* When the header is unparseable we fall back to `fallbackMs` — the IMAP
|
|
58
|
+
* server's own INTERNALDATE receive time, always a valid integer. The raw
|
|
59
|
+
* header is preserved separately in `dateRaw`, so nothing is lost.
|
|
60
|
+
*/
|
|
61
|
+
export declare const parseHeaderDate: (raw: string | undefined, fallbackMs: number) => {
|
|
62
|
+
value: number;
|
|
63
|
+
usedFallback: boolean;
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* @deprecated Use ManagedConnectionFactory instead
|
|
67
|
+
*/
|
|
68
|
+
export type ImapConnectionFactory = () => {
|
|
69
|
+
connect(): Promise<void>;
|
|
70
|
+
disconnect(): Promise<void>;
|
|
71
|
+
};
|
|
72
|
+
export interface SyncLogger {
|
|
73
|
+
info(obj: Record<string, unknown>, msg: string): void;
|
|
74
|
+
warn(obj: Record<string, unknown>, msg: string): void;
|
|
75
|
+
error(obj: Record<string, unknown>, msg: string): void;
|
|
76
|
+
}
|
|
77
|
+
export interface SyncedMessage {
|
|
78
|
+
messageId: string;
|
|
79
|
+
uid: number;
|
|
80
|
+
}
|
|
81
|
+
export interface SyncMessagesResult {
|
|
82
|
+
syncedCount: number;
|
|
83
|
+
syncedMessageIds: string[];
|
|
84
|
+
syncedMessages: SyncedMessage[];
|
|
85
|
+
hasMore: boolean;
|
|
86
|
+
remainingCount: number;
|
|
87
|
+
/**
|
|
88
|
+
* The round had work to do and moved no cursor. Nothing throws on this
|
|
89
|
+
* path — a message that cannot be applied is caught and held back — so
|
|
90
|
+
* without this flag a mailbox that stops syncing looks exactly like one
|
|
91
|
+
* with nothing to sync. Callers must surface it.
|
|
92
|
+
*/
|
|
93
|
+
cursorStalled: boolean;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Pick the UIDs a full-enumeration round should sync, newest first.
|
|
97
|
+
*
|
|
98
|
+
* 1. New messages: UIDs above the forward watermark.
|
|
99
|
+
* 2. Backfill: UIDs below the lowest one synced so far.
|
|
100
|
+
* 3. A mailbox with no watermarks at all syncs everything.
|
|
101
|
+
*/
|
|
102
|
+
export declare const selectUidsToSync: (allUids: number[], lastSyncUid: number, highWaterMarkUid: number) => number[];
|
|
103
|
+
export declare class MessageSyncService {
|
|
104
|
+
private connectionFactory;
|
|
105
|
+
private mailboxService;
|
|
106
|
+
/**
|
|
107
|
+
* The account’s folder-role map. Required, not optional: which folder
|
|
108
|
+
* holds the Junk role decides whether a sighting withholds its sender
|
|
109
|
+
* from autocomplete, and a construction without it would quietly harvest
|
|
110
|
+
* spammers as correspondents.
|
|
111
|
+
*/
|
|
112
|
+
private mailboxSpecialUseService;
|
|
113
|
+
private messageService;
|
|
114
|
+
private envelopeService;
|
|
115
|
+
private addressService;
|
|
116
|
+
private threadMessageService;
|
|
117
|
+
private syncWarningService;
|
|
118
|
+
/**
|
|
119
|
+
* Pending outbound flag-push markers (#1273). Supplied, an inbound
|
|
120
|
+
* metadata change never overwrites a local flip that IMAP has not been
|
|
121
|
+
* told about yet; omitted, the server always wins.
|
|
122
|
+
*/
|
|
123
|
+
private flagPushMarkerService?;
|
|
124
|
+
/**
|
|
125
|
+
* The canonical flag record. Supplied, an inbound metadata change lands
|
|
126
|
+
* on the same record the outbound flip path reads, so a user's next
|
|
127
|
+
* flip is never dismissed as redundant.
|
|
128
|
+
*/
|
|
129
|
+
private messageFlagService?;
|
|
130
|
+
/**
|
|
131
|
+
* The set of messages already set aside (issue #72). Message sync never
|
|
132
|
+
* writes a record — only the body path can attribute a failure to the
|
|
133
|
+
* message — but it reads the set so a uid the body path quarantined is
|
|
134
|
+
* not re-fetched on every round.
|
|
135
|
+
*/
|
|
136
|
+
private quarantineService?;
|
|
137
|
+
private log;
|
|
138
|
+
constructor(connectionFactory: ManagedConnectionFactory, mailboxService: IMailboxRepository,
|
|
139
|
+
/**
|
|
140
|
+
* The account’s folder-role map. Required, not optional: which folder
|
|
141
|
+
* holds the Junk role decides whether a sighting withholds its sender
|
|
142
|
+
* from autocomplete, and a construction without it would quietly harvest
|
|
143
|
+
* spammers as correspondents.
|
|
144
|
+
*/
|
|
145
|
+
mailboxSpecialUseService: IMailboxSpecialUseRepository, messageService: IMessageRepository, envelopeService: IEnvelopeRepository, addressService: IAddressRepository, threadMessageService: IThreadMessageRepository, syncWarningService: SyncWarningService, logger?: SyncLogger,
|
|
146
|
+
/**
|
|
147
|
+
* Pending outbound flag-push markers (#1273). Supplied, an inbound
|
|
148
|
+
* metadata change never overwrites a local flip that IMAP has not been
|
|
149
|
+
* told about yet; omitted, the server always wins.
|
|
150
|
+
*/
|
|
151
|
+
flagPushMarkerService?: IMessageFlagPushRepository | undefined,
|
|
152
|
+
/**
|
|
153
|
+
* The canonical flag record. Supplied, an inbound metadata change lands
|
|
154
|
+
* on the same record the outbound flip path reads, so a user's next
|
|
155
|
+
* flip is never dismissed as redundant.
|
|
156
|
+
*/
|
|
157
|
+
messageFlagService?: IMessageFlagRepository | undefined,
|
|
158
|
+
/**
|
|
159
|
+
* The set of messages already set aside (issue #72). Message sync never
|
|
160
|
+
* writes a record — only the body path can attribute a failure to the
|
|
161
|
+
* message — but it reads the set so a uid the body path quarantined is
|
|
162
|
+
* not re-fetched on every round.
|
|
163
|
+
*/
|
|
164
|
+
quarantineService?: QuarantineService | undefined);
|
|
165
|
+
/**
|
|
166
|
+
* Sync ONE batch of messages for a mailbox using newest-first strategy.
|
|
167
|
+
*
|
|
168
|
+
* Uses dual-watermark tracking:
|
|
169
|
+
* - highWaterMarkUid: highest UID ever seen (detects new messages)
|
|
170
|
+
* - lastSyncUid: lowest UID processed (tracks backfill progress)
|
|
171
|
+
*
|
|
172
|
+
* Returns hasMore=true if there are more messages to sync. The caller
|
|
173
|
+
* should re-enqueue another sync event to continue processing.
|
|
174
|
+
*
|
|
175
|
+
* @param mailboxId - The database mailbox ID
|
|
176
|
+
* @param accountId - The account ID (scopes message/thread identity)
|
|
177
|
+
* @param accountConfigId - The account config ID (used for address linking)
|
|
178
|
+
* @param batchSize - Number of messages to process per batch
|
|
179
|
+
*/
|
|
180
|
+
syncMessages(mailboxId: string, accountId: string, accountConfigId: string, batchSize?: number): Promise<SyncMessagesResult>;
|
|
181
|
+
/**
|
|
182
|
+
* Re-key a mailbox's stored UIDs against a new UIDVALIDITY axis (#1272).
|
|
183
|
+
*
|
|
184
|
+
* One envelope-level pass (UID + Message-ID + INTERNALDATE, no body
|
|
185
|
+
* fetches — {@link matchCursorRebuild}) matched against the rows already
|
|
186
|
+
* stored for this mailbox:
|
|
187
|
+
* - Match → rewrite the row's UID mapping in place; bodies and threads are
|
|
188
|
+
* untouched.
|
|
189
|
+
* - Server message with no row → normal new-message sync (the same
|
|
190
|
+
* `trySaveMessage` pipeline the regular batch sync uses).
|
|
191
|
+
* - Row with no counterpart → expunged; removed through `deleteIfPlacement`
|
|
192
|
+
* on the uid the rebuild read (#1283 — the exact same "gone upstream"
|
|
193
|
+
* outcome as a body-sync retry exhaustion finding a stale row), so a row
|
|
194
|
+
* a move re-pointed in the meantime is left alone.
|
|
195
|
+
*
|
|
196
|
+
* Idempotent by construction: re-entering mid-rebuild (crash recovery —
|
|
197
|
+
* the mailbox was left `rebuilding`) simply redoes the same match/rewrite
|
|
198
|
+
* pass, which converges on the same result. `cursorState` is stamped
|
|
199
|
+
* `rebuilding` before any write and only cleared to `normal` after the
|
|
200
|
+
* watermarks are rebuilt, so a crash anywhere in between leaves the
|
|
201
|
+
* mailbox paused rather than falling back to the stale axis.
|
|
202
|
+
*/
|
|
203
|
+
private rebuildCursor;
|
|
204
|
+
/**
|
|
205
|
+
* Page through every ThreadMessage row for this mailbox, projecting just
|
|
206
|
+
* the fields {@link matchCursorRebuild} needs. ThreadMessage (not
|
|
207
|
+
* Message) is the read source: it already denormalizes `messageIdHeader`
|
|
208
|
+
* and `internalDate` alongside `uid`, so this needs no per-row Envelope
|
|
209
|
+
* lookup.
|
|
210
|
+
*/
|
|
211
|
+
private listExistingCursorRows;
|
|
212
|
+
/**
|
|
213
|
+
* One CHANGEDSINCE round (issue #20).
|
|
214
|
+
*
|
|
215
|
+
* A single `FETCH ... (CHANGEDSINCE <modseq>)` over the whole UID space
|
|
216
|
+
* returns both the messages that arrived and the messages whose metadata
|
|
217
|
+
* changed since the stored watermark, so a flag flipped on another client
|
|
218
|
+
* is picked up without enumerating the folder. Rows already present in
|
|
219
|
+
* this folder take the metadata path — the envelope, body structure and
|
|
220
|
+
* addresses are immutable, so re-writing them for a read-state change would
|
|
221
|
+
* be pure waste; everything else, a row another client moved here from a
|
|
222
|
+
* folder it points at included, takes the normal save pipeline (#1386).
|
|
223
|
+
*
|
|
224
|
+
* Expunges are invisible here (CONDSTORE without QRESYNC never reports
|
|
225
|
+
* them, RFC 7162 Section 3.1.2.1) — they remain the reconcile path's job.
|
|
226
|
+
*/
|
|
227
|
+
private syncChangedSince;
|
|
228
|
+
private settleHeaderWarnings;
|
|
229
|
+
/**
|
|
230
|
+
* Apply one message from a CHANGEDSINCE result without ever rejecting —
|
|
231
|
+
* same contract as {@link trySaveMessage}, so a single unapplicable change
|
|
232
|
+
* holds the watermark back instead of failing the round.
|
|
233
|
+
*/
|
|
234
|
+
private tryApplyChange;
|
|
235
|
+
private applyChange;
|
|
236
|
+
/**
|
|
237
|
+
* Bring a stored row's read and star state, and the $Junk/$NotJunk
|
|
238
|
+
* keywords on the canonical MessageFlag record, in line with the
|
|
239
|
+
* server's flags.
|
|
240
|
+
*
|
|
241
|
+
* A field with a pending outbound push is left alone: the user flipped it
|
|
242
|
+
* locally, IMAP has not been told yet, and the server's answer is
|
|
243
|
+
* therefore known-stale for that field. Writing it back would revert the
|
|
244
|
+
* flip in front of the user and then push the reverted value.
|
|
245
|
+
*/
|
|
246
|
+
private applyServerFlags;
|
|
247
|
+
/**
|
|
248
|
+
* Set or clear one flag on the canonical record. Both repository calls are
|
|
249
|
+
* idempotent, so a re-applied change is a no-op rather than a conflict.
|
|
250
|
+
*/
|
|
251
|
+
private setMessageFlag;
|
|
252
|
+
private hasPendingPush;
|
|
253
|
+
/**
|
|
254
|
+
* Mirror one RFC 5788 keyword ($Junk / $NotJunk) from the server into the
|
|
255
|
+
* canonical MessageFlag record, guarded the same way as the system flags:
|
|
256
|
+
* a pending local push wins over the server's known-stale answer.
|
|
257
|
+
*
|
|
258
|
+
* This mailbox is never remit-only — Apple Mail or another IMAP client is
|
|
259
|
+
* routinely connected too — so the keyword is message state, nothing
|
|
260
|
+
* more. It never reaches the Address model: reading another client's
|
|
261
|
+
* keyword as a standing rule about the sender is the exact fight this
|
|
262
|
+
* mirror must not pick.
|
|
263
|
+
*/
|
|
264
|
+
private mirrorKeywordFlag;
|
|
265
|
+
/**
|
|
266
|
+
* Messages the forwarded-message repair queued — their part rows dropped,
|
|
267
|
+
* the lost row only on the mail server — are fetched again by UID and saved
|
|
268
|
+
* through the same path as any other sighting, which writes their parts. No
|
|
269
|
+
* watermark moves: these UIDs are already behind them. Each leaves the queue
|
|
270
|
+
* once saved or once the server returns no usable row for it, whatever
|
|
271
|
+
* parts came back; only a failed save keeps it for the next round. Resolves
|
|
272
|
+
* `true` when it touched the connection, which the probe of departures may
|
|
273
|
+
* have left on another folder.
|
|
274
|
+
*/
|
|
275
|
+
private restoreMissingStructure;
|
|
276
|
+
/**
|
|
277
|
+
* Fetch a batch of messages using the managed connection.
|
|
278
|
+
* Assumes mailbox is already open from fetchUidsToSync.
|
|
279
|
+
*/
|
|
280
|
+
private fetchMessageBatch;
|
|
281
|
+
/**
|
|
282
|
+
* Save a single message without ever rejecting. Any error is caught and
|
|
283
|
+
* reported as a `failed` outcome so it cannot abort the surrounding `pMap`
|
|
284
|
+
* batch — the failed UID is held back from the watermark and retried next
|
|
285
|
+
* cycle. This is the guardrail that stops a single unsaveable message from
|
|
286
|
+
* permanently freezing the mailbox (#817).
|
|
287
|
+
*/
|
|
288
|
+
/**
|
|
289
|
+
* Which folders hold Junk and Trash for this account, resolved once per
|
|
290
|
+
* batch rather than per message: the lookup reads an appointment row and
|
|
291
|
+
* the account’s mailbox list, and every message in a batch shares both.
|
|
292
|
+
*/
|
|
293
|
+
private folderRolesFor;
|
|
294
|
+
/**
|
|
295
|
+
* Ask the folders this batch's rows point at whether they still hold them
|
|
296
|
+
* (#1146).
|
|
297
|
+
*
|
|
298
|
+
* Between the batch FETCH and the save pass, in all three rounds, and in that
|
|
299
|
+
* position on purpose. It SELECTs another folder on the one connection this
|
|
300
|
+
* sync has, so it may not run while this mailbox's own fetches are still
|
|
301
|
+
* outstanding; and the save pass has to have the answer already, because the
|
|
302
|
+
* transaction that re-points a row is where the answer is spent. Nothing
|
|
303
|
+
* after the save pass touches IMAP, so leaving another folder selected costs
|
|
304
|
+
* the round nothing.
|
|
305
|
+
*/
|
|
306
|
+
private probeDepartures;
|
|
307
|
+
private trySaveMessage;
|
|
308
|
+
private saveMessage;
|
|
309
|
+
/**
|
|
310
|
+
* Whether this folder already holds a copy the user made of this message
|
|
311
|
+
* (#75). A copy is a second row under its own derived id, so the sighting is
|
|
312
|
+
* that copy standing where it was put — the original has not moved, and
|
|
313
|
+
* following it would take the mail out of the folder the user copied it from.
|
|
314
|
+
*/
|
|
315
|
+
private holdsCopyOf;
|
|
316
|
+
private saveAddresses;
|
|
317
|
+
/**
|
|
318
|
+
* The thread a synced message files under. A message keeps the thread it is
|
|
319
|
+
* already stored in; a reply joins the thread its parent is stored in;
|
|
320
|
+
* otherwise the thread is derived from the root `threadRootOf` names.
|
|
321
|
+
*/
|
|
322
|
+
private threadFor;
|
|
323
|
+
/**
|
|
324
|
+
* Joins the rows written on either side of a parent and its reply, after
|
|
325
|
+
* this message's own row exists. Replies that arrived before this message,
|
|
326
|
+
* and so were keyed on its own id, follow it into its thread; and a reply
|
|
327
|
+
* whose parent was saved while it was being saved follows the parent, but
|
|
328
|
+
* only from the thread it derived for itself — a thread it joined or was
|
|
329
|
+
* already stored in is left as it is.
|
|
330
|
+
*
|
|
331
|
+
* Both checks run after the create, so of a parent and a reply saved
|
|
332
|
+
* concurrently at least one sees the other's row: neither is left in a
|
|
333
|
+
* thread of its own.
|
|
334
|
+
*/
|
|
335
|
+
private joinThreads;
|
|
336
|
+
/**
|
|
337
|
+
* Create the ThreadMessage for a synced message, then join it to its
|
|
338
|
+
* parent and replies.
|
|
339
|
+
*
|
|
340
|
+
* `category` is the value the caller already holds from the Message it just
|
|
341
|
+
* wrote, so the row starts out agreeing with it rather than defaulting to
|
|
342
|
+
* `uncategorized` behind an already-classified message.
|
|
343
|
+
*/
|
|
344
|
+
private createThreadForMessage;
|
|
345
|
+
}
|