@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stamhoofd/backend",
3
- "version": "2.91.0",
3
+ "version": "2.93.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.91.0",
49
- "@stamhoofd/backend-middleware": "2.91.0",
50
- "@stamhoofd/email": "2.91.0",
51
- "@stamhoofd/models": "2.91.0",
52
- "@stamhoofd/queues": "2.91.0",
53
- "@stamhoofd/sql": "2.91.0",
54
- "@stamhoofd/structures": "2.91.0",
55
- "@stamhoofd/utility": "2.91.0",
48
+ "@stamhoofd/backend-i18n": "2.93.0",
49
+ "@stamhoofd/backend-middleware": "2.93.0",
50
+ "@stamhoofd/email": "2.93.0",
51
+ "@stamhoofd/models": "2.93.0",
52
+ "@stamhoofd/queues": "2.93.0",
53
+ "@stamhoofd/sql": "2.93.0",
54
+ "@stamhoofd/structures": "2.93.0",
55
+ "@stamhoofd/utility": "2.93.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": "37ff6e89f07320c0736d32c9b911c8c1c35dc421"
73
+ "gitHead": "05b8228a7fc3871aa767609a95a2c1511132e548"
74
74
  }
@@ -1,5 +1,5 @@
1
- import { Email, EmailRecipient, replaceHtml } from '@stamhoofd/models';
2
- import { AuditLogReplacement, AuditLogReplacementType, AuditLogType, EmailStatus } from '@stamhoofd/structures';
1
+ import { Email, EmailRecipient } from '@stamhoofd/models';
2
+ import { AuditLogReplacement, AuditLogReplacementType, AuditLogType, EmailStatus, replaceEmailHtml } from '@stamhoofd/structures';
3
3
  import { Formatter } from '@stamhoofd/utility';
4
4
  import { ModelLogger } from './ModelLogger';
5
5
 
@@ -54,17 +54,17 @@ export const EmailLogger = new ModelLogger(Email, {
54
54
  const map = new Map([
55
55
  ['e', AuditLogReplacement.create({
56
56
  id: model.id,
57
- value: replaceHtml(model.subject ?? '', options.data.recipient?.replacements ?? []),
57
+ value: replaceEmailHtml(model.subject ?? '', options.data.recipient?.replacements ?? []),
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) {
66
66
  map.set('html', AuditLogReplacement.html(
67
- replaceHtml(model.html ?? '', options.data.recipient?.replacements ?? []),
67
+ replaceEmailHtml(model.html ?? '', options.data.recipient?.replacements ?? []),
68
68
  ));
69
69
  }
70
70
  return map;
@@ -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) {
@@ -33,6 +33,12 @@ export async function endFunctionsOfUsersWithoutRegistration() {
33
33
  return;
34
34
  }
35
35
 
36
+ // If period is ending within 15 days, also skip cleanup
37
+ if (period.endDate && period.endDate < new Date(Date.now() + 1000 * 60 * 60 * 24 * 15)) {
38
+ console.warn('Current registration period is ending within 15 days or has ended, skipping cleanup.');
39
+ return;
40
+ }
41
+
36
42
  await FlagMomentCleanup.endFunctionsOfUsersWithoutRegistration();
37
43
  lastCleanupYear = currentYear;
38
44
  lastCleanupMonth = currentMonth;
@@ -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,
@@ -1,9 +1,10 @@
1
1
  import { Decoder } from '@simonbackx/simple-encoding';
2
2
  import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
3
- import { Email, RateLimiter } from '@stamhoofd/models';
3
+ import { Email, Platform, RateLimiter } from '@stamhoofd/models';
4
4
  import { EmailPreview, EmailStatus, Email as EmailStruct, EmailTemplate as EmailTemplateStruct } from '@stamhoofd/structures';
5
5
 
6
6
  import { Context } from '../../../helpers/Context';
7
+ import { SimpleError } from '@simonbackx/simple-errors';
7
8
 
8
9
  type Params = Record<string, never>;
9
10
  type Query = undefined;
@@ -64,8 +65,11 @@ export class CreateEmailEndpoint extends Endpoint<Params, Query, Body, ResponseB
64
65
  const organization = await Context.setOptionalOrganizationScope();
65
66
  const { user } = await Context.authenticate();
66
67
 
67
- if (!Context.auth.canSendEmails()) {
68
- throw Context.auth.error();
68
+ if (!await Context.auth.canSendEmails(organization)) {
69
+ throw Context.auth.error({
70
+ message: 'Cannot send emails',
71
+ human: $t('f7b7ac75-f7df-49cc-8961-b2478d9683e3'),
72
+ });
69
73
  }
70
74
 
71
75
  const model = new Email();
@@ -79,8 +83,28 @@ export class CreateEmailEndpoint extends Endpoint<Params, Query, Body, ResponseB
79
83
  model.json = request.body.json;
80
84
  model.status = request.body.status;
81
85
  model.attachments = request.body.attachments;
82
- model.fromAddress = request.body.fromAddress;
83
- model.fromName = request.body.fromName;
86
+
87
+ const list = organization ? organization.privateMeta.emails : (await Platform.getShared()).privateConfig.emails;
88
+ const sender = list.find(e => e.id === request.body.senderId);
89
+ if (sender) {
90
+ if (!await Context.auth.canSendEmailsFrom(organization, sender.id)) {
91
+ throw Context.auth.error({
92
+ message: 'Cannot send emails from this sender',
93
+ human: $t('1b509614-30b0-484c-af72-57d4bc9ea788'),
94
+ });
95
+ }
96
+ model.senderId = sender.id;
97
+ model.fromAddress = sender.email;
98
+ model.fromName = sender.name;
99
+ }
100
+ else {
101
+ throw new SimpleError({
102
+ code: 'invalid_sender',
103
+ human: 'Sender not found',
104
+ message: $t(`94adb4e0-2ef1-4ee8-9f02-5a76efa51c1d`),
105
+ statusCode: 400,
106
+ });
107
+ }
84
108
 
85
109
  model.validateAttachments();
86
110
 
@@ -96,8 +120,14 @@ export class CreateEmailEndpoint extends Endpoint<Params, Query, Body, ResponseB
96
120
  await model.buildExampleRecipient();
97
121
  model.updateCount();
98
122
 
99
- if (request.body.status === EmailStatus.Sending || request.body.status === EmailStatus.Sent) {
100
- 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();
101
131
  }
102
132
 
103
133
  return new Response(await model.getPreviewStructure());
@@ -0,0 +1,205 @@
1
+ import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
2
+ import { assertSort, CountFilteredRequest, EmailPreview, EmailStatus, getSortFilter, LimitedFilteredRequest, PaginatedResponse, StamhoofdFilter } from '@stamhoofd/structures';
3
+
4
+ import { Decoder } from '@simonbackx/simple-encoding';
5
+ import { SimpleError } from '@simonbackx/simple-errors';
6
+ import { Email, Platform } from '@stamhoofd/models';
7
+ import { applySQLSorter, compileToSQLFilter, SQLFilterDefinitions, SQLSortDefinitions } from '@stamhoofd/sql';
8
+ import { Context } from '../../../helpers/Context';
9
+ import { emailFilterCompilers } from '../../../sql-filters/emails';
10
+ import { emailSorters } from '../../../sql-sorters/emails';
11
+
12
+ type Params = Record<string, never>;
13
+ type Query = LimitedFilteredRequest;
14
+ type Body = undefined;
15
+ type ResponseBody = PaginatedResponse<EmailPreview[], LimitedFilteredRequest>;
16
+
17
+ const filterCompilers: SQLFilterDefinitions = emailFilterCompilers;
18
+ const sorters: SQLSortDefinitions<Email> = emailSorters;
19
+
20
+ export class GetAdminEmailsEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
21
+ queryDecoder = LimitedFilteredRequest as Decoder<LimitedFilteredRequest>;
22
+
23
+ protected doesMatch(request: Request): [true, Params] | [false] {
24
+ if (request.method !== 'GET') {
25
+ return [false];
26
+ }
27
+
28
+ const params = Endpoint.parseParameters(request.url, '/email', {});
29
+
30
+ if (params) {
31
+ return [true, params as Params];
32
+ }
33
+ return [false];
34
+ }
35
+
36
+ static async buildQuery(q: CountFilteredRequest | LimitedFilteredRequest) {
37
+ const organization = Context.organization;
38
+ const user = Context.user;
39
+ if (!user) {
40
+ throw new Error('Not authenticated');
41
+ }
42
+
43
+ let scopeFilter: StamhoofdFilter | undefined = undefined;
44
+
45
+ const canReadAllEmails = await Context.auth.canReadAllEmails(organization ?? null);
46
+ scopeFilter = {
47
+ organizationId: organization?.id ?? null,
48
+ };
49
+
50
+ if (!canReadAllEmails) {
51
+ const senders = organization ? organization.privateMeta.emails : (await Platform.getShared()).privateConfig.emails;
52
+ const ids: string[] = [];
53
+ for (const sender of senders) {
54
+ if (await Context.auth.canReadAllEmails(organization ?? null, sender.id)) {
55
+ ids.push(sender.id);
56
+ }
57
+ }
58
+ if (ids.length === 0) {
59
+ throw Context.auth.error();
60
+ }
61
+
62
+ scopeFilter = {
63
+ $and: [
64
+ {
65
+ organizationId: organization?.id ?? null,
66
+ },
67
+ {
68
+ $or: [
69
+ {
70
+ senderId: {
71
+ $in: ids,
72
+ },
73
+ status: {
74
+ $neq: EmailStatus.Draft,
75
+ },
76
+ },
77
+ {
78
+ userId: user.id,
79
+ },
80
+ ],
81
+ },
82
+ ],
83
+ };
84
+ }
85
+ else {
86
+ scopeFilter = {
87
+ $and: [
88
+ {
89
+ organizationId: organization?.id ?? null,
90
+ },
91
+ {
92
+ $or: [
93
+ {
94
+ status: {
95
+ $neq: EmailStatus.Draft,
96
+ },
97
+ },
98
+ {
99
+ userId: user.id,
100
+ },
101
+ ],
102
+ },
103
+ ],
104
+ };
105
+ }
106
+
107
+ const query = Email.select();
108
+
109
+ if (scopeFilter) {
110
+ query.where(await compileToSQLFilter(scopeFilter, filterCompilers));
111
+ }
112
+
113
+ if (q.filter) {
114
+ query.where(await compileToSQLFilter(q.filter, filterCompilers));
115
+ }
116
+
117
+ if (q.search) {
118
+ let searchFilter: StamhoofdFilter | null = null;
119
+
120
+ searchFilter = {
121
+ subject: {
122
+ $contains: q.search,
123
+ },
124
+ };
125
+
126
+ if (searchFilter) {
127
+ query.where(await compileToSQLFilter(searchFilter, filterCompilers));
128
+ }
129
+ }
130
+
131
+ if (q instanceof LimitedFilteredRequest) {
132
+ if (q.pageFilter) {
133
+ query.where(await compileToSQLFilter(q.pageFilter, filterCompilers));
134
+ }
135
+
136
+ q.sort = assertSort(q.sort, [{ key: 'id' }]);
137
+ applySQLSorter(query, q.sort, sorters);
138
+ query.limit(q.limit);
139
+ }
140
+
141
+ return query;
142
+ }
143
+
144
+ static async buildData(requestQuery: LimitedFilteredRequest) {
145
+ const query = await GetAdminEmailsEndpoint.buildQuery(requestQuery);
146
+ const emails = await query.fetch();
147
+
148
+ let next: LimitedFilteredRequest | undefined;
149
+
150
+ if (emails.length >= requestQuery.limit) {
151
+ const lastObject = emails[emails.length - 1];
152
+ const nextFilter = getSortFilter(lastObject, sorters, requestQuery.sort);
153
+
154
+ next = new LimitedFilteredRequest({
155
+ filter: requestQuery.filter,
156
+ pageFilter: nextFilter,
157
+ sort: requestQuery.sort,
158
+ limit: requestQuery.limit,
159
+ search: requestQuery.search,
160
+ });
161
+
162
+ if (JSON.stringify(nextFilter) === JSON.stringify(requestQuery.pageFilter)) {
163
+ console.error('Found infinite loading loop for', requestQuery);
164
+ next = undefined;
165
+ }
166
+ }
167
+
168
+ return new PaginatedResponse<EmailPreview[], LimitedFilteredRequest>({
169
+ results: await Promise.all(emails.map(email => email.getPreviewStructure())),
170
+ next,
171
+ });
172
+ }
173
+
174
+ async handle(request: DecodedRequest<Params, Query, Body>) {
175
+ const organization = await Context.setOptionalOrganizationScope();
176
+ await Context.authenticate();
177
+
178
+ if (!await Context.auth.canReadEmails(organization)) {
179
+ // This is a first fast check, we'll limit it later in the scope query
180
+ throw Context.auth.error();
181
+ }
182
+
183
+ const maxLimit = Context.auth.hasSomePlatformAccess() ? 1000 : 100;
184
+
185
+ if (request.query.limit > maxLimit) {
186
+ throw new SimpleError({
187
+ code: 'invalid_field',
188
+ field: 'limit',
189
+ message: 'Limit can not be more than ' + maxLimit,
190
+ });
191
+ }
192
+
193
+ if (request.query.limit < 1) {
194
+ throw new SimpleError({
195
+ code: 'invalid_field',
196
+ field: 'limit',
197
+ message: 'Limit can not be less than 1',
198
+ });
199
+ }
200
+
201
+ return new Response(
202
+ await GetAdminEmailsEndpoint.buildData(request.query),
203
+ );
204
+ }
205
+ }
@@ -32,7 +32,7 @@ export class GetEmailEndpoint extends Endpoint<Params, Query, Body, ResponseBody
32
32
  const organization = await Context.setOptionalOrganizationScope();
33
33
  const { user } = await Context.authenticate();
34
34
 
35
- if (!Context.auth.canSendEmails()) {
35
+ if (!await Context.auth.canReadEmails(organization)) {
36
36
  throw Context.auth.error();
37
37
  }
38
38
 
@@ -46,6 +46,10 @@ export class GetEmailEndpoint extends Endpoint<Params, Query, Body, ResponseBody
46
46
  });
47
47
  }
48
48
 
49
+ if (!await Context.auth.canAccessEmail(model)) {
50
+ throw Context.auth.error();
51
+ }
52
+
49
53
  return new Response(await model.getPreviewStructure());
50
54
  }
51
55
  }