@stamhoofd/backend 2.79.7 → 2.80.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/.nvmrc +1 -0
- package/index.ts +1 -1
- package/package.json +11 -11
- package/src/audit-logs/ModelLogger.ts +2 -2
- package/src/crons/amazon-ses.ts +215 -227
- package/src/crons/clearExcelCache.test.ts +1 -1
- package/src/endpoints/admin/members/ChargeMembersEndpoint.ts +105 -0
- package/src/endpoints/admin/organizations/ChargeOrganizationsEndpoint.ts +6 -10
- package/src/endpoints/global/events/PatchEventNotificationsEndpoint.test.ts +997 -0
- package/src/endpoints/global/events/PatchEventNotificationsEndpoint.ts +19 -3
- package/src/endpoints/global/members/GetMembersEndpoint.ts +9 -9
- package/src/endpoints/global/organizations/SearchOrganizationEndpoint.test.ts +185 -1
- package/src/endpoints/global/organizations/SearchOrganizationEndpoint.ts +36 -18
- package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +9 -0
- package/src/endpoints/organization/dashboard/organization/SetOrganizationDomainEndpoint.ts +10 -3
- package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +9 -0
- package/src/helpers/AdminPermissionChecker.ts +3 -3
- package/src/helpers/ForwardHandler.ts +3 -2
- package/src/helpers/MemberCharger.ts +39 -0
- package/src/helpers/OrganizationCharger.ts +9 -20
- package/src/services/EventNotificationService.ts +3 -0
- package/tests/e2e/charge-members.test.ts +429 -0
- package/tests/jest.setup.ts +7 -1
- package/tests/toMatchMap.ts +68 -0
- package/src/services/diff.ts +0 -514
package/.nvmrc
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
20.12
|
package/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import backendEnv from '@stamhoofd/backend-env';
|
|
2
|
-
backendEnv.load();
|
|
2
|
+
backendEnv.load({ service: 'api' });
|
|
3
3
|
|
|
4
4
|
import { Column, Database, Migration } from '@simonbackx/simple-database';
|
|
5
5
|
import { CORSPreflightEndpoint, Router, RouterServer } from '@simonbackx/simple-endpoints';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stamhoofd/backend",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.80.0",
|
|
4
4
|
"main": "./dist/index.js",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -35,17 +35,17 @@
|
|
|
35
35
|
"@bwip-js/node": "^4.5.1",
|
|
36
36
|
"@mollie/api-client": "3.7.0",
|
|
37
37
|
"@simonbackx/simple-database": "1.29.1",
|
|
38
|
-
"@simonbackx/simple-encoding": "2.
|
|
38
|
+
"@simonbackx/simple-encoding": "2.22.0",
|
|
39
39
|
"@simonbackx/simple-endpoints": "1.19.1",
|
|
40
40
|
"@simonbackx/simple-logging": "^1.0.1",
|
|
41
|
-
"@stamhoofd/backend-i18n": "2.
|
|
42
|
-
"@stamhoofd/backend-middleware": "2.
|
|
43
|
-
"@stamhoofd/email": "2.
|
|
44
|
-
"@stamhoofd/models": "2.
|
|
45
|
-
"@stamhoofd/queues": "2.
|
|
46
|
-
"@stamhoofd/sql": "2.
|
|
47
|
-
"@stamhoofd/structures": "2.
|
|
48
|
-
"@stamhoofd/utility": "2.
|
|
41
|
+
"@stamhoofd/backend-i18n": "2.80.0",
|
|
42
|
+
"@stamhoofd/backend-middleware": "2.80.0",
|
|
43
|
+
"@stamhoofd/email": "2.80.0",
|
|
44
|
+
"@stamhoofd/models": "2.80.0",
|
|
45
|
+
"@stamhoofd/queues": "2.80.0",
|
|
46
|
+
"@stamhoofd/sql": "2.80.0",
|
|
47
|
+
"@stamhoofd/structures": "2.80.0",
|
|
48
|
+
"@stamhoofd/utility": "2.80.0",
|
|
49
49
|
"archiver": "^7.0.1",
|
|
50
50
|
"aws-sdk": "^2.885.0",
|
|
51
51
|
"axios": "1.6.8",
|
|
@@ -65,5 +65,5 @@
|
|
|
65
65
|
"publishConfig": {
|
|
66
66
|
"access": "public"
|
|
67
67
|
},
|
|
68
|
-
"gitHead": "
|
|
68
|
+
"gitHead": "252faf1dbd8ebdd8469810e9c51e510d93f21fc2"
|
|
69
69
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { Model, ModelEvent } from '@simonbackx/simple-database';
|
|
2
2
|
import { AuditLog } from '@stamhoofd/models';
|
|
3
|
+
import { ObjectDiffer } from '@stamhoofd/object-differ';
|
|
3
4
|
import { AuditLogPatchItem, AuditLogPatchItemType, AuditLogReplacement, AuditLogSource, AuditLogType } from '@stamhoofd/structures';
|
|
4
5
|
import { ContextInstance } from '../helpers/Context';
|
|
5
6
|
import { AuditLogService } from '../services/AuditLogService';
|
|
6
|
-
import { diffUnknown } from '../services/diff';
|
|
7
7
|
|
|
8
8
|
export type ModelEventLogOptions<D> = {
|
|
9
9
|
type: AuditLogType;
|
|
@@ -180,7 +180,7 @@ export class ModelLogger<ModelType extends typeof Model, M extends InstanceType<
|
|
|
180
180
|
}));
|
|
181
181
|
}
|
|
182
182
|
else {
|
|
183
|
-
log.patchList.push(...
|
|
183
|
+
log.patchList.push(...ObjectDiffer.diff(
|
|
184
184
|
key in oldModel ? oldModel[key] : undefined,
|
|
185
185
|
key in event.model ? event.model[key] : undefined,
|
|
186
186
|
AuditLogReplacement.key(renamedKey),
|
package/src/crons/amazon-ses.ts
CHANGED
|
@@ -27,13 +27,17 @@ async function saveLog({ email, organization, type, subType, subject, response,
|
|
|
27
27
|
type: AuditLogReplacementType.EmailAddress,
|
|
28
28
|
})],
|
|
29
29
|
['subType', AuditLogReplacement.key(subType || 'unknown')],
|
|
30
|
-
['response', AuditLogReplacement.longText(response)],
|
|
31
30
|
['subject', AuditLogReplacement.string(subject)],
|
|
32
31
|
['sender', AuditLogReplacement.create({
|
|
33
32
|
value: sender,
|
|
34
33
|
type: AuditLogReplacementType.EmailAddress,
|
|
35
34
|
})],
|
|
36
35
|
]);
|
|
36
|
+
|
|
37
|
+
if (response) {
|
|
38
|
+
log.replacements.set('response', AuditLogReplacement.longText(response));
|
|
39
|
+
}
|
|
40
|
+
|
|
37
41
|
// Check if we already logged this bounce
|
|
38
42
|
const existing = await AuditLog.select().where('externalId', log.externalId).first(false);
|
|
39
43
|
if (existing) {
|
|
@@ -44,140 +48,180 @@ async function saveLog({ email, organization, type, subType, subject, response,
|
|
|
44
48
|
await log.save();
|
|
45
49
|
}
|
|
46
50
|
|
|
47
|
-
async function
|
|
48
|
-
if (
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
51
|
+
async function handleBounce(message: any) {
|
|
52
|
+
if (message.bounce) {
|
|
53
|
+
const b = message.bounce;
|
|
54
|
+
// Block all receivers that generate a permanent bounce
|
|
55
|
+
const type = b.bounceType;
|
|
56
|
+
const subtype = b.bounceSubType;
|
|
57
|
+
|
|
58
|
+
const source = message.mail.source;
|
|
59
|
+
|
|
60
|
+
// try to find organization that is responsible for this e-mail address
|
|
61
|
+
|
|
62
|
+
for (const recipient of b.bouncedRecipients) {
|
|
63
|
+
const email = recipient.emailAddress;
|
|
64
|
+
|
|
65
|
+
if (
|
|
66
|
+
type === 'Permanent'
|
|
67
|
+
|| (
|
|
68
|
+
recipient.diagnosticCode && (
|
|
69
|
+
(recipient.diagnosticCode as string).toLowerCase().includes('invalid domain')
|
|
70
|
+
|| (recipient.diagnosticCode as string).toLowerCase().includes('unable to lookup dns')
|
|
71
|
+
)
|
|
72
|
+
)
|
|
73
|
+
) {
|
|
74
|
+
const organization: Organization | undefined = source ? await Organization.getByEmail(source) : undefined;
|
|
75
|
+
if (organization) {
|
|
76
|
+
const emailAddress = await EmailAddress.getOrCreate(email, organization.id);
|
|
77
|
+
emailAddress.hardBounce = true;
|
|
78
|
+
await emailAddress.save();
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
console.error('[AWS BOUNCES] Unknown organization for email address ' + source);
|
|
67
82
|
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
try {
|
|
71
|
-
if (message.Body) {
|
|
72
|
-
// decode the JSON value
|
|
73
|
-
const bounce = JSON.parse(message.Body);
|
|
74
83
|
|
|
75
|
-
|
|
76
|
-
|
|
84
|
+
await saveLog({
|
|
85
|
+
id: b.feedbackId,
|
|
86
|
+
email,
|
|
87
|
+
organization,
|
|
88
|
+
type: AuditLogType.EmailAddressHardBounced,
|
|
89
|
+
subType: subtype || 'unknown',
|
|
90
|
+
sender: source,
|
|
91
|
+
response: recipient.diagnosticCode || '',
|
|
92
|
+
subject: message.mail.commonHeaders?.subject || '',
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
else if (
|
|
96
|
+
type === 'Transient'
|
|
97
|
+
) {
|
|
98
|
+
const organization: Organization | undefined = source ? await Organization.getByEmail(source) : undefined;
|
|
99
|
+
await saveLog({
|
|
100
|
+
id: b.feedbackId,
|
|
101
|
+
email,
|
|
102
|
+
organization,
|
|
103
|
+
type: AuditLogType.EmailAddressSoftBounced,
|
|
104
|
+
subType: subtype || 'unknown',
|
|
105
|
+
sender: source,
|
|
106
|
+
response: recipient.diagnosticCode || '',
|
|
107
|
+
subject: message.mail.commonHeaders?.subject || '',
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
console.log('[AWS BOUNCES] For domain ' + source);
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
console.log("[AWS BOUNCES] 'bounce' field missing in bounce message");
|
|
115
|
+
}
|
|
116
|
+
}
|
|
77
117
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
}
|
|
106
|
-
else {
|
|
107
|
-
console.error('[AWS BOUNCES] Unknown organization for email address ' + source);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
await saveLog({
|
|
111
|
-
id: b.feedbackId,
|
|
112
|
-
email,
|
|
113
|
-
organization,
|
|
114
|
-
type: AuditLogType.EmailAddressHardBounced,
|
|
115
|
-
subType: subtype || 'unknown',
|
|
116
|
-
sender: source,
|
|
117
|
-
response: b.diagnosticCode || '',
|
|
118
|
-
subject: message.mail.commonHeaders?.subject || '',
|
|
119
|
-
});
|
|
120
|
-
}
|
|
121
|
-
else if (
|
|
122
|
-
type === 'Transient'
|
|
123
|
-
) {
|
|
124
|
-
const organization: Organization | undefined = source ? await Organization.getByEmail(source) : undefined;
|
|
125
|
-
await saveLog({
|
|
126
|
-
id: b.feedbackId,
|
|
127
|
-
email,
|
|
128
|
-
organization,
|
|
129
|
-
type: AuditLogType.EmailAddressSoftBounced,
|
|
130
|
-
subType: subtype || 'unknown',
|
|
131
|
-
sender: source,
|
|
132
|
-
response: b.diagnosticCode || '',
|
|
133
|
-
subject: message.mail.commonHeaders?.subject || '',
|
|
134
|
-
});
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
console.log('[AWS BOUNCES] For domain ' + source);
|
|
138
|
-
}
|
|
139
|
-
else {
|
|
140
|
-
console.log("[AWS BOUNCES] 'bounce' field missing in bounce message");
|
|
141
|
-
}
|
|
118
|
+
async function handleComplaint(message: any) {
|
|
119
|
+
if (message.complaint) {
|
|
120
|
+
const b = message.complaint;
|
|
121
|
+
const source = message.mail.source;
|
|
122
|
+
const organization: Organization | undefined = source ? await Organization.getByEmail(source) : undefined;
|
|
123
|
+
|
|
124
|
+
const type: 'abuse' | 'auth-failure' | 'fraud' | 'not-spam' | 'other' | 'virus' = b.complaintFeedbackType;
|
|
125
|
+
|
|
126
|
+
if (organization) {
|
|
127
|
+
for (const recipient of b.complainedRecipients) {
|
|
128
|
+
const email = recipient.emailAddress;
|
|
129
|
+
const emailAddress = await EmailAddress.getOrCreate(email, organization.id);
|
|
130
|
+
emailAddress.markedAsSpam = type !== 'not-spam';
|
|
131
|
+
await emailAddress.save();
|
|
132
|
+
|
|
133
|
+
if (type !== 'not-spam') {
|
|
134
|
+
if (type === 'virus' || type === 'fraud') {
|
|
135
|
+
await saveLog({
|
|
136
|
+
id: b.feedbackId,
|
|
137
|
+
email: source,
|
|
138
|
+
organization,
|
|
139
|
+
type: AuditLogType.EmailAddressFraudComplaint,
|
|
140
|
+
subType: type || 'unknown',
|
|
141
|
+
sender: source,
|
|
142
|
+
response: recipient.diagnosticCode || '',
|
|
143
|
+
subject: message.mail.commonHeaders?.subject || '',
|
|
144
|
+
});
|
|
142
145
|
}
|
|
143
146
|
else {
|
|
144
|
-
|
|
147
|
+
await saveLog({
|
|
148
|
+
id: b.feedbackId,
|
|
149
|
+
email: source,
|
|
150
|
+
organization,
|
|
151
|
+
type: AuditLogType.EmailAddressMarkedAsSpam,
|
|
152
|
+
subType: type || 'unknown',
|
|
153
|
+
sender: source,
|
|
154
|
+
response: recipient.diagnosticCode || '',
|
|
155
|
+
subject: message.mail.commonHeaders?.subject || '',
|
|
156
|
+
});
|
|
145
157
|
}
|
|
146
158
|
}
|
|
147
|
-
else {
|
|
148
|
-
console.log('[AWS BOUNCES] Message Body missing in bounce');
|
|
149
|
-
}
|
|
150
159
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
console.error('[AWS COMPLAINTS] Unknown organization for email address ' + source);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (type === 'virus' || type === 'fraud') {
|
|
166
|
+
console.error('[AWS COMPLAINTS] Received virus / fraud complaint!');
|
|
167
|
+
console.error('[AWS COMPLAINTS]', message.complaint);
|
|
168
|
+
if (STAMHOOFD.environment !== 'development') {
|
|
169
|
+
Email.sendWebmaster({
|
|
170
|
+
subject: 'Received a ' + type + ' email notification',
|
|
171
|
+
text: 'We received a ' + type + ' notification for an e-mail from the organization: ' + organization?.name + '. Please check and adjust if needed.\n',
|
|
172
|
+
});
|
|
155
173
|
}
|
|
156
174
|
}
|
|
157
175
|
}
|
|
176
|
+
else {
|
|
177
|
+
console.log('[AWS COMPLAINTS] Missing complaint field');
|
|
178
|
+
}
|
|
158
179
|
}
|
|
159
180
|
|
|
160
|
-
async function
|
|
161
|
-
if (
|
|
162
|
-
|
|
181
|
+
async function handleForward(message: any) {
|
|
182
|
+
if (message.mail && message.content && message.receipt) {
|
|
183
|
+
const content = message.content;
|
|
184
|
+
const receipt = message.receipt as {
|
|
185
|
+
recipients: string[];
|
|
186
|
+
spamVerdict: { status: 'PASS' | string };
|
|
187
|
+
virusVerdict: { status: 'PASS' | string };
|
|
188
|
+
spfVerdict: { status: 'PASS' | string };
|
|
189
|
+
dkimVerdict: { status: 'PASS' | string };
|
|
190
|
+
dmarcVerdict: { status: 'PASS' | string };
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const options = await ForwardHandler.handle(content, receipt);
|
|
194
|
+
if (options) {
|
|
195
|
+
if (STAMHOOFD.environment !== 'development') {
|
|
196
|
+
Email.send(options);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
console.log('[AWS FORWARDING] Missing mail, content or receipt field');
|
|
163
202
|
}
|
|
203
|
+
}
|
|
164
204
|
|
|
165
|
-
|
|
205
|
+
async function readFromQueue(queueUrl: string) {
|
|
206
|
+
console.log('[AWS Queue] Checking ' + queueUrl);
|
|
166
207
|
const sqs = new AWS.SQS();
|
|
167
|
-
const messages = await sqs.receiveMessage({ QueueUrl:
|
|
208
|
+
const messages = await sqs.receiveMessage({ QueueUrl: queueUrl, MaxNumberOfMessages: 10 }).promise();
|
|
209
|
+
let didProcess = 0;
|
|
168
210
|
if (messages.Messages) {
|
|
169
211
|
for (const message of messages.Messages) {
|
|
170
|
-
console.log('Received message
|
|
212
|
+
console.log('[AWS Queue] Received message');
|
|
213
|
+
console.log('[AWS Queue]', message);
|
|
171
214
|
|
|
172
215
|
if (message.ReceiptHandle) {
|
|
173
|
-
if (STAMHOOFD.environment
|
|
216
|
+
if (STAMHOOFD.environment !== 'development') {
|
|
174
217
|
await sqs.deleteMessage({
|
|
175
|
-
QueueUrl:
|
|
218
|
+
QueueUrl: queueUrl,
|
|
176
219
|
ReceiptHandle: message.ReceiptHandle,
|
|
177
220
|
}).promise();
|
|
178
|
-
console.log('Deleted from queue');
|
|
221
|
+
console.log('[AWS Queue] Deleted from queue');
|
|
179
222
|
}
|
|
180
223
|
}
|
|
224
|
+
didProcess += 1;
|
|
181
225
|
|
|
182
226
|
try {
|
|
183
227
|
if (message.Body) {
|
|
@@ -187,138 +231,82 @@ async function checkReplies() {
|
|
|
187
231
|
if (bounce.Message) {
|
|
188
232
|
const message = JSON.parse(bounce.Message);
|
|
189
233
|
|
|
234
|
+
// Docs: https://docs.aws.amazon.com/ses/latest/dg/event-publishing-retrieving-sns-contents.html
|
|
235
|
+
if (message.bounce) {
|
|
236
|
+
await handleBounce(message);
|
|
237
|
+
}
|
|
238
|
+
else if (message.complaint) {
|
|
239
|
+
await handleComplaint(message);
|
|
240
|
+
}
|
|
190
241
|
// Read message content
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
spfVerdict: { status: 'PASS' | string };
|
|
198
|
-
dkimVerdict: { status: 'PASS' | string };
|
|
199
|
-
dmarcVerdict: { status: 'PASS' | string };
|
|
200
|
-
};
|
|
201
|
-
|
|
202
|
-
const options = await ForwardHandler.handle(content, receipt);
|
|
203
|
-
if (options) {
|
|
204
|
-
if (STAMHOOFD.environment === 'production') {
|
|
205
|
-
Email.send(options);
|
|
206
|
-
}
|
|
207
|
-
}
|
|
242
|
+
// https://docs.aws.amazon.com/ses/latest/dg/receiving-email-notifications-contents.html
|
|
243
|
+
else if (message.mail && message.content && message.receipt) {
|
|
244
|
+
await handleForward(message);
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
console.log('[AWS Queue] Unsupported message');
|
|
208
248
|
}
|
|
209
249
|
}
|
|
250
|
+
else {
|
|
251
|
+
console.log("[AWS Queue] 'Message' field missing");
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
console.log('[AWS Queue] Message Body missing in bounce');
|
|
210
256
|
}
|
|
211
257
|
}
|
|
212
258
|
catch (e) {
|
|
213
|
-
console.
|
|
259
|
+
console.log('[AWS Queue] Message processing failed:');
|
|
260
|
+
console.log('[AWS Queue]', e);
|
|
261
|
+
|
|
262
|
+
console.error('[AWS Queue] Message processing failed:');
|
|
263
|
+
console.error('[AWS Queue]', e);
|
|
214
264
|
}
|
|
215
265
|
}
|
|
216
266
|
}
|
|
267
|
+
|
|
268
|
+
if (didProcess) {
|
|
269
|
+
console.log(`[AWS Queue] Processed ${didProcess} message(s) from queue`);
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
console.log(`[AWS Queue] No message to process from queue`);
|
|
273
|
+
}
|
|
274
|
+
return didProcess;
|
|
217
275
|
}
|
|
218
276
|
|
|
219
|
-
async function
|
|
220
|
-
|
|
277
|
+
async function readAllFromQueue(queueUrl: string) {
|
|
278
|
+
let readCount = 0;
|
|
279
|
+
while (readCount < 20) {
|
|
280
|
+
const didProcess = await readFromQueue(queueUrl);
|
|
281
|
+
if (!didProcess) {
|
|
282
|
+
break;
|
|
283
|
+
}
|
|
284
|
+
readCount += didProcess;
|
|
285
|
+
}
|
|
286
|
+
console.log(`[AWS Queue] Finished processing all messages from queue (${readCount} messages processed)`);
|
|
287
|
+
return true;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async function checkBounces() {
|
|
291
|
+
if (!STAMHOOFD.AWS_ACCESS_KEY_ID || !STAMHOOFD.AWS_BOUNCE_QUEUE_URL) {
|
|
221
292
|
return;
|
|
222
293
|
}
|
|
223
294
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
const messages = await sqs.receiveMessage({ QueueUrl: 'https://sqs.eu-west-1.amazonaws.com/118244293157/stamhoofd-complaints-queue', MaxNumberOfMessages: 10 }).promise();
|
|
227
|
-
if (messages.Messages) {
|
|
228
|
-
for (const message of messages.Messages) {
|
|
229
|
-
console.log('[AWS COMPLAINTS] Received complaint message');
|
|
230
|
-
console.log('[AWS COMPLAINTS]', message);
|
|
295
|
+
await readAllFromQueue(STAMHOOFD.AWS_BOUNCE_QUEUE_URL);
|
|
296
|
+
}
|
|
231
297
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
ReceiptHandle: message.ReceiptHandle,
|
|
237
|
-
}).promise();
|
|
238
|
-
console.log('[AWS COMPLAINTS] Deleted from queue');
|
|
239
|
-
}
|
|
240
|
-
}
|
|
298
|
+
async function checkReplies() {
|
|
299
|
+
if (!STAMHOOFD.AWS_ACCESS_KEY_ID || !STAMHOOFD.AWS_FORWARDING_QUEUE_URL) {
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
241
302
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
if (complaint.Message) {
|
|
249
|
-
const message = JSON.parse(complaint.Message);
|
|
250
|
-
|
|
251
|
-
if (message.complaint) {
|
|
252
|
-
const b = message.complaint;
|
|
253
|
-
const source = message.mail.source;
|
|
254
|
-
const organization: Organization | undefined = source ? await Organization.getByEmail(source) : undefined;
|
|
255
|
-
|
|
256
|
-
const type: 'abuse' | 'auth-failure' | 'fraud' | 'not-spam' | 'other' | 'virus' = b.complaintFeedbackType;
|
|
257
|
-
|
|
258
|
-
if (organization) {
|
|
259
|
-
for (const recipient of b.complainedRecipients) {
|
|
260
|
-
const email = recipient.emailAddress;
|
|
261
|
-
const emailAddress = await EmailAddress.getOrCreate(email, organization.id);
|
|
262
|
-
emailAddress.markedAsSpam = type !== 'not-spam';
|
|
263
|
-
await emailAddress.save();
|
|
264
|
-
|
|
265
|
-
if (type !== 'not-spam') {
|
|
266
|
-
if (type === 'virus' || type === 'fraud') {
|
|
267
|
-
await saveLog({
|
|
268
|
-
id: b.feedbackId,
|
|
269
|
-
email: source,
|
|
270
|
-
organization,
|
|
271
|
-
type: AuditLogType.EmailAddressFraudComplaint,
|
|
272
|
-
subType: type || 'unknown',
|
|
273
|
-
sender: source,
|
|
274
|
-
response: b.diagnosticCode || '',
|
|
275
|
-
subject: message.mail.commonHeaders?.subject || '',
|
|
276
|
-
});
|
|
277
|
-
}
|
|
278
|
-
else {
|
|
279
|
-
await saveLog({
|
|
280
|
-
id: b.feedbackId,
|
|
281
|
-
email: source,
|
|
282
|
-
organization,
|
|
283
|
-
type: AuditLogType.EmailAddressMarkedAsSpam,
|
|
284
|
-
subType: type || 'unknown',
|
|
285
|
-
sender: source,
|
|
286
|
-
response: b.diagnosticCode || '',
|
|
287
|
-
subject: message.mail.commonHeaders?.subject || '',
|
|
288
|
-
});
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
else {
|
|
294
|
-
console.error('[AWS COMPLAINTS] Unknown organization for email address ' + source);
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
if (type === 'virus' || type === 'fraud') {
|
|
298
|
-
console.error('[AWS COMPLAINTS] Received virus / fraud complaint!');
|
|
299
|
-
console.error('[AWS COMPLAINTS]', complaint);
|
|
300
|
-
if (STAMHOOFD.environment === 'production') {
|
|
301
|
-
Email.sendWebmaster({
|
|
302
|
-
subject: 'Received a ' + type + ' email notification',
|
|
303
|
-
text: 'We received a ' + type + ' notification for an e-mail from the organization: ' + organization?.name + '. Please check and adjust if needed.\n',
|
|
304
|
-
});
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
else {
|
|
309
|
-
console.log('[AWS COMPLAINTS] Missing complaint field');
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
else {
|
|
313
|
-
console.log('[AWS COMPLAINTS] Missing message field in complaint');
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
catch (e) {
|
|
318
|
-
console.log('[AWS COMPLAINTS] Complain message processing failed:');
|
|
319
|
-
console.error('[AWS COMPLAINTS] Complain message processing failed:');
|
|
320
|
-
console.error('[AWS COMPLAINTS]', e);
|
|
321
|
-
}
|
|
322
|
-
}
|
|
303
|
+
await readAllFromQueue(STAMHOOFD.AWS_FORWARDING_QUEUE_URL);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
async function checkComplaints() {
|
|
307
|
+
if (!STAMHOOFD.AWS_ACCESS_KEY_ID || !STAMHOOFD.AWS_COMPLAINTS_QUEUE_URL) {
|
|
308
|
+
return;
|
|
323
309
|
}
|
|
310
|
+
|
|
311
|
+
await readAllFromQueue(STAMHOOFD.AWS_COMPLAINTS_QUEUE_URL);
|
|
324
312
|
}
|
|
@@ -4,7 +4,7 @@ import { clearExcelCacheHelper } from './clearExcelCache';
|
|
|
4
4
|
|
|
5
5
|
const testPath = '/Users/user/project/backend/app/api/.cache';
|
|
6
6
|
jest.mock('fs/promises');
|
|
7
|
-
const fsMock = jest.mocked(fs, true);
|
|
7
|
+
const fsMock = jest.mocked(fs, { shallow: true });
|
|
8
8
|
|
|
9
9
|
describe('clearExcelCacheHelper', () => {
|
|
10
10
|
it('should only run between 3 and 6 AM', async () => {
|