mirror of
https://github.com/dawidd6/action-send-mail.git
synced 2026-06-18 15:40:52 +07:00
node_modules: update (#297)
Co-authored-by: dawidd6 <9713907+dawidd6@users.noreply.github.com>
This commit is contained in:
+6
-6
@@ -58,9 +58,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/brace-expansion": {
|
"node_modules/brace-expansion": {
|
||||||
"version": "5.0.5",
|
"version": "5.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
|
||||||
"integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
|
"integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"balanced-match": "^4.0.2"
|
"balanced-match": "^4.0.2"
|
||||||
@@ -94,9 +94,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/nodemailer": {
|
"node_modules/nodemailer": {
|
||||||
"version": "8.0.7",
|
"version": "9.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-9.0.0.tgz",
|
||||||
"integrity": "sha512-pkjE4mkBzQjdJT4/UmlKl3pX0rC9fZmjh7c6C9o7lv66Ac6w9WCnzPzhbPNxwZAzlF4mdq4CSWB5+FbK6FWCow==",
|
"integrity": "sha512-tbPTid7d/p9jAA8CRZ3iomvrMaST0o6NYuY7v6JQZHpPRZ61mLFSPKYd7342NtOFuej9/+L48SOIxwfu2uDvtw==",
|
||||||
"license": "MIT-0",
|
"license": "MIT-0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
|
|||||||
+1
-1
@@ -155,7 +155,7 @@ function expand_(str, max, isTop) {
|
|||||||
}
|
}
|
||||||
const pad = n.some(isPadded);
|
const pad = n.some(isPadded);
|
||||||
N = [];
|
N = [];
|
||||||
for (let i = x; test(i, y); i += incr) {
|
for (let i = x; test(i, y) && N.length < max; i += incr) {
|
||||||
let c;
|
let c;
|
||||||
if (isAlphaSequence) {
|
if (isAlphaSequence) {
|
||||||
c = String.fromCharCode(i);
|
c = String.fromCharCode(i);
|
||||||
|
|||||||
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
@@ -151,7 +151,7 @@ function expand_(str, max, isTop) {
|
|||||||
}
|
}
|
||||||
const pad = n.some(isPadded);
|
const pad = n.some(isPadded);
|
||||||
N = [];
|
N = [];
|
||||||
for (let i = x; test(i, y); i += incr) {
|
for (let i = x; test(i, y) && N.length < max; i += incr) {
|
||||||
let c;
|
let c;
|
||||||
if (isAlphaSequence) {
|
if (isAlphaSequence) {
|
||||||
c = String.fromCharCode(i);
|
c = String.fromCharCode(i);
|
||||||
|
|||||||
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "brace-expansion",
|
"name": "brace-expansion",
|
||||||
"description": "Brace expansion as known from sh/bash",
|
"description": "Brace expansion as known from sh/bash",
|
||||||
"version": "5.0.5",
|
"version": "5.0.6",
|
||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist"
|
||||||
],
|
],
|
||||||
|
|||||||
+47
@@ -1,5 +1,52 @@
|
|||||||
# CHANGELOG
|
# CHANGELOG
|
||||||
|
|
||||||
|
## [9.0.0](https://github.com/nodemailer/nodemailer/compare/v8.0.11...v9.0.0) (2026-06-14)
|
||||||
|
|
||||||
|
|
||||||
|
### ⚠ BREAKING CHANGES
|
||||||
|
|
||||||
|
* HTTPS requests made while fetching remote content (attachment href/path URLs, OAuth2 token endpoints, HTTP/HTTPS proxy CONNECT) now validate the server's TLS certificate by default. Requests to hosts with self-signed, expired, or hostname-mismatched certificates that previously succeeded will now fail. Opt back out per request with tls.rejectUnauthorized=false (transport options, or a per-attachment `tls` option).
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* replace deprecated url.parse with a WHATWG URL wrapper ([0c080fb](https://github.com/nodemailer/nodemailer/commit/0c080fbf3278926f013a5c2ad06f5f6f0e18f5ed))
|
||||||
|
* validate TLS certificates by default when fetching remote content ([6a947ac](https://github.com/nodemailer/nodemailer/commit/6a947ac7114a16da1e6a50d9a6f4e17026ce145d))
|
||||||
|
|
||||||
|
## [8.0.11](https://github.com/nodemailer/nodemailer/compare/v8.0.10...v8.0.11) (2026-06-10)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* apply the transport-level newline option in stream and sendmail transports ([cb4f904](https://github.com/nodemailer/nodemailer/commit/cb4f904a53d2c2feeaf327203c92378d46304398))
|
||||||
|
* include icalEvent path/href content in the application/ics attachment ([b801c48](https://github.com/nodemailer/nodemailer/commit/b801c48fab8e9b71bc7e0ea1fb32ce6b34675b15))
|
||||||
|
* parse Ethereal response props without polynomial regex backtracking ([067aebe](https://github.com/nodemailer/nodemailer/commit/067aebec83b8cbe7682905e89b30ab19d260b503))
|
||||||
|
* resolve oauth2_provision_cb at send time for non-pooled SMTP transports ([203c8ec](https://github.com/nodemailer/nodemailer/commit/203c8ecf97594ac2e69919b0f3ba966c0f86750e))
|
||||||
|
* return the promise from every resolveContent branch ([07ffe8c](https://github.com/nodemailer/nodemailer/commit/07ffe8cfd97f0486b8c7b541f398922ddab47882))
|
||||||
|
* strip the url scheme from List-ID header values ([77e5885](https://github.com/nodemailer/nodemailer/commit/77e5885cfa0c6723ea7749c1ee74b1c11aeb78bd))
|
||||||
|
* tag AWS SES transport errors with the ESES code ([efa647a](https://github.com/nodemailer/nodemailer/commit/efa647a125dd698413a7cf6813b8e36881a06f91))
|
||||||
|
|
||||||
|
## [8.0.10](https://github.com/nodemailer/nodemailer/compare/v8.0.9...v8.0.10) (2026-05-29)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* fall back to lower-severity handler when custom logger lacks a level method ([6d849df](https://github.com/nodemailer/nodemailer/commit/6d849df59a56184b48844ed10b5fb7b8e9f74634))
|
||||||
|
|
||||||
|
## [8.0.9](https://github.com/nodemailer/nodemailer/compare/v8.0.8...v8.0.9) (2026-05-26)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* two pending security advisories (jsonTransport access bypass, List-* CRLF injection) ([#1820](https://github.com/nodemailer/nodemailer/issues/1820)) ([5f69497](https://github.com/nodemailer/nodemailer/commit/5f694977da2e0e13dc947037566e8e689a01217e))
|
||||||
|
|
||||||
|
## [8.0.8](https://github.com/nodemailer/nodemailer/compare/v8.0.7...v8.0.8) (2026-05-23)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* enforce strict TLS for OAuth2 and Ethereal credential requests ([#1818](https://github.com/nodemailer/nodemailer/issues/1818)) ([833d6e5](https://github.com/nodemailer/nodemailer/commit/833d6e58c8b717962bbb1b23e16923cd267c3bc9))
|
||||||
|
* four listener/stream leaks in SMTP transport, connection, pool ([#1817](https://github.com/nodemailer/nodemailer/issues/1817)) ([850bb91](https://github.com/nodemailer/nodemailer/commit/850bb91bff7707ed498c1424df01c4e5b30ea14b))
|
||||||
|
|
||||||
## [8.0.7](https://github.com/nodemailer/nodemailer/compare/v8.0.6...v8.0.7) (2026-04-27)
|
## [8.0.7](https://github.com/nodemailer/nodemailer/compare/v8.0.6...v8.0.7) (2026-04-27)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+1
@@ -50,6 +50,7 @@ Conventional Commit prefixes used in this repo: `fix:`, `feat:`, `chore:`, `docs
|
|||||||
## Security
|
## Security
|
||||||
|
|
||||||
This is a widely-deployed library — security-sensitive changes get extra scrutiny:
|
This is a widely-deployed library — security-sensitive changes get extra scrutiny:
|
||||||
|
|
||||||
- SMTP command injection: any user-controllable value that flows into a written SMTP command (envelope addresses, sizes, the `name`/EHLO option, headers) must be CRLF-stripped or rejected at the boundary. Sanitize at the assignment, not at every call site.
|
- SMTP command injection: any user-controllable value that flows into a written SMTP command (envelope addresses, sizes, the `name`/EHLO option, headers) must be CRLF-stripped or rejected at the boundary. Sanitize at the assignment, not at every call site.
|
||||||
- Server reply parsing in `lib/smtp-connection/index.js` uses a `'binary'` byte-container intermediate to reassemble multi-byte UTF-8 across socket chunks; the actual decode happens at line boundaries via `decodeServerResponse`. Don't change the chunk-buffering encoding without understanding why.
|
- Server reply parsing in `lib/smtp-connection/index.js` uses a `'binary'` byte-container intermediate to reassemble multi-byte UTF-8 across socket chunks; the actual decode happens at line boundaries via `decodeServerResponse`. Don't change the chunk-buffering encoding without understanding why.
|
||||||
- Reference the GHSA ID in commit messages for advisories.
|
- Reference the GHSA ID in commit messages for advisories.
|
||||||
|
|||||||
+65
@@ -0,0 +1,65 @@
|
|||||||
|
# Security Policy
|
||||||
|
|
||||||
|
Nodemailer is a widely deployed, zero-dependency e-mail library. We take security
|
||||||
|
reports seriously and aim to respond quickly.
|
||||||
|
|
||||||
|
## Supported Versions
|
||||||
|
|
||||||
|
Security fixes are released only against the latest major version. We do not
|
||||||
|
backport patches to older majors — upgrading to the current release line is the
|
||||||
|
supported way to receive security updates.
|
||||||
|
|
||||||
|
| Version | Supported |
|
||||||
|
| ------- | ------------------ |
|
||||||
|
| 8.x | :white_check_mark: |
|
||||||
|
| < 8.0 | :x: |
|
||||||
|
|
||||||
|
If you are on an older major, please upgrade. See the migration notes at
|
||||||
|
<https://nodemailer.com/> before updating.
|
||||||
|
|
||||||
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
**Please do not report security vulnerabilities through public GitHub issues,
|
||||||
|
pull requests, or discussions.**
|
||||||
|
|
||||||
|
Report privately through one of the following channels:
|
||||||
|
|
||||||
|
1. **GitHub Security Advisories (preferred).** Open a private report at
|
||||||
|
<https://github.com/nodemailer/nodemailer/security/advisories/new>. This keeps
|
||||||
|
the discussion private until a fix is published and lets us credit you.
|
||||||
|
2. **Email.** Send details to **andris@reinman.eu** (the contact listed in
|
||||||
|
[`SECURITY.txt`](SECURITY.txt)). Encrypt sensitive details if possible.
|
||||||
|
|
||||||
|
When reporting, please include as much of the following as you can:
|
||||||
|
|
||||||
|
- The affected version(s) and environment (Node.js version, OS).
|
||||||
|
- The component involved (e.g. SMTP connection, address parsing, MIME/header
|
||||||
|
generation, DKIM).
|
||||||
|
- A clear description of the issue and its impact (e.g. header/SMTP command
|
||||||
|
injection, information disclosure, DoS).
|
||||||
|
- A minimal proof of concept or reproduction steps.
|
||||||
|
- Any suggested remediation, if you have one.
|
||||||
|
|
||||||
|
Nodemailer is maintained by a single person, so there is no guaranteed response
|
||||||
|
time — sometimes reports are handled within hours, sometimes they take longer.
|
||||||
|
Accepted issues are fixed in a new release and coordinated through a GitHub
|
||||||
|
Security Advisory, and reporters who wish to be named are credited.
|
||||||
|
|
||||||
|
## CVEs
|
||||||
|
|
||||||
|
We track and disclose vulnerabilities through GitHub Security Advisories. We do
|
||||||
|
not request or manage CVE identifiers ourselves. If you need a CVE assigned for a
|
||||||
|
reported issue, please request one yourself — for example, through GitHub's own
|
||||||
|
CVE request flow on the published advisory, or another CNA.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
In scope: the `nodemailer` package source in this repository — message and MIME
|
||||||
|
generation, SMTP/LMTP client behaviour, address parsing, header handling, DKIM
|
||||||
|
signing, and the bundled transports.
|
||||||
|
|
||||||
|
Out of scope: vulnerabilities in your own application code, misconfiguration of
|
||||||
|
your mail server or credentials, social-engineering reports, and issues in
|
||||||
|
third-party services Nodemailer connects to.
|
||||||
|
|
||||||
|
Thank you for helping keep Nodemailer and its users safe.
|
||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
// module to handle cookies
|
// module to handle cookies
|
||||||
|
|
||||||
const urllib = require('url');
|
const urllib = require('../shared/url');
|
||||||
|
|
||||||
const SESSION_TIMEOUT = 1800; // 30 min
|
const SESSION_TIMEOUT = 1800; // 30 min
|
||||||
|
|
||||||
|
|||||||
+26
-3
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
const http = require('http');
|
const http = require('http');
|
||||||
const https = require('https');
|
const https = require('https');
|
||||||
const urllib = require('url');
|
const urllib = require('../shared/url');
|
||||||
const zlib = require('zlib');
|
const zlib = require('zlib');
|
||||||
const { PassThrough } = require('stream');
|
const { PassThrough } = require('stream');
|
||||||
const Cookies = require('./cookies');
|
const Cookies = require('./cookies');
|
||||||
@@ -123,7 +123,10 @@ function nmfetch(url, options) {
|
|||||||
path: parsed.path,
|
path: parsed.path,
|
||||||
port: parsed.port ? parsed.port : parsed.protocol === 'https:' ? 443 : 80,
|
port: parsed.port ? parsed.port : parsed.protocol === 'https:' ? 443 : 80,
|
||||||
headers,
|
headers,
|
||||||
rejectUnauthorized: false,
|
// Validate TLS certificates by default. Callers that genuinely need to
|
||||||
|
// reach a self-signed/internal host opt out explicitly with
|
||||||
|
// options.tls = { rejectUnauthorized: false }.
|
||||||
|
rejectUnauthorized: true,
|
||||||
agent: false
|
agent: false
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -212,7 +215,27 @@ function nmfetch(url, options) {
|
|||||||
// redirect does not include POST body
|
// redirect does not include POST body
|
||||||
options.method = 'GET';
|
options.method = 'GET';
|
||||||
options.body = false;
|
options.body = false;
|
||||||
return nmfetch(urllib.resolve(url, res.headers.location), options);
|
|
||||||
|
const redirectUrl = urllib.resolve(url, res.headers.location);
|
||||||
|
const redirectParsed = urllib.parse(redirectUrl);
|
||||||
|
|
||||||
|
// Do not forward credentials when the redirect leaves the original
|
||||||
|
// security context: a different host, or a downgrade from https to
|
||||||
|
// http (which would otherwise put them on the wire in cleartext).
|
||||||
|
// Strip sensitive request headers so an attacker who controls the
|
||||||
|
// redirect target cannot harvest them.
|
||||||
|
const crossHost = redirectParsed.hostname !== parsed.hostname;
|
||||||
|
const downgrade = parsed.protocol === 'https:' && redirectParsed.protocol === 'http:';
|
||||||
|
if (options.headers && (crossHost || downgrade)) {
|
||||||
|
const sensitive = ['authorization', 'cookie', 'proxy-authorization'];
|
||||||
|
Object.keys(options.headers).forEach(key => {
|
||||||
|
if (sensitive.includes(key.toLowerCase())) {
|
||||||
|
delete options.headers[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return nmfetch(redirectUrl, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchRes.statusCode = res.statusCode;
|
fetchRes.statusCode = res.statusCode;
|
||||||
|
|||||||
+67
-33
@@ -83,7 +83,7 @@ class MailComposer {
|
|||||||
* @returns {Object} An object of arrays (`related` and `attached`)
|
* @returns {Object} An object of arrays (`related` and `attached`)
|
||||||
*/
|
*/
|
||||||
getAttachments(findRelated) {
|
getAttachments(findRelated) {
|
||||||
let icalEvent, eventObject;
|
let eventObject;
|
||||||
const attachments = [].concat(this.mail.attachments || []).map((attachment, i) => {
|
const attachments = [].concat(this.mail.attachments || []).map((attachment, i) => {
|
||||||
if (/^data:/i.test(attachment.path || attachment.href)) {
|
if (/^data:/i.test(attachment.path || attachment.href)) {
|
||||||
attachment = this._processDataUrl(attachment);
|
attachment = this._processDataUrl(attachment);
|
||||||
@@ -142,7 +142,8 @@ class MailComposer {
|
|||||||
} else if (attachment.href) {
|
} else if (attachment.href) {
|
||||||
data.content = {
|
data.content = {
|
||||||
href: attachment.href,
|
href: attachment.href,
|
||||||
httpHeaders: attachment.httpHeaders
|
httpHeaders: attachment.httpHeaders,
|
||||||
|
tls: attachment.tls
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
data.content = attachment.content || '';
|
data.content = attachment.content || '';
|
||||||
@@ -160,18 +161,7 @@ class MailComposer {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (this.mail.icalEvent) {
|
if (this.mail.icalEvent) {
|
||||||
if (
|
eventObject = Object.assign({}, this._getIcalEvent());
|
||||||
typeof this.mail.icalEvent === 'object' &&
|
|
||||||
(this.mail.icalEvent.content || this.mail.icalEvent.path || this.mail.icalEvent.href || this.mail.icalEvent.raw)
|
|
||||||
) {
|
|
||||||
icalEvent = this.mail.icalEvent;
|
|
||||||
} else {
|
|
||||||
icalEvent = {
|
|
||||||
content: this.mail.icalEvent
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
eventObject = Object.assign({}, icalEvent);
|
|
||||||
|
|
||||||
eventObject.contentType = 'application/ics';
|
eventObject.contentType = 'application/ics';
|
||||||
if (!eventObject.headers) {
|
if (!eventObject.headers) {
|
||||||
@@ -195,6 +185,67 @@ class MailComposer {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the icalEvent value with `path`/`href`/data uri input normalized into
|
||||||
|
* a `content` entry, the same way as for regular attachments. The same event is
|
||||||
|
* included twice (as a text/calendar alternative and as an application/ics
|
||||||
|
* attachment), so the shared content object is marked to be resolved just once
|
||||||
|
* and the buffered result is reused by the second node.
|
||||||
|
*
|
||||||
|
* @returns {Object} Normalized icalEvent data
|
||||||
|
*/
|
||||||
|
_getIcalEvent() {
|
||||||
|
if (!this._icalEvent) {
|
||||||
|
let icalEvent;
|
||||||
|
if (
|
||||||
|
typeof this.mail.icalEvent === 'object' &&
|
||||||
|
(this.mail.icalEvent.content || this.mail.icalEvent.path || this.mail.icalEvent.href || this.mail.icalEvent.raw)
|
||||||
|
) {
|
||||||
|
icalEvent = Object.assign({}, this.mail.icalEvent);
|
||||||
|
} else {
|
||||||
|
icalEvent = {
|
||||||
|
content: this.mail.icalEvent
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/^data:/i.test(icalEvent.path || icalEvent.href)) {
|
||||||
|
icalEvent = this._processDataUrl(icalEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/^https?:\/\//i.test(icalEvent.path)) {
|
||||||
|
icalEvent.href = icalEvent.path;
|
||||||
|
icalEvent.path = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!icalEvent.raw) {
|
||||||
|
// map file path and URL values into `content`, otherwise the content
|
||||||
|
// nodes would render an empty body
|
||||||
|
if (icalEvent.path) {
|
||||||
|
icalEvent.content = {
|
||||||
|
path: icalEvent.path
|
||||||
|
};
|
||||||
|
icalEvent.path = undefined;
|
||||||
|
} else if (icalEvent.href) {
|
||||||
|
icalEvent.content = {
|
||||||
|
href: icalEvent.href,
|
||||||
|
httpHeaders: icalEvent.httpHeaders
|
||||||
|
};
|
||||||
|
icalEvent.href = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (icalEvent.content && typeof icalEvent.content === 'object') {
|
||||||
|
// we are going to have the same attachment twice, so mark this to be
|
||||||
|
// resolved just once
|
||||||
|
icalEvent.content._resolve = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._icalEvent = icalEvent;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this._icalEvent;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List alternatives. Resulting objects can be used as input for MimeNode nodes
|
* List alternatives. Resulting objects can be used as input for MimeNode nodes
|
||||||
*
|
*
|
||||||
@@ -202,7 +253,7 @@ class MailComposer {
|
|||||||
*/
|
*/
|
||||||
getAlternatives() {
|
getAlternatives() {
|
||||||
const alternatives = [];
|
const alternatives = [];
|
||||||
let text, html, watchHtml, amp, icalEvent, eventObject;
|
let text, html, watchHtml, amp, eventObject;
|
||||||
|
|
||||||
if (this.mail.text) {
|
if (this.mail.text) {
|
||||||
if (
|
if (
|
||||||
@@ -248,24 +299,7 @@ class MailComposer {
|
|||||||
|
|
||||||
// NB! when including attachments with a calendar alternative you might end up in a blank screen on some clients
|
// NB! when including attachments with a calendar alternative you might end up in a blank screen on some clients
|
||||||
if (this.mail.icalEvent) {
|
if (this.mail.icalEvent) {
|
||||||
if (
|
eventObject = Object.assign({}, this._getIcalEvent());
|
||||||
typeof this.mail.icalEvent === 'object' &&
|
|
||||||
(this.mail.icalEvent.content || this.mail.icalEvent.path || this.mail.icalEvent.href || this.mail.icalEvent.raw)
|
|
||||||
) {
|
|
||||||
icalEvent = this.mail.icalEvent;
|
|
||||||
} else {
|
|
||||||
icalEvent = {
|
|
||||||
content: this.mail.icalEvent
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
eventObject = Object.assign({}, icalEvent);
|
|
||||||
|
|
||||||
if (eventObject.content && typeof eventObject.content === 'object') {
|
|
||||||
// we are going to have the same attachment twice, so mark this to be
|
|
||||||
// resolved just once
|
|
||||||
eventObject.content._resolve = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
eventObject.filename = false;
|
eventObject.filename = false;
|
||||||
eventObject.contentType =
|
eventObject.contentType =
|
||||||
|
|||||||
+34
-26
@@ -8,7 +8,7 @@ const DKIM = require('../dkim');
|
|||||||
const httpProxyClient = require('../smtp-connection/http-proxy-client');
|
const httpProxyClient = require('../smtp-connection/http-proxy-client');
|
||||||
const errors = require('../errors');
|
const errors = require('../errors');
|
||||||
const util = require('util');
|
const util = require('util');
|
||||||
const urllib = require('url');
|
const urllib = require('../shared/url');
|
||||||
const packageData = require('../../package.json');
|
const packageData = require('../../package.json');
|
||||||
const MailMessage = require('./mail-message');
|
const MailMessage = require('./mail-message');
|
||||||
const net = require('net');
|
const net = require('net');
|
||||||
@@ -324,7 +324,7 @@ class Mail extends EventEmitter {
|
|||||||
// Connect using a HTTP CONNECT method
|
// Connect using a HTTP CONNECT method
|
||||||
case 'http':
|
case 'http':
|
||||||
case 'https':
|
case 'https':
|
||||||
httpProxyClient(proxy.href, options.port, options.host, (err, socket) => {
|
httpProxyClient(proxy.href, options.port, options.host, this.options.tls || {}, (err, socket) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
return callback(err);
|
return callback(err);
|
||||||
}
|
}
|
||||||
@@ -407,31 +407,39 @@ class Mail extends EventEmitter {
|
|||||||
if ((!this.options.attachDataUrls && !mail.data.attachDataUrls) || !mail.data.html) {
|
if ((!this.options.attachDataUrls && !mail.data.attachDataUrls) || !mail.data.html) {
|
||||||
return callback();
|
return callback();
|
||||||
}
|
}
|
||||||
mail.resolveContent(mail.data, 'html', (err, html) => {
|
mail.resolveContent(
|
||||||
if (err) {
|
mail.data,
|
||||||
return callback(err);
|
'html',
|
||||||
|
{ disableFileAccess: mail.data.disableFileAccess, disableUrlAccess: mail.data.disableUrlAccess },
|
||||||
|
(err, html) => {
|
||||||
|
if (err) {
|
||||||
|
return callback(err);
|
||||||
|
}
|
||||||
|
let cidCounter = 0;
|
||||||
|
html = (html || '')
|
||||||
|
.toString()
|
||||||
|
.replace(
|
||||||
|
/(<img\b[^<>]{0,1024} src\s{0,20}=[\s"']{0,20})(data:([^;]+);[^"'>\s]+)/gi,
|
||||||
|
(match, prefix, dataUri, mimeType) => {
|
||||||
|
const cid = crypto.randomBytes(10).toString('hex') + '@localhost';
|
||||||
|
if (!mail.data.attachments) {
|
||||||
|
mail.data.attachments = [];
|
||||||
|
}
|
||||||
|
if (!Array.isArray(mail.data.attachments)) {
|
||||||
|
mail.data.attachments = [].concat(mail.data.attachments || []);
|
||||||
|
}
|
||||||
|
mail.data.attachments.push({
|
||||||
|
path: dataUri,
|
||||||
|
cid,
|
||||||
|
filename: 'image-' + ++cidCounter + '.' + mimeTypes.detectExtension(mimeType)
|
||||||
|
});
|
||||||
|
return prefix + 'cid:' + cid;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
mail.data.html = html;
|
||||||
|
callback();
|
||||||
}
|
}
|
||||||
let cidCounter = 0;
|
);
|
||||||
html = (html || '')
|
|
||||||
.toString()
|
|
||||||
.replace(/(<img\b[^<>]{0,1024} src\s{0,20}=[\s"']{0,20})(data:([^;]+);[^"'>\s]+)/gi, (match, prefix, dataUri, mimeType) => {
|
|
||||||
const cid = crypto.randomBytes(10).toString('hex') + '@localhost';
|
|
||||||
if (!mail.data.attachments) {
|
|
||||||
mail.data.attachments = [];
|
|
||||||
}
|
|
||||||
if (!Array.isArray(mail.data.attachments)) {
|
|
||||||
mail.data.attachments = [].concat(mail.data.attachments || []);
|
|
||||||
}
|
|
||||||
mail.data.attachments.push({
|
|
||||||
path: dataUri,
|
|
||||||
cid,
|
|
||||||
filename: 'image-' + ++cidCounter + '.' + mimeTypes.detectExtension(mimeType)
|
|
||||||
});
|
|
||||||
return prefix + 'cid:' + cid;
|
|
||||||
});
|
|
||||||
mail.data.html = html;
|
|
||||||
callback();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
set(key, value) {
|
set(key, value) {
|
||||||
|
|||||||
+30
-20
@@ -111,25 +111,29 @@ class MailMessage {
|
|||||||
if (!args[0] || !args[0][args[1]]) {
|
if (!args[0] || !args[0][args[1]]) {
|
||||||
return resolveNext();
|
return resolveNext();
|
||||||
}
|
}
|
||||||
shared.resolveContent(...args, (err, value) => {
|
shared.resolveContent(
|
||||||
if (err) {
|
...args,
|
||||||
return callback(err);
|
{ disableFileAccess: this.data.disableFileAccess, disableUrlAccess: this.data.disableUrlAccess },
|
||||||
}
|
(err, value) => {
|
||||||
|
if (err) {
|
||||||
|
return callback(err);
|
||||||
|
}
|
||||||
|
|
||||||
const node = {
|
const node = {
|
||||||
content: value
|
content: value
|
||||||
};
|
};
|
||||||
if (args[0][args[1]] && typeof args[0][args[1]] === 'object' && !Buffer.isBuffer(args[0][args[1]])) {
|
if (args[0][args[1]] && typeof args[0][args[1]] === 'object' && !Buffer.isBuffer(args[0][args[1]])) {
|
||||||
Object.keys(args[0][args[1]]).forEach(key => {
|
Object.keys(args[0][args[1]]).forEach(key => {
|
||||||
if (!(key in node) && !['content', 'path', 'href', 'raw'].includes(key)) {
|
if (!(key in node) && !['content', 'path', 'href', 'raw'].includes(key)) {
|
||||||
node[key] = args[0][args[1]][key];
|
node[key] = args[0][args[1]][key];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
args[0][args[1]] = node;
|
args[0][args[1]] = node;
|
||||||
resolveNext();
|
resolveNext();
|
||||||
});
|
}
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
setImmediate(() => resolveNext());
|
setImmediate(() => resolveNext());
|
||||||
@@ -269,18 +273,24 @@ class MailMessage {
|
|||||||
if (value && value.url) {
|
if (value && value.url) {
|
||||||
if (key.toLowerCase().trim() === 'id') {
|
if (key.toLowerCase().trim() === 'id') {
|
||||||
// List-ID: "comment" <domain>
|
// List-ID: "comment" <domain>
|
||||||
let comment = value.comment || '';
|
// strip CR/LF so a comment can't inject extra header lines
|
||||||
|
let comment = (value.comment || '').toString().replace(/\r?\n|\r/g, ' ');
|
||||||
if (mimeFuncs.isPlainText(comment)) {
|
if (mimeFuncs.isPlainText(comment)) {
|
||||||
comment = '"' + comment + '"';
|
comment = '"' + comment + '"';
|
||||||
} else {
|
} else {
|
||||||
comment = mimeFuncs.encodeWord(comment);
|
comment = mimeFuncs.encodeWord(comment);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (value.comment ? comment + ' ' : '') + this._formatListUrl(value.url).replace(/^<[^:]+\/{,2}/, '');
|
// List-ID expects a bare domain-like identifier, so strip the
|
||||||
|
// scheme prefix that _formatListUrl adds or passes through
|
||||||
|
return (
|
||||||
|
(value.comment ? comment + ' ' : '') + this._formatListUrl(value.url).replace(/^<[^:]+:\/{0,2}/, '<')
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// List-*: <http://domain> (comment)
|
// List-*: <http://domain> (comment)
|
||||||
let comment = value.comment || '';
|
// strip CR/LF so a comment can't inject extra header lines
|
||||||
|
let comment = (value.comment || '').toString().replace(/\r?\n|\r/g, ' ');
|
||||||
if (!mimeFuncs.isPlainText(comment)) {
|
if (!mimeFuncs.isPlainText(comment)) {
|
||||||
comment = mimeFuncs.encodeWord(comment);
|
comment = mimeFuncs.encodeWord(comment);
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -2087,7 +2087,7 @@ module.exports = {
|
|||||||
if (!mimeType) {
|
if (!mimeType) {
|
||||||
return defaultExtension;
|
return defaultExtension;
|
||||||
}
|
}
|
||||||
const parts = (mimeType || '').toLowerCase().trim().split('/');
|
const parts = mimeType.toLowerCase().trim().split('/');
|
||||||
const rootType = parts.shift().trim();
|
const rootType = parts.shift().trim();
|
||||||
const subType = parts.join('/').trim();
|
const subType = parts.join('/').trim();
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1006,7 +1006,7 @@ class MimeNode {
|
|||||||
return contentStream;
|
return contentStream;
|
||||||
}
|
}
|
||||||
// fetch URL
|
// fetch URL
|
||||||
return nmfetch(content.href, { headers: content.httpHeaders });
|
return nmfetch(content.href, { headers: content.httpHeaders, tls: content.tls });
|
||||||
}
|
}
|
||||||
|
|
||||||
// pass string or buffer content as a stream
|
// pass string or buffer content as a stream
|
||||||
|
|||||||
+26
-7
@@ -95,12 +95,22 @@ module.exports.createTestAccount = function (apiUrl, callback) {
|
|||||||
requestHeaders.Authorization = 'Bearer ' + ETHEREAL_API_KEY;
|
requestHeaders.Authorization = 'Bearer ' + ETHEREAL_API_KEY;
|
||||||
}
|
}
|
||||||
|
|
||||||
const req = nmfetch(apiUrl + '/user', {
|
const fetchOptions = {
|
||||||
contentType: 'application/json',
|
contentType: 'application/json',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: requestHeaders,
|
headers: requestHeaders,
|
||||||
body: Buffer.from(JSON.stringify(requestBody))
|
body: Buffer.from(JSON.stringify(requestBody))
|
||||||
});
|
};
|
||||||
|
|
||||||
|
// Credential-bearing request to the Ethereal API. lib/fetch already
|
||||||
|
// validates certs by default; pin rejectUnauthorized:true here so this
|
||||||
|
// call stays strict regardless of any future default change and is never
|
||||||
|
// relaxed for a real-cert endpoint.
|
||||||
|
if (/^https:/i.test(apiUrl)) {
|
||||||
|
fetchOptions.tls = { rejectUnauthorized: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
const req = nmfetch(apiUrl + '/user', fetchOptions);
|
||||||
|
|
||||||
req.on('readable', () => {
|
req.on('readable', () => {
|
||||||
let chunk;
|
let chunk;
|
||||||
@@ -137,11 +147,20 @@ module.exports.getTestMessageUrl = function (info) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const infoProps = new Map();
|
const infoProps = new Map();
|
||||||
info.response.replace(/\[([^\]]+)\]$/, (m, props) => {
|
|
||||||
props.replace(/\b([A-Z0-9]+)=([^\s]+)/g, (m, key, value) => {
|
// Extract the trailing "[...]" part of the response (no "]" allowed inside)
|
||||||
infoProps.set(key, value);
|
// with linear string scanning; the equivalent regex /\[([^\]]+)\]$/ was
|
||||||
});
|
// flagged for polynomial backtracking on adversarial server responses
|
||||||
});
|
const response = info.response.toString();
|
||||||
|
if (response.length > 2 && response.charAt(response.length - 1) === ']') {
|
||||||
|
const open = response.indexOf('[', response.lastIndexOf(']', response.length - 2) + 1);
|
||||||
|
if (open >= 0 && open < response.length - 2) {
|
||||||
|
const props = response.substring(open + 1, response.length - 1);
|
||||||
|
props.replace(/\b([A-Z0-9]+)=([^\s]+)/g, (m, key, value) => {
|
||||||
|
infoProps.set(key, value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (infoProps.has('STATUS') && infoProps.has('MSGID')) {
|
if (infoProps.has('STATUS') && infoProps.has('MSGID')) {
|
||||||
return (testAccount.web || ETHEREAL_WEB) + '/message/' + infoProps.get('MSGID');
|
return (testAccount.web || ETHEREAL_WEB) + '/message/' + infoProps.get('MSGID');
|
||||||
|
|||||||
+14
-2
@@ -4,6 +4,8 @@ const { spawn } = require('child_process');
|
|||||||
const packageData = require('../../package.json');
|
const packageData = require('../../package.json');
|
||||||
const shared = require('../shared');
|
const shared = require('../shared');
|
||||||
const errors = require('../errors');
|
const errors = require('../errors');
|
||||||
|
const LeWindows = require('../mime-node/le-windows');
|
||||||
|
const LeUnix = require('../mime-node/le-unix');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a Transport object for Sendmail
|
* Generates a Transport object for Sendmail
|
||||||
@@ -46,6 +48,8 @@ class SendmailTransport {
|
|||||||
this.args = options.args;
|
this.args = options.args;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.winbreak = ['win', 'windows', 'dos', '\r\n'].includes((options.newline || '').toString().toLowerCase());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -178,7 +182,15 @@ class SendmailTransport {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const sourceStream = mail.message.createReadStream();
|
const sourceStream = mail.message.createReadStream();
|
||||||
sourceStream.once('error', err => {
|
let stream = sourceStream;
|
||||||
|
if (this.options.newline) {
|
||||||
|
// apply the transport-level line ending transform; the message-level
|
||||||
|
// `newline` option is handled by MimeNode in createReadStream()
|
||||||
|
stream = sourceStream.pipe(this.winbreak ? new LeWindows() : new LeUnix());
|
||||||
|
sourceStream.once('error', err => stream.emit('error', err));
|
||||||
|
}
|
||||||
|
|
||||||
|
stream.once('error', err => {
|
||||||
this.logger.error(
|
this.logger.error(
|
||||||
{
|
{
|
||||||
err,
|
err,
|
||||||
@@ -193,7 +205,7 @@ class SendmailTransport {
|
|||||||
callback(err);
|
callback(err);
|
||||||
});
|
});
|
||||||
|
|
||||||
sourceStream.pipe(sendmail.stdin);
|
stream.pipe(sendmail.stdin);
|
||||||
} else {
|
} else {
|
||||||
const err = new Error('sendmail was not found');
|
const err = new Error('sendmail was not found');
|
||||||
err.code = errors.ESENDMAIL;
|
err.code = errors.ESENDMAIL;
|
||||||
|
|||||||
+19
-7
@@ -3,9 +3,22 @@
|
|||||||
const EventEmitter = require('events');
|
const EventEmitter = require('events');
|
||||||
const packageData = require('../../package.json');
|
const packageData = require('../../package.json');
|
||||||
const shared = require('../shared');
|
const shared = require('../shared');
|
||||||
|
const errors = require('../errors');
|
||||||
const LeWindows = require('../mime-node/le-windows');
|
const LeWindows = require('../mime-node/le-windows');
|
||||||
const MimeNode = require('../mime-node');
|
const MimeNode = require('../mime-node');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tags AWS SDK rejections that carry no `code` property (SDK v3 errors only
|
||||||
|
* have a `name`) with the generic SES transport error code, keeping the
|
||||||
|
* original error object intact
|
||||||
|
*/
|
||||||
|
function tagSesError(err) {
|
||||||
|
if (err && typeof err === 'object' && !err.code) {
|
||||||
|
err.code = errors.ESES;
|
||||||
|
}
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a Transport object for AWS SES
|
* Generates a Transport object for AWS SES
|
||||||
*
|
*
|
||||||
@@ -157,6 +170,7 @@ class SESTransport extends EventEmitter {
|
|||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
|
tagSesError(err);
|
||||||
this.logger.error(
|
this.logger.error(
|
||||||
{
|
{
|
||||||
err,
|
err,
|
||||||
@@ -188,7 +202,7 @@ class SESTransport extends EventEmitter {
|
|||||||
|
|
||||||
const cb = err => {
|
const cb = err => {
|
||||||
if (err && !['InvalidParameterValue', 'MessageRejected'].includes(err.code || err.Code || err.name)) {
|
if (err && !['InvalidParameterValue', 'MessageRejected'].includes(err.code || err.Code || err.name)) {
|
||||||
return callback(err);
|
return callback(tagSesError(err));
|
||||||
}
|
}
|
||||||
return callback(null, true);
|
return callback(null, true);
|
||||||
};
|
};
|
||||||
@@ -205,15 +219,13 @@ class SESTransport extends EventEmitter {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
this.getRegion((err, region) => {
|
// the region value is not used for anything when verifying, but the lookup
|
||||||
if (err || !region) {
|
// exercises the client configuration the same way as send() does
|
||||||
region = 'us-east-1';
|
this.getRegion(() => {
|
||||||
}
|
|
||||||
|
|
||||||
const command = new this.ses.SendEmailCommand(sesMessage);
|
const command = new this.ses.SendEmailCommand(sesMessage);
|
||||||
const sendPromise = this.ses.sesClient.send(command);
|
const sendPromise = this.ses.sesClient.send(command);
|
||||||
|
|
||||||
sendPromise.then(data => cb(null, data)).catch(err => cb(err));
|
sendPromise.then(() => cb(null)).catch(err => cb(err));
|
||||||
});
|
});
|
||||||
|
|
||||||
return promise;
|
return promise;
|
||||||
|
|||||||
+44
-11
@@ -2,10 +2,11 @@
|
|||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const urllib = require('url');
|
const urllib = require('./url');
|
||||||
const util = require('util');
|
const util = require('util');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const nmfetch = require('../fetch');
|
const nmfetch = require('../fetch');
|
||||||
|
const errors = require('../errors');
|
||||||
const dns = require('dns');
|
const dns = require('dns');
|
||||||
const net = require('net');
|
const net = require('net');
|
||||||
const os = require('os');
|
const os = require('os');
|
||||||
@@ -366,7 +367,16 @@ module.exports._logFunc = (logger, level, defaults, data, message, ...args) => {
|
|||||||
const entry = Object.assign({}, defaults || {}, data || {});
|
const entry = Object.assign({}, defaults || {}, data || {});
|
||||||
delete entry.level;
|
delete entry.level;
|
||||||
|
|
||||||
logger[level](entry, message, ...args);
|
let logLevel = level;
|
||||||
|
if (typeof logger[logLevel] !== 'function') {
|
||||||
|
// Provided logger does not implement this level. Fall back to a
|
||||||
|
// lower-severity handler instead of throwing.
|
||||||
|
logLevel = ['info', 'debug', 'log', 'trace', 'warn', 'error'].find(name => typeof logger[name] === 'function');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (logLevel) {
|
||||||
|
logger[logLevel](entry, message, ...args);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -501,9 +511,17 @@ module.exports.parseDataURI = uri => {
|
|||||||
*
|
*
|
||||||
* @param {Object} data An object or an Array you want to resolve an element for
|
* @param {Object} data An object or an Array you want to resolve an element for
|
||||||
* @param {String|Number} key Property name or an Array index
|
* @param {String|Number} key Property name or an Array index
|
||||||
|
* @param {Object} [options] Optional access policy: { disableFileAccess, disableUrlAccess }
|
||||||
* @param {Function} callback Callback function with (err, value)
|
* @param {Function} callback Callback function with (err, value)
|
||||||
*/
|
*/
|
||||||
module.exports.resolveContent = (data, key, callback) => {
|
module.exports.resolveContent = (data, key, options, callback) => {
|
||||||
|
// options is optional; support the legacy resolveContent(data, key, callback) signature
|
||||||
|
if (!callback && typeof options === 'function') {
|
||||||
|
callback = options;
|
||||||
|
options = false;
|
||||||
|
}
|
||||||
|
options = options || {};
|
||||||
|
|
||||||
let promise;
|
let promise;
|
||||||
|
|
||||||
if (!callback) {
|
if (!callback) {
|
||||||
@@ -512,6 +530,12 @@ module.exports.resolveContent = (data, key, callback) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resolveContentValue(data, key, options, callback);
|
||||||
|
|
||||||
|
return promise;
|
||||||
|
};
|
||||||
|
|
||||||
|
function resolveContentValue(data, key, options, callback) {
|
||||||
let content = (data && data[key] && data[key].content) || data[key];
|
let content = (data && data[key] && data[key].content) || data[key];
|
||||||
const encoding = ((typeof data[key] === 'object' && data[key].encoding) || 'utf8')
|
const encoding = ((typeof data[key] === 'object' && data[key].encoding) || 'utf8')
|
||||||
.toString()
|
.toString()
|
||||||
@@ -538,15 +562,26 @@ module.exports.resolveContent = (data, key, callback) => {
|
|||||||
callback(null, value);
|
callback(null, value);
|
||||||
});
|
});
|
||||||
} else if (/^https?:\/\//i.test(content.path || content.href)) {
|
} else if (/^https?:\/\//i.test(content.path || content.href)) {
|
||||||
return resolveStream(nmfetch(content.path || content.href), callback);
|
if (options.disableUrlAccess) {
|
||||||
|
return setImmediate(() => {
|
||||||
|
const err = new Error('Url access rejected for ' + (content.path || content.href));
|
||||||
|
err.code = errors.EURLACCESS;
|
||||||
|
callback(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return resolveStream(nmfetch(content.path || content.href, { headers: content.httpHeaders, tls: content.tls }), callback);
|
||||||
} else if (/^data:/i.test(content.path || content.href)) {
|
} else if (/^data:/i.test(content.path || content.href)) {
|
||||||
const parsedDataUri = module.exports.parseDataURI(content.path || content.href);
|
const parsedDataUri = module.exports.parseDataURI(content.path || content.href);
|
||||||
|
|
||||||
if (!parsedDataUri || !parsedDataUri.data) {
|
return callback(null, parsedDataUri && parsedDataUri.data ? parsedDataUri.data : Buffer.alloc(0));
|
||||||
return callback(null, Buffer.from(0));
|
|
||||||
}
|
|
||||||
return callback(null, parsedDataUri.data);
|
|
||||||
} else if (content.path) {
|
} else if (content.path) {
|
||||||
|
if (options.disableFileAccess) {
|
||||||
|
return setImmediate(() => {
|
||||||
|
const err = new Error('File access rejected for ' + content.path);
|
||||||
|
err.code = errors.EFILEACCESS;
|
||||||
|
callback(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
return resolveStream(fs.createReadStream(content.path), callback);
|
return resolveStream(fs.createReadStream(content.path), callback);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -557,9 +592,7 @@ module.exports.resolveContent = (data, key, callback) => {
|
|||||||
|
|
||||||
// default action, return as is
|
// default action, return as is
|
||||||
setImmediate(() => callback(null, content));
|
setImmediate(() => callback(null, content));
|
||||||
|
}
|
||||||
return promise;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Copies properties from source objects to target objects
|
* Copies properties from source objects to target objects
|
||||||
|
|||||||
+151
@@ -0,0 +1,151 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
// URL parsing wrapper. Prefers the WHATWG `URL` (a global on Node 10+, and
|
||||||
|
// available as `require('url').URL` since Node 6.13+) and only falls back to the
|
||||||
|
// legacy, deprecation-warning-emitting `url.parse()` / `url.resolve()` on ancient
|
||||||
|
// Node versions that predate the WHATWG implementation.
|
||||||
|
//
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
const urllib = require('url');
|
||||||
|
const punycode = require('../punycode');
|
||||||
|
|
||||||
|
// WHATWG URL constructor if available, otherwise undefined (Node < 6.13).
|
||||||
|
const URLImpl = (typeof URL !== 'undefined' && URL) || urllib.URL;
|
||||||
|
|
||||||
|
// 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+.-]*:)(?!\/\/)(.+)$/;
|
||||||
|
|
||||||
|
// 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 punycode form. Idempotent on plain ASCII and already-punycode
|
||||||
|
// hosts, so special-scheme hosts (already IDNA-mapped by WHATWG) pass through.
|
||||||
|
function normalizeHostname(raw) {
|
||||||
|
let 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);
|
||||||
|
}
|
||||||
|
return punycode.toASCII(safeDecode(hostname));
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.parse = (input, parseQueryString) => {
|
||||||
|
input = input || '';
|
||||||
|
|
||||||
|
if (!URLImpl) {
|
||||||
|
// Node < 6.13: no WHATWG URL available, use the legacy parser.
|
||||||
|
return urllib.parse(input, parseQueryString);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 URLImpl(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 urllib.parse(input, parseQueryString);
|
||||||
|
}
|
||||||
|
|
||||||
|
const hostname = normalizeHostname(u.hostname);
|
||||||
|
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;
|
||||||
|
if (u.username || u.password) {
|
||||||
|
// Gate on password too: legacy url.parse('smtps://:pass@host').auth was
|
||||||
|
// ':pass'. Dropping it would silently connect unauthenticated.
|
||||||
|
auth = safeDecode(u.username) + (u.password ? ':' + safeDecode(u.password) : '');
|
||||||
|
}
|
||||||
|
|
||||||
|
let query;
|
||||||
|
if (parseQueryString) {
|
||||||
|
// Mirror querystring.parse(): null-prototype object, repeated keys → array.
|
||||||
|
query = Object.create(null);
|
||||||
|
u.searchParams.forEach((value, key) => {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(query, key)) {
|
||||||
|
if (Array.isArray(query[key])) {
|
||||||
|
query[key].push(value);
|
||||||
|
} else {
|
||||||
|
query[key] = [query[key], value];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
query[key] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} 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,
|
||||||
|
query
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports.resolve = (from, to) => {
|
||||||
|
if (!URLImpl) {
|
||||||
|
return urllib.resolve(from, to);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return new URLImpl(to, from).href;
|
||||||
|
} catch (_err) {
|
||||||
|
// Malformed target — fall back to the legacy resolver.
|
||||||
|
return urllib.resolve(from, to);
|
||||||
|
}
|
||||||
|
};
|
||||||
+15
-6
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
const net = require('net');
|
const net = require('net');
|
||||||
const tls = require('tls');
|
const tls = require('tls');
|
||||||
const urllib = require('url');
|
const urllib = require('../shared/url');
|
||||||
const errors = require('../errors');
|
const errors = require('../errors');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -19,20 +19,29 @@ const errors = require('../errors');
|
|||||||
* @param {String} proxyUrl proxy configuration, etg "http://proxy.host:3128/"
|
* @param {String} proxyUrl proxy configuration, etg "http://proxy.host:3128/"
|
||||||
* @param {Number} destinationPort Port to open in destination host
|
* @param {Number} destinationPort Port to open in destination host
|
||||||
* @param {String} destinationHost Destination hostname
|
* @param {String} destinationHost Destination hostname
|
||||||
|
* @param {Object} [tlsOptions] Optional TLS options for an HTTPS proxy (e.g. { rejectUnauthorized: false })
|
||||||
* @param {Function} callback Callback to run with the rocket object once connection is established
|
* @param {Function} callback Callback to run with the rocket object once connection is established
|
||||||
*/
|
*/
|
||||||
function httpProxyClient(proxyUrl, destinationPort, destinationHost, callback) {
|
function httpProxyClient(proxyUrl, destinationPort, destinationHost, tlsOptions, callback) {
|
||||||
|
if (typeof tlsOptions === 'function') {
|
||||||
|
callback = tlsOptions;
|
||||||
|
tlsOptions = {};
|
||||||
|
}
|
||||||
|
tlsOptions = tlsOptions || {};
|
||||||
|
|
||||||
const proxy = urllib.parse(proxyUrl);
|
const proxy = urllib.parse(proxyUrl);
|
||||||
|
|
||||||
const options = {
|
const connectOptions = {
|
||||||
host: proxy.hostname,
|
host: proxy.hostname,
|
||||||
port: Number(proxy.port) ? Number(proxy.port) : proxy.protocol === 'https:' ? 443 : 80
|
port: Number(proxy.port) ? Number(proxy.port) : proxy.protocol === 'https:' ? 443 : 80
|
||||||
};
|
};
|
||||||
|
|
||||||
let connect;
|
let connect;
|
||||||
if (proxy.protocol === 'https:') {
|
if (proxy.protocol === 'https:') {
|
||||||
// we can use untrusted proxies as long as we verify actual SMTP certificates
|
// Validate the proxy's TLS certificate by default. A caller that uses a
|
||||||
options.rejectUnauthorized = false;
|
// self-signed proxy (e.g. integration tests) opts out explicitly with
|
||||||
|
// tls.rejectUnauthorized === false.
|
||||||
|
connectOptions.rejectUnauthorized = tlsOptions.rejectUnauthorized !== false;
|
||||||
connect = tls.connect.bind(tls);
|
connect = tls.connect.bind(tls);
|
||||||
} else {
|
} else {
|
||||||
connect = net.connect.bind(net);
|
connect = net.connect.bind(net);
|
||||||
@@ -62,7 +71,7 @@ function httpProxyClient(proxyUrl, destinationPort, destinationHost, callback) {
|
|||||||
tempSocketErr(err);
|
tempSocketErr(err);
|
||||||
};
|
};
|
||||||
|
|
||||||
socket = connect(options, () => {
|
socket = connect(connectOptions, () => {
|
||||||
if (finished) {
|
if (finished) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
+40
-9
@@ -51,9 +51,9 @@ function decodeServerResponse(str) {
|
|||||||
* * **requireTLS** - forces the client to use STARTTLS
|
* * **requireTLS** - forces the client to use STARTTLS
|
||||||
* * **name** - the name of the client server
|
* * **name** - the name of the client server
|
||||||
* * **localAddress** - outbound address to bind to (see: http://nodejs.org/api/net.html#net_net_connect_options_connectionlistener)
|
* * **localAddress** - outbound address to bind to (see: http://nodejs.org/api/net.html#net_net_connect_options_connectionlistener)
|
||||||
* * **greetingTimeout** - Time to wait in ms until greeting message is received from the server (defaults to 10000)
|
* * **greetingTimeout** - Time to wait in ms until greeting message is received from the server (defaults to 30 seconds)
|
||||||
* * **connectionTimeout** - how many milliseconds to wait for the connection to establish
|
* * **connectionTimeout** - how many milliseconds to wait for the connection to establish (defaults to 2 minutes)
|
||||||
* * **socketTimeout** - Time of inactivity until the connection is closed (defaults to 1 hour)
|
* * **socketTimeout** - Time of inactivity until the connection is closed (defaults to 10 minutes)
|
||||||
* * **dnsTimeout** - Time to wait in ms for the DNS requests to be resolved (defaults to 30 seconds)
|
* * **dnsTimeout** - Time to wait in ms for the DNS requests to be resolved (defaults to 30 seconds)
|
||||||
* * **lmtp** - if true, uses LMTP instead of SMTP protocol
|
* * **lmtp** - if true, uses LMTP instead of SMTP protocol
|
||||||
* * **logger** - bunyan compatible logger interface
|
* * **logger** - bunyan compatible logger interface
|
||||||
@@ -211,6 +211,13 @@ class SMTPConnection extends EventEmitter {
|
|||||||
*/
|
*/
|
||||||
this._closing = false;
|
this._closing = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Message DATA stream currently piped to the socket, if any. Tracked so
|
||||||
|
* close() can unpipe it before tearing the socket down.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
this._currentDataStream = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Callbacks for socket's listeners
|
* Callbacks for socket's listeners
|
||||||
*/
|
*/
|
||||||
@@ -470,6 +477,17 @@ class SMTPConnection extends EventEmitter {
|
|||||||
|
|
||||||
const socket = (this._socket && this._socket.socket) || this._socket;
|
const socket = (this._socket && this._socket.socket) || this._socket;
|
||||||
|
|
||||||
|
// Detach any in-flight DATA stream from the socket so the source stream
|
||||||
|
// can be garbage-collected once the socket is gone.
|
||||||
|
if (this._currentDataStream) {
|
||||||
|
try {
|
||||||
|
this._currentDataStream.unpipe(this._socket);
|
||||||
|
} catch (_E) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
this._currentDataStream = false;
|
||||||
|
}
|
||||||
|
|
||||||
if (socket && !socket.destroyed) {
|
if (socket && !socket.destroyed) {
|
||||||
try {
|
try {
|
||||||
// Clear socket timeout to prevent timer leaks
|
// Clear socket timeout to prevent timer leaks
|
||||||
@@ -820,7 +838,7 @@ class SMTPConnection extends EventEmitter {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let data = (chunk || '').toString('binary');
|
let data = chunk.toString('binary');
|
||||||
let lines = (this._remainder + data).split(/\r?\n/);
|
let lines = (this._remainder + data).split(/\r?\n/);
|
||||||
let lastline;
|
let lastline;
|
||||||
|
|
||||||
@@ -953,7 +971,9 @@ class SMTPConnection extends EventEmitter {
|
|||||||
*/
|
*/
|
||||||
_onEnd() {
|
_onEnd() {
|
||||||
if (this._socket && !this._socket.destroyed) {
|
if (this._socket && !this._socket.destroyed) {
|
||||||
this._socket.destroy();
|
// Peer sent FIN — finish our half of the close gracefully rather
|
||||||
|
// than destroying. 'close' fires after the OS finalizes teardown.
|
||||||
|
this._socket.end();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1005,6 +1025,15 @@ class SMTPConnection extends EventEmitter {
|
|||||||
opts.servername = this.servername;
|
opts.servername = this.servername;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remove all listeners from the plain socket to allow proper garbage
|
||||||
|
// collection. Used on both the TLS-success path and the synchronous
|
||||||
|
// tls.connect() throw path; either way the plain socket is done.
|
||||||
|
const removePlainSocketListeners = () => {
|
||||||
|
socketPlain.removeListener('close', this._onSocketClose);
|
||||||
|
socketPlain.removeListener('end', this._onSocketEnd);
|
||||||
|
socketPlain.removeListener('error', this._onSocketError);
|
||||||
|
};
|
||||||
|
|
||||||
this.upgrading = true;
|
this.upgrading = true;
|
||||||
// tls.connect is not an asynchronous function however it may still throw errors and requires to be wrapped with try/catch
|
// tls.connect is not an asynchronous function however it may still throw errors and requires to be wrapped with try/catch
|
||||||
try {
|
try {
|
||||||
@@ -1013,14 +1042,12 @@ class SMTPConnection extends EventEmitter {
|
|||||||
this.upgrading = false;
|
this.upgrading = false;
|
||||||
this._socket.on('data', this._onSocketData);
|
this._socket.on('data', this._onSocketData);
|
||||||
|
|
||||||
// Remove all listeners from the plain socket to allow proper garbage collection
|
removePlainSocketListeners();
|
||||||
socketPlain.removeListener('close', this._onSocketClose);
|
|
||||||
socketPlain.removeListener('end', this._onSocketEnd);
|
|
||||||
socketPlain.removeListener('error', this._onSocketError);
|
|
||||||
|
|
||||||
return callback(null, true);
|
return callback(null, true);
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
removePlainSocketListeners();
|
||||||
return callback(err);
|
return callback(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1297,6 +1324,7 @@ class SMTPConnection extends EventEmitter {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this._currentDataStream = dataStream;
|
||||||
dataStream.pipe(this._socket, {
|
dataStream.pipe(this._socket, {
|
||||||
end: false
|
end: false
|
||||||
});
|
});
|
||||||
@@ -1318,6 +1346,9 @@ class SMTPConnection extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dataStream.once('end', () => {
|
dataStream.once('end', () => {
|
||||||
|
if (this._currentDataStream === dataStream) {
|
||||||
|
this._currentDataStream = false;
|
||||||
|
}
|
||||||
this.logger.info(
|
this.logger.info(
|
||||||
{
|
{
|
||||||
tnx: 'message',
|
tnx: 'message',
|
||||||
|
|||||||
+38
-11
@@ -71,13 +71,19 @@ class SMTPTransport extends EventEmitter {
|
|||||||
|
|
||||||
getAuth(authOpts) {
|
getAuth(authOpts) {
|
||||||
if (!authOpts) {
|
if (!authOpts) {
|
||||||
|
if (this.auth && this.auth.oauth2 && this.mailer) {
|
||||||
|
// Transport-level auth is resolved in the constructor, before the Mail wrapper
|
||||||
|
// assigns `this.mailer`, so a provision callback registered with
|
||||||
|
// `transporter.set('oauth2_provision_cb', ...)` has to be re-checked here
|
||||||
|
this.auth.oauth2.provisionCallback = this.mailer.get('oauth2_provision_cb') || this.auth.oauth2.provisionCallback;
|
||||||
|
}
|
||||||
return this.auth;
|
return this.auth;
|
||||||
}
|
}
|
||||||
|
|
||||||
const authData = Object.assign(
|
const authData = Object.assign(
|
||||||
{},
|
{},
|
||||||
this.options.auth && typeof this.options.auth === 'object' ? this.options.auth : {},
|
this.options.auth && typeof this.options.auth === 'object' ? this.options.auth : {},
|
||||||
authOpts && typeof authOpts === 'object' ? authOpts : {}
|
typeof authOpts === 'object' ? authOpts : {}
|
||||||
);
|
);
|
||||||
|
|
||||||
if (Object.keys(authData).length === 0) {
|
if (Object.keys(authData).length === 0) {
|
||||||
@@ -151,11 +157,20 @@ class SMTPTransport extends EventEmitter {
|
|||||||
|
|
||||||
const connection = new SMTPConnection(options);
|
const connection = new SMTPConnection(options);
|
||||||
|
|
||||||
|
let perCallAuth;
|
||||||
|
const cleanupPerCallAuth = () => {
|
||||||
|
if (perCallAuth && perCallAuth !== this.auth && perCallAuth.oauth2) {
|
||||||
|
perCallAuth.oauth2.removeAllListeners();
|
||||||
|
}
|
||||||
|
perCallAuth = null;
|
||||||
|
};
|
||||||
|
|
||||||
connection.once('error', err => {
|
connection.once('error', err => {
|
||||||
if (returned) {
|
if (returned) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
returned = true;
|
returned = true;
|
||||||
|
cleanupPerCallAuth();
|
||||||
connection.close();
|
connection.close();
|
||||||
return callback(err);
|
return callback(err);
|
||||||
});
|
});
|
||||||
@@ -170,6 +185,7 @@ class SMTPTransport extends EventEmitter {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
returned = true;
|
returned = true;
|
||||||
|
cleanupPerCallAuth();
|
||||||
// still have not returned, this means we have an unexpected connection close
|
// still have not returned, this means we have an unexpected connection close
|
||||||
const err = new Error('Unexpected socket close');
|
const err = new Error('Unexpected socket close');
|
||||||
if (connection && connection._socket && connection._socket.upgrading) {
|
if (connection && connection._socket && connection._socket.upgrading) {
|
||||||
@@ -216,6 +232,7 @@ class SMTPTransport extends EventEmitter {
|
|||||||
|
|
||||||
connection.send(envelope, mail.message.createReadStream(), (err, info) => {
|
connection.send(envelope, mail.message.createReadStream(), (err, info) => {
|
||||||
returned = true;
|
returned = true;
|
||||||
|
cleanupPerCallAuth();
|
||||||
connection.close();
|
connection.close();
|
||||||
if (err) {
|
if (err) {
|
||||||
this.logger.error(
|
this.logger.error(
|
||||||
@@ -255,13 +272,11 @@ class SMTPTransport extends EventEmitter {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auth = this.getAuth(mail.data.auth);
|
perCallAuth = this.getAuth(mail.data.auth);
|
||||||
|
|
||||||
if (auth && (connection.allowsAuth || options.forceAuth)) {
|
if (perCallAuth && (connection.allowsAuth || options.forceAuth)) {
|
||||||
connection.login(auth, err => {
|
connection.login(perCallAuth, err => {
|
||||||
if (auth && auth !== this.auth && auth.oauth2) {
|
cleanupPerCallAuth();
|
||||||
auth.oauth2.removeAllListeners();
|
|
||||||
}
|
|
||||||
if (returned) {
|
if (returned) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -323,12 +338,20 @@ class SMTPTransport extends EventEmitter {
|
|||||||
|
|
||||||
const connection = new SMTPConnection(options);
|
const connection = new SMTPConnection(options);
|
||||||
let returned = false;
|
let returned = false;
|
||||||
|
let perCallAuth;
|
||||||
|
const cleanupPerCallAuth = () => {
|
||||||
|
if (perCallAuth && perCallAuth !== this.auth && perCallAuth.oauth2) {
|
||||||
|
perCallAuth.oauth2.removeAllListeners();
|
||||||
|
}
|
||||||
|
perCallAuth = null;
|
||||||
|
};
|
||||||
|
|
||||||
connection.once('error', err => {
|
connection.once('error', err => {
|
||||||
if (returned) {
|
if (returned) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
returned = true;
|
returned = true;
|
||||||
|
cleanupPerCallAuth();
|
||||||
connection.close();
|
connection.close();
|
||||||
return callback(err);
|
return callback(err);
|
||||||
});
|
});
|
||||||
@@ -338,6 +361,7 @@ class SMTPTransport extends EventEmitter {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
returned = true;
|
returned = true;
|
||||||
|
cleanupPerCallAuth();
|
||||||
return callback(new Error('Connection closed'));
|
return callback(new Error('Connection closed'));
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -346,6 +370,7 @@ class SMTPTransport extends EventEmitter {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
returned = true;
|
returned = true;
|
||||||
|
cleanupPerCallAuth();
|
||||||
connection.quit();
|
connection.quit();
|
||||||
return callback(null, true);
|
return callback(null, true);
|
||||||
};
|
};
|
||||||
@@ -355,10 +380,11 @@ class SMTPTransport extends EventEmitter {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const authData = this.getAuth({});
|
perCallAuth = this.getAuth({});
|
||||||
|
|
||||||
if (authData && (connection.allowsAuth || options.forceAuth)) {
|
if (perCallAuth && (connection.allowsAuth || options.forceAuth)) {
|
||||||
connection.login(authData, err => {
|
connection.login(perCallAuth, err => {
|
||||||
|
cleanupPerCallAuth();
|
||||||
if (returned) {
|
if (returned) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -371,11 +397,12 @@ class SMTPTransport extends EventEmitter {
|
|||||||
|
|
||||||
finalize();
|
finalize();
|
||||||
});
|
});
|
||||||
} else if (!authData && connection.allowsAuth && options.forceAuth) {
|
} else if (!perCallAuth && connection.allowsAuth && options.forceAuth) {
|
||||||
const err = new Error('Authentication info was not provided');
|
const err = new Error('Authentication info was not provided');
|
||||||
err.code = errors.ENOAUTH;
|
err.code = errors.ENOAUTH;
|
||||||
|
|
||||||
returned = true;
|
returned = true;
|
||||||
|
cleanupPerCallAuth();
|
||||||
connection.close();
|
connection.close();
|
||||||
return callback(err);
|
return callback(err);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+9
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
const packageData = require('../../package.json');
|
const packageData = require('../../package.json');
|
||||||
const shared = require('../shared');
|
const shared = require('../shared');
|
||||||
|
const LeWindows = require('../mime-node/le-windows');
|
||||||
|
const LeUnix = require('../mime-node/le-unix');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a Transport object for streaming
|
* Generates a Transport object for streaming
|
||||||
@@ -63,6 +65,13 @@ class StreamTransport {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
stream = mail.message.createReadStream();
|
stream = mail.message.createReadStream();
|
||||||
|
if (this.options.newline) {
|
||||||
|
// apply the transport-level line ending transform; the message-level
|
||||||
|
// `newline` option is handled by MimeNode in createReadStream()
|
||||||
|
const sourceStream = stream;
|
||||||
|
stream = sourceStream.pipe(this.winbreak ? new LeWindows() : new LeUnix());
|
||||||
|
sourceStream.once('error', err => stream.emit('error', err));
|
||||||
|
}
|
||||||
} catch (E) {
|
} catch (E) {
|
||||||
this.logger.error(
|
this.logger.error(
|
||||||
{
|
{
|
||||||
|
|||||||
+14
-2
@@ -33,6 +33,7 @@ const errors = require('../errors');
|
|||||||
* @param {Number} options.expires Optional Access Token expire time in ms
|
* @param {Number} options.expires Optional Access Token expire time in ms
|
||||||
* @param {Number} options.timeout Optional TTL for Access Token in seconds
|
* @param {Number} options.timeout Optional TTL for Access Token in seconds
|
||||||
* @param {Function} options.provisionCallback Function to run when a new access token is required
|
* @param {Function} options.provisionCallback Function to run when a new access token is required
|
||||||
|
* @param {Object} options.tls Optional TLS options forwarded to the HTTPS token request. Defaults to strict cert validation; supply { rejectUnauthorized: false } only for self-hosted OAuth providers on private CAs.
|
||||||
*/
|
*/
|
||||||
class XOAuth2 extends Stream {
|
class XOAuth2 extends Stream {
|
||||||
constructor(options, logger) {
|
constructor(options, logger) {
|
||||||
@@ -370,12 +371,23 @@ class XOAuth2 extends Stream {
|
|||||||
const chunks = [];
|
const chunks = [];
|
||||||
let chunklen = 0;
|
let chunklen = 0;
|
||||||
|
|
||||||
const req = nmfetch(url, {
|
const fetchOptions = {
|
||||||
method: 'post',
|
method: 'post',
|
||||||
headers: params.customHeaders,
|
headers: params.customHeaders,
|
||||||
body: payload,
|
body: payload,
|
||||||
allowErrorResponse: true
|
allowErrorResponse: true
|
||||||
});
|
};
|
||||||
|
|
||||||
|
// OAuth2 token endpoints are credential-bearing. lib/fetch already
|
||||||
|
// validates certs by default; pin rejectUnauthorized:true here so the
|
||||||
|
// token fetch stays strict, while still layering params.tls (the
|
||||||
|
// user's options.tls) on top so callers with a self-hosted provider on
|
||||||
|
// a private CA can override.
|
||||||
|
if (/^https:/i.test(url)) {
|
||||||
|
fetchOptions.tls = Object.assign({ rejectUnauthorized: true }, params.tls || {});
|
||||||
|
}
|
||||||
|
|
||||||
|
const req = nmfetch(url, fetchOptions);
|
||||||
|
|
||||||
req.on('readable', () => {
|
req.on('readable', () => {
|
||||||
let chunk;
|
let chunk;
|
||||||
|
|||||||
+8
-8
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "nodemailer",
|
"name": "nodemailer",
|
||||||
"version": "8.0.7",
|
"version": "9.0.0",
|
||||||
"description": "Easy as cake e-mail sending from your Node.js applications",
|
"description": "Easy as cake e-mail sending from your Node.js applications",
|
||||||
"main": "lib/nodemailer.js",
|
"main": "lib/nodemailer.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "node --test --test-concurrency=1 test/**/*.test.js test/**/*-test.js",
|
"test": "node --test --test-concurrency=1 $(find test \\( -name '*-test.js' -o -name '*.test.js' \\))",
|
||||||
"test:coverage": "c8 node --test --test-concurrency=1 test/**/*.test.js test/**/*-test.js",
|
"test:coverage": "c8 node --test --test-concurrency=1 $(find test \\( -name '*-test.js' -o -name '*.test.js' \\))",
|
||||||
"format": "prettier --write \"**/*.{js,json,md}\"",
|
"format": "prettier --write \"**/*.{js,json,md}\"",
|
||||||
"format:check": "prettier --check \"**/*.{js,json,md}\"",
|
"format:check": "prettier --check \"**/*.{js,json,md}\"",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
@@ -27,19 +27,19 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://nodemailer.com/",
|
"homepage": "https://nodemailer.com/",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@aws-sdk/client-sesv2": "3.1037.0",
|
"@aws-sdk/client-sesv2": "3.1065.0",
|
||||||
"bunyan": "1.8.15",
|
"bunyan": "1.8.15",
|
||||||
"c8": "11.0.0",
|
"c8": "11.0.0",
|
||||||
"eslint": "10.2.1",
|
"eslint": "10.4.1",
|
||||||
"eslint-config-prettier": "10.1.8",
|
"eslint-config-prettier": "10.1.8",
|
||||||
"globals": "17.5.0",
|
"globals": "17.6.0",
|
||||||
"libbase64": "1.3.0",
|
"libbase64": "1.3.0",
|
||||||
"libmime": "5.3.8",
|
"libmime": "5.3.8",
|
||||||
"libqp": "2.1.1",
|
"libqp": "2.1.1",
|
||||||
"prettier": "3.8.3",
|
"prettier": "3.8.4",
|
||||||
"proxy": "1.0.2",
|
"proxy": "1.0.2",
|
||||||
"proxy-test-server": "1.0.0",
|
"proxy-test-server": "1.0.0",
|
||||||
"smtp-server": "3.18.4"
|
"smtp-server": "3.18.5"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user