@stamhoofd/backend 2.92.0 → 2.94.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 (32) hide show
  1. package/package.json +10 -10
  2. package/src/audit-logs/EmailLogger.ts +2 -2
  3. package/src/crons/amazon-ses.ts +100 -4
  4. package/src/crons/balance-emails.ts +1 -1
  5. package/src/email-recipient-loaders/receivable-balances.ts +3 -1
  6. package/src/endpoints/global/email/CreateEmailEndpoint.ts +26 -2
  7. package/src/endpoints/global/email/GetAdminEmailsEndpoint.ts +2 -3
  8. package/src/endpoints/global/email/PatchEmailEndpoint.ts +27 -14
  9. package/src/endpoints/global/email-recipients/GetEmailRecipientsCountEndpoint.ts +47 -0
  10. package/src/endpoints/global/email-recipients/GetEmailRecipientsEndpoint.test.ts +225 -0
  11. package/src/endpoints/global/email-recipients/GetEmailRecipientsEndpoint.ts +164 -0
  12. package/src/endpoints/global/email-recipients/helpers/validateEmailRecipientFilter.ts +64 -0
  13. package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +5 -1
  14. package/src/endpoints/global/registration-periods/GetRegistrationPeriodsEndpoint.ts +19 -1
  15. package/src/endpoints/global/registration-periods/PatchRegistrationPeriodsEndpoint.ts +4 -15
  16. package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +2 -0
  17. package/src/helpers/AdminPermissionChecker.ts +4 -0
  18. package/src/helpers/AuthenticatedStructures.ts +1 -1
  19. package/src/helpers/EmailResumer.ts +2 -2
  20. package/src/seeds/1755790070-fill-email-recipient-errors.ts +96 -0
  21. package/src/seeds/1756115432-remove-old-drafts.ts +16 -0
  22. package/src/seeds/1756115433-fill-email-recipient-organization-id.ts +30 -0
  23. package/src/seeds/1756293699-fill-previous-next-period-id.ts +34 -0
  24. package/src/seeds/1756303697-update-email-counts.ts +76 -0
  25. package/src/sql-filters/email-recipients.ts +59 -0
  26. package/src/sql-filters/emails.ts +37 -2
  27. package/src/sql-filters/events.ts +10 -0
  28. package/src/sql-filters/members.ts +42 -1
  29. package/src/sql-filters/registration-periods.ts +5 -0
  30. package/src/sql-sorters/email-recipients.ts +69 -0
  31. package/src/seeds/1734596144-fill-previous-period-id.ts +0 -55
  32. /package/src/seeds/{1755181288-remove-duplicate-members.ts → 1755876819-remove-duplicate-members.ts} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stamhoofd/backend",
3
- "version": "2.92.0",
3
+ "version": "2.94.0",
4
4
  "main": "./dist/index.js",
5
5
  "exports": {
6
6
  ".": {
@@ -45,14 +45,14 @@
45
45
  "@simonbackx/simple-encoding": "2.22.0",
46
46
  "@simonbackx/simple-endpoints": "1.20.1",
47
47
  "@simonbackx/simple-logging": "^1.0.1",
48
- "@stamhoofd/backend-i18n": "2.92.0",
49
- "@stamhoofd/backend-middleware": "2.92.0",
50
- "@stamhoofd/email": "2.92.0",
51
- "@stamhoofd/models": "2.92.0",
52
- "@stamhoofd/queues": "2.92.0",
53
- "@stamhoofd/sql": "2.92.0",
54
- "@stamhoofd/structures": "2.92.0",
55
- "@stamhoofd/utility": "2.92.0",
48
+ "@stamhoofd/backend-i18n": "2.94.0",
49
+ "@stamhoofd/backend-middleware": "2.94.0",
50
+ "@stamhoofd/email": "2.94.0",
51
+ "@stamhoofd/models": "2.94.0",
52
+ "@stamhoofd/queues": "2.94.0",
53
+ "@stamhoofd/sql": "2.94.0",
54
+ "@stamhoofd/structures": "2.94.0",
55
+ "@stamhoofd/utility": "2.94.0",
56
56
  "archiver": "^7.0.1",
57
57
  "axios": "^1.8.2",
58
58
  "cookie": "^0.7.0",
@@ -70,5 +70,5 @@
70
70
  "publishConfig": {
71
71
  "access": "public"
72
72
  },
73
- "gitHead": "43d1edfd8061dada1d418a02691fe5fb158aca6a"
73
+ "gitHead": "7a1a0be03249d69ad0664ee85d446c7b596768f0"
74
74
  }
@@ -58,8 +58,8 @@ export const EmailLogger = new ModelLogger(Email, {
58
58
  type: AuditLogReplacementType.Email,
59
59
  })],
60
60
  ['c', AuditLogReplacement.create({
61
- value: Formatter.integer(model.recipientCount ?? 0),
62
- count: model.recipientCount ?? 0,
61
+ value: Formatter.integer(model.emailRecipientsCount ?? 0),
62
+ count: model.emailRecipientsCount ?? 0,
63
63
  })],
64
64
  ]);
65
65
  if (options.data.recipient) {
@@ -6,9 +6,10 @@ import {
6
6
  } from '@aws-sdk/client-sqs';
7
7
  import { registerCron } from '@stamhoofd/crons';
8
8
  import { Email, EmailAddress } from '@stamhoofd/email';
9
- import { AuditLog, Organization } from '@stamhoofd/models';
9
+ import { AuditLog, EmailRecipient, Organization } from '@stamhoofd/models';
10
10
  import { AuditLogReplacement, AuditLogReplacementType, AuditLogSource, AuditLogType } from '@stamhoofd/structures';
11
11
  import { ForwardHandler } from '../helpers/ForwardHandler';
12
+ import { Email as EmailModel } from '@stamhoofd/models';
12
13
 
13
14
  registerCron('checkComplaints', checkComplaints);
14
15
  registerCron('checkReplies', checkReplies);
@@ -52,13 +53,88 @@ async function saveLog({ email, organization, type, subType, subject, response,
52
53
  await log.save();
53
54
  }
54
55
 
56
+ async function storeEmailStatus({ headers, type, message }: { headers: Record<string, string>; type: 'hard-bounce' | 'soft-bounce' | 'complaint'; message: string }) {
57
+ const emailId = headers['x-email-id'];
58
+ const recipientId = headers['x-email-recipient-id'];
59
+
60
+ if (emailId && recipientId) {
61
+ // check
62
+ const emailRecipient = await EmailRecipient.select()
63
+ .where('id', recipientId)
64
+ .where('emailId', emailId)
65
+ .first(false);
66
+
67
+ if (!emailRecipient) {
68
+ console.log('[AWS FORWARDING] Invalid email or recipient id in headers', headers);
69
+ return;
70
+ }
71
+
72
+ let isNew = true;
73
+
74
+ switch (type) {
75
+ case 'hard-bounce': {
76
+ if (emailRecipient.hardBounceError) {
77
+ isNew = false;
78
+ }
79
+ emailRecipient.hardBounceError = message;
80
+ break;
81
+ }
82
+ case 'soft-bounce': {
83
+ if (emailRecipient.softBounceError) {
84
+ isNew = false;
85
+ }
86
+ emailRecipient.softBounceError = message;
87
+ break;
88
+ }
89
+ case 'complaint': {
90
+ if (emailRecipient.spamComplaintError) {
91
+ isNew = false;
92
+ }
93
+ emailRecipient.spamComplaintError = message;
94
+ break;
95
+ }
96
+ }
97
+
98
+ console.log('[AWS FORWARDING] Marking email recipient ' + recipientId + ' for email ' + emailId + ' as ' + type);
99
+ if (await emailRecipient.save()) {
100
+ if (isNew) {
101
+ await EmailModel.bumpNotificationCount(emailId, type);
102
+ }
103
+ }
104
+ }
105
+ }
106
+
107
+ function readHeaders(message: any) {
108
+ try {
109
+ const mail = message.mail;
110
+ const headers: Record<string, string> = {};
111
+ if (mail && typeof mail === 'object' && mail !== null && Array.isArray(mail.headers)) {
112
+ for (const header of mail.headers) {
113
+ if (header.name && header.value) {
114
+ headers[(header.name as string).toLowerCase()] = header.value;
115
+ }
116
+ }
117
+ }
118
+ else {
119
+ console.log('[AWS] Missing mail headers', message);
120
+ }
121
+ return headers;
122
+ }
123
+ catch (e) {
124
+ console.log('[AWS] Failed to read headers', e, message);
125
+ return {};
126
+ }
127
+ }
128
+
55
129
  async function handleBounce(message: any) {
56
130
  if (message.bounce) {
131
+ console.log('[AWS BOUNCES] Handling bounce message', message);
132
+ const headers = readHeaders(message);
133
+
57
134
  const b = message.bounce;
58
135
  // Block all receivers that generate a permanent bounce
59
136
  const type = b.bounceType;
60
137
  const subtype = b.bounceSubType;
61
-
62
138
  const source = message.mail.source;
63
139
 
64
140
  // try to find organization that is responsible for this e-mail address
@@ -80,6 +156,12 @@ async function handleBounce(message: any) {
80
156
  emailAddress.hardBounce = true;
81
157
  await emailAddress.save();
82
158
 
159
+ await storeEmailStatus({
160
+ headers,
161
+ type: 'hard-bounce',
162
+ message: recipient.diagnosticCode || 'Permanent bounce',
163
+ });
164
+
83
165
  await saveLog({
84
166
  id: b.feedbackId,
85
167
  email,
@@ -95,6 +177,13 @@ async function handleBounce(message: any) {
95
177
  type === 'Transient'
96
178
  ) {
97
179
  const organization: Organization | undefined = source ? await Organization.getByEmail(source) : undefined;
180
+
181
+ await storeEmailStatus({
182
+ headers,
183
+ type: 'soft-bounce',
184
+ message: recipient.diagnosticCode || 'Soft bounce',
185
+ });
186
+
98
187
  await saveLog({
99
188
  id: b.feedbackId,
100
189
  email,
@@ -116,6 +205,7 @@ async function handleBounce(message: any) {
116
205
 
117
206
  async function handleComplaint(message: any) {
118
207
  if (message.complaint) {
208
+ const headers = readHeaders(message);
119
209
  const b = message.complaint;
120
210
  const source = message.mail.source;
121
211
  const organization: Organization | undefined = source ? await Organization.getByEmail(source) : undefined;
@@ -129,10 +219,16 @@ async function handleComplaint(message: any) {
129
219
  await emailAddress.save();
130
220
 
131
221
  if (type !== 'not-spam') {
222
+ await storeEmailStatus({
223
+ headers,
224
+ type: 'complaint',
225
+ message: recipient.diagnosticCode || type || 'Complaint',
226
+ });
227
+
132
228
  if (type === 'virus' || type === 'fraud') {
133
229
  await saveLog({
134
230
  id: b.feedbackId,
135
- email: source,
231
+ email,
136
232
  organization,
137
233
  type: AuditLogType.EmailAddressFraudComplaint,
138
234
  subType: type || 'unknown',
@@ -144,7 +240,7 @@ async function handleComplaint(message: any) {
144
240
  else {
145
241
  await saveLog({
146
242
  id: b.feedbackId,
147
- email: source,
243
+ email,
148
244
  organization,
149
245
  type: AuditLogType.EmailAddressMarkedAsSpam,
150
246
  subType: type || 'unknown',
@@ -198,7 +198,7 @@ async function sendTemplate({
198
198
 
199
199
  try {
200
200
  const upToDate = await ContextInstance.startForUser(systemUser, organization, async () => {
201
- return await model.send();
201
+ return await model.queueForSending(true);
202
202
  });
203
203
 
204
204
  if (!upToDate) {
@@ -1,5 +1,5 @@
1
1
  import { CachedBalance, Email } from '@stamhoofd/models';
2
- import { BalanceItem as BalanceItemStruct, compileToInMemoryFilter, EmailRecipient, EmailRecipientFilterType, LimitedFilteredRequest, PaginatedResponse, receivableBalanceObjectContactInMemoryFilterCompilers, Replacement, StamhoofdFilter } from '@stamhoofd/structures';
2
+ import { BalanceItem as BalanceItemStruct, compileToInMemoryFilter, EmailRecipient, EmailRecipientFilterType, LimitedFilteredRequest, PaginatedResponse, receivableBalanceObjectContactInMemoryFilterCompilers, ReceivableBalanceType, Replacement, StamhoofdFilter } from '@stamhoofd/structures';
3
3
  import { Formatter } from '@stamhoofd/utility';
4
4
  import { GetReceivableBalancesEndpoint } from '../endpoints/organization/dashboard/receivable-balances/GetReceivableBalancesEndpoint';
5
5
 
@@ -22,6 +22,8 @@ async function fetch(query: LimitedFilteredRequest, subfilter: StamhoofdFilter |
22
22
  for (const email of contact.emails) {
23
23
  const recipient = EmailRecipient.create({
24
24
  objectId: balance.id, // Note: not set member, user or organization id here - should be the queryable balance id
25
+ userId: balance.objectType === ReceivableBalanceType.user ? balance.object.id : null,
26
+ memberId: balance.objectType === ReceivableBalanceType.member ? balance.object.id : null,
25
27
  firstName: contact.firstName,
26
28
  lastName: contact.lastName,
27
29
  email,
@@ -120,8 +120,32 @@ export class CreateEmailEndpoint extends Endpoint<Params, Query, Body, ResponseB
120
120
  await model.buildExampleRecipient();
121
121
  model.updateCount();
122
122
 
123
- if (request.body.status === EmailStatus.Sending || request.body.status === EmailStatus.Sent) {
124
- model.send().catch(console.error);
123
+ if (request.body.status === EmailStatus.Sending || request.body.status === EmailStatus.Sent || request.body.status === EmailStatus.Queued) {
124
+ if (!await Context.auth.canSendEmail(model)) {
125
+ throw Context.auth.error({
126
+ message: 'Cannot send emails from this sender',
127
+ human: $t('1b509614-30b0-484c-af72-57d4bc9ea788'),
128
+ });
129
+ }
130
+ await model.queueForSending();
131
+ }
132
+
133
+ // Delete open drafts with the same content, from the same user
134
+ const duplicates = await Email.select()
135
+ .where('userId', user.id)
136
+ .where('organizationId', model.organizationId)
137
+ .where('status', EmailStatus.Draft)
138
+ .where('subject', model.subject)
139
+ .where('html', model.html)
140
+ .where('text', model.text)
141
+ .where('deletedAt', null)
142
+ .whereNot('id', model.id)
143
+ .limit(100)
144
+ .fetch();
145
+
146
+ for (const duplicate of duplicates) {
147
+ duplicate.deletedAt = new Date();
148
+ await duplicate.save();
125
149
  }
126
150
 
127
151
  return new Response(await model.getPreviewStructure());
@@ -104,7 +104,8 @@ export class GetAdminEmailsEndpoint extends Endpoint<Params, Query, Body, Respon
104
104
  };
105
105
  }
106
106
 
107
- const query = Email.select();
107
+ const query = Email.select()
108
+ .where('deletedAt', null);
108
109
 
109
110
  if (scopeFilter) {
110
111
  query.where(await compileToSQLFilter(scopeFilter, filterCompilers));
@@ -138,8 +139,6 @@ export class GetAdminEmailsEndpoint extends Endpoint<Params, Query, Body, Respon
138
139
  query.limit(q.limit);
139
140
  }
140
141
 
141
- console.log('Building query for GetAdminEmailsEndpoint', query.getSQL());
142
-
143
142
  return query;
144
143
  }
145
144
 
@@ -1,6 +1,6 @@
1
1
  import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
2
2
  import { Email, Platform } from '@stamhoofd/models';
3
- import { EmailPreview, EmailStatus, Email as EmailStruct, PermissionLevel } from '@stamhoofd/structures';
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';
@@ -50,15 +50,6 @@ export class PatchEmailEndpoint extends Endpoint<Params, Query, Body, ResponseBo
50
50
  throw Context.auth.error();
51
51
  }
52
52
 
53
- if (model.status !== EmailStatus.Draft) {
54
- throw new SimpleError({
55
- code: 'not_draft',
56
- human: 'Email is not a draft',
57
- message: $t(`298b5a46-2899-4aa1-89df-9b634c20806b`),
58
- statusCode: 400,
59
- });
60
- }
61
-
62
53
  let rebuild = false;
63
54
 
64
55
  if (request.body.subject !== undefined) {
@@ -78,6 +69,11 @@ export class PatchEmailEndpoint extends Endpoint<Params, Query, Body, ResponseBo
78
69
  model.senderId = sender.id;
79
70
  model.fromAddress = sender.email;
80
71
  model.fromName = sender.name;
72
+
73
+ // Check if we still have write access to the email
74
+ if (!await Context.auth.canAccessEmail(model, PermissionLevel.Write)) {
75
+ throw Context.auth.error();
76
+ }
81
77
  }
82
78
  else {
83
79
  throw new SimpleError({
@@ -119,6 +115,15 @@ export class PatchEmailEndpoint extends Endpoint<Params, Query, Body, ResponseBo
119
115
  }
120
116
 
121
117
  if (request.body.recipientFilter) {
118
+ if (model.status !== EmailStatus.Draft) {
119
+ throw new SimpleError({
120
+ code: 'not_draft',
121
+ human: 'Email is not a draft',
122
+ message: $t(`Je kan de ontvangerslijst alleen aanpassen als de e-mail nog een concept is`),
123
+ statusCode: 400,
124
+ });
125
+ }
126
+
122
127
  model.recipientFilter = patchObject(model.recipientFilter, request.body.recipientFilter);
123
128
  rebuild = true;
124
129
  }
@@ -129,17 +134,24 @@ export class PatchEmailEndpoint extends Endpoint<Params, Query, Body, ResponseBo
129
134
  model.validateAttachments();
130
135
  }
131
136
 
137
+ if (request.body.deletedAt !== undefined) {
138
+ if (!await Context.auth.canAccessEmail(model, PermissionLevel.Full)) {
139
+ throw Context.auth.error();
140
+ }
141
+ model.deletedAt = request.body.deletedAt;
142
+ }
143
+
132
144
  await model.save();
133
145
 
134
146
  if (rebuild) {
135
147
  await model.buildExampleRecipient();
136
- model.updateCount();
137
148
 
138
149
  // Force null - because we have stale data
139
- model.recipientCount = null;
150
+ model.emailRecipientsCount = null;
151
+ model.updateCount();
140
152
  }
141
153
 
142
- if (request.body.status === EmailStatus.Sending || request.body.status === EmailStatus.Sent) {
154
+ if (request.body.status === EmailStatus.Sending || request.body.status === EmailStatus.Sent || request.body.status === EmailStatus.Queued) {
143
155
  if (!await Context.auth.canSendEmail(model)) {
144
156
  throw Context.auth.error({
145
157
  message: 'Cannot send emails from this sender',
@@ -175,7 +187,8 @@ export class PatchEmailEndpoint extends Endpoint<Params, Query, Body, ResponseBo
175
187
  }
176
188
  }
177
189
 
178
- model.send().catch(console.error);
190
+ // Preview the sending status
191
+ await model.queueForSending();
179
192
  }
180
193
 
181
194
  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
+ }
@@ -0,0 +1,225 @@
1
+ import { STExpect, TestUtils } from '@stamhoofd/test-utils';
2
+ import { GetEmailRecipientsEndpoint } from './GetEmailRecipientsEndpoint';
3
+ import { AccessRight, EmailStatus, LimitedFilteredRequest, OrganizationEmail, PermissionLevel, Permissions, PermissionsResourceType, ResourcePermissions } from '@stamhoofd/structures';
4
+ import { Email, EmailRecipient, Organization, OrganizationFactory, RegistrationPeriod, RegistrationPeriodFactory, Token, User, UserFactory } from '@stamhoofd/models';
5
+ import { Request } from '@simonbackx/simple-endpoints';
6
+ import { testServer } from '../../../../tests/helpers/TestServer';
7
+
8
+ const baseUrl = `/email-recipients`;
9
+
10
+ describe('Endpoint.GetEmailRecipients', () => {
11
+ const endpoint = new GetEmailRecipientsEndpoint();
12
+ let period: RegistrationPeriod;
13
+ let organization: Organization;
14
+ let token: Token;
15
+ let user: User;
16
+ let sender: OrganizationEmail;
17
+ let sender2: OrganizationEmail;
18
+
19
+ let token2: Token;
20
+ let user2: User;
21
+
22
+ beforeAll(async () => {
23
+ TestUtils.setPermanentEnvironment('userMode', 'platform');
24
+ period = await new RegistrationPeriodFactory({
25
+ startDate: new Date(2023, 0, 1),
26
+ endDate: new Date(2023, 11, 31),
27
+ }).create();
28
+
29
+ organization = await new OrganizationFactory({ period })
30
+ .create();
31
+
32
+ sender = OrganizationEmail.create({
33
+ email: 'groepsleiding@voorbeeld.com',
34
+ name: 'Groepsleiding',
35
+ });
36
+ sender2 = OrganizationEmail.create({
37
+ email: 'kapoenen@voorbeeld.com',
38
+ name: 'Kapoenen',
39
+ });
40
+
41
+ organization.privateMeta.emails.push(sender);
42
+ organization.privateMeta.emails.push(sender2);
43
+ await organization.save();
44
+
45
+ user = await new UserFactory({
46
+ organization,
47
+ permissions: Permissions.create({
48
+ level: PermissionLevel.None,
49
+ resources: new Map([
50
+ [PermissionsResourceType.Senders, new Map([['', ResourcePermissions.create({
51
+ resourceName: sender.name!,
52
+ level: PermissionLevel.Read,
53
+ })]])],
54
+ ]),
55
+ }),
56
+ })
57
+ .create();
58
+
59
+ token = await Token.createToken(user);
60
+
61
+ user2 = await new UserFactory({
62
+ organization,
63
+ permissions: Permissions.create({
64
+ level: PermissionLevel.None,
65
+ resources: new Map([
66
+ [PermissionsResourceType.Senders, new Map([[sender2.id, ResourcePermissions.create({
67
+ resourceName: sender.name!,
68
+ level: PermissionLevel.Read,
69
+ })]])],
70
+ ]),
71
+ }),
72
+ })
73
+ .create();
74
+
75
+ token2 = await Token.createToken(user2);
76
+ });
77
+
78
+ test('It can request all email recipients if read permission for all senders', async () => {
79
+ const email = new Email();
80
+ email.subject = 'test subject';
81
+ email.status = EmailStatus.Draft;
82
+ email.text = 'test email';
83
+ email.html = `<p style="margin: 0; padding: 0; line-height: 1.4;">test email</p>`;
84
+ email.json = {};
85
+ email.organizationId = organization.id;
86
+ email.senderId = sender.id;
87
+ await email.save();
88
+
89
+ const emailRecipient = new EmailRecipient();
90
+ emailRecipient.email = 'jan.janssens@geenemail.com';
91
+ emailRecipient.firstName = 'Jan';
92
+ emailRecipient.lastName = 'Janssens';
93
+ emailRecipient.emailId = email.id;
94
+ emailRecipient.organizationId = organization.id;
95
+ await emailRecipient.save();
96
+
97
+ const request = Request.get({
98
+ path: baseUrl,
99
+ host: organization.getApiHost(),
100
+ query: new LimitedFilteredRequest({
101
+ filter: {},
102
+ limit: 10,
103
+ }),
104
+ headers: {
105
+ authorization: 'Bearer ' + token.accessToken,
106
+ },
107
+ });
108
+ const result = await testServer.test(endpoint, request);
109
+ expect(result.body.results).toHaveLength(1);
110
+ expect(result.body.results[0]).toMatchObject({
111
+ id: emailRecipient.id,
112
+ });
113
+ });
114
+
115
+ test('It can not request all email recipients if not read permission for all senders', async () => {
116
+ const email = new Email();
117
+ email.subject = 'test subject';
118
+ email.status = EmailStatus.Draft;
119
+ email.text = 'test email';
120
+ email.html = `<p style="margin: 0; padding: 0; line-height: 1.4;">test email</p>`;
121
+ email.json = {};
122
+ email.organizationId = organization.id;
123
+ email.senderId = sender2.id;
124
+ await email.save();
125
+
126
+ const emailRecipient = new EmailRecipient();
127
+ emailRecipient.email = 'jan.janssens@geenemail.com';
128
+ emailRecipient.firstName = 'Jan';
129
+ emailRecipient.lastName = 'Janssens';
130
+ emailRecipient.emailId = email.id;
131
+ emailRecipient.organizationId = organization.id;
132
+ await emailRecipient.save();
133
+
134
+ const request = Request.get({
135
+ path: baseUrl,
136
+ host: organization.getApiHost(),
137
+ query: new LimitedFilteredRequest({
138
+ filter: {},
139
+ limit: 10,
140
+ }),
141
+ headers: {
142
+ authorization: 'Bearer ' + token2.accessToken,
143
+ },
144
+ });
145
+ await expect(testServer.test(endpoint, request))
146
+ .rejects
147
+ .toThrow(STExpect.errorWithCode('permission_denied'));
148
+ });
149
+
150
+ test('It request all email recipients of a single email if read permission for that sender', async () => {
151
+ const email = new Email();
152
+ email.subject = 'test subject';
153
+ email.status = EmailStatus.Draft;
154
+ email.text = 'test email';
155
+ email.html = `<p style="margin: 0; padding: 0; line-height: 1.4;">test email</p>`;
156
+ email.json = {};
157
+ email.organizationId = organization.id;
158
+ email.senderId = sender2.id;
159
+ await email.save();
160
+
161
+ const emailRecipient = new EmailRecipient();
162
+ emailRecipient.email = 'jan.janssens@geenemail.com';
163
+ emailRecipient.firstName = 'Jan';
164
+ emailRecipient.lastName = 'Janssens';
165
+ emailRecipient.emailId = email.id;
166
+ emailRecipient.organizationId = organization.id;
167
+ await emailRecipient.save();
168
+
169
+ const request = Request.get({
170
+ path: baseUrl,
171
+ host: organization.getApiHost(),
172
+ query: new LimitedFilteredRequest({
173
+ filter: {
174
+ emailId: email.id,
175
+ },
176
+ limit: 10,
177
+ }),
178
+ headers: {
179
+ authorization: 'Bearer ' + token2.accessToken,
180
+ },
181
+ });
182
+ const result = await testServer.test(endpoint, request);
183
+ expect(result.body.results).toHaveLength(1);
184
+ expect(result.body.results[0]).toMatchObject({
185
+ id: emailRecipient.id,
186
+ });
187
+ });
188
+
189
+ test('It cannot request all email recipients of a single email if read permission for another sender', async () => {
190
+ const email = new Email();
191
+ email.subject = 'test subject';
192
+ email.status = EmailStatus.Draft;
193
+ email.text = 'test email';
194
+ email.html = `<p style="margin: 0; padding: 0; line-height: 1.4;">test email</p>`;
195
+ email.json = {};
196
+ email.organizationId = organization.id;
197
+ email.senderId = sender.id;
198
+ await email.save();
199
+
200
+ const emailRecipient = new EmailRecipient();
201
+ emailRecipient.email = 'jan.janssens@geenemail.com';
202
+ emailRecipient.firstName = 'Jan';
203
+ emailRecipient.lastName = 'Janssens';
204
+ emailRecipient.emailId = email.id;
205
+ emailRecipient.organizationId = organization.id;
206
+ await emailRecipient.save();
207
+
208
+ const request = Request.get({
209
+ path: baseUrl,
210
+ host: organization.getApiHost(),
211
+ query: new LimitedFilteredRequest({
212
+ filter: {
213
+ emailId: email.id,
214
+ },
215
+ limit: 10,
216
+ }),
217
+ headers: {
218
+ authorization: 'Bearer ' + token2.accessToken,
219
+ },
220
+ });
221
+ await expect(testServer.test(endpoint, request))
222
+ .rejects
223
+ .toThrow(STExpect.errorWithCode('permission_denied'));
224
+ });
225
+ });