@stamhoofd/models 2.96.2 → 2.97.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/BalanceItemFactory.d.ts +1 -0
- package/dist/src/factories/BalanceItemFactory.d.ts.map +1 -1
- package/dist/src/factories/BalanceItemFactory.js +2 -0
- package/dist/src/factories/BalanceItemFactory.js.map +1 -1
- package/dist/src/factories/UserFactory.d.ts +1 -1
- package/dist/src/factories/UserFactory.d.ts.map +1 -1
- package/dist/src/factories/UserFactory.js +4 -1
- package/dist/src/factories/UserFactory.js.map +1 -1
- package/dist/src/helpers/EmailBuilder.d.ts +5 -5
- package/dist/src/helpers/EmailBuilder.d.ts.map +1 -1
- package/dist/src/helpers/EmailBuilder.js +41 -16
- package/dist/src/helpers/EmailBuilder.js.map +1 -1
- package/dist/src/migrations/1756821154-email-send-as-email.sql +3 -0
- package/dist/src/models/Email.d.ts +14 -0
- package/dist/src/models/Email.d.ts.map +1 -1
- package/dist/src/models/Email.js +269 -196
- package/dist/src/models/Email.js.map +1 -1
- package/dist/src/models/Email.test.js +332 -46
- package/dist/src/models/Email.test.js.map +1 -1
- package/package.json +2 -2
- package/src/factories/BalanceItemFactory.ts +2 -0
- package/src/factories/UserFactory.ts +4 -1
- package/src/helpers/EmailBuilder.ts +52 -26
- package/src/migrations/1756821154-email-send-as-email.sql +3 -0
- package/src/models/Email.test.ts +401 -47
- package/src/models/Email.ts +277 -201
package/src/models/Email.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { EmailRecipient } from './EmailRecipient';
|
|
|
13
13
|
import { EmailTemplate } from './EmailTemplate';
|
|
14
14
|
import { Organization } from './Organization';
|
|
15
15
|
import { User } from './User';
|
|
16
|
+
import { Platform } from './Platform';
|
|
16
17
|
|
|
17
18
|
function errorToSimpleErrors(e: unknown) {
|
|
18
19
|
if (isSimpleErrors(e)) {
|
|
@@ -26,7 +27,7 @@ function errorToSimpleErrors(e: unknown) {
|
|
|
26
27
|
new SimpleError({
|
|
27
28
|
code: 'unknown_error',
|
|
28
29
|
message: ((typeof e === 'object' && e !== null && 'message' in e && typeof e.message === 'string') ? e.message : 'Unknown error'),
|
|
29
|
-
human: $t(`
|
|
30
|
+
human: $t(`41db9fc8-77f4-49a7-a77b-40a4ae8c4d8f`),
|
|
30
31
|
}),
|
|
31
32
|
);
|
|
32
33
|
}
|
|
@@ -51,6 +52,23 @@ export class Email extends QueryableModel {
|
|
|
51
52
|
@column({ type: 'string', nullable: true })
|
|
52
53
|
userId: string | null = null;
|
|
53
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Send the message as an email.
|
|
57
|
+
* You can't edit this after the message has been published.
|
|
58
|
+
*
|
|
59
|
+
* If false, when sending the message, it will switch to 'Sent' directly without adjusting the email_recipients directly.
|
|
60
|
+
*/
|
|
61
|
+
@column({ type: 'boolean' })
|
|
62
|
+
sendAsEmail = true;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Show the message in the member portal
|
|
66
|
+
*
|
|
67
|
+
* Note: status should be 'Sent' for the message to be visible
|
|
68
|
+
*/
|
|
69
|
+
@column({ type: 'boolean' })
|
|
70
|
+
showInMemberPortal = true;
|
|
71
|
+
|
|
54
72
|
@column({ type: 'json', decoder: EmailRecipientFilter })
|
|
55
73
|
recipientFilter: EmailRecipientFilter = EmailRecipientFilter.create({});
|
|
56
74
|
|
|
@@ -241,7 +259,7 @@ export class Email extends QueryableModel {
|
|
|
241
259
|
throw new SimpleError({
|
|
242
260
|
code: 'invalid_recipients',
|
|
243
261
|
message: 'Failed to build recipients (count)',
|
|
244
|
-
human: $t(`
|
|
262
|
+
human: $t(`457ec920-2867-4d59-bbec-4466677e1b50`) + ' ' + this.recipientsErrors.getHuman(),
|
|
245
263
|
});
|
|
246
264
|
}
|
|
247
265
|
|
|
@@ -249,13 +267,50 @@ export class Email extends QueryableModel {
|
|
|
249
267
|
throw new SimpleError({
|
|
250
268
|
code: 'invalid_state',
|
|
251
269
|
message: 'Email is deleted',
|
|
252
|
-
human: $t(`
|
|
270
|
+
human: $t(`a0524f41-bdde-4fcc-9a9a-9350905377d8`),
|
|
253
271
|
});
|
|
254
272
|
}
|
|
255
273
|
|
|
256
274
|
this.validateAttachments();
|
|
257
275
|
}
|
|
258
276
|
|
|
277
|
+
throwIfNoUnsubscribeButton() {
|
|
278
|
+
if (this.sendAsEmail === false) {
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (this.emailType) {
|
|
283
|
+
// System email, no need for unsubscribe button
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const replacement = '{{unsubscribeUrl}}';
|
|
288
|
+
|
|
289
|
+
if (this.html) {
|
|
290
|
+
// Check email contains an unsubscribe button
|
|
291
|
+
if (!this.html.includes(replacement)) {
|
|
292
|
+
throw new SimpleError({
|
|
293
|
+
code: 'missing_unsubscribe_button',
|
|
294
|
+
message: 'Missing unsubscribe button',
|
|
295
|
+
human: $t(`dd55e04b-e5d9-4d9a-befc-443eef4175a8`),
|
|
296
|
+
field: 'html',
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (this.text) {
|
|
302
|
+
// Check email contains an unsubscribe button
|
|
303
|
+
if (!this.text.includes(replacement)) {
|
|
304
|
+
throw new SimpleError({
|
|
305
|
+
code: 'missing_unsubscribe_button',
|
|
306
|
+
message: 'Missing unsubscribe button',
|
|
307
|
+
human: $t(`dd55e04b-e5d9-4d9a-befc-443eef4175a8`),
|
|
308
|
+
field: 'text',
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
259
314
|
validateAttachments() {
|
|
260
315
|
// Validate attachments
|
|
261
316
|
const size = this.attachments.reduce((value: number, attachment) => {
|
|
@@ -526,6 +581,7 @@ export class Email extends QueryableModel {
|
|
|
526
581
|
|
|
527
582
|
async queueForSending(waitForSending = false) {
|
|
528
583
|
this.throwIfNotReadyToSend();
|
|
584
|
+
this.throwIfNoUnsubscribeButton();
|
|
529
585
|
await this.lock(async (upToDate) => {
|
|
530
586
|
if (upToDate.status === EmailStatus.Draft) {
|
|
531
587
|
upToDate.status = EmailStatus.Queued;
|
|
@@ -585,6 +641,7 @@ export class Email extends QueryableModel {
|
|
|
585
641
|
|
|
586
642
|
try {
|
|
587
643
|
upToDate.throwIfNotReadyToSend();
|
|
644
|
+
upToDate.throwIfNoUnsubscribeButton();
|
|
588
645
|
|
|
589
646
|
if (!from) {
|
|
590
647
|
throw new SimpleError({
|
|
@@ -629,223 +686,225 @@ export class Email extends QueryableModel {
|
|
|
629
686
|
}
|
|
630
687
|
|
|
631
688
|
// Create a buffer of all attachments
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
689
|
+
if (upToDate.sendAsEmail === true) {
|
|
690
|
+
for (const attachment of upToDate.attachments) {
|
|
691
|
+
if (!attachment.content && !attachment.file) {
|
|
692
|
+
console.warn('Attachment without content found, skipping', attachment);
|
|
693
|
+
continue;
|
|
694
|
+
}
|
|
637
695
|
|
|
638
|
-
|
|
696
|
+
let filename = $t('b1291584-d2ad-4ebd-88ed-cbda4f3755b4');
|
|
639
697
|
|
|
640
|
-
|
|
698
|
+
if (attachment.contentType === 'application/pdf') {
|
|
641
699
|
// tmp solution for pdf only
|
|
642
|
-
|
|
643
|
-
|
|
700
|
+
filename += '.pdf';
|
|
701
|
+
}
|
|
644
702
|
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
703
|
+
if (attachment.file?.name) {
|
|
704
|
+
filename = attachment.file.name.toLowerCase().replace(/[^a-z0-9.]+/g, '-').replace(/^-+/, '').replace(/-+$/, '');
|
|
705
|
+
}
|
|
648
706
|
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
707
|
+
// Correct file name if needed
|
|
708
|
+
if (attachment.filename) {
|
|
709
|
+
filename = attachment.filename.toLowerCase().replace(/[^a-z0-9.]+/g, '-').replace(/^-+/, '').replace(/-+$/, '');
|
|
710
|
+
}
|
|
653
711
|
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
});
|
|
661
|
-
}
|
|
662
|
-
else {
|
|
663
|
-
// Note: because we send lots of emails, we better download the file here so we can reuse it in every email instead of downloading it every time
|
|
664
|
-
const withSigned = await attachment.file!.withSignedUrl();
|
|
665
|
-
if (!withSigned || !withSigned.signedUrl) {
|
|
666
|
-
throw new SimpleError({
|
|
667
|
-
code: 'attachment_not_found',
|
|
668
|
-
message: 'Attachment not found',
|
|
669
|
-
human: $t(`ce6ddaf0-8347-42c5-b4b7-fbe860c7b7f2`),
|
|
712
|
+
if (attachment.content) {
|
|
713
|
+
attachments.push({
|
|
714
|
+
filename: filename,
|
|
715
|
+
content: attachment.content,
|
|
716
|
+
contentType: attachment.contentType ?? undefined,
|
|
717
|
+
encoding: 'base64',
|
|
670
718
|
});
|
|
671
719
|
}
|
|
720
|
+
else {
|
|
721
|
+
// Note: because we send lots of emails, we better download the file here so we can reuse it in every email instead of downloading it every time
|
|
722
|
+
const withSigned = await attachment.file!.withSignedUrl();
|
|
723
|
+
if (!withSigned || !withSigned.signedUrl) {
|
|
724
|
+
throw new SimpleError({
|
|
725
|
+
code: 'attachment_not_found',
|
|
726
|
+
message: 'Attachment not found',
|
|
727
|
+
human: $t(`ce6ddaf0-8347-42c5-b4b7-fbe860c7b7f2`),
|
|
728
|
+
});
|
|
729
|
+
}
|
|
672
730
|
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
731
|
+
const filePath = withSigned.signedUrl;
|
|
732
|
+
let fileBuffer: Buffer | null = null;
|
|
733
|
+
try {
|
|
734
|
+
const response = await fetch(filePath);
|
|
735
|
+
fileBuffer = Buffer.from(await response.arrayBuffer());
|
|
736
|
+
}
|
|
737
|
+
catch (e) {
|
|
738
|
+
throw new SimpleError({
|
|
739
|
+
code: 'attachment_not_found',
|
|
740
|
+
message: 'Attachment not found',
|
|
741
|
+
human: $t(`ce6ddaf0-8347-42c5-b4b7-fbe860c7b7f2`),
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
attachments.push({
|
|
746
|
+
filename: filename,
|
|
747
|
+
contentType: attachment.contentType ?? undefined,
|
|
748
|
+
content: fileBuffer,
|
|
684
749
|
});
|
|
685
750
|
}
|
|
686
|
-
|
|
687
|
-
attachments.push({
|
|
688
|
-
filename: filename,
|
|
689
|
-
contentType: attachment.contentType ?? undefined,
|
|
690
|
-
content: fileBuffer,
|
|
691
|
-
});
|
|
692
751
|
}
|
|
693
|
-
}
|
|
694
752
|
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
753
|
+
// Start actually sending in batches of recipients that are not yet sent
|
|
754
|
+
let idPointer = '';
|
|
755
|
+
const batchSize = 100;
|
|
756
|
+
let isSavingStatus = false;
|
|
757
|
+
let lastStatusSave = new Date();
|
|
700
758
|
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
759
|
+
async function saveStatus() {
|
|
760
|
+
if (!upToDate) {
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
if (isSavingStatus) {
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
if ((new Date().getTime() - lastStatusSave.getTime()) < 1000 * 5) {
|
|
709
767
|
// Save at most every 5 seconds
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
if (succeededCount < upToDate.succeededCount || softFailedCount < upToDate.softFailedCount || failedCount < upToDate.failedCount) {
|
|
713
771
|
// Do not update on retries
|
|
714
|
-
|
|
715
|
-
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
716
774
|
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
775
|
+
lastStatusSave = new Date();
|
|
776
|
+
isSavingStatus = true;
|
|
777
|
+
upToDate.succeededCount = succeededCount;
|
|
778
|
+
upToDate.softFailedCount = softFailedCount;
|
|
779
|
+
upToDate.failedCount = failedCount;
|
|
722
780
|
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
while (true) {
|
|
732
|
-
abort.throwIfAborted();
|
|
733
|
-
const data = await SQL.select()
|
|
734
|
-
.from('email_recipients')
|
|
735
|
-
.where('emailId', upToDate.id)
|
|
736
|
-
.where('id', SQLWhereSign.Greater, idPointer)
|
|
737
|
-
.orderBy(SQL.column('id'), 'ASC')
|
|
738
|
-
.limit(batchSize)
|
|
739
|
-
.fetch();
|
|
740
|
-
|
|
741
|
-
const recipients = EmailRecipient.fromRows(data, 'email_recipients');
|
|
742
|
-
|
|
743
|
-
if (recipients.length === 0) {
|
|
744
|
-
break;
|
|
781
|
+
try {
|
|
782
|
+
await upToDate.save();
|
|
783
|
+
}
|
|
784
|
+
finally {
|
|
785
|
+
isSavingStatus = false;
|
|
786
|
+
}
|
|
745
787
|
}
|
|
746
788
|
|
|
747
|
-
|
|
748
|
-
|
|
789
|
+
while (true) {
|
|
790
|
+
abort.throwIfAborted();
|
|
791
|
+
const data = await SQL.select()
|
|
792
|
+
.from('email_recipients')
|
|
793
|
+
.where('emailId', upToDate.id)
|
|
794
|
+
.where('id', SQLWhereSign.Greater, idPointer)
|
|
795
|
+
.orderBy(SQL.column('id'), 'ASC')
|
|
796
|
+
.limit(batchSize)
|
|
797
|
+
.fetch();
|
|
749
798
|
|
|
750
|
-
|
|
751
|
-
idPointer = recipient.id;
|
|
799
|
+
const recipients = EmailRecipient.fromRows(data, 'email_recipients');
|
|
752
800
|
|
|
753
|
-
if (
|
|
754
|
-
|
|
755
|
-
await saveStatus();
|
|
756
|
-
skipped++;
|
|
757
|
-
continue;
|
|
801
|
+
if (recipients.length === 0) {
|
|
802
|
+
break;
|
|
758
803
|
}
|
|
759
804
|
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
continue;
|
|
763
|
-
}
|
|
805
|
+
const sendingPromises: Promise<void>[] = [];
|
|
806
|
+
let skipped = 0;
|
|
764
807
|
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
continue;
|
|
768
|
-
}
|
|
808
|
+
for (const recipient of recipients) {
|
|
809
|
+
idPointer = recipient.id;
|
|
769
810
|
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
811
|
+
if (recipient.sentAt) {
|
|
812
|
+
succeededCount += 1;
|
|
813
|
+
await saveStatus();
|
|
814
|
+
skipped++;
|
|
815
|
+
continue;
|
|
816
|
+
}
|
|
774
817
|
|
|
775
|
-
|
|
818
|
+
if (!recipient.email) {
|
|
819
|
+
skipped++;
|
|
820
|
+
continue;
|
|
821
|
+
}
|
|
776
822
|
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
return;
|
|
823
|
+
if (recipient.duplicateOfRecipientId) {
|
|
824
|
+
skipped++;
|
|
825
|
+
continue;
|
|
781
826
|
}
|
|
782
|
-
resolved = true;
|
|
783
827
|
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
828
|
+
let promiseResolve: (value: void | PromiseLike<void>) => void;
|
|
829
|
+
const promise = new Promise<void>((resolve) => {
|
|
830
|
+
promiseResolve = resolve;
|
|
831
|
+
});
|
|
788
832
|
|
|
789
|
-
|
|
790
|
-
recipient.replacements = virtualRecipient.replacements;
|
|
833
|
+
const virtualRecipient = recipient.getRecipient();
|
|
791
834
|
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
835
|
+
let resolved = false;
|
|
836
|
+
const callback = async (error: Error | null) => {
|
|
837
|
+
if (resolved) {
|
|
838
|
+
return;
|
|
795
839
|
}
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
840
|
+
resolved = true;
|
|
841
|
+
|
|
842
|
+
try {
|
|
843
|
+
if (error === null) {
|
|
844
|
+
// Mark saved
|
|
845
|
+
recipient.sentAt = new Date();
|
|
846
|
+
|
|
847
|
+
// Update repacements that have been generated
|
|
848
|
+
recipient.replacements = virtualRecipient.replacements;
|
|
849
|
+
|
|
850
|
+
succeededCount += 1;
|
|
851
|
+
await recipient.save();
|
|
852
|
+
await saveStatus();
|
|
805
853
|
}
|
|
806
854
|
else {
|
|
807
|
-
|
|
855
|
+
recipient.failCount += 1;
|
|
856
|
+
recipient.failErrorMessage = error.message;
|
|
857
|
+
recipient.failError = errorToSimpleErrors(error);
|
|
858
|
+
recipient.firstFailedAt = recipient.firstFailedAt ?? new Date();
|
|
859
|
+
recipient.lastFailedAt = new Date();
|
|
860
|
+
|
|
861
|
+
if (isSoftEmailRecipientError(recipient.failError)) {
|
|
862
|
+
softFailedCount += 1;
|
|
863
|
+
}
|
|
864
|
+
else {
|
|
865
|
+
failedCount += 1;
|
|
866
|
+
}
|
|
867
|
+
await recipient.save();
|
|
868
|
+
await saveStatus();
|
|
808
869
|
}
|
|
809
|
-
await recipient.save();
|
|
810
|
-
await saveStatus();
|
|
811
870
|
}
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
}
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
}
|
|
871
|
+
catch (e) {
|
|
872
|
+
console.error(e);
|
|
873
|
+
}
|
|
874
|
+
promiseResolve();
|
|
875
|
+
};
|
|
876
|
+
|
|
877
|
+
// Do send the email
|
|
878
|
+
// Create e-mail builder
|
|
879
|
+
const builder = await getEmailBuilder(organization ?? null, {
|
|
880
|
+
recipients: [
|
|
881
|
+
virtualRecipient,
|
|
882
|
+
],
|
|
883
|
+
from,
|
|
884
|
+
replyTo,
|
|
885
|
+
subject: upToDate.subject!,
|
|
886
|
+
html: upToDate.html!,
|
|
887
|
+
type: upToDate.emailType ? 'transactional' : 'broadcast',
|
|
888
|
+
attachments,
|
|
889
|
+
callback(error: Error | null) {
|
|
890
|
+
callback(error).catch(console.error);
|
|
891
|
+
},
|
|
892
|
+
headers: {
|
|
893
|
+
'X-Email-Id': upToDate.id,
|
|
894
|
+
'X-Email-Recipient-Id': recipient.id,
|
|
895
|
+
},
|
|
896
|
+
});
|
|
897
|
+
abort.throwIfAborted(); // do not schedule if aborted
|
|
898
|
+
EmailClass.schedule(builder);
|
|
899
|
+
sendingPromises.push(promise);
|
|
900
|
+
}
|
|
843
901
|
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
902
|
+
if (sendingPromises.length > 0 || skipped > 0) {
|
|
903
|
+
await Promise.all(sendingPromises);
|
|
904
|
+
}
|
|
905
|
+
else {
|
|
906
|
+
break;
|
|
907
|
+
}
|
|
849
908
|
}
|
|
850
909
|
}
|
|
851
910
|
}
|
|
@@ -870,7 +929,7 @@ export class Email extends QueryableModel {
|
|
|
870
929
|
throw e;
|
|
871
930
|
}
|
|
872
931
|
|
|
873
|
-
if (upToDate.emailRecipientsCount === 0 && upToDate.userId === null) {
|
|
932
|
+
if (upToDate.sendAsEmail && upToDate.emailRecipientsCount === 0 && upToDate.userId === null) {
|
|
874
933
|
// We only delete automated emails (email type) if they have no recipients
|
|
875
934
|
console.log('No recipients found for email ', upToDate.id, ' deleting...');
|
|
876
935
|
await upToDate.delete();
|
|
@@ -880,13 +939,13 @@ export class Email extends QueryableModel {
|
|
|
880
939
|
console.log('Finished sending email', upToDate.id);
|
|
881
940
|
// Mark email as sent
|
|
882
941
|
|
|
883
|
-
if ((succeededCount + failedCount + softFailedCount) === 0) {
|
|
942
|
+
if (upToDate.sendAsEmail && !upToDate.showInMemberPortal && (succeededCount + failedCount + softFailedCount) === 0) {
|
|
884
943
|
upToDate.status = EmailStatus.Failed;
|
|
885
944
|
upToDate.emailErrors = new SimpleErrors(
|
|
886
945
|
new SimpleError({
|
|
887
946
|
code: 'no_recipients',
|
|
888
947
|
message: 'No recipients',
|
|
889
|
-
human: $t(`
|
|
948
|
+
human: $t(`9fe3de8e-090c-4949-97da-4810ce9e61c7`),
|
|
890
949
|
}),
|
|
891
950
|
);
|
|
892
951
|
}
|
|
@@ -996,6 +1055,15 @@ export class Email extends QueryableModel {
|
|
|
996
1055
|
}
|
|
997
1056
|
|
|
998
1057
|
abort.throwIfAborted();
|
|
1058
|
+
const organization = upToDate.organizationId ? (await Organization.getByID(upToDate.organizationId) ?? null) : null;
|
|
1059
|
+
if (upToDate.organizationId && !organization) {
|
|
1060
|
+
throw new SimpleError({
|
|
1061
|
+
code: 'organization_not_found',
|
|
1062
|
+
message: 'Organization not found',
|
|
1063
|
+
human: $t(`f3c6e2b1-2f3a-4e2f-8f7a-1e5f3d3c8e2a`),
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
1066
|
+
const platform = await Platform.getSharedPrivateStruct();
|
|
999
1067
|
|
|
1000
1068
|
console.log('Building recipients for email', id);
|
|
1001
1069
|
|
|
@@ -1045,7 +1113,26 @@ export class Email extends QueryableModel {
|
|
|
1045
1113
|
continue;
|
|
1046
1114
|
}
|
|
1047
1115
|
|
|
1048
|
-
|
|
1116
|
+
const recipient = new EmailRecipient();
|
|
1117
|
+
recipient.emailType = upToDate.emailType;
|
|
1118
|
+
recipient.objectId = item.objectId;
|
|
1119
|
+
recipient.emailId = upToDate.id;
|
|
1120
|
+
recipient.email = item.email;
|
|
1121
|
+
recipient.firstName = item.firstName;
|
|
1122
|
+
recipient.lastName = item.lastName;
|
|
1123
|
+
recipient.replacements = item.replacements;
|
|
1124
|
+
recipient.memberId = item.memberId ?? null;
|
|
1125
|
+
recipient.userId = item.userId ?? null;
|
|
1126
|
+
recipient.organizationId = upToDate.organizationId ?? null;
|
|
1127
|
+
|
|
1128
|
+
await fillRecipientReplacements(recipient, {
|
|
1129
|
+
platform,
|
|
1130
|
+
organization,
|
|
1131
|
+
from: upToDate.getFromAddress(),
|
|
1132
|
+
replyTo: null,
|
|
1133
|
+
forPreview: false,
|
|
1134
|
+
});
|
|
1135
|
+
recipient.replacements = removeUnusedReplacements(upToDate.html ?? '', recipient.replacements);
|
|
1049
1136
|
|
|
1050
1137
|
let duplicateOfRecipientId: string | null = null;
|
|
1051
1138
|
if (item.email && emailsSet.has(item.email)) {
|
|
@@ -1055,12 +1142,13 @@ export class Email extends QueryableModel {
|
|
|
1055
1142
|
const existing = await EmailRecipient.select()
|
|
1056
1143
|
.where('emailId', upToDate.id)
|
|
1057
1144
|
.where('email', item.email)
|
|
1145
|
+
.where('duplicateOfRecipientId', null)
|
|
1058
1146
|
.fetch();
|
|
1059
1147
|
|
|
1060
1148
|
for (const other of existing) {
|
|
1061
|
-
const merged = mergeReplacementsIfEqual(other.replacements,
|
|
1149
|
+
const merged = mergeReplacementsIfEqual(other.replacements, recipient.replacements);
|
|
1062
1150
|
if (merged !== false) {
|
|
1063
|
-
console.log('Found duplicate email recipient', item.email, other.id);
|
|
1151
|
+
console.log('Found mergeable duplicate email recipient', item.email, other.id);
|
|
1064
1152
|
duplicateOfRecipientId = other.id;
|
|
1065
1153
|
|
|
1066
1154
|
other.replacements = merged;
|
|
@@ -1068,7 +1156,7 @@ export class Email extends QueryableModel {
|
|
|
1068
1156
|
other.lastName = other.lastName || item.lastName;
|
|
1069
1157
|
await other.save();
|
|
1070
1158
|
|
|
1071
|
-
|
|
1159
|
+
recipient.replacements = merged;
|
|
1072
1160
|
|
|
1073
1161
|
break;
|
|
1074
1162
|
}
|
|
@@ -1077,18 +1165,6 @@ export class Email extends QueryableModel {
|
|
|
1077
1165
|
}
|
|
1078
1166
|
}
|
|
1079
1167
|
}
|
|
1080
|
-
|
|
1081
|
-
const recipient = new EmailRecipient();
|
|
1082
|
-
recipient.emailType = upToDate.emailType;
|
|
1083
|
-
recipient.objectId = item.objectId;
|
|
1084
|
-
recipient.emailId = upToDate.id;
|
|
1085
|
-
recipient.email = item.email;
|
|
1086
|
-
recipient.firstName = item.firstName;
|
|
1087
|
-
recipient.lastName = item.lastName;
|
|
1088
|
-
recipient.replacements = item.replacements;
|
|
1089
|
-
recipient.memberId = item.memberId ?? null;
|
|
1090
|
-
recipient.userId = item.userId ?? null;
|
|
1091
|
-
recipient.organizationId = upToDate.organizationId ?? null;
|
|
1092
1168
|
recipient.duplicateOfRecipientId = duplicateOfRecipientId;
|
|
1093
1169
|
|
|
1094
1170
|
await recipient.save();
|