@stamhoofd/backend 2.39.1 → 2.40.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/eslint.config.mjs +5 -0
- package/index.ts +81 -74
- package/jest.config.cjs +10 -0
- package/migrations.ts +16 -14
- package/package.json +11 -11
- package/src/crons/clear-excel-cache.test.ts +48 -50
- package/src/crons/clear-excel-cache.ts +18 -18
- package/src/crons/setup-steps.ts +2 -2
- package/src/crons.ts +325 -306
- package/src/decoders/StringArrayDecoder.ts +7 -7
- package/src/decoders/StringNullableDecoder.ts +1 -2
- package/src/email-recipient-loaders/members.ts +22 -22
- package/src/endpoints/admin/memberships/ChargeMembershipsEndpoint.ts +8 -9
- package/src/endpoints/admin/memberships/GetChargeMembershipsSummaryEndpoint.ts +39 -40
- package/src/endpoints/admin/organizations/GetOrganizationsCountEndpoint.ts +8 -8
- package/src/endpoints/admin/organizations/GetOrganizationsEndpoint.ts +44 -45
- package/src/endpoints/admin/organizations/PatchOrganizationsEndpoint.ts +58 -57
- package/src/endpoints/auth/CreateAdminEndpoint.ts +48 -45
- package/src/endpoints/auth/CreateTokenEndpoint.test.ts +31 -31
- package/src/endpoints/auth/CreateTokenEndpoint.ts +146 -147
- package/src/endpoints/auth/DeleteTokenEndpoint.ts +7 -7
- package/src/endpoints/auth/DeleteUserEndpoint.ts +15 -15
- package/src/endpoints/auth/ForgotPasswordEndpoint.ts +17 -18
- package/src/endpoints/auth/GetOtherUserEndpoint.ts +9 -10
- package/src/endpoints/auth/GetUserEndpoint.test.ts +32 -35
- package/src/endpoints/auth/GetUserEndpoint.ts +5 -6
- package/src/endpoints/auth/PatchApiUserEndpoint.ts +35 -33
- package/src/endpoints/auth/PatchUserEndpoint.ts +55 -52
- package/src/endpoints/auth/PollEmailVerificationEndpoint.ts +9 -9
- package/src/endpoints/auth/RetryEmailVerificationEndpoint.ts +8 -8
- package/src/endpoints/auth/SignupEndpoint.ts +37 -36
- package/src/endpoints/auth/VerifyEmailEndpoint.ts +29 -28
- package/src/endpoints/global/addresses/SearchRegionsEndpoint.ts +33 -33
- package/src/endpoints/global/addresses/ValidateAddressEndpoint.ts +7 -7
- package/src/endpoints/global/caddy/CheckDomainCertEndpoint.ts +37 -37
- package/src/endpoints/global/email/CreateEmailEndpoint.ts +30 -30
- package/src/endpoints/global/email/GetEmailAddressEndpoint.ts +13 -13
- package/src/endpoints/global/email/GetEmailEndpoint.ts +13 -13
- package/src/endpoints/global/email/ManageEmailAddressEndpoint.ts +16 -16
- package/src/endpoints/global/email/PatchEmailEndpoint.ts +25 -25
- package/src/endpoints/global/events/GetEventsEndpoint.ts +43 -44
- package/src/endpoints/global/events/PatchEventsEndpoint.ts +127 -172
- package/src/endpoints/global/files/ExportToExcelEndpoint.ts +49 -50
- package/src/endpoints/global/files/GetFileCache.ts +13 -13
- package/src/endpoints/global/files/UploadFile.ts +51 -54
- package/src/endpoints/global/files/UploadImage.ts +53 -53
- package/src/endpoints/global/groups/GetGroupsEndpoint.ts +25 -25
- package/src/endpoints/global/members/GetMemberFamilyEndpoint.ts +24 -23
- package/src/endpoints/global/members/GetMembersCountEndpoint.ts +8 -8
- package/src/endpoints/global/members/GetMembersEndpoint.ts +105 -102
- package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +240 -239
- package/src/endpoints/global/organizations/CheckRegisterCodeEndpoint.ts +12 -14
- package/src/endpoints/global/organizations/CreateOrganizationEndpoint.test.ts +32 -33
- package/src/endpoints/global/organizations/CreateOrganizationEndpoint.ts +48 -57
- package/src/endpoints/global/organizations/GetOrganizationFromDomainEndpoint.test.ts +21 -22
- package/src/endpoints/global/organizations/GetOrganizationFromDomainEndpoint.ts +28 -28
- package/src/endpoints/global/organizations/GetOrganizationFromUriEndpoint.ts +18 -18
- package/src/endpoints/global/organizations/SearchOrganizationEndpoint.test.ts +20 -20
- package/src/endpoints/global/organizations/SearchOrganizationEndpoint.ts +17 -17
- package/src/endpoints/global/payments/StripeWebhookEndpoint.ts +81 -75
- package/src/endpoints/global/platform/GetPlatformAdminsEndpoint.ts +14 -14
- package/src/endpoints/global/platform/GetPlatformEnpoint.ts +11 -11
- package/src/endpoints/global/platform/PatchPlatformEnpoint.ts +71 -68
- package/src/endpoints/global/registration/GetPaymentRegistrations.ts +27 -27
- package/src/endpoints/global/registration/GetUserBillingStatusEndpoint.ts +30 -30
- package/src/endpoints/global/registration/GetUserDetailedBillingStatusEndpoint.ts +34 -34
- package/src/endpoints/global/registration/GetUserDocumentsEndpoint.ts +26 -26
- package/src/endpoints/global/registration/GetUserMembersEndpoint.ts +12 -12
- package/src/endpoints/global/registration/PatchUserMembersEndpoint.ts +90 -90
- package/src/endpoints/global/registration/RegisterMembersEndpoint.test.ts +118 -121
- package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +362 -350
- package/src/endpoints/global/registration-periods/GetRegistrationPeriodsEndpoint.ts +8 -9
- package/src/endpoints/global/registration-periods/PatchRegistrationPeriodsEndpoint.ts +21 -21
- package/src/endpoints/global/webshops/GetWebshopFromDomainEndpoint.ts +65 -65
- package/src/endpoints/organization/dashboard/billing/GetOrganizationBillingStatusEndpoint.ts +9 -9
- package/src/endpoints/organization/dashboard/billing/GetOrganizationDetailedBillingStatusEndpoint.ts +14 -14
- package/src/endpoints/organization/dashboard/documents/GetDocumentTemplateXML.ts +17 -17
- package/src/endpoints/organization/dashboard/documents/GetDocumentTemplatesEndpoint.ts +21 -21
- package/src/endpoints/organization/dashboard/documents/GetDocumentsEndpoint.ts +15 -15
- package/src/endpoints/organization/dashboard/documents/PatchDocumentEndpoint.ts +52 -52
- package/src/endpoints/organization/dashboard/documents/PatchDocumentTemplateEndpoint.ts +37 -37
- package/src/endpoints/organization/dashboard/email/CheckEmailBouncesEndpoint.ts +14 -14
- package/src/endpoints/organization/dashboard/email/EmailEndpoint.ts +113 -112
- package/src/endpoints/organization/dashboard/email-templates/GetEmailTemplatesEndpoint.ts +29 -29
- package/src/endpoints/organization/dashboard/email-templates/PatchEmailTemplatesEndpoint.ts +48 -47
- package/src/endpoints/organization/dashboard/mollie/CheckMollieEndpoint.ts +22 -21
- package/src/endpoints/organization/dashboard/mollie/ConnectMollieEndpoint.ts +13 -14
- package/src/endpoints/organization/dashboard/mollie/DisconnectMollieEndpoint.ts +12 -13
- package/src/endpoints/organization/dashboard/mollie/GetMollieDashboardEndpoint.ts +24 -24
- package/src/endpoints/organization/dashboard/nolt/CreateNoltTokenEndpoint.ts +10 -12
- package/src/endpoints/organization/dashboard/organization/GetOrganizationArchivedGroups.ts +14 -14
- package/src/endpoints/organization/dashboard/organization/GetOrganizationDeletedGroups.ts +13 -13
- package/src/endpoints/organization/dashboard/organization/GetOrganizationSSOEndpoint.ts +12 -12
- package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.test.ts +120 -124
- package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +172 -173
- package/src/endpoints/organization/dashboard/organization/SetOrganizationDomainEndpoint.ts +88 -89
- package/src/endpoints/organization/dashboard/organization/SetOrganizationSSOEndpoint.ts +12 -12
- package/src/endpoints/organization/dashboard/payments/GetMemberBalanceEndpoint.ts +17 -17
- package/src/endpoints/organization/dashboard/payments/GetPaymentsCountEndpoint.ts +8 -8
- package/src/endpoints/organization/dashboard/payments/GetPaymentsEndpoint.ts +66 -67
- package/src/endpoints/organization/dashboard/payments/PatchBalanceItemsEndpoint.ts +47 -47
- package/src/endpoints/organization/dashboard/payments/PatchPaymentsEndpoint.ts +93 -91
- package/src/endpoints/organization/dashboard/registration-periods/GetOrganizationRegistrationPeriodsEndpoint.ts +16 -17
- package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +170 -167
- package/src/endpoints/organization/dashboard/registration-periods/SetupStepReviewEndpoint.ts +25 -24
- package/src/endpoints/organization/dashboard/stripe/ConnectStripeEndpoint.ts +22 -23
- package/src/endpoints/organization/dashboard/stripe/DeleteStripeAccountEndpoint.ts +22 -22
- package/src/endpoints/organization/dashboard/stripe/GetStripeAccountLinkEndpoint.ts +17 -18
- package/src/endpoints/organization/dashboard/stripe/GetStripeAccountsEndpoint.ts +8 -9
- package/src/endpoints/organization/dashboard/stripe/GetStripeLoginLinkEndpoint.ts +17 -18
- package/src/endpoints/organization/dashboard/stripe/UpdateStripeAccountEndpoint.ts +14 -15
- package/src/endpoints/organization/dashboard/users/CreateApiUserEndpoint.ts +19 -19
- package/src/endpoints/organization/dashboard/users/DeleteUserEndpoint.ts +19 -19
- package/src/endpoints/organization/dashboard/users/GetApiUsersEndpoint.ts +14 -14
- package/src/endpoints/organization/dashboard/users/GetOrganizationAdminsEndpoint.ts +12 -12
- package/src/endpoints/organization/dashboard/webshops/CreateWebshopEndpoint.ts +103 -100
- package/src/endpoints/organization/dashboard/webshops/DeleteWebshopEndpoint.ts +11 -12
- package/src/endpoints/organization/dashboard/webshops/GetDiscountCodesEndpoint.ts +15 -15
- package/src/endpoints/organization/dashboard/webshops/GetWebshopOrdersEndpoint.ts +14 -14
- package/src/endpoints/organization/dashboard/webshops/GetWebshopTicketsEndpoint.ts +14 -14
- package/src/endpoints/organization/dashboard/webshops/GetWebshopUriAvailabilityEndpoint.ts +23 -23
- package/src/endpoints/organization/dashboard/webshops/PatchDiscountCodesEndpoint.ts +54 -52
- package/src/endpoints/organization/dashboard/webshops/PatchWebshopEndpoint.ts +84 -81
- package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.ts +120 -111
- package/src/endpoints/organization/dashboard/webshops/PatchWebshopTicketsEndpoint.ts +24 -24
- package/src/endpoints/organization/dashboard/webshops/VerifyWebshopDomainEndpoint.ts +18 -18
- package/src/endpoints/organization/shared/ExchangePaymentEndpoint.ts +141 -130
- package/src/endpoints/organization/shared/GetDocumentHtml.ts +25 -25
- package/src/endpoints/organization/shared/GetPaymentEndpoint.ts +18 -18
- package/src/endpoints/organization/shared/auth/GetOrganizationEndpoint.test.ts +36 -37
- package/src/endpoints/organization/shared/auth/GetOrganizationEndpoint.ts +9 -9
- package/src/endpoints/organization/shared/auth/OpenIDConnectCallbackEndpoint.ts +11 -11
- package/src/endpoints/organization/shared/auth/OpenIDConnectStartEndpoint.ts +28 -27
- package/src/endpoints/organization/webshops/CheckWebshopDiscountCodesEndpoint.ts +20 -20
- package/src/endpoints/organization/webshops/GetOrderByPaymentEndpoint.ts +22 -22
- package/src/endpoints/organization/webshops/GetOrderEndpoint.ts +14 -14
- package/src/endpoints/organization/webshops/GetTicketsEndpoint.ts +57 -56
- package/src/endpoints/organization/webshops/GetWebshopEndpoint.test.ts +65 -66
- package/src/endpoints/organization/webshops/GetWebshopEndpoint.ts +18 -17
- package/src/endpoints/organization/webshops/PlaceOrderEndpoint.test.ts +124 -128
- package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +154 -145
- package/src/excel-loaders/members.ts +102 -103
- package/src/excel-loaders/payments.ts +155 -156
- package/src/helpers/AddressValidator.test.ts +32 -32
- package/src/helpers/AddressValidator.ts +128 -122
- package/src/helpers/AdminPermissionChecker.ts +339 -236
- package/src/helpers/AuthenticatedStructures.ts +233 -134
- package/src/helpers/BuckarooHelper.ts +134 -134
- package/src/helpers/CheckSettlements.ts +94 -88
- package/src/helpers/Context.ts +87 -86
- package/src/helpers/CookieHelper.ts +23 -22
- package/src/helpers/EmailResumer.ts +10 -10
- package/src/helpers/FileCache.ts +62 -62
- package/src/helpers/ForwardHandler.test.ts +122 -124
- package/src/helpers/ForwardHandler.ts +76 -70
- package/src/helpers/MemberUserSyncer.ts +101 -96
- package/src/helpers/MembershipCharger.ts +69 -69
- package/src/helpers/MembershipHelper.ts +11 -12
- package/src/helpers/OpenIDConnectHelper.ts +85 -82
- package/src/helpers/PeriodHelper.ts +65 -70
- package/src/helpers/StripeHelper.ts +146 -137
- package/src/helpers/StripePayoutChecker.ts +51 -52
- package/src/helpers/ViesHelper.ts +46 -44
- package/src/helpers/fetchToAsyncIterator.ts +14 -14
- package/src/helpers/xlsxAddressTransformerColumnFactory.ts +50 -52
- package/src/middleware/ContextMiddleware.ts +5 -5
- package/src/migrations/1646578856-validate-addresses.ts +6 -9
- package/src/seeds/0000000000-example.ts +3 -5
- package/src/seeds/1715028563-user-permissions.ts +16 -18
- package/src/seeds/1722256498-group-update-occupancy.ts +12 -12
- package/src/seeds/1722344162-sync-member-users.ts +14 -15
- package/src/seeds/1722344162-update-membership.ts +6 -6
- package/src/seeds/1726055544-balance-item-paid.ts +4 -4
- package/src/seeds/1726055545-balance-item-pending.ts +4 -4
- package/src/seeds/1726494419-update-cached-outstanding-balance.ts +16 -16
- package/src/seeds/1726494420-update-cached-outstanding-balance-from-items.ts +12 -12
- package/src/seeds/1726572303-schedule-stock-updates.ts +12 -12
- package/src/seeds/1726847064-setup-steps.ts +16 -0
- package/src/sql-filters/balance-item-payments.ts +7 -7
- package/src/sql-filters/events.ts +14 -14
- package/src/sql-filters/members.ts +96 -96
- package/src/sql-filters/organizations.ts +139 -75
- package/src/sql-filters/payments.ts +28 -28
- package/src/sql-filters/registrations.ts +14 -14
- package/src/sql-sorters/events.ts +25 -25
- package/src/sql-sorters/members.ts +26 -26
- package/src/sql-sorters/organizations.ts +36 -36
- package/src/sql-sorters/payments.ts +26 -26
- package/tests/e2e/stock.test.ts +616 -621
- package/tests/e2e/tickets.test.ts +255 -260
- package/tests/helpers/StripeMocker.ts +177 -179
- package/tests/helpers/TestServer.ts +9 -9
- package/tests/jest.global.setup.ts +14 -13
- package/tests/jest.setup.ts +33 -32
- package/.eslintrc.js +0 -61
- package/jest.config.js +0 -11
- package/src/helpers/SetupStepsUpdater.ts +0 -359
- package/src/seeds/1724076679-setup-steps.ts +0 -16
|
@@ -1,187 +1,185 @@
|
|
|
1
|
-
|
|
1
|
+
import { EmailAddress } from '@stamhoofd/email';
|
|
2
|
+
import { OrganizationFactory, UserFactory } from '@stamhoofd/models';
|
|
3
|
+
import { OrganizationEmail, PermissionLevel, Permissions } from '@stamhoofd/structures';
|
|
2
4
|
|
|
3
|
-
import {
|
|
4
|
-
import { OrganizationFactory, UserFactory } from "@stamhoofd/models"
|
|
5
|
-
import { OrganizationEmail, PermissionLevel, Permissions } from "@stamhoofd/structures"
|
|
5
|
+
import { ForwardHandler } from './ForwardHandler';
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
it("should send to default e-mail", async () => {
|
|
11
|
-
const organization = await new OrganizationFactory({}).create()
|
|
7
|
+
describe('ForwardHandler', () => {
|
|
8
|
+
it('should send to default e-mail', async () => {
|
|
9
|
+
const organization = await new OrganizationFactory({}).create();
|
|
12
10
|
organization.privateMeta.emails.push(OrganizationEmail.create({
|
|
13
|
-
name:
|
|
14
|
-
email:
|
|
15
|
-
}))
|
|
11
|
+
name: 'First',
|
|
12
|
+
email: 'first@example.com',
|
|
13
|
+
}));
|
|
16
14
|
organization.privateMeta.emails.push(OrganizationEmail.create({
|
|
17
|
-
name:
|
|
18
|
-
email:
|
|
19
|
-
default: true
|
|
20
|
-
}))
|
|
21
|
-
await organization.save()
|
|
22
|
-
|
|
23
|
-
const options = await ForwardHandler.handle(
|
|
24
|
-
recipients: [organization.uri +
|
|
15
|
+
name: 'default',
|
|
16
|
+
email: 'def@example.com',
|
|
17
|
+
default: true,
|
|
18
|
+
}));
|
|
19
|
+
await organization.save();
|
|
20
|
+
|
|
21
|
+
const options = await ForwardHandler.handle('From: someone@example.com\nSubject: Hello\nTo: ' + organization.uri + '@stamhoofd.email\nContent-Type: text/plain\n\nContent hier', {
|
|
22
|
+
recipients: [organization.uri + '@stamhoofd.email'],
|
|
25
23
|
spamVerdict: { status: 'PASS' },
|
|
26
24
|
virusVerdict: { status: 'PASS' },
|
|
27
25
|
spfVerdict: { status: 'PASS' },
|
|
28
26
|
dkimVerdict: { status: 'PASS' },
|
|
29
27
|
dmarcVerdict: { status: 'PASS' },
|
|
30
|
-
})
|
|
28
|
+
});
|
|
31
29
|
expect(options).toMatchObject({
|
|
32
30
|
to: [
|
|
33
31
|
{
|
|
34
|
-
email:
|
|
35
|
-
name:
|
|
36
|
-
}
|
|
32
|
+
email: 'def@example.com',
|
|
33
|
+
name: 'default',
|
|
34
|
+
},
|
|
37
35
|
],
|
|
38
|
-
subject:
|
|
39
|
-
replyTo:
|
|
40
|
-
})
|
|
41
|
-
expect(options!.text).toContain(
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
it(
|
|
45
|
-
const organization = await new OrganizationFactory({}).create()
|
|
36
|
+
subject: 'Hello',
|
|
37
|
+
replyTo: 'someone@example.com',
|
|
38
|
+
});
|
|
39
|
+
expect(options!.text).toContain('Content hier');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should send to first e-mail', async () => {
|
|
43
|
+
const organization = await new OrganizationFactory({}).create();
|
|
46
44
|
organization.privateMeta.emails.push(OrganizationEmail.create({
|
|
47
|
-
name:
|
|
48
|
-
email:
|
|
49
|
-
}))
|
|
45
|
+
name: 'First',
|
|
46
|
+
email: 'first@example.com',
|
|
47
|
+
}));
|
|
50
48
|
organization.privateMeta.emails.push(OrganizationEmail.create({
|
|
51
|
-
name:
|
|
52
|
-
email:
|
|
53
|
-
}))
|
|
54
|
-
await organization.save()
|
|
49
|
+
name: 'second',
|
|
50
|
+
email: 'second@example.com',
|
|
51
|
+
}));
|
|
52
|
+
await organization.save();
|
|
55
53
|
|
|
56
|
-
const options = await ForwardHandler.handle(
|
|
57
|
-
recipients: [organization.uri +
|
|
54
|
+
const options = await ForwardHandler.handle('From: someone@example.com\nSubject: Hello\nTo: ' + organization.uri + '@stamhoofd.email\nContent-Type: text/plain\n\nContent hier', {
|
|
55
|
+
recipients: [organization.uri + '@stamhoofd.email'],
|
|
58
56
|
spamVerdict: { status: 'PASS' },
|
|
59
57
|
virusVerdict: { status: 'PASS' },
|
|
60
58
|
spfVerdict: { status: 'PASS' },
|
|
61
59
|
dkimVerdict: { status: 'PASS' },
|
|
62
60
|
dmarcVerdict: { status: 'PASS' },
|
|
63
|
-
})
|
|
61
|
+
});
|
|
64
62
|
expect(options).toMatchObject({
|
|
65
63
|
to: [
|
|
66
64
|
{
|
|
67
|
-
email:
|
|
68
|
-
name:
|
|
69
|
-
}
|
|
65
|
+
email: 'first@example.com',
|
|
66
|
+
name: 'First',
|
|
67
|
+
},
|
|
70
68
|
],
|
|
71
|
-
subject:
|
|
72
|
-
replyTo:
|
|
73
|
-
})
|
|
74
|
-
expect(options!.text).toContain(
|
|
75
|
-
})
|
|
76
|
-
|
|
77
|
-
it(
|
|
78
|
-
const organization = await new OrganizationFactory({}).create()
|
|
79
|
-
const user = await new UserFactory({ organization, permissions: Permissions.create({ level: PermissionLevel.Full }) }).create()
|
|
80
|
-
|
|
81
|
-
const options = await ForwardHandler.handle(
|
|
82
|
-
recipients: [organization.uri +
|
|
69
|
+
subject: 'Hello',
|
|
70
|
+
replyTo: 'someone@example.com',
|
|
71
|
+
});
|
|
72
|
+
expect(options!.text).toContain('Content hier');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should send to administrators if no emails defined', async () => {
|
|
76
|
+
const organization = await new OrganizationFactory({}).create();
|
|
77
|
+
const user = await new UserFactory({ organization, permissions: Permissions.create({ level: PermissionLevel.Full }) }).create();
|
|
78
|
+
|
|
79
|
+
const options = await ForwardHandler.handle('From: someone@example.com\nSubject: Hello\nTo: ' + organization.uri + '@stamhoofd.email\nContent-Type: text/plain\n\nContent hier', {
|
|
80
|
+
recipients: [organization.uri + '@stamhoofd.email'],
|
|
83
81
|
spamVerdict: { status: 'PASS' },
|
|
84
82
|
virusVerdict: { status: 'PASS' },
|
|
85
83
|
spfVerdict: { status: 'PASS' },
|
|
86
84
|
dkimVerdict: { status: 'PASS' },
|
|
87
85
|
dmarcVerdict: { status: 'PASS' },
|
|
88
|
-
})
|
|
86
|
+
});
|
|
89
87
|
expect(options).toMatchObject({
|
|
90
88
|
to: [
|
|
91
89
|
{
|
|
92
90
|
email: user.email,
|
|
93
|
-
name: null
|
|
94
|
-
}
|
|
91
|
+
name: null,
|
|
92
|
+
},
|
|
95
93
|
],
|
|
96
|
-
subject:
|
|
97
|
-
replyTo:
|
|
98
|
-
})
|
|
99
|
-
expect(options!.text).toContain(
|
|
94
|
+
subject: 'Hello',
|
|
95
|
+
replyTo: 'someone@example.com',
|
|
96
|
+
});
|
|
97
|
+
expect(options!.text).toContain('Content hier');
|
|
100
98
|
|
|
101
99
|
// Check notice
|
|
102
|
-
expect(options!.text).toContain(
|
|
103
|
-
})
|
|
104
|
-
|
|
105
|
-
it(
|
|
106
|
-
const organization = await new OrganizationFactory({}).create()
|
|
107
|
-
const user = await new UserFactory({ organization, permissions: Permissions.create({ level: PermissionLevel.Full }) }).create()
|
|
108
|
-
const user2 = await new UserFactory({ organization, permissions: Permissions.create({ level: PermissionLevel.Full }) }).create()
|
|
109
|
-
|
|
100
|
+
expect(options!.text).toContain('naar alle beheerders');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('should send to all full administrators if no emails defined', async () => {
|
|
104
|
+
const organization = await new OrganizationFactory({}).create();
|
|
105
|
+
const user = await new UserFactory({ organization, permissions: Permissions.create({ level: PermissionLevel.Full }) }).create();
|
|
106
|
+
const user2 = await new UserFactory({ organization, permissions: Permissions.create({ level: PermissionLevel.Full }) }).create();
|
|
107
|
+
|
|
110
108
|
// Admin that should get ignored
|
|
111
|
-
await new UserFactory({ organization, permissions: Permissions.create({ level: PermissionLevel.Read }) }).create()
|
|
109
|
+
await new UserFactory({ organization, permissions: Permissions.create({ level: PermissionLevel.Read }) }).create();
|
|
112
110
|
|
|
113
|
-
const options = await ForwardHandler.handle(
|
|
114
|
-
recipients: [organization.uri +
|
|
111
|
+
const options = await ForwardHandler.handle('From: someone@example.com\nSubject: Hello\nTo: ' + organization.uri + '@stamhoofd.email\nContent-Type: text/plain\n\nContent hier', {
|
|
112
|
+
recipients: [organization.uri + '@stamhoofd.email'],
|
|
115
113
|
spamVerdict: { status: 'PASS' },
|
|
116
114
|
virusVerdict: { status: 'PASS' },
|
|
117
115
|
spfVerdict: { status: 'PASS' },
|
|
118
116
|
dkimVerdict: { status: 'PASS' },
|
|
119
117
|
dmarcVerdict: { status: 'PASS' },
|
|
120
|
-
})
|
|
118
|
+
});
|
|
121
119
|
|
|
122
120
|
expect(options).toMatchObject({
|
|
123
|
-
subject:
|
|
124
|
-
replyTo:
|
|
125
|
-
})
|
|
121
|
+
subject: 'Hello',
|
|
122
|
+
replyTo: 'someone@example.com',
|
|
123
|
+
});
|
|
126
124
|
expect(options!.to).toIncludeAllMembers([
|
|
127
125
|
{
|
|
128
126
|
email: user.email,
|
|
129
|
-
name: null
|
|
127
|
+
name: null,
|
|
130
128
|
},
|
|
131
129
|
{
|
|
132
130
|
email: user2.email,
|
|
133
|
-
name: null
|
|
134
|
-
}
|
|
135
|
-
])
|
|
136
|
-
expect(options!.text).toContain(
|
|
131
|
+
name: null,
|
|
132
|
+
},
|
|
133
|
+
]);
|
|
134
|
+
expect(options!.text).toContain('Content hier');
|
|
137
135
|
|
|
138
136
|
// Check notice
|
|
139
|
-
expect(options!.text).toContain(
|
|
140
|
-
})
|
|
137
|
+
expect(options!.text).toContain('naar alle beheerders');
|
|
138
|
+
});
|
|
141
139
|
|
|
142
|
-
it(
|
|
143
|
-
const organization = await new OrganizationFactory({}).create()
|
|
140
|
+
it('should ignore aws bounce emails', async () => {
|
|
141
|
+
const organization = await new OrganizationFactory({}).create();
|
|
144
142
|
organization.privateMeta.emails.push(OrganizationEmail.create({
|
|
145
|
-
name:
|
|
146
|
-
email:
|
|
147
|
-
}))
|
|
143
|
+
name: 'First',
|
|
144
|
+
email: 'first@example.com',
|
|
145
|
+
}));
|
|
148
146
|
organization.privateMeta.emails.push(OrganizationEmail.create({
|
|
149
|
-
name:
|
|
150
|
-
email:
|
|
151
|
-
}))
|
|
152
|
-
await organization.save()
|
|
147
|
+
name: 'second',
|
|
148
|
+
email: 'second@example.com',
|
|
149
|
+
}));
|
|
150
|
+
await organization.save();
|
|
153
151
|
|
|
154
|
-
const options = await ForwardHandler.handle(
|
|
155
|
-
recipients: [organization.uri +
|
|
152
|
+
const options = await ForwardHandler.handle('From: bounces@amazonses.com\nSubject: Hello\nTo: ' + organization.uri + '@stamhoofd.email\nContent-Type: text/plain\n\nContent hier', {
|
|
153
|
+
recipients: [organization.uri + '@stamhoofd.email'],
|
|
156
154
|
spamVerdict: { status: 'PASS' },
|
|
157
155
|
virusVerdict: { status: 'PASS' },
|
|
158
156
|
spfVerdict: { status: 'PASS' },
|
|
159
157
|
dkimVerdict: { status: 'PASS' },
|
|
160
158
|
dmarcVerdict: { status: 'PASS' },
|
|
161
|
-
})
|
|
162
|
-
expect(options).toBeUndefined()
|
|
163
|
-
})
|
|
159
|
+
});
|
|
160
|
+
expect(options).toBeUndefined();
|
|
161
|
+
});
|
|
164
162
|
|
|
165
|
-
it(
|
|
166
|
-
const options = await ForwardHandler.handle(
|
|
167
|
-
recipients: [
|
|
163
|
+
it('should ignore aws bounce emails for unknown organizations', async () => {
|
|
164
|
+
const options = await ForwardHandler.handle('From: bounces@amazonses.com\nSubject: Hello\nTo: ksjdgsdgkjlsdg@stamhoofd.email\nContent-Type: text/plain\n\nContent hier', {
|
|
165
|
+
recipients: ['ksjdgsdgkjlsdg@stamhoofd.email'],
|
|
168
166
|
spamVerdict: { status: 'PASS' },
|
|
169
167
|
virusVerdict: { status: 'PASS' },
|
|
170
168
|
spfVerdict: { status: 'PASS' },
|
|
171
169
|
dkimVerdict: { status: 'PASS' },
|
|
172
170
|
dmarcVerdict: { status: 'PASS' },
|
|
173
|
-
})
|
|
174
|
-
expect(options).toBeUndefined()
|
|
175
|
-
})
|
|
176
|
-
|
|
177
|
-
it(
|
|
178
|
-
const address = new EmailAddress()
|
|
179
|
-
address.email =
|
|
180
|
-
address.organizationId = null
|
|
171
|
+
});
|
|
172
|
+
expect(options).toBeUndefined();
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('should unsubscribe email addresses that send to unsubscribe', async () => {
|
|
176
|
+
const address = new EmailAddress();
|
|
177
|
+
address.email = 'exampleaddress-unsusbcribe-test@example.com';
|
|
178
|
+
address.organizationId = null;
|
|
181
179
|
address.token = null;
|
|
182
|
-
await address.save()
|
|
180
|
+
await address.save();
|
|
183
181
|
|
|
184
|
-
const id = address.id
|
|
182
|
+
const id = address.id;
|
|
185
183
|
|
|
186
184
|
const options = await ForwardHandler.handle(`From: bounces@amazonses.com\nSubject: Hello\nTo: unsubscribe+${id}@stamhoofd.email\nContent-Type: text/plain\n\nContent hier`, {
|
|
187
185
|
recipients: [`unsubscribe+${id}@stamhoofd.email`],
|
|
@@ -190,16 +188,16 @@ describe("ForwardHandler", () => {
|
|
|
190
188
|
spfVerdict: { status: 'PASS' },
|
|
191
189
|
dkimVerdict: { status: 'PASS' },
|
|
192
190
|
dmarcVerdict: { status: 'PASS' },
|
|
193
|
-
})
|
|
194
|
-
expect(options).toBeUndefined()
|
|
191
|
+
});
|
|
192
|
+
expect(options).toBeUndefined();
|
|
195
193
|
|
|
196
194
|
// Refresh adress and check unsubscribed for all
|
|
197
|
-
const updatedAddress = await EmailAddress.getByID(id)
|
|
198
|
-
expect(updatedAddress).toBeDefined()
|
|
195
|
+
const updatedAddress = await EmailAddress.getByID(id);
|
|
196
|
+
expect(updatedAddress).toBeDefined();
|
|
199
197
|
expect(updatedAddress!.unsubscribedAll).toEqual(true);
|
|
200
|
-
})
|
|
198
|
+
});
|
|
201
199
|
|
|
202
|
-
it(
|
|
200
|
+
it('should forward unsubscribe emails to unrecognized id', async () => {
|
|
203
201
|
const options = await ForwardHandler.handle(`From: bounces@amazonses.com\nSubject: Hello\nTo: unsubscribe+testid@stamhoofd.email\nContent-Type: text/plain\n\nContent hier`, {
|
|
204
202
|
recipients: [`unsubscribe+testid@stamhoofd.email`],
|
|
205
203
|
spamVerdict: { status: 'PASS' },
|
|
@@ -207,10 +205,10 @@ describe("ForwardHandler", () => {
|
|
|
207
205
|
spfVerdict: { status: 'PASS' },
|
|
208
206
|
dkimVerdict: { status: 'PASS' },
|
|
209
207
|
dmarcVerdict: { status: 'PASS' },
|
|
210
|
-
})
|
|
208
|
+
});
|
|
211
209
|
expect(options).toMatchObject({
|
|
212
|
-
|
|
213
|
-
subject:
|
|
210
|
+
to: 'hallo@stamhoofd.be',
|
|
211
|
+
subject: 'E-mail unsubscribe mislukt',
|
|
214
212
|
});
|
|
215
|
-
})
|
|
216
|
-
})
|
|
213
|
+
});
|
|
214
|
+
});
|
|
@@ -1,119 +1,125 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
|
2
|
+
/* eslint-disable @typescript-eslint/no-redundant-type-constituents */
|
|
3
|
+
import { Email, EmailAddress, EmailInterfaceRecipient } from '@stamhoofd/email';
|
|
4
|
+
import { Organization } from '@stamhoofd/models';
|
|
5
|
+
import { Formatter } from '@stamhoofd/utility';
|
|
6
|
+
import { simpleParser } from 'mailparser';
|
|
5
7
|
|
|
6
8
|
export class ForwardHandler {
|
|
7
9
|
static async handle(content: any, receipt: {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
recipients: string[];
|
|
11
|
+
spamVerdict: { status: 'PASS' | string };
|
|
12
|
+
virusVerdict: { status: 'PASS' | string };
|
|
13
|
+
spfVerdict: { status: 'PASS' | string };
|
|
14
|
+
dkimVerdict: { status: 'PASS' | string };
|
|
15
|
+
dmarcVerdict: { status: 'PASS' | string };
|
|
16
|
+
},
|
|
15
17
|
) {
|
|
16
|
-
const recipients = receipt.recipients
|
|
17
|
-
const email: string | undefined = recipients[0]
|
|
18
|
-
const organization: Organization | undefined = email ? await Organization.getByEmail(email) : undefined
|
|
18
|
+
const recipients = receipt.recipients;
|
|
19
|
+
const email: string | undefined = recipients[0];
|
|
20
|
+
const organization: Organization | undefined = email ? await Organization.getByEmail(email) : undefined;
|
|
19
21
|
|
|
20
22
|
const parsed = await simpleParser(content);
|
|
21
|
-
const from = parsed.from?.value[0]?.address
|
|
23
|
+
const from = parsed.from?.value[0]?.address;
|
|
22
24
|
|
|
23
|
-
if (from && from.endsWith(
|
|
24
|
-
console.log(
|
|
25
|
+
if (from && from.endsWith('amazonses.com') && organization) {
|
|
26
|
+
console.log('Bounce e-mails from AWS SES for organizations are not forwarded. Received from ' + from + ', to ' + email);
|
|
25
27
|
return;
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
// Unsubscribe email?
|
|
29
31
|
for (const domain of Object.values(STAMHOOFD.domains.defaultBroadcastEmail ?? {})) {
|
|
30
|
-
if (email && email?.startsWith(
|
|
32
|
+
if (email && email?.startsWith('unsubscribe+') && email.endsWith('@' + domain)) {
|
|
31
33
|
// Get id
|
|
32
|
-
const id = email.substring(
|
|
33
|
-
const model = await EmailAddress.getByID(id)
|
|
34
|
-
|
|
34
|
+
const id = email.substring('unsubscribe+'.length, email.indexOf('@' + domain));
|
|
35
|
+
const model = await EmailAddress.getByID(id);
|
|
36
|
+
|
|
35
37
|
if (model) {
|
|
36
|
-
console.log('[Unsubscribe] Received an unsubscribe request for ' + model.email + ' from ' + from)
|
|
38
|
+
console.log('[Unsubscribe] Received an unsubscribe request for ' + model.email + ' from ' + from);
|
|
37
39
|
if (model.unsubscribedAll) {
|
|
38
40
|
// Ignore
|
|
39
41
|
return;
|
|
40
42
|
}
|
|
41
|
-
model.unsubscribedAll = true
|
|
42
|
-
await model.save()
|
|
43
|
-
}
|
|
44
|
-
|
|
43
|
+
model.unsubscribedAll = true;
|
|
44
|
+
await model.save();
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
console.error('[Unsubscribe] Received an unsubscribe request for unknown ID ' + id + ' from ' + from);
|
|
45
48
|
|
|
46
49
|
// Forward
|
|
47
50
|
return {
|
|
48
51
|
from: Email.getWebmasterFromEmail(),
|
|
49
52
|
to: Email.getWebmasterToEmail(),
|
|
50
|
-
subject:
|
|
51
|
-
text:
|
|
52
|
-
}
|
|
53
|
-
|
|
53
|
+
subject: 'E-mail unsubscribe mislukt',
|
|
54
|
+
text: 'Beste,\n\nEr werd een unsubscribe gemeld op ' + email + ' die niet kon worden verwerkt. Gelieve dit na te kijken.\n\nStamhoofd',
|
|
55
|
+
};
|
|
54
56
|
}
|
|
55
57
|
return;
|
|
56
58
|
}
|
|
57
59
|
}
|
|
58
60
|
|
|
59
|
-
if (receipt.spamVerdict.status
|
|
60
|
-
console.error(
|
|
61
|
+
if (receipt.spamVerdict.status !== 'PASS' || receipt.virusVerdict.status !== 'PASS' || !(receipt.spfVerdict.status == 'PASS' || receipt.dkimVerdict.status == 'PASS')) {
|
|
62
|
+
console.error('Received spam or virus e-mail. Ignoring', 'to', recipients, 'from', email, 'subject', parsed.subject);
|
|
61
63
|
return;
|
|
62
64
|
}
|
|
63
65
|
|
|
64
66
|
// Send a new e-mail
|
|
65
|
-
let defaultEmail: EmailInterfaceRecipient[]|string = Email.getWebmasterToEmail()
|
|
66
|
-
let organizationEmails: EmailInterfaceRecipient[] = []
|
|
67
|
-
const extraDescription =
|
|
68
|
-
|
|
67
|
+
let defaultEmail: EmailInterfaceRecipient[] | string = Email.getWebmasterToEmail();
|
|
68
|
+
let organizationEmails: EmailInterfaceRecipient[] = [];
|
|
69
|
+
const extraDescription = 'Dit bericht werd verstuurd naar ' + email + ', en werd automatisch doorgestuurd naar alle beheerders. Stel in Stamhoofd de e-mailadressen in om ervoor te zorgen dat antwoorden naar een specifiek e-mailadres worden verstuurd.';
|
|
70
|
+
|
|
69
71
|
function doBounce() {
|
|
70
72
|
if (!from) {
|
|
71
|
-
return
|
|
73
|
+
return;
|
|
72
74
|
}
|
|
73
75
|
|
|
74
|
-
if (from.endsWith(
|
|
76
|
+
if (from.endsWith('@amazonses.com')) {
|
|
75
77
|
// Ignore
|
|
76
|
-
return
|
|
78
|
+
return;
|
|
77
79
|
}
|
|
78
80
|
|
|
79
81
|
// Send back to receiver without including the original message to avoid spam
|
|
80
82
|
return {
|
|
81
83
|
from: email ?? Email.getWebmasterToEmail(),
|
|
82
84
|
to: from,
|
|
83
|
-
subject:
|
|
84
|
-
text:
|
|
85
|
-
}
|
|
85
|
+
subject: 'Ongeldig e-mailadres',
|
|
86
|
+
text: 'Beste,\n\nDe vereniging die je probeert te bereiken via ' + email + ' is helaas niet bereikbaar via dit e-mailadres. Dit e-mailadres wordt enkel gebruikt voor het versturen van automatische e-mails in naam van een vereniging. Probeer de vereniging te contacteren via een ander e-mailadres.\n\nBedankt.',
|
|
87
|
+
};
|
|
86
88
|
}
|
|
87
|
-
|
|
89
|
+
|
|
88
90
|
if (organization) {
|
|
89
|
-
organizationEmails = await organization.getReplyEmails()
|
|
91
|
+
organizationEmails = await organization.getReplyEmails();
|
|
90
92
|
if (!organizationEmails) {
|
|
91
|
-
if (STAMHOOFD.environment ===
|
|
93
|
+
if (STAMHOOFD.environment === 'test') {
|
|
92
94
|
// ignore
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
console.error('Missing reply emails for organization ' + organization.id);
|
|
95
98
|
}
|
|
96
99
|
return doBounce();
|
|
97
|
-
} else {
|
|
98
|
-
defaultEmail = organizationEmails
|
|
99
100
|
}
|
|
100
|
-
|
|
101
|
+
else {
|
|
102
|
+
defaultEmail = organizationEmails;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
101
106
|
return doBounce();
|
|
102
107
|
}
|
|
103
108
|
|
|
104
|
-
console.log(
|
|
105
|
-
|
|
106
|
-
let html: string | undefined = undefined
|
|
109
|
+
console.log('Forward to ' + defaultEmail);
|
|
110
|
+
|
|
111
|
+
let html: string | undefined = undefined;
|
|
107
112
|
|
|
108
113
|
if (parsed.html !== false) {
|
|
109
114
|
// Search for body
|
|
110
|
-
const body = parsed.html.toLowerCase().indexOf(
|
|
115
|
+
const body = parsed.html.toLowerCase().indexOf('<body');
|
|
111
116
|
|
|
112
|
-
if (body
|
|
113
|
-
const endTag = parsed.html.indexOf(
|
|
114
|
-
html = parsed.html.substring(0, endTag + 1) +
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
+
if (body !== -1) {
|
|
118
|
+
const endTag = parsed.html.indexOf('>', body);
|
|
119
|
+
html = parsed.html.substring(0, endTag + 1) + '<p><i>' + Formatter.escapeHtml(extraDescription) + '<br><br></i></p>' + parsed.html.substring(endTag + 1);
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
html = '<p><i>' + Formatter.escapeHtml(extraDescription) + '<br><br></i></p>' + parsed.html;
|
|
117
123
|
}
|
|
118
124
|
}
|
|
119
125
|
|
|
@@ -121,22 +127,22 @@ export class ForwardHandler {
|
|
|
121
127
|
from: email ?? Email.getWebmasterToEmail(),
|
|
122
128
|
to: defaultEmail,
|
|
123
129
|
replyTo: parsed.from?.text,
|
|
124
|
-
subject: parsed.subject ??
|
|
125
|
-
text: parsed.text ? extraDescription +
|
|
130
|
+
subject: parsed.subject ?? 'Doorgestuurd bericht',
|
|
131
|
+
text: parsed.text ? extraDescription + '\n\n' + parsed.text : undefined,
|
|
126
132
|
html: html,
|
|
127
|
-
attachments: parsed.attachments.flatMap(a => {
|
|
133
|
+
attachments: parsed.attachments.flatMap((a) => {
|
|
128
134
|
if (a.cid) {
|
|
129
135
|
// Already done inline in html
|
|
130
|
-
return []
|
|
136
|
+
return [];
|
|
131
137
|
}
|
|
132
138
|
return [{
|
|
133
|
-
filename: a.filename ??
|
|
134
|
-
content: a.content.toString(
|
|
135
|
-
contentType: a.contentType
|
|
136
|
-
}]
|
|
137
|
-
})
|
|
138
|
-
}
|
|
139
|
+
filename: a.filename ?? '',
|
|
140
|
+
content: a.content.toString('utf-8'),
|
|
141
|
+
contentType: a.contentType,
|
|
142
|
+
}];
|
|
143
|
+
}),
|
|
144
|
+
};
|
|
139
145
|
|
|
140
|
-
return options
|
|
146
|
+
return options;
|
|
141
147
|
}
|
|
142
148
|
}
|