mirror of
https://github.com/dawidd6/action-send-mail.git
synced 2026-09-17 09:06:48 +07:00
67ce3558d6
Co-authored-by: dawidd6 <9713907+dawidd6@users.noreply.github.com>
256 lines
12 KiB
JavaScript
256 lines
12 KiB
JavaScript
"use strict";
|
|
// URL parsing wrapper around the WHATWG `URL` class. It only falls back to the
|
|
// legacy, deprecation-warning-emitting `url.parse()` / `url.resolve()` for input
|
|
// the WHATWG parser rejects.
|
|
//
|
|
// The WHATWG `URL` exposes a different shape than the legacy parser, so results
|
|
// are normalized back into the legacy field names the rest of the codebase reads
|
|
// (`protocol`, `hostname`, `port`, `pathname`, `path`, `search`, `auth`, `query`,
|
|
// `href`). This keeps every existing call site unchanged.
|
|
//
|
|
// Known, accepted divergences from the legacy parser:
|
|
// - non-special schemes (smtp:/smtps:/direct:) are not host-lowercased by
|
|
// WHATWG; cosmetic only, SMTP/DNS hosts are case-insensitive. (IDNA mapping
|
|
// and IPv6 brackets are normalized back by normalizeHostname below.)
|
|
// - a literal unescaped ':' inside a password is percent-encoded by WHATWG;
|
|
// such passwords should be percent-encoded by the caller anyway.
|
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
if (k2 === undefined) k2 = k;
|
|
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
}
|
|
Object.defineProperty(o, k2, desc);
|
|
}) : (function(o, m, k, k2) {
|
|
if (k2 === undefined) k2 = k;
|
|
o[k2] = m[k];
|
|
}));
|
|
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
}) : function(o, v) {
|
|
o["default"] = v;
|
|
});
|
|
var __importStar = (this && this.__importStar) || (function () {
|
|
var ownKeys = function(o) {
|
|
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
var ar = [];
|
|
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
return ar;
|
|
};
|
|
return ownKeys(o);
|
|
};
|
|
return function (mod) {
|
|
if (mod && mod.__esModule) return mod;
|
|
var result = {};
|
|
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
__setModuleDefault(result, mod);
|
|
return result;
|
|
};
|
|
})();
|
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
};
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.resolve = exports.parse = void 0;
|
|
const node_net_1 = __importDefault(require("node:net"));
|
|
const node_url_1 = __importDefault(require("node:url"));
|
|
const punycode = __importStar(require("../punycode/index.js"));
|
|
// Matches a "scheme:" not followed by "//" (and with something after it), used
|
|
// to re-insert the authority separator the legacy parser did not require.
|
|
const SLASHLESS_AUTHORITY = /^([a-zA-Z][a-zA-Z0-9+.-]*:)(?!\/\/)([\s\S]+)$/;
|
|
// Leading and trailing C0 controls and spaces, which the WHATWG parser strips before it
|
|
// looks at the input. Stripped up front so that the slash-less form is recognized in a
|
|
// value read from a file with a trailing newline as well.
|
|
const SURROUNDING_WHITESPACE = /^[\x00-\x20]+|[\x00-\x20]+$/g;
|
|
// Leading characters legacy url.parse() skips before it reads the scheme
|
|
const LEGACY_TRIM = /^[\x00-\x20\u00a0\ufeff]+/;
|
|
// The authority of a "scheme://authority/..." or a scheme-relative "//authority/..."
|
|
// string: the scheme, if any (anything the legacy parser takes for one, it is less strict
|
|
// than WHATWG about the first character), and what the legacy parser has to report as the
|
|
// host once the userinfo is removed, see legacyParse. The legacy parser treats a backslash
|
|
// as a slash.
|
|
const AUTHORITY = /^([a-zA-Z0-9+.-]+:)?[\\/]{2}([^\\/?#]*)/;
|
|
// The WHATWG forbidden domain code points, except '%' which an opaque host may carry: the
|
|
// C0 control characters, space, DEL and the URL delimiters. CONTROL_CHARS is the C0 and DEL
|
|
// subset, checked on its own in legacyParse where the host string still carries the port
|
|
// and IPv6 brackets.
|
|
const FORBIDDEN_HOST_CHARS = /[\x00-\x20#/:<>?@[\\\]^|\x7f]/;
|
|
const CONTROL_CHARS = /[\x00-\x1f\x7f]/;
|
|
// The error the WHATWG parser throws, for a host that only fails the checks in this module
|
|
function invalidUrl(input) {
|
|
const err = new TypeError('Invalid URL');
|
|
err.code = 'ERR_INVALID_URL';
|
|
err.input = input;
|
|
return err;
|
|
}
|
|
// Legacy url.parse() for input the WHATWG parser refused. The legacy parser does not
|
|
// reject a host it can not represent: a NUL byte, a percent-encoded byte, a space or a
|
|
// '<' inside the host ends the host early and the rest becomes the path, so
|
|
// 'localhost%00.example.com' silently turns into a request to 'localhost'. When the
|
|
// input has an authority that the legacy parser reads as the host (always for
|
|
// "scheme://", for "//host" only when slashesDenoteHost is set, as url.resolve() does
|
|
// for its target), the legacy result is accepted only if that host is the whole written
|
|
// authority, lowercased and IDNA mapped the way the legacy parser does it, and carries
|
|
// no control character. A legacy result with a host (an empty one included, 'http:///x'
|
|
// is a request to localhost) that no such authority accounts for is refused as well.
|
|
// Otherwise the WHATWG error is reported. Relative input has no authority to check and
|
|
// keeps the legacy behavior.
|
|
function legacyParse(input, parseQueryString, whatwgError, slashesDenoteHost) {
|
|
const parsed = node_url_1.default.parse(input, parseQueryString, slashesDenoteHost);
|
|
const authority = AUTHORITY.exec(input.replace(LEGACY_TRIM, ''));
|
|
if (authority && (authority[1] || parsed.hostname !== null)) {
|
|
const written = authority[2].slice(authority[2].lastIndexOf('@') + 1);
|
|
if (!written || CONTROL_CHARS.test(written) || (parsed.host || '').toLowerCase() !== punycode.toASCII(written.toLowerCase())) {
|
|
throw whatwgError;
|
|
}
|
|
// the legacy parser takes any bracketed value for an IPv6 literal
|
|
if (written.charAt(0) === '[' && !node_net_1.default.isIPv6(written.slice(1, written.indexOf(']')))) {
|
|
throw whatwgError;
|
|
}
|
|
}
|
|
else if (parsed.hostname !== null) {
|
|
throw whatwgError;
|
|
}
|
|
// the legacy parser only offers the joined form, split it on the first colon
|
|
const legacyAuth = parsed.auth === null || parsed.auth === undefined ? null : parsed.auth.split(':');
|
|
const result = parsed;
|
|
result.username = legacyAuth ? legacyAuth.shift() : null;
|
|
result.password = legacyAuth && legacyAuth.length ? legacyAuth.join(':') : null;
|
|
return result;
|
|
}
|
|
// decodeURIComponent that never throws. Legacy url.parse() decodes the auth
|
|
// component but tolerates malformed percent sequences, so mirror that.
|
|
function safeDecode(str) {
|
|
try {
|
|
return decodeURIComponent(str);
|
|
}
|
|
catch (_err) {
|
|
return str;
|
|
}
|
|
}
|
|
// Derives the legacy-shaped bare hostname from a WHATWG URL. WHATWG keeps IPv6
|
|
// literals bracketed ('[::1]') and, for non-special schemes (smtp:/smtps:/socks:),
|
|
// percent-encodes a non-ASCII host instead of IDNA-mapping it. Both forms are
|
|
// un-resolvable when handed to net/dns/http.request, which is what every call
|
|
// site does, so map them back to what legacy url.parse() returned: the bare
|
|
// address and the IDNA mapped (lowercased, punycode) form. Idempotent on plain
|
|
// ASCII and already-punycode hosts, so special-scheme hosts (already IDNA-mapped
|
|
// by WHATWG) pass through.
|
|
function normalizeHostname(raw, href) {
|
|
const hostname = raw || '';
|
|
if (!hostname) {
|
|
// Host-less URL (e.g. 'direct:'): legacy returned '' here, not null;
|
|
// consumers do `hostname.length` / `'.' + hostname`, so keep it a string.
|
|
return '';
|
|
}
|
|
if (hostname.charAt(0) === '[' && hostname.charAt(hostname.length - 1) === ']') {
|
|
return hostname.slice(1, -1);
|
|
}
|
|
const decoded = safeDecode(hostname);
|
|
// domainToASCII applies the WHATWG host rules (IDNA mapping included) and returns an
|
|
// empty string for a host it refuses, the forbidden characters among them
|
|
const mapped = FORBIDDEN_HOST_CHARS.test(decoded) ? '' : node_url_1.default.domainToASCII(decoded);
|
|
if (!mapped) {
|
|
throw invalidUrl(href);
|
|
}
|
|
return mapped;
|
|
}
|
|
const parse = (input, parseQueryString) => {
|
|
input = (input || '').replace(SURROUNDING_WHITESPACE, '');
|
|
// Legacy url.parse() parses a "user:pass@host:port" authority that follows
|
|
// the scheme even without the "//" separator, for schemes outside its
|
|
// built-in slashed-protocol list (smtp:/smtps:/socks:/...). The WHATWG
|
|
// parser instead treats a scheme not followed by "//" as an opaque path.
|
|
// Re-insert the "//" so slash-less connection/proxy URLs keep resolving to
|
|
// an authority, as they did before. This assumes a slash-authority scheme,
|
|
// which every consumer here uses (http/https/smtp/smtps/socks/direct); an
|
|
// opaque scheme like mailto:/data:/tel: would be mis-split, but none reach
|
|
// this module.
|
|
const slashless = SLASHLESS_AUTHORITY.exec(input);
|
|
const normalized = slashless ? slashless[1] + '//' + slashless[2] : input;
|
|
let u;
|
|
try {
|
|
u = new URL(normalized);
|
|
}
|
|
catch (err) {
|
|
// WHATWG rejects some input the legacy parser tolerated (empty/relative
|
|
// strings, scheme-relative '//host/path', out-of-range ports, ...). Fall
|
|
// back to the legacy parser so behavior, including the downstream errors
|
|
// callers rely on, is preserved. This is the only path that can still
|
|
// emit a deprecation warning; it fires for anything WHATWG cannot
|
|
// represent, including legitimate relative URLs, not just malformed input.
|
|
return legacyParse(normalized, parseQueryString, err);
|
|
}
|
|
const hostname = normalizeHostname(u.hostname, u.href);
|
|
const port = u.port || null;
|
|
const pathname = u.pathname || null;
|
|
const search = u.search || null;
|
|
// Legacy `.auth` is the decoded "user[:pass]" string; WHATWG keeps the
|
|
// username/password percent-encoded, so decode to stay byte-compatible with
|
|
// existing consumers (parseConnectionUrl, Basic/Proxy-Authorization headers).
|
|
let auth = null;
|
|
let username = null;
|
|
let password = null;
|
|
if (u.username || u.password) {
|
|
// Gate on password too: legacy url.parse('smtps://:pass@host').auth was
|
|
// ':pass'. Dropping it would silently connect unauthenticated.
|
|
username = safeDecode(u.username);
|
|
password = u.password ? safeDecode(u.password) : null;
|
|
// the joined form is ambiguous once the user name contains a colon, so
|
|
// consumers that need the parts read username and password instead
|
|
auth = username + (password !== null ? ':' + password : '');
|
|
}
|
|
let query;
|
|
if (parseQueryString) {
|
|
// Mirror querystring.parse(): null-prototype object, repeated keys become an array.
|
|
const parsed = Object.create(null);
|
|
u.searchParams.forEach((value, key) => {
|
|
if (Object.prototype.hasOwnProperty.call(parsed, key)) {
|
|
const existing = parsed[key];
|
|
if (Array.isArray(existing)) {
|
|
existing.push(value);
|
|
}
|
|
else {
|
|
parsed[key] = [existing, value];
|
|
}
|
|
}
|
|
else {
|
|
parsed[key] = value;
|
|
}
|
|
});
|
|
query = parsed;
|
|
}
|
|
else {
|
|
query = search ? search.slice(1) : null;
|
|
}
|
|
return {
|
|
protocol: u.protocol || null,
|
|
host: u.host || null,
|
|
hostname,
|
|
port,
|
|
pathname,
|
|
search,
|
|
path: (pathname || '') + (search || '') || null,
|
|
href: u.href,
|
|
auth,
|
|
username,
|
|
password,
|
|
query
|
|
};
|
|
};
|
|
exports.parse = parse;
|
|
const resolve = (from, to) => {
|
|
try {
|
|
return new URL(to, from).href;
|
|
}
|
|
catch (err) {
|
|
// Malformed target, fall back to the legacy resolver, but only when the legacy
|
|
// parser reads the same host out of both inputs that was written. The target
|
|
// decides the host when it is absolute or scheme-relative, the base otherwise
|
|
legacyParse(from, false, err, true);
|
|
legacyParse(to, false, err, true);
|
|
return node_url_1.default.resolve(from, to);
|
|
}
|
|
};
|
|
exports.resolve = resolve;
|