nodemailer 7.0.13 → 8.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/lib/errors.js +61 -0
- package/lib/fetch/index.js +11 -10
- package/lib/mailer/index.js +7 -2
- package/lib/mime-node/index.js +11 -2
- package/lib/nodemailer.js +2 -1
- package/lib/sendmail-transport/index.js +8 -2
- package/lib/shared/index.js +57 -67
- package/lib/smtp-connection/http-proxy-client.js +4 -1
- package/lib/smtp-connection/index.js +127 -27
- package/lib/smtp-pool/index.js +2 -1
- package/lib/smtp-pool/pool-resource.js +3 -2
- package/lib/smtp-transport/index.js +3 -2
- package/lib/well-known/services.json +8 -0
- package/lib/xoauth2/index.js +22 -7
- package/package.json +5 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
|
+
## [8.0.1](https://github.com/nodemailer/nodemailer/compare/v8.0.0...v8.0.1) (2026-02-07)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* absorb TLS errors during socket teardown ([7f8dde4](https://github.com/nodemailer/nodemailer/commit/7f8dde41438c66b8311e888fa5f8c518fcaba6f1))
|
|
9
|
+
* absorb TLS errors during socket teardown ([381f628](https://github.com/nodemailer/nodemailer/commit/381f628d55e62bb3131bd2a452fa1ce00bc48aea))
|
|
10
|
+
* Add Gmail Workspace service configuration ([#1787](https://github.com/nodemailer/nodemailer/issues/1787)) ([dc97ede](https://github.com/nodemailer/nodemailer/commit/dc97ede417b3030b311771541b1f17f5ca76bcbf))
|
|
11
|
+
|
|
12
|
+
## [8.0.0](https://github.com/nodemailer/nodemailer/compare/v7.0.13...v8.0.0) (2026-02-04)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
### ⚠ BREAKING CHANGES
|
|
16
|
+
|
|
17
|
+
* Error code 'NoAuth' renamed to 'ENOAUTH'
|
|
18
|
+
|
|
19
|
+
### Bug Fixes
|
|
20
|
+
|
|
21
|
+
* add connection fallback to alternative DNS addresses ([e726d6f](https://github.com/nodemailer/nodemailer/commit/e726d6f44aa7ca14e943d4303243cb5494b09c75))
|
|
22
|
+
* centralize and standardize error codes ([45062ce](https://github.com/nodemailer/nodemailer/commit/45062ce7a4705f3e63c5d9e606547f4d99fd29b5))
|
|
23
|
+
* harden DNS fallback against race conditions and cleanup issues ([4fa3c63](https://github.com/nodemailer/nodemailer/commit/4fa3c63a1f36aefdbaea7f57a133adc458413a47))
|
|
24
|
+
* improve socket cleanup to prevent potential memory leaks ([6069fdc](https://github.com/nodemailer/nodemailer/commit/6069fdcff68a3eef9a9bb16b2bf5ddb924c02091))
|
|
25
|
+
|
|
3
26
|
## [7.0.13](https://github.com/nodemailer/nodemailer/compare/v7.0.12...v7.0.13) (2026-01-27)
|
|
4
27
|
|
|
5
28
|
|
package/lib/errors.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Nodemailer Error Codes
|
|
5
|
+
*
|
|
6
|
+
* Centralized error code definitions for consistent error handling.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* const errors = require('./errors');
|
|
10
|
+
* let err = new Error('Connection closed');
|
|
11
|
+
* err.code = errors.ECONNECTION;
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Error code descriptions for documentation and debugging
|
|
16
|
+
*/
|
|
17
|
+
const ERROR_CODES = {
|
|
18
|
+
// Connection errors
|
|
19
|
+
ECONNECTION: 'Connection closed unexpectedly',
|
|
20
|
+
ETIMEDOUT: 'Connection or operation timed out',
|
|
21
|
+
ESOCKET: 'Socket-level error',
|
|
22
|
+
EDNS: 'DNS resolution failed',
|
|
23
|
+
|
|
24
|
+
// TLS/Security errors
|
|
25
|
+
ETLS: 'TLS handshake or STARTTLS failed',
|
|
26
|
+
EREQUIRETLS: 'REQUIRETLS not supported by server (RFC 8689)',
|
|
27
|
+
|
|
28
|
+
// Protocol errors
|
|
29
|
+
EPROTOCOL: 'Invalid SMTP server response',
|
|
30
|
+
EENVELOPE: 'Invalid mail envelope (sender or recipients)',
|
|
31
|
+
EMESSAGE: 'Message delivery error',
|
|
32
|
+
ESTREAM: 'Stream processing error',
|
|
33
|
+
|
|
34
|
+
// Authentication errors
|
|
35
|
+
EAUTH: 'Authentication failed',
|
|
36
|
+
ENOAUTH: 'Authentication credentials not provided',
|
|
37
|
+
EOAUTH2: 'OAuth2 token generation or refresh error',
|
|
38
|
+
|
|
39
|
+
// Resource errors
|
|
40
|
+
EMAXLIMIT: 'Pool resource limit reached (max messages per connection)',
|
|
41
|
+
|
|
42
|
+
// Transport-specific errors
|
|
43
|
+
ESENDMAIL: 'Sendmail command error',
|
|
44
|
+
ESES: 'AWS SES transport error',
|
|
45
|
+
|
|
46
|
+
// Configuration and access errors
|
|
47
|
+
ECONFIG: 'Invalid configuration',
|
|
48
|
+
EPROXY: 'Proxy connection error',
|
|
49
|
+
EFILEACCESS: 'File access rejected (disableFileAccess is set)',
|
|
50
|
+
EURLACCESS: 'URL access rejected (disableUrlAccess is set)',
|
|
51
|
+
EFETCH: 'HTTP fetch error'
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// Export error codes as string constants and the full definitions object
|
|
55
|
+
module.exports = Object.keys(ERROR_CODES).reduce(
|
|
56
|
+
(exports, code) => {
|
|
57
|
+
exports[code] = code;
|
|
58
|
+
return exports;
|
|
59
|
+
},
|
|
60
|
+
{ ERROR_CODES }
|
|
61
|
+
);
|
package/lib/fetch/index.js
CHANGED
|
@@ -8,6 +8,7 @@ const PassThrough = require('stream').PassThrough;
|
|
|
8
8
|
const Cookies = require('./cookies');
|
|
9
9
|
const packageData = require('../../package.json');
|
|
10
10
|
const net = require('net');
|
|
11
|
+
const errors = require('../errors');
|
|
11
12
|
|
|
12
13
|
const MAX_REDIRECTS = 5;
|
|
13
14
|
|
|
@@ -76,7 +77,7 @@ function nmfetch(url, options) {
|
|
|
76
77
|
return;
|
|
77
78
|
}
|
|
78
79
|
finished = true;
|
|
79
|
-
err.
|
|
80
|
+
err.code = errors.EFETCH;
|
|
80
81
|
err.sourceUrl = url;
|
|
81
82
|
fetchRes.emit('error', err);
|
|
82
83
|
});
|
|
@@ -99,7 +100,7 @@ function nmfetch(url, options) {
|
|
|
99
100
|
return;
|
|
100
101
|
}
|
|
101
102
|
finished = true;
|
|
102
|
-
E.
|
|
103
|
+
E.code = errors.EFETCH;
|
|
103
104
|
E.sourceUrl = url;
|
|
104
105
|
fetchRes.emit('error', E);
|
|
105
106
|
return;
|
|
@@ -147,7 +148,7 @@ function nmfetch(url, options) {
|
|
|
147
148
|
} catch (E) {
|
|
148
149
|
finished = true;
|
|
149
150
|
setImmediate(() => {
|
|
150
|
-
E.
|
|
151
|
+
E.code = errors.EFETCH;
|
|
151
152
|
E.sourceUrl = url;
|
|
152
153
|
fetchRes.emit('error', E);
|
|
153
154
|
});
|
|
@@ -162,7 +163,7 @@ function nmfetch(url, options) {
|
|
|
162
163
|
finished = true;
|
|
163
164
|
req.abort();
|
|
164
165
|
let err = new Error('Request Timeout');
|
|
165
|
-
err.
|
|
166
|
+
err.code = errors.EFETCH;
|
|
166
167
|
err.sourceUrl = url;
|
|
167
168
|
fetchRes.emit('error', err);
|
|
168
169
|
});
|
|
@@ -173,7 +174,7 @@ function nmfetch(url, options) {
|
|
|
173
174
|
return;
|
|
174
175
|
}
|
|
175
176
|
finished = true;
|
|
176
|
-
err.
|
|
177
|
+
err.code = errors.EFETCH;
|
|
177
178
|
err.sourceUrl = url;
|
|
178
179
|
fetchRes.emit('error', err);
|
|
179
180
|
});
|
|
@@ -204,7 +205,7 @@ function nmfetch(url, options) {
|
|
|
204
205
|
if (options.redirects > options.maxRedirects) {
|
|
205
206
|
finished = true;
|
|
206
207
|
let err = new Error('Maximum redirect count exceeded');
|
|
207
|
-
err.
|
|
208
|
+
err.code = errors.EFETCH;
|
|
208
209
|
err.sourceUrl = url;
|
|
209
210
|
fetchRes.emit('error', err);
|
|
210
211
|
req.abort();
|
|
@@ -222,7 +223,7 @@ function nmfetch(url, options) {
|
|
|
222
223
|
if (res.statusCode >= 300 && !options.allowErrorResponse) {
|
|
223
224
|
finished = true;
|
|
224
225
|
let err = new Error('Invalid status code ' + res.statusCode);
|
|
225
|
-
err.
|
|
226
|
+
err.code = errors.EFETCH;
|
|
226
227
|
err.sourceUrl = url;
|
|
227
228
|
fetchRes.emit('error', err);
|
|
228
229
|
req.abort();
|
|
@@ -234,7 +235,7 @@ function nmfetch(url, options) {
|
|
|
234
235
|
return;
|
|
235
236
|
}
|
|
236
237
|
finished = true;
|
|
237
|
-
err.
|
|
238
|
+
err.code = errors.EFETCH;
|
|
238
239
|
err.sourceUrl = url;
|
|
239
240
|
fetchRes.emit('error', err);
|
|
240
241
|
req.abort();
|
|
@@ -247,7 +248,7 @@ function nmfetch(url, options) {
|
|
|
247
248
|
return;
|
|
248
249
|
}
|
|
249
250
|
finished = true;
|
|
250
|
-
err.
|
|
251
|
+
err.code = errors.EFETCH;
|
|
251
252
|
err.sourceUrl = url;
|
|
252
253
|
fetchRes.emit('error', err);
|
|
253
254
|
req.abort();
|
|
@@ -267,7 +268,7 @@ function nmfetch(url, options) {
|
|
|
267
268
|
}
|
|
268
269
|
} catch (err) {
|
|
269
270
|
finished = true;
|
|
270
|
-
err.
|
|
271
|
+
err.code = errors.EFETCH;
|
|
271
272
|
err.sourceUrl = url;
|
|
272
273
|
fetchRes.emit('error', err);
|
|
273
274
|
return;
|
package/lib/mailer/index.js
CHANGED
|
@@ -6,6 +6,7 @@ const mimeTypes = require('../mime-funcs/mime-types');
|
|
|
6
6
|
const MailComposer = require('../mail-composer');
|
|
7
7
|
const DKIM = require('../dkim');
|
|
8
8
|
const httpProxyClient = require('../smtp-connection/http-proxy-client');
|
|
9
|
+
const errors = require('../errors');
|
|
9
10
|
const util = require('util');
|
|
10
11
|
const urllib = require('url');
|
|
11
12
|
const packageData = require('../../package.json');
|
|
@@ -337,7 +338,9 @@ class Mail extends EventEmitter {
|
|
|
337
338
|
case 'socks4':
|
|
338
339
|
case 'socks4a': {
|
|
339
340
|
if (!this.meta.has('proxy_socks_module')) {
|
|
340
|
-
|
|
341
|
+
let err = new Error('Socks module not loaded');
|
|
342
|
+
err.code = errors.EPROXY;
|
|
343
|
+
return callback(err);
|
|
341
344
|
}
|
|
342
345
|
let connect = ipaddress => {
|
|
343
346
|
let proxyV2 = !!this.meta.get('proxy_socks_module').SocksClient;
|
|
@@ -394,7 +397,9 @@ class Mail extends EventEmitter {
|
|
|
394
397
|
});
|
|
395
398
|
}
|
|
396
399
|
}
|
|
397
|
-
|
|
400
|
+
let err = new Error('Unknown proxy configuration');
|
|
401
|
+
err.code = errors.EPROXY;
|
|
402
|
+
callback(err);
|
|
398
403
|
};
|
|
399
404
|
}
|
|
400
405
|
|
package/lib/mime-node/index.js
CHANGED
|
@@ -13,6 +13,7 @@ const qp = require('../qp');
|
|
|
13
13
|
const base64 = require('../base64');
|
|
14
14
|
const addressparser = require('../addressparser');
|
|
15
15
|
const nmfetch = require('../fetch');
|
|
16
|
+
const errors = require('../errors');
|
|
16
17
|
const LastNewline = require('./last-newline');
|
|
17
18
|
|
|
18
19
|
const LeWindows = require('./le-windows');
|
|
@@ -979,7 +980,11 @@ class MimeNode {
|
|
|
979
980
|
} else if (content && typeof content.path === 'string' && !content.href) {
|
|
980
981
|
if (this.disableFileAccess) {
|
|
981
982
|
contentStream = new PassThrough();
|
|
982
|
-
setImmediate(() =>
|
|
983
|
+
setImmediate(() => {
|
|
984
|
+
let err = new Error('File access rejected for ' + content.path);
|
|
985
|
+
err.code = errors.EFILEACCESS;
|
|
986
|
+
contentStream.emit('error', err);
|
|
987
|
+
});
|
|
983
988
|
return contentStream;
|
|
984
989
|
}
|
|
985
990
|
// read file
|
|
@@ -987,7 +992,11 @@ class MimeNode {
|
|
|
987
992
|
} else if (content && typeof content.href === 'string') {
|
|
988
993
|
if (this.disableUrlAccess) {
|
|
989
994
|
contentStream = new PassThrough();
|
|
990
|
-
setImmediate(() =>
|
|
995
|
+
setImmediate(() => {
|
|
996
|
+
let err = new Error('Url access rejected for ' + content.href);
|
|
997
|
+
err.code = errors.EURLACCESS;
|
|
998
|
+
contentStream.emit('error', err);
|
|
999
|
+
});
|
|
991
1000
|
return contentStream;
|
|
992
1001
|
}
|
|
993
1002
|
// fetch URL
|
package/lib/nodemailer.js
CHANGED
|
@@ -8,6 +8,7 @@ const SendmailTransport = require('./sendmail-transport');
|
|
|
8
8
|
const StreamTransport = require('./stream-transport');
|
|
9
9
|
const JSONTransport = require('./json-transport');
|
|
10
10
|
const SESTransport = require('./ses-transport');
|
|
11
|
+
const errors = require('./errors');
|
|
11
12
|
const nmfetch = require('./fetch');
|
|
12
13
|
const packageData = require('../package.json');
|
|
13
14
|
|
|
@@ -49,7 +50,7 @@ module.exports.createTransport = function (transporter, defaults) {
|
|
|
49
50
|
let error = new Error(
|
|
50
51
|
'Using legacy SES configuration, expecting @aws-sdk/client-sesv2, see https://nodemailer.com/transports/ses/'
|
|
51
52
|
);
|
|
52
|
-
error.code =
|
|
53
|
+
error.code = errors.ECONFIG;
|
|
53
54
|
throw error;
|
|
54
55
|
}
|
|
55
56
|
transporter = new SESTransport(options);
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const spawn = require('child_process').spawn;
|
|
4
4
|
const packageData = require('../../package.json');
|
|
5
5
|
const shared = require('../shared');
|
|
6
|
+
const errors = require('../errors');
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Generates a Transport object for Sendmail
|
|
@@ -72,7 +73,9 @@ class SendmailTransport {
|
|
|
72
73
|
.concat(envelope.to || [])
|
|
73
74
|
.some(addr => /^-/.test(addr));
|
|
74
75
|
if (hasInvalidAddresses) {
|
|
75
|
-
|
|
76
|
+
let err = new Error('Can not send mail. Invalid envelope addresses.');
|
|
77
|
+
err.code = errors.ESENDMAIL;
|
|
78
|
+
return done(err);
|
|
76
79
|
}
|
|
77
80
|
|
|
78
81
|
if (this.args) {
|
|
@@ -141,6 +144,7 @@ class SendmailTransport {
|
|
|
141
144
|
} else {
|
|
142
145
|
err = new Error('Sendmail exited with code ' + code);
|
|
143
146
|
}
|
|
147
|
+
err.code = errors.ESENDMAIL;
|
|
144
148
|
|
|
145
149
|
this.logger.error(
|
|
146
150
|
{
|
|
@@ -202,7 +206,9 @@ class SendmailTransport {
|
|
|
202
206
|
|
|
203
207
|
sourceStream.pipe(sendmail.stdin);
|
|
204
208
|
} else {
|
|
205
|
-
|
|
209
|
+
let err = new Error('sendmail was not found');
|
|
210
|
+
err.code = errors.ESENDMAIL;
|
|
211
|
+
return callback(err);
|
|
206
212
|
}
|
|
207
213
|
}
|
|
208
214
|
}
|
package/lib/shared/index.js
CHANGED
|
@@ -82,15 +82,22 @@ const formatDNSValue = (value, extra) => {
|
|
|
82
82
|
return Object.assign({}, extra || {});
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
let addresses = value.addresses || [];
|
|
86
|
+
|
|
87
|
+
// Select a random address from available addresses, or null if none
|
|
88
|
+
let host = null;
|
|
89
|
+
if (addresses.length === 1) {
|
|
90
|
+
host = addresses[0];
|
|
91
|
+
} else if (addresses.length > 1) {
|
|
92
|
+
host = addresses[Math.floor(Math.random() * addresses.length)];
|
|
93
|
+
}
|
|
94
|
+
|
|
85
95
|
return Object.assign(
|
|
86
96
|
{
|
|
87
97
|
servername: value.servername,
|
|
88
|
-
host
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
: value.addresses.length === 1
|
|
92
|
-
? value.addresses[0]
|
|
93
|
-
: value.addresses[Math.floor(Math.random() * value.addresses.length)]
|
|
98
|
+
host,
|
|
99
|
+
// Include all addresses for connection fallback support
|
|
100
|
+
_addresses: addresses
|
|
94
101
|
},
|
|
95
102
|
extra || {}
|
|
96
103
|
);
|
|
@@ -151,46 +158,51 @@ module.exports.resolveHostname = (options, callback) => {
|
|
|
151
158
|
}
|
|
152
159
|
}
|
|
153
160
|
|
|
161
|
+
// Resolve both IPv4 and IPv6 addresses for fallback support
|
|
162
|
+
let ipv4Addresses = [];
|
|
163
|
+
let ipv6Addresses = [];
|
|
164
|
+
let ipv4Error = null;
|
|
165
|
+
let ipv6Error = null;
|
|
166
|
+
|
|
154
167
|
resolver(4, options.host, options, (err, addresses) => {
|
|
155
168
|
if (err) {
|
|
156
|
-
|
|
169
|
+
ipv4Error = err;
|
|
170
|
+
} else {
|
|
171
|
+
ipv4Addresses = addresses || [];
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
resolver(6, options.host, options, (err, addresses) => {
|
|
175
|
+
if (err) {
|
|
176
|
+
ipv6Error = err;
|
|
177
|
+
} else {
|
|
178
|
+
ipv6Addresses = addresses || [];
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Combine addresses: IPv4 first, then IPv6
|
|
182
|
+
let allAddresses = ipv4Addresses.concat(ipv6Addresses);
|
|
183
|
+
|
|
184
|
+
if (allAddresses.length) {
|
|
185
|
+
let value = {
|
|
186
|
+
addresses: allAddresses,
|
|
187
|
+
servername: options.servername || options.host
|
|
188
|
+
};
|
|
189
|
+
|
|
157
190
|
dnsCache.set(options.host, {
|
|
158
|
-
value
|
|
191
|
+
value,
|
|
159
192
|
expires: Date.now() + (options.dnsTtl || DNS_TTL)
|
|
160
193
|
});
|
|
161
194
|
|
|
162
195
|
return callback(
|
|
163
196
|
null,
|
|
164
|
-
formatDNSValue(
|
|
165
|
-
cached:
|
|
166
|
-
error: err
|
|
197
|
+
formatDNSValue(value, {
|
|
198
|
+
cached: false
|
|
167
199
|
})
|
|
168
200
|
);
|
|
169
201
|
}
|
|
170
|
-
return callback(err);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
if (addresses && addresses.length) {
|
|
174
|
-
let value = {
|
|
175
|
-
addresses,
|
|
176
|
-
servername: options.servername || options.host
|
|
177
|
-
};
|
|
178
202
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
return callback(
|
|
185
|
-
null,
|
|
186
|
-
formatDNSValue(value, {
|
|
187
|
-
cached: false
|
|
188
|
-
})
|
|
189
|
-
);
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
resolver(6, options.host, options, (err, addresses) => {
|
|
193
|
-
if (err) {
|
|
203
|
+
// No addresses from resolve4/resolve6, try dns.lookup as fallback
|
|
204
|
+
if (ipv4Error && ipv6Error) {
|
|
205
|
+
// Both resolvers had errors
|
|
194
206
|
if (cached) {
|
|
195
207
|
dnsCache.set(options.host, {
|
|
196
208
|
value: cached.value,
|
|
@@ -201,30 +213,10 @@ module.exports.resolveHostname = (options, callback) => {
|
|
|
201
213
|
null,
|
|
202
214
|
formatDNSValue(cached.value, {
|
|
203
215
|
cached: true,
|
|
204
|
-
error:
|
|
216
|
+
error: ipv4Error
|
|
205
217
|
})
|
|
206
218
|
);
|
|
207
219
|
}
|
|
208
|
-
return callback(err);
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
if (addresses && addresses.length) {
|
|
212
|
-
let value = {
|
|
213
|
-
addresses,
|
|
214
|
-
servername: options.servername || options.host
|
|
215
|
-
};
|
|
216
|
-
|
|
217
|
-
dnsCache.set(options.host, {
|
|
218
|
-
value,
|
|
219
|
-
expires: Date.now() + (options.dnsTtl || DNS_TTL)
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
return callback(
|
|
223
|
-
null,
|
|
224
|
-
formatDNSValue(value, {
|
|
225
|
-
cached: false
|
|
226
|
-
})
|
|
227
|
-
);
|
|
228
220
|
}
|
|
229
221
|
|
|
230
222
|
try {
|
|
@@ -247,19 +239,17 @@ module.exports.resolveHostname = (options, callback) => {
|
|
|
247
239
|
return callback(err);
|
|
248
240
|
}
|
|
249
241
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
.shift()
|
|
255
|
-
: false;
|
|
242
|
+
// Get all supported addresses from dns.lookup
|
|
243
|
+
let supportedAddresses = addresses
|
|
244
|
+
? addresses.filter(addr => isFamilySupported(addr.family)).map(addr => addr.address)
|
|
245
|
+
: [];
|
|
256
246
|
|
|
257
|
-
if (addresses && addresses.length && !
|
|
247
|
+
if (addresses && addresses.length && !supportedAddresses.length) {
|
|
258
248
|
// there are addresses but none can be used
|
|
259
249
|
console.warn(`Failed to resolve IPv${addresses[0].family} addresses with current network`);
|
|
260
250
|
}
|
|
261
251
|
|
|
262
|
-
if (!
|
|
252
|
+
if (!supportedAddresses.length && cached) {
|
|
263
253
|
// nothing was found, fallback to cached value
|
|
264
254
|
return callback(
|
|
265
255
|
null,
|
|
@@ -270,7 +260,7 @@ module.exports.resolveHostname = (options, callback) => {
|
|
|
270
260
|
}
|
|
271
261
|
|
|
272
262
|
let value = {
|
|
273
|
-
addresses:
|
|
263
|
+
addresses: supportedAddresses.length ? supportedAddresses : [options.host],
|
|
274
264
|
servername: options.servername || options.host
|
|
275
265
|
};
|
|
276
266
|
|
|
@@ -286,7 +276,7 @@ module.exports.resolveHostname = (options, callback) => {
|
|
|
286
276
|
})
|
|
287
277
|
);
|
|
288
278
|
});
|
|
289
|
-
} catch (
|
|
279
|
+
} catch (lookupErr) {
|
|
290
280
|
if (cached) {
|
|
291
281
|
dnsCache.set(options.host, {
|
|
292
282
|
value: cached.value,
|
|
@@ -297,11 +287,11 @@ module.exports.resolveHostname = (options, callback) => {
|
|
|
297
287
|
null,
|
|
298
288
|
formatDNSValue(cached.value, {
|
|
299
289
|
cached: true,
|
|
300
|
-
error:
|
|
290
|
+
error: lookupErr
|
|
301
291
|
})
|
|
302
292
|
);
|
|
303
293
|
}
|
|
304
|
-
return callback(
|
|
294
|
+
return callback(ipv4Error || ipv6Error || lookupErr);
|
|
305
295
|
}
|
|
306
296
|
});
|
|
307
297
|
});
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
const net = require('net');
|
|
8
8
|
const tls = require('tls');
|
|
9
9
|
const urllib = require('url');
|
|
10
|
+
const errors = require('../errors');
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Establishes proxied connection to destinationPort
|
|
@@ -121,7 +122,9 @@ function httpProxyClient(proxyUrl, destinationPort, destinationHost, callback) {
|
|
|
121
122
|
} catch (_E) {
|
|
122
123
|
// ignore
|
|
123
124
|
}
|
|
124
|
-
|
|
125
|
+
let err = new Error('Invalid response from proxy' + ((match && ': ' + match[1]) || ''));
|
|
126
|
+
err.code = errors.EPROXY;
|
|
127
|
+
return callback(err);
|
|
125
128
|
}
|
|
126
129
|
|
|
127
130
|
socket.removeListener('error', tempSocketErr);
|
|
@@ -15,6 +15,7 @@ const CONNECTION_TIMEOUT = 2 * 60 * 1000; // how much to wait for the connection
|
|
|
15
15
|
const SOCKET_TIMEOUT = 10 * 60 * 1000; // how much to wait for socket inactivity before disconnecting the client
|
|
16
16
|
const GREETING_TIMEOUT = 30 * 1000; // how much to wait after connection is established but SMTP greeting is not receieved
|
|
17
17
|
const DNS_TIMEOUT = 30 * 1000; // how much to wait for resolveHostname
|
|
18
|
+
const TEARDOWN_NOOP = () => {}; // reusable no-op handler for absorbing errors during socket teardown
|
|
18
19
|
|
|
19
20
|
/**
|
|
20
21
|
* Generates a SMTP connection object
|
|
@@ -197,6 +198,17 @@ class SMTPConnection extends EventEmitter {
|
|
|
197
198
|
this._onSocketClose = () => this._onClose();
|
|
198
199
|
this._onSocketEnd = () => this._onEnd();
|
|
199
200
|
this._onSocketTimeout = () => this._onTimeout();
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Connection-phase error handler (supports fallback to alternative addresses)
|
|
204
|
+
*/
|
|
205
|
+
this._onConnectionSocketError = err => this._onConnectionError(err, 'ESOCKET');
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Connection attempt counter for fallback race condition protection
|
|
209
|
+
* @private
|
|
210
|
+
*/
|
|
211
|
+
this._connectionAttemptId = 0;
|
|
200
212
|
}
|
|
201
213
|
|
|
202
214
|
/**
|
|
@@ -232,18 +244,10 @@ class SMTPConnection extends EventEmitter {
|
|
|
232
244
|
opts.localAddress = this.options.localAddress;
|
|
233
245
|
}
|
|
234
246
|
|
|
235
|
-
let setupConnectionHandlers = () => {
|
|
236
|
-
this._connectionTimeout = setTimeout(() => {
|
|
237
|
-
this._onError('Connection timeout', 'ETIMEDOUT', false, 'CONN');
|
|
238
|
-
}, this.options.connectionTimeout || CONNECTION_TIMEOUT);
|
|
239
|
-
|
|
240
|
-
this._socket.on('error', this._onSocketError);
|
|
241
|
-
};
|
|
242
|
-
|
|
243
247
|
if (this.options.connection) {
|
|
244
248
|
// connection is already opened
|
|
245
249
|
this._socket = this.options.connection;
|
|
246
|
-
|
|
250
|
+
this._setupConnectionHandlers();
|
|
247
251
|
|
|
248
252
|
if (this.secureConnection && !this.alreadySecured) {
|
|
249
253
|
setImmediate(() =>
|
|
@@ -288,7 +292,7 @@ class SMTPConnection extends EventEmitter {
|
|
|
288
292
|
this._socket.setKeepAlive(true);
|
|
289
293
|
this._onConnect();
|
|
290
294
|
});
|
|
291
|
-
|
|
295
|
+
this._setupConnectionHandlers();
|
|
292
296
|
} catch (E) {
|
|
293
297
|
return setImmediate(() => this._onError(E, 'ECONNECTION', false, 'CONN'));
|
|
294
298
|
}
|
|
@@ -327,15 +331,12 @@ class SMTPConnection extends EventEmitter {
|
|
|
327
331
|
opts[key] = resolved[key];
|
|
328
332
|
}
|
|
329
333
|
});
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
} catch (E) {
|
|
337
|
-
return setImmediate(() => this._onError(E, 'ECONNECTION', false, 'CONN'));
|
|
338
|
-
}
|
|
334
|
+
|
|
335
|
+
// Store fallback addresses for retry on connection failure
|
|
336
|
+
this._fallbackAddresses = (resolved._addresses || []).filter(addr => addr !== opts.host);
|
|
337
|
+
this._connectOpts = Object.assign({}, opts);
|
|
338
|
+
|
|
339
|
+
this._connectToHost(opts, true);
|
|
339
340
|
});
|
|
340
341
|
} else {
|
|
341
342
|
// connect using plaintext
|
|
@@ -360,19 +361,101 @@ class SMTPConnection extends EventEmitter {
|
|
|
360
361
|
opts[key] = resolved[key];
|
|
361
362
|
}
|
|
362
363
|
});
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
364
|
+
|
|
365
|
+
// Store fallback addresses for retry on connection failure
|
|
366
|
+
this._fallbackAddresses = (resolved._addresses || []).filter(addr => addr !== opts.host);
|
|
367
|
+
this._connectOpts = Object.assign({}, opts);
|
|
368
|
+
|
|
369
|
+
this._connectToHost(opts, false);
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Attempts to connect to the specified host address
|
|
376
|
+
*
|
|
377
|
+
* @param {Object} opts Connection options
|
|
378
|
+
* @param {Boolean} secure Whether to use TLS
|
|
379
|
+
*/
|
|
380
|
+
_connectToHost(opts, secure) {
|
|
381
|
+
this._connectionAttemptId++;
|
|
382
|
+
const currentAttemptId = this._connectionAttemptId;
|
|
383
|
+
|
|
384
|
+
let connectFn = secure ? tls.connect : net.connect;
|
|
385
|
+
try {
|
|
386
|
+
this._socket = connectFn(opts, () => {
|
|
387
|
+
// Ignore callback if this is a stale connection attempt
|
|
388
|
+
if (this._connectionAttemptId !== currentAttemptId) {
|
|
389
|
+
return;
|
|
371
390
|
}
|
|
391
|
+
this._socket.setKeepAlive(true);
|
|
392
|
+
this._onConnect();
|
|
372
393
|
});
|
|
394
|
+
this._setupConnectionHandlers();
|
|
395
|
+
} catch (E) {
|
|
396
|
+
return setImmediate(() => this._onError(E, 'ECONNECTION', false, 'CONN'));
|
|
373
397
|
}
|
|
374
398
|
}
|
|
375
399
|
|
|
400
|
+
/**
|
|
401
|
+
* Sets up connection timeout and error handlers
|
|
402
|
+
*/
|
|
403
|
+
_setupConnectionHandlers() {
|
|
404
|
+
this._connectionTimeout = setTimeout(() => {
|
|
405
|
+
this._onConnectionError('Connection timeout', 'ETIMEDOUT');
|
|
406
|
+
}, this.options.connectionTimeout || CONNECTION_TIMEOUT);
|
|
407
|
+
|
|
408
|
+
this._socket.on('error', this._onConnectionSocketError);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Handles connection errors with fallback to alternative addresses
|
|
413
|
+
*
|
|
414
|
+
* @param {Error|String} err Error object or message
|
|
415
|
+
* @param {String} code Error code
|
|
416
|
+
*/
|
|
417
|
+
_onConnectionError(err, code) {
|
|
418
|
+
clearTimeout(this._connectionTimeout);
|
|
419
|
+
|
|
420
|
+
// Check if we have fallback addresses to try
|
|
421
|
+
let canFallback = this._fallbackAddresses && this._fallbackAddresses.length && this.stage === 'init' && !this._destroyed;
|
|
422
|
+
|
|
423
|
+
if (!canFallback) {
|
|
424
|
+
// No more fallback addresses, report the error
|
|
425
|
+
this._onError(err, code, false, 'CONN');
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
let nextHost = this._fallbackAddresses.shift();
|
|
430
|
+
|
|
431
|
+
this.logger.info(
|
|
432
|
+
{
|
|
433
|
+
tnx: 'network',
|
|
434
|
+
failedHost: this._connectOpts.host,
|
|
435
|
+
nextHost,
|
|
436
|
+
error: err.message || err
|
|
437
|
+
},
|
|
438
|
+
'Connection to %s failed, trying %s',
|
|
439
|
+
this._connectOpts.host,
|
|
440
|
+
nextHost
|
|
441
|
+
);
|
|
442
|
+
|
|
443
|
+
// Clean up current socket
|
|
444
|
+
if (this._socket) {
|
|
445
|
+
try {
|
|
446
|
+
this._socket.removeListener('error', this._onConnectionSocketError);
|
|
447
|
+
this._socket.destroy();
|
|
448
|
+
} catch (_E) {
|
|
449
|
+
// ignore
|
|
450
|
+
}
|
|
451
|
+
this._socket = null;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Update host and retry
|
|
455
|
+
this._connectOpts.host = nextHost;
|
|
456
|
+
this._connectToHost(this._connectOpts, this.secureConnection);
|
|
457
|
+
}
|
|
458
|
+
|
|
376
459
|
/**
|
|
377
460
|
* Sends QUIT
|
|
378
461
|
*/
|
|
@@ -414,6 +497,18 @@ class SMTPConnection extends EventEmitter {
|
|
|
414
497
|
|
|
415
498
|
if (socket && !socket.destroyed) {
|
|
416
499
|
try {
|
|
500
|
+
// Clear socket timeout to prevent timer leaks
|
|
501
|
+
socket.setTimeout(0);
|
|
502
|
+
// Remove all listeners to allow proper garbage collection
|
|
503
|
+
socket.removeListener('data', this._onSocketData);
|
|
504
|
+
socket.removeListener('timeout', this._onSocketTimeout);
|
|
505
|
+
socket.removeListener('close', this._onSocketClose);
|
|
506
|
+
socket.removeListener('end', this._onSocketEnd);
|
|
507
|
+
socket.removeListener('error', this._onSocketError);
|
|
508
|
+
socket.removeListener('error', this._onConnectionSocketError);
|
|
509
|
+
// Absorb errors that may fire during socket teardown (e.g. server
|
|
510
|
+
// sending cleartext after TLS shutdown triggers ERR_SSL_BAD_RECORD_TYPE)
|
|
511
|
+
socket.on('error', TEARDOWN_NOOP);
|
|
417
512
|
socket[closeMethod]();
|
|
418
513
|
} catch (_E) {
|
|
419
514
|
// just ignore
|
|
@@ -715,7 +810,10 @@ class SMTPConnection extends EventEmitter {
|
|
|
715
810
|
this._socket.removeListener('timeout', this._onSocketTimeout);
|
|
716
811
|
this._socket.removeListener('close', this._onSocketClose);
|
|
717
812
|
this._socket.removeListener('end', this._onSocketEnd);
|
|
813
|
+
// Switch from connection-phase error handler to normal error handler
|
|
814
|
+
this._socket.removeListener('error', this._onConnectionSocketError);
|
|
718
815
|
|
|
816
|
+
this._socket.on('error', this._onSocketError);
|
|
719
817
|
this._socket.on('data', this._onSocketData);
|
|
720
818
|
this._socket.once('close', this._onSocketClose);
|
|
721
819
|
this._socket.once('end', this._onSocketEnd);
|
|
@@ -941,8 +1039,10 @@ class SMTPConnection extends EventEmitter {
|
|
|
941
1039
|
this.upgrading = false;
|
|
942
1040
|
this._socket.on('data', this._onSocketData);
|
|
943
1041
|
|
|
1042
|
+
// Remove all listeners from the plain socket to allow proper garbage collection
|
|
944
1043
|
socketPlain.removeListener('close', this._onSocketClose);
|
|
945
1044
|
socketPlain.removeListener('end', this._onSocketEnd);
|
|
1045
|
+
socketPlain.removeListener('error', this._onSocketError);
|
|
946
1046
|
|
|
947
1047
|
return callback(null, true);
|
|
948
1048
|
});
|
package/lib/smtp-pool/index.js
CHANGED
|
@@ -5,6 +5,7 @@ const PoolResource = require('./pool-resource');
|
|
|
5
5
|
const SMTPConnection = require('../smtp-connection');
|
|
6
6
|
const wellKnown = require('../well-known');
|
|
7
7
|
const shared = require('../shared');
|
|
8
|
+
const errors = require('../errors');
|
|
8
9
|
const packageData = require('../../package.json');
|
|
9
10
|
|
|
10
11
|
/**
|
|
@@ -633,7 +634,7 @@ class SMTPPool extends EventEmitter {
|
|
|
633
634
|
});
|
|
634
635
|
} else if (!auth && connection.allowsAuth && options.forceAuth) {
|
|
635
636
|
let err = new Error('Authentication info was not provided');
|
|
636
|
-
err.code =
|
|
637
|
+
err.code = errors.ENOAUTH;
|
|
637
638
|
|
|
638
639
|
returned = true;
|
|
639
640
|
connection.close();
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const SMTPConnection = require('../smtp-connection');
|
|
4
4
|
const assign = require('../shared').assign;
|
|
5
5
|
const XOAuth2 = require('../xoauth2');
|
|
6
|
+
const errors = require('../errors');
|
|
6
7
|
const EventEmitter = require('events');
|
|
7
8
|
|
|
8
9
|
/**
|
|
@@ -121,7 +122,7 @@ class PoolResource extends EventEmitter {
|
|
|
121
122
|
let err = new Error('Unexpected socket close');
|
|
122
123
|
if (this.connection && this.connection._socket && this.connection._socket.upgrading) {
|
|
123
124
|
// starttls connection errors
|
|
124
|
-
err.code =
|
|
125
|
+
err.code = errors.ETLS;
|
|
125
126
|
}
|
|
126
127
|
callback(err);
|
|
127
128
|
}, 1000);
|
|
@@ -226,7 +227,7 @@ class PoolResource extends EventEmitter {
|
|
|
226
227
|
let err;
|
|
227
228
|
if (this.messages >= this.options.maxMessages) {
|
|
228
229
|
err = new Error('Resource exhausted');
|
|
229
|
-
err.code =
|
|
230
|
+
err.code = errors.EMAXLIMIT;
|
|
230
231
|
this.connection.close();
|
|
231
232
|
this.emit('error', err);
|
|
232
233
|
} else {
|
|
@@ -5,6 +5,7 @@ const SMTPConnection = require('../smtp-connection');
|
|
|
5
5
|
const wellKnown = require('../well-known');
|
|
6
6
|
const shared = require('../shared');
|
|
7
7
|
const XOAuth2 = require('../xoauth2');
|
|
8
|
+
const errors = require('../errors');
|
|
8
9
|
const packageData = require('../../package.json');
|
|
9
10
|
|
|
10
11
|
/**
|
|
@@ -190,7 +191,7 @@ class SMTPTransport extends EventEmitter {
|
|
|
190
191
|
let err = new Error('Unexpected socket close');
|
|
191
192
|
if (connection && connection._socket && connection._socket.upgrading) {
|
|
192
193
|
// starttls connection errors
|
|
193
|
-
err.code =
|
|
194
|
+
err.code = errors.ETLS;
|
|
194
195
|
}
|
|
195
196
|
callback(err);
|
|
196
197
|
}, 1000);
|
|
@@ -392,7 +393,7 @@ class SMTPTransport extends EventEmitter {
|
|
|
392
393
|
});
|
|
393
394
|
} else if (!authData && connection.allowsAuth && options.forceAuth) {
|
|
394
395
|
let err = new Error('Authentication info was not provided');
|
|
395
|
-
err.code =
|
|
396
|
+
err.code = errors.ENOAUTH;
|
|
396
397
|
|
|
397
398
|
returned = true;
|
|
398
399
|
connection.close();
|
|
@@ -147,6 +147,14 @@
|
|
|
147
147
|
"secure": true
|
|
148
148
|
},
|
|
149
149
|
|
|
150
|
+
"GmailWorkspace": {
|
|
151
|
+
"description": "Gmail Workspace",
|
|
152
|
+
"aliases": ["Google Workspace Mail"],
|
|
153
|
+
"host": "smtp-relay.gmail.com",
|
|
154
|
+
"port": 465,
|
|
155
|
+
"secure": true
|
|
156
|
+
},
|
|
157
|
+
|
|
150
158
|
"GMX": {
|
|
151
159
|
"description": "GMX Mail",
|
|
152
160
|
"domains": ["gmx.com", "gmx.net", "gmx.de"],
|
package/lib/xoauth2/index.js
CHANGED
|
@@ -4,6 +4,7 @@ const Stream = require('stream').Stream;
|
|
|
4
4
|
const nmfetch = require('../fetch');
|
|
5
5
|
const crypto = require('crypto');
|
|
6
6
|
const shared = require('../shared');
|
|
7
|
+
const errors = require('../errors');
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* XOAUTH2 access_token generator for Gmail.
|
|
@@ -41,7 +42,9 @@ class XOAuth2 extends Stream {
|
|
|
41
42
|
|
|
42
43
|
if (options && options.serviceClient) {
|
|
43
44
|
if (!options.privateKey || !options.user) {
|
|
44
|
-
|
|
45
|
+
let err = new Error('Options "privateKey" and "user" are required for service account!');
|
|
46
|
+
err.code = errors.EOAUTH2;
|
|
47
|
+
setImmediate(() => this.emit('error', err));
|
|
45
48
|
return;
|
|
46
49
|
}
|
|
47
50
|
|
|
@@ -120,7 +123,9 @@ class XOAuth2 extends Stream {
|
|
|
120
123
|
'Cannot renew access token for %s: No refresh mechanism available',
|
|
121
124
|
this.options.user
|
|
122
125
|
);
|
|
123
|
-
|
|
126
|
+
let err = new Error("Can't create new access token for user");
|
|
127
|
+
err.code = errors.EOAUTH2;
|
|
128
|
+
return callback(err);
|
|
124
129
|
}
|
|
125
130
|
|
|
126
131
|
// If renewal already in progress, queue this request instead of starting another
|
|
@@ -218,7 +223,9 @@ class XOAuth2 extends Stream {
|
|
|
218
223
|
try {
|
|
219
224
|
token = this.jwtSignRS256(tokenData);
|
|
220
225
|
} catch (_err) {
|
|
221
|
-
|
|
226
|
+
let err = new Error("Can't generate token. Check your auth options");
|
|
227
|
+
err.code = errors.EOAUTH2;
|
|
228
|
+
return callback(err);
|
|
222
229
|
}
|
|
223
230
|
|
|
224
231
|
urlOptions = {
|
|
@@ -232,7 +239,9 @@ class XOAuth2 extends Stream {
|
|
|
232
239
|
};
|
|
233
240
|
} else {
|
|
234
241
|
if (!this.options.refreshToken) {
|
|
235
|
-
|
|
242
|
+
let err = new Error("Can't create new access token for user");
|
|
243
|
+
err.code = errors.EOAUTH2;
|
|
244
|
+
return callback(err);
|
|
236
245
|
}
|
|
237
246
|
|
|
238
247
|
// web app - https://developers.google.com/identity/protocols/OAuth2WebServer
|
|
@@ -289,7 +298,9 @@ class XOAuth2 extends Stream {
|
|
|
289
298
|
'Response: %s',
|
|
290
299
|
(body || '').toString()
|
|
291
300
|
);
|
|
292
|
-
|
|
301
|
+
let err = new Error('Invalid authentication response');
|
|
302
|
+
err.code = errors.EOAUTH2;
|
|
303
|
+
return callback(err);
|
|
293
304
|
}
|
|
294
305
|
|
|
295
306
|
let logData = {};
|
|
@@ -320,7 +331,9 @@ class XOAuth2 extends Stream {
|
|
|
320
331
|
if (data.error_uri) {
|
|
321
332
|
errorMessage += ' (' + data.error_uri + ')';
|
|
322
333
|
}
|
|
323
|
-
|
|
334
|
+
let err = new Error(errorMessage);
|
|
335
|
+
err.code = errors.EOAUTH2;
|
|
336
|
+
return callback(err);
|
|
324
337
|
}
|
|
325
338
|
|
|
326
339
|
if (data.access_token) {
|
|
@@ -328,7 +341,9 @@ class XOAuth2 extends Stream {
|
|
|
328
341
|
return callback(null, this.accessToken);
|
|
329
342
|
}
|
|
330
343
|
|
|
331
|
-
|
|
344
|
+
let err = new Error('No access token');
|
|
345
|
+
err.code = errors.EOAUTH2;
|
|
346
|
+
return callback(err);
|
|
332
347
|
});
|
|
333
348
|
}
|
|
334
349
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nodemailer",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "8.0.1",
|
|
4
4
|
"description": "Easy as cake e-mail sending from your Node.js applications",
|
|
5
5
|
"main": "lib/nodemailer.js",
|
|
6
6
|
"scripts": {
|
|
@@ -26,12 +26,12 @@
|
|
|
26
26
|
},
|
|
27
27
|
"homepage": "https://nodemailer.com/",
|
|
28
28
|
"devDependencies": {
|
|
29
|
-
"@aws-sdk/client-sesv2": "3.
|
|
29
|
+
"@aws-sdk/client-sesv2": "3.985.0",
|
|
30
30
|
"bunyan": "1.8.15",
|
|
31
31
|
"c8": "10.1.3",
|
|
32
|
-
"eslint": "
|
|
32
|
+
"eslint": "10.0.0",
|
|
33
33
|
"eslint-config-prettier": "10.1.8",
|
|
34
|
-
"globals": "17.
|
|
34
|
+
"globals": "17.3.0",
|
|
35
35
|
"libbase64": "1.3.0",
|
|
36
36
|
"libmime": "5.3.7",
|
|
37
37
|
"libqp": "2.1.1",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"prettier": "3.8.1",
|
|
40
40
|
"proxy": "1.0.2",
|
|
41
41
|
"proxy-test-server": "1.0.0",
|
|
42
|
-
"smtp-server": "3.18.
|
|
42
|
+
"smtp-server": "3.18.1"
|
|
43
43
|
},
|
|
44
44
|
"engines": {
|
|
45
45
|
"node": ">=6.0.0"
|