@stamhoofd/backend 2.137.4 → 2.137.5

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 (68) hide show
  1. package/package.json +23 -17
  2. package/src/crons/drip-emails.ts +2 -1
  3. package/src/crons.ts +4 -2
  4. package/src/email-recipient-loaders/payments.ts +3 -2
  5. package/src/endpoints/auth/CreateAdminEndpoint.ts +3 -2
  6. package/src/endpoints/auth/CreateTokenEndpoint.ts +2 -1
  7. package/src/endpoints/auth/ForgotPasswordEndpoint.ts +3 -2
  8. package/src/endpoints/auth/PatchUserEndpoint.ts +6 -3
  9. package/src/endpoints/auth/RetryEmailVerificationEndpoint.ts +2 -1
  10. package/src/endpoints/auth/SignupEndpoint.ts +4 -2
  11. package/src/endpoints/global/email/CreateEmailEndpoint.ts +2 -1
  12. package/src/endpoints/global/email/GetAdminEmailsEndpoint.test.ts +1 -1
  13. package/src/endpoints/global/email/GetAdminEmailsEndpoint.ts +2 -1
  14. package/src/endpoints/global/email/GetEmailEndpoint.ts +2 -1
  15. package/src/endpoints/global/email/GetUserEmailsEndpoint.test.ts +1 -1
  16. package/src/endpoints/global/email/GetUserEmailsEndpoint.ts +2 -1
  17. package/src/endpoints/global/email/PatchEmailEndpoint.ts +2 -1
  18. package/src/endpoints/global/files/ExportToExcelEndpoint.ts +9 -5
  19. package/src/endpoints/global/files/UploadFile.test.ts +206 -0
  20. package/src/endpoints/global/files/UploadFile.ts +31 -6
  21. package/src/endpoints/global/files/UploadImage.test.ts +177 -0
  22. package/src/endpoints/global/files/UploadImage.ts +23 -4
  23. package/src/endpoints/global/files/upload-security.test.ts +837 -0
  24. package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +1 -1
  25. package/src/endpoints/global/organizations/CreateOrganizationEndpoint.ts +2 -1
  26. package/src/endpoints/organization/dashboard/documents/GetDocumentTemplateXML.ts +2 -1
  27. package/src/endpoints/organization/dashboard/organization/SetOrganizationDomainEndpoint.ts +3 -2
  28. package/src/endpoints/organization/dashboard/users/PatchApiUserEndpoint.ts +5 -3
  29. package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.ts +4 -3
  30. package/src/endpoints/organization/shared/GetDocumentHtml.ts +2 -1
  31. package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +4 -3
  32. package/src/excel-loaders/balance-items.ts +1 -1
  33. package/src/excel-loaders/event-notifications.ts +6 -7
  34. package/src/excel-loaders/members.test.ts +2 -2
  35. package/src/excel-loaders/members.ts +9 -10
  36. package/src/excel-loaders/organizations.ts +14 -20
  37. package/src/excel-loaders/payments.ts +1 -1
  38. package/src/excel-loaders/platform-memberships.ts +7 -7
  39. package/src/excel-loaders/platform-sheets.test.ts +113 -0
  40. package/src/excel-loaders/receivable-balances.ts +1 -1
  41. package/src/excel-loaders/registrations.ts +9 -9
  42. package/src/helpers/AdminPermissionChecker.ts +1 -1
  43. package/src/helpers/AuthenticatedStructures.ts +12 -9
  44. package/src/helpers/MembershipCharger.ts +3 -2
  45. package/src/services/BalanceItemService.ts +2 -1
  46. package/src/services/DocumentRenderService.test.ts +229 -0
  47. package/src/services/DocumentRenderService.ts +180 -0
  48. package/src/services/EmailPreviewService.test.ts +300 -0
  49. package/src/services/EmailPreviewService.ts +239 -0
  50. package/src/services/FileSignService.test.ts +85 -0
  51. package/src/services/FileSignService.ts +23 -1
  52. package/src/services/OrderService.test.ts +308 -0
  53. package/src/services/OrderService.ts +214 -0
  54. package/src/services/OrganizationDNSService.test.ts +177 -0
  55. package/src/services/OrganizationDNSService.ts +282 -0
  56. package/src/services/OrganizationEmailService.test.ts +57 -0
  57. package/src/services/OrganizationEmailService.ts +211 -0
  58. package/src/services/PasswordForgotService.test.ts +99 -0
  59. package/src/services/PasswordForgotService.ts +38 -0
  60. package/src/services/PlatformMembershipService.test.ts +180 -0
  61. package/src/services/PlatformMembershipService.ts +281 -4
  62. package/src/services/RegistrationService.ts +3 -2
  63. package/src/services/STPackageService.test.ts +191 -0
  64. package/src/services/STPackageService.ts +114 -2
  65. package/src/services/VerificationCodeService.test.ts +71 -0
  66. package/src/services/VerificationCodeService.ts +100 -0
  67. package/tests/e2e/documents.test.ts +2 -1
  68. package/tests/e2e/private-files.test.ts +77 -14
@@ -0,0 +1,177 @@
1
+ import { EmailMocker } from '@stamhoofd/email';
2
+ import type { Organization } from '@stamhoofd/models';
3
+ import { EmailTemplateFactory, OrganizationFactory, UserFactory } from '@stamhoofd/models';
4
+ import { EmailTemplateType, PermissionLevel, Permissions } from '@stamhoofd/structures';
5
+ import { TestUtils } from '@stamhoofd/test-utils';
6
+ import { OrganizationDNSService } from './OrganizationDNSService.js';
7
+
8
+ const validateDNSRecordsMock = vi.hoisted(() => vi.fn());
9
+
10
+ vi.mock('@stamhoofd/models/helpers/DNSValidator.js', () => ({
11
+ validateDNSRecords: validateDNSRecordsMock,
12
+ }));
13
+
14
+ const templateTypes = [
15
+ EmailTemplateType.OrganizationStableDNS,
16
+ EmailTemplateType.OrganizationUnstableDNS,
17
+ EmailTemplateType.OrganizationValidDNS,
18
+ EmailTemplateType.OrganizationInvalidDNS,
19
+ EmailTemplateType.OrganizationDNSSetupComplete,
20
+ ];
21
+
22
+ async function createTemplates() {
23
+ for (const type of templateTypes) {
24
+ await new EmailTemplateFactory({
25
+ type,
26
+ subject: type,
27
+ html: '<p>{{organizationName}} - {{mailDomain}}</p>',
28
+ text: '{{organizationName}} - {{mailDomain}}',
29
+ }).create();
30
+ }
31
+ }
32
+
33
+ async function createOrganizationWithAdmin(): Promise<Organization> {
34
+ const organization = await new OrganizationFactory({}).create();
35
+ await new UserFactory({
36
+ organization,
37
+ email: 'admin@example.com',
38
+ firstName: 'Ad',
39
+ lastName: 'Min',
40
+ permissions: Permissions.create({ level: PermissionLevel.Full }),
41
+ }).create();
42
+ return organization;
43
+ }
44
+
45
+ function daysAgo(days: number) {
46
+ return new Date(Date.now() - days * 24 * 60 * 60 * 1000);
47
+ }
48
+
49
+ function setupEnvironment() {
50
+ TestUtils.setEnvironment('userMode', 'organization');
51
+
52
+ // The notification emails localize the marketing domain, which the shared test environment does not set
53
+ TestUtils.setEnvironment('domains', {
54
+ ...STAMHOOFD.domains,
55
+ marketing: { '': 'stamhoofd.dev', BE: 'be.stamhoofd.dev', NL: 'nl.stamhoofd.dev' },
56
+ });
57
+ }
58
+
59
+ describe('OrganizationDNSService.updateDNSRecords notifications', () => {
60
+ beforeAll(async () => {
61
+ await createTemplates();
62
+ });
63
+
64
+ beforeEach(() => {
65
+ setupEnvironment();
66
+ validateDNSRecordsMock.mockReset();
67
+ });
68
+
69
+ test('Sends the setup complete email the first time the mail domain becomes valid', async () => {
70
+ validateDNSRecordsMock.mockResolvedValue({ allValid: true, hasAllNonTXT: true });
71
+
72
+ const organization = await createOrganizationWithAdmin();
73
+ organization.privateMeta.pendingMailDomain = 'mail.example.com';
74
+ organization.privateMeta.mailDomainActive = false;
75
+ organization.serverMeta.didSendDomainSetupMail = false;
76
+ await organization.save();
77
+
78
+ await OrganizationDNSService.updateDNSRecords(organization);
79
+
80
+ const emails = await EmailMocker.transactional.getSucceededEmails();
81
+ expect(emails).toHaveLength(1);
82
+ expect(emails[0].subject).toBe(EmailTemplateType.OrganizationDNSSetupComplete);
83
+ expect(emails[0].to).toContain('admin@example.com');
84
+ expect(emails[0].html).toContain('mail.example.com');
85
+ expect(emails[0].html).toContain(organization.name);
86
+ expect(emails[0].bcc).toBeUndefined();
87
+
88
+ expect(organization.privateMeta.mailDomain).toBe('mail.example.com');
89
+ expect(organization.privateMeta.pendingMailDomain).toBe(null);
90
+ expect(organization.privateMeta.mailDomainActive).toBe(true);
91
+ expect(organization.serverMeta.didSendDomainSetupMail).toBe(true);
92
+ });
93
+
94
+ test('Sends the valid DNS email instead of the setup complete email once the setup email was sent before', async () => {
95
+ validateDNSRecordsMock.mockResolvedValue({ allValid: true, hasAllNonTXT: true });
96
+
97
+ const organization = await createOrganizationWithAdmin();
98
+ organization.privateMeta.pendingMailDomain = 'mail.example.com';
99
+ organization.privateMeta.mailDomainActive = false;
100
+ organization.serverMeta.didSendDomainSetupMail = true;
101
+ organization.serverMeta.DNSRecordWarningCount = 1;
102
+ await organization.save();
103
+
104
+ await OrganizationDNSService.updateDNSRecords(organization);
105
+
106
+ const emails = await EmailMocker.transactional.getSucceededEmails();
107
+ expect(emails).toHaveLength(1);
108
+ expect(emails[0].subject).toBe(EmailTemplateType.OrganizationValidDNS);
109
+ expect(emails[0].bcc).toBeUndefined();
110
+ expect(organization.serverMeta.DNSRecordWarningCount).toBe(0);
111
+ });
112
+
113
+ test('Sends the stable DNS email with a bcc when the DNS settings become stable again', async () => {
114
+ validateDNSRecordsMock.mockResolvedValue({ allValid: true, hasAllNonTXT: true });
115
+
116
+ const organization = await createOrganizationWithAdmin();
117
+ organization.serverMeta.isDNSUnstable = true;
118
+ organization.serverMeta.lastInvalidDNSDates = [];
119
+ await organization.save();
120
+
121
+ await OrganizationDNSService.updateDNSRecords(organization);
122
+
123
+ const emails = await EmailMocker.transactional.getSucceededEmails();
124
+ expect(emails).toHaveLength(1);
125
+ expect(emails[0].subject).toBe(EmailTemplateType.OrganizationStableDNS);
126
+ expect(emails[0].bcc).toContain('simon@stamhoofd.be');
127
+ expect(organization.serverMeta.isDNSUnstable).toBe(false);
128
+ });
129
+
130
+ test('Sends the unstable DNS email with a bcc when the DNS settings become unstable', async () => {
131
+ validateDNSRecordsMock.mockResolvedValue({ allValid: false, hasAllNonTXT: false });
132
+
133
+ const organization = await createOrganizationWithAdmin();
134
+ organization.privateMeta.mailDomain = 'mail.example.com';
135
+ organization.privateMeta.mailDomainActive = true;
136
+ organization.serverMeta.isDNSUnstable = false;
137
+ organization.serverMeta.firstInvalidDNSRecords = undefined;
138
+
139
+ // 4 earlier failures + the one of this run makes the DNS unstable
140
+ organization.serverMeta.lastInvalidDNSDates = [daysAgo(1), daysAgo(2), daysAgo(3), daysAgo(4)];
141
+ await organization.save();
142
+
143
+ await OrganizationDNSService.updateDNSRecords(organization);
144
+
145
+ const emails = await EmailMocker.transactional.getSucceededEmails();
146
+ expect(emails).toHaveLength(1);
147
+ expect(emails[0].subject).toBe(EmailTemplateType.OrganizationUnstableDNS);
148
+ expect(emails[0].bcc).toContain('simon@stamhoofd.be');
149
+
150
+ expect(organization.serverMeta.isDNSUnstable).toBe(true);
151
+ expect(organization.privateMeta.mailDomain).toBe(null);
152
+ expect(organization.privateMeta.pendingMailDomain).toBe('mail.example.com');
153
+ expect(organization.privateMeta.mailDomainActive).toBe(false);
154
+ });
155
+
156
+ test('Sends the invalid DNS email once when the DNS settings break without becoming unstable', async () => {
157
+ validateDNSRecordsMock.mockResolvedValue({ allValid: false, hasAllNonTXT: false });
158
+
159
+ const organization = await createOrganizationWithAdmin();
160
+ organization.privateMeta.mailDomain = 'mail.example.com';
161
+ organization.privateMeta.mailDomainActive = true;
162
+ organization.serverMeta.didSendDomainSetupMail = true;
163
+ organization.serverMeta.DNSRecordWarningCount = 0;
164
+ await organization.save();
165
+
166
+ await OrganizationDNSService.updateDNSRecords(organization);
167
+
168
+ const emails = await EmailMocker.transactional.getSucceededEmails();
169
+ expect(emails).toHaveLength(1);
170
+ expect(emails[0].subject).toBe(EmailTemplateType.OrganizationInvalidDNS);
171
+ expect(emails[0].bcc).toBeUndefined();
172
+ expect(organization.serverMeta.DNSRecordWarningCount).toBe(1);
173
+
174
+ await OrganizationDNSService.updateDNSRecords(organization);
175
+ expect(await EmailMocker.transactional.getSucceededEmails()).toHaveLength(1);
176
+ });
177
+ });
@@ -0,0 +1,282 @@
1
+ import type { GetEmailIdentityCommandOutput } from '@aws-sdk/client-sesv2';
2
+ import { CreateEmailIdentityCommand, DeleteEmailIdentityCommand, GetEmailIdentityCommand, PutEmailIdentityFeedbackAttributesCommand, PutEmailIdentityMailFromAttributesCommand, SESv2Client } from '@aws-sdk/client-sesv2';
3
+ import type { Organization } from '@stamhoofd/models';
4
+ import { validateDNSRecords } from '@stamhoofd/models/helpers/DNSValidator.js';
5
+ import { DNSRecordStatus, EmailTemplateType } from '@stamhoofd/structures';
6
+ import { Formatter } from '@stamhoofd/utility';
7
+ import { OrganizationEmailService } from './OrganizationEmailService.js';
8
+
9
+ export class OrganizationDNSService {
10
+ static async updateDNSRecords(organization: Organization) {
11
+ // Check initial status
12
+ let isValidRecords = true;
13
+ for (const record of organization.privateMeta.dnsRecords) {
14
+ if (record.status !== DNSRecordStatus.Valid) {
15
+ isValidRecords = false;
16
+ }
17
+ }
18
+
19
+ const { allValid } = await validateDNSRecords(organization.privateMeta.dnsRecords);
20
+
21
+ if (organization.registerDomain ?? organization.privateMeta.pendingRegisterDomain) {
22
+ const registerDomainRecord = (organization.privateMeta.pendingRegisterDomain ?? organization.registerDomain) + '.';
23
+ const records = organization.privateMeta.dnsRecords.filter(r => r.name === registerDomainRecord);
24
+ const areRegisterDomainRecordsValid = records.length === 0 || records.every(r => r.status === DNSRecordStatus.Valid);
25
+
26
+ if (areRegisterDomainRecordsValid) {
27
+ // We can setup the register domain if needed
28
+ if (organization.privateMeta.pendingRegisterDomain !== null) {
29
+ organization.registerDomain = organization.privateMeta.pendingRegisterDomain;
30
+ organization.privateMeta.pendingRegisterDomain = null;
31
+
32
+ console.log('Did set register domain for ' + organization.id + ' to ' + organization.registerDomain);
33
+ }
34
+ } else {
35
+ // Clear register domain
36
+ if (organization.registerDomain) {
37
+ // We need to clear it, to prevent sending e-mails with invalid links
38
+ organization.privateMeta.pendingRegisterDomain = organization.privateMeta.pendingRegisterDomain ?? organization.registerDomain;
39
+ organization.registerDomain = null;
40
+
41
+ console.log('Cleared register domain for ' + organization.id + ' because of invalid non txt records');
42
+ }
43
+ }
44
+ }
45
+
46
+ if (allValid) {
47
+ if (organization.privateMeta.pendingMailDomain !== null) {
48
+ organization.privateMeta.mailDomain = organization.privateMeta.pendingMailDomain;
49
+ organization.privateMeta.pendingMailDomain = null;
50
+ }
51
+
52
+ const wasUnstable = organization.serverMeta.isDNSUnstable;
53
+ organization.serverMeta.markDNSValid();
54
+
55
+ const didSendDomainSetupMail = organization.serverMeta.didSendDomainSetupMail;
56
+ const didSendWarning = organization.serverMeta.DNSRecordWarningCount > 0;
57
+ organization.serverMeta.DNSRecordWarningCount = 0;
58
+
59
+ const wasActive = organization.privateMeta.mailDomainActive;
60
+ await this.updateAWSMailIdenitity(organization);
61
+
62
+ // yay! Do not Save until after doing AWS changes
63
+ await organization.save();
64
+
65
+ if (wasUnstable && !organization.serverMeta.isDNSUnstable) {
66
+ console.warn('DNS settings became stable for ' + organization.name + ' ' + organization.id);
67
+
68
+ await OrganizationEmailService.sendEmailTemplate(organization, {
69
+ type: EmailTemplateType.OrganizationStableDNS,
70
+ bcc: true,
71
+ });
72
+ } else if (!wasActive && organization.privateMeta.mailDomainActive && (!didSendDomainSetupMail || didSendWarning) && !organization.serverMeta.isDNSUnstable) {
73
+ organization.serverMeta.didSendDomainSetupMail = true;
74
+ await organization.save();
75
+
76
+ if (!didSendDomainSetupMail) {
77
+ await OrganizationEmailService.sendEmailTemplate(organization, {
78
+ type: EmailTemplateType.OrganizationDNSSetupComplete,
79
+ });
80
+ } else {
81
+ await OrganizationEmailService.sendEmailTemplate(organization, {
82
+ type: EmailTemplateType.OrganizationValidDNS,
83
+ });
84
+ }
85
+ }
86
+ } else {
87
+ // DNS settings gone broken
88
+ if (organization.privateMeta.mailDomain) {
89
+ organization.privateMeta.pendingMailDomain = organization.privateMeta.pendingMailDomain ?? organization.privateMeta.mailDomain;
90
+ organization.privateMeta.mailDomain = null;
91
+ }
92
+
93
+ const wasDNSUnstable = organization.serverMeta.isDNSUnstable;
94
+
95
+ organization.serverMeta.markDNSFailure();
96
+
97
+ // disable AWS emails
98
+ organization.privateMeta.mailDomainActive = false;
99
+
100
+ // save
101
+ await organization.save();
102
+
103
+ if (!wasDNSUnstable && organization.serverMeta.isDNSUnstable) {
104
+ // DNS became instable
105
+ console.warn('DNS settings became instable for ' + organization.name + ' ' + organization.id);
106
+
107
+ await OrganizationEmailService.sendEmailTemplate(organization, {
108
+ type: EmailTemplateType.OrganizationUnstableDNS,
109
+ bcc: true,
110
+ });
111
+ } else if (!organization.serverMeta.isDNSUnstable && organization.serverMeta.didSendDomainSetupMail && organization.serverMeta.DNSRecordWarningCount == 0) {
112
+ organization.serverMeta.DNSRecordWarningCount += 1;
113
+ await organization.save();
114
+
115
+ await OrganizationEmailService.sendEmailTemplate(organization, {
116
+ type: EmailTemplateType.OrganizationInvalidDNS,
117
+ });
118
+ }
119
+ }
120
+ }
121
+
122
+ static get forbiddenEmailDomains() {
123
+ return [
124
+ STAMHOOFD.domains.dashboard,
125
+ ...Object.values(STAMHOOFD.domains.defaultBroadcastEmail ?? {}),
126
+ ...Object.values(STAMHOOFD.domains.defaultTransactionalEmail ?? {}),
127
+ ];
128
+ }
129
+
130
+ static async deleteAWSMailIdenitity(organization: Organization, mailDomain: string) {
131
+ // Protect specific domain names
132
+ if (this.forbiddenEmailDomains.includes(mailDomain.toLowerCase())) {
133
+ return;
134
+ }
135
+
136
+ if (STAMHOOFD.environment !== 'production') {
137
+ // Temporary ignore this
138
+ return;
139
+ }
140
+
141
+ const client = new SESv2Client({});
142
+
143
+ // Check if mail identitiy already exists..
144
+ try {
145
+ const cmd = new GetEmailIdentityCommand({
146
+ EmailIdentity: mailDomain,
147
+ });
148
+ const result = await client.send(cmd);
149
+
150
+ if (result.VerifiedForSendingStatus === true) {
151
+ console.log('Cant delete AWS mail idenitiy @' + organization.id + ' for ' + mailDomain + ': already validated and might be in use by other organizations');
152
+ return;
153
+ }
154
+
155
+ console.log('Deleting AWS mail identity @' + organization.id + ' for ' + mailDomain);
156
+
157
+ const deleteCmd = new DeleteEmailIdentityCommand({
158
+ EmailIdentity: mailDomain,
159
+ });
160
+
161
+ await client.send(deleteCmd);
162
+ console.log('Deleted AWS mail idenitiy @' + organization.id + ' for ' + organization.privateMeta.mailDomain);
163
+ } catch (e) {
164
+ console.error('Could not delete AWS email identitiy @' + organization.id + ' for ' + organization.privateMeta.mailDomain);
165
+ console.error(e);
166
+ }
167
+ }
168
+
169
+ /**
170
+ * Create or update the AWS mail idenitiy and also update the active state of the mailDomain
171
+ */
172
+ static async updateAWSMailIdenitity(organization: Organization) {
173
+ if (organization.privateMeta.mailDomain === null) {
174
+ return;
175
+ }
176
+
177
+ // Protect specific domain names
178
+ if (['stamhoofd.be', 'stamhoofd.nl', 'stamhoofd.shop', 'stamhoofd.app', 'stamhoofd.email'].includes(organization.privateMeta.mailDomain)) {
179
+ console.error('Tried to validate AWS mail identity with protected domains @' + organization.id);
180
+ organization.privateMeta.mailDomainActive = false;
181
+ return;
182
+ }
183
+
184
+ if (STAMHOOFD.environment !== 'production') {
185
+ // Temporary ignore this
186
+ organization.privateMeta.mailDomainActive = true;
187
+ return;
188
+ }
189
+
190
+ const client = new SESv2Client({});
191
+ const expectedConfigurationSetName = Formatter.slug(STAMHOOFD.platformName + '-domains');
192
+
193
+ // Check if mail identitiy already exists..
194
+ let exists = false;
195
+ let existing: GetEmailIdentityCommandOutput | undefined = undefined;
196
+ try {
197
+ const cmd = new GetEmailIdentityCommand({
198
+ EmailIdentity: organization.privateMeta.mailDomain,
199
+ });
200
+
201
+ existing = await client.send(cmd);
202
+ exists = true;
203
+
204
+ console.log('AWS mail idenitiy exists already: just checking the verification status in AWS @' + organization.id);
205
+
206
+ if (existing.ConfigurationSetName !== expectedConfigurationSetName) {
207
+ // Not allowed to use this identity
208
+ organization.privateMeta.mailDomainActive = false;
209
+ console.error('Organization is not allowed to use email identity ' + organization.privateMeta.mailDomain + ' @' + organization.id + ', got ' + existing.ConfigurationSetName);
210
+ return;
211
+ }
212
+
213
+ organization.privateMeta.mailDomainActive = existing.VerifiedForSendingStatus ?? false;
214
+
215
+ if (existing.VerifiedForSendingStatus !== true) {
216
+ console.error('Not validated @' + organization.id);
217
+ }
218
+
219
+ if (existing.VerifiedForSendingStatus !== true && existing.DkimAttributes?.Status === 'FAILED') {
220
+ console.error('AWS failed to verify DKIM records. Triggering a forced recheck @' + organization.id);
221
+
222
+ const deleteCmd = new DeleteEmailIdentityCommand({
223
+ EmailIdentity: organization.privateMeta.mailDomain,
224
+ });
225
+ await client.send(deleteCmd);
226
+
227
+ // Recreate it immediately
228
+ exists = false;
229
+ }
230
+ } catch (e) {
231
+ console.error(e);
232
+ }
233
+
234
+ if (!exists) {
235
+ console.log('Creating email identity in AWS SES...');
236
+
237
+ const cmd = new CreateEmailIdentityCommand({
238
+ EmailIdentity: organization.privateMeta.mailDomain,
239
+ ConfigurationSetName: expectedConfigurationSetName,
240
+ DkimSigningAttributes: {
241
+ DomainSigningPrivateKey: organization.serverMeta.privateDKIMKey!,
242
+ DomainSigningSelector: Formatter.slug(STAMHOOFD.platformName),
243
+ },
244
+ Tags: [
245
+ {
246
+ Key: 'OrganizationId',
247
+ Value: organization.id,
248
+ },
249
+ {
250
+ Key: 'Environment',
251
+ Value: STAMHOOFD.environment ?? 'Unknown',
252
+ },
253
+ ],
254
+ });
255
+
256
+ const result = await client.send(cmd);
257
+ organization.privateMeta.mailDomainActive = result.VerifiedForSendingStatus ?? false;
258
+
259
+ // Disable email forwarding of bounces and complaints
260
+ // We handle this now with the configuration set
261
+ const putFeedbackCmd = new PutEmailIdentityFeedbackAttributesCommand({
262
+ EmailIdentity: organization.privateMeta.mailDomain,
263
+ EmailForwardingEnabled: false,
264
+ });
265
+
266
+ await client.send(putFeedbackCmd);
267
+ }
268
+
269
+ if (organization.privateMeta.mailFromDomain && (!exists || (existing && (!existing.MailFromAttributes || existing.MailFromAttributes.MailFromDomain !== organization.privateMeta.mailFromDomain)))) {
270
+ // Also set a from domain, to fix SPF
271
+ console.log('Setting mail from domain: ' + organization.privateMeta.mailFromDomain + ' for ' + organization.id);
272
+
273
+ const cmd = new PutEmailIdentityMailFromAttributesCommand({
274
+ EmailIdentity: organization.privateMeta.mailDomain,
275
+ BehaviorOnMxFailure: 'USE_DEFAULT_VALUE',
276
+ MailFromDomain: organization.privateMeta.mailFromDomain,
277
+ });
278
+
279
+ await client.send(cmd);
280
+ }
281
+ }
282
+ }
@@ -0,0 +1,57 @@
1
+ import { EmailMocker } from '@stamhoofd/email';
2
+ import type { Organization } from '@stamhoofd/models';
3
+ import { EmailTemplateFactory, OrganizationFactory, UserFactory } from '@stamhoofd/models';
4
+ import { EmailTemplateType, PermissionLevel, Permissions } from '@stamhoofd/structures';
5
+ import { TestUtils } from '@stamhoofd/test-utils';
6
+ import { OrganizationEmailService } from './OrganizationEmailService.js';
7
+
8
+ async function createOrganizationWithAdmin(): Promise<Organization> {
9
+ const organization = await new OrganizationFactory({}).create();
10
+ await new UserFactory({
11
+ organization,
12
+ email: 'admin@example.com',
13
+ firstName: 'Ad',
14
+ lastName: 'Min',
15
+ permissions: Permissions.create({ level: PermissionLevel.Full }),
16
+ }).create();
17
+ return organization;
18
+ }
19
+
20
+ function setupEnvironment() {
21
+ TestUtils.setEnvironment('userMode', 'organization');
22
+
23
+ // The notification emails localize the marketing domain, which the shared test environment does not set
24
+ TestUtils.setEnvironment('domains', {
25
+ ...STAMHOOFD.domains,
26
+ marketing: { '': 'stamhoofd.dev', BE: 'be.stamhoofd.dev', NL: 'nl.stamhoofd.dev' },
27
+ });
28
+ }
29
+
30
+ describe('OrganizationEmailService.checkDrips', () => {
31
+ beforeAll(async () => {
32
+ await new EmailTemplateFactory({
33
+ type: EmailTemplateType.OrganizationDripWelcome,
34
+ subject: EmailTemplateType.OrganizationDripWelcome,
35
+ html: '<p>{{organizationName}} - {{mailDomain}}</p>',
36
+ text: '{{organizationName}} - {{mailDomain}}',
37
+ }).create();
38
+ });
39
+
40
+ beforeEach(() => {
41
+ setupEnvironment();
42
+ });
43
+
44
+ test('Sends the welcome drip email only once', async () => {
45
+ const organization = await createOrganizationWithAdmin();
46
+
47
+ await OrganizationEmailService.checkDrips(organization);
48
+
49
+ const emails = await EmailMocker.transactional.getSucceededEmails();
50
+ expect(emails).toHaveLength(1);
51
+ expect(emails[0].subject).toBe(EmailTemplateType.OrganizationDripWelcome);
52
+ expect(organization.serverMeta.hasEmail(EmailTemplateType.OrganizationDripWelcome)).toBe(true);
53
+
54
+ await OrganizationEmailService.checkDrips(organization);
55
+ expect(await EmailMocker.transactional.getSucceededEmails()).toHaveLength(1);
56
+ });
57
+ });