nodemailer 10.0.16 → 10.1.0
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 +27 -0
- package/dist/cjs/base64/index.d.ts +3 -0
- package/dist/cjs/base64/index.js +80 -41
- package/dist/cjs/dkim/index.js +43 -29
- package/dist/cjs/dkim/message-parser.js +2 -2
- package/dist/cjs/errors.d.ts +17 -0
- package/dist/cjs/errors.js +12 -1
- package/dist/cjs/fetch/index.js +23 -3
- package/dist/cjs/mailer/index.js +23 -7
- package/dist/cjs/mailer/mail-message.d.ts +5 -0
- package/dist/cjs/mailer/mail-message.js +20 -0
- package/dist/cjs/mime-node/index.d.ts +1 -1
- package/dist/cjs/mime-node/index.js +165 -105
- package/dist/cjs/nodemailer.js +12 -9
- package/dist/cjs/package-info.d.ts +1 -1
- package/dist/cjs/package-info.js +1 -1
- package/dist/cjs/qp/index.d.ts +2 -0
- package/dist/cjs/qp/index.js +103 -33
- package/dist/cjs/sendmail-transport/index.js +54 -14
- package/dist/cjs/shared/index.d.ts +32 -1
- package/dist/cjs/shared/index.js +212 -115
- package/dist/cjs/smtp-connection/http-proxy-client.d.ts +3 -1
- package/dist/cjs/smtp-connection/http-proxy-client.js +88 -68
- package/dist/cjs/smtp-connection/index.d.ts +7 -1
- package/dist/cjs/smtp-connection/index.js +348 -136
- package/dist/cjs/smtp-pool/index.d.ts +3 -0
- package/dist/cjs/smtp-pool/index.js +7 -1
- package/dist/cjs/smtp-pool/pool-resource.js +106 -17
- package/dist/cjs/smtp-transport/index.js +8 -2
- package/dist/esm/base64/index.d.ts +3 -0
- package/dist/esm/base64/index.js +80 -41
- package/dist/esm/dkim/index.js +43 -29
- package/dist/esm/dkim/message-parser.js +2 -2
- package/dist/esm/errors.d.ts +17 -0
- package/dist/esm/errors.js +10 -0
- package/dist/esm/fetch/index.js +23 -3
- package/dist/esm/mailer/index.js +23 -7
- package/dist/esm/mailer/mail-message.d.ts +5 -0
- package/dist/esm/mailer/mail-message.js +20 -0
- package/dist/esm/mime-node/index.d.ts +1 -1
- package/dist/esm/mime-node/index.js +166 -106
- package/dist/esm/nodemailer.js +12 -9
- package/dist/esm/package-info.d.ts +1 -1
- package/dist/esm/package-info.js +1 -1
- package/dist/esm/qp/index.d.ts +2 -0
- package/dist/esm/qp/index.js +103 -33
- package/dist/esm/sendmail-transport/index.js +54 -14
- package/dist/esm/shared/index.d.ts +32 -1
- package/dist/esm/shared/index.js +207 -115
- package/dist/esm/smtp-connection/http-proxy-client.d.ts +3 -1
- package/dist/esm/smtp-connection/http-proxy-client.js +88 -68
- package/dist/esm/smtp-connection/index.d.ts +7 -1
- package/dist/esm/smtp-connection/index.js +348 -136
- package/dist/esm/smtp-pool/index.d.ts +3 -0
- package/dist/esm/smtp-pool/index.js +7 -1
- package/dist/esm/smtp-pool/pool-resource.js +106 -17
- package/dist/esm/smtp-transport/index.js +8 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
|
+
## [10.1.0](https://github.com/nodemailer/nodemailer/compare/v10.0.16...v10.1.0) (2026-10-10)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* harden message streams, SMTP connections and the connection pool ([8009da6](https://github.com/nodemailer/nodemailer/commit/8009da6e10bb8e10d7f0948a25d65895fdcd64f1))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* do not read the environment or the interface table on import ([094179d](https://github.com/nodemailer/nodemailer/commit/094179d176d2ebe7a28742411ad427d3c87a3981))
|
|
14
|
+
* end the SMTP session on any 421 reply and drive the envelope through one step ([5638ac1](https://github.com/nodemailer/nodemailer/commit/5638ac1914342460df270f736e7ceb0a844cf83a))
|
|
15
|
+
* keep ERR_ACCESS_DENIED from the Node.js permission model ([8b33a3d](https://github.com/nodemailer/nodemailer/commit/8b33a3d4fedee3431b97da7da12af954c3c8dceb))
|
|
16
|
+
* let the proxy handshake and the SMTP connection share one deadline ([91ed683](https://github.com/nodemailer/nodemailer/commit/91ed683f7a1e9ff8c56e24fb76facf16af08b162))
|
|
17
|
+
* **mime-node:** decode an SMTPUTF8 domain from its A-label form ([e436f4d](https://github.com/nodemailer/nodemailer/commit/e436f4dccbc8b0fb57b4747ebaafa5bb035c15cb))
|
|
18
|
+
* **mime-node:** finish an encoded part only after its output was read ([ec97d8f](https://github.com/nodemailer/nodemailer/commit/ec97d8f679a1d9d47bbebb03ea064f5fa6f58d01))
|
|
19
|
+
* **qp:** encode a lone CR or LF in binary quoted-printable parts ([42b91d9](https://github.com/nodemailer/nodemailer/commit/42b91d9a0fe18eea412c9774c5c142adf5b3efc7))
|
|
20
|
+
* **smtp-connection:** count the wait for the greeting against connectionTimeout ([bdcd046](https://github.com/nodemailer/nodemailer/commit/bdcd0462cab163eca3c34058d09b9981ab671c47))
|
|
21
|
+
* **smtp-connection:** drop the connection when DATA is taken after a refused sender ([f90fb04](https://github.com/nodemailer/nodemailer/commit/f90fb045bc24f96a128710791b2fa443e64f55fd))
|
|
22
|
+
* **smtp-connection:** refuse a second connect() call ([24ba2e0](https://github.com/nodemailer/nodemailer/commit/24ba2e0847ecb8202540f1c5e53483bd119ae4f2))
|
|
23
|
+
* turn Nagle off for SMTP sockets and report an early sendmail exit as such ([fa9b8d3](https://github.com/nodemailer/nodemailer/commit/fa9b8d300bf642cd9e11f6732ebb312c09868677))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
### Performance Improvements
|
|
27
|
+
|
|
28
|
+
* encode base64 and quoted-printable without per-chunk delays ([fd377bb](https://github.com/nodemailer/nodemailer/commit/fd377bbf559363860aedbedba6f107348fc152d2))
|
|
29
|
+
|
|
3
30
|
## [10.0.16](https://github.com/nodemailer/nodemailer/compare/v10.0.15...v10.0.16) (2026-10-07)
|
|
4
31
|
|
|
5
32
|
|
|
@@ -24,6 +24,9 @@ export interface EncoderOptions {
|
|
|
24
24
|
/**
|
|
25
25
|
* Creates a transform stream for encoding data to base64 encoding
|
|
26
26
|
*
|
|
27
|
+
* The output is the same as `wrap(encode(input), lineLength)` no matter how the input is split
|
|
28
|
+
* into chunks: every line but the last one ends with a line break, the last one does not
|
|
29
|
+
*
|
|
27
30
|
* @constructor
|
|
28
31
|
* @param options Stream options
|
|
29
32
|
* @param [options.lineLength=76] Maximum length for lines, set to false to disable wrapping
|
package/dist/cjs/base64/index.js
CHANGED
|
@@ -16,6 +16,47 @@ function encode(buffer) {
|
|
|
16
16
|
}
|
|
17
17
|
return buffer.toString('base64');
|
|
18
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* Turns a line length option into a whole number of characters, the default for anything unusable
|
|
21
|
+
*/
|
|
22
|
+
function normalizeLineLength(lineLength) {
|
|
23
|
+
const length = Math.floor(Number(lineLength));
|
|
24
|
+
return Number.isFinite(length) && length >= 1 ? length : 76;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Splits the bytes of `src` into lines of `lineLength` bytes, each followed by a line break. With
|
|
28
|
+
* `final` set the last line, which may be shorter, gets no line break; otherwise only complete
|
|
29
|
+
* lines are taken and the rest is left for the caller
|
|
30
|
+
*
|
|
31
|
+
* @param src Bytes to wrap
|
|
32
|
+
* @param lineLength Line length
|
|
33
|
+
* @param final Whether `src` ends the output
|
|
34
|
+
* @returns The wrapped bytes and the number of trailing bytes not taken
|
|
35
|
+
*/
|
|
36
|
+
function wrapBuffer(src, lineLength, final) {
|
|
37
|
+
const lines = Math.ceil(src.length / lineLength);
|
|
38
|
+
// the last line waits for more data unless this is the end: whether it gets a line break
|
|
39
|
+
// depends on whether anything follows it
|
|
40
|
+
const complete = Math.max(lines - 1, 0);
|
|
41
|
+
let rest = src.length - complete * lineLength;
|
|
42
|
+
const output = Buffer.allocUnsafe(complete * (lineLength + 2) + (final ? rest : 0));
|
|
43
|
+
let to = 0;
|
|
44
|
+
for (let from = 0; from < complete * lineLength; from += lineLength) {
|
|
45
|
+
src.copy(output, to, from, from + lineLength);
|
|
46
|
+
to += lineLength;
|
|
47
|
+
output[to++] = 0x0d;
|
|
48
|
+
output[to++] = 0x0a;
|
|
49
|
+
}
|
|
50
|
+
if (final) {
|
|
51
|
+
to += src.copy(output, to, complete * lineLength);
|
|
52
|
+
rest = 0;
|
|
53
|
+
}
|
|
54
|
+
if (to !== output.length) {
|
|
55
|
+
// never hand out bytes of the unfilled allocation
|
|
56
|
+
throw new Error('Unexpected wrapped length');
|
|
57
|
+
}
|
|
58
|
+
return { output, rest };
|
|
59
|
+
}
|
|
19
60
|
/**
|
|
20
61
|
* Adds soft line breaks to a base64 string
|
|
21
62
|
*
|
|
@@ -44,6 +85,9 @@ function wrap(str, lineLength) {
|
|
|
44
85
|
/**
|
|
45
86
|
* Creates a transform stream for encoding data to base64 encoding
|
|
46
87
|
*
|
|
88
|
+
* The output is the same as `wrap(encode(input), lineLength)` no matter how the input is split
|
|
89
|
+
* into chunks: every line but the last one ends with a line break, the last one does not
|
|
90
|
+
*
|
|
47
91
|
* @constructor
|
|
48
92
|
* @param options Stream options
|
|
49
93
|
* @param [options.lineLength=76] Maximum length for lines, set to false to disable wrapping
|
|
@@ -53,66 +97,61 @@ class Encoder extends node_stream_1.Transform {
|
|
|
53
97
|
super();
|
|
54
98
|
this.options = options || {};
|
|
55
99
|
if (this.options.lineLength !== false) {
|
|
56
|
-
this.options.lineLength = this.options.lineLength
|
|
100
|
+
this.options.lineLength = normalizeLineLength(this.options.lineLength);
|
|
57
101
|
}
|
|
58
102
|
this._curLine = '';
|
|
59
103
|
this._remainingBytes = false;
|
|
60
104
|
this.inputBytes = 0;
|
|
61
105
|
this.outputBytes = 0;
|
|
62
106
|
}
|
|
107
|
+
/**
|
|
108
|
+
* Emits the encoded characters `b64` that follow the current line, keeping what can not be
|
|
109
|
+
* emitted yet as the new current line
|
|
110
|
+
*
|
|
111
|
+
* @internal
|
|
112
|
+
*/
|
|
113
|
+
_emit(b64, final) {
|
|
114
|
+
const src = Buffer.from(this._curLine + b64, 'latin1');
|
|
115
|
+
if (!src.length) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
let output = src;
|
|
119
|
+
if (this.options.lineLength) {
|
|
120
|
+
const wrapped = wrapBuffer(src, this.options.lineLength, final);
|
|
121
|
+
output = wrapped.output;
|
|
122
|
+
this._curLine = wrapped.rest ? src.toString('latin1', src.length - wrapped.rest) : '';
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
this._curLine = '';
|
|
126
|
+
}
|
|
127
|
+
if (output.length) {
|
|
128
|
+
this.outputBytes += output.length;
|
|
129
|
+
this.push(output);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
63
132
|
/** @internal */
|
|
64
133
|
_transform(chunk, encoding, done) {
|
|
65
134
|
let buf = encoding !== 'buffer' ? Buffer.from(chunk, encoding) : chunk;
|
|
66
135
|
if (!buf || !buf.length) {
|
|
67
|
-
|
|
68
|
-
return;
|
|
136
|
+
return done();
|
|
69
137
|
}
|
|
70
138
|
this.inputBytes += buf.length;
|
|
71
|
-
if (this._remainingBytes
|
|
139
|
+
if (this._remainingBytes) {
|
|
72
140
|
buf = Buffer.concat([this._remainingBytes, buf], this._remainingBytes.length + buf.length);
|
|
73
141
|
this._remainingBytes = false;
|
|
74
142
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
else {
|
|
80
|
-
this._remainingBytes = false;
|
|
81
|
-
}
|
|
82
|
-
let b64 = this._curLine + encode(buf);
|
|
83
|
-
if (this.options.lineLength) {
|
|
84
|
-
b64 = wrap(b64, this.options.lineLength);
|
|
85
|
-
// remove last line as it is still most probably incomplete
|
|
86
|
-
const lastLF = b64.lastIndexOf('\n');
|
|
87
|
-
if (lastLF < 0) {
|
|
88
|
-
this._curLine = b64;
|
|
89
|
-
b64 = '';
|
|
90
|
-
}
|
|
91
|
-
else if (lastLF === b64.length - 1) {
|
|
92
|
-
this._curLine = '';
|
|
93
|
-
}
|
|
94
|
-
else {
|
|
95
|
-
this._curLine = b64.substring(lastLF + 1);
|
|
96
|
-
b64 = b64.substring(0, lastLF + 1);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
if (b64) {
|
|
100
|
-
this.outputBytes += b64.length;
|
|
101
|
-
this.push(Buffer.from(b64, 'ascii'));
|
|
143
|
+
const extra = buf.length % 3;
|
|
144
|
+
if (extra) {
|
|
145
|
+
this._remainingBytes = buf.subarray(buf.length - extra);
|
|
146
|
+
buf = buf.subarray(0, buf.length - extra);
|
|
102
147
|
}
|
|
103
|
-
|
|
148
|
+
this._emit(encode(buf), false);
|
|
149
|
+
done();
|
|
104
150
|
}
|
|
105
151
|
/** @internal */
|
|
106
152
|
_flush(done) {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
}
|
|
110
|
-
if (this._curLine) {
|
|
111
|
-
this._curLine = wrap(this._curLine, this.options.lineLength);
|
|
112
|
-
this.outputBytes += this._curLine.length;
|
|
113
|
-
this.push(Buffer.from(this._curLine, 'ascii'));
|
|
114
|
-
this._curLine = '';
|
|
115
|
-
}
|
|
153
|
+
this._emit(this._remainingBytes ? encode(this._remainingBytes) : '', true);
|
|
154
|
+
this._remainingBytes = false;
|
|
116
155
|
done();
|
|
117
156
|
}
|
|
118
157
|
}
|
package/dist/cjs/dkim/index.js
CHANGED
|
@@ -37,51 +37,67 @@ class DKIMSigner {
|
|
|
37
37
|
this.output = output;
|
|
38
38
|
this.output.usingCache = false;
|
|
39
39
|
this.hasErrored = false;
|
|
40
|
-
this.input.on('error', err =>
|
|
40
|
+
this.input.on('error', err => this.fail(err));
|
|
41
|
+
// A consumer that goes away before the signed message was read in full destroys the
|
|
42
|
+
// output. Stop reading the input and drop the cache file instead of leaving them open
|
|
43
|
+
this.output.once('close', () => {
|
|
44
|
+
if (this.output.writableFinished) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
41
47
|
this.hasErrored = true;
|
|
48
|
+
this.input.unpipe();
|
|
49
|
+
this.input.destroy();
|
|
42
50
|
this.cleanup();
|
|
43
|
-
output.emit('error', err);
|
|
44
51
|
});
|
|
45
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* Ends the output with an error and releases the cache file
|
|
55
|
+
*/
|
|
56
|
+
fail(err) {
|
|
57
|
+
this.hasErrored = true;
|
|
58
|
+
this.cleanup();
|
|
59
|
+
this.output.destroy(err);
|
|
60
|
+
}
|
|
46
61
|
cleanup() {
|
|
47
62
|
if (!this.cache || !this.cachePath) {
|
|
48
63
|
return;
|
|
49
64
|
}
|
|
65
|
+
const cache = this.cache;
|
|
66
|
+
this.cache = false;
|
|
67
|
+
cache.destroy();
|
|
50
68
|
node_fs_1.default.unlink(this.cachePath, () => false);
|
|
51
69
|
}
|
|
52
70
|
createReadCache() {
|
|
53
71
|
// pipe remainings to cache file
|
|
54
72
|
this.cache = node_fs_1.default.createReadStream(this.cachePath);
|
|
55
|
-
this.cache.once('error', err =>
|
|
56
|
-
this.cleanup();
|
|
57
|
-
this.output.emit('error', err);
|
|
58
|
-
});
|
|
73
|
+
this.cache.once('error', err => this.fail(err));
|
|
59
74
|
this.cache.once('close', () => {
|
|
60
75
|
this.cleanup();
|
|
61
76
|
});
|
|
62
77
|
this.cache.pipe(this.output);
|
|
63
78
|
}
|
|
64
79
|
sendNextChunk() {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
80
|
+
while (!this.hasErrored) {
|
|
81
|
+
if (this.readPos >= this.chunks.length) {
|
|
82
|
+
if (!this.cache) {
|
|
83
|
+
this.output.end();
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
return this.createReadCache();
|
|
87
|
+
}
|
|
88
|
+
const chunk = this.chunks[this.readPos++];
|
|
89
|
+
if (this.output.write(chunk) === false) {
|
|
90
|
+
this.output.once('drain', () => {
|
|
91
|
+
this.sendNextChunk();
|
|
92
|
+
});
|
|
71
93
|
return;
|
|
72
94
|
}
|
|
73
|
-
return this.createReadCache();
|
|
74
|
-
}
|
|
75
|
-
const chunk = this.chunks[this.readPos++];
|
|
76
|
-
if (this.output.write(chunk) === false) {
|
|
77
|
-
this.output.once('drain', () => {
|
|
78
|
-
this.sendNextChunk();
|
|
79
|
-
});
|
|
80
|
-
return;
|
|
81
95
|
}
|
|
82
|
-
setImmediate(() => this.sendNextChunk());
|
|
83
96
|
}
|
|
84
97
|
sendSignedOutput() {
|
|
98
|
+
if (this.hasErrored) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
85
101
|
let keyPos = 0;
|
|
86
102
|
const signNextKey = () => {
|
|
87
103
|
if (keyPos >= this.keys.length) {
|
|
@@ -101,9 +117,7 @@ class DKIMSigner {
|
|
|
101
117
|
});
|
|
102
118
|
}
|
|
103
119
|
catch (err) {
|
|
104
|
-
this.
|
|
105
|
-
this.cleanup();
|
|
106
|
-
this.output.emit('error', err);
|
|
120
|
+
this.fail(err);
|
|
107
121
|
return;
|
|
108
122
|
}
|
|
109
123
|
if (dkimField) {
|
|
@@ -122,7 +136,6 @@ class DKIMSigner {
|
|
|
122
136
|
// pipe remainings to cache file
|
|
123
137
|
this.cache = node_fs_1.default.createWriteStream(this.cachePath);
|
|
124
138
|
this.cache.once('error', err => {
|
|
125
|
-
this.cleanup();
|
|
126
139
|
// drain input
|
|
127
140
|
this.relaxedBody.unpipe(this.cache);
|
|
128
141
|
this.relaxedBody.on('readable', () => {
|
|
@@ -130,11 +143,12 @@ class DKIMSigner {
|
|
|
130
143
|
// do nothing
|
|
131
144
|
}
|
|
132
145
|
});
|
|
133
|
-
this.
|
|
134
|
-
// emit error
|
|
135
|
-
this.output.emit('error', err);
|
|
146
|
+
this.fail(err);
|
|
136
147
|
});
|
|
137
148
|
this.cache.once('close', () => {
|
|
149
|
+
if (this.hasErrored) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
138
152
|
this.sendSignedOutput();
|
|
139
153
|
});
|
|
140
154
|
this.relaxedBody.removeAllListeners('readable');
|
|
@@ -211,7 +225,7 @@ class DKIM {
|
|
|
211
225
|
catch (_E) {
|
|
212
226
|
// the body hash is created here, an unknown hashAlgo throws inside this timer
|
|
213
227
|
// where nothing else could catch it
|
|
214
|
-
|
|
228
|
+
signer.fail(sign_js_1.default.unsupportedHashAlgoError(signer.hashAlgo));
|
|
215
229
|
return;
|
|
216
230
|
}
|
|
217
231
|
if (writeValue) {
|
|
@@ -107,7 +107,7 @@ class MessageParser extends node_stream_1.Transform {
|
|
|
107
107
|
const chunk = data.slice(headerPos);
|
|
108
108
|
this.bodySize += chunk.length;
|
|
109
109
|
// this would be the first chunk of data sent downstream
|
|
110
|
-
|
|
110
|
+
this.push(chunk);
|
|
111
111
|
}
|
|
112
112
|
return false;
|
|
113
113
|
}
|
|
@@ -136,7 +136,7 @@ class MessageParser extends node_stream_1.Transform {
|
|
|
136
136
|
this.bodySize += chunk.length;
|
|
137
137
|
this.push(chunk);
|
|
138
138
|
}
|
|
139
|
-
|
|
139
|
+
callback();
|
|
140
140
|
}
|
|
141
141
|
/** @internal */
|
|
142
142
|
_flush(callback) {
|
package/dist/cjs/errors.d.ts
CHANGED
|
@@ -61,6 +61,19 @@ export declare const EPROXY = "EPROXY";
|
|
|
61
61
|
export declare const EFILEACCESS = "EFILEACCESS";
|
|
62
62
|
export declare const EURLACCESS = "EURLACCESS";
|
|
63
63
|
export declare const EFETCH = "EFETCH";
|
|
64
|
+
/**
|
|
65
|
+
* Tells whether the error ended the connection rather than rejected what was sent over it
|
|
66
|
+
*/
|
|
67
|
+
export declare const isTransientError: (err: {
|
|
68
|
+
code?: string | undefined;
|
|
69
|
+
responseCode?: number | undefined;
|
|
70
|
+
}) => boolean;
|
|
71
|
+
/**
|
|
72
|
+
* Code Node.js sets on an error when its permission model (`--permission`) denies an
|
|
73
|
+
* operation. It is not replaced with the Nodemailer code of the failing step (`ESOCKET`,
|
|
74
|
+
* `EDNS`, `EFETCH`, ...), so a missing grant such as `--allow-net` stays recognizable.
|
|
75
|
+
*/
|
|
76
|
+
export declare const ERR_ACCESS_DENIED = "ERR_ACCESS_DENIED";
|
|
64
77
|
/**
|
|
65
78
|
* An Error together with the properties Nodemailer attaches to the errors it
|
|
66
79
|
* hands to callers. Every property is optional, the set that is present
|
|
@@ -84,6 +97,10 @@ export interface NodemailerError extends NodeJS.ErrnoException {
|
|
|
84
97
|
rejected?: string[] | undefined;
|
|
85
98
|
/** Per-recipient errors for the rejected addresses */
|
|
86
99
|
rejectedErrors?: NodemailerError[] | undefined;
|
|
100
|
+
/** The code the error had before Nodemailer set its own, such as ECONNREFUSED for an ESOCKET error */
|
|
101
|
+
originalCode?: string | undefined;
|
|
102
|
+
/** Which wait an ETIMEDOUT error ended: connecting, the greeting, a TLS upgrade or an idle socket */
|
|
103
|
+
timeoutType?: 'CONNECT_TIMEOUT' | 'GREETING_TIMEOUT' | 'UPGRADE_TIMEOUT' | 'SOCKET_TIMEOUT' | undefined;
|
|
87
104
|
}
|
|
88
105
|
/**
|
|
89
106
|
* Node style callback: called with an error, or with null and the result
|
package/dist/cjs/errors.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* err.code = errors.ECONNECTION;
|
|
11
11
|
*/
|
|
12
12
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
-
exports.EFETCH = exports.EURLACCESS = exports.EFILEACCESS = exports.EPROXY = exports.ECONFIG = exports.ESES = exports.ESENDMAIL = exports.EMAXRECIPIENTS = exports.EMAXLIMIT = exports.EOAUTH2 = exports.ENOAUTH = exports.EAUTH = exports.ESTREAM = exports.EMESSAGE = exports.EENVELOPE = exports.EPROTOCOL = exports.EREQUIRETLS = exports.ETLS = exports.EDNS = exports.ESOCKET = exports.ETIMEDOUT = exports.ECONNECTION = exports.ERROR_CODES = void 0;
|
|
13
|
+
exports.ERR_ACCESS_DENIED = exports.isTransientError = exports.EFETCH = exports.EURLACCESS = exports.EFILEACCESS = exports.EPROXY = exports.ECONFIG = exports.ESES = exports.ESENDMAIL = exports.EMAXRECIPIENTS = exports.EMAXLIMIT = exports.EOAUTH2 = exports.ENOAUTH = exports.EAUTH = exports.ESTREAM = exports.EMESSAGE = exports.EENVELOPE = exports.EPROTOCOL = exports.EREQUIRETLS = exports.ETLS = exports.EDNS = exports.ESOCKET = exports.ETIMEDOUT = exports.ECONNECTION = exports.ERROR_CODES = void 0;
|
|
14
14
|
/**
|
|
15
15
|
* Error code descriptions for documentation and debugging
|
|
16
16
|
*/
|
|
@@ -68,3 +68,14 @@ exports.EPROXY = 'EPROXY';
|
|
|
68
68
|
exports.EFILEACCESS = 'EFILEACCESS';
|
|
69
69
|
exports.EURLACCESS = 'EURLACCESS';
|
|
70
70
|
exports.EFETCH = 'EFETCH';
|
|
71
|
+
/**
|
|
72
|
+
* Tells whether the error ended the connection rather than rejected what was sent over it
|
|
73
|
+
*/
|
|
74
|
+
const isTransientError = (err) => err.responseCode === 421 || err.code === exports.ECONNECTION || err.code === exports.ESOCKET || err.code === exports.ETIMEDOUT;
|
|
75
|
+
exports.isTransientError = isTransientError;
|
|
76
|
+
/**
|
|
77
|
+
* Code Node.js sets on an error when its permission model (`--permission`) denies an
|
|
78
|
+
* operation. It is not replaced with the Nodemailer code of the failing step (`ESOCKET`,
|
|
79
|
+
* `EDNS`, `EFETCH`, ...), so a missing grant such as `--allow-net` stays recognizable.
|
|
80
|
+
*/
|
|
81
|
+
exports.ERR_ACCESS_DENIED = 'ERR_ACCESS_DENIED';
|
package/dist/cjs/fetch/index.js
CHANGED
|
@@ -81,6 +81,17 @@ const TLS_OPTION_KEYS = [
|
|
|
81
81
|
'sessionIdContext',
|
|
82
82
|
'sigalgs'
|
|
83
83
|
];
|
|
84
|
+
/**
|
|
85
|
+
* Marks an error of the request as a fetch error, unless it is a permission model
|
|
86
|
+
* denial, which keeps its own code (see ERR_ACCESS_DENIED)
|
|
87
|
+
*
|
|
88
|
+
* @param err Error to mark
|
|
89
|
+
*/
|
|
90
|
+
function setFetchCode(err) {
|
|
91
|
+
if (err.code !== errors.ERR_ACCESS_DENIED) {
|
|
92
|
+
err.code = errors.EFETCH;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
84
95
|
/**
|
|
85
96
|
* Resolves a URL only if it is one this module is willing to request.
|
|
86
97
|
*
|
|
@@ -177,7 +188,7 @@ function nmfetch(url, options) {
|
|
|
177
188
|
return;
|
|
178
189
|
}
|
|
179
190
|
finished = true;
|
|
180
|
-
err
|
|
191
|
+
setFetchCode(err);
|
|
181
192
|
err.sourceUrl = url;
|
|
182
193
|
fetchRes.emit('error', err);
|
|
183
194
|
});
|
|
@@ -250,7 +261,7 @@ function nmfetch(url, options) {
|
|
|
250
261
|
catch (E) {
|
|
251
262
|
finished = true;
|
|
252
263
|
setImmediate(() => {
|
|
253
|
-
E
|
|
264
|
+
setFetchCode(E);
|
|
254
265
|
E.sourceUrl = url;
|
|
255
266
|
fetchRes.emit('error', E);
|
|
256
267
|
});
|
|
@@ -262,7 +273,7 @@ function nmfetch(url, options) {
|
|
|
262
273
|
return;
|
|
263
274
|
}
|
|
264
275
|
finished = true;
|
|
265
|
-
err
|
|
276
|
+
setFetchCode(err);
|
|
266
277
|
err.sourceUrl = sourceUrl;
|
|
267
278
|
fetchRes.emit('error', err);
|
|
268
279
|
req.abort();
|
|
@@ -272,6 +283,15 @@ function nmfetch(url, options) {
|
|
|
272
283
|
req.setTimeout(timeout, () => fail(new Error('Request Timeout')));
|
|
273
284
|
}
|
|
274
285
|
req.on('error', (err) => fail(err));
|
|
286
|
+
// a consumer that destroys the response stream before it ended does not want the rest of
|
|
287
|
+
// the body, release the request now instead of when the timeout fires
|
|
288
|
+
fetchRes.once('close', () => {
|
|
289
|
+
if (finished || fetchRes.readableEnded) {
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
finished = true;
|
|
293
|
+
req.destroy();
|
|
294
|
+
});
|
|
275
295
|
req.on('response', res => {
|
|
276
296
|
let inflate;
|
|
277
297
|
if (finished) {
|
package/dist/cjs/mailer/index.js
CHANGED
|
@@ -157,6 +157,11 @@ class Mail extends node_events_1.EventEmitter {
|
|
|
157
157
|
this.getSocket = false;
|
|
158
158
|
}
|
|
159
159
|
const mail = new mail_message_js_1.default(this, data);
|
|
160
|
+
// a failed message is not going to be read anymore, release its content streams
|
|
161
|
+
const fail = (err) => {
|
|
162
|
+
mail.releaseStreams();
|
|
163
|
+
done(err);
|
|
164
|
+
};
|
|
160
165
|
this.logger.debug({
|
|
161
166
|
tnx: 'transport',
|
|
162
167
|
name: this.transporter.name,
|
|
@@ -170,7 +175,7 @@ class Mail extends node_events_1.EventEmitter {
|
|
|
170
175
|
tnx: 'plugin',
|
|
171
176
|
action: 'compile'
|
|
172
177
|
}, 'PluginCompile Error: %s', err.message);
|
|
173
|
-
return
|
|
178
|
+
return fail(err);
|
|
174
179
|
}
|
|
175
180
|
let recipientCount;
|
|
176
181
|
try {
|
|
@@ -187,7 +192,7 @@ class Mail extends node_events_1.EventEmitter {
|
|
|
187
192
|
tnx: 'transport',
|
|
188
193
|
action: 'send'
|
|
189
194
|
}, 'Compile Error: %s', err.message);
|
|
190
|
-
return
|
|
195
|
+
return fail(err);
|
|
191
196
|
}
|
|
192
197
|
const maxRecipients = mail.data.maxRecipients === undefined ? DEFAULT_MAX_RECIPIENTS : mail.data.maxRecipients;
|
|
193
198
|
if (maxRecipients && recipientCount > maxRecipients) {
|
|
@@ -198,7 +203,7 @@ class Mail extends node_events_1.EventEmitter {
|
|
|
198
203
|
tnx: 'transport',
|
|
199
204
|
action: 'send'
|
|
200
205
|
}, 'Send Error: %s', err.message);
|
|
201
|
-
return
|
|
206
|
+
return fail(err);
|
|
202
207
|
}
|
|
203
208
|
this._processPlugins('stream', mail, err => {
|
|
204
209
|
if (err) {
|
|
@@ -207,7 +212,7 @@ class Mail extends node_events_1.EventEmitter {
|
|
|
207
212
|
tnx: 'plugin',
|
|
208
213
|
action: 'stream'
|
|
209
214
|
}, 'PluginStream Error: %s', err.message);
|
|
210
|
-
return
|
|
215
|
+
return fail(err);
|
|
211
216
|
}
|
|
212
217
|
if (mail.data.dkim || this.dkim) {
|
|
213
218
|
mail.message.processFunc(input => {
|
|
@@ -222,6 +227,7 @@ class Mail extends node_events_1.EventEmitter {
|
|
|
222
227
|
}
|
|
223
228
|
this.transporter.send(mail, (...args) => {
|
|
224
229
|
if (args[0]) {
|
|
230
|
+
mail.releaseStreams();
|
|
225
231
|
this.logger.error({
|
|
226
232
|
err: args[0],
|
|
227
233
|
tnx: 'transport',
|
|
@@ -289,6 +295,11 @@ class Mail extends node_events_1.EventEmitter {
|
|
|
289
295
|
// setup socket handler for the mailer object
|
|
290
296
|
this.getSocket = (options, callback) => {
|
|
291
297
|
const protocol = proxy.protocol.replace(/:$/, '').toLowerCase();
|
|
298
|
+
// The proxy handshake is a part of connecting and draws from the same deadline as the
|
|
299
|
+
// SMTP connection that follows it: whatever the proxy takes, the connection does not get
|
|
300
|
+
const configuredTimeout = Number(this.options.connectionTimeout) || 0;
|
|
301
|
+
const connectStartedAt = Number(options.connectStartedAt) || Date.now();
|
|
302
|
+
const connectionTimeout = configuredTimeout ? Math.max(connectStartedAt + configuredTimeout - Date.now(), 1) : undefined;
|
|
292
303
|
if (this.meta.has('proxy_handler_' + protocol)) {
|
|
293
304
|
return this.meta.get('proxy_handler_' + protocol)(proxy, options, callback);
|
|
294
305
|
}
|
|
@@ -296,7 +307,7 @@ class Mail extends node_events_1.EventEmitter {
|
|
|
296
307
|
// Connect using a HTTP CONNECT method
|
|
297
308
|
case 'http':
|
|
298
309
|
case 'https':
|
|
299
|
-
(0, http_proxy_client_js_1.default)(proxy.href, options.port, options.host, this.options.tls || {}, (err, socket) => {
|
|
310
|
+
(0, http_proxy_client_js_1.default)(proxy.href, options.port, options.host, connectionTimeout ? Object.assign({}, this.options.tls, { timeout: connectionTimeout }) : this.options.tls || {}, (err, socket) => {
|
|
300
311
|
if (err) {
|
|
301
312
|
return callback(err);
|
|
302
313
|
}
|
|
@@ -330,6 +341,9 @@ class Mail extends node_events_1.EventEmitter {
|
|
|
330
341
|
},
|
|
331
342
|
command: 'connect'
|
|
332
343
|
};
|
|
344
|
+
if (connectionTimeout) {
|
|
345
|
+
connectionOpts.timeout = connectionTimeout;
|
|
346
|
+
}
|
|
333
347
|
if (proxy.username || proxy.password) {
|
|
334
348
|
const username = proxy.username || '';
|
|
335
349
|
const password = proxy.password || '';
|
|
@@ -359,11 +373,13 @@ class Mail extends node_events_1.EventEmitter {
|
|
|
359
373
|
if (node_net_1.default.isIP(proxy.hostname)) {
|
|
360
374
|
return connect(proxy.hostname);
|
|
361
375
|
}
|
|
362
|
-
|
|
376
|
+
// lookup goes through the hosts file and returns an IPv6 address as well,
|
|
377
|
+
// the same way a connection to the proxy host would be resolved otherwise
|
|
378
|
+
return node_dns_1.default.lookup(proxy.hostname, (err, address) => {
|
|
363
379
|
if (err) {
|
|
364
380
|
return callback(err);
|
|
365
381
|
}
|
|
366
|
-
connect(
|
|
382
|
+
connect(address);
|
|
367
383
|
});
|
|
368
384
|
}
|
|
369
385
|
}
|
|
@@ -90,6 +90,11 @@ export default class MailMessage<T = SentMessageInfo> {
|
|
|
90
90
|
[key: string]: any;
|
|
91
91
|
}, key: string | number, options?: ResolveContentOptions | false): Promise<any>;
|
|
92
92
|
resolveAll(callback: MailMessageDataCallback): void;
|
|
93
|
+
/**
|
|
94
|
+
* Destroys the content streams of the message once sending failed. A stream that was not
|
|
95
|
+
* read to the end would otherwise keep the file or the socket behind it open
|
|
96
|
+
*/
|
|
97
|
+
releaseStreams(): void;
|
|
93
98
|
normalize(callback: MailMessageDataCallback): void;
|
|
94
99
|
setMailerHeader(): void;
|
|
95
100
|
setPriorityHeaders(): void;
|
|
@@ -174,6 +174,26 @@ class MailMessage {
|
|
|
174
174
|
};
|
|
175
175
|
setImmediate(() => resolveNext());
|
|
176
176
|
}
|
|
177
|
+
/**
|
|
178
|
+
* Destroys the content streams of the message once sending failed. A stream that was not
|
|
179
|
+
* read to the end would otherwise keep the file or the socket behind it open
|
|
180
|
+
*/
|
|
181
|
+
releaseStreams() {
|
|
182
|
+
const data = this.data;
|
|
183
|
+
const values = [data.html, data.text, data.watchHtml, data.amp, data.raw, data.icalEvent];
|
|
184
|
+
for (const key of ['attachments', 'alternatives']) {
|
|
185
|
+
if (Array.isArray(data[key])) {
|
|
186
|
+
values.push(...data[key]);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
for (const value of values) {
|
|
190
|
+
if (value && typeof value === 'object') {
|
|
191
|
+
shared.destroyStream(value);
|
|
192
|
+
shared.destroyStream(value.content);
|
|
193
|
+
shared.destroyStream(value.raw);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
177
197
|
normalize(callback) {
|
|
178
198
|
const envelope = this.message.getEnvelope();
|
|
179
199
|
const messageId = this.message.messageId();
|
|
@@ -181,7 +181,7 @@ declare class MimeNode {
|
|
|
181
181
|
childNodes: MimeNode[];
|
|
182
182
|
/** Filename for this node. Useful with attachments */
|
|
183
183
|
filename?: string | undefined;
|
|
184
|
-
/** Body content
|
|
184
|
+
/** Body content */
|
|
185
185
|
content?: MimeNodeContent | Error | undefined;
|
|
186
186
|
/** Lowercase content type, set when the headers are built */
|
|
187
187
|
contentType?: string | undefined;
|