@stamhoofd/backend 2.91.0 → 2.93.0

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.
Files changed (36) hide show
  1. package/package.json +10 -10
  2. package/src/audit-logs/EmailLogger.ts +6 -6
  3. package/src/crons/amazon-ses.ts +100 -4
  4. package/src/crons/balance-emails.ts +1 -1
  5. package/src/crons/endFunctionsOfUsersWithoutRegistration.ts +6 -0
  6. package/src/email-recipient-loaders/receivable-balances.ts +3 -1
  7. package/src/endpoints/global/email/CreateEmailEndpoint.ts +37 -7
  8. package/src/endpoints/global/email/GetAdminEmailsEndpoint.ts +205 -0
  9. package/src/endpoints/global/email/GetEmailEndpoint.ts +5 -1
  10. package/src/endpoints/global/email/PatchEmailEndpoint.test.ts +404 -8
  11. package/src/endpoints/global/email/PatchEmailEndpoint.ts +81 -26
  12. package/src/endpoints/global/email-recipients/GetEmailRecipientsCountEndpoint.ts +47 -0
  13. package/src/endpoints/global/email-recipients/GetEmailRecipientsEndpoint.test.ts +225 -0
  14. package/src/endpoints/global/email-recipients/GetEmailRecipientsEndpoint.ts +164 -0
  15. package/src/endpoints/global/email-recipients/helpers/validateEmailRecipientFilter.ts +64 -0
  16. package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +5 -1
  17. package/src/endpoints/global/registration-periods/GetRegistrationPeriodsEndpoint.ts +19 -1
  18. package/src/endpoints/organization/dashboard/webshops/DeleteWebshopEndpoint.ts +10 -1
  19. package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.ts +8 -1
  20. package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +2 -67
  21. package/src/helpers/AdminPermissionChecker.ts +81 -5
  22. package/src/helpers/EmailResumer.ts +2 -2
  23. package/src/seeds/1752848560-groups-registration-periods.ts +768 -0
  24. package/src/seeds/1755532883-update-email-sender-ids.ts +47 -0
  25. package/src/seeds/1755790070-fill-email-recipient-errors.ts +96 -0
  26. package/src/seeds/1755876819-remove-duplicate-members.ts +145 -0
  27. package/src/seeds/1756115432-remove-old-drafts.ts +16 -0
  28. package/src/seeds/1756115433-fill-email-recipient-organization-id.ts +30 -0
  29. package/src/services/uitpas/UitpasService.ts +71 -2
  30. package/src/services/uitpas/checkUitpasNumbers.ts +1 -0
  31. package/src/sql-filters/email-recipients.ts +59 -0
  32. package/src/sql-filters/emails.ts +95 -0
  33. package/src/sql-filters/members.ts +42 -1
  34. package/src/sql-filters/registration-periods.ts +5 -0
  35. package/src/sql-sorters/email-recipients.ts +69 -0
  36. package/src/sql-sorters/emails.ts +47 -0
@@ -1,9 +1,10 @@
1
1
  import { Request } from '@simonbackx/simple-endpoints';
2
2
  import { Email, Organization, OrganizationFactory, RegistrationPeriod, RegistrationPeriodFactory, Token, User, UserFactory } from '@stamhoofd/models';
3
- import { EmailStatus, Email as EmailStruct, PermissionLevel, Permissions, Version } from '@stamhoofd/structures';
4
- import { TestUtils } from '@stamhoofd/test-utils';
3
+ import { AccessRight, EmailStatus, Email as EmailStruct, OrganizationEmail, PermissionLevel, Permissions, PermissionsResourceType, ResourcePermissions, Version } from '@stamhoofd/structures';
4
+ import { STExpect, TestUtils } from '@stamhoofd/test-utils';
5
5
  import { testServer } from '../../../../tests/helpers/TestServer';
6
6
  import { PatchEmailEndpoint } from './PatchEmailEndpoint';
7
+ import { AutoEncoderPatchType } from '@simonbackx/simple-encoding';
7
8
 
8
9
  const baseUrl = `/v${Version}/email`;
9
10
 
@@ -13,8 +14,13 @@ describe('Endpoint.PatchEmailEndpoint', () => {
13
14
  let organization: Organization;
14
15
  let token: Token;
15
16
  let user: User;
17
+ let sender: OrganizationEmail;
18
+ let sender2: OrganizationEmail;
16
19
 
17
- const patchEmail = async (email: EmailStruct, token: Token, organization?: Organization) => {
20
+ let token2: Token;
21
+ let user2: User;
22
+
23
+ const patchEmail = async (email: AutoEncoderPatchType<EmailStruct>, token: Token, organization?: Organization) => {
18
24
  const id = email.id;
19
25
  const request = Request.buildJson('PATCH', `${baseUrl}/${id}`, organization?.getApiHost(), email);
20
26
  request.headers.authorization = 'Bearer ' + token.accessToken;
@@ -34,15 +40,360 @@ describe('Endpoint.PatchEmailEndpoint', () => {
34
40
  organization = await new OrganizationFactory({ period })
35
41
  .create();
36
42
 
43
+ sender = OrganizationEmail.create({
44
+ email: 'groepsleiding@voorbeeld.com',
45
+ name: 'Groepsleiding',
46
+ });
47
+ sender2 = OrganizationEmail.create({
48
+ email: 'kapoenen@voorbeeld.com',
49
+ name: 'Kapoenen',
50
+ });
51
+
52
+ organization.privateMeta.emails.push(sender);
53
+ organization.privateMeta.emails.push(sender2);
54
+ await organization.save();
55
+
37
56
  user = await new UserFactory({
38
57
  organization,
39
58
  permissions: Permissions.create({
40
- level: PermissionLevel.Read,
59
+ level: PermissionLevel.None,
60
+ resources: new Map([
61
+ [PermissionsResourceType.Senders, new Map([[sender.id, ResourcePermissions.create({
62
+ resourceName: sender.name!,
63
+ level: PermissionLevel.None,
64
+ accessRights: [AccessRight.SendMessages],
65
+ })]])],
66
+ ]),
41
67
  }),
42
68
  })
43
69
  .create();
44
70
 
45
71
  token = await Token.createToken(user);
72
+
73
+ user2 = await new UserFactory({
74
+ organization,
75
+ permissions: Permissions.create({
76
+ level: PermissionLevel.None,
77
+ resources: new Map([
78
+ [PermissionsResourceType.Senders, new Map([[sender2.id, ResourcePermissions.create({
79
+ resourceName: sender.name!,
80
+ level: PermissionLevel.Write,
81
+ accessRights: [AccessRight.SendMessages],
82
+ })]])],
83
+ ]),
84
+ }),
85
+ })
86
+ .create();
87
+
88
+ token2 = await Token.createToken(user2);
89
+ });
90
+
91
+ test('Should throw for invalid senderId', async () => {
92
+ const email = new Email();
93
+ email.subject = 'test subject';
94
+ email.status = EmailStatus.Draft;
95
+ email.text = 'test email {{unsubscribeUrl}}';
96
+ email.html = `<!DOCTYPE html>
97
+ <html>
98
+
99
+ <head>
100
+ <meta charset="utf-8" />
101
+ <meta http-equiv="X-UA-Compatible" content="IE=edge" />
102
+ <meta name="viewport" content="width=device-width,initial-scale=1.0" />
103
+ <title>test</title>
104
+ </head>
105
+
106
+ <body>
107
+ <p style="margin: 0; padding: 0; line-height: 1.4;">test email</p>
108
+
109
+ {{unsubscribeUrl}}
110
+ </body>
111
+
112
+ </html>`;
113
+ email.json = {
114
+ content: [
115
+ {
116
+ content: [
117
+ {
118
+ text: 'test email',
119
+ type: 'text',
120
+ },
121
+ ],
122
+ type: 'paragraph',
123
+ },
124
+ ],
125
+ type: 'doc',
126
+ };
127
+ email.userId = user.id;
128
+ email.organizationId = organization.id;
129
+
130
+ await email.save();
131
+
132
+ const body = EmailStruct.patch({
133
+ id: email.id,
134
+ status: EmailStatus.Sending,
135
+ senderId: 'invalid-sender-id',
136
+ });
137
+
138
+ await expect(async () => await patchEmail(body, token, organization))
139
+ .rejects
140
+ .toThrow(STExpect.errorWithCode('invalid_sender'));
141
+ });
142
+
143
+ test('Should throw when patching other users email without sender id', async () => {
144
+ const email = new Email();
145
+ email.subject = 'test subject';
146
+ email.status = EmailStatus.Draft;
147
+ email.text = 'test email {{unsubscribeUrl}}';
148
+ email.html = `<!DOCTYPE html>
149
+ <html>
150
+
151
+ <head>
152
+ <meta charset="utf-8" />
153
+ <meta http-equiv="X-UA-Compatible" content="IE=edge" />
154
+ <meta name="viewport" content="width=device-width,initial-scale=1.0" />
155
+ <title>test</title>
156
+ </head>
157
+
158
+ <body>
159
+ <p style="margin: 0; padding: 0; line-height: 1.4;">test email</p>
160
+
161
+ {{unsubscribeUrl}}
162
+ </body>
163
+
164
+ </html>`;
165
+ email.json = {
166
+ content: [
167
+ {
168
+ content: [
169
+ {
170
+ text: 'test email',
171
+ type: 'text',
172
+ },
173
+ ],
174
+ type: 'paragraph',
175
+ },
176
+ ],
177
+ type: 'doc',
178
+ };
179
+ email.userId = user2.id;
180
+ email.organizationId = organization.id;
181
+
182
+ await email.save();
183
+
184
+ const body = EmailStruct.patch({
185
+ id: email.id,
186
+ subject: 'new subject',
187
+ });
188
+
189
+ await expect(async () => await patchEmail(body, token, organization))
190
+ .rejects
191
+ .toThrow(STExpect.errorWithCode('permission_denied'));
192
+ });
193
+
194
+ test('Should throw when patching other users email even when matching sender', async () => {
195
+ const email = new Email();
196
+ email.subject = 'test subject';
197
+ email.status = EmailStatus.Draft;
198
+ email.text = 'test email {{unsubscribeUrl}}';
199
+ email.html = `<!DOCTYPE html>
200
+ <html>
201
+
202
+ <head>
203
+ <meta charset="utf-8" />
204
+ <meta http-equiv="X-UA-Compatible" content="IE=edge" />
205
+ <meta name="viewport" content="width=device-width,initial-scale=1.0" />
206
+ <title>test</title>
207
+ </head>
208
+
209
+ <body>
210
+ <p style="margin: 0; padding: 0; line-height: 1.4;">test email</p>
211
+
212
+ {{unsubscribeUrl}}
213
+ </body>
214
+
215
+ </html>`;
216
+ email.json = {
217
+ content: [
218
+ {
219
+ content: [
220
+ {
221
+ text: 'test email',
222
+ type: 'text',
223
+ },
224
+ ],
225
+ type: 'paragraph',
226
+ },
227
+ ],
228
+ type: 'doc',
229
+ };
230
+ email.userId = user2.id;
231
+ email.organizationId = organization.id;
232
+ email.senderId = sender.id;
233
+
234
+ await email.save();
235
+
236
+ const body = EmailStruct.patch({
237
+ id: email.id,
238
+ subject: 'new subject',
239
+ });
240
+
241
+ await expect(async () => await patchEmail(body, token, organization))
242
+ .rejects
243
+ .toThrow(STExpect.errorWithCode('permission_denied'));
244
+ });
245
+
246
+ test('Should not throw when patching other users email when having write access to sender', async () => {
247
+ const email = new Email();
248
+ email.subject = 'test subject';
249
+ email.status = EmailStatus.Draft;
250
+ email.text = 'test email {{unsubscribeUrl}}';
251
+ email.html = `<!DOCTYPE html>
252
+ <html>
253
+
254
+ <head>
255
+ <meta charset="utf-8" />
256
+ <meta http-equiv="X-UA-Compatible" content="IE=edge" />
257
+ <meta name="viewport" content="width=device-width,initial-scale=1.0" />
258
+ <title>test</title>
259
+ </head>
260
+
261
+ <body>
262
+ <p style="margin: 0; padding: 0; line-height: 1.4;">test email</p>
263
+
264
+ {{unsubscribeUrl}}
265
+ </body>
266
+
267
+ </html>`;
268
+ email.json = {
269
+ content: [
270
+ {
271
+ content: [
272
+ {
273
+ text: 'test email',
274
+ type: 'text',
275
+ },
276
+ ],
277
+ type: 'paragraph',
278
+ },
279
+ ],
280
+ type: 'doc',
281
+ };
282
+ email.userId = user.id; // other user
283
+ email.organizationId = organization.id;
284
+ email.senderId = sender2.id; // write access to this sender
285
+
286
+ await email.save();
287
+
288
+ const body = EmailStruct.patch({
289
+ id: email.id,
290
+ subject: 'new subject',
291
+ });
292
+
293
+ await expect(patchEmail(body, token2, organization)).toResolve();
294
+ });
295
+
296
+ test('Should throw when patching if no permission for sender', async () => {
297
+ const email = new Email();
298
+ email.subject = 'test subject';
299
+ email.status = EmailStatus.Draft;
300
+ email.text = 'test email {{unsubscribeUrl}}';
301
+ email.html = `<!DOCTYPE html>
302
+ <html>
303
+
304
+ <head>
305
+ <meta charset="utf-8" />
306
+ <meta http-equiv="X-UA-Compatible" content="IE=edge" />
307
+ <meta name="viewport" content="width=device-width,initial-scale=1.0" />
308
+ <title>test</title>
309
+ </head>
310
+
311
+ <body>
312
+ <p style="margin: 0; padding: 0; line-height: 1.4;">test email</p>
313
+
314
+ {{unsubscribeUrl}}
315
+ </body>
316
+
317
+ </html>`;
318
+ email.json = {
319
+ content: [
320
+ {
321
+ content: [
322
+ {
323
+ text: 'test email',
324
+ type: 'text',
325
+ },
326
+ ],
327
+ type: 'paragraph',
328
+ },
329
+ ],
330
+ type: 'doc',
331
+ };
332
+ email.userId = user.id;
333
+ email.organizationId = organization.id;
334
+
335
+ await email.save();
336
+
337
+ const body = EmailStruct.patch({
338
+ id: email.id,
339
+ senderId: sender2.id,
340
+ });
341
+
342
+ await expect(async () => await patchEmail(body, token, organization))
343
+ .rejects
344
+ .toThrow(STExpect.errorWithCode('permission_denied'));
345
+ });
346
+
347
+ test('Should throw when sending if no permission for sender', async () => {
348
+ const email = new Email();
349
+ email.subject = 'test subject';
350
+ email.status = EmailStatus.Draft;
351
+ email.text = 'test email {{unsubscribeUrl}}';
352
+ email.html = `<!DOCTYPE html>
353
+ <html>
354
+
355
+ <head>
356
+ <meta charset="utf-8" />
357
+ <meta http-equiv="X-UA-Compatible" content="IE=edge" />
358
+ <meta name="viewport" content="width=device-width,initial-scale=1.0" />
359
+ <title>test</title>
360
+ </head>
361
+
362
+ <body>
363
+ <p style="margin: 0; padding: 0; line-height: 1.4;">test email</p>
364
+
365
+ {{unsubscribeUrl}}
366
+ </body>
367
+
368
+ </html>`;
369
+ email.json = {
370
+ content: [
371
+ {
372
+ content: [
373
+ {
374
+ text: 'test email',
375
+ type: 'text',
376
+ },
377
+ ],
378
+ type: 'paragraph',
379
+ },
380
+ ],
381
+ type: 'doc',
382
+ };
383
+ email.userId = user.id;
384
+ email.organizationId = organization.id;
385
+ email.senderId = sender2.id;
386
+
387
+ await email.save();
388
+
389
+ const body = EmailStruct.patch({
390
+ id: email.id,
391
+ status: EmailStatus.Sending,
392
+ });
393
+
394
+ await expect(async () => await patchEmail(body, token, organization))
395
+ .rejects
396
+ .toThrow(STExpect.errorWithCode('permission_denied'));
46
397
  });
47
398
 
48
399
  test('Should throw error if no unsubscribe button in email html', async () => {
@@ -84,11 +435,11 @@ describe('Endpoint.PatchEmailEndpoint', () => {
84
435
 
85
436
  await email.save();
86
437
 
87
- const body = EmailStruct.create({ ...email, fromAddress: 'test@test.be', status: EmailStatus.Sending });
438
+ const body = EmailStruct.patch({ id: email.id, senderId: sender.id, status: EmailStatus.Sending });
88
439
 
89
440
  await expect(async () => await patchEmail(body, token, organization))
90
441
  .rejects
91
- .toThrow('Missing unsubscribe button');
442
+ .toThrow(STExpect.errorWithCode('missing_unsubscribe_button'));
92
443
  });
93
444
 
94
445
  test('Should throw error if no unsubscribe button in email text', async () => {
@@ -130,10 +481,55 @@ describe('Endpoint.PatchEmailEndpoint', () => {
130
481
 
131
482
  await email.save();
132
483
 
133
- const body = EmailStruct.create({ ...email, fromAddress: 'test@test.be', status: EmailStatus.Sending });
484
+ const body = EmailStruct.patch({ id: email.id, senderId: sender.id, status: EmailStatus.Sending });
134
485
 
135
486
  await expect(async () => await patchEmail(body, token, organization))
136
487
  .rejects
137
- .toThrow('Missing unsubscribe button');
488
+ .toThrow(STExpect.errorWithCode('missing_unsubscribe_button'));
489
+ });
490
+
491
+ test('Can send an email', async () => {
492
+ const email = new Email();
493
+ email.subject = 'test subject';
494
+ email.status = EmailStatus.Draft;
495
+ email.text = 'test email {{unsubscribeUrl}}';
496
+ email.html = `<!DOCTYPE html>
497
+ <html>
498
+
499
+ <head>
500
+ <meta charset="utf-8" />
501
+ <meta http-equiv="X-UA-Compatible" content="IE=edge" />
502
+ <meta name="viewport" content="width=device-width,initial-scale=1.0" />
503
+ <title>test</title>
504
+ </head>
505
+
506
+ <body>
507
+ <p style="margin: 0; padding: 0; line-height: 1.4;">test email {{unsubscribeUrl}}</p>
508
+ </body>
509
+
510
+ </html>`;
511
+ email.json = {
512
+ content: [
513
+ {
514
+ content: [
515
+ {
516
+ text: 'test email',
517
+ type: 'text',
518
+ },
519
+ ],
520
+ type: 'paragraph',
521
+ },
522
+ ],
523
+ type: 'doc',
524
+ };
525
+ email.userId = user.id;
526
+ email.organizationId = organization.id;
527
+ email.senderId = sender.id;
528
+
529
+ await email.save();
530
+
531
+ const body = EmailStruct.patch({ id: email.id, status: EmailStatus.Sending });
532
+
533
+ await expect(patchEmail(body, token, organization)).toResolve();
138
534
  });
139
535
  });
@@ -1,6 +1,6 @@
1
1
  import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
2
- import { Email } from '@stamhoofd/models';
3
- import { EmailPreview, EmailStatus, Email as EmailStruct } from '@stamhoofd/structures';
2
+ import { Email, Platform } from '@stamhoofd/models';
3
+ import { EmailPreview, EmailRecipientsStatus, EmailStatus, Email as EmailStruct, PermissionLevel } from '@stamhoofd/structures';
4
4
 
5
5
  import { AutoEncoderPatchType, Decoder, patchObject } from '@simonbackx/simple-encoding';
6
6
  import { SimpleError } from '@simonbackx/simple-errors';
@@ -29,14 +29,15 @@ export class PatchEmailEndpoint extends Endpoint<Params, Query, Body, ResponseBo
29
29
 
30
30
  async handle(request: DecodedRequest<Params, Query, Body>) {
31
31
  const organization = await Context.setOptionalOrganizationScope();
32
- const { user } = await Context.authenticate();
32
+ await Context.authenticate();
33
33
 
34
- if (!Context.auth.canSendEmails()) {
34
+ if (!await Context.auth.canReadEmails(organization)) {
35
+ // Fast fail before query
35
36
  throw Context.auth.error();
36
37
  }
37
38
 
38
39
  const model = await Email.getByID(request.params.id);
39
- if (!model || model.userId !== user.id || (model.organizationId !== (organization?.id ?? null))) {
40
+ if (!model || (model.organizationId !== (organization?.id ?? null))) {
40
41
  throw new SimpleError({
41
42
  code: 'not_found',
42
43
  human: 'Email not found',
@@ -45,6 +46,10 @@ export class PatchEmailEndpoint extends Endpoint<Params, Query, Body, ResponseBo
45
46
  });
46
47
  }
47
48
 
49
+ if (!await Context.auth.canAccessEmail(model, PermissionLevel.Write)) {
50
+ throw Context.auth.error();
51
+ }
52
+
48
53
  if (model.status !== EmailStatus.Draft) {
49
54
  throw new SimpleError({
50
55
  code: 'not_draft',
@@ -60,6 +65,47 @@ export class PatchEmailEndpoint extends Endpoint<Params, Query, Body, ResponseBo
60
65
  model.subject = request.body.subject;
61
66
  }
62
67
 
68
+ if (request.body.senderId !== undefined) {
69
+ const list = organization ? organization.privateMeta.emails : (await Platform.getShared()).privateConfig.emails;
70
+ const sender = list.find(e => e.id === request.body.senderId);
71
+ if (sender) {
72
+ if (!await Context.auth.canSendEmailsFrom(organization, sender.id)) {
73
+ throw Context.auth.error({
74
+ message: 'Cannot send emails from this sender',
75
+ human: $t('1b509614-30b0-484c-af72-57d4bc9ea788'),
76
+ });
77
+ }
78
+ model.senderId = sender.id;
79
+ model.fromAddress = sender.email;
80
+ model.fromName = sender.name;
81
+ }
82
+ else {
83
+ throw new SimpleError({
84
+ code: 'invalid_sender',
85
+ human: 'Sender not found',
86
+ message: $t(`94adb4e0-2ef1-4ee8-9f02-5a76efa51c1d`),
87
+ statusCode: 400,
88
+ });
89
+ }
90
+ }
91
+ else if (model.senderId) {
92
+ // Update data, to avoid sending from an old address
93
+ const list = organization ? organization.privateMeta.emails : (await Platform.getShared()).privateConfig.emails;
94
+ const sender = list.find(e => e.id === model.senderId);
95
+ if (sender) {
96
+ model.fromAddress = sender.email;
97
+ model.fromName = sender.name;
98
+ }
99
+ else {
100
+ throw new SimpleError({
101
+ code: 'invalid_sender',
102
+ human: 'Sender not found',
103
+ message: $t(`f08cccb3-faf9-473f-b729-16120fadec9c`),
104
+ statusCode: 400,
105
+ });
106
+ }
107
+ }
108
+
63
109
  if (request.body.html !== undefined) {
64
110
  model.html = request.body.html;
65
111
  }
@@ -72,15 +118,16 @@ export class PatchEmailEndpoint extends Endpoint<Params, Query, Body, ResponseBo
72
118
  model.json = request.body.json;
73
119
  }
74
120
 
75
- if (request.body.fromAddress !== undefined) {
76
- model.fromAddress = request.body.fromAddress;
77
- }
78
-
79
- if (request.body.fromName !== undefined) {
80
- model.fromName = request.body.fromName;
81
- }
82
-
83
121
  if (request.body.recipientFilter) {
122
+ if (model.status !== EmailStatus.Draft) {
123
+ throw new SimpleError({
124
+ code: 'not_draft',
125
+ human: 'Email is not a draft',
126
+ message: $t(`Je kan de ontvangerslijst alleen aanpassen als de e-mail nog een concept is`),
127
+ statusCode: 400,
128
+ });
129
+ }
130
+
84
131
  model.recipientFilter = patchObject(model.recipientFilter, request.body.recipientFilter);
85
132
  rebuild = true;
86
133
  }
@@ -95,26 +142,33 @@ export class PatchEmailEndpoint extends Endpoint<Params, Query, Body, ResponseBo
95
142
 
96
143
  if (rebuild) {
97
144
  await model.buildExampleRecipient();
98
- model.updateCount();
99
145
 
100
146
  // Force null - because we have stale data
101
- model.recipientCount = null;
147
+ model.emailRecipientsCount = null;
148
+ model.updateCount();
102
149
  }
103
150
 
104
- if (request.body.status === EmailStatus.Sending || request.body.status === EmailStatus.Sent) {
151
+ if (request.body.status === EmailStatus.Sending || request.body.status === EmailStatus.Sent || request.body.status === EmailStatus.Queued) {
152
+ if (!await Context.auth.canSendEmail(model)) {
153
+ throw Context.auth.error({
154
+ message: 'Cannot send emails from this sender',
155
+ human: $t('1b509614-30b0-484c-af72-57d4bc9ea788'),
156
+ });
157
+ }
158
+
105
159
  model.throwIfNotReadyToSend();
106
160
 
107
161
  const replacement = '{{unsubscribeUrl}}';
108
-
162
+
109
163
  if (model.html) {
110
164
  // Check email contains an unsubscribe button
111
165
  if (!model.html.includes(replacement)) {
112
166
  throw new SimpleError({
113
- code: "missing_unsubscribe_button",
114
- message: "Missing unsubscribe button",
167
+ code: 'missing_unsubscribe_button',
168
+ message: 'Missing unsubscribe button',
115
169
  human: $t(`dd55e04b-e5d9-4d9a-befc-443eef4175a8`),
116
- field: "html"
117
- })
170
+ field: 'html',
171
+ });
118
172
  }
119
173
  }
120
174
 
@@ -122,15 +176,16 @@ export class PatchEmailEndpoint extends Endpoint<Params, Query, Body, ResponseBo
122
176
  // Check email contains an unsubscribe button
123
177
  if (!model.text.includes(replacement)) {
124
178
  throw new SimpleError({
125
- code: "missing_unsubscribe_button",
126
- message: "Missing unsubscribe button",
179
+ code: 'missing_unsubscribe_button',
180
+ message: 'Missing unsubscribe button',
127
181
  human: $t(`dd55e04b-e5d9-4d9a-befc-443eef4175a8`),
128
- field: "text"
129
- })
182
+ field: 'text',
183
+ });
130
184
  }
131
185
  }
132
186
 
133
- model.send().catch(console.error);
187
+ // Preview the sending status
188
+ await model.queueForSending();
134
189
  }
135
190
 
136
191
  return new Response(await model.getPreviewStructure());
@@ -0,0 +1,47 @@
1
+ import { Decoder } from '@simonbackx/simple-encoding';
2
+ import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
3
+ import { CountFilteredRequest, CountResponse } from '@stamhoofd/structures';
4
+
5
+ import { Context } from '../../../helpers/Context';
6
+ import { GetEmailRecipientsEndpoint } from './GetEmailRecipientsEndpoint';
7
+
8
+ type Params = Record<string, never>;
9
+ type Query = CountFilteredRequest;
10
+ type Body = undefined;
11
+ type ResponseBody = CountResponse;
12
+
13
+ export class GetEmailRecipientsCountEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
14
+ queryDecoder = CountFilteredRequest as Decoder<CountFilteredRequest>;
15
+
16
+ protected doesMatch(request: Request): [true, Params] | [false] {
17
+ if (request.method !== 'GET') {
18
+ return [false];
19
+ }
20
+
21
+ const params = Endpoint.parseParameters(request.url, '/email-recipients/count', {});
22
+
23
+ if (params) {
24
+ return [true, params as Params];
25
+ }
26
+ return [false];
27
+ }
28
+
29
+ async handle(request: DecodedRequest<Params, Query, Body>) {
30
+ const organization = await Context.setOptionalOrganizationScope();
31
+ await Context.authenticate();
32
+
33
+ if (!await Context.auth.canReadEmails(organization)) {
34
+ throw Context.auth.error();
35
+ }
36
+ const query = await GetEmailRecipientsEndpoint.buildQuery(request.query);
37
+
38
+ const count = await query
39
+ .count();
40
+
41
+ return new Response(
42
+ CountResponse.create({
43
+ count,
44
+ }),
45
+ );
46
+ }
47
+ }