@stamhoofd/models 2.78.3 → 2.79.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/factories/GroupFactory.d.ts +4 -0
- package/dist/src/factories/GroupFactory.d.ts.map +1 -1
- package/dist/src/factories/GroupFactory.js +11 -1
- package/dist/src/factories/GroupFactory.js.map +1 -1
- package/dist/src/factories/MemberResponsibilityRecordFactory.d.ts +14 -0
- package/dist/src/factories/MemberResponsibilityRecordFactory.d.ts.map +1 -0
- package/dist/src/factories/MemberResponsibilityRecordFactory.js +34 -0
- package/dist/src/factories/MemberResponsibilityRecordFactory.js.map +1 -0
- package/dist/src/factories/OrganizationFactory.d.ts +1 -0
- package/dist/src/factories/OrganizationFactory.d.ts.map +1 -1
- package/dist/src/factories/OrganizationFactory.js +4 -0
- package/dist/src/factories/OrganizationFactory.js.map +1 -1
- package/dist/src/factories/OrganizationRegistrationPeriodFactory.d.ts +13 -0
- package/dist/src/factories/OrganizationRegistrationPeriodFactory.d.ts.map +1 -0
- package/dist/src/factories/OrganizationRegistrationPeriodFactory.js +20 -0
- package/dist/src/factories/OrganizationRegistrationPeriodFactory.js.map +1 -0
- package/dist/src/factories/OrganizationTagFactory.d.ts +10 -0
- package/dist/src/factories/OrganizationTagFactory.d.ts.map +1 -0
- package/dist/src/factories/OrganizationTagFactory.js +23 -0
- package/dist/src/factories/OrganizationTagFactory.js.map +1 -0
- package/dist/src/factories/PlatformResponsibilityFactory.d.ts +11 -0
- package/dist/src/factories/PlatformResponsibilityFactory.d.ts.map +1 -0
- package/dist/src/factories/PlatformResponsibilityFactory.js +25 -0
- package/dist/src/factories/PlatformResponsibilityFactory.js.map +1 -0
- package/dist/src/factories/RegistrationFactory.d.ts +6 -2
- package/dist/src/factories/RegistrationFactory.d.ts.map +1 -1
- package/dist/src/factories/RegistrationFactory.js +6 -9
- package/dist/src/factories/RegistrationFactory.js.map +1 -1
- package/dist/src/factories/RegistrationPeriodFactory.d.ts +1 -0
- package/dist/src/factories/RegistrationPeriodFactory.d.ts.map +1 -1
- package/dist/src/factories/RegistrationPeriodFactory.js +4 -0
- package/dist/src/factories/RegistrationPeriodFactory.js.map +1 -1
- package/dist/src/factories/index.d.ts +18 -0
- package/dist/src/factories/index.d.ts.map +1 -0
- package/dist/src/factories/index.js +21 -0
- package/dist/src/factories/index.js.map +1 -0
- package/dist/src/helpers/EmailBuilder.d.ts +8 -10
- package/dist/src/helpers/EmailBuilder.d.ts.map +1 -1
- package/dist/src/helpers/EmailBuilder.js +44 -42
- package/dist/src/helpers/EmailBuilder.js.map +1 -1
- package/dist/src/index.d.ts +2 -14
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +3 -14
- package/dist/src/index.js.map +1 -1
- package/dist/src/models/Email.d.ts +9 -2
- package/dist/src/models/Email.d.ts.map +1 -1
- package/dist/src/models/Email.js +18 -16
- package/dist/src/models/Email.js.map +1 -1
- package/dist/src/models/Email.test.js +183 -2
- package/dist/src/models/Email.test.js.map +1 -1
- package/dist/src/models/Member.d.ts +3 -1
- package/dist/src/models/Member.d.ts.map +1 -1
- package/dist/src/models/Member.js +14 -8
- package/dist/src/models/Member.js.map +1 -1
- package/dist/src/models/Order.d.ts +0 -2
- package/dist/src/models/Order.d.ts.map +1 -1
- package/dist/src/models/Order.js +0 -48
- package/dist/src/models/Order.js.map +1 -1
- package/dist/src/models/Organization.d.ts +1 -16
- package/dist/src/models/Organization.d.ts.map +1 -1
- package/dist/src/models/Organization.js +5 -86
- package/dist/src/models/Organization.js.map +1 -1
- package/dist/src/models/Platform.d.ts +11 -3
- package/dist/src/models/Platform.d.ts.map +1 -1
- package/dist/src/models/Platform.js +76 -24
- package/dist/src/models/Platform.js.map +1 -1
- package/dist/src/models/Platform.test.d.ts +2 -0
- package/dist/src/models/Platform.test.d.ts.map +1 -0
- package/dist/src/models/Platform.test.js +90 -0
- package/dist/src/models/Platform.test.js.map +1 -0
- package/dist/src/models/index.d.ts +1 -1
- package/dist/src/models/index.d.ts.map +1 -1
- package/dist/src/models/index.js +2 -3
- package/dist/src/models/index.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -4
- package/src/factories/GroupFactory.ts +16 -3
- package/src/factories/MemberResponsibilityRecordFactory.ts +35 -0
- package/src/factories/OrganizationFactory.ts +5 -0
- package/src/factories/OrganizationRegistrationPeriodFactory.ts +22 -0
- package/src/factories/OrganizationTagFactory.ts +23 -0
- package/src/factories/PlatformResponsibilityFactory.ts +25 -0
- package/src/factories/RegistrationFactory.ts +15 -6
- package/src/factories/RegistrationPeriodFactory.ts +4 -0
- package/src/factories/index.ts +17 -0
- package/src/helpers/EmailBuilder.ts +54 -50
- package/src/index.ts +4 -15
- package/src/models/Email.test.ts +217 -5
- package/src/models/Email.ts +22 -20
- package/src/models/Member.ts +20 -10
- package/src/models/Order.ts +0 -55
- package/src/models/Organization.ts +6 -101
- package/src/models/Platform.test.ts +107 -0
- package/src/models/Platform.ts +86 -25
- package/src/models/index.ts +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Email, EmailAddress, EmailBuilder } from '@stamhoofd/email';
|
|
1
|
+
import { Email, EmailAddress, EmailBuilder, EmailInterfaceRecipient } from '@stamhoofd/email';
|
|
2
2
|
import { BalanceItem as BalanceItemStruct, EmailTemplateType, OrganizationEmail, Platform as PlatformStruct, ReceivableBalanceType, Recipient, Replacement } from '@stamhoofd/structures';
|
|
3
3
|
import { Formatter } from '@stamhoofd/utility';
|
|
4
4
|
|
|
@@ -104,40 +104,37 @@ export async function getDefaultEmailFrom(organization: Organization | null, opt
|
|
|
104
104
|
|
|
105
105
|
if (organization) {
|
|
106
106
|
// Default email address for the chosen email type
|
|
107
|
-
let from = organization.getDefaultFrom(organization.i18n,
|
|
107
|
+
let from = organization.getDefaultFrom(organization.i18n, options.type ?? 'broadcast');
|
|
108
108
|
|
|
109
109
|
const sender: OrganizationEmail | undefined = (preferEmailId ? organization.privateMeta.emails.find(e => e.id === preferEmailId) : null) ?? organization.privateMeta.emails.find(e => e.default) ?? organization.privateMeta.emails[0];
|
|
110
|
-
let replyTo:
|
|
110
|
+
let replyTo: EmailInterfaceRecipient | undefined = undefined;
|
|
111
111
|
|
|
112
112
|
if (sender) {
|
|
113
|
-
replyTo =
|
|
113
|
+
replyTo = {
|
|
114
|
+
email: sender.email,
|
|
115
|
+
name: sender.name,
|
|
116
|
+
};
|
|
114
117
|
|
|
115
118
|
// Can we send from this e-mail or reply-to?
|
|
116
119
|
if (await canSendFromEmail(sender.email, organization)) {
|
|
117
|
-
from =
|
|
120
|
+
from = {
|
|
121
|
+
email: sender.email,
|
|
122
|
+
name: sender.name,
|
|
123
|
+
};
|
|
118
124
|
replyTo = undefined;
|
|
119
125
|
}
|
|
120
126
|
|
|
121
|
-
//
|
|
122
|
-
if (
|
|
123
|
-
from =
|
|
124
|
-
}
|
|
125
|
-
else {
|
|
126
|
-
from = '"' + organization.name.replaceAll('"', '\\"') + '" <' + from + '>';
|
|
127
|
+
// Default to organization name
|
|
128
|
+
if (!from.name) {
|
|
129
|
+
from.name = organization.name;
|
|
127
130
|
}
|
|
128
131
|
|
|
129
132
|
if (replyTo) {
|
|
130
|
-
if (
|
|
131
|
-
replyTo =
|
|
132
|
-
}
|
|
133
|
-
else {
|
|
134
|
-
replyTo = '"' + organization.name.replaceAll('"', '\\"') + '" <' + replyTo + '>';
|
|
133
|
+
if (!replyTo.name) {
|
|
134
|
+
replyTo.name = organization.name;
|
|
135
135
|
}
|
|
136
136
|
}
|
|
137
137
|
}
|
|
138
|
-
else {
|
|
139
|
-
from = '"' + organization.name.replaceAll('"', '\\"') + '" <' + from + '>';
|
|
140
|
-
}
|
|
141
138
|
|
|
142
139
|
return {
|
|
143
140
|
from, replyTo,
|
|
@@ -151,42 +148,41 @@ export async function getDefaultEmailFrom(organization: Organization | null, opt
|
|
|
151
148
|
const broadcastDomain = i18n.localizedDomains.defaultBroadcastEmail();
|
|
152
149
|
|
|
153
150
|
const domain = (options.type === 'transactional' ? transactionalDomain : broadcastDomain);
|
|
154
|
-
let from =
|
|
151
|
+
let from: EmailInterfaceRecipient = {
|
|
152
|
+
email: 'hallo@' + domain,
|
|
153
|
+
};
|
|
155
154
|
|
|
156
155
|
// Platform
|
|
157
156
|
const sender: OrganizationEmail | undefined = (preferEmailId ? platform.privateConfig.emails.find(e => e.id === preferEmailId) : null) ?? platform.privateConfig.emails.find(e => e.default) ?? platform.privateConfig.emails[0];
|
|
158
|
-
let replyTo:
|
|
157
|
+
let replyTo: EmailInterfaceRecipient | undefined = undefined;
|
|
159
158
|
|
|
160
159
|
if (sender) {
|
|
161
|
-
replyTo =
|
|
160
|
+
replyTo = {
|
|
161
|
+
email: sender.email,
|
|
162
|
+
name: sender.name,
|
|
163
|
+
};
|
|
162
164
|
|
|
163
165
|
// Are we allowed to send an e-mail from this domain?
|
|
164
166
|
if (await canSendFromEmail(sender.email, null)) {
|
|
165
167
|
// Allowed to send from
|
|
166
|
-
from =
|
|
168
|
+
from = {
|
|
169
|
+
email: sender.email,
|
|
170
|
+
name: sender.name,
|
|
171
|
+
};
|
|
167
172
|
replyTo = undefined;
|
|
168
173
|
}
|
|
169
174
|
|
|
170
|
-
//
|
|
171
|
-
if (
|
|
172
|
-
from =
|
|
173
|
-
}
|
|
174
|
-
else {
|
|
175
|
-
from = '"' + platform.config.name.replaceAll('"', '\\"') + '" <' + from + '>';
|
|
175
|
+
// Default to platform name
|
|
176
|
+
if (!from.name) {
|
|
177
|
+
from.name = platform.config.name;
|
|
176
178
|
}
|
|
177
179
|
|
|
178
180
|
if (replyTo) {
|
|
179
|
-
if (
|
|
180
|
-
replyTo =
|
|
181
|
-
}
|
|
182
|
-
else {
|
|
183
|
-
replyTo = '"' + platform.config.name.replaceAll('"', '\\"') + '" <' + replyTo + '>';
|
|
181
|
+
if (!replyTo.name) {
|
|
182
|
+
replyTo.name = platform.config.name;
|
|
184
183
|
}
|
|
185
184
|
}
|
|
186
185
|
}
|
|
187
|
-
else {
|
|
188
|
-
from = '"' + platform.config.name.replaceAll('"', '\\"') + '" <' + from + '>';
|
|
189
|
-
}
|
|
190
186
|
|
|
191
187
|
return {
|
|
192
188
|
from, replyTo,
|
|
@@ -206,7 +202,7 @@ export async function sendEmailTemplate(organization: Organization | null, optio
|
|
|
206
202
|
}
|
|
207
203
|
}
|
|
208
204
|
|
|
209
|
-
|
|
205
|
+
async function getEmailBuilderForTemplate(organization: Organization | null, options: Omit<EmailBuilderOptions, 'subject' | 'html'> & { template: Omit<EmailTemplateOptions, 'organizationId'> }) {
|
|
210
206
|
const template = await getEmailTemplate({
|
|
211
207
|
...options.template,
|
|
212
208
|
organizationId: organization?.id ?? null,
|
|
@@ -226,8 +222,8 @@ export async function getEmailBuilderForTemplate(organization: Organization | nu
|
|
|
226
222
|
export type EmailBuilderOptions = {
|
|
227
223
|
defaultReplacements?: Replacement[];
|
|
228
224
|
recipients: Recipient[];
|
|
229
|
-
from:
|
|
230
|
-
replyTo?:
|
|
225
|
+
from: EmailInterfaceRecipient;
|
|
226
|
+
replyTo?: EmailInterfaceRecipient | null;
|
|
231
227
|
subject: string;
|
|
232
228
|
html: string;
|
|
233
229
|
attachments?: {
|
|
@@ -239,7 +235,7 @@ export type EmailBuilderOptions = {
|
|
|
239
235
|
type?: 'transactional' | 'broadcast';
|
|
240
236
|
unsubscribeType?: 'all' | 'marketing';
|
|
241
237
|
fromStamhoofd?: boolean;
|
|
242
|
-
singleBcc?:
|
|
238
|
+
singleBcc?: EmailInterfaceRecipient;
|
|
243
239
|
replaceAll?: { from: string; to: string }[]; // replace in all e-mails, not recipient dependent
|
|
244
240
|
callback?: (error: Error | null) => void; // for each email
|
|
245
241
|
};
|
|
@@ -333,8 +329,6 @@ export async function getEmailBuilder(organization: Organization | null, email:
|
|
|
333
329
|
}
|
|
334
330
|
email.recipients = cleaned;
|
|
335
331
|
|
|
336
|
-
const fromAddress = Email.parseEmailStr(email.from)[0];
|
|
337
|
-
|
|
338
332
|
// Update recipients
|
|
339
333
|
for (const recipient of email.recipients) {
|
|
340
334
|
recipient.replacements = recipient.replacements.slice();
|
|
@@ -346,7 +340,8 @@ export async function getEmailBuilder(organization: Organization | null, email:
|
|
|
346
340
|
await fillRecipientReplacements(recipient, {
|
|
347
341
|
organization,
|
|
348
342
|
platform,
|
|
349
|
-
|
|
343
|
+
from: email.from,
|
|
344
|
+
replyTo: email.replyTo ?? null,
|
|
350
345
|
});
|
|
351
346
|
}
|
|
352
347
|
|
|
@@ -382,7 +377,7 @@ export async function getEmailBuilder(organization: Organization | null, email:
|
|
|
382
377
|
return {
|
|
383
378
|
from: email.from,
|
|
384
379
|
replyTo: email.replyTo ?? undefined,
|
|
385
|
-
bcc: emailIndex === 1 ? email.singleBcc : undefined,
|
|
380
|
+
bcc: emailIndex === 1 && email.singleBcc ? [email.singleBcc] : undefined,
|
|
386
381
|
to: [
|
|
387
382
|
{
|
|
388
383
|
// Name will get cleaned by email service
|
|
@@ -404,12 +399,13 @@ export async function getEmailBuilder(organization: Organization | null, email:
|
|
|
404
399
|
export async function fillRecipientReplacements(recipient: Recipient, options: {
|
|
405
400
|
organization: Organization | null;
|
|
406
401
|
platform?: PlatformStruct;
|
|
407
|
-
|
|
402
|
+
from: EmailInterfaceRecipient | null;
|
|
403
|
+
replyTo: EmailInterfaceRecipient | null;
|
|
408
404
|
}) {
|
|
409
405
|
if (!options.platform) {
|
|
410
406
|
options.platform = await Platform.getSharedPrivateStruct();
|
|
411
407
|
}
|
|
412
|
-
const { organization, platform,
|
|
408
|
+
const { organization, platform, from, replyTo } = options;
|
|
413
409
|
recipient.replacements = recipient.replacements.slice();
|
|
414
410
|
|
|
415
411
|
// Default signInUrl
|
|
@@ -454,11 +450,19 @@ export async function fillRecipientReplacements(recipient: Recipient, options: {
|
|
|
454
450
|
);
|
|
455
451
|
}
|
|
456
452
|
|
|
457
|
-
if (
|
|
453
|
+
if (from || replyTo) {
|
|
458
454
|
recipient.replacements.push(Replacement.create({
|
|
459
455
|
token: 'fromAddress',
|
|
460
|
-
value:
|
|
456
|
+
value: replyTo?.email ?? from!.email,
|
|
461
457
|
}));
|
|
458
|
+
|
|
459
|
+
const name = replyTo?.name ?? from?.name;
|
|
460
|
+
if (name) {
|
|
461
|
+
recipient.replacements.push(Replacement.create({
|
|
462
|
+
token: 'fromName',
|
|
463
|
+
value: name,
|
|
464
|
+
}));
|
|
465
|
+
}
|
|
462
466
|
}
|
|
463
467
|
|
|
464
468
|
recipient.replacements.push(...recipient.getDefaultReplacements());
|
|
@@ -469,6 +473,6 @@ export async function fillRecipientReplacements(recipient: Recipient, options: {
|
|
|
469
473
|
}
|
|
470
474
|
|
|
471
475
|
// Defaults
|
|
472
|
-
const extra = platform.config.getEmailReplacements();
|
|
476
|
+
const extra = platform.config.getEmailReplacements(platform);
|
|
473
477
|
recipient.replacements.push(...extra);
|
|
474
478
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,24 +1,13 @@
|
|
|
1
|
-
export * from './factories/AddressFactory';
|
|
2
|
-
export * from './factories/EmergencyContactFactory';
|
|
3
|
-
export * from './factories/GroupFactory';
|
|
4
|
-
export * from './factories/MemberFactory';
|
|
5
|
-
export * from './factories/OrganizationFactory';
|
|
6
|
-
export * from './factories/ParentFactory';
|
|
7
|
-
export * from './factories/RecordFactory';
|
|
8
|
-
export * from './factories/RegisterCodeFactory';
|
|
9
|
-
export * from './factories/RegistrationFactory';
|
|
10
|
-
export * from './factories/UserFactory';
|
|
11
|
-
export * from './factories/WebshopFactory';
|
|
12
|
-
export * from './factories/BalanceItemFactory';
|
|
13
|
-
export * from './factories/RegistrationPeriodFactory';
|
|
14
|
-
|
|
15
1
|
// Helpers
|
|
16
2
|
export * from './helpers/EmailBuilder';
|
|
17
3
|
export * from './helpers/GroupBuilder';
|
|
4
|
+
export * from './helpers/MemberMerger';
|
|
18
5
|
export * from './helpers/RateLimiter';
|
|
19
6
|
export * from './helpers/WebshopCounter';
|
|
20
|
-
export * from './helpers/MemberMerger';
|
|
21
7
|
|
|
22
8
|
// Models
|
|
23
9
|
export * from './models';
|
|
24
10
|
export * from './structures/OrganizationServerMetaData';
|
|
11
|
+
|
|
12
|
+
// Factories
|
|
13
|
+
export * from './factories';
|
package/src/models/Email.test.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { EmailRecipientFilter, EmailRecipientFilterType, EmailRecipientsStatus, EmailRecipient as EmailRecipientStruct, EmailRecipientSubfilter, EmailStatus, LimitedFilteredRequest, PaginatedResponse } from '@stamhoofd/structures';
|
|
1
|
+
import { EmailRecipientFilter, EmailRecipientFilterType, EmailRecipientsStatus, EmailRecipient as EmailRecipientStruct, EmailRecipientSubfilter, EmailStatus, LimitedFilteredRequest, OrganizationMetaData, PaginatedResponse } from '@stamhoofd/structures';
|
|
2
2
|
import { Email } from './Email';
|
|
3
3
|
import { EmailRecipient } from './EmailRecipient';
|
|
4
4
|
import { EmailMocker } from '@stamhoofd/email';
|
|
@@ -43,7 +43,7 @@ async function buildEmail(data: {
|
|
|
43
43
|
model.status = data.status ?? EmailStatus.Draft;
|
|
44
44
|
model.attachments = [];
|
|
45
45
|
model.fromAddress = data.fromAddress ?? 'test@stamhoofd.be';
|
|
46
|
-
model.fromName = data.fromName ??
|
|
46
|
+
model.fromName = data.fromName ?? null;
|
|
47
47
|
|
|
48
48
|
await model.save();
|
|
49
49
|
|
|
@@ -493,10 +493,9 @@ describe('Model.Email', () => {
|
|
|
493
493
|
},
|
|
494
494
|
});
|
|
495
495
|
|
|
496
|
-
const organization = await new OrganizationFactory({
|
|
497
|
-
}).create();
|
|
496
|
+
const organization = await new OrganizationFactory({}).create();
|
|
498
497
|
|
|
499
|
-
const platform = await Platform.
|
|
498
|
+
const platform = await Platform.getForEditing();
|
|
500
499
|
platform.membershipOrganizationId = organization.id;
|
|
501
500
|
await platform.save();
|
|
502
501
|
|
|
@@ -530,4 +529,217 @@ describe('Model.Email', () => {
|
|
|
530
529
|
replyTo: undefined,
|
|
531
530
|
});
|
|
532
531
|
}, 15_000);
|
|
532
|
+
|
|
533
|
+
describe('Replacements', () => {
|
|
534
|
+
it('[Regression] When sending an email the organization fromAddress is replaced to the reply-to if it is not allowed to be sent from', async () => {
|
|
535
|
+
TestUtils.setEnvironment('domains', {
|
|
536
|
+
...STAMHOOFD.domains,
|
|
537
|
+
defaultTransactionalEmail: {
|
|
538
|
+
'': 'my-platform.com',
|
|
539
|
+
},
|
|
540
|
+
defaultBroadcastEmail: {
|
|
541
|
+
'': 'broadcast.my-platform.com',
|
|
542
|
+
},
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
const organization = await new OrganizationFactory({}).create();
|
|
546
|
+
|
|
547
|
+
const model = await buildEmail({
|
|
548
|
+
organizationId: organization.id,
|
|
549
|
+
recipients: [
|
|
550
|
+
EmailRecipientStruct.create({
|
|
551
|
+
email: 'example@domain.be',
|
|
552
|
+
}),
|
|
553
|
+
],
|
|
554
|
+
fromAddress: 'custom@customdomain.com',
|
|
555
|
+
fromName: 'Custom Name',
|
|
556
|
+
html: '{{fromAddress}}',
|
|
557
|
+
text: '{{fromAddress}}',
|
|
558
|
+
subject: '{{fromAddress}}',
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
await model.send();
|
|
562
|
+
await model.refresh();
|
|
563
|
+
|
|
564
|
+
// Check if it was sent correctly
|
|
565
|
+
expect(model.recipientsStatus).toBe(EmailRecipientsStatus.Created);
|
|
566
|
+
expect(model.recipientCount).toBe(1);
|
|
567
|
+
expect(model.status).toBe(EmailStatus.Sent);
|
|
568
|
+
|
|
569
|
+
// Both have succeeded
|
|
570
|
+
expect(EmailMocker.broadcast.getSucceededCount()).toBe(1);
|
|
571
|
+
expect(EmailMocker.broadcast.getFailedCount()).toBe(0);
|
|
572
|
+
|
|
573
|
+
// Check to header
|
|
574
|
+
expect(EmailMocker.broadcast.getSucceededEmail(0)).toMatchObject({
|
|
575
|
+
to: 'example@domain.be',
|
|
576
|
+
from: '"Custom Name" <noreply-' + organization.uri + '@broadcast.my-platform.com>',
|
|
577
|
+
replyTo: '"Custom Name" <custom@customdomain.com>', // domain has changed here
|
|
578
|
+
subject: 'custom@customdomain.com',
|
|
579
|
+
html: 'custom@customdomain.com',
|
|
580
|
+
text: 'custom@customdomain.com',
|
|
581
|
+
});
|
|
582
|
+
}, 15_000);
|
|
583
|
+
|
|
584
|
+
it('Default organization replacements work as expected', async () => {
|
|
585
|
+
TestUtils.setEnvironment('domains', {
|
|
586
|
+
...STAMHOOFD.domains,
|
|
587
|
+
defaultTransactionalEmail: {
|
|
588
|
+
'': 'my-platform.com',
|
|
589
|
+
},
|
|
590
|
+
defaultBroadcastEmail: {
|
|
591
|
+
'': 'broadcast.my-platform.com',
|
|
592
|
+
},
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
const brightYellow = '#FFD700';
|
|
596
|
+
const expectedContrastColor = '#000'; // black
|
|
597
|
+
|
|
598
|
+
const organization = await new OrganizationFactory({
|
|
599
|
+
meta: OrganizationMetaData.create({
|
|
600
|
+
color: brightYellow,
|
|
601
|
+
}),
|
|
602
|
+
}).create();
|
|
603
|
+
|
|
604
|
+
const model = await buildEmail({
|
|
605
|
+
organizationId: organization.id,
|
|
606
|
+
recipients: [
|
|
607
|
+
EmailRecipientStruct.create({
|
|
608
|
+
email: 'example@domain.be',
|
|
609
|
+
}),
|
|
610
|
+
],
|
|
611
|
+
fromAddress: 'custom@customdomain.com',
|
|
612
|
+
html: '{{primaryColor}};{{primaryColorContrast}};{{organizationName}};{{fromName}}',
|
|
613
|
+
text: '{{primaryColor}};{{primaryColorContrast}};{{organizationName}};{{fromName}}',
|
|
614
|
+
subject: '{{primaryColor}};{{primaryColorContrast}};{{organizationName}};{{fromName}}',
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
await model.send();
|
|
618
|
+
await model.refresh();
|
|
619
|
+
|
|
620
|
+
// Check if it was sent correctly
|
|
621
|
+
expect(model.recipientsStatus).toBe(EmailRecipientsStatus.Created);
|
|
622
|
+
expect(model.recipientCount).toBe(1);
|
|
623
|
+
expect(model.status).toBe(EmailStatus.Sent);
|
|
624
|
+
|
|
625
|
+
// Both have succeeded
|
|
626
|
+
expect(EmailMocker.broadcast.getSucceededCount()).toBe(1);
|
|
627
|
+
expect(EmailMocker.broadcast.getFailedCount()).toBe(0);
|
|
628
|
+
|
|
629
|
+
// Check to header
|
|
630
|
+
expect(EmailMocker.broadcast.getSucceededEmail(0)).toMatchObject({
|
|
631
|
+
subject: `${brightYellow};${expectedContrastColor};${organization.name};${organization.name}`,
|
|
632
|
+
html: `${brightYellow};${expectedContrastColor};${organization.name};${organization.name}`,
|
|
633
|
+
text: `${brightYellow};${expectedContrastColor};${organization.name};${organization.name}`,
|
|
634
|
+
});
|
|
635
|
+
}, 15_000);
|
|
636
|
+
|
|
637
|
+
it('Organization replacements default to platform replacements if not set', async () => {
|
|
638
|
+
TestUtils.setEnvironment('domains', {
|
|
639
|
+
...STAMHOOFD.domains,
|
|
640
|
+
defaultTransactionalEmail: {
|
|
641
|
+
'': 'my-platform.com',
|
|
642
|
+
},
|
|
643
|
+
defaultBroadcastEmail: {
|
|
644
|
+
'': 'broadcast.my-platform.com',
|
|
645
|
+
},
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
const brightBlue = '#0000FF';
|
|
649
|
+
const expectedContrastColor = '#fff'; // white
|
|
650
|
+
|
|
651
|
+
const platform = await Platform.getForEditing();
|
|
652
|
+
platform.config.color = brightBlue;
|
|
653
|
+
await platform.save();
|
|
654
|
+
|
|
655
|
+
const organization = await new OrganizationFactory({
|
|
656
|
+
meta: OrganizationMetaData.create({
|
|
657
|
+
color: null,
|
|
658
|
+
}),
|
|
659
|
+
}).create();
|
|
660
|
+
|
|
661
|
+
const model = await buildEmail({
|
|
662
|
+
organizationId: organization.id,
|
|
663
|
+
recipients: [
|
|
664
|
+
EmailRecipientStruct.create({
|
|
665
|
+
email: 'example@domain.be',
|
|
666
|
+
}),
|
|
667
|
+
],
|
|
668
|
+
fromAddress: 'custom@customdomain.com',
|
|
669
|
+
fromName: 'Custom Name',
|
|
670
|
+
html: '{{primaryColor}};{{primaryColorContrast}};{{organizationName}};{{fromName}}',
|
|
671
|
+
text: '{{primaryColor}};{{primaryColorContrast}};{{organizationName}};{{fromName}}',
|
|
672
|
+
subject: '{{primaryColor}};{{primaryColorContrast}};{{organizationName}};{{fromName}}',
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
await model.send();
|
|
676
|
+
await model.refresh();
|
|
677
|
+
|
|
678
|
+
// Check if it was sent correctly
|
|
679
|
+
expect(model.recipientsStatus).toBe(EmailRecipientsStatus.Created);
|
|
680
|
+
expect(model.recipientCount).toBe(1);
|
|
681
|
+
expect(model.status).toBe(EmailStatus.Sent);
|
|
682
|
+
|
|
683
|
+
// Both have succeeded
|
|
684
|
+
expect(EmailMocker.broadcast.getSucceededCount()).toBe(1);
|
|
685
|
+
expect(EmailMocker.broadcast.getFailedCount()).toBe(0);
|
|
686
|
+
|
|
687
|
+
// Check to header
|
|
688
|
+
expect(EmailMocker.broadcast.getSucceededEmail(0)).toMatchObject({
|
|
689
|
+
subject: `${brightBlue};${expectedContrastColor};${organization.name};Custom Name`,
|
|
690
|
+
html: `${brightBlue};${expectedContrastColor};${organization.name};Custom Name`,
|
|
691
|
+
text: `${brightBlue};${expectedContrastColor};${organization.name};Custom Name`,
|
|
692
|
+
});
|
|
693
|
+
}, 15_000);
|
|
694
|
+
|
|
695
|
+
it('Platform replacements are used for platform level emails', async () => {
|
|
696
|
+
TestUtils.setEnvironment('domains', {
|
|
697
|
+
...STAMHOOFD.domains,
|
|
698
|
+
defaultTransactionalEmail: {
|
|
699
|
+
'': 'my-platform.com',
|
|
700
|
+
},
|
|
701
|
+
defaultBroadcastEmail: {
|
|
702
|
+
'': 'broadcast.my-platform.com',
|
|
703
|
+
},
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
const darkRed = '#8B0000';
|
|
707
|
+
const expectedContrastColor = '#fff'; // white
|
|
708
|
+
|
|
709
|
+
const platform = await Platform.getForEditing();
|
|
710
|
+
platform.config.color = darkRed;
|
|
711
|
+
await platform.save();
|
|
712
|
+
|
|
713
|
+
const model = await buildEmail({
|
|
714
|
+
recipients: [
|
|
715
|
+
EmailRecipientStruct.create({
|
|
716
|
+
email: 'example@domain.be',
|
|
717
|
+
}),
|
|
718
|
+
],
|
|
719
|
+
fromAddress: 'custom@customdomain.com',
|
|
720
|
+
html: '{{primaryColor}};{{primaryColorContrast}};{{organizationName}};{{fromName}}',
|
|
721
|
+
text: '{{primaryColor}};{{primaryColorContrast}};{{organizationName}};{{fromName}}',
|
|
722
|
+
subject: '{{primaryColor}};{{primaryColorContrast}};{{organizationName}};{{fromName}}',
|
|
723
|
+
});
|
|
724
|
+
|
|
725
|
+
await model.send();
|
|
726
|
+
await model.refresh();
|
|
727
|
+
|
|
728
|
+
// Check if it was sent correctly
|
|
729
|
+
expect(model.recipientsStatus).toBe(EmailRecipientsStatus.Created);
|
|
730
|
+
expect(model.recipientCount).toBe(1);
|
|
731
|
+
expect(model.status).toBe(EmailStatus.Sent);
|
|
732
|
+
|
|
733
|
+
// Both have succeeded
|
|
734
|
+
expect(EmailMocker.broadcast.getSucceededCount()).toBe(1);
|
|
735
|
+
expect(EmailMocker.broadcast.getFailedCount()).toBe(0);
|
|
736
|
+
|
|
737
|
+
// Check to header
|
|
738
|
+
expect(EmailMocker.broadcast.getSucceededEmail(0)).toMatchObject({
|
|
739
|
+
subject: `${darkRed};${expectedContrastColor};${platform.config.name};${platform.config.name}`,
|
|
740
|
+
html: `${darkRed};${expectedContrastColor};${platform.config.name};${platform.config.name}`,
|
|
741
|
+
text: `${darkRed};${expectedContrastColor};${platform.config.name};${platform.config.name}`,
|
|
742
|
+
});
|
|
743
|
+
}, 15_000);
|
|
744
|
+
});
|
|
533
745
|
});
|
package/src/models/Email.ts
CHANGED
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
import { column } from '@simonbackx/simple-database';
|
|
2
|
-
import { EmailAttachment, EmailPreview, EmailRecipientFilter, EmailRecipientFilterType, EmailRecipientsStatus, EmailRecipient as EmailRecipientStruct, EmailStatus, Email as EmailStruct, EmailTemplateType, getExampleRecipient, LimitedFilteredRequest, PaginatedResponse,
|
|
2
|
+
import { EmailAttachment, EmailPreview, EmailRecipientFilter, EmailRecipientFilterType, EmailRecipientsStatus, EmailRecipient as EmailRecipientStruct, EmailStatus, Email as EmailStruct, EmailTemplateType, getExampleRecipient, LimitedFilteredRequest, PaginatedResponse, SortItemDirection, StamhoofdFilter } from '@stamhoofd/structures';
|
|
3
3
|
import { v4 as uuidv4 } from 'uuid';
|
|
4
4
|
|
|
5
5
|
import { AnyDecoder, ArrayDecoder } from '@simonbackx/simple-encoding';
|
|
6
6
|
import { SimpleError } from '@simonbackx/simple-errors';
|
|
7
7
|
import { I18n } from '@stamhoofd/backend-i18n';
|
|
8
|
-
import { Email as EmailClass } from '@stamhoofd/email';
|
|
8
|
+
import { Email as EmailClass, EmailInterfaceRecipient } from '@stamhoofd/email';
|
|
9
9
|
import { QueueHandler } from '@stamhoofd/queues';
|
|
10
10
|
import { QueryableModel, SQL, SQLWhereSign } from '@stamhoofd/sql';
|
|
11
|
-
import { Formatter } from '@stamhoofd/utility';
|
|
12
11
|
import { canSendFromEmail, fillRecipientReplacements, getEmailBuilder } from '../helpers/EmailBuilder';
|
|
13
12
|
import { EmailRecipient } from './EmailRecipient';
|
|
14
|
-
import { Organization } from './Organization';
|
|
15
13
|
import { EmailTemplate } from './EmailTemplate';
|
|
14
|
+
import { Organization } from './Organization';
|
|
16
15
|
|
|
17
16
|
export class Email extends QueryableModel {
|
|
18
17
|
static table = 'emails';
|
|
@@ -183,33 +182,35 @@ export class Email extends QueryableModel {
|
|
|
183
182
|
|
|
184
183
|
getFromAddress() {
|
|
185
184
|
if (!this.fromName) {
|
|
186
|
-
return
|
|
185
|
+
return {
|
|
186
|
+
email: this.fromAddress!,
|
|
187
|
+
};
|
|
187
188
|
}
|
|
188
189
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
}
|
|
193
|
-
return '"' + cleanedName + '" <' + this.fromAddress + '>';
|
|
190
|
+
return {
|
|
191
|
+
name: this.fromName,
|
|
192
|
+
email: this.fromAddress!,
|
|
193
|
+
};
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
-
getDefaultFromAddress(organization?: Organization | null):
|
|
196
|
+
getDefaultFromAddress(organization?: Organization | null): EmailInterfaceRecipient {
|
|
197
197
|
const i18n = new I18n($getLanguage(), $getCountry());
|
|
198
|
-
let address =
|
|
198
|
+
let address: EmailInterfaceRecipient = {
|
|
199
|
+
email: 'noreply@' + i18n.localizedDomains.defaultBroadcastEmail(),
|
|
200
|
+
};
|
|
199
201
|
|
|
200
202
|
if (organization) {
|
|
201
|
-
address = organization.getDefaultFrom(organization.i18n,
|
|
203
|
+
address = organization.getDefaultFrom(organization.i18n, 'broadcast');
|
|
202
204
|
}
|
|
203
205
|
|
|
204
206
|
if (!this.fromName) {
|
|
205
207
|
return address;
|
|
206
208
|
}
|
|
207
209
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
}
|
|
212
|
-
return '"' + cleanedName + '" <' + address + '>';
|
|
210
|
+
return {
|
|
211
|
+
name: this.fromName,
|
|
212
|
+
email: address.email,
|
|
213
|
+
};
|
|
213
214
|
}
|
|
214
215
|
|
|
215
216
|
async setFromTemplate(type: EmailTemplateType) {
|
|
@@ -274,7 +275,7 @@ export class Email extends QueryableModel {
|
|
|
274
275
|
upToDate.throwIfNotReadyToSend();
|
|
275
276
|
|
|
276
277
|
let from = upToDate.getDefaultFromAddress(organization);
|
|
277
|
-
let replyTo:
|
|
278
|
+
let replyTo: EmailInterfaceRecipient | null = upToDate.getFromAddress();
|
|
278
279
|
|
|
279
280
|
if (!from) {
|
|
280
281
|
throw new SimpleError({
|
|
@@ -679,7 +680,8 @@ export class Email extends QueryableModel {
|
|
|
679
680
|
|
|
680
681
|
await fillRecipientReplacements(virtualRecipient, {
|
|
681
682
|
organization: this.organizationId ? (await Organization.getByID(this.organizationId))! : null,
|
|
682
|
-
|
|
683
|
+
from: this.getFromAddress(),
|
|
684
|
+
replyTo: null,
|
|
683
685
|
});
|
|
684
686
|
|
|
685
687
|
recipientRow.replacements = virtualRecipient.replacements;
|
package/src/models/Member.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { column, Database, ManyToManyRelation, ManyToOneRelation, OneToManyRelation } from '@simonbackx/simple-database';
|
|
2
2
|
import { QueryableModel, SQL } from '@stamhoofd/sql';
|
|
3
|
-
import { MemberDetails, RegistrationWithMember as RegistrationWithMemberStruct, TinyMember } from '@stamhoofd/structures';
|
|
3
|
+
import { MemberDetails, NationalRegisterNumberOptOut, RegistrationWithMember as RegistrationWithMemberStruct, TinyMember } from '@stamhoofd/structures';
|
|
4
4
|
import { Formatter } from '@stamhoofd/utility';
|
|
5
5
|
import { v4 as uuidv4 } from 'uuid';
|
|
6
6
|
|
|
7
7
|
import { Group, MemberResponsibilityRecord, Payment, Registration, User } from './';
|
|
8
|
-
export type
|
|
8
|
+
export type MemberWithUsers = Member & {
|
|
9
9
|
users: User[];
|
|
10
|
+
};
|
|
11
|
+
export type MemberWithRegistrations = MemberWithUsers & {
|
|
10
12
|
registrations: (Registration & { group: Group })[];
|
|
11
13
|
};
|
|
12
14
|
|
|
@@ -353,29 +355,37 @@ export class Member extends QueryableModel {
|
|
|
353
355
|
}
|
|
354
356
|
|
|
355
357
|
async isSafeToMergeDuplicateWithoutSecurityCode() {
|
|
356
|
-
|
|
357
|
-
const responsibilities = await MemberResponsibilityRecord.where({ memberId: this.id }, { limit: 1 });
|
|
358
|
-
if (responsibilities.length > 0) {
|
|
358
|
+
if (this.details.recordAnswers.size > 0) {
|
|
359
359
|
return false;
|
|
360
360
|
}
|
|
361
361
|
|
|
362
|
-
if (this.details.
|
|
362
|
+
if (this.details.parents.length > 0) {
|
|
363
363
|
return false;
|
|
364
364
|
}
|
|
365
365
|
|
|
366
|
-
if (this.details.
|
|
366
|
+
if (this.details.emergencyContacts.length > 0) {
|
|
367
367
|
return false;
|
|
368
368
|
}
|
|
369
369
|
|
|
370
|
-
if (this.details.
|
|
370
|
+
if (this.details.uitpasNumber) {
|
|
371
371
|
return false;
|
|
372
372
|
}
|
|
373
373
|
|
|
374
|
-
if (this.details.
|
|
374
|
+
if (this.details.nationalRegisterNumber !== null && this.details.nationalRegisterNumber !== NationalRegisterNumberOptOut) {
|
|
375
375
|
return false;
|
|
376
376
|
}
|
|
377
377
|
|
|
378
|
-
if (this.details.
|
|
378
|
+
if (this.details.requiresFinancialSupport !== null) {
|
|
379
|
+
return false;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (this.details.address || this.details.phone || this.details.email || this.details.alternativeEmails.length > 0 || this.details.unverifiedAddresses.length > 0 || this.details.unverifiedPhones.length > 0 || this.details.unverifiedEmails.length > 0) {
|
|
383
|
+
return false;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// If responsibilities: not safe
|
|
387
|
+
const responsibilities = await MemberResponsibilityRecord.where({ memberId: this.id }, { limit: 1 });
|
|
388
|
+
if (responsibilities.length > 0) {
|
|
379
389
|
return false;
|
|
380
390
|
}
|
|
381
391
|
|