@stamhoofd/backend 2.60.0 → 2.61.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 +10 -10
- package/src/audit-logs/EmailAddressLogger.ts +45 -0
- package/src/audit-logs/EmailLogger.ts +67 -0
- package/src/audit-logs/EmailTemplateLogger.ts +33 -0
- package/src/audit-logs/ModelLogger.ts +22 -6
- package/src/audit-logs/OrderLogger.ts +1 -1
- package/src/audit-logs/UserLogger.ts +45 -0
- package/src/crons/amazon-ses.ts +324 -0
- package/src/crons/clearExcelCache.ts +3 -0
- package/src/crons/endFunctionsOfUsersWithoutRegistration.ts +3 -0
- package/src/crons/index.ts +4 -0
- package/src/crons/postmark.ts +223 -0
- package/src/crons.ts +3 -315
- package/src/helpers/MemberUserSyncer.ts +11 -7
- package/src/services/AuditLogService.ts +8 -0
- package/src/services/diff.ts +10 -8
- package/src/crons/updateSetupSteps.ts +0 -9
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stamhoofd/backend",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.61.1",
|
|
4
4
|
"main": "./dist/index.js",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -37,14 +37,14 @@
|
|
|
37
37
|
"@simonbackx/simple-encoding": "2.18.0",
|
|
38
38
|
"@simonbackx/simple-endpoints": "1.15.0",
|
|
39
39
|
"@simonbackx/simple-logging": "^1.0.1",
|
|
40
|
-
"@stamhoofd/backend-i18n": "2.
|
|
41
|
-
"@stamhoofd/backend-middleware": "2.
|
|
42
|
-
"@stamhoofd/email": "2.
|
|
43
|
-
"@stamhoofd/models": "2.
|
|
44
|
-
"@stamhoofd/queues": "2.
|
|
45
|
-
"@stamhoofd/sql": "2.
|
|
46
|
-
"@stamhoofd/structures": "2.
|
|
47
|
-
"@stamhoofd/utility": "2.
|
|
40
|
+
"@stamhoofd/backend-i18n": "2.61.1",
|
|
41
|
+
"@stamhoofd/backend-middleware": "2.61.1",
|
|
42
|
+
"@stamhoofd/email": "2.61.1",
|
|
43
|
+
"@stamhoofd/models": "2.61.1",
|
|
44
|
+
"@stamhoofd/queues": "2.61.1",
|
|
45
|
+
"@stamhoofd/sql": "2.61.1",
|
|
46
|
+
"@stamhoofd/structures": "2.61.1",
|
|
47
|
+
"@stamhoofd/utility": "2.61.1",
|
|
48
48
|
"archiver": "^7.0.1",
|
|
49
49
|
"aws-sdk": "^2.885.0",
|
|
50
50
|
"axios": "1.6.8",
|
|
@@ -64,5 +64,5 @@
|
|
|
64
64
|
"publishConfig": {
|
|
65
65
|
"access": "public"
|
|
66
66
|
},
|
|
67
|
-
"gitHead": "
|
|
67
|
+
"gitHead": "f092f61cf3641682fb5a5a9cb50c7977b1e350ef"
|
|
68
68
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { EmailAddress } from '@stamhoofd/email';
|
|
2
|
+
import { AuditLogReplacement, AuditLogReplacementType, AuditLogType } from '@stamhoofd/structures';
|
|
3
|
+
import { ModelLogger } from './ModelLogger';
|
|
4
|
+
|
|
5
|
+
export const EmailAddressLogger = new ModelLogger(EmailAddress, {
|
|
6
|
+
async optionsGenerator(event) {
|
|
7
|
+
if (event.type === 'deleted') {
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const wasUnsubscribed = event.type === 'updated' ? !!event.originalFields.unsubscribedAll : false;
|
|
12
|
+
const isUnsubscribed = event.model.unsubscribedAll;
|
|
13
|
+
|
|
14
|
+
if (!wasUnsubscribed && isUnsubscribed) {
|
|
15
|
+
return {
|
|
16
|
+
type: AuditLogType.EmailAddressUnsubscribed,
|
|
17
|
+
data: {},
|
|
18
|
+
generatePatchList: false,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const wasUnsubscribedMarketing = event.type === 'updated' ? !!event.originalFields.unsubscribedMarketing : false;
|
|
23
|
+
const isUnsubscribedMarketing = event.model.unsubscribedMarketing;
|
|
24
|
+
|
|
25
|
+
if (!wasUnsubscribedMarketing && isUnsubscribedMarketing) {
|
|
26
|
+
return {
|
|
27
|
+
type: AuditLogType.EmailAddressUnsubscribed,
|
|
28
|
+
data: {},
|
|
29
|
+
generatePatchList: false,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
createReplacements(model, options) {
|
|
35
|
+
const map = new Map([
|
|
36
|
+
['e', AuditLogReplacement.create({
|
|
37
|
+
id: model.id,
|
|
38
|
+
value: model.email || '',
|
|
39
|
+
type: AuditLogReplacementType.EmailAddress,
|
|
40
|
+
})],
|
|
41
|
+
]);
|
|
42
|
+
return map;
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { Email, EmailRecipient, replaceHtml } from '@stamhoofd/models';
|
|
2
|
+
import { AuditLogReplacement, AuditLogReplacementType, AuditLogType, EmailStatus } from '@stamhoofd/structures';
|
|
3
|
+
import { Formatter } from '@stamhoofd/utility';
|
|
4
|
+
import { ModelLogger } from './ModelLogger';
|
|
5
|
+
|
|
6
|
+
export const EmailLogger = new ModelLogger(Email, {
|
|
7
|
+
async optionsGenerator(event) {
|
|
8
|
+
if (event.type === 'deleted') {
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
let oldStatus = EmailStatus.Draft;
|
|
12
|
+
|
|
13
|
+
if (event.type === 'updated') {
|
|
14
|
+
oldStatus = event.originalFields.status as EmailStatus;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const newStatus = event.model.status as EmailStatus;
|
|
18
|
+
if (newStatus === oldStatus) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (newStatus !== EmailStatus.Sent && newStatus !== EmailStatus.Sending) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (newStatus === EmailStatus.Sent) {
|
|
27
|
+
const recipient = await EmailRecipient.select().where('emailId', event.model.id).whereNot('sentAt', null).first(false);
|
|
28
|
+
// Get first recipient
|
|
29
|
+
return {
|
|
30
|
+
type: AuditLogType.EmailSent,
|
|
31
|
+
data: {
|
|
32
|
+
recipient,
|
|
33
|
+
},
|
|
34
|
+
generatePatchList: false,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
type: AuditLogType.EmailSending,
|
|
40
|
+
data: {
|
|
41
|
+
recipient: null,
|
|
42
|
+
},
|
|
43
|
+
generatePatchList: false,
|
|
44
|
+
};
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
createReplacements(model, options) {
|
|
48
|
+
const map = new Map([
|
|
49
|
+
['e', AuditLogReplacement.create({
|
|
50
|
+
id: model.id,
|
|
51
|
+
value: model.subject || '',
|
|
52
|
+
type: AuditLogReplacementType.Email,
|
|
53
|
+
})],
|
|
54
|
+
['c', AuditLogReplacement.create({
|
|
55
|
+
value: Formatter.integer(model.recipientCount ?? 0),
|
|
56
|
+
count: model.recipientCount ?? 0,
|
|
57
|
+
})],
|
|
58
|
+
]);
|
|
59
|
+
if (options.data.recipient) {
|
|
60
|
+
map.set('html', AuditLogReplacement.html(
|
|
61
|
+
replaceHtml(model.html ?? '', options.data.recipient?.replacements ?? []),
|
|
62
|
+
));
|
|
63
|
+
}
|
|
64
|
+
return map;
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { EmailTemplate } from '@stamhoofd/models';
|
|
2
|
+
import { AuditLogReplacement, AuditLogReplacementType, AuditLogType } from '@stamhoofd/structures';
|
|
3
|
+
import { getDefaultGenerator, ModelLogger } from './ModelLogger';
|
|
4
|
+
const defaultGenerator = getDefaultGenerator({
|
|
5
|
+
created: AuditLogType.EmailTemplateAdded,
|
|
6
|
+
updated: AuditLogType.EmailTemplateEdited,
|
|
7
|
+
deleted: AuditLogType.EmailTemplateDeleted,
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
export const EmailTemplateLogger = new ModelLogger(EmailTemplate, {
|
|
11
|
+
skipKeys: ['json', 'text'], // html uses a special rendering method
|
|
12
|
+
async optionsGenerator(event) {
|
|
13
|
+
const result = await defaultGenerator(event);
|
|
14
|
+
|
|
15
|
+
if (!result) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Manually inject helper methods to compare html
|
|
20
|
+
|
|
21
|
+
return result;
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
createReplacements(model) {
|
|
25
|
+
return new Map([
|
|
26
|
+
['e', AuditLogReplacement.create({
|
|
27
|
+
id: model.id,
|
|
28
|
+
value: model.type.includes('Saved') ? model.subject : model.type, // Translated in UI
|
|
29
|
+
type: AuditLogReplacementType.EmailTemplate,
|
|
30
|
+
})],
|
|
31
|
+
]);
|
|
32
|
+
},
|
|
33
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Model, ModelEvent } from '@simonbackx/simple-database';
|
|
2
2
|
import { AuditLog } from '@stamhoofd/models';
|
|
3
|
-
import { AuditLogReplacement, AuditLogSource, AuditLogType } from '@stamhoofd/structures';
|
|
3
|
+
import { AuditLogPatchItem, AuditLogPatchItemType, AuditLogReplacement, AuditLogSource, AuditLogType } from '@stamhoofd/structures';
|
|
4
4
|
import { ContextInstance } from '../helpers/Context';
|
|
5
5
|
import { AuditLogService } from '../services/AuditLogService';
|
|
6
6
|
import { diffUnknown } from '../services/diff';
|
|
@@ -24,6 +24,8 @@ type EventOptionsGenerator<M extends Model, D> = (event: ModelEvent<M>) => Model
|
|
|
24
24
|
export type ModelLoggerOptions<M extends Model, D = undefined> = {
|
|
25
25
|
optionsGenerator: EventOptionsGenerator<M, D>;
|
|
26
26
|
skipKeys?: string[];
|
|
27
|
+
sensitiveKeys?: string[];
|
|
28
|
+
renamedKeys?: Record<string, string>;
|
|
27
29
|
generateDescription?(event: ModelEvent<M>, options: ModelEventLogOptions<D>): string | null | undefined;
|
|
28
30
|
createReplacements?(model: M, options: ModelEventLogOptions<D>): Map<string, AuditLogReplacement>;
|
|
29
31
|
postProcess?(event: ModelEvent<M>, options: ModelEventLogOptions<D>, log: AuditLog): Promise<void> | void;
|
|
@@ -88,10 +90,13 @@ export class ModelLogger<ModelType extends typeof Model, M extends InstanceType<
|
|
|
88
90
|
model: ModelType;
|
|
89
91
|
optionsGenerator: EventOptionsGenerator<M, D>;
|
|
90
92
|
skipKeys: string[] = [];
|
|
93
|
+
sensitiveKeys: string[] = [];
|
|
94
|
+
renamedKeys: Record<string, string> = {};
|
|
91
95
|
generateDescription?: (event: ModelEvent<M>, options: ModelEventLogOptions<D>) => string | null | undefined;
|
|
92
96
|
createReplacements?: (model: M, options: ModelEventLogOptions<D>) => Map<string, AuditLogReplacement>;
|
|
93
97
|
postProcess?: (event: ModelEvent<M>, options: ModelEventLogOptions<D>, log: AuditLog) => Promise<void> | void;
|
|
94
98
|
static sharedSkipKeys = ['id', 'createdAt', 'updatedAt', 'deletedAt'];
|
|
99
|
+
static sharedSensitiveKeys = ['password'];
|
|
95
100
|
|
|
96
101
|
constructor(model: ModelType, options: ModelLoggerOptions<M, D>) {
|
|
97
102
|
this.model = model;
|
|
@@ -100,6 +105,8 @@ export class ModelLogger<ModelType extends typeof Model, M extends InstanceType<
|
|
|
100
105
|
this.generateDescription = options.generateDescription;
|
|
101
106
|
this.createReplacements = options.createReplacements;
|
|
102
107
|
this.postProcess = options.postProcess;
|
|
108
|
+
this.sensitiveKeys = options.sensitiveKeys ?? [];
|
|
109
|
+
this.renamedKeys = options.renamedKeys ?? {};
|
|
103
110
|
}
|
|
104
111
|
|
|
105
112
|
async logEvent(event: ModelEvent<M>) {
|
|
@@ -150,11 +157,20 @@ export class ModelLogger<ModelType extends typeof Model, M extends InstanceType<
|
|
|
150
157
|
// Ignore relations
|
|
151
158
|
continue;
|
|
152
159
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
160
|
+
const renamedKey = this.renamedKeys?.[key] ?? key;
|
|
161
|
+
if (ModelLogger.sharedSensitiveKeys.includes(key) || (this.skipKeys && this.skipKeys.includes(key))) {
|
|
162
|
+
log.patchList.push(AuditLogPatchItem.create({
|
|
163
|
+
key: AuditLogReplacement.key(renamedKey),
|
|
164
|
+
type: AuditLogPatchItemType.Changed,
|
|
165
|
+
}));
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
log.patchList.push(...diffUnknown(
|
|
169
|
+
key in oldModel ? oldModel[key] : undefined,
|
|
170
|
+
key in event.model ? event.model[key] : undefined,
|
|
171
|
+
AuditLogReplacement.key(renamedKey),
|
|
172
|
+
));
|
|
173
|
+
}
|
|
158
174
|
}
|
|
159
175
|
|
|
160
176
|
// Remove skipped keys
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Order, Webshop } from '@stamhoofd/models';
|
|
2
|
-
import { getDefaultGenerator, ModelLogger } from './ModelLogger';
|
|
3
2
|
import { AuditLogReplacement, AuditLogReplacementType, AuditLogType, OrderStatus } from '@stamhoofd/structures';
|
|
3
|
+
import { getDefaultGenerator, ModelLogger } from './ModelLogger';
|
|
4
4
|
|
|
5
5
|
const defaultGenerator = getDefaultGenerator({
|
|
6
6
|
created: AuditLogType.OrderAdded,
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { User } from '@stamhoofd/models';
|
|
2
|
+
import { AuditLogReplacement, AuditLogReplacementType, AuditLogType } from '@stamhoofd/structures';
|
|
3
|
+
import { getDefaultGenerator, ModelLogger } from './ModelLogger';
|
|
4
|
+
|
|
5
|
+
const defaultGenerator = getDefaultGenerator({
|
|
6
|
+
created: AuditLogType.UserAdded,
|
|
7
|
+
updated: AuditLogType.UserEdited,
|
|
8
|
+
deleted: AuditLogType.UserDeleted,
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
export const UserLogger = new ModelLogger(User, {
|
|
12
|
+
skipKeys: ['meta'],
|
|
13
|
+
sensitiveKeys: ['password'],
|
|
14
|
+
renamedKeys: {
|
|
15
|
+
memberId: 'linkedMember',
|
|
16
|
+
verified: 'emailVerified',
|
|
17
|
+
},
|
|
18
|
+
async optionsGenerator(event) {
|
|
19
|
+
const result = await defaultGenerator(event);
|
|
20
|
+
|
|
21
|
+
if (!result) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (!event.model.hasAccount() && !event.model.permissions) {
|
|
26
|
+
// Do not log changes to placeholder users
|
|
27
|
+
|
|
28
|
+
if (event.type !== 'updated' || (event.originalFields.permissions === null)) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return result;
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
createReplacements: (model, options) => {
|
|
37
|
+
return new Map([
|
|
38
|
+
['u', AuditLogReplacement.create({
|
|
39
|
+
id: model.id,
|
|
40
|
+
value: model.email,
|
|
41
|
+
type: AuditLogReplacementType.User,
|
|
42
|
+
})],
|
|
43
|
+
]);
|
|
44
|
+
},
|
|
45
|
+
});
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
|
2
|
+
import { registerCron } from '@stamhoofd/crons';
|
|
3
|
+
import { Email, EmailAddress } from '@stamhoofd/email';
|
|
4
|
+
import { AuditLog, Organization } from '@stamhoofd/models';
|
|
5
|
+
import { AuditLogReplacement, AuditLogReplacementType, AuditLogSource, AuditLogType } from '@stamhoofd/structures';
|
|
6
|
+
import AWS from 'aws-sdk';
|
|
7
|
+
import { ForwardHandler } from '../helpers/ForwardHandler';
|
|
8
|
+
|
|
9
|
+
registerCron('checkComplaints', checkComplaints);
|
|
10
|
+
registerCron('checkReplies', checkReplies);
|
|
11
|
+
registerCron('checkBounces', checkBounces);
|
|
12
|
+
|
|
13
|
+
async function saveLog({ email, organization, type, subType, subject, response, sender, id }: { id: string; email: string; organization: Organization | undefined; type: AuditLogType; subType?: string; subject: string; response: string; sender: string }) {
|
|
14
|
+
if (!id || typeof id !== 'string') {
|
|
15
|
+
throw new Error('Invalid AWS SES id received');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const log = new AuditLog();
|
|
19
|
+
log.organizationId = organization?.id ?? null;
|
|
20
|
+
log.externalId = 'aws-ses-message-' + id.toString();
|
|
21
|
+
log.type = type;
|
|
22
|
+
log.source = AuditLogSource.System;
|
|
23
|
+
log.objectId = email;
|
|
24
|
+
log.replacements = new Map([
|
|
25
|
+
['e', AuditLogReplacement.create({
|
|
26
|
+
value: email || '',
|
|
27
|
+
type: AuditLogReplacementType.EmailAddress,
|
|
28
|
+
})],
|
|
29
|
+
['subType', AuditLogReplacement.key(subType || 'unknown')],
|
|
30
|
+
['response', AuditLogReplacement.longText(response)],
|
|
31
|
+
['subject', AuditLogReplacement.string(subject)],
|
|
32
|
+
['sender', AuditLogReplacement.create({
|
|
33
|
+
value: sender,
|
|
34
|
+
type: AuditLogReplacementType.EmailAddress,
|
|
35
|
+
})],
|
|
36
|
+
]);
|
|
37
|
+
// Check if we already logged this bounce
|
|
38
|
+
const existing = await AuditLog.select().where('externalId', log.externalId).first(false);
|
|
39
|
+
if (existing) {
|
|
40
|
+
console.log('Already logged this bounce, skipping');
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
await log.save();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function checkBounces() {
|
|
48
|
+
if (STAMHOOFD.environment !== 'production' || !STAMHOOFD.AWS_ACCESS_KEY_ID) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
console.log('[AWS BOUNCES] Checking bounces from AWS SQS');
|
|
53
|
+
const sqs = new AWS.SQS();
|
|
54
|
+
const messages = await sqs.receiveMessage({ QueueUrl: 'https://sqs.eu-west-1.amazonaws.com/118244293157/stamhoofd-bounces-queue', MaxNumberOfMessages: 10 }).promise();
|
|
55
|
+
if (messages.Messages) {
|
|
56
|
+
for (const message of messages.Messages) {
|
|
57
|
+
console.log('[AWS BOUNCES] Received bounce message');
|
|
58
|
+
console.log('[AWS BOUNCES]', message);
|
|
59
|
+
|
|
60
|
+
if (message.ReceiptHandle) {
|
|
61
|
+
if (STAMHOOFD.environment === 'production') {
|
|
62
|
+
await sqs.deleteMessage({
|
|
63
|
+
QueueUrl: 'https://sqs.eu-west-1.amazonaws.com/118244293157/stamhoofd-bounces-queue',
|
|
64
|
+
ReceiptHandle: message.ReceiptHandle,
|
|
65
|
+
}).promise();
|
|
66
|
+
console.log('[AWS BOUNCES] Deleted from queue');
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
if (message.Body) {
|
|
72
|
+
// decode the JSON value
|
|
73
|
+
const bounce = JSON.parse(message.Body);
|
|
74
|
+
|
|
75
|
+
if (bounce.Message) {
|
|
76
|
+
const message = JSON.parse(bounce.Message);
|
|
77
|
+
|
|
78
|
+
if (message.bounce) {
|
|
79
|
+
const b = message.bounce;
|
|
80
|
+
// Block all receivers that generate a permanent bounce
|
|
81
|
+
const type = b.bounceType;
|
|
82
|
+
const subtype = b.bounceSubType;
|
|
83
|
+
|
|
84
|
+
const source = message.mail.source;
|
|
85
|
+
|
|
86
|
+
// try to find organization that is responsible for this e-mail address
|
|
87
|
+
|
|
88
|
+
for (const recipient of b.bouncedRecipients) {
|
|
89
|
+
const email = recipient.emailAddress;
|
|
90
|
+
|
|
91
|
+
if (
|
|
92
|
+
type === 'Permanent'
|
|
93
|
+
|| (
|
|
94
|
+
recipient.diagnosticCode && (
|
|
95
|
+
(recipient.diagnosticCode as string).toLowerCase().includes('invalid domain')
|
|
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
|
+
}
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
console.log('[AWS BOUNCES] Message Body missing in bounce');
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
catch (e) {
|
|
152
|
+
console.log('[AWS BOUNCES] Bounce message processing failed:');
|
|
153
|
+
console.error('[AWS BOUNCES] Bounce message processing failed:');
|
|
154
|
+
console.error('[AWS BOUNCES]', e);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function checkReplies() {
|
|
161
|
+
if (STAMHOOFD.environment !== 'production' || !STAMHOOFD.AWS_ACCESS_KEY_ID) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
console.log('Checking replies from AWS SQS');
|
|
166
|
+
const sqs = new AWS.SQS();
|
|
167
|
+
const messages = await sqs.receiveMessage({ QueueUrl: 'https://sqs.eu-west-1.amazonaws.com/118244293157/stamhoofd-email-forwarding', MaxNumberOfMessages: 10 }).promise();
|
|
168
|
+
if (messages.Messages) {
|
|
169
|
+
for (const message of messages.Messages) {
|
|
170
|
+
console.log('Received message from forwarding queue');
|
|
171
|
+
|
|
172
|
+
if (message.ReceiptHandle) {
|
|
173
|
+
if (STAMHOOFD.environment === 'production') {
|
|
174
|
+
await sqs.deleteMessage({
|
|
175
|
+
QueueUrl: 'https://sqs.eu-west-1.amazonaws.com/118244293157/stamhoofd-email-forwarding',
|
|
176
|
+
ReceiptHandle: message.ReceiptHandle,
|
|
177
|
+
}).promise();
|
|
178
|
+
console.log('Deleted from queue');
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
if (message.Body) {
|
|
184
|
+
// decode the JSON value
|
|
185
|
+
const bounce = JSON.parse(message.Body);
|
|
186
|
+
|
|
187
|
+
if (bounce.Message) {
|
|
188
|
+
const message = JSON.parse(bounce.Message);
|
|
189
|
+
|
|
190
|
+
// Read message content
|
|
191
|
+
if (message.mail && message.content && message.receipt) {
|
|
192
|
+
const content = message.content;
|
|
193
|
+
const receipt = message.receipt as {
|
|
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
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
catch (e) {
|
|
213
|
+
console.error(e);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async function checkComplaints() {
|
|
220
|
+
if (STAMHOOFD.environment !== 'production' || !STAMHOOFD.AWS_ACCESS_KEY_ID) {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
console.log('[AWS COMPLAINTS] Checking complaints from AWS SQS');
|
|
225
|
+
const sqs = new AWS.SQS();
|
|
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);
|
|
231
|
+
|
|
232
|
+
if (message.ReceiptHandle) {
|
|
233
|
+
if (STAMHOOFD.environment === 'production') {
|
|
234
|
+
await sqs.deleteMessage({
|
|
235
|
+
QueueUrl: 'https://sqs.eu-west-1.amazonaws.com/118244293157/stamhoofd-complaints-queue',
|
|
236
|
+
ReceiptHandle: message.ReceiptHandle,
|
|
237
|
+
}).promise();
|
|
238
|
+
console.log('[AWS COMPLAINTS] Deleted from queue');
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
try {
|
|
243
|
+
if (message.Body) {
|
|
244
|
+
// decode the JSON value
|
|
245
|
+
const complaint = JSON.parse(message.Body);
|
|
246
|
+
console.log('[AWS COMPLAINTS]', complaint);
|
|
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
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
import { registerCron } from '@stamhoofd/crons';
|
|
1
2
|
import fs from 'fs/promises';
|
|
2
3
|
|
|
3
4
|
const msIn22Hours = 79200000;
|
|
4
5
|
let lastExcelClear: number | null = null;
|
|
5
6
|
|
|
7
|
+
registerCron('clearExcelCache', clearExcelCache);
|
|
8
|
+
|
|
6
9
|
export async function clearExcelCache() {
|
|
7
10
|
const now = new Date();
|
|
8
11
|
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
import { registerCron } from '@stamhoofd/crons';
|
|
1
2
|
import { FlagMomentCleanup } from '../helpers/FlagMomentCleanup';
|
|
2
3
|
|
|
3
4
|
let lastCleanupYear: number = -1;
|
|
4
5
|
let lastCleanupMonth: number = -1;
|
|
5
6
|
|
|
7
|
+
registerCron('endFunctionsOfUsersWithoutRegistration', endFunctionsOfUsersWithoutRegistration);
|
|
8
|
+
|
|
6
9
|
export async function endFunctionsOfUsersWithoutRegistration() {
|
|
7
10
|
const now = new Date();
|
|
8
11
|
const currentYear = now.getFullYear();
|