@stamhoofd/models 2.78.2 → 2.78.3

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.
Files changed (38) hide show
  1. package/dist/src/factories/GroupFactory.d.ts +0 -3
  2. package/dist/src/factories/GroupFactory.d.ts.map +1 -1
  3. package/dist/src/factories/GroupFactory.js +0 -20
  4. package/dist/src/factories/GroupFactory.js.map +1 -1
  5. package/dist/src/factories/MemberFactory.js +2 -2
  6. package/dist/src/factories/MemberFactory.js.map +1 -1
  7. package/dist/src/factories/UserFactory.d.ts.map +1 -1
  8. package/dist/src/factories/UserFactory.js +7 -4
  9. package/dist/src/factories/UserFactory.js.map +1 -1
  10. package/dist/src/helpers/EmailBuilder.d.ts +2 -1
  11. package/dist/src/helpers/EmailBuilder.d.ts.map +1 -1
  12. package/dist/src/helpers/EmailBuilder.js +36 -4
  13. package/dist/src/helpers/EmailBuilder.js.map +1 -1
  14. package/dist/src/models/Email.d.ts.map +1 -1
  15. package/dist/src/models/Email.js +39 -38
  16. package/dist/src/models/Email.js.map +1 -1
  17. package/dist/src/models/Email.test.d.ts +2 -0
  18. package/dist/src/models/Email.test.d.ts.map +1 -0
  19. package/dist/src/models/Email.test.js +461 -0
  20. package/dist/src/models/Email.test.js.map +1 -0
  21. package/dist/src/models/Organization.d.ts +0 -7
  22. package/dist/src/models/Organization.d.ts.map +1 -1
  23. package/dist/src/models/Organization.js +0 -11
  24. package/dist/src/models/Organization.js.map +1 -1
  25. package/dist/tests/jest.global.setup.d.ts.map +1 -1
  26. package/dist/tests/jest.global.setup.js +11 -1
  27. package/dist/tests/jest.global.setup.js.map +1 -1
  28. package/dist/tests/jest.setup.js +14 -0
  29. package/dist/tests/jest.setup.js.map +1 -1
  30. package/dist/tsconfig.tsbuildinfo +1 -1
  31. package/package.json +4 -3
  32. package/src/factories/GroupFactory.ts +0 -21
  33. package/src/factories/MemberFactory.ts +2 -2
  34. package/src/factories/UserFactory.ts +7 -4
  35. package/src/helpers/EmailBuilder.ts +43 -6
  36. package/src/models/Email.test.ts +533 -0
  37. package/src/models/Email.ts +42 -42
  38. package/src/models/Organization.ts +0 -14
@@ -0,0 +1,533 @@
1
+ import { EmailRecipientFilter, EmailRecipientFilterType, EmailRecipientsStatus, EmailRecipient as EmailRecipientStruct, EmailRecipientSubfilter, EmailStatus, LimitedFilteredRequest, PaginatedResponse } from '@stamhoofd/structures';
2
+ import { Email } from './Email';
3
+ import { EmailRecipient } from './EmailRecipient';
4
+ import { EmailMocker } from '@stamhoofd/email';
5
+ import { TestUtils } from '@stamhoofd/test-utils';
6
+ import { OrganizationFactory } from '../factories/OrganizationFactory';
7
+ import { Platform } from './Platform';
8
+
9
+ async function buildEmail(data: {
10
+ recipients: EmailRecipientStruct[];
11
+ } & Partial<Email>) {
12
+ Email.recipientLoaders.set(EmailRecipientFilterType.Members, {
13
+ fetch: async (query: LimitedFilteredRequest) => {
14
+ return Promise.resolve(
15
+ new PaginatedResponse<EmailRecipientStruct[], LimitedFilteredRequest>({
16
+ results: data.recipients,
17
+ next: undefined,
18
+ }),
19
+ );
20
+ },
21
+
22
+ count: async (query: LimitedFilteredRequest) => {
23
+ return data.recipients.length;
24
+ },
25
+ });
26
+
27
+ const model = new Email();
28
+ model.userId = null;
29
+ model.organizationId = data.organizationId ?? null;
30
+ model.recipientFilter = EmailRecipientFilter.create({
31
+ filters: [
32
+ EmailRecipientSubfilter.create({
33
+ type: EmailRecipientFilterType.Members,
34
+ filter: { id: { $in: ['test'] } },
35
+ }),
36
+ ],
37
+ });
38
+
39
+ model.subject = data.subject ?? 'This is a test email';
40
+ model.html = data.html ?? '<p>This is a test email</p>';
41
+ model.text = data.text ?? 'This is a test email';
42
+ model.json = data.json ?? {};
43
+ model.status = data.status ?? EmailStatus.Draft;
44
+ model.attachments = [];
45
+ model.fromAddress = data.fromAddress ?? 'test@stamhoofd.be';
46
+ model.fromName = data.fromName ?? 'Stamhoofd Test Suite';
47
+
48
+ await model.save();
49
+
50
+ return model;
51
+ }
52
+
53
+ describe('Model.Email', () => {
54
+ it('Correctly whitelists email recipients', async () => {
55
+ TestUtils.setEnvironment('WHITELISTED_EMAIL_DESTINATIONS', ['*@stamhoofd-allowed-domain-tests.be']);
56
+ const model = await buildEmail({
57
+ recipients: [
58
+ EmailRecipientStruct.create({
59
+ firstName: 'Test',
60
+ lastName: 'Test',
61
+ email: 'example@not-whitelisted-domain.be',
62
+ }),
63
+ EmailRecipientStruct.create({
64
+ firstName: 'Test',
65
+ lastName: 'Test',
66
+ email: 'example2@stamhoofd-allowed-domain-tests.be',
67
+ }),
68
+ ],
69
+ });
70
+ await model.send();
71
+ await model.refresh();
72
+
73
+ // Check if it was sent correctly
74
+ expect(model.recipientsStatus).toBe(EmailRecipientsStatus.Created);
75
+ expect(model.recipientCount).toBe(2);
76
+ expect(model.status).toBe(EmailStatus.Sent);
77
+ expect(EmailMocker.broadcast.getSucceededCount()).toBe(1);
78
+ expect(EmailMocker.broadcast.getFailedCount()).toBe(0); // never tried to send any failed emails (whitelist)
79
+
80
+ // Load recipietns
81
+ const recipients = await EmailRecipient.select().where('emailId', model.id).fetch();
82
+ expect(recipients).toIncludeSameMembers([
83
+ expect.objectContaining({
84
+ email: 'example2@stamhoofd-allowed-domain-tests.be',
85
+ sentAt: expect.any(Date),
86
+ failCount: 0,
87
+ failErrorMessage: null,
88
+ firstFailedAt: null,
89
+ lastFailedAt: null,
90
+ }),
91
+ expect.objectContaining({
92
+ email: 'example@not-whitelisted-domain.be',
93
+ sentAt: null,
94
+ failCount: 1,
95
+ failErrorMessage: 'All recipients are filtered due to environment',
96
+ firstFailedAt: expect.any(Date),
97
+ lastFailedAt: expect.any(Date),
98
+ }),
99
+ ]);
100
+ }, 15_000);
101
+
102
+ it('Retries emails on network errors', async () => {
103
+ const model = await buildEmail({
104
+ recipients: [
105
+ EmailRecipientStruct.create({
106
+ firstName: 'Test',
107
+ lastName: 'Test',
108
+ email: 'example@domain.be',
109
+ }),
110
+ EmailRecipientStruct.create({
111
+ firstName: 'Test',
112
+ lastName: 'Test',
113
+ email: 'example2@domain.be',
114
+ }),
115
+ ],
116
+ });
117
+
118
+ // Only one failure (the first email)
119
+ // It should automatically retry to send the email
120
+ EmailMocker.broadcast.failNext(new Error('This is a simulated network error'));
121
+
122
+ await model.send();
123
+ await model.refresh();
124
+
125
+ // Check if it was sent correctly
126
+ expect(model.recipientsStatus).toBe(EmailRecipientsStatus.Created);
127
+ expect(model.recipientCount).toBe(2);
128
+ expect(model.status).toBe(EmailStatus.Sent);
129
+
130
+ // Both have succeeded
131
+ expect(EmailMocker.broadcast.getSucceededCount()).toBe(2);
132
+ expect(EmailMocker.broadcast.getFailedCount()).toBe(1); // One retry
133
+
134
+ // Load recipietns
135
+ const recipients = await EmailRecipient.select().where('emailId', model.id).fetch();
136
+ expect(recipients).toIncludeSameMembers([
137
+ expect.objectContaining({
138
+ email: 'example@domain.be',
139
+ sentAt: expect.any(Date),
140
+ failCount: 0,
141
+ }),
142
+ expect.objectContaining({
143
+ email: 'example2@domain.be',
144
+ sentAt: expect.any(Date),
145
+ failCount: 0,
146
+ }),
147
+ ]);
148
+ }, 15_000);
149
+
150
+ it('Marks email recipient as failed if fails three times', async () => {
151
+ const model = await buildEmail({
152
+ recipients: [
153
+ EmailRecipientStruct.create({
154
+ firstName: 'Test',
155
+ lastName: 'Test',
156
+ email: 'example@domain.be',
157
+ }),
158
+ EmailRecipientStruct.create({
159
+ firstName: 'Test',
160
+ lastName: 'Test',
161
+ email: 'example2@domain.be',
162
+ }),
163
+ ],
164
+ });
165
+
166
+ // Only one failure (the first email)
167
+ // It should automatically retry to send the email
168
+ EmailMocker.broadcast.failNext(new Error('This is a simulated network error 1'));
169
+ EmailMocker.broadcast.failNext(new Error('This is a simulated network error 2'));
170
+ EmailMocker.broadcast.failNext(new Error('This is a simulated network error 3'));
171
+ EmailMocker.broadcast.failNext(new Error('This is a simulated network error 4'));
172
+ EmailMocker.broadcast.failNext(new Error('This is a simulated network error 5'));
173
+ EmailMocker.broadcast.failNext(new Error('This is a simulated network error 6'));
174
+
175
+ await model.send();
176
+ await model.refresh();
177
+
178
+ // Check if it was sent correctly
179
+ expect(model.recipientsStatus).toBe(EmailRecipientsStatus.Created);
180
+ expect(model.recipientCount).toBe(2);
181
+ expect(model.status).toBe(EmailStatus.Sent);
182
+
183
+ // Both have succeeded
184
+ expect(EmailMocker.broadcast.getSucceededCount()).toBe(0);
185
+ expect(EmailMocker.broadcast.getFailedCount()).toBe(6); // Two retries for each recipient
186
+
187
+ // Load recipietns
188
+ const recipients = await EmailRecipient.select().where('emailId', model.id).fetch();
189
+ expect(recipients).toIncludeSameMembers([
190
+ expect.objectContaining({
191
+ email: 'example@domain.be',
192
+ sentAt: null,
193
+ failCount: 1,
194
+ firstFailedAt: expect.any(Date),
195
+ lastFailedAt: expect.any(Date),
196
+ }),
197
+ expect.objectContaining({
198
+ email: 'example2@domain.be',
199
+ sentAt: null,
200
+ failCount: 1,
201
+ firstFailedAt: expect.any(Date),
202
+ lastFailedAt: expect.any(Date),
203
+ }),
204
+ ]);
205
+ }, 15_000);
206
+
207
+ it('Can handle recipients without name', async () => {
208
+ const model = await buildEmail({
209
+ recipients: [
210
+ EmailRecipientStruct.create({
211
+ email: 'example@domain.be',
212
+ }),
213
+ ],
214
+ });
215
+
216
+ await model.send();
217
+ await model.refresh();
218
+
219
+ // Check if it was sent correctly
220
+ expect(model.recipientsStatus).toBe(EmailRecipientsStatus.Created);
221
+ expect(model.recipientCount).toBe(1);
222
+ expect(model.status).toBe(EmailStatus.Sent);
223
+
224
+ // Both have succeeded
225
+ expect(EmailMocker.broadcast.getSucceededCount()).toBe(1);
226
+ expect(EmailMocker.broadcast.getFailedCount()).toBe(0);
227
+
228
+ // Load recipietns
229
+ const recipients = await EmailRecipient.select().where('emailId', model.id).fetch();
230
+ expect(recipients).toIncludeSameMembers([
231
+ expect.objectContaining({
232
+ email: 'example@domain.be',
233
+ sentAt: expect.any(Date),
234
+ failCount: 0,
235
+ }),
236
+ ]);
237
+
238
+ // Check to header
239
+ expect(EmailMocker.broadcast.getSucceededEmail(0).to).toEqual('example@domain.be');
240
+ }, 15_000);
241
+
242
+ it('Includes recipient names in mail header', async () => {
243
+ const model = await buildEmail({
244
+ recipients: [
245
+ EmailRecipientStruct.create({
246
+ firstName: 'John',
247
+ lastName: 'Von Doe',
248
+ email: 'example@domain.be',
249
+ }),
250
+ ],
251
+ });
252
+
253
+ await model.send();
254
+ await model.refresh();
255
+
256
+ // Check if it was sent correctly
257
+ expect(model.recipientsStatus).toBe(EmailRecipientsStatus.Created);
258
+ expect(model.recipientCount).toBe(1);
259
+ expect(model.status).toBe(EmailStatus.Sent);
260
+
261
+ // Both have succeeded
262
+ expect(EmailMocker.broadcast.getSucceededCount()).toBe(1);
263
+ expect(EmailMocker.broadcast.getFailedCount()).toBe(0);
264
+
265
+ // Load recipietns
266
+ const recipients = await EmailRecipient.select().where('emailId', model.id).fetch();
267
+ expect(recipients).toIncludeSameMembers([
268
+ expect.objectContaining({
269
+ firstName: 'John',
270
+ lastName: 'Von Doe',
271
+ email: 'example@domain.be',
272
+ sentAt: expect.any(Date),
273
+ failCount: 0,
274
+ }),
275
+ ]);
276
+
277
+ // Check to header
278
+ expect(EmailMocker.broadcast.getSucceededEmail(0).to).toEqual('"John Von Doe" <example@domain.be>');
279
+ }, 15_000);
280
+
281
+ it('Skips invalid email addresses', async () => {
282
+ const model = await buildEmail({
283
+ recipients: [
284
+ EmailRecipientStruct.create({
285
+ firstName: 'John',
286
+ lastName: 'Von Doe',
287
+ email: 'invalid',
288
+ }),
289
+ ],
290
+ });
291
+
292
+ await model.send();
293
+ await model.refresh();
294
+
295
+ // Check if it was sent correctly
296
+ expect(model.recipientsStatus).toBe(EmailRecipientsStatus.Created);
297
+ expect(model.recipientCount).toBe(1);
298
+ expect(model.status).toBe(EmailStatus.Sent);
299
+
300
+ // Both have succeeded
301
+ expect(EmailMocker.broadcast.getSucceededCount()).toBe(0);
302
+ expect(EmailMocker.broadcast.getFailedCount()).toBe(0);
303
+
304
+ // Load recipietns
305
+ const recipients = await EmailRecipient.select().where('emailId', model.id).fetch();
306
+ expect(recipients).toIncludeSameMembers([
307
+ expect.objectContaining({
308
+ firstName: 'John',
309
+ lastName: 'Von Doe',
310
+ email: 'invalid',
311
+ failCount: 1,
312
+ failErrorMessage: 'Invalid email address',
313
+ firstFailedAt: expect.any(Date),
314
+ lastFailedAt: expect.any(Date),
315
+ }),
316
+ ]);
317
+ }, 15_000);
318
+
319
+ it('Skips empty email addresses while creating recipients', async () => {
320
+ const model = await buildEmail({
321
+ recipients: [
322
+ EmailRecipientStruct.create({
323
+ firstName: '',
324
+ lastName: '',
325
+ email: '',
326
+ }),
327
+ EmailRecipientStruct.create({
328
+ firstName: '',
329
+ lastName: '',
330
+ email: 'valid@example.com',
331
+ }),
332
+ ],
333
+ });
334
+
335
+ await model.send();
336
+ await model.refresh();
337
+
338
+ // Check if it was sent correctly
339
+ expect(model.recipientsStatus).toBe(EmailRecipientsStatus.Created);
340
+ expect(model.recipientCount).toBe(1);
341
+ expect(model.status).toBe(EmailStatus.Sent);
342
+
343
+ // Both have succeeded
344
+ expect(EmailMocker.broadcast.getSucceededCount()).toBe(1);
345
+ expect(EmailMocker.broadcast.getFailedCount()).toBe(0);
346
+
347
+ // Load recipietns
348
+ const recipients = await EmailRecipient.select().where('emailId', model.id).fetch();
349
+ expect(recipients).toIncludeSameMembers([
350
+ expect.objectContaining({
351
+ email: 'valid@example.com',
352
+ sentAt: expect.any(Date),
353
+ }),
354
+ ]);
355
+ }, 15_000);
356
+
357
+ it('Platform can send from default domain', async () => {
358
+ TestUtils.setEnvironment('domains', {
359
+ ...STAMHOOFD.domains,
360
+ defaultTransactionalEmail: {
361
+ '': 'my-platform.com',
362
+ },
363
+ defaultBroadcastEmail: {
364
+ '': 'broadcast.my-platform.com',
365
+ },
366
+ });
367
+
368
+ const model = await buildEmail({
369
+ recipients: [
370
+ EmailRecipientStruct.create({
371
+ email: 'example@domain.be',
372
+ }),
373
+ ],
374
+ fromAddress: 'info@my-platform.com',
375
+ fromName: 'My Platform',
376
+ });
377
+
378
+ await model.send();
379
+ await model.refresh();
380
+
381
+ // Check if it was sent correctly
382
+ expect(model.recipientsStatus).toBe(EmailRecipientsStatus.Created);
383
+ expect(model.recipientCount).toBe(1);
384
+ expect(model.status).toBe(EmailStatus.Sent);
385
+
386
+ // Both have succeeded
387
+ expect(EmailMocker.broadcast.getSucceededCount()).toBe(1);
388
+ expect(EmailMocker.broadcast.getFailedCount()).toBe(0);
389
+
390
+ // Check to header
391
+ expect(EmailMocker.broadcast.getSucceededEmail(0)).toMatchObject({
392
+ to: 'example@domain.be',
393
+ from: '"My Platform" <info@my-platform.com>',
394
+ replyTo: undefined,
395
+ });
396
+ }, 15_000);
397
+
398
+ it('Platform cannot send from unsupported domain', async () => {
399
+ TestUtils.setEnvironment('domains', {
400
+ ...STAMHOOFD.domains,
401
+ defaultTransactionalEmail: {
402
+ '': 'my-platform.com',
403
+ },
404
+ defaultBroadcastEmail: {
405
+ '': 'broadcast.my-platform.com',
406
+ },
407
+ });
408
+
409
+ const model = await buildEmail({
410
+ recipients: [
411
+ EmailRecipientStruct.create({
412
+ email: 'example@domain.be',
413
+ }),
414
+ ],
415
+ fromAddress: 'info@other-platform.com',
416
+ fromName: 'My Platform',
417
+ });
418
+
419
+ await model.send();
420
+ await model.refresh();
421
+
422
+ // Check if it was sent correctly
423
+ expect(model.recipientsStatus).toBe(EmailRecipientsStatus.Created);
424
+ expect(model.recipientCount).toBe(1);
425
+ expect(model.status).toBe(EmailStatus.Sent);
426
+
427
+ // Both have succeeded
428
+ expect(EmailMocker.broadcast.getSucceededCount()).toBe(1);
429
+ expect(EmailMocker.broadcast.getFailedCount()).toBe(0);
430
+
431
+ // Check to header
432
+ expect(EmailMocker.broadcast.getSucceededEmail(0)).toMatchObject({
433
+ to: 'example@domain.be',
434
+ from: '"My Platform" <noreply@broadcast.my-platform.com>', // domain has changed here
435
+ replyTo: '"My Platform" <info@other-platform.com>', // Reply to should be set
436
+ });
437
+ }, 15_000);
438
+
439
+ it('Organization cannot send from platform domain', async () => {
440
+ TestUtils.setEnvironment('domains', {
441
+ ...STAMHOOFD.domains,
442
+ defaultTransactionalEmail: {
443
+ '': 'my-platform.com',
444
+ },
445
+ defaultBroadcastEmail: {
446
+ '': 'broadcast.my-platform.com',
447
+ },
448
+ });
449
+
450
+ const organization = await new OrganizationFactory({
451
+ uri: 'uritest',
452
+ }).create();
453
+
454
+ const model = await buildEmail({
455
+ organizationId: organization.id,
456
+ recipients: [
457
+ EmailRecipientStruct.create({
458
+ email: 'example@domain.be',
459
+ }),
460
+ ],
461
+ fromAddress: 'info@my-platform.com',
462
+ fromName: 'My Platform',
463
+ });
464
+
465
+ await model.send();
466
+ await model.refresh();
467
+
468
+ // Check if it was sent correctly
469
+ expect(model.recipientsStatus).toBe(EmailRecipientsStatus.Created);
470
+ expect(model.recipientCount).toBe(1);
471
+ expect(model.status).toBe(EmailStatus.Sent);
472
+
473
+ // Both have succeeded
474
+ expect(EmailMocker.broadcast.getSucceededCount()).toBe(1);
475
+ expect(EmailMocker.broadcast.getFailedCount()).toBe(0);
476
+
477
+ // Check to header
478
+ expect(EmailMocker.broadcast.getSucceededEmail(0)).toMatchObject({
479
+ to: 'example@domain.be',
480
+ from: '"My Platform" <noreply-uritest@broadcast.my-platform.com>', // domain has changed here
481
+ replyTo: '"My Platform" <info@my-platform.com>', // Reply to should be set
482
+ });
483
+ }, 15_000);
484
+
485
+ it('Organization can send from platform domain if default membership organization', async () => {
486
+ TestUtils.setEnvironment('domains', {
487
+ ...STAMHOOFD.domains,
488
+ defaultTransactionalEmail: {
489
+ '': 'my-platform.com',
490
+ },
491
+ defaultBroadcastEmail: {
492
+ '': 'broadcast.my-platform.com',
493
+ },
494
+ });
495
+
496
+ const organization = await new OrganizationFactory({
497
+ }).create();
498
+
499
+ const platform = await Platform.getShared();
500
+ platform.membershipOrganizationId = organization.id;
501
+ await platform.save();
502
+
503
+ const model = await buildEmail({
504
+ organizationId: organization.id,
505
+ recipients: [
506
+ EmailRecipientStruct.create({
507
+ email: 'example@domain.be',
508
+ }),
509
+ ],
510
+ fromAddress: 'info@my-platform.com',
511
+ fromName: 'My Platform',
512
+ });
513
+
514
+ await model.send();
515
+ await model.refresh();
516
+
517
+ // Check if it was sent correctly
518
+ expect(model.recipientsStatus).toBe(EmailRecipientsStatus.Created);
519
+ expect(model.recipientCount).toBe(1);
520
+ expect(model.status).toBe(EmailStatus.Sent);
521
+
522
+ // Both have succeeded
523
+ expect(EmailMocker.broadcast.getSucceededCount()).toBe(1);
524
+ expect(EmailMocker.broadcast.getFailedCount()).toBe(0);
525
+
526
+ // Check to header
527
+ expect(EmailMocker.broadcast.getSucceededEmail(0)).toMatchObject({
528
+ to: 'example@domain.be',
529
+ from: '"My Platform" <info@my-platform.com>', // domain has changed here
530
+ replyTo: undefined,
531
+ });
532
+ }, 15_000);
533
+ });
@@ -9,7 +9,7 @@ import { Email as EmailClass } from '@stamhoofd/email';
9
9
  import { QueueHandler } from '@stamhoofd/queues';
10
10
  import { QueryableModel, SQL, SQLWhereSign } from '@stamhoofd/sql';
11
11
  import { Formatter } from '@stamhoofd/utility';
12
- import { fillRecipientReplacements, getEmailBuilder } from '../helpers/EmailBuilder';
12
+ import { canSendFromEmail, fillRecipientReplacements, getEmailBuilder } from '../helpers/EmailBuilder';
13
13
  import { EmailRecipient } from './EmailRecipient';
14
14
  import { Organization } from './Organization';
15
15
  import { EmailTemplate } from './EmailTemplate';
@@ -194,7 +194,7 @@ export class Email extends QueryableModel {
194
194
  }
195
195
 
196
196
  getDefaultFromAddress(organization?: Organization | null): string {
197
- const i18n = new I18n('nl', 'BE');
197
+ const i18n = new I18n($getLanguage(), $getCountry());
198
198
  let address = 'noreply@' + i18n.localizedDomains.defaultBroadcastEmail();
199
199
 
200
200
  if (organization) {
@@ -285,27 +285,9 @@ export class Email extends QueryableModel {
285
285
  }
286
286
 
287
287
  // Can we send from this e-mail or reply-to?
288
- if (organization) {
289
- if (organization.privateMeta.mailDomain && organization.privateMeta.mailDomainActive && upToDate.fromAddress!.endsWith('@' + organization.privateMeta.mailDomain)) {
290
- from = upToDate.getFromAddress();
291
- replyTo = null;
292
- }
293
- }
294
- else {
295
- // Platform
296
- // Is the platform allowed to send from the provided email address?
297
- const domains = [
298
- ...Object.values(STAMHOOFD.domains.defaultTransactionalEmail ?? {}),
299
- ...Object.values(STAMHOOFD.domains.defaultBroadcastEmail ?? {}),
300
- ];
301
-
302
- for (const domain of domains) {
303
- if (upToDate.fromAddress!.endsWith('@' + domain)) {
304
- from = upToDate.getFromAddress();
305
- replyTo = null;
306
- break;
307
- }
308
- }
288
+ if (upToDate.fromAddress && await canSendFromEmail(upToDate.fromAddress, organization ?? null)) {
289
+ from = upToDate.getFromAddress();
290
+ replyTo = null;
309
291
  }
310
292
 
311
293
  upToDate.status = EmailStatus.Sending;
@@ -371,7 +353,7 @@ export class Email extends QueryableModel {
371
353
 
372
354
  const recipients = EmailRecipient.fromRows(data, 'email_recipients');
373
355
 
374
- if (recipients.length == 0) {
356
+ if (recipients.length === 0) {
375
357
  break;
376
358
  }
377
359
 
@@ -394,21 +376,32 @@ export class Email extends QueryableModel {
394
376
 
395
377
  const virtualRecipient = recipient.getRecipient();
396
378
 
379
+ let resolved = false;
397
380
  const callback = async (error: Error | null) => {
398
- if (error === null) {
399
- // Mark saved
400
- recipient.sentAt = new Date();
401
-
402
- // Update repacements that have been generated
403
- recipient.replacements = virtualRecipient.replacements;
404
- await recipient.save();
381
+ if (resolved) {
382
+ return;
405
383
  }
406
- else {
407
- recipient.failCount += 1;
408
- recipient.failErrorMessage = error.message;
409
- recipient.firstFailedAt = recipient.firstFailedAt ?? new Date();
410
- recipient.lastFailedAt = new Date();
411
- await recipient.save();
384
+ resolved = true;
385
+
386
+ try {
387
+ if (error === null) {
388
+ // Mark saved
389
+ recipient.sentAt = new Date();
390
+
391
+ // Update repacements that have been generated
392
+ recipient.replacements = virtualRecipient.replacements;
393
+ await recipient.save();
394
+ }
395
+ else {
396
+ recipient.failCount += 1;
397
+ recipient.failErrorMessage = error.message;
398
+ recipient.firstFailedAt = recipient.firstFailedAt ?? new Date();
399
+ recipient.lastFailedAt = new Date();
400
+ await recipient.save();
401
+ }
402
+ }
403
+ catch (e) {
404
+ console.error(e);
412
405
  }
413
406
  promiseResolve();
414
407
  };
@@ -433,7 +426,12 @@ export class Email extends QueryableModel {
433
426
  EmailClass.schedule(builder);
434
427
  }
435
428
 
436
- await Promise.all(sendingPromises);
429
+ if (sendingPromises.length > 0) {
430
+ await Promise.all(sendingPromises);
431
+ }
432
+ else {
433
+ break;
434
+ }
437
435
  }
438
436
 
439
437
  if (upToDate.recipientCount === 0 && upToDate.userId === null) {
@@ -557,9 +555,11 @@ export class Email extends QueryableModel {
557
555
  while (request) {
558
556
  const response = await loader.fetch(request, subfilter.subfilter);
559
557
 
560
- count += response.results.length;
561
-
562
558
  for (const item of response.results) {
559
+ if (!item.email) {
560
+ continue;
561
+ }
562
+ count += 1;
563
563
  const recipient = new EmailRecipient();
564
564
  recipient.emailType = upToDate.emailType;
565
565
  recipient.objectId = item.objectId;
@@ -586,7 +586,7 @@ export class Email extends QueryableModel {
586
586
  upToDate.recipientsStatus = EmailRecipientsStatus.NotCreated;
587
587
  await upToDate.save();
588
588
  }
589
- }).catch(console.error);
589
+ });
590
590
  }
591
591
 
592
592
  async buildExampleRecipient() {
@@ -653,7 +653,7 @@ export class Email extends QueryableModel {
653
653
  console.error('Failed to build example recipient for email', id);
654
654
  console.error(e);
655
655
  }
656
- }).catch(console.error);
656
+ });
657
657
  }
658
658
 
659
659
  getStructure() {