nodemailer 10.0.16 → 10.1.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.
Files changed (59) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/SECURITY.md +105 -29
  3. package/dist/cjs/base64/index.d.ts +3 -0
  4. package/dist/cjs/base64/index.js +80 -41
  5. package/dist/cjs/dkim/index.js +43 -29
  6. package/dist/cjs/dkim/message-parser.js +2 -2
  7. package/dist/cjs/errors.d.ts +17 -0
  8. package/dist/cjs/errors.js +12 -1
  9. package/dist/cjs/fetch/index.js +39 -13
  10. package/dist/cjs/mailer/index.js +23 -7
  11. package/dist/cjs/mailer/mail-message.d.ts +5 -0
  12. package/dist/cjs/mailer/mail-message.js +20 -0
  13. package/dist/cjs/mime-node/index.d.ts +1 -1
  14. package/dist/cjs/mime-node/index.js +165 -105
  15. package/dist/cjs/nodemailer.js +12 -9
  16. package/dist/cjs/package-info.d.ts +1 -1
  17. package/dist/cjs/package-info.js +1 -1
  18. package/dist/cjs/qp/index.d.ts +2 -0
  19. package/dist/cjs/qp/index.js +103 -33
  20. package/dist/cjs/sendmail-transport/index.js +54 -14
  21. package/dist/cjs/shared/index.d.ts +32 -1
  22. package/dist/cjs/shared/index.js +212 -115
  23. package/dist/cjs/smtp-connection/http-proxy-client.d.ts +3 -1
  24. package/dist/cjs/smtp-connection/http-proxy-client.js +88 -68
  25. package/dist/cjs/smtp-connection/index.d.ts +7 -1
  26. package/dist/cjs/smtp-connection/index.js +348 -136
  27. package/dist/cjs/smtp-pool/index.d.ts +3 -0
  28. package/dist/cjs/smtp-pool/index.js +7 -1
  29. package/dist/cjs/smtp-pool/pool-resource.js +106 -17
  30. package/dist/cjs/smtp-transport/index.js +8 -2
  31. package/dist/esm/base64/index.d.ts +3 -0
  32. package/dist/esm/base64/index.js +80 -41
  33. package/dist/esm/dkim/index.js +43 -29
  34. package/dist/esm/dkim/message-parser.js +2 -2
  35. package/dist/esm/errors.d.ts +17 -0
  36. package/dist/esm/errors.js +10 -0
  37. package/dist/esm/fetch/index.js +39 -13
  38. package/dist/esm/mailer/index.js +23 -7
  39. package/dist/esm/mailer/mail-message.d.ts +5 -0
  40. package/dist/esm/mailer/mail-message.js +20 -0
  41. package/dist/esm/mime-node/index.d.ts +1 -1
  42. package/dist/esm/mime-node/index.js +166 -106
  43. package/dist/esm/nodemailer.js +12 -9
  44. package/dist/esm/package-info.d.ts +1 -1
  45. package/dist/esm/package-info.js +1 -1
  46. package/dist/esm/qp/index.d.ts +2 -0
  47. package/dist/esm/qp/index.js +103 -33
  48. package/dist/esm/sendmail-transport/index.js +54 -14
  49. package/dist/esm/shared/index.d.ts +32 -1
  50. package/dist/esm/shared/index.js +207 -115
  51. package/dist/esm/smtp-connection/http-proxy-client.d.ts +3 -1
  52. package/dist/esm/smtp-connection/http-proxy-client.js +88 -68
  53. package/dist/esm/smtp-connection/index.d.ts +7 -1
  54. package/dist/esm/smtp-connection/index.js +348 -136
  55. package/dist/esm/smtp-pool/index.d.ts +3 -0
  56. package/dist/esm/smtp-pool/index.js +7 -1
  57. package/dist/esm/smtp-pool/pool-resource.js +106 -17
  58. package/dist/esm/smtp-transport/index.js +8 -2
  59. package/package.json +6 -6
package/CHANGELOG.md CHANGED
@@ -1,5 +1,39 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## [10.1.1](https://github.com/nodemailer/nodemailer/compare/v10.1.0...v10.1.1) (2026-10-14)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **fetch:** strip credentials on any cross-origin redirect ([a15360c](https://github.com/nodemailer/nodemailer/commit/a15360cd2cd12e4670e96ab0b7290e8c862391cf))
9
+
10
+ ## [10.1.0](https://github.com/nodemailer/nodemailer/compare/v10.0.16...v10.1.0) (2026-10-10)
11
+
12
+
13
+ ### Features
14
+
15
+ * harden message streams, SMTP connections and the connection pool ([8009da6](https://github.com/nodemailer/nodemailer/commit/8009da6e10bb8e10d7f0948a25d65895fdcd64f1))
16
+
17
+
18
+ ### Bug Fixes
19
+
20
+ * do not read the environment or the interface table on import ([094179d](https://github.com/nodemailer/nodemailer/commit/094179d176d2ebe7a28742411ad427d3c87a3981))
21
+ * end the SMTP session on any 421 reply and drive the envelope through one step ([5638ac1](https://github.com/nodemailer/nodemailer/commit/5638ac1914342460df270f736e7ceb0a844cf83a))
22
+ * keep ERR_ACCESS_DENIED from the Node.js permission model ([8b33a3d](https://github.com/nodemailer/nodemailer/commit/8b33a3d4fedee3431b97da7da12af954c3c8dceb))
23
+ * let the proxy handshake and the SMTP connection share one deadline ([91ed683](https://github.com/nodemailer/nodemailer/commit/91ed683f7a1e9ff8c56e24fb76facf16af08b162))
24
+ * **mime-node:** decode an SMTPUTF8 domain from its A-label form ([e436f4d](https://github.com/nodemailer/nodemailer/commit/e436f4dccbc8b0fb57b4747ebaafa5bb035c15cb))
25
+ * **mime-node:** finish an encoded part only after its output was read ([ec97d8f](https://github.com/nodemailer/nodemailer/commit/ec97d8f679a1d9d47bbebb03ea064f5fa6f58d01))
26
+ * **qp:** encode a lone CR or LF in binary quoted-printable parts ([42b91d9](https://github.com/nodemailer/nodemailer/commit/42b91d9a0fe18eea412c9774c5c142adf5b3efc7))
27
+ * **smtp-connection:** count the wait for the greeting against connectionTimeout ([bdcd046](https://github.com/nodemailer/nodemailer/commit/bdcd0462cab163eca3c34058d09b9981ab671c47))
28
+ * **smtp-connection:** drop the connection when DATA is taken after a refused sender ([f90fb04](https://github.com/nodemailer/nodemailer/commit/f90fb045bc24f96a128710791b2fa443e64f55fd))
29
+ * **smtp-connection:** refuse a second connect() call ([24ba2e0](https://github.com/nodemailer/nodemailer/commit/24ba2e0847ecb8202540f1c5e53483bd119ae4f2))
30
+ * turn Nagle off for SMTP sockets and report an early sendmail exit as such ([fa9b8d3](https://github.com/nodemailer/nodemailer/commit/fa9b8d300bf642cd9e11f6732ebb312c09868677))
31
+
32
+
33
+ ### Performance Improvements
34
+
35
+ * encode base64 and quoted-printable without per-chunk delays ([fd377bb](https://github.com/nodemailer/nodemailer/commit/fd377bbf559363860aedbedba6f107348fc152d2))
36
+
3
37
  ## [10.0.16](https://github.com/nodemailer/nodemailer/compare/v10.0.15...v10.0.16) (2026-10-07)
4
38
 
5
39
 
package/SECURITY.md CHANGED
@@ -1,7 +1,9 @@
1
1
  # Security Policy
2
2
 
3
- Nodemailer is a widely deployed, zero-dependency e-mail library. We take security
4
- reports seriously and aim to respond quickly.
3
+ Nodemailer is a widely deployed, zero-dependency email library. We welcome
4
+ responsible reports of vulnerabilities that present a credible security risk to
5
+ users. Please distinguish security vulnerabilities from ordinary bugs, unusual
6
+ edge cases, and hardening suggestions.
5
7
 
6
8
  ## Supported Versions
7
9
 
@@ -17,45 +19,119 @@ the supported way to receive security updates.
17
19
  If you are on an older major, please upgrade. See the migration notes at
18
20
  <https://nodemailer.com/> before updating.
19
21
 
20
- ## Reporting a Vulnerability
22
+ ## What Qualifies as a Security Vulnerability
21
23
 
22
- **Please do not report security vulnerabilities through public GitHub issues,
23
- pull requests, or discussions.**
24
+ A security report should identify a realistic way for an attacker to cross a
25
+ security boundary in a supported version of Nodemailer. Unexpected behavior,
26
+ a failing test, or a technically correct proof of concept is not sufficient on
27
+ its own to establish security impact.
24
28
 
25
- Report privately through one of the following channels:
29
+ In particular, explain:
26
30
 
27
- 1. **GitHub Security Advisories (preferred).** Open a private report at
28
- <https://github.com/nodemailer/nodemailer/security/advisories/new>. This keeps
29
- the discussion private until a fix is published and lets us credit you.
30
- 2. **Email.** Send details to **andris@reinman.eu** (the contact listed in
31
- [`SECURITY.txt`](SECURITY.txt)). Encrypt sensitive details if possible.
31
+ - **Attacker capabilities:** Who is the attacker, and what inputs, systems, or
32
+ responses can they control _without_ already having the privileges or secrets
33
+ they are trying to gain?
34
+ - **Attack path:** How does attacker-controlled input reach the affected code
35
+ through a supported Nodemailer API or a realistic application integration?
36
+ - **Prerequisites:** What configuration and user actions are required? If the
37
+ attack depends on unusual options or deliberately malformed input, explain
38
+ why those conditions could arise in a real application.
39
+ - **Impact:** What confidentiality, integrity, or availability boundary is
40
+ crossed, and what can the attacker achieve as a result?
32
41
 
33
- When reporting, please include as much of the following as you can:
42
+ A vulnerability involving an optional or less common feature can still qualify
43
+ if these conditions are met. Conversely, the existence of a defensive check in
44
+ the code does not, by itself, establish that every bypass of that check has a
45
+ realistic security impact.
34
46
 
35
- - The affected version(s) and environment (Node.js version, OS).
36
- - The component involved (e.g. SMTP connection, address parsing, MIME/header
37
- generation, DKIM).
38
- - A clear description of the issue and its impact (e.g. header/SMTP command
39
- injection, information disclosure, DoS).
40
- - A minimal proof of concept or reproduction steps.
41
- - Any suggested remediation, if you have one.
47
+ ## Reporting a Vulnerability
42
48
 
43
- Nodemailer is maintained by a single person, so there is no guaranteed response
44
- time: sometimes reports are handled within hours, sometimes they take longer.
45
- Accepted issues are fixed in a new release and coordinated through a GitHub
46
- Security Advisory, and reporters who wish to be named are credited.
49
+ **Do not disclose suspected exploitable vulnerabilities through public GitHub
50
+ issues, pull requests, or discussions before coordinating with the maintainer.**
47
51
 
48
- ## CVEs
52
+ Report privately through one of the following channels:
49
53
 
50
- We track and disclose vulnerabilities through GitHub Security Advisories. We do
51
- not request or manage CVE identifiers ourselves. If you need a CVE assigned for a
52
- reported issue, please request one yourself, for example through GitHub's own
53
- CVE request flow on the published advisory, or another CNA.
54
+ 1. **GitHub Security Advisories (preferred):**
55
+ <https://github.com/nodemailer/nodemailer/security/advisories/new>.
56
+ 2. **Email:** **andris@reinman.eu**, as listed in
57
+ [`SECURITY.txt`](SECURITY.txt). Encrypt sensitive details if possible.
58
+
59
+ ### Required Information
60
+
61
+ Please provide enough information to evaluate both the defect and its security
62
+ impact. Reports should include:
63
+
64
+ 1. The affected Nodemailer version or commit, Node.js version, and relevant
65
+ configuration.
66
+ 2. A concise description of the issue and the affected public API or code path.
67
+ 3. A minimal, tested reproduction, including steps to run it, expected behavior,
68
+ and observed behavior.
69
+ 4. A concrete threat model: attacker control, prerequisites, trust boundary, and
70
+ resulting impact. Clearly distinguish what your reproduction proves from what
71
+ you believe might be possible.
72
+ 5. An explanation of why the issue affects applications beyond a deliberately
73
+ constructed test setup, where this is not obvious.
74
+
75
+ A suggested fix is welcome but does not replace an explanation of exploitability.
76
+ A complete real-world exploit is not always necessary, but the report must make
77
+ a credible security impact clear.
78
+
79
+ ### Reports That May Be Treated as Ordinary Bugs
80
+
81
+ The following generally do **not** qualify for private security handling unless
82
+ you can demonstrate an additional, meaningful security boundary crossing:
83
+
84
+ - Issues that require the attacker to control trusted application code,
85
+ administrator-only configuration, or the secret they supposedly extract.
86
+ - Problems caused solely by application-level misuse, exposed credentials, or
87
+ insecure deployment choices outside Nodemailer's control.
88
+ - Contrived proof-of-concept setups where the tester controls both sides of a
89
+ request, without explaining how a separate attacker could trigger the issue.
90
+ - Parser quirks, unexpected behavior, and bypasses of internal checks with no
91
+ demonstrated security impact.
92
+ - Speculative findings based only on static analysis or an automated tool's
93
+ assessment, without a verified reproduction and credible attack scenario.
94
+
95
+ These may still be valid bugs and may be considered through the normal issue
96
+ tracker. Please coordinate before making any potentially exploitable details
97
+ public. Classification as a bug does not guarantee a fix or a particular
98
+ priority.
99
+
100
+ ### Automated and AI-Assisted Reports
101
+
102
+ Automated analysis and AI tools are welcome as research aids, but the person
103
+ submitting a report is responsible for verifying its claims. **Do not submit
104
+ bulk, unverified, or speculative AI-generated findings.** Test each finding
105
+ against a supported release and establish the actual security impact. Reports
106
+ that do not meet the requirements above may be closed without further
107
+ investigation or individual feedback; they can be resubmitted with stronger
108
+ evidence.
109
+
110
+ ## Triage and Disclosure
111
+
112
+ Nodemailer is maintained by a single person, and there is **no guaranteed
113
+ response or resolution time**. Reports are prioritized based on demonstrated
114
+ impact, exploitability, affected usage, and available maintenance resources.
115
+
116
+ A private report does not automatically become a security advisory. Confirmed
117
+ vulnerabilities may be fixed and disclosed through GitHub Security Advisories;
118
+ other findings may be closed, deferred, or handled as ordinary bugs. Please do
119
+ not disclose details of an unresolved, potentially exploitable issue publicly
120
+ without coordinating with the maintainer.
121
+
122
+ ## CVEs and Credit
123
+
124
+ We do not request or manage CVE identifiers. A report being acknowledged, a bug
125
+ being fixed, or a pull request being merged does **not** mean that Nodemailer
126
+ has confirmed a security vulnerability or endorsed a CVE assignment. We do not
127
+ guarantee advisories, CVE identifiers, or public credit for reports. For
128
+ confirmed vulnerabilities disclosed through an advisory, substantive reporters
129
+ may be credited if they wish.
54
130
 
55
131
  ## Scope
56
132
 
57
133
  In scope: the `nodemailer` package source in this repository: message and MIME
58
- generation, SMTP/LMTP client behaviour, address parsing, header handling, DKIM
134
+ generation, SMTP/LMTP client behavior, address parsing, header handling, DKIM
59
135
  signing, and the bundled transports.
60
136
 
61
137
  Out of scope: vulnerabilities in your own application code, misconfiguration of
@@ -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
@@ -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 || 76;
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
- setImmediate(done);
68
- return;
136
+ return done();
69
137
  }
70
138
  this.inputBytes += buf.length;
71
- if (this._remainingBytes && this._remainingBytes.length) {
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
- if (buf.length % 3) {
76
- this._remainingBytes = buf.slice(buf.length - (buf.length % 3));
77
- buf = buf.slice(0, buf.length - (buf.length % 3));
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
- setImmediate(done);
148
+ this._emit(encode(buf), false);
149
+ done();
104
150
  }
105
151
  /** @internal */
106
152
  _flush(done) {
107
- if (this._remainingBytes && this._remainingBytes.length) {
108
- this._curLine += encode(this._remainingBytes);
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
  }
@@ -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
- if (this.hasErrored) {
66
- return;
67
- }
68
- if (this.readPos >= this.chunks.length) {
69
- if (!this.cache) {
70
- this.output.end();
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.hasErrored = true;
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.hasErrored = true;
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
- output.emit('error', sign_js_1.default.unsupportedHashAlgoError(signer.hashAlgo));
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
- setImmediate(() => this.push(chunk));
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
- setImmediate(callback);
139
+ callback();
140
140
  }
141
141
  /** @internal */
142
142
  _flush(callback) {
@@ -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
@@ -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';