@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.
- package/package.json +10 -10
- package/src/audit-logs/EmailLogger.ts +6 -6
- package/src/crons/amazon-ses.ts +100 -4
- package/src/crons/balance-emails.ts +1 -1
- package/src/crons/endFunctionsOfUsersWithoutRegistration.ts +6 -0
- package/src/email-recipient-loaders/receivable-balances.ts +3 -1
- package/src/endpoints/global/email/CreateEmailEndpoint.ts +37 -7
- package/src/endpoints/global/email/GetAdminEmailsEndpoint.ts +205 -0
- package/src/endpoints/global/email/GetEmailEndpoint.ts +5 -1
- package/src/endpoints/global/email/PatchEmailEndpoint.test.ts +404 -8
- package/src/endpoints/global/email/PatchEmailEndpoint.ts +81 -26
- package/src/endpoints/global/email-recipients/GetEmailRecipientsCountEndpoint.ts +47 -0
- package/src/endpoints/global/email-recipients/GetEmailRecipientsEndpoint.test.ts +225 -0
- package/src/endpoints/global/email-recipients/GetEmailRecipientsEndpoint.ts +164 -0
- package/src/endpoints/global/email-recipients/helpers/validateEmailRecipientFilter.ts +64 -0
- package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +5 -1
- package/src/endpoints/global/registration-periods/GetRegistrationPeriodsEndpoint.ts +19 -1
- package/src/endpoints/organization/dashboard/webshops/DeleteWebshopEndpoint.ts +10 -1
- package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.ts +8 -1
- package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +2 -67
- package/src/helpers/AdminPermissionChecker.ts +81 -5
- package/src/helpers/EmailResumer.ts +2 -2
- package/src/seeds/1752848560-groups-registration-periods.ts +768 -0
- package/src/seeds/1755532883-update-email-sender-ids.ts +47 -0
- package/src/seeds/1755790070-fill-email-recipient-errors.ts +96 -0
- package/src/seeds/1755876819-remove-duplicate-members.ts +145 -0
- package/src/seeds/1756115432-remove-old-drafts.ts +16 -0
- package/src/seeds/1756115433-fill-email-recipient-organization-id.ts +30 -0
- package/src/services/uitpas/UitpasService.ts +71 -2
- package/src/services/uitpas/checkUitpasNumbers.ts +1 -0
- package/src/sql-filters/email-recipients.ts +59 -0
- package/src/sql-filters/emails.ts +95 -0
- package/src/sql-filters/members.ts +42 -1
- package/src/sql-filters/registration-periods.ts +5 -0
- package/src/sql-sorters/email-recipients.ts +69 -0
- 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.
|
|
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.
|
|
49
|
-
"@stamhoofd/backend-middleware": "2.
|
|
50
|
-
"@stamhoofd/email": "2.
|
|
51
|
-
"@stamhoofd/models": "2.
|
|
52
|
-
"@stamhoofd/queues": "2.
|
|
53
|
-
"@stamhoofd/sql": "2.
|
|
54
|
-
"@stamhoofd/structures": "2.
|
|
55
|
-
"@stamhoofd/utility": "2.
|
|
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": "
|
|
73
|
+
"gitHead": "05b8228a7fc3871aa767609a95a2c1511132e548"
|
|
74
74
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Email, EmailRecipient
|
|
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:
|
|
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.
|
|
62
|
-
count: model.
|
|
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
|
-
|
|
67
|
+
replaceEmailHtml(model.html ?? '', options.data.recipient?.replacements ?? []),
|
|
68
68
|
));
|
|
69
69
|
}
|
|
70
70
|
return map;
|
package/src/crons/amazon-ses.ts
CHANGED
|
@@ -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
|
|
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
|
|
243
|
+
email,
|
|
148
244
|
organization,
|
|
149
245
|
type: AuditLogType.EmailAddressMarkedAsSpam,
|
|
150
246
|
subType: type || 'unknown',
|
|
@@ -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
|
-
|
|
83
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
}
|