From f1a76f659ba33a9e56d044d37239a185f4fb8363 Mon Sep 17 00:00:00 2001 From: Raphael Date: Sun, 1 Nov 2026 18:50:05 +0100 Subject: [PATCH] feat: transform smtp-action into mail-action with IMAP receive (#82) Rename the SMTP action to a general-purpose mail action that can both send mail (SMTP, as before) and receive it via a new IMAP trigger. - rename actions/smtp-action -> actions/mail-action (package, action id) - add per-project IMAP server config (imap_host/port/secure/username/ password/mailbox) alongside the existing SMTP config - rename SMTP_SEND_RESULT -> MAIL_SEND_RESULT; send functions unchanged - add RECEIVED_MAIL data type and the MAIL_RECEIVED RuntimeEvent trigger with an `addresses` flow setting (string array; empty matches all) - add imapListener: a reconciling ImapFlow connection manager (one per project with a bound flow) that parses incoming mail and executes each flow whose configured addresses match the message recipients - update CI matrix and READMEs Co-Authored-By: Claude Opus 4.8 --- .gitlab-ci.yml | 3 +- README.md | 2 +- .../{smtp-action => mail-action}/.gitignore | 0 .../{smtp-action => mail-action}/Dockerfile | 0 actions/mail-action/README.md | 90 ++ .../package-lock.json | 803 ++++++++++++++---- .../{smtp-action => mail-action}/package.json | 5 +- .../src/data_types/mailSendResult.ts} | 12 +- .../src/data_types/receivedMail.ts | 26 + .../mail-action/src/events/mailReceived.ts | 43 + .../src/functions/sendEmailFunction.ts | 6 +- .../sendEmailWithAttachmentsFunction.ts | 6 +- .../src/helpers.ts | 8 +- actions/mail-action/src/imapListener.ts | 316 +++++++ .../{smtp-action => mail-action}/src/index.ts | 66 +- actions/mail-action/test/index.test.ts | 59 ++ .../tsconfig.json | 0 .../vite.config.ts | 0 actions/smtp-action/README.md | 61 -- actions/smtp-action/test/index.test.ts | 21 - 20 files changed, 1237 insertions(+), 290 deletions(-) rename actions/{smtp-action => mail-action}/.gitignore (100%) rename actions/{smtp-action => mail-action}/Dockerfile (100%) create mode 100644 actions/mail-action/README.md rename actions/{smtp-action => mail-action}/package-lock.json (77%) rename actions/{smtp-action => mail-action}/package.json (81%) rename actions/{smtp-action/src/data_types/smtpSendResult.ts => mail-action/src/data_types/mailSendResult.ts} (78%) create mode 100644 actions/mail-action/src/data_types/receivedMail.ts create mode 100644 actions/mail-action/src/events/mailReceived.ts rename actions/{smtp-action => mail-action}/src/functions/sendEmailFunction.ts (96%) rename actions/{smtp-action => mail-action}/src/functions/sendEmailWithAttachmentsFunction.ts (96%) rename actions/{smtp-action => mail-action}/src/helpers.ts (95%) create mode 100644 actions/mail-action/src/imapListener.ts rename actions/{smtp-action => mail-action}/src/index.ts (53%) create mode 100644 actions/mail-action/test/index.test.ts rename actions/{smtp-action => mail-action}/tsconfig.json (100%) rename actions/{smtp-action => mail-action}/vite.config.ts (100%) delete mode 100644 actions/smtp-action/README.md delete mode 100644 actions/smtp-action/test/index.test.ts diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index e9953a6..fb09ac0 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -27,14 +27,13 @@ include: - shopware-action - twilio-action - stripe-action - - smtp-action + - mail-action - github-action .rust-actions: - cron-action - rest-action - mcp-action - - smtp-action test-node: image: node:24.10.0 diff --git a/README.md b/README.md index 32adaf6..f63de5f 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ The "central" place for all actions — a monorepo containing integrations provi |--------|-------------| | [GLS](docs/Actions/GLS/overview.md) | GLS ShipIT integration for creating and managing shipments | | [Stripe](actions/stripe-action/README.md) | Stripe payments integration for managing customers, payment intents and refunds | -| SMTP | Send emails (with attachments) through any SMTP server via nodemailer | +| [Mail](actions/mail-action/README.md) | Send mail via SMTP and receive mail via an IMAP trigger | ## ENV All actions share the following base environment variables, which connect them to the Hercules/Aquila infrastructure: diff --git a/actions/smtp-action/.gitignore b/actions/mail-action/.gitignore similarity index 100% rename from actions/smtp-action/.gitignore rename to actions/mail-action/.gitignore diff --git a/actions/smtp-action/Dockerfile b/actions/mail-action/Dockerfile similarity index 100% rename from actions/smtp-action/Dockerfile rename to actions/mail-action/Dockerfile diff --git a/actions/mail-action/README.md b/actions/mail-action/README.md new file mode 100644 index 0000000..1f3ba76 --- /dev/null +++ b/actions/mail-action/README.md @@ -0,0 +1,90 @@ +# Mail Action + +Send and receive email from a Hercules flow. Sending is built on +[nodemailer](https://nodemailer.com/), the de-facto standard SMTP client for +Node.js. Receiving is built on [imapflow](https://imapflow.com/) (connection +and IDLE handling) and [mailparser](https://nodemailer.com/extras/mailparser/) +(message parsing). + +This action exposes functions to send mail and build attachments (like the +GLS and Twilio actions), and registers a single trigger, `MAIL_RECEIVED`, that +fires a flow whenever new mail arrives on a watched IMAP mailbox. + +## 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 `. | +| `imap_host` | TEXT | yes (for receiving) | Hostname of the IMAP server to watch for incoming mail, e.g. `imap.example.com`. | +| `imap_port` | TEXT | no (default `993`) | IMAP port. Defaults to `993` (implicit TLS). | +| `imap_secure` | TEXT | no (default `true`) | `"true"` to use implicit TLS (port 993), `"false"` for an unencrypted/STARTTLS connection. | +| `imap_username` | TEXT | no | Username for IMAP authentication. | +| `imap_password` | TEXT | no | Password for IMAP authentication. | +| `imap_mailbox` | TEXT | no (default `INBOX`) | The mailbox folder to watch for incoming mail. | + +The IMAP side only connects for a project once that project has a flow using +the `MAIL_RECEIVED` trigger, so the SMTP-only config keys above remain +sufficient if you only ever send mail. + +Beyond the shared Hercules variables (`HERCULES_AUTH_TOKEN`, +`HERCULES_AQUILA_URL`, `HERCULES_ACTION_ID`, `HERCULES_SDK_VERSION`), no +provider-specific environment variables are required — mail credentials are +supplied through the configuration above. + +## Functions + +| Identifier | Signature | Description | +|---|---|---| +| `sendEmail` | `(To, Subject, Text, Html?, From?, Cc?, Bcc?, ReplyTo?): MAIL_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?): MAIL_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. | + +## Trigger + +| Identifier | Signature | Description | +|---|---|---| +| `MAIL_RECEIVED` | `(addresses: TEXT[]): RECEIVED_MAIL` | Fires a flow when new mail arrives on the project's configured IMAP mailbox. | + +The `addresses` flow setting is a list of email addresses. When it is +non-empty, only mail whose `To`/`Cc` recipients include one of those +addresses (matched case-insensitively) triggers the flow; when empty, every +incoming message triggers the flow. + +The action watches one IMAP connection per project that has at least one +`MAIL_RECEIVED` flow, and reconciles connections (opening/closing them) as +flows are created, updated, or deleted. + +## Data types + +- `MAIL_SEND_RESULT` — normalized nodemailer `SentMessageInfo`: `messageId`, + `accepted`, `rejected`, `response`, `from`, `to`. +- `RECEIVED_MAIL` — a parsed incoming message: `messageId`, `from`, `to`, + `cc`, `subject`, `text`, `html`, `date`. +- `SMTP_ATTACHMENT` — `filename`, `content`, optional `contentType` and + `encoding`. + +## Provider setup + +Most providers (Gmail, Microsoft 365, Amazon SES, Postmark, Mailgun, etc.) +expose both SMTP and IMAP 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` (SMTP) and `imap_host`/ +`imap_port` (IMAP) at the provider's endpoints and set `secure`/`imap_secure` +to match the port (465/993 → `true`, 587/143 → `false`). + +## Development + +```bash +cd actions/mail-action +npm install +npm run typecheck +npm run build +npm run test +``` diff --git a/actions/smtp-action/package-lock.json b/actions/mail-action/package-lock.json similarity index 77% rename from actions/smtp-action/package-lock.json rename to actions/mail-action/package-lock.json index 788c560..4a560ba 100644 --- a/actions/smtp-action/package-lock.json +++ b/actions/mail-action/package-lock.json @@ -1,17 +1,20 @@ { - "name": "@code0-tech/smtp-action", + "name": "@code0-tech/mail-action", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "@code0-tech/smtp-action", + "name": "@code0-tech/mail-action", "version": "1.0.0", "dependencies": { "@code0-tech/hercules": "^1.6.0", + "imapflow": "^2.10.0", + "mailparser": "^3.14.1", "nodemailer": "^6.9.16" }, "devDependencies": { + "@types/mailparser": "^3.4.0", "@types/node": "^22.0.0", "@types/nodemailer": "^6.4.16", "tsx": "^4.0.0", @@ -23,7 +26,6 @@ "node_modules/@clack/core": { "version": "1.0.0-alpha.4", "license": "MIT", - "peer": true, "dependencies": { "picocolors": "^1.0.0", "sisteransi": "^1.0.5" @@ -32,7 +34,6 @@ "node_modules/@clack/prompts": { "version": "1.0.0-alpha.4", "license": "MIT", - "peer": true, "dependencies": { "@clack/core": "1.0.0-alpha.4", "picocolors": "^1.0.0", @@ -519,7 +520,6 @@ "node_modules/@grpc/proto-loader": { "version": "0.8.1", "license": "Apache-2.0", - "peer": true, "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", @@ -541,7 +541,6 @@ "node_modules/@js-sdsl/ordered-map": { "version": "4.4.2", "license": "MIT", - "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/js-sdsl" @@ -550,7 +549,6 @@ "node_modules/@oclif/core": { "version": "4.14.0", "license": "MIT", - "peer": true, "dependencies": { "ansi-escapes": "^4.3.2", "ansis": "^3.17.0", @@ -583,6 +581,12 @@ "url": "https://github.com/sponsors/oxc-project" } }, + "node_modules/@pinojs/redact": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", + "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", + "license": "MIT" + }, "node_modules/@protobuf-ts/grpc-backend": { "version": "2.11.1", "license": "Apache-2.0", @@ -622,51 +626,42 @@ }, "node_modules/@protobufjs/aspromise": { "version": "1.1.2", - "license": "BSD-3-Clause", - "peer": true + "license": "BSD-3-Clause" }, "node_modules/@protobufjs/base64": { "version": "1.1.2", - "license": "BSD-3-Clause", - "peer": true + "license": "BSD-3-Clause" }, "node_modules/@protobufjs/codegen": { "version": "2.0.5", - "license": "BSD-3-Clause", - "peer": true + "license": "BSD-3-Clause" }, "node_modules/@protobufjs/eventemitter": { "version": "1.1.1", - "license": "BSD-3-Clause", - "peer": true + "license": "BSD-3-Clause" }, "node_modules/@protobufjs/fetch": { "version": "1.1.1", "license": "BSD-3-Clause", - "peer": true, "dependencies": { "@protobufjs/aspromise": "^1.1.1" } }, "node_modules/@protobufjs/float": { "version": "1.0.2", - "license": "BSD-3-Clause", - "peer": true + "license": "BSD-3-Clause" }, "node_modules/@protobufjs/path": { "version": "1.1.2", - "license": "BSD-3-Clause", - "peer": true + "license": "BSD-3-Clause" }, "node_modules/@protobufjs/pool": { "version": "1.1.0", - "license": "BSD-3-Clause", - "peer": true + "license": "BSD-3-Clause" }, "node_modules/@protobufjs/utf8": { "version": "1.1.2", - "license": "BSD-3-Clause", - "peer": true + "license": "BSD-3-Clause" }, "node_modules/@rolldown/binding-android-arm-eabi": { "version": "1.2.11", @@ -776,9 +771,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -796,9 +788,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -816,9 +805,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -836,9 +822,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -856,9 +839,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -876,9 +856,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -944,6 +921,22 @@ "dev": true, "license": "MIT" }, + "node_modules/@selderee/plugin-htmlparser2": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.12.0.tgz", + "integrity": "sha512-oELmoyA6ML9jDRMV3kgcMQFKxUfBU0yFVn6yTctVaLT5ygXnxH52I3TZEgV9EhXJC68/uFvE5Daj1/25c0Xa/A==", + "license": "MIT", + "dependencies": { + "domelementtype": "~2.3.0", + "domhandler": "~5.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/KillyMXI" + }, + "peerDependencies": { + "selderee": "~0.12.0" + } + }, "node_modules/@standard-schema/spec": { "version": "1.1.0", "dev": true, @@ -952,7 +945,6 @@ "node_modules/@ts-morph/common": { "version": "0.29.0", "license": "MIT", - "peer": true, "dependencies": { "minimatch": "^10.0.1", "path-browserify": "^1.0.1", @@ -978,6 +970,17 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/mailparser": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/@types/mailparser/-/mailparser-3.9.0.tgz", + "integrity": "sha512-YnhrRiRVDiEAphFvd72hKa1n8y2b9Q3rgSQiONZQyhQz67h5g4MZHNApET3XWXVEFdInWtHqDHKEwnyjxU3QVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "iconv-lite": "^0.6.3" + } + }, "node_modules/@types/node": { "version": "22.20.4", "license": "MIT", @@ -996,7 +999,6 @@ "node_modules/@typescript/vfs": { "version": "1.6.5", "license": "MIT", - "peer": true, "dependencies": { "debug": "^4.4.3" }, @@ -1103,10 +1105,23 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/@zone-eu/mailsplit": { + "version": "5.4.29", + "resolved": "https://registry.npmjs.org/@zone-eu/mailsplit/-/mailsplit-5.4.29.tgz", + "integrity": "sha512-CCUNycmUjrV5M0wNf23waWAt9eOHczhjLbUQS7k0FNJvR0py9VSqHFt+tSl/D0+XI73VLG0U+PwYtq9R0zsm9Q==", + "license": "(MIT OR EUPL-1.1+)", + "dependencies": { + "libbase64": "1.4.1", + "libmime": "5.6.3", + "libqp": "3.0.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/ansi-escapes": { "version": "4.3.2", "license": "MIT", - "peer": true, "dependencies": { "type-fest": "^0.21.3" }, @@ -1120,7 +1135,6 @@ "node_modules/ansi-regex": { "version": "6.3.0", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -1131,7 +1145,6 @@ "node_modules/ansi-styles": { "version": "6.2.3", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -1142,7 +1155,6 @@ "node_modules/ansis": { "version": "3.17.0", "license": "ISC", - "peer": true, "engines": { "node": ">=14" } @@ -1155,10 +1167,18 @@ "node": ">=12" } }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/balanced-match": { "version": "4.0.4", "license": "MIT", - "peer": true, "engines": { "node": "18 || 20 || >=22" } @@ -1166,7 +1186,6 @@ "node_modules/brace-expansion": { "version": "5.0.12", "license": "MIT", - "peer": true, "dependencies": { "balanced-match": "^4.0.2" }, @@ -1185,7 +1204,6 @@ "node_modules/chokidar": { "version": "4.0.3", "license": "MIT", - "peer": true, "dependencies": { "readdirp": "^4.0.1" }, @@ -1199,7 +1217,6 @@ "node_modules/clean-stack": { "version": "3.0.1", "license": "MIT", - "peer": true, "dependencies": { "escape-string-regexp": "4.0.0" }, @@ -1213,7 +1230,6 @@ "node_modules/cli-cursor": { "version": "5.0.0", "license": "MIT", - "peer": true, "dependencies": { "restore-cursor": "^5.0.0" }, @@ -1227,7 +1243,6 @@ "node_modules/cli-spinners": { "version": "2.9.2", "license": "MIT", - "peer": true, "engines": { "node": ">=6" }, @@ -1238,7 +1253,6 @@ "node_modules/cli-truncate": { "version": "5.2.0", "license": "MIT", - "peer": true, "dependencies": { "slice-ansi": "^8.0.0", "string-width": "^8.2.0" @@ -1253,7 +1267,6 @@ "node_modules/cli-truncate/node_modules/string-width": { "version": "8.3.0", "license": "MIT", - "peer": true, "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" @@ -1268,7 +1281,6 @@ "node_modules/cliui": { "version": "8.0.1", "license": "ISC", - "peer": true, "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", @@ -1281,7 +1293,6 @@ "node_modules/cliui/node_modules/ansi-regex": { "version": "5.0.1", "license": "MIT", - "peer": true, "engines": { "node": ">=8" } @@ -1289,7 +1300,6 @@ "node_modules/cliui/node_modules/strip-ansi": { "version": "6.0.1", "license": "MIT", - "peer": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -1299,13 +1309,11 @@ }, "node_modules/code-block-writer": { "version": "13.0.3", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/color-convert": { "version": "2.0.1", "license": "MIT", - "peer": true, "dependencies": { "color-name": "~1.1.4" }, @@ -1315,13 +1323,11 @@ }, "node_modules/color-name": { "version": "1.1.4", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/colorette": { "version": "2.0.20", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/convert-source-map": { "version": "2.0.0", @@ -1331,7 +1337,6 @@ "node_modules/debug": { "version": "4.4.3", "license": "MIT", - "peer": true, "dependencies": { "ms": "^2.1.3" }, @@ -1344,6 +1349,25 @@ } } }, + "node_modules/deepmerge-ts": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-8.0.2.tgz", + "integrity": "sha512-uqbvqLUMrc6p0MO+WBRtTxY55hmyh94WRwI5a++PZe54X+bfVh59FSN7uWCBCW1CCVjzjnrwzfI8zidE2obMMw==", + "funding": [ + { + "type": "ko-fi", + "url": "https://ko-fi.com/rebeccastevens" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/deepmerge-ts" + } + ], + "license": "BSD-3-Clause", + "engines": { + "node": ">=16.9.0" + } + }, "node_modules/detect-libc": { "version": "2.1.2", "dev": true, @@ -1352,10 +1376,64 @@ "node": ">=8" } }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, "node_modules/ejs": { "version": "6.0.1", "license": "Apache-2.0", - "peer": true, "bin": { "ejs": "bin/cli.js" }, @@ -1365,13 +1443,32 @@ }, "node_modules/emoji-regex": { "version": "8.0.0", + "license": "MIT" + }, + "node_modules/encoding-japanese": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/encoding-japanese/-/encoding-japanese-2.4.0.tgz", + "integrity": "sha512-PE0KjlsDy9JSsoU4C2Q8NXb1kC4c+bZRac9ZvHXUE4q1Ht63gScKMdEy9jst9+Fi3M4eDkqzkEhBZvix4blP2A==", "license": "MIT", - "peer": true + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } }, "node_modules/environment": { "version": "1.1.0", "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -1427,7 +1524,6 @@ "node_modules/escalade": { "version": "3.2.0", "license": "MIT", - "peer": true, "engines": { "node": ">=6" } @@ -1435,7 +1531,6 @@ "node_modules/escape-string-regexp": { "version": "4.0.0", "license": "MIT", - "peer": true, "engines": { "node": ">=10" }, @@ -1453,8 +1548,7 @@ }, "node_modules/eventemitter3": { "version": "5.0.4", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/expect-type": { "version": "1.4.0", @@ -1494,7 +1588,6 @@ "node_modules/get-caller-file": { "version": "2.0.5", "license": "ISC", - "peer": true, "engines": { "node": "6.* || 8.* || >= 10.*" } @@ -1502,7 +1595,6 @@ "node_modules/get-east-asian-width": { "version": "1.7.0", "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -1513,7 +1605,6 @@ "node_modules/get-package-type": { "version": "0.1.0", "license": "MIT", - "peer": true, "engines": { "node": ">=8.0.0" } @@ -1521,23 +1612,136 @@ "node_modules/has-flag": { "version": "4.0.0", "license": "MIT", - "peer": true, "engines": { "node": ">=8" } }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/html-to-text": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/html-to-text/-/html-to-text-10.0.1.tgz", + "integrity": "sha512-GiVhRI1BatGARSCmlXWNCjDT0cWrwBWoeduLoV0WSKAgaV/wa+hUWy5LiQLUs4UwiUrE52ZCMfBGiKD87TDPrg==", + "license": "MIT", + "dependencies": { + "@selderee/plugin-htmlparser2": "~0.12.0", + "deepmerge-ts": "^8.0.1", + "dom-serializer": "^2.0.0", + "htmlparser2": "^10.1.0", + "selderee": "~0.12.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/KillyMXI" + } + }, + "node_modules/htmlparser2": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", + "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "entities": "^7.0.1" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/imapflow": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/imapflow/-/imapflow-2.10.0.tgz", + "integrity": "sha512-J4YH1eHwemE00xaCT2vn30s+81iJi+C4OHQEoO6/hptA/z75nGdP1isU3oDfMaiheHD240LIhMMJcMMCJ5RDYA==", + "license": "MIT", + "dependencies": { + "@zone-eu/mailsplit": "5.4.29", + "encoding-japanese": "2.4.0", + "iconv-lite": "0.7.3", + "libbase64": "1.4.1", + "libmime": "5.6.3", + "libqp": "3.0.1", + "pino": "10.3.1", + "socks": "2.8.10" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/imapflow/node_modules/iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/indent-string": { "version": "4.0.0", "license": "MIT", - "peer": true, "engines": { "node": ">=8" } }, + "node_modules/ip-address": { + "version": "10.7.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.5.tgz", + "integrity": "sha512-t2v0rkILYv9WXmSL7TpAaU3HXgaFYOPUcH3QHgwQ++gpJuH3bqyMy4Fvt/DJCTKwzH3Cf+bAp5W/mFL39KPLzA==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/is-docker": { "version": "3.0.0", "license": "MIT", - "peer": true, "bin": { "is-docker": "cli.js" }, @@ -1551,7 +1755,6 @@ "node_modules/is-fullwidth-code-point": { "version": "5.1.0", "license": "MIT", - "peer": true, "dependencies": { "get-east-asian-width": "^1.3.1" }, @@ -1565,7 +1768,6 @@ "node_modules/is-inside-container": { "version": "1.0.0", "license": "MIT", - "peer": true, "dependencies": { "is-docker": "^3.0.0" }, @@ -1582,7 +1784,6 @@ "node_modules/is-wsl": { "version": "3.1.1", "license": "MIT", - "peer": true, "dependencies": { "is-inside-container": "^1.0.0" }, @@ -1593,6 +1794,64 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/leac": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/leac/-/leac-0.7.0.tgz", + "integrity": "sha512-qMrZeyEekgdRQ9o6a4NAB2EQZrv827GJdn1vnapwSJ90hWRB4TzUSunvacPkxQ2TnNqHNI1/zSt0hlo0crG8Jw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/KillyMXI" + } + }, + "node_modules/libbase64": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/libbase64/-/libbase64-1.4.1.tgz", + "integrity": "sha512-Nyh3H90B3gRtrcxIe56Ayy7635J0jmh6AbxbQgQae8wrSPQMH5wFqJChiMTB13yKG512L3YspbA5DzumhFTITA==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/libmime": { + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/libmime/-/libmime-5.6.3.tgz", + "integrity": "sha512-cuTUW/Jybvh5O4/U9p61gyutHOjDtZyGQYuY99+bfnaFwqcDf2Nb+R6hMHY6DDen2H9wLKQSMpofWUgn2TI5JQ==", + "license": "MIT", + "dependencies": { + "encoding-japanese": "2.4.0", + "iconv-lite": "0.7.3", + "libbase64": "1.4.1", + "libqp": "3.0.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/libmime/node_modules/iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/libqp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/libqp/-/libqp-3.0.1.tgz", + "integrity": "sha512-7QZxpPX25pga1WiMueduWxmrHMVBFrZPH6ZUaozGYa/H98JH6NVBSET3qVw+4NxzoraIW7rJ81YxSU9IalvCpg==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/lightningcss": { "version": "1.33.0", "dev": true, @@ -1732,9 +1991,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -1756,9 +2012,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -1780,9 +2033,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -1804,9 +2054,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -1865,7 +2112,6 @@ "node_modules/lilconfig": { "version": "3.1.3", "license": "MIT", - "peer": true, "engines": { "node": ">=14" }, @@ -1873,10 +2119,28 @@ "url": "https://github.com/sponsors/antonk52" } }, + "node_modules/linkify-it": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.2.tgz", + "integrity": "sha512-ONTm2jCMAVZjgQa/Fy1kScXsuOoF5NPTsoFBdE1KVIZ2vAh/r9+Bqo+0jINCBYnavTPQZz38QzFTme79ENoN3Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, "node_modules/listr2": { "version": "9.0.5", "license": "MIT", - "peer": true, "dependencies": { "cli-truncate": "^5.0.0", "colorette": "^2.0.20", @@ -1891,13 +2155,11 @@ }, "node_modules/listr2/node_modules/emoji-regex": { "version": "10.6.0", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/listr2/node_modules/string-width": { "version": "7.2.0", "license": "MIT", - "peer": true, "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", @@ -1913,7 +2175,6 @@ "node_modules/listr2/node_modules/wrap-ansi": { "version": "9.0.2", "license": "MIT", - "peer": true, "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", @@ -1928,13 +2189,11 @@ }, "node_modules/lodash.camelcase": { "version": "4.3.0", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/log-update": { "version": "6.1.0", "license": "MIT", - "peer": true, "dependencies": { "ansi-escapes": "^7.0.0", "cli-cursor": "^5.0.0", @@ -1952,7 +2211,6 @@ "node_modules/log-update/node_modules/ansi-escapes": { "version": "7.3.0", "license": "MIT", - "peer": true, "dependencies": { "environment": "^1.0.0" }, @@ -1965,13 +2223,11 @@ }, "node_modules/log-update/node_modules/emoji-regex": { "version": "10.6.0", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/log-update/node_modules/slice-ansi": { "version": "7.1.2", "license": "MIT", - "peer": true, "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" @@ -1986,7 +2242,6 @@ "node_modules/log-update/node_modules/string-width": { "version": "7.2.0", "license": "MIT", - "peer": true, "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", @@ -2002,7 +2257,6 @@ "node_modules/log-update/node_modules/wrap-ansi": { "version": "9.0.2", "license": "MIT", - "peer": true, "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", @@ -2017,8 +2271,7 @@ }, "node_modules/long": { "version": "5.3.2", - "license": "Apache-2.0", - "peer": true + "license": "Apache-2.0" }, "node_modules/magic-string": { "version": "0.30.21", @@ -2028,10 +2281,37 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/mailparser": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/mailparser/-/mailparser-3.14.1.tgz", + "integrity": "sha512-Z0LLk7F0yhSCx3xjyL8BMm6dXO3vuWrSAeVhm1F18wVGVBbLAKOsVsCjPyOhJy+KuyDW9PRyDuyiPb7yJCHwVA==", + "license": "MIT", + "dependencies": { + "@zone-eu/mailsplit": "5.4.29", + "he": "1.2.0", + "html-to-text": "10.0.1", + "libmime": "5.6.3", + "linkify-it": "5.0.2", + "nodemailer": "10.3.1", + "punycode.js": "2.3.1", + "tlds": "1.261.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/mailparser/node_modules/nodemailer": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-10.3.1.tgz", + "integrity": "sha512-uF0AFsJC07Y1vdW7p580DKAnh7rAO+4UPSSB3mDCPNDufoiaOebV6JmrzpxB14OjEEEiBsNdYoM2Y+fN8M7BBA==", + "license": "MIT-0", + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/mimic-function": { "version": "5.0.1", "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -2042,7 +2322,6 @@ "node_modules/minimatch": { "version": "10.2.6", "license": "BlueOak-1.0.0", - "peer": true, "dependencies": { "brace-expansion": "^5.0.8" }, @@ -2055,8 +2334,7 @@ }, "node_modules/ms": { "version": "2.1.3", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/nanoid": { "version": "3.3.19", @@ -2094,10 +2372,18 @@ "node": ">=12.20.0" } }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/onetime": { "version": "7.0.0", "license": "MIT", - "peer": true, "dependencies": { "mimic-function": "^5.0.0" }, @@ -2108,16 +2394,37 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parseley": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/parseley/-/parseley-0.13.1.tgz", + "integrity": "sha512-uNBJZzmb60l6p6VWLTmevizNAGnE0xoSf1n0B4q3ntegDNzcS68NRCcBDZTcyXHxt2XhBChsCuqj4M+nChvE/A==", + "license": "MIT", + "dependencies": { + "leac": "^0.7.0", + "peberminta": "^0.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/KillyMXI" + } + }, "node_modules/path-browserify": { "version": "1.0.1", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/pathe": { "version": "2.0.3", "dev": true, "license": "MIT" }, + "node_modules/peberminta": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/peberminta/-/peberminta-0.10.0.tgz", + "integrity": "sha512-80B2AsU+I4Qdb0ZAPSfe9UwvGzwkM37IKIFEvdS3D/3Ndgv2bsuJ0bfG1+iEYO+l7Gfd4EUJmuRyq7efLgRMzQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/KillyMXI" + } + }, "node_modules/picocolors": { "version": "1.1.1", "license": "ISC" @@ -2125,6 +2432,7 @@ "node_modules/picomatch": { "version": "4.0.7", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -2132,6 +2440,43 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pino": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/pino/-/pino-10.3.1.tgz", + "integrity": "sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==", + "license": "MIT", + "dependencies": { + "@pinojs/redact": "^0.4.0", + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^3.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^4.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-3.0.0.tgz", + "integrity": "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==", + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-std-serializers": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", + "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", + "license": "MIT" + }, "node_modules/postcss": { "version": "8.5.28", "dev": true, @@ -2162,7 +2507,6 @@ "node_modules/powershell-utils": { "version": "0.1.0", "license": "MIT", - "peer": true, "engines": { "node": ">=20" }, @@ -2170,11 +2514,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/process-warning": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.1.1.tgz", + "integrity": "sha512-oCkNVj4OKnKEcocVBcDfiIugN1/cE6rgfT3nCtzeqZPA8+rIxMaQ0xXzIkGP8Gj2qUaP1UR7uhJ1VD8lUCpzCg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, "node_modules/protobufjs": { "version": "7.6.6", "hasInstallScript": true, "license": "BSD-3-Clause", - "peer": true, "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", @@ -2192,10 +2551,24 @@ "node": ">=12.0.0" } }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "license": "MIT" + }, "node_modules/readdirp": { "version": "4.1.2", "license": "MIT", - "peer": true, "engines": { "node": ">= 14.18.0" }, @@ -2204,6 +2577,15 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, "node_modules/reflect-metadata": { "version": "0.2.2", "license": "Apache-2.0", @@ -2212,7 +2594,6 @@ "node_modules/require-directory": { "version": "2.1.1", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -2220,7 +2601,6 @@ "node_modules/restore-cursor": { "version": "5.1.0", "license": "MIT", - "peer": true, "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" @@ -2234,8 +2614,7 @@ }, "node_modules/rfdc": { "version": "1.4.1", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/rolldown": { "version": "1.2.11", @@ -2269,10 +2648,37 @@ "@rolldown/binding-win32-x64-msvc": "1.2.11" } }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/selderee": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/selderee/-/selderee-0.12.0.tgz", + "integrity": "sha512-b1YMh3+DHZp59DLna3qVwQ5iOla/nrI6mLBNW02XxU77M3046Df6VLkoaJyFz20VsGIG5kkp+FK0kg4K4HnUFw==", + "license": "MIT", + "peer": true, + "dependencies": { + "parseley": "~0.13.1" + }, + "funding": { + "url": "https://github.com/sponsors/KillyMXI" + } + }, "node_modules/semver": { "version": "7.8.5", "license": "ISC", - "peer": true, "bin": { "semver": "bin/semver.js" }, @@ -2288,7 +2694,6 @@ "node_modules/signal-exit": { "version": "4.1.0", "license": "ISC", - "peer": true, "engines": { "node": ">=14" }, @@ -2298,13 +2703,11 @@ }, "node_modules/sisteransi": { "version": "1.0.5", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/slash": { "version": "5.1.0", "license": "MIT", - "peer": true, "engines": { "node": ">=14.16" }, @@ -2315,7 +2718,6 @@ "node_modules/slice-ansi": { "version": "8.0.0", "license": "MIT", - "peer": true, "dependencies": { "ansi-styles": "^6.2.3", "is-fullwidth-code-point": "^5.1.0" @@ -2327,6 +2729,39 @@ "url": "https://github.com/chalk/slice-ansi?sponsor=1" } }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.10", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.10.tgz", + "integrity": "sha512-e0VyvkVTwVYViNovRkZ9aodhxVlyoMn7eJhVUPxZ+eK9P/7CBkxvvsBOHqFPEH416726W8tLXXXjKwqgTErrCQ==", + "license": "MIT", + "dependencies": { + "ip-address": "^10.1.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/sonic-boom": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", + "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "dev": true, @@ -2335,6 +2770,15 @@ "node": ">=0.10.0" } }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, "node_modules/stackback": { "version": "0.0.2", "dev": true, @@ -2348,7 +2792,6 @@ "node_modules/string-width": { "version": "4.2.3", "license": "MIT", - "peer": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -2361,7 +2804,6 @@ "node_modules/string-width/node_modules/ansi-regex": { "version": "5.0.1", "license": "MIT", - "peer": true, "engines": { "node": ">=8" } @@ -2369,7 +2811,6 @@ "node_modules/string-width/node_modules/is-fullwidth-code-point": { "version": "3.0.0", "license": "MIT", - "peer": true, "engines": { "node": ">=8" } @@ -2377,7 +2818,6 @@ "node_modules/string-width/node_modules/strip-ansi": { "version": "6.0.1", "license": "MIT", - "peer": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -2388,7 +2828,6 @@ "node_modules/strip-ansi": { "version": "7.2.0", "license": "MIT", - "peer": true, "dependencies": { "ansi-regex": "^6.2.2" }, @@ -2402,7 +2841,6 @@ "node_modules/supports-color": { "version": "8.1.1", "license": "MIT", - "peer": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -2416,7 +2854,6 @@ "node_modules/text-camel-case": { "version": "1.2.11", "license": "MIT", - "peer": true, "dependencies": { "text-pascal-case": "^1.2.11" } @@ -2424,7 +2861,6 @@ "node_modules/text-capital-case": { "version": "1.2.11", "license": "MIT", - "peer": true, "dependencies": { "text-no-case": "^1.2.11", "text-upper-case-first": "^1.2.11" @@ -2433,7 +2869,6 @@ "node_modules/text-case": { "version": "1.2.11", "license": "MIT", - "peer": true, "dependencies": { "text-camel-case": "^1.2.11", "text-capital-case": "^1.2.11", @@ -2460,7 +2895,6 @@ "node_modules/text-constant-case": { "version": "1.2.11", "license": "MIT", - "peer": true, "dependencies": { "text-no-case": "^1.2.11", "text-upper-case": "^1.2.11" @@ -2469,7 +2903,6 @@ "node_modules/text-dot-case": { "version": "1.2.11", "license": "MIT", - "peer": true, "dependencies": { "text-no-case": "^1.2.11" } @@ -2477,43 +2910,36 @@ "node_modules/text-header-case": { "version": "1.2.11", "license": "MIT", - "peer": true, "dependencies": { "text-capital-case": "^1.2.11" } }, "node_modules/text-is-lower-case": { "version": "1.2.11", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/text-is-upper-case": { "version": "1.2.11", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/text-kebab-case": { "version": "1.2.11", "license": "MIT", - "peer": true, "dependencies": { "text-no-case": "^1.2.11" } }, "node_modules/text-lower-case": { "version": "1.2.11", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/text-lower-case-first": { "version": "1.2.11", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/text-no-case": { "version": "1.2.11", "license": "MIT", - "peer": true, "dependencies": { "text-lower-case": "^1.2.11" } @@ -2521,7 +2947,6 @@ "node_modules/text-param-case": { "version": "1.2.11", "license": "MIT", - "peer": true, "dependencies": { "text-dot-case": "^1.2.11" } @@ -2529,7 +2954,6 @@ "node_modules/text-pascal-case": { "version": "1.2.11", "license": "MIT", - "peer": true, "dependencies": { "text-no-case": "^1.2.11" } @@ -2537,7 +2961,6 @@ "node_modules/text-path-case": { "version": "1.2.11", "license": "MIT", - "peer": true, "dependencies": { "text-dot-case": "^1.2.11" } @@ -2545,7 +2968,6 @@ "node_modules/text-sentence-case": { "version": "1.2.11", "license": "MIT", - "peer": true, "dependencies": { "text-no-case": "^1.2.11", "text-upper-case-first": "^1.2.11" @@ -2554,20 +2976,17 @@ "node_modules/text-snake-case": { "version": "1.2.11", "license": "MIT", - "peer": true, "dependencies": { "text-dot-case": "^1.2.11" } }, "node_modules/text-swap-case": { "version": "1.2.11", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/text-title-case": { "version": "1.2.11", "license": "MIT", - "peer": true, "dependencies": { "text-no-case": "^1.2.11", "text-upper-case-first": "^1.2.11" @@ -2575,13 +2994,29 @@ }, "node_modules/text-upper-case": { "version": "1.2.11", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/text-upper-case-first": { "version": "1.2.11", + "license": "MIT" + }, + "node_modules/thread-stream": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.2.0.tgz", + "integrity": "sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ==", "license": "MIT", - "peer": true + "dependencies": { + "real-require": "^1.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/thread-stream/node_modules/real-require": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-1.0.0.tgz", + "integrity": "sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g==", + "license": "MIT" }, "node_modules/tinybench": { "version": "2.9.0", @@ -2618,6 +3053,15 @@ "node": ">=14.0.0" } }, + "node_modules/tlds": { + "version": "1.261.0", + "resolved": "https://registry.npmjs.org/tlds/-/tlds-1.261.0.tgz", + "integrity": "sha512-QXqwfEl9ddlGBaRFXIvNKK6OhipSiLXuRuLJX5DErz0o0Q0rYxulWLdFryTkV5PkdZct5iMInwYEGe/eR++1AA==", + "license": "MIT", + "bin": { + "tlds": "bin.js" + } + }, "node_modules/ts-morph": { "version": "28.0.0", "license": "MIT", @@ -2650,13 +3094,11 @@ }, "node_modules/tslib": { "version": "2.8.1", - "license": "0BSD", - "peer": true + "license": "0BSD" }, "node_modules/tsutils": { "version": "3.21.0", "license": "MIT", - "peer": true, "dependencies": { "tslib": "^1.8.1" }, @@ -2669,13 +3111,13 @@ }, "node_modules/tsutils/node_modules/tslib": { "version": "1.14.1", - "license": "0BSD", - "peer": true + "license": "0BSD" }, "node_modules/tsx": { "version": "4.23.15", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "~0.28.0" }, @@ -2692,7 +3134,6 @@ "node_modules/type-fest": { "version": "0.21.3", "license": "(MIT OR CC0-1.0)", - "peer": true, "engines": { "node": ">=10" }, @@ -2703,6 +3144,7 @@ "node_modules/typescript": { "version": "5.9.3", "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -2711,6 +3153,12 @@ "node": ">=14.17" } }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "license": "MIT" + }, "node_modules/undici-types": { "version": "6.21.0", "license": "MIT" @@ -2719,6 +3167,7 @@ "version": "8.3.1", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "lightningcss": "^1.33.0", "picomatch": "^4.0.7", @@ -2897,7 +3346,6 @@ "node_modules/widest-line": { "version": "3.1.0", "license": "MIT", - "peer": true, "dependencies": { "string-width": "^4.0.0" }, @@ -2907,13 +3355,11 @@ }, "node_modules/wordwrap": { "version": "1.0.0", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/wrap-ansi": { "version": "7.0.0", "license": "MIT", - "peer": true, "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -2929,7 +3375,6 @@ "node_modules/wrap-ansi/node_modules/ansi-regex": { "version": "5.0.1", "license": "MIT", - "peer": true, "engines": { "node": ">=8" } @@ -2937,7 +3382,6 @@ "node_modules/wrap-ansi/node_modules/ansi-styles": { "version": "4.3.0", "license": "MIT", - "peer": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -2951,7 +3395,6 @@ "node_modules/wrap-ansi/node_modules/strip-ansi": { "version": "6.0.1", "license": "MIT", - "peer": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -2962,7 +3405,6 @@ "node_modules/wsl-utils": { "version": "0.4.0", "license": "MIT", - "peer": true, "dependencies": { "is-wsl": "^3.1.0", "powershell-utils": "^0.1.0" @@ -2977,7 +3419,6 @@ "node_modules/y18n": { "version": "5.0.8", "license": "ISC", - "peer": true, "engines": { "node": ">=10" } @@ -2985,7 +3426,6 @@ "node_modules/yargs": { "version": "17.7.3", "license": "MIT", - "peer": true, "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", @@ -3002,7 +3442,6 @@ "node_modules/yargs-parser": { "version": "21.1.1", "license": "ISC", - "peer": true, "engines": { "node": ">=12" } diff --git a/actions/smtp-action/package.json b/actions/mail-action/package.json similarity index 81% rename from actions/smtp-action/package.json rename to actions/mail-action/package.json index 26d73d3..85240ee 100644 --- a/actions/smtp-action/package.json +++ b/actions/mail-action/package.json @@ -1,5 +1,5 @@ { - "name": "@code0-tech/smtp-action", + "name": "@code0-tech/mail-action", "version": "1.0.0", "private": true, "type": "module", @@ -13,9 +13,12 @@ }, "dependencies": { "@code0-tech/hercules": "^1.6.0", + "imapflow": "^2.10.0", + "mailparser": "^3.14.1", "nodemailer": "^6.9.16" }, "devDependencies": { + "@types/mailparser": "^3.4.0", "@types/node": "^22.0.0", "@types/nodemailer": "^6.4.16", "tsx": "^4.0.0", diff --git a/actions/smtp-action/src/data_types/smtpSendResult.ts b/actions/mail-action/src/data_types/mailSendResult.ts similarity index 78% rename from actions/smtp-action/src/data_types/smtpSendResult.ts rename to actions/mail-action/src/data_types/mailSendResult.ts index 021918c..d28e8a1 100644 --- a/actions/smtp-action/src/data_types/smtpSendResult.ts +++ b/actions/mail-action/src/data_types/mailSendResult.ts @@ -2,13 +2,13 @@ import { DisplayMessage, Identifier, Name, Schema } from "@code0-tech/hercules"; import { z } from "zod"; /** - * SMTP_SEND_RESULT is a normalized view of nodemailer's SentMessageInfo + * MAIL_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 SmtpSendResultSchema = z.object({ +export const MailSendResultSchema = 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."), @@ -16,10 +16,10 @@ export const SmtpSendResultSchema = 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 SmtpSendResult = z.infer; +export type MailSendResult = z.infer; -@Identifier("SMTP_SEND_RESULT") +@Identifier("MAIL_SEND_RESULT") @Name({ code: "en-US", content: "Email send result" }) @DisplayMessage({ code: "en-US", content: "Email send result" }) -@Schema(SmtpSendResultSchema) -export class SmtpSendResultDataType {} +@Schema(MailSendResultSchema) +export class MailSendResultDataType {} diff --git a/actions/mail-action/src/data_types/receivedMail.ts b/actions/mail-action/src/data_types/receivedMail.ts new file mode 100644 index 0000000..54ad41d --- /dev/null +++ b/actions/mail-action/src/data_types/receivedMail.ts @@ -0,0 +1,26 @@ +import { DisplayMessage, Identifier, Name, Schema } from "@code0-tech/hercules"; +import { z } from "zod"; + +/** + * RECEIVED_MAIL is a normalized view of an incoming message parsed with + * mailparser's `simpleParser` (see https://nodemailer.com/extras/mailparser/). + * It is the payload passed to flows triggered by the MAIL_RECEIVED event. + */ + +export const ReceivedMailSchema = z.object({ + messageId: z.string().describe("The Message-ID header of the received message."), + from: z.string().describe("The sender address."), + to: z.array(z.string()).describe("The recipient (To) addresses."), + cc: z.array(z.string()).describe("The carbon-copy (Cc) addresses."), + subject: z.string().describe("The subject line of the message."), + text: z.string().describe("The plain-text body of the message."), + html: z.string().describe("The HTML body of the message, empty string when none."), + date: z.string().describe("The message date as an ISO 8601 string, empty string when missing."), +}); +export type ReceivedMail = z.infer; + +@Identifier("RECEIVED_MAIL") +@Name({ code: "en-US", content: "Received mail" }) +@DisplayMessage({ code: "en-US", content: "Received mail" }) +@Schema(ReceivedMailSchema) +export class ReceivedMailDataType {} diff --git a/actions/mail-action/src/events/mailReceived.ts b/actions/mail-action/src/events/mailReceived.ts new file mode 100644 index 0000000..ddf59fb --- /dev/null +++ b/actions/mail-action/src/events/mailReceived.ts @@ -0,0 +1,43 @@ +import { + Alias, + Description, + DisplayIcon, + DisplayMessage, + EventSetting, + Identifier, + Name, + RuntimeEventRunnable, + Signature, +} from "@code0-tech/hercules"; + +/** + * The `MAIL_RECEIVED` flow trigger. + * + * Registering this RuntimeEvent is what lets users attach a flow to incoming + * mail in the platform UI. The action itself watches the configured IMAP + * mailbox (see `imapListener.ts`) and drives execution of matching flows via + * `action.executeFlow`. + */ +@Identifier("MAIL_RECEIVED") +@DisplayIcon("tabler:mail-down") +@Signature("(addresses: TEXT[]): RECEIVED_MAIL") +@Name({ code: "en-US", content: "Mail received" }) +@Description({ + code: "en-US", + content: "Triggered when a new mail arrives on the configured IMAP server for one of the given recipient addresses.", +}) +@DisplayMessage({ code: "en-US", content: "Mail received for ${addresses}" }) +@Alias({ code: "en-US", content: "mail;email;imap;receive;inbox;trigger" }) +@EventSetting({ + identifier: "addresses", + name: [{ code: "en-US", content: "Mail addresses" }], + description: [ + { + code: "en-US", + content: "Incoming mail whose recipient (To/Cc) matches one of these addresses triggers this flow. Leave empty to match all incoming mail.", + }, + ], + optional: true, + linkedDataTypeIdentifiers: ["TEXT"], +}) +export class MailReceivedEvent implements RuntimeEventRunnable {} diff --git a/actions/smtp-action/src/functions/sendEmailFunction.ts b/actions/mail-action/src/functions/sendEmailFunction.ts similarity index 96% rename from actions/smtp-action/src/functions/sendEmailFunction.ts rename to actions/mail-action/src/functions/sendEmailFunction.ts index 3fb0b41..fb0cfa9 100644 --- a/actions/smtp-action/src/functions/sendEmailFunction.ts +++ b/actions/mail-action/src/functions/sendEmailFunction.ts @@ -11,11 +11,11 @@ import { Signature, } from "@code0-tech/hercules"; import { sendEmail } from "../helpers.js"; -import { SmtpSendResult } from "../data_types/smtpSendResult.js"; +import { MailSendResult } from "../data_types/mailSendResult.js"; @Identifier("sendEmail") @DisplayIcon("tabler:mail") -@Signature("(Recipients: string, Subject: string, Text: string, Html?: string, From?: string, CarbonCopy?: string, Bcc?: string, ReplyTo?: string): SMTP_SEND_RESULT") +@Signature("(Recipients: string, Subject: string, Text: string, Html?: string, From?: string, CarbonCopy?: string, Bcc?: string, ReplyTo?: string): MAIL_SEND_RESULT") @Name({ code: "en-US", content: "Send email" }) @DisplayMessage({ code: "en-US", content: "Send email to ${Recipients}" }) @Documentation({ @@ -83,7 +83,7 @@ export class SendEmailFunction { CarbonCopy?: string, Bcc?: string, ReplyTo?: string - ): Promise { + ): Promise { try { return await sendEmail({ To:Recipients, Subject, Text, Html, From, Cc: CarbonCopy, Bcc, ReplyTo }, context); } catch (error) { diff --git a/actions/smtp-action/src/functions/sendEmailWithAttachmentsFunction.ts b/actions/mail-action/src/functions/sendEmailWithAttachmentsFunction.ts similarity index 96% rename from actions/smtp-action/src/functions/sendEmailWithAttachmentsFunction.ts rename to actions/mail-action/src/functions/sendEmailWithAttachmentsFunction.ts index 3758f81..346884d 100644 --- a/actions/smtp-action/src/functions/sendEmailWithAttachmentsFunction.ts +++ b/actions/mail-action/src/functions/sendEmailWithAttachmentsFunction.ts @@ -12,11 +12,11 @@ import { File, } from "@code0-tech/hercules"; import { sendEmail } from "../helpers.js"; -import { SmtpSendResult } from "../data_types/smtpSendResult.js"; +import { MailSendResult } from "../data_types/mailSendResult.js"; @Identifier("sendEmailWithAttachments") @DisplayIcon("tabler:mail") -@Signature("(Recipients: string, Subject: string, Text: string, Attachments: FILE[], Html?: string, From?: string, CarbonCopy?: string, Bcc?: string): SMTP_SEND_RESULT") +@Signature("(Recipients: string, Subject: string, Text: string, Attachments: FILE[], Html?: string, From?: string, CarbonCopy?: string, Bcc?: string): MAIL_SEND_RESULT") @Name({ code: "en-US", content: "Send email with attachments" }) @DisplayMessage({ code: "en-US", content: "Send email with attachments to ${Recipients}" }) @Documentation({ @@ -83,7 +83,7 @@ export class SendEmailWithAttachmentsFunction { From?: string, CarbonCopy?: string, Bcc?: string - ): Promise { + ): Promise { try { return await sendEmail( { To:Recipients, Subject, Text, Html, From, Cc:CarbonCopy, Bcc, Attachments: Attachments ?? [] }, diff --git a/actions/smtp-action/src/helpers.ts b/actions/mail-action/src/helpers.ts similarity index 95% rename from actions/smtp-action/src/helpers.ts rename to actions/mail-action/src/helpers.ts index a0090ec..d6eef4f 100644 --- a/actions/smtp-action/src/helpers.ts +++ b/actions/mail-action/src/helpers.ts @@ -1,6 +1,6 @@ import { FunctionContext, RuntimeError, File } from "@code0-tech/hercules"; import nodemailer, { type Transporter } from "nodemailer"; -import { SmtpSendResult, SmtpSendResultSchema } from "./data_types/smtpSendResult.js"; +import { MailSendResult, MailSendResultSchema } from "./data_types/mailSendResult.js"; /** * Splits a comma-separated address string into a trimmed, non-empty array. @@ -74,7 +74,7 @@ function normalizeAddress(address: unknown): string { /** * Sends an email through the configured SMTP server using nodemailer and maps - * the result onto the SMTP_SEND_RESULT data type. + * the result onto the MAIL_SEND_RESULT data type. */ export const sendEmail = async ( data:{ @@ -89,7 +89,7 @@ export const sendEmail = async ( ReplyTo?: string }, context: FunctionContext -): Promise => { +): Promise => { const transport = getTransport(context); const from = resolveFrom(context, data.From); @@ -121,7 +121,7 @@ export const sendEmail = async ( const envelope = (info.envelope ?? {}) as { from?: unknown; to?: unknown }; - return SmtpSendResultSchema.parse({ + return MailSendResultSchema.parse({ messageId: info.messageId ?? "", accepted: (info.accepted ?? []).map(normalizeAddress), rejected: (info.rejected ?? []).map(normalizeAddress), diff --git a/actions/mail-action/src/imapListener.ts b/actions/mail-action/src/imapListener.ts new file mode 100644 index 0000000..de64c6d --- /dev/null +++ b/actions/mail-action/src/imapListener.ts @@ -0,0 +1,316 @@ +import { Action, CodeZeroEvent } from "@code0-tech/hercules"; +import { toAllowedValue } from "@code0-tech/tucana/helpers"; +import { ImapFlow } from "imapflow"; +import { simpleParser } from "mailparser"; +import type { AddressObject } from "mailparser"; + +const RECONNECT_DELAY_MS = 5000; + +interface MailConnection { + stop: () => Promise; +} + +interface ProjectImapConfig { + host: string; + port: string; + secure: string; + username: string; + password: string; + mailbox: string; +} + +interface ReceivedMailPayload { + messageId: string; + from: string; + to: string[]; + cc: string[]; + subject: string; + text: string; + html: string; + date: string; +} + +/** Live IMAP connections, keyed by project id (as a string). */ +const connections = new Map(); + +let reconciling = false; +let reconcileQueued = false; + +/** + * Returns true when `recipients` should trigger a flow configured with + * `configured` addresses. An empty `configured` list matches everything. + * Matching is an exact, case-insensitive comparison of email addresses. + */ +export function matchesAddresses(recipients: string[], configured: string[]): boolean { + if (configured.length === 0) return true; + const normalizedRecipients = new Set(recipients.map((address) => address.trim().toLowerCase())); + return configured.some((address) => normalizedRecipients.has(address.trim().toLowerCase())); +} + +let started = false; + +/** + * Starts the reconciling IMAP connection manager: one IMAP connection per + * project that has at least one flow bound to the MAIL_RECEIVED trigger. + * Connections are opened/closed as flows are created/updated/deleted. + * + * Safe to call more than once (e.g. once per `connected` event after a + * reconnect) — only the first call wires up the listeners. + */ +export function startMailListener(action: Action): void { + if (started) return; + started = true; + + scheduleReconcile(action); + + action.on(CodeZeroEvent.flowUpdated, () => scheduleReconcile(action)); + action.on(CodeZeroEvent.flowDeleted, () => scheduleReconcile(action)); + action.on(CodeZeroEvent.moduleUpdated, () => scheduleReconcile(action)); +} + +/** + * Queues a reconcile pass, folding concurrent requests into a single + * follow-up run so overlapping flow/module update events can't race. + */ +function scheduleReconcile(action: Action): void { + if (reconciling) { + reconcileQueued = true; + return; + } + + reconciling = true; + reconcileConnections(action) + .catch((error: unknown) => { + console.error("Failed to reconcile IMAP listeners:", error); + }) + .finally(() => { + reconciling = false; + if (reconcileQueued) { + reconcileQueued = false; + scheduleReconcile(action); + } + }); +} + +/** + * Computes the set of projects that currently need an IMAP connection (i.e. + * have a MAIL_RECEIVED flow), opens connections for the ones missing one, and + * closes connections for projects that no longer need one. + */ +async function reconcileConnections(action: Action): Promise { + const activeProjectIds = new Set(); + for (const flow of action.flows.values()) { + if (flow.type === "MAIL_RECEIVED") { + activeProjectIds.add(flow.projectId.toString()); + } + } + + for (const [projectIdKey, connection] of connections) { + if (activeProjectIds.has(projectIdKey)) continue; + connections.delete(projectIdKey); + try { + await connection.stop(); + } catch (error) { + console.error(`Failed to stop the IMAP connection for project ${projectIdKey}:`, error); + } + } + + for (const projectIdKey of activeProjectIds) { + if (connections.has(projectIdKey)) continue; + + const projectId = BigInt(projectIdKey); + const config = action.configs.get(projectId); + if (!config) { + console.warn(`Project ${projectIdKey} has MAIL_RECEIVED flow(s) but no configuration yet; skipping for now.`); + continue; + } + + const host = config.findConfig("imap_host") as string | undefined; + if (!host) { + console.warn(`Project ${projectIdKey} has MAIL_RECEIVED flow(s) but no imap_host configured; skipping.`); + continue; + } + + const imapConfig: ProjectImapConfig = { + host, + port: (config.findConfig("imap_port") as string | undefined) ?? "993", + secure: (config.findConfig("imap_secure") as string | undefined) ?? "true", + username: (config.findConfig("imap_username") as string | undefined) ?? "", + password: (config.findConfig("imap_password") as string | undefined) ?? "", + mailbox: (config.findConfig("imap_mailbox") as string | undefined) ?? "INBOX", + }; + + // Reserve the slot immediately so a second reconcile pass triggered + // while this connection is still being established doesn't try to + // open a duplicate one. + connections.set(projectIdKey, { stop: async () => {} }); + + connectProject(action, projectId, imapConfig) + .then((connection) => { + connections.set(projectIdKey, connection); + }) + .catch((error: unknown) => { + connections.delete(projectIdKey); + console.error(`Failed to open the IMAP connection for project ${projectIdKey}:`, error); + setTimeout(() => scheduleReconcile(action), RECONNECT_DELAY_MS); + }); + } +} + +/** + * Opens an IMAP connection for a single project, holds a lock on its + * configured mailbox so the client idles and reports new mail, and wires up + * reconnect-on-error/close behaviour mirroring the action's own Aquila + * connection handling. + */ +async function connectProject(action: Action, projectId: bigint, config: ProjectImapConfig): Promise { + const client = new ImapFlow({ + host: config.host, + port: Number(config.port) || 993, + secure: String(config.secure).toLowerCase() === "true", + auth: { user: config.username, pass: config.password }, + logger: false, + }); + + let stopped = false; + let highWatermark = 0; + + const scheduleReconnect = (): void => { + if (stopped) return; + stopped = true; + connections.delete(projectId.toString()); + client.close(); + console.log(`Reconnecting IMAP for project ${projectId} in ${RECONNECT_DELAY_MS}ms...`); + setTimeout(() => scheduleReconcile(action), RECONNECT_DELAY_MS); + }; + + client.on("error", (error: Error) => { + console.error(`IMAP connection error for project ${projectId}:`, error.message); + scheduleReconnect(); + }); + + client.on("close", () => { + if (!stopped) { + console.log(`IMAP connection for project ${projectId} closed unexpectedly.`); + scheduleReconnect(); + } + }); + + // Serialize processing so overlapping `exists` events can't both run against + // the same stale high-watermark and dispatch a message twice. + let processing: Promise = Promise.resolve(); + client.on("exists", () => { + processing = processing + .then(() => processNewMessages(action, projectId, client, highWatermark)) + .then((newHighWatermark) => { + highWatermark = newHighWatermark; + }) + .catch((error: unknown) => { + console.error(`Failed to process new mail for project ${projectId}:`, error); + }); + }); + + await client.connect(); + // Open the mailbox without holding a lock: a held lock keeps the connection + // "busy", which suppresses ImapFlow's automatic IDLE and the `exists` events + // we rely on to detect new mail. + const mailbox = await client.mailboxOpen(config.mailbox); + highWatermark = mailbox.uidNext - 1; + + console.log(`Watching IMAP mailbox "${config.mailbox}" for project ${projectId} (project config: ${config.host}).`); + + const stop = async (): Promise => { + stopped = true; + try { + await client.logout(); + } catch { + client.close(); + } + }; + + return { stop }; +} + +/** + * Fetches every message with a UID above `highWatermark`, dispatches each to + * the matching MAIL_RECEIVED flows, and returns the new high-watermark. + */ +async function processNewMessages(action: Action, projectId: bigint, client: ImapFlow, highWatermark: number): Promise { + let newHighWatermark = highWatermark; + + for await (const message of client.fetch(`${highWatermark + 1}:*`, { uid: true, source: true }, { uid: true })) { + if (message.uid <= highWatermark) continue; + newHighWatermark = Math.max(newHighWatermark, message.uid); + + try { + await dispatchMessage(action, projectId, message.source); + } catch (error) { + console.error(`Failed to dispatch received mail (UID ${message.uid}) for project ${projectId}:`, error); + } + } + + return newHighWatermark; +} + +/** + * Parses a raw message and executes every MAIL_RECEIVED flow of the given + * project whose configured addresses match one of the message's recipients. + */ +async function dispatchMessage(action: Action, projectId: bigint, source: Buffer | undefined): Promise { + if (!source) return; + + const parsed = await simpleParser(source); + + const to = extractAddresses(parsed.to); + const cc = extractAddresses(parsed.cc); + const recipients = [...to, ...cc]; + + const payload: ReceivedMailPayload = { + messageId: parsed.messageId ?? "", + from: parsed.from?.value?.[0]?.address ?? parsed.from?.text ?? "", + to, + cc, + subject: parsed.subject ?? "", + text: parsed.text ?? "", + html: typeof parsed.html === "string" ? parsed.html : "", + date: parsed.date ? parsed.date.toISOString() : "", + }; + + for (const flow of action.flows.values()) { + if (flow.type !== "MAIL_RECEIVED") continue; + if (flow.projectId !== projectId) continue; + + const addressesSetting = flow.settings.find((setting) => setting.flowSettingId === "addresses"); + const configuredAddresses = toStringArray(addressesSetting?.value ? toAllowedValue(addressesSetting.value) : undefined); + + if (!matchesAddresses(recipients, configuredAddresses)) continue; + + try { + await action.executeFlow(flow.flowId, payload); + } catch (error) { + console.error(`Failed to execute flow ${flow.flowId} for received mail:`, error); + } + } +} + +/** Flattens a mailparser address header field down to a list of addresses. */ +function extractAddresses(field: AddressObject | AddressObject[] | undefined): string[] { + if (!field) return []; + const objects = Array.isArray(field) ? field : [field]; + return objects.flatMap((object) => + (object.value ?? []) + .map((entry) => entry.address) + .filter((address): address is string => Boolean(address)) + ); +} + +/** Coerces a PlainValue (from a flow's `addresses` setting) into a string array. */ +function toStringArray(value: unknown): string[] { + if (Array.isArray(value)) { + return value.filter((entry): entry is string => typeof entry === "string"); + } + if (typeof value === "string" && value.length > 0) { + return [value]; + } + return []; +} diff --git a/actions/smtp-action/src/index.ts b/actions/mail-action/src/index.ts similarity index 53% rename from actions/smtp-action/src/index.ts rename to actions/mail-action/src/index.ts index e76f7b1..64a7fc2 100644 --- a/actions/smtp-action/src/index.ts +++ b/actions/mail-action/src/index.ts @@ -1,17 +1,20 @@ import "reflect-metadata"; import { Action, CodeZeroEvent } from "@code0-tech/hercules"; -import { SmtpSendResultDataType } from "./data_types/smtpSendResult.js"; +import { MailSendResultDataType } from "./data_types/mailSendResult.js"; +import { ReceivedMailDataType } from "./data_types/receivedMail.js"; import { SendEmailFunction } from "./functions/sendEmailFunction.js"; import { SendEmailWithAttachmentsFunction } from "./functions/sendEmailWithAttachmentsFunction.js"; +import { MailReceivedEvent } from "./events/mailReceived.js"; +import { startMailListener } from "./imapListener.js"; const action = new Action( - process.env.ACTION_ID ?? "smtp-action", + process.env.ACTION_ID ?? "mail-action", process.env.VERSION ?? "1.0.0", process.env.AQUILA_URL ?? "127.0.0.1:8081", "code0-tech", "tabler:mail", - "SMTP integration: send transactional and notification emails (with attachments) through any SMTP server via nodemailer.", + "Mail integration: send transactional and notification emails (with attachments) through any SMTP server via nodemailer, and trigger flows on mail received through an IMAP mailbox.", [{ code: "en-US", content: "Email" }], [ { @@ -61,29 +64,80 @@ const action = new Action( description: [{ code: "en-US", content: "The default From address used when an email does not specify a From value, e.g. \"Acme \"." }], linkedDataTypes: ["TEXT"], }, + { + identifier: "imap_host", + type: "TEXT", + name: [{ code: "en-US", content: "IMAP host" }], + description: [{ code: "en-US", content: "The hostname of the IMAP server to watch for incoming mail, e.g. imap.example.com." }], + linkedDataTypes: ["TEXT"], + }, + { + identifier: "imap_port", + type: "TEXT", + defaultValue: "993", + name: [{ code: "en-US", content: "IMAP port" }], + description: [{ code: "en-US", content: "The port of the IMAP server. Defaults to 993 (implicit TLS)." }], + linkedDataTypes: ["TEXT"], + }, + { + identifier: "imap_secure", + type: "TEXT", + defaultValue: "true", + name: [{ code: "en-US", content: "Use implicit TLS" }], + description: [{ code: "en-US", content: "Set to \"true\" to connect using implicit TLS (typically port 993). Use \"false\" for an unencrypted or STARTTLS connection." }], + linkedDataTypes: ["TEXT"], + }, + { + identifier: "imap_username", + type: "TEXT", + defaultValue: "", + name: [{ code: "en-US", content: "IMAP username" }], + description: [{ code: "en-US", content: "The username used to authenticate with the IMAP server." }], + linkedDataTypes: ["TEXT"], + }, + { + identifier: "imap_password", + type: "TEXT", + defaultValue: "", + name: [{ code: "en-US", content: "IMAP password" }], + description: [{ code: "en-US", content: "The password used to authenticate with the IMAP server." }], + linkedDataTypes: ["TEXT"], + }, + { + identifier: "imap_mailbox", + type: "TEXT", + defaultValue: "INBOX", + name: [{ code: "en-US", content: "Mailbox" }], + description: [{ code: "en-US", content: "The mailbox folder to watch for incoming mail." }], + linkedDataTypes: ["TEXT"], + }, ] ); -action.registerDataTypeClass(SmtpSendResultDataType); +action.registerDataTypeClass(MailSendResultDataType); +action.registerDataTypeClass(ReceivedMailDataType); action.registerRuntimeFunction(SendEmailFunction); action.registerRuntimeFunction(SendEmailWithAttachmentsFunction); +action.registerRuntimeEventClass(MailReceivedEvent); + action.on(CodeZeroEvent.connected, () => { console.log("Connected to aquila"); + startMailListener(action); }); action.on(CodeZeroEvent.error, (error: Error) => { console.error("Stream error:", error.message); console.log("Attempting to reconnect in 5s..."); setTimeout(() => { - action.connect(process.env.AUTH_TOKEN ?? "smtp-action").catch((err: Error) => { + action.connect(process.env.AUTH_TOKEN ?? "mail-action").catch((err: Error) => { action.emit(CodeZeroEvent.error, err); }); }, 5000); }); -action.connect(process.env.AUTH_TOKEN ?? "smtp-action").catch((err: Error) => { +action.connect(process.env.AUTH_TOKEN ?? "mail-action").catch((err: Error) => { action.emit(CodeZeroEvent.error, err); }); diff --git a/actions/mail-action/test/index.test.ts b/actions/mail-action/test/index.test.ts new file mode 100644 index 0000000..0d0cc1b --- /dev/null +++ b/actions/mail-action/test/index.test.ts @@ -0,0 +1,59 @@ +import "reflect-metadata"; +import { describe, expect, it } from "vitest"; +import { MailSendResultSchema } from "../src/data_types/mailSendResult.js"; +import { ReceivedMailSchema } from "../src/data_types/receivedMail.js"; +import { matchesAddresses } from "../src/imapListener.js"; + +describe("MailSendResultSchema", () => { + it("parses a representative send result", () => { + const result = { + messageId: "", + accepted: ["a@example.com"], + rejected: [], + response: "250 2.0.0 OK", + from: "no-reply@example.com", + to: ["a@example.com"], + }; + const parsed = MailSendResultSchema.parse(result); + expect(parsed.messageId).toEqual(result.messageId); + expect(parsed.accepted).toEqual(["a@example.com"]); + expect(parsed.from).toEqual("no-reply@example.com"); + expect(parsed.to).toEqual(["a@example.com"]); + }); +}); + +describe("ReceivedMailSchema", () => { + it("parses a representative received mail", () => { + const result = { + messageId: "", + from: "sender@example.com", + to: ["recipient@example.com"], + cc: ["other@example.com"], + subject: "Hello", + text: "Hello there", + html: "

Hello there

", + date: "2026-01-01T00:00:00.000Z", + }; + const parsed = ReceivedMailSchema.parse(result); + expect(parsed.messageId).toEqual(result.messageId); + expect(parsed.from).toEqual("sender@example.com"); + expect(parsed.to).toEqual(["recipient@example.com"]); + expect(parsed.cc).toEqual(["other@example.com"]); + expect(parsed.subject).toEqual("Hello"); + expect(parsed.html).toEqual("

Hello there

"); + }); +}); + +describe("matchesAddresses", () => { + it("matches everything when no addresses are configured", () => { + expect(matchesAddresses(["someone@example.com"], [])).toBe(true); + }); + + it("matches a configured address case-insensitively", () => { + expect(matchesAddresses(["Someone@Example.com"], ["someone@example.com"])).toBe(true); + }); + + it("does not match when no recipient is configured", () => { + expect(matchesAddresses(["someone@example.com"], ["other@example.com"])).toBe(false); + }); +}); diff --git a/actions/smtp-action/tsconfig.json b/actions/mail-action/tsconfig.json similarity index 100% rename from actions/smtp-action/tsconfig.json rename to actions/mail-action/tsconfig.json diff --git a/actions/smtp-action/vite.config.ts b/actions/mail-action/vite.config.ts similarity index 100% rename from actions/smtp-action/vite.config.ts rename to actions/mail-action/vite.config.ts diff --git a/actions/smtp-action/README.md b/actions/smtp-action/README.md deleted file mode 100644 index da7e277..0000000 --- a/actions/smtp-action/README.md +++ /dev/null @@ -1,61 +0,0 @@ -# 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 `. | - -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 -``` diff --git a/actions/smtp-action/test/index.test.ts b/actions/smtp-action/test/index.test.ts deleted file mode 100644 index 775365b..0000000 --- a/actions/smtp-action/test/index.test.ts +++ /dev/null @@ -1,21 +0,0 @@ -import "reflect-metadata"; -import { describe, expect, it } from "vitest"; -import { SmtpSendResultSchema } from "../src/data_types/smtpSendResult.js"; - -describe("SmtpSendResultSchema", () => { - it("parses a representative send result", () => { - const result = { - messageId: "", - accepted: ["a@example.com"], - rejected: [], - response: "250 2.0.0 OK", - from: "no-reply@example.com", - to: ["a@example.com"], - }; - const parsed = SmtpSendResultSchema.parse(result); - expect(parsed.messageId).toEqual(result.messageId); - expect(parsed.accepted).toEqual(["a@example.com"]); - expect(parsed.from).toEqual("no-reply@example.com"); - expect(parsed.to).toEqual(["a@example.com"]); - }); -});