@remit/mailbox-service 0.0.105 → 0.0.107

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/mailbox-service",
3
- "version": "0.0.105",
3
+ "version": "0.0.107",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -70,6 +70,14 @@ describe("loadOutboxAttachmentContents", () => {
70
70
  );
71
71
  });
72
72
 
73
+ it("gives each file the content-id its reservation handed out", async () => {
74
+ const deps = await setup([row("shot")], ["shot"]);
75
+
76
+ const [content] = await loadOutboxAttachmentContents(deps, OWNER, NOW);
77
+
78
+ assert.equal(content.cid, "shot@remit");
79
+ });
80
+
73
81
  it("leaves out a reservation that lapsed without its bytes", async () => {
74
82
  const deps = await setup(
75
83
  [
@@ -6,8 +6,12 @@ export interface OutboxAttachmentContent {
6
6
  filename: string;
7
7
  contentType: string;
8
8
  content: Buffer;
9
+ cid: string;
9
10
  }
10
11
 
12
+ export const outboxAttachmentContentId = (outboxAttachmentId: string): string =>
13
+ `${outboxAttachmentId}@remit`;
14
+
11
15
  /**
12
16
  * A file on a message that cannot be put into it. Carries the file's name so
13
17
  * whoever settles the message can tell the sender which file to fix.
@@ -93,6 +97,7 @@ export const loadOutboxAttachmentContents = async (
93
97
  filename: item.filename,
94
98
  contentType: item.contentType,
95
99
  content,
100
+ cid: outboxAttachmentContentId(item.outboxAttachmentId),
96
101
  };
97
102
  }),
98
103
  );
@@ -224,6 +224,23 @@ describe("reserving room on a draft", () => {
224
224
  assert.ok(row?.storageKey.endsWith(result.reservation.outboxAttachmentId));
225
225
  });
226
226
 
227
+ it("names the content-id a body can refer to the file by", async () => {
228
+ const { service } = build();
229
+
230
+ const result = await mint(service, {
231
+ filename: "shot.png",
232
+ contentType: "image/png",
233
+ sizeBytes: 64,
234
+ });
235
+
236
+ assert.equal(result.outcome, "Minted");
237
+ if (result.outcome !== "Minted") return;
238
+ assert.equal(
239
+ result.reservation.contentId,
240
+ `${result.reservation.outboxAttachmentId}@remit`,
241
+ );
242
+ });
243
+
227
244
  it("refuses a declared size over the cap", async () => {
228
245
  const { service } = build();
229
246
 
@@ -16,6 +16,7 @@ import {
16
16
  import {
17
17
  loadOutboxAttachmentContents,
18
18
  type OutboxAttachmentContent,
19
+ outboxAttachmentContentId,
19
20
  } from "./outbox-attachment-content.js";
20
21
  import {
21
22
  normalizeAttachmentContentType,
@@ -45,6 +46,7 @@ export interface OutboxAttachmentReservation {
45
46
  filename: string;
46
47
  contentType: string;
47
48
  sizeBytes: number;
49
+ contentId: string;
48
50
  uploadUrl: string;
49
51
  uploadExpiresAt: number;
50
52
  }
@@ -271,6 +273,7 @@ export class OutboxAttachmentService {
271
273
  filename: reserved.item.filename,
272
274
  contentType: reserved.item.contentType,
273
275
  sizeBytes: reserved.item.sizeBytes,
276
+ contentId: outboxAttachmentContentId(reserved.item.outboxAttachmentId),
274
277
  uploadUrl: target.uploadUrl,
275
278
  uploadExpiresAt: reservationExpiresAt,
276
279
  },
@@ -125,7 +125,9 @@ const createHarness = (
125
125
  outboxAttachmentService: {
126
126
  unfinishedUpload: async () => undefined,
127
127
  } as unknown as OutboxAttachmentService,
128
- accountService: {} as unknown as IAccountRepository,
128
+ accountService: {
129
+ get: async () => ({ syncedServices: ["Mail"] }),
130
+ } as unknown as IAccountRepository,
129
131
  sqsSmtpQueueUrl: "http://localhost/queue",
130
132
  sqsClient: {
131
133
  send: async (command: { input: { MessageBody: string } }) => {
@@ -89,7 +89,9 @@ const createHarness = (stored: OutboxMessageItem): Harness => {
89
89
  },
90
90
  unfinishedUpload: async () => undefined,
91
91
  } as unknown as OutboxAttachmentService,
92
- accountService: {} as unknown as IAccountRepository,
92
+ accountService: {
93
+ get: async () => ({ syncedServices: ["Mail"] }),
94
+ } as unknown as IAccountRepository,
93
95
  sqsSmtpQueueUrl: "http://localhost/queue",
94
96
  sqsClient: {
95
97
  send: async (command: { input: { MessageBody: string } }) => {
@@ -18,7 +18,7 @@ import type {
18
18
  OutboxAttachmentItem,
19
19
  OutboxMessageItem,
20
20
  } from "@remit/data-ports";
21
- import { BadRequestError } from "@remit/data-ports/errors";
21
+ import { BadRequestError, MailSyncOffError } from "@remit/data-ports/errors";
22
22
  import { OutboxMessageStatus } from "@remit/domain-enums";
23
23
  import type { OutboxAttachmentService } from "./outbox-attachment.js";
24
24
  import { OutboxQueueService } from "./outbox-queue.js";
@@ -55,6 +55,7 @@ const createHarness = (
55
55
  stored: OutboxMessageItem,
56
56
  attachments: OutboxAttachmentItem[] = [],
57
57
  lateAttachments: OutboxAttachmentItem[] = [],
58
+ syncedServices: string[] = ["Mail"],
58
59
  ): Harness => {
59
60
  let unfinishedChecks = 0;
60
61
  const harness: Harness = {
@@ -98,7 +99,9 @@ const createHarness = (
98
99
  return held.find((item) => item.state !== "Stored");
99
100
  },
100
101
  } as unknown as OutboxAttachmentService,
101
- accountService: {} as unknown as IAccountRepository,
102
+ accountService: {
103
+ get: async () => ({ syncedServices }),
104
+ } as unknown as IAccountRepository,
102
105
  sqsSmtpQueueUrl: "http://localhost/queue",
103
106
  sqsClient: {
104
107
  send: async (command: { input: { MessageBody: string } }) => {
@@ -241,3 +244,48 @@ describe("OutboxQueueService and a message with nowhere to go", () => {
241
244
  assert.equal(harness.enqueued.length, 1);
242
245
  });
243
246
  });
247
+
248
+ describe("OutboxQueueService and an account with mail off", () => {
249
+ const refusesWithTheFix = (error: unknown): boolean => {
250
+ assert.ok(error instanceof MailSyncOffError);
251
+ assert.equal(error.statusCode, 400);
252
+ assert.equal(error.publicApiError?.code, "mail_sync_off");
253
+ assert.match(error.message, /Turn Mail back on .* in Settings/);
254
+ return true;
255
+ };
256
+
257
+ it("refuses to queue a stored draft and leaves it a draft", async () => {
258
+ const harness = createHarness(draft({}), [], [], ["Calendar"]);
259
+
260
+ await assert.rejects(
261
+ () => harness.service.send(ACCOUNT_CONFIG_ID, OUTBOX_MESSAGE_ID),
262
+ refusesWithTheFix,
263
+ );
264
+
265
+ assert.deepEqual(harness.statusWrites, []);
266
+ assert.deepEqual(harness.enqueued, []);
267
+ });
268
+
269
+ it("refuses a send-immediately create before writing a row", async () => {
270
+ const harness = createHarness(draft({}), [], [], ["Calendar"]);
271
+
272
+ await assert.rejects(
273
+ () =>
274
+ harness.service.createAndSend(
275
+ sendInput({ toAddresses: ["them@example.com"] }),
276
+ ),
277
+ refusesWithTheFix,
278
+ );
279
+
280
+ assert.equal(harness.created, 0);
281
+ assert.deepEqual(harness.enqueued, []);
282
+ });
283
+
284
+ it("sends from an account syncing mail and calendar", async () => {
285
+ const harness = createHarness(draft({}), [], [], ["Mail", "Calendar"]);
286
+
287
+ await harness.service.send(ACCOUNT_CONFIG_ID, OUTBOX_MESSAGE_ID);
288
+
289
+ assert.equal(harness.enqueued.length, 1);
290
+ });
291
+ });
@@ -5,8 +5,12 @@ import type {
5
5
  IOutboxMessageRepository,
6
6
  OutboxMessageItem,
7
7
  } from "@remit/data-ports";
8
- import { BadRequestError, ConflictError } from "@remit/data-ports/errors";
9
- import { OutboxMessageStatus } from "@remit/domain-enums";
8
+ import {
9
+ BadRequestError,
10
+ ConflictError,
11
+ MailSyncOffError,
12
+ } from "@remit/data-ports/errors";
13
+ import { AccountService, OutboxMessageStatus } from "@remit/domain-enums";
10
14
  import { createQueueProducer } from "@remit/sqs-client/producer";
11
15
  import type { OutboxAttachmentService } from "./outbox-attachment.js";
12
16
  import { isOpenForWork } from "./outbox-status.js";
@@ -88,6 +92,9 @@ const hasNowhereToGo = (message: {
88
92
  const NO_RECIPIENT_MESSAGE =
89
93
  "This message has nobody to send to. Add a recipient before sending it.";
90
94
 
95
+ const MAIL_SYNC_OFF_SEND_MESSAGE =
96
+ "Mail is turned off for this account, so it cannot send. Turn Mail back on for this account in Settings to send from it.";
97
+
91
98
  const ENQUEUE_FAILED_MESSAGE =
92
99
  "This message could not be handed to the outgoing queue, so it was not sent. Send it again.";
93
100
 
@@ -250,6 +257,12 @@ export class OutboxQueueService {
250
257
  return updated;
251
258
  };
252
259
 
260
+ private refuseWithoutMail = async (accountId: string): Promise<void> => {
261
+ const account = await this.accountService.get(accountId);
262
+ if (account.syncedServices.includes(AccountService.Mail)) return;
263
+ throw new MailSyncOffError(MAIL_SYNC_OFF_SEND_MESSAGE, accountId);
264
+ };
265
+
253
266
  send = async (
254
267
  accountConfigId: string,
255
268
  outboxMessageId: string,
@@ -268,6 +281,7 @@ export class OutboxQueueService {
268
281
  if (hasNowhereToGo(existing)) {
269
282
  throw new BadRequestError(NO_RECIPIENT_MESSAGE);
270
283
  }
284
+ await this.refuseWithoutMail(existing.accountId);
271
285
 
272
286
  const unfinished = await this.outboxAttachmentService.unfinishedUpload(
273
287
  accountConfigId,
@@ -339,6 +353,7 @@ export class OutboxQueueService {
339
353
  if (hasNowhereToGo(input)) {
340
354
  throw new BadRequestError(NO_RECIPIENT_MESSAGE);
341
355
  }
356
+ await this.refuseWithoutMail(input.accountId);
342
357
 
343
358
  const domain = extractDomain(input.fromAddress);
344
359
  const messageIdValue = generateMessageId(domain);