mirror of
https://github.com/dawidd6/action-send-mail.git
synced 2026-03-15 21:44:04 +07:00
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4758fd2f0e | ||
|
|
512815fe9c | ||
|
|
4ca48c76b4 | ||
|
|
e9d44227e5 | ||
|
|
949ec3d78a | ||
|
|
f0e039bce4 | ||
|
|
41125078ce | ||
|
|
e035119249 | ||
|
|
ba302ba66e | ||
|
|
7c60f8f904 | ||
|
|
09ee758a9b | ||
|
|
5335a581b9 | ||
|
|
63e792e90a | ||
|
|
62a2d05b79 | ||
|
|
a4eb4faebc |
1
.github/workflows/test.yml
vendored
1
.github/workflows/test.yml
vendored
@@ -46,6 +46,7 @@ jobs:
|
||||
convert_markdown: true
|
||||
attachments: package.json,action.yml
|
||||
priority: high
|
||||
headers: '{"X-My-Test-Header": "Passed"}'
|
||||
|
||||
- name: Get mail
|
||||
run: |
|
||||
|
||||
53
README.md
53
README.md
@@ -27,51 +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
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -57,6 +57,9 @@ inputs:
|
||||
priority:
|
||||
description: Set Priority level for the mail message to 'high', 'normal' (default) or 'low'
|
||||
required: false
|
||||
headers:
|
||||
description: 'Custom headers for the mail message as a JSON string (e.g., {"X-Custom-Header": "value"})'
|
||||
required: false
|
||||
nodemailerlog:
|
||||
description: Log option for nodemailer
|
||||
required: false
|
||||
@@ -70,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
|
||||
|
||||
73
main.js
73
main.js
@@ -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");
|
||||
@@ -115,6 +141,14 @@ async function main() {
|
||||
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 (!to && !cc && !bcc) {
|
||||
@@ -149,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,
|
||||
@@ -166,17 +197,17 @@ async function main() {
|
||||
? getText(htmlBody, convertMarkdown)
|
||||
: undefined,
|
||||
priority: priority ? priority : undefined,
|
||||
headers: headers ? JSON.parse(headers) : undefined,
|
||||
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
18
node_modules/.package-lock.json
generated
vendored
@@ -185,9 +185,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"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": "7.0.13",
|
||||
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.13.tgz",
|
||||
"integrity": "sha512-PNDFSJdP+KFgdsG3ZzMXCgquO7I6McjY2vlqILjtJd0hy8wEvtugS9xKRF2NWlPNGxvLCXlTNIae4serI7dinw==",
|
||||
"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
37
node_modules/minimatch/README.md
generated
vendored
@@ -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
|
||||
|
||||
260
node_modules/minimatch/minimatch.js
generated
vendored
260
node_modules/minimatch/minimatch.js
generated
vendored
@@ -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
|
||||
@@ -390,6 +392,9 @@ function parse (pattern, isSub) {
|
||||
continue
|
||||
}
|
||||
|
||||
// coalesce consecutive non-globstar * characters
|
||||
if (c === '*' && stateChar === '*') continue
|
||||
|
||||
// if we already have a stateChar, then it means
|
||||
// that there was something like ** or +? in there.
|
||||
// Handle the stateChar, then proceed with this one.
|
||||
@@ -784,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]
|
||||
@@ -806,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
|
||||
@@ -903,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.
|
||||
|
||||
4
node_modules/minimatch/package.json
generated
vendored
4
node_modules/minimatch/package.json
generated
vendored
@@ -2,9 +2,9 @@
|
||||
"author": "Isaac Z. Schlueter <i@izs.me> (http://blog.izs.me)",
|
||||
"name": "minimatch",
|
||||
"description": "a glob matcher in javascript",
|
||||
"version": "3.1.2",
|
||||
"version": "3.1.5",
|
||||
"publishConfig": {
|
||||
"tag": "v3-legacy"
|
||||
"tag": "legacy-v3"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
30
node_modules/nodemailer/CHANGELOG.md
generated
vendored
30
node_modules/nodemailer/CHANGELOG.md
generated
vendored
@@ -1,5 +1,35 @@
|
||||
# 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)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* absorb TLS errors during socket teardown ([7f8dde4](https://github.com/nodemailer/nodemailer/commit/7f8dde41438c66b8311e888fa5f8c518fcaba6f1))
|
||||
* absorb TLS errors during socket teardown ([381f628](https://github.com/nodemailer/nodemailer/commit/381f628d55e62bb3131bd2a452fa1ce00bc48aea))
|
||||
* Add Gmail Workspace service configuration ([#1787](https://github.com/nodemailer/nodemailer/issues/1787)) ([dc97ede](https://github.com/nodemailer/nodemailer/commit/dc97ede417b3030b311771541b1f17f5ca76bcbf))
|
||||
|
||||
## [8.0.0](https://github.com/nodemailer/nodemailer/compare/v7.0.13...v8.0.0) (2026-02-04)
|
||||
|
||||
|
||||
### ⚠ BREAKING CHANGES
|
||||
|
||||
* Error code 'NoAuth' renamed to 'ENOAUTH'
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add connection fallback to alternative DNS addresses ([e726d6f](https://github.com/nodemailer/nodemailer/commit/e726d6f44aa7ca14e943d4303243cb5494b09c75))
|
||||
* centralize and standardize error codes ([45062ce](https://github.com/nodemailer/nodemailer/commit/45062ce7a4705f3e63c5d9e606547f4d99fd29b5))
|
||||
* harden DNS fallback against race conditions and cleanup issues ([4fa3c63](https://github.com/nodemailer/nodemailer/commit/4fa3c63a1f36aefdbaea7f57a133adc458413a47))
|
||||
* improve socket cleanup to prevent potential memory leaks ([6069fdc](https://github.com/nodemailer/nodemailer/commit/6069fdcff68a3eef9a9bb16b2bf5ddb924c02091))
|
||||
|
||||
## [7.0.13](https://github.com/nodemailer/nodemailer/compare/v7.0.12...v7.0.13) (2026-01-27)
|
||||
|
||||
|
||||
|
||||
14
node_modules/nodemailer/lib/addressparser/index.js
generated
vendored
14
node_modules/nodemailer/lib/addressparser/index.js
generated
vendored
@@ -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 => {
|
||||
|
||||
61
node_modules/nodemailer/lib/errors.js
generated
vendored
Normal file
61
node_modules/nodemailer/lib/errors.js
generated
vendored
Normal file
@@ -0,0 +1,61 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Nodemailer Error Codes
|
||||
*
|
||||
* Centralized error code definitions for consistent error handling.
|
||||
*
|
||||
* Usage:
|
||||
* const errors = require('./errors');
|
||||
* let err = new Error('Connection closed');
|
||||
* err.code = errors.ECONNECTION;
|
||||
*/
|
||||
|
||||
/**
|
||||
* Error code descriptions for documentation and debugging
|
||||
*/
|
||||
const ERROR_CODES = {
|
||||
// Connection errors
|
||||
ECONNECTION: 'Connection closed unexpectedly',
|
||||
ETIMEDOUT: 'Connection or operation timed out',
|
||||
ESOCKET: 'Socket-level error',
|
||||
EDNS: 'DNS resolution failed',
|
||||
|
||||
// TLS/Security errors
|
||||
ETLS: 'TLS handshake or STARTTLS failed',
|
||||
EREQUIRETLS: 'REQUIRETLS not supported by server (RFC 8689)',
|
||||
|
||||
// Protocol errors
|
||||
EPROTOCOL: 'Invalid SMTP server response',
|
||||
EENVELOPE: 'Invalid mail envelope (sender or recipients)',
|
||||
EMESSAGE: 'Message delivery error',
|
||||
ESTREAM: 'Stream processing error',
|
||||
|
||||
// Authentication errors
|
||||
EAUTH: 'Authentication failed',
|
||||
ENOAUTH: 'Authentication credentials not provided',
|
||||
EOAUTH2: 'OAuth2 token generation or refresh error',
|
||||
|
||||
// Resource errors
|
||||
EMAXLIMIT: 'Pool resource limit reached (max messages per connection)',
|
||||
|
||||
// Transport-specific errors
|
||||
ESENDMAIL: 'Sendmail command error',
|
||||
ESES: 'AWS SES transport error',
|
||||
|
||||
// Configuration and access errors
|
||||
ECONFIG: 'Invalid configuration',
|
||||
EPROXY: 'Proxy connection error',
|
||||
EFILEACCESS: 'File access rejected (disableFileAccess is set)',
|
||||
EURLACCESS: 'URL access rejected (disableUrlAccess is set)',
|
||||
EFETCH: 'HTTP fetch error'
|
||||
};
|
||||
|
||||
// Export error codes as string constants and the full definitions object
|
||||
module.exports = Object.keys(ERROR_CODES).reduce(
|
||||
(exports, code) => {
|
||||
exports[code] = code;
|
||||
return exports;
|
||||
},
|
||||
{ ERROR_CODES }
|
||||
);
|
||||
21
node_modules/nodemailer/lib/fetch/index.js
generated
vendored
21
node_modules/nodemailer/lib/fetch/index.js
generated
vendored
@@ -8,6 +8,7 @@ const PassThrough = require('stream').PassThrough;
|
||||
const Cookies = require('./cookies');
|
||||
const packageData = require('../../package.json');
|
||||
const net = require('net');
|
||||
const errors = require('../errors');
|
||||
|
||||
const MAX_REDIRECTS = 5;
|
||||
|
||||
@@ -76,7 +77,7 @@ function nmfetch(url, options) {
|
||||
return;
|
||||
}
|
||||
finished = true;
|
||||
err.type = 'FETCH';
|
||||
err.code = errors.EFETCH;
|
||||
err.sourceUrl = url;
|
||||
fetchRes.emit('error', err);
|
||||
});
|
||||
@@ -99,7 +100,7 @@ function nmfetch(url, options) {
|
||||
return;
|
||||
}
|
||||
finished = true;
|
||||
E.type = 'FETCH';
|
||||
E.code = errors.EFETCH;
|
||||
E.sourceUrl = url;
|
||||
fetchRes.emit('error', E);
|
||||
return;
|
||||
@@ -147,7 +148,7 @@ function nmfetch(url, options) {
|
||||
} catch (E) {
|
||||
finished = true;
|
||||
setImmediate(() => {
|
||||
E.type = 'FETCH';
|
||||
E.code = errors.EFETCH;
|
||||
E.sourceUrl = url;
|
||||
fetchRes.emit('error', E);
|
||||
});
|
||||
@@ -162,7 +163,7 @@ function nmfetch(url, options) {
|
||||
finished = true;
|
||||
req.abort();
|
||||
let err = new Error('Request Timeout');
|
||||
err.type = 'FETCH';
|
||||
err.code = errors.EFETCH;
|
||||
err.sourceUrl = url;
|
||||
fetchRes.emit('error', err);
|
||||
});
|
||||
@@ -173,7 +174,7 @@ function nmfetch(url, options) {
|
||||
return;
|
||||
}
|
||||
finished = true;
|
||||
err.type = 'FETCH';
|
||||
err.code = errors.EFETCH;
|
||||
err.sourceUrl = url;
|
||||
fetchRes.emit('error', err);
|
||||
});
|
||||
@@ -204,7 +205,7 @@ function nmfetch(url, options) {
|
||||
if (options.redirects > options.maxRedirects) {
|
||||
finished = true;
|
||||
let err = new Error('Maximum redirect count exceeded');
|
||||
err.type = 'FETCH';
|
||||
err.code = errors.EFETCH;
|
||||
err.sourceUrl = url;
|
||||
fetchRes.emit('error', err);
|
||||
req.abort();
|
||||
@@ -222,7 +223,7 @@ function nmfetch(url, options) {
|
||||
if (res.statusCode >= 300 && !options.allowErrorResponse) {
|
||||
finished = true;
|
||||
let err = new Error('Invalid status code ' + res.statusCode);
|
||||
err.type = 'FETCH';
|
||||
err.code = errors.EFETCH;
|
||||
err.sourceUrl = url;
|
||||
fetchRes.emit('error', err);
|
||||
req.abort();
|
||||
@@ -234,7 +235,7 @@ function nmfetch(url, options) {
|
||||
return;
|
||||
}
|
||||
finished = true;
|
||||
err.type = 'FETCH';
|
||||
err.code = errors.EFETCH;
|
||||
err.sourceUrl = url;
|
||||
fetchRes.emit('error', err);
|
||||
req.abort();
|
||||
@@ -247,7 +248,7 @@ function nmfetch(url, options) {
|
||||
return;
|
||||
}
|
||||
finished = true;
|
||||
err.type = 'FETCH';
|
||||
err.code = errors.EFETCH;
|
||||
err.sourceUrl = url;
|
||||
fetchRes.emit('error', err);
|
||||
req.abort();
|
||||
@@ -267,7 +268,7 @@ function nmfetch(url, options) {
|
||||
}
|
||||
} catch (err) {
|
||||
finished = true;
|
||||
err.type = 'FETCH';
|
||||
err.code = errors.EFETCH;
|
||||
err.sourceUrl = url;
|
||||
fetchRes.emit('error', err);
|
||||
return;
|
||||
|
||||
9
node_modules/nodemailer/lib/mailer/index.js
generated
vendored
9
node_modules/nodemailer/lib/mailer/index.js
generated
vendored
@@ -6,6 +6,7 @@ const mimeTypes = require('../mime-funcs/mime-types');
|
||||
const MailComposer = require('../mail-composer');
|
||||
const DKIM = require('../dkim');
|
||||
const httpProxyClient = require('../smtp-connection/http-proxy-client');
|
||||
const errors = require('../errors');
|
||||
const util = require('util');
|
||||
const urllib = require('url');
|
||||
const packageData = require('../../package.json');
|
||||
@@ -337,7 +338,9 @@ class Mail extends EventEmitter {
|
||||
case 'socks4':
|
||||
case 'socks4a': {
|
||||
if (!this.meta.has('proxy_socks_module')) {
|
||||
return callback(new Error('Socks module not loaded'));
|
||||
let err = new Error('Socks module not loaded');
|
||||
err.code = errors.EPROXY;
|
||||
return callback(err);
|
||||
}
|
||||
let connect = ipaddress => {
|
||||
let proxyV2 = !!this.meta.get('proxy_socks_module').SocksClient;
|
||||
@@ -394,7 +397,9 @@ class Mail extends EventEmitter {
|
||||
});
|
||||
}
|
||||
}
|
||||
callback(new Error('Unknown proxy configuration'));
|
||||
let err = new Error('Unknown proxy configuration');
|
||||
err.code = errors.EPROXY;
|
||||
callback(err);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
13
node_modules/nodemailer/lib/mime-node/index.js
generated
vendored
13
node_modules/nodemailer/lib/mime-node/index.js
generated
vendored
@@ -13,6 +13,7 @@ const qp = require('../qp');
|
||||
const base64 = require('../base64');
|
||||
const addressparser = require('../addressparser');
|
||||
const nmfetch = require('../fetch');
|
||||
const errors = require('../errors');
|
||||
const LastNewline = require('./last-newline');
|
||||
|
||||
const LeWindows = require('./le-windows');
|
||||
@@ -979,7 +980,11 @@ class MimeNode {
|
||||
} else if (content && typeof content.path === 'string' && !content.href) {
|
||||
if (this.disableFileAccess) {
|
||||
contentStream = new PassThrough();
|
||||
setImmediate(() => contentStream.emit('error', new Error('File access rejected for ' + content.path)));
|
||||
setImmediate(() => {
|
||||
let err = new Error('File access rejected for ' + content.path);
|
||||
err.code = errors.EFILEACCESS;
|
||||
contentStream.emit('error', err);
|
||||
});
|
||||
return contentStream;
|
||||
}
|
||||
// read file
|
||||
@@ -987,7 +992,11 @@ class MimeNode {
|
||||
} else if (content && typeof content.href === 'string') {
|
||||
if (this.disableUrlAccess) {
|
||||
contentStream = new PassThrough();
|
||||
setImmediate(() => contentStream.emit('error', new Error('Url access rejected for ' + content.href)));
|
||||
setImmediate(() => {
|
||||
let err = new Error('Url access rejected for ' + content.href);
|
||||
err.code = errors.EURLACCESS;
|
||||
contentStream.emit('error', err);
|
||||
});
|
||||
return contentStream;
|
||||
}
|
||||
// fetch URL
|
||||
|
||||
3
node_modules/nodemailer/lib/nodemailer.js
generated
vendored
3
node_modules/nodemailer/lib/nodemailer.js
generated
vendored
@@ -8,6 +8,7 @@ const SendmailTransport = require('./sendmail-transport');
|
||||
const StreamTransport = require('./stream-transport');
|
||||
const JSONTransport = require('./json-transport');
|
||||
const SESTransport = require('./ses-transport');
|
||||
const errors = require('./errors');
|
||||
const nmfetch = require('./fetch');
|
||||
const packageData = require('../package.json');
|
||||
|
||||
@@ -49,7 +50,7 @@ module.exports.createTransport = function (transporter, defaults) {
|
||||
let error = new Error(
|
||||
'Using legacy SES configuration, expecting @aws-sdk/client-sesv2, see https://nodemailer.com/transports/ses/'
|
||||
);
|
||||
error.code = 'LegacyConfig';
|
||||
error.code = errors.ECONFIG;
|
||||
throw error;
|
||||
}
|
||||
transporter = new SESTransport(options);
|
||||
|
||||
10
node_modules/nodemailer/lib/sendmail-transport/index.js
generated
vendored
10
node_modules/nodemailer/lib/sendmail-transport/index.js
generated
vendored
@@ -3,6 +3,7 @@
|
||||
const spawn = require('child_process').spawn;
|
||||
const packageData = require('../../package.json');
|
||||
const shared = require('../shared');
|
||||
const errors = require('../errors');
|
||||
|
||||
/**
|
||||
* Generates a Transport object for Sendmail
|
||||
@@ -72,7 +73,9 @@ class SendmailTransport {
|
||||
.concat(envelope.to || [])
|
||||
.some(addr => /^-/.test(addr));
|
||||
if (hasInvalidAddresses) {
|
||||
return done(new Error('Can not send mail. Invalid envelope addresses.'));
|
||||
let err = new Error('Can not send mail. Invalid envelope addresses.');
|
||||
err.code = errors.ESENDMAIL;
|
||||
return done(err);
|
||||
}
|
||||
|
||||
if (this.args) {
|
||||
@@ -141,6 +144,7 @@ class SendmailTransport {
|
||||
} else {
|
||||
err = new Error('Sendmail exited with code ' + code);
|
||||
}
|
||||
err.code = errors.ESENDMAIL;
|
||||
|
||||
this.logger.error(
|
||||
{
|
||||
@@ -202,7 +206,9 @@ class SendmailTransport {
|
||||
|
||||
sourceStream.pipe(sendmail.stdin);
|
||||
} else {
|
||||
return callback(new Error('sendmail was not found'));
|
||||
let err = new Error('sendmail was not found');
|
||||
err.code = errors.ESENDMAIL;
|
||||
return callback(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
128
node_modules/nodemailer/lib/shared/index.js
generated
vendored
128
node_modules/nodemailer/lib/shared/index.js
generated
vendored
@@ -82,15 +82,22 @@ const formatDNSValue = (value, extra) => {
|
||||
return Object.assign({}, extra || {});
|
||||
}
|
||||
|
||||
let addresses = value.addresses || [];
|
||||
|
||||
// Select a random address from available addresses, or null if none
|
||||
let host = null;
|
||||
if (addresses.length === 1) {
|
||||
host = addresses[0];
|
||||
} else if (addresses.length > 1) {
|
||||
host = addresses[Math.floor(Math.random() * addresses.length)];
|
||||
}
|
||||
|
||||
return Object.assign(
|
||||
{
|
||||
servername: value.servername,
|
||||
host:
|
||||
!value.addresses || !value.addresses.length
|
||||
? null
|
||||
: value.addresses.length === 1
|
||||
? value.addresses[0]
|
||||
: value.addresses[Math.floor(Math.random() * value.addresses.length)]
|
||||
host,
|
||||
// Include all addresses for connection fallback support
|
||||
_addresses: addresses
|
||||
},
|
||||
extra || {}
|
||||
);
|
||||
@@ -151,66 +158,32 @@ module.exports.resolveHostname = (options, callback) => {
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve both IPv4 and IPv6 addresses for fallback support
|
||||
let ipv4Addresses = [];
|
||||
let ipv6Addresses = [];
|
||||
let ipv4Error = null;
|
||||
let ipv6Error = null;
|
||||
|
||||
resolver(4, options.host, options, (err, addresses) => {
|
||||
if (err) {
|
||||
if (cached) {
|
||||
dnsCache.set(options.host, {
|
||||
value: cached.value,
|
||||
expires: Date.now() + (options.dnsTtl || DNS_TTL)
|
||||
});
|
||||
|
||||
return callback(
|
||||
null,
|
||||
formatDNSValue(cached.value, {
|
||||
cached: true,
|
||||
error: err
|
||||
})
|
||||
);
|
||||
}
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (addresses && addresses.length) {
|
||||
let value = {
|
||||
addresses,
|
||||
servername: options.servername || options.host
|
||||
};
|
||||
|
||||
dnsCache.set(options.host, {
|
||||
value,
|
||||
expires: Date.now() + (options.dnsTtl || DNS_TTL)
|
||||
});
|
||||
|
||||
return callback(
|
||||
null,
|
||||
formatDNSValue(value, {
|
||||
cached: false
|
||||
})
|
||||
);
|
||||
ipv4Error = err;
|
||||
} else {
|
||||
ipv4Addresses = addresses || [];
|
||||
}
|
||||
|
||||
resolver(6, options.host, options, (err, addresses) => {
|
||||
if (err) {
|
||||
if (cached) {
|
||||
dnsCache.set(options.host, {
|
||||
value: cached.value,
|
||||
expires: Date.now() + (options.dnsTtl || DNS_TTL)
|
||||
});
|
||||
|
||||
return callback(
|
||||
null,
|
||||
formatDNSValue(cached.value, {
|
||||
cached: true,
|
||||
error: err
|
||||
})
|
||||
);
|
||||
}
|
||||
return callback(err);
|
||||
ipv6Error = err;
|
||||
} else {
|
||||
ipv6Addresses = addresses || [];
|
||||
}
|
||||
|
||||
if (addresses && addresses.length) {
|
||||
// Combine addresses: IPv4 first, then IPv6
|
||||
let allAddresses = ipv4Addresses.concat(ipv6Addresses);
|
||||
|
||||
if (allAddresses.length) {
|
||||
let value = {
|
||||
addresses,
|
||||
addresses: allAddresses,
|
||||
servername: options.servername || options.host
|
||||
};
|
||||
|
||||
@@ -227,6 +200,25 @@ module.exports.resolveHostname = (options, callback) => {
|
||||
);
|
||||
}
|
||||
|
||||
// No addresses from resolve4/resolve6, try dns.lookup as fallback
|
||||
if (ipv4Error && ipv6Error) {
|
||||
// Both resolvers had errors
|
||||
if (cached) {
|
||||
dnsCache.set(options.host, {
|
||||
value: cached.value,
|
||||
expires: Date.now() + (options.dnsTtl || DNS_TTL)
|
||||
});
|
||||
|
||||
return callback(
|
||||
null,
|
||||
formatDNSValue(cached.value, {
|
||||
cached: true,
|
||||
error: ipv4Error
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
dns.lookup(options.host, { all: true }, (err, addresses) => {
|
||||
if (err) {
|
||||
@@ -247,19 +239,17 @@ module.exports.resolveHostname = (options, callback) => {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let address = addresses
|
||||
? addresses
|
||||
.filter(addr => isFamilySupported(addr.family))
|
||||
.map(addr => addr.address)
|
||||
.shift()
|
||||
: false;
|
||||
// Get all supported addresses from dns.lookup
|
||||
let supportedAddresses = addresses
|
||||
? addresses.filter(addr => isFamilySupported(addr.family)).map(addr => addr.address)
|
||||
: [];
|
||||
|
||||
if (addresses && addresses.length && !address) {
|
||||
if (addresses && addresses.length && !supportedAddresses.length) {
|
||||
// there are addresses but none can be used
|
||||
console.warn(`Failed to resolve IPv${addresses[0].family} addresses with current network`);
|
||||
}
|
||||
|
||||
if (!address && cached) {
|
||||
if (!supportedAddresses.length && cached) {
|
||||
// nothing was found, fallback to cached value
|
||||
return callback(
|
||||
null,
|
||||
@@ -270,7 +260,7 @@ module.exports.resolveHostname = (options, callback) => {
|
||||
}
|
||||
|
||||
let value = {
|
||||
addresses: address ? [address] : [options.host],
|
||||
addresses: supportedAddresses.length ? supportedAddresses : [options.host],
|
||||
servername: options.servername || options.host
|
||||
};
|
||||
|
||||
@@ -286,7 +276,7 @@ module.exports.resolveHostname = (options, callback) => {
|
||||
})
|
||||
);
|
||||
});
|
||||
} catch (_err) {
|
||||
} catch (lookupErr) {
|
||||
if (cached) {
|
||||
dnsCache.set(options.host, {
|
||||
value: cached.value,
|
||||
@@ -297,11 +287,11 @@ module.exports.resolveHostname = (options, callback) => {
|
||||
null,
|
||||
formatDNSValue(cached.value, {
|
||||
cached: true,
|
||||
error: err
|
||||
error: lookupErr
|
||||
})
|
||||
);
|
||||
}
|
||||
return callback(err);
|
||||
return callback(ipv4Error || ipv6Error || lookupErr);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
5
node_modules/nodemailer/lib/smtp-connection/http-proxy-client.js
generated
vendored
5
node_modules/nodemailer/lib/smtp-connection/http-proxy-client.js
generated
vendored
@@ -7,6 +7,7 @@
|
||||
const net = require('net');
|
||||
const tls = require('tls');
|
||||
const urllib = require('url');
|
||||
const errors = require('../errors');
|
||||
|
||||
/**
|
||||
* Establishes proxied connection to destinationPort
|
||||
@@ -121,7 +122,9 @@ function httpProxyClient(proxyUrl, destinationPort, destinationHost, callback) {
|
||||
} catch (_E) {
|
||||
// ignore
|
||||
}
|
||||
return callback(new Error('Invalid response from proxy' + ((match && ': ' + match[1]) || '')));
|
||||
let err = new Error('Invalid response from proxy' + ((match && ': ' + match[1]) || ''));
|
||||
err.code = errors.EPROXY;
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
socket.removeListener('error', tempSocketErr);
|
||||
|
||||
144
node_modules/nodemailer/lib/smtp-connection/index.js
generated
vendored
144
node_modules/nodemailer/lib/smtp-connection/index.js
generated
vendored
@@ -15,6 +15,7 @@ const CONNECTION_TIMEOUT = 2 * 60 * 1000; // how much to wait for the connection
|
||||
const SOCKET_TIMEOUT = 10 * 60 * 1000; // how much to wait for socket inactivity before disconnecting the client
|
||||
const GREETING_TIMEOUT = 30 * 1000; // how much to wait after connection is established but SMTP greeting is not receieved
|
||||
const DNS_TIMEOUT = 30 * 1000; // how much to wait for resolveHostname
|
||||
const TEARDOWN_NOOP = () => {}; // reusable no-op handler for absorbing errors during socket teardown
|
||||
|
||||
/**
|
||||
* Generates a SMTP connection object
|
||||
@@ -197,6 +198,17 @@ class SMTPConnection extends EventEmitter {
|
||||
this._onSocketClose = () => this._onClose();
|
||||
this._onSocketEnd = () => this._onEnd();
|
||||
this._onSocketTimeout = () => this._onTimeout();
|
||||
|
||||
/**
|
||||
* Connection-phase error handler (supports fallback to alternative addresses)
|
||||
*/
|
||||
this._onConnectionSocketError = err => this._onConnectionError(err, 'ESOCKET');
|
||||
|
||||
/**
|
||||
* Connection attempt counter for fallback race condition protection
|
||||
* @private
|
||||
*/
|
||||
this._connectionAttemptId = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -232,18 +244,10 @@ class SMTPConnection extends EventEmitter {
|
||||
opts.localAddress = this.options.localAddress;
|
||||
}
|
||||
|
||||
let setupConnectionHandlers = () => {
|
||||
this._connectionTimeout = setTimeout(() => {
|
||||
this._onError('Connection timeout', 'ETIMEDOUT', false, 'CONN');
|
||||
}, this.options.connectionTimeout || CONNECTION_TIMEOUT);
|
||||
|
||||
this._socket.on('error', this._onSocketError);
|
||||
};
|
||||
|
||||
if (this.options.connection) {
|
||||
// connection is already opened
|
||||
this._socket = this.options.connection;
|
||||
setupConnectionHandlers();
|
||||
this._setupConnectionHandlers();
|
||||
|
||||
if (this.secureConnection && !this.alreadySecured) {
|
||||
setImmediate(() =>
|
||||
@@ -288,7 +292,7 @@ class SMTPConnection extends EventEmitter {
|
||||
this._socket.setKeepAlive(true);
|
||||
this._onConnect();
|
||||
});
|
||||
setupConnectionHandlers();
|
||||
this._setupConnectionHandlers();
|
||||
} catch (E) {
|
||||
return setImmediate(() => this._onError(E, 'ECONNECTION', false, 'CONN'));
|
||||
}
|
||||
@@ -327,15 +331,12 @@ class SMTPConnection extends EventEmitter {
|
||||
opts[key] = resolved[key];
|
||||
}
|
||||
});
|
||||
try {
|
||||
this._socket = tls.connect(opts, () => {
|
||||
this._socket.setKeepAlive(true);
|
||||
this._onConnect();
|
||||
});
|
||||
setupConnectionHandlers();
|
||||
} catch (E) {
|
||||
return setImmediate(() => this._onError(E, 'ECONNECTION', false, 'CONN'));
|
||||
}
|
||||
|
||||
// Store fallback addresses for retry on connection failure
|
||||
this._fallbackAddresses = (resolved._addresses || []).filter(addr => addr !== opts.host);
|
||||
this._connectOpts = Object.assign({}, opts);
|
||||
|
||||
this._connectToHost(opts, true);
|
||||
});
|
||||
} else {
|
||||
// connect using plaintext
|
||||
@@ -360,17 +361,99 @@ class SMTPConnection extends EventEmitter {
|
||||
opts[key] = resolved[key];
|
||||
}
|
||||
});
|
||||
|
||||
// Store fallback addresses for retry on connection failure
|
||||
this._fallbackAddresses = (resolved._addresses || []).filter(addr => addr !== opts.host);
|
||||
this._connectOpts = Object.assign({}, opts);
|
||||
|
||||
this._connectToHost(opts, false);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to connect to the specified host address
|
||||
*
|
||||
* @param {Object} opts Connection options
|
||||
* @param {Boolean} secure Whether to use TLS
|
||||
*/
|
||||
_connectToHost(opts, secure) {
|
||||
this._connectionAttemptId++;
|
||||
const currentAttemptId = this._connectionAttemptId;
|
||||
|
||||
let connectFn = secure ? tls.connect : net.connect;
|
||||
try {
|
||||
this._socket = net.connect(opts, () => {
|
||||
this._socket = connectFn(opts, () => {
|
||||
// Ignore callback if this is a stale connection attempt
|
||||
if (this._connectionAttemptId !== currentAttemptId) {
|
||||
return;
|
||||
}
|
||||
this._socket.setKeepAlive(true);
|
||||
this._onConnect();
|
||||
});
|
||||
setupConnectionHandlers();
|
||||
this._setupConnectionHandlers();
|
||||
} catch (E) {
|
||||
return setImmediate(() => this._onError(E, 'ECONNECTION', false, 'CONN'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up connection timeout and error handlers
|
||||
*/
|
||||
_setupConnectionHandlers() {
|
||||
this._connectionTimeout = setTimeout(() => {
|
||||
this._onConnectionError('Connection timeout', 'ETIMEDOUT');
|
||||
}, this.options.connectionTimeout || CONNECTION_TIMEOUT);
|
||||
|
||||
this._socket.on('error', this._onConnectionSocketError);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles connection errors with fallback to alternative addresses
|
||||
*
|
||||
* @param {Error|String} err Error object or message
|
||||
* @param {String} code Error code
|
||||
*/
|
||||
_onConnectionError(err, code) {
|
||||
clearTimeout(this._connectionTimeout);
|
||||
|
||||
// Check if we have fallback addresses to try
|
||||
let canFallback = this._fallbackAddresses && this._fallbackAddresses.length && this.stage === 'init' && !this._destroyed;
|
||||
|
||||
if (!canFallback) {
|
||||
// No more fallback addresses, report the error
|
||||
this._onError(err, code, false, 'CONN');
|
||||
return;
|
||||
}
|
||||
|
||||
let nextHost = this._fallbackAddresses.shift();
|
||||
|
||||
this.logger.info(
|
||||
{
|
||||
tnx: 'network',
|
||||
failedHost: this._connectOpts.host,
|
||||
nextHost,
|
||||
error: err.message || err
|
||||
},
|
||||
'Connection to %s failed, trying %s',
|
||||
this._connectOpts.host,
|
||||
nextHost
|
||||
);
|
||||
|
||||
// Clean up current socket
|
||||
if (this._socket) {
|
||||
try {
|
||||
this._socket.removeListener('error', this._onConnectionSocketError);
|
||||
this._socket.destroy();
|
||||
} catch (_E) {
|
||||
// ignore
|
||||
}
|
||||
this._socket = null;
|
||||
}
|
||||
|
||||
// Update host and retry
|
||||
this._connectOpts.host = nextHost;
|
||||
this._connectToHost(this._connectOpts, this.secureConnection);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -414,6 +497,18 @@ class SMTPConnection extends EventEmitter {
|
||||
|
||||
if (socket && !socket.destroyed) {
|
||||
try {
|
||||
// Clear socket timeout to prevent timer leaks
|
||||
socket.setTimeout(0);
|
||||
// Remove all listeners to allow proper garbage collection
|
||||
socket.removeListener('data', this._onSocketData);
|
||||
socket.removeListener('timeout', this._onSocketTimeout);
|
||||
socket.removeListener('close', this._onSocketClose);
|
||||
socket.removeListener('end', this._onSocketEnd);
|
||||
socket.removeListener('error', this._onSocketError);
|
||||
socket.removeListener('error', this._onConnectionSocketError);
|
||||
// Absorb errors that may fire during socket teardown (e.g. server
|
||||
// sending cleartext after TLS shutdown triggers ERR_SSL_BAD_RECORD_TYPE)
|
||||
socket.on('error', TEARDOWN_NOOP);
|
||||
socket[closeMethod]();
|
||||
} catch (_E) {
|
||||
// just ignore
|
||||
@@ -715,7 +810,10 @@ class SMTPConnection extends EventEmitter {
|
||||
this._socket.removeListener('timeout', this._onSocketTimeout);
|
||||
this._socket.removeListener('close', this._onSocketClose);
|
||||
this._socket.removeListener('end', this._onSocketEnd);
|
||||
// Switch from connection-phase error handler to normal error handler
|
||||
this._socket.removeListener('error', this._onConnectionSocketError);
|
||||
|
||||
this._socket.on('error', this._onSocketError);
|
||||
this._socket.on('data', this._onSocketData);
|
||||
this._socket.once('close', this._onSocketClose);
|
||||
this._socket.once('end', this._onSocketEnd);
|
||||
@@ -941,8 +1039,10 @@ class SMTPConnection extends EventEmitter {
|
||||
this.upgrading = false;
|
||||
this._socket.on('data', this._onSocketData);
|
||||
|
||||
// Remove all listeners from the plain socket to allow proper garbage collection
|
||||
socketPlain.removeListener('close', this._onSocketClose);
|
||||
socketPlain.removeListener('end', this._onSocketEnd);
|
||||
socketPlain.removeListener('error', this._onSocketError);
|
||||
|
||||
return callback(null, true);
|
||||
});
|
||||
|
||||
3
node_modules/nodemailer/lib/smtp-pool/index.js
generated
vendored
3
node_modules/nodemailer/lib/smtp-pool/index.js
generated
vendored
@@ -5,6 +5,7 @@ const PoolResource = require('./pool-resource');
|
||||
const SMTPConnection = require('../smtp-connection');
|
||||
const wellKnown = require('../well-known');
|
||||
const shared = require('../shared');
|
||||
const errors = require('../errors');
|
||||
const packageData = require('../../package.json');
|
||||
|
||||
/**
|
||||
@@ -633,7 +634,7 @@ class SMTPPool extends EventEmitter {
|
||||
});
|
||||
} else if (!auth && connection.allowsAuth && options.forceAuth) {
|
||||
let err = new Error('Authentication info was not provided');
|
||||
err.code = 'NoAuth';
|
||||
err.code = errors.ENOAUTH;
|
||||
|
||||
returned = true;
|
||||
connection.close();
|
||||
|
||||
5
node_modules/nodemailer/lib/smtp-pool/pool-resource.js
generated
vendored
5
node_modules/nodemailer/lib/smtp-pool/pool-resource.js
generated
vendored
@@ -3,6 +3,7 @@
|
||||
const SMTPConnection = require('../smtp-connection');
|
||||
const assign = require('../shared').assign;
|
||||
const XOAuth2 = require('../xoauth2');
|
||||
const errors = require('../errors');
|
||||
const EventEmitter = require('events');
|
||||
|
||||
/**
|
||||
@@ -121,7 +122,7 @@ class PoolResource extends EventEmitter {
|
||||
let err = new Error('Unexpected socket close');
|
||||
if (this.connection && this.connection._socket && this.connection._socket.upgrading) {
|
||||
// starttls connection errors
|
||||
err.code = 'ETLS';
|
||||
err.code = errors.ETLS;
|
||||
}
|
||||
callback(err);
|
||||
}, 1000);
|
||||
@@ -226,7 +227,7 @@ class PoolResource extends EventEmitter {
|
||||
let err;
|
||||
if (this.messages >= this.options.maxMessages) {
|
||||
err = new Error('Resource exhausted');
|
||||
err.code = 'EMAXLIMIT';
|
||||
err.code = errors.EMAXLIMIT;
|
||||
this.connection.close();
|
||||
this.emit('error', err);
|
||||
} else {
|
||||
|
||||
5
node_modules/nodemailer/lib/smtp-transport/index.js
generated
vendored
5
node_modules/nodemailer/lib/smtp-transport/index.js
generated
vendored
@@ -5,6 +5,7 @@ const SMTPConnection = require('../smtp-connection');
|
||||
const wellKnown = require('../well-known');
|
||||
const shared = require('../shared');
|
||||
const XOAuth2 = require('../xoauth2');
|
||||
const errors = require('../errors');
|
||||
const packageData = require('../../package.json');
|
||||
|
||||
/**
|
||||
@@ -190,7 +191,7 @@ class SMTPTransport extends EventEmitter {
|
||||
let err = new Error('Unexpected socket close');
|
||||
if (connection && connection._socket && connection._socket.upgrading) {
|
||||
// starttls connection errors
|
||||
err.code = 'ETLS';
|
||||
err.code = errors.ETLS;
|
||||
}
|
||||
callback(err);
|
||||
}, 1000);
|
||||
@@ -392,7 +393,7 @@ class SMTPTransport extends EventEmitter {
|
||||
});
|
||||
} else if (!authData && connection.allowsAuth && options.forceAuth) {
|
||||
let err = new Error('Authentication info was not provided');
|
||||
err.code = 'NoAuth';
|
||||
err.code = errors.ENOAUTH;
|
||||
|
||||
returned = true;
|
||||
connection.close();
|
||||
|
||||
8
node_modules/nodemailer/lib/well-known/services.json
generated
vendored
8
node_modules/nodemailer/lib/well-known/services.json
generated
vendored
@@ -147,6 +147,14 @@
|
||||
"secure": true
|
||||
},
|
||||
|
||||
"GmailWorkspace": {
|
||||
"description": "Gmail Workspace",
|
||||
"aliases": ["Google Workspace Mail"],
|
||||
"host": "smtp-relay.gmail.com",
|
||||
"port": 465,
|
||||
"secure": true
|
||||
},
|
||||
|
||||
"GMX": {
|
||||
"description": "GMX Mail",
|
||||
"domains": ["gmx.com", "gmx.net", "gmx.de"],
|
||||
|
||||
29
node_modules/nodemailer/lib/xoauth2/index.js
generated
vendored
29
node_modules/nodemailer/lib/xoauth2/index.js
generated
vendored
@@ -4,6 +4,7 @@ const Stream = require('stream').Stream;
|
||||
const nmfetch = require('../fetch');
|
||||
const crypto = require('crypto');
|
||||
const shared = require('../shared');
|
||||
const errors = require('../errors');
|
||||
|
||||
/**
|
||||
* XOAUTH2 access_token generator for Gmail.
|
||||
@@ -41,7 +42,9 @@ class XOAuth2 extends Stream {
|
||||
|
||||
if (options && options.serviceClient) {
|
||||
if (!options.privateKey || !options.user) {
|
||||
setImmediate(() => this.emit('error', new Error('Options "privateKey" and "user" are required for service account!')));
|
||||
let err = new Error('Options "privateKey" and "user" are required for service account!');
|
||||
err.code = errors.EOAUTH2;
|
||||
setImmediate(() => this.emit('error', err));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -120,7 +123,9 @@ class XOAuth2 extends Stream {
|
||||
'Cannot renew access token for %s: No refresh mechanism available',
|
||||
this.options.user
|
||||
);
|
||||
return callback(new Error("Can't create new access token for user"));
|
||||
let err = new Error("Can't create new access token for user");
|
||||
err.code = errors.EOAUTH2;
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
// If renewal already in progress, queue this request instead of starting another
|
||||
@@ -218,7 +223,9 @@ class XOAuth2 extends Stream {
|
||||
try {
|
||||
token = this.jwtSignRS256(tokenData);
|
||||
} catch (_err) {
|
||||
return callback(new Error("Can't generate token. Check your auth options"));
|
||||
let err = new Error("Can't generate token. Check your auth options");
|
||||
err.code = errors.EOAUTH2;
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
urlOptions = {
|
||||
@@ -232,7 +239,9 @@ class XOAuth2 extends Stream {
|
||||
};
|
||||
} else {
|
||||
if (!this.options.refreshToken) {
|
||||
return callback(new Error("Can't create new access token for user"));
|
||||
let err = new Error("Can't create new access token for user");
|
||||
err.code = errors.EOAUTH2;
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
// web app - https://developers.google.com/identity/protocols/OAuth2WebServer
|
||||
@@ -289,7 +298,9 @@ class XOAuth2 extends Stream {
|
||||
'Response: %s',
|
||||
(body || '').toString()
|
||||
);
|
||||
return callback(new Error('Invalid authentication response'));
|
||||
let err = new Error('Invalid authentication response');
|
||||
err.code = errors.EOAUTH2;
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let logData = {};
|
||||
@@ -320,7 +331,9 @@ class XOAuth2 extends Stream {
|
||||
if (data.error_uri) {
|
||||
errorMessage += ' (' + data.error_uri + ')';
|
||||
}
|
||||
return callback(new Error(errorMessage));
|
||||
let err = new Error(errorMessage);
|
||||
err.code = errors.EOAUTH2;
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (data.access_token) {
|
||||
@@ -328,7 +341,9 @@ class XOAuth2 extends Stream {
|
||||
return callback(null, this.accessToken);
|
||||
}
|
||||
|
||||
return callback(new Error('No access token'));
|
||||
let err = new Error('No access token');
|
||||
err.code = errors.EOAUTH2;
|
||||
return callback(err);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
12
node_modules/nodemailer/package.json
generated
vendored
12
node_modules/nodemailer/package.json
generated
vendored
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "nodemailer",
|
||||
"version": "7.0.13",
|
||||
"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.975.0",
|
||||
"@aws-sdk/client-sesv2": "3.1004.0",
|
||||
"bunyan": "1.8.15",
|
||||
"c8": "10.1.3",
|
||||
"eslint": "9.39.2",
|
||||
"c8": "11.0.0",
|
||||
"eslint": "10.0.3",
|
||||
"eslint-config-prettier": "10.1.8",
|
||||
"globals": "17.1.0",
|
||||
"globals": "17.4.0",
|
||||
"libbase64": "1.3.0",
|
||||
"libmime": "5.3.7",
|
||||
"libqp": "2.1.1",
|
||||
@@ -39,7 +39,7 @@
|
||||
"prettier": "3.8.1",
|
||||
"proxy": "1.0.2",
|
||||
"proxy-test-server": "1.0.0",
|
||||
"smtp-server": "3.18.0"
|
||||
"smtp-server": "3.18.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
|
||||
1
node_modules/undici/docs/docs/api/Errors.md
generated
vendored
1
node_modules/undici/docs/docs/api/Errors.md
generated
vendored
@@ -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`
|
||||
|
||||
|
||||
8
node_modules/undici/docs/docs/api/WebSocket.md
generated
vendored
8
node_modules/undici/docs/docs/api/WebSocket.md
generated
vendored
@@ -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.
|
||||
|
||||
21
node_modules/undici/lib/core/errors.js
generated
vendored
21
node_modules/undici/lib/core/errors.js
generated
vendored
@@ -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
|
||||
}
|
||||
|
||||
14
node_modules/undici/lib/core/request.js
generated
vendored
14
node_modules/undici/lib/core/request.js
generated
vendored
@@ -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')
|
||||
|
||||
50
node_modules/undici/lib/web/websocket/permessage-deflate.js
generated
vendored
50
node_modules/undici/lib/web/websocket/permessage-deflate.js
generated
vendored
@@ -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)
|
||||
})
|
||||
|
||||
13
node_modules/undici/lib/web/websocket/receiver.js
generated
vendored
13
node_modules/undici/lib/web/websocket/receiver.js
generated
vendored
@@ -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
|
||||
}
|
||||
|
||||
|
||||
10
node_modules/undici/lib/web/websocket/util.js
generated
vendored
10
node_modules/undici/lib/web/websocket/util.js
generated
vendored
@@ -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
|
||||
|
||||
2
node_modules/undici/lib/web/websocket/websocket.js
generated
vendored
2
node_modules/undici/lib/web/websocket/websocket.js
generated
vendored
@@ -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 "response’s 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
4
node_modules/undici/package.json
generated
vendored
@@ -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",
|
||||
|
||||
6
node_modules/undici/types/errors.d.ts
generated
vendored
6
node_modules/undici/types/errors.d.ts
generated
vendored
@@ -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
20
package-lock.json
generated
@@ -8,7 +8,7 @@
|
||||
"dependencies": {
|
||||
"@actions/core": "^3.0.0",
|
||||
"@actions/glob": "^0.6.1",
|
||||
"nodemailer": "^7.0.13",
|
||||
"nodemailer": "^8.0.2",
|
||||
"showdown": "^1.9.1"
|
||||
}
|
||||
},
|
||||
@@ -194,9 +194,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"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": "7.0.13",
|
||||
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.13.tgz",
|
||||
"integrity": "sha512-PNDFSJdP+KFgdsG3ZzMXCgquO7I6McjY2vlqILjtJd0hy8wEvtugS9xKRF2NWlPNGxvLCXlTNIae4serI7dinw==",
|
||||
"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"
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"dependencies": {
|
||||
"@actions/core": "^3.0.0",
|
||||
"@actions/glob": "^0.6.1",
|
||||
"nodemailer": "^7.0.13",
|
||||
"nodemailer": "^8.0.2",
|
||||
"showdown": "^1.9.1"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user