@stamhoofd/models 2.97.3 → 2.98.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.
@@ -0,0 +1,2 @@
1
+ ALTER TABLE `email_recipients`
2
+ ADD COLUMN `previousFailError` json NULL AFTER `failError`;
@@ -121,7 +121,9 @@ export declare class Email extends QueryableModel {
121
121
  static checkNeedsNotificationCountUpdate(emailId: string, didUpdate?: boolean): Promise<void>;
122
122
  static updateNotificationsCounts(emailId: string): Promise<void>;
123
123
  queueForSending(waitForSending?: boolean): Promise<Email | null>;
124
- resumeSending(): Promise<Email | null>;
124
+ private loadAttachments;
125
+ private sendSingleRecipient;
126
+ resumeSending(singleRecipientId?: string | null): Promise<Email | null>;
125
127
  updateCount(): void;
126
128
  buildRecipients(): Promise<void>;
127
129
  buildExampleRecipient(): Promise<void>;
@@ -1 +1 @@
1
- {"version":3,"file":"Email.d.ts","sourceRoot":"","sources":["../../../src/models/Email.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"Email.d.ts","sourceRoot":"","sources":["../../../src/models/Email.ts"],"names":[],"mappings":"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"}
@@ -426,39 +426,174 @@ class Email extends sql_1.QueryableModel {
426
426
  }
427
427
  return this;
428
428
  }
429
- async resumeSending() {
429
+ async loadAttachments() {
430
+ const attachments = [];
431
+ for (const attachment of this.attachments) {
432
+ if (!attachment.content && !attachment.file) {
433
+ console.warn('Attachment without content found, skipping', attachment);
434
+ continue;
435
+ }
436
+ let filename = $t('b1291584-d2ad-4ebd-88ed-cbda4f3755b4');
437
+ if (attachment.contentType === 'application/pdf') {
438
+ // tmp solution for pdf only
439
+ filename += '.pdf';
440
+ }
441
+ if (attachment.file?.name) {
442
+ filename = attachment.file.name.toLowerCase().replace(/[^a-z0-9.]+/g, '-').replace(/^-+/, '').replace(/-+$/, '');
443
+ }
444
+ // Correct file name if needed
445
+ if (attachment.filename) {
446
+ filename = attachment.filename.toLowerCase().replace(/[^a-z0-9.]+/g, '-').replace(/^-+/, '').replace(/-+$/, '');
447
+ }
448
+ if (attachment.content) {
449
+ attachments.push({
450
+ filename: filename,
451
+ content: attachment.content,
452
+ contentType: attachment.contentType ?? undefined,
453
+ encoding: 'base64',
454
+ });
455
+ }
456
+ else {
457
+ // 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
458
+ const withSigned = await attachment.file.withSignedUrl();
459
+ if (!withSigned || !withSigned.signedUrl) {
460
+ throw new simple_errors_1.SimpleError({
461
+ code: 'attachment_not_found',
462
+ message: 'Attachment not found',
463
+ human: $t(`ce6ddaf0-8347-42c5-b4b7-fbe860c7b7f2`),
464
+ });
465
+ }
466
+ const filePath = withSigned.signedUrl;
467
+ let fileBuffer = null;
468
+ try {
469
+ const response = await fetch(filePath);
470
+ fileBuffer = Buffer.from(await response.arrayBuffer());
471
+ }
472
+ catch (e) {
473
+ throw new simple_errors_1.SimpleError({
474
+ code: 'attachment_not_found',
475
+ message: 'Attachment not found',
476
+ human: $t(`ce6ddaf0-8347-42c5-b4b7-fbe860c7b7f2`),
477
+ });
478
+ }
479
+ attachments.push({
480
+ filename: filename,
481
+ contentType: attachment.contentType ?? undefined,
482
+ content: fileBuffer,
483
+ });
484
+ }
485
+ }
486
+ return attachments;
487
+ }
488
+ async sendSingleRecipient(recipient, organization, data) {
489
+ let promiseResolve;
490
+ const promise = new Promise((resolve) => {
491
+ promiseResolve = resolve;
492
+ });
493
+ const virtualRecipient = recipient.getRecipient();
494
+ let resolved = false;
495
+ const callback = async (error) => {
496
+ if (resolved) {
497
+ return;
498
+ }
499
+ resolved = true;
500
+ try {
501
+ if (error === null) {
502
+ // Mark saved
503
+ recipient.sentAt = new Date();
504
+ recipient.failErrorMessage = null;
505
+ if (recipient.failError) {
506
+ recipient.previousFailError = recipient.failError;
507
+ }
508
+ recipient.failError = null;
509
+ // Update repacements that have been generated
510
+ recipient.replacements = virtualRecipient.replacements;
511
+ await recipient.save();
512
+ }
513
+ else {
514
+ recipient.failCount += 1;
515
+ recipient.failErrorMessage = error.message;
516
+ if (recipient.failError) {
517
+ recipient.previousFailError = recipient.failError;
518
+ }
519
+ recipient.failError = errorToSimpleErrors(error);
520
+ recipient.firstFailedAt = recipient.firstFailedAt ?? new Date();
521
+ recipient.lastFailedAt = new Date();
522
+ await recipient.save();
523
+ }
524
+ }
525
+ catch (e) {
526
+ console.error(e);
527
+ }
528
+ promiseResolve();
529
+ };
530
+ // Do send the email
531
+ // Create e-mail builder
532
+ const builder = await (0, EmailBuilder_1.getEmailBuilder)(organization ?? null, {
533
+ recipients: [
534
+ virtualRecipient,
535
+ ],
536
+ from: data.from,
537
+ replyTo: data.replyTo,
538
+ subject: this.subject,
539
+ html: this.html,
540
+ type: this.emailType ? 'transactional' : 'broadcast',
541
+ attachments: data.attachments,
542
+ callback(error) {
543
+ callback(error).catch(console.error);
544
+ },
545
+ headers: {
546
+ 'X-Email-Id': this.id,
547
+ 'X-Email-Recipient-Id': recipient.id,
548
+ },
549
+ });
550
+ email_1.Email.schedule(builder);
551
+ return await promise;
552
+ }
553
+ async resumeSending(singleRecipientId = null) {
430
554
  const id = this.id;
431
555
  return await queues_1.QueueHandler.schedule('send-email', async ({ abort }) => {
432
556
  return await this.lock(async function (upToDate) {
433
557
  if (upToDate.status === structures_1.EmailStatus.Sent) {
434
558
  // Already done
435
559
  // In other cases -> queue has stopped and we can retry
436
- return upToDate;
437
- }
438
- if (upToDate.status === structures_1.EmailStatus.Sending) {
439
- // This is an automatic retry.
440
- if (upToDate.emailType) {
441
- // Not eligible for retry
442
- upToDate.status = structures_1.EmailStatus.Failed;
443
- await upToDate.save();
560
+ if (!singleRecipientId) {
561
+ console.log('Email already sent, skipping...', upToDate.id);
444
562
  return upToDate;
445
563
  }
446
- if (upToDate.createdAt < new Date(new Date().getTime() - 1000 * 60 * 60 * 24 * 2)) {
447
- // Too long
448
- console.error('Email has been sending for too long. Marking as failed...', upToDate.id);
449
- upToDate.status = structures_1.EmailStatus.Failed;
450
- await upToDate.save();
564
+ }
565
+ else {
566
+ if (singleRecipientId) {
567
+ // Not possible
568
+ throw new simple_errors_1.SimpleError({
569
+ code: 'invalid_state',
570
+ message: 'Cannot retry single recipient for email that is not yet sent',
571
+ });
572
+ }
573
+ if (upToDate.status === structures_1.EmailStatus.Sending) {
574
+ // This is an automatic retry.
575
+ if (upToDate.emailType) {
576
+ // Not eligible for retry
577
+ upToDate.status = structures_1.EmailStatus.Failed;
578
+ await upToDate.save();
579
+ return upToDate;
580
+ }
581
+ if (upToDate.createdAt < new Date(new Date().getTime() - 1000 * 60 * 60 * 24 * 2)) {
582
+ // Too long
583
+ console.error('Email has been sending for too long. Marking as failed...', upToDate.id);
584
+ upToDate.status = structures_1.EmailStatus.Failed;
585
+ await upToDate.save();
586
+ return upToDate;
587
+ }
588
+ }
589
+ else if (upToDate.status !== structures_1.EmailStatus.Queued) {
590
+ console.error('Email is not queued or sending, cannot send', upToDate.id, upToDate.status);
451
591
  return upToDate;
452
592
  }
453
593
  }
454
- else if (upToDate.status !== structures_1.EmailStatus.Queued) {
455
- console.error('Email is not queued or sending, cannot send', upToDate.id, upToDate.status);
456
- return upToDate;
457
- }
458
594
  const organization = upToDate.organizationId ? await Organization_1.Organization.getByID(upToDate.organizationId) : null;
459
595
  let from = upToDate.getDefaultFromAddress(organization);
460
596
  let replyTo = upToDate.getFromAddress();
461
- const attachments = [];
462
597
  let succeededCount = 0;
463
598
  let softFailedCount = 0;
464
599
  let failedCount = 0;
@@ -478,7 +613,9 @@ class Email extends sql_1.QueryableModel {
478
613
  replyTo = null;
479
614
  }
480
615
  abort.throwIfAborted();
481
- upToDate.status = structures_1.EmailStatus.Sending;
616
+ if (!singleRecipientId) {
617
+ upToDate.status = structures_1.EmailStatus.Sending;
618
+ }
482
619
  upToDate.sentAt = upToDate.sentAt ?? new Date();
483
620
  await upToDate.save();
484
621
  // Create recipients if not yet created
@@ -503,67 +640,17 @@ class Email extends sql_1.QueryableModel {
503
640
  }
504
641
  // Create a buffer of all attachments
505
642
  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;
510
- }
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(/-+$/, '');
518
- }
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,
558
- });
559
- }
560
- }
643
+ const attachments = await upToDate.loadAttachments();
561
644
  // Start actually sending in batches of recipients that are not yet sent
562
645
  let idPointer = '';
563
646
  const batchSize = 100;
564
647
  let isSavingStatus = false;
565
648
  let lastStatusSave = new Date();
566
649
  async function saveStatus() {
650
+ if (singleRecipientId) {
651
+ // Don't save during looping
652
+ return;
653
+ }
567
654
  if (!upToDate) {
568
655
  return;
569
656
  }
@@ -621,71 +708,41 @@ class Email extends sql_1.QueryableModel {
621
708
  skipped++;
622
709
  continue;
623
710
  }
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;
633
- }
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();
711
+ if (singleRecipientId) {
712
+ if (recipient.id !== singleRecipientId) {
713
+ // Failed or soft-failed
714
+ if (recipient.failError && (0, structures_1.isSoftEmailRecipientError)(recipient.failError)) {
715
+ softFailedCount += 1;
644
716
  }
645
717
  else {
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();
718
+ failedCount += 1;
659
719
  }
720
+ skipped++;
721
+ await saveStatus();
722
+ continue;
660
723
  }
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
- ],
724
+ }
725
+ const promise = upToDate.sendSingleRecipient(recipient, organization ?? null, {
672
726
  from,
673
727
  replyTo,
674
- subject: upToDate.subject,
675
- html: upToDate.html,
676
- type: upToDate.emailType ? 'transactional' : 'broadcast',
677
728
  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
729
  });
686
- abort.throwIfAborted(); // do not schedule if aborted
687
- email_1.Email.schedule(builder);
688
- sendingPromises.push(promise);
730
+ sendingPromises.push(promise.then(async () => {
731
+ if (recipient.sentAt) {
732
+ succeededCount += 1;
733
+ await saveStatus();
734
+ }
735
+ else {
736
+ // Failed or soft-failed
737
+ if (recipient.failError && (0, structures_1.isSoftEmailRecipientError)(recipient.failError)) {
738
+ softFailedCount += 1;
739
+ }
740
+ else {
741
+ failedCount += 1;
742
+ }
743
+ await saveStatus();
744
+ }
745
+ }));
689
746
  }
690
747
  if (sendingPromises.length > 0 || skipped > 0) {
691
748
  await Promise.all(sendingPromises);
@@ -700,38 +757,42 @@ class Email extends sql_1.QueryableModel {
700
757
  if (!upToDate) {
701
758
  throw e;
702
759
  }
703
- upToDate.succeededCount = succeededCount;
704
- upToDate.softFailedCount = softFailedCount;
705
- upToDate.failedCount = failedCount;
706
- if ((0, queues_1.isAbortedError)(e) || (((0, simple_errors_1.isSimpleError)(e) || (0, simple_errors_1.isSimpleErrors)(e)) && e.hasCode('SHUTDOWN'))) {
707
- // Keep sending status: we'll resume after the reboot
760
+ if (!singleRecipientId) {
761
+ upToDate.succeededCount = succeededCount;
762
+ upToDate.softFailedCount = softFailedCount;
763
+ upToDate.failedCount = failedCount;
764
+ if ((0, queues_1.isAbortedError)(e) || (((0, simple_errors_1.isSimpleError)(e) || (0, simple_errors_1.isSimpleErrors)(e)) && e.hasCode('SHUTDOWN'))) {
765
+ // Keep sending status: we'll resume after the reboot
766
+ await upToDate.save();
767
+ throw e;
768
+ }
769
+ upToDate.emailErrors = errorToSimpleErrors(e);
770
+ upToDate.status = structures_1.EmailStatus.Failed;
708
771
  await upToDate.save();
709
- throw e;
710
772
  }
711
- upToDate.emailErrors = errorToSimpleErrors(e);
712
- upToDate.status = structures_1.EmailStatus.Failed;
713
- await upToDate.save();
714
773
  throw e;
715
774
  }
716
- if (upToDate.sendAsEmail && upToDate.emailRecipientsCount === 0 && upToDate.userId === null) {
717
- // We only delete automated emails (email type) if they have no recipients
718
- console.log('No recipients found for email ', upToDate.id, ' deleting...');
719
- await upToDate.delete();
720
- return null;
721
- }
722
- console.log('Finished sending email', upToDate.id);
723
- // Mark email as sent
724
- if (upToDate.sendAsEmail && !upToDate.showInMemberPortal && (succeededCount + failedCount + softFailedCount) === 0) {
725
- upToDate.status = structures_1.EmailStatus.Failed;
726
- upToDate.emailErrors = new simple_errors_1.SimpleErrors(new simple_errors_1.SimpleError({
727
- code: 'no_recipients',
728
- message: 'No recipients',
729
- human: $t(`9fe3de8e-090c-4949-97da-4810ce9e61c7`),
730
- }));
731
- }
732
- else {
733
- upToDate.status = structures_1.EmailStatus.Sent;
734
- upToDate.emailErrors = null;
775
+ if (!singleRecipientId) {
776
+ if (upToDate.sendAsEmail && upToDate.emailRecipientsCount === 0 && upToDate.userId === null) {
777
+ // We only delete automated emails (email type) if they have no recipients
778
+ console.log('No recipients found for email ', upToDate.id, ' deleting...');
779
+ await upToDate.delete();
780
+ return null;
781
+ }
782
+ console.log('Finished sending email', upToDate.id);
783
+ // Mark email as sent
784
+ if (upToDate.sendAsEmail && !upToDate.showInMemberPortal && (succeededCount + failedCount + softFailedCount) === 0) {
785
+ upToDate.status = structures_1.EmailStatus.Failed;
786
+ upToDate.emailErrors = new simple_errors_1.SimpleErrors(new simple_errors_1.SimpleError({
787
+ code: 'no_recipients',
788
+ message: 'No recipients',
789
+ human: $t(`9fe3de8e-090c-4949-97da-4810ce9e61c7`),
790
+ }));
791
+ }
792
+ else {
793
+ upToDate.status = structures_1.EmailStatus.Sent;
794
+ upToDate.emailErrors = null;
795
+ }
735
796
  }
736
797
  upToDate.succeededCount = succeededCount;
737
798
  upToDate.softFailedCount = softFailedCount;