@remit/mailbox-service 0.0.110 → 0.0.112
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/package.json +21 -6
- package/src/body-part-mapper.ts +51 -1
- package/src/body-sync-queue.test.ts +39 -99
- package/src/body-sync-terminal.test.ts +115 -277
- package/src/body-sync-terminal.ts +10 -12
- package/src/body-sync.test.ts +925 -0
- package/src/body-sync.ts +226 -200
- package/src/classification-backfill.test.ts +197 -527
- package/src/classify-message.test.ts +11 -74
- package/src/external-move.test.ts +102 -209
- package/src/external-move.ts +13 -0
- package/src/filter-operations.test.ts +319 -0
- package/src/filter-operations.ts +377 -0
- package/src/filter-update.test.ts +313 -0
- package/src/filters/filter-config.test.ts +55 -197
- package/src/filters/filter-config.ts +4 -39
- package/src/filters/match.test.ts +1 -38
- package/src/filters/match.ts +3 -52
- package/src/filters/pipeline.test.ts +158 -289
- package/src/filters/pipeline.ts +56 -170
- package/src/flag-push-terminal.ts +9 -11
- package/src/flag-push.test.ts +76 -372
- package/src/flag-queue.test.ts +197 -0
- package/src/folder-operations.test.ts +273 -0
- package/src/folder-operations.ts +217 -0
- package/src/forwarded-message.test.ts +122 -0
- package/src/imapflow-connect-retry.test.ts +76 -113
- package/src/imapflow-connection.test.ts +34 -0
- package/src/imapflow-connection.ts +19 -15
- package/src/index.ts +59 -12
- package/src/list-id-backfill.test.ts +133 -499
- package/src/mailbox-cursor.test.ts +81 -189
- package/src/mailbox-delete-intent.test.ts +78 -205
- package/src/mailbox-intent.ts +5 -3
- package/src/mailbox-management.test.ts +107 -220
- package/src/mailbox-management.ts +1 -2
- package/src/mailbox-presence.test.ts +36 -34
- package/src/mailbox-queue.ts +2 -20
- package/src/mailbox-rename-intent.test.ts +156 -325
- package/src/mailbox-rename-subtree.test.ts +205 -582
- package/src/mailbox-sync.test.ts +182 -744
- package/src/message-move.test.ts +726 -0
- package/src/message-move.ts +227 -57
- package/src/message-read.ts +87 -0
- package/src/message-save-retry.test.ts +90 -0
- package/src/message-sync-flags.test.ts +10 -1
- package/src/message-sync.test.ts +762 -0
- package/src/message-sync.ts +409 -239
- package/src/mime-walker.ts +16 -15
- package/src/organize-job.ts +93 -0
- package/src/outbox-attachment.test.ts +97 -209
- package/src/outbox-failed-recovery.test.ts +198 -269
- package/src/outbox-queue.test.ts +24 -0
- package/src/outbox-queue.ts +25 -8
- package/src/ownership.ts +140 -0
- package/src/placement-move-terminal.ts +9 -12
- package/src/placement-move.ts +33 -6
- package/src/placement-settled.ts +28 -4
- package/src/placement.test.ts +404 -0
- package/src/quarantine.test.ts +90 -148
- package/src/quarantine.ts +3 -2
- package/src/refusal.ts +29 -0
- package/src/restore-source-placement.ts +0 -4
- package/src/spam-report.test.ts +238 -710
- package/src/sync-warning.test.ts +213 -0
- package/src/sync-warning.ts +56 -0
- package/src/test-helpers/fake-imap.ts +690 -0
- package/src/test-helpers/test-queue.ts +56 -0
- package/src/test-helpers/test-store.ts +299 -0
- package/src/thread-root.test.ts +29 -0
- package/src/thread-root.ts +51 -0
- package/src/types/mailparser-augment.d.ts +8 -0
- package/src/types.ts +1 -1
- package/src/adversarial-mime.e2e.test.ts +0 -307
- 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-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-resync.test.ts +0 -605
- package/src/body-sync-unclassifiable-once.test.ts +0 -260
- package/src/body-sync-unsubscribed-auto-read.test.ts +0 -176
- package/src/filters/anchor-drift.ts +0 -55
- package/src/filters/message-embedder.test.ts +0 -39
- package/src/filters/message-embedder.ts +0 -33
- package/src/flag-push-terminal.test.ts +0 -209
- package/src/flag-queue-stars.test.ts +0 -171
- package/src/imapflow-connection.e2e.test.ts +0 -335
- package/src/imapflow-connection.integ.test.ts +0 -316
- 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-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-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/outbox-queue-settled-row.test.ts +0 -140
- package/src/pass-through-unit-of-work.ts +0 -17
- package/src/placement-move-terminal.test.ts +0 -300
- package/src/placement-move-unsettled.test.ts +0 -275
- package/src/placement-transition-race.test.ts +0 -482
- package/src/stale-message-reconcile.test.ts +0 -82
- package/src/stale-message-reconcile.ts +0 -55
- package/src/test-helpers/isolated-mailbox.ts +0 -44
- package/src/test-helpers/mailfuzz-connection.ts +0 -27
- 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/src/message-sync.ts
CHANGED
|
@@ -8,7 +8,6 @@ import type {
|
|
|
8
8
|
IMessageFlagRepository,
|
|
9
9
|
IMessageRepository,
|
|
10
10
|
IThreadMessageRepository,
|
|
11
|
-
IUnitOfWork,
|
|
12
11
|
MailboxItem,
|
|
13
12
|
ThreadMessageItem,
|
|
14
13
|
} from "@remit/data-ports";
|
|
@@ -19,9 +18,11 @@ import {
|
|
|
19
18
|
deriveBodyPartId,
|
|
20
19
|
deriveCopyMessageId,
|
|
21
20
|
deriveEnvelopeId,
|
|
21
|
+
deriveMessageId,
|
|
22
22
|
deriveMessageIdFromSource,
|
|
23
23
|
deriveThreadId,
|
|
24
24
|
isValidMessageId,
|
|
25
|
+
wireMessageId,
|
|
25
26
|
} from "@remit/data-ports/id";
|
|
26
27
|
import {
|
|
27
28
|
AddressRole,
|
|
@@ -29,6 +30,7 @@ import {
|
|
|
29
30
|
MessageKeywordFlag,
|
|
30
31
|
MessageSystemFlag,
|
|
31
32
|
StarColor,
|
|
33
|
+
SyncWarningStage,
|
|
32
34
|
} from "@remit/domain-enums";
|
|
33
35
|
import pMap from "p-map";
|
|
34
36
|
import type { ManagedConnectionFactory } from "./connection-factory.js";
|
|
@@ -45,9 +47,8 @@ import {
|
|
|
45
47
|
matchCursorRebuild,
|
|
46
48
|
} from "./mailbox-cursor-rebuild.js";
|
|
47
49
|
import { ROOT_PART_PATH, walkMimeStructure } from "./mime-walker.js";
|
|
48
|
-
import { PassThroughUnitOfWork } from "./pass-through-unit-of-work.js";
|
|
49
50
|
import type { QuarantineService } from "./quarantine.js";
|
|
50
|
-
import {
|
|
51
|
+
import type { SyncWarningRound, SyncWarningService } from "./sync-warning.js";
|
|
51
52
|
import {
|
|
52
53
|
advanceChangeCursor,
|
|
53
54
|
advanceUidWatermarks,
|
|
@@ -59,6 +60,7 @@ import {
|
|
|
59
60
|
parseChangeCursor,
|
|
60
61
|
parseModseq,
|
|
61
62
|
} from "./sync-watermarks.js";
|
|
63
|
+
import { messageReferencesOf, parentOf, threadRootOf } from "./thread-root.js";
|
|
62
64
|
import type {
|
|
63
65
|
ImapAddress,
|
|
64
66
|
ImapBodyStructure,
|
|
@@ -262,6 +264,21 @@ const unresolvedSightingUids = (outcomes: BatchOutcome[]): number[] =>
|
|
|
262
264
|
: [],
|
|
263
265
|
);
|
|
264
266
|
|
|
267
|
+
const syncedUidsOf = (outcomes: BatchOutcome[]): number[] =>
|
|
268
|
+
outcomes.flatMap((outcome) =>
|
|
269
|
+
outcome.kind === "saved" && !outcome.result?.unresolvedSighting
|
|
270
|
+
? [outcome.uid]
|
|
271
|
+
: [],
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
const saveFailedUidsOf = (outcomes: BatchOutcome[]): number[] =>
|
|
275
|
+
outcomes.flatMap((outcome) =>
|
|
276
|
+
outcome.kind === "failed" ? [outcome.uid] : [],
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
const envelopeLessUidsOf = (messages: ImapMessage[]): number[] =>
|
|
280
|
+
messages.flatMap((msg) => (hasEnvelope(msg) ? [] : [msg.uid]));
|
|
281
|
+
|
|
265
282
|
const emptySyncResult = (): SyncMessagesResult => ({
|
|
266
283
|
syncedCount: 0,
|
|
267
284
|
syncedMessageIds: [],
|
|
@@ -321,7 +338,6 @@ const messageIdForSighting = (
|
|
|
321
338
|
|
|
322
339
|
export class MessageSyncService {
|
|
323
340
|
private log: SyncLogger;
|
|
324
|
-
private unitOfWork: IUnitOfWork;
|
|
325
341
|
|
|
326
342
|
constructor(
|
|
327
343
|
private connectionFactory: ManagedConnectionFactory,
|
|
@@ -334,11 +350,11 @@ export class MessageSyncService {
|
|
|
334
350
|
*/
|
|
335
351
|
private mailboxSpecialUseService: IMailboxSpecialUseRepository,
|
|
336
352
|
private messageService: IMessageRepository,
|
|
337
|
-
envelopeService: IEnvelopeRepository,
|
|
338
|
-
addressService: IAddressRepository,
|
|
353
|
+
private envelopeService: IEnvelopeRepository,
|
|
354
|
+
private addressService: IAddressRepository,
|
|
339
355
|
private threadMessageService: IThreadMessageRepository,
|
|
356
|
+
private syncWarningService: SyncWarningService,
|
|
340
357
|
logger?: SyncLogger,
|
|
341
|
-
unitOfWork?: IUnitOfWork,
|
|
342
358
|
/**
|
|
343
359
|
* Pending outbound flag-push markers (#1273). Supplied, an inbound
|
|
344
360
|
* metadata change never overwrites a local flip that IMAP has not been
|
|
@@ -360,14 +376,6 @@ export class MessageSyncService {
|
|
|
360
376
|
private quarantineService?: QuarantineService,
|
|
361
377
|
) {
|
|
362
378
|
this.log = logger ?? noopLogger;
|
|
363
|
-
this.unitOfWork =
|
|
364
|
-
unitOfWork ??
|
|
365
|
-
new PassThroughUnitOfWork({
|
|
366
|
-
message: messageService,
|
|
367
|
-
envelope: envelopeService,
|
|
368
|
-
address: addressService,
|
|
369
|
-
threadMessage: threadMessageService,
|
|
370
|
-
});
|
|
371
379
|
}
|
|
372
380
|
|
|
373
381
|
/**
|
|
@@ -430,6 +438,17 @@ export class MessageSyncService {
|
|
|
430
438
|
return emptySyncResult();
|
|
431
439
|
}
|
|
432
440
|
|
|
441
|
+
if (
|
|
442
|
+
await this.restoreMissingStructure(
|
|
443
|
+
mailbox,
|
|
444
|
+
accountId,
|
|
445
|
+
accountConfigId,
|
|
446
|
+
batchSize,
|
|
447
|
+
)
|
|
448
|
+
) {
|
|
449
|
+
await connection.openBox(mailboxPath);
|
|
450
|
+
}
|
|
451
|
+
|
|
433
452
|
const cursor = parseChangeCursor(mailbox.highestModseq);
|
|
434
453
|
if (hasChangeCursor(cursor) && connection.supportsCondstore()) {
|
|
435
454
|
return this.syncChangedSince({
|
|
@@ -453,6 +472,17 @@ export class MessageSyncService {
|
|
|
453
472
|
const deletedCount = status.deletedCount;
|
|
454
473
|
|
|
455
474
|
if (uids.length === 0) {
|
|
475
|
+
await this.settleHeaderWarnings(
|
|
476
|
+
accountConfigId,
|
|
477
|
+
mailbox,
|
|
478
|
+
box.uidvalidity,
|
|
479
|
+
{
|
|
480
|
+
failedUids: [],
|
|
481
|
+
syncedUids: [],
|
|
482
|
+
serverUids: { kind: "Listed", uids: allUids },
|
|
483
|
+
},
|
|
484
|
+
);
|
|
485
|
+
|
|
456
486
|
// Nothing left to enumerate: the folder is fully covered on this
|
|
457
487
|
// UIDVALIDITY axis, which is the one moment a mod-sequence watermark
|
|
458
488
|
// can be seeded without hiding unsynced history behind it. From the
|
|
@@ -616,6 +646,12 @@ export class MessageSyncService {
|
|
|
616
646
|
// `failedUids` too: the round did not finish with it either.
|
|
617
647
|
const enumerationComplete = !hasMore && failedUids.size === 0;
|
|
618
648
|
|
|
649
|
+
await this.settleHeaderWarnings(accountConfigId, mailbox, box.uidvalidity, {
|
|
650
|
+
failedUids: [...saveFailedUids, ...envelopeLessUidsOf(messages)],
|
|
651
|
+
syncedUids: syncedUidsOf(outcomes),
|
|
652
|
+
serverUids: { kind: "Listed", uids: allUids },
|
|
653
|
+
});
|
|
654
|
+
|
|
619
655
|
await this.mailboxService.update(accountId, mailboxId, {
|
|
620
656
|
lastSyncUid: newLastSyncUid,
|
|
621
657
|
highWaterMarkUid: newHighWaterMark,
|
|
@@ -682,9 +718,10 @@ export class MessageSyncService {
|
|
|
682
718
|
* untouched.
|
|
683
719
|
* - Server message with no row → normal new-message sync (the same
|
|
684
720
|
* `trySaveMessage` pipeline the regular batch sync uses).
|
|
685
|
-
* - Row with no counterpart → expunged;
|
|
686
|
-
*
|
|
687
|
-
* as a body-sync retry exhaustion finding a stale row)
|
|
721
|
+
* - Row with no counterpart → expunged; removed through `deleteIfPlacement`
|
|
722
|
+
* on the uid the rebuild read (#1283 — the exact same "gone upstream"
|
|
723
|
+
* outcome as a body-sync retry exhaustion finding a stale row), so a row
|
|
724
|
+
* a move re-pointed in the meantime is left alone.
|
|
688
725
|
*
|
|
689
726
|
* Idempotent by construction: re-entering mid-rebuild (crash recovery —
|
|
690
727
|
* the mailbox was left `rebuilding`) simply redoes the same match/rewrite
|
|
@@ -737,9 +774,7 @@ export class MessageSyncService {
|
|
|
737
774
|
await pMap(
|
|
738
775
|
matched,
|
|
739
776
|
async ({ messageId, newUid, threadMessage }) => {
|
|
740
|
-
await this.
|
|
741
|
-
repos.message.updateUid(messageId, newUid, mailboxId),
|
|
742
|
-
);
|
|
777
|
+
await this.messageService.updateUid(messageId, newUid, mailboxId);
|
|
743
778
|
// Rewrite the denormalized ThreadMessage.uid alongside Message.uid —
|
|
744
779
|
// a normal move keeps both in sync (see buildThreadMessageMoveUpdate
|
|
745
780
|
// in message-move.ts), and the rebuild must too, or a resumed
|
|
@@ -747,41 +782,34 @@ export class MessageSyncService {
|
|
|
747
782
|
// reads uid from ThreadMessage) and any reader of the list
|
|
748
783
|
// projection sees a stale UID.
|
|
749
784
|
if (threadMessage) {
|
|
750
|
-
await this.
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
{
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
hasAttachment: threadMessage.hasAttachment,
|
|
763
|
-
},
|
|
785
|
+
await this.threadMessageService.update(
|
|
786
|
+
threadMessage.accountConfigId,
|
|
787
|
+
threadMessage.threadMessageId,
|
|
788
|
+
{ uid: newUid },
|
|
789
|
+
{
|
|
790
|
+
composites: {
|
|
791
|
+
sentDate: threadMessage.sentDate,
|
|
792
|
+
mailboxId: threadMessage.mailboxId,
|
|
793
|
+
isRead: threadMessage.isRead,
|
|
794
|
+
isDeleted: threadMessage.isDeleted,
|
|
795
|
+
hasStars: threadMessage.hasStars,
|
|
796
|
+
hasAttachment: threadMessage.hasAttachment,
|
|
764
797
|
},
|
|
765
|
-
|
|
798
|
+
},
|
|
766
799
|
);
|
|
767
800
|
}
|
|
768
801
|
},
|
|
769
802
|
{ concurrency: MESSAGE_SAVE_CONCURRENCY },
|
|
770
803
|
);
|
|
771
804
|
|
|
805
|
+
const stale = new Set(staleMessageIds);
|
|
772
806
|
await pMap(
|
|
773
|
-
|
|
774
|
-
(
|
|
775
|
-
this.
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
threadMessageService: repos.threadMessage,
|
|
780
|
-
},
|
|
781
|
-
accountConfigId,
|
|
782
|
-
messageId,
|
|
783
|
-
),
|
|
784
|
-
),
|
|
807
|
+
existingRows.filter((row) => stale.has(row.messageId)),
|
|
808
|
+
(row) =>
|
|
809
|
+
this.messageService.deleteIfPlacement(row.messageId, {
|
|
810
|
+
mailboxId,
|
|
811
|
+
uid: row.uid,
|
|
812
|
+
}),
|
|
785
813
|
{ concurrency: MESSAGE_SAVE_CONCURRENCY },
|
|
786
814
|
);
|
|
787
815
|
|
|
@@ -857,6 +885,15 @@ export class MessageSyncService {
|
|
|
857
885
|
: Number.POSITIVE_INFINITY;
|
|
858
886
|
const coveredUids = serverUids.filter((uid) => uid < lowestFailure);
|
|
859
887
|
|
|
888
|
+
await this.settleHeaderWarnings(accountConfigId, mailbox, box.uidvalidity, {
|
|
889
|
+
failedUids: [
|
|
890
|
+
...saveFailedUidsOf(outcomes),
|
|
891
|
+
...envelopeLessUidsOf(newMessages),
|
|
892
|
+
],
|
|
893
|
+
syncedUids: syncedUidsOf(outcomes),
|
|
894
|
+
serverUids: { kind: "Listed", uids: allUids },
|
|
895
|
+
});
|
|
896
|
+
|
|
860
897
|
await this.mailboxService.update(accountId, mailboxId, {
|
|
861
898
|
cursorState: MailboxCursorState.normal,
|
|
862
899
|
uidValidity: box.uidvalidity,
|
|
@@ -959,11 +996,11 @@ export class MessageSyncService {
|
|
|
959
996
|
* A single `FETCH ... (CHANGEDSINCE <modseq>)` over the whole UID space
|
|
960
997
|
* returns both the messages that arrived and the messages whose metadata
|
|
961
998
|
* changed since the stored watermark, so a flag flipped on another client
|
|
962
|
-
* is picked up without enumerating the folder. Rows already present
|
|
963
|
-
* the metadata path — the envelope, body structure and
|
|
964
|
-
* immutable, so re-writing them for a read-state change would
|
|
965
|
-
* waste; everything else
|
|
966
|
-
* pipeline.
|
|
999
|
+
* is picked up without enumerating the folder. Rows already present in
|
|
1000
|
+
* this folder take the metadata path — the envelope, body structure and
|
|
1001
|
+
* addresses are immutable, so re-writing them for a read-state change would
|
|
1002
|
+
* be pure waste; everything else, a row another client moved here from a
|
|
1003
|
+
* folder it points at included, takes the normal save pipeline (#1386).
|
|
967
1004
|
*
|
|
968
1005
|
* Expunges are invisible here (CONDSTORE without QRESYNC never reports
|
|
969
1006
|
* them, RFC 7162 Section 3.1.2.1) — they remain the reconcile path's job.
|
|
@@ -1087,6 +1124,15 @@ export class MessageSyncService {
|
|
|
1087
1124
|
});
|
|
1088
1125
|
const highestModseq = formatChangeCursor(nextCursor);
|
|
1089
1126
|
|
|
1127
|
+
await this.settleHeaderWarnings(accountConfigId, mailbox, box.uidvalidity, {
|
|
1128
|
+
failedUids: [...saveFailedUids, ...unusableUids],
|
|
1129
|
+
syncedUids: syncedUidsOf(outcomes),
|
|
1130
|
+
serverUids: {
|
|
1131
|
+
kind: "OnRequest",
|
|
1132
|
+
list: () => connection.search(["ALL"]),
|
|
1133
|
+
},
|
|
1134
|
+
});
|
|
1135
|
+
|
|
1090
1136
|
// The UID watermark obeys the same clamp as the enumeration path even
|
|
1091
1137
|
// though the cursor governs retries here: a mailbox that later falls
|
|
1092
1138
|
// back to enumeration must not find a failed UID already behind its
|
|
@@ -1153,6 +1199,24 @@ export class MessageSyncService {
|
|
|
1153
1199
|
};
|
|
1154
1200
|
}
|
|
1155
1201
|
|
|
1202
|
+
private async settleHeaderWarnings(
|
|
1203
|
+
accountConfigId: string,
|
|
1204
|
+
mailbox: MailboxItem,
|
|
1205
|
+
uidValidity: number,
|
|
1206
|
+
round: SyncWarningRound,
|
|
1207
|
+
): Promise<void> {
|
|
1208
|
+
await this.syncWarningService.settle(
|
|
1209
|
+
{
|
|
1210
|
+
accountConfigId,
|
|
1211
|
+
accountId: mailbox.accountId,
|
|
1212
|
+
mailboxId: mailbox.mailboxId,
|
|
1213
|
+
uidValidity,
|
|
1214
|
+
},
|
|
1215
|
+
SyncWarningStage.Header,
|
|
1216
|
+
round,
|
|
1217
|
+
);
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1156
1220
|
/**
|
|
1157
1221
|
* Apply one message from a CHANGEDSINCE result without ever rejecting —
|
|
1158
1222
|
* same contract as {@link trySaveMessage}, so a single unapplicable change
|
|
@@ -1218,7 +1282,16 @@ export class MessageSyncService {
|
|
|
1218
1282
|
}
|
|
1219
1283
|
|
|
1220
1284
|
await this.applyServerFlags(existing, msg.flags);
|
|
1221
|
-
return null;
|
|
1285
|
+
if (!departures.needsSave.has(messageId)) return null;
|
|
1286
|
+
|
|
1287
|
+
return this.saveMessage(
|
|
1288
|
+
mailbox,
|
|
1289
|
+
accountId,
|
|
1290
|
+
accountConfigId,
|
|
1291
|
+
msg,
|
|
1292
|
+
roles,
|
|
1293
|
+
departures,
|
|
1294
|
+
);
|
|
1222
1295
|
}
|
|
1223
1296
|
|
|
1224
1297
|
/**
|
|
@@ -1392,6 +1465,72 @@ export class MessageSyncService {
|
|
|
1392
1465
|
await this.setMessageFlag(messageId, flagName, present);
|
|
1393
1466
|
}
|
|
1394
1467
|
|
|
1468
|
+
/**
|
|
1469
|
+
* Messages the forwarded-message repair queued — their part rows dropped,
|
|
1470
|
+
* the lost row only on the mail server — are fetched again by UID and saved
|
|
1471
|
+
* through the same path as any other sighting, which writes their parts. No
|
|
1472
|
+
* watermark moves: these UIDs are already behind them. Each leaves the queue
|
|
1473
|
+
* once saved or once the server returns no usable row for it, whatever
|
|
1474
|
+
* parts came back; only a failed save keeps it for the next round. Resolves
|
|
1475
|
+
* `true` when it touched the connection, which the probe of departures may
|
|
1476
|
+
* have left on another folder.
|
|
1477
|
+
*/
|
|
1478
|
+
private async restoreMissingStructure(
|
|
1479
|
+
mailbox: MailboxItem,
|
|
1480
|
+
accountId: string,
|
|
1481
|
+
accountConfigId: string,
|
|
1482
|
+
batchSize: number,
|
|
1483
|
+
): Promise<boolean> {
|
|
1484
|
+
const queued = await this.envelopeService.listQueuedStructureRefetches(
|
|
1485
|
+
mailbox.mailboxId,
|
|
1486
|
+
batchSize,
|
|
1487
|
+
);
|
|
1488
|
+
if (queued.length === 0) return false;
|
|
1489
|
+
|
|
1490
|
+
const applicable = (
|
|
1491
|
+
await this.fetchMessageBatch(queued.map(({ uid }) => uid))
|
|
1492
|
+
).filter(hasEnvelope);
|
|
1493
|
+
const roles = await this.folderRolesFor(accountId, accountConfigId);
|
|
1494
|
+
const departures = await this.probeDepartures(
|
|
1495
|
+
mailbox,
|
|
1496
|
+
accountId,
|
|
1497
|
+
applicable,
|
|
1498
|
+
);
|
|
1499
|
+
const outcomes = await pMap(
|
|
1500
|
+
applicable,
|
|
1501
|
+
(msg) =>
|
|
1502
|
+
this.trySaveMessage(
|
|
1503
|
+
mailbox,
|
|
1504
|
+
accountId,
|
|
1505
|
+
accountConfigId,
|
|
1506
|
+
msg,
|
|
1507
|
+
roles,
|
|
1508
|
+
departures,
|
|
1509
|
+
),
|
|
1510
|
+
{ concurrency: MESSAGE_SAVE_CONCURRENCY },
|
|
1511
|
+
);
|
|
1512
|
+
const failedUids = new Set(
|
|
1513
|
+
outcomes.flatMap((outcome) =>
|
|
1514
|
+
outcome.kind === "failed" ? [outcome.uid] : [],
|
|
1515
|
+
),
|
|
1516
|
+
);
|
|
1517
|
+
await this.envelopeService.clearStructureRefetches(
|
|
1518
|
+
queued
|
|
1519
|
+
.filter(({ uid }) => !failedUids.has(uid))
|
|
1520
|
+
.map(({ messageId }) => messageId),
|
|
1521
|
+
);
|
|
1522
|
+
this.log.info(
|
|
1523
|
+
{
|
|
1524
|
+
mailboxId: mailbox.mailboxId,
|
|
1525
|
+
requested: queued.length,
|
|
1526
|
+
saved: outcomes.length - failedUids.size,
|
|
1527
|
+
failed: failedUids.size,
|
|
1528
|
+
},
|
|
1529
|
+
"Re-read the structure of messages the forwarded-message repair queued",
|
|
1530
|
+
);
|
|
1531
|
+
return true;
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1395
1534
|
/**
|
|
1396
1535
|
* Fetch a batch of messages using the managed connection.
|
|
1397
1536
|
* Assumes mailbox is already open from fetchUidsToSync.
|
|
@@ -1555,123 +1694,129 @@ export class MessageSyncService {
|
|
|
1555
1694
|
let owned = false;
|
|
1556
1695
|
let unresolvedSighting = false;
|
|
1557
1696
|
|
|
1558
|
-
//
|
|
1559
|
-
//
|
|
1560
|
-
//
|
|
1561
|
-
//
|
|
1562
|
-
//
|
|
1563
|
-
//
|
|
1564
|
-
//
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1697
|
+
// Every write below is an idempotent upsert keyed on a derived id, so a
|
|
1698
|
+
// save that fails part-way is repaired by the next round repeating it: the
|
|
1699
|
+
// UID's watermark only moves once the save returns. The order is the
|
|
1700
|
+
// guarantee a reader gets in between: the Envelope lands before the
|
|
1701
|
+
// Message (#1072), and the ThreadMessage is written last so the list path
|
|
1702
|
+
// never anchors on a ThreadMessage whose Message does not yet exist
|
|
1703
|
+
// (#1209).
|
|
1704
|
+
await this.envelopeService.upsertEnvelope({
|
|
1705
|
+
envelopeId,
|
|
1706
|
+
messageId,
|
|
1707
|
+
dateValue: sentDate,
|
|
1708
|
+
dateRaw: envelope.date,
|
|
1709
|
+
subject: envelope.subject,
|
|
1710
|
+
messageIdValue: envelope.messageId,
|
|
1711
|
+
});
|
|
1712
|
+
|
|
1713
|
+
const messageReferences = messageReferencesOf(envelope, msg.references);
|
|
1714
|
+
if (messageReferences.length > 0) {
|
|
1715
|
+
await this.envelopeService.replaceMessageReferences(
|
|
1570
1716
|
messageId,
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
messageIdValue: envelope.messageId,
|
|
1575
|
-
});
|
|
1717
|
+
messageReferences,
|
|
1718
|
+
);
|
|
1719
|
+
}
|
|
1576
1720
|
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1721
|
+
for (const { addresses, role } of addressOps) {
|
|
1722
|
+
await this.saveAddresses(
|
|
1723
|
+
this.addressService,
|
|
1724
|
+
messageId,
|
|
1725
|
+
accountConfigId,
|
|
1726
|
+
addresses,
|
|
1727
|
+
role,
|
|
1728
|
+
sighting,
|
|
1729
|
+
);
|
|
1730
|
+
}
|
|
1587
1731
|
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1732
|
+
// IMAP returns BODYSTRUCTURE in the same FETCH that returns the
|
|
1733
|
+
// envelope, so persisting BodyParts here is "free" — no extra round-trip.
|
|
1734
|
+
if (bodyParts.length > 0) {
|
|
1735
|
+
await this.envelopeService.upsertBodyParts(messageId, bodyParts);
|
|
1736
|
+
}
|
|
1593
1737
|
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1738
|
+
const { item, created } = await this.messageService.upsertWithStatus({
|
|
1739
|
+
messageId,
|
|
1740
|
+
mailboxId,
|
|
1741
|
+
uid: msg.uid,
|
|
1742
|
+
sequenceNumber: msg.seq,
|
|
1743
|
+
rfc822Size: msg.size ?? 0,
|
|
1744
|
+
internalDate: msg.internalDate.getTime(),
|
|
1745
|
+
envelopeId,
|
|
1746
|
+
rootBodyPartId,
|
|
1747
|
+
});
|
|
1604
1748
|
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
}
|
|
1749
|
+
// The server has just named this folder and this UID for a message the
|
|
1750
|
+
// database holds under another folder. `upsertWithStatus` left the row
|
|
1751
|
+
// alone, so the pointer is repaired here — mailbox and UID together,
|
|
1752
|
+
// because a UID only means anything inside the folder that issued it.
|
|
1753
|
+
//
|
|
1754
|
+
// Only when the message has really left the folder the row points at
|
|
1755
|
+
// (#1146). A sighting alone does not say that: a Gmail label, a Sieve
|
|
1756
|
+
// `fileinto` beside a `keep` and an echoed Sent copy all put the same
|
|
1757
|
+
// message in a second real folder while the first still holds it, and
|
|
1758
|
+
// following those took the mail out of the Inbox. `confirmDepartures`
|
|
1759
|
+
// asked the source before this batch was saved, and its verdict names
|
|
1760
|
+
// the placement it was reached for — so a row that has moved since
|
|
1761
|
+
// matches nothing, and `unresolvedSighting` holds this UID back for
|
|
1762
|
+
// the next round rather than repairing the row against a stale answer.
|
|
1763
|
+
const contested = !created && sightingContestsPlacement(mailbox, item);
|
|
1764
|
+
const verdict = placementKey(messageId, item.mailboxId, item.uid);
|
|
1765
|
+
unresolvedSighting = contested && !departures.settled.has(verdict);
|
|
1766
|
+
const repointable =
|
|
1767
|
+
contested &&
|
|
1768
|
+
departures.departed.has(verdict) &&
|
|
1769
|
+
!(await this.holdsCopyOf(this.messageService, messageId, mailboxId));
|
|
1770
|
+
// A user move landing between the read above and this write wins: the
|
|
1771
|
+
// re-point names the placement it read, writes nothing when the row has
|
|
1772
|
+
// moved since, and the UID waits for the next round.
|
|
1773
|
+
const repointed =
|
|
1774
|
+
repointable &&
|
|
1775
|
+
(await this.messageService.repointIfPlacement(
|
|
1776
|
+
messageId,
|
|
1777
|
+
{ status: item.status, mailboxId: item.mailboxId, uid: item.uid },
|
|
1778
|
+
{ mailboxId, uid: msg.uid, isDeleted: sighting === "discarded" },
|
|
1779
|
+
));
|
|
1637
1780
|
|
|
1638
|
-
|
|
1781
|
+
if (repointable && !repointed) unresolvedSighting = true;
|
|
1639
1782
|
|
|
1640
|
-
|
|
1641
|
-
// pointer could have changed the answer: a save into Junk, and a
|
|
1642
|
-
// re-point in either direction. The reconcile both applies and lifts,
|
|
1643
|
-
// so a sender whose only mail was moved out of Junk is restored by the
|
|
1644
|
-
// same call that would have withheld them (#859).
|
|
1645
|
-
if (sighting === "junk" || repointed) {
|
|
1646
|
-
await repos.address.reconcileJunkOnlyForMessage(
|
|
1647
|
-
messageId,
|
|
1648
|
-
roles.configJunkRoles,
|
|
1649
|
-
);
|
|
1650
|
-
}
|
|
1783
|
+
owned = created || item.mailboxId === mailboxId || repointed;
|
|
1651
1784
|
|
|
1652
|
-
|
|
1653
|
-
|
|
1785
|
+
// Withholding reads the pointer, so it is re-derived whenever the
|
|
1786
|
+
// pointer could have changed the answer: a save into Junk, and a
|
|
1787
|
+
// re-point in either direction. The reconcile both applies and lifts,
|
|
1788
|
+
// so a sender whose only mail was moved out of Junk is restored by the
|
|
1789
|
+
// same call that would have withheld them (#859).
|
|
1790
|
+
if (sighting === "junk" || repointed) {
|
|
1791
|
+
await this.addressService.reconcileJunkOnlyForMessage(
|
|
1654
1792
|
messageId,
|
|
1655
|
-
|
|
1656
|
-
accountConfigId,
|
|
1657
|
-
msg.uid,
|
|
1658
|
-
msg.internalDate.getTime(),
|
|
1659
|
-
sentDate,
|
|
1660
|
-
envelope,
|
|
1661
|
-
msg.flags,
|
|
1662
|
-
// The category of the Message this same save just wrote. A new
|
|
1663
|
-
// message is `uncategorized` until body-sync classifies it; a row
|
|
1664
|
-
// created for a Message that is ALREADY classified takes the decided
|
|
1665
|
-
// value rather than sitting stale forever behind a body-sync pass that
|
|
1666
|
-
// will skip it. That happens on thread-root drift — the same message
|
|
1667
|
-
// re-saved under different `References` mints a second threadId and so
|
|
1668
|
-
// a second row — not on a second mailbox, which resolves to the row
|
|
1669
|
-
// that already exists (issue #320).
|
|
1670
|
-
item.category,
|
|
1671
|
-
msg.references,
|
|
1672
|
-
hasAttachment,
|
|
1793
|
+
roles.configJunkRoles,
|
|
1673
1794
|
);
|
|
1674
|
-
}
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
await this.createThreadForMessage(
|
|
1798
|
+
this.threadMessageService,
|
|
1799
|
+
messageId,
|
|
1800
|
+
mailboxId,
|
|
1801
|
+
accountId,
|
|
1802
|
+
accountConfigId,
|
|
1803
|
+
msg.uid,
|
|
1804
|
+
msg.internalDate.getTime(),
|
|
1805
|
+
sentDate,
|
|
1806
|
+
envelope,
|
|
1807
|
+
msg.flags,
|
|
1808
|
+
// The category of the Message this same save just wrote. A new
|
|
1809
|
+
// message is `uncategorized` until body-sync classifies it; a row
|
|
1810
|
+
// created for a Message that is ALREADY classified takes the decided
|
|
1811
|
+
// value rather than sitting stale forever behind a body-sync pass that
|
|
1812
|
+
// will skip it. That happens on thread-root drift — the same message
|
|
1813
|
+
// re-saved under different `References` mints a second threadId and so
|
|
1814
|
+
// a second row — not on a second mailbox, which resolves to the row
|
|
1815
|
+
// that already exists (issue #320).
|
|
1816
|
+
item.category,
|
|
1817
|
+
msg.references,
|
|
1818
|
+
hasAttachment,
|
|
1819
|
+
);
|
|
1675
1820
|
|
|
1676
1821
|
return { messageId, uid: msg.uid, owned, unresolvedSighting };
|
|
1677
1822
|
}
|
|
@@ -1693,52 +1838,6 @@ export class MessageSyncService {
|
|
|
1693
1838
|
return rows.length > 0;
|
|
1694
1839
|
}
|
|
1695
1840
|
|
|
1696
|
-
/**
|
|
1697
|
-
* Follow a re-pointed Message with the ThreadMessage row that renders it.
|
|
1698
|
-
* The listing and every folder count read that row, so leaving it behind
|
|
1699
|
-
* would show the message in the folder it just left and hide it from the one
|
|
1700
|
-
* it landed in. `isDeleted` follows the same rule the move path uses — true
|
|
1701
|
-
* in Trash, false anywhere else — so a message another client rescues out of
|
|
1702
|
-
* Trash stops being filtered out of listings.
|
|
1703
|
-
*/
|
|
1704
|
-
private async repointThreadMessage(
|
|
1705
|
-
threadMessageService: IThreadMessageRepository,
|
|
1706
|
-
accountConfigId: string,
|
|
1707
|
-
messageId: string,
|
|
1708
|
-
mailboxId: string,
|
|
1709
|
-
uid: number,
|
|
1710
|
-
isDeleted: boolean,
|
|
1711
|
-
): Promise<void> {
|
|
1712
|
-
const row = await threadMessageService.findByMessageId(
|
|
1713
|
-
accountConfigId,
|
|
1714
|
-
messageId,
|
|
1715
|
-
);
|
|
1716
|
-
if (row === null) return;
|
|
1717
|
-
if (
|
|
1718
|
-
row.mailboxId === mailboxId &&
|
|
1719
|
-
row.uid === uid &&
|
|
1720
|
-
row.isDeleted === isDeleted
|
|
1721
|
-
) {
|
|
1722
|
-
return;
|
|
1723
|
-
}
|
|
1724
|
-
|
|
1725
|
-
await threadMessageService.update(
|
|
1726
|
-
row.accountConfigId,
|
|
1727
|
-
row.threadMessageId,
|
|
1728
|
-
{ mailboxId, uid, isDeleted },
|
|
1729
|
-
{
|
|
1730
|
-
composites: {
|
|
1731
|
-
mailboxId: row.mailboxId,
|
|
1732
|
-
sentDate: row.sentDate,
|
|
1733
|
-
isRead: row.isRead,
|
|
1734
|
-
isDeleted: row.isDeleted,
|
|
1735
|
-
hasStars: row.hasStars,
|
|
1736
|
-
hasAttachment: row.hasAttachment,
|
|
1737
|
-
},
|
|
1738
|
-
},
|
|
1739
|
-
);
|
|
1740
|
-
}
|
|
1741
|
-
|
|
1742
1841
|
private async saveAddresses(
|
|
1743
1842
|
addressService: IAddressRepository,
|
|
1744
1843
|
messageId: string,
|
|
@@ -1810,15 +1909,91 @@ export class MessageSyncService {
|
|
|
1810
1909
|
}
|
|
1811
1910
|
|
|
1812
1911
|
/**
|
|
1813
|
-
*
|
|
1814
|
-
*
|
|
1815
|
-
*
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1912
|
+
* The thread a synced message files under. A message keeps the thread it is
|
|
1913
|
+
* already stored in; a reply joins the thread its parent is stored in;
|
|
1914
|
+
* otherwise the thread is derived from the root `threadRootOf` names.
|
|
1915
|
+
*/
|
|
1916
|
+
private async threadFor(
|
|
1917
|
+
threadMessageService: IThreadMessageRepository,
|
|
1918
|
+
messageId: string,
|
|
1919
|
+
accountId: string,
|
|
1920
|
+
accountConfigId: string,
|
|
1921
|
+
envelope: ImapEnvelope,
|
|
1922
|
+
references: string[] | undefined,
|
|
1923
|
+
): Promise<string> {
|
|
1924
|
+
const own = await threadMessageService.findByMessageId(
|
|
1925
|
+
accountConfigId,
|
|
1926
|
+
messageId,
|
|
1927
|
+
);
|
|
1928
|
+
if (own) return own.threadId;
|
|
1929
|
+
|
|
1930
|
+
const parent = parentOf(envelope, references);
|
|
1931
|
+
const parentRow = parent
|
|
1932
|
+
? await threadMessageService.findByMessageId(
|
|
1933
|
+
accountConfigId,
|
|
1934
|
+
deriveMessageId(accountId, parent),
|
|
1935
|
+
)
|
|
1936
|
+
: null;
|
|
1937
|
+
return (
|
|
1938
|
+
parentRow?.threadId ??
|
|
1939
|
+
deriveThreadId(
|
|
1940
|
+
accountConfigId,
|
|
1941
|
+
threadRootOf(envelope, references, messageId),
|
|
1942
|
+
)
|
|
1943
|
+
);
|
|
1944
|
+
}
|
|
1945
|
+
|
|
1946
|
+
/**
|
|
1947
|
+
* Joins the rows written on either side of a parent and its reply, after
|
|
1948
|
+
* this message's own row exists. Replies that arrived before this message,
|
|
1949
|
+
* and so were keyed on its own id, follow it into its thread; and a reply
|
|
1950
|
+
* whose parent was saved while it was being saved follows the parent, but
|
|
1951
|
+
* only from the thread it derived for itself — a thread it joined or was
|
|
1952
|
+
* already stored in is left as it is.
|
|
1820
1953
|
*
|
|
1821
|
-
*
|
|
1954
|
+
* Both checks run after the create, so of a parent and a reply saved
|
|
1955
|
+
* concurrently at least one sees the other's row: neither is left in a
|
|
1956
|
+
* thread of its own.
|
|
1957
|
+
*/
|
|
1958
|
+
private async joinThreads(
|
|
1959
|
+
threadMessageService: IThreadMessageRepository,
|
|
1960
|
+
messageId: string,
|
|
1961
|
+
accountId: string,
|
|
1962
|
+
accountConfigId: string,
|
|
1963
|
+
envelope: ImapEnvelope,
|
|
1964
|
+
references: string[] | undefined,
|
|
1965
|
+
threadId: string,
|
|
1966
|
+
): Promise<void> {
|
|
1967
|
+
if (isValidMessageId(envelope.messageId)) {
|
|
1968
|
+
await threadMessageService.rekeyThread(
|
|
1969
|
+
accountConfigId,
|
|
1970
|
+
deriveThreadId(accountConfigId, wireMessageId(envelope.messageId)),
|
|
1971
|
+
threadId,
|
|
1972
|
+
);
|
|
1973
|
+
}
|
|
1974
|
+
|
|
1975
|
+
const parent = parentOf(envelope, references);
|
|
1976
|
+
if (!parent) return;
|
|
1977
|
+
const derived = deriveThreadId(
|
|
1978
|
+
accountConfigId,
|
|
1979
|
+
threadRootOf(envelope, references, messageId),
|
|
1980
|
+
);
|
|
1981
|
+
if (threadId !== derived) return;
|
|
1982
|
+
const parentRow = await threadMessageService.findByMessageId(
|
|
1983
|
+
accountConfigId,
|
|
1984
|
+
deriveMessageId(accountId, parent),
|
|
1985
|
+
);
|
|
1986
|
+
if (!parentRow || parentRow.threadId === threadId) return;
|
|
1987
|
+
await threadMessageService.rekeyThread(
|
|
1988
|
+
accountConfigId,
|
|
1989
|
+
threadId,
|
|
1990
|
+
parentRow.threadId,
|
|
1991
|
+
);
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1994
|
+
/**
|
|
1995
|
+
* Create the ThreadMessage for a synced message, then join it to its
|
|
1996
|
+
* parent and replies.
|
|
1822
1997
|
*
|
|
1823
1998
|
* `category` is the value the caller already holds from the Message it just
|
|
1824
1999
|
* wrote, so the row starts out agreeing with it rather than defaulting to
|
|
@@ -1828,6 +2003,7 @@ export class MessageSyncService {
|
|
|
1828
2003
|
threadMessageService: IThreadMessageRepository,
|
|
1829
2004
|
messageId: string,
|
|
1830
2005
|
mailboxId: string,
|
|
2006
|
+
accountId: string,
|
|
1831
2007
|
accountConfigId: string,
|
|
1832
2008
|
uid: number,
|
|
1833
2009
|
internalDate: number,
|
|
@@ -1838,30 +2014,14 @@ export class MessageSyncService {
|
|
|
1838
2014
|
references?: string[],
|
|
1839
2015
|
hasAttachment = false,
|
|
1840
2016
|
): Promise<void> {
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
// (This is a reply to a single message, which becomes the root)
|
|
1850
|
-
rootMessageIdHeader = envelope.inReplyTo;
|
|
1851
|
-
} else if (isValidMessageId(envelope.messageId)) {
|
|
1852
|
-
// No References, no In-Reply-To - this message is a thread root
|
|
1853
|
-
rootMessageIdHeader = envelope.messageId;
|
|
1854
|
-
} else {
|
|
1855
|
-
// No usable header (missing, empty, or a "<>" delivery-failure
|
|
1856
|
-
// placeholder). Fall back to the always-present internal messageId so
|
|
1857
|
-
// this message becomes a standalone thread-of-one. Distinct headerless
|
|
1858
|
-
// messages keep distinct ids, so they never collide into one bogus
|
|
1859
|
-
// thread, and every persisted Message gets exactly one ThreadMessage.
|
|
1860
|
-
rootMessageIdHeader = messageId;
|
|
1861
|
-
}
|
|
1862
|
-
|
|
1863
|
-
// Derive threadId from the root Message-ID (deterministic)
|
|
1864
|
-
const threadId = deriveThreadId(accountConfigId, rootMessageIdHeader);
|
|
2017
|
+
const threadId = await this.threadFor(
|
|
2018
|
+
threadMessageService,
|
|
2019
|
+
messageId,
|
|
2020
|
+
accountId,
|
|
2021
|
+
accountConfigId,
|
|
2022
|
+
envelope,
|
|
2023
|
+
references,
|
|
2024
|
+
);
|
|
1865
2025
|
|
|
1866
2026
|
const isRead = flags.includes(MessageSystemFlag.Seen);
|
|
1867
2027
|
|
|
@@ -1920,6 +2080,16 @@ export class MessageSyncService {
|
|
|
1920
2080
|
}
|
|
1921
2081
|
throw error;
|
|
1922
2082
|
});
|
|
2083
|
+
|
|
2084
|
+
await this.joinThreads(
|
|
2085
|
+
threadMessageService,
|
|
2086
|
+
messageId,
|
|
2087
|
+
accountId,
|
|
2088
|
+
accountConfigId,
|
|
2089
|
+
envelope,
|
|
2090
|
+
references,
|
|
2091
|
+
threadId,
|
|
2092
|
+
);
|
|
1923
2093
|
}
|
|
1924
2094
|
}
|
|
1925
2095
|
|