@bernierllc/email 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +76 -217
  2. package/dist/index.d.ts +3 -0
  3. package/dist/index.d.ts.map +1 -0
  4. package/dist/index.js +28 -0
  5. package/dist/index.js.map +1 -0
  6. package/dist/simple-email-service.d.ts +58 -0
  7. package/dist/simple-email-service.d.ts.map +1 -0
  8. package/dist/simple-email-service.js +416 -0
  9. package/dist/simple-email-service.js.map +1 -0
  10. package/dist/types.d.ts +311 -0
  11. package/dist/types.d.ts.map +1 -0
  12. package/dist/types.js +33 -0
  13. package/dist/types.js.map +1 -0
  14. package/package.json +53 -22
  15. package/.eslintrc.json +0 -112
  16. package/.flake8 +0 -18
  17. package/.github/workflows/ci.yml +0 -300
  18. package/EXTRACTION_SUMMARY.md +0 -265
  19. package/IMPLEMENTATION_STATUS.md +0 -159
  20. package/OPEN_SOURCE_SETUP.md +0 -420
  21. package/PACKAGE_USAGE.md +0 -471
  22. package/examples/fastapi-example/main.py +0 -257
  23. package/examples/nextjs-example/next-env.d.ts +0 -13
  24. package/examples/nextjs-example/package.json +0 -26
  25. package/examples/nextjs-example/pages/admin/templates.tsx +0 -157
  26. package/examples/nextjs-example/tsconfig.json +0 -28
  27. package/packages/core/package.json +0 -70
  28. package/packages/core/rollup.config.js +0 -37
  29. package/packages/core/specification.md +0 -416
  30. package/packages/core/src/adapters/supabase.ts +0 -291
  31. package/packages/core/src/core/scheduler.ts +0 -356
  32. package/packages/core/src/core/template-manager.ts +0 -388
  33. package/packages/core/src/index.ts +0 -30
  34. package/packages/core/src/providers/base.ts +0 -104
  35. package/packages/core/src/providers/sendgrid.ts +0 -368
  36. package/packages/core/src/types/provider.ts +0 -91
  37. package/packages/core/src/types/scheduled.ts +0 -78
  38. package/packages/core/src/types/template.ts +0 -97
  39. package/packages/core/tsconfig.json +0 -23
  40. package/packages/python/README.md +0 -106
  41. package/packages/python/email_template_manager/__init__.py +0 -66
  42. package/packages/python/email_template_manager/config.py +0 -98
  43. package/packages/python/email_template_manager/core/magic_links.py +0 -245
  44. package/packages/python/email_template_manager/core/manager.py +0 -344
  45. package/packages/python/email_template_manager/core/scheduler.py +0 -473
  46. package/packages/python/email_template_manager/exceptions.py +0 -67
  47. package/packages/python/email_template_manager/models/magic_link.py +0 -59
  48. package/packages/python/email_template_manager/models/scheduled.py +0 -78
  49. package/packages/python/email_template_manager/models/template.py +0 -90
  50. package/packages/python/email_template_manager/providers/aws_ses.py +0 -44
  51. package/packages/python/email_template_manager/providers/base.py +0 -94
  52. package/packages/python/email_template_manager/providers/sendgrid.py +0 -325
  53. package/packages/python/email_template_manager/providers/smtp.py +0 -44
  54. package/packages/python/pyproject.toml +0 -133
  55. package/packages/python/setup.py +0 -93
  56. package/packages/python/specification.md +0 -930
  57. package/packages/react/README.md +0 -13
  58. package/packages/react/package.json +0 -105
  59. package/packages/react/rollup.config.js +0 -37
  60. package/packages/react/specification.md +0 -569
  61. package/packages/react/src/index.ts +0 -20
  62. package/packages/react/tsconfig.json +0 -24
  63. package/src/index.js +0 -1
  64. package/test_package.py +0 -125
@@ -1,388 +0,0 @@
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
-
9
- /**
10
- * Email Template Manager - Core template management
11
- */
12
-
13
- import { Liquid } from 'liquidjs';
14
- import { EmailTemplate, RenderedTemplate, TemplateFilters, ValidationResult } from '../types/template';
15
-
16
- export interface DatabaseConfig {
17
- type: 'memory' | 'supabase' | 'postgresql' | 'mysql';
18
- connectionString?: string;
19
- supabaseUrl?: string;
20
- supabaseKey?: string;
21
- tableName?: string;
22
- }
23
-
24
- export class EmailTemplateManager {
25
- private templates: Map<string, EmailTemplate> = new Map();
26
- private liquid: Liquid;
27
- private dbConfig: DatabaseConfig;
28
- private supabaseAdapter?: SupabaseEmailAdapter;
29
-
30
- constructor(config: { database: DatabaseConfig }) {
31
- this.dbConfig = config.database;
32
- this.liquid = new Liquid({
33
- strictFilters: false,
34
- strictVariables: false,
35
- });
36
-
37
- // Initialize database connection
38
- this.initializeDatabase();
39
- }
40
-
41
- private async initializeDatabase() {
42
- if (this.dbConfig.type === 'memory') {
43
- // Use in-memory storage for development/testing
44
- return;
45
- }
46
-
47
- if (this.dbConfig.type === 'supabase') {
48
- if (!this.dbConfig.supabaseUrl || !this.dbConfig.supabaseKey) {
49
- throw new Error('Supabase URL and key are required for Supabase database type');
50
- }
51
-
52
- this.supabaseAdapter = new SupabaseEmailAdapter({
53
- url: this.dbConfig.supabaseUrl,
54
- key: this.dbConfig.supabaseKey,
55
- templatesTable: this.dbConfig.tableName,
56
- });
57
- }
58
-
59
- // For other database types, implement connection logic
60
- // This will be extended based on the database type
61
- }
62
-
63
- /**
64
- * Create a new email template
65
- */
66
- async createTemplate(template: Omit<EmailTemplate, 'id' | 'createdAt' | 'updatedAt'>): Promise<EmailTemplate> {
67
- const newTemplate: EmailTemplate = {
68
- ...template,
69
- id: this.generateId(),
70
- createdAt: new Date(),
71
- updatedAt: new Date(),
72
- };
73
-
74
- // Validate template
75
- const validation = this.validateTemplate(newTemplate);
76
- if (!validation.isValid) {
77
- throw new Error(`Template validation failed: ${validation.errors.map(e => e.message).join(', ')}`);
78
- }
79
-
80
- // Store template
81
- if (this.dbConfig.type === 'memory') {
82
- this.templates.set(newTemplate.id!, newTemplate);
83
- } else {
84
- await this.saveTemplateToDatabase(newTemplate);
85
- }
86
-
87
- return newTemplate;
88
- }
89
-
90
- /**
91
- * Get template by ID
92
- */
93
- async getTemplate(id: string): Promise<EmailTemplate | null> {
94
- if (this.dbConfig.type === 'memory') {
95
- return this.templates.get(id) || null;
96
- }
97
-
98
- return await this.loadTemplateFromDatabase(id);
99
- }
100
-
101
- /**
102
- * Get template by name
103
- */
104
- async getTemplateByName(name: string): Promise<EmailTemplate | null> {
105
- if (this.dbConfig.type === 'memory') {
106
- for (const template of this.templates.values()) {
107
- if (template.name === name) {
108
- return template;
109
- }
110
- }
111
- return null;
112
- }
113
-
114
- return await this.loadTemplateFromDatabaseByName(name);
115
- }
116
-
117
- /**
118
- * Update an existing template
119
- */
120
- async updateTemplate(id: string, updates: Partial<EmailTemplate>): Promise<EmailTemplate> {
121
- const existingTemplate = await this.getTemplate(id);
122
- if (!existingTemplate) {
123
- throw new Error(`Template with id ${id} not found`);
124
- }
125
-
126
- const updatedTemplate: EmailTemplate = {
127
- ...existingTemplate,
128
- ...updates,
129
- id: existingTemplate.id,
130
- updatedAt: new Date(),
131
- };
132
-
133
- // Validate updated template
134
- const validation = this.validateTemplate(updatedTemplate);
135
- if (!validation.isValid) {
136
- throw new Error(`Template validation failed: ${validation.errors.map(e => e.message).join(', ')}`);
137
- }
138
-
139
- // Store updated template
140
- if (this.dbConfig.type === 'memory') {
141
- this.templates.set(id, updatedTemplate);
142
- } else {
143
- await this.saveTemplateToDatabase(updatedTemplate);
144
- }
145
-
146
- return updatedTemplate;
147
- }
148
-
149
- /**
150
- * Delete a template
151
- */
152
- async deleteTemplate(id: string): Promise<boolean> {
153
- if (this.dbConfig.type === 'memory') {
154
- return this.templates.delete(id);
155
- }
156
-
157
- return await this.deleteTemplateFromDatabase(id);
158
- }
159
-
160
- /**
161
- * List templates with optional filtering
162
- */
163
- async listTemplates(filters?: TemplateFilters): Promise<EmailTemplate[]> {
164
- let templates: EmailTemplate[];
165
-
166
- if (this.dbConfig.type === 'memory') {
167
- templates = Array.from(this.templates.values());
168
- } else {
169
- templates = await this.loadTemplatesFromDatabase(filters);
170
- }
171
-
172
- // Apply filters if provided
173
- if (filters) {
174
- templates = this.applyFilters(templates, filters);
175
- }
176
-
177
- return templates;
178
- }
179
-
180
- /**
181
- * Render a template with variables
182
- */
183
- async renderTemplate(templateId: string, variables: Record<string, any>): Promise<RenderedTemplate> {
184
- const template = await this.getTemplate(templateId);
185
- if (!template) {
186
- throw new Error(`Template with id ${templateId} not found`);
187
- }
188
-
189
- return this.renderTemplateObject(template, variables);
190
- }
191
-
192
- /**
193
- * Render a template by name with variables
194
- */
195
- async renderTemplateByName(templateName: string, variables: Record<string, any>): Promise<RenderedTemplate> {
196
- const template = await this.getTemplateByName(templateName);
197
- if (!template) {
198
- throw new Error(`Template with name ${templateName} not found`);
199
- }
200
-
201
- return this.renderTemplateObject(template, variables);
202
- }
203
-
204
- /**
205
- * Render a template object with variables
206
- */
207
- private async renderTemplateObject(template: EmailTemplate, variables: Record<string, any>): Promise<RenderedTemplate> {
208
- try {
209
- // Validate required variables
210
- const missingVariables = template.variables
211
- .filter(v => v.required && !(v.name in variables))
212
- .map(v => v.name);
213
-
214
- if (missingVariables.length > 0) {
215
- throw new Error(`Missing required variables: ${missingVariables.join(', ')}`);
216
- }
217
-
218
- // Render template parts
219
- const subject = await this.liquid.parseAndRender(template.subject, variables);
220
- const htmlContent = await this.liquid.parseAndRender(template.htmlBody, variables);
221
- const textContent = await this.liquid.parseAndRender(template.textBody, variables);
222
-
223
- return {
224
- subject,
225
- htmlContent,
226
- textContent,
227
- variables,
228
- templateId: template.id!,
229
- };
230
- } catch (error) {
231
- throw new Error(`Template rendering failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
232
- }
233
- }
234
-
235
- /**
236
- * Validate a template
237
- */
238
- private validateTemplate(template: EmailTemplate): ValidationResult {
239
- const errors: any[] = [];
240
- const warnings: any[] = [];
241
-
242
- // Required fields
243
- if (!template.name) {
244
- errors.push({ field: 'name', message: 'Template name is required', code: 'REQUIRED' });
245
- }
246
-
247
- if (!template.subject) {
248
- errors.push({ field: 'subject', message: 'Template subject is required', code: 'REQUIRED' });
249
- }
250
-
251
- if (!template.htmlBody && !template.textBody) {
252
- errors.push({ field: 'body', message: 'Template must have either HTML or text body', code: 'REQUIRED' });
253
- }
254
-
255
- // Variable validation
256
- template.variables.forEach((variable, index) => {
257
- if (!variable.name) {
258
- errors.push({
259
- field: `variables[${index}].name`,
260
- message: 'Variable name is required',
261
- code: 'REQUIRED'
262
- });
263
- }
264
-
265
- if (!variable.type) {
266
- errors.push({
267
- field: `variables[${index}].type`,
268
- message: 'Variable type is required',
269
- code: 'REQUIRED'
270
- });
271
- }
272
- });
273
-
274
- // Check for duplicate variable names
275
- const variableNames = template.variables.map(v => v.name).filter(Boolean);
276
- const duplicates = variableNames.filter((name, index) => variableNames.indexOf(name) !== index);
277
- if (duplicates.length > 0) {
278
- errors.push({
279
- field: 'variables',
280
- message: `Duplicate variable names: ${duplicates.join(', ')}`,
281
- code: 'DUPLICATE'
282
- });
283
- }
284
-
285
- return {
286
- isValid: errors.length === 0,
287
- errors,
288
- warnings,
289
- };
290
- }
291
-
292
- /**
293
- * Apply filters to template list
294
- */
295
- private applyFilters(templates: EmailTemplate[], filters: TemplateFilters): EmailTemplate[] {
296
- return templates.filter(template => {
297
- if (filters.categoryId && template.categoryId !== filters.categoryId) {
298
- return false;
299
- }
300
-
301
- if (filters.isActive !== undefined && template.isActive !== filters.isActive) {
302
- return false;
303
- }
304
-
305
- if (filters.tags && filters.tags.length > 0) {
306
- const hasMatchingTag = filters.tags.some(tag => template.tags.includes(tag));
307
- if (!hasMatchingTag) {
308
- return false;
309
- }
310
- }
311
-
312
- if (filters.search) {
313
- const searchLower = filters.search.toLowerCase();
314
- const matchesSearch =
315
- template.name.toLowerCase().includes(searchLower) ||
316
- template.subject.toLowerCase().includes(searchLower) ||
317
- template.tags.some(tag => tag.toLowerCase().includes(searchLower));
318
-
319
- if (!matchesSearch) {
320
- return false;
321
- }
322
- }
323
-
324
- if (filters.createdBy && template.createdBy !== filters.createdBy) {
325
- return false;
326
- }
327
-
328
- if (filters.createdAfter && template.createdAt && template.createdAt < filters.createdAfter) {
329
- return false;
330
- }
331
-
332
- if (filters.createdBefore && template.createdAt && template.createdAt > filters.createdBefore) {
333
- return false;
334
- }
335
-
336
- return true;
337
- });
338
- }
339
-
340
- /**
341
- * Generate a unique ID
342
- */
343
- private generateId(): string {
344
- return `tpl_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
345
- }
346
-
347
- // Database methods (to be implemented based on database type)
348
- private async saveTemplateToDatabase(template: EmailTemplate): Promise<void> {
349
- if (this.dbConfig.type === 'supabase' && this.supabaseAdapter) {
350
- await this.supabaseAdapter.saveTemplate(template);
351
- return;
352
- }
353
-
354
- throw new Error(`Database operations not implemented for type: ${this.dbConfig.type}`);
355
- }
356
-
357
- private async loadTemplateFromDatabase(id: string): Promise<EmailTemplate | null> {
358
- if (this.dbConfig.type === 'supabase' && this.supabaseAdapter) {
359
- return await this.supabaseAdapter.loadTemplate(id);
360
- }
361
-
362
- throw new Error(`Database operations not implemented for type: ${this.dbConfig.type}`);
363
- }
364
-
365
- private async loadTemplateFromDatabaseByName(name: string): Promise<EmailTemplate | null> {
366
- if (this.dbConfig.type === 'supabase' && this.supabaseAdapter) {
367
- return await this.supabaseAdapter.loadTemplateByName(name);
368
- }
369
-
370
- throw new Error(`Database operations not implemented for type: ${this.dbConfig.type}`);
371
- }
372
-
373
- private async loadTemplatesFromDatabase(filters?: TemplateFilters): Promise<EmailTemplate[]> {
374
- if (this.dbConfig.type === 'supabase' && this.supabaseAdapter) {
375
- return await this.supabaseAdapter.loadTemplates(filters);
376
- }
377
-
378
- throw new Error(`Database operations not implemented for type: ${this.dbConfig.type}`);
379
- }
380
-
381
- private async deleteTemplateFromDatabase(id: string): Promise<boolean> {
382
- if (this.dbConfig.type === 'supabase' && this.supabaseAdapter) {
383
- return await this.supabaseAdapter.deleteTemplate(id);
384
- }
385
-
386
- throw new Error(`Database operations not implemented for type: ${this.dbConfig.type}`);
387
- }
388
- }
@@ -1,30 +0,0 @@
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
-
9
- /**
10
- * Email Template Manager - Core Library
11
- * Main entry point for the email template management system
12
- */
13
-
14
- // Core types
15
- export * from './types/provider';
16
- export * from './types/scheduled';
17
- export * from './types/template';
18
-
19
- // Core classes
20
- export { EmailScheduler } from './core/scheduler';
21
- export { EmailTemplateManager } from './core/template-manager';
22
-
23
- // Database adapters
24
- export { SupabaseEmailAdapter } from './adapters/supabase';
25
-
26
- // Base provider class and types
27
- export * from './providers/base';
28
-
29
- // SendGrid provider
30
- export { SendGridProvider } from './providers/sendgrid';
@@ -1,104 +0,0 @@
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
-
9
- /**
10
- * Base EmailProvider abstract class
11
- */
12
-
13
- import { EmailMessage, SendResult, DeliveryStatus, WebhookEvent, EmailProviderCapabilities } from '../types/provider';
14
-
15
- // Re-export types for consumers
16
- export { EmailMessage, SendResult, DeliveryStatus, WebhookEvent, EmailProviderCapabilities };
17
-
18
- export abstract class EmailProvider {
19
- protected config: Record<string, any>;
20
-
21
- constructor(config: Record<string, any>) {
22
- this.config = config;
23
- }
24
-
25
- /**
26
- * Send a single email
27
- */
28
- abstract sendEmail(email: EmailMessage): Promise<SendResult>;
29
-
30
- /**
31
- * Send multiple emails
32
- */
33
- abstract sendBatch(emails: EmailMessage[]): Promise<SendResult[]>;
34
-
35
- /**
36
- * Get delivery status for a message
37
- */
38
- abstract getDeliveryStatus(messageId: string): Promise<DeliveryStatus | null>;
39
-
40
- /**
41
- * Process webhook events from the provider
42
- */
43
- abstract processWebhook(payload: any, signature: string): Promise<WebhookEvent[]>;
44
-
45
- /**
46
- * Validate provider configuration
47
- */
48
- validateConfiguration(): boolean {
49
- return true;
50
- }
51
-
52
- /**
53
- * Get provider name
54
- */
55
- getProviderName(): string {
56
- return this.constructor.name.replace('Provider', '').toLowerCase();
57
- }
58
-
59
- /**
60
- * Get provider capabilities
61
- */
62
- getCapabilities(): EmailProviderCapabilities {
63
- return {
64
- supportsBatch: true,
65
- supportsTemplates: false,
66
- supportsWebhooks: false,
67
- supportsAttachments: true,
68
- supportsDeliveryTracking: false,
69
- maxBatchSize: 100,
70
- maxAttachmentSize: 25 * 1024 * 1024 // 25MB
71
- };
72
- }
73
-
74
- /**
75
- * Create a template in the provider (if supported)
76
- */
77
- async createTemplate?(templateData: {
78
- name: string;
79
- subject: string;
80
- htmlContent: string;
81
- textContent?: string;
82
- }): Promise<string>;
83
-
84
- /**
85
- * Send an email using a provider template (if supported)
86
- */
87
- async sendTemplateEmail?(
88
- templateId: string,
89
- toEmail: string,
90
- templateData: Record<string, any>,
91
- toName?: string
92
- ): Promise<SendResult>;
93
-
94
- /**
95
- * Test the provider connection
96
- */
97
- async testConnection(): Promise<boolean> {
98
- try {
99
- return this.validateConfiguration();
100
- } catch {
101
- return false;
102
- }
103
- }
104
- }