mirror of
https://github.com/dawidd6/action-send-mail.git
synced 2026-06-20 00:07:26 +07:00
node_modules: update (#297)
Co-authored-by: dawidd6 <9713907+dawidd6@users.noreply.github.com>
This commit is contained in:
+1
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
// module to handle cookies
|
||||
|
||||
const urllib = require('url');
|
||||
const urllib = require('../shared/url');
|
||||
|
||||
const SESSION_TIMEOUT = 1800; // 30 min
|
||||
|
||||
|
||||
+26
-3
@@ -2,7 +2,7 @@
|
||||
|
||||
const http = require('http');
|
||||
const https = require('https');
|
||||
const urllib = require('url');
|
||||
const urllib = require('../shared/url');
|
||||
const zlib = require('zlib');
|
||||
const { PassThrough } = require('stream');
|
||||
const Cookies = require('./cookies');
|
||||
@@ -123,7 +123,10 @@ function nmfetch(url, options) {
|
||||
path: parsed.path,
|
||||
port: parsed.port ? parsed.port : parsed.protocol === 'https:' ? 443 : 80,
|
||||
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
|
||||
};
|
||||
|
||||
@@ -212,7 +215,27 @@ function nmfetch(url, options) {
|
||||
// redirect does not include POST body
|
||||
options.method = 'GET';
|
||||
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;
|
||||
|
||||
+67
-33
@@ -83,7 +83,7 @@ class MailComposer {
|
||||
* @returns {Object} An object of arrays (`related` and `attached`)
|
||||
*/
|
||||
getAttachments(findRelated) {
|
||||
let icalEvent, eventObject;
|
||||
let eventObject;
|
||||
const attachments = [].concat(this.mail.attachments || []).map((attachment, i) => {
|
||||
if (/^data:/i.test(attachment.path || attachment.href)) {
|
||||
attachment = this._processDataUrl(attachment);
|
||||
@@ -142,7 +142,8 @@ class MailComposer {
|
||||
} else if (attachment.href) {
|
||||
data.content = {
|
||||
href: attachment.href,
|
||||
httpHeaders: attachment.httpHeaders
|
||||
httpHeaders: attachment.httpHeaders,
|
||||
tls: attachment.tls
|
||||
};
|
||||
} else {
|
||||
data.content = attachment.content || '';
|
||||
@@ -160,18 +161,7 @@ class MailComposer {
|
||||
});
|
||||
|
||||
if (this.mail.icalEvent) {
|
||||
if (
|
||||
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 = Object.assign({}, this._getIcalEvent());
|
||||
|
||||
eventObject.contentType = 'application/ics';
|
||||
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
|
||||
*
|
||||
@@ -202,7 +253,7 @@ class MailComposer {
|
||||
*/
|
||||
getAlternatives() {
|
||||
const alternatives = [];
|
||||
let text, html, watchHtml, amp, icalEvent, eventObject;
|
||||
let text, html, watchHtml, amp, eventObject;
|
||||
|
||||
if (this.mail.text) {
|
||||
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
|
||||
if (this.mail.icalEvent) {
|
||||
if (
|
||||
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 = Object.assign({}, this._getIcalEvent());
|
||||
|
||||
eventObject.filename = false;
|
||||
eventObject.contentType =
|
||||
|
||||
+34
-26
@@ -8,7 +8,7 @@ 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 urllib = require('../shared/url');
|
||||
const packageData = require('../../package.json');
|
||||
const MailMessage = require('./mail-message');
|
||||
const net = require('net');
|
||||
@@ -324,7 +324,7 @@ class Mail extends EventEmitter {
|
||||
// Connect using a HTTP CONNECT method
|
||||
case 'http':
|
||||
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) {
|
||||
return callback(err);
|
||||
}
|
||||
@@ -407,31 +407,39 @@ class Mail extends EventEmitter {
|
||||
if ((!this.options.attachDataUrls && !mail.data.attachDataUrls) || !mail.data.html) {
|
||||
return callback();
|
||||
}
|
||||
mail.resolveContent(mail.data, 'html', (err, html) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
mail.resolveContent(
|
||||
mail.data,
|
||||
'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) {
|
||||
|
||||
+30
-20
@@ -111,25 +111,29 @@ class MailMessage {
|
||||
if (!args[0] || !args[0][args[1]]) {
|
||||
return resolveNext();
|
||||
}
|
||||
shared.resolveContent(...args, (err, value) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
shared.resolveContent(
|
||||
...args,
|
||||
{ disableFileAccess: this.data.disableFileAccess, disableUrlAccess: this.data.disableUrlAccess },
|
||||
(err, value) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
const node = {
|
||||
content: value
|
||||
};
|
||||
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 => {
|
||||
if (!(key in node) && !['content', 'path', 'href', 'raw'].includes(key)) {
|
||||
node[key] = args[0][args[1]][key];
|
||||
}
|
||||
});
|
||||
}
|
||||
const node = {
|
||||
content: value
|
||||
};
|
||||
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 => {
|
||||
if (!(key in node) && !['content', 'path', 'href', 'raw'].includes(key)) {
|
||||
node[key] = args[0][args[1]][key];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
args[0][args[1]] = node;
|
||||
resolveNext();
|
||||
});
|
||||
args[0][args[1]] = node;
|
||||
resolveNext();
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
setImmediate(() => resolveNext());
|
||||
@@ -269,18 +273,24 @@ class MailMessage {
|
||||
if (value && value.url) {
|
||||
if (key.toLowerCase().trim() === 'id') {
|
||||
// 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)) {
|
||||
comment = '"' + comment + '"';
|
||||
} else {
|
||||
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)
|
||||
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)) {
|
||||
comment = mimeFuncs.encodeWord(comment);
|
||||
}
|
||||
|
||||
+1
-1
@@ -2087,7 +2087,7 @@ module.exports = {
|
||||
if (!mimeType) {
|
||||
return defaultExtension;
|
||||
}
|
||||
const parts = (mimeType || '').toLowerCase().trim().split('/');
|
||||
const parts = mimeType.toLowerCase().trim().split('/');
|
||||
const rootType = parts.shift().trim();
|
||||
const subType = parts.join('/').trim();
|
||||
|
||||
|
||||
+1
-1
@@ -1006,7 +1006,7 @@ class MimeNode {
|
||||
return contentStream;
|
||||
}
|
||||
// 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
|
||||
|
||||
+26
-7
@@ -95,12 +95,22 @@ module.exports.createTestAccount = function (apiUrl, callback) {
|
||||
requestHeaders.Authorization = 'Bearer ' + ETHEREAL_API_KEY;
|
||||
}
|
||||
|
||||
const req = nmfetch(apiUrl + '/user', {
|
||||
const fetchOptions = {
|
||||
contentType: 'application/json',
|
||||
method: 'POST',
|
||||
headers: requestHeaders,
|
||||
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', () => {
|
||||
let chunk;
|
||||
@@ -137,11 +147,20 @@ module.exports.getTestMessageUrl = function (info) {
|
||||
}
|
||||
|
||||
const infoProps = new Map();
|
||||
info.response.replace(/\[([^\]]+)\]$/, (m, props) => {
|
||||
props.replace(/\b([A-Z0-9]+)=([^\s]+)/g, (m, key, value) => {
|
||||
infoProps.set(key, value);
|
||||
});
|
||||
});
|
||||
|
||||
// Extract the trailing "[...]" part of the response (no "]" allowed inside)
|
||||
// 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')) {
|
||||
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 shared = require('../shared');
|
||||
const errors = require('../errors');
|
||||
const LeWindows = require('../mime-node/le-windows');
|
||||
const LeUnix = require('../mime-node/le-unix');
|
||||
|
||||
/**
|
||||
* Generates a Transport object for Sendmail
|
||||
@@ -46,6 +48,8 @@ class SendmailTransport {
|
||||
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();
|
||||
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(
|
||||
{
|
||||
err,
|
||||
@@ -193,7 +205,7 @@ class SendmailTransport {
|
||||
callback(err);
|
||||
});
|
||||
|
||||
sourceStream.pipe(sendmail.stdin);
|
||||
stream.pipe(sendmail.stdin);
|
||||
} else {
|
||||
const err = new Error('sendmail was not found');
|
||||
err.code = errors.ESENDMAIL;
|
||||
|
||||
+19
-7
@@ -3,9 +3,22 @@
|
||||
const EventEmitter = require('events');
|
||||
const packageData = require('../../package.json');
|
||||
const shared = require('../shared');
|
||||
const errors = require('../errors');
|
||||
const LeWindows = require('../mime-node/le-windows');
|
||||
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
|
||||
*
|
||||
@@ -157,6 +170,7 @@ class SESTransport extends EventEmitter {
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
tagSesError(err);
|
||||
this.logger.error(
|
||||
{
|
||||
err,
|
||||
@@ -188,7 +202,7 @@ class SESTransport extends EventEmitter {
|
||||
|
||||
const cb = err => {
|
||||
if (err && !['InvalidParameterValue', 'MessageRejected'].includes(err.code || err.Code || err.name)) {
|
||||
return callback(err);
|
||||
return callback(tagSesError(err));
|
||||
}
|
||||
return callback(null, true);
|
||||
};
|
||||
@@ -205,15 +219,13 @@ class SESTransport extends EventEmitter {
|
||||
}
|
||||
};
|
||||
|
||||
this.getRegion((err, region) => {
|
||||
if (err || !region) {
|
||||
region = 'us-east-1';
|
||||
}
|
||||
|
||||
// the region value is not used for anything when verifying, but the lookup
|
||||
// exercises the client configuration the same way as send() does
|
||||
this.getRegion(() => {
|
||||
const command = new this.ses.SendEmailCommand(sesMessage);
|
||||
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;
|
||||
|
||||
+44
-11
@@ -2,10 +2,11 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
const urllib = require('url');
|
||||
const urllib = require('./url');
|
||||
const util = require('util');
|
||||
const fs = require('fs');
|
||||
const nmfetch = require('../fetch');
|
||||
const errors = require('../errors');
|
||||
const dns = require('dns');
|
||||
const net = require('net');
|
||||
const os = require('os');
|
||||
@@ -366,7 +367,16 @@ module.exports._logFunc = (logger, level, defaults, data, message, ...args) => {
|
||||
const entry = Object.assign({}, defaults || {}, data || {});
|
||||
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 {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)
|
||||
*/
|
||||
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;
|
||||
|
||||
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];
|
||||
const encoding = ((typeof data[key] === 'object' && data[key].encoding) || 'utf8')
|
||||
.toString()
|
||||
@@ -538,15 +562,26 @@ module.exports.resolveContent = (data, key, callback) => {
|
||||
callback(null, value);
|
||||
});
|
||||
} 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)) {
|
||||
const parsedDataUri = module.exports.parseDataURI(content.path || content.href);
|
||||
|
||||
if (!parsedDataUri || !parsedDataUri.data) {
|
||||
return callback(null, Buffer.from(0));
|
||||
}
|
||||
return callback(null, parsedDataUri.data);
|
||||
return callback(null, parsedDataUri && parsedDataUri.data ? parsedDataUri.data : Buffer.alloc(0));
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
@@ -557,9 +592,7 @@ module.exports.resolveContent = (data, key, callback) => {
|
||||
|
||||
// default action, return as is
|
||||
setImmediate(() => callback(null, content));
|
||||
|
||||
return promise;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 tls = require('tls');
|
||||
const urllib = require('url');
|
||||
const urllib = require('../shared/url');
|
||||
const errors = require('../errors');
|
||||
|
||||
/**
|
||||
@@ -19,20 +19,29 @@ const errors = require('../errors');
|
||||
* @param {String} proxyUrl proxy configuration, etg "http://proxy.host:3128/"
|
||||
* @param {Number} destinationPort Port to open in destination host
|
||||
* @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
|
||||
*/
|
||||
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 options = {
|
||||
const connectOptions = {
|
||||
host: proxy.hostname,
|
||||
port: Number(proxy.port) ? Number(proxy.port) : proxy.protocol === 'https:' ? 443 : 80
|
||||
};
|
||||
|
||||
let connect;
|
||||
if (proxy.protocol === 'https:') {
|
||||
// we can use untrusted proxies as long as we verify actual SMTP certificates
|
||||
options.rejectUnauthorized = false;
|
||||
// Validate the proxy's TLS certificate by default. A caller that uses a
|
||||
// self-signed proxy (e.g. integration tests) opts out explicitly with
|
||||
// tls.rejectUnauthorized === false.
|
||||
connectOptions.rejectUnauthorized = tlsOptions.rejectUnauthorized !== false;
|
||||
connect = tls.connect.bind(tls);
|
||||
} else {
|
||||
connect = net.connect.bind(net);
|
||||
@@ -62,7 +71,7 @@ function httpProxyClient(proxyUrl, destinationPort, destinationHost, callback) {
|
||||
tempSocketErr(err);
|
||||
};
|
||||
|
||||
socket = connect(options, () => {
|
||||
socket = connect(connectOptions, () => {
|
||||
if (finished) {
|
||||
return;
|
||||
}
|
||||
|
||||
+40
-9
@@ -51,9 +51,9 @@ function decodeServerResponse(str) {
|
||||
* * **requireTLS** - forces the client to use STARTTLS
|
||||
* * **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)
|
||||
* * **greetingTimeout** - Time to wait in ms until greeting message is received from the server (defaults to 10000)
|
||||
* * **connectionTimeout** - how many milliseconds to wait for the connection to establish
|
||||
* * **socketTimeout** - Time of inactivity until the connection is closed (defaults to 1 hour)
|
||||
* * **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 (defaults to 2 minutes)
|
||||
* * **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)
|
||||
* * **lmtp** - if true, uses LMTP instead of SMTP protocol
|
||||
* * **logger** - bunyan compatible logger interface
|
||||
@@ -211,6 +211,13 @@ class SMTPConnection extends EventEmitter {
|
||||
*/
|
||||
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
|
||||
*/
|
||||
@@ -470,6 +477,17 @@ class SMTPConnection extends EventEmitter {
|
||||
|
||||
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) {
|
||||
try {
|
||||
// Clear socket timeout to prevent timer leaks
|
||||
@@ -820,7 +838,7 @@ class SMTPConnection extends EventEmitter {
|
||||
return;
|
||||
}
|
||||
|
||||
let data = (chunk || '').toString('binary');
|
||||
let data = chunk.toString('binary');
|
||||
let lines = (this._remainder + data).split(/\r?\n/);
|
||||
let lastline;
|
||||
|
||||
@@ -953,7 +971,9 @@ class SMTPConnection extends EventEmitter {
|
||||
*/
|
||||
_onEnd() {
|
||||
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;
|
||||
}
|
||||
|
||||
// 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;
|
||||
// tls.connect is not an asynchronous function however it may still throw errors and requires to be wrapped with try/catch
|
||||
try {
|
||||
@@ -1013,14 +1042,12 @@ 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);
|
||||
removePlainSocketListeners();
|
||||
|
||||
return callback(null, true);
|
||||
});
|
||||
} catch (err) {
|
||||
removePlainSocketListeners();
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
@@ -1297,6 +1324,7 @@ class SMTPConnection extends EventEmitter {
|
||||
});
|
||||
}
|
||||
|
||||
this._currentDataStream = dataStream;
|
||||
dataStream.pipe(this._socket, {
|
||||
end: false
|
||||
});
|
||||
@@ -1318,6 +1346,9 @@ class SMTPConnection extends EventEmitter {
|
||||
}
|
||||
|
||||
dataStream.once('end', () => {
|
||||
if (this._currentDataStream === dataStream) {
|
||||
this._currentDataStream = false;
|
||||
}
|
||||
this.logger.info(
|
||||
{
|
||||
tnx: 'message',
|
||||
|
||||
+38
-11
@@ -71,13 +71,19 @@ class SMTPTransport extends EventEmitter {
|
||||
|
||||
getAuth(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;
|
||||
}
|
||||
|
||||
const authData = Object.assign(
|
||||
{},
|
||||
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) {
|
||||
@@ -151,11 +157,20 @@ class SMTPTransport extends EventEmitter {
|
||||
|
||||
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 => {
|
||||
if (returned) {
|
||||
return;
|
||||
}
|
||||
returned = true;
|
||||
cleanupPerCallAuth();
|
||||
connection.close();
|
||||
return callback(err);
|
||||
});
|
||||
@@ -170,6 +185,7 @@ class SMTPTransport extends EventEmitter {
|
||||
return;
|
||||
}
|
||||
returned = true;
|
||||
cleanupPerCallAuth();
|
||||
// still have not returned, this means we have an unexpected connection close
|
||||
const err = new Error('Unexpected socket close');
|
||||
if (connection && connection._socket && connection._socket.upgrading) {
|
||||
@@ -216,6 +232,7 @@ class SMTPTransport extends EventEmitter {
|
||||
|
||||
connection.send(envelope, mail.message.createReadStream(), (err, info) => {
|
||||
returned = true;
|
||||
cleanupPerCallAuth();
|
||||
connection.close();
|
||||
if (err) {
|
||||
this.logger.error(
|
||||
@@ -255,13 +272,11 @@ class SMTPTransport extends EventEmitter {
|
||||
return;
|
||||
}
|
||||
|
||||
const auth = this.getAuth(mail.data.auth);
|
||||
perCallAuth = this.getAuth(mail.data.auth);
|
||||
|
||||
if (auth && (connection.allowsAuth || options.forceAuth)) {
|
||||
connection.login(auth, err => {
|
||||
if (auth && auth !== this.auth && auth.oauth2) {
|
||||
auth.oauth2.removeAllListeners();
|
||||
}
|
||||
if (perCallAuth && (connection.allowsAuth || options.forceAuth)) {
|
||||
connection.login(perCallAuth, err => {
|
||||
cleanupPerCallAuth();
|
||||
if (returned) {
|
||||
return;
|
||||
}
|
||||
@@ -323,12 +338,20 @@ class SMTPTransport extends EventEmitter {
|
||||
|
||||
const connection = new SMTPConnection(options);
|
||||
let returned = false;
|
||||
let perCallAuth;
|
||||
const cleanupPerCallAuth = () => {
|
||||
if (perCallAuth && perCallAuth !== this.auth && perCallAuth.oauth2) {
|
||||
perCallAuth.oauth2.removeAllListeners();
|
||||
}
|
||||
perCallAuth = null;
|
||||
};
|
||||
|
||||
connection.once('error', err => {
|
||||
if (returned) {
|
||||
return;
|
||||
}
|
||||
returned = true;
|
||||
cleanupPerCallAuth();
|
||||
connection.close();
|
||||
return callback(err);
|
||||
});
|
||||
@@ -338,6 +361,7 @@ class SMTPTransport extends EventEmitter {
|
||||
return;
|
||||
}
|
||||
returned = true;
|
||||
cleanupPerCallAuth();
|
||||
return callback(new Error('Connection closed'));
|
||||
});
|
||||
|
||||
@@ -346,6 +370,7 @@ class SMTPTransport extends EventEmitter {
|
||||
return;
|
||||
}
|
||||
returned = true;
|
||||
cleanupPerCallAuth();
|
||||
connection.quit();
|
||||
return callback(null, true);
|
||||
};
|
||||
@@ -355,10 +380,11 @@ class SMTPTransport extends EventEmitter {
|
||||
return;
|
||||
}
|
||||
|
||||
const authData = this.getAuth({});
|
||||
perCallAuth = this.getAuth({});
|
||||
|
||||
if (authData && (connection.allowsAuth || options.forceAuth)) {
|
||||
connection.login(authData, err => {
|
||||
if (perCallAuth && (connection.allowsAuth || options.forceAuth)) {
|
||||
connection.login(perCallAuth, err => {
|
||||
cleanupPerCallAuth();
|
||||
if (returned) {
|
||||
return;
|
||||
}
|
||||
@@ -371,11 +397,12 @@ class SMTPTransport extends EventEmitter {
|
||||
|
||||
finalize();
|
||||
});
|
||||
} else if (!authData && connection.allowsAuth && options.forceAuth) {
|
||||
} else if (!perCallAuth && connection.allowsAuth && options.forceAuth) {
|
||||
const err = new Error('Authentication info was not provided');
|
||||
err.code = errors.ENOAUTH;
|
||||
|
||||
returned = true;
|
||||
cleanupPerCallAuth();
|
||||
connection.close();
|
||||
return callback(err);
|
||||
} else {
|
||||
|
||||
+9
@@ -2,6 +2,8 @@
|
||||
|
||||
const packageData = require('../../package.json');
|
||||
const shared = require('../shared');
|
||||
const LeWindows = require('../mime-node/le-windows');
|
||||
const LeUnix = require('../mime-node/le-unix');
|
||||
|
||||
/**
|
||||
* Generates a Transport object for streaming
|
||||
@@ -63,6 +65,13 @@ class StreamTransport {
|
||||
|
||||
try {
|
||||
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) {
|
||||
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.timeout Optional TTL for Access Token in seconds
|
||||
* @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 {
|
||||
constructor(options, logger) {
|
||||
@@ -370,12 +371,23 @@ class XOAuth2 extends Stream {
|
||||
const chunks = [];
|
||||
let chunklen = 0;
|
||||
|
||||
const req = nmfetch(url, {
|
||||
const fetchOptions = {
|
||||
method: 'post',
|
||||
headers: params.customHeaders,
|
||||
body: payload,
|
||||
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', () => {
|
||||
let chunk;
|
||||
|
||||
Reference in New Issue
Block a user