@stamhoofd/backend 2.85.4 → 2.87.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 -1
- package/index.ts +9 -231
- package/migrations.ts +11 -33
- package/package.json +23 -23
- package/src/boot.ts +240 -0
- package/src/crons/clearExcelCache.test.ts +4 -1
- package/src/crons/delete-old-email-drafts.ts +37 -0
- package/src/crons/endFunctionsOfUsersWithoutRegistration.ts +6 -2
- package/src/crons/index.ts +1 -1
- package/src/endpoints/global/files/UploadFile.ts +4 -34
- package/src/endpoints/global/members/GetMembersEndpoint.ts +1 -0
- package/src/endpoints/global/organizations/CreateOrganizationEndpoint.ts +14 -2
- package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +2 -2
- package/src/endpoints/organization/dashboard/stripe/ConnectStripeEndpoint.ts +4 -0
- package/src/helpers/AdminPermissionChecker.ts +18 -8
- package/src/helpers/AuthenticatedStructures.ts +36 -21
- package/src/helpers/FlagMomentCleanup.ts +14 -2
- package/src/migrate.ts +65 -0
- package/src/seeds/0000000001-development-user.ts +49 -0
- package/src/seeds/1751445358-upload-email-attachments.ts +106 -0
- package/src/services/EventNotificationService.ts +1 -1
- package/src/sql-filters/members.ts +5 -1
- package/src/crons/postmark.ts +0 -223
package/src/crons/postmark.ts
DELETED
|
@@ -1,223 +0,0 @@
|
|
|
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
|
-
}
|