feat(scripts): add send-email script using nodemailer
Add a sandboxed send-email.js workflow script with SMTP settings exposed via script config and password loaded from named secrets. Allow nodemailer in the script sandbox require whitelist. Co-authored-by: Nasyarobby Putra <nasyarobby@gmail.com>
This commit is contained in:
@@ -22,6 +22,7 @@
|
||||
"knex": "^3.3.0",
|
||||
"node-cron": "^4.6.0",
|
||||
"node-html-parser": "^9.0.1",
|
||||
"nodemailer": "^9.0.5",
|
||||
"pino": "^10.3.1",
|
||||
"pino-roll": "^4.0.0",
|
||||
"yaml": "^2.9.0"
|
||||
|
||||
@@ -12,7 +12,13 @@ import { getSecretPlaintext } from "./secrets-store.js";
|
||||
|
||||
const hostRequire = createRequire(import.meta.url);
|
||||
|
||||
const ALLOWED_MODULES = new Set(["axios", "jsonata", "node-html-parser", "rss-parser"]);
|
||||
const ALLOWED_MODULES = new Set([
|
||||
"axios",
|
||||
"jsonata",
|
||||
"node-html-parser",
|
||||
"nodemailer",
|
||||
"rss-parser",
|
||||
]);
|
||||
|
||||
const BUILTIN_NAMES = [
|
||||
"Infinity",
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
import nodemailer from "nodemailer";
|
||||
|
||||
/**
|
||||
* @param {import("nodemailer").TransportOptions} transportOptions
|
||||
*/
|
||||
function createTransport(transportOptions) {
|
||||
return nodemailer.createTransport(transportOptions);
|
||||
}
|
||||
|
||||
async function sendEmail(ctx) {
|
||||
const config = ctx.config ?? {};
|
||||
const host = config.host;
|
||||
if (typeof host !== "string" || host.length === 0) {
|
||||
throw new Error("config.host is required");
|
||||
}
|
||||
|
||||
const user = config.user;
|
||||
if (typeof user !== "string" || user.length === 0) {
|
||||
throw new Error("config.user is required");
|
||||
}
|
||||
|
||||
const passwordSecret = config.passwordSecret;
|
||||
if (typeof passwordSecret !== "string" || passwordSecret.length === 0) {
|
||||
throw new Error("config.passwordSecret is required");
|
||||
}
|
||||
|
||||
const to = ctx.data?.to;
|
||||
if (typeof to !== "string" || to.length === 0) {
|
||||
throw new Error("data.to is required");
|
||||
}
|
||||
|
||||
const subject = ctx.data?.subject;
|
||||
if (typeof subject !== "string" || subject.length === 0) {
|
||||
throw new Error("data.subject is required");
|
||||
}
|
||||
|
||||
const text = ctx.data?.text ?? ctx.data?.body ?? ctx.data?.message;
|
||||
if (typeof text !== "string" || text.length === 0) {
|
||||
throw new Error("data.text is required (plain-text body)");
|
||||
}
|
||||
|
||||
const password = $secrets.reveal(await $secrets.get(passwordSecret));
|
||||
const from =
|
||||
typeof config.from === "string" && config.from.length > 0 ? config.from : user;
|
||||
const port = Number(config.port ?? 587);
|
||||
const secure = config.secure === true;
|
||||
|
||||
log.info(
|
||||
{ host, port, secure, from, to, subjectLength: subject.length, textLength: text.length },
|
||||
"send-email: sending plain-text message",
|
||||
);
|
||||
|
||||
const transporter = createTransport({
|
||||
host,
|
||||
port,
|
||||
secure,
|
||||
auth: { user, pass: password },
|
||||
});
|
||||
|
||||
const info = await transporter.sendMail({
|
||||
from,
|
||||
to,
|
||||
subject,
|
||||
text,
|
||||
});
|
||||
|
||||
log.info({ messageId: info.messageId }, "send-email: message sent");
|
||||
|
||||
return {
|
||||
sent: true,
|
||||
messageId: info.messageId ?? null,
|
||||
to,
|
||||
subject,
|
||||
};
|
||||
}
|
||||
|
||||
sendEmail.meta = {
|
||||
description: "Send a plain-text email via SMTP (nodemailer)",
|
||||
config: {
|
||||
host: {
|
||||
type: "string",
|
||||
required: true,
|
||||
description: "SMTP server hostname (e.g. smtp.gmail.com)",
|
||||
},
|
||||
port: {
|
||||
type: "number",
|
||||
default: 587,
|
||||
description: "SMTP port (587 for STARTTLS, 465 for SSL)",
|
||||
},
|
||||
secure: {
|
||||
type: "boolean",
|
||||
default: false,
|
||||
description: "Use TLS on connect (true for port 465)",
|
||||
},
|
||||
user: {
|
||||
type: "string",
|
||||
required: true,
|
||||
description: "SMTP auth username (usually the sender email)",
|
||||
},
|
||||
from: {
|
||||
type: "string",
|
||||
required: false,
|
||||
description: "From address (defaults to user)",
|
||||
},
|
||||
passwordSecret: {
|
||||
type: "string",
|
||||
required: true,
|
||||
description: "Named secret holding the SMTP password or app password",
|
||||
},
|
||||
},
|
||||
input: {
|
||||
to: { type: "string", required: true, description: "Recipient email address" },
|
||||
subject: { type: "string", required: true, description: "Email subject" },
|
||||
text: {
|
||||
type: "string",
|
||||
required: true,
|
||||
description: "Plain-text body (aliases: body, message)",
|
||||
},
|
||||
},
|
||||
output: {
|
||||
sent: { type: "boolean", description: "Whether the message was sent" },
|
||||
messageId: { type: "string", description: "SMTP message id when available" },
|
||||
to: { type: "string" },
|
||||
subject: { type: "string" },
|
||||
},
|
||||
example: {
|
||||
data: {
|
||||
to: "recipient@example.com",
|
||||
subject: "Hello from scrunner",
|
||||
text: "This is a plain-text test message.",
|
||||
},
|
||||
config: {
|
||||
host: "smtp.gmail.com",
|
||||
port: 587,
|
||||
secure: false,
|
||||
user: "you@gmail.com",
|
||||
passwordSecret: "gmail_app_password",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default sendEmail;
|
||||
Generated
+9
@@ -50,6 +50,9 @@ importers:
|
||||
node-html-parser:
|
||||
specifier: ^9.0.1
|
||||
version: 9.0.1
|
||||
nodemailer:
|
||||
specifier: ^9.0.5
|
||||
version: 9.0.5
|
||||
pino:
|
||||
specifier: ^10.3.1
|
||||
version: 10.3.1
|
||||
@@ -1607,6 +1610,10 @@ packages:
|
||||
resolution: {integrity: sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
nodemailer@9.0.5:
|
||||
resolution: {integrity: sha512-wvjiKvjczmsN7U/8006JOdXubgBk2XFAbioDMbT+sM7cPs0QrhJTa6KBRX7P5REGGkDcLUz/EarWidb8G8C1jQ==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
|
||||
nth-check@2.1.1:
|
||||
resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
|
||||
|
||||
@@ -3400,6 +3407,8 @@ snapshots:
|
||||
|
||||
node-releases@2.0.53: {}
|
||||
|
||||
nodemailer@9.0.5: {}
|
||||
|
||||
nth-check@2.1.1:
|
||||
dependencies:
|
||||
boolbase: 1.0.0
|
||||
|
||||
Reference in New Issue
Block a user