9 Commits
v10 ... master

Author SHA1 Message Date
Dawid Dziurla
4758fd2f0e node_modules: update (#276)
Co-authored-by: dawidd6 <9713907+dawidd6@users.noreply.github.com>
2026-03-14 19:11:45 +01:00
dependabot[bot]
512815fe9c build(deps): bump undici from 6.23.0 to 6.24.1 (#275)
Bumps [undici](https://github.com/nodejs/undici) from 6.23.0 to 6.24.1.
- [Release notes](https://github.com/nodejs/undici/releases)
- [Commits](https://github.com/nodejs/undici/compare/v6.23.0...v6.24.1)

---
updated-dependencies:
- dependency-name: undici
  dependency-version: 6.24.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-14 19:10:39 +01:00
Fritz Elfert
4ca48c76b4 Fix 234 (#271)
* Fix envelope handling

* Updated documentation

* Update main.js

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update README.md

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Implemented review suggestions

* Implemented review suggestions

* Implemented review suggestions

* Use nodemailer's addressparser instead of regular expressions

* Updated README regarding address formats

* Updated README regarding address formats

* Update README.md

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Use addressparser regardless of envelopeX ist set or not.

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-13 10:51:21 +01:00
Fritz Elfert
e9d44227e5 Upgrade to node24 (#273) 2026-03-11 09:04:18 +01:00
Dawid Dziurla
949ec3d78a node_modules: update (#270)
Co-authored-by: dawidd6 <9713907+dawidd6@users.noreply.github.com>
2026-03-10 10:09:25 +01:00
dependabot[bot]
f0e039bce4 build(deps): bump nodemailer from 8.0.1 to 8.0.2 (#269)
Bumps [nodemailer](https://github.com/nodemailer/nodemailer) from 8.0.1 to 8.0.2.
- [Release notes](https://github.com/nodemailer/nodemailer/releases)
- [Changelog](https://github.com/nodemailer/nodemailer/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodemailer/nodemailer/compare/v8.0.1...v8.0.2)

---
updated-dependencies:
- dependency-name: nodemailer
  dependency-version: 8.0.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-10 10:08:28 +01:00
Fritz Elfert
41125078ce Misc. fixes (#267)
* Misc. fixes

- Do not expose 'username' to a potential recipient.
- Make 'from' mandatory, because there is no fallback anymore.
- Validate 'from' to make shure, it contains an email address.
- Avoid namespace pollution: Use let instead of var.

* Suggested changes from codereview
2026-03-10 10:08:10 +01:00
dependabot[bot]
e035119249 build(deps): bump minimatch from 3.1.3 to 3.1.5 (#265)
Bumps [minimatch](https://github.com/isaacs/minimatch) from 3.1.3 to 3.1.5.
- [Changelog](https://github.com/isaacs/minimatch/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/minimatch/compare/v3.1.3...v3.1.5)

---
updated-dependencies:
- dependency-name: minimatch
  dependency-version: 3.1.5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-27 13:42:35 +01:00
Spiros Kalogeropoulos
ba302ba66e Add support for custom email headers via JSON input (#264)
* Adding custom header with JSON support

* Adding headers in test workflow

---------

Co-authored-by: W506810_wexinc <spiros.kalogeropoulos@wexinc.com>
2026-02-27 13:38:27 +01:00
23 changed files with 476 additions and 190 deletions

View File

@@ -46,6 +46,7 @@ jobs:
convert_markdown: true convert_markdown: true
attachments: package.json,action.yml attachments: package.json,action.yml
priority: high priority: high
headers: '{"X-My-Test-Header": "Passed"}'
- name: Get mail - name: Get mail
run: | run: |

View File

@@ -27,51 +27,86 @@ Some features:
# * smtp://user:password@server:port # * smtp://user:password@server:port
# * smtp+starttls://user:password@server:port # * smtp+starttls://user:password@server:port
connection_url: ${{secrets.MAIL_CONNECTION}} connection_url: ${{secrets.MAIL_CONNECTION}}
# Required mail server address if not connection_url: # Required mail server address if not connection_url:
server_address: smtp.gmail.com server_address: smtp.gmail.com
# Server port, default 25: # Server port, default 25:
server_port: 465 server_port: 465
# Optional whether this connection use TLS (default is true if server_port is 465) # Optional whether this connection use TLS (default is true if server_port is 465)
secure: true secure: true
# Optional (recommended) mail server username: # Optional (recommended) mail server username:
username: ${{secrets.MAIL_USERNAME}} username: ${{secrets.MAIL_USERNAME}}
# Optional (recommended) mail server password: # Optional (recommended) mail server password:
password: ${{secrets.MAIL_PASSWORD}} password: ${{secrets.MAIL_PASSWORD}}
# Required mail subject: # Required mail subject:
subject: Github Actions job result subject: Github Actions job result
# Required recipients' addresses:
to: obiwan@example.com,yoda@example.com # Optional recipients. Separate multiple addresses by a comma (possibly surrounded by whitespace):
# Required sender full name (address can be skipped): to: obiwan@example.com, yoda@example.com
from: Luke Skywalker # <user@example.com>
# Required sender (supported formats: see "Supported address formats" below)
from: 'Luke Skywalker <user@example.com>'
# Optional plain body: # Optional plain body:
body: Build job of ${{github.repository}} completed successfully! body: Build job of ${{github.repository}} completed successfully!
# Optional HTML body read from file: # Optional HTML body read from file:
html_body: file://README.html html_body: file://README.html
# Optional carbon copy recipients:
cc: kyloren@example.com,leia@example.com # Optional carbon copy recipients. Separate multiple addresses by a comma (possibly surrounded by whitespace):
# Optional blind carbon copy recipients: cc: 'kyloren@example.com, "Her Majesty, Princess Leia" <leia@example.com>'
bcc: r2d2@example.com,hansolo@example.com
# Optional blind carbon copy recipients. Separate multiple addresses by a comma (possibly surrounded by whitespace):
bcc: r2d2@example.com, hansolo@example.com
# Optional recipient of the email response: # Optional recipient of the email response:
reply_to: luke@example.com reply_to: luke@example.com
# Optional Message ID this message is replying to: # Optional Message ID this message is replying to:
in_reply_to: <random-luke@example.com> in_reply_to: '<3cc627c8-6181-453b-d90b-04aae9e23b21@github.com>'
# Optional unsigned/invalid certificates allowance: # Optional unsigned/invalid certificates allowance:
ignore_cert: true ignore_cert: true
# Optional converting Markdown to HTML (set content_type to text/html too): # Optional converting Markdown to HTML (set content_type to text/html too):
convert_markdown: true convert_markdown: true
# Optional attachments: # Optional attachments:
attachments: attachments.zip,git.diff,./dist/static/*.js attachments: attachments.zip,git.diff,./dist/static/*.js
# Optional priority: 'high', 'normal' (default) or 'low' # Optional priority: 'high', 'normal' (default) or 'low'
priority: low priority: low
# Optional custom headers:
headers: '{"X-Priority": "3 (Normal)", "X-My-Header": "MyValue"}'
# Optional nodemailerlog: true/false # Optional nodemailerlog: true/false
nodemailerlog: false nodemailerlog: false
# Optional nodemailerdebug: true/false if true lognodem will also be set true
# Optional nodemailerdebug: true/false if true nodemailerlog will also be set true
nodemailerdebug: false nodemailerdebug: false
# Optional custom SMTP MAIL FROM address (overrides username): # Optional custom SMTP MAIL FROM address (overrides username):
envelope_from: mailer@example.com envelope_from: mailer@example.com
# Optional custom SMTP RCPT TO addresses (overrides to, cc, bcc):
envelope_to: mailer@example.com,admin@example.com # Optional custom SMTP RCPT TO addresses (overrides to, cc, bcc). Separate multiple addresses by a comma (possibly surrounded by whitespace):
envelope_to: mailer@example.com, admin@example.com
``` ```
### Remark for `envelope_from` and `envelope_to`
[nodemailer](https://nodemailer.com/) (the node module that does the actual sending) requires that if the optional custom envelope is used, **both** its attributes `from` and `to` must be set. To facilitate setting only one of `envelope_from` or `envelope_to`, this action sets the other one from the regular message fields in the following way:
* If only `envelope_from` is set, `envelope_to` will be set to the concatenation of `to`, `cc` and `bcc` (with duplicates removed).
* If only `envelope_to` is set, `envelope_from` will be set to the address specified in `from`.
### Supported address formats
This action now uses nodemailer's addressparser. The supported address formats are described [here](https://nodemailer.com/message/addresses).
Mail addresses can contain YAML special characters like '<' and '>'. To avoid YAML parsing issues, addresses that contain such characters should be enclosed in single quotes.
## Troubleshooting ## Troubleshooting

View File

@@ -25,7 +25,7 @@ inputs:
description: Recipients mail addresses (separated with comma) description: Recipients mail addresses (separated with comma)
required: false required: false
from: from:
description: Full name of mail sender (might be with an email address specified in <>) description: 'Either a plain email address, or full name of the sender, followed by whitespace, followed by email address enclosed in <>'
required: true required: true
body: body:
description: Body of mail message (might be a filename prefixed with file:// to read from) description: Body of mail message (might be a filename prefixed with file:// to read from)
@@ -57,6 +57,9 @@ inputs:
priority: priority:
description: Set Priority level for the mail message to 'high', 'normal' (default) or 'low' description: Set Priority level for the mail message to 'high', 'normal' (default) or 'low'
required: false required: false
headers:
description: 'Custom headers for the mail message as a JSON string (e.g., {"X-Custom-Header": "value"})'
required: false
nodemailerlog: nodemailerlog:
description: Log option for nodemailer description: Log option for nodemailer
required: false required: false
@@ -70,5 +73,5 @@ inputs:
description: Custom envelope recipient addresses for SMTP RCPT TO command (separated with comma) description: Custom envelope recipient addresses for SMTP RCPT TO command (separated with comma)
required: false required: false
runs: runs:
using: node20 using: node24
main: main.js main: main.js

99
main.js
View File

@@ -1,4 +1,5 @@
import nodemailer from "nodemailer"; import nodemailer from "nodemailer";
import addressparser from "nodemailer/lib/addressparser/index.js";
import * as core from "@actions/core"; import * as core from "@actions/core";
import * as glob from "@actions/glob"; import * as glob from "@actions/glob";
import fs from "node:fs"; import fs from "node:fs";
@@ -23,14 +24,6 @@ function getText(textOrFile, convertMarkdown) {
return text; return text;
} }
function getFrom(from, username) {
if (from.match(/.+ <.+@.+>/)) {
return from;
}
return `"${from}" <${username}>`;
}
async function getAttachments(attachments) { async function getAttachments(attachments) {
const globber = await glob.create(attachments.split(",").join("\n")); const globber = await glob.create(attachments.split(",").join("\n"));
const files = await globber.glob(); const files = await globber.glob();
@@ -47,6 +40,39 @@ function sleep(ms) {
}); });
} }
/**
* Prepare an envelope object for nodemailer.
*
* If only one of envelopeFrom or envelopeTo is set, make sure that both
* are set in the returned object. Furthermore, make sure that the attribute 'to'
* is an array of email addresses, not a comma-separated string.
*/
function setupEnvelope(envelopeFrom, envelopeTo, from, to, cc, bcc) {
if (envelopeFrom || envelopeTo) {
// Take address in from, if envelopeFrom is not set.
envelopeFrom = envelopeFrom ? addressparser(envelopeFrom) : addressparser(from);
if (envelopeFrom.length != 1 || envelopeFrom[0].address == '') {
throw new Error("'envelopeFrom' address is invalid");
}
if (envelopeTo) {
envelopeTo = addressparser(envelopeTo);
} else {
// Take addresses in to, cc and bcc. Deduplication is handled by nodemailer.
for (const src of [to, cc, bcc]) {
if (src) {
let parsed = addressparser(src);
envelopeTo = envelopeTo ? envelopeTo.concat(parsed) : parsed;
}
}
}
return {
from: envelopeFrom,
to: envelopeTo,
};
}
return undefined;
}
async function main() { async function main() {
try { try {
let serverAddress = core.getInput("server_address"); let serverAddress = core.getInput("server_address");
@@ -115,6 +141,14 @@ async function main() {
required: false, required: false,
}); });
const envelopeTo = core.getInput("envelope_to", { required: false }); const envelopeTo = core.getInput("envelope_to", { required: false });
const headers = core.getInput("headers", { required: false });
// Basic check for an email sender address
// Either: "Plain Simple Name <user@doma.in>" or just "user@doma.in" (without the <>)
let parsed = addressparser(from);
if (parsed.length != 1 || parsed[0].address == '') {
throw new Error("'from' address is invalid");
}
// if neither to, cc or bcc is provided, throw error // if neither to, cc or bcc is provided, throw error
if (!to && !cc && !bcc) { if (!to && !cc && !bcc) {
@@ -149,34 +183,31 @@ async function main() {
proxy: process.env.HTTP_PROXY, proxy: process.env.HTTP_PROXY,
}); });
var i = 1; const messageOptions = {
from: from,
to: to,
subject: getText(subject, false),
cc: cc ? cc : undefined,
bcc: bcc ? bcc : undefined,
replyTo: replyTo ? replyTo : undefined,
inReplyTo: inReplyTo ? inReplyTo : undefined,
references: inReplyTo ? inReplyTo : undefined,
text: body ? getText(body, false) : undefined,
html: htmlBody
? getText(htmlBody, convertMarkdown)
: undefined,
priority: priority ? priority : undefined,
headers: headers ? JSON.parse(headers) : undefined,
attachments: attachments
? await getAttachments(attachments)
: undefined,
envelope: setupEnvelope(envelopeFrom, envelopeTo, from, to, cc, bcc),
};
let i = 1;
while (true) { while (true) {
try { try {
const info = await transport.sendMail({ const info = await transport.sendMail(messageOptions);
from: getFrom(from, username),
to: to,
subject: getText(subject, false),
cc: cc ? cc : undefined,
bcc: bcc ? bcc : undefined,
replyTo: replyTo ? replyTo : undefined,
inReplyTo: inReplyTo ? inReplyTo : undefined,
references: inReplyTo ? inReplyTo : undefined,
text: body ? getText(body, false) : undefined,
html: htmlBody
? getText(htmlBody, convertMarkdown)
: undefined,
priority: priority ? priority : undefined,
attachments: attachments
? await getAttachments(attachments)
: undefined,
envelope:
envelopeFrom || envelopeTo
? {
from: envelopeFrom ? envelopeFrom : undefined,
to: envelopeTo ? envelopeTo : undefined,
}
: undefined,
});
break; break;
} catch (error) { } catch (error) {
if (!error.message.includes("Try again later,")) { if (!error.message.includes("Try again later,")) {

18
node_modules/.package-lock.json generated vendored
View File

@@ -185,9 +185,9 @@
} }
}, },
"node_modules/minimatch": { "node_modules/minimatch": {
"version": "3.1.3", "version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.3.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
"integrity": "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==", "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"brace-expansion": "^1.1.7" "brace-expansion": "^1.1.7"
@@ -197,9 +197,9 @@
} }
}, },
"node_modules/nodemailer": { "node_modules/nodemailer": {
"version": "8.0.1", "version": "8.0.2",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.1.tgz", "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.2.tgz",
"integrity": "sha512-5kcldIXmaEjZcHR6F28IKGSgpmZHaF1IXLWFTG+Xh3S+Cce4MiakLtWY+PlBU69fLbRa8HlaGIrC/QolUpHkhg==", "integrity": "sha512-zbj002pZAIkWQFxyAaqoxvn+zoIwRnS40hgjqTXudKOOJkiFFgBeNqjgD3/YCR12sZnrghWYBY+yP1ZucdDRpw==",
"license": "MIT-0", "license": "MIT-0",
"engines": { "engines": {
"node": ">=6.0.0" "node": ">=6.0.0"
@@ -319,9 +319,9 @@
} }
}, },
"node_modules/undici": { "node_modules/undici": {
"version": "6.23.0", "version": "6.24.1",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.23.0.tgz", "resolved": "https://registry.npmjs.org/undici/-/undici-6.24.1.tgz",
"integrity": "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==", "integrity": "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=18.17" "node": ">=18.17"

37
node_modules/minimatch/README.md generated vendored
View File

@@ -10,6 +10,43 @@ This is the matching library used internally by npm.
It works by converting glob expressions into JavaScript `RegExp` It works by converting glob expressions into JavaScript `RegExp`
objects. objects.
## Important Security Consideration!
> [!WARNING]
> This library uses JavaScript regular expressions. Please read
> the following warning carefully, and be thoughtful about what
> you provide to this library in production systems.
_Any_ library in JavaScript that deals with matching string
patterns using regular expressions will be subject to
[ReDoS](https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS)
if the pattern is generated using untrusted input.
Efforts have been made to mitigate risk as much as is feasible in
such a library, providing maximum recursion depths and so forth,
but these measures can only ultimately protect against accidents,
not malice. A dedicated attacker can _always_ find patterns that
cannot be defended against by a bash-compatible glob pattern
matching system that uses JavaScript regular expressions.
To be extremely clear:
> [!WARNING]
> **If you create a system where you take user input, and use
> that input as the source of a Regular Expression pattern, in
> this or any extant glob matcher in JavaScript, you will be
> pwned.**
A future version of this library _may_ use a different matching
algorithm which does not exhibit backtracking problems. If and
when that happens, it will likely be a sweeping change, and those
improvements will **not** be backported to legacy versions.
In the near term, it is not reasonable to continue to play
whack-a-mole with security advisories, and so any future ReDoS
reports will be considered "working as intended", and resolved
entirely by this warning.
## Usage ## Usage
```javascript ```javascript

259
node_modules/minimatch/minimatch.js generated vendored
View File

@@ -142,6 +142,8 @@ function Minimatch (pattern, options) {
} }
this.options = options this.options = options
this.maxGlobstarRecursion = options.maxGlobstarRecursion !== undefined
? options.maxGlobstarRecursion : 200
this.set = [] this.set = []
this.pattern = pattern this.pattern = pattern
this.regexp = null this.regexp = null
@@ -787,19 +789,163 @@ Minimatch.prototype.match = function match (f, partial) {
// out of pattern, then that's fine, as long as all // out of pattern, then that's fine, as long as all
// the parts match. // the parts match.
Minimatch.prototype.matchOne = function (file, pattern, partial) { Minimatch.prototype.matchOne = function (file, pattern, partial) {
var options = this.options if (pattern.indexOf(GLOBSTAR) !== -1) {
return this._matchGlobstar(file, pattern, partial, 0, 0)
}
return this._matchOne(file, pattern, partial, 0, 0)
}
this.debug('matchOne', Minimatch.prototype._matchGlobstar = function (file, pattern, partial, fileIndex, patternIndex) {
{ 'this': this, file: file, pattern: pattern }) var i
this.debug('matchOne', file.length, pattern.length) // find first globstar from patternIndex
var firstgs = -1
for (i = patternIndex; i < pattern.length; i++) {
if (pattern[i] === GLOBSTAR) { firstgs = i; break }
}
for (var fi = 0, // find last globstar
pi = 0, var lastgs = -1
fl = file.length, for (i = pattern.length - 1; i >= 0; i--) {
pl = pattern.length if (pattern[i] === GLOBSTAR) { lastgs = i; break }
; (fi < fl) && (pi < pl) }
; fi++, pi++) {
var head = pattern.slice(patternIndex, firstgs)
var body = partial ? pattern.slice(firstgs + 1) : pattern.slice(firstgs + 1, lastgs)
var tail = partial ? [] : pattern.slice(lastgs + 1)
// check the head
if (head.length) {
var fileHead = file.slice(fileIndex, fileIndex + head.length)
if (!this._matchOne(fileHead, head, partial, 0, 0)) {
return false
}
fileIndex += head.length
}
// check the tail
var fileTailMatch = 0
if (tail.length) {
if (tail.length + fileIndex > file.length) return false
var tailStart = file.length - tail.length
if (this._matchOne(file, tail, partial, tailStart, 0)) {
fileTailMatch = tail.length
} else {
// affordance for stuff like a/**/* matching a/b/
if (file[file.length - 1] !== '' ||
fileIndex + tail.length === file.length) {
return false
}
tailStart--
if (!this._matchOne(file, tail, partial, tailStart, 0)) {
return false
}
fileTailMatch = tail.length + 1
}
}
// if body is empty (single ** between head and tail)
if (!body.length) {
var sawSome = !!fileTailMatch
for (i = fileIndex; i < file.length - fileTailMatch; i++) {
var f = String(file[i])
sawSome = true
if (f === '.' || f === '..' ||
(!this.options.dot && f.charAt(0) === '.')) {
return false
}
}
return partial || sawSome
}
// split body into segments at each GLOBSTAR
var bodySegments = [[[], 0]]
var currentBody = bodySegments[0]
var nonGsParts = 0
var nonGsPartsSums = [0]
for (var bi = 0; bi < body.length; bi++) {
var b = body[bi]
if (b === GLOBSTAR) {
nonGsPartsSums.push(nonGsParts)
currentBody = [[], 0]
bodySegments.push(currentBody)
} else {
currentBody[0].push(b)
nonGsParts++
}
}
var idx = bodySegments.length - 1
var fileLength = file.length - fileTailMatch
for (var si = 0; si < bodySegments.length; si++) {
bodySegments[si][1] = fileLength -
(nonGsPartsSums[idx--] + bodySegments[si][0].length)
}
return !!this._matchGlobStarBodySections(
file, bodySegments, fileIndex, 0, partial, 0, !!fileTailMatch
)
}
// return false for "nope, not matching"
// return null for "not matching, cannot keep trying"
Minimatch.prototype._matchGlobStarBodySections = function (
file, bodySegments, fileIndex, bodyIndex, partial, globStarDepth, sawTail
) {
var bs = bodySegments[bodyIndex]
if (!bs) {
// just make sure there are no bad dots
for (var i = fileIndex; i < file.length; i++) {
sawTail = true
var f = file[i]
if (f === '.' || f === '..' ||
(!this.options.dot && f.charAt(0) === '.')) {
return false
}
}
return sawTail
}
var body = bs[0]
var after = bs[1]
while (fileIndex <= after) {
var m = this._matchOne(
file.slice(0, fileIndex + body.length),
body,
partial,
fileIndex,
0
)
// if limit exceeded, no match. intentional false negative,
// acceptable break in correctness for security.
if (m && globStarDepth < this.maxGlobstarRecursion) {
var sub = this._matchGlobStarBodySections(
file, bodySegments,
fileIndex + body.length, bodyIndex + 1,
partial, globStarDepth + 1, sawTail
)
if (sub !== false) {
return sub
}
}
var f = file[fileIndex]
if (f === '.' || f === '..' ||
(!this.options.dot && f.charAt(0) === '.')) {
return false
}
fileIndex++
}
return partial || null
}
Minimatch.prototype._matchOne = function (file, pattern, partial, fileIndex, patternIndex) {
var fi, pi, fl, pl
for (
fi = fileIndex, pi = patternIndex, fl = file.length, pl = pattern.length
; (fi < fl) && (pi < pl)
; fi++, pi++
) {
this.debug('matchOne loop') this.debug('matchOne loop')
var p = pattern[pi] var p = pattern[pi]
var f = file[fi] var f = file[fi]
@@ -809,87 +955,7 @@ Minimatch.prototype.matchOne = function (file, pattern, partial) {
// should be impossible. // should be impossible.
// some invalid regexp stuff in the set. // some invalid regexp stuff in the set.
/* istanbul ignore if */ /* istanbul ignore if */
if (p === false) return false if (p === false || p === GLOBSTAR) return false
if (p === GLOBSTAR) {
this.debug('GLOBSTAR', [pattern, p, f])
// "**"
// a/**/b/**/c would match the following:
// a/b/x/y/z/c
// a/x/y/z/b/c
// a/b/x/b/x/c
// a/b/c
// To do this, take the rest of the pattern after
// the **, and see if it would match the file remainder.
// If so, return success.
// If not, the ** "swallows" a segment, and try again.
// This is recursively awful.
//
// a/**/b/**/c matching a/b/x/y/z/c
// - a matches a
// - doublestar
// - matchOne(b/x/y/z/c, b/**/c)
// - b matches b
// - doublestar
// - matchOne(x/y/z/c, c) -> no
// - matchOne(y/z/c, c) -> no
// - matchOne(z/c, c) -> no
// - matchOne(c, c) yes, hit
var fr = fi
var pr = pi + 1
if (pr === pl) {
this.debug('** at the end')
// a ** at the end will just swallow the rest.
// We have found a match.
// however, it will not swallow /.x, unless
// options.dot is set.
// . and .. are *never* matched by **, for explosively
// exponential reasons.
for (; fi < fl; fi++) {
if (file[fi] === '.' || file[fi] === '..' ||
(!options.dot && file[fi].charAt(0) === '.')) return false
}
return true
}
// ok, let's see if we can swallow whatever we can.
while (fr < fl) {
var swallowee = file[fr]
this.debug('\nglobstar while', file, fr, pattern, pr, swallowee)
// XXX remove this slice. Just pass the start index.
if (this.matchOne(file.slice(fr), pattern.slice(pr), partial)) {
this.debug('globstar found match!', fr, fl, swallowee)
// found a match.
return true
} else {
// can't swallow "." or ".." ever.
// can only swallow ".foo" when explicitly asked.
if (swallowee === '.' || swallowee === '..' ||
(!options.dot && swallowee.charAt(0) === '.')) {
this.debug('dot detected!', file, fr, pattern, pr)
break
}
// ** swallows a segment, and continue.
this.debug('globstar swallow a segment, and continue')
fr++
}
}
// no match was found.
// However, in partial mode, we can't say this is necessarily over.
// If there's more *pattern* left, then
/* istanbul ignore if */
if (partial) {
// ran out of file
this.debug('\n>>> no match, partial?', file, fr, pattern, pr)
if (fr === fl) return true
}
return false
}
// something other than ** // something other than **
// non-magic patterns just have to match exactly // non-magic patterns just have to match exactly
@@ -906,17 +972,6 @@ Minimatch.prototype.matchOne = function (file, pattern, partial) {
if (!hit) return false if (!hit) return false
} }
// Note: ending in / means that we'll get a final ""
// at the end of the pattern. This can only match a
// corresponding "" at the end of the file.
// If the file ends in /, then it can only match a
// a pattern that ends in /, unless the pattern just
// doesn't have any more for it. But, a/b/ should *not*
// match "a/b/*", even though "" matches against the
// [^/]*? pattern, except in partial mode, where it might
// simply not be reached yet.
// However, a/b/ should still satisfy a/*
// now either we fell off the end of the pattern, or we're done. // now either we fell off the end of the pattern, or we're done.
if (fi === fl && pi === pl) { if (fi === fl && pi === pl) {
// ran out of pattern and filename at the same time. // ran out of pattern and filename at the same time.

View File

@@ -2,7 +2,7 @@
"author": "Isaac Z. Schlueter <i@izs.me> (http://blog.izs.me)", "author": "Isaac Z. Schlueter <i@izs.me> (http://blog.izs.me)",
"name": "minimatch", "name": "minimatch",
"description": "a glob matcher in javascript", "description": "a glob matcher in javascript",
"version": "3.1.3", "version": "3.1.5",
"publishConfig": { "publishConfig": {
"tag": "legacy-v3" "tag": "legacy-v3"
}, },

View File

@@ -1,5 +1,12 @@
# CHANGELOG # CHANGELOG
## [8.0.2](https://github.com/nodemailer/nodemailer/compare/v8.0.1...v8.0.2) (2026-03-09)
### Bug Fixes
* merge fragmented display names with unquoted commas in addressparser ([fe27f7f](https://github.com/nodemailer/nodemailer/commit/fe27f7fd57f7587d897274438da2f628ad0ad7d9))
## [8.0.1](https://github.com/nodemailer/nodemailer/compare/v8.0.0...v8.0.1) (2026-02-07) ## [8.0.1](https://github.com/nodemailer/nodemailer/compare/v8.0.0...v8.0.1) (2026-02-07)

View File

@@ -361,6 +361,20 @@ function addressparser(str, options) {
} }
}); });
// Merge fragments from unquoted display names containing commas/semicolons.
// When "Joe Foo, PhD <joe@example.com>" is split on the comma, it produces
// [{name:"Joe Foo", address:""}, {name:"PhD", address:"joe@example.com"}].
// Detect this pattern and recombine: a name-only entry followed by an entry
// that has both a name and an address (from angle-bracket notation).
for (let i = parsedAddresses.length - 2; i >= 0; i--) {
let current = parsedAddresses[i];
let next = parsedAddresses[i + 1];
if (current.address === '' && current.name && !current.group && next.address && next.name && !next.group) {
next.name = current.name + ', ' + next.name;
parsedAddresses.splice(i, 1);
}
}
if (options.flatten) { if (options.flatten) {
let addresses = []; let addresses = [];
let walkAddressList = list => { let walkAddressList = list => {

10
node_modules/nodemailer/package.json generated vendored
View File

@@ -1,6 +1,6 @@
{ {
"name": "nodemailer", "name": "nodemailer",
"version": "8.0.1", "version": "8.0.2",
"description": "Easy as cake e-mail sending from your Node.js applications", "description": "Easy as cake e-mail sending from your Node.js applications",
"main": "lib/nodemailer.js", "main": "lib/nodemailer.js",
"scripts": { "scripts": {
@@ -26,12 +26,12 @@
}, },
"homepage": "https://nodemailer.com/", "homepage": "https://nodemailer.com/",
"devDependencies": { "devDependencies": {
"@aws-sdk/client-sesv2": "3.985.0", "@aws-sdk/client-sesv2": "3.1004.0",
"bunyan": "1.8.15", "bunyan": "1.8.15",
"c8": "10.1.3", "c8": "11.0.0",
"eslint": "10.0.0", "eslint": "10.0.3",
"eslint-config-prettier": "10.1.8", "eslint-config-prettier": "10.1.8",
"globals": "17.3.0", "globals": "17.4.0",
"libbase64": "1.3.0", "libbase64": "1.3.0",
"libmime": "5.3.7", "libmime": "5.3.7",
"libqp": "2.1.1", "libqp": "2.1.1",

View File

@@ -27,6 +27,7 @@ import { errors } from 'undici'
| `InformationalError` | `UND_ERR_INFO` | expected error with reason | | `InformationalError` | `UND_ERR_INFO` | expected error with reason |
| `ResponseExceededMaxSizeError` | `UND_ERR_RES_EXCEEDED_MAX_SIZE` | response body exceed the max size allowed | | `ResponseExceededMaxSizeError` | `UND_ERR_RES_EXCEEDED_MAX_SIZE` | response body exceed the max size allowed |
| `SecureProxyConnectionError` | `UND_ERR_PRX_TLS` | tls connection to a proxy failed | | `SecureProxyConnectionError` | `UND_ERR_PRX_TLS` | tls connection to a proxy failed |
| `MessageSizeExceededError` | `UND_ERR_WS_MESSAGE_SIZE_EXCEEDED` | WebSocket decompressed message exceeded the maximum allowed size |
### `SocketError` ### `SocketError`

View File

@@ -13,6 +13,14 @@ Arguments:
* **url** `URL | string` - The url's protocol *must* be `ws` or `wss`. * **url** `URL | string` - The url's protocol *must* be `ws` or `wss`.
* **protocol** `string | string[] | WebSocketInit` (optional) - Subprotocol(s) to request the server use, or a [`Dispatcher`](./Dispatcher.md). * **protocol** `string | string[] | WebSocketInit` (optional) - Subprotocol(s) to request the server use, or a [`Dispatcher`](./Dispatcher.md).
### WebSocketInit
When passing an object as the second argument, the following options are available:
* **protocols** `string | string[]` (optional) - Subprotocol(s) to request the server use.
* **dispatcher** `Dispatcher` (optional) - A custom [`Dispatcher`](/docs/docs/api/Dispatcher.md) to use for the connection.
* **headers** `HeadersInit` (optional) - Custom headers to include in the WebSocket handshake request.
### Example: ### Example:
This example will not work in browsers or other platforms that don't allow passing an object. This example will not work in browsers or other platforms that don't allow passing an object.

View File

@@ -379,6 +379,24 @@ class SecureProxyConnectionError extends UndiciError {
[kSecureProxyConnectionError] = true [kSecureProxyConnectionError] = true
} }
const kMessageSizeExceededError = Symbol.for('undici.error.UND_ERR_WS_MESSAGE_SIZE_EXCEEDED')
class MessageSizeExceededError extends UndiciError {
constructor (message) {
super(message)
this.name = 'MessageSizeExceededError'
this.message = message || 'Max decompressed message size exceeded'
this.code = 'UND_ERR_WS_MESSAGE_SIZE_EXCEEDED'
}
static [Symbol.hasInstance] (instance) {
return instance && instance[kMessageSizeExceededError] === true
}
get [kMessageSizeExceededError] () {
return true
}
}
module.exports = { module.exports = {
AbortError, AbortError,
HTTPParserError, HTTPParserError,
@@ -402,5 +420,6 @@ module.exports = {
ResponseExceededMaxSizeError, ResponseExceededMaxSizeError,
RequestRetryError, RequestRetryError,
ResponseError, ResponseError,
SecureProxyConnectionError SecureProxyConnectionError,
MessageSizeExceededError
} }

View File

@@ -66,6 +66,10 @@ class Request {
throw new InvalidArgumentError('upgrade must be a string') throw new InvalidArgumentError('upgrade must be a string')
} }
if (upgrade && !isValidHeaderValue(upgrade)) {
throw new InvalidArgumentError('invalid upgrade header')
}
if (headersTimeout != null && (!Number.isFinite(headersTimeout) || headersTimeout < 0)) { if (headersTimeout != null && (!Number.isFinite(headersTimeout) || headersTimeout < 0)) {
throw new InvalidArgumentError('invalid headersTimeout') throw new InvalidArgumentError('invalid headersTimeout')
} }
@@ -360,13 +364,19 @@ function processHeader (request, key, val) {
val = `${val}` val = `${val}`
} }
if (request.host === null && headerName === 'host') { if (headerName === 'host') {
if (request.host !== null) {
throw new InvalidArgumentError('duplicate host header')
}
if (typeof val !== 'string') { if (typeof val !== 'string') {
throw new InvalidArgumentError('invalid host header') throw new InvalidArgumentError('invalid host header')
} }
// Consumed by Client // Consumed by Client
request.host = val request.host = val
} else if (request.contentLength === null && headerName === 'content-length') { } else if (headerName === 'content-length') {
if (request.contentLength !== null) {
throw new InvalidArgumentError('duplicate content-length header')
}
request.contentLength = parseInt(val, 10) request.contentLength = parseInt(val, 10)
if (!Number.isFinite(request.contentLength)) { if (!Number.isFinite(request.contentLength)) {
throw new InvalidArgumentError('invalid content-length header') throw new InvalidArgumentError('invalid content-length header')

View File

@@ -2,17 +2,30 @@
const { createInflateRaw, Z_DEFAULT_WINDOWBITS } = require('node:zlib') const { createInflateRaw, Z_DEFAULT_WINDOWBITS } = require('node:zlib')
const { isValidClientWindowBits } = require('./util') const { isValidClientWindowBits } = require('./util')
const { MessageSizeExceededError } = require('../../core/errors')
const tail = Buffer.from([0x00, 0x00, 0xff, 0xff]) const tail = Buffer.from([0x00, 0x00, 0xff, 0xff])
const kBuffer = Symbol('kBuffer') const kBuffer = Symbol('kBuffer')
const kLength = Symbol('kLength') const kLength = Symbol('kLength')
// Default maximum decompressed message size: 4 MB
const kDefaultMaxDecompressedSize = 4 * 1024 * 1024
class PerMessageDeflate { class PerMessageDeflate {
/** @type {import('node:zlib').InflateRaw} */ /** @type {import('node:zlib').InflateRaw} */
#inflate #inflate
#options = {} #options = {}
/** @type {boolean} */
#aborted = false
/** @type {Function|null} */
#currentCallback = null
/**
* @param {Map<string, string>} extensions
*/
constructor (extensions) { constructor (extensions) {
this.#options.serverNoContextTakeover = extensions.has('server_no_context_takeover') this.#options.serverNoContextTakeover = extensions.has('server_no_context_takeover')
this.#options.serverMaxWindowBits = extensions.get('server_max_window_bits') this.#options.serverMaxWindowBits = extensions.get('server_max_window_bits')
@@ -24,6 +37,11 @@ class PerMessageDeflate {
// payload of the message. // payload of the message.
// 2. Decompress the resulting data using DEFLATE. // 2. Decompress the resulting data using DEFLATE.
if (this.#aborted) {
callback(new MessageSizeExceededError())
return
}
if (!this.#inflate) { if (!this.#inflate) {
let windowBits = Z_DEFAULT_WINDOWBITS let windowBits = Z_DEFAULT_WINDOWBITS
@@ -36,13 +54,37 @@ class PerMessageDeflate {
windowBits = Number.parseInt(this.#options.serverMaxWindowBits) windowBits = Number.parseInt(this.#options.serverMaxWindowBits)
} }
this.#inflate = createInflateRaw({ windowBits }) try {
this.#inflate = createInflateRaw({ windowBits })
} catch (err) {
callback(err)
return
}
this.#inflate[kBuffer] = [] this.#inflate[kBuffer] = []
this.#inflate[kLength] = 0 this.#inflate[kLength] = 0
this.#inflate.on('data', (data) => { this.#inflate.on('data', (data) => {
this.#inflate[kBuffer].push(data) if (this.#aborted) {
return
}
this.#inflate[kLength] += data.length this.#inflate[kLength] += data.length
if (this.#inflate[kLength] > kDefaultMaxDecompressedSize) {
this.#aborted = true
this.#inflate.removeAllListeners()
this.#inflate.destroy()
this.#inflate = null
if (this.#currentCallback) {
const cb = this.#currentCallback
this.#currentCallback = null
cb(new MessageSizeExceededError())
}
return
}
this.#inflate[kBuffer].push(data)
}) })
this.#inflate.on('error', (err) => { this.#inflate.on('error', (err) => {
@@ -51,16 +93,22 @@ class PerMessageDeflate {
}) })
} }
this.#currentCallback = callback
this.#inflate.write(chunk) this.#inflate.write(chunk)
if (fin) { if (fin) {
this.#inflate.write(tail) this.#inflate.write(tail)
} }
this.#inflate.flush(() => { this.#inflate.flush(() => {
if (this.#aborted || !this.#inflate) {
return
}
const full = Buffer.concat(this.#inflate[kBuffer], this.#inflate[kLength]) const full = Buffer.concat(this.#inflate[kBuffer], this.#inflate[kLength])
this.#inflate[kBuffer].length = 0 this.#inflate[kBuffer].length = 0
this.#inflate[kLength] = 0 this.#inflate[kLength] = 0
this.#currentCallback = null
callback(null, full) callback(null, full)
}) })

View File

@@ -37,6 +37,10 @@ class ByteParser extends Writable {
/** @type {Map<string, PerMessageDeflate>} */ /** @type {Map<string, PerMessageDeflate>} */
#extensions #extensions
/**
* @param {import('./websocket').WebSocket} ws
* @param {Map<string, string>|null} extensions
*/
constructor (ws, extensions) { constructor (ws, extensions) {
super() super()
@@ -179,6 +183,7 @@ class ByteParser extends Writable {
const buffer = this.consume(8) const buffer = this.consume(8)
const upper = buffer.readUInt32BE(0) const upper = buffer.readUInt32BE(0)
const lower = buffer.readUInt32BE(4)
// 2^31 is the maximum bytes an arraybuffer can contain // 2^31 is the maximum bytes an arraybuffer can contain
// on 32-bit systems. Although, on 64-bit systems, this is // on 32-bit systems. Although, on 64-bit systems, this is
@@ -186,14 +191,12 @@ class ByteParser extends Writable {
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Invalid_array_length // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Invalid_array_length
// https://source.chromium.org/chromium/chromium/src/+/main:v8/src/common/globals.h;drc=1946212ac0100668f14eb9e2843bdd846e510a1e;bpv=1;bpt=1;l=1275 // https://source.chromium.org/chromium/chromium/src/+/main:v8/src/common/globals.h;drc=1946212ac0100668f14eb9e2843bdd846e510a1e;bpv=1;bpt=1;l=1275
// https://source.chromium.org/chromium/chromium/src/+/main:v8/src/objects/js-array-buffer.h;l=34;drc=1946212ac0100668f14eb9e2843bdd846e510a1e // https://source.chromium.org/chromium/chromium/src/+/main:v8/src/objects/js-array-buffer.h;l=34;drc=1946212ac0100668f14eb9e2843bdd846e510a1e
if (upper > 2 ** 31 - 1) { if (upper !== 0 || lower > 2 ** 31 - 1) {
failWebsocketConnection(this.ws, 'Received payload length > 2^31 bytes.') failWebsocketConnection(this.ws, 'Received payload length > 2^31 bytes.')
return return
} }
const lower = buffer.readUInt32BE(4) this.#info.payloadLength = lower
this.#info.payloadLength = (upper << 8) + lower
this.#state = parserStates.READ_DATA this.#state = parserStates.READ_DATA
} else if (this.#state === parserStates.READ_DATA) { } else if (this.#state === parserStates.READ_DATA) {
if (this.#byteOffset < this.#info.payloadLength) { if (this.#byteOffset < this.#info.payloadLength) {
@@ -223,7 +226,7 @@ class ByteParser extends Writable {
} else { } else {
this.#extensions.get('permessage-deflate').decompress(body, this.#info.fin, (error, data) => { this.#extensions.get('permessage-deflate').decompress(body, this.#info.fin, (error, data) => {
if (error) { if (error) {
closeWebSocketConnection(this.ws, 1007, error.message, error.message.length) failWebsocketConnection(this.ws, error.message)
return return
} }

View File

@@ -266,6 +266,12 @@ function parseExtensions (extensions) {
* @param {string} value * @param {string} value
*/ */
function isValidClientWindowBits (value) { function isValidClientWindowBits (value) {
// Must have at least one character
if (value.length === 0) {
return false
}
// Check all characters are ASCII digits
for (let i = 0; i < value.length; i++) { for (let i = 0; i < value.length; i++) {
const byte = value.charCodeAt(i) const byte = value.charCodeAt(i)
@@ -274,7 +280,9 @@ function isValidClientWindowBits (value) {
} }
} }
return true // Check numeric range: zlib requires windowBits in range 8-15
const num = Number.parseInt(value, 10)
return num >= 8 && num <= 15
} }
// https://nodejs.org/api/intl.html#detecting-internationalization-support // https://nodejs.org/api/intl.html#detecting-internationalization-support

View File

@@ -431,7 +431,7 @@ class WebSocket extends EventTarget {
* @see https://websockets.spec.whatwg.org/#feedback-from-the-protocol * @see https://websockets.spec.whatwg.org/#feedback-from-the-protocol
*/ */
#onConnectionEstablished (response, parsedExtensions) { #onConnectionEstablished (response, parsedExtensions) {
// processResponse is called when the "responses header list has been received and initialized." // processResponse is called when the "response's header list has been received and initialized."
// once this happens, the connection is open // once this happens, the connection is open
this[kResponse] = response this[kResponse] = response

4
node_modules/undici/package.json generated vendored
View File

@@ -1,6 +1,6 @@
{ {
"name": "undici", "name": "undici",
"version": "6.23.0", "version": "6.24.1",
"description": "An HTTP/1.1 client, written from scratch for Node.js", "description": "An HTTP/1.1 client, written from scratch for Node.js",
"homepage": "https://undici.nodejs.org", "homepage": "https://undici.nodejs.org",
"bugs": { "bugs": {
@@ -107,6 +107,7 @@
"devDependencies": { "devDependencies": {
"@fastify/busboy": "2.1.1", "@fastify/busboy": "2.1.1",
"@matteo.collina/tspl": "^0.1.1", "@matteo.collina/tspl": "^0.1.1",
"@metcoder95/https-pem": "^1.0.0",
"@sinonjs/fake-timers": "^11.1.0", "@sinonjs/fake-timers": "^11.1.0",
"@types/node": "~18.19.50", "@types/node": "~18.19.50",
"abort-controller": "^3.0.0", "abort-controller": "^3.0.0",
@@ -117,7 +118,6 @@
"fast-check": "^3.17.1", "fast-check": "^3.17.1",
"form-data": "^4.0.0", "form-data": "^4.0.0",
"formdata-node": "^6.0.3", "formdata-node": "^6.0.3",
"https-pem": "^3.0.0",
"husky": "^9.0.7", "husky": "^9.0.7",
"jest": "^29.0.2", "jest": "^29.0.2",
"jsdom": "^24.0.0", "jsdom": "^24.0.0",

View File

@@ -146,4 +146,10 @@ declare namespace Errors {
name: 'SecureProxyConnectionError'; name: 'SecureProxyConnectionError';
code: 'UND_ERR_PRX_TLS'; code: 'UND_ERR_PRX_TLS';
} }
/** WebSocket decompressed message exceeded maximum size. */
export class MessageSizeExceededError extends UndiciError {
name: 'MessageSizeExceededError'
code: 'UND_ERR_WS_MESSAGE_SIZE_EXCEEDED'
}
} }

20
package-lock.json generated
View File

@@ -8,7 +8,7 @@
"dependencies": { "dependencies": {
"@actions/core": "^3.0.0", "@actions/core": "^3.0.0",
"@actions/glob": "^0.6.1", "@actions/glob": "^0.6.1",
"nodemailer": "^8.0.1", "nodemailer": "^8.0.2",
"showdown": "^1.9.1" "showdown": "^1.9.1"
} }
}, },
@@ -194,9 +194,9 @@
} }
}, },
"node_modules/minimatch": { "node_modules/minimatch": {
"version": "3.1.3", "version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.3.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
"integrity": "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==", "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"brace-expansion": "^1.1.7" "brace-expansion": "^1.1.7"
@@ -206,9 +206,9 @@
} }
}, },
"node_modules/nodemailer": { "node_modules/nodemailer": {
"version": "8.0.1", "version": "8.0.2",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.1.tgz", "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.2.tgz",
"integrity": "sha512-5kcldIXmaEjZcHR6F28IKGSgpmZHaF1IXLWFTG+Xh3S+Cce4MiakLtWY+PlBU69fLbRa8HlaGIrC/QolUpHkhg==", "integrity": "sha512-zbj002pZAIkWQFxyAaqoxvn+zoIwRnS40hgjqTXudKOOJkiFFgBeNqjgD3/YCR12sZnrghWYBY+yP1ZucdDRpw==",
"license": "MIT-0", "license": "MIT-0",
"engines": { "engines": {
"node": ">=6.0.0" "node": ">=6.0.0"
@@ -328,9 +328,9 @@
} }
}, },
"node_modules/undici": { "node_modules/undici": {
"version": "6.23.0", "version": "6.24.1",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.23.0.tgz", "resolved": "https://registry.npmjs.org/undici/-/undici-6.24.1.tgz",
"integrity": "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==", "integrity": "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=18.17" "node": ">=18.17"

View File

@@ -5,7 +5,7 @@
"dependencies": { "dependencies": {
"@actions/core": "^3.0.0", "@actions/core": "^3.0.0",
"@actions/glob": "^0.6.1", "@actions/glob": "^0.6.1",
"nodemailer": "^8.0.1", "nodemailer": "^8.0.2",
"showdown": "^1.9.1" "showdown": "^1.9.1"
} }
} }