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