Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitlab-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ include:
- woocommerce-action
- shopware-action
- twilio-action
- smtp-action

test-node:
image: node:24.10.0
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ The "central" place for all actions — a monorepo containing integrations provi
| Action | Description |
|--------|-------------|
| [GLS](docs/Actions/GLS/overview.md) | GLS ShipIT integration for creating and managing shipments |
| SMTP | Send emails (with attachments) through any SMTP server via nodemailer |

## ENV

Expand Down
3 changes: 3 additions & 0 deletions actions/smtp-action/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.env
node_modules/
dist/
13 changes: 13 additions & 0 deletions actions/smtp-action/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
FROM node:24.10.0-alpine

WORKDIR /app

COPY package.json package-lock.json* ./

RUN npm install

COPY . .

RUN npm run build

CMD ["npm", "run", "start"]
61 changes: 61 additions & 0 deletions actions/smtp-action/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# SMTP Action

Send emails through any SMTP server from a Hercules flow. Built on
[nodemailer](https://nodemailer.com/), the de-facto standard SMTP client for
Node.js.

This action is function-only (like the GLS and Twilio actions) — it exposes
functions to send mail and build attachments, and does not register any
triggers.

## Configuration

Configured via the action's `ConfigurationDefinition`s:

| Identifier | Type | Required | Description |
|---|---|---|---|
| `host` | TEXT | yes | Hostname of the SMTP server, e.g. `smtp.example.com`. |
| `port` | TEXT | no (default `587`) | SMTP port. Common values: `587` (STARTTLS), `465` (implicit TLS). |
| `secure` | TEXT | no (default `false`) | `"true"` to use implicit TLS (port 465), `"false"` for STARTTLS (port 587). |
| `username` | TEXT | no | Username for SMTP AUTH. Leave empty for unauthenticated relays. |
| `password` | TEXT | no | Password for SMTP AUTH. |
| `from_address` | TEXT | no | Default `From` used when a function call omits it, e.g. `Acme <[email protected]>`. |

Beyond the shared Hercules variables (`HERCULES_AUTH_TOKEN`,
`HERCULES_AQUILA_URL`, `HERCULES_ACTION_ID`, `HERCULES_SDK_VERSION`), no
provider-specific environment variables are required — SMTP credentials are
supplied through the configuration above.

## Functions

| Identifier | Signature | Description |
|---|---|---|
| `sendEmail` | `(To, Subject, Text, Html?, From?, Cc?, Bcc?, ReplyTo?): SMTP_SEND_RESULT` | Send a plain-text (and optionally HTML) email. `To`/`Cc`/`Bcc` are comma-separated address lists. |
| `sendEmailWithAttachments` | `(To, Subject, Text, Attachments, Html?, From?, Cc?, Bcc?): SMTP_SEND_RESULT` | Send an email with one or more attachments built via `createAttachment`. |
| `createAttachment` | `(Filename, Content, ContentType?, Encoding?): SMTP_ATTACHMENT` | Build an attachment object. Use `base64` encoding for binary files. |

## Data types

- `SMTP_SEND_RESULT` — normalized nodemailer `SentMessageInfo`: `messageId`,
`accepted`, `rejected`, `response`, `envelope`.
- `SMTP_ENVELOPE` — the SMTP envelope (`from`, `to`) used for delivery.
- `SMTP_ATTACHMENT` — `filename`, `content`, optional `contentType` and
`encoding`.

## Provider setup

Most providers (Gmail, Microsoft 365, Amazon SES, Postmark, Mailgun, etc.)
expose SMTP credentials. For Gmail/Workspace you typically need an
[App Password](https://support.google.com/accounts/answer/185833) rather than
your account password. Point `host`/`port` at the provider's SMTP endpoint and
set `secure` to match the port (465 → `true`, 587 → `false`).

## Development

```bash
cd actions/smtp-action
npm install
npm run typecheck
npm run build
npm run test
```
26 changes: 26 additions & 0 deletions actions/smtp-action/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"name": "@code0-tech/smtp-action",
"version": "1.0.0",
"private": true,
"type": "module",
"main": "dist/index.js",
"scripts": {
"dev": "tsx watch src/index.ts",
"typecheck": "tsc --noEmit",
"build": "vite build",
"test": "vitest run",
"start": "node dist/index.js"
},
"dependencies": {
"@code0-tech/hercules": "^1.1.1",
"nodemailer": "^6.9.16"
},
"devDependencies": {
"@types/node": "^22.0.0",
"@types/nodemailer": "^6.4.16",
"tsx": "^4.0.0",
"typescript": "^5.9.3",
"vite": "^8.0.3",
"vitest": "^4.1.10"
}
}
23 changes: 23 additions & 0 deletions actions/smtp-action/src/data_types/smtpAttachment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { DisplayMessage, Identifier, Name, Schema } from "@code0-tech/hercules";
import { z } from "zod";

/**
* SMTP_ATTACHMENT mirrors the shape of a nodemailer attachment object
* (see https://nodemailer.com/message/attachments/). Only the fields that make
* sense to build from a flow are exposed; `content` is always provided as a
* string and interpreted according to `encoding` (e.g. "base64" for binary
* files, "utf-8" for text).
*/
export const SmtpAttachmentSchema = z.object({
filename: z.string().describe("The file name shown to the recipient, e.g. \"invoice.pdf\"."),
content: z.string().describe("The attachment content as a string. Interpreted using the encoding field."),
contentType: z.string().optional().describe("The MIME type of the attachment, e.g. \"application/pdf\". Derived from the filename when omitted."),
encoding: z.string().optional().describe("How to decode content, e.g. \"base64\" or \"utf-8\". Defaults to utf-8."),
});
export type SmtpAttachment = z.infer<typeof SmtpAttachmentSchema>;

@Identifier("SMTP_ATTACHMENT")
@Name({ code: "en-US", content: "Email attachment" })
@DisplayMessage({ code: "en-US", content: "Email attachment ${filename}" })
@Schema(SmtpAttachmentSchema)
export class SmtpAttachmentDataType {}
35 changes: 35 additions & 0 deletions actions/smtp-action/src/data_types/smtpSendResult.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { DisplayMessage, Identifier, Name, Schema } from "@code0-tech/hercules";
import { z } from "zod";

/**
* SMTP_SEND_RESULT is a normalized view of nodemailer's SentMessageInfo
* (see https://nodemailer.com/usage/#sending-mail). It captures the message id
* assigned by the SMTP server together with the accepted/rejected recipient
* lists and the raw server response so flows can branch on delivery outcome.
*/
export const SmtpEnvelopeSchema = z.object({
from: z.string().describe("The envelope MAIL FROM address, empty when not set."),
to: z.array(z.string()).describe("The envelope RCPT TO addresses."),
});
export type SmtpEnvelope = z.infer<typeof SmtpEnvelopeSchema>;

export const SmtpSendResultSchema = z.object({
messageId: z.string().describe("The Message-ID assigned to the sent message."),
accepted: z.array(z.string()).describe("Recipient addresses the SMTP server accepted."),
rejected: z.array(z.string()).describe("Recipient addresses the SMTP server rejected."),
response: z.string().describe("The last SMTP response string from the server."),
envelope: SmtpEnvelopeSchema.describe("The SMTP envelope actually used for delivery."),
});
export type SmtpSendResult = z.infer<typeof SmtpSendResultSchema>;

@Identifier("SMTP_ENVELOPE")
@Name({ code: "en-US", content: "Email envelope" })
@DisplayMessage({ code: "en-US", content: "Email envelope" })
@Schema(SmtpEnvelopeSchema)
export class SmtpEnvelopeDataType {}

@Identifier("SMTP_SEND_RESULT")
@Name({ code: "en-US", content: "Email send result" })
@DisplayMessage({ code: "en-US", content: "Email send result" })
@Schema(SmtpSendResultSchema)
export class SmtpSendResultDataType {}
99 changes: 99 additions & 0 deletions actions/smtp-action/src/functions/sendEmailFunction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import {
Description,
DisplayIcon,
DisplayMessage,
Documentation,
FunctionContext,
Identifier,
Name,
Parameter,
RuntimeError,
Signature,
} from "@code0-tech/hercules";
import { sendEmail } from "../helpers.js";
import { SmtpSendResult } from "../data_types/smtpSendResult.js";

@Identifier("sendEmail")
@DisplayIcon("tabler:mail")
@Signature("(To: string, Subject: string, Text: string, Html?: string, From?: string, Cc?: string, Bcc?: string, ReplyTo?: string): SMTP_SEND_RESULT")
@Name({ code: "en-US", content: "Send email" })
@DisplayMessage({ code: "en-US", content: "Send email to ${To}" })
@Documentation({
code: "en-US",
content:
"Sends an email through the configured SMTP server using nodemailer.\nProvide `Html` to send a rich body alongside the plain text, and `From` to override the action's default sender. `To`, `Cc`, and `Bcc` accept comma-separated address lists.",
})
@Description({
code: "en-US",
content: "Sends an email through the configured SMTP server.",
})
@Parameter({
runtimeName: "To",
name: [{ code: "en-US", content: "To" }],
description: [{ code: "en-US", content: "Comma-separated list of recipient email addresses." }],
})
@Parameter({
runtimeName: "Subject",
name: [{ code: "en-US", content: "Subject" }],
description: [{ code: "en-US", content: "The subject line of the email." }],
})
@Parameter({
runtimeName: "Text",
name: [{ code: "en-US", content: "Text body" }],
description: [{ code: "en-US", content: "The plain-text body of the email." }],
})
@Parameter({
runtimeName: "Html",
name: [{ code: "en-US", content: "HTML body" }],
description: [{ code: "en-US", content: "Optional HTML body. When set it is sent alongside the plain-text body." }],
optional: true,
})
@Parameter({
runtimeName: "From",
name: [{ code: "en-US", content: "From" }],
description: [{ code: "en-US", content: "The sender address. Falls back to the configured default sender when omitted." }],
optional: true,
})
@Parameter({
runtimeName: "Cc",
name: [{ code: "en-US", content: "CC" }],
description: [{ code: "en-US", content: "Comma-separated list of CC recipients." }],
optional: true,
})
@Parameter({
runtimeName: "Bcc",
name: [{ code: "en-US", content: "BCC" }],
description: [{ code: "en-US", content: "Comma-separated list of BCC recipients." }],
optional: true,
})
@Parameter({
runtimeName: "ReplyTo",
name: [{ code: "en-US", content: "Reply-To" }],
description: [{ code: "en-US", content: "The Reply-To address for the email." }],
optional: true,
})
export class SendEmailFunction {
async run(
context: FunctionContext,
To: string,
Subject: string,
Text: string,
Html?: string,
From?: string,
Cc?: string,
Bcc?: string,
ReplyTo?: string
): Promise<SmtpSendResult> {
try {
return await sendEmail({ To, Subject, Text, Html, From, Cc, Bcc, ReplyTo }, context);
} catch (error) {
if (error instanceof RuntimeError) {
throw error;
}
if (error instanceof Error) {
throw new RuntimeError("ERROR_SENDING_EMAIL", error.message);
}
throw new RuntimeError("ERROR_SENDING_EMAIL", "An error occurred while sending the email.");
}
}
}
102 changes: 102 additions & 0 deletions actions/smtp-action/src/functions/sendEmailWithAttachmentsFunction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import {
Description,
DisplayIcon,
DisplayMessage,
Documentation,
FunctionContext,
Identifier,
Name,
Parameter,
RuntimeError,
Signature,
} from "@code0-tech/hercules";
import { sendEmail } from "../helpers.js";
import { SmtpSendResult } from "../data_types/smtpSendResult.js";
import { SmtpAttachment } from "../data_types/smtpAttachment.js";

@Identifier("sendEmailWithAttachments")
@DisplayIcon("tabler:mail")
@Signature("(To: string, Subject: string, Text: string, Attachments: SMTP_ATTACHMENT, Html?: string, From?: string, Cc?: string, Bcc?: string): SMTP_SEND_RESULT")
@Name({ code: "en-US", content: "Send email with attachments" })
@DisplayMessage({ code: "en-US", content: "Send email with attachments to ${To}" })
@Documentation({
code: "en-US",
content:
"Sends an email with one or more attachments through the configured SMTP server.\nBuild each attachment with the `createAttachment` function. `To`, `Cc`, and `Bcc` accept comma-separated address lists.",
})
@Description({
code: "en-US",
content: "Sends an email with attachments through the configured SMTP server.",
})
@Parameter({
runtimeName: "To",
name: [{ code: "en-US", content: "To" }],
description: [{ code: "en-US", content: "Comma-separated list of recipient email addresses." }],
})
@Parameter({
runtimeName: "Subject",
name: [{ code: "en-US", content: "Subject" }],
description: [{ code: "en-US", content: "The subject line of the email." }],
})
@Parameter({
runtimeName: "Text",
name: [{ code: "en-US", content: "Text body" }],
description: [{ code: "en-US", content: "The plain-text body of the email." }],
})
@Parameter({
runtimeName: "Attachments",
name: [{ code: "en-US", content: "Attachments" }],
description: [{ code: "en-US", content: "One or more attachments built with the createAttachment function." }],
})
@Parameter({
runtimeName: "Html",
name: [{ code: "en-US", content: "HTML body" }],
description: [{ code: "en-US", content: "Optional HTML body. When set it is sent alongside the plain-text body." }],
optional: true,
})
@Parameter({
runtimeName: "From",
name: [{ code: "en-US", content: "From" }],
description: [{ code: "en-US", content: "The sender address. Falls back to the configured default sender when omitted." }],
optional: true,
})
@Parameter({
runtimeName: "Cc",
name: [{ code: "en-US", content: "CC" }],
description: [{ code: "en-US", content: "Comma-separated list of CC recipients." }],
optional: true,
})
@Parameter({
runtimeName: "Bcc",
name: [{ code: "en-US", content: "BCC" }],
description: [{ code: "en-US", content: "Comma-separated list of BCC recipients." }],
optional: true,
})
export class SendEmailWithAttachmentsFunction {
async run(
context: FunctionContext,
To: string,
Subject: string,
Text: string,
Attachments: SmtpAttachment[],
Html?: string,
From?: string,
Cc?: string,
Bcc?: string
): Promise<SmtpSendResult> {
try {
return await sendEmail(
{ To, Subject, Text, Html, From, Cc, Bcc, Attachments: Attachments ?? [] },
context
);
} catch (error) {
if (error instanceof RuntimeError) {
throw error;
}
if (error instanceof Error) {
throw new RuntimeError("ERROR_SENDING_EMAIL", error.message);
}
throw new RuntimeError("ERROR_SENDING_EMAIL", "An error occurred while sending the email.");
}
}
}
Loading