@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.
- package/README.md +76 -217
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +28 -0
- package/dist/index.js.map +1 -0
- package/dist/simple-email-service.d.ts +58 -0
- package/dist/simple-email-service.d.ts.map +1 -0
- package/dist/simple-email-service.js +416 -0
- package/dist/simple-email-service.js.map +1 -0
- package/dist/types.d.ts +311 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +33 -0
- package/dist/types.js.map +1 -0
- package/package.json +57 -22
- package/.eslintrc.json +0 -112
- package/.flake8 +0 -18
- package/.github/workflows/ci.yml +0 -300
- package/EXTRACTION_SUMMARY.md +0 -265
- package/IMPLEMENTATION_STATUS.md +0 -159
- package/OPEN_SOURCE_SETUP.md +0 -420
- package/PACKAGE_USAGE.md +0 -471
- package/examples/fastapi-example/main.py +0 -257
- package/examples/nextjs-example/next-env.d.ts +0 -13
- package/examples/nextjs-example/package.json +0 -26
- package/examples/nextjs-example/pages/admin/templates.tsx +0 -157
- package/examples/nextjs-example/tsconfig.json +0 -28
- package/packages/core/package.json +0 -70
- package/packages/core/rollup.config.js +0 -37
- package/packages/core/specification.md +0 -416
- package/packages/core/src/adapters/supabase.ts +0 -291
- package/packages/core/src/core/scheduler.ts +0 -356
- package/packages/core/src/core/template-manager.ts +0 -388
- package/packages/core/src/index.ts +0 -30
- package/packages/core/src/providers/base.ts +0 -104
- package/packages/core/src/providers/sendgrid.ts +0 -368
- package/packages/core/src/types/provider.ts +0 -91
- package/packages/core/src/types/scheduled.ts +0 -78
- package/packages/core/src/types/template.ts +0 -97
- package/packages/core/tsconfig.json +0 -23
- package/packages/python/README.md +0 -106
- package/packages/python/email_template_manager/__init__.py +0 -66
- package/packages/python/email_template_manager/config.py +0 -98
- package/packages/python/email_template_manager/core/magic_links.py +0 -245
- package/packages/python/email_template_manager/core/manager.py +0 -344
- package/packages/python/email_template_manager/core/scheduler.py +0 -473
- package/packages/python/email_template_manager/exceptions.py +0 -67
- package/packages/python/email_template_manager/models/magic_link.py +0 -59
- package/packages/python/email_template_manager/models/scheduled.py +0 -78
- package/packages/python/email_template_manager/models/template.py +0 -90
- package/packages/python/email_template_manager/providers/aws_ses.py +0 -44
- package/packages/python/email_template_manager/providers/base.py +0 -94
- package/packages/python/email_template_manager/providers/sendgrid.py +0 -325
- package/packages/python/email_template_manager/providers/smtp.py +0 -44
- package/packages/python/pyproject.toml +0 -133
- package/packages/python/setup.py +0 -93
- package/packages/python/specification.md +0 -930
- package/packages/react/README.md +0 -13
- package/packages/react/package.json +0 -105
- package/packages/react/rollup.config.js +0 -37
- package/packages/react/specification.md +0 -569
- package/packages/react/src/index.ts +0 -20
- package/packages/react/tsconfig.json +0 -24
- package/plans/email-template-manager_app-admin.md +0 -590
- package/src/index.js +0 -1
- 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
|
-
}
|