@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.
- package/dist/src/factories/GroupFactory.d.ts +0 -3
- package/dist/src/factories/GroupFactory.d.ts.map +1 -1
- package/dist/src/factories/GroupFactory.js +0 -20
- package/dist/src/factories/GroupFactory.js.map +1 -1
- package/dist/src/factories/MemberFactory.js +2 -2
- package/dist/src/factories/MemberFactory.js.map +1 -1
- package/dist/src/factories/UserFactory.d.ts.map +1 -1
- package/dist/src/factories/UserFactory.js +7 -4
- package/dist/src/factories/UserFactory.js.map +1 -1
- package/dist/src/helpers/EmailBuilder.d.ts +2 -1
- package/dist/src/helpers/EmailBuilder.d.ts.map +1 -1
- package/dist/src/helpers/EmailBuilder.js +36 -4
- package/dist/src/helpers/EmailBuilder.js.map +1 -1
- package/dist/src/models/Email.d.ts.map +1 -1
- package/dist/src/models/Email.js +39 -38
- package/dist/src/models/Email.js.map +1 -1
- package/dist/src/models/Email.test.d.ts +2 -0
- package/dist/src/models/Email.test.d.ts.map +1 -0
- package/dist/src/models/Email.test.js +461 -0
- package/dist/src/models/Email.test.js.map +1 -0
- package/dist/src/models/Organization.d.ts +0 -7
- package/dist/src/models/Organization.d.ts.map +1 -1
- package/dist/src/models/Organization.js +0 -11
- package/dist/src/models/Organization.js.map +1 -1
- package/dist/tests/jest.global.setup.d.ts.map +1 -1
- package/dist/tests/jest.global.setup.js +11 -1
- package/dist/tests/jest.global.setup.js.map +1 -1
- package/dist/tests/jest.setup.js +14 -0
- package/dist/tests/jest.setup.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -3
- package/src/factories/GroupFactory.ts +0 -21
- package/src/factories/MemberFactory.ts +2 -2
- package/src/factories/UserFactory.ts +7 -4
- package/src/helpers/EmailBuilder.ts +43 -6
- package/src/models/Email.test.ts +533 -0
- package/src/models/Email.ts +42 -42
- 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
|
+
});
|
package/src/models/Email.ts
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
290
|
-
|
|
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
|
|
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 (
|
|
399
|
-
|
|
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
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
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
|
-
|
|
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
|
-
})
|
|
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
|
-
})
|
|
656
|
+
});
|
|
657
657
|
}
|
|
658
658
|
|
|
659
659
|
getStructure() {
|