@bernierllc/email-manager 0.4.0 → 0.4.1
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/README.md +653 -0
- package/dist/capabilities/degradation-logger.d.ts +59 -0
- package/dist/capabilities/degradation-logger.d.ts.map +1 -0
- package/dist/capabilities/degradation-logger.js +106 -0
- package/dist/capabilities/degradation-logger.js.map +1 -0
- package/dist/capabilities/feature-router.d.ts +55 -0
- package/dist/capabilities/feature-router.d.ts.map +1 -0
- package/dist/capabilities/feature-router.js +143 -0
- package/dist/capabilities/feature-router.js.map +1 -0
- package/dist/capabilities/in-memory-resolver.d.ts +27 -0
- package/dist/capabilities/in-memory-resolver.d.ts.map +1 -0
- package/dist/capabilities/in-memory-resolver.js +79 -0
- package/dist/capabilities/in-memory-resolver.js.map +1 -0
- package/dist/capabilities/index.d.ts +13 -0
- package/dist/capabilities/index.d.ts.map +1 -0
- package/dist/capabilities/index.js +21 -0
- package/dist/capabilities/index.js.map +1 -0
- package/dist/capabilities/matrix.d.ts +66 -0
- package/dist/capabilities/matrix.d.ts.map +1 -0
- package/dist/capabilities/matrix.js +247 -0
- package/dist/capabilities/matrix.js.map +1 -0
- package/dist/capabilities/redis-resolver.d.ts +95 -0
- package/dist/capabilities/redis-resolver.d.ts.map +1 -0
- package/dist/capabilities/redis-resolver.js +227 -0
- package/dist/capabilities/redis-resolver.js.map +1 -0
- package/dist/capabilities/resolver-factory.d.ts +30 -0
- package/dist/capabilities/resolver-factory.d.ts.map +1 -0
- package/dist/capabilities/resolver-factory.js +70 -0
- package/dist/capabilities/resolver-factory.js.map +1 -0
- package/dist/capabilities/resolver.d.ts +40 -0
- package/dist/capabilities/resolver.d.ts.map +1 -0
- package/dist/capabilities/resolver.js +18 -0
- package/dist/capabilities/resolver.js.map +1 -0
- package/dist/capabilities/routing-metadata.d.ts +16 -0
- package/dist/capabilities/routing-metadata.d.ts.map +1 -0
- package/dist/capabilities/routing-metadata.js +17 -0
- package/dist/capabilities/routing-metadata.js.map +1 -0
- package/dist/capabilities/safe-resolver.d.ts +24 -0
- package/dist/capabilities/safe-resolver.d.ts.map +1 -0
- package/dist/capabilities/safe-resolver.js +48 -0
- package/dist/capabilities/safe-resolver.js.map +1 -0
- package/dist/config/schema.d.ts +99 -4
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +17 -0
- package/dist/config/schema.js.map +1 -1
- package/dist/email-manager.d.ts.map +1 -1
- package/dist/email-manager.js +28 -1
- package/dist/email-manager.js.map +1 -1
- package/dist/enhanced-email-manager.d.ts +163 -1
- package/dist/enhanced-email-manager.d.ts.map +1 -1
- package/dist/enhanced-email-manager.js +412 -8
- package/dist/enhanced-email-manager.js.map +1 -1
- package/dist/errors.d.ts +11 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +13 -0
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +13 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -1
- package/dist/index.js.map +1 -1
- package/dist/managers/provider-manager.d.ts +43 -0
- package/dist/managers/provider-manager.d.ts.map +1 -1
- package/dist/managers/provider-manager.js +102 -4
- package/dist/managers/provider-manager.js.map +1 -1
- package/dist/types.d.ts +66 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +35 -23
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { EmailManager } from './email-manager.js';
|
|
2
|
-
import {
|
|
2
|
+
import type { CalendarEvent, CalendarAttendee } from '@bernierllc/email-calendar';
|
|
3
|
+
import type { BatchSenderOptions, BatchResult, BatchEmailInput } from '@bernierllc/email-batch-sender';
|
|
4
|
+
import type { SubscriptionStore, SubscriberList, Subscriber, AddSubscriberInput, CreateListOptions, BulkAddResult } from '@bernierllc/email-subscription';
|
|
5
|
+
import { EnhancedEmailManagerConfig, EmailRecipient, SendTemplateOptions, EnhancedSenderConfiguration, CreateSenderRequest, SenderSelectionOptions, EnhancedSenderValidationResult, EnhancedTemplateValidationResult, VariableContextDefinition, EnhancedVariableSuggestion, TemplateSyncResult, BatchTemplateSyncResult, BootstrapResult, SystemHealthReport, SendResult, EmailData, EnhancedSendOptions, VariableDefinition, CalendarInviteSendOptions } from './types.js';
|
|
3
6
|
/**
|
|
4
7
|
* Internal template store for enhanced template management
|
|
5
8
|
*/
|
|
@@ -43,6 +46,10 @@ export declare class EnhancedEmailManager extends EmailManager {
|
|
|
43
46
|
private senderStore;
|
|
44
47
|
private variableContexts;
|
|
45
48
|
private initialized;
|
|
49
|
+
private subscriptionStore;
|
|
50
|
+
private subscriptionListManager;
|
|
51
|
+
private unsubscribeManager;
|
|
52
|
+
private suppressionManager;
|
|
46
53
|
constructor(config: EnhancedEmailManagerConfig);
|
|
47
54
|
/**
|
|
48
55
|
* Send a templated email with variable validation and smart sender selection
|
|
@@ -129,6 +136,159 @@ export declare class EnhancedEmailManager extends EmailManager {
|
|
|
129
136
|
* Validate template data against a context
|
|
130
137
|
*/
|
|
131
138
|
validateTemplateData(context: string, data: Record<string, unknown>): Promise<EnhancedTemplateValidationResult>;
|
|
139
|
+
/**
|
|
140
|
+
* Send a calendar invite email to recipients.
|
|
141
|
+
*
|
|
142
|
+
* Generates an ICS file from the CalendarEvent and attaches it to the email.
|
|
143
|
+
* The ICS attachment uses METHOD:REQUEST by default.
|
|
144
|
+
*
|
|
145
|
+
* @param event - The calendar event to send
|
|
146
|
+
* @param recipients - Array of recipient email addresses
|
|
147
|
+
* @param options - Optional subject, body, and provider overrides
|
|
148
|
+
* @returns SendResult from the email send operation
|
|
149
|
+
*
|
|
150
|
+
* @throws {Error} When the calendar event is invalid (missing title, bad dates, no attendees)
|
|
151
|
+
* @throws {EmailValidationError} When recipient validation fails
|
|
152
|
+
*/
|
|
153
|
+
sendCalendarInvite(event: CalendarEvent, recipients: string[], options?: CalendarInviteSendOptions): Promise<SendResult>;
|
|
154
|
+
/**
|
|
155
|
+
* Send a calendar cancellation email.
|
|
156
|
+
*
|
|
157
|
+
* Generates a METHOD:CANCEL ICS and sends it to the specified attendees.
|
|
158
|
+
*
|
|
159
|
+
* @param eventUid - The UID of the event to cancel
|
|
160
|
+
* @param organizer - The organizer attendee
|
|
161
|
+
* @param attendees - The attendees to notify
|
|
162
|
+
* @param options - Optional subject and body overrides
|
|
163
|
+
* @returns SendResult from the email send operation
|
|
164
|
+
*/
|
|
165
|
+
sendCalendarCancel(eventUid: string, organizer: CalendarAttendee, attendees: CalendarAttendee[], options?: CalendarInviteSendOptions): Promise<SendResult>;
|
|
166
|
+
/**
|
|
167
|
+
* Send a batch of emails with concurrency control, rate limiting, and retry.
|
|
168
|
+
*
|
|
169
|
+
* Wraps the existing email sending mechanism with the BatchSender from
|
|
170
|
+
* @bernierllc/email-batch-sender, providing efficient bulk email operations.
|
|
171
|
+
*
|
|
172
|
+
* @param emails - Array of batch email inputs
|
|
173
|
+
* @param options - Batch sender configuration (concurrency, rate limits, retry, etc.)
|
|
174
|
+
* @returns BatchResult with per-recipient results, timing, and abort status
|
|
175
|
+
*
|
|
176
|
+
* @throws {BatchSenderError} When batch input is invalid (missing toEmail/subject)
|
|
177
|
+
*/
|
|
178
|
+
sendBatch(emails: BatchEmailInput[], options?: BatchSenderOptions): Promise<BatchResult>;
|
|
179
|
+
/**
|
|
180
|
+
* Create a new subscriber list.
|
|
181
|
+
*
|
|
182
|
+
* @param name - The list name
|
|
183
|
+
* @param options - Optional description and metadata
|
|
184
|
+
* @returns The created SubscriberList
|
|
185
|
+
*/
|
|
186
|
+
createSubscriberList(name: string, options?: CreateListOptions): Promise<SubscriberList>;
|
|
187
|
+
/**
|
|
188
|
+
* Get a subscriber list by ID.
|
|
189
|
+
*
|
|
190
|
+
* @param listId - The list ID
|
|
191
|
+
* @returns The SubscriberList or null if not found
|
|
192
|
+
*/
|
|
193
|
+
getSubscriberList(listId: string): Promise<SubscriberList | null>;
|
|
194
|
+
/**
|
|
195
|
+
* Delete a subscriber list and all its subscribers.
|
|
196
|
+
*
|
|
197
|
+
* @param listId - The list ID
|
|
198
|
+
* @returns true if the list was deleted
|
|
199
|
+
*/
|
|
200
|
+
deleteSubscriberList(listId: string): Promise<boolean>;
|
|
201
|
+
/**
|
|
202
|
+
* Add a subscriber to a list.
|
|
203
|
+
*
|
|
204
|
+
* Checks for suppression and duplicates before adding.
|
|
205
|
+
*
|
|
206
|
+
* @param listId - The list ID
|
|
207
|
+
* @param input - Subscriber email, name, and metadata
|
|
208
|
+
* @throws {SubscriptionError} If list not found, email suppressed, or duplicate
|
|
209
|
+
*/
|
|
210
|
+
addSubscriber(listId: string, input: AddSubscriberInput): Promise<void>;
|
|
211
|
+
/**
|
|
212
|
+
* Add multiple subscribers to a list in bulk.
|
|
213
|
+
*
|
|
214
|
+
* Skips duplicates and suppressed emails without throwing.
|
|
215
|
+
*
|
|
216
|
+
* @param listId - The list ID
|
|
217
|
+
* @param subscribers - Array of subscriber inputs
|
|
218
|
+
* @returns BulkAddResult with added/skipped counts
|
|
219
|
+
*/
|
|
220
|
+
addSubscribers(listId: string, subscribers: ReadonlyArray<{
|
|
221
|
+
email: string;
|
|
222
|
+
name?: string;
|
|
223
|
+
}>): Promise<BulkAddResult>;
|
|
224
|
+
/**
|
|
225
|
+
* Remove a subscriber from a list.
|
|
226
|
+
*
|
|
227
|
+
* @param listId - The list ID
|
|
228
|
+
* @param email - The subscriber email
|
|
229
|
+
* @returns true if the subscriber was removed
|
|
230
|
+
*/
|
|
231
|
+
removeSubscriber(listId: string, email: string): Promise<boolean>;
|
|
232
|
+
/**
|
|
233
|
+
* Get a subscriber from a list.
|
|
234
|
+
*
|
|
235
|
+
* @param listId - The list ID
|
|
236
|
+
* @param email - The subscriber email
|
|
237
|
+
* @returns The Subscriber or null if not found
|
|
238
|
+
*/
|
|
239
|
+
getSubscriber(listId: string, email: string): Promise<Subscriber | null>;
|
|
240
|
+
/**
|
|
241
|
+
* Generate an unsubscribe URL for a subscriber.
|
|
242
|
+
*
|
|
243
|
+
* Requires subscription.unsubscribe configuration in the manager config.
|
|
244
|
+
*
|
|
245
|
+
* @param email - The subscriber email
|
|
246
|
+
* @param listId - The list ID
|
|
247
|
+
* @returns The signed unsubscribe URL
|
|
248
|
+
* @throws {Error} If unsubscribe manager is not configured
|
|
249
|
+
*/
|
|
250
|
+
generateUnsubscribeUrl(email: string, listId: string): string;
|
|
251
|
+
/**
|
|
252
|
+
* Process an unsubscribe request from a signed token.
|
|
253
|
+
*
|
|
254
|
+
* Verifies the token, updates subscriber status, and adds suppression.
|
|
255
|
+
*
|
|
256
|
+
* @param token - The unsubscribe token from the URL
|
|
257
|
+
* @throws {Error} If unsubscribe manager is not configured
|
|
258
|
+
* @throws {SubscriptionError} If token is invalid or expired
|
|
259
|
+
*/
|
|
260
|
+
processUnsubscribe(token: string): Promise<void>;
|
|
261
|
+
/**
|
|
262
|
+
* Check if an email address is suppressed.
|
|
263
|
+
*
|
|
264
|
+
* @param email - The email address to check
|
|
265
|
+
* @param listId - Optional list ID for scope-specific checks
|
|
266
|
+
* @returns true if the email is suppressed
|
|
267
|
+
*/
|
|
268
|
+
isSuppressed(email: string, listId?: string): Promise<boolean>;
|
|
269
|
+
/**
|
|
270
|
+
* Send an email with List-Unsubscribe headers automatically added.
|
|
271
|
+
*
|
|
272
|
+
* Generates unsubscribe URL and adds RFC 2369/8058 headers to the email.
|
|
273
|
+
* Requires subscription.unsubscribe configuration.
|
|
274
|
+
*
|
|
275
|
+
* @param email - The email data to send
|
|
276
|
+
* @param listId - The list ID for unsubscribe scoping
|
|
277
|
+
* @param recipientEmail - The recipient email for unsubscribe token
|
|
278
|
+
* @param oneClick - Whether to include RFC 8058 one-click unsubscribe
|
|
279
|
+
* @returns SendResult from the email send operation
|
|
280
|
+
* @throws {Error} If unsubscribe manager is not configured
|
|
281
|
+
*/
|
|
282
|
+
sendWithUnsubscribe(email: EmailData, listId: string, recipientEmail: string, oneClick?: boolean): Promise<SendResult>;
|
|
283
|
+
/**
|
|
284
|
+
* Set a custom subscription store (e.g., database-backed).
|
|
285
|
+
*
|
|
286
|
+
* Replaces the default InMemorySubscriptionStore and reinitializes
|
|
287
|
+
* all subscription managers to use the new store.
|
|
288
|
+
*
|
|
289
|
+
* @param store - A SubscriptionStore implementation
|
|
290
|
+
*/
|
|
291
|
+
setSubscriptionStore(store: SubscriptionStore): void;
|
|
132
292
|
/**
|
|
133
293
|
* Bootstrap the email manager system
|
|
134
294
|
*/
|
|
@@ -145,6 +305,8 @@ export declare class EnhancedEmailManager extends EmailManager {
|
|
|
145
305
|
private getTemplateSystemStats;
|
|
146
306
|
private getSenderSystemStats;
|
|
147
307
|
private getProviderHealthStats;
|
|
308
|
+
private getSubscriptionHealthStats;
|
|
309
|
+
private getBatchSenderHealthStats;
|
|
148
310
|
private calculateOverallHealth;
|
|
149
311
|
private generateHealthRecommendations;
|
|
150
312
|
private renderSimpleTemplate;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"enhanced-email-manager.d.ts","sourceRoot":"","sources":["../src/enhanced-email-manager.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAGlD,OAAO,EACL,0BAA0B,EAC1B,cAAc,EACd,mBAAmB,EACnB,2BAA2B,EAC3B,mBAAmB,EACnB,sBAAsB,EACtB,8BAA8B,EAG9B,gCAAgC,EAChC,yBAAyB,EACzB,0BAA0B,EAC1B,kBAAkB,EAClB,uBAAuB,EACvB,eAAe,EAEf,kBAAkB,EAIlB,UAAU,EACV,SAAS,EACT,mBAAmB,EACnB,kBAAkB,
|
|
1
|
+
{"version":3,"file":"enhanced-email-manager.d.ts","sourceRoot":"","sources":["../src/enhanced-email-manager.ts"],"names":[],"mappings":"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"}
|
|
@@ -7,6 +7,10 @@ Redistribution or use in other products or commercial offerings is not permitted
|
|
|
7
7
|
*/
|
|
8
8
|
import { EmailManager } from './email-manager.js';
|
|
9
9
|
import { v4 as uuidv4 } from 'uuid';
|
|
10
|
+
import { createCalendarInvite, createCalendarCancel, toCalendarAttachment } from '@bernierllc/email-calendar';
|
|
11
|
+
import { createBatchSender } from '@bernierllc/email-batch-sender';
|
|
12
|
+
import { SubscriptionListManager, UnsubscribeManager, SuppressionManager, InMemorySubscriptionStore, } from '@bernierllc/email-subscription';
|
|
13
|
+
import { createListUnsubscribeHeaders } from '@bernierllc/email-headers';
|
|
10
14
|
/**
|
|
11
15
|
* EnhancedEmailManager - Extended email management with template service,
|
|
12
16
|
* sender management, and variable validation integration.
|
|
@@ -27,7 +31,15 @@ export class EnhancedEmailManager extends EmailManager {
|
|
|
27
31
|
this.senderStore = new Map();
|
|
28
32
|
this.variableContexts = new Map();
|
|
29
33
|
this.initialized = false;
|
|
34
|
+
this.unsubscribeManager = null;
|
|
30
35
|
this.enhancedConfig = config;
|
|
36
|
+
// Initialize subscription components
|
|
37
|
+
this.subscriptionStore = new InMemorySubscriptionStore();
|
|
38
|
+
this.subscriptionListManager = new SubscriptionListManager(this.subscriptionStore);
|
|
39
|
+
this.suppressionManager = new SuppressionManager(this.subscriptionStore);
|
|
40
|
+
if (config.subscription?.unsubscribe) {
|
|
41
|
+
this.unsubscribeManager = new UnsubscribeManager(this.subscriptionStore, config.subscription.unsubscribe);
|
|
42
|
+
}
|
|
31
43
|
}
|
|
32
44
|
// ======================
|
|
33
45
|
// Enhanced Email Sending
|
|
@@ -440,6 +452,305 @@ export class EnhancedEmailManager extends EmailManager {
|
|
|
440
452
|
};
|
|
441
453
|
}
|
|
442
454
|
// ======================
|
|
455
|
+
// Calendar Invite Methods
|
|
456
|
+
// ======================
|
|
457
|
+
/**
|
|
458
|
+
* Send a calendar invite email to recipients.
|
|
459
|
+
*
|
|
460
|
+
* Generates an ICS file from the CalendarEvent and attaches it to the email.
|
|
461
|
+
* The ICS attachment uses METHOD:REQUEST by default.
|
|
462
|
+
*
|
|
463
|
+
* @param event - The calendar event to send
|
|
464
|
+
* @param recipients - Array of recipient email addresses
|
|
465
|
+
* @param options - Optional subject, body, and provider overrides
|
|
466
|
+
* @returns SendResult from the email send operation
|
|
467
|
+
*
|
|
468
|
+
* @throws {Error} When the calendar event is invalid (missing title, bad dates, no attendees)
|
|
469
|
+
* @throws {EmailValidationError} When recipient validation fails
|
|
470
|
+
*/
|
|
471
|
+
async sendCalendarInvite(event, recipients, options) {
|
|
472
|
+
if (!recipients || recipients.length === 0) {
|
|
473
|
+
throw new Error('At least one recipient is required');
|
|
474
|
+
}
|
|
475
|
+
// Generate ICS content
|
|
476
|
+
const icsContent = createCalendarInvite(event);
|
|
477
|
+
// Convert to email attachment
|
|
478
|
+
const calendarAttachment = toCalendarAttachment(icsContent, 'REQUEST');
|
|
479
|
+
// Build subject line
|
|
480
|
+
const subject = options?.subject ?? `Calendar Invite: ${event.title}`;
|
|
481
|
+
// Build default body
|
|
482
|
+
const textBody = options?.textBody ??
|
|
483
|
+
`You have been invited to: ${event.title}\nWhen: ${event.start.toISOString()} - ${event.end.toISOString()}${event.location ? `\nWhere: ${event.location}` : ''}${event.description ? `\n\n${event.description}` : ''}`;
|
|
484
|
+
const htmlBody = options?.htmlBody ??
|
|
485
|
+
`<h2>${event.title}</h2><p><strong>When:</strong> ${event.start.toISOString()} - ${event.end.toISOString()}</p>${event.location ? `<p><strong>Where:</strong> ${event.location}</p>` : ''}${event.description ? `<p>${event.description}</p>` : ''}`;
|
|
486
|
+
// Build email data with calendar attachment
|
|
487
|
+
const emailData = {
|
|
488
|
+
to: recipients,
|
|
489
|
+
subject,
|
|
490
|
+
text: textBody,
|
|
491
|
+
html: htmlBody,
|
|
492
|
+
attachments: [{
|
|
493
|
+
filename: calendarAttachment.filename,
|
|
494
|
+
content: calendarAttachment.content,
|
|
495
|
+
contentType: calendarAttachment.contentType,
|
|
496
|
+
}],
|
|
497
|
+
provider: options?.provider,
|
|
498
|
+
metadata: {
|
|
499
|
+
...options?.metadata,
|
|
500
|
+
calendarEvent: true,
|
|
501
|
+
eventTitle: event.title,
|
|
502
|
+
},
|
|
503
|
+
};
|
|
504
|
+
return this.sendEmail(emailData);
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
507
|
+
* Send a calendar cancellation email.
|
|
508
|
+
*
|
|
509
|
+
* Generates a METHOD:CANCEL ICS and sends it to the specified attendees.
|
|
510
|
+
*
|
|
511
|
+
* @param eventUid - The UID of the event to cancel
|
|
512
|
+
* @param organizer - The organizer attendee
|
|
513
|
+
* @param attendees - The attendees to notify
|
|
514
|
+
* @param options - Optional subject and body overrides
|
|
515
|
+
* @returns SendResult from the email send operation
|
|
516
|
+
*/
|
|
517
|
+
async sendCalendarCancel(eventUid, organizer, attendees, options) {
|
|
518
|
+
if (!attendees || attendees.length === 0) {
|
|
519
|
+
throw new Error('At least one attendee is required');
|
|
520
|
+
}
|
|
521
|
+
const icsContent = createCalendarCancel(eventUid, organizer, attendees);
|
|
522
|
+
const calendarAttachment = toCalendarAttachment(icsContent, 'CANCEL');
|
|
523
|
+
const subject = options?.subject ?? 'Event Cancelled';
|
|
524
|
+
const recipients = attendees.map(a => a.email);
|
|
525
|
+
const emailData = {
|
|
526
|
+
to: recipients,
|
|
527
|
+
subject,
|
|
528
|
+
text: options?.textBody ?? 'This event has been cancelled.',
|
|
529
|
+
html: options?.htmlBody ?? '<p>This event has been cancelled.</p>',
|
|
530
|
+
attachments: [{
|
|
531
|
+
filename: calendarAttachment.filename,
|
|
532
|
+
content: calendarAttachment.content,
|
|
533
|
+
contentType: calendarAttachment.contentType,
|
|
534
|
+
}],
|
|
535
|
+
provider: options?.provider,
|
|
536
|
+
metadata: {
|
|
537
|
+
...options?.metadata,
|
|
538
|
+
calendarCancel: true,
|
|
539
|
+
eventUid,
|
|
540
|
+
},
|
|
541
|
+
};
|
|
542
|
+
return this.sendEmail(emailData);
|
|
543
|
+
}
|
|
544
|
+
// ======================
|
|
545
|
+
// Batch Send Methods
|
|
546
|
+
// ======================
|
|
547
|
+
/**
|
|
548
|
+
* Send a batch of emails with concurrency control, rate limiting, and retry.
|
|
549
|
+
*
|
|
550
|
+
* Wraps the existing email sending mechanism with the BatchSender from
|
|
551
|
+
* @bernierllc/email-batch-sender, providing efficient bulk email operations.
|
|
552
|
+
*
|
|
553
|
+
* @param emails - Array of batch email inputs
|
|
554
|
+
* @param options - Batch sender configuration (concurrency, rate limits, retry, etc.)
|
|
555
|
+
* @returns BatchResult with per-recipient results, timing, and abort status
|
|
556
|
+
*
|
|
557
|
+
* @throws {BatchSenderError} When batch input is invalid (missing toEmail/subject)
|
|
558
|
+
*/
|
|
559
|
+
async sendBatch(emails, options) {
|
|
560
|
+
// Create an adapter that wraps our sendEmail into the EmailSenderInterface
|
|
561
|
+
const senderAdapter = {
|
|
562
|
+
send: async (message) => {
|
|
563
|
+
try {
|
|
564
|
+
const emailData = {
|
|
565
|
+
to: message.toEmail,
|
|
566
|
+
subject: message.subject,
|
|
567
|
+
html: message.htmlContent,
|
|
568
|
+
text: message.textContent,
|
|
569
|
+
metadata: message.metadata,
|
|
570
|
+
};
|
|
571
|
+
const result = await this.sendEmail(emailData);
|
|
572
|
+
return {
|
|
573
|
+
success: result.success,
|
|
574
|
+
messageId: result.messageId,
|
|
575
|
+
provider: result.provider,
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
catch (error) {
|
|
579
|
+
return {
|
|
580
|
+
success: false,
|
|
581
|
+
errorMessage: error instanceof Error ? error.message : 'Unknown send error',
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
};
|
|
586
|
+
const batchSender = createBatchSender(senderAdapter, options);
|
|
587
|
+
return batchSender.send(emails);
|
|
588
|
+
}
|
|
589
|
+
// ======================
|
|
590
|
+
// Subscription Management Methods
|
|
591
|
+
// ======================
|
|
592
|
+
/**
|
|
593
|
+
* Create a new subscriber list.
|
|
594
|
+
*
|
|
595
|
+
* @param name - The list name
|
|
596
|
+
* @param options - Optional description and metadata
|
|
597
|
+
* @returns The created SubscriberList
|
|
598
|
+
*/
|
|
599
|
+
async createSubscriberList(name, options) {
|
|
600
|
+
return this.subscriptionListManager.createList(name, options);
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* Get a subscriber list by ID.
|
|
604
|
+
*
|
|
605
|
+
* @param listId - The list ID
|
|
606
|
+
* @returns The SubscriberList or null if not found
|
|
607
|
+
*/
|
|
608
|
+
async getSubscriberList(listId) {
|
|
609
|
+
return this.subscriptionListManager.getList(listId);
|
|
610
|
+
}
|
|
611
|
+
/**
|
|
612
|
+
* Delete a subscriber list and all its subscribers.
|
|
613
|
+
*
|
|
614
|
+
* @param listId - The list ID
|
|
615
|
+
* @returns true if the list was deleted
|
|
616
|
+
*/
|
|
617
|
+
async deleteSubscriberList(listId) {
|
|
618
|
+
return this.subscriptionListManager.deleteList(listId);
|
|
619
|
+
}
|
|
620
|
+
/**
|
|
621
|
+
* Add a subscriber to a list.
|
|
622
|
+
*
|
|
623
|
+
* Checks for suppression and duplicates before adding.
|
|
624
|
+
*
|
|
625
|
+
* @param listId - The list ID
|
|
626
|
+
* @param input - Subscriber email, name, and metadata
|
|
627
|
+
* @throws {SubscriptionError} If list not found, email suppressed, or duplicate
|
|
628
|
+
*/
|
|
629
|
+
async addSubscriber(listId, input) {
|
|
630
|
+
return this.subscriptionListManager.addSubscriber(listId, input);
|
|
631
|
+
}
|
|
632
|
+
/**
|
|
633
|
+
* Add multiple subscribers to a list in bulk.
|
|
634
|
+
*
|
|
635
|
+
* Skips duplicates and suppressed emails without throwing.
|
|
636
|
+
*
|
|
637
|
+
* @param listId - The list ID
|
|
638
|
+
* @param subscribers - Array of subscriber inputs
|
|
639
|
+
* @returns BulkAddResult with added/skipped counts
|
|
640
|
+
*/
|
|
641
|
+
async addSubscribers(listId, subscribers) {
|
|
642
|
+
return this.subscriptionListManager.addSubscribers(listId, subscribers);
|
|
643
|
+
}
|
|
644
|
+
/**
|
|
645
|
+
* Remove a subscriber from a list.
|
|
646
|
+
*
|
|
647
|
+
* @param listId - The list ID
|
|
648
|
+
* @param email - The subscriber email
|
|
649
|
+
* @returns true if the subscriber was removed
|
|
650
|
+
*/
|
|
651
|
+
async removeSubscriber(listId, email) {
|
|
652
|
+
return this.subscriptionListManager.removeSubscriber(listId, email);
|
|
653
|
+
}
|
|
654
|
+
/**
|
|
655
|
+
* Get a subscriber from a list.
|
|
656
|
+
*
|
|
657
|
+
* @param listId - The list ID
|
|
658
|
+
* @param email - The subscriber email
|
|
659
|
+
* @returns The Subscriber or null if not found
|
|
660
|
+
*/
|
|
661
|
+
async getSubscriber(listId, email) {
|
|
662
|
+
return this.subscriptionListManager.getSubscriber(listId, email);
|
|
663
|
+
}
|
|
664
|
+
/**
|
|
665
|
+
* Generate an unsubscribe URL for a subscriber.
|
|
666
|
+
*
|
|
667
|
+
* Requires subscription.unsubscribe configuration in the manager config.
|
|
668
|
+
*
|
|
669
|
+
* @param email - The subscriber email
|
|
670
|
+
* @param listId - The list ID
|
|
671
|
+
* @returns The signed unsubscribe URL
|
|
672
|
+
* @throws {Error} If unsubscribe manager is not configured
|
|
673
|
+
*/
|
|
674
|
+
generateUnsubscribeUrl(email, listId) {
|
|
675
|
+
if (!this.unsubscribeManager) {
|
|
676
|
+
throw new Error('UnsubscribeManager is not configured. Provide subscription.unsubscribe config.');
|
|
677
|
+
}
|
|
678
|
+
return this.unsubscribeManager.generateUnsubscribeUrl(email, listId);
|
|
679
|
+
}
|
|
680
|
+
/**
|
|
681
|
+
* Process an unsubscribe request from a signed token.
|
|
682
|
+
*
|
|
683
|
+
* Verifies the token, updates subscriber status, and adds suppression.
|
|
684
|
+
*
|
|
685
|
+
* @param token - The unsubscribe token from the URL
|
|
686
|
+
* @throws {Error} If unsubscribe manager is not configured
|
|
687
|
+
* @throws {SubscriptionError} If token is invalid or expired
|
|
688
|
+
*/
|
|
689
|
+
async processUnsubscribe(token) {
|
|
690
|
+
if (!this.unsubscribeManager) {
|
|
691
|
+
throw new Error('UnsubscribeManager is not configured. Provide subscription.unsubscribe config.');
|
|
692
|
+
}
|
|
693
|
+
return this.unsubscribeManager.processUnsubscribe(token);
|
|
694
|
+
}
|
|
695
|
+
/**
|
|
696
|
+
* Check if an email address is suppressed.
|
|
697
|
+
*
|
|
698
|
+
* @param email - The email address to check
|
|
699
|
+
* @param listId - Optional list ID for scope-specific checks
|
|
700
|
+
* @returns true if the email is suppressed
|
|
701
|
+
*/
|
|
702
|
+
async isSuppressed(email, listId) {
|
|
703
|
+
return this.suppressionManager.isSuppressed(email, listId);
|
|
704
|
+
}
|
|
705
|
+
/**
|
|
706
|
+
* Send an email with List-Unsubscribe headers automatically added.
|
|
707
|
+
*
|
|
708
|
+
* Generates unsubscribe URL and adds RFC 2369/8058 headers to the email.
|
|
709
|
+
* Requires subscription.unsubscribe configuration.
|
|
710
|
+
*
|
|
711
|
+
* @param email - The email data to send
|
|
712
|
+
* @param listId - The list ID for unsubscribe scoping
|
|
713
|
+
* @param recipientEmail - The recipient email for unsubscribe token
|
|
714
|
+
* @param oneClick - Whether to include RFC 8058 one-click unsubscribe
|
|
715
|
+
* @returns SendResult from the email send operation
|
|
716
|
+
* @throws {Error} If unsubscribe manager is not configured
|
|
717
|
+
*/
|
|
718
|
+
async sendWithUnsubscribe(email, listId, recipientEmail, oneClick = true) {
|
|
719
|
+
if (!this.unsubscribeManager) {
|
|
720
|
+
throw new Error('UnsubscribeManager is not configured. Provide subscription.unsubscribe config.');
|
|
721
|
+
}
|
|
722
|
+
// Generate unsubscribe URL
|
|
723
|
+
const unsubscribeUrl = this.unsubscribeManager.generateUnsubscribeUrl(recipientEmail, listId);
|
|
724
|
+
// Create List-Unsubscribe headers
|
|
725
|
+
const unsubscribeHeaders = createListUnsubscribeHeaders(unsubscribeUrl, undefined, oneClick);
|
|
726
|
+
// Merge unsubscribe headers into email metadata (headers)
|
|
727
|
+
const emailWithHeaders = {
|
|
728
|
+
...email,
|
|
729
|
+
metadata: {
|
|
730
|
+
...email.metadata,
|
|
731
|
+
unsubscribeHeaders,
|
|
732
|
+
listId,
|
|
733
|
+
},
|
|
734
|
+
};
|
|
735
|
+
return this.sendEmail(emailWithHeaders);
|
|
736
|
+
}
|
|
737
|
+
/**
|
|
738
|
+
* Set a custom subscription store (e.g., database-backed).
|
|
739
|
+
*
|
|
740
|
+
* Replaces the default InMemorySubscriptionStore and reinitializes
|
|
741
|
+
* all subscription managers to use the new store.
|
|
742
|
+
*
|
|
743
|
+
* @param store - A SubscriptionStore implementation
|
|
744
|
+
*/
|
|
745
|
+
setSubscriptionStore(store) {
|
|
746
|
+
this.subscriptionStore = store;
|
|
747
|
+
this.subscriptionListManager = new SubscriptionListManager(store);
|
|
748
|
+
this.suppressionManager = new SuppressionManager(store);
|
|
749
|
+
if (this.enhancedConfig.subscription?.unsubscribe) {
|
|
750
|
+
this.unsubscribeManager = new UnsubscribeManager(store, this.enhancedConfig.subscription.unsubscribe);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
// ======================
|
|
443
754
|
// System Operations
|
|
444
755
|
// ======================
|
|
445
756
|
/**
|
|
@@ -507,21 +818,39 @@ export class EnhancedEmailManager extends EmailManager {
|
|
|
507
818
|
const templateStats = await this.getTemplateSystemStats();
|
|
508
819
|
const senderStats = await this.getSenderSystemStats();
|
|
509
820
|
const providerStats = await this.getProviderHealthStats();
|
|
510
|
-
const
|
|
821
|
+
const subscriptionStats = await this.getSubscriptionHealthStats();
|
|
822
|
+
const batchSenderStats = this.getBatchSenderHealthStats();
|
|
823
|
+
const allComponents = [
|
|
511
824
|
emailStats,
|
|
512
825
|
templateStats,
|
|
513
826
|
senderStats,
|
|
514
827
|
providerStats
|
|
515
|
-
]
|
|
828
|
+
];
|
|
829
|
+
// Only include subscription and batch sender in overall health
|
|
830
|
+
// calculation if they are configured
|
|
831
|
+
if (subscriptionStats) {
|
|
832
|
+
allComponents.push(subscriptionStats);
|
|
833
|
+
}
|
|
834
|
+
if (batchSenderStats) {
|
|
835
|
+
allComponents.push(batchSenderStats);
|
|
836
|
+
}
|
|
837
|
+
const overallHealth = this.calculateOverallHealth(allComponents);
|
|
838
|
+
const components = {
|
|
839
|
+
email: emailStats,
|
|
840
|
+
templates: templateStats,
|
|
841
|
+
senders: senderStats,
|
|
842
|
+
providers: providerStats
|
|
843
|
+
};
|
|
844
|
+
if (subscriptionStats) {
|
|
845
|
+
components.subscription = subscriptionStats;
|
|
846
|
+
}
|
|
847
|
+
if (batchSenderStats) {
|
|
848
|
+
components.batchSender = batchSenderStats;
|
|
849
|
+
}
|
|
516
850
|
return {
|
|
517
851
|
overall: overallHealth,
|
|
518
852
|
timestamp: new Date(),
|
|
519
|
-
components
|
|
520
|
-
email: emailStats,
|
|
521
|
-
templates: templateStats,
|
|
522
|
-
senders: senderStats,
|
|
523
|
-
providers: providerStats
|
|
524
|
-
},
|
|
853
|
+
components,
|
|
525
854
|
recommendations: this.generateHealthRecommendations({
|
|
526
855
|
emailStats,
|
|
527
856
|
templateStats,
|
|
@@ -722,6 +1051,81 @@ export class EnhancedEmailManager extends EmailManager {
|
|
|
722
1051
|
issues
|
|
723
1052
|
};
|
|
724
1053
|
}
|
|
1054
|
+
async getSubscriptionHealthStats() {
|
|
1055
|
+
if (!this.enhancedConfig.subscription?.enabled) {
|
|
1056
|
+
return null;
|
|
1057
|
+
}
|
|
1058
|
+
try {
|
|
1059
|
+
const lists = await this.subscriptionListManager.listLists();
|
|
1060
|
+
const suppressions = await this.suppressionManager.listSuppressions();
|
|
1061
|
+
const totalSubscribers = lists.items.reduce((sum, list) => sum + list.subscriberCount, 0);
|
|
1062
|
+
const issues = [];
|
|
1063
|
+
if (lists.total === 0) {
|
|
1064
|
+
issues.push({
|
|
1065
|
+
severity: 'info',
|
|
1066
|
+
message: 'No subscription lists configured',
|
|
1067
|
+
details: 'Create subscription lists to manage email recipients'
|
|
1068
|
+
});
|
|
1069
|
+
}
|
|
1070
|
+
if (suppressions.total > 0 && lists.total > 0) {
|
|
1071
|
+
const suppressionRatio = suppressions.total / Math.max(totalSubscribers, 1);
|
|
1072
|
+
if (suppressionRatio > 0.3) {
|
|
1073
|
+
issues.push({
|
|
1074
|
+
severity: 'warning',
|
|
1075
|
+
message: 'High suppression ratio',
|
|
1076
|
+
details: `${suppressions.total} suppressions vs ${totalSubscribers} subscribers`
|
|
1077
|
+
});
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
const hasDegraded = issues.some(i => i.severity === 'warning');
|
|
1081
|
+
const score = Math.max(0, 100 - issues.filter(i => i.severity === 'warning').length * 20);
|
|
1082
|
+
return {
|
|
1083
|
+
status: hasDegraded ? 'degraded' : 'healthy',
|
|
1084
|
+
score,
|
|
1085
|
+
metrics: {
|
|
1086
|
+
listCount: lists.total,
|
|
1087
|
+
totalSubscribers,
|
|
1088
|
+
suppressionCount: suppressions.total
|
|
1089
|
+
},
|
|
1090
|
+
issues
|
|
1091
|
+
};
|
|
1092
|
+
}
|
|
1093
|
+
catch (error) {
|
|
1094
|
+
return {
|
|
1095
|
+
status: 'degraded',
|
|
1096
|
+
score: 50,
|
|
1097
|
+
metrics: {
|
|
1098
|
+
listCount: 0,
|
|
1099
|
+
totalSubscribers: 0,
|
|
1100
|
+
suppressionCount: 0
|
|
1101
|
+
},
|
|
1102
|
+
issues: [{
|
|
1103
|
+
severity: 'warning',
|
|
1104
|
+
message: 'Failed to retrieve subscription health',
|
|
1105
|
+
details: error instanceof Error ? error.message : 'Unknown error'
|
|
1106
|
+
}]
|
|
1107
|
+
};
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
getBatchSenderHealthStats() {
|
|
1111
|
+
// The batch sender is available if the email-batch-sender package is
|
|
1112
|
+
// imported and the manager has at least one active provider configured.
|
|
1113
|
+
// Since createBatchSender is a factory that wraps an EmailSenderInterface,
|
|
1114
|
+
// it is always structurally available but only useful with providers.
|
|
1115
|
+
const hasProviders = this.enhancedConfig.providers.length > 0;
|
|
1116
|
+
if (!hasProviders) {
|
|
1117
|
+
return null;
|
|
1118
|
+
}
|
|
1119
|
+
return {
|
|
1120
|
+
status: 'healthy',
|
|
1121
|
+
score: 100,
|
|
1122
|
+
metrics: {
|
|
1123
|
+
available: 1,
|
|
1124
|
+
configured: 1
|
|
1125
|
+
},
|
|
1126
|
+
issues: []
|
|
1127
|
+
};
|
|
1128
|
+
}
|
|
725
1129
|
calculateOverallHealth(components) {
|
|
726
1130
|
const avgScore = components.reduce((sum, c) => sum + c.score, 0) / components.length;
|
|
727
1131
|
const hasUnhealthy = components.some(c => c.status === 'unhealthy');
|