@remit/mailbox-service 0.0.45 → 0.0.46
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 +1 -1
- package/src/message-move-same-mailbox.test.ts +125 -0
- package/src/message-move.ts +14 -0
package/package.json
CHANGED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
|
+
import type {
|
|
4
|
+
IMailboxRepository,
|
|
5
|
+
IMailboxSpecialUseRepository,
|
|
6
|
+
IMessageRepository,
|
|
7
|
+
IThreadMessageRepository,
|
|
8
|
+
} from "@remit/data-ports";
|
|
9
|
+
import { type MessageMoveConfig, MessageMoveService } from "./message-move.js";
|
|
10
|
+
|
|
11
|
+
const ACCOUNT = "acc-1";
|
|
12
|
+
const ACCOUNT_CONFIG = "cfg-1";
|
|
13
|
+
const INBOX = "mbx-inbox";
|
|
14
|
+
const ARCHIVE = "mbx-archive";
|
|
15
|
+
const MESSAGE_ID = "msg-1";
|
|
16
|
+
|
|
17
|
+
const buildWorld = () => {
|
|
18
|
+
const message = {
|
|
19
|
+
messageId: MESSAGE_ID,
|
|
20
|
+
mailboxId: INBOX,
|
|
21
|
+
uid: 337,
|
|
22
|
+
status: "active",
|
|
23
|
+
syncStatus: "synced",
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const threadRow = {
|
|
27
|
+
accountConfigId: ACCOUNT_CONFIG,
|
|
28
|
+
threadMessageId: "tm-1",
|
|
29
|
+
messageId: MESSAGE_ID,
|
|
30
|
+
mailboxId: INBOX,
|
|
31
|
+
sentDate: 1_700_000_000_000,
|
|
32
|
+
isRead: false,
|
|
33
|
+
isDeleted: false,
|
|
34
|
+
hasStars: false,
|
|
35
|
+
hasAttachment: false,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const mailboxes = new Map<string, Record<string, unknown>>([
|
|
39
|
+
[INBOX, { mailboxId: INBOX, fullPath: "INBOX", accountId: ACCOUNT }],
|
|
40
|
+
[ARCHIVE, { mailboxId: ARCHIVE, fullPath: "Archive", accountId: ACCOUNT }],
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
const messageService = {
|
|
44
|
+
get: async () => message,
|
|
45
|
+
update: async (_id: string, patch: Record<string, unknown>) =>
|
|
46
|
+
Object.assign(message, patch),
|
|
47
|
+
updateForMove: async (_id: string, patch: Record<string, unknown>) =>
|
|
48
|
+
Object.assign(message, patch),
|
|
49
|
+
} as unknown as IMessageRepository;
|
|
50
|
+
|
|
51
|
+
const threadMessageService = {
|
|
52
|
+
getByMessageId: async () => threadRow,
|
|
53
|
+
update: async (_cfg: string, _id: string, patch: Record<string, unknown>) =>
|
|
54
|
+
Object.assign(threadRow, patch),
|
|
55
|
+
} as unknown as IThreadMessageRepository;
|
|
56
|
+
|
|
57
|
+
const mailboxService = {
|
|
58
|
+
get: async (_acc: string, id: string) => mailboxes.get(id),
|
|
59
|
+
} as unknown as IMailboxRepository;
|
|
60
|
+
|
|
61
|
+
const mailboxSpecialUseService = {
|
|
62
|
+
findTrashMailbox: async () => null,
|
|
63
|
+
} as unknown as IMailboxSpecialUseRepository;
|
|
64
|
+
|
|
65
|
+
const config: MessageMoveConfig = {
|
|
66
|
+
messageService,
|
|
67
|
+
mailboxService,
|
|
68
|
+
mailboxSpecialUseService,
|
|
69
|
+
threadMessageService,
|
|
70
|
+
sqsQueueUrl: "http://localhost:9324/000000000000/remit-messages.fifo",
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const service = new MessageMoveService(config);
|
|
74
|
+
const sent: unknown[] = [];
|
|
75
|
+
(
|
|
76
|
+
service as unknown as { sqs: { send: (c: unknown) => Promise<unknown> } }
|
|
77
|
+
).sqs = {
|
|
78
|
+
send: async (command: unknown) => {
|
|
79
|
+
sent.push(command);
|
|
80
|
+
return {};
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
return { service, message, threadRow, sent };
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// IMAP has no in-place MOVE: a same-mailbox move expunges the message and
|
|
88
|
+
// re-appends it under a new UID, so a request naming the folder the message is
|
|
89
|
+
// already in destroys its identity for nothing. It reached the mail server
|
|
90
|
+
// because the client can fire the same move twice — the second one after the
|
|
91
|
+
// first has already landed.
|
|
92
|
+
describe("MessageMoveService.moveMessage — same-mailbox move (#594)", () => {
|
|
93
|
+
it("enqueues nothing when the destination is the mailbox the message is in", async () => {
|
|
94
|
+
const { service, sent } = buildWorld();
|
|
95
|
+
|
|
96
|
+
await service.moveMessages(ACCOUNT_CONFIG, [MESSAGE_ID], INBOX, ACCOUNT);
|
|
97
|
+
|
|
98
|
+
assert.equal(sent.length, 0, "no IMAP move event was enqueued");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("leaves the message row exactly as it was", async () => {
|
|
102
|
+
const { service, message, threadRow } = buildWorld();
|
|
103
|
+
|
|
104
|
+
await service.moveMessages(ACCOUNT_CONFIG, [MESSAGE_ID], INBOX, ACCOUNT);
|
|
105
|
+
|
|
106
|
+
assert.deepEqual(message, {
|
|
107
|
+
messageId: MESSAGE_ID,
|
|
108
|
+
mailboxId: INBOX,
|
|
109
|
+
uid: 337,
|
|
110
|
+
status: "active",
|
|
111
|
+
syncStatus: "synced",
|
|
112
|
+
});
|
|
113
|
+
assert.equal(threadRow.mailboxId, INBOX);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("still moves a message whose destination is a different mailbox", async () => {
|
|
117
|
+
const { service, message, sent } = buildWorld();
|
|
118
|
+
|
|
119
|
+
await service.moveMessages(ACCOUNT_CONFIG, [MESSAGE_ID], ARCHIVE, ACCOUNT);
|
|
120
|
+
|
|
121
|
+
assert.equal(sent.length, 1, "the real move is enqueued");
|
|
122
|
+
assert.equal(message.mailboxId, ARCHIVE);
|
|
123
|
+
assert.equal(message.syncStatus, "pending");
|
|
124
|
+
});
|
|
125
|
+
});
|
package/src/message-move.ts
CHANGED
|
@@ -335,6 +335,20 @@ export class MessageMoveService {
|
|
|
335
335
|
accountId: string,
|
|
336
336
|
): Promise<void> => {
|
|
337
337
|
const message = await this.messageService.get(messageId);
|
|
338
|
+
|
|
339
|
+
// IMAP has no same-mailbox MOVE: the server copies the message, expunges
|
|
340
|
+
// the original and hands back a fresh UID, so a request that asks for the
|
|
341
|
+
// mailbox the message is already in destroys its identity to reach the
|
|
342
|
+
// state it was already in. The requested end state holds, so there is
|
|
343
|
+
// nothing to record as pending and nothing to enqueue.
|
|
344
|
+
if (message.mailboxId === destinationMailboxId) {
|
|
345
|
+
this.log.info(
|
|
346
|
+
{ messageId, mailboxId: destinationMailboxId },
|
|
347
|
+
"Skipping move: message is already in the destination mailbox",
|
|
348
|
+
);
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
|
|
338
352
|
const sourceMailbox = await this.mailboxService.get(
|
|
339
353
|
accountId,
|
|
340
354
|
message.mailboxId,
|