@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
|
@@ -1,378 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Issue #665. While a placement move is in flight the Message row names the
|
|
3
|
-
* destination folder but still carries the SOURCE folder's uid, so a user move
|
|
4
|
-
* that binds the pair as it stands sends the worker to `openBox(destination)`
|
|
5
|
-
* and `moveMessages([uid])` — the destination folder's own message at that uid,
|
|
6
|
-
* carried off to wherever the user pointed. The reachable press is Undo on the
|
|
7
|
-
* auto-moved badge, inside the seconds before the placement push confirms.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import assert from "node:assert/strict";
|
|
11
|
-
import { describe, it } from "node:test";
|
|
12
|
-
import type {
|
|
13
|
-
IAddressRepository,
|
|
14
|
-
IMailboxRepository,
|
|
15
|
-
IMailboxSpecialUseRepository,
|
|
16
|
-
IMessageRepository,
|
|
17
|
-
IThreadMessageRepository,
|
|
18
|
-
} from "@remit/data-ports";
|
|
19
|
-
import { MessagePlacementUnsettledError } from "@remit/data-ports/errors";
|
|
20
|
-
import { MessageMoveService } from "./message-move.js";
|
|
21
|
-
import { NO_JUNK_ROLES } from "./test-helpers/folder-roles.js";
|
|
22
|
-
|
|
23
|
-
const ACCOUNT = "acc-1";
|
|
24
|
-
const ACCOUNT_CONFIG = "cfg-1";
|
|
25
|
-
const INBOX = "mbx-inbox";
|
|
26
|
-
const ARCHIVE = "mbx-archive";
|
|
27
|
-
|
|
28
|
-
const MOVING_ID = "msg-moving";
|
|
29
|
-
const SETTLED_ID = "msg-settled";
|
|
30
|
-
const STRANDED_ID = "msg-stranded";
|
|
31
|
-
const DELETING_ID = "msg-deleting";
|
|
32
|
-
const COPY_ID = "msg-copy";
|
|
33
|
-
|
|
34
|
-
const INBOX_UID = 42;
|
|
35
|
-
const ARCHIVE_UID = 907;
|
|
36
|
-
const SETTLED_UID = 11;
|
|
37
|
-
|
|
38
|
-
interface CapturedEvent {
|
|
39
|
-
type: string;
|
|
40
|
-
messageId?: string;
|
|
41
|
-
uid?: number;
|
|
42
|
-
sourceMailboxPath?: string;
|
|
43
|
-
destinationMailboxPath?: string;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const mailboxes = [
|
|
47
|
-
{ mailboxId: INBOX, fullPath: "INBOX", accountId: ACCOUNT },
|
|
48
|
-
{ mailboxId: ARCHIVE, fullPath: "Archive", accountId: ACCOUNT },
|
|
49
|
-
];
|
|
50
|
-
|
|
51
|
-
/** A row mid-move: Archive is already written, the uid is still INBOX's. */
|
|
52
|
-
const movingRow = () => ({
|
|
53
|
-
messageId: MOVING_ID,
|
|
54
|
-
mailboxId: ARCHIVE,
|
|
55
|
-
uid: INBOX_UID,
|
|
56
|
-
status: "moving",
|
|
57
|
-
syncStatus: "pending",
|
|
58
|
-
originalMailboxId: INBOX,
|
|
59
|
-
originalUid: INBOX_UID,
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* A row a mutation gave up on. R3 defines the give-up as a hand-back: the row
|
|
64
|
-
* goes back on the placement the mail server confirmed, and only then carries
|
|
65
|
-
* `abandoned`. The pair is honest, so nothing downstream refuses it — the
|
|
66
|
-
* failure is a statement the reading pane makes, never a gate.
|
|
67
|
-
*/
|
|
68
|
-
const strandedRow = () => ({
|
|
69
|
-
messageId: STRANDED_ID,
|
|
70
|
-
mailboxId: INBOX,
|
|
71
|
-
uid: INBOX_UID,
|
|
72
|
-
status: "active",
|
|
73
|
-
syncStatus: "abandoned",
|
|
74
|
-
abandonedMutation: "move",
|
|
75
|
-
originalMailboxId: INBOX,
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* The same shape a delete leaves: Trash is already written, the uid is still
|
|
80
|
-
* INBOX's, and `deleting` is what says so. R3 calls this in flight exactly as
|
|
81
|
-
* `moving` is — reading only `moving` let a move enqueued behind a delete bind
|
|
82
|
-
* this pair and be dropped when the delete settled.
|
|
83
|
-
*/
|
|
84
|
-
const deletingRow = () => ({
|
|
85
|
-
messageId: DELETING_ID,
|
|
86
|
-
mailboxId: ARCHIVE,
|
|
87
|
-
uid: INBOX_UID,
|
|
88
|
-
status: "deleting",
|
|
89
|
-
syncStatus: "pending",
|
|
90
|
-
originalMailboxId: INBOX,
|
|
91
|
-
originalUid: INBOX_UID,
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
/** An ordinary settled row. */
|
|
95
|
-
const settledRow = () => ({
|
|
96
|
-
messageId: SETTLED_ID,
|
|
97
|
-
mailboxId: INBOX,
|
|
98
|
-
uid: SETTLED_UID,
|
|
99
|
-
status: "active",
|
|
100
|
-
syncStatus: "synced",
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* A freshly copied row: `moving` until COPYUID lands, with no server-side uid
|
|
105
|
-
* at all and no move behind it. Its uid is not ready; it does not name anyone.
|
|
106
|
-
*/
|
|
107
|
-
const freshCopyRow = () => ({
|
|
108
|
-
messageId: COPY_ID,
|
|
109
|
-
mailboxId: ARCHIVE,
|
|
110
|
-
uid: 0,
|
|
111
|
-
status: "moving",
|
|
112
|
-
syncStatus: "pending",
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
const buildWorld = (seed: Array<Record<string, unknown>>) => {
|
|
116
|
-
const patches: Array<{ messageId: string; patch: Record<string, unknown> }> =
|
|
117
|
-
[];
|
|
118
|
-
const events: CapturedEvent[] = [];
|
|
119
|
-
const rows = new Map<string, Record<string, unknown>>(
|
|
120
|
-
seed.map((row): [string, Record<string, unknown>] => [
|
|
121
|
-
String(row.messageId),
|
|
122
|
-
row,
|
|
123
|
-
]),
|
|
124
|
-
);
|
|
125
|
-
|
|
126
|
-
/** What `updateUid` writes when the placement move confirms. */
|
|
127
|
-
const settle = (messageId: string) => {
|
|
128
|
-
Object.assign(rows.get(messageId) ?? {}, {
|
|
129
|
-
uid: ARCHIVE_UID,
|
|
130
|
-
status: "active",
|
|
131
|
-
syncStatus: "synced",
|
|
132
|
-
});
|
|
133
|
-
};
|
|
134
|
-
|
|
135
|
-
const messageService = {
|
|
136
|
-
get: async (ids: string | string[]) =>
|
|
137
|
-
Array.isArray(ids)
|
|
138
|
-
? ids.flatMap((id) => {
|
|
139
|
-
const row = rows.get(id);
|
|
140
|
-
return row ? [row] : [];
|
|
141
|
-
})
|
|
142
|
-
: rows.get(ids),
|
|
143
|
-
update: async (id: string, patch: Record<string, unknown>) => {
|
|
144
|
-
patches.push({ messageId: id, patch });
|
|
145
|
-
return Object.assign(rows.get(id) ?? {}, patch);
|
|
146
|
-
},
|
|
147
|
-
transitionPlacement: async (
|
|
148
|
-
id: string,
|
|
149
|
-
_expected: Record<string, unknown>,
|
|
150
|
-
patch: Record<string, unknown>,
|
|
151
|
-
) => {
|
|
152
|
-
patches.push({ messageId: id, patch });
|
|
153
|
-
return Object.assign(rows.get(id) ?? {}, patch);
|
|
154
|
-
},
|
|
155
|
-
} as unknown as IMessageRepository;
|
|
156
|
-
|
|
157
|
-
const threadMessageService = {
|
|
158
|
-
getByMessageId: async (_cfg: string, messageId: string) => ({
|
|
159
|
-
accountConfigId: ACCOUNT_CONFIG,
|
|
160
|
-
threadMessageId: `tm-${messageId}`,
|
|
161
|
-
messageId,
|
|
162
|
-
threadId: "thr-1",
|
|
163
|
-
mailboxId: rows.get(messageId)?.mailboxId,
|
|
164
|
-
sentDate: 1,
|
|
165
|
-
isRead: false,
|
|
166
|
-
isDeleted: false,
|
|
167
|
-
hasStars: false,
|
|
168
|
-
hasAttachment: false,
|
|
169
|
-
}),
|
|
170
|
-
update: async () => {},
|
|
171
|
-
} as unknown as IThreadMessageRepository;
|
|
172
|
-
|
|
173
|
-
const mailboxService = {
|
|
174
|
-
get: async (_accountId: string, ids: string | string[]) =>
|
|
175
|
-
Array.isArray(ids)
|
|
176
|
-
? mailboxes.filter((mailbox) => ids.includes(mailbox.mailboxId))
|
|
177
|
-
: mailboxes.find((mailbox) => mailbox.mailboxId === ids),
|
|
178
|
-
} as unknown as IMailboxRepository;
|
|
179
|
-
|
|
180
|
-
const mailboxSpecialUseService = {
|
|
181
|
-
findTrashMailbox: async () => null,
|
|
182
|
-
resolveJunkRolesForConfig: async () => NO_JUNK_ROLES,
|
|
183
|
-
} as unknown as IMailboxSpecialUseRepository;
|
|
184
|
-
|
|
185
|
-
const addressService = {
|
|
186
|
-
reconcileJunkOnlyForMessage: async () => {},
|
|
187
|
-
} as unknown as IAddressRepository;
|
|
188
|
-
|
|
189
|
-
const service = new MessageMoveService({
|
|
190
|
-
messageService,
|
|
191
|
-
mailboxService,
|
|
192
|
-
mailboxSpecialUseService,
|
|
193
|
-
threadMessageService,
|
|
194
|
-
addressService,
|
|
195
|
-
sqsQueueUrl: "http://localhost:9324/000000000000/remit-messages.fifo",
|
|
196
|
-
moveSettleTimeoutMs: 200,
|
|
197
|
-
moveSettlePollMs: 10,
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
(
|
|
201
|
-
service as unknown as { enqueueEvent: (e: CapturedEvent) => Promise<void> }
|
|
202
|
-
).enqueueEvent = async (event) => {
|
|
203
|
-
events.push(event);
|
|
204
|
-
};
|
|
205
|
-
|
|
206
|
-
return { service, patches, events, rows, settle };
|
|
207
|
-
};
|
|
208
|
-
|
|
209
|
-
describe("a move never binds the folder/uid pair of an in-flight move (#665)", () => {
|
|
210
|
-
it("binds the confirmed pair once the placement move settles", async () => {
|
|
211
|
-
const { service, events, settle } = buildWorld([movingRow()]);
|
|
212
|
-
|
|
213
|
-
setTimeout(() => settle(MOVING_ID), 30);
|
|
214
|
-
await service.moveMessage(ACCOUNT_CONFIG, MOVING_ID, INBOX, ACCOUNT);
|
|
215
|
-
|
|
216
|
-
assert.equal(events.length, 1);
|
|
217
|
-
assert.equal(events[0].type, "MESSAGE_MOVE");
|
|
218
|
-
assert.equal(
|
|
219
|
-
events[0].sourceMailboxPath,
|
|
220
|
-
"Archive",
|
|
221
|
-
"the folder the confirmed move left the message in",
|
|
222
|
-
);
|
|
223
|
-
assert.equal(
|
|
224
|
-
events[0].uid,
|
|
225
|
-
ARCHIVE_UID,
|
|
226
|
-
"and that folder's own uid, never the source folder's",
|
|
227
|
-
);
|
|
228
|
-
assert.equal(events[0].destinationMailboxPath, "INBOX");
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
it("refuses the move, and touches nothing, when the placement never settles", async () => {
|
|
232
|
-
const { service, patches, events, rows } = buildWorld([movingRow()]);
|
|
233
|
-
|
|
234
|
-
await assert.rejects(
|
|
235
|
-
() => service.moveMessage(ACCOUNT_CONFIG, MOVING_ID, INBOX, ACCOUNT),
|
|
236
|
-
(error: unknown) =>
|
|
237
|
-
error instanceof MessagePlacementUnsettledError &&
|
|
238
|
-
error.statusCode === 409 &&
|
|
239
|
-
error.publicApiError?.code === "message_placement_unsettled" &&
|
|
240
|
-
error.publicApiError?.details?.messageId === MOVING_ID &&
|
|
241
|
-
error.publicApiError?.details?.accountId === ACCOUNT &&
|
|
242
|
-
error.publicApiError?.details?.reason === "in_flight",
|
|
243
|
-
);
|
|
244
|
-
|
|
245
|
-
assert.deepEqual(events, [], "nothing was enqueued");
|
|
246
|
-
assert.deepEqual(patches, [], "and no local write was made");
|
|
247
|
-
assert.equal(rows.get(MOVING_ID)?.mailboxId, ARCHIVE);
|
|
248
|
-
assert.equal(rows.get(MOVING_ID)?.uid, INBOX_UID);
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
it("never answers the same-mailbox skip from a folder the message has not reached", async () => {
|
|
252
|
-
// The skip compares the row's `mailboxId` against the destination. An
|
|
253
|
-
// in-flight move has already written that folder optimistically, so the
|
|
254
|
-
// comparison would report the message home and return a silent success
|
|
255
|
-
// for a move the server has not made.
|
|
256
|
-
const { service, events } = buildWorld([movingRow()]);
|
|
257
|
-
|
|
258
|
-
await assert.rejects(
|
|
259
|
-
() => service.moveMessage(ACCOUNT_CONFIG, MOVING_ID, ARCHIVE, ACCOUNT),
|
|
260
|
-
MessagePlacementUnsettledError,
|
|
261
|
-
);
|
|
262
|
-
|
|
263
|
-
assert.deepEqual(events, []);
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
it("moves no part of a batch when one row is in flight", async () => {
|
|
267
|
-
// All-or-nothing: the settled row is movable on its own and a per-row
|
|
268
|
-
// gate would have moved it before reaching the refusal.
|
|
269
|
-
const { service, patches, events } = buildWorld([
|
|
270
|
-
settledRow(),
|
|
271
|
-
movingRow(),
|
|
272
|
-
]);
|
|
273
|
-
|
|
274
|
-
await assert.rejects(
|
|
275
|
-
() =>
|
|
276
|
-
service.moveMessages(
|
|
277
|
-
ACCOUNT_CONFIG,
|
|
278
|
-
[SETTLED_ID, MOVING_ID],
|
|
279
|
-
ARCHIVE,
|
|
280
|
-
ACCOUNT,
|
|
281
|
-
),
|
|
282
|
-
MessagePlacementUnsettledError,
|
|
283
|
-
);
|
|
284
|
-
|
|
285
|
-
assert.deepEqual(
|
|
286
|
-
events,
|
|
287
|
-
[],
|
|
288
|
-
"the settled row was not moved alongside the refusal",
|
|
289
|
-
);
|
|
290
|
-
assert.deepEqual(patches, []);
|
|
291
|
-
});
|
|
292
|
-
|
|
293
|
-
it("moves the whole batch once the in-flight row settles", async () => {
|
|
294
|
-
const { service, events, settle } = buildWorld([settledRow(), movingRow()]);
|
|
295
|
-
|
|
296
|
-
setTimeout(() => settle(MOVING_ID), 30);
|
|
297
|
-
await service.moveMessages(
|
|
298
|
-
ACCOUNT_CONFIG,
|
|
299
|
-
[SETTLED_ID, MOVING_ID],
|
|
300
|
-
INBOX,
|
|
301
|
-
ACCOUNT,
|
|
302
|
-
);
|
|
303
|
-
|
|
304
|
-
assert.deepEqual(
|
|
305
|
-
events.map((event) => [event.messageId, event.uid]),
|
|
306
|
-
[[MOVING_ID, ARCHIVE_UID]],
|
|
307
|
-
"the settled row was already in INBOX and skipped; the moved row went with the confirmed uid",
|
|
308
|
-
);
|
|
309
|
-
});
|
|
310
|
-
|
|
311
|
-
it("waits one row's ceiling for a batch, not one per row", async () => {
|
|
312
|
-
const rows = [movingRow(), movingRow(), movingRow()].map((row, index) => ({
|
|
313
|
-
...row,
|
|
314
|
-
messageId: `${MOVING_ID}-${index}`,
|
|
315
|
-
}));
|
|
316
|
-
const { service } = buildWorld(rows);
|
|
317
|
-
|
|
318
|
-
const startedAt = Date.now();
|
|
319
|
-
await assert.rejects(
|
|
320
|
-
() =>
|
|
321
|
-
service.moveMessages(
|
|
322
|
-
ACCOUNT_CONFIG,
|
|
323
|
-
rows.map((row) => row.messageId),
|
|
324
|
-
INBOX,
|
|
325
|
-
ACCOUNT,
|
|
326
|
-
),
|
|
327
|
-
MessagePlacementUnsettledError,
|
|
328
|
-
);
|
|
329
|
-
|
|
330
|
-
assert.ok(
|
|
331
|
-
Date.now() - startedAt < 500,
|
|
332
|
-
"three rows refused within one 200ms ceiling, not three",
|
|
333
|
-
);
|
|
334
|
-
});
|
|
335
|
-
});
|
|
336
|
-
|
|
337
|
-
describe("the move gate refuses only a uid that names somebody else (#665)", () => {
|
|
338
|
-
it("moves a freshly copied row rather than waiting on it", async () => {
|
|
339
|
-
// A copy is `moving` with no server-side uid yet. That uid is not ready,
|
|
340
|
-
// which is not the same as naming another folder's message.
|
|
341
|
-
const { service, events } = buildWorld([freshCopyRow()]);
|
|
342
|
-
|
|
343
|
-
const startedAt = Date.now();
|
|
344
|
-
await service.moveMessage(ACCOUNT_CONFIG, COPY_ID, INBOX, ACCOUNT);
|
|
345
|
-
|
|
346
|
-
assert.equal(events.length, 1);
|
|
347
|
-
assert.equal(events[0].type, "MESSAGE_MOVE");
|
|
348
|
-
assert.ok(Date.now() - startedAt < 100, "and never entered the wait");
|
|
349
|
-
});
|
|
350
|
-
|
|
351
|
-
it("waits out a delete in flight rather than binding its borrowed uid", async () => {
|
|
352
|
-
const { service, events, patches } = buildWorld([deletingRow()]);
|
|
353
|
-
|
|
354
|
-
await assert.rejects(
|
|
355
|
-
() => service.moveMessage(ACCOUNT_CONFIG, DELETING_ID, INBOX, ACCOUNT),
|
|
356
|
-
(error: unknown) => error instanceof MessagePlacementUnsettledError,
|
|
357
|
-
);
|
|
358
|
-
|
|
359
|
-
assert.deepEqual(events, [], "nothing was enqueued");
|
|
360
|
-
assert.deepEqual(patches, [], "and no local write was made");
|
|
361
|
-
});
|
|
362
|
-
|
|
363
|
-
it("acts on a row a move gave up on, rather than refusing it", async () => {
|
|
364
|
-
// A give-up hands the row back to the placement the server confirmed
|
|
365
|
-
// before it stops (R3), so the folder and uid name the same message and
|
|
366
|
-
// the user's next move is an ordinary one. Refusing it — which is what a
|
|
367
|
-
// give-up left `moving` used to earn — made the message unmovable for
|
|
368
|
-
// good, with nothing routine to repair it.
|
|
369
|
-
const { service, events } = buildWorld([strandedRow()]);
|
|
370
|
-
|
|
371
|
-
const startedAt = Date.now();
|
|
372
|
-
await service.moveMessage(ACCOUNT_CONFIG, STRANDED_ID, ARCHIVE, ACCOUNT);
|
|
373
|
-
|
|
374
|
-
assert.equal(events.length, 1);
|
|
375
|
-
assert.equal(events[0].type, "MESSAGE_MOVE");
|
|
376
|
-
assert.ok(Date.now() - startedAt < 100, "and never entered the wait");
|
|
377
|
-
});
|
|
378
|
-
});
|
|
@@ -1,293 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { describe, it } from "node:test";
|
|
3
|
-
import type {
|
|
4
|
-
CreateAddressInput,
|
|
5
|
-
CreateEnvelopeAddressInput,
|
|
6
|
-
CreateThreadMessageInput,
|
|
7
|
-
IAddressRepository,
|
|
8
|
-
IEnvelopeRepository,
|
|
9
|
-
IMailboxRepository,
|
|
10
|
-
IMessageRepository,
|
|
11
|
-
IThreadMessageRepository,
|
|
12
|
-
MailboxItem,
|
|
13
|
-
MessageItem,
|
|
14
|
-
ThreadMessageItem,
|
|
15
|
-
} from "@remit/data-ports";
|
|
16
|
-
import type { ManagedConnectionFactory } from "./connection-factory.js";
|
|
17
|
-
import { type AccountFolderRoles, MessageSyncService } from "./message-sync.js";
|
|
18
|
-
import { NO_FOLDER_ROLES, noFolderRoles } from "./test-helpers/folder-roles.js";
|
|
19
|
-
import type { ImapAddress, ImapEnvelope, ImapMessage } from "./types.js";
|
|
20
|
-
|
|
21
|
-
const stub = <T>(): T => ({}) as T;
|
|
22
|
-
|
|
23
|
-
const emptyEnvelope: ImapEnvelope = {
|
|
24
|
-
date: new Date(0).toISOString(),
|
|
25
|
-
messageId: "<root@example.com>",
|
|
26
|
-
subject: "Subject",
|
|
27
|
-
from: [],
|
|
28
|
-
sender: [],
|
|
29
|
-
replyTo: [],
|
|
30
|
-
to: [],
|
|
31
|
-
cc: [],
|
|
32
|
-
bcc: [],
|
|
33
|
-
inReplyTo: "",
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
interface Saved {
|
|
37
|
-
addresses: CreateAddressInput[];
|
|
38
|
-
envelopeAddresses: CreateEnvelopeAddressInput[];
|
|
39
|
-
threadMessages: CreateThreadMessageInput[];
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const harvest = async (envelope: Partial<ImapEnvelope>): Promise<Saved> => {
|
|
43
|
-
const saved: Saved = {
|
|
44
|
-
addresses: [],
|
|
45
|
-
envelopeAddresses: [],
|
|
46
|
-
threadMessages: [],
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
const messageService = {
|
|
50
|
-
upsertWithStatus: async (input: unknown) => ({
|
|
51
|
-
item: input as MessageItem,
|
|
52
|
-
created: true,
|
|
53
|
-
}),
|
|
54
|
-
} as unknown as IMessageRepository;
|
|
55
|
-
|
|
56
|
-
const threadMessageService = {
|
|
57
|
-
create: async (input: CreateThreadMessageInput) => {
|
|
58
|
-
saved.threadMessages.push(input);
|
|
59
|
-
return input as unknown as ThreadMessageItem;
|
|
60
|
-
},
|
|
61
|
-
} as unknown as IThreadMessageRepository;
|
|
62
|
-
|
|
63
|
-
const envelopeService = {
|
|
64
|
-
upsertEnvelope: async () => {},
|
|
65
|
-
upsertBodyParts: async () => {},
|
|
66
|
-
} as unknown as IEnvelopeRepository;
|
|
67
|
-
|
|
68
|
-
const addressService = {
|
|
69
|
-
upsertCorrespondentAddress: async (input: CreateAddressInput) => {
|
|
70
|
-
saved.addresses.push(input);
|
|
71
|
-
},
|
|
72
|
-
upsertEnvelopeAddress: async (input: CreateEnvelopeAddressInput) => {
|
|
73
|
-
saved.envelopeAddresses.push(input);
|
|
74
|
-
},
|
|
75
|
-
} as unknown as IAddressRepository;
|
|
76
|
-
|
|
77
|
-
const service = new MessageSyncService(
|
|
78
|
-
stub<ManagedConnectionFactory>(),
|
|
79
|
-
stub<IMailboxRepository>(),
|
|
80
|
-
noFolderRoles,
|
|
81
|
-
messageService,
|
|
82
|
-
envelopeService,
|
|
83
|
-
addressService,
|
|
84
|
-
threadMessageService,
|
|
85
|
-
);
|
|
86
|
-
|
|
87
|
-
const msg = {
|
|
88
|
-
uid: 42,
|
|
89
|
-
seq: 1,
|
|
90
|
-
size: 100,
|
|
91
|
-
internalDate: new Date(0),
|
|
92
|
-
flags: [],
|
|
93
|
-
envelope: { ...emptyEnvelope, ...envelope },
|
|
94
|
-
} as unknown as ImapMessage;
|
|
95
|
-
|
|
96
|
-
await (
|
|
97
|
-
service as unknown as {
|
|
98
|
-
saveMessage: (
|
|
99
|
-
mailbox: MailboxItem,
|
|
100
|
-
accountId: string,
|
|
101
|
-
accountConfigId: string,
|
|
102
|
-
msg: ImapMessage,
|
|
103
|
-
roles: AccountFolderRoles,
|
|
104
|
-
) => Promise<unknown>;
|
|
105
|
-
}
|
|
106
|
-
).saveMessage(
|
|
107
|
-
{ mailboxId: "mbx-1", fullPath: "INBOX" } as MailboxItem,
|
|
108
|
-
"acct-1",
|
|
109
|
-
"cfg-1",
|
|
110
|
-
msg,
|
|
111
|
-
NO_FOLDER_ROLES,
|
|
112
|
-
);
|
|
113
|
-
|
|
114
|
-
return saved;
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
const spoof: ImapAddress = {
|
|
118
|
-
name: "matthijs@ischen.nl",
|
|
119
|
-
mailbox: "aramirez",
|
|
120
|
-
host: "secresaludguaviare.gov.co",
|
|
121
|
-
};
|
|
122
|
-
|
|
123
|
-
const onlyAddress = async (
|
|
124
|
-
envelope: Partial<ImapEnvelope>,
|
|
125
|
-
): Promise<CreateAddressInput> => {
|
|
126
|
-
const { addresses } = await harvest(envelope);
|
|
127
|
-
assert.equal(addresses.length, 1);
|
|
128
|
-
const [input] = addresses;
|
|
129
|
-
assert.ok(input);
|
|
130
|
-
return input;
|
|
131
|
-
};
|
|
132
|
-
|
|
133
|
-
describe("harvesting an envelope display name", () => {
|
|
134
|
-
it("drops a name that is an address the row does not own", async () => {
|
|
135
|
-
const input = await onlyAddress({ from: [spoof] });
|
|
136
|
-
|
|
137
|
-
assert.equal(input.displayName, "");
|
|
138
|
-
assert.equal(input.normalizedEmail, "aramirez@secresaludguaviare.gov.co");
|
|
139
|
-
assert.equal(
|
|
140
|
-
input.normalizedCompound,
|
|
141
|
-
"aramirez@secresaludguaviare.gov.co",
|
|
142
|
-
);
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
it("keeps what a name says besides the address it claims", async () => {
|
|
146
|
-
const input = await onlyAddress({
|
|
147
|
-
from: [{ ...spoof, name: "Matthijs <matthijs@ischen.nl>" }],
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
assert.equal(input.displayName, "Matthijs");
|
|
151
|
-
assert.equal(
|
|
152
|
-
input.normalizedCompound,
|
|
153
|
-
"matthijs aramirez@secresaludguaviare.gov.co",
|
|
154
|
-
);
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
it("keeps a name a sender wrote its own support address into", async () => {
|
|
158
|
-
const input = await onlyAddress({
|
|
159
|
-
from: [
|
|
160
|
-
{
|
|
161
|
-
name: "Support (support@acme.com)",
|
|
162
|
-
mailbox: "noreply",
|
|
163
|
-
host: "acme.com",
|
|
164
|
-
},
|
|
165
|
-
],
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
assert.equal(input.displayName, "Support");
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
it("keeps a name that is the address it labels", async () => {
|
|
172
|
-
const input = await onlyAddress({
|
|
173
|
-
from: [
|
|
174
|
-
{
|
|
175
|
-
name: "ING@ing-nl-mailing.nl",
|
|
176
|
-
mailbox: "ing",
|
|
177
|
-
host: "ing-nl-mailing.nl",
|
|
178
|
-
},
|
|
179
|
-
],
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
assert.equal(input.displayName, "ING@ing-nl-mailing.nl");
|
|
183
|
-
assert.equal(
|
|
184
|
-
input.normalizedCompound,
|
|
185
|
-
"ing@ing-nl-mailing.nl ing@ing-nl-mailing.nl",
|
|
186
|
-
);
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
it("keeps a non-ASCII name that is the address it labels", async () => {
|
|
190
|
-
const input = await onlyAddress({
|
|
191
|
-
from: [
|
|
192
|
-
{ name: "Özcan@example.com", mailbox: "Özcan", host: "example.com" },
|
|
193
|
-
],
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
assert.equal(input.displayName, "Özcan@example.com");
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
it("keeps an ordinary human name", async () => {
|
|
200
|
-
const input = await onlyAddress({
|
|
201
|
-
from: [
|
|
202
|
-
{ name: "Matthijs van Henten", mailbox: "matthijs", host: "ischen.nl" },
|
|
203
|
-
],
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
assert.equal(input.displayName, "Matthijs van Henten");
|
|
207
|
-
assert.equal(
|
|
208
|
-
input.normalizedCompound,
|
|
209
|
-
"matthijs van henten matthijs@ischen.nl",
|
|
210
|
-
);
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
it("drops the name on every harvested envelope role", async () => {
|
|
214
|
-
const { addresses } = await harvest({
|
|
215
|
-
from: [spoof],
|
|
216
|
-
sender: [spoof],
|
|
217
|
-
replyTo: [spoof],
|
|
218
|
-
to: [spoof],
|
|
219
|
-
cc: [spoof],
|
|
220
|
-
bcc: [spoof],
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
assert.equal(addresses.length, 6);
|
|
224
|
-
for (const input of addresses) {
|
|
225
|
-
assert.equal(input.displayName, "");
|
|
226
|
-
}
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
it("still harvests the address from every envelope role", async () => {
|
|
230
|
-
const named = (mailbox: string): ImapAddress[] => [
|
|
231
|
-
{ name: "Someone", mailbox, host: "example.com" },
|
|
232
|
-
];
|
|
233
|
-
const { addresses } = await harvest({
|
|
234
|
-
from: named("from"),
|
|
235
|
-
sender: named("sender"),
|
|
236
|
-
replyTo: named("reply-to"),
|
|
237
|
-
to: named("to"),
|
|
238
|
-
cc: named("cc"),
|
|
239
|
-
bcc: named("bcc"),
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
assert.deepEqual(
|
|
243
|
-
addresses.map((input) => input.normalizedEmail),
|
|
244
|
-
[
|
|
245
|
-
"from@example.com",
|
|
246
|
-
"sender@example.com",
|
|
247
|
-
"reply-to@example.com",
|
|
248
|
-
"to@example.com",
|
|
249
|
-
"cc@example.com",
|
|
250
|
-
"bcc@example.com",
|
|
251
|
-
],
|
|
252
|
-
);
|
|
253
|
-
for (const input of addresses) {
|
|
254
|
-
assert.equal(input.displayName, "Someone");
|
|
255
|
-
}
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
it("drops the name on the envelope the message header renders", async () => {
|
|
259
|
-
const { envelopeAddresses } = await harvest({ from: [spoof] });
|
|
260
|
-
|
|
261
|
-
assert.equal(envelopeAddresses.length, 1);
|
|
262
|
-
assert.equal(envelopeAddresses[0]?.displayName, "");
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
it("keeps the sender label's text and drops only the address", async () => {
|
|
266
|
-
const { threadMessages } = await harvest({
|
|
267
|
-
from: [{ ...spoof, name: "Support <matthijs@ischen.nl>" }],
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
assert.equal(threadMessages[0]?.fromName, "Support");
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
it("drops the sender label the message list shows", async () => {
|
|
274
|
-
const { threadMessages } = await harvest({ from: [spoof] });
|
|
275
|
-
|
|
276
|
-
assert.equal(threadMessages.length, 1);
|
|
277
|
-
assert.equal(threadMessages[0]?.fromName, undefined);
|
|
278
|
-
assert.equal(
|
|
279
|
-
threadMessages[0]?.fromEmail,
|
|
280
|
-
"aramirez@secresaludguaviare.gov.co",
|
|
281
|
-
);
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
it("keeps the sender label when it is an ordinary name", async () => {
|
|
285
|
-
const { threadMessages } = await harvest({
|
|
286
|
-
from: [
|
|
287
|
-
{ name: "Alejandro Ramirez", mailbox: "aramirez", host: "example.gov" },
|
|
288
|
-
],
|
|
289
|
-
});
|
|
290
|
-
|
|
291
|
-
assert.equal(threadMessages[0]?.fromName, "Alejandro Ramirez");
|
|
292
|
-
});
|
|
293
|
-
});
|