emdash-aws-ses 1.0.0 → 1.0.2

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/.gitattributes ADDED
@@ -0,0 +1,2 @@
1
+ # Auto detect text files and perform LF normalization
2
+ * text=auto
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "emdash-aws-ses",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Amazon SES SMTP email transport plugin for EmDash CMS",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -14,9 +14,6 @@
14
14
  "main": "./dist/index.cjs",
15
15
  "module": "./dist/index.js",
16
16
  "types": "./dist/index.d.ts",
17
- "files": [
18
- "dist"
19
- ],
20
17
  "exports": {
21
18
  ".": {
22
19
  "types": "./dist/index.d.ts",
@@ -1,5 +1,6 @@
1
- // src/index.ts
2
- function awsSesPlugin() {
1
+ import type { PluginDescriptor } from "emdash";
2
+
3
+ export function awsSesPlugin(): PluginDescriptor {
3
4
  return {
4
5
  id: "emdash-aws-ses",
5
6
  version: "1.0.0",
@@ -7,12 +8,10 @@ function awsSesPlugin() {
7
8
  entrypoint: "emdash-aws-ses/sandbox",
8
9
  options: {},
9
10
  capabilities: ["hooks.email-transport:register"],
11
+ allowedHosts: [],
10
12
  adminPages: [
11
13
  { path: "/settings", label: "AWS SES Settings", icon: "mail" },
12
- { path: "/test", label: "Test & Status", icon: "send" }
13
- ]
14
+ { path: "/test", label: "AWS SES Test & Status", icon: "send" },
15
+ ],
14
16
  };
15
17
  }
16
- export {
17
- awsSesPlugin
18
- };
@@ -1,7 +1,20 @@
1
- // src/sandbox-entry.ts
2
- import { definePlugin } from "emdash";
1
+ import type { SandboxedPlugin, SandboxedRouteContext, PluginContext, EmailDeliverEvent, LifecycleEvent } from "emdash/plugin";
3
2
  import nodemailer from "nodemailer";
4
- async function getSettings(ctx) {
3
+ import type { Transporter } from "nodemailer";
4
+ import type { BlockInteraction } from "@emdash-cms/blocks";
5
+
6
+ interface SesSettings {
7
+ smtpHost: string;
8
+ smtpPort: number;
9
+ smtpUser: string;
10
+ smtpPassword: string;
11
+ region: string;
12
+ defaultFrom: string;
13
+ fromName: string;
14
+ replyTo: string;
15
+ }
16
+
17
+ async function getSettings(ctx: PluginContext): Promise<SesSettings> {
5
18
  const defaults = {
6
19
  smtpHost: "email-smtp.us-east-1.amazonaws.com",
7
20
  smtpPort: 587,
@@ -10,18 +23,20 @@ async function getSettings(ctx) {
10
23
  fromName: "",
11
24
  replyTo: "",
12
25
  smtpUser: "",
13
- smtpPassword: ""
26
+ smtpPassword: "",
14
27
  };
28
+
15
29
  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")
30
+ ctx.kv.get<string>("settings:smtpHost"),
31
+ ctx.kv.get<number>("settings:smtpPort"),
32
+ ctx.kv.get<string>("settings:smtpUser"),
33
+ ctx.kv.get<string>("settings:smtpPassword"),
34
+ ctx.kv.get<string>("settings:region"),
35
+ ctx.kv.get<string>("settings:defaultFrom"),
36
+ ctx.kv.get<string>("settings:fromName"),
37
+ ctx.kv.get<string>("settings:replyTo"),
24
38
  ]);
39
+
25
40
  return {
26
41
  smtpHost: smtpHost ?? defaults.smtpHost,
27
42
  smtpPort: Number(smtpPortRaw ?? defaults.smtpPort),
@@ -30,30 +45,33 @@ async function getSettings(ctx) {
30
45
  region: region ?? defaults.region,
31
46
  defaultFrom: defaultFrom ?? defaults.defaultFrom,
32
47
  fromName: fromName ?? defaults.fromName,
33
- replyTo: replyTo ?? defaults.replyTo
48
+ replyTo: replyTo ?? defaults.replyTo,
34
49
  };
35
50
  }
36
- function createTransporter(settings) {
51
+
52
+ function createTransporter(settings: SesSettings): Transporter {
37
53
  const secure = settings.smtpPort === 465;
38
54
  const requireTLS = settings.smtpPort === 587 || settings.smtpPort === 25 || settings.smtpPort === 2587;
55
+
39
56
  return nodemailer.createTransport({
40
57
  host: settings.smtpHost,
41
58
  port: settings.smtpPort,
42
59
  secure,
43
60
  auth: {
44
61
  user: settings.smtpUser,
45
- pass: settings.smtpPassword
62
+ pass: settings.smtpPassword,
46
63
  },
47
64
  tls: {
48
- rejectUnauthorized: true
65
+ rejectUnauthorized: true,
49
66
  },
50
67
  requireTLS,
51
- connectionTimeout: 1e4,
52
- greetingTimeout: 1e4,
53
- socketTimeout: 1e4
68
+ connectionTimeout: 10000,
69
+ greetingTimeout: 10000,
70
+ socketTimeout: 10000,
54
71
  });
55
72
  }
56
- async function validateSMTP(ctx) {
73
+
74
+ async function validateSMTP(ctx: PluginContext): Promise<{ ok: boolean; error?: string }> {
57
75
  try {
58
76
  const settings = await getSettings(ctx);
59
77
  if (!settings.smtpHost || !settings.smtpUser || !settings.smtpPassword) {
@@ -67,7 +85,8 @@ async function validateSMTP(ctx) {
67
85
  return { ok: false, error: `Connection failed: ${message}` };
68
86
  }
69
87
  }
70
- async function sendTestEmail(ctx, recipient) {
88
+
89
+ async function sendTestEmail(ctx: PluginContext, recipient: string): Promise<{ ok: boolean; error?: string }> {
71
90
  try {
72
91
  const settings = await getSettings(ctx);
73
92
  if (!settings.defaultFrom) {
@@ -80,7 +99,7 @@ async function sendTestEmail(ctx, recipient) {
80
99
  subject: "Test email from EmDash + AWS SES",
81
100
  text: "This is a test email sent via Amazon SES from your EmDash CMS.",
82
101
  html: "<p>This is a test email sent via <strong>Amazon SES</strong> from your EmDash CMS.</p>",
83
- replyTo: settings.replyTo || void 0
102
+ replyTo: settings.replyTo || undefined,
84
103
  });
85
104
  return { ok: true };
86
105
  } catch (err) {
@@ -88,8 +107,10 @@ async function sendTestEmail(ctx, recipient) {
88
107
  return { ok: false, error: `Send failed: ${message}` };
89
108
  }
90
109
  }
91
- async function renderSettingsPage(ctx) {
110
+
111
+ async function renderSettingsPage(ctx: PluginContext) {
92
112
  const settings = await getSettings(ctx);
113
+
93
114
  return {
94
115
  blocks: [
95
116
  { type: "header", text: "AWS SES Settings" },
@@ -104,7 +125,7 @@ async function renderSettingsPage(ctx) {
104
125
  action_id: "smtpHost",
105
126
  label: "SMTP Host",
106
127
  initial_value: settings.smtpHost,
107
- placeholder: "email-smtp.us-east-1.amazonaws.com"
128
+ placeholder: "email-smtp.us-east-1.amazonaws.com",
108
129
  },
109
130
  {
110
131
  type: "number_input",
@@ -112,66 +133,73 @@ async function renderSettingsPage(ctx) {
112
133
  label: "SMTP Port",
113
134
  initial_value: settings.smtpPort,
114
135
  min: 1,
115
- max: 65535
136
+ max: 65535,
116
137
  },
117
138
  {
118
139
  type: "text_input",
119
140
  action_id: "smtpUser",
120
141
  label: "SMTP Username",
121
142
  initial_value: settings.smtpUser,
122
- placeholder: "AWS SES SMTP username (IAM Access Key ID)"
143
+ placeholder: "AWS SES SMTP username (IAM Access Key ID)",
123
144
  },
124
145
  {
125
146
  type: "secret_input",
126
147
  action_id: "smtpPassword",
127
148
  label: "SMTP Password",
128
- initial_value: ""
149
+ initial_value: "",
129
150
  },
130
151
  {
131
152
  type: "text_input",
132
153
  action_id: "region",
133
154
  label: "AWS Region",
134
155
  initial_value: settings.region,
135
- placeholder: "us-east-1"
156
+ placeholder: "us-east-1",
136
157
  },
137
158
  {
138
159
  type: "text_input",
139
160
  action_id: "defaultFrom",
140
161
  label: "Default From Address",
141
162
  initial_value: settings.defaultFrom,
142
- placeholder: "noreply@example.com"
163
+ placeholder: "noreply@example.com",
143
164
  },
144
165
  {
145
166
  type: "text_input",
146
167
  action_id: "fromName",
147
168
  label: "From Name",
148
169
  initial_value: settings.fromName,
149
- placeholder: "My Site"
170
+ placeholder: "My Site",
150
171
  },
151
172
  {
152
173
  type: "text_input",
153
174
  action_id: "replyTo",
154
175
  label: "Reply-To Address",
155
176
  initial_value: settings.replyTo,
156
- placeholder: "support@example.com"
157
- }
177
+ placeholder: "support@example.com",
178
+ },
158
179
  ],
159
- submit: { label: "Save Settings", action_id: "save_settings" }
160
- }
161
- ]
180
+ submit: { label: "Save Settings", action_id: "save_settings" },
181
+ },
182
+ { type: "divider" },
183
+ { type: "header", text: "Email Provider Activation" },
184
+ { type: "section", text: "To activate AWS SES as your active email provider, go to Settings > Email in the EmDash admin and select emdash-aws-ses from the list. This routes all EmDash email (magic links, invites, notifications) through AWS SES." },
185
+ ],
162
186
  };
163
187
  }
164
- function getDefaultEndpoint(region) {
188
+
189
+ function getDefaultEndpoint(region: string): string {
165
190
  return `email-smtp.${region}.amazonaws.com`;
166
191
  }
167
- async function saveSettings(ctx, values) {
192
+
193
+ async function saveSettings(ctx: PluginContext, values: Record<string, unknown>) {
168
194
  const currentSettings = await getSettings(ctx);
195
+
169
196
  if (typeof values.region === "string" && values.region.length > 0 && values.region !== currentSettings.region) {
170
197
  const oldEndpoint = getDefaultEndpoint(currentSettings.region);
171
198
  if (!currentSettings.smtpHost || currentSettings.smtpHost === oldEndpoint) {
172
199
  values.smtpHost = getDefaultEndpoint(values.region);
173
200
  }
174
201
  }
202
+
175
203
  if (typeof values.smtpHost === "string" && values.smtpHost.length > 0) {
176
204
  await ctx.kv.set("settings:smtpHost", values.smtpHost);
177
205
  }
@@ -197,30 +225,38 @@ async function saveSettings(ctx, values) {
197
225
  await ctx.kv.set("settings:replyTo", values.replyTo);
198
226
  }
199
227
  }
200
- async function renderTestPage(ctx, validation, sendResult) {
228
+
229
+ async function renderTestPage(
230
+ ctx: PluginContext,
231
+ validation?: { ok: boolean; error?: string } | null,
232
+ sendResult?: { ok: boolean; error?: string; recipient?: string } | null,
233
+ ) {
201
234
  const settings = await getSettings(ctx);
202
- const blocks = [
235
+
236
+ const blocks: Array<Record<string, unknown>> = [
203
237
  { type: "header", text: "AWS SES Test & Status" },
204
238
  { type: "section", text: `Current endpoint: ${settings.smtpHost}:${settings.smtpPort} (${settings.region})` },
205
- { type: "divider" }
239
+ { type: "divider" },
206
240
  ];
241
+
207
242
  if (validation) {
208
243
  if (validation.ok) {
209
244
  blocks.push({
210
245
  type: "banner",
211
246
  title: "Connection verified",
212
247
  description: "SMTP credentials are valid.",
213
- variant: "default"
248
+ variant: "default",
214
249
  });
215
250
  } else {
216
251
  blocks.push({
217
252
  type: "banner",
218
253
  title: "Connection failed",
219
254
  description: validation.error,
220
- variant: "error"
255
+ variant: "error",
221
256
  });
222
257
  }
223
258
  }
259
+
224
260
  blocks.push(
225
261
  { type: "section", text: "Test your SMTP connection and verify your credentials are correct." },
226
262
  {
@@ -229,31 +265,33 @@ async function renderTestPage(ctx, validation, sendResult) {
229
265
  {
230
266
  type: "button",
231
267
  label: "Test Connection",
232
- action_id: "validate"
233
- }
234
- ]
268
+ action_id: "validate",
269
+ },
270
+ ],
235
271
  },
236
272
  { type: "divider" },
237
273
  { 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." }
274
+ { type: "section", text: "Send a test email to verify the full transport pipeline. Enter a recipient address below." },
239
275
  );
276
+
240
277
  if (sendResult) {
241
278
  if (sendResult.ok) {
242
279
  blocks.push({
243
280
  type: "banner",
244
281
  title: "Email sent",
245
282
  description: `Test email sent successfully to ${sendResult.recipient}.`,
246
- variant: "default"
283
+ variant: "default",
247
284
  });
248
285
  } else {
249
286
  blocks.push({
250
287
  type: "banner",
251
288
  title: "Email send failed",
252
289
  description: sendResult.error,
253
- variant: "error"
290
+ variant: "error",
254
291
  });
255
292
  }
256
293
  }
294
+
257
295
  blocks.push({
258
296
  type: "form",
259
297
  block_id: "test_form",
@@ -263,51 +301,57 @@ async function renderTestPage(ctx, validation, sendResult) {
263
301
  action_id: "recipient",
264
302
  label: "Recipient Email",
265
303
  placeholder: "you@example.com",
266
- initial_value: sendResult?.ok ? "" : void 0
267
- }
304
+ initial_value: sendResult?.ok ? "" : undefined,
305
+ },
268
306
  ],
269
- submit: { label: "Send Test", action_id: "send_test" }
307
+ submit: { label: "Send Test", action_id: "send_test" },
270
308
  });
309
+
271
310
  return { blocks };
272
311
  }
273
- var sandbox_entry_default = definePlugin({
274
- id: "emdash-aws-ses",
275
- version: "1.0.0",
276
- capabilities: ["hooks.email-transport:register"],
312
+
313
+ export default {
277
314
  hooks: {
278
- "plugin:install": async (_event, ctx) => {
315
+ "plugin:install": async (_event: LifecycleEvent, ctx: PluginContext) => {
279
316
  await ctx.kv.set("settings:smtpHost", "email-smtp.us-east-1.amazonaws.com");
280
317
  await ctx.kv.set("settings:smtpPort", 587);
281
318
  await ctx.kv.set("settings:region", "us-east-1");
282
319
  ctx.log.info("AWS SES plugin defaults seeded");
283
320
  },
321
+
284
322
  "email:deliver": {
285
323
  exclusive: true,
286
- handler: async ({ message, source }, ctx) => {
324
+ handler: async ({ message, source }: EmailDeliverEvent, ctx: PluginContext) => {
287
325
  const settings = await getSettings(ctx);
288
326
  const transporter = createTransporter(settings);
327
+
289
328
  await transporter.sendMail({
290
329
  from: settings.fromName ? `"${settings.fromName}" <${settings.defaultFrom}>` : settings.defaultFrom,
291
330
  to: message.to,
292
331
  subject: message.subject,
293
332
  text: message.text,
294
333
  html: message.html,
295
- replyTo: settings.replyTo || void 0
334
+ replyTo: settings.replyTo || undefined,
296
335
  });
336
+
297
337
  ctx.log.info(`Email sent via AWS SES to ${message.to}`, { source });
298
- }
299
- }
338
+ },
339
+ },
300
340
  },
341
+
301
342
  routes: {
302
343
  admin: {
303
- handler: async (routeCtx, ctx) => {
304
- const interaction = routeCtx.input;
344
+ handler: async (routeCtx: SandboxedRouteContext, ctx: PluginContext) => {
345
+ const interaction = routeCtx.input as BlockInteraction;
346
+
305
347
  if (interaction.type === "page_load" && (!interaction.page || interaction.page === "/")) {
306
348
  return renderSettingsPage(ctx);
307
349
  }
350
+
308
351
  if (interaction.type === "page_load" && interaction.page === "/settings") {
309
352
  return renderSettingsPage(ctx);
310
353
  }
354
+
311
355
  if (interaction.type === "form_submit" && interaction.action_id === "save_settings") {
312
356
  await saveSettings(ctx, interaction.values ?? {});
313
357
  const settingsPage = await renderSettingsPage(ctx);
@@ -319,42 +363,47 @@ var sandbox_entry_default = definePlugin({
319
363
  type: "banner",
320
364
  title: "Settings saved",
321
365
  description: "Your AWS SES configuration has been updated successfully.",
322
- variant: "default"
366
+ variant: "default",
323
367
  },
324
- ...settingsBlocks.slice(1)
368
+ ...settingsBlocks.slice(1),
325
369
  ],
326
- toast: { message: "Settings saved successfully", type: "success" }
370
+ toast: { message: "Settings saved successfully", type: "success" },
327
371
  };
328
372
  }
373
+
329
374
  if (interaction.type === "page_load" && interaction.page === "/test") {
330
375
  return renderTestPage(ctx);
331
376
  }
377
+
332
378
  if (interaction.type === "block_action" && interaction.action_id === "validate") {
333
379
  const result = await validateSMTP(ctx);
334
380
  return {
335
- ...await renderTestPage(ctx, result),
336
- toast: result.ok ? { message: "Connection verified successfully", type: "success" } : { message: result.error ?? "Validation failed", type: "error" }
381
+ ...(await renderTestPage(ctx, result)),
382
+ toast: result.ok
383
+ ? { message: "Connection verified successfully", type: "success" }
384
+ : { message: result.error ?? "Validation failed", type: "error" },
337
385
  };
338
386
  }
387
+
339
388
  if (interaction.type === "form_submit" && interaction.action_id === "send_test") {
340
- const recipient = interaction.values?.recipient?.trim();
389
+ const recipient = (interaction.values?.recipient as string)?.trim();
341
390
  if (!recipient) {
342
391
  return {
343
- ...await renderTestPage(ctx),
344
- toast: { message: "Please enter a recipient email", type: "error" }
392
+ ...(await renderTestPage(ctx)),
393
+ toast: { message: "Please enter a recipient email", type: "error" },
345
394
  };
346
395
  }
347
396
  const result = await sendTestEmail(ctx, recipient);
348
397
  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" }
398
+ ...(await renderTestPage(ctx, null, { ...result, recipient })),
399
+ toast: result.ok
400
+ ? { message: `Test email sent to ${recipient}`, type: "success" }
401
+ : { message: result.error ?? "Send failed", type: "error" },
351
402
  };
352
403
  }
404
+
353
405
  return { blocks: [] };
354
- }
355
- }
356
- }
357
- });
358
- export {
359
- sandbox_entry_default as default
360
- };
406
+ },
407
+ },
408
+ },
409
+ } satisfies SandboxedPlugin;
package/tsconfig.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "strict": true,
7
+ "noImplicitAny": false, // <--- Añade esto para silenciar los 'any' implícitos
8
+ "esModuleInterop": true,
9
+ "skipLibCheck": true,
10
+ "declaration": true,
11
+ "outDir": "./dist"
12
+ },
13
+ "include": ["src"]
14
+ }
package/dist/index.cjs DELETED
@@ -1,43 +0,0 @@
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
- });
@@ -1,391 +0,0 @@
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
- });