@remit/imap-worker 0.0.46 → 0.0.47

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.46",
3
+ "version": "0.0.47",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "exports": {
@@ -31,12 +31,7 @@ interface Harness {
31
31
  deletedAt?: number;
32
32
  };
33
33
  outbox: Record<string, unknown>;
34
- specialUseSent: { mailboxId: string; fullPath: string } | null;
35
- mailboxes: {
36
- mailboxId: string;
37
- fullPath: string;
38
- hierarchyDelimiter: string;
39
- }[];
34
+ sentMailbox: { mailboxId: string; fullPath: string } | null;
40
35
  append: (
41
36
  path: string,
42
37
  raw: Buffer,
@@ -72,15 +67,7 @@ const fresh = (): Harness => ({
72
67
  inReplyTo: "parent@example.com",
73
68
  sentAt: 1700000000000,
74
69
  },
75
- specialUseSent: { mailboxId: "sent-mbx", fullPath: "Sent" },
76
- mailboxes: [
77
- { mailboxId: "inbox-mbx", fullPath: "INBOX", hierarchyDelimiter: "/" },
78
- {
79
- mailboxId: "sent-items-mbx",
80
- fullPath: "Sent Items",
81
- hierarchyDelimiter: "/",
82
- },
83
- ],
70
+ sentMailbox: { mailboxId: "sent-mbx", fullPath: "INBOX/Sent" },
84
71
  append: async (path, raw, flags) => {
85
72
  h.calls.push({ method: "connection.append", args: [path, raw, flags] });
86
73
  return { uid: 55, uidValidity: 7 };
@@ -107,10 +94,7 @@ const deps = (): AppendSentMessageDeps =>
107
94
  discardAll: record("outboxAttachment.discardAll"),
108
95
  },
109
96
  mailboxSpecialUse: {
110
- findBySpecialUse: async () => h.specialUseSent,
111
- },
112
- mailbox: {
113
- listAllByAccount: async () => h.mailboxes,
97
+ findSentMailbox: async () => h.sentMailbox,
114
98
  },
115
99
  secrets: {},
116
100
  }),
@@ -150,7 +134,7 @@ describe("handleAppendSentMessage", () => {
150
134
  await handleAppendSentMessage(event, noopLog, 1, deps());
151
135
 
152
136
  const append = called("connection.append")[0];
153
- assert.equal(append?.args[0], "Sent");
137
+ assert.equal(append?.args[0], "INBOX/Sent");
154
138
  assert.deepEqual(append?.args[2], ["\\Seen"]);
155
139
  assert.deepEqual(called("outboxMessage.delete")[0]?.args, [
156
140
  "cfg-1",
@@ -194,60 +178,8 @@ describe("handleAppendSentMessage", () => {
194
178
  assert.match(raw, /^From: alice@example\.com$/m);
195
179
  });
196
180
 
197
- it("falls back to a conventionally-named Sent folder when no special-use flag is set", async () => {
198
- h.specialUseSent = null;
199
-
200
- await handleAppendSentMessage(event, noopLog, 1, deps());
201
-
202
- assert.equal(called("connection.append")[0]?.args[0], "Sent Items");
203
- });
204
-
205
- it("files into a Sent folder nested under INBOX when no special-use flag is set", async () => {
206
- h.specialUseSent = null;
207
- h.mailboxes = [
208
- { mailboxId: "inbox-mbx", fullPath: "INBOX", hierarchyDelimiter: "/" },
209
- {
210
- mailboxId: "sent-mbx",
211
- fullPath: "INBOX/Sent",
212
- hierarchyDelimiter: "/",
213
- },
214
- {
215
- mailboxId: "sent-messages-mbx",
216
- fullPath: "INBOX/Sent Messages",
217
- hierarchyDelimiter: "/",
218
- },
219
- ];
220
-
221
- await handleAppendSentMessage(event, noopLog, 1, deps());
222
-
223
- assert.equal(called("connection.append")[0]?.args[0], "INBOX/Sent");
224
- assert.deepEqual(called("outboxMessage.delete")[0]?.args, [
225
- "cfg-1",
226
- "out-1",
227
- ]);
228
- });
229
-
230
- it("files into a nested Sent folder under a non-INBOX prefix and a dot delimiter", async () => {
231
- h.specialUseSent = null;
232
- h.mailboxes = [
233
- { mailboxId: "inbox-mbx", fullPath: "INBOX", hierarchyDelimiter: "." },
234
- {
235
- mailboxId: "sent-mbx",
236
- fullPath: "Mail.Sent Items",
237
- hierarchyDelimiter: ".",
238
- },
239
- ];
240
-
241
- await handleAppendSentMessage(event, noopLog, 1, deps());
242
-
243
- assert.equal(called("connection.append")[0]?.args[0], "Mail.Sent Items");
244
- });
245
-
246
181
  it("settles the row as unfiled when the account has no Sent folder at all", async () => {
247
- h.specialUseSent = null;
248
- h.mailboxes = [
249
- { mailboxId: "inbox-mbx", fullPath: "INBOX", hierarchyDelimiter: "/" },
250
- ];
182
+ h.sentMailbox = null;
251
183
 
252
184
  await handleAppendSentMessage(event, noopLog, 1, deps());
253
185
 
@@ -261,7 +193,7 @@ describe("handleAppendSentMessage", () => {
261
193
  assert.deepEqual(update?.args.slice(0, 2), ["cfg-1", "out-1"]);
262
194
  const patch = update?.args[2] as { status: string; lastError: string };
263
195
  assert.equal(patch.status, "unfiled");
264
- assert.match(patch.lastError, /no Sent folder/);
196
+ assert.match(patch.lastError, /no folder appointed to the Sent role/);
265
197
  });
266
198
 
267
199
  it("skips the append while the outbox row is not yet sent", async () => {
@@ -1,9 +1,5 @@
1
1
  import { getClient } from "@remit/backend/client";
2
- import type {
3
- IMailboxRepository,
4
- IMailboxSpecialUseRepository,
5
- } from "@remit/data-ports";
6
- import { MailboxSpecialUse, OutboxMessageStatus } from "@remit/domain-enums";
2
+ import { OutboxMessageStatus } from "@remit/domain-enums";
7
3
  import type { Logger } from "@remit/logger-lambda";
8
4
  import {
9
5
  buildMailMessage,
@@ -12,29 +8,11 @@ import {
12
8
  import { isAccountDeleted } from "../account-check.js";
13
9
  import { createConnectionScopeWithCredentials } from "../connection-scope.js";
14
10
  import type { AppendSentMessageEvent } from "../events.js";
15
- import { resolveSentMailboxByName } from "../sent-mailbox.js";
16
11
  import { withOAuthLifecycle } from "../with-oauth-lifecycle.js";
17
12
  import { buildLifecycleDeps } from "../with-oauth-lifecycle-deps.js";
18
13
 
19
- const findSentMailbox = async (
20
- mailboxSpecialUseService: IMailboxSpecialUseRepository,
21
- mailboxService: IMailboxRepository,
22
- accountId: string,
23
- ): Promise<{ mailboxId: string; fullPath: string } | null> => {
24
- const bySpecialUse = await mailboxSpecialUseService.findBySpecialUse(
25
- accountId,
26
- MailboxSpecialUse.Sent,
27
- );
28
- if (bySpecialUse) {
29
- return bySpecialUse;
30
- }
31
-
32
- const mailboxes = await mailboxService.listAllByAccount(accountId);
33
- return resolveSentMailboxByName(mailboxes);
34
- };
35
-
36
14
  const UNFILED_NO_SENT_MAILBOX =
37
- "Sent, but not filed: this account has no Sent folder. Create one named Sent and later messages will be filed there.";
15
+ "Sent, but not filed: this account has no folder appointed to the Sent role and none that a Sent folder could be recognised by. Appoint one in the account's folder settings and later messages will be filed there.";
38
16
 
39
17
  const UNFILED_SIGNED_OUT =
40
18
  "Sent, but not filed: this account has to be signed in again before a copy can be stored in Sent.";
@@ -100,7 +78,6 @@ export const handleAppendSentMessage = async (
100
78
  outboxMessage: outboxMessageService,
101
79
  outboxAttachment: outboxAttachmentService,
102
80
  mailboxSpecialUse: mailboxSpecialUseService,
103
- mailbox: mailboxService,
104
81
  secrets,
105
82
  } = await getClient();
106
83
 
@@ -144,11 +121,7 @@ export const handleAppendSentMessage = async (
144
121
  );
145
122
  };
146
123
 
147
- const sentMailbox = await findSentMailbox(
148
- mailboxSpecialUseService,
149
- mailboxService,
150
- accountId,
151
- );
124
+ const sentMailbox = await mailboxSpecialUseService.findSentMailbox(accountId);
152
125
  if (!sentMailbox) {
153
126
  await settleUnfiled(UNFILED_NO_SENT_MAILBOX);
154
127
  return;
@@ -383,6 +383,7 @@ const syncMailboxMessages = async (
383
383
  const syncService = new MessageSyncService(
384
384
  connectionFactory,
385
385
  mailboxService,
386
+ mailboxSpecialUseService,
386
387
  messageService,
387
388
  envelopeService,
388
389
  addressService,
@@ -1,112 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { describe, it } from "node:test";
3
- import {
4
- resolveSentMailboxByName,
5
- type SentMailboxCandidate,
6
- } from "./sent-mailbox.js";
7
-
8
- const mailbox = (
9
- fullPath: string,
10
- hierarchyDelimiter = "/",
11
- ): SentMailboxCandidate => ({
12
- mailboxId: `mbx-${fullPath}`,
13
- fullPath,
14
- hierarchyDelimiter,
15
- });
16
-
17
- describe("resolveSentMailboxByName", () => {
18
- it("finds a top-level Sent folder", () => {
19
- const found = resolveSentMailboxByName([mailbox("INBOX"), mailbox("Sent")]);
20
-
21
- assert.equal(found?.fullPath, "Sent");
22
- });
23
-
24
- it("finds a Sent folder nested under INBOX", () => {
25
- const found = resolveSentMailboxByName([
26
- mailbox("INBOX"),
27
- mailbox("INBOX/Archive"),
28
- mailbox("INBOX/Sent"),
29
- ]);
30
-
31
- assert.equal(found?.fullPath, "INBOX/Sent");
32
- });
33
-
34
- it("finds a nested Sent folder under a non-INBOX prefix with a non-slash delimiter", () => {
35
- const found = resolveSentMailboxByName([
36
- mailbox("Mail.Drafts", "."),
37
- mailbox("Mail.Sent Items", "."),
38
- ]);
39
-
40
- assert.equal(found?.fullPath, "Mail.Sent Items");
41
- });
42
-
43
- it("finds the Gmail Sent folder by its leaf name", () => {
44
- const found = resolveSentMailboxByName([
45
- mailbox("INBOX"),
46
- mailbox("[Gmail]/All Mail"),
47
- mailbox("[Gmail]/Sent Mail"),
48
- ]);
49
-
50
- assert.equal(found?.fullPath, "[Gmail]/Sent Mail");
51
- });
52
-
53
- it("prefers the shallowest folder when several leaves match", () => {
54
- const found = resolveSentMailboxByName([
55
- mailbox("INBOX/Clients/Sent"),
56
- mailbox("INBOX/Sent"),
57
- ]);
58
-
59
- assert.equal(found?.fullPath, "INBOX/Sent");
60
- });
61
-
62
- it("prefers the plain Sent name over the longer variants", () => {
63
- const found = resolveSentMailboxByName([
64
- mailbox("INBOX/Sent Messages"),
65
- mailbox("INBOX/Sent"),
66
- ]);
67
-
68
- assert.equal(found?.fullPath, "INBOX/Sent");
69
- });
70
-
71
- it("matches the leaf regardless of case", () => {
72
- const found = resolveSentMailboxByName([mailbox("INBOX.sent items", ".")]);
73
-
74
- assert.equal(found?.fullPath, "INBOX.sent items");
75
- });
76
-
77
- it("does not match a folder that merely starts with a Sent name", () => {
78
- const found = resolveSentMailboxByName([
79
- mailbox("INBOX/Sent Archive 2024"),
80
- mailbox("INBOX/Sentinel"),
81
- ]);
82
-
83
- assert.equal(found, null);
84
- });
85
-
86
- it("prefers a shallower Sent over a deeper better-named one", () => {
87
- const found = resolveSentMailboxByName([
88
- mailbox("INBOX.Sent Items", "."),
89
- mailbox("INBOX.Trash.Sent", "."),
90
- ]);
91
-
92
- assert.equal(found?.fullPath, "INBOX.Sent Items");
93
- });
94
-
95
- it("resolves a flat namespace that reports no delimiter at all", () => {
96
- const found = resolveSentMailboxByName([
97
- mailbox("INBOX", ""),
98
- mailbox("Sent", ""),
99
- ]);
100
-
101
- assert.equal(found?.fullPath, "Sent");
102
- });
103
-
104
- it("returns null when the account has no Sent folder", () => {
105
- const found = resolveSentMailboxByName([
106
- mailbox("INBOX"),
107
- mailbox("INBOX/Trash"),
108
- ]);
109
-
110
- assert.equal(found, null);
111
- });
112
- });
@@ -1,20 +0,0 @@
1
- import type { MailboxItem } from "@remit/data-ports";
2
- import { resolveMailboxByLeafName } from "@remit/data-ports/mailbox-name";
3
-
4
- export type SentMailboxCandidate = Pick<
5
- MailboxItem,
6
- "mailboxId" | "fullPath" | "hierarchyDelimiter"
7
- >;
8
-
9
- const SENT_FOLDER_NAMES = ["sent", "sent items", "sent messages", "sent mail"];
10
-
11
- /**
12
- * The Sent folder by conventional name, for servers that advertise no `\Sent`
13
- * special-use. Matches the folder's own leaf segment, so it resolves at any
14
- * depth under any prefix (`INBOX/Sent`, `Mail.Sent Items`, `[Gmail]/Sent Mail`)
15
- * without knowing which prefixes a server uses.
16
- */
17
- export const resolveSentMailboxByName = (
18
- mailboxes: SentMailboxCandidate[],
19
- ): SentMailboxCandidate | null =>
20
- resolveMailboxByLeafName(mailboxes, SENT_FOLDER_NAMES);