nodemailer 10.0.11 → 10.0.12
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 +7 -0
- package/dist/cjs/dkim/index.js +25 -8
- package/dist/cjs/dkim/sign.d.ts +2 -0
- package/dist/cjs/dkim/sign.js +17 -1
- package/dist/cjs/fetch/index.d.ts +4 -1
- package/dist/cjs/fetch/index.js +34 -59
- package/dist/cjs/package-info.d.ts +1 -1
- package/dist/cjs/package-info.js +1 -1
- package/dist/cjs/smtp-connection/data-stream.d.ts +2 -1
- package/dist/cjs/smtp-connection/data-stream.js +32 -24
- package/dist/cjs/smtp-connection/index.d.ts +5 -5
- package/dist/cjs/smtp-connection/index.js +60 -10
- package/dist/cjs/smtp-pool/index.d.ts +2 -1
- package/dist/cjs/smtp-pool/index.js +31 -5
- package/dist/cjs/smtp-pool/pool-resource.js +27 -29
- package/dist/cjs/smtp-transport/index.js +17 -27
- package/dist/cjs/xoauth2/index.d.ts +2 -0
- package/dist/cjs/xoauth2/index.js +11 -3
- package/dist/esm/dkim/index.js +25 -8
- package/dist/esm/dkim/sign.d.ts +2 -0
- package/dist/esm/dkim/sign.js +17 -1
- package/dist/esm/fetch/index.d.ts +4 -1
- package/dist/esm/fetch/index.js +34 -59
- package/dist/esm/package-info.d.ts +1 -1
- package/dist/esm/package-info.js +1 -1
- package/dist/esm/smtp-connection/data-stream.d.ts +2 -1
- package/dist/esm/smtp-connection/data-stream.js +32 -24
- package/dist/esm/smtp-connection/index.d.ts +5 -5
- package/dist/esm/smtp-connection/index.js +60 -10
- package/dist/esm/smtp-pool/index.d.ts +2 -1
- package/dist/esm/smtp-pool/index.js +31 -5
- package/dist/esm/smtp-pool/pool-resource.js +27 -29
- package/dist/esm/smtp-transport/index.js +17 -27
- package/dist/esm/xoauth2/index.d.ts +2 -0
- package/dist/esm/xoauth2/index.js +11 -3
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
|
+
## [10.0.12](https://github.com/nodemailer/nodemailer/compare/v10.0.11...v10.0.12) (2026-09-28)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* settle every send on a connection error, back off pool requeues, turn a bare CR into CRLF, bound fetch, honour requireTLS ([63ccd66](https://github.com/nodemailer/nodemailer/commit/63ccd66c894bbaac18c82084ee385bf6488226cf))
|
|
9
|
+
|
|
3
10
|
## [10.0.11](https://github.com/nodemailer/nodemailer/compare/v10.0.10...v10.0.11) (2026-09-27)
|
|
4
11
|
|
|
5
12
|
|
package/dist/cjs/dkim/index.js
CHANGED
|
@@ -90,13 +90,22 @@ class DKIMSigner {
|
|
|
90
90
|
return;
|
|
91
91
|
}
|
|
92
92
|
const key = this.keys[keyPos++];
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
93
|
+
let dkimField;
|
|
94
|
+
try {
|
|
95
|
+
dkimField = (0, sign_js_1.default)(this.headers, this.hashAlgo, this.bodyHash, {
|
|
96
|
+
domainName: key.domainName,
|
|
97
|
+
keySelector: key.keySelector,
|
|
98
|
+
privateKey: key.privateKey,
|
|
99
|
+
headerFieldNames: this.options.headerFieldNames,
|
|
100
|
+
skipFields: this.options.skipFields
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
this.hasErrored = true;
|
|
105
|
+
this.cleanup();
|
|
106
|
+
this.output.emit('error', err);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
100
109
|
if (dkimField) {
|
|
101
110
|
this.output.write(Buffer.from(dkimField + '\r\n'));
|
|
102
111
|
}
|
|
@@ -196,7 +205,15 @@ class DKIM {
|
|
|
196
205
|
}
|
|
197
206
|
const signer = new DKIMSigner(options, this.keys, inputStream, output);
|
|
198
207
|
setImmediate(() => {
|
|
199
|
-
|
|
208
|
+
try {
|
|
209
|
+
signer.signStream();
|
|
210
|
+
}
|
|
211
|
+
catch (_E) {
|
|
212
|
+
// the body hash is created here, an unknown hashAlgo throws inside this timer
|
|
213
|
+
// where nothing else could catch it
|
|
214
|
+
output.emit('error', sign_js_1.default.unsupportedHashAlgoError(signer.hashAlgo));
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
200
217
|
if (writeValue) {
|
|
201
218
|
setImmediate(() => {
|
|
202
219
|
inputStream.end(writeValue);
|
package/dist/cjs/dkim/sign.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import crypto from 'node:crypto';
|
|
2
|
+
import type { NodemailerError } from '../errors.js';
|
|
2
3
|
import type { MessageParserHeaderLine } from './message-parser.js';
|
|
3
4
|
/**
|
|
4
5
|
* Private key accepted by crypto.Sign#sign: a PEM string, a Buffer, a KeyObject or an
|
|
@@ -45,5 +46,6 @@ export interface DKIMRelaxedHeaders {
|
|
|
45
46
|
declare function sign(headers: MessageParserHeaderLine[], hashAlgo: string, bodyHash: string, options?: DKIMSignOptions): string | false;
|
|
46
47
|
declare namespace sign {
|
|
47
48
|
var relaxedHeaders: (headers: MessageParserHeaderLine[], fieldNames?: string, skipFields?: string) => DKIMRelaxedHeaders;
|
|
49
|
+
var unsupportedHashAlgoError: (hashAlgo: string) => NodemailerError;
|
|
48
50
|
}
|
|
49
51
|
export default sign;
|
package/dist/cjs/dkim/sign.js
CHANGED
|
@@ -39,6 +39,15 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
39
39
|
const punycode = __importStar(require("../punycode/index.js"));
|
|
40
40
|
const mimeFuncs = __importStar(require("../mime-funcs/index.js"));
|
|
41
41
|
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
42
|
+
const errors = __importStar(require("../errors.js"));
|
|
43
|
+
/**
|
|
44
|
+
* Error for a hashAlgo value the crypto module does not know
|
|
45
|
+
*/
|
|
46
|
+
function unsupportedHashAlgoError(hashAlgo) {
|
|
47
|
+
const err = new Error('Unsupported DKIM hash algorithm "' + hashAlgo + '"');
|
|
48
|
+
err.code = errors.ECONFIG;
|
|
49
|
+
return err;
|
|
50
|
+
}
|
|
42
51
|
/**
|
|
43
52
|
* Returns DKIM signature header line
|
|
44
53
|
*
|
|
@@ -63,7 +72,13 @@ function sign(headers, hashAlgo, bodyHash, options) {
|
|
|
63
72
|
const canonicalizedHeaderData = relaxedHeaders(headers, fieldNames, options.skipFields);
|
|
64
73
|
const dkimHeader = generateDKIMHeader(options.domainName, options.keySelector, canonicalizedHeaderData.fieldNames, hashAlgo, bodyHash);
|
|
65
74
|
canonicalizedHeaderData.headers += 'dkim-signature:' + relaxedHeaderLine(dkimHeader);
|
|
66
|
-
|
|
75
|
+
let signer;
|
|
76
|
+
try {
|
|
77
|
+
signer = node_crypto_1.default.createSign(('rsa-' + hashAlgo).toUpperCase());
|
|
78
|
+
}
|
|
79
|
+
catch (_E) {
|
|
80
|
+
throw unsupportedHashAlgoError(hashAlgo);
|
|
81
|
+
}
|
|
67
82
|
// the header lines are 'binary' strings, so this reproduces the original header bytes
|
|
68
83
|
signer.update(canonicalizedHeaderData.headers, 'latin1');
|
|
69
84
|
let signature;
|
|
@@ -76,6 +91,7 @@ function sign(headers, hashAlgo, bodyHash, options) {
|
|
|
76
91
|
return dkimHeader + signature.replace(/(^.{73}|.{75}(?!\r?\n|\r))/g, '$&\r\n ').trim();
|
|
77
92
|
}
|
|
78
93
|
sign.relaxedHeaders = relaxedHeaders;
|
|
94
|
+
sign.unsupportedHashAlgoError = unsupportedHashAlgoError;
|
|
79
95
|
exports.default = sign;
|
|
80
96
|
function generateDKIMHeader(domainName, keySelector, fieldNames, hashAlgo, bodyHash) {
|
|
81
97
|
// the caller supplied tag values are interpolated straight into the tag list, and none of
|
|
@@ -25,8 +25,10 @@ export interface FetchOptions {
|
|
|
25
25
|
tls?: {
|
|
26
26
|
[key: string]: any;
|
|
27
27
|
} | undefined;
|
|
28
|
-
/**
|
|
28
|
+
/** Socket inactivity timeout in milliseconds, defaults to 60000, 0 disables it */
|
|
29
29
|
timeout?: number | undefined;
|
|
30
|
+
/** Maximum size of the (decoded) response body in bytes, defaults to 64 MB, Infinity disables the limit */
|
|
31
|
+
maxBytes?: number | undefined;
|
|
30
32
|
/** Maximum number of redirects to follow (default 5) */
|
|
31
33
|
maxRedirects?: number | undefined;
|
|
32
34
|
/** Resolve responses with a status code of 300 or above instead of emitting an error */
|
|
@@ -47,6 +49,7 @@ export interface FetchResponse extends PassThrough {
|
|
|
47
49
|
declare function nmfetch(url: string, options?: FetchOptions): FetchResponse;
|
|
48
50
|
declare namespace nmfetch {
|
|
49
51
|
var Cookies: typeof import("./cookies.js").default;
|
|
52
|
+
var DEFAULT_TIMEOUT: number;
|
|
50
53
|
}
|
|
51
54
|
type CookiesJar = Cookies;
|
|
52
55
|
/**
|
package/dist/cjs/fetch/index.js
CHANGED
|
@@ -47,6 +47,10 @@ const node_net_1 = __importDefault(require("node:net"));
|
|
|
47
47
|
const errors = __importStar(require("../errors.js"));
|
|
48
48
|
const objects_js_1 = require("../shared/objects.js");
|
|
49
49
|
const MAX_REDIRECTS = 5;
|
|
50
|
+
// a stalled server would otherwise hold the request, and whatever waits on it, open forever
|
|
51
|
+
const DEFAULT_TIMEOUT = 60 * 1000;
|
|
52
|
+
// the body is usually buffered in memory by the caller, so an unbounded download is refused
|
|
53
|
+
const DEFAULT_MAX_BYTES = 64 * 1024 * 1024;
|
|
50
54
|
// Only genuine TLS settings are taken from options.tls. That object reaches us straight
|
|
51
55
|
// from a user supplied attachment (content.tls), so keys like host, port, path, socketPath
|
|
52
56
|
// or lookup would otherwise repoint the request at a destination that never went through
|
|
@@ -252,28 +256,22 @@ function nmfetch(url, options) {
|
|
|
252
256
|
});
|
|
253
257
|
return fetchRes;
|
|
254
258
|
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
if (finished) {
|
|
258
|
-
return;
|
|
259
|
-
}
|
|
260
|
-
finished = true;
|
|
261
|
-
req.abort();
|
|
262
|
-
const err = new Error('Request Timeout');
|
|
263
|
-
err.code = errors.EFETCH;
|
|
264
|
-
err.sourceUrl = url;
|
|
265
|
-
fetchRes.emit('error', err);
|
|
266
|
-
});
|
|
267
|
-
}
|
|
268
|
-
req.on('error', (err) => {
|
|
259
|
+
// reports the first failure of this request on fetchRes and releases the request
|
|
260
|
+
const fail = (err, sourceUrl = url) => {
|
|
269
261
|
if (finished) {
|
|
270
262
|
return;
|
|
271
263
|
}
|
|
272
264
|
finished = true;
|
|
273
265
|
err.code = errors.EFETCH;
|
|
274
|
-
err.sourceUrl =
|
|
266
|
+
err.sourceUrl = sourceUrl;
|
|
275
267
|
fetchRes.emit('error', err);
|
|
276
|
-
|
|
268
|
+
req.abort();
|
|
269
|
+
};
|
|
270
|
+
const timeout = typeof options.timeout === 'number' && options.timeout >= 0 ? options.timeout : DEFAULT_TIMEOUT;
|
|
271
|
+
if (timeout) {
|
|
272
|
+
req.setTimeout(timeout, () => fail(new Error('Request Timeout')));
|
|
273
|
+
}
|
|
274
|
+
req.on('error', (err) => fail(err));
|
|
277
275
|
req.on('response', res => {
|
|
278
276
|
let inflate;
|
|
279
277
|
if (finished) {
|
|
@@ -294,13 +292,7 @@ function nmfetch(url, options) {
|
|
|
294
292
|
// redirect
|
|
295
293
|
options.redirects++;
|
|
296
294
|
if (options.redirects > options.maxRedirects) {
|
|
297
|
-
|
|
298
|
-
const err = new Error('Maximum redirect count exceeded');
|
|
299
|
-
err.code = errors.EFETCH;
|
|
300
|
-
err.sourceUrl = url;
|
|
301
|
-
fetchRes.emit('error', err);
|
|
302
|
-
req.abort();
|
|
303
|
-
return;
|
|
295
|
+
return fail(new Error('Maximum redirect count exceeded'));
|
|
304
296
|
}
|
|
305
297
|
// redirect does not include POST body
|
|
306
298
|
options.method = 'GET';
|
|
@@ -320,13 +312,7 @@ function nmfetch(url, options) {
|
|
|
320
312
|
// call: that call gets its own `finished` flag and no handle on this
|
|
321
313
|
// request, so this one would stay open and could emit a second error on
|
|
322
314
|
// the shared fetchRes once it times out. Callers listen with req.once().
|
|
323
|
-
|
|
324
|
-
const err = new Error('Unsupported protocol for URL ' + redirectUrl);
|
|
325
|
-
err.code = errors.EFETCH;
|
|
326
|
-
err.sourceUrl = redirectUrl;
|
|
327
|
-
fetchRes.emit('error', err);
|
|
328
|
-
req.abort();
|
|
329
|
-
return;
|
|
315
|
+
return fail(new Error('Unsupported protocol for URL ' + redirectUrl), redirectUrl);
|
|
330
316
|
}
|
|
331
317
|
// Do not forward credentials when the redirect leaves the original
|
|
332
318
|
// security context: a different host, or a downgrade from https to
|
|
@@ -343,41 +329,33 @@ function nmfetch(url, options) {
|
|
|
343
329
|
}
|
|
344
330
|
});
|
|
345
331
|
}
|
|
332
|
+
// this request is done with, release it so its socket and timeout do not
|
|
333
|
+
// outlive it and report a late error on the shared fetchRes
|
|
334
|
+
finished = true;
|
|
335
|
+
res.resume();
|
|
336
|
+
req.abort();
|
|
346
337
|
return nmfetch(redirectUrl, options);
|
|
347
338
|
}
|
|
348
339
|
fetchRes.statusCode = res.statusCode;
|
|
349
340
|
fetchRes.headers = res.headers;
|
|
350
341
|
if (res.statusCode >= 300 && !options.allowErrorResponse) {
|
|
351
|
-
|
|
352
|
-
const err = new Error('Invalid status code ' + res.statusCode);
|
|
353
|
-
err.code = errors.EFETCH;
|
|
354
|
-
err.sourceUrl = url;
|
|
355
|
-
fetchRes.emit('error', err);
|
|
356
|
-
req.abort();
|
|
357
|
-
return;
|
|
342
|
+
return fail(new Error('Invalid status code ' + res.statusCode));
|
|
358
343
|
}
|
|
359
|
-
res.on('error', (err) =>
|
|
360
|
-
|
|
344
|
+
res.on('error', (err) => fail(err));
|
|
345
|
+
const maxBytes = typeof options.maxBytes === 'number' && options.maxBytes > 0 ? options.maxBytes : DEFAULT_MAX_BYTES;
|
|
346
|
+
const source = inflate || res;
|
|
347
|
+
let received = 0;
|
|
348
|
+
source.on('data', (chunk) => {
|
|
349
|
+
received += chunk.length;
|
|
350
|
+
if (received <= maxBytes || finished) {
|
|
361
351
|
return;
|
|
362
352
|
}
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
err.sourceUrl = url;
|
|
366
|
-
fetchRes.emit('error', err);
|
|
367
|
-
req.abort();
|
|
353
|
+
source.unpipe(fetchRes);
|
|
354
|
+
fail(new Error('Response size exceeds the allowed ' + maxBytes + ' bytes'));
|
|
368
355
|
});
|
|
369
356
|
if (inflate) {
|
|
370
357
|
res.pipe(inflate).pipe(fetchRes);
|
|
371
|
-
inflate.on('error', (err) =>
|
|
372
|
-
if (finished) {
|
|
373
|
-
return;
|
|
374
|
-
}
|
|
375
|
-
finished = true;
|
|
376
|
-
err.code = errors.EFETCH;
|
|
377
|
-
err.sourceUrl = url;
|
|
378
|
-
fetchRes.emit('error', err);
|
|
379
|
-
req.abort();
|
|
380
|
-
});
|
|
358
|
+
inflate.on('error', (err) => fail(err));
|
|
381
359
|
}
|
|
382
360
|
else {
|
|
383
361
|
res.pipe(fetchRes);
|
|
@@ -392,11 +370,7 @@ function nmfetch(url, options) {
|
|
|
392
370
|
req.write(body);
|
|
393
371
|
}
|
|
394
372
|
catch (err) {
|
|
395
|
-
|
|
396
|
-
err.code = errors.EFETCH;
|
|
397
|
-
err.sourceUrl = url;
|
|
398
|
-
fetchRes.emit('error', err);
|
|
399
|
-
return;
|
|
373
|
+
return fail(err);
|
|
400
374
|
}
|
|
401
375
|
}
|
|
402
376
|
req.end();
|
|
@@ -404,6 +378,7 @@ function nmfetch(url, options) {
|
|
|
404
378
|
return fetchRes;
|
|
405
379
|
}
|
|
406
380
|
nmfetch.Cookies = cookies_js_1.default;
|
|
381
|
+
nmfetch.DEFAULT_TIMEOUT = DEFAULT_TIMEOUT;
|
|
407
382
|
exports.default = nmfetch;
|
|
408
383
|
module.exports = exports.default;
|
|
409
384
|
Object.defineProperty(module.exports, 'default', { value: exports.default, enumerable: false, writable: true, configurable: true });
|
package/dist/cjs/package-info.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Transform, type TransformOptions } from 'node:stream';
|
|
2
2
|
/**
|
|
3
3
|
* Escapes dots in the beginning of lines. Ends the stream with <CR><LF>.<CR><LF>
|
|
4
|
-
* Also makes sure that only <CR><LF> sequences are used for linebreaks
|
|
4
|
+
* Also makes sure that only <CR><LF> sequences are used for linebreaks, bare CR and bare LF
|
|
5
|
+
* are both turned into <CR><LF>
|
|
5
6
|
*
|
|
6
7
|
* @param options Stream options
|
|
7
8
|
*/
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
const node_stream_1 = require("node:stream");
|
|
4
|
+
// bytes inserted into the output, shared as they are only ever copied by Buffer.concat
|
|
5
|
+
const INSERT_LF = Buffer.from('\n');
|
|
6
|
+
const INSERT_LF_DOT = Buffer.from('\n.');
|
|
7
|
+
const INSERT_CR = Buffer.from('\r');
|
|
8
|
+
const INSERT_DOT = Buffer.from('.');
|
|
4
9
|
/**
|
|
5
10
|
* Escapes dots in the beginning of lines. Ends the stream with <CR><LF>.<CR><LF>
|
|
6
|
-
* Also makes sure that only <CR><LF> sequences are used for linebreaks
|
|
11
|
+
* Also makes sure that only <CR><LF> sequences are used for linebreaks, bare CR and bare LF
|
|
12
|
+
* are both turned into <CR><LF>
|
|
7
13
|
*
|
|
8
14
|
* @param options Stream options
|
|
9
15
|
*/
|
|
@@ -32,33 +38,35 @@ class DataStream extends node_stream_1.Transform {
|
|
|
32
38
|
}
|
|
33
39
|
this.inByteCount += chunk.length;
|
|
34
40
|
for (i = 0, len = chunk.length; i < len; i++) {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
const byte = chunk[i];
|
|
42
|
+
const prev = i ? chunk[i - 1] : this.lastByte;
|
|
43
|
+
let insert = false;
|
|
44
|
+
if (prev === 0x0d && byte !== 0x0a) {
|
|
45
|
+
// a bare CR becomes CRLF. A receiver that treats a lone CR as a line end would
|
|
46
|
+
// otherwise see "\r.\r" as the end of the data (SMTP smuggling), so a dot
|
|
47
|
+
// following it is stuffed like at the start of any other line
|
|
48
|
+
insert = byte === 0x2e ? INSERT_LF_DOT : INSERT_LF;
|
|
49
|
+
}
|
|
50
|
+
else if (byte === 0x0a && prev !== 0x0d) {
|
|
51
|
+
// a bare LF becomes CRLF
|
|
52
|
+
insert = INSERT_CR;
|
|
44
53
|
}
|
|
45
|
-
else if (
|
|
46
|
-
//
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
chunklen += 2;
|
|
55
|
-
}
|
|
56
|
-
chunks.push(Buffer.from('\r\n'));
|
|
57
|
-
lastPos = i + 1;
|
|
54
|
+
else if (byte === 0x2e && (prev === 0x0a || prev === false)) {
|
|
55
|
+
// a dot at the start of a line
|
|
56
|
+
insert = INSERT_DOT;
|
|
57
|
+
}
|
|
58
|
+
if (insert) {
|
|
59
|
+
if (i > lastPos) {
|
|
60
|
+
buf = chunk.slice(lastPos, i);
|
|
61
|
+
chunks.push(buf);
|
|
62
|
+
chunklen += buf.length;
|
|
58
63
|
}
|
|
64
|
+
chunks.push(insert);
|
|
65
|
+
chunklen += insert.length;
|
|
66
|
+
lastPos = i;
|
|
59
67
|
}
|
|
60
68
|
}
|
|
61
|
-
if (
|
|
69
|
+
if (chunks.length) {
|
|
62
70
|
// add last piece
|
|
63
71
|
if (lastPos < chunk.length) {
|
|
64
72
|
buf = chunk.slice(lastPos);
|
|
@@ -26,11 +26,11 @@ export interface SMTPConnectionOptions {
|
|
|
26
26
|
secured?: boolean | undefined;
|
|
27
27
|
/** Server name for SNI, defaults to host when that is not an IP address */
|
|
28
28
|
servername?: string | undefined;
|
|
29
|
-
/** Ignore STARTTLS even when the server advertises it */
|
|
29
|
+
/** Ignore STARTTLS even when the server advertises it, has no effect when requireTLS is set */
|
|
30
30
|
ignoreTLS?: boolean | undefined;
|
|
31
|
-
/** Force STARTTLS, fail when the server does not support it */
|
|
31
|
+
/** Force STARTTLS, fail when the server does not support it. Takes precedence over ignoreTLS and opportunisticTLS */
|
|
32
32
|
requireTLS?: boolean | undefined;
|
|
33
|
-
/** Continue unencrypted when the STARTTLS upgrade fails */
|
|
33
|
+
/** Continue unencrypted when the STARTTLS upgrade fails, has no effect when requireTLS is set */
|
|
34
34
|
opportunisticTLS?: boolean | undefined;
|
|
35
35
|
/** Name of the client server, sent with EHLO/HELO, CRLF is stripped */
|
|
36
36
|
name?: string | undefined;
|
|
@@ -288,8 +288,8 @@ export type SMTPConnectionResponseAction = (str: string) => void;
|
|
|
288
288
|
* * **port** - is the port to connect to (defaults to 587 or 465)
|
|
289
289
|
* * **host** - is the hostname or IP address to connect to (defaults to 'localhost')
|
|
290
290
|
* * **secure** - use SSL
|
|
291
|
-
* * **ignoreTLS** - ignore server support for STARTTLS
|
|
292
|
-
* * **requireTLS** - forces the client to use STARTTLS
|
|
291
|
+
* * **ignoreTLS** - ignore server support for STARTTLS (has no effect when requireTLS is set)
|
|
292
|
+
* * **requireTLS** - forces the client to use STARTTLS, takes precedence over ignoreTLS and opportunisticTLS
|
|
293
293
|
* * **name** - the name of the client server
|
|
294
294
|
* * **localAddress** - outbound address to bind to (see: http://nodejs.org/api/net.html#net_net_connect_options_connectionlistener)
|
|
295
295
|
* * **greetingTimeout** - Time to wait in ms until greeting message is received from the server (defaults to 30 seconds)
|
|
@@ -104,8 +104,8 @@ function isPartialLine(line) {
|
|
|
104
104
|
* * **port** - is the port to connect to (defaults to 587 or 465)
|
|
105
105
|
* * **host** - is the hostname or IP address to connect to (defaults to 'localhost')
|
|
106
106
|
* * **secure** - use SSL
|
|
107
|
-
* * **ignoreTLS** - ignore server support for STARTTLS
|
|
108
|
-
* * **requireTLS** - forces the client to use STARTTLS
|
|
107
|
+
* * **ignoreTLS** - ignore server support for STARTTLS (has no effect when requireTLS is set)
|
|
108
|
+
* * **requireTLS** - forces the client to use STARTTLS, takes precedence over ignoreTLS and opportunisticTLS
|
|
109
109
|
* * **name** - the name of the client server
|
|
110
110
|
* * **localAddress** - outbound address to bind to (see: http://nodejs.org/api/net.html#net_net_connect_options_connectionlistener)
|
|
111
111
|
* * **greetingTimeout** - Time to wait in ms until greeting message is received from the server (defaults to 30 seconds)
|
|
@@ -130,6 +130,11 @@ class SMTPConnection extends node_events_1.EventEmitter {
|
|
|
130
130
|
this.id = node_crypto_1.default.randomBytes(8).toString('base64').replace(/\W/g, '');
|
|
131
131
|
this.stage = 'init';
|
|
132
132
|
this.options = options || {};
|
|
133
|
+
if (this.options.requireTLS && (this.options.ignoreTLS || this.options.opportunisticTLS)) {
|
|
134
|
+
// requireTLS wins, a contradictory configuration must not quietly fall back to plaintext.
|
|
135
|
+
// Copied so the caller's (possibly shared) options object is left as it was
|
|
136
|
+
this.options = Object.assign({}, this.options, { ignoreTLS: false, opportunisticTLS: false });
|
|
137
|
+
}
|
|
133
138
|
this.secureConnection = !!this.options.secure;
|
|
134
139
|
this.alreadySecured = !!this.options.secured;
|
|
135
140
|
this.port = Number(this.options.port) || (this.secureConnection ? 465 : 587);
|
|
@@ -173,6 +178,8 @@ class SMTPConnection extends node_events_1.EventEmitter {
|
|
|
173
178
|
this._destroyed = false;
|
|
174
179
|
this._closing = false;
|
|
175
180
|
this._currentDataStream = false;
|
|
181
|
+
this._pendingSend = false;
|
|
182
|
+
this._connectCallback = false;
|
|
176
183
|
this._onSocketData = chunk => this._onData(chunk);
|
|
177
184
|
this._onSocketError = error => this._onError(error, 'ESOCKET', false, 'CONN');
|
|
178
185
|
this._onSocketClose = () => this._onClose();
|
|
@@ -187,7 +194,9 @@ class SMTPConnection extends node_events_1.EventEmitter {
|
|
|
187
194
|
*/
|
|
188
195
|
connect(connectCallback) {
|
|
189
196
|
if (typeof connectCallback === 'function') {
|
|
197
|
+
this._connectCallback = connectCallback;
|
|
190
198
|
this.once('connect', () => {
|
|
199
|
+
this._connectCallback = false;
|
|
191
200
|
this.logger.debug({
|
|
192
201
|
tnx: 'smtp'
|
|
193
202
|
}, 'SMTP handshake finished');
|
|
@@ -419,6 +428,16 @@ class SMTPConnection extends node_events_1.EventEmitter {
|
|
|
419
428
|
}
|
|
420
429
|
this._currentDataStream = false;
|
|
421
430
|
}
|
|
431
|
+
// Detach from the message stream as well. The listener is swapped for a no-op rather than
|
|
432
|
+
// removed, a stream destroyed with an error later on would otherwise throw it as unhandled
|
|
433
|
+
if (this._pendingSend) {
|
|
434
|
+
const { stream, onStreamError } = this._pendingSend;
|
|
435
|
+
if (stream) {
|
|
436
|
+
stream.removeListener('error', onStreamError);
|
|
437
|
+
stream.on('error', TEARDOWN_NOOP);
|
|
438
|
+
}
|
|
439
|
+
this._pendingSend = false;
|
|
440
|
+
}
|
|
422
441
|
if (socket && !socket.destroyed) {
|
|
423
442
|
try {
|
|
424
443
|
// Clear socket timeout to prevent timer leaks
|
|
@@ -606,6 +625,9 @@ class SMTPConnection extends node_events_1.EventEmitter {
|
|
|
606
625
|
return;
|
|
607
626
|
}
|
|
608
627
|
returned = true;
|
|
628
|
+
if (this._pendingSend && this._pendingSend.callback === callback) {
|
|
629
|
+
this._pendingSend = false;
|
|
630
|
+
}
|
|
609
631
|
done(err, info);
|
|
610
632
|
};
|
|
611
633
|
if (!message) {
|
|
@@ -622,9 +644,16 @@ class SMTPConnection extends node_events_1.EventEmitter {
|
|
|
622
644
|
});
|
|
623
645
|
return;
|
|
624
646
|
}
|
|
647
|
+
const pendingSend = {
|
|
648
|
+
callback,
|
|
649
|
+
stream: false,
|
|
650
|
+
onStreamError: err => callback(this._formatError(err, 'ESTREAM', false, 'API'))
|
|
651
|
+
};
|
|
625
652
|
if (typeof message.on === 'function') {
|
|
626
|
-
|
|
653
|
+
pendingSend.stream = message;
|
|
654
|
+
pendingSend.stream.on('error', pendingSend.onStreamError);
|
|
627
655
|
}
|
|
656
|
+
this._pendingSend = pendingSend;
|
|
628
657
|
const startTime = Date.now();
|
|
629
658
|
this._setEnvelope(envelope, (err, info) => {
|
|
630
659
|
if (err) {
|
|
@@ -817,8 +846,14 @@ class SMTPConnection extends node_events_1.EventEmitter {
|
|
|
817
846
|
else {
|
|
818
847
|
this.logger.error(data, err.message);
|
|
819
848
|
}
|
|
849
|
+
// close() forgets the send in flight, it is completed with this same error afterwards so
|
|
850
|
+
// a late message stream error has nothing left to report
|
|
851
|
+
const pendingSend = this._pendingSend;
|
|
820
852
|
this.emit('error', err);
|
|
821
853
|
this.close();
|
|
854
|
+
if (pendingSend) {
|
|
855
|
+
pendingSend.callback(err);
|
|
856
|
+
}
|
|
822
857
|
}
|
|
823
858
|
/** @internal */
|
|
824
859
|
_formatError(message, type, response, command) {
|
|
@@ -864,14 +899,25 @@ class SMTPConnection extends node_events_1.EventEmitter {
|
|
|
864
899
|
this.logger.info({
|
|
865
900
|
tnx: 'network'
|
|
866
901
|
}, 'Connection closed');
|
|
902
|
+
// the unterminated remainder is only reported as a reply (and so gives the error a responseCode)
|
|
903
|
+
// when it starts like a complete failure reply, not for a fragment such as "55" or a 250
|
|
904
|
+
const failureResponse = typeof serverResponse === 'string' && /^[45]\d{2}[ -]/.test(serverResponse) ? serverResponse : false;
|
|
867
905
|
if (this.upgrading && !this._destroyed) {
|
|
868
|
-
return this._onError(new Error('Connection closed unexpectedly'), 'ETLS',
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
|
|
906
|
+
return this._onError(new Error('Connection closed unexpectedly'), 'ETLS', failureResponse, 'CONN');
|
|
907
|
+
}
|
|
908
|
+
if (!failureResponse && this._responseActions[0] === this._actionGreeting && this._connectCallback && !this._destroyed) {
|
|
909
|
+
// A silent close before the greeting is handed to the connect() callback rather than
|
|
910
|
+
// emitted as 'error', callers that never saw an error for it must not start throwing one
|
|
911
|
+
const connectCallback = this._connectCallback;
|
|
912
|
+
this._connectCallback = false;
|
|
913
|
+
const err = this._formatError(new Error('Connection closed unexpectedly'), 'ECONNECTION', false, 'CONN');
|
|
914
|
+
this.logger.warn({ tnx: 'network' }, err.message);
|
|
915
|
+
connectCallback(err);
|
|
916
|
+
this.close();
|
|
917
|
+
return;
|
|
872
918
|
}
|
|
873
|
-
|
|
874
|
-
return this._onError(new Error('Connection closed unexpectedly'), 'ECONNECTION',
|
|
919
|
+
if (failureResponse || (this._responseActions[0] !== this.close && !this._destroyed)) {
|
|
920
|
+
return this._onError(new Error('Connection closed unexpectedly'), 'ECONNECTION', failureResponse, 'CONN');
|
|
875
921
|
}
|
|
876
922
|
this._destroy();
|
|
877
923
|
}
|
|
@@ -1622,7 +1668,11 @@ class SMTPConnection extends node_events_1.EventEmitter {
|
|
|
1622
1668
|
this._sendCommand('DATA');
|
|
1623
1669
|
}
|
|
1624
1670
|
else {
|
|
1625
|
-
|
|
1671
|
+
// report a temporary rejection when there is one, taking the last reply would mark the
|
|
1672
|
+
// whole message as permanently failed although some recipients were only deferred
|
|
1673
|
+
const deferred = envelope.rejectedErrors.find(rejectedErr => rejectedErr.responseCode && rejectedErr.responseCode < 500);
|
|
1674
|
+
const reply = deferred?.response ?? str;
|
|
1675
|
+
err = this._formatError("Can't send mail - all recipients were rejected", 'EENVELOPE', reply, 'RCPT TO');
|
|
1626
1676
|
err.rejected = envelope.rejected;
|
|
1627
1677
|
err.rejectedErrors = envelope.rejectedErrors;
|
|
1628
1678
|
return callback(err);
|
|
@@ -17,7 +17,7 @@ export interface SMTPPoolOptions extends SMTPTransportOptions {
|
|
|
17
17
|
rateLimit?: number | undefined;
|
|
18
18
|
/** Time window for rateLimit in milliseconds, defaults to 1000 */
|
|
19
19
|
rateDelta?: number | undefined;
|
|
20
|
-
/** How many times a message is requeued when its connection closes while sending,
|
|
20
|
+
/** How many times a message is requeued when its connection closes while sending, defaults to 5, a negative value means unlimited */
|
|
21
21
|
maxRequeues?: number | undefined;
|
|
22
22
|
}
|
|
23
23
|
/**
|
|
@@ -26,6 +26,7 @@ export interface SMTPPoolOptions extends SMTPTransportOptions {
|
|
|
26
26
|
export type SMTPPoolResolvedOptions = SMTPPoolOptions & {
|
|
27
27
|
maxConnections: number;
|
|
28
28
|
maxMessages: number;
|
|
29
|
+
maxRequeues: number;
|
|
29
30
|
};
|
|
30
31
|
/**
|
|
31
32
|
* Result of a message sent through the pool, same as for the SMTP transport
|