@bernierllc/email 1.0.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.
- package/.eslintrc.json +112 -0
- package/.flake8 +18 -0
- package/.github/workflows/ci.yml +300 -0
- package/EXTRACTION_SUMMARY.md +265 -0
- package/IMPLEMENTATION_STATUS.md +159 -0
- package/LICENSE +7 -0
- package/OPEN_SOURCE_SETUP.md +420 -0
- package/PACKAGE_USAGE.md +471 -0
- package/README.md +232 -0
- package/examples/fastapi-example/main.py +257 -0
- package/examples/nextjs-example/next-env.d.ts +13 -0
- package/examples/nextjs-example/package.json +26 -0
- package/examples/nextjs-example/pages/admin/templates.tsx +157 -0
- package/examples/nextjs-example/tsconfig.json +28 -0
- package/package.json +32 -0
- package/packages/core/package.json +70 -0
- package/packages/core/rollup.config.js +37 -0
- package/packages/core/specification.md +416 -0
- package/packages/core/src/adapters/supabase.ts +291 -0
- package/packages/core/src/core/scheduler.ts +356 -0
- package/packages/core/src/core/template-manager.ts +388 -0
- package/packages/core/src/index.ts +30 -0
- package/packages/core/src/providers/base.ts +104 -0
- package/packages/core/src/providers/sendgrid.ts +368 -0
- package/packages/core/src/types/provider.ts +91 -0
- package/packages/core/src/types/scheduled.ts +78 -0
- package/packages/core/src/types/template.ts +97 -0
- package/packages/core/tsconfig.json +23 -0
- package/packages/python/README.md +106 -0
- package/packages/python/email_template_manager/__init__.py +66 -0
- package/packages/python/email_template_manager/config.py +98 -0
- package/packages/python/email_template_manager/core/magic_links.py +245 -0
- package/packages/python/email_template_manager/core/manager.py +344 -0
- package/packages/python/email_template_manager/core/scheduler.py +473 -0
- package/packages/python/email_template_manager/exceptions.py +67 -0
- package/packages/python/email_template_manager/models/magic_link.py +59 -0
- package/packages/python/email_template_manager/models/scheduled.py +78 -0
- package/packages/python/email_template_manager/models/template.py +90 -0
- package/packages/python/email_template_manager/providers/aws_ses.py +44 -0
- package/packages/python/email_template_manager/providers/base.py +94 -0
- package/packages/python/email_template_manager/providers/sendgrid.py +325 -0
- package/packages/python/email_template_manager/providers/smtp.py +44 -0
- package/packages/python/pyproject.toml +133 -0
- package/packages/python/setup.py +93 -0
- package/packages/python/specification.md +930 -0
- package/packages/react/README.md +13 -0
- package/packages/react/package.json +105 -0
- package/packages/react/rollup.config.js +37 -0
- package/packages/react/specification.md +569 -0
- package/packages/react/src/index.ts +20 -0
- package/packages/react/tsconfig.json +24 -0
- package/src/index.js +1 -0
- package/test_package.py +125 -0
|
@@ -0,0 +1,368 @@
|
|
|
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
|
+
* SendGrid email provider implementation
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { EmailProvider, EmailMessage, SendResult, DeliveryStatus, WebhookEvent } from './base';
|
|
14
|
+
|
|
15
|
+
export interface SendGridConfig {
|
|
16
|
+
apiKey: string;
|
|
17
|
+
fromEmail: string;
|
|
18
|
+
fromName?: string;
|
|
19
|
+
webhookUrl?: string;
|
|
20
|
+
webhookSecret?: string;
|
|
21
|
+
sandbox?: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class SendGridProvider extends EmailProvider {
|
|
25
|
+
private apiKey: string;
|
|
26
|
+
private fromEmail: string;
|
|
27
|
+
private fromName?: string;
|
|
28
|
+
private webhookUrl?: string;
|
|
29
|
+
private webhookSecret?: string;
|
|
30
|
+
private sandbox: boolean;
|
|
31
|
+
|
|
32
|
+
constructor(config: SendGridConfig) {
|
|
33
|
+
super(config);
|
|
34
|
+
|
|
35
|
+
if (!config.apiKey) {
|
|
36
|
+
throw new Error('SendGrid API key is required');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
this.apiKey = config.apiKey;
|
|
40
|
+
this.fromEmail = config.fromEmail;
|
|
41
|
+
this.fromName = config.fromName;
|
|
42
|
+
this.webhookUrl = config.webhookUrl;
|
|
43
|
+
this.webhookSecret = config.webhookSecret;
|
|
44
|
+
this.sandbox = config.sandbox || false;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async sendEmail(email: EmailMessage): Promise<SendResult> {
|
|
48
|
+
try {
|
|
49
|
+
const payload = {
|
|
50
|
+
personalizations: [{
|
|
51
|
+
to: [{
|
|
52
|
+
email: email.toEmail,
|
|
53
|
+
name: email.toName
|
|
54
|
+
}],
|
|
55
|
+
subject: email.subject,
|
|
56
|
+
custom_args: email.metadata || {}
|
|
57
|
+
}],
|
|
58
|
+
from: {
|
|
59
|
+
email: email.fromEmail || this.fromEmail,
|
|
60
|
+
name: email.fromName || this.fromName
|
|
61
|
+
},
|
|
62
|
+
content: this.buildContent(email),
|
|
63
|
+
headers: email.headers || {},
|
|
64
|
+
mail_settings: {
|
|
65
|
+
sandbox_mode: {
|
|
66
|
+
enable: this.sandbox
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// Add reply-to if specified
|
|
72
|
+
if (email.replyTo) {
|
|
73
|
+
(payload as any).reply_to = { email: email.replyTo };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Convert attachments to SendGrid format
|
|
77
|
+
if (email.attachments && email.attachments.length > 0) {
|
|
78
|
+
(payload as any).attachments = email.attachments.map((att: any) => ({
|
|
79
|
+
filename: att.filename,
|
|
80
|
+
content: att.content,
|
|
81
|
+
type: att.contentType,
|
|
82
|
+
disposition: 'attachment'
|
|
83
|
+
}));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const response = await this.makeRequest('/mail/send', 'POST', payload);
|
|
87
|
+
|
|
88
|
+
// SendGrid returns 202 for successful sends
|
|
89
|
+
if (response.status === 202) {
|
|
90
|
+
return {
|
|
91
|
+
success: true,
|
|
92
|
+
messageId: response.headers?.['x-message-id'] || `sg-${Date.now()}`,
|
|
93
|
+
providerResponse: response.data,
|
|
94
|
+
metadata: {
|
|
95
|
+
provider: 'sendgrid',
|
|
96
|
+
statusCode: response.status
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
success: false,
|
|
103
|
+
errorMessage: `SendGrid API returned status ${response.status}`,
|
|
104
|
+
metadata: {
|
|
105
|
+
provider: 'sendgrid',
|
|
106
|
+
statusCode: response.status,
|
|
107
|
+
response: response.data
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
} catch (error) {
|
|
112
|
+
return {
|
|
113
|
+
success: false,
|
|
114
|
+
errorMessage: error instanceof Error ? error.message : 'Unknown error',
|
|
115
|
+
metadata: {
|
|
116
|
+
provider: 'sendgrid'
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async sendBatch(emails: EmailMessage[]): Promise<SendResult[]> {
|
|
123
|
+
// SendGrid doesn't have a true batch API, so we send individually
|
|
124
|
+
// but we can optimize by sending them concurrently
|
|
125
|
+
const promises = emails.map(email => this.sendEmail(email));
|
|
126
|
+
return Promise.all(promises);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async getDeliveryStatus(messageId: string): Promise<DeliveryStatus | null> {
|
|
130
|
+
try {
|
|
131
|
+
// Use SendGrid's Event API
|
|
132
|
+
const response = await this.makeRequest(`/messages/${messageId}`, 'GET');
|
|
133
|
+
|
|
134
|
+
if (response.status === 200 && response.data) {
|
|
135
|
+
return {
|
|
136
|
+
messageId,
|
|
137
|
+
status: this.mapSendGridStatus(response.data.status),
|
|
138
|
+
deliveredAt: response.data.delivered_at ? new Date(response.data.delivered_at) : undefined,
|
|
139
|
+
openedAt: response.data.opened_at ? new Date(response.data.opened_at) : undefined,
|
|
140
|
+
clickedAt: response.data.clicked_at ? new Date(response.data.clicked_at) : undefined,
|
|
141
|
+
metadata: response.data
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return null;
|
|
146
|
+
} catch (error) {
|
|
147
|
+
console.error('Failed to get delivery status:', error);
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async processWebhook(payload: any, signature: string): Promise<WebhookEvent[]> {
|
|
153
|
+
// Verify webhook signature if configured
|
|
154
|
+
if (this.webhookSecret && !this.verifyWebhookSignature(payload, signature)) {
|
|
155
|
+
throw new Error('Invalid webhook signature');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const events: WebhookEvent[] = [];
|
|
159
|
+
|
|
160
|
+
// SendGrid sends events as an array
|
|
161
|
+
if (Array.isArray(payload)) {
|
|
162
|
+
for (const eventData of payload) {
|
|
163
|
+
events.push({
|
|
164
|
+
messageId: eventData.sg_message_id,
|
|
165
|
+
eventType: this.mapSendGridEvent(eventData.event),
|
|
166
|
+
timestamp: new Date(eventData.timestamp * 1000),
|
|
167
|
+
email: eventData.email,
|
|
168
|
+
reason: eventData.reason,
|
|
169
|
+
response: eventData.response,
|
|
170
|
+
metadata: eventData
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return events;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async createTemplate(templateData: {
|
|
179
|
+
name: string;
|
|
180
|
+
subject: string;
|
|
181
|
+
htmlContent: string;
|
|
182
|
+
textContent?: string;
|
|
183
|
+
}): Promise<string> {
|
|
184
|
+
try {
|
|
185
|
+
// Create the template
|
|
186
|
+
const templatePayload = {
|
|
187
|
+
name: templateData.name,
|
|
188
|
+
generation: 'dynamic'
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const templateResponse = await this.makeRequest('/templates', 'POST', templatePayload);
|
|
192
|
+
|
|
193
|
+
if (templateResponse.status !== 201) {
|
|
194
|
+
throw new Error(`Failed to create template: ${templateResponse.status}`);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const templateId = templateResponse.data.id;
|
|
198
|
+
|
|
199
|
+
// Create a version for the template
|
|
200
|
+
const versionPayload = {
|
|
201
|
+
template_id: templateId,
|
|
202
|
+
subject: templateData.subject,
|
|
203
|
+
html_content: templateData.htmlContent,
|
|
204
|
+
plain_content: templateData.textContent || '',
|
|
205
|
+
active: 1
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const versionResponse = await this.makeRequest(
|
|
209
|
+
`/templates/${templateId}/versions`,
|
|
210
|
+
'POST',
|
|
211
|
+
versionPayload
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
if (versionResponse.status !== 201) {
|
|
215
|
+
throw new Error(`Failed to create template version: ${versionResponse.status}`);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return templateId;
|
|
219
|
+
} catch (error) {
|
|
220
|
+
throw new Error(`Template creation failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async sendTemplateEmail(
|
|
225
|
+
templateId: string,
|
|
226
|
+
toEmail: string,
|
|
227
|
+
templateData: Record<string, any>,
|
|
228
|
+
toName?: string
|
|
229
|
+
): Promise<SendResult> {
|
|
230
|
+
try {
|
|
231
|
+
const payload = {
|
|
232
|
+
personalizations: [{
|
|
233
|
+
to: [{
|
|
234
|
+
email: toEmail,
|
|
235
|
+
name: toName
|
|
236
|
+
}],
|
|
237
|
+
dynamic_template_data: templateData
|
|
238
|
+
}],
|
|
239
|
+
from: {
|
|
240
|
+
email: this.fromEmail,
|
|
241
|
+
name: this.fromName
|
|
242
|
+
},
|
|
243
|
+
template_id: templateId,
|
|
244
|
+
mail_settings: {
|
|
245
|
+
sandbox_mode: {
|
|
246
|
+
enable: this.sandbox
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
const response = await this.makeRequest('/mail/send', 'POST', payload);
|
|
252
|
+
|
|
253
|
+
if (response.status === 202) {
|
|
254
|
+
return {
|
|
255
|
+
success: true,
|
|
256
|
+
messageId: response.headers?.['x-message-id'] || `sg-tpl-${Date.now()}`,
|
|
257
|
+
providerResponse: response.data,
|
|
258
|
+
metadata: {
|
|
259
|
+
provider: 'sendgrid',
|
|
260
|
+
templateId,
|
|
261
|
+
statusCode: response.status
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return {
|
|
267
|
+
success: false,
|
|
268
|
+
errorMessage: `SendGrid API returned status ${response.status}`,
|
|
269
|
+
metadata: {
|
|
270
|
+
provider: 'sendgrid',
|
|
271
|
+
templateId,
|
|
272
|
+
statusCode: response.status
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
} catch (error) {
|
|
277
|
+
return {
|
|
278
|
+
success: false,
|
|
279
|
+
errorMessage: error instanceof Error ? error.message : 'Unknown error',
|
|
280
|
+
metadata: {
|
|
281
|
+
provider: 'sendgrid',
|
|
282
|
+
templateId
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
private buildContent(email: EmailMessage): Array<{ type: string; value: string }> {
|
|
289
|
+
const content: Array<{ type: string; value: string }> = [];
|
|
290
|
+
|
|
291
|
+
if (email.textContent) {
|
|
292
|
+
content.push({
|
|
293
|
+
type: 'text/plain',
|
|
294
|
+
value: email.textContent
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (email.htmlContent) {
|
|
299
|
+
content.push({
|
|
300
|
+
type: 'text/html',
|
|
301
|
+
value: email.htmlContent
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return content;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
private async makeRequest(endpoint: string, method: string, requestData?: any): Promise<any> {
|
|
309
|
+
const url = `https://api.sendgrid.com/v3${endpoint}`;
|
|
310
|
+
|
|
311
|
+
const response = await fetch(url, {
|
|
312
|
+
method,
|
|
313
|
+
headers: {
|
|
314
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
315
|
+
'Content-Type': 'application/json'
|
|
316
|
+
},
|
|
317
|
+
body: requestData ? JSON.stringify(requestData) : undefined
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
const responseText = await response.text();
|
|
321
|
+
let data;
|
|
322
|
+
try {
|
|
323
|
+
data = JSON.parse(responseText);
|
|
324
|
+
} catch {
|
|
325
|
+
data = responseText;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return {
|
|
329
|
+
status: response.status,
|
|
330
|
+
headers: {},
|
|
331
|
+
data: data
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
private mapSendGridStatus(status: string): 'delivered' | 'bounced' | 'failed' | 'pending' | 'sent' | 'opened' | 'clicked' | 'spam' | 'unsubscribed' {
|
|
336
|
+
const statusMap: Record<string, 'delivered' | 'bounced' | 'failed' | 'pending' | 'sent' | 'opened' | 'clicked' | 'spam' | 'unsubscribed'> = {
|
|
337
|
+
'delivered': 'delivered',
|
|
338
|
+
'processed': 'sent',
|
|
339
|
+
'bounce': 'bounced',
|
|
340
|
+
'dropped': 'failed',
|
|
341
|
+
'deferred': 'pending',
|
|
342
|
+
'blocked': 'failed'
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
return statusMap[status] || 'failed';
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
private mapSendGridEvent(event: string): string {
|
|
349
|
+
const eventMap: Record<string, string> = {
|
|
350
|
+
'delivered': 'delivered',
|
|
351
|
+
'processed': 'sent',
|
|
352
|
+
'open': 'opened',
|
|
353
|
+
'click': 'clicked',
|
|
354
|
+
'bounce': 'bounced',
|
|
355
|
+
'dropped': 'failed',
|
|
356
|
+
'spam_report': 'spam',
|
|
357
|
+
'unsubscribe': 'unsubscribed'
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
return eventMap[event] || event;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
private verifyWebhookSignature(payload: any, signature: string): boolean {
|
|
364
|
+
// Implement SendGrid webhook signature verification
|
|
365
|
+
// This is a simplified version - you'd need to implement the actual verification
|
|
366
|
+
return true;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
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 provider type definitions
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export interface EmailMessage {
|
|
14
|
+
toEmail: string;
|
|
15
|
+
toName?: string;
|
|
16
|
+
fromEmail?: string;
|
|
17
|
+
fromName?: string;
|
|
18
|
+
subject: string;
|
|
19
|
+
htmlContent?: string;
|
|
20
|
+
textContent?: string;
|
|
21
|
+
replyTo?: string;
|
|
22
|
+
headers?: Record<string, string>;
|
|
23
|
+
attachments?: EmailAttachment[];
|
|
24
|
+
metadata?: Record<string, any>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface EmailAttachment {
|
|
28
|
+
filename: string;
|
|
29
|
+
content: string; // Base64 encoded
|
|
30
|
+
contentType?: string;
|
|
31
|
+
disposition?: 'attachment' | 'inline';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface SendResult {
|
|
35
|
+
success: boolean;
|
|
36
|
+
messageId?: string;
|
|
37
|
+
errorMessage?: string;
|
|
38
|
+
providerResponse?: any;
|
|
39
|
+
metadata?: Record<string, any>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface DeliveryStatus {
|
|
43
|
+
messageId: string;
|
|
44
|
+
status: 'delivered' | 'bounced' | 'failed' | 'pending' | 'sent' | 'opened' | 'clicked' | 'spam' | 'unsubscribed';
|
|
45
|
+
deliveredAt?: Date;
|
|
46
|
+
openedAt?: Date;
|
|
47
|
+
clickedAt?: Date;
|
|
48
|
+
bouncedAt?: Date;
|
|
49
|
+
reason?: string;
|
|
50
|
+
metadata?: Record<string, any>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface WebhookEvent {
|
|
54
|
+
messageId: string;
|
|
55
|
+
eventType: string;
|
|
56
|
+
timestamp: Date;
|
|
57
|
+
email: string;
|
|
58
|
+
reason?: string;
|
|
59
|
+
response?: string;
|
|
60
|
+
metadata?: Record<string, any>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface ProviderConfig {
|
|
64
|
+
provider: 'sendgrid' | 'ses' | 'mailgun' | 'postmark' | 'smtp';
|
|
65
|
+
apiKey?: string;
|
|
66
|
+
region?: string;
|
|
67
|
+
endpoint?: string;
|
|
68
|
+
fromEmail: string;
|
|
69
|
+
fromName?: string;
|
|
70
|
+
replyTo?: string;
|
|
71
|
+
webhookUrl?: string;
|
|
72
|
+
webhookSecret?: string;
|
|
73
|
+
sandbox?: boolean;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface BatchEmailOptions {
|
|
77
|
+
templateId?: string;
|
|
78
|
+
variables?: Record<string, any>;
|
|
79
|
+
scheduledFor?: Date;
|
|
80
|
+
metadata?: Record<string, any>;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface EmailProviderCapabilities {
|
|
84
|
+
supportsBatch: boolean;
|
|
85
|
+
supportsTemplates: boolean;
|
|
86
|
+
supportsWebhooks: boolean;
|
|
87
|
+
supportsAttachments: boolean;
|
|
88
|
+
supportsDeliveryTracking: boolean;
|
|
89
|
+
maxBatchSize: number;
|
|
90
|
+
maxAttachmentSize: number;
|
|
91
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
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
|
+
* Scheduled email type definitions
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export type TriggerType = 'immediate' | 'relative' | 'absolute';
|
|
14
|
+
export type EmailStatus = 'pending' | 'sent' | 'failed' | 'cancelled' | 'paused';
|
|
15
|
+
|
|
16
|
+
export interface ScheduledEmail {
|
|
17
|
+
id?: string;
|
|
18
|
+
templateId: string;
|
|
19
|
+
recipientEmail: string;
|
|
20
|
+
recipientName?: string;
|
|
21
|
+
scheduledFor: Date;
|
|
22
|
+
triggerType: TriggerType;
|
|
23
|
+
triggerOffset?: number; // days before/after reference date
|
|
24
|
+
referenceDate?: Date;
|
|
25
|
+
variables: Record<string, any>;
|
|
26
|
+
status: EmailStatus;
|
|
27
|
+
sentAt?: Date;
|
|
28
|
+
errorMessage?: string;
|
|
29
|
+
retryCount: number;
|
|
30
|
+
maxRetries: number;
|
|
31
|
+
metadata: Record<string, any>;
|
|
32
|
+
createdAt?: Date;
|
|
33
|
+
updatedAt?: Date;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface SchedulingFilters {
|
|
37
|
+
templateId?: string;
|
|
38
|
+
recipientEmail?: string;
|
|
39
|
+
status?: EmailStatus;
|
|
40
|
+
scheduledAfter?: Date;
|
|
41
|
+
scheduledBefore?: Date;
|
|
42
|
+
createdAfter?: Date;
|
|
43
|
+
createdBefore?: Date;
|
|
44
|
+
limit?: number;
|
|
45
|
+
offset?: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface ProcessingResult {
|
|
49
|
+
processedCount: number;
|
|
50
|
+
successCount: number;
|
|
51
|
+
failedCount: number;
|
|
52
|
+
errors: Array<{
|
|
53
|
+
emailId: string;
|
|
54
|
+
error: string;
|
|
55
|
+
}>;
|
|
56
|
+
processingTimeSeconds: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface EmailBatch {
|
|
60
|
+
templateId: string;
|
|
61
|
+
recipients: Array<{
|
|
62
|
+
email: string;
|
|
63
|
+
name?: string;
|
|
64
|
+
variables: Record<string, any>;
|
|
65
|
+
}>;
|
|
66
|
+
scheduledFor?: Date;
|
|
67
|
+
triggerType?: TriggerType;
|
|
68
|
+
triggerOffset?: number;
|
|
69
|
+
referenceDate?: Date;
|
|
70
|
+
metadata?: Record<string, any>;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface SchedulingConfig {
|
|
74
|
+
batchSize: number;
|
|
75
|
+
retryDelays: number[]; // minutes
|
|
76
|
+
maxConcurrency: number;
|
|
77
|
+
processingInterval: number; // minutes
|
|
78
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
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 type definitions
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export type TemplateVariableType = 'text' | 'number' | 'date' | 'boolean' | 'url' | 'email';
|
|
14
|
+
|
|
15
|
+
export interface TemplateVariable {
|
|
16
|
+
name: string;
|
|
17
|
+
type: TemplateVariableType;
|
|
18
|
+
description?: string;
|
|
19
|
+
defaultValue?: any;
|
|
20
|
+
required: boolean;
|
|
21
|
+
validation?: {
|
|
22
|
+
pattern?: string;
|
|
23
|
+
minLength?: number;
|
|
24
|
+
maxLength?: number;
|
|
25
|
+
min?: number;
|
|
26
|
+
max?: number;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface EmailTemplate {
|
|
31
|
+
id?: string;
|
|
32
|
+
name: string;
|
|
33
|
+
subject: string;
|
|
34
|
+
htmlBody: string;
|
|
35
|
+
textBody: string;
|
|
36
|
+
variables: TemplateVariable[];
|
|
37
|
+
categoryId?: string;
|
|
38
|
+
tags: string[];
|
|
39
|
+
isActive: boolean;
|
|
40
|
+
version: number;
|
|
41
|
+
createdAt?: Date;
|
|
42
|
+
updatedAt?: Date;
|
|
43
|
+
createdBy?: string;
|
|
44
|
+
metadata?: Record<string, any>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface TemplateCategory {
|
|
48
|
+
id: string;
|
|
49
|
+
name: string;
|
|
50
|
+
description?: string;
|
|
51
|
+
color?: string;
|
|
52
|
+
isActive: boolean;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface RenderedTemplate {
|
|
56
|
+
subject: string;
|
|
57
|
+
htmlContent: string;
|
|
58
|
+
textContent: string;
|
|
59
|
+
variables: Record<string, any>;
|
|
60
|
+
templateId: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface TemplateFilters {
|
|
64
|
+
categoryId?: string;
|
|
65
|
+
tags?: string[];
|
|
66
|
+
isActive?: boolean;
|
|
67
|
+
search?: string;
|
|
68
|
+
createdBy?: string;
|
|
69
|
+
createdAfter?: Date;
|
|
70
|
+
createdBefore?: Date;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface ValidationResult {
|
|
74
|
+
isValid: boolean;
|
|
75
|
+
errors: ValidationError[];
|
|
76
|
+
warnings: ValidationWarning[];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface ValidationError {
|
|
80
|
+
field: string;
|
|
81
|
+
message: string;
|
|
82
|
+
code: string;
|
|
83
|
+
value?: any;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface ValidationWarning {
|
|
87
|
+
field: string;
|
|
88
|
+
message: string;
|
|
89
|
+
code: string;
|
|
90
|
+
value?: any;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface TemplatePreview {
|
|
94
|
+
template: EmailTemplate;
|
|
95
|
+
variables: Record<string, any>;
|
|
96
|
+
rendered: RenderedTemplate;
|
|
97
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"lib": ["ES2020"],
|
|
6
|
+
"declaration": true,
|
|
7
|
+
"declarationMap": true,
|
|
8
|
+
"sourceMap": true,
|
|
9
|
+
"outDir": "./dist",
|
|
10
|
+
"rootDir": "./src",
|
|
11
|
+
"strict": true,
|
|
12
|
+
"esModuleInterop": true,
|
|
13
|
+
"skipLibCheck": true,
|
|
14
|
+
"forceConsistentCasingInFileNames": true,
|
|
15
|
+
"moduleResolution": "node",
|
|
16
|
+
"allowSyntheticDefaultImports": true,
|
|
17
|
+
"resolveJsonModule": true,
|
|
18
|
+
"isolatedModules": true,
|
|
19
|
+
"noEmit": false
|
|
20
|
+
},
|
|
21
|
+
"include": ["src/**/*"],
|
|
22
|
+
"exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"]
|
|
23
|
+
}
|