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