@stamhoofd/models 2.92.0 → 2.93.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/helpers/EmailBuilder.d.ts +1 -0
- package/dist/src/helpers/EmailBuilder.d.ts.map +1 -1
- package/dist/src/helpers/EmailBuilder.js +5 -3
- package/dist/src/helpers/EmailBuilder.js.map +1 -1
- package/dist/src/migrations/1755789797-email-counts-errors.sql +7 -0
- package/dist/src/migrations/1755789798-email-recipient-errors.sql +2 -0
- package/dist/src/migrations/1756115313-email-recipient-ids-and-errors.sql +10 -0
- package/dist/src/migrations/1756115314-email-recipient-email-optional.sql +2 -0
- package/dist/src/migrations/1756115315-email-recipients-count.sql +3 -0
- package/dist/src/migrations/1756115316-email-cached-counts.sql +4 -0
- package/dist/src/models/Email.d.ts +62 -2
- package/dist/src/models/Email.d.ts.map +1 -1
- package/dist/src/models/Email.js +555 -194
- package/dist/src/models/Email.js.map +1 -1
- package/dist/src/models/Email.test.js +151 -43
- package/dist/src/models/Email.test.js.map +1 -1
- package/dist/src/models/EmailRecipient.d.ts +31 -1
- package/dist/src/models/EmailRecipient.d.ts.map +1 -1
- package/dist/src/models/EmailRecipient.js +53 -2
- package/dist/src/models/EmailRecipient.js.map +1 -1
- package/package.json +2 -2
- package/src/helpers/EmailBuilder.ts +6 -3
- package/src/migrations/1755789797-email-counts-errors.sql +7 -0
- package/src/migrations/1755789798-email-recipient-errors.sql +2 -0
- package/src/migrations/1756115313-email-recipient-ids-and-errors.sql +10 -0
- package/src/migrations/1756115314-email-recipient-email-optional.sql +2 -0
- package/src/migrations/1756115315-email-recipients-count.sql +3 -0
- package/src/migrations/1756115316-email-cached-counts.sql +4 -0
- package/src/models/Email.test.ts +165 -44
- package/src/models/Email.ts +636 -207
- package/src/models/EmailRecipient.ts +46 -2
package/src/models/Email.ts
CHANGED
|
@@ -3,16 +3,44 @@ import { EmailAttachment, EmailPreview, EmailRecipientFilter, EmailRecipientFilt
|
|
|
3
3
|
import { v4 as uuidv4 } from 'uuid';
|
|
4
4
|
|
|
5
5
|
import { AnyDecoder, ArrayDecoder } from '@simonbackx/simple-encoding';
|
|
6
|
-
import { SimpleError } from '@simonbackx/simple-errors';
|
|
6
|
+
import { isSimpleError, isSimpleErrors, SimpleError, SimpleErrors } from '@simonbackx/simple-errors';
|
|
7
7
|
import { I18n } from '@stamhoofd/backend-i18n';
|
|
8
8
|
import { Email as EmailClass, EmailInterfaceRecipient } from '@stamhoofd/email';
|
|
9
|
-
import { QueueHandler } from '@stamhoofd/queues';
|
|
10
|
-
import { QueryableModel, SQL, SQLWhereSign } from '@stamhoofd/sql';
|
|
9
|
+
import { isAbortedError, QueueHandler, QueueHandlerOptions } from '@stamhoofd/queues';
|
|
10
|
+
import { QueryableModel, readDynamicSQLExpression, SQL, SQLAlias, SQLCalculation, SQLCount, SQLPlusSign, SQLSelectAs, SQLWhereSign } from '@stamhoofd/sql';
|
|
11
11
|
import { canSendFromEmail, fillRecipientReplacements, getEmailBuilder } from '../helpers/EmailBuilder';
|
|
12
12
|
import { EmailRecipient } from './EmailRecipient';
|
|
13
13
|
import { EmailTemplate } from './EmailTemplate';
|
|
14
14
|
import { Organization } from './Organization';
|
|
15
15
|
|
|
16
|
+
function errorToSimpleErrors(e: unknown) {
|
|
17
|
+
if (isSimpleErrors(e)) {
|
|
18
|
+
return e;
|
|
19
|
+
}
|
|
20
|
+
else if (isSimpleError(e)) {
|
|
21
|
+
return new SimpleErrors(e);
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
return new SimpleErrors(
|
|
25
|
+
new SimpleError({
|
|
26
|
+
code: 'unknown_error',
|
|
27
|
+
message: ((typeof e === 'object' && e !== null && 'message' in e && typeof e.message === 'string') ? e.message : 'Unknown error'),
|
|
28
|
+
human: $t(`Onbekende fout`),
|
|
29
|
+
}),
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function isSoftEmailRecipientError(error: SimpleErrors) {
|
|
35
|
+
if (error.hasCode('email_skipped_unsubscribed')) {
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
if (error.hasCode('email_skipped_duplicate_recipient')) {
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
|
|
16
44
|
export class Email extends QueryableModel {
|
|
17
45
|
static table = 'emails';
|
|
18
46
|
|
|
@@ -61,8 +89,64 @@ export class Email extends QueryableModel {
|
|
|
61
89
|
@column({ type: 'string', nullable: true })
|
|
62
90
|
fromName: string | null = null;
|
|
63
91
|
|
|
92
|
+
/**
|
|
93
|
+
* Amount of recipients with an email address
|
|
94
|
+
*/
|
|
64
95
|
@column({ type: 'integer', nullable: true })
|
|
65
|
-
|
|
96
|
+
emailRecipientsCount: number | null = null;
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Amount of recipients without an email address
|
|
100
|
+
*/
|
|
101
|
+
@column({ type: 'integer', nullable: true })
|
|
102
|
+
otherRecipientsCount: number | null = null;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Amount of recipients that have successfully received the email.
|
|
106
|
+
*/
|
|
107
|
+
@column({ type: 'integer' })
|
|
108
|
+
succeededCount = 0;
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Amount of recipients that somehow failed to receive the email,
|
|
112
|
+
* but with a soft error that doesn't require action.
|
|
113
|
+
* - Duplicate email in recipient list
|
|
114
|
+
* - Unsubscribed
|
|
115
|
+
*/
|
|
116
|
+
@column({ type: 'integer' })
|
|
117
|
+
softFailedCount = 0;
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Amount of recipients that somehow failed to receive the email:
|
|
121
|
+
* - Invalid email address
|
|
122
|
+
* - Full email inbox
|
|
123
|
+
*/
|
|
124
|
+
@column({ type: 'integer' })
|
|
125
|
+
failedCount = 0;
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Unique amount of members that are in the recipients list.
|
|
129
|
+
*/
|
|
130
|
+
@column({ type: 'integer' })
|
|
131
|
+
membersCount = 0;
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Does only include bounces AFTER sending the email
|
|
135
|
+
*/
|
|
136
|
+
@column({ type: 'integer' })
|
|
137
|
+
hardBouncesCount = 0;
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Does only include bounces AFTER sending the email
|
|
141
|
+
*/
|
|
142
|
+
@column({ type: 'integer' })
|
|
143
|
+
softBouncesCount = 0;
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Does only include bounces AFTER sending the email
|
|
147
|
+
*/
|
|
148
|
+
@column({ type: 'integer' })
|
|
149
|
+
spamComplaintsCount = 0;
|
|
66
150
|
|
|
67
151
|
@column({ type: 'string' })
|
|
68
152
|
status = EmailStatus.Draft;
|
|
@@ -70,6 +154,18 @@ export class Email extends QueryableModel {
|
|
|
70
154
|
@column({ type: 'string' })
|
|
71
155
|
recipientsStatus = EmailRecipientsStatus.NotCreated;
|
|
72
156
|
|
|
157
|
+
/**
|
|
158
|
+
* Errors related to creating the recipients.
|
|
159
|
+
*/
|
|
160
|
+
@column({ type: 'json', nullable: true, decoder: SimpleErrors })
|
|
161
|
+
recipientsErrors: SimpleErrors | null = null;
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Errors related to sending the email.
|
|
165
|
+
*/
|
|
166
|
+
@column({ type: 'json', nullable: true, decoder: SimpleErrors })
|
|
167
|
+
emailErrors: SimpleErrors | null = null;
|
|
168
|
+
|
|
73
169
|
/**
|
|
74
170
|
* todo: ignore automatically
|
|
75
171
|
*/
|
|
@@ -109,6 +205,8 @@ export class Email extends QueryableModel {
|
|
|
109
205
|
count(request: LimitedFilteredRequest, subfilter: StamhoofdFilter | null): Promise<number>;
|
|
110
206
|
}> = new Map();
|
|
111
207
|
|
|
208
|
+
static pendingNotificationCountUpdates: Map<string, { timer: NodeJS.Timeout | null; lastUpdate: Date | null }> = new Map();
|
|
209
|
+
|
|
112
210
|
throwIfNotReadyToSend() {
|
|
113
211
|
if (this.subject == null || this.subject.length == 0) {
|
|
114
212
|
throw new SimpleError({
|
|
@@ -142,6 +240,14 @@ export class Email extends QueryableModel {
|
|
|
142
240
|
});
|
|
143
241
|
}
|
|
144
242
|
|
|
243
|
+
if (this.recipientsErrors !== null && this.recipientsStatus !== EmailRecipientsStatus.Created) {
|
|
244
|
+
throw new SimpleError({
|
|
245
|
+
code: 'invalid_recipients',
|
|
246
|
+
message: 'Failed to build recipients (count)',
|
|
247
|
+
human: $t(`Er ging iets mis bij het aanmaken van de ontvangers. Probeer je selectie aan te passen. Neem contact op als het probleem zich blijft voordoen.`) + ' ' + this.recipientsErrors.getHuman(),
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
145
251
|
this.validateAttachments();
|
|
146
252
|
}
|
|
147
253
|
|
|
@@ -264,13 +370,10 @@ export class Email extends QueryableModel {
|
|
|
264
370
|
return true;
|
|
265
371
|
}
|
|
266
372
|
|
|
267
|
-
async
|
|
268
|
-
this.throwIfNotReadyToSend();
|
|
269
|
-
await this.save();
|
|
270
|
-
|
|
373
|
+
async lock<T>(callback: (upToDate: Email, options: QueueHandlerOptions) => Promise<T> | T): Promise<T> {
|
|
271
374
|
const id = this.id;
|
|
272
|
-
return await QueueHandler.schedule('
|
|
273
|
-
|
|
375
|
+
return await QueueHandler.schedule('lock-email-' + id, async (options) => {
|
|
376
|
+
const upToDate = await Email.getByID(id);
|
|
274
377
|
if (!upToDate) {
|
|
275
378
|
throw new SimpleError({
|
|
276
379
|
code: 'not_found',
|
|
@@ -278,258 +381,546 @@ export class Email extends QueryableModel {
|
|
|
278
381
|
human: $t(`55899a7c-f3d4-43fe-a431-70a3a9e78e34`),
|
|
279
382
|
});
|
|
280
383
|
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
384
|
+
const c = await callback(upToDate, options);
|
|
385
|
+
this.copyFrom(upToDate);
|
|
386
|
+
return c;
|
|
387
|
+
});
|
|
388
|
+
}
|
|
286
389
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
return upToDate;
|
|
301
|
-
}
|
|
390
|
+
static async bumpNotificationCount(emailId: string, type: 'hard-bounce' | 'soft-bounce' | 'complaint') {
|
|
391
|
+
// Send an update query
|
|
392
|
+
const base = Email.update()
|
|
393
|
+
.where('id', emailId);
|
|
394
|
+
|
|
395
|
+
switch (type) {
|
|
396
|
+
case 'hard-bounce': {
|
|
397
|
+
base.set('hardBouncesCount', new SQLCalculation(
|
|
398
|
+
SQL.column('hardBouncesCount'),
|
|
399
|
+
new SQLPlusSign(),
|
|
400
|
+
readDynamicSQLExpression(1),
|
|
401
|
+
));
|
|
402
|
+
break;
|
|
302
403
|
}
|
|
303
|
-
|
|
304
|
-
|
|
404
|
+
case 'soft-bounce': {
|
|
405
|
+
base.set('softBouncesCount', new SQLCalculation(
|
|
406
|
+
SQL.column('softBouncesCount'),
|
|
407
|
+
new SQLPlusSign(),
|
|
408
|
+
readDynamicSQLExpression(1),
|
|
409
|
+
));
|
|
410
|
+
break;
|
|
411
|
+
}
|
|
412
|
+
case 'complaint': {
|
|
413
|
+
base.set('spamComplaintsCount', new SQLCalculation(
|
|
414
|
+
SQL.column('spamComplaintsCount'),
|
|
415
|
+
new SQLPlusSign(),
|
|
416
|
+
readDynamicSQLExpression(1),
|
|
417
|
+
));
|
|
418
|
+
break;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
305
421
|
|
|
306
|
-
|
|
307
|
-
let replyTo: EmailInterfaceRecipient | null = upToDate.getFromAddress();
|
|
422
|
+
await base.update();
|
|
308
423
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
code: 'invalid_field',
|
|
312
|
-
message: 'Missing from',
|
|
313
|
-
human: $t(`e92cd077-b0f1-4b0a-82a0-8a8baa82e73a`),
|
|
314
|
-
});
|
|
315
|
-
}
|
|
424
|
+
await this.checkNeedsNotificationCountUpdate(emailId, true);
|
|
425
|
+
}
|
|
316
426
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
427
|
+
static async checkNeedsNotificationCountUpdate(emailId: string, didUpdate = false) {
|
|
428
|
+
const existing = this.pendingNotificationCountUpdates.get(emailId);
|
|
429
|
+
const object = existing ?? {
|
|
430
|
+
timer: null,
|
|
431
|
+
lastUpdate: didUpdate ? new Date() : null,
|
|
432
|
+
};
|
|
322
433
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
await upToDate.save();
|
|
434
|
+
if (didUpdate) {
|
|
435
|
+
object.lastUpdate = new Date();
|
|
436
|
+
}
|
|
327
437
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
438
|
+
if (existing) {
|
|
439
|
+
this.pendingNotificationCountUpdates.set(emailId, object);
|
|
440
|
+
}
|
|
331
441
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
throw new SimpleError({
|
|
336
|
-
code: 'not_found',
|
|
337
|
-
message: 'Email not found',
|
|
338
|
-
human: $t(`55899a7c-f3d4-43fe-a431-70a3a9e78e34`),
|
|
339
|
-
});
|
|
340
|
-
}
|
|
442
|
+
if (object.lastUpdate && object.lastUpdate < new Date(Date.now() - 5 * 60 * 1000)) {
|
|
443
|
+
// After 5 minutes without notifications, run an update.
|
|
444
|
+
await this.updateNotificationsCounts(emailId);
|
|
341
445
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
446
|
+
// Stop here
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Schedule a slow update of all counts
|
|
451
|
+
if (!object.timer) {
|
|
452
|
+
object.timer = setTimeout(() => {
|
|
453
|
+
object.timer = null;
|
|
454
|
+
this.checkNeedsNotificationCountUpdate(emailId).catch(console.error);
|
|
455
|
+
}, 1 * 60 * 1000);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
static async updateNotificationsCounts(emailId: string) {
|
|
460
|
+
QueueHandler.cancel('updateNotificationsCounts-' + emailId);
|
|
461
|
+
return await QueueHandler.schedule('updateNotificationsCounts-' + emailId, async () => {
|
|
462
|
+
const query = SQL.select(
|
|
463
|
+
new SQLSelectAs(
|
|
464
|
+
new SQLCount(
|
|
465
|
+
SQL.column('hardBounceError'),
|
|
466
|
+
),
|
|
467
|
+
new SQLAlias('data__hardBounces'),
|
|
468
|
+
),
|
|
469
|
+
// If the current amount_due is negative, we can ignore that negative part if there is a future due item
|
|
470
|
+
new SQLSelectAs(
|
|
471
|
+
new SQLCount(
|
|
472
|
+
SQL.column('softBounceError'),
|
|
473
|
+
),
|
|
474
|
+
new SQLAlias('data__softBounces'),
|
|
475
|
+
),
|
|
476
|
+
|
|
477
|
+
new SQLSelectAs(
|
|
478
|
+
new SQLCount(
|
|
479
|
+
SQL.column('spamComplaintError'),
|
|
480
|
+
),
|
|
481
|
+
new SQLAlias('data__complaints'),
|
|
482
|
+
),
|
|
483
|
+
)
|
|
484
|
+
.from(EmailRecipient.table)
|
|
485
|
+
.where('emailId', emailId);
|
|
486
|
+
|
|
487
|
+
const result = await query.fetch();
|
|
488
|
+
if (result.length !== 1) {
|
|
489
|
+
console.error('Unexpected result', result);
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
const row = result[0]['data'];
|
|
493
|
+
if (!row) {
|
|
494
|
+
console.error('Unexpected result row', result);
|
|
495
|
+
return;
|
|
348
496
|
}
|
|
349
497
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
const
|
|
353
|
-
const recipientsSet = new Set<string>();
|
|
498
|
+
const hardBounces = row['hardBounces'];
|
|
499
|
+
const softBounces = row['softBounces'];
|
|
500
|
+
const complaints = row['complaints'];
|
|
354
501
|
|
|
355
|
-
|
|
356
|
-
|
|
502
|
+
if (typeof hardBounces !== 'number' || typeof softBounces !== 'number' || typeof complaints !== 'number') {
|
|
503
|
+
console.error('Unexpected result values', row);
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
357
506
|
|
|
358
|
-
|
|
359
|
-
if (!attachment.content && !attachment.file) {
|
|
360
|
-
console.warn('Attachment without content found, skipping', attachment);
|
|
361
|
-
continue;
|
|
362
|
-
}
|
|
507
|
+
console.log('Updating email notification counts', emailId, hardBounces, softBounces, complaints);
|
|
363
508
|
|
|
364
|
-
|
|
509
|
+
// Send an update query
|
|
510
|
+
await Email.update()
|
|
511
|
+
.where('id', emailId)
|
|
512
|
+
.set('hardBouncesCount', hardBounces)
|
|
513
|
+
.set('softBouncesCount', softBounces)
|
|
514
|
+
.set('spamComplaintsCount', complaints)
|
|
515
|
+
.update();
|
|
516
|
+
});
|
|
517
|
+
}
|
|
365
518
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
519
|
+
async queueForSending(waitForSending = false) {
|
|
520
|
+
this.throwIfNotReadyToSend();
|
|
521
|
+
await this.lock(async (upToDate) => {
|
|
522
|
+
if (upToDate.status === EmailStatus.Draft) {
|
|
523
|
+
upToDate.status = EmailStatus.Queued;
|
|
524
|
+
}
|
|
525
|
+
await upToDate.save();
|
|
526
|
+
});
|
|
527
|
+
if (waitForSending) {
|
|
528
|
+
return await this.resumeSending();
|
|
529
|
+
}
|
|
530
|
+
else {
|
|
531
|
+
this.resumeSending().catch(console.error);
|
|
532
|
+
}
|
|
533
|
+
return this;
|
|
534
|
+
}
|
|
370
535
|
|
|
371
|
-
|
|
372
|
-
|
|
536
|
+
async resumeSending(): Promise<Email | null> {
|
|
537
|
+
const id = this.id;
|
|
538
|
+
return await QueueHandler.schedule('send-email', async ({ abort }) => {
|
|
539
|
+
return await this.lock(async function (upToDate: Email) {
|
|
540
|
+
if (upToDate.status === EmailStatus.Sent) {
|
|
541
|
+
// Already done
|
|
542
|
+
// In other cases -> queue has stopped and we can retry
|
|
543
|
+
return upToDate;
|
|
373
544
|
}
|
|
374
545
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
546
|
+
if (upToDate.status === EmailStatus.Sending) {
|
|
547
|
+
// This is an automatic retry.
|
|
548
|
+
if (upToDate.emailType) {
|
|
549
|
+
// Not eligible for retry
|
|
550
|
+
upToDate.status = EmailStatus.Failed;
|
|
551
|
+
await upToDate.save();
|
|
552
|
+
return upToDate;
|
|
553
|
+
}
|
|
554
|
+
if (upToDate.createdAt < new Date(new Date().getTime() - 1000 * 60 * 60 * 24 * 2)) {
|
|
555
|
+
// Too long
|
|
556
|
+
console.error('Email has been sending for too long. Marking as failed...', upToDate.id);
|
|
557
|
+
upToDate.status = EmailStatus.Failed;
|
|
558
|
+
await upToDate.save();
|
|
559
|
+
return upToDate;
|
|
560
|
+
}
|
|
378
561
|
}
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
filename: filename,
|
|
383
|
-
content: attachment.content,
|
|
384
|
-
contentType: attachment.contentType ?? undefined,
|
|
385
|
-
encoding: 'base64',
|
|
386
|
-
});
|
|
562
|
+
else if (upToDate.status !== EmailStatus.Queued) {
|
|
563
|
+
console.error('Email is not queued or sending, cannot send', upToDate.id, upToDate.status);
|
|
564
|
+
return upToDate;
|
|
387
565
|
}
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
566
|
+
const organization = upToDate.organizationId ? await Organization.getByID(upToDate.organizationId) : null;
|
|
567
|
+
let from = upToDate.getDefaultFromAddress(organization);
|
|
568
|
+
let replyTo: EmailInterfaceRecipient | null = upToDate.getFromAddress();
|
|
569
|
+
const attachments: { filename: string; path?: string; href?: string; content?: string | Buffer; contentType?: string; encoding?: string }[] = [];
|
|
570
|
+
let succeededCount = 0;
|
|
571
|
+
let softFailedCount = 0;
|
|
572
|
+
let failedCount = 0;
|
|
573
|
+
|
|
574
|
+
try {
|
|
575
|
+
upToDate.throwIfNotReadyToSend();
|
|
576
|
+
|
|
577
|
+
if (!from) {
|
|
392
578
|
throw new SimpleError({
|
|
393
|
-
code: '
|
|
394
|
-
message: '
|
|
395
|
-
human: $t(`
|
|
579
|
+
code: 'invalid_field',
|
|
580
|
+
message: 'Missing from',
|
|
581
|
+
human: $t(`e92cd077-b0f1-4b0a-82a0-8a8baa82e73a`),
|
|
396
582
|
});
|
|
397
583
|
}
|
|
398
584
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
fileBuffer = Buffer.from(await response.arrayBuffer());
|
|
585
|
+
// Can we send from this e-mail or reply-to?
|
|
586
|
+
if (upToDate.fromAddress && await canSendFromEmail(upToDate.fromAddress, organization ?? null)) {
|
|
587
|
+
from = upToDate.getFromAddress();
|
|
588
|
+
replyTo = null;
|
|
404
589
|
}
|
|
405
|
-
|
|
590
|
+
|
|
591
|
+
abort.throwIfAborted();
|
|
592
|
+
upToDate.status = EmailStatus.Sending;
|
|
593
|
+
upToDate.sentAt = upToDate.sentAt ?? new Date();
|
|
594
|
+
await upToDate.save();
|
|
595
|
+
|
|
596
|
+
// Create recipients if not yet created
|
|
597
|
+
await upToDate.buildRecipients();
|
|
598
|
+
abort.throwIfAborted();
|
|
599
|
+
|
|
600
|
+
// Refresh model
|
|
601
|
+
const c = (await Email.getByID(id))!;
|
|
602
|
+
if (!c) {
|
|
406
603
|
throw new SimpleError({
|
|
407
|
-
code: '
|
|
408
|
-
message: '
|
|
409
|
-
human: $t(`
|
|
604
|
+
code: 'not_found',
|
|
605
|
+
message: 'Email not found',
|
|
606
|
+
human: $t(`55899a7c-f3d4-43fe-a431-70a3a9e78e34`),
|
|
410
607
|
});
|
|
411
608
|
}
|
|
609
|
+
upToDate.copyFrom(c);
|
|
412
610
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
611
|
+
if (upToDate.recipientsStatus !== EmailRecipientsStatus.Created) {
|
|
612
|
+
throw new SimpleError({
|
|
613
|
+
code: 'recipients_not_created',
|
|
614
|
+
message: 'Failed to create recipients',
|
|
615
|
+
human: $t(`f660b2eb-e382-4d21-86e4-673ca7bc2d4a`),
|
|
616
|
+
});
|
|
617
|
+
}
|
|
420
618
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
.where('id', SQLWhereSign.Greater, idPointer)
|
|
428
|
-
.orderBy(SQL.column('id'), 'ASC')
|
|
429
|
-
.limit(batchSize)
|
|
430
|
-
.fetch();
|
|
431
|
-
|
|
432
|
-
const recipients = EmailRecipient.fromRows(data, 'email_recipients');
|
|
433
|
-
|
|
434
|
-
if (recipients.length === 0) {
|
|
435
|
-
break;
|
|
436
|
-
}
|
|
619
|
+
// Create a buffer of all attachments
|
|
620
|
+
for (const attachment of upToDate.attachments) {
|
|
621
|
+
if (!attachment.content && !attachment.file) {
|
|
622
|
+
console.warn('Attachment without content found, skipping', attachment);
|
|
623
|
+
continue;
|
|
624
|
+
}
|
|
437
625
|
|
|
438
|
-
|
|
626
|
+
let filename = $t('b1291584-d2ad-4ebd-88ed-cbda4f3755b4');
|
|
439
627
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
}
|
|
628
|
+
if (attachment.contentType === 'application/pdf') {
|
|
629
|
+
// tmp solution for pdf only
|
|
630
|
+
filename += '.pdf';
|
|
631
|
+
}
|
|
445
632
|
|
|
446
|
-
|
|
447
|
-
|
|
633
|
+
if (attachment.file?.name) {
|
|
634
|
+
filename = attachment.file.name.toLowerCase().replace(/[^a-z0-9.]+/g, '-').replace(/^-+/, '').replace(/-+$/, '');
|
|
635
|
+
}
|
|
448
636
|
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
637
|
+
// Correct file name if needed
|
|
638
|
+
if (attachment.filename) {
|
|
639
|
+
filename = attachment.filename.toLowerCase().replace(/[^a-z0-9.]+/g, '-').replace(/^-+/, '').replace(/-+$/, '');
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
if (attachment.content) {
|
|
643
|
+
attachments.push({
|
|
644
|
+
filename: filename,
|
|
645
|
+
content: attachment.content,
|
|
646
|
+
contentType: attachment.contentType ?? undefined,
|
|
647
|
+
encoding: 'base64',
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
else {
|
|
651
|
+
// 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
|
|
652
|
+
const withSigned = await attachment.file!.withSignedUrl();
|
|
653
|
+
if (!withSigned || !withSigned.signedUrl) {
|
|
654
|
+
throw new SimpleError({
|
|
655
|
+
code: 'attachment_not_found',
|
|
656
|
+
message: 'Attachment not found',
|
|
657
|
+
human: $t(`ce6ddaf0-8347-42c5-b4b7-fbe860c7b7f2`),
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
const filePath = withSigned.signedUrl;
|
|
662
|
+
let fileBuffer: Buffer | null = null;
|
|
663
|
+
try {
|
|
664
|
+
const response = await fetch(filePath);
|
|
665
|
+
fileBuffer = Buffer.from(await response.arrayBuffer());
|
|
666
|
+
}
|
|
667
|
+
catch (e) {
|
|
668
|
+
throw new SimpleError({
|
|
669
|
+
code: 'attachment_not_found',
|
|
670
|
+
message: 'Attachment not found',
|
|
671
|
+
human: $t(`ce6ddaf0-8347-42c5-b4b7-fbe860c7b7f2`),
|
|
672
|
+
});
|
|
673
|
+
}
|
|
453
674
|
|
|
454
|
-
|
|
675
|
+
attachments.push({
|
|
676
|
+
filename: filename,
|
|
677
|
+
contentType: attachment.contentType ?? undefined,
|
|
678
|
+
content: fileBuffer,
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
}
|
|
455
682
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
683
|
+
// Start actually sending in batches of recipients that are not yet sent
|
|
684
|
+
let idPointer = '';
|
|
685
|
+
const batchSize = 100;
|
|
686
|
+
const recipientsSet = new Set<string>();
|
|
687
|
+
let isSavingStatus = false;
|
|
688
|
+
let lastStatusSave = new Date();
|
|
689
|
+
|
|
690
|
+
async function saveStatus() {
|
|
691
|
+
if (!upToDate) {
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
if (isSavingStatus) {
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
if ((new Date().getTime() - lastStatusSave.getTime()) < 1000 * 5) {
|
|
698
|
+
// Save at most every 5 seconds
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
if (succeededCount < upToDate.succeededCount || softFailedCount < upToDate.softFailedCount || failedCount < upToDate.failedCount) {
|
|
702
|
+
// Do not update on retries
|
|
459
703
|
return;
|
|
460
704
|
}
|
|
461
|
-
|
|
705
|
+
|
|
706
|
+
lastStatusSave = new Date();
|
|
707
|
+
isSavingStatus = true;
|
|
708
|
+
upToDate.succeededCount = succeededCount;
|
|
709
|
+
upToDate.softFailedCount = softFailedCount;
|
|
710
|
+
upToDate.failedCount = failedCount;
|
|
462
711
|
|
|
463
712
|
try {
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
713
|
+
await upToDate.save();
|
|
714
|
+
}
|
|
715
|
+
finally {
|
|
716
|
+
isSavingStatus = false;
|
|
717
|
+
}
|
|
718
|
+
}
|
|
467
719
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
720
|
+
while (true) {
|
|
721
|
+
abort.throwIfAborted();
|
|
722
|
+
const data = await SQL.select()
|
|
723
|
+
.from('email_recipients')
|
|
724
|
+
.where('emailId', upToDate.id)
|
|
725
|
+
.where('id', SQLWhereSign.Greater, idPointer)
|
|
726
|
+
.orderBy(SQL.column('id'), 'ASC')
|
|
727
|
+
.limit(batchSize)
|
|
728
|
+
.fetch();
|
|
729
|
+
|
|
730
|
+
const recipients = EmailRecipient.fromRows(data, 'email_recipients');
|
|
731
|
+
|
|
732
|
+
if (recipients.length === 0) {
|
|
733
|
+
break;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
const sendingPromises: Promise<void>[] = [];
|
|
737
|
+
let skipped = 0;
|
|
738
|
+
|
|
739
|
+
for (const recipient of recipients) {
|
|
740
|
+
idPointer = recipient.id;
|
|
741
|
+
|
|
742
|
+
if (recipient.sentAt) {
|
|
743
|
+
// Already sent
|
|
744
|
+
if (recipient.email) {
|
|
745
|
+
recipientsSet.add(recipient.email);
|
|
746
|
+
}
|
|
747
|
+
succeededCount += 1;
|
|
748
|
+
await saveStatus();
|
|
749
|
+
skipped++;
|
|
750
|
+
continue;
|
|
471
751
|
}
|
|
472
|
-
|
|
752
|
+
|
|
753
|
+
if (!recipient.email) {
|
|
754
|
+
skipped++;
|
|
755
|
+
continue;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
if (recipientsSet.has(recipient.id)) {
|
|
759
|
+
console.error('Found duplicate recipient while sending email', recipient.id);
|
|
760
|
+
softFailedCount += 1;
|
|
761
|
+
|
|
473
762
|
recipient.failCount += 1;
|
|
474
|
-
recipient.failErrorMessage =
|
|
763
|
+
recipient.failErrorMessage = 'Duplicate recipient';
|
|
764
|
+
recipient.failError = new SimpleErrors(
|
|
765
|
+
new SimpleError({
|
|
766
|
+
code: 'email_skipped_duplicate_recipient',
|
|
767
|
+
message: 'Duplicate recipient',
|
|
768
|
+
human: $t('Dit e-mailadres staat meerdere keren tussen de ontvangers en werd daarom overgeslagen'),
|
|
769
|
+
}),
|
|
770
|
+
);
|
|
771
|
+
|
|
475
772
|
recipient.firstFailedAt = recipient.firstFailedAt ?? new Date();
|
|
476
773
|
recipient.lastFailedAt = new Date();
|
|
477
774
|
await recipient.save();
|
|
775
|
+
|
|
776
|
+
await saveStatus();
|
|
777
|
+
skipped++;
|
|
778
|
+
continue;
|
|
478
779
|
}
|
|
780
|
+
recipientsSet.add(recipient.email);
|
|
781
|
+
|
|
782
|
+
let promiseResolve: (value: void | PromiseLike<void>) => void;
|
|
783
|
+
const promise = new Promise<void>((resolve) => {
|
|
784
|
+
promiseResolve = resolve;
|
|
785
|
+
});
|
|
786
|
+
|
|
787
|
+
const virtualRecipient = recipient.getRecipient();
|
|
788
|
+
|
|
789
|
+
let resolved = false;
|
|
790
|
+
const callback = async (error: Error | null) => {
|
|
791
|
+
if (resolved) {
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
resolved = true;
|
|
795
|
+
|
|
796
|
+
try {
|
|
797
|
+
if (error === null) {
|
|
798
|
+
// Mark saved
|
|
799
|
+
recipient.sentAt = new Date();
|
|
800
|
+
|
|
801
|
+
// Update repacements that have been generated
|
|
802
|
+
recipient.replacements = virtualRecipient.replacements;
|
|
803
|
+
|
|
804
|
+
succeededCount += 1;
|
|
805
|
+
await recipient.save();
|
|
806
|
+
await saveStatus();
|
|
807
|
+
}
|
|
808
|
+
else {
|
|
809
|
+
recipient.failCount += 1;
|
|
810
|
+
recipient.failErrorMessage = error.message;
|
|
811
|
+
recipient.failError = errorToSimpleErrors(error);
|
|
812
|
+
recipient.firstFailedAt = recipient.firstFailedAt ?? new Date();
|
|
813
|
+
recipient.lastFailedAt = new Date();
|
|
814
|
+
|
|
815
|
+
if (isSoftEmailRecipientError(recipient.failError)) {
|
|
816
|
+
softFailedCount += 1;
|
|
817
|
+
}
|
|
818
|
+
else {
|
|
819
|
+
failedCount += 1;
|
|
820
|
+
}
|
|
821
|
+
await recipient.save();
|
|
822
|
+
await saveStatus();
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
catch (e) {
|
|
826
|
+
console.error(e);
|
|
827
|
+
}
|
|
828
|
+
promiseResolve();
|
|
829
|
+
};
|
|
830
|
+
|
|
831
|
+
// Do send the email
|
|
832
|
+
// Create e-mail builder
|
|
833
|
+
const builder = await getEmailBuilder(organization ?? null, {
|
|
834
|
+
recipients: [
|
|
835
|
+
virtualRecipient,
|
|
836
|
+
],
|
|
837
|
+
from,
|
|
838
|
+
replyTo,
|
|
839
|
+
subject: upToDate.subject!,
|
|
840
|
+
html: upToDate.html!,
|
|
841
|
+
type: upToDate.emailType ? 'transactional' : 'broadcast',
|
|
842
|
+
attachments,
|
|
843
|
+
callback(error: Error | null) {
|
|
844
|
+
callback(error).catch(console.error);
|
|
845
|
+
},
|
|
846
|
+
headers: {
|
|
847
|
+
'X-Email-Id': upToDate.id,
|
|
848
|
+
'X-Email-Recipient-Id': recipient.id,
|
|
849
|
+
},
|
|
850
|
+
});
|
|
851
|
+
abort.throwIfAborted(); // do not schedule if aborted
|
|
852
|
+
EmailClass.schedule(builder);
|
|
853
|
+
sendingPromises.push(promise);
|
|
479
854
|
}
|
|
480
|
-
|
|
481
|
-
|
|
855
|
+
|
|
856
|
+
if (sendingPromises.length > 0 || skipped > 0) {
|
|
857
|
+
await Promise.all(sendingPromises);
|
|
482
858
|
}
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
// Create e-mail builder
|
|
488
|
-
const builder = await getEmailBuilder(organization ?? null, {
|
|
489
|
-
recipients: [
|
|
490
|
-
virtualRecipient,
|
|
491
|
-
],
|
|
492
|
-
from,
|
|
493
|
-
replyTo,
|
|
494
|
-
subject: upToDate.subject!,
|
|
495
|
-
html: upToDate.html!,
|
|
496
|
-
type: upToDate.emailType ? 'transactional' : 'broadcast',
|
|
497
|
-
attachments,
|
|
498
|
-
callback(error: Error | null) {
|
|
499
|
-
callback(error).catch(console.error);
|
|
500
|
-
},
|
|
501
|
-
});
|
|
502
|
-
abort.throwIfAborted(); // do not schedule if aborted
|
|
503
|
-
EmailClass.schedule(builder);
|
|
504
|
-
sendingPromises.push(promise);
|
|
859
|
+
else {
|
|
860
|
+
break;
|
|
861
|
+
}
|
|
862
|
+
}
|
|
505
863
|
}
|
|
864
|
+
catch (e) {
|
|
865
|
+
if (!upToDate) {
|
|
866
|
+
throw e;
|
|
867
|
+
}
|
|
506
868
|
|
|
507
|
-
|
|
508
|
-
|
|
869
|
+
upToDate.succeededCount = succeededCount;
|
|
870
|
+
upToDate.softFailedCount = softFailedCount;
|
|
871
|
+
upToDate.failedCount = failedCount;
|
|
872
|
+
|
|
873
|
+
if (isAbortedError(e) || ((isSimpleError(e) || isSimpleErrors(e)) && e.hasCode('SHUTDOWN'))) {
|
|
874
|
+
// Keep sending status: we'll resume after the reboot
|
|
875
|
+
await upToDate.save();
|
|
876
|
+
throw e;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
upToDate.emailErrors = errorToSimpleErrors(e);
|
|
880
|
+
upToDate.status = EmailStatus.Failed;
|
|
881
|
+
await upToDate.save();
|
|
882
|
+
throw e;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
if (upToDate.emailRecipientsCount === 0 && upToDate.userId === null) {
|
|
886
|
+
// We only delete automated emails (email type) if they have no recipients
|
|
887
|
+
console.log('No recipients found for email ', upToDate.id, ' deleting...');
|
|
888
|
+
await upToDate.delete();
|
|
889
|
+
return null;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
console.log('Finished sending email', upToDate.id);
|
|
893
|
+
// Mark email as sent
|
|
894
|
+
|
|
895
|
+
if ((succeededCount + failedCount + softFailedCount) === 0) {
|
|
896
|
+
upToDate.status = EmailStatus.Failed;
|
|
897
|
+
upToDate.emailErrors = new SimpleErrors(
|
|
898
|
+
new SimpleError({
|
|
899
|
+
code: 'no_recipients',
|
|
900
|
+
message: 'No recipients',
|
|
901
|
+
human: $t(`Geen ontvangers gevonden`),
|
|
902
|
+
}),
|
|
903
|
+
);
|
|
509
904
|
}
|
|
510
905
|
else {
|
|
511
|
-
|
|
906
|
+
upToDate.status = EmailStatus.Sent;
|
|
907
|
+
upToDate.emailErrors = null;
|
|
512
908
|
}
|
|
513
|
-
}
|
|
514
909
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
await upToDate.delete();
|
|
519
|
-
return null;
|
|
520
|
-
}
|
|
910
|
+
upToDate.succeededCount = succeededCount;
|
|
911
|
+
upToDate.softFailedCount = softFailedCount;
|
|
912
|
+
upToDate.failedCount = failedCount;
|
|
521
913
|
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
await upToDate.save();
|
|
526
|
-
return upToDate;
|
|
914
|
+
await upToDate.save();
|
|
915
|
+
return upToDate;
|
|
916
|
+
});
|
|
527
917
|
});
|
|
528
918
|
}
|
|
529
919
|
|
|
530
920
|
updateCount() {
|
|
531
921
|
const id = this.id;
|
|
532
|
-
QueueHandler.
|
|
922
|
+
QueueHandler.abort('email-count-' + this.id);
|
|
923
|
+
QueueHandler.schedule('email-count-' + this.id, async function ({ abort }) {
|
|
533
924
|
let upToDate = await Email.getByID(id);
|
|
534
925
|
|
|
535
926
|
if (!upToDate || upToDate.sentAt || !upToDate.id || upToDate.status !== EmailStatus.Draft) {
|
|
@@ -558,12 +949,14 @@ export class Email extends QueryableModel {
|
|
|
558
949
|
search: subfilter.search,
|
|
559
950
|
});
|
|
560
951
|
|
|
952
|
+
abort.throwIfAborted();
|
|
561
953
|
const c = await loader.count(request, subfilter.subfilter);
|
|
562
954
|
|
|
563
955
|
count += c;
|
|
564
956
|
}
|
|
957
|
+
abort.throwIfAborted();
|
|
565
958
|
|
|
566
|
-
// Check if we have a more reliable
|
|
959
|
+
// Check if we have a more reliable emailRecipientsCount in the meantime
|
|
567
960
|
upToDate = await Email.getByID(id);
|
|
568
961
|
|
|
569
962
|
if (!upToDate) {
|
|
@@ -572,12 +965,27 @@ export class Email extends QueryableModel {
|
|
|
572
965
|
if (upToDate.recipientsStatus === EmailRecipientsStatus.Created) {
|
|
573
966
|
return;
|
|
574
967
|
}
|
|
575
|
-
upToDate.
|
|
968
|
+
upToDate.emailRecipientsCount = count;
|
|
576
969
|
await upToDate.save();
|
|
577
970
|
}
|
|
578
971
|
catch (e) {
|
|
972
|
+
if (isAbortedError(e)) {
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
579
975
|
console.error('Failed to update count for email', id);
|
|
580
976
|
console.error(e);
|
|
977
|
+
|
|
978
|
+
// Check if we have a more reliable emailRecipientsCount in the meantime
|
|
979
|
+
upToDate = await Email.getByID(id);
|
|
980
|
+
|
|
981
|
+
if (!upToDate) {
|
|
982
|
+
return;
|
|
983
|
+
}
|
|
984
|
+
if (upToDate.recipientsStatus === EmailRecipientsStatus.Created) {
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
upToDate.recipientsErrors = errorToSimpleErrors(e);
|
|
988
|
+
await upToDate.save();
|
|
581
989
|
}
|
|
582
990
|
}).catch(console.error);
|
|
583
991
|
}
|
|
@@ -606,7 +1014,10 @@ export class Email extends QueryableModel {
|
|
|
606
1014
|
upToDate.recipientsStatus = EmailRecipientsStatus.Creating;
|
|
607
1015
|
await upToDate.save();
|
|
608
1016
|
|
|
1017
|
+
const membersSet = new Set<string>();
|
|
1018
|
+
|
|
609
1019
|
let count = 0;
|
|
1020
|
+
let countWithoutEmail = 0;
|
|
610
1021
|
|
|
611
1022
|
try {
|
|
612
1023
|
// Delete all recipients
|
|
@@ -639,10 +1050,10 @@ export class Email extends QueryableModel {
|
|
|
639
1050
|
const response = await loader.fetch(request, subfilter.subfilter);
|
|
640
1051
|
|
|
641
1052
|
for (const item of response.results) {
|
|
642
|
-
if (!item.email) {
|
|
1053
|
+
if (!item.email && !item.memberId && !item.userId) {
|
|
643
1054
|
continue;
|
|
644
1055
|
}
|
|
645
|
-
|
|
1056
|
+
|
|
646
1057
|
const recipient = new EmailRecipient();
|
|
647
1058
|
recipient.emailType = upToDate.emailType;
|
|
648
1059
|
recipient.objectId = item.objectId;
|
|
@@ -651,8 +1062,22 @@ export class Email extends QueryableModel {
|
|
|
651
1062
|
recipient.firstName = item.firstName;
|
|
652
1063
|
recipient.lastName = item.lastName;
|
|
653
1064
|
recipient.replacements = item.replacements;
|
|
1065
|
+
recipient.memberId = item.memberId ?? null;
|
|
1066
|
+
recipient.userId = item.userId ?? null;
|
|
1067
|
+
recipient.organizationId = upToDate.organizationId ?? null;
|
|
654
1068
|
|
|
655
1069
|
await recipient.save();
|
|
1070
|
+
|
|
1071
|
+
if (recipient.memberId) {
|
|
1072
|
+
membersSet.add(recipient.memberId);
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
if (!recipient.email) {
|
|
1076
|
+
countWithoutEmail += 1;
|
|
1077
|
+
}
|
|
1078
|
+
else {
|
|
1079
|
+
count += 1;
|
|
1080
|
+
}
|
|
656
1081
|
}
|
|
657
1082
|
|
|
658
1083
|
request = response.next ?? null;
|
|
@@ -660,13 +1085,17 @@ export class Email extends QueryableModel {
|
|
|
660
1085
|
}
|
|
661
1086
|
|
|
662
1087
|
upToDate.recipientsStatus = EmailRecipientsStatus.Created;
|
|
663
|
-
upToDate.
|
|
1088
|
+
upToDate.emailRecipientsCount = count;
|
|
1089
|
+
upToDate.otherRecipientsCount = countWithoutEmail;
|
|
1090
|
+
upToDate.recipientsErrors = null;
|
|
1091
|
+
upToDate.membersCount = membersSet.size;
|
|
664
1092
|
await upToDate.save();
|
|
665
1093
|
}
|
|
666
|
-
catch (e) {
|
|
1094
|
+
catch (e: unknown) {
|
|
667
1095
|
console.error('Failed to build recipients for email', id);
|
|
668
1096
|
console.error(e);
|
|
669
1097
|
upToDate.recipientsStatus = EmailRecipientsStatus.NotCreated;
|
|
1098
|
+
upToDate.recipientsErrors = errorToSimpleErrors(e);
|
|
670
1099
|
await upToDate.save();
|
|
671
1100
|
}
|
|
672
1101
|
});
|