@bernierllc/email-service 4.0.1 → 4.0.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/email-service.js +74 -34
- package/dist/types.d.ts +1 -1
- package/package.json +15 -15
package/dist/email-service.js
CHANGED
|
@@ -178,19 +178,26 @@ export class EmailService {
|
|
|
178
178
|
}
|
|
179
179
|
// Create delivery record
|
|
180
180
|
const deliveryId = this.generateId();
|
|
181
|
-
const
|
|
181
|
+
const deliveryData = {
|
|
182
182
|
id: deliveryId,
|
|
183
|
-
|
|
184
|
-
|
|
183
|
+
message_id: '',
|
|
184
|
+
to_address: Array.isArray(request.to) ? request.to[0].toString() : request.to,
|
|
185
|
+
from_address: typeof request.from === 'string' ? request.from : request.from.email,
|
|
185
186
|
subject,
|
|
186
187
|
provider,
|
|
187
188
|
status: DeliveryStatus.SENDING,
|
|
188
|
-
|
|
189
|
-
metadata: request.metadata,
|
|
190
|
-
|
|
191
|
-
|
|
189
|
+
template_id: request.templateId || null,
|
|
190
|
+
metadata: request.metadata ? JSON.stringify(request.metadata) : null,
|
|
191
|
+
sent_at: null,
|
|
192
|
+
delivered_at: null,
|
|
193
|
+
opened_at: null,
|
|
194
|
+
clicked_at: null,
|
|
195
|
+
bounced_at: null,
|
|
196
|
+
error: null,
|
|
197
|
+
created_at: new Date().toISOString(),
|
|
198
|
+
updated_at: new Date().toISOString()
|
|
192
199
|
};
|
|
193
|
-
await this.db.insert('email_deliveries',
|
|
200
|
+
await this.db.insert('email_deliveries', deliveryData);
|
|
194
201
|
// Send email with retry logic
|
|
195
202
|
const toEmail = Array.isArray(request.to)
|
|
196
203
|
? (typeof request.to[0] === 'string' ? request.to[0] : request.to[0].email)
|
|
@@ -211,11 +218,11 @@ export class EmailService {
|
|
|
211
218
|
const result = await this.sendWithRetry(sender, emailMessage, deliveryId, provider);
|
|
212
219
|
// Update delivery record
|
|
213
220
|
await this.db.update('email_deliveries', deliveryId, {
|
|
214
|
-
|
|
221
|
+
message_id: result.messageId || '',
|
|
215
222
|
status: result.success ? DeliveryStatus.SENT : DeliveryStatus.FAILED,
|
|
216
|
-
|
|
217
|
-
error: result.errorMessage,
|
|
218
|
-
|
|
223
|
+
sent_at: result.success ? new Date().toISOString() : null,
|
|
224
|
+
error: result.errorMessage || null,
|
|
225
|
+
updated_at: new Date().toISOString()
|
|
219
226
|
});
|
|
220
227
|
this.logger.info(`Email sent via ${provider}`, { deliveryId, messageId: result.messageId });
|
|
221
228
|
return {
|
|
@@ -274,8 +281,9 @@ export class EmailService {
|
|
|
274
281
|
try {
|
|
275
282
|
const id = this.generateId();
|
|
276
283
|
const now = new Date();
|
|
277
|
-
// Extract variables from template
|
|
278
|
-
const
|
|
284
|
+
// Extract variables from all template parts
|
|
285
|
+
const allContent = [request.subject, request.html, request.text].filter(Boolean).join(' ');
|
|
286
|
+
const variables = this.extractVariables(allContent);
|
|
279
287
|
const template = {
|
|
280
288
|
id,
|
|
281
289
|
name: request.name,
|
|
@@ -288,8 +296,15 @@ export class EmailService {
|
|
|
288
296
|
updatedAt: now
|
|
289
297
|
};
|
|
290
298
|
await this.db.insert('email_templates', {
|
|
291
|
-
|
|
292
|
-
|
|
299
|
+
id,
|
|
300
|
+
name: request.name,
|
|
301
|
+
subject: request.subject,
|
|
302
|
+
html: request.html,
|
|
303
|
+
text: request.text || null,
|
|
304
|
+
variables: JSON.stringify(variables),
|
|
305
|
+
version: 1,
|
|
306
|
+
created_at: now.toISOString(),
|
|
307
|
+
updated_at: now.toISOString()
|
|
293
308
|
});
|
|
294
309
|
if (this.config.templates?.cacheEnabled) {
|
|
295
310
|
this.templateCache.set(id, template);
|
|
@@ -329,14 +344,17 @@ export class EmailService {
|
|
|
329
344
|
throw new TemplateError(`Template ${id} not found`);
|
|
330
345
|
}
|
|
331
346
|
const html = update.html || existing.html;
|
|
332
|
-
const
|
|
347
|
+
const subject = update.subject || existing.subject;
|
|
348
|
+
const text = update.text || existing.text;
|
|
349
|
+
const allContent = [subject, html, text].filter(Boolean).join(' ');
|
|
350
|
+
const variables = this.extractVariables(allContent);
|
|
333
351
|
const updated = await this.db.update('email_templates', id, {
|
|
334
352
|
subject: update.subject || existing.subject,
|
|
335
353
|
html,
|
|
336
354
|
text: update.text || existing.text,
|
|
337
355
|
variables: JSON.stringify(variables),
|
|
338
356
|
version: existing.version + 1,
|
|
339
|
-
|
|
357
|
+
updated_at: new Date().toISOString()
|
|
340
358
|
});
|
|
341
359
|
// Invalidate cache
|
|
342
360
|
this.templateCache.delete(id);
|
|
@@ -388,15 +406,15 @@ export class EmailService {
|
|
|
388
406
|
async trackOpen(deliveryId) {
|
|
389
407
|
await this.db.update('email_deliveries', deliveryId, {
|
|
390
408
|
status: DeliveryStatus.OPENED,
|
|
391
|
-
|
|
392
|
-
|
|
409
|
+
opened_at: new Date().toISOString(),
|
|
410
|
+
updated_at: new Date().toISOString()
|
|
393
411
|
});
|
|
394
412
|
}
|
|
395
413
|
async trackClick(deliveryId) {
|
|
396
414
|
await this.db.update('email_deliveries', deliveryId, {
|
|
397
415
|
status: DeliveryStatus.CLICKED,
|
|
398
|
-
|
|
399
|
-
|
|
416
|
+
clicked_at: new Date().toISOString(),
|
|
417
|
+
updated_at: new Date().toISOString()
|
|
400
418
|
});
|
|
401
419
|
}
|
|
402
420
|
async getDeliveryStats(startDate, endDate) {
|
|
@@ -457,9 +475,9 @@ export class EmailService {
|
|
|
457
475
|
lists: JSON.stringify(request.lists || []),
|
|
458
476
|
tags: request.tags ? JSON.stringify(request.tags) : null,
|
|
459
477
|
metadata: request.metadata ? JSON.stringify(request.metadata) : null,
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
478
|
+
subscribed_at: now.toISOString(),
|
|
479
|
+
created_at: now.toISOString(),
|
|
480
|
+
updated_at: now.toISOString()
|
|
463
481
|
});
|
|
464
482
|
this.logger.info(`Subscriber created: ${request.email}`, { id });
|
|
465
483
|
return this.parseSubscriber(subscriber);
|
|
@@ -470,7 +488,7 @@ export class EmailService {
|
|
|
470
488
|
}
|
|
471
489
|
async updateSubscriber(id, update) {
|
|
472
490
|
const updateData = {
|
|
473
|
-
|
|
491
|
+
updated_at: new Date().toISOString()
|
|
474
492
|
};
|
|
475
493
|
if (update.name !== undefined)
|
|
476
494
|
updateData.name = update.name;
|
|
@@ -492,8 +510,8 @@ export class EmailService {
|
|
|
492
510
|
}
|
|
493
511
|
await this.db.update('subscribers', subscriber.id, {
|
|
494
512
|
status: 'unsubscribed',
|
|
495
|
-
|
|
496
|
-
|
|
513
|
+
unsubscribed_at: new Date().toISOString(),
|
|
514
|
+
updated_at: new Date().toISOString()
|
|
497
515
|
});
|
|
498
516
|
this.logger.info(`Subscriber unsubscribed: ${email}`);
|
|
499
517
|
return true;
|
|
@@ -513,14 +531,22 @@ export class EmailService {
|
|
|
513
531
|
async createList(name, description) {
|
|
514
532
|
const id = this.generateId();
|
|
515
533
|
const now = new Date();
|
|
516
|
-
|
|
534
|
+
const record = await this.db.insert('subscriber_lists', {
|
|
517
535
|
id,
|
|
518
536
|
name,
|
|
519
537
|
description,
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
538
|
+
subscriber_count: 0,
|
|
539
|
+
created_at: now.toISOString(),
|
|
540
|
+
updated_at: now.toISOString()
|
|
523
541
|
});
|
|
542
|
+
return {
|
|
543
|
+
id: record.id,
|
|
544
|
+
name: record.name,
|
|
545
|
+
description: record.description,
|
|
546
|
+
subscriberCount: record.subscriber_count,
|
|
547
|
+
createdAt: new Date(record.created_at),
|
|
548
|
+
updatedAt: new Date(record.updated_at)
|
|
549
|
+
};
|
|
524
550
|
}
|
|
525
551
|
// ===== Webhooks =====
|
|
526
552
|
registerWebhookHandler(event, handler) {
|
|
@@ -610,7 +636,21 @@ export class EmailService {
|
|
|
610
636
|
...config.customData
|
|
611
637
|
}
|
|
612
638
|
};
|
|
613
|
-
|
|
639
|
+
let emailResult;
|
|
640
|
+
try {
|
|
641
|
+
emailResult = await this.send(emailRequest);
|
|
642
|
+
}
|
|
643
|
+
catch (sendError) {
|
|
644
|
+
const sendErr = sendError instanceof Error ? sendError : new Error(String(sendError));
|
|
645
|
+
this.logger.error('Failed to send magic link email', sendErr);
|
|
646
|
+
return {
|
|
647
|
+
success: false,
|
|
648
|
+
magicLink,
|
|
649
|
+
token,
|
|
650
|
+
expiresAt,
|
|
651
|
+
error: sendErr.message
|
|
652
|
+
};
|
|
653
|
+
}
|
|
614
654
|
this.logger.info(`Magic link email sent`, {
|
|
615
655
|
recipient: config.recipient,
|
|
616
656
|
purpose: config.purpose,
|
|
@@ -627,7 +667,7 @@ export class EmailService {
|
|
|
627
667
|
}
|
|
628
668
|
catch (error) {
|
|
629
669
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
630
|
-
this.logger.error('Failed to
|
|
670
|
+
this.logger.error('Failed to generate magic link', err);
|
|
631
671
|
return {
|
|
632
672
|
success: false,
|
|
633
673
|
error: err.message
|
package/dist/types.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { DatabaseConfig } from '@bernierllc/database-adapter';
|
|
2
2
|
import type { QueueOptions } from '@bernierllc/queue-manager';
|
|
3
3
|
import type { TemplateContext } from '@bernierllc/template-engine';
|
|
4
|
-
export type EmailProvider = 'sendgrid' | 'mailgun' | 'ses' | 'smtp';
|
|
4
|
+
export type EmailProvider = 'sendgrid' | 'mailgun' | 'ses' | 'smtp' | 'postmark' | 'mandrill' | 'smtp2go';
|
|
5
5
|
export interface ProviderConfig {
|
|
6
6
|
type: EmailProvider;
|
|
7
7
|
apiKey?: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bernierllc/email-service",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.3",
|
|
4
4
|
"description": "Comprehensive email service orchestrating template management, multi-provider delivery, tracking, and subscriber management",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -26,19 +26,19 @@
|
|
|
26
26
|
"author": "Bernier LLC",
|
|
27
27
|
"license": "PROPRIETARY",
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@bernierllc/
|
|
30
|
-
"@bernierllc/
|
|
31
|
-
"@bernierllc/
|
|
32
|
-
"@bernierllc/
|
|
33
|
-
"@bernierllc/
|
|
34
|
-
"@bernierllc/
|
|
35
|
-
"@bernierllc/retry-
|
|
36
|
-
"@bernierllc/
|
|
37
|
-
"@bernierllc/
|
|
38
|
-
"@bernierllc/
|
|
39
|
-
"@bernierllc/
|
|
40
|
-
"@bernierllc/
|
|
41
|
-
"@bernierllc/crypto-utils": "1.0.
|
|
29
|
+
"@bernierllc/database-adapter": "1.2.2",
|
|
30
|
+
"@bernierllc/email-parser": "0.4.1",
|
|
31
|
+
"@bernierllc/email-sender": "5.2.1",
|
|
32
|
+
"@bernierllc/email-validator": "3.0.2",
|
|
33
|
+
"@bernierllc/logger": "1.3.2",
|
|
34
|
+
"@bernierllc/queue-manager": "1.2.2",
|
|
35
|
+
"@bernierllc/retry-metrics": "0.2.2",
|
|
36
|
+
"@bernierllc/retry-policy": "0.3.2",
|
|
37
|
+
"@bernierllc/retry-state": "0.2.2",
|
|
38
|
+
"@bernierllc/template-engine": "0.4.2",
|
|
39
|
+
"@bernierllc/config-manager": "1.0.7",
|
|
40
|
+
"@bernierllc/webhook-validator": "1.0.5",
|
|
41
|
+
"@bernierllc/crypto-utils": "1.0.8"
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
|
44
44
|
"@sendgrid/mail": "^8.0.0",
|
|
@@ -80,7 +80,7 @@
|
|
|
80
80
|
"prebuild": "npm run clean",
|
|
81
81
|
"clean": "rm -rf dist",
|
|
82
82
|
"test": "jest",
|
|
83
|
-
"test:run": "jest --watchAll=false",
|
|
83
|
+
"test:run": "jest --watchAll=false --forceExit",
|
|
84
84
|
"test:watch": "jest --watch",
|
|
85
85
|
"test:coverage": "jest --coverage",
|
|
86
86
|
"lint": "eslint src --ext .ts"
|