@remit/imap-worker 0.0.86 → 0.0.87
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/README.md +3 -1
- package/package.json +10 -2
- package/src/account-check.ts +1 -1
- package/src/body-sync-gate.test.ts +1 -1
- package/src/body-sync-gate.ts +1 -1
- package/src/e2e-processor-shim.ts +6 -21
- package/src/emit.ts +1 -1
- package/src/failure-kind.ts +1 -1
- package/src/handlers/append-sent-message.sqlite.test.ts +176 -0
- package/src/handlers/append-sent-message.ts +3 -24
- package/src/handlers/delete-account-objects.test.ts +1 -1
- package/src/handlers/delete-account-objects.ts +1 -1
- package/src/handlers/empty-trash.sqlite.test.ts +209 -0
- package/src/handlers/empty-trash.ts +3 -24
- package/src/handlers/flag-push.ts +11 -18
- package/src/handlers/mailbox-management.sqlite.test.ts +265 -0
- package/src/handlers/mailbox-management.ts +12 -48
- package/src/handlers/message-copy.sqlite.test.ts +298 -0
- package/src/handlers/message-copy.ts +31 -49
- package/src/handlers/message-delete-terminal.ts +12 -10
- package/src/handlers/message-delete.sqlite.test.ts +291 -0
- package/src/handlers/message-delete.ts +26 -32
- package/src/handlers/message-move-terminal.ts +12 -10
- package/src/handlers/message-move.sqlite.test.ts +271 -0
- package/src/handlers/message-move.test.ts +76 -863
- package/src/handlers/message-move.ts +49 -39
- package/src/handlers/placement-move-push.test.ts +96 -329
- package/src/handlers/placement-move-push.ts +75 -29
- package/src/handlers/placement-push.sqlite.test.ts +298 -0
- package/src/handlers/probe-message-id.sqlite.test.ts +197 -0
- package/src/handlers/probe-message-id.ts +15 -0
- package/src/handlers/sync-mailboxes.test.ts +0 -1
- package/src/handlers/sync-mailboxes.ts +1 -1
- package/src/handlers/sync-message-body.sqlite.test.ts +314 -0
- package/src/handlers/sync-message-body.test.ts +3 -299
- package/src/handlers/sync-message-body.ts +21 -11
- package/src/handlers/sync-messages.sqlite.test.ts +183 -0
- package/src/handlers/sync-messages.ts +27 -33
- package/src/index.ts +5 -3
- package/src/mail-sync-selection.sqlite.test.ts +16 -18
- package/src/poller.ts +5 -4
- package/src/processor.test.ts +1 -1
- package/src/processor.ts +1 -1
- package/src/scheduler/attachment-sweep.sqlite.test.ts +74 -0
- package/src/scheduler/attachment-sweep.ts +1 -1
- package/src/scheduler/{calendar-subscriptions.test.ts → calendar-subscriptions.sqlite.test.ts} +43 -55
- package/src/scheduler/calendar-subscriptions.ts +6 -6
- package/src/scheduler/half-deleted-accounts.sqlite.test.ts +203 -0
- package/src/scheduler/handler.ts +12 -1
- package/src/scheduler/loop.test.ts +2 -0
- package/src/scheduler/run-tick.sqlite.test.ts +192 -0
- package/src/scheduler/run-tick.ts +39 -1
- package/src/scheduler/runner.ts +21 -1
- package/src/scheduler/stranded-placement-redrive.ts +265 -0
- package/src/stranded-placement.sqlite.test.ts +795 -0
- package/src/test-helpers/test-world.ts +398 -0
- package/src/thread-join.sqlite.test.ts +115 -0
- package/src/with-oauth-lifecycle-deps.sqlite.test.ts +138 -14
- package/src/with-oauth-lifecycle.ts +1 -1
- package/src/handlers/append-sent-message.test.ts +0 -529
- package/src/handlers/empty-trash.test.ts +0 -728
- package/src/handlers/flag-push-placement-race.test.ts +0 -385
- package/src/handlers/flag-push.test.ts +0 -325
- package/src/handlers/mailbox-management.test.ts +0 -850
- package/src/handlers/message-copy.test.ts +0 -654
- package/src/handlers/message-delete.test.ts +0 -1869
- package/src/handlers/message-move-terminal.test.ts +0 -412
- package/src/handlers/sync-messages.test.ts +0 -569
- package/src/scheduler/attachment-sweep.test.ts +0 -169
- package/src/scheduler/run-tick.test.ts +0 -478
- package/src/with-oauth-lifecycle.test.ts +0 -318
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { after, before, describe, it } from "node:test";
|
|
3
|
+
import type { RemitClient } from "@remit/backend/client";
|
|
4
|
+
import {
|
|
5
|
+
MailboxCursorState,
|
|
6
|
+
MailboxSpecialUse,
|
|
7
|
+
MessageMutation,
|
|
8
|
+
MessageStatus,
|
|
9
|
+
MessageSyncStatus,
|
|
10
|
+
} from "@remit/domain-enums";
|
|
11
|
+
import type { MessageDeleteEvent } from "../events.js";
|
|
12
|
+
import { mail, openStore, type World } from "../test-helpers/test-world.js";
|
|
13
|
+
import { handleMessageDelete } from "./message-delete.js";
|
|
14
|
+
|
|
15
|
+
let store: Awaited<ReturnType<typeof openStore>>;
|
|
16
|
+
|
|
17
|
+
before(async () => {
|
|
18
|
+
store = await openStore();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
after(() => store.close());
|
|
22
|
+
|
|
23
|
+
interface Arranged {
|
|
24
|
+
w: World;
|
|
25
|
+
messageId: string;
|
|
26
|
+
uid: number;
|
|
27
|
+
messageIdHeader: string;
|
|
28
|
+
inboxId: string;
|
|
29
|
+
trashId: string;
|
|
30
|
+
event: MessageDeleteEvent;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* A message in INBOX, deleted through the service the API calls, so the row
|
|
35
|
+
* the handler finds and the event it gets are the ones a person's delete
|
|
36
|
+
* leaves behind.
|
|
37
|
+
*/
|
|
38
|
+
const arrange = async (
|
|
39
|
+
options: { permanent?: boolean; olderCopyInTrash?: boolean } = {},
|
|
40
|
+
): Promise<Arranged> => {
|
|
41
|
+
const w = await store.world();
|
|
42
|
+
const inbox = await w.folder("INBOX");
|
|
43
|
+
const trash = await w.folder("Trash", {
|
|
44
|
+
specialUse: MailboxSpecialUse.Trash,
|
|
45
|
+
});
|
|
46
|
+
const seeded = await w.message(inbox);
|
|
47
|
+
if (options.olderCopyInTrash)
|
|
48
|
+
w.imap.deliver("Trash", mail(seeded.messageIdHeader));
|
|
49
|
+
|
|
50
|
+
await w.client.messageMove.deleteMessage(
|
|
51
|
+
w.account.accountConfigId,
|
|
52
|
+
seeded.messageId,
|
|
53
|
+
w.account.accountId,
|
|
54
|
+
options.permanent ? { permanent: true } : { toTrash: true },
|
|
55
|
+
);
|
|
56
|
+
const event = w
|
|
57
|
+
.queued()
|
|
58
|
+
.find(
|
|
59
|
+
(body) => body.type === "MESSAGE_DELETE",
|
|
60
|
+
) as unknown as MessageDeleteEvent;
|
|
61
|
+
assert.ok(event, "the delete enqueued a MESSAGE_DELETE");
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
w,
|
|
65
|
+
messageId: seeded.messageId,
|
|
66
|
+
uid: seeded.uid,
|
|
67
|
+
messageIdHeader: seeded.messageIdHeader,
|
|
68
|
+
inboxId: inbox.mailboxId,
|
|
69
|
+
trashId: trash.mailboxId,
|
|
70
|
+
event,
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const run = (a: Arranged, receiveCount = 1, event = a.event) =>
|
|
75
|
+
handleMessageDelete(event, a.w.log, receiveCount, a.w.scope);
|
|
76
|
+
|
|
77
|
+
const resyncs = (w: World): unknown[] =>
|
|
78
|
+
w
|
|
79
|
+
.queued()
|
|
80
|
+
.filter((body) => body.type === "SYNC_MESSAGES")
|
|
81
|
+
.map((body) => body.mailboxId);
|
|
82
|
+
|
|
83
|
+
const client = (): RemitClient => store.client;
|
|
84
|
+
|
|
85
|
+
describe("a move to Trash", () => {
|
|
86
|
+
it("lands in Trash and settles the row on the uid the server gave it", async () => {
|
|
87
|
+
const a = await arrange();
|
|
88
|
+
|
|
89
|
+
await run(a);
|
|
90
|
+
|
|
91
|
+
assert.deepEqual(a.w.imap.uids("INBOX"), []);
|
|
92
|
+
const [trashUid] = a.w.imap.uids("Trash", a.messageIdHeader);
|
|
93
|
+
const row = await a.w.row(a.messageId);
|
|
94
|
+
assert.equal(row?.mailboxId, a.trashId);
|
|
95
|
+
assert.equal(row?.uid, trashUid);
|
|
96
|
+
assert.equal(row?.status, MessageStatus.active);
|
|
97
|
+
assert.deepEqual(await a.w.threadRows(a.messageId), [
|
|
98
|
+
{ mailboxId: a.trashId, uid: trashUid, isDeleted: true },
|
|
99
|
+
]);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Issue #1122. Without UIDPLUS the destination is asked by Message-ID, and
|
|
103
|
+
// Trash already holds an older copy: binding the row to it lets Empty Trash
|
|
104
|
+
// expunge the wrong one and leaves the fresh copy unreachable.
|
|
105
|
+
it("settles on the copy this move delivered when the server sends no COPYUID", async () => {
|
|
106
|
+
const a = await arrange({ olderCopyInTrash: true });
|
|
107
|
+
a.w.imap.uidplus = false;
|
|
108
|
+
|
|
109
|
+
await run(a);
|
|
110
|
+
|
|
111
|
+
const copies = a.w.imap.uids("Trash", a.messageIdHeader);
|
|
112
|
+
assert.equal(copies.length, 2);
|
|
113
|
+
assert.equal((await a.w.row(a.messageId))?.uid, Math.max(...copies));
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// One Message-ID can have several server copies. A source that still holds
|
|
117
|
+
// the uid proves the MOVE never ran, so the older copy in Trash is not this
|
|
118
|
+
// message and must never settle the row.
|
|
119
|
+
it("never settles on an older copy in Trash while the source still holds the message", async () => {
|
|
120
|
+
const a = await arrange({ olderCopyInTrash: true });
|
|
121
|
+
a.w.imap.uidplus = false;
|
|
122
|
+
a.w.imap.ignoreNext.add("MOVE");
|
|
123
|
+
|
|
124
|
+
await assert.rejects(run(a), /unconfirmed/);
|
|
125
|
+
|
|
126
|
+
const row = await a.w.row(a.messageId);
|
|
127
|
+
assert.equal(row?.uid, a.uid);
|
|
128
|
+
assert.equal(row?.status, MessageStatus.moving);
|
|
129
|
+
assert.equal(row?.syncStatus, MessageSyncStatus.failed);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("issues nothing when a redelivery finds the delete already settled", async () => {
|
|
133
|
+
const a = await arrange();
|
|
134
|
+
await run(a);
|
|
135
|
+
const settled = await a.w.row(a.messageId);
|
|
136
|
+
const commands = a.w.imap.log.length;
|
|
137
|
+
|
|
138
|
+
await run(a);
|
|
139
|
+
|
|
140
|
+
assert.equal(
|
|
141
|
+
a.w.imap.log.length,
|
|
142
|
+
commands,
|
|
143
|
+
"no IMAP command on redelivery",
|
|
144
|
+
);
|
|
145
|
+
assert.deepEqual(await a.w.row(a.messageId), settled);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("marks the row failed and redelivers when the connection drops mid-move", async () => {
|
|
149
|
+
const a = await arrange();
|
|
150
|
+
a.w.imap.failNext.MOVE = new Error("Connection reset");
|
|
151
|
+
|
|
152
|
+
await assert.rejects(run(a), /Connection reset/);
|
|
153
|
+
|
|
154
|
+
const row = await a.w.row(a.messageId);
|
|
155
|
+
assert.equal(row?.status, MessageStatus.moving);
|
|
156
|
+
assert.equal(row?.syncStatus, MessageSyncStatus.failed);
|
|
157
|
+
assert.deepEqual(a.w.imap.uids("INBOX"), [a.uid]);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// Issue #980. At the ceiling the server is asked where the message is, and
|
|
161
|
+
// a message that never left INBOX puts the row back there.
|
|
162
|
+
it("puts the row back on its source at the ceiling when the message never left", async () => {
|
|
163
|
+
const a = await arrange();
|
|
164
|
+
a.w.imap.failNext.MOVE = new Error("Connection reset");
|
|
165
|
+
|
|
166
|
+
await run(a, 3);
|
|
167
|
+
|
|
168
|
+
const row = await a.w.row(a.messageId);
|
|
169
|
+
assert.equal(row?.mailboxId, a.inboxId);
|
|
170
|
+
assert.equal(row?.uid, a.uid);
|
|
171
|
+
assert.notEqual(row?.status, MessageStatus.moving);
|
|
172
|
+
assert.deepEqual(resyncs(a.w), [a.inboxId, a.trashId]);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("hands the row back without creating a Trash the server does not have", async () => {
|
|
176
|
+
const a = await arrange();
|
|
177
|
+
a.w.imap.removeFolder("Trash");
|
|
178
|
+
|
|
179
|
+
await run(a);
|
|
180
|
+
|
|
181
|
+
assert.deepEqual(a.w.imap.commands("CREATE"), []);
|
|
182
|
+
const row = await a.w.row(a.messageId);
|
|
183
|
+
assert.equal(row?.mailboxId, a.inboxId);
|
|
184
|
+
assert.equal(row?.uid, a.uid);
|
|
185
|
+
assert.equal(row?.syncStatus, MessageSyncStatus.abandoned);
|
|
186
|
+
assert.equal(row?.abandonedMutation, MessageMutation.delete);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("hands the row back without connecting while the source cursor is rebuilding", async () => {
|
|
190
|
+
const a = await arrange();
|
|
191
|
+
await client().mailbox.update(a.w.account.accountId, a.inboxId, {
|
|
192
|
+
cursorState: MailboxCursorState.rebuilding,
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
await run(a);
|
|
196
|
+
|
|
197
|
+
assert.equal(a.w.imap.connections, 0);
|
|
198
|
+
const row = await a.w.row(a.messageId);
|
|
199
|
+
assert.equal(row?.mailboxId, a.inboxId);
|
|
200
|
+
assert.equal(row?.status, MessageStatus.active);
|
|
201
|
+
assert.equal(
|
|
202
|
+
a.w.logs.filter(
|
|
203
|
+
(line) => line.fields.metric === "message_delete_dropped_on_pause",
|
|
204
|
+
).length,
|
|
205
|
+
1,
|
|
206
|
+
);
|
|
207
|
+
assert.deepEqual(resyncs(a.w), [a.inboxId, a.trashId]);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// The folder was deleted and recreated on the server: uid 1 now names
|
|
211
|
+
// whatever the server put there. Nothing may be moved by it.
|
|
212
|
+
it("moves nothing when the source's UIDVALIDITY changed under it", async () => {
|
|
213
|
+
const a = await arrange();
|
|
214
|
+
a.w.imap.setUidValidity("INBOX", 2);
|
|
215
|
+
|
|
216
|
+
await run(a);
|
|
217
|
+
|
|
218
|
+
assert.deepEqual(a.w.imap.commands("MOVE"), []);
|
|
219
|
+
assert.equal((await a.w.row(a.messageId))?.mailboxId, a.inboxId);
|
|
220
|
+
const inbox = await client().mailbox.get(a.w.account.accountId, a.inboxId);
|
|
221
|
+
assert.notEqual(inbox.cursorState, MailboxCursorState.normal);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("moves from the folder's current path when it was renamed after enqueue", async () => {
|
|
225
|
+
const a = await arrange();
|
|
226
|
+
a.w.imap.renameFolder("INBOX", "Old/Inbox");
|
|
227
|
+
await client().mailbox.update(a.w.account.accountId, a.inboxId, {
|
|
228
|
+
fullPath: "Old/Inbox",
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
await run(a);
|
|
232
|
+
|
|
233
|
+
assert.deepEqual(a.w.imap.uids("Old/Inbox"), []);
|
|
234
|
+
assert.equal(a.w.imap.uids("Trash", a.messageIdHeader).length, 1);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it("acks without connecting when the source mailbox was deleted", async () => {
|
|
238
|
+
const a = await arrange();
|
|
239
|
+
await client().mailbox.delete(a.w.account.accountId, a.inboxId);
|
|
240
|
+
|
|
241
|
+
await run(a);
|
|
242
|
+
|
|
243
|
+
assert.equal(a.w.imap.connections, 0);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it("refuses an event minted under an unknown contract and hands the row back", async () => {
|
|
247
|
+
const a = await arrange();
|
|
248
|
+
|
|
249
|
+
await run(a, 1, {
|
|
250
|
+
...a.event,
|
|
251
|
+
schemaVersion: 99,
|
|
252
|
+
} as unknown as MessageDeleteEvent);
|
|
253
|
+
|
|
254
|
+
assert.equal(a.w.imap.connections, 0);
|
|
255
|
+
const row = await a.w.row(a.messageId);
|
|
256
|
+
assert.equal(row?.mailboxId, a.inboxId);
|
|
257
|
+
assert.equal(row?.syncStatus, MessageSyncStatus.abandoned);
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
describe("a permanent delete", () => {
|
|
262
|
+
it("expunges the message and removes every local row", async () => {
|
|
263
|
+
const a = await arrange({ permanent: true });
|
|
264
|
+
|
|
265
|
+
await run(a);
|
|
266
|
+
|
|
267
|
+
assert.deepEqual(a.w.imap.uids("INBOX"), []);
|
|
268
|
+
assert.equal(await a.w.row(a.messageId), undefined);
|
|
269
|
+
assert.deepEqual(await a.w.threadRows(a.messageId), []);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it("cleans up locally when another client already expunged it", async () => {
|
|
273
|
+
const a = await arrange({ permanent: true });
|
|
274
|
+
a.w.imap.empty("INBOX");
|
|
275
|
+
a.w.imap.failNext.EXPUNGE = new Error("Message not found (NONEXISTENT)");
|
|
276
|
+
|
|
277
|
+
await run(a);
|
|
278
|
+
|
|
279
|
+
assert.equal(await a.w.row(a.messageId), undefined);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it("keeps the row when the server still holds the message after a failed expunge", async () => {
|
|
283
|
+
const a = await arrange({ permanent: true });
|
|
284
|
+
a.w.imap.failNext.EXPUNGE = new Error("Message not found (NONEXISTENT)");
|
|
285
|
+
|
|
286
|
+
await assert.rejects(run(a), /NONEXISTENT/);
|
|
287
|
+
|
|
288
|
+
assert.equal((await a.w.row(a.messageId))?.status, MessageStatus.deleting);
|
|
289
|
+
assert.deepEqual(a.w.imap.uids("INBOX"), [a.uid]);
|
|
290
|
+
});
|
|
291
|
+
});
|
|
@@ -10,8 +10,8 @@ import {
|
|
|
10
10
|
MessageStatus,
|
|
11
11
|
MessageSyncStatus,
|
|
12
12
|
} from "@remit/domain-enums";
|
|
13
|
-
import type { Logger } from "@remit/logger
|
|
14
|
-
import { recordImapFailure } from "@remit/logger-lambda";
|
|
13
|
+
import type { Logger } from "@remit/logger";
|
|
14
|
+
import { recordImapFailure } from "@remit/logger-lambda/metrics";
|
|
15
15
|
import {
|
|
16
16
|
buildThreadMessageTrashUpdate,
|
|
17
17
|
guardConnectionCursor,
|
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
probePausedPlacement,
|
|
38
38
|
searchMailboxForHighestMessageIdUid,
|
|
39
39
|
} from "./message-move.js";
|
|
40
|
+
import { probeableMessageIdHeader } from "./probe-message-id.js";
|
|
40
41
|
|
|
41
42
|
interface FolderPaths {
|
|
42
43
|
source: string;
|
|
@@ -109,6 +110,8 @@ const confirmTrashMoveUid = async (
|
|
|
109
110
|
sourceConnection: IImapConnection,
|
|
110
111
|
destinationConnection: IImapConnection,
|
|
111
112
|
messageService: Pick<IMessageRepository, "get">,
|
|
113
|
+
threadMessageService: Pick<IThreadMessageRepository, "findByMessageId">,
|
|
114
|
+
accountConfigId: string,
|
|
112
115
|
messageId: string,
|
|
113
116
|
sourceMailboxPath: string,
|
|
114
117
|
destinationMailboxPath: string,
|
|
@@ -121,34 +124,23 @@ const confirmTrashMoveUid = async (
|
|
|
121
124
|
|
|
122
125
|
const [message] = await messageService.get([messageId]);
|
|
123
126
|
if (!message) return { outcome: "row-gone" };
|
|
124
|
-
|
|
127
|
+
const messageIdHeader = await probeableMessageIdHeader(
|
|
128
|
+
threadMessageService,
|
|
129
|
+
accountConfigId,
|
|
130
|
+
messageId,
|
|
131
|
+
);
|
|
132
|
+
if (!messageIdHeader) return { outcome: "unprobeable" };
|
|
125
133
|
|
|
126
134
|
const probedUid = await searchMailboxForHighestMessageIdUid(
|
|
127
135
|
destinationConnection,
|
|
128
136
|
destinationMailboxPath,
|
|
129
|
-
|
|
137
|
+
messageIdHeader,
|
|
130
138
|
);
|
|
131
139
|
return probedUid === null
|
|
132
140
|
? { outcome: "unconfirmed" }
|
|
133
141
|
: { outcome: "confirmed", uid: probedUid };
|
|
134
142
|
};
|
|
135
143
|
|
|
136
|
-
export interface MessageDeleteDeps {
|
|
137
|
-
getClient: typeof getClient;
|
|
138
|
-
buildLifecycleDeps: typeof buildLifecycleDeps;
|
|
139
|
-
withOAuthLifecycle: typeof withOAuthLifecycle;
|
|
140
|
-
createConnectionScope: typeof createConnectionScopeWithCredentials;
|
|
141
|
-
emitEvent: typeof emitEvent;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
const defaultDeps: MessageDeleteDeps = {
|
|
145
|
-
getClient,
|
|
146
|
-
buildLifecycleDeps,
|
|
147
|
-
withOAuthLifecycle,
|
|
148
|
-
createConnectionScope: createConnectionScopeWithCredentials,
|
|
149
|
-
emitEvent,
|
|
150
|
-
};
|
|
151
|
-
|
|
152
144
|
/**
|
|
153
145
|
* Handle MESSAGE_DELETE events.
|
|
154
146
|
* Either moves to Trash (IMAP MOVE) or permanently deletes (IMAP DELETE).
|
|
@@ -162,16 +154,8 @@ export const handleMessageDelete = async (
|
|
|
162
154
|
event: MessageDeleteEvent,
|
|
163
155
|
log: Logger,
|
|
164
156
|
receiveCount = 1,
|
|
165
|
-
|
|
157
|
+
createConnectionScope = createConnectionScopeWithCredentials,
|
|
166
158
|
): Promise<void> => {
|
|
167
|
-
const {
|
|
168
|
-
getClient,
|
|
169
|
-
buildLifecycleDeps,
|
|
170
|
-
withOAuthLifecycle,
|
|
171
|
-
createConnectionScope: createConnectionScopeWithCredentials,
|
|
172
|
-
emitEvent,
|
|
173
|
-
} = deps;
|
|
174
|
-
|
|
175
159
|
const {
|
|
176
160
|
account: accountService,
|
|
177
161
|
message: messageService,
|
|
@@ -446,8 +430,11 @@ export const handleMessageDelete = async (
|
|
|
446
430
|
|
|
447
431
|
const placement: PausedPlacement = isProbeableTrashMove
|
|
448
432
|
? await probePausedPlacement(await getRawConnection(), {
|
|
449
|
-
messageIdHeader:
|
|
450
|
-
|
|
433
|
+
messageIdHeader: await probeableMessageIdHeader(
|
|
434
|
+
threadMessageService,
|
|
435
|
+
account.accountConfigId,
|
|
436
|
+
messageId,
|
|
437
|
+
),
|
|
451
438
|
sourceMailboxPath: paths.source,
|
|
452
439
|
destinationMailboxPath,
|
|
453
440
|
})
|
|
@@ -498,6 +485,11 @@ export const handleMessageDelete = async (
|
|
|
498
485
|
const settled = await resolveExhaustedMessageDeleteFailure(
|
|
499
486
|
{ messageService, threadMessageService, log },
|
|
500
487
|
{
|
|
488
|
+
placement: {
|
|
489
|
+
status: message.status,
|
|
490
|
+
mailboxId: message.mailboxId,
|
|
491
|
+
uid: message.uid,
|
|
492
|
+
},
|
|
501
493
|
accountId,
|
|
502
494
|
accountConfigId,
|
|
503
495
|
messageId,
|
|
@@ -642,7 +634,7 @@ export const handleMessageDelete = async (
|
|
|
642
634
|
destination: destinationMailboxPath,
|
|
643
635
|
};
|
|
644
636
|
|
|
645
|
-
const scope =
|
|
637
|
+
const scope = createConnectionScope(account, credentials);
|
|
646
638
|
|
|
647
639
|
// The terminal resolver opens the source itself, and its answer now
|
|
648
640
|
// WRITES a placement, so it has to reach IMAP through the same
|
|
@@ -737,6 +729,8 @@ export const handleMessageDelete = async (
|
|
|
737
729
|
connection,
|
|
738
730
|
rawConnection,
|
|
739
731
|
messageService,
|
|
732
|
+
threadMessageService,
|
|
733
|
+
account.accountConfigId,
|
|
740
734
|
messageId,
|
|
741
735
|
mailboxPath,
|
|
742
736
|
destinationMailboxPath,
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
IMessageRepository,
|
|
3
3
|
IThreadMessageRepository,
|
|
4
|
+
MessageItem,
|
|
4
5
|
} from "@remit/data-ports";
|
|
5
6
|
import { MessageMutation, MessageSyncStatus } from "@remit/domain-enums";
|
|
6
7
|
import {
|
|
7
8
|
type IImapConnection,
|
|
8
9
|
isMessageGoneFromOpenMailbox,
|
|
9
|
-
reconcileStaleMessage,
|
|
10
10
|
restoreSourcePlacement,
|
|
11
|
-
type StaleMessageReconcileDeps,
|
|
12
11
|
} from "@remit/mailbox-service";
|
|
13
12
|
|
|
14
13
|
export interface MessageMoveTerminalLogger {
|
|
@@ -16,9 +15,11 @@ export interface MessageMoveTerminalLogger {
|
|
|
16
15
|
error(obj: Record<string, unknown>, msg: string): void;
|
|
17
16
|
}
|
|
18
17
|
|
|
19
|
-
export interface ResolveExhaustedMessageMoveDeps
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
export interface ResolveExhaustedMessageMoveDeps {
|
|
19
|
+
messageService: Pick<
|
|
20
|
+
IMessageRepository,
|
|
21
|
+
"deleteIfPlacement" | "transitionPlacement"
|
|
22
|
+
>;
|
|
22
23
|
threadMessageService: Pick<
|
|
23
24
|
IThreadMessageRepository,
|
|
24
25
|
"findAllByMessageId" | "deleteMany" | "update"
|
|
@@ -27,6 +28,8 @@ export interface ResolveExhaustedMessageMoveDeps
|
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
export interface ResolveExhaustedMessageMoveInput {
|
|
31
|
+
/** Where the row stood when the caller read it; the delete holds only while it still does. */
|
|
32
|
+
placement: Pick<MessageItem, "status" | "mailboxId" | "uid">;
|
|
30
33
|
accountId: string;
|
|
31
34
|
accountConfigId: string;
|
|
32
35
|
messageId: string;
|
|
@@ -58,7 +61,7 @@ export interface ResolveExhaustedMessageMoveResult {
|
|
|
58
61
|
* the connection dropped before the tagged OK was read, or a foreign client
|
|
59
62
|
* moved or expunged the message; from here those are indistinguishable and
|
|
60
63
|
* have the same answer. The stale Message/ThreadMessage rows are deleted
|
|
61
|
-
* via
|
|
64
|
+
* via `deleteIfPlacement` and the caller resyncs both folders, so
|
|
62
65
|
* whichever folder actually holds the message re-projects it with the
|
|
63
66
|
* server's own UID. Metric only, no alarm — routine.
|
|
64
67
|
* 2. BROKEN — the message is still at the source, so the move never took
|
|
@@ -110,10 +113,9 @@ export const resolveExhaustedMessageMoveFailure = async (
|
|
|
110
113
|
await connection.openBox(sourceMailboxPath, true);
|
|
111
114
|
|
|
112
115
|
if (await isMessageGoneFromOpenMailbox(connection, uid)) {
|
|
113
|
-
const
|
|
114
|
-
deps,
|
|
115
|
-
accountConfigId,
|
|
116
|
+
const reconciled = await deps.messageService.deleteIfPlacement(
|
|
116
117
|
messageId,
|
|
118
|
+
input.placement,
|
|
117
119
|
);
|
|
118
120
|
deps.log.info(
|
|
119
121
|
{
|
|
@@ -123,7 +125,7 @@ export const resolveExhaustedMessageMoveFailure = async (
|
|
|
123
125
|
messageId,
|
|
124
126
|
uid,
|
|
125
127
|
sourceMailboxPath,
|
|
126
|
-
|
|
128
|
+
reconciled,
|
|
127
129
|
},
|
|
128
130
|
"Message no longer at its move source after retry exhaustion (move landed server-side, or an external delete or move); stale row reconciled",
|
|
129
131
|
);
|