@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
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { Email, EmailAddress } from '@stamhoofd/email';
|
|
2
|
+
import { AuditLog, Organization } from '@stamhoofd/models';
|
|
3
|
+
import { DateTime } from 'luxon';
|
|
4
|
+
|
|
5
|
+
import { registerCron } from '@stamhoofd/crons';
|
|
6
|
+
import { AuditLogReplacement, AuditLogReplacementType, AuditLogSource, AuditLogType } from '@stamhoofd/structures';
|
|
7
|
+
|
|
8
|
+
// Importing postmark returns undefined (this is a bug, so we need to use require)
|
|
9
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
10
|
+
const postmark = require('postmark') as typeof import('postmark');
|
|
11
|
+
|
|
12
|
+
let lastPostmarkCheck: Date | null = null;
|
|
13
|
+
let lastPostmarkIds: Set<number> = new Set();
|
|
14
|
+
|
|
15
|
+
registerCron('checkPostmarkBounces', checkPostmarkBounces);
|
|
16
|
+
|
|
17
|
+
async function saveLog({ email, organization, type, subType, id, response, subject, sender }: { id: number; sender: string; email: string; response: string; subject: string;organization: Organization | undefined; type: AuditLogType; subType?: string }) {
|
|
18
|
+
const log = new AuditLog();
|
|
19
|
+
log.organizationId = organization?.id ?? null;
|
|
20
|
+
log.externalId = 'postmark-bounce-' + id.toString();
|
|
21
|
+
log.type = type;
|
|
22
|
+
log.objectId = email;
|
|
23
|
+
log.source = AuditLogSource.System;
|
|
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
|
+
['sender', AuditLogReplacement.create({
|
|
32
|
+
value: sender,
|
|
33
|
+
type: AuditLogReplacementType.EmailAddress,
|
|
34
|
+
})],
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
if (subject) {
|
|
38
|
+
log.replacements.set('subject', AuditLogReplacement.string(subject));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Check if we already logged this bounce
|
|
42
|
+
const existing = await AuditLog.select().where('externalId', log.externalId).first(false);
|
|
43
|
+
if (existing) {
|
|
44
|
+
console.log('Already logged this bounce, skipping');
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
await log.save();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function checkPostmarkBounces() {
|
|
52
|
+
if (STAMHOOFD.environment !== 'production') {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const token = STAMHOOFD.POSTMARK_SERVER_TOKEN;
|
|
57
|
+
if (!token) {
|
|
58
|
+
console.log('No postmark token, skipping postmark bounces');
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const fromDate = (lastPostmarkCheck ?? new Date(new Date().getTime() - 24 * 60 * 60 * 1000 * 2));
|
|
62
|
+
const ET = DateTime.fromJSDate(fromDate).setZone('EST').toISO({ includeOffset: false });
|
|
63
|
+
|
|
64
|
+
if (!ET) {
|
|
65
|
+
console.error('Could not convert date to EST:', fromDate);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
console.log('Checking bounces from Postmark since', fromDate, ET);
|
|
69
|
+
const client = new postmark.ServerClient(token);
|
|
70
|
+
|
|
71
|
+
const toDate = DateTime.now().setZone('EST').toISO({ includeOffset: false });
|
|
72
|
+
|
|
73
|
+
if (!toDate) {
|
|
74
|
+
console.error('Could not convert date to EST:', new Date());
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
let offset = 0;
|
|
79
|
+
let total = 1;
|
|
80
|
+
const count = 500;
|
|
81
|
+
|
|
82
|
+
// Sadly the postmark api returns bounces in the wrong order, to make them easier fetchable so we need to fetch them all in one go every time
|
|
83
|
+
while (offset < total && offset <= 10000 - count) {
|
|
84
|
+
const bounces = await client.getBounces({
|
|
85
|
+
fromDate: ET,
|
|
86
|
+
toDate,
|
|
87
|
+
count,
|
|
88
|
+
offset,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
if (bounces.TotalCount === 0) {
|
|
92
|
+
console.log('No Postmark bounces at this time');
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
total = bounces.TotalCount;
|
|
97
|
+
|
|
98
|
+
console.log('Found', bounces.TotalCount, 'bounces from Postmark');
|
|
99
|
+
|
|
100
|
+
let lastId: number | null = null;
|
|
101
|
+
const idList = new Set<number>();
|
|
102
|
+
let newEventCount = 0;
|
|
103
|
+
|
|
104
|
+
for (const bounce of bounces.Bounces) {
|
|
105
|
+
idList.add(bounce.ID);
|
|
106
|
+
if (lastPostmarkIds.has(bounce.ID)) {
|
|
107
|
+
lastId = bounce.ID;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
newEventCount += 1;
|
|
111
|
+
|
|
112
|
+
// Try to get the organization, if possible, else default to global blocking: "null", which is not visible for an organization, but it is applied
|
|
113
|
+
const source = bounce.From;
|
|
114
|
+
const organization = source ? await Organization.getByEmail(source) : undefined;
|
|
115
|
+
console.log(bounce);
|
|
116
|
+
|
|
117
|
+
if (bounce.Type === 'SpamComplaint' || bounce.Type === 'SpamNotification' || bounce.Type === 'VirusNotification') {
|
|
118
|
+
console.log('Postmark ' + bounce.Type + ' for: ', bounce.Email, 'from', source, 'organization', organization?.name);
|
|
119
|
+
const emailAddress = await EmailAddress.getOrCreate(bounce.Email, organization?.id ?? null);
|
|
120
|
+
emailAddress.markedAsSpam = true;
|
|
121
|
+
await emailAddress.save();
|
|
122
|
+
|
|
123
|
+
if (bounce.Type === 'VirusNotification') {
|
|
124
|
+
await saveLog({
|
|
125
|
+
email: bounce.Email,
|
|
126
|
+
organization,
|
|
127
|
+
type: AuditLogType.EmailAddressFraudComplaint,
|
|
128
|
+
subType: bounce.Type,
|
|
129
|
+
response: bounce.Details,
|
|
130
|
+
id: bounce.ID,
|
|
131
|
+
subject: bounce.Subject,
|
|
132
|
+
sender: bounce.From,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
await saveLog({
|
|
137
|
+
email: bounce.Email,
|
|
138
|
+
organization,
|
|
139
|
+
type: AuditLogType.EmailAddressMarkedAsSpam,
|
|
140
|
+
subType: bounce.Type,
|
|
141
|
+
response: bounce.Details,
|
|
142
|
+
id: bounce.ID,
|
|
143
|
+
subject: bounce.Subject,
|
|
144
|
+
sender: bounce.From,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
else if (bounce.Inactive) {
|
|
149
|
+
// Block for everyone, but not visible
|
|
150
|
+
console.log('Postmark ' + bounce.Type + ' for: ', bounce.Email, 'from', source, 'organization', organization?.name);
|
|
151
|
+
const emailAddress = await EmailAddress.getOrCreate(bounce.Email, organization?.id ?? null);
|
|
152
|
+
emailAddress.hardBounce = true;
|
|
153
|
+
await emailAddress.save();
|
|
154
|
+
await saveLog({
|
|
155
|
+
email: bounce.Email,
|
|
156
|
+
organization,
|
|
157
|
+
type: AuditLogType.EmailAddressHardBounced,
|
|
158
|
+
subType: bounce.Type,
|
|
159
|
+
response: bounce.Details,
|
|
160
|
+
id: bounce.ID,
|
|
161
|
+
subject: bounce.Subject,
|
|
162
|
+
sender: bounce.From,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
if (bounce.Type === 'SMTPApiError' && bounce.Details.startsWith("ErrorCode: '406'")) {
|
|
167
|
+
console.log('Email on Postmark suppression list: ' + bounce.Type + ': ', bounce.Email, 'from', source, 'organization', organization?.name);
|
|
168
|
+
|
|
169
|
+
// We've sent a message to an email that is blocked by Postmark
|
|
170
|
+
await saveLog({
|
|
171
|
+
email: bounce.Email,
|
|
172
|
+
organization,
|
|
173
|
+
type: AuditLogType.EmailAddressHardBounced,
|
|
174
|
+
subType: 'ExternalSuppressionList',
|
|
175
|
+
response: bounce.Details,
|
|
176
|
+
id: bounce.ID,
|
|
177
|
+
subject: '', // bounce.Subject is not correct here for some reason
|
|
178
|
+
sender: bounce.From,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
if (bounce.Type === 'SMTPApiError') {
|
|
183
|
+
// Log internally
|
|
184
|
+
Email.sendWebmaster({
|
|
185
|
+
subject: 'Received an SMTPApiError from Postmark',
|
|
186
|
+
text: 'We received an SMTPApiError for an e-mail from the organization: ' + organization?.name + '. Please check and adjust if needed.\n' + JSON.stringify(bounce, undefined, 4),
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
console.log('Unhandled Postmark ' + bounce.Type + ': ', bounce.Email, 'from', source, 'organization', organization?.name);
|
|
191
|
+
|
|
192
|
+
await saveLog({
|
|
193
|
+
email: bounce.Email,
|
|
194
|
+
organization,
|
|
195
|
+
type: AuditLogType.EmailAddressSoftBounced,
|
|
196
|
+
subType: bounce.Type,
|
|
197
|
+
response: bounce.Details,
|
|
198
|
+
id: bounce.ID,
|
|
199
|
+
subject: bounce.Subject,
|
|
200
|
+
sender: bounce.From,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const bouncedAt = new Date(bounce.BouncedAt);
|
|
207
|
+
lastPostmarkCheck = lastPostmarkCheck ? new Date(Math.max(bouncedAt.getTime(), lastPostmarkCheck.getTime())) : bouncedAt;
|
|
208
|
+
|
|
209
|
+
lastId = bounce.ID;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (lastId && newEventCount === 0) {
|
|
213
|
+
console.log('Postmark has no new bounces');
|
|
214
|
+
// Increase timestamp by one second to avoid refetching it every time
|
|
215
|
+
if (lastPostmarkCheck) {
|
|
216
|
+
lastPostmarkCheck = new Date(lastPostmarkCheck.getTime() + 1000);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
lastPostmarkIds = idList;
|
|
220
|
+
|
|
221
|
+
offset += bounces.Bounces.length;
|
|
222
|
+
}
|
|
223
|
+
}
|
package/src/crons.ts
CHANGED
|
@@ -1,25 +1,13 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
|
2
1
|
import { Database } from '@simonbackx/simple-database';
|
|
3
|
-
import { Email, EmailAddress } from '@stamhoofd/email';
|
|
4
2
|
import { Group, Organization, Payment, Registration, STPackage, Webshop } from '@stamhoofd/models';
|
|
5
3
|
import { PaymentMethod, PaymentProvider, PaymentStatus } from '@stamhoofd/structures';
|
|
6
4
|
import { Formatter } from '@stamhoofd/utility';
|
|
7
|
-
import AWS from 'aws-sdk';
|
|
8
|
-
import { DateTime } from 'luxon';
|
|
9
5
|
|
|
10
6
|
import { registerCron } from '@stamhoofd/crons';
|
|
11
|
-
import { clearExcelCache } from './crons/clearExcelCache';
|
|
12
|
-
import { endFunctionsOfUsersWithoutRegistration } from './crons/endFunctionsOfUsersWithoutRegistration';
|
|
13
|
-
import { ExchangePaymentEndpoint } from './endpoints/organization/shared/ExchangePaymentEndpoint';
|
|
14
7
|
import { checkSettlements } from './helpers/CheckSettlements';
|
|
15
|
-
import { ForwardHandler } from './helpers/ForwardHandler';
|
|
16
8
|
import { PaymentService } from './services/PaymentService';
|
|
17
9
|
import { RegistrationService } from './services/RegistrationService';
|
|
18
10
|
|
|
19
|
-
// Importing postmark returns undefined (this is a bug, so we need to use require)
|
|
20
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
21
|
-
const postmark = require('postmark');
|
|
22
|
-
|
|
23
11
|
let lastDNSCheck: Date | null = null;
|
|
24
12
|
let lastDNSId = '';
|
|
25
13
|
async function checkDNS() {
|
|
@@ -133,303 +121,6 @@ async function checkWebshopDNS() {
|
|
|
133
121
|
lastWebshopDNSId = webshops[webshops.length - 1].id;
|
|
134
122
|
}
|
|
135
123
|
|
|
136
|
-
async function checkReplies() {
|
|
137
|
-
if (STAMHOOFD.environment !== 'production' || !STAMHOOFD.AWS_ACCESS_KEY_ID) {
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
console.log('Checking replies from AWS SQS');
|
|
142
|
-
const sqs = new AWS.SQS();
|
|
143
|
-
const messages = await sqs.receiveMessage({ QueueUrl: 'https://sqs.eu-west-1.amazonaws.com/118244293157/stamhoofd-email-forwarding', MaxNumberOfMessages: 10 }).promise();
|
|
144
|
-
if (messages.Messages) {
|
|
145
|
-
for (const message of messages.Messages) {
|
|
146
|
-
console.log('Received message from forwarding queue');
|
|
147
|
-
|
|
148
|
-
if (message.ReceiptHandle) {
|
|
149
|
-
if (STAMHOOFD.environment === 'production') {
|
|
150
|
-
await sqs.deleteMessage({
|
|
151
|
-
QueueUrl: 'https://sqs.eu-west-1.amazonaws.com/118244293157/stamhoofd-email-forwarding',
|
|
152
|
-
ReceiptHandle: message.ReceiptHandle,
|
|
153
|
-
}).promise();
|
|
154
|
-
console.log('Deleted from queue');
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
try {
|
|
159
|
-
if (message.Body) {
|
|
160
|
-
// decode the JSON value
|
|
161
|
-
const bounce = JSON.parse(message.Body);
|
|
162
|
-
|
|
163
|
-
if (bounce.Message) {
|
|
164
|
-
const message = JSON.parse(bounce.Message);
|
|
165
|
-
|
|
166
|
-
// Read message content
|
|
167
|
-
if (message.mail && message.content && message.receipt) {
|
|
168
|
-
const content = message.content;
|
|
169
|
-
const receipt = message.receipt as {
|
|
170
|
-
recipients: string[];
|
|
171
|
-
spamVerdict: { status: 'PASS' | string };
|
|
172
|
-
virusVerdict: { status: 'PASS' | string };
|
|
173
|
-
spfVerdict: { status: 'PASS' | string };
|
|
174
|
-
dkimVerdict: { status: 'PASS' | string };
|
|
175
|
-
dmarcVerdict: { status: 'PASS' | string };
|
|
176
|
-
};
|
|
177
|
-
|
|
178
|
-
const options = await ForwardHandler.handle(content, receipt);
|
|
179
|
-
if (options) {
|
|
180
|
-
if (STAMHOOFD.environment === 'production') {
|
|
181
|
-
Email.send(options);
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
catch (e) {
|
|
189
|
-
console.error(e);
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
let lastPostmarkCheck: Date | null = null;
|
|
196
|
-
let lastPostmarkId: string | null = null;
|
|
197
|
-
async function checkPostmarkBounces() {
|
|
198
|
-
if (STAMHOOFD.environment !== 'production') {
|
|
199
|
-
return;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
const token = STAMHOOFD.POSTMARK_SERVER_TOKEN;
|
|
203
|
-
if (!token) {
|
|
204
|
-
console.log('[POSTMARK BOUNCES] No postmark token, skipping postmark bounces');
|
|
205
|
-
return;
|
|
206
|
-
}
|
|
207
|
-
const fromDate = (lastPostmarkCheck ?? new Date(new Date().getTime() - 24 * 60 * 60 * 1000 * 2));
|
|
208
|
-
const ET = DateTime.fromJSDate(fromDate).setZone('EST').toISO({ includeOffset: false });
|
|
209
|
-
console.log('[POSTMARK BOUNCES] Checking bounces from Postmark since', fromDate, ET);
|
|
210
|
-
const client = new postmark.ServerClient(token);
|
|
211
|
-
|
|
212
|
-
const bounces = await client.getBounces({
|
|
213
|
-
fromdate: ET,
|
|
214
|
-
todate: DateTime.now().setZone('EST').toISO({ includeOffset: false }),
|
|
215
|
-
count: 500,
|
|
216
|
-
offset: 0,
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
if (bounces.TotalCount == 0) {
|
|
220
|
-
console.log('[POSTMARK BOUNCES] No Postmark bounces at this time');
|
|
221
|
-
return;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
let lastId: string | null = null;
|
|
225
|
-
|
|
226
|
-
for (const bounce of bounces.Bounces) {
|
|
227
|
-
// Try to get the organization, if possible, else default to global blocking: "null", which is not visible for an organization, but it is applied
|
|
228
|
-
const source = bounce.From;
|
|
229
|
-
const organization = source ? await Organization.getByEmail(source) : undefined;
|
|
230
|
-
|
|
231
|
-
if (bounce.Type === 'HardBounce' || bounce.Type === 'BadEmailAddress' || bounce.Type === 'Blocked') {
|
|
232
|
-
// Block for everyone, but not visible
|
|
233
|
-
console.log('[POSTMARK BOUNCES] Postmark ' + bounce.Type + ' for: ', bounce.Email, 'from', source, 'organization', organization?.name);
|
|
234
|
-
const emailAddress = await EmailAddress.getOrCreate(bounce.Email, organization?.id ?? null);
|
|
235
|
-
emailAddress.hardBounce = true;
|
|
236
|
-
await emailAddress.save();
|
|
237
|
-
}
|
|
238
|
-
else if (bounce.Type === 'SpamComplaint' || bounce.Type === 'SpamNotification' || bounce.Type === 'VirusNotification') {
|
|
239
|
-
console.log('[POSTMARK BOUNCES] Postmark ' + bounce.Type + ' for: ', bounce.Email, 'from', source, 'organization', organization?.name);
|
|
240
|
-
const emailAddress = await EmailAddress.getOrCreate(bounce.Email, organization?.id ?? null);
|
|
241
|
-
emailAddress.markedAsSpam = true;
|
|
242
|
-
await emailAddress.save();
|
|
243
|
-
}
|
|
244
|
-
else {
|
|
245
|
-
console.log('[POSTMARK BOUNCES] Unhandled Postmark ' + bounce.Type + ': ', bounce.Email, 'from', source, 'organization', organization?.name);
|
|
246
|
-
console.error('[POSTMARK BOUNCES] Unhandled Postmark ' + bounce.Type + ': ', bounce.Email, 'from', source, 'organization', organization?.name);
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
const bouncedAt = new Date(bounce.BouncedAt);
|
|
250
|
-
lastPostmarkCheck = lastPostmarkCheck ? new Date(Math.max(bouncedAt.getTime(), lastPostmarkCheck.getTime())) : bouncedAt;
|
|
251
|
-
|
|
252
|
-
lastId = bounce.ID;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
if (lastId && lastPostmarkId) {
|
|
256
|
-
if (lastId === lastPostmarkId) {
|
|
257
|
-
console.log('[POSTMARK BOUNCES] Postmark has no new bounces');
|
|
258
|
-
// Increase timestamp by one second to avoid refetching it every time
|
|
259
|
-
if (lastPostmarkCheck) {
|
|
260
|
-
lastPostmarkCheck = new Date(lastPostmarkCheck.getTime() + 1000);
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
lastPostmarkId = lastId;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
async function checkBounces() {
|
|
268
|
-
if (STAMHOOFD.environment !== 'production' || !STAMHOOFD.AWS_ACCESS_KEY_ID) {
|
|
269
|
-
return;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
console.log('[AWS BOUNCES] Checking bounces from AWS SQS');
|
|
273
|
-
const sqs = new AWS.SQS();
|
|
274
|
-
const messages = await sqs.receiveMessage({ QueueUrl: 'https://sqs.eu-west-1.amazonaws.com/118244293157/stamhoofd-bounces-queue', MaxNumberOfMessages: 10 }).promise();
|
|
275
|
-
if (messages.Messages) {
|
|
276
|
-
for (const message of messages.Messages) {
|
|
277
|
-
console.log('[AWS BOUNCES] Received bounce message');
|
|
278
|
-
console.log('[AWS BOUNCES]', message);
|
|
279
|
-
|
|
280
|
-
if (message.ReceiptHandle) {
|
|
281
|
-
if (STAMHOOFD.environment === 'production') {
|
|
282
|
-
await sqs.deleteMessage({
|
|
283
|
-
QueueUrl: 'https://sqs.eu-west-1.amazonaws.com/118244293157/stamhoofd-bounces-queue',
|
|
284
|
-
ReceiptHandle: message.ReceiptHandle,
|
|
285
|
-
}).promise();
|
|
286
|
-
console.log('[AWS BOUNCES] Deleted from queue');
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
try {
|
|
291
|
-
if (message.Body) {
|
|
292
|
-
// decode the JSON value
|
|
293
|
-
const bounce = JSON.parse(message.Body);
|
|
294
|
-
|
|
295
|
-
if (bounce.Message) {
|
|
296
|
-
const message = JSON.parse(bounce.Message);
|
|
297
|
-
|
|
298
|
-
if (message.bounce) {
|
|
299
|
-
const b = message.bounce;
|
|
300
|
-
// Block all receivers that generate a permanent bounce
|
|
301
|
-
const type = b.bounceType;
|
|
302
|
-
|
|
303
|
-
const source = message.mail.source;
|
|
304
|
-
|
|
305
|
-
// try to find organization that is responsible for this e-mail address
|
|
306
|
-
|
|
307
|
-
for (const recipient of b.bouncedRecipients) {
|
|
308
|
-
const email = recipient.emailAddress;
|
|
309
|
-
|
|
310
|
-
if (
|
|
311
|
-
type === 'Permanent'
|
|
312
|
-
|| (
|
|
313
|
-
recipient.diagnosticCode && (
|
|
314
|
-
(recipient.diagnosticCode as string).toLowerCase().includes('invalid domain')
|
|
315
|
-
|| (recipient.diagnosticCode as string).toLowerCase().includes('unable to lookup dns')
|
|
316
|
-
)
|
|
317
|
-
)
|
|
318
|
-
) {
|
|
319
|
-
const organization: Organization | undefined = source ? await Organization.getByEmail(source) : undefined;
|
|
320
|
-
if (organization) {
|
|
321
|
-
const emailAddress = await EmailAddress.getOrCreate(email, organization.id);
|
|
322
|
-
emailAddress.hardBounce = true;
|
|
323
|
-
await emailAddress.save();
|
|
324
|
-
}
|
|
325
|
-
else {
|
|
326
|
-
console.error('[AWS BOUNCES] Unknown organization for email address ' + source);
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
console.log('[AWS BOUNCES] For domain ' + source);
|
|
331
|
-
}
|
|
332
|
-
else {
|
|
333
|
-
console.log("[AWS BOUNCES] 'bounce' field missing in bounce message");
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
else {
|
|
337
|
-
console.log("[AWS BOUNCES] 'Message' field missing in bounce message");
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
else {
|
|
341
|
-
console.log('[AWS BOUNCES] Message Body missing in bounce');
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
catch (e) {
|
|
345
|
-
console.log('[AWS BOUNCES] Bounce message processing failed:');
|
|
346
|
-
console.error('[AWS BOUNCES] Bounce message processing failed:');
|
|
347
|
-
console.error('[AWS BOUNCES]', e);
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
async function checkComplaints() {
|
|
354
|
-
if (STAMHOOFD.environment !== 'production' || !STAMHOOFD.AWS_ACCESS_KEY_ID) {
|
|
355
|
-
return;
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
console.log('[AWS COMPLAINTS] Checking complaints from AWS SQS');
|
|
359
|
-
const sqs = new AWS.SQS();
|
|
360
|
-
const messages = await sqs.receiveMessage({ QueueUrl: 'https://sqs.eu-west-1.amazonaws.com/118244293157/stamhoofd-complaints-queue', MaxNumberOfMessages: 10 }).promise();
|
|
361
|
-
if (messages.Messages) {
|
|
362
|
-
for (const message of messages.Messages) {
|
|
363
|
-
console.log('[AWS COMPLAINTS] Received complaint message');
|
|
364
|
-
console.log('[AWS COMPLAINTS]', message);
|
|
365
|
-
|
|
366
|
-
if (message.ReceiptHandle) {
|
|
367
|
-
if (STAMHOOFD.environment === 'production') {
|
|
368
|
-
await sqs.deleteMessage({
|
|
369
|
-
QueueUrl: 'https://sqs.eu-west-1.amazonaws.com/118244293157/stamhoofd-complaints-queue',
|
|
370
|
-
ReceiptHandle: message.ReceiptHandle,
|
|
371
|
-
}).promise();
|
|
372
|
-
console.log('[AWS COMPLAINTS] Deleted from queue');
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
try {
|
|
377
|
-
if (message.Body) {
|
|
378
|
-
// decode the JSON value
|
|
379
|
-
const complaint = JSON.parse(message.Body);
|
|
380
|
-
console.log('[AWS COMPLAINTS]', complaint);
|
|
381
|
-
|
|
382
|
-
if (complaint.Message) {
|
|
383
|
-
const message = JSON.parse(complaint.Message);
|
|
384
|
-
|
|
385
|
-
if (message.complaint) {
|
|
386
|
-
const b = message.complaint;
|
|
387
|
-
const source = message.mail.source;
|
|
388
|
-
const organization: Organization | undefined = source ? await Organization.getByEmail(source) : undefined;
|
|
389
|
-
|
|
390
|
-
const type: 'abuse' | 'auth-failure' | 'fraud' | 'not-spam' | 'other' | 'virus' = b.complaintFeedbackType;
|
|
391
|
-
|
|
392
|
-
if (organization) {
|
|
393
|
-
for (const recipient of b.complainedRecipients) {
|
|
394
|
-
const email = recipient.emailAddress;
|
|
395
|
-
const emailAddress = await EmailAddress.getOrCreate(email, organization.id);
|
|
396
|
-
emailAddress.markedAsSpam = type !== 'not-spam';
|
|
397
|
-
await emailAddress.save();
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
else {
|
|
401
|
-
console.error('[AWS COMPLAINTS] Unknown organization for email address ' + source);
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
if (type == 'virus' || type == 'fraud') {
|
|
405
|
-
console.error('[AWS COMPLAINTS] Received virus / fraud complaint!');
|
|
406
|
-
console.error('[AWS COMPLAINTS]', complaint);
|
|
407
|
-
if (STAMHOOFD.environment === 'production') {
|
|
408
|
-
Email.sendWebmaster({
|
|
409
|
-
subject: 'Received a ' + type + ' email notification',
|
|
410
|
-
text: 'We received a ' + type + ' notification for an e-mail from the organization: ' + organization?.name + '. Please check and adjust if needed.\n',
|
|
411
|
-
});
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
else {
|
|
416
|
-
console.log('[AWS COMPLAINTS] Missing complaint field');
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
else {
|
|
420
|
-
console.log('[AWS COMPLAINTS] Missing message field in complaint');
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
catch (e) {
|
|
425
|
-
console.log('[AWS COMPLAINTS] Complain message processing failed:');
|
|
426
|
-
console.error('[AWS COMPLAINTS] Complain message processing failed:');
|
|
427
|
-
console.error('[AWS COMPLAINTS]', e);
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
|
|
433
124
|
// Keep checking pending paymetns for 3 days
|
|
434
125
|
async function checkPayments() {
|
|
435
126
|
if (STAMHOOFD.environment === 'development') {
|
|
@@ -658,14 +349,11 @@ async function checkDrips() {
|
|
|
658
349
|
|
|
659
350
|
registerCron('checkSettlements', checkSettlements);
|
|
660
351
|
registerCron('checkExpirationEmails', checkExpirationEmails);
|
|
661
|
-
registerCron('checkPostmarkBounces', checkPostmarkBounces);
|
|
662
352
|
registerCron('checkReservedUntil', checkReservedUntil);
|
|
663
|
-
registerCron('checkComplaints', checkComplaints);
|
|
664
|
-
registerCron('checkReplies', checkReplies);
|
|
665
|
-
registerCron('checkBounces', checkBounces);
|
|
666
353
|
registerCron('checkDNS', checkDNS);
|
|
667
354
|
registerCron('checkWebshopDNS', checkWebshopDNS);
|
|
668
355
|
registerCron('checkPayments', checkPayments);
|
|
669
356
|
registerCron('checkDrips', checkDrips);
|
|
670
|
-
|
|
671
|
-
|
|
357
|
+
|
|
358
|
+
// Register other crons
|
|
359
|
+
import './crons/index.js';
|
|
@@ -187,7 +187,6 @@ export class MemberUserSyncerStatic {
|
|
|
187
187
|
|
|
188
188
|
if (user.memberId === member.id) {
|
|
189
189
|
user.memberId = null;
|
|
190
|
-
await user.save();
|
|
191
190
|
}
|
|
192
191
|
|
|
193
192
|
// Update model relation to correct response
|
|
@@ -239,11 +238,7 @@ export class MemberUserSyncerStatic {
|
|
|
239
238
|
await this.updateInheritedPermissions(user);
|
|
240
239
|
}
|
|
241
240
|
else {
|
|
242
|
-
|
|
243
|
-
// Unlink: parents are never 'equal' to the member
|
|
244
|
-
user.memberId = null;
|
|
245
|
-
await this.updateInheritedPermissions(user);
|
|
246
|
-
}
|
|
241
|
+
let shouldSave = false;
|
|
247
242
|
|
|
248
243
|
if (!user.firstName && !user.lastName) {
|
|
249
244
|
const parents = member.details.parents.filter(p => p.email === email);
|
|
@@ -252,13 +247,22 @@ export class MemberUserSyncerStatic {
|
|
|
252
247
|
user.firstName = parents[0].firstName;
|
|
253
248
|
user.lastName = parents[0].lastName;
|
|
254
249
|
}
|
|
255
|
-
|
|
250
|
+
shouldSave = true;
|
|
256
251
|
}
|
|
257
252
|
}
|
|
258
253
|
|
|
259
254
|
if (user.firstName === member.details.firstName && user.lastName === member.details.lastName) {
|
|
260
255
|
user.firstName = null;
|
|
261
256
|
user.lastName = null;
|
|
257
|
+
shouldSave = true;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (user.memberId === member.id) {
|
|
261
|
+
// Unlink: parents are never 'equal' to the member
|
|
262
|
+
user.memberId = null;
|
|
263
|
+
await this.updateInheritedPermissions(user);
|
|
264
|
+
}
|
|
265
|
+
if (shouldSave) {
|
|
262
266
|
await user.save();
|
|
263
267
|
}
|
|
264
268
|
}
|
|
@@ -16,6 +16,10 @@ import { RegistrationLogger } from '../audit-logs/RegistrationLogger';
|
|
|
16
16
|
import { RegistrationPeriodLogger } from '../audit-logs/RegistrationPeriodLogger';
|
|
17
17
|
import { StripeAccountLogger } from '../audit-logs/StripeAccountLogger';
|
|
18
18
|
import { WebshopLogger } from '../audit-logs/WebshopLogger';
|
|
19
|
+
import { EmailLogger } from '../audit-logs/EmailLogger';
|
|
20
|
+
import { EmailTemplateLogger } from '../audit-logs/EmailTemplateLogger';
|
|
21
|
+
import { EmailAddressLogger } from '../audit-logs/EmailAddressLogger';
|
|
22
|
+
import { UserLogger } from '../audit-logs/UserLogger';
|
|
19
23
|
|
|
20
24
|
export type AuditLogContextSettings = {
|
|
21
25
|
disable?: boolean;
|
|
@@ -100,3 +104,7 @@ modelLogDefinitions.set(PaymentLogger.model, PaymentLogger);
|
|
|
100
104
|
modelLogDefinitions.set(MemberPlatformMembershipLogger.model, MemberPlatformMembershipLogger);
|
|
101
105
|
modelLogDefinitions.set(MemberResponsibilityRecordLogger.model, MemberResponsibilityRecordLogger);
|
|
102
106
|
modelLogDefinitions.set(DocumentTemplateLogger.model, DocumentTemplateLogger);
|
|
107
|
+
modelLogDefinitions.set(EmailLogger.model, EmailLogger);
|
|
108
|
+
modelLogDefinitions.set(EmailTemplateLogger.model, EmailTemplateLogger);
|
|
109
|
+
modelLogDefinitions.set(EmailAddressLogger.model, EmailAddressLogger);
|
|
110
|
+
modelLogDefinitions.set(UserLogger.model, UserLogger);
|
package/src/services/diff.ts
CHANGED
|
@@ -84,6 +84,9 @@ function getDiffName(autoEncoder: unknown): AuditLogReplacement | null {
|
|
|
84
84
|
if (typeof name === 'string') {
|
|
85
85
|
return name ? AuditLogReplacement.string(name) : AuditLogReplacement.key('untitled');
|
|
86
86
|
}
|
|
87
|
+
if (name instanceof AuditLogReplacement) {
|
|
88
|
+
return name;
|
|
89
|
+
}
|
|
87
90
|
}
|
|
88
91
|
|
|
89
92
|
if (typeof autoEncoder === 'object' && autoEncoder !== null && 'name' in autoEncoder && typeof autoEncoder.name === 'string') {
|
|
@@ -109,6 +112,12 @@ function getDiffValue(autoEncoder: unknown, key?: AuditLogReplacement): AuditLog
|
|
|
109
112
|
if (DataValidator.isUuid(autoEncoder)) {
|
|
110
113
|
return AuditLogReplacement.uuid(autoEncoder);
|
|
111
114
|
}
|
|
115
|
+
|
|
116
|
+
// Is html
|
|
117
|
+
if (autoEncoder.startsWith('<!DOCTYPE html>')) {
|
|
118
|
+
return AuditLogReplacement.html(autoEncoder);
|
|
119
|
+
}
|
|
120
|
+
|
|
112
121
|
if (key && key?.lastValue() === 'status') {
|
|
113
122
|
// Will be an enum
|
|
114
123
|
return AuditLogReplacement.key(autoEncoder);
|
|
@@ -157,13 +166,6 @@ function getDiffValue(autoEncoder: unknown, key?: AuditLogReplacement): AuditLog
|
|
|
157
166
|
return null;
|
|
158
167
|
}
|
|
159
168
|
|
|
160
|
-
function getKeySingular(key?: string) {
|
|
161
|
-
if (key === undefined) {
|
|
162
|
-
return undefined;
|
|
163
|
-
}
|
|
164
|
-
return key.replace(/ies$/, 'y').replace(/s$/, '');
|
|
165
|
-
}
|
|
166
|
-
|
|
167
169
|
function findOriginalById(id: unknown, oldArray: unknown[]): unknown | null {
|
|
168
170
|
return id ? oldArray.find(v => getId(v) === id) : null;
|
|
169
171
|
}
|
|
@@ -397,7 +399,7 @@ export function diffUnknown(oldValue: unknown, value: unknown, key?: AuditLogRep
|
|
|
397
399
|
|
|
398
400
|
if (v && ov) {
|
|
399
401
|
// Simplify change
|
|
400
|
-
if (v.
|
|
402
|
+
if (v.toKey() === ov.toKey()) {
|
|
401
403
|
v = null;
|
|
402
404
|
ov = null;
|
|
403
405
|
|