@stamhoofd/backend 2.79.8 → 2.80.1
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 +11 -11
- package/src/audit-logs/ModelLogger.ts +2 -2
- package/src/crons/amazon-ses.ts +207 -229
- 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/email/GetEmailAddressEndpoint.ts +1 -1
- 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 +7 -7
- package/src/endpoints/organization/dashboard/organization/SetOrganizationDomainEndpoint.ts +10 -3
- 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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stamhoofd/backend",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.80.1",
|
|
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.1",
|
|
42
|
+
"@stamhoofd/backend-middleware": "2.80.1",
|
|
43
|
+
"@stamhoofd/email": "2.80.1",
|
|
44
|
+
"@stamhoofd/models": "2.80.1",
|
|
45
|
+
"@stamhoofd/queues": "2.80.1",
|
|
46
|
+
"@stamhoofd/sql": "2.80.1",
|
|
47
|
+
"@stamhoofd/structures": "2.80.1",
|
|
48
|
+
"@stamhoofd/utility": "2.80.1",
|
|
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": "37793d55487f7e8792d7b97df20750f8bd5be9a3"
|
|
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,170 @@ 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
|
-
|
|
67
|
-
|
|
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
|
+
const emailAddress = await EmailAddress.getOrCreate(email, organization?.id ?? null);
|
|
76
|
+
emailAddress.hardBounce = true;
|
|
77
|
+
await emailAddress.save();
|
|
78
|
+
|
|
79
|
+
await saveLog({
|
|
80
|
+
id: b.feedbackId,
|
|
81
|
+
email,
|
|
82
|
+
organization,
|
|
83
|
+
type: AuditLogType.EmailAddressHardBounced,
|
|
84
|
+
subType: subtype || 'unknown',
|
|
85
|
+
sender: source,
|
|
86
|
+
response: recipient.diagnosticCode || '',
|
|
87
|
+
subject: message.mail.commonHeaders?.subject || '',
|
|
88
|
+
});
|
|
68
89
|
}
|
|
90
|
+
else if (
|
|
91
|
+
type === 'Transient'
|
|
92
|
+
) {
|
|
93
|
+
const organization: Organization | undefined = source ? await Organization.getByEmail(source) : undefined;
|
|
94
|
+
await saveLog({
|
|
95
|
+
id: b.feedbackId,
|
|
96
|
+
email,
|
|
97
|
+
organization,
|
|
98
|
+
type: AuditLogType.EmailAddressSoftBounced,
|
|
99
|
+
subType: subtype || 'unknown',
|
|
100
|
+
sender: source,
|
|
101
|
+
response: recipient.diagnosticCode || '',
|
|
102
|
+
subject: message.mail.commonHeaders?.subject || '',
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
console.log('[AWS BOUNCES] For domain ' + source);
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
console.log("[AWS BOUNCES] 'bounce' field missing in bounce message");
|
|
110
|
+
}
|
|
111
|
+
}
|
|
69
112
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|| (recipient.diagnosticCode as string).toLowerCase().includes('unable to lookup dns')
|
|
97
|
-
)
|
|
98
|
-
)
|
|
99
|
-
) {
|
|
100
|
-
const organization: Organization | undefined = source ? await Organization.getByEmail(source) : undefined;
|
|
101
|
-
if (organization) {
|
|
102
|
-
const emailAddress = await EmailAddress.getOrCreate(email, organization.id);
|
|
103
|
-
emailAddress.hardBounce = true;
|
|
104
|
-
await emailAddress.save();
|
|
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
|
-
}
|
|
142
|
-
}
|
|
143
|
-
else {
|
|
144
|
-
console.log("[AWS BOUNCES] 'Message' field missing in bounce message");
|
|
145
|
-
}
|
|
113
|
+
async function handleComplaint(message: any) {
|
|
114
|
+
if (message.complaint) {
|
|
115
|
+
const b = message.complaint;
|
|
116
|
+
const source = message.mail.source;
|
|
117
|
+
const organization: Organization | undefined = source ? await Organization.getByEmail(source) : undefined;
|
|
118
|
+
|
|
119
|
+
const type: 'abuse' | 'auth-failure' | 'fraud' | 'not-spam' | 'other' | 'virus' = b.complaintFeedbackType;
|
|
120
|
+
|
|
121
|
+
for (const recipient of b.complainedRecipients) {
|
|
122
|
+
const email = recipient.emailAddress;
|
|
123
|
+
const emailAddress = await EmailAddress.getOrCreate(email, organization?.id ?? null);
|
|
124
|
+
emailAddress.markedAsSpam = type !== 'not-spam';
|
|
125
|
+
await emailAddress.save();
|
|
126
|
+
|
|
127
|
+
if (type !== 'not-spam') {
|
|
128
|
+
if (type === 'virus' || type === 'fraud') {
|
|
129
|
+
await saveLog({
|
|
130
|
+
id: b.feedbackId,
|
|
131
|
+
email: source,
|
|
132
|
+
organization,
|
|
133
|
+
type: AuditLogType.EmailAddressFraudComplaint,
|
|
134
|
+
subType: type || 'unknown',
|
|
135
|
+
sender: source,
|
|
136
|
+
response: recipient.diagnosticCode || '',
|
|
137
|
+
subject: message.mail.commonHeaders?.subject || '',
|
|
138
|
+
});
|
|
146
139
|
}
|
|
147
140
|
else {
|
|
148
|
-
|
|
141
|
+
await saveLog({
|
|
142
|
+
id: b.feedbackId,
|
|
143
|
+
email: source,
|
|
144
|
+
organization,
|
|
145
|
+
type: AuditLogType.EmailAddressMarkedAsSpam,
|
|
146
|
+
subType: type || 'unknown',
|
|
147
|
+
sender: source,
|
|
148
|
+
response: recipient.diagnosticCode || '',
|
|
149
|
+
subject: message.mail.commonHeaders?.subject || '',
|
|
150
|
+
});
|
|
149
151
|
}
|
|
150
152
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (type === 'virus' || type === 'fraud') {
|
|
156
|
+
console.error('[AWS COMPLAINTS] Received virus / fraud complaint!');
|
|
157
|
+
console.error('[AWS COMPLAINTS]', message.complaint);
|
|
158
|
+
if (STAMHOOFD.environment !== 'development') {
|
|
159
|
+
Email.sendWebmaster({
|
|
160
|
+
subject: 'Received a ' + type + ' email notification',
|
|
161
|
+
text: 'We received a ' + type + ' notification for an e-mail from the organization: ' + organization?.name + '. Please check and adjust if needed.\n',
|
|
162
|
+
});
|
|
155
163
|
}
|
|
156
164
|
}
|
|
157
165
|
}
|
|
166
|
+
else {
|
|
167
|
+
console.log('[AWS COMPLAINTS] Missing complaint field');
|
|
168
|
+
}
|
|
158
169
|
}
|
|
159
170
|
|
|
160
|
-
async function
|
|
161
|
-
if (
|
|
162
|
-
|
|
171
|
+
async function handleForward(message: any) {
|
|
172
|
+
if (message.mail && message.content && message.receipt) {
|
|
173
|
+
const content = message.content;
|
|
174
|
+
const receipt = message.receipt as {
|
|
175
|
+
recipients: string[];
|
|
176
|
+
spamVerdict: { status: 'PASS' | string };
|
|
177
|
+
virusVerdict: { status: 'PASS' | string };
|
|
178
|
+
spfVerdict: { status: 'PASS' | string };
|
|
179
|
+
dkimVerdict: { status: 'PASS' | string };
|
|
180
|
+
dmarcVerdict: { status: 'PASS' | string };
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const options = await ForwardHandler.handle(content, receipt);
|
|
184
|
+
if (options) {
|
|
185
|
+
if (STAMHOOFD.environment !== 'development') {
|
|
186
|
+
Email.send(options);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
console.log('[AWS FORWARDING] Missing mail, content or receipt field');
|
|
163
192
|
}
|
|
193
|
+
}
|
|
164
194
|
|
|
165
|
-
|
|
195
|
+
async function readFromQueue(queueUrl: string) {
|
|
196
|
+
console.log('[AWS Queue] Checking ' + queueUrl);
|
|
166
197
|
const sqs = new AWS.SQS();
|
|
167
|
-
const messages = await sqs.receiveMessage({ QueueUrl:
|
|
198
|
+
const messages = await sqs.receiveMessage({ QueueUrl: queueUrl, MaxNumberOfMessages: 10 }).promise();
|
|
199
|
+
let didProcess = 0;
|
|
168
200
|
if (messages.Messages) {
|
|
169
201
|
for (const message of messages.Messages) {
|
|
170
|
-
console.log('Received message
|
|
202
|
+
console.log('[AWS Queue] Received message');
|
|
203
|
+
console.log('[AWS Queue]', message);
|
|
171
204
|
|
|
172
205
|
if (message.ReceiptHandle) {
|
|
173
|
-
if (STAMHOOFD.environment
|
|
206
|
+
if (STAMHOOFD.environment !== 'development') {
|
|
174
207
|
await sqs.deleteMessage({
|
|
175
|
-
QueueUrl:
|
|
208
|
+
QueueUrl: queueUrl,
|
|
176
209
|
ReceiptHandle: message.ReceiptHandle,
|
|
177
210
|
}).promise();
|
|
178
|
-
console.log('Deleted from queue');
|
|
211
|
+
console.log('[AWS Queue] Deleted from queue');
|
|
179
212
|
}
|
|
180
213
|
}
|
|
214
|
+
didProcess += 1;
|
|
181
215
|
|
|
182
216
|
try {
|
|
183
217
|
if (message.Body) {
|
|
@@ -187,138 +221,82 @@ async function checkReplies() {
|
|
|
187
221
|
if (bounce.Message) {
|
|
188
222
|
const message = JSON.parse(bounce.Message);
|
|
189
223
|
|
|
224
|
+
// Docs: https://docs.aws.amazon.com/ses/latest/dg/event-publishing-retrieving-sns-contents.html
|
|
225
|
+
if (message.bounce) {
|
|
226
|
+
await handleBounce(message);
|
|
227
|
+
}
|
|
228
|
+
else if (message.complaint) {
|
|
229
|
+
await handleComplaint(message);
|
|
230
|
+
}
|
|
190
231
|
// Read message content
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
recipients: string[];
|
|
195
|
-
spamVerdict: { status: 'PASS' | string };
|
|
196
|
-
virusVerdict: { status: 'PASS' | string };
|
|
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
|
-
}
|
|
232
|
+
// https://docs.aws.amazon.com/ses/latest/dg/receiving-email-notifications-contents.html
|
|
233
|
+
else if (message.mail && message.content && message.receipt) {
|
|
234
|
+
await handleForward(message);
|
|
208
235
|
}
|
|
236
|
+
else {
|
|
237
|
+
console.log('[AWS Queue] Unsupported message');
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
console.log("[AWS Queue] 'Message' field missing");
|
|
209
242
|
}
|
|
210
243
|
}
|
|
244
|
+
else {
|
|
245
|
+
console.log('[AWS Queue] Message Body missing in bounce');
|
|
246
|
+
}
|
|
211
247
|
}
|
|
212
248
|
catch (e) {
|
|
213
|
-
console.
|
|
249
|
+
console.log('[AWS Queue] Message processing failed:');
|
|
250
|
+
console.log('[AWS Queue]', e);
|
|
251
|
+
|
|
252
|
+
console.error('[AWS Queue] Message processing failed:');
|
|
253
|
+
console.error('[AWS Queue]', e);
|
|
214
254
|
}
|
|
215
255
|
}
|
|
216
256
|
}
|
|
257
|
+
|
|
258
|
+
if (didProcess) {
|
|
259
|
+
console.log(`[AWS Queue] Processed ${didProcess} message(s) from queue`);
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
console.log(`[AWS Queue] No message to process from queue`);
|
|
263
|
+
}
|
|
264
|
+
return didProcess;
|
|
217
265
|
}
|
|
218
266
|
|
|
219
|
-
async function
|
|
220
|
-
|
|
267
|
+
async function readAllFromQueue(queueUrl: string) {
|
|
268
|
+
let readCount = 0;
|
|
269
|
+
while (readCount < 20) {
|
|
270
|
+
const didProcess = await readFromQueue(queueUrl);
|
|
271
|
+
if (!didProcess) {
|
|
272
|
+
break;
|
|
273
|
+
}
|
|
274
|
+
readCount += didProcess;
|
|
275
|
+
}
|
|
276
|
+
console.log(`[AWS Queue] Finished processing all messages from queue (${readCount} messages processed)`);
|
|
277
|
+
return true;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async function checkBounces() {
|
|
281
|
+
if (!STAMHOOFD.AWS_ACCESS_KEY_ID || !STAMHOOFD.AWS_BOUNCE_QUEUE_URL) {
|
|
221
282
|
return;
|
|
222
283
|
}
|
|
223
284
|
|
|
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);
|
|
285
|
+
await readAllFromQueue(STAMHOOFD.AWS_BOUNCE_QUEUE_URL);
|
|
286
|
+
}
|
|
231
287
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
ReceiptHandle: message.ReceiptHandle,
|
|
237
|
-
}).promise();
|
|
238
|
-
console.log('[AWS COMPLAINTS] Deleted from queue');
|
|
239
|
-
}
|
|
240
|
-
}
|
|
288
|
+
async function checkReplies() {
|
|
289
|
+
if (!STAMHOOFD.AWS_ACCESS_KEY_ID || !STAMHOOFD.AWS_FORWARDING_QUEUE_URL) {
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
241
292
|
|
|
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
|
-
}
|
|
293
|
+
await readAllFromQueue(STAMHOOFD.AWS_FORWARDING_QUEUE_URL);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
async function checkComplaints() {
|
|
297
|
+
if (!STAMHOOFD.AWS_ACCESS_KEY_ID || !STAMHOOFD.AWS_COMPLAINTS_QUEUE_URL) {
|
|
298
|
+
return;
|
|
323
299
|
}
|
|
300
|
+
|
|
301
|
+
await readAllFromQueue(STAMHOOFD.AWS_COMPLAINTS_QUEUE_URL);
|
|
324
302
|
}
|
|
@@ -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 () => {
|