8 Commits
v11 ... v15

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
22 changed files with 469 additions and 191 deletions

View File

@@ -27,53 +27,86 @@ Some features:
# * smtp://user:password@server:port
# * smtp+starttls://user:password@server:port
connection_url: ${{secrets.MAIL_CONNECTION}}
# Required mail server address if not connection_url:
server_address: smtp.gmail.com
# Server port, default 25:
server_port: 465
# Optional whether this connection use TLS (default is true if server_port is 465)
secure: true
# Optional (recommended) mail server username:
username: ${{secrets.MAIL_USERNAME}}
# Optional (recommended) mail server password:
password: ${{secrets.MAIL_PASSWORD}}
# Required mail subject:
subject: Github Actions job result
# Required recipients' addresses:
# Optional recipients. Separate multiple addresses by a comma (possibly surrounded by whitespace):
to: obiwan@example.com, yoda@example.com
# Required sender full name (address can be skipped):
from: Luke Skywalker # <user@example.com>
# Required sender (supported formats: see "Supported address formats" below)
from: 'Luke Skywalker <user@example.com>'
# Optional plain body:
body: Build job of ${{github.repository}} completed successfully!
# Optional HTML body read from file:
html_body: file://README.html
# Optional carbon copy recipients:
cc: kyloren@example.com,leia@example.com
# Optional blind carbon copy recipients:
# Optional carbon copy recipients. Separate multiple addresses by a comma (possibly surrounded by whitespace):
cc: 'kyloren@example.com, "Her Majesty, Princess Leia" <leia@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:
reply_to: luke@example.com
# 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:
ignore_cert: true
# Optional converting Markdown to HTML (set content_type to text/html too):
convert_markdown: true
# Optional attachments:
attachments: attachments.zip,git.diff,./dist/static/*.js
# Optional priority: 'high', 'normal' (default) or 'low'
priority: low
# Optional custom headers:
headers: '{"X-Priority": "3 (Normal)", "X-My-Header": "MyValue"}'
# Optional nodemailerlog: true/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
# Optional custom SMTP MAIL FROM address (overrides username):
envelope_from: mailer@example.com
# Optional custom SMTP RCPT TO addresses (overrides to, cc, bcc):
# 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

View File

@@ -25,7 +25,7 @@ inputs:
description: Recipients mail addresses (separated with comma)
required: false
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
body:
description: Body of mail message (might be a filename prefixed with file:// to read from)
@@ -73,5 +73,5 @@ inputs:
description: Custom envelope recipient addresses for SMTP RCPT TO command (separated with comma)
required: false
runs:
using: node20
using: node24
main: main.js

71
main.js
View File

@@ -1,4 +1,5 @@
import nodemailer from "nodemailer";
import addressparser from "nodemailer/lib/addressparser/index.js";
import * as core from "@actions/core";
import * as glob from "@actions/glob";
import fs from "node:fs";
@@ -23,14 +24,6 @@ function getText(textOrFile, convertMarkdown) {
return text;
}
function getFrom(from, username) {
if (from.match(/.+ <.+@.+>/)) {
return from;
}
return `"${from}" <${username}>`;
}
async function getAttachments(attachments) {
const globber = await glob.create(attachments.split(",").join("\n"));
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() {
try {
let serverAddress = core.getInput("server_address");
@@ -117,6 +143,13 @@ async function main() {
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 (!to && !cc && !bcc) {
throw new Error(
@@ -150,11 +183,8 @@ async function main() {
proxy: process.env.HTTP_PROXY,
});
var i = 1;
while (true) {
try {
const info = await transport.sendMail({
from: getFrom(from, username),
const messageOptions = {
from: from,
to: to,
subject: getText(subject, false),
cc: cc ? cc : undefined,
@@ -171,14 +201,13 @@ async function main() {
attachments: attachments
? await getAttachments(attachments)
: undefined,
envelope:
envelopeFrom || envelopeTo
? {
from: envelopeFrom ? envelopeFrom : undefined,
to: envelopeTo ? envelopeTo : undefined,
}
: undefined,
});
envelope: setupEnvelope(envelopeFrom, envelopeTo, from, to, cc, bcc),
};
let i = 1;
while (true) {
try {
const info = await transport.sendMail(messageOptions);
break;
} catch (error) {
if (!error.message.includes("Try again later,")) {

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

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

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

@@ -142,6 +142,8 @@ function Minimatch (pattern, options) {
}
this.options = options
this.maxGlobstarRecursion = options.maxGlobstarRecursion !== undefined
? options.maxGlobstarRecursion : 200
this.set = []
this.pattern = pattern
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
// the parts match.
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',
{ 'this': this, file: file, pattern: pattern })
Minimatch.prototype._matchGlobstar = function (file, pattern, partial, fileIndex, patternIndex) {
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,
pi = 0,
fl = file.length,
pl = pattern.length
// find last globstar
var lastgs = -1
for (i = pattern.length - 1; i >= 0; i--) {
if (pattern[i] === GLOBSTAR) { lastgs = i; break }
}
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++) {
; fi++, pi++
) {
this.debug('matchOne loop')
var p = pattern[pi]
var f = file[fi]
@@ -809,87 +955,7 @@ Minimatch.prototype.matchOne = function (file, pattern, partial) {
// should be impossible.
// some invalid regexp stuff in the set.
/* istanbul ignore if */
if (p === false) 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
}
if (p === false || p === GLOBSTAR) return false
// something other than **
// non-magic patterns just have to match exactly
@@ -906,17 +972,6 @@ Minimatch.prototype.matchOne = function (file, pattern, partial) {
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.
if (fi === fl && pi === pl) {
// 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)",
"name": "minimatch",
"description": "a glob matcher in javascript",
"version": "3.1.3",
"version": "3.1.5",
"publishConfig": {
"tag": "legacy-v3"
},

View File

@@ -1,5 +1,12 @@
# 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)

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) {
let addresses = [];
let walkAddressList = list => {

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

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

View File

@@ -27,6 +27,7 @@ import { errors } from 'undici'
| `InformationalError` | `UND_ERR_INFO` | expected error with reason |
| `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 |
| `MessageSizeExceededError` | `UND_ERR_WS_MESSAGE_SIZE_EXCEEDED` | WebSocket decompressed message exceeded the maximum allowed size |
### `SocketError`

View File

@@ -13,6 +13,14 @@ Arguments:
* **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).
### 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:
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
}
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 = {
AbortError,
HTTPParserError,
@@ -402,5 +420,6 @@ module.exports = {
ResponseExceededMaxSizeError,
RequestRetryError,
ResponseError,
SecureProxyConnectionError
SecureProxyConnectionError,
MessageSizeExceededError
}

View File

@@ -66,6 +66,10 @@ class Request {
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)) {
throw new InvalidArgumentError('invalid headersTimeout')
}
@@ -360,13 +364,19 @@ function processHeader (request, key, 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') {
throw new InvalidArgumentError('invalid host header')
}
// Consumed by Client
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)
if (!Number.isFinite(request.contentLength)) {
throw new InvalidArgumentError('invalid content-length header')

View File

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

View File

@@ -37,6 +37,10 @@ class ByteParser extends Writable {
/** @type {Map<string, PerMessageDeflate>} */
#extensions
/**
* @param {import('./websocket').WebSocket} ws
* @param {Map<string, string>|null} extensions
*/
constructor (ws, extensions) {
super()
@@ -179,6 +183,7 @@ class ByteParser extends Writable {
const buffer = this.consume(8)
const upper = buffer.readUInt32BE(0)
const lower = buffer.readUInt32BE(4)
// 2^31 is the maximum bytes an arraybuffer can contain
// 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://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
if (upper > 2 ** 31 - 1) {
if (upper !== 0 || lower > 2 ** 31 - 1) {
failWebsocketConnection(this.ws, 'Received payload length > 2^31 bytes.')
return
}
const lower = buffer.readUInt32BE(4)
this.#info.payloadLength = (upper << 8) + lower
this.#info.payloadLength = lower
this.#state = parserStates.READ_DATA
} else if (this.#state === parserStates.READ_DATA) {
if (this.#byteOffset < this.#info.payloadLength) {
@@ -223,7 +226,7 @@ class ByteParser extends Writable {
} else {
this.#extensions.get('permessage-deflate').decompress(body, this.#info.fin, (error, data) => {
if (error) {
closeWebSocketConnection(this.ws, 1007, error.message, error.message.length)
failWebsocketConnection(this.ws, error.message)
return
}

View File

@@ -266,6 +266,12 @@ function parseExtensions (extensions) {
* @param {string} 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++) {
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

View File

@@ -431,7 +431,7 @@ class WebSocket extends EventTarget {
* @see https://websockets.spec.whatwg.org/#feedback-from-the-protocol
*/
#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
this[kResponse] = response

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

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

View File

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

View File

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