@bernierllc/email-manager 0.1.1 → 0.1.4
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 +6 -0
- package/dist/config/loadConfiguration.d.ts +60 -0
- package/dist/config/loadConfiguration.d.ts.map +1 -0
- package/dist/config/loadConfiguration.js +468 -0
- package/dist/config/loadConfiguration.js.map +1 -0
- package/dist/config/schema.d.ts +867 -0
- package/dist/config/schema.d.ts.map +1 -0
- package/dist/config/schema.js +301 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/config/types.d.ts +826 -0
- package/dist/config/types.d.ts.map +1 -0
- package/dist/config/types.js +9 -0
- package/dist/config/types.js.map +1 -0
- package/dist/email-manager.d.ts.map +1 -1
- package/dist/email-manager.js +9 -13
- package/dist/email-manager.js.map +1 -1
- package/dist/enhanced-email-manager.d.ts +173 -0
- package/dist/enhanced-email-manager.d.ts.map +1 -0
- package/dist/enhanced-email-manager.js +866 -0
- package/dist/enhanced-email-manager.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +356 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +46 -18
|
@@ -0,0 +1,866 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright (c) 2025 Bernier LLC
|
|
3
|
+
|
|
4
|
+
This file is licensed to the client under a limited-use license.
|
|
5
|
+
The client may use and modify this code *only within the scope of the project it was delivered for*.
|
|
6
|
+
Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
|
|
7
|
+
*/
|
|
8
|
+
import { EmailManager } from './email-manager.js';
|
|
9
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
10
|
+
/**
|
|
11
|
+
* EnhancedEmailManager - Extended email management with template service,
|
|
12
|
+
* sender management, and variable validation integration.
|
|
13
|
+
*
|
|
14
|
+
* This class extends EmailManager to provide:
|
|
15
|
+
* - Advanced template management
|
|
16
|
+
* - Sender configuration and selection
|
|
17
|
+
* - Template variable validation
|
|
18
|
+
* - System bootstrap and health monitoring
|
|
19
|
+
*
|
|
20
|
+
* Note: This implementation provides built-in functionality that can be
|
|
21
|
+
* replaced with external services when available.
|
|
22
|
+
*/
|
|
23
|
+
export class EnhancedEmailManager extends EmailManager {
|
|
24
|
+
constructor(config) {
|
|
25
|
+
super(config);
|
|
26
|
+
this.templateStore = new Map();
|
|
27
|
+
this.senderStore = new Map();
|
|
28
|
+
this.variableContexts = new Map();
|
|
29
|
+
this.initialized = false;
|
|
30
|
+
this.enhancedConfig = config;
|
|
31
|
+
}
|
|
32
|
+
// ======================
|
|
33
|
+
// Enhanced Email Sending
|
|
34
|
+
// ======================
|
|
35
|
+
/**
|
|
36
|
+
* Send a templated email with variable validation and smart sender selection
|
|
37
|
+
*/
|
|
38
|
+
async sendEnhancedTemplatedEmail(templateId, templateData, recipients, options = {}) {
|
|
39
|
+
const results = [];
|
|
40
|
+
try {
|
|
41
|
+
// 1. Get template from internal store
|
|
42
|
+
const template = this.templateStore.get(templateId);
|
|
43
|
+
if (!template) {
|
|
44
|
+
throw new Error(`Template not found: ${templateId}`);
|
|
45
|
+
}
|
|
46
|
+
// 2. Validate template variables if enabled
|
|
47
|
+
if (options.validateVariables && template.context) {
|
|
48
|
+
const validation = await this.validateTemplateData(template.context, templateData);
|
|
49
|
+
if (!validation.isValid && options.strictMode) {
|
|
50
|
+
return [{
|
|
51
|
+
success: false,
|
|
52
|
+
provider: 'none',
|
|
53
|
+
sentAt: new Date(),
|
|
54
|
+
errors: validation.errors.map(e => ({
|
|
55
|
+
code: 'VALIDATION_ERROR',
|
|
56
|
+
message: e.message
|
|
57
|
+
}))
|
|
58
|
+
}];
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// 3. Send to all recipients
|
|
62
|
+
for (const recipient of recipients) {
|
|
63
|
+
// Select best sender for this recipient
|
|
64
|
+
let sender = options.senderOverride;
|
|
65
|
+
if (!sender && options.selectBestSender) {
|
|
66
|
+
const selectedSender = await this.selectBestSender(recipient.email, options.preferredProvider, { allowUnverified: false });
|
|
67
|
+
sender = selectedSender || undefined;
|
|
68
|
+
}
|
|
69
|
+
// Create email data
|
|
70
|
+
const emailData = {
|
|
71
|
+
to: recipient.email,
|
|
72
|
+
subject: this.renderSimpleTemplate(template.subject, templateData),
|
|
73
|
+
html: this.renderSimpleTemplate(template.htmlContent, templateData),
|
|
74
|
+
text: template.textContent ? this.renderSimpleTemplate(template.textContent, templateData) : undefined,
|
|
75
|
+
templateId,
|
|
76
|
+
metadata: {
|
|
77
|
+
templateVersion: template.version,
|
|
78
|
+
senderId: sender?.id,
|
|
79
|
+
context: template.context,
|
|
80
|
+
recipientMetadata: recipient.metadata
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
// Send email
|
|
84
|
+
const result = await this.sendEmail(emailData);
|
|
85
|
+
results.push(result);
|
|
86
|
+
}
|
|
87
|
+
return results;
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
return [{
|
|
91
|
+
success: false,
|
|
92
|
+
provider: 'none',
|
|
93
|
+
sentAt: new Date(),
|
|
94
|
+
errors: [{
|
|
95
|
+
code: 'ENHANCED_SEND_ERROR',
|
|
96
|
+
message: error instanceof Error ? error.message : 'Unknown error occurred'
|
|
97
|
+
}]
|
|
98
|
+
}];
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Send email with variable validation
|
|
103
|
+
*/
|
|
104
|
+
async sendWithVariableValidation(email, context, _options) {
|
|
105
|
+
if (context && email.templateContext) {
|
|
106
|
+
const validation = await this.validateTemplateData(context, email.templateContext);
|
|
107
|
+
if (!validation.isValid) {
|
|
108
|
+
return {
|
|
109
|
+
success: false,
|
|
110
|
+
provider: 'none',
|
|
111
|
+
sentAt: new Date(),
|
|
112
|
+
errors: validation.errors.map(e => ({
|
|
113
|
+
code: 'VALIDATION_ERROR',
|
|
114
|
+
message: e.message
|
|
115
|
+
}))
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return await this.sendEmail(email);
|
|
120
|
+
}
|
|
121
|
+
// ======================
|
|
122
|
+
// Template Management
|
|
123
|
+
// ======================
|
|
124
|
+
/**
|
|
125
|
+
* Create a template with validation
|
|
126
|
+
*/
|
|
127
|
+
async createEnhancedTemplate(template) {
|
|
128
|
+
try {
|
|
129
|
+
// Validate template if context is provided
|
|
130
|
+
if (template.context) {
|
|
131
|
+
const validation = await this.validateEnhancedTemplate(template.htmlContent, template.context);
|
|
132
|
+
if (!validation.isValid) {
|
|
133
|
+
return {
|
|
134
|
+
success: false,
|
|
135
|
+
errors: validation.errors.map(e => e.message)
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const id = uuidv4();
|
|
140
|
+
const now = new Date();
|
|
141
|
+
const internalTemplate = {
|
|
142
|
+
id,
|
|
143
|
+
name: template.name,
|
|
144
|
+
subject: template.subject,
|
|
145
|
+
htmlContent: template.htmlContent,
|
|
146
|
+
textContent: template.textContent,
|
|
147
|
+
context: template.context,
|
|
148
|
+
category: template.category,
|
|
149
|
+
version: '1.0.0',
|
|
150
|
+
isActive: true,
|
|
151
|
+
createdAt: now,
|
|
152
|
+
updatedAt: now
|
|
153
|
+
};
|
|
154
|
+
this.templateStore.set(id, internalTemplate);
|
|
155
|
+
return { success: true, templateId: id };
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
return {
|
|
159
|
+
success: false,
|
|
160
|
+
errors: [error instanceof Error ? error.message : 'Failed to create template']
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Get a template by ID from enhanced store
|
|
166
|
+
*/
|
|
167
|
+
async getEnhancedTemplate(templateId) {
|
|
168
|
+
return this.templateStore.get(templateId) || null;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* List all enhanced templates
|
|
172
|
+
*/
|
|
173
|
+
async listEnhancedTemplates() {
|
|
174
|
+
const items = Array.from(this.templateStore.values());
|
|
175
|
+
return { items, total: items.length };
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Sync a template to providers (placeholder for future integration)
|
|
179
|
+
*/
|
|
180
|
+
async syncTemplate(templateId, providerId) {
|
|
181
|
+
const template = this.templateStore.get(templateId);
|
|
182
|
+
if (!template) {
|
|
183
|
+
return {
|
|
184
|
+
success: false,
|
|
185
|
+
action: 'failed',
|
|
186
|
+
localTemplateId: templateId,
|
|
187
|
+
error: 'Template not found'
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
// Placeholder - would integrate with actual provider plugins
|
|
191
|
+
return {
|
|
192
|
+
success: true,
|
|
193
|
+
action: providerId ? 'updated' : 'synced_all',
|
|
194
|
+
localTemplateId: templateId,
|
|
195
|
+
providerId,
|
|
196
|
+
metadata: { note: 'Sync placeholder - integrate with provider plugins' }
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Validate a template against a variable context
|
|
201
|
+
*/
|
|
202
|
+
async validateEnhancedTemplate(templateContent, context) {
|
|
203
|
+
const variableContext = this.variableContexts.get(context);
|
|
204
|
+
if (!variableContext) {
|
|
205
|
+
return {
|
|
206
|
+
isValid: true,
|
|
207
|
+
errors: [],
|
|
208
|
+
warnings: [],
|
|
209
|
+
info: [`No variable context '${context}' registered, validation skipped`]
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
// Extract variables from template (simple Handlebars-style extraction)
|
|
213
|
+
const variablePattern = /\{\{\s*([a-zA-Z_][a-zA-Z0-9_.]*)\s*\}\}/g;
|
|
214
|
+
const usedVariables = [];
|
|
215
|
+
let match;
|
|
216
|
+
while ((match = variablePattern.exec(templateContent)) !== null) {
|
|
217
|
+
usedVariables.push(match[1]);
|
|
218
|
+
}
|
|
219
|
+
// Check for required variables
|
|
220
|
+
const errors = [];
|
|
221
|
+
const definedPaths = variableContext.variables.map(v => v.path);
|
|
222
|
+
for (const variable of usedVariables) {
|
|
223
|
+
if (!definedPaths.includes(variable) && !this.isNestedPath(variable, definedPaths)) {
|
|
224
|
+
errors.push({
|
|
225
|
+
message: `Undefined variable: ${variable}`,
|
|
226
|
+
path: variable
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return {
|
|
231
|
+
isValid: errors.length === 0,
|
|
232
|
+
errors,
|
|
233
|
+
warnings: []
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Sync all templates to providers
|
|
238
|
+
*/
|
|
239
|
+
async syncAllTemplates() {
|
|
240
|
+
const templates = Array.from(this.templateStore.values());
|
|
241
|
+
const results = [];
|
|
242
|
+
for (const template of templates) {
|
|
243
|
+
const result = await this.syncTemplate(template.id);
|
|
244
|
+
results.push(result);
|
|
245
|
+
}
|
|
246
|
+
const synced = results.filter(r => r.success).length;
|
|
247
|
+
return {
|
|
248
|
+
success: synced > 0,
|
|
249
|
+
total: results.length,
|
|
250
|
+
synced,
|
|
251
|
+
failed: results.length - synced,
|
|
252
|
+
results
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
// ======================
|
|
256
|
+
// Sender Management
|
|
257
|
+
// ======================
|
|
258
|
+
/**
|
|
259
|
+
* Create a new sender configuration
|
|
260
|
+
*/
|
|
261
|
+
async createSender(sender) {
|
|
262
|
+
try {
|
|
263
|
+
// Validate email format
|
|
264
|
+
if (!this.validateEmail(sender.fromEmail)) {
|
|
265
|
+
throw new Error(`Invalid email format: ${sender.fromEmail}`);
|
|
266
|
+
}
|
|
267
|
+
const id = uuidv4();
|
|
268
|
+
const now = new Date();
|
|
269
|
+
const internalSender = {
|
|
270
|
+
id,
|
|
271
|
+
name: sender.name,
|
|
272
|
+
fromEmail: sender.fromEmail,
|
|
273
|
+
fromName: sender.fromName,
|
|
274
|
+
replyToEmail: sender.replyToEmail,
|
|
275
|
+
provider: sender.provider,
|
|
276
|
+
priority: sender.priority ?? 100,
|
|
277
|
+
isDefault: sender.isDefault ?? false,
|
|
278
|
+
isActive: true,
|
|
279
|
+
isVerified: sender.isVerified ?? false,
|
|
280
|
+
verificationStatus: sender.verificationStatus ?? 'pending',
|
|
281
|
+
createdAt: now,
|
|
282
|
+
updatedAt: now
|
|
283
|
+
};
|
|
284
|
+
this.senderStore.set(id, internalSender);
|
|
285
|
+
return this.mapToEnhancedSender(internalSender);
|
|
286
|
+
}
|
|
287
|
+
catch (error) {
|
|
288
|
+
console.error('Failed to create sender:', error);
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Get a sender by ID
|
|
294
|
+
*/
|
|
295
|
+
async getSender(senderId) {
|
|
296
|
+
const sender = this.senderStore.get(senderId);
|
|
297
|
+
return sender ? this.mapToEnhancedSender(sender) : null;
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* List all senders
|
|
301
|
+
*/
|
|
302
|
+
async listSenders() {
|
|
303
|
+
const items = Array.from(this.senderStore.values()).map(s => this.mapToEnhancedSender(s));
|
|
304
|
+
return { items, total: items.length };
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Select the best sender for an email
|
|
308
|
+
*/
|
|
309
|
+
async selectBestSender(fromEmail, provider, options = {}) {
|
|
310
|
+
const senders = Array.from(this.senderStore.values())
|
|
311
|
+
.filter(s => s.isActive)
|
|
312
|
+
.filter(s => !options.excludeIds?.includes(s.id))
|
|
313
|
+
.filter(s => options.allowUnverified || s.isVerified)
|
|
314
|
+
.filter(s => !provider || s.provider === provider);
|
|
315
|
+
if (senders.length === 0) {
|
|
316
|
+
return null;
|
|
317
|
+
}
|
|
318
|
+
// Strategy selection
|
|
319
|
+
let selected;
|
|
320
|
+
const strategy = options.strategy || 'domain_match';
|
|
321
|
+
if (strategy === 'domain_match') {
|
|
322
|
+
// Try to match domain
|
|
323
|
+
const domain = this.extractDomain(fromEmail);
|
|
324
|
+
selected = senders.find(s => this.extractDomain(s.fromEmail) === domain);
|
|
325
|
+
}
|
|
326
|
+
// Fallback to priority-based selection
|
|
327
|
+
if (!selected) {
|
|
328
|
+
senders.sort((a, b) => a.priority - b.priority);
|
|
329
|
+
selected = senders[0];
|
|
330
|
+
}
|
|
331
|
+
return selected ? this.mapToEnhancedSender(selected) : null;
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Validate a sender configuration
|
|
335
|
+
*/
|
|
336
|
+
async validateSender(senderId) {
|
|
337
|
+
const sender = this.senderStore.get(senderId);
|
|
338
|
+
if (!sender) {
|
|
339
|
+
return {
|
|
340
|
+
isValid: false,
|
|
341
|
+
errors: [{ field: 'id', message: `Sender not found: ${senderId}` }],
|
|
342
|
+
warnings: []
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
const errors = [];
|
|
346
|
+
const warnings = [];
|
|
347
|
+
// Basic validation
|
|
348
|
+
if (!sender.fromEmail) {
|
|
349
|
+
errors.push({ field: 'fromEmail', message: 'From email is required' });
|
|
350
|
+
}
|
|
351
|
+
else if (!this.validateEmail(sender.fromEmail)) {
|
|
352
|
+
errors.push({ field: 'fromEmail', message: 'Invalid email format' });
|
|
353
|
+
}
|
|
354
|
+
if (!sender.fromName) {
|
|
355
|
+
warnings.push({ field: 'fromName', message: 'From name is recommended' });
|
|
356
|
+
}
|
|
357
|
+
if (!sender.isVerified) {
|
|
358
|
+
warnings.push({ field: 'isVerified', message: 'Sender is not verified' });
|
|
359
|
+
}
|
|
360
|
+
return {
|
|
361
|
+
isValid: errors.length === 0,
|
|
362
|
+
errors,
|
|
363
|
+
warnings,
|
|
364
|
+
metadata: {
|
|
365
|
+
lastValidated: new Date(),
|
|
366
|
+
validatedBy: 'email-manager',
|
|
367
|
+
checks: ['basic', 'email-format', 'verification-status']
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
// ======================
|
|
372
|
+
// Variable Management
|
|
373
|
+
// ======================
|
|
374
|
+
/**
|
|
375
|
+
* Register a variable context
|
|
376
|
+
*/
|
|
377
|
+
async registerVariableContext(name, contextDef) {
|
|
378
|
+
this.variableContexts.set(name, {
|
|
379
|
+
name,
|
|
380
|
+
description: contextDef.description,
|
|
381
|
+
variables: contextDef.variables
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Get a variable context
|
|
386
|
+
*/
|
|
387
|
+
async getVariableContext(name) {
|
|
388
|
+
return this.variableContexts.get(name) || null;
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Get variable suggestions for a context
|
|
392
|
+
*/
|
|
393
|
+
async getVariableSuggestions(context, prefix) {
|
|
394
|
+
const variableContext = this.variableContexts.get(context);
|
|
395
|
+
if (!variableContext) {
|
|
396
|
+
return [];
|
|
397
|
+
}
|
|
398
|
+
let variables = variableContext.variables;
|
|
399
|
+
if (prefix) {
|
|
400
|
+
variables = variables.filter(v => v.path.startsWith(prefix));
|
|
401
|
+
}
|
|
402
|
+
return variables.map((v, index) => ({
|
|
403
|
+
path: v.path,
|
|
404
|
+
type: v.type,
|
|
405
|
+
description: v.description,
|
|
406
|
+
required: v.required ?? false,
|
|
407
|
+
score: 100 - index // Simple scoring based on order
|
|
408
|
+
}));
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Validate template data against a context
|
|
412
|
+
*/
|
|
413
|
+
async validateTemplateData(context, data) {
|
|
414
|
+
const variableContext = this.variableContexts.get(context);
|
|
415
|
+
if (!variableContext) {
|
|
416
|
+
return {
|
|
417
|
+
isValid: true,
|
|
418
|
+
errors: [],
|
|
419
|
+
warnings: [],
|
|
420
|
+
info: [`Variable context '${context}' not registered`]
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
const errors = [];
|
|
424
|
+
// Check required variables
|
|
425
|
+
for (const variable of variableContext.variables) {
|
|
426
|
+
if (variable.required) {
|
|
427
|
+
const value = this.getNestedValue(data, variable.path);
|
|
428
|
+
if (value === undefined) {
|
|
429
|
+
errors.push({
|
|
430
|
+
message: `Required variable missing: ${variable.path}`,
|
|
431
|
+
path: variable.path
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
return {
|
|
437
|
+
isValid: errors.length === 0,
|
|
438
|
+
errors,
|
|
439
|
+
warnings: []
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
// ======================
|
|
443
|
+
// System Operations
|
|
444
|
+
// ======================
|
|
445
|
+
/**
|
|
446
|
+
* Bootstrap the email manager system
|
|
447
|
+
*/
|
|
448
|
+
async bootstrap() {
|
|
449
|
+
const result = {
|
|
450
|
+
success: true,
|
|
451
|
+
components: {},
|
|
452
|
+
errors: [],
|
|
453
|
+
warnings: []
|
|
454
|
+
};
|
|
455
|
+
try {
|
|
456
|
+
// 1. Bootstrap senders
|
|
457
|
+
if (this.enhancedConfig.bootstrap?.senders?.enabled) {
|
|
458
|
+
const senderResult = await this.bootstrapSenders();
|
|
459
|
+
result.components.senders = senderResult;
|
|
460
|
+
if (!senderResult.success) {
|
|
461
|
+
result.errors.push(...senderResult.errors);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
// 2. Bootstrap variable contexts
|
|
465
|
+
if (this.enhancedConfig.bootstrap?.variables?.enabled) {
|
|
466
|
+
const variableResult = await this.bootstrapVariableContexts();
|
|
467
|
+
result.components.variables = variableResult;
|
|
468
|
+
if (!variableResult.success) {
|
|
469
|
+
result.errors.push(...variableResult.errors);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
// 3. Bootstrap templates
|
|
473
|
+
if (this.enhancedConfig.bootstrap?.templates?.enabled) {
|
|
474
|
+
const templateResult = await this.bootstrapTemplates();
|
|
475
|
+
result.components.templates = templateResult;
|
|
476
|
+
if (!templateResult.success) {
|
|
477
|
+
result.errors.push(...templateResult.errors);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
// 4. Bootstrap providers
|
|
481
|
+
if (this.enhancedConfig.bootstrap?.providers?.enabled) {
|
|
482
|
+
const providerResult = await this.bootstrapProviders();
|
|
483
|
+
result.components.providers = providerResult;
|
|
484
|
+
if (!providerResult.success) {
|
|
485
|
+
result.warnings.push(...providerResult.errors);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
// 5. Final health check
|
|
489
|
+
const health = await this.getSystemHealth();
|
|
490
|
+
if (health.overall.status === 'unhealthy') {
|
|
491
|
+
result.warnings.push('System health check indicates issues after bootstrap');
|
|
492
|
+
}
|
|
493
|
+
this.initialized = true;
|
|
494
|
+
return result;
|
|
495
|
+
}
|
|
496
|
+
catch (error) {
|
|
497
|
+
result.success = false;
|
|
498
|
+
result.errors.push(`Bootstrap failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
499
|
+
return result;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* Get comprehensive system health report
|
|
504
|
+
*/
|
|
505
|
+
async getSystemHealth() {
|
|
506
|
+
const emailStats = await this.getEmailDeliveryStats();
|
|
507
|
+
const templateStats = await this.getTemplateSystemStats();
|
|
508
|
+
const senderStats = await this.getSenderSystemStats();
|
|
509
|
+
const providerStats = await this.getProviderHealthStats();
|
|
510
|
+
const overallHealth = this.calculateOverallHealth([
|
|
511
|
+
emailStats,
|
|
512
|
+
templateStats,
|
|
513
|
+
senderStats,
|
|
514
|
+
providerStats
|
|
515
|
+
]);
|
|
516
|
+
return {
|
|
517
|
+
overall: overallHealth,
|
|
518
|
+
timestamp: new Date(),
|
|
519
|
+
components: {
|
|
520
|
+
email: emailStats,
|
|
521
|
+
templates: templateStats,
|
|
522
|
+
senders: senderStats,
|
|
523
|
+
providers: providerStats
|
|
524
|
+
},
|
|
525
|
+
recommendations: this.generateHealthRecommendations({
|
|
526
|
+
emailStats,
|
|
527
|
+
templateStats,
|
|
528
|
+
senderStats,
|
|
529
|
+
providerHealth: providerStats
|
|
530
|
+
})
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
// ======================
|
|
534
|
+
// Private Helper Methods
|
|
535
|
+
// ======================
|
|
536
|
+
async bootstrapSenders() {
|
|
537
|
+
const result = {
|
|
538
|
+
success: true,
|
|
539
|
+
created: [],
|
|
540
|
+
updated: [],
|
|
541
|
+
errors: []
|
|
542
|
+
};
|
|
543
|
+
const defaultSenders = this.enhancedConfig.bootstrap?.senders?.defaultSenders || [];
|
|
544
|
+
for (const senderDef of defaultSenders) {
|
|
545
|
+
try {
|
|
546
|
+
const created = await this.createSender(senderDef);
|
|
547
|
+
if (created) {
|
|
548
|
+
result.created.push(created.id);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
catch (error) {
|
|
552
|
+
result.errors.push(`Failed to bootstrap sender "${senderDef.name}": ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
553
|
+
result.success = false;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
return result;
|
|
557
|
+
}
|
|
558
|
+
async bootstrapVariableContexts() {
|
|
559
|
+
const result = {
|
|
560
|
+
success: true,
|
|
561
|
+
created: [],
|
|
562
|
+
updated: [],
|
|
563
|
+
errors: []
|
|
564
|
+
};
|
|
565
|
+
const contexts = this.enhancedConfig.bootstrap?.variables?.contexts || [];
|
|
566
|
+
for (const contextDef of contexts) {
|
|
567
|
+
try {
|
|
568
|
+
await this.registerVariableContext(contextDef.name, contextDef);
|
|
569
|
+
result.created.push(contextDef.name);
|
|
570
|
+
}
|
|
571
|
+
catch (error) {
|
|
572
|
+
result.errors.push(`Failed to bootstrap context "${contextDef.name}": ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
573
|
+
result.success = false;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
return result;
|
|
577
|
+
}
|
|
578
|
+
async bootstrapTemplates() {
|
|
579
|
+
const result = {
|
|
580
|
+
success: true,
|
|
581
|
+
created: [],
|
|
582
|
+
updated: [],
|
|
583
|
+
errors: []
|
|
584
|
+
};
|
|
585
|
+
const defaultTemplates = this.enhancedConfig.bootstrap?.templates?.defaultTemplates || [];
|
|
586
|
+
for (const templateDef of defaultTemplates) {
|
|
587
|
+
try {
|
|
588
|
+
const createResult = await this.createEnhancedTemplate(templateDef);
|
|
589
|
+
if (createResult.success && createResult.templateId) {
|
|
590
|
+
result.created.push(createResult.templateId);
|
|
591
|
+
// Sync to providers if enabled
|
|
592
|
+
if (this.enhancedConfig.bootstrap?.templates?.syncToProviders) {
|
|
593
|
+
await this.syncTemplate(createResult.templateId);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
else {
|
|
597
|
+
result.errors.push(...(createResult.errors || []));
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
catch (error) {
|
|
601
|
+
result.errors.push(`Failed to bootstrap template "${templateDef.name}": ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
602
|
+
result.success = false;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
return result;
|
|
606
|
+
}
|
|
607
|
+
async bootstrapProviders() {
|
|
608
|
+
const result = {
|
|
609
|
+
success: true,
|
|
610
|
+
created: [],
|
|
611
|
+
updated: [],
|
|
612
|
+
errors: []
|
|
613
|
+
};
|
|
614
|
+
// Test provider connections if enabled
|
|
615
|
+
if (this.enhancedConfig.bootstrap?.providers?.testConnections) {
|
|
616
|
+
const providers = await this.listProviders();
|
|
617
|
+
for (const provider of providers.providers) {
|
|
618
|
+
try {
|
|
619
|
+
const testResult = await this.testConnection(provider.id);
|
|
620
|
+
if (testResult.success) {
|
|
621
|
+
result.updated.push(provider.id);
|
|
622
|
+
}
|
|
623
|
+
else {
|
|
624
|
+
result.errors.push(`Provider "${provider.name}" connection test failed: ${testResult.message}`);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
catch (error) {
|
|
628
|
+
result.errors.push(`Provider "${provider.name}" test error: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
return result;
|
|
633
|
+
}
|
|
634
|
+
async getEmailDeliveryStats() {
|
|
635
|
+
const summary = await this.getAnalyticsSummary(7);
|
|
636
|
+
const bounceRate = summary.bounceRate || 0;
|
|
637
|
+
const issues = [];
|
|
638
|
+
if (bounceRate > 5) {
|
|
639
|
+
issues.push({
|
|
640
|
+
severity: 'warning',
|
|
641
|
+
message: 'High bounce rate detected',
|
|
642
|
+
details: `Current bounce rate: ${bounceRate.toFixed(2)}%`
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
return {
|
|
646
|
+
status: bounceRate > 10 ? 'unhealthy' : bounceRate > 5 ? 'degraded' : 'healthy',
|
|
647
|
+
score: Math.max(0, 100 - bounceRate * 10),
|
|
648
|
+
metrics: {
|
|
649
|
+
totalSent: summary.totalSent || 0,
|
|
650
|
+
totalDelivered: summary.totalDelivered || 0,
|
|
651
|
+
bounceRate,
|
|
652
|
+
deliveryRate: summary.deliveryRate || 100
|
|
653
|
+
},
|
|
654
|
+
issues
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
async getTemplateSystemStats() {
|
|
658
|
+
const templates = Array.from(this.templateStore.values());
|
|
659
|
+
const activeTemplates = templates.filter(t => t.isActive);
|
|
660
|
+
return {
|
|
661
|
+
status: 'healthy',
|
|
662
|
+
score: 100,
|
|
663
|
+
metrics: {
|
|
664
|
+
totalTemplates: templates.length,
|
|
665
|
+
activeTemplates: activeTemplates.length
|
|
666
|
+
},
|
|
667
|
+
issues: []
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
async getSenderSystemStats() {
|
|
671
|
+
const senders = Array.from(this.senderStore.values());
|
|
672
|
+
const activeSenders = senders.filter(s => s.isActive);
|
|
673
|
+
const verifiedSenders = activeSenders.filter(s => s.isVerified);
|
|
674
|
+
const defaultSenders = senders.filter(s => s.isDefault);
|
|
675
|
+
const issues = [];
|
|
676
|
+
if (senders.length > 0 && defaultSenders.length === 0) {
|
|
677
|
+
issues.push({
|
|
678
|
+
severity: 'error',
|
|
679
|
+
message: 'No default sender configured'
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
const verificationRate = activeSenders.length > 0 ?
|
|
683
|
+
(verifiedSenders.length / activeSenders.length) * 100 : 100;
|
|
684
|
+
if (verificationRate < 50 && activeSenders.length > 0) {
|
|
685
|
+
issues.push({
|
|
686
|
+
severity: 'warning',
|
|
687
|
+
message: 'Low sender verification rate',
|
|
688
|
+
details: `Only ${Math.round(verificationRate)}% of active senders are verified`
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
return {
|
|
692
|
+
status: issues.some(i => i.severity === 'error') ? 'unhealthy' :
|
|
693
|
+
issues.length > 0 ? 'degraded' : 'healthy',
|
|
694
|
+
score: Math.max(0, 100 - issues.length * 20),
|
|
695
|
+
metrics: {
|
|
696
|
+
totalSenders: senders.length,
|
|
697
|
+
activeSenders: activeSenders.length,
|
|
698
|
+
verifiedSenders: verifiedSenders.length,
|
|
699
|
+
defaultSenders: defaultSenders.length,
|
|
700
|
+
verificationRate: Math.round(verificationRate)
|
|
701
|
+
},
|
|
702
|
+
issues
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
async getProviderHealthStats() {
|
|
706
|
+
const providers = await this.listProviders();
|
|
707
|
+
const activeProviders = providers.providers.filter(p => p.isActive);
|
|
708
|
+
const issues = [];
|
|
709
|
+
if (activeProviders.length === 0) {
|
|
710
|
+
issues.push({
|
|
711
|
+
severity: 'error',
|
|
712
|
+
message: 'No active email providers configured'
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
return {
|
|
716
|
+
status: activeProviders.length === 0 ? 'unhealthy' : 'healthy',
|
|
717
|
+
score: activeProviders.length > 0 ? 100 : 0,
|
|
718
|
+
metrics: {
|
|
719
|
+
totalProviders: providers.providers.length,
|
|
720
|
+
activeProviders: activeProviders.length
|
|
721
|
+
},
|
|
722
|
+
issues
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
calculateOverallHealth(components) {
|
|
726
|
+
const avgScore = components.reduce((sum, c) => sum + c.score, 0) / components.length;
|
|
727
|
+
const hasUnhealthy = components.some(c => c.status === 'unhealthy');
|
|
728
|
+
const hasDegraded = components.some(c => c.status === 'degraded');
|
|
729
|
+
return {
|
|
730
|
+
status: hasUnhealthy ? 'unhealthy' : hasDegraded ? 'degraded' : 'healthy',
|
|
731
|
+
score: Math.round(avgScore),
|
|
732
|
+
metrics: {
|
|
733
|
+
componentsHealthy: components.filter(c => c.status === 'healthy').length,
|
|
734
|
+
componentsDegraded: components.filter(c => c.status === 'degraded').length,
|
|
735
|
+
componentsUnhealthy: components.filter(c => c.status === 'unhealthy').length
|
|
736
|
+
},
|
|
737
|
+
issues: components.flatMap(c => c.issues)
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
generateHealthRecommendations(stats) {
|
|
741
|
+
const recommendations = [];
|
|
742
|
+
// Email delivery recommendations
|
|
743
|
+
if (stats.emailStats.metrics.bounceRate > 5) {
|
|
744
|
+
recommendations.push({
|
|
745
|
+
priority: 'high',
|
|
746
|
+
category: 'delivery',
|
|
747
|
+
title: 'High Bounce Rate Detected',
|
|
748
|
+
description: 'Consider reviewing sender reputation and email list quality',
|
|
749
|
+
action: 'Review and clean email lists, check sender verification status'
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
// Sender verification recommendations
|
|
753
|
+
if (stats.senderStats.metrics.verificationRate < 80) {
|
|
754
|
+
recommendations.push({
|
|
755
|
+
priority: 'medium',
|
|
756
|
+
category: 'senders',
|
|
757
|
+
title: 'Low Sender Verification Rate',
|
|
758
|
+
description: 'Improve email deliverability by verifying more senders',
|
|
759
|
+
action: 'Complete sender verification process with email providers'
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
// Provider health recommendations
|
|
763
|
+
if (stats.providerHealth.metrics.activeProviders === 0) {
|
|
764
|
+
recommendations.push({
|
|
765
|
+
priority: 'high',
|
|
766
|
+
category: 'providers',
|
|
767
|
+
title: 'No Active Providers',
|
|
768
|
+
description: 'Email sending will fail without active providers',
|
|
769
|
+
action: 'Configure and activate at least one email provider'
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
return recommendations;
|
|
773
|
+
}
|
|
774
|
+
renderSimpleTemplate(template, data) {
|
|
775
|
+
let result = template;
|
|
776
|
+
const flattenObject = (obj, prefix = '') => {
|
|
777
|
+
const flattened = {};
|
|
778
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
779
|
+
const path = prefix ? `${prefix}.${key}` : key;
|
|
780
|
+
if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
|
|
781
|
+
Object.assign(flattened, flattenObject(value, path));
|
|
782
|
+
}
|
|
783
|
+
else {
|
|
784
|
+
flattened[path] = String(value ?? '');
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
return flattened;
|
|
788
|
+
};
|
|
789
|
+
const flatData = flattenObject(data);
|
|
790
|
+
for (const [key, value] of Object.entries(flatData)) {
|
|
791
|
+
const patterns = [
|
|
792
|
+
new RegExp(`\\{\\{\\s*${key.replace(/\./g, '\\.')}\\s*\\}\\}`, 'g')
|
|
793
|
+
];
|
|
794
|
+
for (const pattern of patterns) {
|
|
795
|
+
result = result.replace(pattern, value);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
return result;
|
|
799
|
+
}
|
|
800
|
+
mapToEnhancedSender(sender) {
|
|
801
|
+
return {
|
|
802
|
+
id: sender.id,
|
|
803
|
+
name: sender.name,
|
|
804
|
+
fromEmail: sender.fromEmail,
|
|
805
|
+
fromName: sender.fromName,
|
|
806
|
+
replyToEmail: sender.replyToEmail,
|
|
807
|
+
provider: sender.provider,
|
|
808
|
+
priority: sender.priority,
|
|
809
|
+
isDefault: sender.isDefault,
|
|
810
|
+
isActive: sender.isActive,
|
|
811
|
+
isVerified: sender.isVerified,
|
|
812
|
+
verificationStatus: sender.verificationStatus,
|
|
813
|
+
createdAt: sender.createdAt,
|
|
814
|
+
updatedAt: sender.updatedAt
|
|
815
|
+
};
|
|
816
|
+
}
|
|
817
|
+
extractDomain(email) {
|
|
818
|
+
const parts = email.split('@');
|
|
819
|
+
return parts.length > 1 ? parts[1].toLowerCase() : '';
|
|
820
|
+
}
|
|
821
|
+
isNestedPath(path, definedPaths) {
|
|
822
|
+
// Check if the path is a child of any defined paths
|
|
823
|
+
for (const defined of definedPaths) {
|
|
824
|
+
if (path.startsWith(defined + '.')) {
|
|
825
|
+
return true;
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
return false;
|
|
829
|
+
}
|
|
830
|
+
getNestedValue(obj, path) {
|
|
831
|
+
const parts = path.split('.');
|
|
832
|
+
let current = obj;
|
|
833
|
+
for (const part of parts) {
|
|
834
|
+
if (current === null || current === undefined || typeof current !== 'object') {
|
|
835
|
+
return undefined;
|
|
836
|
+
}
|
|
837
|
+
current = current[part];
|
|
838
|
+
}
|
|
839
|
+
return current;
|
|
840
|
+
}
|
|
841
|
+
/**
|
|
842
|
+
* Check if the enhanced manager is initialized
|
|
843
|
+
*/
|
|
844
|
+
isInitialized() {
|
|
845
|
+
return this.initialized;
|
|
846
|
+
}
|
|
847
|
+
/**
|
|
848
|
+
* Get template store size
|
|
849
|
+
*/
|
|
850
|
+
getTemplateCount() {
|
|
851
|
+
return this.templateStore.size;
|
|
852
|
+
}
|
|
853
|
+
/**
|
|
854
|
+
* Get sender store size
|
|
855
|
+
*/
|
|
856
|
+
getSenderCount() {
|
|
857
|
+
return this.senderStore.size;
|
|
858
|
+
}
|
|
859
|
+
/**
|
|
860
|
+
* Get variable context count
|
|
861
|
+
*/
|
|
862
|
+
getVariableContextCount() {
|
|
863
|
+
return this.variableContexts.size;
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
//# sourceMappingURL=enhanced-email-manager.js.map
|