@remit/mailbox-service 0.0.112 → 0.0.114
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} +16 -72
- 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} +38 -64
- 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 +129 -0
- package/dist/mailbox-queue.js +216 -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 +280 -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} +13 -68
- 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 -97
- 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 -88
- package/src/account-credentials.ts +0 -210
- 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 -410
- package/src/body-sync-queue.test.ts +0 -64
- package/src/body-sync-queue.ts +0 -131
- package/src/body-sync-terminal.test.ts +0 -148
- package/src/body-sync-terminal.ts +0 -174
- package/src/body-sync.test.ts +0 -925
- package/src/body-sync.ts +0 -1891
- package/src/calendar-parts.ts +0 -41
- package/src/classification-backfill.test.ts +0 -285
- package/src/classification-backfill.ts +0 -363
- package/src/classify-message.test.ts +0 -142
- 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 -262
- package/src/external-move.ts +0 -285
- package/src/filter-operations.test.ts +0 -319
- package/src/filter-operations.ts +0 -377
- package/src/filter-update.test.ts +0 -313
- package/src/filters/filter-config.test.ts +0 -87
- package/src/filters/filter-config.ts +0 -34
- package/src/filters/list-id.test.ts +0 -50
- package/src/filters/match.test.ts +0 -392
- package/src/filters/match.ts +0 -225
- package/src/filters/pipeline.test.ts +0 -199
- package/src/filters/pipeline.ts +0 -170
- package/src/flag-push.test.ts +0 -112
- package/src/flag-push.ts +0 -198
- package/src/flag-queue.test.ts +0 -197
- package/src/flag-queue.ts +0 -509
- package/src/folder-operations.test.ts +0 -273
- package/src/folder-operations.ts +0 -217
- package/src/folder-placement.ts +0 -56
- package/src/forwarded-message.test.ts +0 -122
- 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 -97
- package/src/imapflow-connection.test.ts +0 -503
- package/src/imapflow-connection.ts +0 -1476
- package/src/index.ts +0 -397
- package/src/list-id-backfill.test.ts +0 -207
- 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 -141
- package/src/mailbox-delete-intent.test.ts +0 -119
- package/src/mailbox-management.test.ts +0 -243
- package/src/mailbox-management.ts +0 -838
- package/src/mailbox-presence.test.ts +0 -74
- package/src/mailbox-queue.ts +0 -397
- package/src/mailbox-rename-intent.test.ts +0 -220
- package/src/mailbox-rename-subtree.test.ts +0 -287
- package/src/mailbox-sync.test.ts +0 -286
- package/src/mailbox-sync.ts +0 -724
- package/src/message-move.test.ts +0 -726
- package/src/message-move.ts +0 -1518
- package/src/message-parser.ts +0 -26
- package/src/message-presence.test.ts +0 -50
- package/src/message-read.ts +0 -87
- package/src/message-save-retry.test.ts +0 -90
- package/src/message-sync-flags.test.ts +0 -167
- package/src/message-sync.test.ts +0 -762
- package/src/message-sync.ts +0 -2133
- package/src/mime-walker.test.ts +0 -311
- package/src/mime-walker.ts +0 -306
- package/src/organize-job.ts +0 -93
- 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 -626
- package/src/outbox-attachment.ts +0 -536
- package/src/outbox-failed-recovery.test.ts +0 -299
- package/src/outbox-queue.test.ts +0 -315
- package/src/outbox-queue.ts +0 -525
- package/src/ownership.ts +0 -140
- package/src/placement-move-terminal.ts +0 -145
- package/src/placement-move.ts +0 -363
- package/src/placement-settled.test.ts +0 -147
- package/src/placement.test.ts +0 -404
- package/src/quarantine.test.ts +0 -133
- package/src/quarantine.ts +0 -229
- package/src/refusal.ts +0 -29
- package/src/snippet.test.ts +0 -213
- package/src/snippet.ts +0 -152
- package/src/spam-report.test.ts +0 -329
- package/src/spam-report.ts +0 -357
- package/src/sync-warning.test.ts +0 -213
- package/src/sync-warning.ts +0 -56
- 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/fake-imap.ts +0 -690
- package/src/test-helpers/folder-roles.ts +0 -66
- package/src/test-helpers/mime-tree-generator.ts +0 -461
- package/src/test-helpers/test-queue.ts +0 -56
- package/src/test-helpers/test-store.ts +0 -299
- 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/thread-root.test.ts +0 -29
- package/src/thread-root.ts +0 -51
- package/src/types/mailparser-augment.d.ts +0 -23
- 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/tsconfig.json +0 -8
package/src/body-sync.test.ts
DELETED
|
@@ -1,925 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { afterEach, beforeEach, describe, it } from "node:test";
|
|
3
|
-
import { DEFAULT_CALENDAR_URL_SEGMENT } from "@remit/calendar-service";
|
|
4
|
-
import type {
|
|
5
|
-
AccountItem,
|
|
6
|
-
AddressFlags,
|
|
7
|
-
MailboxItem,
|
|
8
|
-
UpdateMessageInput,
|
|
9
|
-
} from "@remit/data-ports";
|
|
10
|
-
import {
|
|
11
|
-
MessageGoneFromServerError,
|
|
12
|
-
MessagePlacementUnsettledError,
|
|
13
|
-
} from "@remit/data-ports/errors";
|
|
14
|
-
import { deriveAddressId } from "@remit/data-ports/id";
|
|
15
|
-
import {
|
|
16
|
-
CalendarInviteMethod,
|
|
17
|
-
CalendarSource,
|
|
18
|
-
CalendarSuggestionState,
|
|
19
|
-
FilterClauseField,
|
|
20
|
-
FilterScope,
|
|
21
|
-
MessageCategory,
|
|
22
|
-
MessageClassificationState,
|
|
23
|
-
} from "@remit/domain-enums";
|
|
24
|
-
import type { StorageService } from "@remit/storage-service";
|
|
25
|
-
import { type BodySyncFolder, BodySyncService } from "./body-sync.js";
|
|
26
|
-
import { NO_ACTION } from "./filters/match.js";
|
|
27
|
-
import { FlagPushService } from "./flag-push.js";
|
|
28
|
-
import { FlagQueueService } from "./flag-queue.js";
|
|
29
|
-
import { PlacementMoveService } from "./placement-move.js";
|
|
30
|
-
import { QuarantineService } from "./quarantine.js";
|
|
31
|
-
import { SyncWarningService } from "./sync-warning.js";
|
|
32
|
-
import { eml, FakeImapServer } from "./test-helpers/fake-imap.js";
|
|
33
|
-
import {
|
|
34
|
-
captureQueue,
|
|
35
|
-
messageAtUid,
|
|
36
|
-
openStore,
|
|
37
|
-
type QueueCapture,
|
|
38
|
-
type Store,
|
|
39
|
-
seedAccount,
|
|
40
|
-
seedMailbox,
|
|
41
|
-
silentLog,
|
|
42
|
-
syncFolder,
|
|
43
|
-
} from "./test-helpers/test-store.js";
|
|
44
|
-
|
|
45
|
-
const LINKEDIN = eml(
|
|
46
|
-
{
|
|
47
|
-
From: "LinkedIn <messages-noreply@linkedin.com>",
|
|
48
|
-
Subject: "You have a new invitation",
|
|
49
|
-
"List-Id": "LinkedIn Invitations <invitations.linkedin.com>",
|
|
50
|
-
"List-Unsubscribe": "<https://www.linkedin.com/e/unsub>",
|
|
51
|
-
},
|
|
52
|
-
"invitation body text",
|
|
53
|
-
);
|
|
54
|
-
|
|
55
|
-
const PERSONAL = eml(
|
|
56
|
-
{ From: "Alex <alex@example.com>", Subject: "Dinner Friday?" },
|
|
57
|
-
"Are you free Friday night?",
|
|
58
|
-
);
|
|
59
|
-
|
|
60
|
-
const DEMOTE = eml({
|
|
61
|
-
From: "Support <support@evil-mimic.example>",
|
|
62
|
-
Subject: "Verify your account",
|
|
63
|
-
"Authentication-Results": "mx.example.com; dmarc=fail",
|
|
64
|
-
"DKIM-Signature": "v=1; a=rsa-sha256; d=relay.example.net; s=sel; b=xxx",
|
|
65
|
-
"X-Spam-Status": "No, score=0.1",
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
const SPAM_FLAGGED_DMARC_PASS = eml({
|
|
69
|
-
From: "Sender <someone@example.com>",
|
|
70
|
-
"Authentication-Results": "mx.example.com; dmarc=pass",
|
|
71
|
-
"X-Spam-Status": "Yes, score=6.0",
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
const FROM_SOMEONE = eml({ From: "Sender <someone@example.com>" });
|
|
75
|
-
|
|
76
|
-
const TRAVEL_INVITATION = eml({
|
|
77
|
-
From: "Someone <someone@example.com>",
|
|
78
|
-
Subject: "You have a new invitation to Travel",
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
const ICAL_UID = "invite-4711@example.test";
|
|
82
|
-
|
|
83
|
-
const invitation = (
|
|
84
|
-
sequence = 0,
|
|
85
|
-
method = "REQUEST",
|
|
86
|
-
start = "DTSTART:20260901T080000Z",
|
|
87
|
-
): Buffer =>
|
|
88
|
-
Buffer.from(
|
|
89
|
-
[
|
|
90
|
-
"From: Organizer <organizer@example.test>",
|
|
91
|
-
"To: user@example.test",
|
|
92
|
-
"Subject: Quarterly review",
|
|
93
|
-
'Content-Type: multipart/alternative; boundary="bnd"',
|
|
94
|
-
"",
|
|
95
|
-
"--bnd",
|
|
96
|
-
"Content-Type: text/plain",
|
|
97
|
-
"",
|
|
98
|
-
"You are invited.",
|
|
99
|
-
"--bnd",
|
|
100
|
-
`Content-Type: text/calendar; method=${method}; charset="UTF-8"`,
|
|
101
|
-
"",
|
|
102
|
-
"BEGIN:VCALENDAR",
|
|
103
|
-
"VERSION:2.0",
|
|
104
|
-
"PRODID:-//Example Corp//Scheduler//EN",
|
|
105
|
-
`METHOD:${method}`,
|
|
106
|
-
"BEGIN:VEVENT",
|
|
107
|
-
`UID:${ICAL_UID}`,
|
|
108
|
-
"DTSTAMP:20260801T090000Z",
|
|
109
|
-
`SEQUENCE:${sequence}`,
|
|
110
|
-
start,
|
|
111
|
-
"SUMMARY:Quarterly review",
|
|
112
|
-
"ORGANIZER:mailto:organizer@example.test",
|
|
113
|
-
"END:VEVENT",
|
|
114
|
-
"END:VCALENDAR",
|
|
115
|
-
"--bnd--",
|
|
116
|
-
"",
|
|
117
|
-
].join("\r\n"),
|
|
118
|
-
);
|
|
119
|
-
|
|
120
|
-
const MISSING_BODY = "file:///nonexistent/remit/body.eml";
|
|
121
|
-
|
|
122
|
-
interface World {
|
|
123
|
-
store: Store;
|
|
124
|
-
imap: FakeImapServer;
|
|
125
|
-
queue: QueueCapture;
|
|
126
|
-
account: AccountItem;
|
|
127
|
-
inbox: MailboxItem;
|
|
128
|
-
junk: MailboxItem;
|
|
129
|
-
archive: MailboxItem;
|
|
130
|
-
travel: MailboxItem;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
let world: World;
|
|
134
|
-
|
|
135
|
-
beforeEach(async () => {
|
|
136
|
-
const store = await openStore();
|
|
137
|
-
const account = await seedAccount(store);
|
|
138
|
-
const imap = new FakeImapServer();
|
|
139
|
-
for (const path of ["INBOX", "Junk", "Archive", "Travel"])
|
|
140
|
-
imap.addFolder(path);
|
|
141
|
-
world = {
|
|
142
|
-
store,
|
|
143
|
-
imap,
|
|
144
|
-
queue: captureQueue(),
|
|
145
|
-
account,
|
|
146
|
-
inbox: await seedMailbox(store, account, "INBOX"),
|
|
147
|
-
junk: await seedMailbox(store, account, "Junk", { specialUse: "Junk" }),
|
|
148
|
-
archive: await seedMailbox(store, account, "Archive", {
|
|
149
|
-
specialUse: "Archive",
|
|
150
|
-
}),
|
|
151
|
-
travel: await seedMailbox(store, account, "Travel"),
|
|
152
|
-
};
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
afterEach(async () => {
|
|
156
|
-
world.queue.restore();
|
|
157
|
-
await world.store.close();
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
interface Wiring {
|
|
161
|
-
storage?: StorageService;
|
|
162
|
-
placement?: boolean;
|
|
163
|
-
filters?: boolean;
|
|
164
|
-
quarantine?: boolean;
|
|
165
|
-
unsubscribe?: boolean;
|
|
166
|
-
calendar?: boolean;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
const bodySync = (wiring: Wiring = {}): BodySyncService => {
|
|
170
|
-
const { store } = world;
|
|
171
|
-
const placementMoveService = new PlacementMoveService({
|
|
172
|
-
messageService: store.message,
|
|
173
|
-
threadMessageService: store.threadMessage,
|
|
174
|
-
markerService: store.placementMove,
|
|
175
|
-
addressService: store.address,
|
|
176
|
-
mailboxSpecialUseService: store.mailboxSpecialUse,
|
|
177
|
-
mailboxService: store.mailbox,
|
|
178
|
-
sqsQueueUrl: "https://sqs.test/message-mgmt",
|
|
179
|
-
});
|
|
180
|
-
const flagQueueService = new FlagQueueService({
|
|
181
|
-
messageFlagService: store.messageFlag,
|
|
182
|
-
messageService: store.message,
|
|
183
|
-
threadMessageService: store.threadMessage,
|
|
184
|
-
flagPushService: new FlagPushService({
|
|
185
|
-
markerService: store.flagPush,
|
|
186
|
-
sqsQueueUrl: "https://sqs.test/flags",
|
|
187
|
-
}),
|
|
188
|
-
});
|
|
189
|
-
return new BodySyncService(
|
|
190
|
-
store.message,
|
|
191
|
-
wiring.storage ?? store.storage(),
|
|
192
|
-
store.threadMessage,
|
|
193
|
-
store.address,
|
|
194
|
-
store.envelope,
|
|
195
|
-
new SyncWarningService(store.syncWarning),
|
|
196
|
-
silentLog,
|
|
197
|
-
wiring.placement
|
|
198
|
-
? {
|
|
199
|
-
mailboxSpecialUseService: store.mailboxSpecialUse,
|
|
200
|
-
placementMoveService,
|
|
201
|
-
}
|
|
202
|
-
: undefined,
|
|
203
|
-
wiring.filters
|
|
204
|
-
? {
|
|
205
|
-
filterService: store.filter,
|
|
206
|
-
messageLabelService: store.messageLabel,
|
|
207
|
-
mailboxService: store.mailbox,
|
|
208
|
-
placementMoveService,
|
|
209
|
-
}
|
|
210
|
-
: undefined,
|
|
211
|
-
wiring.quarantine
|
|
212
|
-
? {
|
|
213
|
-
quarantineService: new QuarantineService(
|
|
214
|
-
store.quarantine,
|
|
215
|
-
store.mailboxSpecialUse,
|
|
216
|
-
"sha-test",
|
|
217
|
-
silentLog,
|
|
218
|
-
),
|
|
219
|
-
mailboxId: world.inbox.mailboxId,
|
|
220
|
-
uidValidity: world.inbox.uidValidity,
|
|
221
|
-
attempts: 1,
|
|
222
|
-
}
|
|
223
|
-
: undefined,
|
|
224
|
-
wiring.unsubscribe ? { flagQueueService } : undefined,
|
|
225
|
-
wiring.calendar
|
|
226
|
-
? {
|
|
227
|
-
calendarSuggestionService: store.calendarSuggestion,
|
|
228
|
-
calendarStore: store.calendarStore,
|
|
229
|
-
filterService: store.filter,
|
|
230
|
-
}
|
|
231
|
-
: undefined,
|
|
232
|
-
);
|
|
233
|
-
};
|
|
234
|
-
|
|
235
|
-
const placementOf = (mailbox: MailboxItem): BodySyncFolder => ({
|
|
236
|
-
mailboxId: mailbox.mailboxId,
|
|
237
|
-
fullPath: mailbox.fullPath,
|
|
238
|
-
placement:
|
|
239
|
-
mailbox === world.inbox
|
|
240
|
-
? "inbox"
|
|
241
|
-
: mailbox === world.junk
|
|
242
|
-
? "junk"
|
|
243
|
-
: "other",
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
const arrive = async (
|
|
247
|
-
raw: Buffer,
|
|
248
|
-
mailbox: MailboxItem = world.inbox,
|
|
249
|
-
update: UpdateMessageInput = {},
|
|
250
|
-
): Promise<string> => {
|
|
251
|
-
const uid = world.imap.deliver(mailbox.fullPath, raw);
|
|
252
|
-
await syncFolder(world.store, world.account, mailbox, world.imap);
|
|
253
|
-
const { messageId } = await messageAtUid(world.store, mailbox, uid);
|
|
254
|
-
await world.store.message.update(messageId, update);
|
|
255
|
-
return messageId;
|
|
256
|
-
};
|
|
257
|
-
|
|
258
|
-
const read = (
|
|
259
|
-
service: BodySyncService,
|
|
260
|
-
messageId: string,
|
|
261
|
-
mailbox: MailboxItem = world.inbox,
|
|
262
|
-
) =>
|
|
263
|
-
service.fetchAndGetBody(
|
|
264
|
-
messageId,
|
|
265
|
-
world.account.accountId,
|
|
266
|
-
world.account.accountConfigId,
|
|
267
|
-
placementOf(mailbox),
|
|
268
|
-
async () => world.imap.connection(),
|
|
269
|
-
);
|
|
270
|
-
|
|
271
|
-
const sync = (
|
|
272
|
-
service: BodySyncService,
|
|
273
|
-
messageIds: string[],
|
|
274
|
-
options: { force?: boolean; mailbox?: MailboxItem } = {},
|
|
275
|
-
) =>
|
|
276
|
-
service.syncBodies(
|
|
277
|
-
messageIds,
|
|
278
|
-
world.account.accountId,
|
|
279
|
-
world.account.accountConfigId,
|
|
280
|
-
{
|
|
281
|
-
...placementOf(options.mailbox ?? world.inbox),
|
|
282
|
-
uidValidity: (options.mailbox ?? world.inbox).uidValidity,
|
|
283
|
-
},
|
|
284
|
-
async () => world.imap.connection(),
|
|
285
|
-
options.force ?? false,
|
|
286
|
-
);
|
|
287
|
-
|
|
288
|
-
const flagSender = async (email: string, flags: AddressFlags) => {
|
|
289
|
-
const [localPart, domain] = email.split("@");
|
|
290
|
-
const addressId = deriveAddressId(world.account.accountConfigId, email);
|
|
291
|
-
await world.store.address.upsertAddress({
|
|
292
|
-
addressId,
|
|
293
|
-
accountConfigId: world.account.accountConfigId,
|
|
294
|
-
localPart,
|
|
295
|
-
domain,
|
|
296
|
-
normalizedEmail: email,
|
|
297
|
-
normalizedCompound: email,
|
|
298
|
-
});
|
|
299
|
-
await world.store.address.updateAddress(
|
|
300
|
-
world.account.accountConfigId,
|
|
301
|
-
addressId,
|
|
302
|
-
{ flags },
|
|
303
|
-
);
|
|
304
|
-
};
|
|
305
|
-
|
|
306
|
-
const messageRow = (messageId: string) => world.store.message.get(messageId);
|
|
307
|
-
|
|
308
|
-
const threadRows = (messageId: string) =>
|
|
309
|
-
world.store.threadMessage.findAllByMessageId(
|
|
310
|
-
world.account.accountConfigId,
|
|
311
|
-
messageId,
|
|
312
|
-
);
|
|
313
|
-
|
|
314
|
-
const addFilter = (
|
|
315
|
-
clauses: Array<{ field: string; value: string }>,
|
|
316
|
-
actionMailboxId: string = NO_ACTION,
|
|
317
|
-
actionLabelId: string = NO_ACTION,
|
|
318
|
-
) =>
|
|
319
|
-
world.store.filter.create({
|
|
320
|
-
accountConfigId: world.account.accountConfigId,
|
|
321
|
-
name: "rule",
|
|
322
|
-
scope: FilterScope.Standing,
|
|
323
|
-
literalClauses: clauses as never,
|
|
324
|
-
actionMailboxId,
|
|
325
|
-
actionLabelId,
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
describe("classification is decided on the pass that first stores a body", () => {
|
|
329
|
-
it("writes the category, the examined mark and every denormalized field", async () => {
|
|
330
|
-
const messageId = await arrive(LINKEDIN);
|
|
331
|
-
await world.store.threadMessage.create({
|
|
332
|
-
...(await threadRows(messageId))[0],
|
|
333
|
-
threadId: "drifted-root",
|
|
334
|
-
});
|
|
335
|
-
|
|
336
|
-
await sync(bodySync(), [messageId]);
|
|
337
|
-
|
|
338
|
-
const row = await messageRow(messageId);
|
|
339
|
-
assert.equal(row.category, MessageCategory.social);
|
|
340
|
-
assert.equal(row.classificationState, MessageClassificationState.Examined);
|
|
341
|
-
assert.ok(row.bodyStorageKey);
|
|
342
|
-
const rows = await threadRows(messageId);
|
|
343
|
-
assert.equal(rows.length, 2);
|
|
344
|
-
for (const thread of rows) {
|
|
345
|
-
assert.equal(thread.category, MessageCategory.social);
|
|
346
|
-
assert.equal(thread.listId, "invitations.linkedin.com");
|
|
347
|
-
assert.equal(thread.snippet, "invitation body text");
|
|
348
|
-
}
|
|
349
|
-
});
|
|
350
|
-
|
|
351
|
-
it("examines a body the classifier has nothing to say about once", async () => {
|
|
352
|
-
const messageId = await arrive(PERSONAL);
|
|
353
|
-
const service = bodySync();
|
|
354
|
-
|
|
355
|
-
await sync(service, [messageId]);
|
|
356
|
-
const second = await sync(service, [messageId]);
|
|
357
|
-
|
|
358
|
-
assert.equal(second.skippedCount, 1);
|
|
359
|
-
const row = await messageRow(messageId);
|
|
360
|
-
assert.equal(row.classificationState, MessageClassificationState.Examined);
|
|
361
|
-
});
|
|
362
|
-
|
|
363
|
-
it("leaves mail stored before the classifier ran for the backfill to find", async () => {
|
|
364
|
-
const messageId = await arrive(LINKEDIN, world.inbox, {
|
|
365
|
-
bodyStorageKey: MISSING_BODY,
|
|
366
|
-
classificationState: MessageClassificationState.NotExamined,
|
|
367
|
-
});
|
|
368
|
-
|
|
369
|
-
const result = await sync(bodySync(), [messageId]);
|
|
370
|
-
|
|
371
|
-
assert.equal(result.skippedCount, 1);
|
|
372
|
-
const row = await messageRow(messageId);
|
|
373
|
-
assert.equal(
|
|
374
|
-
row.classificationState,
|
|
375
|
-
MessageClassificationState.NotExamined,
|
|
376
|
-
);
|
|
377
|
-
assert.equal(row.category, MessageCategory.uncategorized);
|
|
378
|
-
});
|
|
379
|
-
|
|
380
|
-
it("classifies by the sender's category override instead of the headers", async () => {
|
|
381
|
-
await flagSender("messages-noreply@linkedin.com", {
|
|
382
|
-
category: { value: MessageCategory.personal, setAt: 1 },
|
|
383
|
-
});
|
|
384
|
-
const messageId = await arrive(LINKEDIN);
|
|
385
|
-
|
|
386
|
-
await read(bodySync(), messageId);
|
|
387
|
-
|
|
388
|
-
assert.equal(
|
|
389
|
-
(await messageRow(messageId)).category,
|
|
390
|
-
MessageCategory.personal,
|
|
391
|
-
);
|
|
392
|
-
assert.equal(
|
|
393
|
-
(await threadRows(messageId))[0].category,
|
|
394
|
-
MessageCategory.personal,
|
|
395
|
-
);
|
|
396
|
-
});
|
|
397
|
-
|
|
398
|
-
it("does not reach back to examined mail when the sender is reclassified later", async () => {
|
|
399
|
-
const messageId = await arrive(LINKEDIN, world.inbox, {
|
|
400
|
-
bodyStorageKey: MISSING_BODY,
|
|
401
|
-
classificationState: MessageClassificationState.Examined,
|
|
402
|
-
});
|
|
403
|
-
await flagSender("messages-noreply@linkedin.com", {
|
|
404
|
-
category: { value: MessageCategory.transactional, setAt: 1 },
|
|
405
|
-
});
|
|
406
|
-
|
|
407
|
-
await sync(bodySync(), [messageId]);
|
|
408
|
-
|
|
409
|
-
assert.equal(
|
|
410
|
-
(await messageRow(messageId)).category,
|
|
411
|
-
MessageCategory.uncategorized,
|
|
412
|
-
);
|
|
413
|
-
});
|
|
414
|
-
});
|
|
415
|
-
|
|
416
|
-
describe("a re-store re-fetches bytes and re-decides nothing", () => {
|
|
417
|
-
const decidedRow = (
|
|
418
|
-
overrides: UpdateMessageInput = {},
|
|
419
|
-
): UpdateMessageInput => ({
|
|
420
|
-
bodyStorageKey: MISSING_BODY,
|
|
421
|
-
category: MessageCategory.marketing,
|
|
422
|
-
classificationState: MessageClassificationState.Examined,
|
|
423
|
-
placementDecidedAt: 500,
|
|
424
|
-
...overrides,
|
|
425
|
-
});
|
|
426
|
-
|
|
427
|
-
for (const path of ["the missing-object read", "a forced sync"] as const) {
|
|
428
|
-
const restore = (
|
|
429
|
-
service: BodySyncService,
|
|
430
|
-
messageId: string,
|
|
431
|
-
mailbox: MailboxItem = world.inbox,
|
|
432
|
-
) =>
|
|
433
|
-
path === "a forced sync"
|
|
434
|
-
? sync(service, [messageId], { force: true, mailbox })
|
|
435
|
-
: read(service, messageId, mailbox);
|
|
436
|
-
|
|
437
|
-
it(`keeps category, placement and read state through ${path}`, async () => {
|
|
438
|
-
await flagSender("support@evil-mimic.example", {
|
|
439
|
-
category: { value: MessageCategory.personal, setAt: 1 },
|
|
440
|
-
unsubscribed: { value: true, setAt: 1 },
|
|
441
|
-
});
|
|
442
|
-
const messageId = await arrive(DEMOTE, world.inbox, decidedRow());
|
|
443
|
-
|
|
444
|
-
await restore(
|
|
445
|
-
bodySync({ placement: true, filters: true, unsubscribe: true }),
|
|
446
|
-
messageId,
|
|
447
|
-
);
|
|
448
|
-
|
|
449
|
-
const row = await messageRow(messageId);
|
|
450
|
-
assert.notEqual(row.bodyStorageKey, MISSING_BODY);
|
|
451
|
-
assert.equal(row.category, MessageCategory.marketing);
|
|
452
|
-
assert.equal(row.mailboxId, world.inbox.mailboxId);
|
|
453
|
-
assert.equal(row.movedByRemit, false);
|
|
454
|
-
assert.equal(row.placementDecidedAt, 500);
|
|
455
|
-
assert.equal((await threadRows(messageId))[0].isRead, false);
|
|
456
|
-
assert.deepEqual(world.queue.sent, []);
|
|
457
|
-
});
|
|
458
|
-
|
|
459
|
-
it(`does not re-fire a standing filter through ${path}`, async () => {
|
|
460
|
-
await addFilter(
|
|
461
|
-
[{ field: FilterClauseField.Subject, value: "invitation" }],
|
|
462
|
-
world.junk.mailboxId,
|
|
463
|
-
);
|
|
464
|
-
const messageId = await arrive(
|
|
465
|
-
TRAVEL_INVITATION,
|
|
466
|
-
world.travel,
|
|
467
|
-
decidedRow(),
|
|
468
|
-
);
|
|
469
|
-
|
|
470
|
-
await restore(bodySync({ filters: true }), messageId, world.travel);
|
|
471
|
-
|
|
472
|
-
const row = await messageRow(messageId);
|
|
473
|
-
assert.equal(row.mailboxId, world.travel.mailboxId);
|
|
474
|
-
assert.equal(row.filterMove, undefined);
|
|
475
|
-
});
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
it("first-stores the never-stored message of a forced batch with the full pass", async () => {
|
|
479
|
-
await addFilter(
|
|
480
|
-
[{ field: FilterClauseField.Subject, value: "invitation" }],
|
|
481
|
-
world.travel.mailboxId,
|
|
482
|
-
);
|
|
483
|
-
const stored = await arrive(DEMOTE, world.inbox, decidedRow());
|
|
484
|
-
const fresh = await arrive(TRAVEL_INVITATION);
|
|
485
|
-
|
|
486
|
-
const result = await sync(
|
|
487
|
-
bodySync({ placement: true, filters: true }),
|
|
488
|
-
[stored, fresh],
|
|
489
|
-
{ force: true },
|
|
490
|
-
);
|
|
491
|
-
|
|
492
|
-
assert.deepEqual(result.syncedMessageIds.sort(), [fresh, stored].sort());
|
|
493
|
-
assert.equal((await messageRow(stored)).mailboxId, world.inbox.mailboxId);
|
|
494
|
-
const filed = await messageRow(fresh);
|
|
495
|
-
assert.equal(filed.mailboxId, world.travel.mailboxId);
|
|
496
|
-
assert.equal(filed.movedByRemit, true);
|
|
497
|
-
});
|
|
498
|
-
});
|
|
499
|
-
|
|
500
|
-
describe("placement follows the sender's flags", () => {
|
|
501
|
-
it("files a blocked sender's inbox mail to Junk with no authentication signal", async () => {
|
|
502
|
-
await flagSender("someone@example.com", {
|
|
503
|
-
blocked: { value: true, setAt: 1_000 },
|
|
504
|
-
});
|
|
505
|
-
const messageId = await arrive(FROM_SOMEONE);
|
|
506
|
-
|
|
507
|
-
await read(bodySync({ placement: true }), messageId);
|
|
508
|
-
|
|
509
|
-
const row = await messageRow(messageId);
|
|
510
|
-
assert.equal(row.mailboxId, world.junk.mailboxId);
|
|
511
|
-
assert.equal(row.movedByRemit, true);
|
|
512
|
-
assert.ok(row.placementDecidedAt);
|
|
513
|
-
assert.equal(
|
|
514
|
-
(await world.store.placementMove.find(messageId))?.state,
|
|
515
|
-
"queued",
|
|
516
|
-
);
|
|
517
|
-
assert.deepEqual(
|
|
518
|
-
world.queue.sent.map((event) => event.body.type),
|
|
519
|
-
["PLACEMENT_MOVE_PUSH"],
|
|
520
|
-
);
|
|
521
|
-
});
|
|
522
|
-
|
|
523
|
-
it("leaves an unflagged sender's inbox mail where it is", async () => {
|
|
524
|
-
const messageId = await arrive(FROM_SOMEONE);
|
|
525
|
-
|
|
526
|
-
await read(bodySync({ placement: true }), messageId);
|
|
527
|
-
|
|
528
|
-
assert.equal(
|
|
529
|
-
(await messageRow(messageId)).mailboxId,
|
|
530
|
-
world.inbox.mailboxId,
|
|
531
|
-
);
|
|
532
|
-
assert.equal(await world.store.placementMove.find(messageId), null);
|
|
533
|
-
});
|
|
534
|
-
|
|
535
|
-
it("rescues from Junk when vip was set after blocked", async () => {
|
|
536
|
-
await flagSender("someone@example.com", {
|
|
537
|
-
blocked: { value: true, setAt: 1_000 },
|
|
538
|
-
vip: { value: true, setAt: 5_000 },
|
|
539
|
-
});
|
|
540
|
-
const messageId = await arrive(SPAM_FLAGGED_DMARC_PASS, world.junk);
|
|
541
|
-
|
|
542
|
-
await read(bodySync({ placement: true }), messageId, world.junk);
|
|
543
|
-
|
|
544
|
-
assert.equal(
|
|
545
|
-
(await messageRow(messageId)).mailboxId,
|
|
546
|
-
world.inbox.mailboxId,
|
|
547
|
-
);
|
|
548
|
-
});
|
|
549
|
-
|
|
550
|
-
it("files an autoArchive sender's inbox mail to Archive", async () => {
|
|
551
|
-
await flagSender("someone@example.com", {
|
|
552
|
-
autoArchive: { value: true, setAt: 1_000 },
|
|
553
|
-
});
|
|
554
|
-
const messageId = await arrive(FROM_SOMEONE);
|
|
555
|
-
|
|
556
|
-
await read(bodySync({ placement: true }), messageId);
|
|
557
|
-
|
|
558
|
-
assert.equal(
|
|
559
|
-
(await messageRow(messageId)).mailboxId,
|
|
560
|
-
world.archive.mailboxId,
|
|
561
|
-
);
|
|
562
|
-
});
|
|
563
|
-
|
|
564
|
-
it("lets a blocked demote win over autoArchive", async () => {
|
|
565
|
-
await flagSender("someone@example.com", {
|
|
566
|
-
blocked: { value: true, setAt: 1_000 },
|
|
567
|
-
autoArchive: { value: true, setAt: 1_000 },
|
|
568
|
-
});
|
|
569
|
-
const messageId = await arrive(FROM_SOMEONE);
|
|
570
|
-
|
|
571
|
-
await read(bodySync({ placement: true }), messageId);
|
|
572
|
-
|
|
573
|
-
assert.equal((await messageRow(messageId)).mailboxId, world.junk.mailboxId);
|
|
574
|
-
});
|
|
575
|
-
|
|
576
|
-
it("does not auto-archive mail out of Junk", async () => {
|
|
577
|
-
await flagSender("someone@example.com", {
|
|
578
|
-
autoArchive: { value: true, setAt: 1_000 },
|
|
579
|
-
});
|
|
580
|
-
const messageId = await arrive(SPAM_FLAGGED_DMARC_PASS, world.junk);
|
|
581
|
-
|
|
582
|
-
await read(bodySync({ placement: true }), messageId, world.junk);
|
|
583
|
-
|
|
584
|
-
assert.equal((await messageRow(messageId)).mailboxId, world.junk.mailboxId);
|
|
585
|
-
});
|
|
586
|
-
|
|
587
|
-
it("does not re-file mail Remit already placed", async () => {
|
|
588
|
-
await flagSender("someone@example.com", {
|
|
589
|
-
autoArchive: { value: true, setAt: 1_000 },
|
|
590
|
-
});
|
|
591
|
-
const messageId = await arrive(FROM_SOMEONE, world.inbox, {
|
|
592
|
-
movedByRemit: true,
|
|
593
|
-
});
|
|
594
|
-
|
|
595
|
-
await read(bodySync({ placement: true }), messageId);
|
|
596
|
-
|
|
597
|
-
assert.equal(
|
|
598
|
-
(await messageRow(messageId)).mailboxId,
|
|
599
|
-
world.inbox.mailboxId,
|
|
600
|
-
);
|
|
601
|
-
});
|
|
602
|
-
});
|
|
603
|
-
|
|
604
|
-
describe("a standing filter runs on the first store", () => {
|
|
605
|
-
it("moves the message and records where it came from", async () => {
|
|
606
|
-
const filter = await addFilter(
|
|
607
|
-
[{ field: FilterClauseField.Subject, value: "invitation" }],
|
|
608
|
-
world.travel.mailboxId,
|
|
609
|
-
);
|
|
610
|
-
const messageId = await arrive(TRAVEL_INVITATION);
|
|
611
|
-
|
|
612
|
-
await read(bodySync({ filters: true }), messageId);
|
|
613
|
-
|
|
614
|
-
const row = await messageRow(messageId);
|
|
615
|
-
assert.equal(row.mailboxId, world.travel.mailboxId);
|
|
616
|
-
assert.equal(row.movedByRemit, true);
|
|
617
|
-
assert.deepEqual(
|
|
618
|
-
{
|
|
619
|
-
filterId: row.filterMove?.filterId,
|
|
620
|
-
sourceMailboxId: row.filterMove?.sourceMailboxId,
|
|
621
|
-
destinationMailboxId: row.filterMove?.destinationMailboxId,
|
|
622
|
-
},
|
|
623
|
-
{
|
|
624
|
-
filterId: filter.filterId,
|
|
625
|
-
sourceMailboxId: world.inbox.mailboxId,
|
|
626
|
-
destinationMailboxId: world.travel.mailboxId,
|
|
627
|
-
},
|
|
628
|
-
);
|
|
629
|
-
});
|
|
630
|
-
|
|
631
|
-
it("records no move for a message already in the filter's destination", async () => {
|
|
632
|
-
await addFilter(
|
|
633
|
-
[{ field: FilterClauseField.Subject, value: "invitation" }],
|
|
634
|
-
world.travel.mailboxId,
|
|
635
|
-
);
|
|
636
|
-
const messageId = await arrive(TRAVEL_INVITATION, world.travel);
|
|
637
|
-
|
|
638
|
-
await read(bodySync({ filters: true }), messageId, world.travel);
|
|
639
|
-
|
|
640
|
-
const row = await messageRow(messageId);
|
|
641
|
-
assert.equal(row.filterMove, undefined);
|
|
642
|
-
assert.equal(row.movedByRemit, false);
|
|
643
|
-
});
|
|
644
|
-
});
|
|
645
|
-
|
|
646
|
-
describe("an unsubscribed sender's mail is marked read once", () => {
|
|
647
|
-
it("marks it read on the first store", async () => {
|
|
648
|
-
await flagSender("someone@example.com", {
|
|
649
|
-
unsubscribed: { value: true, setAt: 1 },
|
|
650
|
-
});
|
|
651
|
-
const messageId = await arrive(FROM_SOMEONE);
|
|
652
|
-
|
|
653
|
-
await read(bodySync({ unsubscribe: true }), messageId);
|
|
654
|
-
|
|
655
|
-
assert.equal((await threadRows(messageId))[0].isRead, true);
|
|
656
|
-
assert.ok(
|
|
657
|
-
await world.store.flagPush.find(messageId, "\\Seen"),
|
|
658
|
-
"the read owes IMAP a \\Seen push",
|
|
659
|
-
);
|
|
660
|
-
});
|
|
661
|
-
|
|
662
|
-
it("leaves read state alone once the flag is unset", async () => {
|
|
663
|
-
await flagSender("someone@example.com", {
|
|
664
|
-
unsubscribed: { value: false, setAt: 1 },
|
|
665
|
-
});
|
|
666
|
-
const messageId = await arrive(FROM_SOMEONE);
|
|
667
|
-
|
|
668
|
-
await read(bodySync({ unsubscribe: true }), messageId);
|
|
669
|
-
|
|
670
|
-
assert.equal((await threadRows(messageId))[0].isRead, false);
|
|
671
|
-
});
|
|
672
|
-
});
|
|
673
|
-
|
|
674
|
-
describe("reading a message the mail server no longer holds", () => {
|
|
675
|
-
const gone = async (inFlight = false) => {
|
|
676
|
-
const messageId = await arrive(FROM_SOMEONE);
|
|
677
|
-
const { uid } = await messageRow(messageId);
|
|
678
|
-
if (inFlight) {
|
|
679
|
-
await world.store.message.transitionPlacement(
|
|
680
|
-
messageId,
|
|
681
|
-
{ status: "active" },
|
|
682
|
-
{ status: "moving", syncStatus: "pending" },
|
|
683
|
-
);
|
|
684
|
-
}
|
|
685
|
-
world.imap.expunge("INBOX", uid);
|
|
686
|
-
const outcome = await read(bodySync(), messageId).then(
|
|
687
|
-
() => assert.fail("expected the read to be refused"),
|
|
688
|
-
(error: unknown) => error,
|
|
689
|
-
);
|
|
690
|
-
return { messageId, outcome };
|
|
691
|
-
};
|
|
692
|
-
|
|
693
|
-
it("answers 404 and removes the settled row with its thread entries", async () => {
|
|
694
|
-
const { messageId, outcome } = await gone();
|
|
695
|
-
|
|
696
|
-
assert.ok(outcome instanceof MessageGoneFromServerError);
|
|
697
|
-
assert.equal(outcome.statusCode, 404);
|
|
698
|
-
await assert.rejects(messageRow(messageId));
|
|
699
|
-
assert.deepEqual(await threadRows(messageId), []);
|
|
700
|
-
});
|
|
701
|
-
|
|
702
|
-
it("answers 409 and deletes nothing while our own move is in flight", async () => {
|
|
703
|
-
const { messageId, outcome } = await gone(true);
|
|
704
|
-
|
|
705
|
-
assert.ok(outcome instanceof MessagePlacementUnsettledError);
|
|
706
|
-
assert.equal(outcome.statusCode, 409);
|
|
707
|
-
assert.equal((await threadRows(messageId)).length, 1);
|
|
708
|
-
});
|
|
709
|
-
|
|
710
|
-
it("answers 409 when another writer moved the row while the server was asked", async () => {
|
|
711
|
-
const messageId = await arrive(FROM_SOMEONE);
|
|
712
|
-
const { uid } = await messageRow(messageId);
|
|
713
|
-
world.imap.expunge("INBOX", uid);
|
|
714
|
-
world.imap.onFetchBody = async () => {
|
|
715
|
-
await world.store.message.transitionPlacement(
|
|
716
|
-
messageId,
|
|
717
|
-
{ status: "active" },
|
|
718
|
-
{ mailboxId: world.travel.mailboxId, uid: 99 },
|
|
719
|
-
);
|
|
720
|
-
};
|
|
721
|
-
|
|
722
|
-
await assert.rejects(
|
|
723
|
-
read(bodySync(), messageId),
|
|
724
|
-
MessagePlacementUnsettledError,
|
|
725
|
-
);
|
|
726
|
-
assert.equal((await threadRows(messageId)).length, 1);
|
|
727
|
-
});
|
|
728
|
-
|
|
729
|
-
it("fails loudly and deletes nothing when the server still lists the UID", async () => {
|
|
730
|
-
const messageId = await arrive(FROM_SOMEONE);
|
|
731
|
-
const connection = world.imap.connection();
|
|
732
|
-
connection.fetchMessageBody = async () => null;
|
|
733
|
-
|
|
734
|
-
await assert.rejects(
|
|
735
|
-
bodySync().fetchAndGetBody(
|
|
736
|
-
messageId,
|
|
737
|
-
world.account.accountId,
|
|
738
|
-
world.account.accountConfigId,
|
|
739
|
-
placementOf(world.inbox),
|
|
740
|
-
async () => connection,
|
|
741
|
-
),
|
|
742
|
-
/which the server still lists/,
|
|
743
|
-
);
|
|
744
|
-
assert.ok(await messageRow(messageId));
|
|
745
|
-
});
|
|
746
|
-
});
|
|
747
|
-
|
|
748
|
-
describe("body sync and the quarantine", () => {
|
|
749
|
-
it("skips a uid already set aside without fetching it", async () => {
|
|
750
|
-
const messageId = await arrive(FROM_SOMEONE);
|
|
751
|
-
const { uid } = await messageRow(messageId);
|
|
752
|
-
await new QuarantineService(
|
|
753
|
-
world.store.quarantine,
|
|
754
|
-
world.store.mailboxSpecialUse,
|
|
755
|
-
"sha-test",
|
|
756
|
-
silentLog,
|
|
757
|
-
).record(
|
|
758
|
-
{
|
|
759
|
-
accountId: world.account.accountId,
|
|
760
|
-
accountConfigId: world.account.accountConfigId,
|
|
761
|
-
mailboxId: world.inbox.mailboxId,
|
|
762
|
-
mailboxPath: "INBOX",
|
|
763
|
-
uidValidity: world.inbox.uidValidity,
|
|
764
|
-
attempts: 3,
|
|
765
|
-
},
|
|
766
|
-
uid,
|
|
767
|
-
{ stage: "BodyParse", code: "UnreadableBody", message: "boundary" },
|
|
768
|
-
);
|
|
769
|
-
|
|
770
|
-
const result = await bodySync({ quarantine: true }).syncBodies(
|
|
771
|
-
[messageId],
|
|
772
|
-
world.account.accountId,
|
|
773
|
-
world.account.accountConfigId,
|
|
774
|
-
{ ...placementOf(world.inbox), uidValidity: world.inbox.uidValidity },
|
|
775
|
-
async () => assert.fail("a quarantined message is not fetched"),
|
|
776
|
-
);
|
|
777
|
-
|
|
778
|
-
assert.equal(result.skippedCount, 1);
|
|
779
|
-
assert.equal((await messageRow(messageId)).bodyStorageKey, undefined);
|
|
780
|
-
});
|
|
781
|
-
|
|
782
|
-
it("requeues a storage failure rather than calling the message unreadable", async () => {
|
|
783
|
-
const messageId = await arrive(FROM_SOMEONE);
|
|
784
|
-
|
|
785
|
-
await assert.rejects(
|
|
786
|
-
sync(
|
|
787
|
-
bodySync({ quarantine: true, storage: world.store.brokenStorage() }),
|
|
788
|
-
[messageId],
|
|
789
|
-
),
|
|
790
|
-
);
|
|
791
|
-
|
|
792
|
-
assert.deepEqual(
|
|
793
|
-
await world.store.quarantine.listByAccountConfigId(
|
|
794
|
-
world.account.accountConfigId,
|
|
795
|
-
),
|
|
796
|
-
[],
|
|
797
|
-
);
|
|
798
|
-
assert.equal((await messageRow(messageId)).bodyStorageKey, undefined);
|
|
799
|
-
});
|
|
800
|
-
});
|
|
801
|
-
|
|
802
|
-
describe("mail-derived calendar suggestions", () => {
|
|
803
|
-
const suggestions = (messageId: string) =>
|
|
804
|
-
world.store.calendarSuggestion.listByMessage(
|
|
805
|
-
world.account.accountConfigId,
|
|
806
|
-
messageId,
|
|
807
|
-
);
|
|
808
|
-
|
|
809
|
-
it("offers an invitation as one Pending card carrying its bytes", async () => {
|
|
810
|
-
const messageId = await arrive(invitation());
|
|
811
|
-
|
|
812
|
-
await read(bodySync({ calendar: true }), messageId);
|
|
813
|
-
|
|
814
|
-
const [row, ...rest] = await suggestions(messageId);
|
|
815
|
-
assert.deepEqual(rest, []);
|
|
816
|
-
assert.equal(row.state, CalendarSuggestionState.Pending);
|
|
817
|
-
assert.equal(row.icalUid, ICAL_UID);
|
|
818
|
-
assert.equal(row.method, CalendarInviteMethod.Request);
|
|
819
|
-
assert.equal(row.organizer, "organizer@example.test");
|
|
820
|
-
assert.match(row.icalData, /UID:invite-4711@example\.test/);
|
|
821
|
-
});
|
|
822
|
-
|
|
823
|
-
it("reads a floating start in the collection's own zone", async () => {
|
|
824
|
-
await world.store.calendarCollection.create({
|
|
825
|
-
accountConfigId: world.account.accountConfigId,
|
|
826
|
-
urlSegment: DEFAULT_CALENDAR_URL_SEGMENT,
|
|
827
|
-
displayName: "Calendar",
|
|
828
|
-
source: CalendarSource.Default,
|
|
829
|
-
timezone: "Europe/Amsterdam",
|
|
830
|
-
});
|
|
831
|
-
const messageId = await arrive(
|
|
832
|
-
invitation(0, "REQUEST", "DTSTART:20260901T100000"),
|
|
833
|
-
);
|
|
834
|
-
|
|
835
|
-
await read(bodySync({ calendar: true }), messageId);
|
|
836
|
-
|
|
837
|
-
const [row] = await suggestions(messageId);
|
|
838
|
-
assert.equal(row.dtStart, "2026-09-01T10:00:00+02:00");
|
|
839
|
-
assert.equal(row.zoneCertainty, "Local");
|
|
840
|
-
});
|
|
841
|
-
|
|
842
|
-
it("offers no card for a sender the user muted", async () => {
|
|
843
|
-
await addFilter([
|
|
844
|
-
{ field: FilterClauseField.From, value: "organizer@example.test" },
|
|
845
|
-
]);
|
|
846
|
-
const messageId = await arrive(invitation());
|
|
847
|
-
|
|
848
|
-
await read(bodySync({ calendar: true }), messageId);
|
|
849
|
-
|
|
850
|
-
assert.deepEqual(await suggestions(messageId), []);
|
|
851
|
-
});
|
|
852
|
-
|
|
853
|
-
it("still offers a card when the rule about this sender files its mail", async () => {
|
|
854
|
-
await addFilter(
|
|
855
|
-
[{ field: FilterClauseField.From, value: "organizer@example.test" }],
|
|
856
|
-
world.travel.mailboxId,
|
|
857
|
-
);
|
|
858
|
-
const messageId = await arrive(invitation());
|
|
859
|
-
|
|
860
|
-
await read(bodySync({ calendar: true }), messageId);
|
|
861
|
-
|
|
862
|
-
assert.equal((await suggestions(messageId)).length, 1);
|
|
863
|
-
});
|
|
864
|
-
|
|
865
|
-
it("offers nothing on a re-store", async () => {
|
|
866
|
-
const messageId = await arrive(invitation(), world.inbox, {
|
|
867
|
-
bodyStorageKey: MISSING_BODY,
|
|
868
|
-
});
|
|
869
|
-
|
|
870
|
-
await read(bodySync({ calendar: true }), messageId);
|
|
871
|
-
|
|
872
|
-
assert.deepEqual(await suggestions(messageId), []);
|
|
873
|
-
});
|
|
874
|
-
|
|
875
|
-
it("supersedes the earlier revision when a higher SEQUENCE arrives", async () => {
|
|
876
|
-
const service = bodySync({ calendar: true });
|
|
877
|
-
const first = await arrive(invitation(0));
|
|
878
|
-
const second = await arrive(invitation(1));
|
|
879
|
-
|
|
880
|
-
await read(service, first);
|
|
881
|
-
await read(service, second);
|
|
882
|
-
|
|
883
|
-
assert.equal(
|
|
884
|
-
(await suggestions(first))[0].state,
|
|
885
|
-
CalendarSuggestionState.Superseded,
|
|
886
|
-
);
|
|
887
|
-
assert.equal(
|
|
888
|
-
(await suggestions(second))[0].state,
|
|
889
|
-
CalendarSuggestionState.Pending,
|
|
890
|
-
);
|
|
891
|
-
});
|
|
892
|
-
|
|
893
|
-
it("offers a cancellation as its own card", async () => {
|
|
894
|
-
const messageId = await arrive(invitation(1, "CANCEL"));
|
|
895
|
-
|
|
896
|
-
await read(bodySync({ calendar: true }), messageId);
|
|
897
|
-
|
|
898
|
-
const [row] = await suggestions(messageId);
|
|
899
|
-
assert.equal(row.method, CalendarInviteMethod.Cancel);
|
|
900
|
-
assert.equal(row.acceptedCalendarObjectId, "");
|
|
901
|
-
});
|
|
902
|
-
|
|
903
|
-
it("stores the body when the iCalendar is unreadable", async () => {
|
|
904
|
-
const messageId = await arrive(
|
|
905
|
-
Buffer.from(
|
|
906
|
-
[
|
|
907
|
-
"From: Organizer <organizer@example.test>",
|
|
908
|
-
'Content-Type: multipart/alternative; boundary="bnd"',
|
|
909
|
-
"",
|
|
910
|
-
"--bnd",
|
|
911
|
-
"Content-Type: text/calendar",
|
|
912
|
-
"",
|
|
913
|
-
"BEGIN:VCALENDAR",
|
|
914
|
-
"--bnd--",
|
|
915
|
-
"",
|
|
916
|
-
].join("\r\n"),
|
|
917
|
-
),
|
|
918
|
-
);
|
|
919
|
-
|
|
920
|
-
await read(bodySync({ calendar: true }), messageId);
|
|
921
|
-
|
|
922
|
-
assert.ok((await messageRow(messageId)).bodyStorageKey);
|
|
923
|
-
assert.deepEqual(await suggestions(messageId), []);
|
|
924
|
-
});
|
|
925
|
-
});
|