emdash-aws-ses 1.0.2 → 1.0.4
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 +159 -0
- package/dist/index.cjs +44 -0
- package/dist/index.d.ts +2 -0
- package/{src/index.ts → dist/index.js} +7 -5
- package/dist/sandbox-entry.cjs +390 -0
- package/dist/sandbox-entry.d.ts +127 -0
- package/{src/sandbox-entry.ts → dist/sandbox-entry.js} +75 -125
- package/package.json +13 -2
- package/.gitattributes +0 -2
- package/tsconfig.json +0 -14
package/README.md
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# emdash-aws-ses
|
|
2
|
+
|
|
3
|
+
A standalone, lightweight SMTP email transport plugin for **EmDash CMS** designed to route all system emails securely through **Amazon SES (Simple Email Service)**.
|
|
4
|
+
|
|
5
|
+
By registering exclusively with EmDash’s mail delivery hooks, this plugin routes all native transactional emails (including passwordless magic links, collaborator invitations, and system notifications) through your AWS infrastructure.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
* **Exclusive Email Routing:** Intercepts the global EmDash `email:deliver` hook with `exclusive: true` to route all outgoing mail using secure SMTP transport powered by `nodemailer`.
|
|
12
|
+
* **Interactive Block Kit UI:** Provides a built-in, responsive configuration and test dashboard directly inside the EmDash administrative panel.
|
|
13
|
+
* **Secure Database Persistence:** Saves your SMTP credentials securely inside your active database (SQLite or PostgreSQL) via the native EmDash key-value (`ctx.kv`) store.
|
|
14
|
+
* **Built-in Diagnostics:** Validates SMTP server handshakes and sends HTML/plaintext test emails to any recipient directly from the visual test tab.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Prerequisites
|
|
19
|
+
|
|
20
|
+
Before installing the plugin, ensure you have configured your AWS environment:
|
|
21
|
+
1. **Verified Identity:** Confirm that your sender domain (e.g., `your-domain.com`) or specific email address (e.g., `info@your-domain.com`) is verified in your **Amazon SES** console. Unverified identities will be rejected by AWS.
|
|
22
|
+
2. **SMTP Credentials:** Go to the *SMTP Settings* tab in your Amazon SES console and click **Create SMTP Credentials** to generate a dedicated IAM SMTP Username and SMTP Password.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
Install the package into your main Astro + EmDash website directory:
|
|
29
|
+
|
|
30
|
+
### From the public npm registry (Recommended):
|
|
31
|
+
```bash
|
|
32
|
+
npm install emdash-aws-ses
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### From a local development directory:
|
|
36
|
+
```bash
|
|
37
|
+
npm install ../path/to/emdash-aws-ses
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Integration in Astro
|
|
43
|
+
|
|
44
|
+
To enable the plugin, import and register `awsSesPlugin()` inside the `plugins` array of the EmDash integration in your `astro.config.mjs` file:
|
|
45
|
+
|
|
46
|
+
```javascript
|
|
47
|
+
// astro.config.mjs
|
|
48
|
+
import "dotenv/config";
|
|
49
|
+
|
|
50
|
+
import node from "@astrojs/node";
|
|
51
|
+
import react from "@astrojs/react";
|
|
52
|
+
import { defineConfig } from "astro/config";
|
|
53
|
+
import emdash, { local } from "emdash/astro";
|
|
54
|
+
import { sqlite } from "emdash/db";
|
|
55
|
+
|
|
56
|
+
// 1. Import the plugin
|
|
57
|
+
import { awsSesPlugin } from "emdash-aws-ses";
|
|
58
|
+
|
|
59
|
+
export default defineConfig({
|
|
60
|
+
output: "server",
|
|
61
|
+
adapter: node({
|
|
62
|
+
mode: "standalone",
|
|
63
|
+
}),
|
|
64
|
+
image: {
|
|
65
|
+
layout: "constrained",
|
|
66
|
+
responsiveStyles: true,
|
|
67
|
+
},
|
|
68
|
+
integrations: [
|
|
69
|
+
react(),
|
|
70
|
+
emdash({
|
|
71
|
+
database: sqlite({
|
|
72
|
+
// Dynamic SQLite database resolution for Coolify
|
|
73
|
+
url: process.env.NODE_ENV === "production"
|
|
74
|
+
? "file:/app/data/data.db"
|
|
75
|
+
: "file:./data.db",
|
|
76
|
+
}),
|
|
77
|
+
storage: local({
|
|
78
|
+
// Dynamic persistent directory mapping for Coolify
|
|
79
|
+
directory: process.env.NODE_ENV === "production"
|
|
80
|
+
? "/app/data/uploads"
|
|
81
|
+
: "./uploads",
|
|
82
|
+
baseUrl: "/_emdash/api/media/file",
|
|
83
|
+
}),
|
|
84
|
+
// 2. Register your plugin here
|
|
85
|
+
plugins: [
|
|
86
|
+
awsSesPlugin(),
|
|
87
|
+
],
|
|
88
|
+
}),
|
|
89
|
+
],
|
|
90
|
+
devToolbar: { enabled: false },
|
|
91
|
+
});
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Configuration & Usage
|
|
97
|
+
|
|
98
|
+
Once your application is running, navigate to your CMS admin panel at `https://your-domain.com/_emdash/admin`.
|
|
99
|
+
|
|
100
|
+
### 1. SMTP Settings Panel
|
|
101
|
+
Go to **Settings** -> **Plugins** -> **AWS SES Settings** and fill out the interactive configuration form:
|
|
102
|
+
|
|
103
|
+
| Field | Description | Example |
|
|
104
|
+
| :--- | :--- | :--- |
|
|
105
|
+
| **SMTP Host** | The SMTP endpoint host provided by Amazon SES. | `email-smtp.us-east-1.amazonaws.com` |
|
|
106
|
+
| **SMTP Port** | The secure SMTP port (STARTTLS or SSL/TLS). | `587` (Recommended) or `465` |
|
|
107
|
+
| **SMTP Username** | Your dedicated IAM SMTP Username generated in AWS. | `AKIA...` |
|
|
108
|
+
| **SMTP Password** | Your dedicated IAM SMTP Password generated in AWS. | `xxxXXXxxx` |
|
|
109
|
+
| **AWS Region** | The geographic AWS region where your SES is hosted. | `us-east-1` |
|
|
110
|
+
| **Default From Address** | A verified and authorized sender email address in AWS. | `noreply@your-domain.com` |
|
|
111
|
+
| **From Name** | The display name recipients will see. | `Your Company` |
|
|
112
|
+
| **Reply-To Address** | An optional reply-to address for user responses. | `support@your-domain.com` |
|
|
113
|
+
|
|
114
|
+
Click **Save Settings** to write your changes securely to the database.
|
|
115
|
+
|
|
116
|
+
### 2. Testing the Connection
|
|
117
|
+
Navigate to the **Test & Status** tab inside the plugin's dashboard to diagnose the pipeline:
|
|
118
|
+
* Click **Test Connection** to trigger an on-demand SMTP handshake validation.
|
|
119
|
+
* Type an email address in the **Recipient Email** field and click **Send Test** to send a test HTML and plaintext message.
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## Under the Hood (Developer Reference)
|
|
124
|
+
|
|
125
|
+
The plugin implements the standard `SandboxedPlugin` interface exposed by EmDash.
|
|
126
|
+
|
|
127
|
+
### `plugin:install` Lifecycle Hook
|
|
128
|
+
Fires automatically when the plugin is registered in the CMS. It seeds default SMTP values (`email-smtp.us-east-1.amazonaws.com` on port `587`) securely inside `ctx.kv` to ensure a smooth first-time startup.
|
|
129
|
+
|
|
130
|
+
### `email:deliver` Event Handler
|
|
131
|
+
Registers with `exclusive: true`. This setting commands EmDash to bypass any default local SMTP transporters or falling-back configurations, sending the `EmailDeliverEvent` payload directly to the custom SES transport handler.
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## Local Development
|
|
136
|
+
|
|
137
|
+
If you want to contribute to this plugin, modify the source code, or test it locally, you must compile the TypeScript files into executable JavaScript:
|
|
138
|
+
|
|
139
|
+
1. Clone the repository and navigate to the directory:
|
|
140
|
+
```bash
|
|
141
|
+
git clone https://github.com/ab6162/emdash-aws-ses.git
|
|
142
|
+
cd emdash-aws-ses
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
2. Install the local development dependencies:
|
|
146
|
+
```bash
|
|
147
|
+
npm install
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
3. Build the distribution files:
|
|
151
|
+
```bash
|
|
152
|
+
npm run build
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
This will generate the `/dist` directory containing the ESM and CommonJS compiled outputs.
|
|
156
|
+
|
|
157
|
+
## License
|
|
158
|
+
|
|
159
|
+
This project is licensed under the MIT License. See the `LICENSE` file for more details.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
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
|
+
allowedHosts: [],
|
|
35
|
+
adminPages: [
|
|
36
|
+
{ path: "/settings", label: "AWS SES Settings", icon: "mail" },
|
|
37
|
+
{ path: "/test", label: "AWS SES Test & Status", icon: "send" }
|
|
38
|
+
]
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
42
|
+
0 && (module.exports = {
|
|
43
|
+
awsSesPlugin
|
|
44
|
+
});
|
package/dist/index.d.ts
ADDED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
export function awsSesPlugin(): PluginDescriptor {
|
|
1
|
+
// src/index.ts
|
|
2
|
+
function awsSesPlugin() {
|
|
4
3
|
return {
|
|
5
4
|
id: "emdash-aws-ses",
|
|
6
5
|
version: "1.0.0",
|
|
@@ -11,7 +10,10 @@ export function awsSesPlugin(): PluginDescriptor {
|
|
|
11
10
|
allowedHosts: [],
|
|
12
11
|
adminPages: [
|
|
13
12
|
{ path: "/settings", label: "AWS SES Settings", icon: "mail" },
|
|
14
|
-
{ path: "/test", label: "AWS SES Test & Status", icon: "send" }
|
|
15
|
-
]
|
|
13
|
+
{ path: "/test", label: "AWS SES Test & Status", icon: "send" }
|
|
14
|
+
]
|
|
16
15
|
};
|
|
17
16
|
}
|
|
17
|
+
export {
|
|
18
|
+
awsSesPlugin
|
|
19
|
+
};
|
|
@@ -0,0 +1,390 @@
|
|
|
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_nodemailer = __toESM(require("nodemailer"), 1);
|
|
37
|
+
async function getSettings(ctx) {
|
|
38
|
+
const defaults = {
|
|
39
|
+
smtpHost: "email-smtp.us-east-1.amazonaws.com",
|
|
40
|
+
smtpPort: 587,
|
|
41
|
+
region: "us-east-1",
|
|
42
|
+
defaultFrom: "",
|
|
43
|
+
fromName: "",
|
|
44
|
+
replyTo: "",
|
|
45
|
+
smtpUser: "",
|
|
46
|
+
smtpPassword: ""
|
|
47
|
+
};
|
|
48
|
+
const [smtpHost, smtpPortRaw, smtpUser, smtpPassword, region, defaultFrom, fromName, replyTo] = await Promise.all([
|
|
49
|
+
ctx.kv.get("settings:smtpHost"),
|
|
50
|
+
ctx.kv.get("settings:smtpPort"),
|
|
51
|
+
ctx.kv.get("settings:smtpUser"),
|
|
52
|
+
ctx.kv.get("settings:smtpPassword"),
|
|
53
|
+
ctx.kv.get("settings:region"),
|
|
54
|
+
ctx.kv.get("settings:defaultFrom"),
|
|
55
|
+
ctx.kv.get("settings:fromName"),
|
|
56
|
+
ctx.kv.get("settings:replyTo")
|
|
57
|
+
]);
|
|
58
|
+
return {
|
|
59
|
+
smtpHost: smtpHost ?? defaults.smtpHost,
|
|
60
|
+
smtpPort: Number(smtpPortRaw ?? defaults.smtpPort),
|
|
61
|
+
smtpUser: smtpUser ?? defaults.smtpUser,
|
|
62
|
+
smtpPassword: smtpPassword ?? defaults.smtpPassword,
|
|
63
|
+
region: region ?? defaults.region,
|
|
64
|
+
defaultFrom: defaultFrom ?? defaults.defaultFrom,
|
|
65
|
+
fromName: fromName ?? defaults.fromName,
|
|
66
|
+
replyTo: replyTo ?? defaults.replyTo
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function createTransporter(settings) {
|
|
70
|
+
const secure = settings.smtpPort === 465;
|
|
71
|
+
const requireTLS = settings.smtpPort === 587 || settings.smtpPort === 25 || settings.smtpPort === 2587;
|
|
72
|
+
return import_nodemailer.default.createTransport({
|
|
73
|
+
host: settings.smtpHost,
|
|
74
|
+
port: settings.smtpPort,
|
|
75
|
+
secure,
|
|
76
|
+
auth: {
|
|
77
|
+
user: settings.smtpUser,
|
|
78
|
+
pass: settings.smtpPassword
|
|
79
|
+
},
|
|
80
|
+
tls: {
|
|
81
|
+
rejectUnauthorized: true
|
|
82
|
+
},
|
|
83
|
+
requireTLS,
|
|
84
|
+
connectionTimeout: 1e4,
|
|
85
|
+
greetingTimeout: 1e4,
|
|
86
|
+
socketTimeout: 1e4
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
async function validateSMTP(ctx) {
|
|
90
|
+
try {
|
|
91
|
+
const settings = await getSettings(ctx);
|
|
92
|
+
if (!settings.smtpHost || !settings.smtpUser || !settings.smtpPassword) {
|
|
93
|
+
return { ok: false, error: "SMTP settings are incomplete. Please configure host, user, and password." };
|
|
94
|
+
}
|
|
95
|
+
const transporter = createTransporter(settings);
|
|
96
|
+
await transporter.verify();
|
|
97
|
+
return { ok: true };
|
|
98
|
+
} catch (err) {
|
|
99
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
100
|
+
return { ok: false, error: `Connection failed: ${message}` };
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
async function sendTestEmail(ctx, recipient) {
|
|
104
|
+
try {
|
|
105
|
+
const settings = await getSettings(ctx);
|
|
106
|
+
if (!settings.defaultFrom) {
|
|
107
|
+
return { ok: false, error: "Default From address is not configured." };
|
|
108
|
+
}
|
|
109
|
+
const transporter = createTransporter(settings);
|
|
110
|
+
await transporter.sendMail({
|
|
111
|
+
from: settings.fromName ? `"${settings.fromName}" <${settings.defaultFrom}>` : settings.defaultFrom,
|
|
112
|
+
to: recipient,
|
|
113
|
+
subject: "Test email from EmDash + AWS SES",
|
|
114
|
+
text: "This is a test email sent via Amazon SES from your EmDash CMS.",
|
|
115
|
+
html: "<p>This is a test email sent via <strong>Amazon SES</strong> from your EmDash CMS.</p>",
|
|
116
|
+
replyTo: settings.replyTo || void 0
|
|
117
|
+
});
|
|
118
|
+
return { ok: true };
|
|
119
|
+
} catch (err) {
|
|
120
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
121
|
+
return { ok: false, error: `Send failed: ${message}` };
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
async function renderSettingsPage(ctx) {
|
|
125
|
+
const settings = await getSettings(ctx);
|
|
126
|
+
return {
|
|
127
|
+
blocks: [
|
|
128
|
+
{ type: "header", text: "AWS SES Settings" },
|
|
129
|
+
{ type: "section", text: "Configure your Amazon SES SMTP transport. All settings are stored in the plugin KV store." },
|
|
130
|
+
{ type: "divider" },
|
|
131
|
+
{
|
|
132
|
+
type: "form",
|
|
133
|
+
block_id: "settings",
|
|
134
|
+
fields: [
|
|
135
|
+
{
|
|
136
|
+
type: "text_input",
|
|
137
|
+
action_id: "smtpHost",
|
|
138
|
+
label: "SMTP Host",
|
|
139
|
+
initial_value: settings.smtpHost,
|
|
140
|
+
placeholder: "email-smtp.us-east-1.amazonaws.com"
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
type: "number_input",
|
|
144
|
+
action_id: "smtpPort",
|
|
145
|
+
label: "SMTP Port",
|
|
146
|
+
initial_value: settings.smtpPort,
|
|
147
|
+
min: 1,
|
|
148
|
+
max: 65535
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
type: "text_input",
|
|
152
|
+
action_id: "smtpUser",
|
|
153
|
+
label: "SMTP Username",
|
|
154
|
+
initial_value: settings.smtpUser,
|
|
155
|
+
placeholder: "AWS SES SMTP username (IAM Access Key ID)"
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
type: "secret_input",
|
|
159
|
+
action_id: "smtpPassword",
|
|
160
|
+
label: "SMTP Password",
|
|
161
|
+
initial_value: ""
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
type: "text_input",
|
|
165
|
+
action_id: "region",
|
|
166
|
+
label: "AWS Region",
|
|
167
|
+
initial_value: settings.region,
|
|
168
|
+
placeholder: "us-east-1"
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
type: "text_input",
|
|
172
|
+
action_id: "defaultFrom",
|
|
173
|
+
label: "Default From Address",
|
|
174
|
+
initial_value: settings.defaultFrom,
|
|
175
|
+
placeholder: "noreply@example.com"
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
type: "text_input",
|
|
179
|
+
action_id: "fromName",
|
|
180
|
+
label: "From Name",
|
|
181
|
+
initial_value: settings.fromName,
|
|
182
|
+
placeholder: "My Site"
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
type: "text_input",
|
|
186
|
+
action_id: "replyTo",
|
|
187
|
+
label: "Reply-To Address",
|
|
188
|
+
initial_value: settings.replyTo,
|
|
189
|
+
placeholder: "support@example.com"
|
|
190
|
+
}
|
|
191
|
+
],
|
|
192
|
+
submit: { label: "Save Settings", action_id: "save_settings" }
|
|
193
|
+
},
|
|
194
|
+
{ type: "divider" },
|
|
195
|
+
{ type: "header", text: "Email Provider Activation" },
|
|
196
|
+
{ 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." }
|
|
197
|
+
]
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
function getDefaultEndpoint(region) {
|
|
201
|
+
return `email-smtp.${region}.amazonaws.com`;
|
|
202
|
+
}
|
|
203
|
+
async function saveSettings(ctx, values) {
|
|
204
|
+
const currentSettings = await getSettings(ctx);
|
|
205
|
+
if (typeof values.region === "string" && values.region.length > 0 && values.region !== currentSettings.region) {
|
|
206
|
+
const oldEndpoint = getDefaultEndpoint(currentSettings.region);
|
|
207
|
+
if (!currentSettings.smtpHost || currentSettings.smtpHost === oldEndpoint) {
|
|
208
|
+
values.smtpHost = getDefaultEndpoint(values.region);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (typeof values.smtpHost === "string" && values.smtpHost.length > 0) {
|
|
212
|
+
await ctx.kv.set("settings:smtpHost", values.smtpHost);
|
|
213
|
+
}
|
|
214
|
+
if (typeof values.smtpPort === "number" && values.smtpPort >= 1 && values.smtpPort <= 65535) {
|
|
215
|
+
await ctx.kv.set("settings:smtpPort", values.smtpPort);
|
|
216
|
+
}
|
|
217
|
+
if (typeof values.smtpUser === "string" && values.smtpUser.length > 0) {
|
|
218
|
+
await ctx.kv.set("settings:smtpUser", values.smtpUser);
|
|
219
|
+
}
|
|
220
|
+
if (typeof values.smtpPassword === "string" && values.smtpPassword.length > 0) {
|
|
221
|
+
await ctx.kv.set("settings:smtpPassword", values.smtpPassword);
|
|
222
|
+
}
|
|
223
|
+
if (typeof values.region === "string" && values.region.length > 0) {
|
|
224
|
+
await ctx.kv.set("settings:region", values.region);
|
|
225
|
+
}
|
|
226
|
+
if (typeof values.defaultFrom === "string") {
|
|
227
|
+
await ctx.kv.set("settings:defaultFrom", values.defaultFrom);
|
|
228
|
+
}
|
|
229
|
+
if (typeof values.fromName === "string") {
|
|
230
|
+
await ctx.kv.set("settings:fromName", values.fromName);
|
|
231
|
+
}
|
|
232
|
+
if (typeof values.replyTo === "string") {
|
|
233
|
+
await ctx.kv.set("settings:replyTo", values.replyTo);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
async function renderTestPage(ctx, validation, sendResult) {
|
|
237
|
+
const settings = await getSettings(ctx);
|
|
238
|
+
const blocks = [
|
|
239
|
+
{ type: "header", text: "AWS SES Test & Status" },
|
|
240
|
+
{ type: "section", text: `Current endpoint: ${settings.smtpHost}:${settings.smtpPort} (${settings.region})` },
|
|
241
|
+
{ type: "divider" }
|
|
242
|
+
];
|
|
243
|
+
if (validation) {
|
|
244
|
+
if (validation.ok) {
|
|
245
|
+
blocks.push({
|
|
246
|
+
type: "banner",
|
|
247
|
+
title: "Connection verified",
|
|
248
|
+
description: "SMTP credentials are valid.",
|
|
249
|
+
variant: "default"
|
|
250
|
+
});
|
|
251
|
+
} else {
|
|
252
|
+
blocks.push({
|
|
253
|
+
type: "banner",
|
|
254
|
+
title: "Connection failed",
|
|
255
|
+
description: validation.error,
|
|
256
|
+
variant: "error"
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
blocks.push(
|
|
261
|
+
{ type: "section", text: "Test your SMTP connection and verify your credentials are correct." },
|
|
262
|
+
{
|
|
263
|
+
type: "actions",
|
|
264
|
+
elements: [
|
|
265
|
+
{
|
|
266
|
+
type: "button",
|
|
267
|
+
label: "Test Connection",
|
|
268
|
+
action_id: "validate"
|
|
269
|
+
}
|
|
270
|
+
]
|
|
271
|
+
},
|
|
272
|
+
{ type: "divider" },
|
|
273
|
+
{ type: "header", text: "Send Test Email" },
|
|
274
|
+
{ type: "section", text: "Send a test email to verify the full transport pipeline. Enter a recipient address below." }
|
|
275
|
+
);
|
|
276
|
+
if (sendResult) {
|
|
277
|
+
if (sendResult.ok) {
|
|
278
|
+
blocks.push({
|
|
279
|
+
type: "banner",
|
|
280
|
+
title: "Email sent",
|
|
281
|
+
description: `Test email sent successfully to ${sendResult.recipient}.`,
|
|
282
|
+
variant: "default"
|
|
283
|
+
});
|
|
284
|
+
} else {
|
|
285
|
+
blocks.push({
|
|
286
|
+
type: "banner",
|
|
287
|
+
title: "Email send failed",
|
|
288
|
+
description: sendResult.error,
|
|
289
|
+
variant: "error"
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
blocks.push({
|
|
294
|
+
type: "form",
|
|
295
|
+
block_id: "test_form",
|
|
296
|
+
fields: [
|
|
297
|
+
{
|
|
298
|
+
type: "text_input",
|
|
299
|
+
action_id: "recipient",
|
|
300
|
+
label: "Recipient Email",
|
|
301
|
+
placeholder: "you@example.com",
|
|
302
|
+
initial_value: sendResult?.ok ? "" : void 0
|
|
303
|
+
}
|
|
304
|
+
],
|
|
305
|
+
submit: { label: "Send Test", action_id: "send_test" }
|
|
306
|
+
});
|
|
307
|
+
return { blocks };
|
|
308
|
+
}
|
|
309
|
+
var sandbox_entry_default = {
|
|
310
|
+
hooks: {
|
|
311
|
+
"plugin:install": async (_event, ctx) => {
|
|
312
|
+
await ctx.kv.set("settings:smtpHost", "email-smtp.us-east-1.amazonaws.com");
|
|
313
|
+
await ctx.kv.set("settings:smtpPort", 587);
|
|
314
|
+
await ctx.kv.set("settings:region", "us-east-1");
|
|
315
|
+
ctx.log.info("AWS SES plugin defaults seeded");
|
|
316
|
+
},
|
|
317
|
+
"email:deliver": {
|
|
318
|
+
exclusive: true,
|
|
319
|
+
handler: async ({ message, source }, ctx) => {
|
|
320
|
+
const settings = await getSettings(ctx);
|
|
321
|
+
const transporter = createTransporter(settings);
|
|
322
|
+
await transporter.sendMail({
|
|
323
|
+
from: settings.fromName ? `"${settings.fromName}" <${settings.defaultFrom}>` : settings.defaultFrom,
|
|
324
|
+
to: message.to,
|
|
325
|
+
subject: message.subject,
|
|
326
|
+
text: message.text,
|
|
327
|
+
html: message.html,
|
|
328
|
+
replyTo: settings.replyTo || void 0
|
|
329
|
+
});
|
|
330
|
+
ctx.log.info(`Email sent via AWS SES to ${message.to}`, { source });
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
},
|
|
334
|
+
routes: {
|
|
335
|
+
admin: {
|
|
336
|
+
handler: async (routeCtx, ctx) => {
|
|
337
|
+
const interaction = routeCtx.input;
|
|
338
|
+
if (interaction.type === "page_load" && (!interaction.page || interaction.page === "/")) {
|
|
339
|
+
return renderSettingsPage(ctx);
|
|
340
|
+
}
|
|
341
|
+
if (interaction.type === "page_load" && interaction.page === "/settings") {
|
|
342
|
+
return renderSettingsPage(ctx);
|
|
343
|
+
}
|
|
344
|
+
if (interaction.type === "form_submit" && interaction.action_id === "save_settings") {
|
|
345
|
+
await saveSettings(ctx, interaction.values ?? {});
|
|
346
|
+
const settingsPage = await renderSettingsPage(ctx);
|
|
347
|
+
const settingsBlocks = settingsPage.blocks;
|
|
348
|
+
return {
|
|
349
|
+
blocks: [
|
|
350
|
+
settingsBlocks[0],
|
|
351
|
+
{
|
|
352
|
+
type: "banner",
|
|
353
|
+
title: "Settings saved",
|
|
354
|
+
description: "Your AWS SES configuration has been updated successfully.",
|
|
355
|
+
variant: "default"
|
|
356
|
+
},
|
|
357
|
+
...settingsBlocks.slice(1)
|
|
358
|
+
],
|
|
359
|
+
toast: { message: "Settings saved successfully", type: "success" }
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
if (interaction.type === "page_load" && interaction.page === "/test") {
|
|
363
|
+
return renderTestPage(ctx);
|
|
364
|
+
}
|
|
365
|
+
if (interaction.type === "block_action" && interaction.action_id === "validate") {
|
|
366
|
+
const result = await validateSMTP(ctx);
|
|
367
|
+
return {
|
|
368
|
+
...await renderTestPage(ctx, result),
|
|
369
|
+
toast: result.ok ? { message: "Connection verified successfully", type: "success" } : { message: result.error ?? "Validation failed", type: "error" }
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
if (interaction.type === "form_submit" && interaction.action_id === "send_test") {
|
|
373
|
+
const recipient = interaction.values?.recipient?.trim();
|
|
374
|
+
if (!recipient) {
|
|
375
|
+
return {
|
|
376
|
+
...await renderTestPage(ctx),
|
|
377
|
+
toast: { message: "Please enter a recipient email", type: "error" }
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
const result = await sendTestEmail(ctx, recipient);
|
|
381
|
+
return {
|
|
382
|
+
...await renderTestPage(ctx, null, { ...result, recipient }),
|
|
383
|
+
toast: result.ok ? { message: `Test email sent to ${recipient}`, type: "success" } : { message: result.error ?? "Send failed", type: "error" }
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
return { blocks: [] };
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
};
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import type { SandboxedRouteContext, PluginContext, EmailDeliverEvent, LifecycleEvent } from "emdash/plugin";
|
|
2
|
+
declare const _default: {
|
|
3
|
+
hooks: {
|
|
4
|
+
"plugin:install": (_event: LifecycleEvent, ctx: PluginContext) => Promise<void>;
|
|
5
|
+
"email:deliver": {
|
|
6
|
+
exclusive: true;
|
|
7
|
+
handler: ({ message, source }: EmailDeliverEvent, ctx: PluginContext) => Promise<void>;
|
|
8
|
+
};
|
|
9
|
+
};
|
|
10
|
+
routes: {
|
|
11
|
+
admin: {
|
|
12
|
+
handler: (routeCtx: SandboxedRouteContext, ctx: PluginContext) => Promise<{
|
|
13
|
+
blocks: ({
|
|
14
|
+
type: string;
|
|
15
|
+
text: string;
|
|
16
|
+
block_id?: undefined;
|
|
17
|
+
fields?: undefined;
|
|
18
|
+
submit?: undefined;
|
|
19
|
+
} | {
|
|
20
|
+
text?: undefined;
|
|
21
|
+
type: string;
|
|
22
|
+
block_id?: undefined;
|
|
23
|
+
fields?: undefined;
|
|
24
|
+
submit?: undefined;
|
|
25
|
+
} | {
|
|
26
|
+
text?: undefined;
|
|
27
|
+
type: string;
|
|
28
|
+
block_id: string;
|
|
29
|
+
fields: ({
|
|
30
|
+
type: string;
|
|
31
|
+
action_id: string;
|
|
32
|
+
label: string;
|
|
33
|
+
initial_value: string;
|
|
34
|
+
placeholder: string;
|
|
35
|
+
min?: undefined;
|
|
36
|
+
max?: undefined;
|
|
37
|
+
} | {
|
|
38
|
+
placeholder?: undefined;
|
|
39
|
+
type: string;
|
|
40
|
+
action_id: string;
|
|
41
|
+
label: string;
|
|
42
|
+
initial_value: number;
|
|
43
|
+
min: number;
|
|
44
|
+
max: number;
|
|
45
|
+
} | {
|
|
46
|
+
placeholder?: undefined;
|
|
47
|
+
min?: undefined;
|
|
48
|
+
max?: undefined;
|
|
49
|
+
type: string;
|
|
50
|
+
action_id: string;
|
|
51
|
+
label: string;
|
|
52
|
+
initial_value: string;
|
|
53
|
+
})[];
|
|
54
|
+
submit: {
|
|
55
|
+
label: string;
|
|
56
|
+
action_id: string;
|
|
57
|
+
};
|
|
58
|
+
})[];
|
|
59
|
+
} | {
|
|
60
|
+
blocks: Record<string, unknown>[];
|
|
61
|
+
} | {
|
|
62
|
+
blocks: ({
|
|
63
|
+
type: string;
|
|
64
|
+
text: string;
|
|
65
|
+
block_id?: undefined;
|
|
66
|
+
fields?: undefined;
|
|
67
|
+
submit?: undefined;
|
|
68
|
+
} | {
|
|
69
|
+
text?: undefined;
|
|
70
|
+
type: string;
|
|
71
|
+
block_id?: undefined;
|
|
72
|
+
fields?: undefined;
|
|
73
|
+
submit?: undefined;
|
|
74
|
+
} | {
|
|
75
|
+
text?: undefined;
|
|
76
|
+
type: string;
|
|
77
|
+
block_id: string;
|
|
78
|
+
fields: ({
|
|
79
|
+
type: string;
|
|
80
|
+
action_id: string;
|
|
81
|
+
label: string;
|
|
82
|
+
initial_value: string;
|
|
83
|
+
placeholder: string;
|
|
84
|
+
min?: undefined;
|
|
85
|
+
max?: undefined;
|
|
86
|
+
} | {
|
|
87
|
+
placeholder?: undefined;
|
|
88
|
+
type: string;
|
|
89
|
+
action_id: string;
|
|
90
|
+
label: string;
|
|
91
|
+
initial_value: number;
|
|
92
|
+
min: number;
|
|
93
|
+
max: number;
|
|
94
|
+
} | {
|
|
95
|
+
placeholder?: undefined;
|
|
96
|
+
min?: undefined;
|
|
97
|
+
max?: undefined;
|
|
98
|
+
type: string;
|
|
99
|
+
action_id: string;
|
|
100
|
+
label: string;
|
|
101
|
+
initial_value: string;
|
|
102
|
+
})[];
|
|
103
|
+
submit: {
|
|
104
|
+
label: string;
|
|
105
|
+
action_id: string;
|
|
106
|
+
};
|
|
107
|
+
} | {
|
|
108
|
+
type: string;
|
|
109
|
+
title: string;
|
|
110
|
+
description: string;
|
|
111
|
+
variant: string;
|
|
112
|
+
})[];
|
|
113
|
+
toast: {
|
|
114
|
+
message: string;
|
|
115
|
+
type: string;
|
|
116
|
+
};
|
|
117
|
+
} | {
|
|
118
|
+
blocks: Record<string, unknown>[];
|
|
119
|
+
toast: {
|
|
120
|
+
message: string;
|
|
121
|
+
type: string;
|
|
122
|
+
};
|
|
123
|
+
}>;
|
|
124
|
+
};
|
|
125
|
+
};
|
|
126
|
+
};
|
|
127
|
+
export default _default;
|
|
@@ -1,20 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
// src/sandbox-entry.ts
|
|
2
2
|
import nodemailer from "nodemailer";
|
|
3
|
-
|
|
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> {
|
|
3
|
+
async function getSettings(ctx) {
|
|
18
4
|
const defaults = {
|
|
19
5
|
smtpHost: "email-smtp.us-east-1.amazonaws.com",
|
|
20
6
|
smtpPort: 587,
|
|
@@ -23,20 +9,18 @@ async function getSettings(ctx: PluginContext): Promise<SesSettings> {
|
|
|
23
9
|
fromName: "",
|
|
24
10
|
replyTo: "",
|
|
25
11
|
smtpUser: "",
|
|
26
|
-
smtpPassword: ""
|
|
12
|
+
smtpPassword: ""
|
|
27
13
|
};
|
|
28
|
-
|
|
29
14
|
const [smtpHost, smtpPortRaw, smtpUser, smtpPassword, region, defaultFrom, fromName, replyTo] = await Promise.all([
|
|
30
|
-
ctx.kv.get
|
|
31
|
-
ctx.kv.get
|
|
32
|
-
ctx.kv.get
|
|
33
|
-
ctx.kv.get
|
|
34
|
-
ctx.kv.get
|
|
35
|
-
ctx.kv.get
|
|
36
|
-
ctx.kv.get
|
|
37
|
-
ctx.kv.get
|
|
15
|
+
ctx.kv.get("settings:smtpHost"),
|
|
16
|
+
ctx.kv.get("settings:smtpPort"),
|
|
17
|
+
ctx.kv.get("settings:smtpUser"),
|
|
18
|
+
ctx.kv.get("settings:smtpPassword"),
|
|
19
|
+
ctx.kv.get("settings:region"),
|
|
20
|
+
ctx.kv.get("settings:defaultFrom"),
|
|
21
|
+
ctx.kv.get("settings:fromName"),
|
|
22
|
+
ctx.kv.get("settings:replyTo")
|
|
38
23
|
]);
|
|
39
|
-
|
|
40
24
|
return {
|
|
41
25
|
smtpHost: smtpHost ?? defaults.smtpHost,
|
|
42
26
|
smtpPort: Number(smtpPortRaw ?? defaults.smtpPort),
|
|
@@ -45,33 +29,30 @@ async function getSettings(ctx: PluginContext): Promise<SesSettings> {
|
|
|
45
29
|
region: region ?? defaults.region,
|
|
46
30
|
defaultFrom: defaultFrom ?? defaults.defaultFrom,
|
|
47
31
|
fromName: fromName ?? defaults.fromName,
|
|
48
|
-
replyTo: replyTo ?? defaults.replyTo
|
|
32
|
+
replyTo: replyTo ?? defaults.replyTo
|
|
49
33
|
};
|
|
50
34
|
}
|
|
51
|
-
|
|
52
|
-
function createTransporter(settings: SesSettings): Transporter {
|
|
35
|
+
function createTransporter(settings) {
|
|
53
36
|
const secure = settings.smtpPort === 465;
|
|
54
37
|
const requireTLS = settings.smtpPort === 587 || settings.smtpPort === 25 || settings.smtpPort === 2587;
|
|
55
|
-
|
|
56
38
|
return nodemailer.createTransport({
|
|
57
39
|
host: settings.smtpHost,
|
|
58
40
|
port: settings.smtpPort,
|
|
59
41
|
secure,
|
|
60
42
|
auth: {
|
|
61
43
|
user: settings.smtpUser,
|
|
62
|
-
pass: settings.smtpPassword
|
|
44
|
+
pass: settings.smtpPassword
|
|
63
45
|
},
|
|
64
46
|
tls: {
|
|
65
|
-
rejectUnauthorized: true
|
|
47
|
+
rejectUnauthorized: true
|
|
66
48
|
},
|
|
67
49
|
requireTLS,
|
|
68
|
-
connectionTimeout:
|
|
69
|
-
greetingTimeout:
|
|
70
|
-
socketTimeout:
|
|
50
|
+
connectionTimeout: 1e4,
|
|
51
|
+
greetingTimeout: 1e4,
|
|
52
|
+
socketTimeout: 1e4
|
|
71
53
|
});
|
|
72
54
|
}
|
|
73
|
-
|
|
74
|
-
async function validateSMTP(ctx: PluginContext): Promise<{ ok: boolean; error?: string }> {
|
|
55
|
+
async function validateSMTP(ctx) {
|
|
75
56
|
try {
|
|
76
57
|
const settings = await getSettings(ctx);
|
|
77
58
|
if (!settings.smtpHost || !settings.smtpUser || !settings.smtpPassword) {
|
|
@@ -85,8 +66,7 @@ async function validateSMTP(ctx: PluginContext): Promise<{ ok: boolean; error?:
|
|
|
85
66
|
return { ok: false, error: `Connection failed: ${message}` };
|
|
86
67
|
}
|
|
87
68
|
}
|
|
88
|
-
|
|
89
|
-
async function sendTestEmail(ctx: PluginContext, recipient: string): Promise<{ ok: boolean; error?: string }> {
|
|
69
|
+
async function sendTestEmail(ctx, recipient) {
|
|
90
70
|
try {
|
|
91
71
|
const settings = await getSettings(ctx);
|
|
92
72
|
if (!settings.defaultFrom) {
|
|
@@ -99,7 +79,7 @@ async function sendTestEmail(ctx: PluginContext, recipient: string): Promise<{ o
|
|
|
99
79
|
subject: "Test email from EmDash + AWS SES",
|
|
100
80
|
text: "This is a test email sent via Amazon SES from your EmDash CMS.",
|
|
101
81
|
html: "<p>This is a test email sent via <strong>Amazon SES</strong> from your EmDash CMS.</p>",
|
|
102
|
-
replyTo: settings.replyTo ||
|
|
82
|
+
replyTo: settings.replyTo || void 0
|
|
103
83
|
});
|
|
104
84
|
return { ok: true };
|
|
105
85
|
} catch (err) {
|
|
@@ -107,10 +87,8 @@ async function sendTestEmail(ctx: PluginContext, recipient: string): Promise<{ o
|
|
|
107
87
|
return { ok: false, error: `Send failed: ${message}` };
|
|
108
88
|
}
|
|
109
89
|
}
|
|
110
|
-
|
|
111
|
-
async function renderSettingsPage(ctx: PluginContext) {
|
|
90
|
+
async function renderSettingsPage(ctx) {
|
|
112
91
|
const settings = await getSettings(ctx);
|
|
113
|
-
|
|
114
92
|
return {
|
|
115
93
|
blocks: [
|
|
116
94
|
{ type: "header", text: "AWS SES Settings" },
|
|
@@ -125,7 +103,7 @@ async function renderSettingsPage(ctx: PluginContext) {
|
|
|
125
103
|
action_id: "smtpHost",
|
|
126
104
|
label: "SMTP Host",
|
|
127
105
|
initial_value: settings.smtpHost,
|
|
128
|
-
placeholder: "email-smtp.us-east-1.amazonaws.com"
|
|
106
|
+
placeholder: "email-smtp.us-east-1.amazonaws.com"
|
|
129
107
|
},
|
|
130
108
|
{
|
|
131
109
|
type: "number_input",
|
|
@@ -133,73 +111,69 @@ async function renderSettingsPage(ctx: PluginContext) {
|
|
|
133
111
|
label: "SMTP Port",
|
|
134
112
|
initial_value: settings.smtpPort,
|
|
135
113
|
min: 1,
|
|
136
|
-
max: 65535
|
|
114
|
+
max: 65535
|
|
137
115
|
},
|
|
138
116
|
{
|
|
139
117
|
type: "text_input",
|
|
140
118
|
action_id: "smtpUser",
|
|
141
119
|
label: "SMTP Username",
|
|
142
120
|
initial_value: settings.smtpUser,
|
|
143
|
-
placeholder: "AWS SES SMTP username (IAM Access Key ID)"
|
|
121
|
+
placeholder: "AWS SES SMTP username (IAM Access Key ID)"
|
|
144
122
|
},
|
|
145
123
|
{
|
|
146
124
|
type: "secret_input",
|
|
147
125
|
action_id: "smtpPassword",
|
|
148
126
|
label: "SMTP Password",
|
|
149
|
-
initial_value: ""
|
|
127
|
+
initial_value: ""
|
|
150
128
|
},
|
|
151
129
|
{
|
|
152
130
|
type: "text_input",
|
|
153
131
|
action_id: "region",
|
|
154
132
|
label: "AWS Region",
|
|
155
133
|
initial_value: settings.region,
|
|
156
|
-
placeholder: "us-east-1"
|
|
134
|
+
placeholder: "us-east-1"
|
|
157
135
|
},
|
|
158
136
|
{
|
|
159
137
|
type: "text_input",
|
|
160
138
|
action_id: "defaultFrom",
|
|
161
139
|
label: "Default From Address",
|
|
162
140
|
initial_value: settings.defaultFrom,
|
|
163
|
-
placeholder: "noreply@example.com"
|
|
141
|
+
placeholder: "noreply@example.com"
|
|
164
142
|
},
|
|
165
143
|
{
|
|
166
144
|
type: "text_input",
|
|
167
145
|
action_id: "fromName",
|
|
168
146
|
label: "From Name",
|
|
169
147
|
initial_value: settings.fromName,
|
|
170
|
-
placeholder: "My Site"
|
|
148
|
+
placeholder: "My Site"
|
|
171
149
|
},
|
|
172
150
|
{
|
|
173
151
|
type: "text_input",
|
|
174
152
|
action_id: "replyTo",
|
|
175
153
|
label: "Reply-To Address",
|
|
176
154
|
initial_value: settings.replyTo,
|
|
177
|
-
placeholder: "support@example.com"
|
|
178
|
-
}
|
|
155
|
+
placeholder: "support@example.com"
|
|
156
|
+
}
|
|
179
157
|
],
|
|
180
|
-
submit: { label: "Save Settings", action_id: "save_settings" }
|
|
158
|
+
submit: { label: "Save Settings", action_id: "save_settings" }
|
|
181
159
|
},
|
|
182
160
|
{ type: "divider" },
|
|
183
161
|
{ 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
|
+
{ 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." }
|
|
163
|
+
]
|
|
186
164
|
};
|
|
187
165
|
}
|
|
188
|
-
|
|
189
|
-
function getDefaultEndpoint(region: string): string {
|
|
166
|
+
function getDefaultEndpoint(region) {
|
|
190
167
|
return `email-smtp.${region}.amazonaws.com`;
|
|
191
168
|
}
|
|
192
|
-
|
|
193
|
-
async function saveSettings(ctx: PluginContext, values: Record<string, unknown>) {
|
|
169
|
+
async function saveSettings(ctx, values) {
|
|
194
170
|
const currentSettings = await getSettings(ctx);
|
|
195
|
-
|
|
196
171
|
if (typeof values.region === "string" && values.region.length > 0 && values.region !== currentSettings.region) {
|
|
197
172
|
const oldEndpoint = getDefaultEndpoint(currentSettings.region);
|
|
198
173
|
if (!currentSettings.smtpHost || currentSettings.smtpHost === oldEndpoint) {
|
|
199
174
|
values.smtpHost = getDefaultEndpoint(values.region);
|
|
200
175
|
}
|
|
201
176
|
}
|
|
202
|
-
|
|
203
177
|
if (typeof values.smtpHost === "string" && values.smtpHost.length > 0) {
|
|
204
178
|
await ctx.kv.set("settings:smtpHost", values.smtpHost);
|
|
205
179
|
}
|
|
@@ -225,38 +199,30 @@ async function saveSettings(ctx: PluginContext, values: Record<string, unknown>)
|
|
|
225
199
|
await ctx.kv.set("settings:replyTo", values.replyTo);
|
|
226
200
|
}
|
|
227
201
|
}
|
|
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
|
-
) {
|
|
202
|
+
async function renderTestPage(ctx, validation, sendResult) {
|
|
234
203
|
const settings = await getSettings(ctx);
|
|
235
|
-
|
|
236
|
-
const blocks: Array<Record<string, unknown>> = [
|
|
204
|
+
const blocks = [
|
|
237
205
|
{ type: "header", text: "AWS SES Test & Status" },
|
|
238
206
|
{ type: "section", text: `Current endpoint: ${settings.smtpHost}:${settings.smtpPort} (${settings.region})` },
|
|
239
|
-
{ type: "divider" }
|
|
207
|
+
{ type: "divider" }
|
|
240
208
|
];
|
|
241
|
-
|
|
242
209
|
if (validation) {
|
|
243
210
|
if (validation.ok) {
|
|
244
211
|
blocks.push({
|
|
245
212
|
type: "banner",
|
|
246
213
|
title: "Connection verified",
|
|
247
214
|
description: "SMTP credentials are valid.",
|
|
248
|
-
variant: "default"
|
|
215
|
+
variant: "default"
|
|
249
216
|
});
|
|
250
217
|
} else {
|
|
251
218
|
blocks.push({
|
|
252
219
|
type: "banner",
|
|
253
220
|
title: "Connection failed",
|
|
254
221
|
description: validation.error,
|
|
255
|
-
variant: "error"
|
|
222
|
+
variant: "error"
|
|
256
223
|
});
|
|
257
224
|
}
|
|
258
225
|
}
|
|
259
|
-
|
|
260
226
|
blocks.push(
|
|
261
227
|
{ type: "section", text: "Test your SMTP connection and verify your credentials are correct." },
|
|
262
228
|
{
|
|
@@ -265,33 +231,31 @@ async function renderTestPage(
|
|
|
265
231
|
{
|
|
266
232
|
type: "button",
|
|
267
233
|
label: "Test Connection",
|
|
268
|
-
action_id: "validate"
|
|
269
|
-
}
|
|
270
|
-
]
|
|
234
|
+
action_id: "validate"
|
|
235
|
+
}
|
|
236
|
+
]
|
|
271
237
|
},
|
|
272
238
|
{ type: "divider" },
|
|
273
239
|
{ type: "header", text: "Send Test Email" },
|
|
274
|
-
{ type: "section", text: "Send a test email to verify the full transport pipeline. Enter a recipient address below." }
|
|
240
|
+
{ type: "section", text: "Send a test email to verify the full transport pipeline. Enter a recipient address below." }
|
|
275
241
|
);
|
|
276
|
-
|
|
277
242
|
if (sendResult) {
|
|
278
243
|
if (sendResult.ok) {
|
|
279
244
|
blocks.push({
|
|
280
245
|
type: "banner",
|
|
281
246
|
title: "Email sent",
|
|
282
247
|
description: `Test email sent successfully to ${sendResult.recipient}.`,
|
|
283
|
-
variant: "default"
|
|
248
|
+
variant: "default"
|
|
284
249
|
});
|
|
285
250
|
} else {
|
|
286
251
|
blocks.push({
|
|
287
252
|
type: "banner",
|
|
288
253
|
title: "Email send failed",
|
|
289
254
|
description: sendResult.error,
|
|
290
|
-
variant: "error"
|
|
255
|
+
variant: "error"
|
|
291
256
|
});
|
|
292
257
|
}
|
|
293
258
|
}
|
|
294
|
-
|
|
295
259
|
blocks.push({
|
|
296
260
|
type: "form",
|
|
297
261
|
block_id: "test_form",
|
|
@@ -301,57 +265,48 @@ async function renderTestPage(
|
|
|
301
265
|
action_id: "recipient",
|
|
302
266
|
label: "Recipient Email",
|
|
303
267
|
placeholder: "you@example.com",
|
|
304
|
-
initial_value: sendResult?.ok ? "" :
|
|
305
|
-
}
|
|
268
|
+
initial_value: sendResult?.ok ? "" : void 0
|
|
269
|
+
}
|
|
306
270
|
],
|
|
307
|
-
submit: { label: "Send Test", action_id: "send_test" }
|
|
271
|
+
submit: { label: "Send Test", action_id: "send_test" }
|
|
308
272
|
});
|
|
309
|
-
|
|
310
273
|
return { blocks };
|
|
311
274
|
}
|
|
312
|
-
|
|
313
|
-
export default {
|
|
275
|
+
var sandbox_entry_default = {
|
|
314
276
|
hooks: {
|
|
315
|
-
"plugin:install": async (_event
|
|
277
|
+
"plugin:install": async (_event, ctx) => {
|
|
316
278
|
await ctx.kv.set("settings:smtpHost", "email-smtp.us-east-1.amazonaws.com");
|
|
317
279
|
await ctx.kv.set("settings:smtpPort", 587);
|
|
318
280
|
await ctx.kv.set("settings:region", "us-east-1");
|
|
319
281
|
ctx.log.info("AWS SES plugin defaults seeded");
|
|
320
282
|
},
|
|
321
|
-
|
|
322
283
|
"email:deliver": {
|
|
323
284
|
exclusive: true,
|
|
324
|
-
handler: async ({ message, source }
|
|
285
|
+
handler: async ({ message, source }, ctx) => {
|
|
325
286
|
const settings = await getSettings(ctx);
|
|
326
287
|
const transporter = createTransporter(settings);
|
|
327
|
-
|
|
328
288
|
await transporter.sendMail({
|
|
329
289
|
from: settings.fromName ? `"${settings.fromName}" <${settings.defaultFrom}>` : settings.defaultFrom,
|
|
330
290
|
to: message.to,
|
|
331
291
|
subject: message.subject,
|
|
332
292
|
text: message.text,
|
|
333
293
|
html: message.html,
|
|
334
|
-
replyTo: settings.replyTo ||
|
|
294
|
+
replyTo: settings.replyTo || void 0
|
|
335
295
|
});
|
|
336
|
-
|
|
337
296
|
ctx.log.info(`Email sent via AWS SES to ${message.to}`, { source });
|
|
338
|
-
}
|
|
339
|
-
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
340
299
|
},
|
|
341
|
-
|
|
342
300
|
routes: {
|
|
343
301
|
admin: {
|
|
344
|
-
handler: async (routeCtx
|
|
345
|
-
const interaction = routeCtx.input
|
|
346
|
-
|
|
302
|
+
handler: async (routeCtx, ctx) => {
|
|
303
|
+
const interaction = routeCtx.input;
|
|
347
304
|
if (interaction.type === "page_load" && (!interaction.page || interaction.page === "/")) {
|
|
348
305
|
return renderSettingsPage(ctx);
|
|
349
306
|
}
|
|
350
|
-
|
|
351
307
|
if (interaction.type === "page_load" && interaction.page === "/settings") {
|
|
352
308
|
return renderSettingsPage(ctx);
|
|
353
309
|
}
|
|
354
|
-
|
|
355
310
|
if (interaction.type === "form_submit" && interaction.action_id === "save_settings") {
|
|
356
311
|
await saveSettings(ctx, interaction.values ?? {});
|
|
357
312
|
const settingsPage = await renderSettingsPage(ctx);
|
|
@@ -363,47 +318,42 @@ export default {
|
|
|
363
318
|
type: "banner",
|
|
364
319
|
title: "Settings saved",
|
|
365
320
|
description: "Your AWS SES configuration has been updated successfully.",
|
|
366
|
-
variant: "default"
|
|
321
|
+
variant: "default"
|
|
367
322
|
},
|
|
368
|
-
...settingsBlocks.slice(1)
|
|
323
|
+
...settingsBlocks.slice(1)
|
|
369
324
|
],
|
|
370
|
-
toast: { message: "Settings saved successfully", type: "success" }
|
|
325
|
+
toast: { message: "Settings saved successfully", type: "success" }
|
|
371
326
|
};
|
|
372
327
|
}
|
|
373
|
-
|
|
374
328
|
if (interaction.type === "page_load" && interaction.page === "/test") {
|
|
375
329
|
return renderTestPage(ctx);
|
|
376
330
|
}
|
|
377
|
-
|
|
378
331
|
if (interaction.type === "block_action" && interaction.action_id === "validate") {
|
|
379
332
|
const result = await validateSMTP(ctx);
|
|
380
333
|
return {
|
|
381
|
-
...
|
|
382
|
-
toast: result.ok
|
|
383
|
-
? { message: "Connection verified successfully", type: "success" }
|
|
384
|
-
: { message: result.error ?? "Validation failed", type: "error" },
|
|
334
|
+
...await renderTestPage(ctx, result),
|
|
335
|
+
toast: result.ok ? { message: "Connection verified successfully", type: "success" } : { message: result.error ?? "Validation failed", type: "error" }
|
|
385
336
|
};
|
|
386
337
|
}
|
|
387
|
-
|
|
388
338
|
if (interaction.type === "form_submit" && interaction.action_id === "send_test") {
|
|
389
|
-
const recipient =
|
|
339
|
+
const recipient = interaction.values?.recipient?.trim();
|
|
390
340
|
if (!recipient) {
|
|
391
341
|
return {
|
|
392
|
-
...
|
|
393
|
-
toast: { message: "Please enter a recipient email", type: "error" }
|
|
342
|
+
...await renderTestPage(ctx),
|
|
343
|
+
toast: { message: "Please enter a recipient email", type: "error" }
|
|
394
344
|
};
|
|
395
345
|
}
|
|
396
346
|
const result = await sendTestEmail(ctx, recipient);
|
|
397
347
|
return {
|
|
398
|
-
...
|
|
399
|
-
toast: result.ok
|
|
400
|
-
? { message: `Test email sent to ${recipient}`, type: "success" }
|
|
401
|
-
: { message: result.error ?? "Send failed", type: "error" },
|
|
348
|
+
...await renderTestPage(ctx, null, { ...result, recipient }),
|
|
349
|
+
toast: result.ok ? { message: `Test email sent to ${recipient}`, type: "success" } : { message: result.error ?? "Send failed", type: "error" }
|
|
402
350
|
};
|
|
403
351
|
}
|
|
404
|
-
|
|
405
352
|
return { blocks: [] };
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
export {
|
|
358
|
+
sandbox_entry_default as default
|
|
359
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "emdash-aws-ses",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "Amazon SES SMTP email transport plugin for EmDash CMS",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"keywords": [
|
|
@@ -10,7 +10,18 @@
|
|
|
10
10
|
"astro",
|
|
11
11
|
"aws"
|
|
12
12
|
],
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+https://github.com/ab6162/emdash-aws-ses.git"
|
|
16
|
+
},
|
|
17
|
+
"bugs": {
|
|
18
|
+
"url": "https://github.com/ab6162/emdash-aws-ses/issues"
|
|
19
|
+
},
|
|
20
|
+
"homepage": "https://github.com/ab6162/emdash-aws-ses#readme",
|
|
13
21
|
"type": "module",
|
|
22
|
+
"files": [
|
|
23
|
+
"dist"
|
|
24
|
+
],
|
|
14
25
|
"main": "./dist/index.cjs",
|
|
15
26
|
"module": "./dist/index.js",
|
|
16
27
|
"types": "./dist/index.d.ts",
|
|
@@ -27,7 +38,7 @@
|
|
|
27
38
|
}
|
|
28
39
|
},
|
|
29
40
|
"scripts": {
|
|
30
|
-
"build": "tsup src/index.ts src/sandbox-entry.ts --format cjs,esm --clean"
|
|
41
|
+
"build": "tsup src/index.ts src/sandbox-entry.ts --format cjs,esm --clean && tsc --emitDeclarationOnly"
|
|
31
42
|
},
|
|
32
43
|
"peerDependencies": {
|
|
33
44
|
"emdash": "^0.29.0"
|
package/.gitattributes
DELETED
package/tsconfig.json
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
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
|
-
}
|