@stamhoofd/models 2.78.4 → 2.79.1

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 (77) hide show
  1. package/dist/src/factories/GroupFactory.d.ts +4 -0
  2. package/dist/src/factories/GroupFactory.d.ts.map +1 -1
  3. package/dist/src/factories/GroupFactory.js +11 -1
  4. package/dist/src/factories/GroupFactory.js.map +1 -1
  5. package/dist/src/factories/MemberResponsibilityRecordFactory.d.ts +14 -0
  6. package/dist/src/factories/MemberResponsibilityRecordFactory.d.ts.map +1 -0
  7. package/dist/src/factories/MemberResponsibilityRecordFactory.js +34 -0
  8. package/dist/src/factories/MemberResponsibilityRecordFactory.js.map +1 -0
  9. package/dist/src/factories/OrganizationRegistrationPeriodFactory.d.ts +13 -0
  10. package/dist/src/factories/OrganizationRegistrationPeriodFactory.d.ts.map +1 -0
  11. package/dist/src/factories/OrganizationRegistrationPeriodFactory.js +20 -0
  12. package/dist/src/factories/OrganizationRegistrationPeriodFactory.js.map +1 -0
  13. package/dist/src/factories/OrganizationTagFactory.js +1 -1
  14. package/dist/src/factories/OrganizationTagFactory.js.map +1 -1
  15. package/dist/src/factories/PlatformResponsibilityFactory.d.ts +11 -0
  16. package/dist/src/factories/PlatformResponsibilityFactory.d.ts.map +1 -0
  17. package/dist/src/factories/PlatformResponsibilityFactory.js +25 -0
  18. package/dist/src/factories/PlatformResponsibilityFactory.js.map +1 -0
  19. package/dist/src/factories/RegistrationPeriodFactory.d.ts +1 -0
  20. package/dist/src/factories/RegistrationPeriodFactory.d.ts.map +1 -1
  21. package/dist/src/factories/RegistrationPeriodFactory.js +4 -0
  22. package/dist/src/factories/RegistrationPeriodFactory.js.map +1 -1
  23. package/dist/src/factories/index.d.ts +3 -0
  24. package/dist/src/factories/index.d.ts.map +1 -1
  25. package/dist/src/factories/index.js +3 -0
  26. package/dist/src/factories/index.js.map +1 -1
  27. package/dist/src/helpers/EmailBuilder.d.ts +8 -10
  28. package/dist/src/helpers/EmailBuilder.d.ts.map +1 -1
  29. package/dist/src/helpers/EmailBuilder.js +44 -42
  30. package/dist/src/helpers/EmailBuilder.js.map +1 -1
  31. package/dist/src/models/Email.d.ts +9 -2
  32. package/dist/src/models/Email.d.ts.map +1 -1
  33. package/dist/src/models/Email.js +18 -16
  34. package/dist/src/models/Email.js.map +1 -1
  35. package/dist/src/models/Email.test.js +183 -2
  36. package/dist/src/models/Email.test.js.map +1 -1
  37. package/dist/src/models/Member.d.ts +3 -1
  38. package/dist/src/models/Member.d.ts.map +1 -1
  39. package/dist/src/models/Member.js.map +1 -1
  40. package/dist/src/models/Order.d.ts +0 -2
  41. package/dist/src/models/Order.d.ts.map +1 -1
  42. package/dist/src/models/Order.js +0 -48
  43. package/dist/src/models/Order.js.map +1 -1
  44. package/dist/src/models/Organization.d.ts +1 -16
  45. package/dist/src/models/Organization.d.ts.map +1 -1
  46. package/dist/src/models/Organization.js +5 -86
  47. package/dist/src/models/Organization.js.map +1 -1
  48. package/dist/src/models/Platform.d.ts +11 -3
  49. package/dist/src/models/Platform.d.ts.map +1 -1
  50. package/dist/src/models/Platform.js +76 -24
  51. package/dist/src/models/Platform.js.map +1 -1
  52. package/dist/src/models/Platform.test.d.ts +2 -0
  53. package/dist/src/models/Platform.test.d.ts.map +1 -0
  54. package/dist/src/models/Platform.test.js +90 -0
  55. package/dist/src/models/Platform.test.js.map +1 -0
  56. package/dist/src/models/index.d.ts +1 -1
  57. package/dist/src/models/index.d.ts.map +1 -1
  58. package/dist/src/models/index.js +2 -3
  59. package/dist/src/models/index.js.map +1 -1
  60. package/dist/tsconfig.tsbuildinfo +1 -1
  61. package/package.json +4 -4
  62. package/src/factories/GroupFactory.ts +16 -3
  63. package/src/factories/MemberResponsibilityRecordFactory.ts +35 -0
  64. package/src/factories/OrganizationRegistrationPeriodFactory.ts +22 -0
  65. package/src/factories/OrganizationTagFactory.ts +1 -1
  66. package/src/factories/PlatformResponsibilityFactory.ts +25 -0
  67. package/src/factories/RegistrationPeriodFactory.ts +4 -0
  68. package/src/factories/index.ts +3 -0
  69. package/src/helpers/EmailBuilder.ts +54 -50
  70. package/src/models/Email.test.ts +217 -5
  71. package/src/models/Email.ts +22 -20
  72. package/src/models/Member.ts +3 -1
  73. package/src/models/Order.ts +0 -55
  74. package/src/models/Organization.ts +6 -101
  75. package/src/models/Platform.test.ts +107 -0
  76. package/src/models/Platform.ts +86 -25
  77. 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, false, options.type ?? 'broadcast');
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: string | undefined = undefined;
110
+ let replyTo: EmailInterfaceRecipient | undefined = undefined;
111
111
 
112
112
  if (sender) {
113
- replyTo = sender.email;
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 = sender.email;
120
+ from = {
121
+ email: sender.email,
122
+ name: sender.name,
123
+ };
118
124
  replyTo = undefined;
119
125
  }
120
126
 
121
- // Include name in form field
122
- if (sender.name) {
123
- from = '"' + sender.name.replaceAll('"', '\\"') + '" <' + 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 (sender.name) {
131
- replyTo = '"' + sender.name.replaceAll('"', '\\"') + '" <' + 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 = 'hallo@' + domain;
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: string | undefined = undefined;
157
+ let replyTo: EmailInterfaceRecipient | undefined = undefined;
159
158
 
160
159
  if (sender) {
161
- replyTo = sender.email;
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 = sender.email;
168
+ from = {
169
+ email: sender.email,
170
+ name: sender.name,
171
+ };
167
172
  replyTo = undefined;
168
173
  }
169
174
 
170
- // Include name in form field
171
- if (sender.name) {
172
- from = '"' + sender.name.replaceAll('"', '\\"') + '" <' + 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 (sender.name) {
180
- replyTo = '"' + sender.name.replaceAll('"', '\\"') + '" <' + 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
- export async function getEmailBuilderForTemplate(organization: Organization | null, options: Omit<EmailBuilderOptions, 'subject' | 'html'> & { template: Omit<EmailTemplateOptions, 'organizationId'> }) {
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: string;
230
- replyTo?: string | null;
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?: string;
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
- fromAddress,
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
- fromAddress: string | null;
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, fromAddress } = options;
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 (fromAddress) {
453
+ if (from || replyTo) {
458
454
  recipient.replacements.push(Replacement.create({
459
455
  token: 'fromAddress',
460
- value: fromAddress,
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
  }
@@ -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 ?? 'Stamhoofd Test Suite';
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.getShared();
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
  });
@@ -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, Recipient, Replacement, SortItemDirection, StamhoofdFilter } from '@stamhoofd/structures';
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 this.fromAddress!;
185
+ return {
186
+ email: this.fromAddress!,
187
+ };
187
188
  }
188
189
 
189
- const cleanedName = Formatter.emailSenderName(this.fromName);
190
- if (cleanedName.length < 2) {
191
- return this.fromAddress!;
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): string {
196
+ getDefaultFromAddress(organization?: Organization | null): EmailInterfaceRecipient {
197
197
  const i18n = new I18n($getLanguage(), $getCountry());
198
- let address = 'noreply@' + i18n.localizedDomains.defaultBroadcastEmail();
198
+ let address: EmailInterfaceRecipient = {
199
+ email: 'noreply@' + i18n.localizedDomains.defaultBroadcastEmail(),
200
+ };
199
201
 
200
202
  if (organization) {
201
- address = organization.getDefaultFrom(organization.i18n, false, 'broadcast');
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
- const cleanedName = Formatter.emailSenderName(this.fromName);
209
- if (cleanedName.length < 2) {
210
- return address;
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: string | null = upToDate.getFromAddress();
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
- fromAddress: this.fromAddress,
683
+ from: this.getFromAddress(),
684
+ replyTo: null,
683
685
  });
684
686
 
685
687
  recipientRow.replacements = virtualRecipient.replacements;
@@ -5,8 +5,10 @@ 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 MemberWithRegistrations = Member & {
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