@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.
- package/package.json +23 -17
- package/src/crons/drip-emails.ts +2 -1
- package/src/crons.ts +4 -2
- package/src/email-recipient-loaders/payments.ts +3 -2
- package/src/endpoints/auth/CreateAdminEndpoint.ts +3 -2
- package/src/endpoints/auth/CreateTokenEndpoint.ts +2 -1
- package/src/endpoints/auth/ForgotPasswordEndpoint.ts +3 -2
- package/src/endpoints/auth/PatchUserEndpoint.ts +6 -3
- package/src/endpoints/auth/RetryEmailVerificationEndpoint.ts +2 -1
- package/src/endpoints/auth/SignupEndpoint.ts +4 -2
- package/src/endpoints/global/email/CreateEmailEndpoint.ts +2 -1
- package/src/endpoints/global/email/GetAdminEmailsEndpoint.test.ts +1 -1
- package/src/endpoints/global/email/GetAdminEmailsEndpoint.ts +2 -1
- package/src/endpoints/global/email/GetEmailEndpoint.ts +2 -1
- package/src/endpoints/global/email/GetUserEmailsEndpoint.test.ts +1 -1
- package/src/endpoints/global/email/GetUserEmailsEndpoint.ts +2 -1
- package/src/endpoints/global/email/PatchEmailEndpoint.ts +2 -1
- package/src/endpoints/global/files/ExportToExcelEndpoint.ts +9 -5
- package/src/endpoints/global/files/UploadFile.test.ts +206 -0
- package/src/endpoints/global/files/UploadFile.ts +31 -6
- package/src/endpoints/global/files/UploadImage.test.ts +177 -0
- package/src/endpoints/global/files/UploadImage.ts +23 -4
- package/src/endpoints/global/files/upload-security.test.ts +837 -0
- package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +1 -1
- package/src/endpoints/global/organizations/CreateOrganizationEndpoint.ts +2 -1
- package/src/endpoints/organization/dashboard/documents/GetDocumentTemplateXML.ts +2 -1
- package/src/endpoints/organization/dashboard/organization/SetOrganizationDomainEndpoint.ts +3 -2
- package/src/endpoints/organization/dashboard/users/PatchApiUserEndpoint.ts +5 -3
- package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.ts +4 -3
- package/src/endpoints/organization/shared/GetDocumentHtml.ts +2 -1
- package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +4 -3
- package/src/excel-loaders/balance-items.ts +1 -1
- package/src/excel-loaders/event-notifications.ts +6 -7
- package/src/excel-loaders/members.test.ts +2 -2
- package/src/excel-loaders/members.ts +9 -10
- package/src/excel-loaders/organizations.ts +14 -20
- package/src/excel-loaders/payments.ts +1 -1
- package/src/excel-loaders/platform-memberships.ts +7 -7
- package/src/excel-loaders/platform-sheets.test.ts +113 -0
- package/src/excel-loaders/receivable-balances.ts +1 -1
- package/src/excel-loaders/registrations.ts +9 -9
- package/src/helpers/AdminPermissionChecker.ts +1 -1
- package/src/helpers/AuthenticatedStructures.ts +12 -9
- package/src/helpers/MembershipCharger.ts +3 -2
- package/src/services/BalanceItemService.ts +2 -1
- package/src/services/DocumentRenderService.test.ts +229 -0
- package/src/services/DocumentRenderService.ts +180 -0
- package/src/services/EmailPreviewService.test.ts +300 -0
- package/src/services/EmailPreviewService.ts +239 -0
- package/src/services/FileSignService.test.ts +85 -0
- package/src/services/FileSignService.ts +23 -1
- package/src/services/OrderService.test.ts +308 -0
- package/src/services/OrderService.ts +214 -0
- package/src/services/OrganizationDNSService.test.ts +177 -0
- package/src/services/OrganizationDNSService.ts +282 -0
- package/src/services/OrganizationEmailService.test.ts +57 -0
- package/src/services/OrganizationEmailService.ts +211 -0
- package/src/services/PasswordForgotService.test.ts +99 -0
- package/src/services/PasswordForgotService.ts +38 -0
- package/src/services/PlatformMembershipService.test.ts +180 -0
- package/src/services/PlatformMembershipService.ts +281 -4
- package/src/services/RegistrationService.ts +3 -2
- package/src/services/STPackageService.test.ts +191 -0
- package/src/services/STPackageService.ts +114 -2
- package/src/services/VerificationCodeService.test.ts +71 -0
- package/src/services/VerificationCodeService.ts +100 -0
- package/tests/e2e/documents.test.ts +2 -1
- 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
|
+
});
|