nodemailer
Advanced tools
Comparing version 4.7.0 to 5.0.0
# CHANGELOG | ||
## 5.0.0 2018-12-28 | ||
- Start using dns.resolve() instead of dns.lookup() for resolving SMTP hostnames. Might be breaking change on some environments so upgrade with care | ||
- Show more logs for renewing OAuth2 tokens, previously it was not possible to see what actually failed | ||
## 4.7.0 2018-11-19 | ||
@@ -4,0 +9,0 @@ |
@@ -9,3 +9,123 @@ /* eslint no-console: 0 */ | ||
const fetch = require('../fetch'); | ||
const dns = require('dns'); | ||
const net = require('net'); | ||
const DNS_TTL = 5 * 60 * 1000; | ||
const resolver = (family, hostname, options, callback) => { | ||
dns['resolve' + family](hostname, options, (err, addresses) => { | ||
if (err) { | ||
switch (err.code) { | ||
case dns.NODATA: | ||
case dns.NOTFOUND: | ||
case dns.NOTIMP: | ||
return callback(null, []); | ||
} | ||
return callback(err); | ||
} | ||
return callback(null, Array.isArray(addresses) ? addresses : [].concat(addresses || [])); | ||
}); | ||
}; | ||
const dnsCache = (module.exports.dnsCache = new Map()); | ||
module.exports.resolveHostname = (options, callback) => { | ||
options = options || {}; | ||
if (!options.host || net.isIP(options.host)) { | ||
// nothing to do here | ||
let value = { | ||
host: options.host, | ||
servername: options.servername || false | ||
}; | ||
return callback(null, value); | ||
} | ||
let cached; | ||
if (dnsCache.has(options.host)) { | ||
cached = dnsCache.get(options.host); | ||
if (!cached.expires || cached.expires >= Date.now()) { | ||
return callback(null, { | ||
host: cached.value.host, | ||
servername: cached.value.servername, | ||
_cached: true | ||
}); | ||
} | ||
} | ||
resolver(4, options.host, {}, (err, addresses) => { | ||
if (err) { | ||
if (cached) { | ||
// ignore error, use expired value | ||
return callback(null, cached.value); | ||
} | ||
return callback(err); | ||
} | ||
if (addresses && addresses.length) { | ||
let value = { | ||
host: addresses[0] || options.host, | ||
servername: options.servername || options.host | ||
}; | ||
dnsCache.set(options.host, { | ||
value, | ||
expires: Date.now() + DNS_TTL | ||
}); | ||
return callback(null, value); | ||
} | ||
resolver(6, options.host, {}, (err, addresses) => { | ||
if (err) { | ||
if (cached) { | ||
// ignore error, use expired value | ||
return callback(null, cached.value); | ||
} | ||
return callback(err); | ||
} | ||
if (addresses && addresses.length) { | ||
let value = { | ||
host: addresses[0] || options.host, | ||
servername: options.servername || options.host | ||
}; | ||
dnsCache.set(options.host, { | ||
value, | ||
expires: Date.now() + DNS_TTL | ||
}); | ||
return callback(null, value); | ||
} | ||
try { | ||
dns.lookup(options.host, {}, (err, address) => { | ||
if (err) { | ||
if (cached) { | ||
// ignore error, use expired value | ||
return callback(null, cached.value); | ||
} | ||
return callback(err); | ||
} | ||
if (!address && cached) { | ||
// nothing was found, fallback to cached value | ||
return callback(null, cached.value); | ||
} | ||
let value = { | ||
host: address || options.host, | ||
servername: options.servername || options.host | ||
}; | ||
dnsCache.set(options.host, { | ||
value, | ||
expires: Date.now() + DNS_TTL | ||
}); | ||
return callback(null, value); | ||
}); | ||
} catch (err) { | ||
if (cached) { | ||
// ignore error, use expired value | ||
return callback(null, cached.value); | ||
} | ||
return callback(err); | ||
} | ||
}); | ||
}); | ||
}; | ||
/** | ||
@@ -150,3 +270,3 @@ * Parses connection url to a structured configuration object | ||
/** | ||
* Wrapper for creating a callback than either resolves or rejects a promise | ||
* Wrapper for creating a callback that either resolves or rejects a promise | ||
* based on input | ||
@@ -153,0 +273,0 @@ * |
@@ -206,2 +206,12 @@ 'use strict'; | ||
let setupConnectionHandlers = () => { | ||
this._connectionTimeout = setTimeout(() => { | ||
this._onError('Connection timeout', 'ETIMEDOUT', false, 'CONN'); | ||
}, this.options.connectionTimeout || CONNECTION_TIMEOUT); | ||
this._socket.on('error', err => { | ||
this._onError(err, 'ECONNECTION', false, 'CONN'); | ||
}); | ||
}; | ||
if (this.options.connection) { | ||
@@ -223,17 +233,41 @@ // connection is already opened | ||
} | ||
return; | ||
} else if (this.options.socket) { | ||
// socket object is set up but not yet connected | ||
this._socket = this.options.socket; | ||
try { | ||
this._socket.connect( | ||
this.port, | ||
this.host, | ||
() => { | ||
this._socket.setKeepAlive(true); | ||
this._onConnect(); | ||
return shared.resolveHostname(opts, (err, resolved) => { | ||
if (err) { | ||
return setImmediate(() => this._onError(err, 'EDNS', false, 'CONN')); | ||
} | ||
this.logger.debug( | ||
{ | ||
tnx: 'dns', | ||
source: opts.host, | ||
resolved: resolved.host, | ||
cached: !!resolved._cached | ||
}, | ||
'Resolved %s as %s [cache %s]', | ||
opts.host, | ||
resolved.host, | ||
resolved._cached ? 'hit' : 'miss' | ||
); | ||
Object.keys(resolved).forEach(key => { | ||
if (key.charAt(0) !== '_' && resolved[key]) { | ||
opts[key] = resolved[key]; | ||
} | ||
); | ||
} catch (E) { | ||
return setImmediate(() => this._onError(E, 'ECONNECTION', false, 'CONN')); | ||
} | ||
}); | ||
try { | ||
this._socket.connect( | ||
this.port, | ||
this.host, | ||
() => { | ||
this._socket.setKeepAlive(true); | ||
this._onConnect(); | ||
} | ||
); | ||
setupConnectionHandlers(); | ||
} catch (E) { | ||
return setImmediate(() => this._onError(E, 'ECONNECTION', false, 'CONN')); | ||
} | ||
}); | ||
} else if (this.secureConnection) { | ||
@@ -246,37 +280,73 @@ // connect using tls | ||
} | ||
try { | ||
this._socket = tls.connect( | ||
this.port, | ||
this.host, | ||
opts, | ||
() => { | ||
this._socket.setKeepAlive(true); | ||
this._onConnect(); | ||
return shared.resolveHostname(opts, (err, resolved) => { | ||
if (err) { | ||
return setImmediate(() => this._onError(err, 'EDNS', false, 'CONN')); | ||
} | ||
this.logger.debug( | ||
{ | ||
tnx: 'dns', | ||
source: opts.host, | ||
resolved: resolved.host, | ||
cached: !!resolved._cached | ||
}, | ||
'Resolved %s as %s [cache %s]', | ||
opts.host, | ||
resolved.host, | ||
resolved._cached ? 'hit' : 'miss' | ||
); | ||
Object.keys(resolved).forEach(key => { | ||
if (key.charAt(0) !== '_' && resolved[key]) { | ||
opts[key] = resolved[key]; | ||
} | ||
); | ||
} catch (E) { | ||
return setImmediate(() => this._onError(E, 'ECONNECTION', false, 'CONN')); | ||
} | ||
}); | ||
try { | ||
this._socket = tls.connect( | ||
opts, | ||
() => { | ||
this._socket.setKeepAlive(true); | ||
this._onConnect(); | ||
} | ||
); | ||
setupConnectionHandlers(); | ||
} catch (E) { | ||
return setImmediate(() => this._onError(E, 'ECONNECTION', false, 'CONN')); | ||
} | ||
}); | ||
} else { | ||
// connect using plaintext | ||
try { | ||
this._socket = net.connect( | ||
opts, | ||
() => { | ||
this._socket.setKeepAlive(true); | ||
this._onConnect(); | ||
return shared.resolveHostname(opts, (err, resolved) => { | ||
if (err) { | ||
return setImmediate(() => this._onError(err, 'EDNS', false, 'CONN')); | ||
} | ||
this.logger.debug( | ||
{ | ||
tnx: 'dns', | ||
source: opts.host, | ||
resolved: resolved.host, | ||
cached: !!resolved._cached | ||
}, | ||
'Resolved %s as %s [cache %s]', | ||
opts.host, | ||
resolved.host, | ||
resolved._cached ? 'hit' : 'miss' | ||
); | ||
Object.keys(resolved).forEach(key => { | ||
if (key.charAt(0) !== '_' && resolved[key]) { | ||
opts[key] = resolved[key]; | ||
} | ||
); | ||
} catch (E) { | ||
return setImmediate(() => this._onError(E, 'ECONNECTION', false, 'CONN')); | ||
} | ||
}); | ||
try { | ||
this._socket = net.connect( | ||
opts, | ||
() => { | ||
this._socket.setKeepAlive(true); | ||
this._onConnect(); | ||
} | ||
); | ||
setupConnectionHandlers(); | ||
} catch (E) { | ||
return setImmediate(() => this._onError(E, 'ECONNECTION', false, 'CONN')); | ||
} | ||
}); | ||
} | ||
this._connectionTimeout = setTimeout(() => { | ||
this._onError('Connection timeout', 'ETIMEDOUT', false, 'CONN'); | ||
}, this.options.connectionTimeout || CONNECTION_TIMEOUT); | ||
this._socket.on('error', err => { | ||
this._onError(err, 'ECONNECTION', false, 'CONN'); | ||
}); | ||
} | ||
@@ -283,0 +353,0 @@ |
@@ -154,6 +154,7 @@ 'use strict'; | ||
let urlOptions; | ||
let loggedUrlOptions; | ||
if (this.options.serviceClient) { | ||
// service account - https://developers.google.com/identity/protocols/OAuth2ServiceAccount | ||
let iat = Math.floor(Date.now() / 1000); // unix time | ||
let token = this.jwtSignRS256({ | ||
let tokenData = { | ||
iss: this.options.serviceClient, | ||
@@ -165,3 +166,4 @@ scope: this.options.scope || 'https://mail.google.com/', | ||
exp: iat + this.options.serviceRequestTimeout | ||
}); | ||
}; | ||
let token = this.jwtSignRS256(tokenData); | ||
@@ -172,2 +174,7 @@ urlOptions = { | ||
}; | ||
loggedUrlOptions = { | ||
grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer', | ||
assertion: tokenData | ||
}; | ||
} else { | ||
@@ -185,2 +192,9 @@ if (!this.options.refreshToken) { | ||
}; | ||
loggedUrlOptions = { | ||
client_id: this.options.clientId || '', | ||
client_secret: (this.options.clientSecret || '').substr(0, 6) + '...', | ||
refresh_token: (this.options.refreshToken || '').substr(0, 6) + '...', | ||
grant_type: 'refresh_token' | ||
}; | ||
} | ||
@@ -190,4 +204,15 @@ | ||
urlOptions[key] = this.options.customParams[key]; | ||
loggedUrlOptions[key] = this.options.customParams[key]; | ||
}); | ||
this.logger.debug( | ||
{ | ||
tnx: 'OAUTH2', | ||
user: this.options.user, | ||
action: 'generate' | ||
}, | ||
'Requesting token using: %s', | ||
JSON.stringify(loggedUrlOptions) | ||
); | ||
this.postRequest(this.options.accessUrl, urlOptions, this.options, (error, body) => { | ||
@@ -207,5 +232,33 @@ let data; | ||
if (!data || typeof data !== 'object') { | ||
this.logger.debug( | ||
{ | ||
tnx: 'OAUTH2', | ||
user: this.options.user, | ||
action: 'post' | ||
}, | ||
'Response: %s', | ||
(body || '').toString() | ||
); | ||
return callback(new Error('Invalid authentication response')); | ||
} | ||
let logData = {}; | ||
Object.keys(data).forEach(key => { | ||
if (key !== 'access_token') { | ||
logData[key] = data[key]; | ||
} else { | ||
logData[key] = (data[key] || '').toString().substr(0, 6) + '...'; | ||
} | ||
}); | ||
this.logger.debug( | ||
{ | ||
tnx: 'OAUTH2', | ||
user: this.options.user, | ||
action: 'post' | ||
}, | ||
'Response: %s', | ||
JSON.stringify(logData) | ||
); | ||
if (data.error) { | ||
@@ -256,3 +309,4 @@ return callback(new Error(data.error)); | ||
headers: params.customHeaders, | ||
body: payload | ||
body: payload, | ||
allowErrorResponse: true | ||
}); | ||
@@ -259,0 +313,0 @@ |
{ | ||
"name": "nodemailer", | ||
"version": "4.7.0", | ||
"version": "5.0.0", | ||
"description": "Easy as cake e-mail sending from your Node.js applications", | ||
@@ -5,0 +5,0 @@ "main": "lib/nodemailer.js", |
# Nodemailer | ||
[![Backers on Open Collective](https://opencollective.com/nodemailer/backers/badge.svg)](#backers) [![Sponsors on Open Collective](https://opencollective.com/nodemailer/sponsors/badge.svg)](#sponsors) | ||
[![Backers on Open Collective](https://opencollective.com/nodemailer/backers/badge.svg)](#backers) [![Sponsors on Open Collective](https://opencollective.com/nodemailer/sponsors/badge.svg)](#sponsors) | ||
[![Nodemailer](https://raw.githubusercontent.com/nodemailer/nodemailer/master/assets/nm_logo_200x136.png)](https://nodemailer.com/about/) | ||
@@ -12,2 +13,6 @@ | ||
## Why version bump to 5? | ||
Nodemailer changed from `dns.lookup()` to `dns.resolve` for resolving SMTP hostnames which might be backwards incompatible and thus the version bump. Nodemailer tries first `resolve4()` and if no match is found then `resolve6()` and finally reverts back to `lookup()`. Additionally found DNS results are cached (for 5 minutes). This should make it easier to manage high performance clients that send a lot of messages in parallel. | ||
## Having an issue? | ||
@@ -39,4 +44,2 @@ | ||
## Contributors | ||
@@ -47,3 +50,2 @@ | ||
## Backers | ||
@@ -55,3 +57,2 @@ | ||
## Sponsors | ||
@@ -72,4 +73,2 @@ | ||
### License | ||
@@ -79,5 +78,4 @@ | ||
-------------------------------------------------------------------------------- | ||
--- | ||
The Nodemailer logo was designed by [Sven Kristjansen](https://www.behance.net/kristjansen). | ||
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
456090
11172
76
17
0