@stamhoofd/models 2.97.3 → 2.99.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/migrations/1758034021-email-recipient-previous-error.sql +2 -0
- package/dist/src/models/Email.d.ts +3 -1
- package/dist/src/models/Email.d.ts.map +1 -1
- package/dist/src/models/Email.js +221 -160
- package/dist/src/models/Email.js.map +1 -1
- package/dist/src/models/Email.test.js +2 -2
- package/dist/src/models/Email.test.js.map +1 -1
- package/dist/src/models/EmailRecipient.d.ts +1 -0
- package/dist/src/models/EmailRecipient.d.ts.map +1 -1
- package/dist/src/models/EmailRecipient.js +4 -0
- package/dist/src/models/EmailRecipient.js.map +1 -1
- package/package.json +2 -2
- package/src/migrations/1758034021-email-recipient-previous-error.sql +2 -0
- package/src/models/Email.test.ts +2 -2
- package/src/models/Email.ts +244 -176
- package/src/models/EmailRecipient.ts +3 -0
package/src/models/Email.ts
CHANGED
|
@@ -15,6 +15,8 @@ import { Organization } from './Organization';
|
|
|
15
15
|
import { User } from './User';
|
|
16
16
|
import { Platform } from './Platform';
|
|
17
17
|
|
|
18
|
+
type Attachment = { filename: string; path?: string; href?: string; content?: string | Buffer; contentType?: string; encoding?: string };
|
|
19
|
+
|
|
18
20
|
function errorToSimpleErrors(e: unknown) {
|
|
19
21
|
if (isSimpleErrors(e)) {
|
|
20
22
|
return e;
|
|
@@ -601,40 +603,191 @@ export class Email extends QueryableModel {
|
|
|
601
603
|
return this;
|
|
602
604
|
}
|
|
603
605
|
|
|
604
|
-
async
|
|
606
|
+
private async loadAttachments(): Promise<Attachment[]> {
|
|
607
|
+
const attachments: Attachment[] = [];
|
|
608
|
+
for (const attachment of this.attachments) {
|
|
609
|
+
if (!attachment.content && !attachment.file) {
|
|
610
|
+
console.warn('Attachment without content found, skipping', attachment);
|
|
611
|
+
continue;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
let filename = $t('b1291584-d2ad-4ebd-88ed-cbda4f3755b4');
|
|
615
|
+
|
|
616
|
+
if (attachment.contentType === 'application/pdf') {
|
|
617
|
+
// tmp solution for pdf only
|
|
618
|
+
filename += '.pdf';
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
if (attachment.file?.name) {
|
|
622
|
+
filename = attachment.file.name.toLowerCase().replace(/[^a-z0-9.]+/g, '-').replace(/^-+/, '').replace(/-+$/, '');
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// Correct file name if needed
|
|
626
|
+
if (attachment.filename) {
|
|
627
|
+
filename = attachment.filename.toLowerCase().replace(/[^a-z0-9.]+/g, '-').replace(/^-+/, '').replace(/-+$/, '');
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
if (attachment.content) {
|
|
631
|
+
attachments.push({
|
|
632
|
+
filename: filename,
|
|
633
|
+
content: attachment.content,
|
|
634
|
+
contentType: attachment.contentType ?? undefined,
|
|
635
|
+
encoding: 'base64',
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
else {
|
|
639
|
+
// 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
|
|
640
|
+
const withSigned = await attachment.file!.withSignedUrl();
|
|
641
|
+
if (!withSigned || !withSigned.signedUrl) {
|
|
642
|
+
throw new SimpleError({
|
|
643
|
+
code: 'attachment_not_found',
|
|
644
|
+
message: 'Attachment not found',
|
|
645
|
+
human: $t(`ce6ddaf0-8347-42c5-b4b7-fbe860c7b7f2`),
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
const filePath = withSigned.signedUrl;
|
|
650
|
+
let fileBuffer: Buffer | null = null;
|
|
651
|
+
try {
|
|
652
|
+
const response = await fetch(filePath);
|
|
653
|
+
fileBuffer = Buffer.from(await response.arrayBuffer());
|
|
654
|
+
}
|
|
655
|
+
catch (e) {
|
|
656
|
+
throw new SimpleError({
|
|
657
|
+
code: 'attachment_not_found',
|
|
658
|
+
message: 'Attachment not found',
|
|
659
|
+
human: $t(`ce6ddaf0-8347-42c5-b4b7-fbe860c7b7f2`),
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
attachments.push({
|
|
664
|
+
filename: filename,
|
|
665
|
+
contentType: attachment.contentType ?? undefined,
|
|
666
|
+
content: fileBuffer,
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
return attachments;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
private async sendSingleRecipient(recipient: EmailRecipient, organization: Organization | null, data: { from: EmailInterfaceRecipient; replyTo: EmailInterfaceRecipient | null; attachments: Attachment[] }) {
|
|
674
|
+
let promiseResolve: (value: void | PromiseLike<void>) => void;
|
|
675
|
+
const promise = new Promise<void>((resolve) => {
|
|
676
|
+
promiseResolve = resolve;
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
const virtualRecipient = recipient.getRecipient();
|
|
680
|
+
|
|
681
|
+
let resolved = false;
|
|
682
|
+
const callback = async (error: Error | null) => {
|
|
683
|
+
if (resolved) {
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
resolved = true;
|
|
687
|
+
|
|
688
|
+
try {
|
|
689
|
+
if (error === null) {
|
|
690
|
+
// Mark saved
|
|
691
|
+
recipient.sentAt = new Date();
|
|
692
|
+
recipient.failErrorMessage = null;
|
|
693
|
+
|
|
694
|
+
if (recipient.failError) {
|
|
695
|
+
recipient.previousFailError = recipient.failError;
|
|
696
|
+
}
|
|
697
|
+
recipient.failError = null;
|
|
698
|
+
|
|
699
|
+
// Update repacements that have been generated
|
|
700
|
+
recipient.replacements = virtualRecipient.replacements;
|
|
701
|
+
await recipient.save();
|
|
702
|
+
}
|
|
703
|
+
else {
|
|
704
|
+
recipient.failCount += 1;
|
|
705
|
+
recipient.failErrorMessage = error.message;
|
|
706
|
+
if (recipient.failError) {
|
|
707
|
+
recipient.previousFailError = recipient.failError;
|
|
708
|
+
}
|
|
709
|
+
recipient.failError = errorToSimpleErrors(error);
|
|
710
|
+
recipient.firstFailedAt = recipient.firstFailedAt ?? new Date();
|
|
711
|
+
recipient.lastFailedAt = new Date();
|
|
712
|
+
await recipient.save();
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
catch (e) {
|
|
716
|
+
console.error(e);
|
|
717
|
+
}
|
|
718
|
+
promiseResolve();
|
|
719
|
+
};
|
|
720
|
+
|
|
721
|
+
// Do send the email
|
|
722
|
+
// Create e-mail builder
|
|
723
|
+
const builder = await getEmailBuilder(organization ?? null, {
|
|
724
|
+
recipients: [
|
|
725
|
+
virtualRecipient,
|
|
726
|
+
],
|
|
727
|
+
from: data.from,
|
|
728
|
+
replyTo: data.replyTo,
|
|
729
|
+
subject: this.subject!,
|
|
730
|
+
html: this.html!,
|
|
731
|
+
type: this.emailType ? 'transactional' : 'broadcast',
|
|
732
|
+
attachments: data.attachments,
|
|
733
|
+
callback(error: Error | null) {
|
|
734
|
+
callback(error).catch(console.error);
|
|
735
|
+
},
|
|
736
|
+
headers: {
|
|
737
|
+
'X-Email-Id': this.id,
|
|
738
|
+
'X-Email-Recipient-Id': recipient.id,
|
|
739
|
+
},
|
|
740
|
+
});
|
|
741
|
+
EmailClass.schedule(builder);
|
|
742
|
+
return await promise;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
async resumeSending(singleRecipientId: string | null = null): Promise<Email | null> {
|
|
605
746
|
const id = this.id;
|
|
606
747
|
return await QueueHandler.schedule('send-email', async ({ abort }) => {
|
|
607
748
|
return await this.lock(async function (upToDate: Email) {
|
|
608
749
|
if (upToDate.status === EmailStatus.Sent) {
|
|
609
750
|
// Already done
|
|
610
751
|
// In other cases -> queue has stopped and we can retry
|
|
611
|
-
|
|
752
|
+
if (!singleRecipientId) {
|
|
753
|
+
console.log('Email already sent, skipping...', upToDate.id);
|
|
754
|
+
return upToDate;
|
|
755
|
+
}
|
|
612
756
|
}
|
|
757
|
+
else {
|
|
758
|
+
if (singleRecipientId) {
|
|
759
|
+
// Not possible
|
|
760
|
+
throw new SimpleError({
|
|
761
|
+
code: 'invalid_state',
|
|
762
|
+
message: 'Cannot retry single recipient for email that is not yet sent',
|
|
763
|
+
});
|
|
764
|
+
}
|
|
613
765
|
|
|
614
|
-
|
|
766
|
+
if (upToDate.status === EmailStatus.Sending) {
|
|
615
767
|
// This is an automatic retry.
|
|
616
|
-
|
|
768
|
+
if (upToDate.emailType) {
|
|
617
769
|
// Not eligible for retry
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
770
|
+
upToDate.status = EmailStatus.Failed;
|
|
771
|
+
await upToDate.save();
|
|
772
|
+
return upToDate;
|
|
773
|
+
}
|
|
774
|
+
if (upToDate.createdAt < new Date(new Date().getTime() - 1000 * 60 * 60 * 24 * 2)) {
|
|
623
775
|
// Too long
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
776
|
+
console.error('Email has been sending for too long. Marking as failed...', upToDate.id);
|
|
777
|
+
upToDate.status = EmailStatus.Failed;
|
|
778
|
+
await upToDate.save();
|
|
779
|
+
return upToDate;
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
else if (upToDate.status !== EmailStatus.Queued) {
|
|
783
|
+
console.error('Email is not queued or sending, cannot send', upToDate.id, upToDate.status);
|
|
627
784
|
return upToDate;
|
|
628
785
|
}
|
|
629
786
|
}
|
|
630
|
-
|
|
631
|
-
console.error('Email is not queued or sending, cannot send', upToDate.id, upToDate.status);
|
|
632
|
-
return upToDate;
|
|
633
|
-
}
|
|
787
|
+
|
|
634
788
|
const organization = upToDate.organizationId ? await Organization.getByID(upToDate.organizationId) : null;
|
|
635
789
|
let from = upToDate.getDefaultFromAddress(organization);
|
|
636
790
|
let replyTo: EmailInterfaceRecipient | null = upToDate.getFromAddress();
|
|
637
|
-
const attachments: { filename: string; path?: string; href?: string; content?: string | Buffer; contentType?: string; encoding?: string }[] = [];
|
|
638
791
|
let succeededCount = 0;
|
|
639
792
|
let softFailedCount = 0;
|
|
640
793
|
let failedCount = 0;
|
|
@@ -658,7 +811,10 @@ export class Email extends QueryableModel {
|
|
|
658
811
|
}
|
|
659
812
|
|
|
660
813
|
abort.throwIfAborted();
|
|
661
|
-
|
|
814
|
+
|
|
815
|
+
if (!singleRecipientId) {
|
|
816
|
+
upToDate.status = EmailStatus.Sending;
|
|
817
|
+
}
|
|
662
818
|
upToDate.sentAt = upToDate.sentAt ?? new Date();
|
|
663
819
|
await upToDate.save();
|
|
664
820
|
|
|
@@ -687,68 +843,7 @@ export class Email extends QueryableModel {
|
|
|
687
843
|
|
|
688
844
|
// Create a buffer of all attachments
|
|
689
845
|
if (upToDate.sendAsEmail === true) {
|
|
690
|
-
|
|
691
|
-
if (!attachment.content && !attachment.file) {
|
|
692
|
-
console.warn('Attachment without content found, skipping', attachment);
|
|
693
|
-
continue;
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
let filename = $t('b1291584-d2ad-4ebd-88ed-cbda4f3755b4');
|
|
697
|
-
|
|
698
|
-
if (attachment.contentType === 'application/pdf') {
|
|
699
|
-
// tmp solution for pdf only
|
|
700
|
-
filename += '.pdf';
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
if (attachment.file?.name) {
|
|
704
|
-
filename = attachment.file.name.toLowerCase().replace(/[^a-z0-9.]+/g, '-').replace(/^-+/, '').replace(/-+$/, '');
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
// Correct file name if needed
|
|
708
|
-
if (attachment.filename) {
|
|
709
|
-
filename = attachment.filename.toLowerCase().replace(/[^a-z0-9.]+/g, '-').replace(/^-+/, '').replace(/-+$/, '');
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
if (attachment.content) {
|
|
713
|
-
attachments.push({
|
|
714
|
-
filename: filename,
|
|
715
|
-
content: attachment.content,
|
|
716
|
-
contentType: attachment.contentType ?? undefined,
|
|
717
|
-
encoding: 'base64',
|
|
718
|
-
});
|
|
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
|
-
}
|
|
730
|
-
|
|
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,
|
|
749
|
-
});
|
|
750
|
-
}
|
|
751
|
-
}
|
|
846
|
+
const attachments = await upToDate.loadAttachments();
|
|
752
847
|
|
|
753
848
|
// Start actually sending in batches of recipients that are not yet sent
|
|
754
849
|
let idPointer = '';
|
|
@@ -757,6 +852,11 @@ export class Email extends QueryableModel {
|
|
|
757
852
|
let lastStatusSave = new Date();
|
|
758
853
|
|
|
759
854
|
async function saveStatus() {
|
|
855
|
+
if (singleRecipientId) {
|
|
856
|
+
// Don't save during looping
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
|
|
760
860
|
if (!upToDate) {
|
|
761
861
|
return;
|
|
762
862
|
}
|
|
@@ -825,78 +925,42 @@ export class Email extends QueryableModel {
|
|
|
825
925
|
continue;
|
|
826
926
|
}
|
|
827
927
|
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
const virtualRecipient = recipient.getRecipient();
|
|
834
|
-
|
|
835
|
-
let resolved = false;
|
|
836
|
-
const callback = async (error: Error | null) => {
|
|
837
|
-
if (resolved) {
|
|
838
|
-
return;
|
|
839
|
-
}
|
|
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();
|
|
928
|
+
if (singleRecipientId) {
|
|
929
|
+
if (recipient.id !== singleRecipientId) {
|
|
930
|
+
// Failed or soft-failed
|
|
931
|
+
if (recipient.failError && isSoftEmailRecipientError(recipient.failError)) {
|
|
932
|
+
softFailedCount += 1;
|
|
853
933
|
}
|
|
854
934
|
else {
|
|
855
|
-
|
|
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();
|
|
935
|
+
failedCount += 1;
|
|
869
936
|
}
|
|
937
|
+
skipped++;
|
|
938
|
+
await saveStatus();
|
|
939
|
+
continue;
|
|
870
940
|
}
|
|
871
|
-
|
|
872
|
-
|
|
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
|
-
],
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
const promise = upToDate.sendSingleRecipient(recipient, organization ?? null, {
|
|
883
944
|
from,
|
|
884
945
|
replyTo,
|
|
885
|
-
subject: upToDate.subject!,
|
|
886
|
-
html: upToDate.html!,
|
|
887
|
-
type: upToDate.emailType ? 'transactional' : 'broadcast',
|
|
888
946
|
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
947
|
});
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
948
|
+
sendingPromises.push(promise.then(async () => {
|
|
949
|
+
if (recipient.sentAt) {
|
|
950
|
+
succeededCount += 1;
|
|
951
|
+
await saveStatus();
|
|
952
|
+
}
|
|
953
|
+
else {
|
|
954
|
+
// Failed or soft-failed
|
|
955
|
+
if (recipient.failError && isSoftEmailRecipientError(recipient.failError)) {
|
|
956
|
+
softFailedCount += 1;
|
|
957
|
+
}
|
|
958
|
+
else {
|
|
959
|
+
failedCount += 1;
|
|
960
|
+
}
|
|
961
|
+
await saveStatus();
|
|
962
|
+
}
|
|
963
|
+
}));
|
|
900
964
|
}
|
|
901
965
|
|
|
902
966
|
if (sendingPromises.length > 0 || skipped > 0) {
|
|
@@ -913,45 +977,49 @@ export class Email extends QueryableModel {
|
|
|
913
977
|
throw e;
|
|
914
978
|
}
|
|
915
979
|
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
980
|
+
if (!singleRecipientId) {
|
|
981
|
+
upToDate.succeededCount = succeededCount;
|
|
982
|
+
upToDate.softFailedCount = softFailedCount;
|
|
983
|
+
upToDate.failedCount = failedCount;
|
|
984
|
+
|
|
985
|
+
if (isAbortedError(e) || ((isSimpleError(e) || isSimpleErrors(e)) && e.hasCode('SHUTDOWN'))) {
|
|
986
|
+
// Keep sending status: we'll resume after the reboot
|
|
987
|
+
await upToDate.save();
|
|
988
|
+
throw e;
|
|
989
|
+
}
|
|
919
990
|
|
|
920
|
-
|
|
921
|
-
|
|
991
|
+
upToDate.emailErrors = errorToSimpleErrors(e);
|
|
992
|
+
upToDate.status = EmailStatus.Failed;
|
|
922
993
|
await upToDate.save();
|
|
923
|
-
throw e;
|
|
924
994
|
}
|
|
925
|
-
|
|
926
|
-
upToDate.emailErrors = errorToSimpleErrors(e);
|
|
927
|
-
upToDate.status = EmailStatus.Failed;
|
|
928
|
-
await upToDate.save();
|
|
929
995
|
throw e;
|
|
930
996
|
}
|
|
931
997
|
|
|
932
|
-
if (
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
998
|
+
if (!singleRecipientId) {
|
|
999
|
+
if (upToDate.sendAsEmail && upToDate.emailRecipientsCount === 0 && upToDate.userId === null) {
|
|
1000
|
+
// We only delete automated emails (email type) if they have no recipients
|
|
1001
|
+
console.log('No recipients found for email ', upToDate.id, ' deleting...');
|
|
1002
|
+
await upToDate.delete();
|
|
1003
|
+
return null;
|
|
1004
|
+
}
|
|
938
1005
|
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
1006
|
+
console.log('Finished sending email', upToDate.id);
|
|
1007
|
+
// Mark email as sent
|
|
1008
|
+
|
|
1009
|
+
if (upToDate.sendAsEmail && !upToDate.showInMemberPortal && (succeededCount + failedCount + softFailedCount) === 0) {
|
|
1010
|
+
upToDate.status = EmailStatus.Failed;
|
|
1011
|
+
upToDate.emailErrors = new SimpleErrors(
|
|
1012
|
+
new SimpleError({
|
|
1013
|
+
code: 'no_recipients',
|
|
1014
|
+
message: 'No recipients',
|
|
1015
|
+
human: $t(`9fe3de8e-090c-4949-97da-4810ce9e61c7`),
|
|
1016
|
+
}),
|
|
1017
|
+
);
|
|
1018
|
+
}
|
|
1019
|
+
else {
|
|
1020
|
+
upToDate.status = EmailStatus.Sent;
|
|
1021
|
+
upToDate.emailErrors = null;
|
|
1022
|
+
}
|
|
955
1023
|
}
|
|
956
1024
|
|
|
957
1025
|
upToDate.succeededCount = succeededCount;
|
|
@@ -57,6 +57,9 @@ export class EmailRecipient extends QueryableModel {
|
|
|
57
57
|
@column({ type: 'json', nullable: true, decoder: SimpleErrors })
|
|
58
58
|
failError: SimpleErrors | null = null;
|
|
59
59
|
|
|
60
|
+
@column({ type: 'json', nullable: true, decoder: SimpleErrors })
|
|
61
|
+
previousFailError: SimpleErrors | null = null;
|
|
62
|
+
|
|
60
63
|
@column({ type: 'string', nullable: true })
|
|
61
64
|
organizationId: string | null = null;
|
|
62
65
|
|