@bernierllc/email 1.0.1 → 1.1.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.
Files changed (65) 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 +57 -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/plans/email-template-manager_app-admin.md +0 -590
  64. package/src/index.js +0 -1
  65. package/test_package.py +0 -125
@@ -1,356 +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 Scheduler - Handles email scheduling and sending
11
- */
12
-
13
- import { EmailProvider } from '../providers/base';
14
- import { EmailMessage } from '../types/provider';
15
- import { EmailStatus, ScheduledEmail } from '../types/scheduled';
16
- import { EmailTemplateManager } from './template-manager';
17
-
18
- export interface SchedulerConfig {
19
- maxRetries?: number;
20
- retryDelay?: number;
21
- batchSize?: number;
22
- concurrency?: number;
23
- }
24
-
25
- export class EmailScheduler {
26
- private templateManager: EmailTemplateManager;
27
- private emailProvider: EmailProvider;
28
- private config: SchedulerConfig;
29
- private scheduledEmails: Map<string, ScheduledEmail> = new Map();
30
- private processingQueue: Set<string> = new Set();
31
-
32
- constructor(
33
- templateManager: EmailTemplateManager,
34
- emailProvider: EmailProvider,
35
- config: SchedulerConfig = {}
36
- ) {
37
- this.templateManager = templateManager;
38
- this.emailProvider = emailProvider;
39
- this.config = {
40
- maxRetries: 3,
41
- retryDelay: 1000,
42
- batchSize: 10,
43
- concurrency: 5,
44
- ...config,
45
- };
46
- }
47
-
48
- /**
49
- * Schedule an email to be sent
50
- */
51
- async scheduleEmail(emailData: Omit<ScheduledEmail, 'id' | 'status' | 'createdAt' | 'updatedAt' | 'attempts'>): Promise<ScheduledEmail> {
52
- const scheduledEmail: ScheduledEmail = {
53
- ...emailData,
54
- id: this.generateId(),
55
- status: 'pending',
56
- attempts: 0,
57
- createdAt: new Date(),
58
- updatedAt: new Date(),
59
- };
60
-
61
- // Store the scheduled email
62
- this.scheduledEmails.set(scheduledEmail.id, scheduledEmail);
63
-
64
- // If it's immediate, process right away
65
- if (emailData.triggerType === 'immediate') {
66
- // Don't await to avoid blocking
67
- this.processEmail(scheduledEmail.id).catch(error => {
68
- console.error('Failed to process immediate email:', error);
69
- });
70
- }
71
-
72
- return scheduledEmail;
73
- }
74
-
75
- /**
76
- * Process a single email
77
- */
78
- async processEmail(emailId: string): Promise<boolean> {
79
- const scheduledEmail = this.scheduledEmails.get(emailId);
80
- if (!scheduledEmail) {
81
- throw new Error(`Scheduled email with id ${emailId} not found`);
82
- }
83
-
84
- if (this.processingQueue.has(emailId)) {
85
- console.log(`Email ${emailId} is already being processed`);
86
- return false;
87
- }
88
-
89
- this.processingQueue.add(emailId);
90
-
91
- try {
92
- // Check if email should be sent now
93
- if (!this.shouldSendNow(scheduledEmail)) {
94
- this.processingQueue.delete(emailId);
95
- return false;
96
- }
97
-
98
- // Update status to processing
99
- scheduledEmail.status = 'processing';
100
- scheduledEmail.updatedAt = new Date();
101
-
102
- // Render the template
103
- const rendered = await this.templateManager.renderTemplateByName(
104
- scheduledEmail.templateName,
105
- scheduledEmail.variables
106
- );
107
-
108
- // Create email message
109
- const emailMessage: EmailMessage = {
110
- toEmail: scheduledEmail.recipientEmail,
111
- toName: scheduledEmail.recipientName,
112
- subject: rendered.subject,
113
- htmlContent: rendered.htmlContent,
114
- textContent: rendered.textContent,
115
- metadata: {
116
- scheduledEmailId: scheduledEmail.id,
117
- templateName: scheduledEmail.templateName,
118
- ...scheduledEmail.metadata,
119
- },
120
- };
121
-
122
- // Send the email
123
- const result = await this.emailProvider.sendEmail(emailMessage);
124
-
125
- if (result.success) {
126
- // Update status to sent
127
- scheduledEmail.status = 'sent';
128
- scheduledEmail.sentAt = new Date();
129
- scheduledEmail.messageId = result.messageId;
130
- scheduledEmail.providerResponse = result.providerResponse;
131
- } else {
132
- // Handle failure
133
- scheduledEmail.attempts += 1;
134
- scheduledEmail.lastError = result.errorMessage;
135
-
136
- if (scheduledEmail.attempts >= this.config.maxRetries!) {
137
- scheduledEmail.status = 'failed';
138
- } else {
139
- scheduledEmail.status = 'pending';
140
- // Schedule retry
141
- scheduledEmail.retryAt = new Date(Date.now() + this.config.retryDelay! * Math.pow(2, scheduledEmail.attempts));
142
- }
143
- }
144
-
145
- scheduledEmail.updatedAt = new Date();
146
- this.processingQueue.delete(emailId);
147
-
148
- return result.success;
149
- } catch (error) {
150
- scheduledEmail.status = 'failed';
151
- scheduledEmail.lastError = error instanceof Error ? error.message : 'Unknown error';
152
- scheduledEmail.updatedAt = new Date();
153
- this.processingQueue.delete(emailId);
154
-
155
- console.error(`Failed to process email ${emailId}:`, error);
156
- return false;
157
- }
158
- }
159
-
160
- /**
161
- * Process all pending emails
162
- */
163
- async processPendingEmails(): Promise<void> {
164
- const pendingEmails = Array.from(this.scheduledEmails.values())
165
- .filter(email => this.shouldProcessEmail(email))
166
- .slice(0, this.config.batchSize);
167
-
168
- if (pendingEmails.length === 0) {
169
- return;
170
- }
171
-
172
- // Process emails with concurrency limit
173
- const chunks = this.chunkArray(pendingEmails, this.config.concurrency!);
174
-
175
- for (const chunk of chunks) {
176
- const promises = chunk.map(email => this.processEmail(email.id));
177
- await Promise.allSettled(promises);
178
- }
179
- }
180
-
181
- /**
182
- * Get scheduled email by ID
183
- */
184
- getScheduledEmail(id: string): ScheduledEmail | null {
185
- return this.scheduledEmails.get(id) || null;
186
- }
187
-
188
- /**
189
- * Get all scheduled emails with optional filtering
190
- */
191
- getScheduledEmails(filters?: {
192
- status?: EmailStatus;
193
- templateName?: string;
194
- recipientEmail?: string;
195
- createdAfter?: Date;
196
- createdBefore?: Date;
197
- }): ScheduledEmail[] {
198
- let emails = Array.from(this.scheduledEmails.values());
199
-
200
- if (filters) {
201
- emails = emails.filter(email => {
202
- if (filters.status && email.status !== filters.status) {
203
- return false;
204
- }
205
-
206
- if (filters.templateName && email.templateName !== filters.templateName) {
207
- return false;
208
- }
209
-
210
- if (filters.recipientEmail && email.recipientEmail !== filters.recipientEmail) {
211
- return false;
212
- }
213
-
214
- if (filters.createdAfter && email.createdAt < filters.createdAfter) {
215
- return false;
216
- }
217
-
218
- if (filters.createdBefore && email.createdAt > filters.createdBefore) {
219
- return false;
220
- }
221
-
222
- return true;
223
- });
224
- }
225
-
226
- return emails;
227
- }
228
-
229
- /**
230
- * Cancel a scheduled email
231
- */
232
- async cancelScheduledEmail(id: string): Promise<boolean> {
233
- const scheduledEmail = this.scheduledEmails.get(id);
234
- if (!scheduledEmail) {
235
- return false;
236
- }
237
-
238
- if (scheduledEmail.status === 'sent' || scheduledEmail.status === 'failed') {
239
- return false; // Cannot cancel already processed emails
240
- }
241
-
242
- scheduledEmail.status = 'cancelled';
243
- scheduledEmail.updatedAt = new Date();
244
-
245
- return true;
246
- }
247
-
248
- /**
249
- * Retry a failed email
250
- */
251
- async retryFailedEmail(id: string): Promise<boolean> {
252
- const scheduledEmail = this.scheduledEmails.get(id);
253
- if (!scheduledEmail) {
254
- return false;
255
- }
256
-
257
- if (scheduledEmail.status !== 'failed') {
258
- return false; // Can only retry failed emails
259
- }
260
-
261
- scheduledEmail.status = 'pending';
262
- scheduledEmail.attempts = 0;
263
- scheduledEmail.lastError = undefined;
264
- scheduledEmail.retryAt = undefined;
265
- scheduledEmail.updatedAt = new Date();
266
-
267
- // Process immediately
268
- return await this.processEmail(id);
269
- }
270
-
271
- /**
272
- * Get email statistics
273
- */
274
- getStatistics(): {
275
- total: number;
276
- pending: number;
277
- processing: number;
278
- sent: number;
279
- failed: number;
280
- cancelled: number;
281
- } {
282
- const emails = Array.from(this.scheduledEmails.values());
283
-
284
- return {
285
- total: emails.length,
286
- pending: emails.filter(e => e.status === 'pending').length,
287
- processing: emails.filter(e => e.status === 'processing').length,
288
- sent: emails.filter(e => e.status === 'sent').length,
289
- failed: emails.filter(e => e.status === 'failed').length,
290
- cancelled: emails.filter(e => e.status === 'cancelled').length,
291
- };
292
- }
293
-
294
- /**
295
- * Check if an email should be sent now
296
- */
297
- private shouldSendNow(email: ScheduledEmail): boolean {
298
- const now = new Date();
299
-
300
- switch (email.triggerType) {
301
- case 'immediate':
302
- return true;
303
-
304
- case 'scheduled':
305
- return email.scheduledFor ? email.scheduledFor <= now : false;
306
-
307
- case 'relative':
308
- if (!email.relativeToDate || !email.relativeOffset) {
309
- return false;
310
- }
311
- const triggerDate = new Date(email.relativeToDate.getTime() + email.relativeOffset);
312
- return triggerDate <= now;
313
-
314
- default:
315
- return false;
316
- }
317
- }
318
-
319
- /**
320
- * Check if an email should be processed (including retries)
321
- */
322
- private shouldProcessEmail(email: ScheduledEmail): boolean {
323
- if (email.status !== 'pending') {
324
- return false;
325
- }
326
-
327
- if (this.processingQueue.has(email.id)) {
328
- return false;
329
- }
330
-
331
- // Check if it's time for retry
332
- if (email.retryAt && email.retryAt > new Date()) {
333
- return false;
334
- }
335
-
336
- return this.shouldSendNow(email);
337
- }
338
-
339
- /**
340
- * Generate a unique ID
341
- */
342
- private generateId(): string {
343
- return `sch_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
344
- }
345
-
346
- /**
347
- * Split array into chunks
348
- */
349
- private chunkArray<T>(array: T[], size: number): T[][] {
350
- const chunks: T[][] = [];
351
- for (let i = 0; i < array.length; i += size) {
352
- chunks.push(array.slice(i, i + size));
353
- }
354
- return chunks;
355
- }
356
- }