@remit/imap-worker 0.0.14 → 0.0.15
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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remit/imap-worker",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.15",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"bundle": "esbuild src/index.ts --sourcemap --bundle --platform=node --format=esm --outfile=dist/index.js",
|
|
21
21
|
"cli": "node --env-file=../../localhost-dev-aws.env src/cli.ts",
|
|
22
22
|
"test:typecheck": "tsgo --noEmit",
|
|
23
|
-
"test:run": "node --env-file=../../localhost-test-unit.env --experimental-test-coverage --test-coverage-include='src/**' --test-coverage-exclude='src/**/*.test.ts' --test-coverage-exclude='src/**/*.test.tsx' --test-coverage-lines=
|
|
23
|
+
"test:run": "node --env-file=../../localhost-test-unit.env --experimental-test-coverage --test-coverage-include='src/**' --test-coverage-exclude='src/**/*.test.ts' --test-coverage-exclude='src/**/*.test.tsx' --test-coverage-lines=70 --test 'src/**/*.test.ts'",
|
|
24
24
|
"test": "npm run test:typecheck && npm run test:run",
|
|
25
25
|
"dev": "node --import tsx src/e2e-processor-shim.ts"
|
|
26
26
|
},
|
|
@@ -1,81 +1,129 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
|
-
import { describe, it } from "node:test";
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
2
|
+
import { afterEach, describe, it } from "node:test";
|
|
3
|
+
import {
|
|
4
|
+
DeleteObjectsCommand,
|
|
5
|
+
ListObjectsV2Command,
|
|
6
|
+
S3Client,
|
|
7
|
+
} from "@aws-sdk/client-s3";
|
|
8
|
+
import { SendMessageCommand, SQSClient } from "@aws-sdk/client-sqs";
|
|
9
|
+
import type { Logger } from "@remit/logger-lambda";
|
|
10
|
+
import { mockClient } from "aws-sdk-client-mock";
|
|
11
|
+
import {
|
|
12
|
+
type DeleteAccountObjectsEvent,
|
|
13
|
+
handleDeleteAccountObjects,
|
|
14
|
+
} from "./delete-account-objects.js";
|
|
15
|
+
|
|
16
|
+
const noopLog = {
|
|
17
|
+
info: () => {},
|
|
18
|
+
warn: () => {},
|
|
19
|
+
error: () => {},
|
|
20
|
+
debug: () => {},
|
|
21
|
+
fatal: () => {},
|
|
22
|
+
trace: () => {},
|
|
23
|
+
child: () => noopLog,
|
|
24
|
+
} as unknown as Logger;
|
|
25
|
+
|
|
26
|
+
const s3Mock = mockClient(S3Client);
|
|
27
|
+
const sqsMock = mockClient(SQSClient);
|
|
28
|
+
|
|
29
|
+
const event: DeleteAccountObjectsEvent = {
|
|
30
|
+
type: "DELETE_ACCOUNT_OBJECTS",
|
|
31
|
+
accountConfigId: "cfg-1",
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
describe("handleDeleteAccountObjects", () => {
|
|
35
|
+
afterEach(() => {
|
|
36
|
+
s3Mock.reset();
|
|
37
|
+
sqsMock.reset();
|
|
15
38
|
});
|
|
16
39
|
|
|
17
|
-
it("
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
40
|
+
it("lists and deletes every object under the account prefix in one page", async () => {
|
|
41
|
+
s3Mock.on(ListObjectsV2Command).resolves({
|
|
42
|
+
Contents: [
|
|
43
|
+
{ Key: "accounts/cfg-1/a.eml" },
|
|
44
|
+
{ Key: "accounts/cfg-1/b.eml" },
|
|
45
|
+
],
|
|
46
|
+
IsTruncated: false,
|
|
47
|
+
});
|
|
48
|
+
s3Mock.on(DeleteObjectsCommand).resolves({});
|
|
49
|
+
|
|
50
|
+
await handleDeleteAccountObjects(event, noopLog);
|
|
51
|
+
|
|
52
|
+
const listCall = s3Mock.commandCalls(ListObjectsV2Command)[0];
|
|
53
|
+
assert.equal(listCall.args[0].input.Prefix, "accounts/cfg-1/");
|
|
54
|
+
|
|
55
|
+
const deleteCall = s3Mock.commandCalls(DeleteObjectsCommand)[0];
|
|
56
|
+
assert.deepEqual(deleteCall.args[0].input.Delete?.Objects, [
|
|
57
|
+
{ Key: "accounts/cfg-1/a.eml" },
|
|
58
|
+
{ Key: "accounts/cfg-1/b.eml" },
|
|
59
|
+
]);
|
|
60
|
+
assert.equal(sqsMock.commandCalls(SendMessageCommand).length, 0);
|
|
25
61
|
});
|
|
26
62
|
|
|
27
|
-
it("
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
63
|
+
it("follows the continuation token across truncated pages", async () => {
|
|
64
|
+
s3Mock
|
|
65
|
+
.on(ListObjectsV2Command)
|
|
66
|
+
.resolvesOnce({
|
|
67
|
+
Contents: [{ Key: "accounts/cfg-1/p1.eml" }],
|
|
68
|
+
IsTruncated: true,
|
|
69
|
+
NextContinuationToken: "tok-2",
|
|
70
|
+
})
|
|
71
|
+
.resolvesOnce({
|
|
72
|
+
Contents: [{ Key: "accounts/cfg-1/p2.eml" }],
|
|
73
|
+
IsTruncated: false,
|
|
74
|
+
});
|
|
75
|
+
s3Mock.on(DeleteObjectsCommand).resolves({});
|
|
76
|
+
|
|
77
|
+
await handleDeleteAccountObjects(event, noopLog);
|
|
78
|
+
|
|
79
|
+
const listCalls = s3Mock.commandCalls(ListObjectsV2Command);
|
|
80
|
+
assert.equal(listCalls.length, 2);
|
|
81
|
+
assert.equal(listCalls[1]?.args[0].input.ContinuationToken, "tok-2");
|
|
82
|
+
assert.equal(s3Mock.commandCalls(DeleteObjectsCommand).length, 2);
|
|
34
83
|
});
|
|
35
84
|
|
|
36
|
-
it("
|
|
37
|
-
|
|
38
|
-
const keys = Array.from({ length: 2500 }, (_, i) => `key-${i}`);
|
|
85
|
+
it("skips the delete call when a page holds no keys", async () => {
|
|
86
|
+
s3Mock.on(ListObjectsV2Command).resolves({ IsTruncated: false });
|
|
39
87
|
|
|
40
|
-
|
|
41
|
-
const batches: string[][] = [];
|
|
42
|
-
for (let i = 0; i < keys.length; i += BATCH_SIZE) {
|
|
43
|
-
batches.push(keys.slice(i, i + BATCH_SIZE));
|
|
44
|
-
}
|
|
88
|
+
await handleDeleteAccountObjects(event, noopLog);
|
|
45
89
|
|
|
46
|
-
assert.equal(
|
|
47
|
-
assert.equal(batches[0].length, 1000);
|
|
48
|
-
assert.equal(batches[1].length, 1000);
|
|
49
|
-
assert.equal(batches[2].length, 500);
|
|
90
|
+
assert.equal(s3Mock.commandCalls(DeleteObjectsCommand).length, 0);
|
|
50
91
|
});
|
|
51
92
|
|
|
52
|
-
it("re-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
93
|
+
it("re-enqueues with the continuation token when time is nearly up", async () => {
|
|
94
|
+
s3Mock.on(ListObjectsV2Command).resolves({ IsTruncated: false });
|
|
95
|
+
sqsMock.on(SendMessageCommand).resolves({});
|
|
96
|
+
|
|
97
|
+
await handleDeleteAccountObjects(
|
|
98
|
+
{ ...event, continuationToken: "tok-mid" },
|
|
99
|
+
noopLog,
|
|
100
|
+
() => 5_000,
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
assert.equal(
|
|
104
|
+
s3Mock.commandCalls(ListObjectsV2Command).length,
|
|
105
|
+
0,
|
|
106
|
+
"bails before starting a new page",
|
|
107
|
+
);
|
|
108
|
+
const send = sqsMock.commandCalls(SendMessageCommand)[0];
|
|
109
|
+
const body = JSON.parse(
|
|
110
|
+
send?.args[0].input.MessageBody ?? "{}",
|
|
111
|
+
) as DeleteAccountObjectsEvent;
|
|
112
|
+
assert.equal(body.type, "DELETE_ACCOUNT_OBJECTS");
|
|
113
|
+
assert.equal(body.accountConfigId, "cfg-1");
|
|
114
|
+
assert.equal(body.continuationToken, "tok-mid");
|
|
68
115
|
});
|
|
69
116
|
|
|
70
|
-
it("
|
|
71
|
-
|
|
117
|
+
it("keeps paging while time remains", async () => {
|
|
118
|
+
s3Mock.on(ListObjectsV2Command).resolves({
|
|
119
|
+
Contents: [{ Key: "accounts/cfg-1/x.eml" }],
|
|
120
|
+
IsTruncated: false,
|
|
121
|
+
});
|
|
122
|
+
s3Mock.on(DeleteObjectsCommand).resolves({});
|
|
72
123
|
|
|
73
|
-
|
|
74
|
-
const getRemainingTimeMs = () => 25_000;
|
|
75
|
-
assert.ok(getRemainingTimeMs() < MIN_REMAINING_MS);
|
|
124
|
+
await handleDeleteAccountObjects(event, noopLog, () => 120_000);
|
|
76
125
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
assert.ok(getRemainingTimeMsOk() >= MIN_REMAINING_MS);
|
|
126
|
+
assert.equal(s3Mock.commandCalls(ListObjectsV2Command).length, 1);
|
|
127
|
+
assert.equal(sqsMock.commandCalls(SendMessageCommand).length, 0);
|
|
80
128
|
});
|
|
81
129
|
});
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { beforeEach, describe, it } from "node:test";
|
|
3
|
+
import type { Logger } from "@remit/logger-lambda";
|
|
4
|
+
import type { MessageCopyEvent } from "../events.js";
|
|
5
|
+
import { handleMessageCopy, type MessageCopyDeps } from "./message-copy.js";
|
|
6
|
+
|
|
7
|
+
const noopLog = {
|
|
8
|
+
info: () => {},
|
|
9
|
+
warn: () => {},
|
|
10
|
+
error: () => {},
|
|
11
|
+
debug: () => {},
|
|
12
|
+
fatal: () => {},
|
|
13
|
+
trace: () => {},
|
|
14
|
+
child: () => noopLog,
|
|
15
|
+
} as unknown as Logger;
|
|
16
|
+
|
|
17
|
+
interface Call {
|
|
18
|
+
method: string;
|
|
19
|
+
args: unknown[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface Connection {
|
|
23
|
+
openBox: (
|
|
24
|
+
path: string,
|
|
25
|
+
readOnly?: boolean,
|
|
26
|
+
) => Promise<{ uidvalidity: number }>;
|
|
27
|
+
copyMessages: (
|
|
28
|
+
uids: number[],
|
|
29
|
+
dest: string,
|
|
30
|
+
) => Promise<{ uidMap: Map<number, number> }>;
|
|
31
|
+
createMailbox: (path: string) => Promise<void>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface Harness {
|
|
35
|
+
calls: Call[];
|
|
36
|
+
account: {
|
|
37
|
+
accountId: string;
|
|
38
|
+
accountConfigId: string;
|
|
39
|
+
deletedAt?: number;
|
|
40
|
+
} | null;
|
|
41
|
+
mailbox: { mailboxId: string; uidValidity: number; cursorState?: string };
|
|
42
|
+
connection: Connection;
|
|
43
|
+
getConnectionCount: number;
|
|
44
|
+
disconnectCount: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let h: Harness;
|
|
48
|
+
|
|
49
|
+
const record =
|
|
50
|
+
(method: string) =>
|
|
51
|
+
async (...args: unknown[]) => {
|
|
52
|
+
h.calls.push({ method, args });
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const buildConnection = (): Connection => ({
|
|
56
|
+
openBox: async () => ({ uidvalidity: 1 }),
|
|
57
|
+
copyMessages: async () => ({ uidMap: new Map([[10, 20]]) }),
|
|
58
|
+
createMailbox: record("createMailbox") as Connection["createMailbox"],
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const fresh = (): Harness => ({
|
|
62
|
+
calls: [],
|
|
63
|
+
account: { accountId: "acc-1", accountConfigId: "cfg-1" },
|
|
64
|
+
mailbox: { mailboxId: "src-mbx", uidValidity: 1, cursorState: undefined },
|
|
65
|
+
connection: buildConnection(),
|
|
66
|
+
getConnectionCount: 0,
|
|
67
|
+
disconnectCount: 0,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const deps = (): MessageCopyDeps =>
|
|
71
|
+
({
|
|
72
|
+
getClient: async () => ({
|
|
73
|
+
account: {
|
|
74
|
+
get: async (accountId: string) => {
|
|
75
|
+
h.calls.push({ method: "account.get", args: [accountId] });
|
|
76
|
+
return h.account;
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
message: {
|
|
80
|
+
updateUid: record("message.updateUid"),
|
|
81
|
+
update: record("message.update"),
|
|
82
|
+
},
|
|
83
|
+
threadMessage: {
|
|
84
|
+
findByMessageId: async (cfg: string) => ({
|
|
85
|
+
accountConfigId: cfg,
|
|
86
|
+
threadMessageId: "tm-1",
|
|
87
|
+
sentDate: 1,
|
|
88
|
+
mailboxId: "src-mbx",
|
|
89
|
+
isRead: false,
|
|
90
|
+
isDeleted: false,
|
|
91
|
+
hasStars: false,
|
|
92
|
+
hasAttachment: false,
|
|
93
|
+
}),
|
|
94
|
+
update: record("threadMessage.update"),
|
|
95
|
+
},
|
|
96
|
+
mailbox: {
|
|
97
|
+
get: async () => h.mailbox,
|
|
98
|
+
update: record("mailbox.update"),
|
|
99
|
+
},
|
|
100
|
+
secrets: {},
|
|
101
|
+
}),
|
|
102
|
+
buildLifecycleDeps: () => ({}),
|
|
103
|
+
withOAuthLifecycle: async (
|
|
104
|
+
_deps: unknown,
|
|
105
|
+
_account: unknown,
|
|
106
|
+
_log: unknown,
|
|
107
|
+
cb: (credentials: unknown) => Promise<void>,
|
|
108
|
+
) => cb({}),
|
|
109
|
+
createConnectionScope: () => ({
|
|
110
|
+
getConnection: async () => {
|
|
111
|
+
h.getConnectionCount += 1;
|
|
112
|
+
return h.connection;
|
|
113
|
+
},
|
|
114
|
+
disconnect: async () => {
|
|
115
|
+
h.disconnectCount += 1;
|
|
116
|
+
},
|
|
117
|
+
}),
|
|
118
|
+
}) as unknown as MessageCopyDeps;
|
|
119
|
+
|
|
120
|
+
const event: MessageCopyEvent = {
|
|
121
|
+
type: "MESSAGE_COPY",
|
|
122
|
+
accountId: "acc-1",
|
|
123
|
+
sourceMessageId: "src-msg",
|
|
124
|
+
newMessageId: "new-msg",
|
|
125
|
+
sourceMailboxId: "src-mbx",
|
|
126
|
+
sourceMailboxPath: "INBOX",
|
|
127
|
+
destinationMailboxPath: "Archive",
|
|
128
|
+
destinationMailboxId: "dst-mbx",
|
|
129
|
+
uid: 10,
|
|
130
|
+
} as MessageCopyEvent;
|
|
131
|
+
|
|
132
|
+
const called = (method: string): Call[] =>
|
|
133
|
+
h.calls.filter((c) => c.method === method);
|
|
134
|
+
|
|
135
|
+
describe("handleMessageCopy", () => {
|
|
136
|
+
beforeEach(() => {
|
|
137
|
+
h = fresh();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("writes the new UID, marks the copy synced, and updates the thread row", async () => {
|
|
141
|
+
await handleMessageCopy(event, noopLog, deps());
|
|
142
|
+
|
|
143
|
+
assert.deepEqual(called("message.updateUid")[0]?.args, [
|
|
144
|
+
"new-msg",
|
|
145
|
+
20,
|
|
146
|
+
"dst-mbx",
|
|
147
|
+
]);
|
|
148
|
+
const statusUpdate = called("message.update")[0];
|
|
149
|
+
assert.equal(
|
|
150
|
+
(statusUpdate?.args[1] as { syncStatus?: string })?.syncStatus,
|
|
151
|
+
"synced",
|
|
152
|
+
);
|
|
153
|
+
assert.equal(called("threadMessage.update").length, 1);
|
|
154
|
+
assert.equal(h.disconnectCount, 1, "the scope is always disconnected");
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("marks the copy failed when the COPYUID response omits the source uid", async () => {
|
|
158
|
+
h.connection.copyMessages = async () => ({ uidMap: new Map() });
|
|
159
|
+
|
|
160
|
+
await handleMessageCopy(event, noopLog, deps());
|
|
161
|
+
|
|
162
|
+
assert.equal(called("message.updateUid").length, 0);
|
|
163
|
+
const update = called("message.update")[0];
|
|
164
|
+
assert.equal(
|
|
165
|
+
(update?.args[1] as { syncStatus?: string })?.syncStatus,
|
|
166
|
+
"failed",
|
|
167
|
+
);
|
|
168
|
+
assert.equal(called("threadMessage.update").length, 0);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("returns early without connecting when the account is soft-deleted", async () => {
|
|
172
|
+
h.account = {
|
|
173
|
+
accountId: "acc-1",
|
|
174
|
+
accountConfigId: "cfg-1",
|
|
175
|
+
deletedAt: Date.now(),
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
await handleMessageCopy(event, noopLog, deps());
|
|
179
|
+
|
|
180
|
+
assert.equal(h.getConnectionCount, 0);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("throws when the account no longer exists", async () => {
|
|
184
|
+
h.account = null;
|
|
185
|
+
|
|
186
|
+
await assert.rejects(
|
|
187
|
+
handleMessageCopy(event, noopLog, deps()),
|
|
188
|
+
/not found/,
|
|
189
|
+
);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("skips the copy without opening a connection when the cursor is rebuilding", async () => {
|
|
193
|
+
h.mailbox = {
|
|
194
|
+
mailboxId: "src-mbx",
|
|
195
|
+
uidValidity: 1,
|
|
196
|
+
cursorState: "rebuilding",
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
await handleMessageCopy(event, noopLog, deps());
|
|
200
|
+
|
|
201
|
+
assert.equal(h.getConnectionCount, 0);
|
|
202
|
+
assert.equal(called("message.updateUid").length, 0);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("pauses quietly when openBox trips a UIDVALIDITY mismatch", async () => {
|
|
206
|
+
h.connection.openBox = async () => ({ uidvalidity: 999 });
|
|
207
|
+
|
|
208
|
+
await handleMessageCopy(event, noopLog, deps());
|
|
209
|
+
|
|
210
|
+
assert.equal(
|
|
211
|
+
(called("mailbox.update")[0]?.args[2] as { cursorState?: string })
|
|
212
|
+
?.cursorState,
|
|
213
|
+
"cursor_invalid",
|
|
214
|
+
"the mismatch trips the mailbox cursor",
|
|
215
|
+
);
|
|
216
|
+
assert.equal(called("message.updateUid").length, 0);
|
|
217
|
+
assert.equal(h.disconnectCount, 1);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("creates the destination and rethrows on a TRYCREATE error", async () => {
|
|
221
|
+
h.connection.copyMessages = async () => {
|
|
222
|
+
throw new Error("TRYCREATE: mailbox does not exist");
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
await assert.rejects(
|
|
226
|
+
handleMessageCopy(event, noopLog, deps()),
|
|
227
|
+
/TRYCREATE/,
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
assert.equal(called("createMailbox")[0]?.args[0], "Archive");
|
|
231
|
+
assert.equal(h.getConnectionCount, 2, "reconnects to create the mailbox");
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("marks the copy deleted-and-failed when the source is gone on the server", async () => {
|
|
235
|
+
h.connection.copyMessages = async () => {
|
|
236
|
+
throw new Error("NONEXISTENT source message");
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
await handleMessageCopy(event, noopLog, deps());
|
|
240
|
+
|
|
241
|
+
const update = called("message.update")[0];
|
|
242
|
+
assert.equal((update?.args[1] as { status?: string })?.status, "deleted");
|
|
243
|
+
assert.equal(called("createMailbox").length, 0);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it("marks failed and rethrows on an unclassified IMAP error", async () => {
|
|
247
|
+
h.connection.copyMessages = async () => {
|
|
248
|
+
throw new Error("server exploded");
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
await assert.rejects(
|
|
252
|
+
handleMessageCopy(event, noopLog, deps()),
|
|
253
|
+
/server exploded/,
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
const update = called("message.update")[0];
|
|
257
|
+
assert.equal(
|
|
258
|
+
(update?.args[1] as { syncStatus?: string })?.syncStatus,
|
|
259
|
+
"failed",
|
|
260
|
+
);
|
|
261
|
+
});
|
|
262
|
+
});
|
|
@@ -12,6 +12,20 @@ import type { MessageCopyEvent } from "../events.js";
|
|
|
12
12
|
import { withOAuthLifecycle } from "../with-oauth-lifecycle.js";
|
|
13
13
|
import { buildLifecycleDeps } from "../with-oauth-lifecycle-deps.js";
|
|
14
14
|
|
|
15
|
+
export interface MessageCopyDeps {
|
|
16
|
+
getClient: typeof getClient;
|
|
17
|
+
buildLifecycleDeps: typeof buildLifecycleDeps;
|
|
18
|
+
withOAuthLifecycle: typeof withOAuthLifecycle;
|
|
19
|
+
createConnectionScope: typeof createConnectionScopeWithCredentials;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const defaultDeps: MessageCopyDeps = {
|
|
23
|
+
getClient,
|
|
24
|
+
buildLifecycleDeps,
|
|
25
|
+
withOAuthLifecycle,
|
|
26
|
+
createConnectionScope: createConnectionScopeWithCredentials,
|
|
27
|
+
};
|
|
28
|
+
|
|
15
29
|
/**
|
|
16
30
|
* Handle MESSAGE_COPY events.
|
|
17
31
|
* Executes IMAP COPY command and updates local state with new UID.
|
|
@@ -19,7 +33,15 @@ import { buildLifecycleDeps } from "../with-oauth-lifecycle-deps.js";
|
|
|
19
33
|
export const handleMessageCopy = async (
|
|
20
34
|
event: MessageCopyEvent,
|
|
21
35
|
log: Logger,
|
|
36
|
+
deps: MessageCopyDeps = defaultDeps,
|
|
22
37
|
): Promise<void> => {
|
|
38
|
+
const {
|
|
39
|
+
getClient,
|
|
40
|
+
buildLifecycleDeps,
|
|
41
|
+
withOAuthLifecycle,
|
|
42
|
+
createConnectionScope: createConnectionScopeWithCredentials,
|
|
43
|
+
} = deps;
|
|
44
|
+
|
|
23
45
|
const {
|
|
24
46
|
account: accountService,
|
|
25
47
|
message: messageService,
|