emdash-aws-ses 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/dist/index.cjs +43 -0
- package/dist/index.js +18 -0
- package/dist/sandbox-entry.cjs +391 -0
- package/dist/sandbox-entry.js +360 -0
- package/package.json +46 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
awsSesPlugin: () => awsSesPlugin
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(index_exports);
|
|
26
|
+
function awsSesPlugin() {
|
|
27
|
+
return {
|
|
28
|
+
id: "emdash-aws-ses",
|
|
29
|
+
version: "1.0.0",
|
|
30
|
+
format: "standard",
|
|
31
|
+
entrypoint: "emdash-aws-ses/sandbox",
|
|
32
|
+
options: {},
|
|
33
|
+
capabilities: ["hooks.email-transport:register"],
|
|
34
|
+
adminPages: [
|
|
35
|
+
{ path: "/settings", label: "AWS SES Settings", icon: "mail" },
|
|
36
|
+
{ path: "/test", label: "Test & Status", icon: "send" }
|
|
37
|
+
]
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
41
|
+
0 && (module.exports = {
|
|
42
|
+
awsSesPlugin
|
|
43
|
+
});
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
function awsSesPlugin() {
|
|
3
|
+
return {
|
|
4
|
+
id: "emdash-aws-ses",
|
|
5
|
+
version: "1.0.0",
|
|
6
|
+
format: "standard",
|
|
7
|
+
entrypoint: "emdash-aws-ses/sandbox",
|
|
8
|
+
options: {},
|
|
9
|
+
capabilities: ["hooks.email-transport:register"],
|
|
10
|
+
adminPages: [
|
|
11
|
+
{ path: "/settings", label: "AWS SES Settings", icon: "mail" },
|
|
12
|
+
{ path: "/test", label: "Test & Status", icon: "send" }
|
|
13
|
+
]
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export {
|
|
17
|
+
awsSesPlugin
|
|
18
|
+
};
|
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/sandbox-entry.ts
|
|
31
|
+
var sandbox_entry_exports = {};
|
|
32
|
+
__export(sandbox_entry_exports, {
|
|
33
|
+
default: () => sandbox_entry_default
|
|
34
|
+
});
|
|
35
|
+
module.exports = __toCommonJS(sandbox_entry_exports);
|
|
36
|
+
var import_emdash = require("emdash");
|
|
37
|
+
var import_nodemailer = __toESM(require("nodemailer"), 1);
|
|
38
|
+
async function getSettings(ctx) {
|
|
39
|
+
const defaults = {
|
|
40
|
+
smtpHost: "email-smtp.us-east-1.amazonaws.com",
|
|
41
|
+
smtpPort: 587,
|
|
42
|
+
region: "us-east-1",
|
|
43
|
+
defaultFrom: "",
|
|
44
|
+
fromName: "",
|
|
45
|
+
replyTo: "",
|
|
46
|
+
smtpUser: "",
|
|
47
|
+
smtpPassword: ""
|
|
48
|
+
};
|
|
49
|
+
const [smtpHost, smtpPortRaw, smtpUser, smtpPassword, region, defaultFrom, fromName, replyTo] = await Promise.all([
|
|
50
|
+
ctx.kv.get("settings:smtpHost"),
|
|
51
|
+
ctx.kv.get("settings:smtpPort"),
|
|
52
|
+
ctx.kv.get("settings:smtpUser"),
|
|
53
|
+
ctx.kv.get("settings:smtpPassword"),
|
|
54
|
+
ctx.kv.get("settings:region"),
|
|
55
|
+
ctx.kv.get("settings:defaultFrom"),
|
|
56
|
+
ctx.kv.get("settings:fromName"),
|
|
57
|
+
ctx.kv.get("settings:replyTo")
|
|
58
|
+
]);
|
|
59
|
+
return {
|
|
60
|
+
smtpHost: smtpHost ?? defaults.smtpHost,
|
|
61
|
+
smtpPort: Number(smtpPortRaw ?? defaults.smtpPort),
|
|
62
|
+
smtpUser: smtpUser ?? defaults.smtpUser,
|
|
63
|
+
smtpPassword: smtpPassword ?? defaults.smtpPassword,
|
|
64
|
+
region: region ?? defaults.region,
|
|
65
|
+
defaultFrom: defaultFrom ?? defaults.defaultFrom,
|
|
66
|
+
fromName: fromName ?? defaults.fromName,
|
|
67
|
+
replyTo: replyTo ?? defaults.replyTo
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function createTransporter(settings) {
|
|
71
|
+
const secure = settings.smtpPort === 465;
|
|
72
|
+
const requireTLS = settings.smtpPort === 587 || settings.smtpPort === 25 || settings.smtpPort === 2587;
|
|
73
|
+
return import_nodemailer.default.createTransport({
|
|
74
|
+
host: settings.smtpHost,
|
|
75
|
+
port: settings.smtpPort,
|
|
76
|
+
secure,
|
|
77
|
+
auth: {
|
|
78
|
+
user: settings.smtpUser,
|
|
79
|
+
pass: settings.smtpPassword
|
|
80
|
+
},
|
|
81
|
+
tls: {
|
|
82
|
+
rejectUnauthorized: true
|
|
83
|
+
},
|
|
84
|
+
requireTLS,
|
|
85
|
+
connectionTimeout: 1e4,
|
|
86
|
+
greetingTimeout: 1e4,
|
|
87
|
+
socketTimeout: 1e4
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
async function validateSMTP(ctx) {
|
|
91
|
+
try {
|
|
92
|
+
const settings = await getSettings(ctx);
|
|
93
|
+
if (!settings.smtpHost || !settings.smtpUser || !settings.smtpPassword) {
|
|
94
|
+
return { ok: false, error: "SMTP settings are incomplete. Please configure host, user, and password." };
|
|
95
|
+
}
|
|
96
|
+
const transporter = createTransporter(settings);
|
|
97
|
+
await transporter.verify();
|
|
98
|
+
return { ok: true };
|
|
99
|
+
} catch (err) {
|
|
100
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
101
|
+
return { ok: false, error: `Connection failed: ${message}` };
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
async function sendTestEmail(ctx, recipient) {
|
|
105
|
+
try {
|
|
106
|
+
const settings = await getSettings(ctx);
|
|
107
|
+
if (!settings.defaultFrom) {
|
|
108
|
+
return { ok: false, error: "Default From address is not configured." };
|
|
109
|
+
}
|
|
110
|
+
const transporter = createTransporter(settings);
|
|
111
|
+
await transporter.sendMail({
|
|
112
|
+
from: settings.fromName ? `"${settings.fromName}" <${settings.defaultFrom}>` : settings.defaultFrom,
|
|
113
|
+
to: recipient,
|
|
114
|
+
subject: "Test email from EmDash + AWS SES",
|
|
115
|
+
text: "This is a test email sent via Amazon SES from your EmDash CMS.",
|
|
116
|
+
html: "<p>This is a test email sent via <strong>Amazon SES</strong> from your EmDash CMS.</p>",
|
|
117
|
+
replyTo: settings.replyTo || void 0
|
|
118
|
+
});
|
|
119
|
+
return { ok: true };
|
|
120
|
+
} catch (err) {
|
|
121
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
122
|
+
return { ok: false, error: `Send failed: ${message}` };
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
async function renderSettingsPage(ctx) {
|
|
126
|
+
const settings = await getSettings(ctx);
|
|
127
|
+
return {
|
|
128
|
+
blocks: [
|
|
129
|
+
{ type: "header", text: "AWS SES Settings" },
|
|
130
|
+
{ type: "section", text: "Configure your Amazon SES SMTP transport. All settings are stored in the plugin KV store." },
|
|
131
|
+
{ type: "divider" },
|
|
132
|
+
{
|
|
133
|
+
type: "form",
|
|
134
|
+
block_id: "settings",
|
|
135
|
+
fields: [
|
|
136
|
+
{
|
|
137
|
+
type: "text_input",
|
|
138
|
+
action_id: "smtpHost",
|
|
139
|
+
label: "SMTP Host",
|
|
140
|
+
initial_value: settings.smtpHost,
|
|
141
|
+
placeholder: "email-smtp.us-east-1.amazonaws.com"
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
type: "number_input",
|
|
145
|
+
action_id: "smtpPort",
|
|
146
|
+
label: "SMTP Port",
|
|
147
|
+
initial_value: settings.smtpPort,
|
|
148
|
+
min: 1,
|
|
149
|
+
max: 65535
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
type: "text_input",
|
|
153
|
+
action_id: "smtpUser",
|
|
154
|
+
label: "SMTP Username",
|
|
155
|
+
initial_value: settings.smtpUser,
|
|
156
|
+
placeholder: "AWS SES SMTP username (IAM Access Key ID)"
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
type: "secret_input",
|
|
160
|
+
action_id: "smtpPassword",
|
|
161
|
+
label: "SMTP Password",
|
|
162
|
+
initial_value: ""
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
type: "text_input",
|
|
166
|
+
action_id: "region",
|
|
167
|
+
label: "AWS Region",
|
|
168
|
+
initial_value: settings.region,
|
|
169
|
+
placeholder: "us-east-1"
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
type: "text_input",
|
|
173
|
+
action_id: "defaultFrom",
|
|
174
|
+
label: "Default From Address",
|
|
175
|
+
initial_value: settings.defaultFrom,
|
|
176
|
+
placeholder: "noreply@example.com"
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
type: "text_input",
|
|
180
|
+
action_id: "fromName",
|
|
181
|
+
label: "From Name",
|
|
182
|
+
initial_value: settings.fromName,
|
|
183
|
+
placeholder: "My Site"
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
type: "text_input",
|
|
187
|
+
action_id: "replyTo",
|
|
188
|
+
label: "Reply-To Address",
|
|
189
|
+
initial_value: settings.replyTo,
|
|
190
|
+
placeholder: "support@example.com"
|
|
191
|
+
}
|
|
192
|
+
],
|
|
193
|
+
submit: { label: "Save Settings", action_id: "save_settings" }
|
|
194
|
+
}
|
|
195
|
+
]
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
function getDefaultEndpoint(region) {
|
|
199
|
+
return `email-smtp.${region}.amazonaws.com`;
|
|
200
|
+
}
|
|
201
|
+
async function saveSettings(ctx, values) {
|
|
202
|
+
const currentSettings = await getSettings(ctx);
|
|
203
|
+
if (typeof values.region === "string" && values.region.length > 0 && values.region !== currentSettings.region) {
|
|
204
|
+
const oldEndpoint = getDefaultEndpoint(currentSettings.region);
|
|
205
|
+
if (!currentSettings.smtpHost || currentSettings.smtpHost === oldEndpoint) {
|
|
206
|
+
values.smtpHost = getDefaultEndpoint(values.region);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
if (typeof values.smtpHost === "string" && values.smtpHost.length > 0) {
|
|
210
|
+
await ctx.kv.set("settings:smtpHost", values.smtpHost);
|
|
211
|
+
}
|
|
212
|
+
if (typeof values.smtpPort === "number" && values.smtpPort >= 1 && values.smtpPort <= 65535) {
|
|
213
|
+
await ctx.kv.set("settings:smtpPort", values.smtpPort);
|
|
214
|
+
}
|
|
215
|
+
if (typeof values.smtpUser === "string" && values.smtpUser.length > 0) {
|
|
216
|
+
await ctx.kv.set("settings:smtpUser", values.smtpUser);
|
|
217
|
+
}
|
|
218
|
+
if (typeof values.smtpPassword === "string" && values.smtpPassword.length > 0) {
|
|
219
|
+
await ctx.kv.set("settings:smtpPassword", values.smtpPassword);
|
|
220
|
+
}
|
|
221
|
+
if (typeof values.region === "string" && values.region.length > 0) {
|
|
222
|
+
await ctx.kv.set("settings:region", values.region);
|
|
223
|
+
}
|
|
224
|
+
if (typeof values.defaultFrom === "string") {
|
|
225
|
+
await ctx.kv.set("settings:defaultFrom", values.defaultFrom);
|
|
226
|
+
}
|
|
227
|
+
if (typeof values.fromName === "string") {
|
|
228
|
+
await ctx.kv.set("settings:fromName", values.fromName);
|
|
229
|
+
}
|
|
230
|
+
if (typeof values.replyTo === "string") {
|
|
231
|
+
await ctx.kv.set("settings:replyTo", values.replyTo);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
async function renderTestPage(ctx, validation, sendResult) {
|
|
235
|
+
const settings = await getSettings(ctx);
|
|
236
|
+
const blocks = [
|
|
237
|
+
{ type: "header", text: "AWS SES Test & Status" },
|
|
238
|
+
{ type: "section", text: `Current endpoint: ${settings.smtpHost}:${settings.smtpPort} (${settings.region})` },
|
|
239
|
+
{ type: "divider" }
|
|
240
|
+
];
|
|
241
|
+
if (validation) {
|
|
242
|
+
if (validation.ok) {
|
|
243
|
+
blocks.push({
|
|
244
|
+
type: "banner",
|
|
245
|
+
title: "Connection verified",
|
|
246
|
+
description: "SMTP credentials are valid.",
|
|
247
|
+
variant: "default"
|
|
248
|
+
});
|
|
249
|
+
} else {
|
|
250
|
+
blocks.push({
|
|
251
|
+
type: "banner",
|
|
252
|
+
title: "Connection failed",
|
|
253
|
+
description: validation.error,
|
|
254
|
+
variant: "error"
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
blocks.push(
|
|
259
|
+
{ type: "section", text: "Test your SMTP connection and verify your credentials are correct." },
|
|
260
|
+
{
|
|
261
|
+
type: "actions",
|
|
262
|
+
elements: [
|
|
263
|
+
{
|
|
264
|
+
type: "button",
|
|
265
|
+
label: "Test Connection",
|
|
266
|
+
action_id: "validate"
|
|
267
|
+
}
|
|
268
|
+
]
|
|
269
|
+
},
|
|
270
|
+
{ type: "divider" },
|
|
271
|
+
{ type: "header", text: "Send Test Email" },
|
|
272
|
+
{ type: "section", text: "Send a test email to verify the full transport pipeline. Enter a recipient address below." }
|
|
273
|
+
);
|
|
274
|
+
if (sendResult) {
|
|
275
|
+
if (sendResult.ok) {
|
|
276
|
+
blocks.push({
|
|
277
|
+
type: "banner",
|
|
278
|
+
title: "Email sent",
|
|
279
|
+
description: `Test email sent successfully to ${sendResult.recipient}.`,
|
|
280
|
+
variant: "default"
|
|
281
|
+
});
|
|
282
|
+
} else {
|
|
283
|
+
blocks.push({
|
|
284
|
+
type: "banner",
|
|
285
|
+
title: "Email send failed",
|
|
286
|
+
description: sendResult.error,
|
|
287
|
+
variant: "error"
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
blocks.push({
|
|
292
|
+
type: "form",
|
|
293
|
+
block_id: "test_form",
|
|
294
|
+
fields: [
|
|
295
|
+
{
|
|
296
|
+
type: "text_input",
|
|
297
|
+
action_id: "recipient",
|
|
298
|
+
label: "Recipient Email",
|
|
299
|
+
placeholder: "you@example.com",
|
|
300
|
+
initial_value: sendResult?.ok ? "" : void 0
|
|
301
|
+
}
|
|
302
|
+
],
|
|
303
|
+
submit: { label: "Send Test", action_id: "send_test" }
|
|
304
|
+
});
|
|
305
|
+
return { blocks };
|
|
306
|
+
}
|
|
307
|
+
var sandbox_entry_default = (0, import_emdash.definePlugin)({
|
|
308
|
+
id: "emdash-aws-ses",
|
|
309
|
+
version: "1.0.0",
|
|
310
|
+
capabilities: ["hooks.email-transport:register"],
|
|
311
|
+
hooks: {
|
|
312
|
+
"plugin:install": async (_event, ctx) => {
|
|
313
|
+
await ctx.kv.set("settings:smtpHost", "email-smtp.us-east-1.amazonaws.com");
|
|
314
|
+
await ctx.kv.set("settings:smtpPort", 587);
|
|
315
|
+
await ctx.kv.set("settings:region", "us-east-1");
|
|
316
|
+
ctx.log.info("AWS SES plugin defaults seeded");
|
|
317
|
+
},
|
|
318
|
+
"email:deliver": {
|
|
319
|
+
exclusive: true,
|
|
320
|
+
handler: async ({ message, source }, ctx) => {
|
|
321
|
+
const settings = await getSettings(ctx);
|
|
322
|
+
const transporter = createTransporter(settings);
|
|
323
|
+
await transporter.sendMail({
|
|
324
|
+
from: settings.fromName ? `"${settings.fromName}" <${settings.defaultFrom}>` : settings.defaultFrom,
|
|
325
|
+
to: message.to,
|
|
326
|
+
subject: message.subject,
|
|
327
|
+
text: message.text,
|
|
328
|
+
html: message.html,
|
|
329
|
+
replyTo: settings.replyTo || void 0
|
|
330
|
+
});
|
|
331
|
+
ctx.log.info(`Email sent via AWS SES to ${message.to}`, { source });
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
},
|
|
335
|
+
routes: {
|
|
336
|
+
admin: {
|
|
337
|
+
handler: async (routeCtx, ctx) => {
|
|
338
|
+
const interaction = routeCtx.input;
|
|
339
|
+
if (interaction.type === "page_load" && (!interaction.page || interaction.page === "/")) {
|
|
340
|
+
return renderSettingsPage(ctx);
|
|
341
|
+
}
|
|
342
|
+
if (interaction.type === "page_load" && interaction.page === "/settings") {
|
|
343
|
+
return renderSettingsPage(ctx);
|
|
344
|
+
}
|
|
345
|
+
if (interaction.type === "form_submit" && interaction.action_id === "save_settings") {
|
|
346
|
+
await saveSettings(ctx, interaction.values ?? {});
|
|
347
|
+
const settingsPage = await renderSettingsPage(ctx);
|
|
348
|
+
const settingsBlocks = settingsPage.blocks;
|
|
349
|
+
return {
|
|
350
|
+
blocks: [
|
|
351
|
+
settingsBlocks[0],
|
|
352
|
+
{
|
|
353
|
+
type: "banner",
|
|
354
|
+
title: "Settings saved",
|
|
355
|
+
description: "Your AWS SES configuration has been updated successfully.",
|
|
356
|
+
variant: "default"
|
|
357
|
+
},
|
|
358
|
+
...settingsBlocks.slice(1)
|
|
359
|
+
],
|
|
360
|
+
toast: { message: "Settings saved successfully", type: "success" }
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
if (interaction.type === "page_load" && interaction.page === "/test") {
|
|
364
|
+
return renderTestPage(ctx);
|
|
365
|
+
}
|
|
366
|
+
if (interaction.type === "block_action" && interaction.action_id === "validate") {
|
|
367
|
+
const result = await validateSMTP(ctx);
|
|
368
|
+
return {
|
|
369
|
+
...await renderTestPage(ctx, result),
|
|
370
|
+
toast: result.ok ? { message: "Connection verified successfully", type: "success" } : { message: result.error ?? "Validation failed", type: "error" }
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
if (interaction.type === "form_submit" && interaction.action_id === "send_test") {
|
|
374
|
+
const recipient = interaction.values?.recipient?.trim();
|
|
375
|
+
if (!recipient) {
|
|
376
|
+
return {
|
|
377
|
+
...await renderTestPage(ctx),
|
|
378
|
+
toast: { message: "Please enter a recipient email", type: "error" }
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
const result = await sendTestEmail(ctx, recipient);
|
|
382
|
+
return {
|
|
383
|
+
...await renderTestPage(ctx, null, { ...result, recipient }),
|
|
384
|
+
toast: result.ok ? { message: `Test email sent to ${recipient}`, type: "success" } : { message: result.error ?? "Send failed", type: "error" }
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
return { blocks: [] };
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
});
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
// src/sandbox-entry.ts
|
|
2
|
+
import { definePlugin } from "emdash";
|
|
3
|
+
import nodemailer from "nodemailer";
|
|
4
|
+
async function getSettings(ctx) {
|
|
5
|
+
const defaults = {
|
|
6
|
+
smtpHost: "email-smtp.us-east-1.amazonaws.com",
|
|
7
|
+
smtpPort: 587,
|
|
8
|
+
region: "us-east-1",
|
|
9
|
+
defaultFrom: "",
|
|
10
|
+
fromName: "",
|
|
11
|
+
replyTo: "",
|
|
12
|
+
smtpUser: "",
|
|
13
|
+
smtpPassword: ""
|
|
14
|
+
};
|
|
15
|
+
const [smtpHost, smtpPortRaw, smtpUser, smtpPassword, region, defaultFrom, fromName, replyTo] = await Promise.all([
|
|
16
|
+
ctx.kv.get("settings:smtpHost"),
|
|
17
|
+
ctx.kv.get("settings:smtpPort"),
|
|
18
|
+
ctx.kv.get("settings:smtpUser"),
|
|
19
|
+
ctx.kv.get("settings:smtpPassword"),
|
|
20
|
+
ctx.kv.get("settings:region"),
|
|
21
|
+
ctx.kv.get("settings:defaultFrom"),
|
|
22
|
+
ctx.kv.get("settings:fromName"),
|
|
23
|
+
ctx.kv.get("settings:replyTo")
|
|
24
|
+
]);
|
|
25
|
+
return {
|
|
26
|
+
smtpHost: smtpHost ?? defaults.smtpHost,
|
|
27
|
+
smtpPort: Number(smtpPortRaw ?? defaults.smtpPort),
|
|
28
|
+
smtpUser: smtpUser ?? defaults.smtpUser,
|
|
29
|
+
smtpPassword: smtpPassword ?? defaults.smtpPassword,
|
|
30
|
+
region: region ?? defaults.region,
|
|
31
|
+
defaultFrom: defaultFrom ?? defaults.defaultFrom,
|
|
32
|
+
fromName: fromName ?? defaults.fromName,
|
|
33
|
+
replyTo: replyTo ?? defaults.replyTo
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function createTransporter(settings) {
|
|
37
|
+
const secure = settings.smtpPort === 465;
|
|
38
|
+
const requireTLS = settings.smtpPort === 587 || settings.smtpPort === 25 || settings.smtpPort === 2587;
|
|
39
|
+
return nodemailer.createTransport({
|
|
40
|
+
host: settings.smtpHost,
|
|
41
|
+
port: settings.smtpPort,
|
|
42
|
+
secure,
|
|
43
|
+
auth: {
|
|
44
|
+
user: settings.smtpUser,
|
|
45
|
+
pass: settings.smtpPassword
|
|
46
|
+
},
|
|
47
|
+
tls: {
|
|
48
|
+
rejectUnauthorized: true
|
|
49
|
+
},
|
|
50
|
+
requireTLS,
|
|
51
|
+
connectionTimeout: 1e4,
|
|
52
|
+
greetingTimeout: 1e4,
|
|
53
|
+
socketTimeout: 1e4
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
async function validateSMTP(ctx) {
|
|
57
|
+
try {
|
|
58
|
+
const settings = await getSettings(ctx);
|
|
59
|
+
if (!settings.smtpHost || !settings.smtpUser || !settings.smtpPassword) {
|
|
60
|
+
return { ok: false, error: "SMTP settings are incomplete. Please configure host, user, and password." };
|
|
61
|
+
}
|
|
62
|
+
const transporter = createTransporter(settings);
|
|
63
|
+
await transporter.verify();
|
|
64
|
+
return { ok: true };
|
|
65
|
+
} catch (err) {
|
|
66
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
67
|
+
return { ok: false, error: `Connection failed: ${message}` };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
async function sendTestEmail(ctx, recipient) {
|
|
71
|
+
try {
|
|
72
|
+
const settings = await getSettings(ctx);
|
|
73
|
+
if (!settings.defaultFrom) {
|
|
74
|
+
return { ok: false, error: "Default From address is not configured." };
|
|
75
|
+
}
|
|
76
|
+
const transporter = createTransporter(settings);
|
|
77
|
+
await transporter.sendMail({
|
|
78
|
+
from: settings.fromName ? `"${settings.fromName}" <${settings.defaultFrom}>` : settings.defaultFrom,
|
|
79
|
+
to: recipient,
|
|
80
|
+
subject: "Test email from EmDash + AWS SES",
|
|
81
|
+
text: "This is a test email sent via Amazon SES from your EmDash CMS.",
|
|
82
|
+
html: "<p>This is a test email sent via <strong>Amazon SES</strong> from your EmDash CMS.</p>",
|
|
83
|
+
replyTo: settings.replyTo || void 0
|
|
84
|
+
});
|
|
85
|
+
return { ok: true };
|
|
86
|
+
} catch (err) {
|
|
87
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
88
|
+
return { ok: false, error: `Send failed: ${message}` };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
async function renderSettingsPage(ctx) {
|
|
92
|
+
const settings = await getSettings(ctx);
|
|
93
|
+
return {
|
|
94
|
+
blocks: [
|
|
95
|
+
{ type: "header", text: "AWS SES Settings" },
|
|
96
|
+
{ type: "section", text: "Configure your Amazon SES SMTP transport. All settings are stored in the plugin KV store." },
|
|
97
|
+
{ type: "divider" },
|
|
98
|
+
{
|
|
99
|
+
type: "form",
|
|
100
|
+
block_id: "settings",
|
|
101
|
+
fields: [
|
|
102
|
+
{
|
|
103
|
+
type: "text_input",
|
|
104
|
+
action_id: "smtpHost",
|
|
105
|
+
label: "SMTP Host",
|
|
106
|
+
initial_value: settings.smtpHost,
|
|
107
|
+
placeholder: "email-smtp.us-east-1.amazonaws.com"
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
type: "number_input",
|
|
111
|
+
action_id: "smtpPort",
|
|
112
|
+
label: "SMTP Port",
|
|
113
|
+
initial_value: settings.smtpPort,
|
|
114
|
+
min: 1,
|
|
115
|
+
max: 65535
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
type: "text_input",
|
|
119
|
+
action_id: "smtpUser",
|
|
120
|
+
label: "SMTP Username",
|
|
121
|
+
initial_value: settings.smtpUser,
|
|
122
|
+
placeholder: "AWS SES SMTP username (IAM Access Key ID)"
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
type: "secret_input",
|
|
126
|
+
action_id: "smtpPassword",
|
|
127
|
+
label: "SMTP Password",
|
|
128
|
+
initial_value: ""
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
type: "text_input",
|
|
132
|
+
action_id: "region",
|
|
133
|
+
label: "AWS Region",
|
|
134
|
+
initial_value: settings.region,
|
|
135
|
+
placeholder: "us-east-1"
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
type: "text_input",
|
|
139
|
+
action_id: "defaultFrom",
|
|
140
|
+
label: "Default From Address",
|
|
141
|
+
initial_value: settings.defaultFrom,
|
|
142
|
+
placeholder: "noreply@example.com"
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
type: "text_input",
|
|
146
|
+
action_id: "fromName",
|
|
147
|
+
label: "From Name",
|
|
148
|
+
initial_value: settings.fromName,
|
|
149
|
+
placeholder: "My Site"
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
type: "text_input",
|
|
153
|
+
action_id: "replyTo",
|
|
154
|
+
label: "Reply-To Address",
|
|
155
|
+
initial_value: settings.replyTo,
|
|
156
|
+
placeholder: "support@example.com"
|
|
157
|
+
}
|
|
158
|
+
],
|
|
159
|
+
submit: { label: "Save Settings", action_id: "save_settings" }
|
|
160
|
+
}
|
|
161
|
+
]
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
function getDefaultEndpoint(region) {
|
|
165
|
+
return `email-smtp.${region}.amazonaws.com`;
|
|
166
|
+
}
|
|
167
|
+
async function saveSettings(ctx, values) {
|
|
168
|
+
const currentSettings = await getSettings(ctx);
|
|
169
|
+
if (typeof values.region === "string" && values.region.length > 0 && values.region !== currentSettings.region) {
|
|
170
|
+
const oldEndpoint = getDefaultEndpoint(currentSettings.region);
|
|
171
|
+
if (!currentSettings.smtpHost || currentSettings.smtpHost === oldEndpoint) {
|
|
172
|
+
values.smtpHost = getDefaultEndpoint(values.region);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (typeof values.smtpHost === "string" && values.smtpHost.length > 0) {
|
|
176
|
+
await ctx.kv.set("settings:smtpHost", values.smtpHost);
|
|
177
|
+
}
|
|
178
|
+
if (typeof values.smtpPort === "number" && values.smtpPort >= 1 && values.smtpPort <= 65535) {
|
|
179
|
+
await ctx.kv.set("settings:smtpPort", values.smtpPort);
|
|
180
|
+
}
|
|
181
|
+
if (typeof values.smtpUser === "string" && values.smtpUser.length > 0) {
|
|
182
|
+
await ctx.kv.set("settings:smtpUser", values.smtpUser);
|
|
183
|
+
}
|
|
184
|
+
if (typeof values.smtpPassword === "string" && values.smtpPassword.length > 0) {
|
|
185
|
+
await ctx.kv.set("settings:smtpPassword", values.smtpPassword);
|
|
186
|
+
}
|
|
187
|
+
if (typeof values.region === "string" && values.region.length > 0) {
|
|
188
|
+
await ctx.kv.set("settings:region", values.region);
|
|
189
|
+
}
|
|
190
|
+
if (typeof values.defaultFrom === "string") {
|
|
191
|
+
await ctx.kv.set("settings:defaultFrom", values.defaultFrom);
|
|
192
|
+
}
|
|
193
|
+
if (typeof values.fromName === "string") {
|
|
194
|
+
await ctx.kv.set("settings:fromName", values.fromName);
|
|
195
|
+
}
|
|
196
|
+
if (typeof values.replyTo === "string") {
|
|
197
|
+
await ctx.kv.set("settings:replyTo", values.replyTo);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
async function renderTestPage(ctx, validation, sendResult) {
|
|
201
|
+
const settings = await getSettings(ctx);
|
|
202
|
+
const blocks = [
|
|
203
|
+
{ type: "header", text: "AWS SES Test & Status" },
|
|
204
|
+
{ type: "section", text: `Current endpoint: ${settings.smtpHost}:${settings.smtpPort} (${settings.region})` },
|
|
205
|
+
{ type: "divider" }
|
|
206
|
+
];
|
|
207
|
+
if (validation) {
|
|
208
|
+
if (validation.ok) {
|
|
209
|
+
blocks.push({
|
|
210
|
+
type: "banner",
|
|
211
|
+
title: "Connection verified",
|
|
212
|
+
description: "SMTP credentials are valid.",
|
|
213
|
+
variant: "default"
|
|
214
|
+
});
|
|
215
|
+
} else {
|
|
216
|
+
blocks.push({
|
|
217
|
+
type: "banner",
|
|
218
|
+
title: "Connection failed",
|
|
219
|
+
description: validation.error,
|
|
220
|
+
variant: "error"
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
blocks.push(
|
|
225
|
+
{ type: "section", text: "Test your SMTP connection and verify your credentials are correct." },
|
|
226
|
+
{
|
|
227
|
+
type: "actions",
|
|
228
|
+
elements: [
|
|
229
|
+
{
|
|
230
|
+
type: "button",
|
|
231
|
+
label: "Test Connection",
|
|
232
|
+
action_id: "validate"
|
|
233
|
+
}
|
|
234
|
+
]
|
|
235
|
+
},
|
|
236
|
+
{ type: "divider" },
|
|
237
|
+
{ type: "header", text: "Send Test Email" },
|
|
238
|
+
{ type: "section", text: "Send a test email to verify the full transport pipeline. Enter a recipient address below." }
|
|
239
|
+
);
|
|
240
|
+
if (sendResult) {
|
|
241
|
+
if (sendResult.ok) {
|
|
242
|
+
blocks.push({
|
|
243
|
+
type: "banner",
|
|
244
|
+
title: "Email sent",
|
|
245
|
+
description: `Test email sent successfully to ${sendResult.recipient}.`,
|
|
246
|
+
variant: "default"
|
|
247
|
+
});
|
|
248
|
+
} else {
|
|
249
|
+
blocks.push({
|
|
250
|
+
type: "banner",
|
|
251
|
+
title: "Email send failed",
|
|
252
|
+
description: sendResult.error,
|
|
253
|
+
variant: "error"
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
blocks.push({
|
|
258
|
+
type: "form",
|
|
259
|
+
block_id: "test_form",
|
|
260
|
+
fields: [
|
|
261
|
+
{
|
|
262
|
+
type: "text_input",
|
|
263
|
+
action_id: "recipient",
|
|
264
|
+
label: "Recipient Email",
|
|
265
|
+
placeholder: "you@example.com",
|
|
266
|
+
initial_value: sendResult?.ok ? "" : void 0
|
|
267
|
+
}
|
|
268
|
+
],
|
|
269
|
+
submit: { label: "Send Test", action_id: "send_test" }
|
|
270
|
+
});
|
|
271
|
+
return { blocks };
|
|
272
|
+
}
|
|
273
|
+
var sandbox_entry_default = definePlugin({
|
|
274
|
+
id: "emdash-aws-ses",
|
|
275
|
+
version: "1.0.0",
|
|
276
|
+
capabilities: ["hooks.email-transport:register"],
|
|
277
|
+
hooks: {
|
|
278
|
+
"plugin:install": async (_event, ctx) => {
|
|
279
|
+
await ctx.kv.set("settings:smtpHost", "email-smtp.us-east-1.amazonaws.com");
|
|
280
|
+
await ctx.kv.set("settings:smtpPort", 587);
|
|
281
|
+
await ctx.kv.set("settings:region", "us-east-1");
|
|
282
|
+
ctx.log.info("AWS SES plugin defaults seeded");
|
|
283
|
+
},
|
|
284
|
+
"email:deliver": {
|
|
285
|
+
exclusive: true,
|
|
286
|
+
handler: async ({ message, source }, ctx) => {
|
|
287
|
+
const settings = await getSettings(ctx);
|
|
288
|
+
const transporter = createTransporter(settings);
|
|
289
|
+
await transporter.sendMail({
|
|
290
|
+
from: settings.fromName ? `"${settings.fromName}" <${settings.defaultFrom}>` : settings.defaultFrom,
|
|
291
|
+
to: message.to,
|
|
292
|
+
subject: message.subject,
|
|
293
|
+
text: message.text,
|
|
294
|
+
html: message.html,
|
|
295
|
+
replyTo: settings.replyTo || void 0
|
|
296
|
+
});
|
|
297
|
+
ctx.log.info(`Email sent via AWS SES to ${message.to}`, { source });
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
},
|
|
301
|
+
routes: {
|
|
302
|
+
admin: {
|
|
303
|
+
handler: async (routeCtx, ctx) => {
|
|
304
|
+
const interaction = routeCtx.input;
|
|
305
|
+
if (interaction.type === "page_load" && (!interaction.page || interaction.page === "/")) {
|
|
306
|
+
return renderSettingsPage(ctx);
|
|
307
|
+
}
|
|
308
|
+
if (interaction.type === "page_load" && interaction.page === "/settings") {
|
|
309
|
+
return renderSettingsPage(ctx);
|
|
310
|
+
}
|
|
311
|
+
if (interaction.type === "form_submit" && interaction.action_id === "save_settings") {
|
|
312
|
+
await saveSettings(ctx, interaction.values ?? {});
|
|
313
|
+
const settingsPage = await renderSettingsPage(ctx);
|
|
314
|
+
const settingsBlocks = settingsPage.blocks;
|
|
315
|
+
return {
|
|
316
|
+
blocks: [
|
|
317
|
+
settingsBlocks[0],
|
|
318
|
+
{
|
|
319
|
+
type: "banner",
|
|
320
|
+
title: "Settings saved",
|
|
321
|
+
description: "Your AWS SES configuration has been updated successfully.",
|
|
322
|
+
variant: "default"
|
|
323
|
+
},
|
|
324
|
+
...settingsBlocks.slice(1)
|
|
325
|
+
],
|
|
326
|
+
toast: { message: "Settings saved successfully", type: "success" }
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
if (interaction.type === "page_load" && interaction.page === "/test") {
|
|
330
|
+
return renderTestPage(ctx);
|
|
331
|
+
}
|
|
332
|
+
if (interaction.type === "block_action" && interaction.action_id === "validate") {
|
|
333
|
+
const result = await validateSMTP(ctx);
|
|
334
|
+
return {
|
|
335
|
+
...await renderTestPage(ctx, result),
|
|
336
|
+
toast: result.ok ? { message: "Connection verified successfully", type: "success" } : { message: result.error ?? "Validation failed", type: "error" }
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
if (interaction.type === "form_submit" && interaction.action_id === "send_test") {
|
|
340
|
+
const recipient = interaction.values?.recipient?.trim();
|
|
341
|
+
if (!recipient) {
|
|
342
|
+
return {
|
|
343
|
+
...await renderTestPage(ctx),
|
|
344
|
+
toast: { message: "Please enter a recipient email", type: "error" }
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
const result = await sendTestEmail(ctx, recipient);
|
|
348
|
+
return {
|
|
349
|
+
...await renderTestPage(ctx, null, { ...result, recipient }),
|
|
350
|
+
toast: result.ok ? { message: `Test email sent to ${recipient}`, type: "success" } : { message: result.error ?? "Send failed", type: "error" }
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
return { blocks: [] };
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
export {
|
|
359
|
+
sandbox_entry_default as default
|
|
360
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "emdash-aws-ses",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Amazon SES SMTP email transport plugin for EmDash CMS",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"emdash",
|
|
8
|
+
"emdash-plugin",
|
|
9
|
+
"aws-ses",
|
|
10
|
+
"astro",
|
|
11
|
+
"aws"
|
|
12
|
+
],
|
|
13
|
+
"type": "module",
|
|
14
|
+
"main": "./dist/index.cjs",
|
|
15
|
+
"module": "./dist/index.js",
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"files": [
|
|
18
|
+
"dist"
|
|
19
|
+
],
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"import": "./dist/index.js",
|
|
24
|
+
"require": "./dist/index.cjs"
|
|
25
|
+
},
|
|
26
|
+
"./sandbox": {
|
|
27
|
+
"types": "./dist/sandbox-entry.d.ts",
|
|
28
|
+
"import": "./dist/sandbox-entry.js",
|
|
29
|
+
"require": "./dist/sandbox-entry.cjs"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "tsup src/index.ts src/sandbox-entry.ts --format cjs,esm --clean"
|
|
34
|
+
},
|
|
35
|
+
"peerDependencies": {
|
|
36
|
+
"emdash": "^0.29.0"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"nodemailer": "^9.0.3"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@types/nodemailer": "^8.0.1",
|
|
43
|
+
"typescript": "^7.0.2",
|
|
44
|
+
"tsup": "^8.5.1"
|
|
45
|
+
}
|
|
46
|
+
}
|