@contentgrowth/content-emailing 0.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.
- package/README.md +96 -0
- package/examples/.env.example +16 -0
- package/examples/README.md +55 -0
- package/examples/mocks/MockD1.js +311 -0
- package/examples/mocks/MockEmailSender.js +64 -0
- package/examples/mocks/index.js +5 -0
- package/examples/package-lock.json +73 -0
- package/examples/package.json +18 -0
- package/examples/portal/index.html +919 -0
- package/examples/server.js +314 -0
- package/package.json +32 -0
- package/release.sh +56 -0
- package/schema.sql +63 -0
- package/src/backend/EmailService.js +474 -0
- package/src/backend/EmailTemplateCacheDO.js +363 -0
- package/src/backend/routes/index.js +30 -0
- package/src/backend/routes/templates.js +98 -0
- package/src/backend/routes/tracking.js +215 -0
- package/src/backend/routes.js +98 -0
- package/src/common/htmlWrapper.js +169 -0
- package/src/common/index.js +11 -0
- package/src/common/utils.js +117 -0
- package/src/frontend/TemplateEditor.jsx +117 -0
- package/src/frontend/TemplateManager.jsx +117 -0
- package/src/index.js +24 -0
package/README.md
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# @emails/backend
|
|
2
|
+
|
|
3
|
+
A comprehensive email management and delivery package designed for **Cloudflare Workers**.
|
|
4
|
+
|
|
5
|
+
This package provides a complete solution for handling transactional emails, managing templates (with Markdown and variable substitution), and tracking email events (opens, clicks). It relies on **Cloudflare D1** for storage and supports optional **Durable Objects** for high-performance caching.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Multi-Provider Support**: Switch between MailChannels, SendGrid, Resend, SendPulse, or Mock (for testing) via configuration.
|
|
10
|
+
- **Template Management**: Create and edit email templates using Markdown.
|
|
11
|
+
- **Dynamic Variables**: Mustache-style variable substitution (`{{ user_name }}`) in subjects and bodies.
|
|
12
|
+
- **Email Tracking**: Built-in support for tracking email opens (pixel) and link clicks.
|
|
13
|
+
- **Cloudflare D1**: Stores templates, settings, and logs in D1 SQL database.
|
|
14
|
+
- **Durable Objects Cache**: Optional caching layer for high-speed template retrieval.
|
|
15
|
+
- **React Components**: Ready-to-use UI components for managing templates (`TemplateManager`, `TemplateEditor`).
|
|
16
|
+
|
|
17
|
+
## Requirements
|
|
18
|
+
|
|
19
|
+
- **Runtime**: Cloudflare Workers (or compatible environment like Hono on Node.js with D1 bindings).
|
|
20
|
+
- **Framework**: built with [Hono](https://hono.dev).
|
|
21
|
+
- **Database**: Cloudflare D1.
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install @emails/backend
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Setup & Usage
|
|
30
|
+
|
|
31
|
+
### 1. Initialize Email Service
|
|
32
|
+
|
|
33
|
+
In your worker or Hono app:
|
|
34
|
+
|
|
35
|
+
```javascript
|
|
36
|
+
import { EmailService } from '@emails/backend';
|
|
37
|
+
|
|
38
|
+
// Initialize with your environment (needs DB binding)
|
|
39
|
+
const emailService = new EmailService(env, {
|
|
40
|
+
defaults: {
|
|
41
|
+
fromName: 'My App',
|
|
42
|
+
fromAddress: 'noreply@myapp.com',
|
|
43
|
+
provider: 'resend' // or 'mailchannels', 'sendgrid', 'mock'
|
|
44
|
+
},
|
|
45
|
+
// API keys from env
|
|
46
|
+
resend_api_key: env.RESEND_API_KEY
|
|
47
|
+
});
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### 2. Database Schema
|
|
51
|
+
|
|
52
|
+
Ensure your D1 database has the required tables. See `examples/mocks/MockD1.js` for the schema structure, or refer to the provided SQL migration files (if available).
|
|
53
|
+
|
|
54
|
+
Tables needed:
|
|
55
|
+
- `system_email_templates`
|
|
56
|
+
- `system_settings`
|
|
57
|
+
- `system_email_sends` (for logs)
|
|
58
|
+
|
|
59
|
+
### 3. Add API Routes
|
|
60
|
+
|
|
61
|
+
Mount the management routes in your Hono app:
|
|
62
|
+
|
|
63
|
+
```javascript
|
|
64
|
+
import { createTemplateRoutes, createTrackingRoutes } from '@emails/backend';
|
|
65
|
+
|
|
66
|
+
// Template management (protected routes recommended)
|
|
67
|
+
app.route('/api/templates', createTemplateRoutes(emailService));
|
|
68
|
+
|
|
69
|
+
// Public tracking routes (opens/clicks)
|
|
70
|
+
app.route('/email', createTrackingRoutes(emailService));
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Running the Example
|
|
74
|
+
|
|
75
|
+
A fully functional example server is included in `examples/`. It uses a **Mock D1** implementation so you can run it locally without deploying to Cloudflare.
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
cd examples
|
|
79
|
+
npm install
|
|
80
|
+
npm start
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Visit `http://localhost:3456/portal/` to try the Template Manager UI.
|
|
84
|
+
|
|
85
|
+
## Environment Variables
|
|
86
|
+
|
|
87
|
+
Configure your provider using standard environment variables or pass them in the config object:
|
|
88
|
+
|
|
89
|
+
- `EMAIL_PROVIDER`: `resend`, `sendgrid`, `mailchannels`, `sendpulse`, or `mock`
|
|
90
|
+
- `RESEND_API_KEY`
|
|
91
|
+
- `SENDGRID_API_KEY`
|
|
92
|
+
- `SENDPULSE_CLIENT_ID` / `SENDPULSE_CLIENT_SECRET`
|
|
93
|
+
|
|
94
|
+
## Architecture Note
|
|
95
|
+
|
|
96
|
+
This package is "Cloudflare-native". It expects `env.DB` to be a D1 binding. If running outside Cloudflare (e.g. Node.js), you must provide a D1-compatible mock or adapter, as demonstrated in the `examples/` folder.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Server Configuration
|
|
2
|
+
PORT=3456
|
|
3
|
+
|
|
4
|
+
# Email Configuration
|
|
5
|
+
# Options: mock, mailchannels, sendgrid, resend, sendpulse
|
|
6
|
+
EMAIL_PROVIDER=mock
|
|
7
|
+
|
|
8
|
+
# Provider Credentials (if using real provider)
|
|
9
|
+
SENDGRID_API_KEY=
|
|
10
|
+
RESEND_API_KEY=
|
|
11
|
+
SENDPULSE_CLIENT_ID=
|
|
12
|
+
SENDPULSE_CLIENT_SECRET=
|
|
13
|
+
|
|
14
|
+
# Sender Info
|
|
15
|
+
FROM_NAME="Example App"
|
|
16
|
+
FROM_ADDRESS="noreply@example.com"
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Email Package Examples
|
|
2
|
+
|
|
3
|
+
This folder contains a working example server and portal UI for testing the email package.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
cd examples
|
|
9
|
+
npm install
|
|
10
|
+
npm start
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Then open http://localhost:3456/portal/ in your browser.
|
|
14
|
+
|
|
15
|
+
## What's Included
|
|
16
|
+
|
|
17
|
+
### Mocks (`mocks/`)
|
|
18
|
+
|
|
19
|
+
- **MockD1.js** - In-memory D1 database implementation with sample templates
|
|
20
|
+
- **MockEmailSender.js** - Simulates email sending (logs to console, stores in memory)
|
|
21
|
+
|
|
22
|
+
### Server (`server.js`)
|
|
23
|
+
|
|
24
|
+
A Hono server that exposes:
|
|
25
|
+
- `GET /api/templates` - List all templates
|
|
26
|
+
- `GET /api/templates/:id` - Get single template
|
|
27
|
+
- `POST /api/templates` - Create/update template
|
|
28
|
+
- `DELETE /api/templates/:id` - Delete template
|
|
29
|
+
- `POST /api/templates/:id/preview` - Render template with variables
|
|
30
|
+
- `POST /api/templates/:id/test` - Send test email (mocked)
|
|
31
|
+
- `GET /api/sent-emails` - View "sent" emails
|
|
32
|
+
- `GET /email/track/open/:token` - Open tracking pixel
|
|
33
|
+
- `GET /r/:token` - Click tracking redirect
|
|
34
|
+
|
|
35
|
+
### Portal (`portal/`)
|
|
36
|
+
|
|
37
|
+
A standalone HTML portal for:
|
|
38
|
+
- Browsing and editing templates
|
|
39
|
+
- Live preview with variable substitution
|
|
40
|
+
- Sending test emails
|
|
41
|
+
- Viewing sent email history
|
|
42
|
+
|
|
43
|
+
## No Real Dependencies
|
|
44
|
+
|
|
45
|
+
This example uses **no real external services**:
|
|
46
|
+
- D1 is mocked with in-memory storage
|
|
47
|
+
- Emails are logged to console, not actually sent
|
|
48
|
+
- All data is ephemeral (resets on server restart)
|
|
49
|
+
|
|
50
|
+
## Sample Templates
|
|
51
|
+
|
|
52
|
+
The mock D1 comes pre-loaded with sample templates:
|
|
53
|
+
- `welcome` - Onboarding welcome email
|
|
54
|
+
- `password_reset` - Password reset request
|
|
55
|
+
- `weekly_digest` - Weekly activity summary
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MockD1 - In-memory D1 database mock for testing
|
|
3
|
+
*
|
|
4
|
+
* This provides a simple in-memory implementation of the D1 interface
|
|
5
|
+
* for testing the email package without a real database.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export class MockD1 {
|
|
9
|
+
constructor(options = {}) {
|
|
10
|
+
this.seedSettings = options.seedSettings !== false;
|
|
11
|
+
|
|
12
|
+
// In-memory storage
|
|
13
|
+
this.tables = {
|
|
14
|
+
system_email_templates: [],
|
|
15
|
+
system_email_preferences: [],
|
|
16
|
+
system_email_sends: [],
|
|
17
|
+
system_email_events: [],
|
|
18
|
+
system_settings: [],
|
|
19
|
+
tenants: []
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// Seed with sample data
|
|
23
|
+
this._seedData();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
_seedData() {
|
|
27
|
+
// Add sample templates
|
|
28
|
+
this.tables.system_email_templates = [
|
|
29
|
+
{
|
|
30
|
+
template_id: 'welcome',
|
|
31
|
+
template_name: 'Welcome Email',
|
|
32
|
+
template_type: 'onboarding',
|
|
33
|
+
subject_template: 'Welcome to {{company_name}}, {{user_name}}!',
|
|
34
|
+
body_markdown: `# Welcome, {{user_name}}!
|
|
35
|
+
|
|
36
|
+
We're thrilled to have you join **{{company_name}}**!
|
|
37
|
+
|
|
38
|
+
## Getting Started
|
|
39
|
+
|
|
40
|
+
Here are a few things you can do:
|
|
41
|
+
|
|
42
|
+
1. **Complete your profile** - Add your photo and bio
|
|
43
|
+
2. **Explore features** - Check out our [documentation]({{docs_url}})
|
|
44
|
+
3. **Join the community** - Connect with other users
|
|
45
|
+
|
|
46
|
+
If you have any questions, just reply to this email.
|
|
47
|
+
|
|
48
|
+
Best regards,
|
|
49
|
+
The {{company_name}} Team`,
|
|
50
|
+
variables: JSON.stringify(['user_name', 'company_name', 'docs_url']),
|
|
51
|
+
description: 'Sent to new users after signup',
|
|
52
|
+
is_active: 1,
|
|
53
|
+
created_at: Math.floor(Date.now() / 1000),
|
|
54
|
+
updated_at: Math.floor(Date.now() / 1000),
|
|
55
|
+
updated_by: 'system'
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
template_id: 'password_reset',
|
|
59
|
+
template_name: 'Password Reset',
|
|
60
|
+
template_type: 'transactional',
|
|
61
|
+
subject_template: 'Reset your password for {{company_name}}',
|
|
62
|
+
body_markdown: `# Password Reset Request
|
|
63
|
+
|
|
64
|
+
Hi {{user_name}},
|
|
65
|
+
|
|
66
|
+
We received a request to reset your password. Click the button below to create a new password:
|
|
67
|
+
|
|
68
|
+
[Reset Password]({{reset_url}})
|
|
69
|
+
|
|
70
|
+
This link will expire in **1 hour**.
|
|
71
|
+
|
|
72
|
+
If you didn't request this, you can safely ignore this email.
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
*This is an automated message from {{company_name}}*`,
|
|
76
|
+
variables: JSON.stringify(['user_name', 'company_name', 'reset_url']),
|
|
77
|
+
description: 'Sent when user requests password reset',
|
|
78
|
+
is_active: 1,
|
|
79
|
+
created_at: Math.floor(Date.now() / 1000),
|
|
80
|
+
updated_at: Math.floor(Date.now() / 1000),
|
|
81
|
+
updated_by: 'system'
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
template_id: 'weekly_digest',
|
|
85
|
+
template_name: 'Weekly Digest',
|
|
86
|
+
template_type: 'marketing',
|
|
87
|
+
subject_template: 'Your weekly update from {{company_name}}',
|
|
88
|
+
body_markdown: `# Weekly Digest
|
|
89
|
+
|
|
90
|
+
Hi {{user_name}},
|
|
91
|
+
|
|
92
|
+
Here's what happened this week:
|
|
93
|
+
|
|
94
|
+
{{#highlights}}
|
|
95
|
+
- {{.}}
|
|
96
|
+
{{/highlights}}
|
|
97
|
+
|
|
98
|
+
## Stats
|
|
99
|
+
|
|
100
|
+
| Metric | Value |
|
|
101
|
+
|--------|-------|
|
|
102
|
+
| Views | {{views}} |
|
|
103
|
+
| Clicks | {{clicks}} |
|
|
104
|
+
|
|
105
|
+
[View Full Report]({{report_url}})
|
|
106
|
+
|
|
107
|
+
See you next week!`,
|
|
108
|
+
variables: JSON.stringify(['user_name', 'company_name', 'highlights', 'views', 'clicks', 'report_url']),
|
|
109
|
+
description: 'Weekly activity summary',
|
|
110
|
+
is_active: 1,
|
|
111
|
+
created_at: Math.floor(Date.now() / 1000),
|
|
112
|
+
updated_at: Math.floor(Date.now() / 1000),
|
|
113
|
+
updated_by: 'system'
|
|
114
|
+
}
|
|
115
|
+
];
|
|
116
|
+
|
|
117
|
+
// Add sample settings only if seedSettings is true
|
|
118
|
+
if (this.seedSettings) {
|
|
119
|
+
const env = (typeof process !== 'undefined' && process.env) || {};
|
|
120
|
+
|
|
121
|
+
this.tables.system_settings = [
|
|
122
|
+
{ setting_key: 'email_provider', setting_value: env.EMAIL_PROVIDER || 'mock' },
|
|
123
|
+
{ setting_key: 'email_from_address', setting_value: env.FROM_ADDRESS || 'noreply@example.com' },
|
|
124
|
+
{ setting_key: 'email_from_name', setting_value: env.FROM_NAME || 'Example App' }
|
|
125
|
+
];
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Prepare a SQL statement
|
|
131
|
+
*/
|
|
132
|
+
prepare(sql) {
|
|
133
|
+
return new MockStatement(this, sql);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Execute a batch of statements
|
|
138
|
+
*/
|
|
139
|
+
async batch(statements) {
|
|
140
|
+
const results = [];
|
|
141
|
+
for (const stmt of statements) {
|
|
142
|
+
results.push(await stmt.run());
|
|
143
|
+
}
|
|
144
|
+
return results;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
class MockStatement {
|
|
149
|
+
constructor(db, sql) {
|
|
150
|
+
this.db = db;
|
|
151
|
+
this.sql = sql;
|
|
152
|
+
this.bindings = [];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
bind(...values) {
|
|
156
|
+
this.bindings = values;
|
|
157
|
+
return this;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async first() {
|
|
161
|
+
const results = await this.all();
|
|
162
|
+
return results.results?.[0] || null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async all() {
|
|
166
|
+
const { operation, table, data } = this._parseSql();
|
|
167
|
+
|
|
168
|
+
switch (operation) {
|
|
169
|
+
case 'SELECT':
|
|
170
|
+
return { results: this._executeSelect(table) };
|
|
171
|
+
case 'INSERT':
|
|
172
|
+
return { results: this._executeInsert(table, data) };
|
|
173
|
+
case 'UPDATE':
|
|
174
|
+
return { results: this._executeUpdate(table, data) };
|
|
175
|
+
case 'DELETE':
|
|
176
|
+
return { results: this._executeDelete(table) };
|
|
177
|
+
default:
|
|
178
|
+
return { results: [] };
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async run() {
|
|
183
|
+
return this.all();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
_parseSql() {
|
|
187
|
+
const sql = this.sql.trim().toUpperCase();
|
|
188
|
+
|
|
189
|
+
if (sql.startsWith('SELECT')) {
|
|
190
|
+
const match = this.sql.match(/FROM\s+(\w+)/i);
|
|
191
|
+
return { operation: 'SELECT', table: match?.[1] };
|
|
192
|
+
}
|
|
193
|
+
if (sql.startsWith('INSERT')) {
|
|
194
|
+
const match = this.sql.match(/INTO\s+(\w+)/i);
|
|
195
|
+
return { operation: 'INSERT', table: match?.[1] };
|
|
196
|
+
}
|
|
197
|
+
if (sql.startsWith('UPDATE')) {
|
|
198
|
+
const match = this.sql.match(/UPDATE\s+(\w+)/i);
|
|
199
|
+
return { operation: 'UPDATE', table: match?.[1] };
|
|
200
|
+
}
|
|
201
|
+
if (sql.startsWith('DELETE')) {
|
|
202
|
+
const match = this.sql.match(/FROM\s+(\w+)/i);
|
|
203
|
+
return { operation: 'DELETE', table: match?.[1] };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return { operation: 'UNKNOWN' };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
_executeSelect(tableName) {
|
|
210
|
+
const table = this.db.tables[tableName] || [];
|
|
211
|
+
|
|
212
|
+
// Handle WHERE clause with bindings
|
|
213
|
+
if (this.sql.includes('WHERE') && this.bindings.length > 0) {
|
|
214
|
+
// Simple WHERE clause parsing
|
|
215
|
+
if (this.sql.includes('template_id = ?')) {
|
|
216
|
+
return table.filter(row => row.template_id === this.bindings[0]);
|
|
217
|
+
}
|
|
218
|
+
if (this.sql.includes('send_id = ?')) {
|
|
219
|
+
return table.filter(row => row.send_id === this.bindings[0]);
|
|
220
|
+
}
|
|
221
|
+
if (this.sql.includes('unsub_token = ?')) {
|
|
222
|
+
return table.filter(row => row.unsub_token === this.bindings[0]);
|
|
223
|
+
}
|
|
224
|
+
if (this.sql.includes('tenant_id = ?')) {
|
|
225
|
+
return table.filter(row => row.tenant_id === this.bindings[0]);
|
|
226
|
+
}
|
|
227
|
+
if (this.sql.includes('setting_key = ?')) {
|
|
228
|
+
return table.filter(row => row.setting_key === this.bindings[0]);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return table;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
_executeInsert(tableName) {
|
|
236
|
+
const table = this.db.tables[tableName];
|
|
237
|
+
if (!table) return [];
|
|
238
|
+
|
|
239
|
+
// Parse column names from SQL
|
|
240
|
+
const colMatch = this.sql.match(/\(([^)]+)\)\s*VALUES/i);
|
|
241
|
+
if (!colMatch) return [];
|
|
242
|
+
|
|
243
|
+
const columns = colMatch[1].split(',').map(c => c.trim());
|
|
244
|
+
const newRow = {};
|
|
245
|
+
|
|
246
|
+
columns.forEach((col, i) => {
|
|
247
|
+
newRow[col] = this.bindings[i];
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// Handle ON CONFLICT for upserts
|
|
251
|
+
if (this.sql.includes('ON CONFLICT')) {
|
|
252
|
+
const existingIndex = table.findIndex(row => row.template_id === newRow.template_id);
|
|
253
|
+
if (existingIndex >= 0) {
|
|
254
|
+
table[existingIndex] = { ...table[existingIndex], ...newRow };
|
|
255
|
+
return [table[existingIndex]];
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
table.push(newRow);
|
|
260
|
+
return [newRow];
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
_executeUpdate(tableName) {
|
|
264
|
+
const table = this.db.tables[tableName];
|
|
265
|
+
if (!table) return [];
|
|
266
|
+
|
|
267
|
+
// Find the row to update using the last binding (usually the WHERE value)
|
|
268
|
+
const whereValue = this.bindings[this.bindings.length - 1];
|
|
269
|
+
|
|
270
|
+
const updated = [];
|
|
271
|
+
for (const row of table) {
|
|
272
|
+
if (row.template_id === whereValue || row.unsub_token === whereValue || row.setting_key === whereValue) {
|
|
273
|
+
// Apply updates (simplified - just takes values from bindings assuming simple SET col=?)
|
|
274
|
+
// NOTE: This Mock is very simple and assumes bindings order matches SET order
|
|
275
|
+
// For: UPDATE system_settings SET setting_value = ? WHERE setting_key = ?
|
|
276
|
+
// Binding 0 is value, Binding 1 is key
|
|
277
|
+
if (tableName === 'system_settings') {
|
|
278
|
+
row.setting_value = this.bindings[0];
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
row.updated_at = Math.floor(Date.now() / 1000);
|
|
282
|
+
updated.push(row);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return updated;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
_executeDelete(tableName) {
|
|
290
|
+
const table = this.db.tables[tableName];
|
|
291
|
+
if (!table) return [];
|
|
292
|
+
|
|
293
|
+
const deleteValue = this.bindings[0];
|
|
294
|
+
const initialLength = table.length;
|
|
295
|
+
|
|
296
|
+
this.db.tables[tableName] = table.filter(row => row.template_id !== deleteValue);
|
|
297
|
+
|
|
298
|
+
return [{ changes: initialLength - this.db.tables[tableName].length }];
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Create a mock environment object
|
|
304
|
+
*/
|
|
305
|
+
export function createMockEnv(options = {}) {
|
|
306
|
+
return {
|
|
307
|
+
DB: new MockD1(options),
|
|
308
|
+
DOMAIN: 'example.com',
|
|
309
|
+
ENVIRONMENT: 'development'
|
|
310
|
+
};
|
|
311
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MockEmailSender - Simulates email sending for testing
|
|
3
|
+
*
|
|
4
|
+
* Instead of actually sending emails, this logs them and stores
|
|
5
|
+
* them in memory so you can view what would have been sent.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export class MockEmailSender {
|
|
9
|
+
constructor() {
|
|
10
|
+
this.sentEmails = [];
|
|
11
|
+
this.failNextSend = false;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Simulate sending an email
|
|
16
|
+
*/
|
|
17
|
+
async send({ to, subject, html, text, from }) {
|
|
18
|
+
const email = {
|
|
19
|
+
id: crypto.randomUUID(),
|
|
20
|
+
to,
|
|
21
|
+
from,
|
|
22
|
+
subject,
|
|
23
|
+
html,
|
|
24
|
+
text,
|
|
25
|
+
sentAt: new Date().toISOString(),
|
|
26
|
+
status: this.failNextSend ? 'failed' : 'sent'
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
this.sentEmails.push(email);
|
|
30
|
+
|
|
31
|
+
console.log(`[MockEmailSender] ${email.status.toUpperCase()}: "${subject}" to ${to}`);
|
|
32
|
+
|
|
33
|
+
if (this.failNextSend) {
|
|
34
|
+
this.failNextSend = false;
|
|
35
|
+
return { success: false, error: 'Simulated failure' };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return { success: true, messageId: email.id };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Get all sent emails
|
|
43
|
+
*/
|
|
44
|
+
getSentEmails() {
|
|
45
|
+
return this.sentEmails;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Clear sent emails history
|
|
50
|
+
*/
|
|
51
|
+
clear() {
|
|
52
|
+
this.sentEmails = [];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Make the next send fail (for testing error handling)
|
|
57
|
+
*/
|
|
58
|
+
simulateFailure() {
|
|
59
|
+
this.failNextSend = true;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Global instance for the example server
|
|
64
|
+
export const mockEmailSender = new MockEmailSender();
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@content-growth/content-emailing-examples",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"lockfileVersion": 3,
|
|
5
|
+
"requires": true,
|
|
6
|
+
"packages": {
|
|
7
|
+
"": {
|
|
8
|
+
"name": "@content-growth/content-emailing-examples",
|
|
9
|
+
"version": "0.1.0",
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@hono/node-server": "^1.3.0",
|
|
12
|
+
"dotenv": "^16.4.5",
|
|
13
|
+
"hono": "^4.0.0",
|
|
14
|
+
"marked": "^9.1.2",
|
|
15
|
+
"mustache": "^4.2.0"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"node_modules/@hono/node-server": {
|
|
19
|
+
"version": "1.19.7",
|
|
20
|
+
"resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.7.tgz",
|
|
21
|
+
"integrity": "sha512-vUcD0uauS7EU2caukW8z5lJKtoGMokxNbJtBiwHgpqxEXokaHCBkQUmCHhjFB1VUTWdqj25QoMkMKzgjq+uhrw==",
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=18.14.1"
|
|
25
|
+
},
|
|
26
|
+
"peerDependencies": {
|
|
27
|
+
"hono": "^4"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"node_modules/dotenv": {
|
|
31
|
+
"version": "16.6.1",
|
|
32
|
+
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
|
|
33
|
+
"integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
|
|
34
|
+
"license": "BSD-2-Clause",
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=12"
|
|
37
|
+
},
|
|
38
|
+
"funding": {
|
|
39
|
+
"url": "https://dotenvx.com"
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"node_modules/hono": {
|
|
43
|
+
"version": "4.11.1",
|
|
44
|
+
"resolved": "https://registry.npmjs.org/hono/-/hono-4.11.1.tgz",
|
|
45
|
+
"integrity": "sha512-KsFcH0xxHes0J4zaQgWbYwmz3UPOOskdqZmItstUG93+Wk1ePBLkLGwbP9zlmh1BFUiL8Qp+Xfu9P7feJWpGNg==",
|
|
46
|
+
"license": "MIT",
|
|
47
|
+
"engines": {
|
|
48
|
+
"node": ">=16.9.0"
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
"node_modules/marked": {
|
|
52
|
+
"version": "9.1.6",
|
|
53
|
+
"resolved": "https://registry.npmjs.org/marked/-/marked-9.1.6.tgz",
|
|
54
|
+
"integrity": "sha512-jcByLnIFkd5gSXZmjNvS1TlmRhCXZjIzHYlaGkPlLIekG55JDR2Z4va9tZwCiP+/RDERiNhMOFu01xd6O5ct1Q==",
|
|
55
|
+
"license": "MIT",
|
|
56
|
+
"bin": {
|
|
57
|
+
"marked": "bin/marked.js"
|
|
58
|
+
},
|
|
59
|
+
"engines": {
|
|
60
|
+
"node": ">= 16"
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
"node_modules/mustache": {
|
|
64
|
+
"version": "4.2.0",
|
|
65
|
+
"resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz",
|
|
66
|
+
"integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==",
|
|
67
|
+
"license": "MIT",
|
|
68
|
+
"bin": {
|
|
69
|
+
"mustache": "bin/mustache"
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@content-growth/content-emailing-examples",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "Example server for testing the email package",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"start": "node server.js",
|
|
9
|
+
"dev": "node --watch server.js"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"hono": "^4.0.0",
|
|
13
|
+
"@hono/node-server": "^1.3.0",
|
|
14
|
+
"marked": "^9.1.2",
|
|
15
|
+
"mustache": "^4.2.0",
|
|
16
|
+
"dotenv": "^16.4.5"
|
|
17
|
+
}
|
|
18
|
+
}
|