@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/dist/src/models/Email.js
CHANGED
|
@@ -16,6 +16,7 @@ const EmailRecipient_1 = require("./EmailRecipient");
|
|
|
16
16
|
const EmailTemplate_1 = require("./EmailTemplate");
|
|
17
17
|
const Organization_1 = require("./Organization");
|
|
18
18
|
const User_1 = require("./User");
|
|
19
|
+
const Platform_1 = require("./Platform");
|
|
19
20
|
function errorToSimpleErrors(e) {
|
|
20
21
|
if ((0, simple_errors_1.isSimpleErrors)(e)) {
|
|
21
22
|
return e;
|
|
@@ -27,7 +28,7 @@ function errorToSimpleErrors(e) {
|
|
|
27
28
|
return new simple_errors_1.SimpleErrors(new simple_errors_1.SimpleError({
|
|
28
29
|
code: 'unknown_error',
|
|
29
30
|
message: ((typeof e === 'object' && e !== null && 'message' in e && typeof e.message === 'string') ? e.message : 'Unknown error'),
|
|
30
|
-
human: $t(`
|
|
31
|
+
human: $t(`41db9fc8-77f4-49a7-a77b-40a4ae8c4d8f`),
|
|
31
32
|
}));
|
|
32
33
|
}
|
|
33
34
|
}
|
|
@@ -37,6 +38,19 @@ class Email extends sql_1.QueryableModel {
|
|
|
37
38
|
organizationId = null;
|
|
38
39
|
senderId = null;
|
|
39
40
|
userId = null;
|
|
41
|
+
/**
|
|
42
|
+
* Send the message as an email.
|
|
43
|
+
* You can't edit this after the message has been published.
|
|
44
|
+
*
|
|
45
|
+
* If false, when sending the message, it will switch to 'Sent' directly without adjusting the email_recipients directly.
|
|
46
|
+
*/
|
|
47
|
+
sendAsEmail = true;
|
|
48
|
+
/**
|
|
49
|
+
* Show the message in the member portal
|
|
50
|
+
*
|
|
51
|
+
* Note: status should be 'Sent' for the message to be visible
|
|
52
|
+
*/
|
|
53
|
+
showInMemberPortal = true;
|
|
40
54
|
recipientFilter = structures_1.EmailRecipientFilter.create({});
|
|
41
55
|
/**
|
|
42
56
|
* Helper to prevent sending too many emails to the same person.
|
|
@@ -144,18 +158,50 @@ class Email extends sql_1.QueryableModel {
|
|
|
144
158
|
throw new simple_errors_1.SimpleError({
|
|
145
159
|
code: 'invalid_recipients',
|
|
146
160
|
message: 'Failed to build recipients (count)',
|
|
147
|
-
human: $t(`
|
|
161
|
+
human: $t(`457ec920-2867-4d59-bbec-4466677e1b50`) + ' ' + this.recipientsErrors.getHuman(),
|
|
148
162
|
});
|
|
149
163
|
}
|
|
150
164
|
if (this.deletedAt) {
|
|
151
165
|
throw new simple_errors_1.SimpleError({
|
|
152
166
|
code: 'invalid_state',
|
|
153
167
|
message: 'Email is deleted',
|
|
154
|
-
human: $t(`
|
|
168
|
+
human: $t(`a0524f41-bdde-4fcc-9a9a-9350905377d8`),
|
|
155
169
|
});
|
|
156
170
|
}
|
|
157
171
|
this.validateAttachments();
|
|
158
172
|
}
|
|
173
|
+
throwIfNoUnsubscribeButton() {
|
|
174
|
+
if (this.sendAsEmail === false) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (this.emailType) {
|
|
178
|
+
// System email, no need for unsubscribe button
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
const replacement = '{{unsubscribeUrl}}';
|
|
182
|
+
if (this.html) {
|
|
183
|
+
// Check email contains an unsubscribe button
|
|
184
|
+
if (!this.html.includes(replacement)) {
|
|
185
|
+
throw new simple_errors_1.SimpleError({
|
|
186
|
+
code: 'missing_unsubscribe_button',
|
|
187
|
+
message: 'Missing unsubscribe button',
|
|
188
|
+
human: $t(`dd55e04b-e5d9-4d9a-befc-443eef4175a8`),
|
|
189
|
+
field: 'html',
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if (this.text) {
|
|
194
|
+
// Check email contains an unsubscribe button
|
|
195
|
+
if (!this.text.includes(replacement)) {
|
|
196
|
+
throw new simple_errors_1.SimpleError({
|
|
197
|
+
code: 'missing_unsubscribe_button',
|
|
198
|
+
message: 'Missing unsubscribe button',
|
|
199
|
+
human: $t(`dd55e04b-e5d9-4d9a-befc-443eef4175a8`),
|
|
200
|
+
field: 'text',
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
159
205
|
validateAttachments() {
|
|
160
206
|
// Validate attachments
|
|
161
207
|
const size = this.attachments.reduce((value, attachment) => {
|
|
@@ -361,6 +407,7 @@ class Email extends sql_1.QueryableModel {
|
|
|
361
407
|
}
|
|
362
408
|
async queueForSending(waitForSending = false) {
|
|
363
409
|
this.throwIfNotReadyToSend();
|
|
410
|
+
this.throwIfNoUnsubscribeButton();
|
|
364
411
|
await this.lock(async (upToDate) => {
|
|
365
412
|
if (upToDate.status === structures_1.EmailStatus.Draft) {
|
|
366
413
|
upToDate.status = structures_1.EmailStatus.Queued;
|
|
@@ -417,6 +464,7 @@ class Email extends sql_1.QueryableModel {
|
|
|
417
464
|
let failedCount = 0;
|
|
418
465
|
try {
|
|
419
466
|
upToDate.throwIfNotReadyToSend();
|
|
467
|
+
upToDate.throwIfNoUnsubscribeButton();
|
|
420
468
|
if (!from) {
|
|
421
469
|
throw new simple_errors_1.SimpleError({
|
|
422
470
|
code: 'invalid_field',
|
|
@@ -454,195 +502,197 @@ class Email extends sql_1.QueryableModel {
|
|
|
454
502
|
});
|
|
455
503
|
}
|
|
456
504
|
// Create a buffer of all attachments
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
let filename = $t('b1291584-d2ad-4ebd-88ed-cbda4f3755b4');
|
|
463
|
-
if (attachment.contentType === 'application/pdf') {
|
|
464
|
-
// tmp solution for pdf only
|
|
465
|
-
filename += '.pdf';
|
|
466
|
-
}
|
|
467
|
-
if (attachment.file?.name) {
|
|
468
|
-
filename = attachment.file.name.toLowerCase().replace(/[^a-z0-9.]+/g, '-').replace(/^-+/, '').replace(/-+$/, '');
|
|
469
|
-
}
|
|
470
|
-
// Correct file name if needed
|
|
471
|
-
if (attachment.filename) {
|
|
472
|
-
filename = attachment.filename.toLowerCase().replace(/[^a-z0-9.]+/g, '-').replace(/^-+/, '').replace(/-+$/, '');
|
|
473
|
-
}
|
|
474
|
-
if (attachment.content) {
|
|
475
|
-
attachments.push({
|
|
476
|
-
filename: filename,
|
|
477
|
-
content: attachment.content,
|
|
478
|
-
contentType: attachment.contentType ?? undefined,
|
|
479
|
-
encoding: 'base64',
|
|
480
|
-
});
|
|
481
|
-
}
|
|
482
|
-
else {
|
|
483
|
-
// 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
|
|
484
|
-
const withSigned = await attachment.file.withSignedUrl();
|
|
485
|
-
if (!withSigned || !withSigned.signedUrl) {
|
|
486
|
-
throw new simple_errors_1.SimpleError({
|
|
487
|
-
code: 'attachment_not_found',
|
|
488
|
-
message: 'Attachment not found',
|
|
489
|
-
human: $t(`ce6ddaf0-8347-42c5-b4b7-fbe860c7b7f2`),
|
|
490
|
-
});
|
|
505
|
+
if (upToDate.sendAsEmail === true) {
|
|
506
|
+
for (const attachment of upToDate.attachments) {
|
|
507
|
+
if (!attachment.content && !attachment.file) {
|
|
508
|
+
console.warn('Attachment without content found, skipping', attachment);
|
|
509
|
+
continue;
|
|
491
510
|
}
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
511
|
+
let filename = $t('b1291584-d2ad-4ebd-88ed-cbda4f3755b4');
|
|
512
|
+
if (attachment.contentType === 'application/pdf') {
|
|
513
|
+
// tmp solution for pdf only
|
|
514
|
+
filename += '.pdf';
|
|
515
|
+
}
|
|
516
|
+
if (attachment.file?.name) {
|
|
517
|
+
filename = attachment.file.name.toLowerCase().replace(/[^a-z0-9.]+/g, '-').replace(/^-+/, '').replace(/-+$/, '');
|
|
497
518
|
}
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
519
|
+
// Correct file name if needed
|
|
520
|
+
if (attachment.filename) {
|
|
521
|
+
filename = attachment.filename.toLowerCase().replace(/[^a-z0-9.]+/g, '-').replace(/^-+/, '').replace(/-+$/, '');
|
|
522
|
+
}
|
|
523
|
+
if (attachment.content) {
|
|
524
|
+
attachments.push({
|
|
525
|
+
filename: filename,
|
|
526
|
+
content: attachment.content,
|
|
527
|
+
contentType: attachment.contentType ?? undefined,
|
|
528
|
+
encoding: 'base64',
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
else {
|
|
532
|
+
// 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
|
|
533
|
+
const withSigned = await attachment.file.withSignedUrl();
|
|
534
|
+
if (!withSigned || !withSigned.signedUrl) {
|
|
535
|
+
throw new simple_errors_1.SimpleError({
|
|
536
|
+
code: 'attachment_not_found',
|
|
537
|
+
message: 'Attachment not found',
|
|
538
|
+
human: $t(`ce6ddaf0-8347-42c5-b4b7-fbe860c7b7f2`),
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
const filePath = withSigned.signedUrl;
|
|
542
|
+
let fileBuffer = null;
|
|
543
|
+
try {
|
|
544
|
+
const response = await fetch(filePath);
|
|
545
|
+
fileBuffer = Buffer.from(await response.arrayBuffer());
|
|
546
|
+
}
|
|
547
|
+
catch (e) {
|
|
548
|
+
throw new simple_errors_1.SimpleError({
|
|
549
|
+
code: 'attachment_not_found',
|
|
550
|
+
message: 'Attachment not found',
|
|
551
|
+
human: $t(`ce6ddaf0-8347-42c5-b4b7-fbe860c7b7f2`),
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
attachments.push({
|
|
555
|
+
filename: filename,
|
|
556
|
+
contentType: attachment.contentType ?? undefined,
|
|
557
|
+
content: fileBuffer,
|
|
503
558
|
});
|
|
504
559
|
}
|
|
505
|
-
attachments.push({
|
|
506
|
-
filename: filename,
|
|
507
|
-
contentType: attachment.contentType ?? undefined,
|
|
508
|
-
content: fileBuffer,
|
|
509
|
-
});
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
// Start actually sending in batches of recipients that are not yet sent
|
|
513
|
-
let idPointer = '';
|
|
514
|
-
const batchSize = 100;
|
|
515
|
-
let isSavingStatus = false;
|
|
516
|
-
let lastStatusSave = new Date();
|
|
517
|
-
async function saveStatus() {
|
|
518
|
-
if (!upToDate) {
|
|
519
|
-
return;
|
|
520
|
-
}
|
|
521
|
-
if (isSavingStatus) {
|
|
522
|
-
return;
|
|
523
|
-
}
|
|
524
|
-
if ((new Date().getTime() - lastStatusSave.getTime()) < 1000 * 5) {
|
|
525
|
-
// Save at most every 5 seconds
|
|
526
|
-
return;
|
|
527
|
-
}
|
|
528
|
-
if (succeededCount < upToDate.succeededCount || softFailedCount < upToDate.softFailedCount || failedCount < upToDate.failedCount) {
|
|
529
|
-
// Do not update on retries
|
|
530
|
-
return;
|
|
531
|
-
}
|
|
532
|
-
lastStatusSave = new Date();
|
|
533
|
-
isSavingStatus = true;
|
|
534
|
-
upToDate.succeededCount = succeededCount;
|
|
535
|
-
upToDate.softFailedCount = softFailedCount;
|
|
536
|
-
upToDate.failedCount = failedCount;
|
|
537
|
-
try {
|
|
538
|
-
await upToDate.save();
|
|
539
|
-
}
|
|
540
|
-
finally {
|
|
541
|
-
isSavingStatus = false;
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
while (true) {
|
|
545
|
-
abort.throwIfAborted();
|
|
546
|
-
const data = await sql_1.SQL.select()
|
|
547
|
-
.from('email_recipients')
|
|
548
|
-
.where('emailId', upToDate.id)
|
|
549
|
-
.where('id', sql_1.SQLWhereSign.Greater, idPointer)
|
|
550
|
-
.orderBy(sql_1.SQL.column('id'), 'ASC')
|
|
551
|
-
.limit(batchSize)
|
|
552
|
-
.fetch();
|
|
553
|
-
const recipients = EmailRecipient_1.EmailRecipient.fromRows(data, 'email_recipients');
|
|
554
|
-
if (recipients.length === 0) {
|
|
555
|
-
break;
|
|
556
560
|
}
|
|
557
|
-
|
|
558
|
-
let
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
continue;
|
|
561
|
+
// Start actually sending in batches of recipients that are not yet sent
|
|
562
|
+
let idPointer = '';
|
|
563
|
+
const batchSize = 100;
|
|
564
|
+
let isSavingStatus = false;
|
|
565
|
+
let lastStatusSave = new Date();
|
|
566
|
+
async function saveStatus() {
|
|
567
|
+
if (!upToDate) {
|
|
568
|
+
return;
|
|
566
569
|
}
|
|
567
|
-
if (
|
|
568
|
-
|
|
569
|
-
continue;
|
|
570
|
+
if (isSavingStatus) {
|
|
571
|
+
return;
|
|
570
572
|
}
|
|
571
|
-
if (
|
|
572
|
-
|
|
573
|
-
|
|
573
|
+
if ((new Date().getTime() - lastStatusSave.getTime()) < 1000 * 5) {
|
|
574
|
+
// Save at most every 5 seconds
|
|
575
|
+
return;
|
|
574
576
|
}
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
577
|
+
if (succeededCount < upToDate.succeededCount || softFailedCount < upToDate.softFailedCount || failedCount < upToDate.failedCount) {
|
|
578
|
+
// Do not update on retries
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
lastStatusSave = new Date();
|
|
582
|
+
isSavingStatus = true;
|
|
583
|
+
upToDate.succeededCount = succeededCount;
|
|
584
|
+
upToDate.softFailedCount = softFailedCount;
|
|
585
|
+
upToDate.failedCount = failedCount;
|
|
586
|
+
try {
|
|
587
|
+
await upToDate.save();
|
|
588
|
+
}
|
|
589
|
+
finally {
|
|
590
|
+
isSavingStatus = false;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
while (true) {
|
|
594
|
+
abort.throwIfAborted();
|
|
595
|
+
const data = await sql_1.SQL.select()
|
|
596
|
+
.from('email_recipients')
|
|
597
|
+
.where('emailId', upToDate.id)
|
|
598
|
+
.where('id', sql_1.SQLWhereSign.Greater, idPointer)
|
|
599
|
+
.orderBy(sql_1.SQL.column('id'), 'ASC')
|
|
600
|
+
.limit(batchSize)
|
|
601
|
+
.fetch();
|
|
602
|
+
const recipients = EmailRecipient_1.EmailRecipient.fromRows(data, 'email_recipients');
|
|
603
|
+
if (recipients.length === 0) {
|
|
604
|
+
break;
|
|
605
|
+
}
|
|
606
|
+
const sendingPromises = [];
|
|
607
|
+
let skipped = 0;
|
|
608
|
+
for (const recipient of recipients) {
|
|
609
|
+
idPointer = recipient.id;
|
|
610
|
+
if (recipient.sentAt) {
|
|
611
|
+
succeededCount += 1;
|
|
612
|
+
await saveStatus();
|
|
613
|
+
skipped++;
|
|
614
|
+
continue;
|
|
584
615
|
}
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
616
|
+
if (!recipient.email) {
|
|
617
|
+
skipped++;
|
|
618
|
+
continue;
|
|
619
|
+
}
|
|
620
|
+
if (recipient.duplicateOfRecipientId) {
|
|
621
|
+
skipped++;
|
|
622
|
+
continue;
|
|
623
|
+
}
|
|
624
|
+
let promiseResolve;
|
|
625
|
+
const promise = new Promise((resolve) => {
|
|
626
|
+
promiseResolve = resolve;
|
|
627
|
+
});
|
|
628
|
+
const virtualRecipient = recipient.getRecipient();
|
|
629
|
+
let resolved = false;
|
|
630
|
+
const callback = async (error) => {
|
|
631
|
+
if (resolved) {
|
|
632
|
+
return;
|
|
595
633
|
}
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
634
|
+
resolved = true;
|
|
635
|
+
try {
|
|
636
|
+
if (error === null) {
|
|
637
|
+
// Mark saved
|
|
638
|
+
recipient.sentAt = new Date();
|
|
639
|
+
// Update repacements that have been generated
|
|
640
|
+
recipient.replacements = virtualRecipient.replacements;
|
|
641
|
+
succeededCount += 1;
|
|
642
|
+
await recipient.save();
|
|
643
|
+
await saveStatus();
|
|
604
644
|
}
|
|
605
645
|
else {
|
|
606
|
-
|
|
646
|
+
recipient.failCount += 1;
|
|
647
|
+
recipient.failErrorMessage = error.message;
|
|
648
|
+
recipient.failError = errorToSimpleErrors(error);
|
|
649
|
+
recipient.firstFailedAt = recipient.firstFailedAt ?? new Date();
|
|
650
|
+
recipient.lastFailedAt = new Date();
|
|
651
|
+
if ((0, structures_1.isSoftEmailRecipientError)(recipient.failError)) {
|
|
652
|
+
softFailedCount += 1;
|
|
653
|
+
}
|
|
654
|
+
else {
|
|
655
|
+
failedCount += 1;
|
|
656
|
+
}
|
|
657
|
+
await recipient.save();
|
|
658
|
+
await saveStatus();
|
|
607
659
|
}
|
|
608
|
-
await recipient.save();
|
|
609
|
-
await saveStatus();
|
|
610
660
|
}
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
661
|
+
catch (e) {
|
|
662
|
+
console.error(e);
|
|
663
|
+
}
|
|
664
|
+
promiseResolve();
|
|
665
|
+
};
|
|
666
|
+
// Do send the email
|
|
667
|
+
// Create e-mail builder
|
|
668
|
+
const builder = await (0, EmailBuilder_1.getEmailBuilder)(organization ?? null, {
|
|
669
|
+
recipients: [
|
|
670
|
+
virtualRecipient,
|
|
671
|
+
],
|
|
672
|
+
from,
|
|
673
|
+
replyTo,
|
|
674
|
+
subject: upToDate.subject,
|
|
675
|
+
html: upToDate.html,
|
|
676
|
+
type: upToDate.emailType ? 'transactional' : 'broadcast',
|
|
677
|
+
attachments,
|
|
678
|
+
callback(error) {
|
|
679
|
+
callback(error).catch(console.error);
|
|
680
|
+
},
|
|
681
|
+
headers: {
|
|
682
|
+
'X-Email-Id': upToDate.id,
|
|
683
|
+
'X-Email-Recipient-Id': recipient.id,
|
|
684
|
+
},
|
|
685
|
+
});
|
|
686
|
+
abort.throwIfAborted(); // do not schedule if aborted
|
|
687
|
+
email_1.Email.schedule(builder);
|
|
688
|
+
sendingPromises.push(promise);
|
|
689
|
+
}
|
|
690
|
+
if (sendingPromises.length > 0 || skipped > 0) {
|
|
691
|
+
await Promise.all(sendingPromises);
|
|
692
|
+
}
|
|
693
|
+
else {
|
|
694
|
+
break;
|
|
695
|
+
}
|
|
646
696
|
}
|
|
647
697
|
}
|
|
648
698
|
}
|
|
@@ -663,7 +713,7 @@ class Email extends sql_1.QueryableModel {
|
|
|
663
713
|
await upToDate.save();
|
|
664
714
|
throw e;
|
|
665
715
|
}
|
|
666
|
-
if (upToDate.emailRecipientsCount === 0 && upToDate.userId === null) {
|
|
716
|
+
if (upToDate.sendAsEmail && upToDate.emailRecipientsCount === 0 && upToDate.userId === null) {
|
|
667
717
|
// We only delete automated emails (email type) if they have no recipients
|
|
668
718
|
console.log('No recipients found for email ', upToDate.id, ' deleting...');
|
|
669
719
|
await upToDate.delete();
|
|
@@ -671,12 +721,12 @@ class Email extends sql_1.QueryableModel {
|
|
|
671
721
|
}
|
|
672
722
|
console.log('Finished sending email', upToDate.id);
|
|
673
723
|
// Mark email as sent
|
|
674
|
-
if ((succeededCount + failedCount + softFailedCount) === 0) {
|
|
724
|
+
if (upToDate.sendAsEmail && !upToDate.showInMemberPortal && (succeededCount + failedCount + softFailedCount) === 0) {
|
|
675
725
|
upToDate.status = structures_1.EmailStatus.Failed;
|
|
676
726
|
upToDate.emailErrors = new simple_errors_1.SimpleErrors(new simple_errors_1.SimpleError({
|
|
677
727
|
code: 'no_recipients',
|
|
678
728
|
message: 'No recipients',
|
|
679
|
-
human: $t(`
|
|
729
|
+
human: $t(`9fe3de8e-090c-4949-97da-4810ce9e61c7`),
|
|
680
730
|
}));
|
|
681
731
|
}
|
|
682
732
|
else {
|
|
@@ -765,6 +815,15 @@ class Email extends sql_1.QueryableModel {
|
|
|
765
815
|
return;
|
|
766
816
|
}
|
|
767
817
|
abort.throwIfAborted();
|
|
818
|
+
const organization = upToDate.organizationId ? (await Organization_1.Organization.getByID(upToDate.organizationId) ?? null) : null;
|
|
819
|
+
if (upToDate.organizationId && !organization) {
|
|
820
|
+
throw new simple_errors_1.SimpleError({
|
|
821
|
+
code: 'organization_not_found',
|
|
822
|
+
message: 'Organization not found',
|
|
823
|
+
human: $t(`f3c6e2b1-2f3a-4e2f-8f7a-1e5f3d3c8e2a`),
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
const platform = await Platform_1.Platform.getSharedPrivateStruct();
|
|
768
827
|
console.log('Building recipients for email', id);
|
|
769
828
|
// If it is already creating -> something went wrong (e.g. server restart) and we can safely try again
|
|
770
829
|
upToDate.recipientsStatus = structures_1.EmailRecipientsStatus.Creating;
|
|
@@ -799,7 +858,25 @@ class Email extends sql_1.QueryableModel {
|
|
|
799
858
|
if (!item.email && !item.memberId && !item.userId) {
|
|
800
859
|
continue;
|
|
801
860
|
}
|
|
802
|
-
|
|
861
|
+
const recipient = new EmailRecipient_1.EmailRecipient();
|
|
862
|
+
recipient.emailType = upToDate.emailType;
|
|
863
|
+
recipient.objectId = item.objectId;
|
|
864
|
+
recipient.emailId = upToDate.id;
|
|
865
|
+
recipient.email = item.email;
|
|
866
|
+
recipient.firstName = item.firstName;
|
|
867
|
+
recipient.lastName = item.lastName;
|
|
868
|
+
recipient.replacements = item.replacements;
|
|
869
|
+
recipient.memberId = item.memberId ?? null;
|
|
870
|
+
recipient.userId = item.userId ?? null;
|
|
871
|
+
recipient.organizationId = upToDate.organizationId ?? null;
|
|
872
|
+
await (0, EmailBuilder_1.fillRecipientReplacements)(recipient, {
|
|
873
|
+
platform,
|
|
874
|
+
organization,
|
|
875
|
+
from: upToDate.getFromAddress(),
|
|
876
|
+
replyTo: null,
|
|
877
|
+
forPreview: false,
|
|
878
|
+
});
|
|
879
|
+
recipient.replacements = (0, EmailBuilder_1.removeUnusedReplacements)(upToDate.html ?? '', recipient.replacements);
|
|
803
880
|
let duplicateOfRecipientId = null;
|
|
804
881
|
if (item.email && emailsSet.has(item.email)) {
|
|
805
882
|
console.log('Found duplicate email recipient', item.email);
|
|
@@ -807,17 +884,18 @@ class Email extends sql_1.QueryableModel {
|
|
|
807
884
|
const existing = await EmailRecipient_1.EmailRecipient.select()
|
|
808
885
|
.where('emailId', upToDate.id)
|
|
809
886
|
.where('email', item.email)
|
|
887
|
+
.where('duplicateOfRecipientId', null)
|
|
810
888
|
.fetch();
|
|
811
889
|
for (const other of existing) {
|
|
812
|
-
const merged = (0, EmailBuilder_1.mergeReplacementsIfEqual)(other.replacements,
|
|
890
|
+
const merged = (0, EmailBuilder_1.mergeReplacementsIfEqual)(other.replacements, recipient.replacements);
|
|
813
891
|
if (merged !== false) {
|
|
814
|
-
console.log('Found duplicate email recipient', item.email, other.id);
|
|
892
|
+
console.log('Found mergeable duplicate email recipient', item.email, other.id);
|
|
815
893
|
duplicateOfRecipientId = other.id;
|
|
816
894
|
other.replacements = merged;
|
|
817
895
|
other.firstName = other.firstName || item.firstName;
|
|
818
896
|
other.lastName = other.lastName || item.lastName;
|
|
819
897
|
await other.save();
|
|
820
|
-
|
|
898
|
+
recipient.replacements = merged;
|
|
821
899
|
break;
|
|
822
900
|
}
|
|
823
901
|
else {
|
|
@@ -825,17 +903,6 @@ class Email extends sql_1.QueryableModel {
|
|
|
825
903
|
}
|
|
826
904
|
}
|
|
827
905
|
}
|
|
828
|
-
const recipient = new EmailRecipient_1.EmailRecipient();
|
|
829
|
-
recipient.emailType = upToDate.emailType;
|
|
830
|
-
recipient.objectId = item.objectId;
|
|
831
|
-
recipient.emailId = upToDate.id;
|
|
832
|
-
recipient.email = item.email;
|
|
833
|
-
recipient.firstName = item.firstName;
|
|
834
|
-
recipient.lastName = item.lastName;
|
|
835
|
-
recipient.replacements = item.replacements;
|
|
836
|
-
recipient.memberId = item.memberId ?? null;
|
|
837
|
-
recipient.userId = item.userId ?? null;
|
|
838
|
-
recipient.organizationId = upToDate.organizationId ?? null;
|
|
839
906
|
recipient.duplicateOfRecipientId = duplicateOfRecipientId;
|
|
840
907
|
await recipient.save();
|
|
841
908
|
if (recipient.memberId) {
|
|
@@ -1069,6 +1136,12 @@ tslib_1.__decorate([
|
|
|
1069
1136
|
tslib_1.__decorate([
|
|
1070
1137
|
(0, simple_database_1.column)({ type: 'string', nullable: true })
|
|
1071
1138
|
], Email.prototype, "userId", void 0);
|
|
1139
|
+
tslib_1.__decorate([
|
|
1140
|
+
(0, simple_database_1.column)({ type: 'boolean' })
|
|
1141
|
+
], Email.prototype, "sendAsEmail", void 0);
|
|
1142
|
+
tslib_1.__decorate([
|
|
1143
|
+
(0, simple_database_1.column)({ type: 'boolean' })
|
|
1144
|
+
], Email.prototype, "showInMemberPortal", void 0);
|
|
1072
1145
|
tslib_1.__decorate([
|
|
1073
1146
|
(0, simple_database_1.column)({ type: 'json', decoder: structures_1.EmailRecipientFilter })
|
|
1074
1147
|
], Email.prototype, "recipientFilter", void 0);
|